Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
13a1a5b
feat: stop by win32 support
waitingsong Apr 2, 2018
0e05556
fix: restore result type of findNodeProcess()
waitingsong Apr 2, 2018
bfb9b5b
chore: result order change to [agentPid, ...appPids] of findWorkerPid…
waitingsong Apr 2, 2018
537c69f
test: add utils-win.js for win32
waitingsong Apr 2, 2018
e7cf849
chore: output without header for running tasklist with findWorkerPids…
waitingsong Apr 2, 2018
40cae99
fix: typo
waitingsong Apr 2, 2018
b9546ae
test: case "should stop by port under win32" with stop-win.test.js
waitingsong Apr 3, 2018
bb3536e
test: add unit "stop without daemon"
waitingsong Apr 3, 2018
744f5b2
test: change to run under none win32
waitingsong Apr 3, 2018
c329886
chore(test): update unit name "stop with not exist" to "stop without…
waitingsong Apr 3, 2018
6749780
test: add unit "stop without existing"
waitingsong Apr 3, 2018
c4c880f
test: add unit "stop --title", but not enable
waitingsong Apr 3, 2018
570bc64
test: add unit "stop all", but not enable
waitingsong Apr 3, 2018
f96e6ae
chore: update comments
waitingsong Apr 3, 2018
3f8866f
fix(test): regexDim missing
waitingsong Apr 3, 2018
5bbc312
test: update "stop --title"
waitingsong Apr 3, 2018
6b1fa52
test: utils.cleanup() with port
waitingsong Apr 3, 2018
29c13e5
test: update unit name
waitingsong Apr 3, 2018
7924816
test: stop by title only for process started with daemon
waitingsong Apr 3, 2018
7622b37
chore: prune
waitingsong Apr 3, 2018
16931be
test: comment definition of parseKeyStr() out
waitingsong Apr 3, 2018
7894b4a
test: comment regexDim out
waitingsong Apr 3, 2018
b799a01
test: revert start.test.js
waitingsong Apr 4, 2018
1fe4cac
test: running at non-win32
waitingsong Apr 5, 2018
74a7173
test: for win32
waitingsong Apr 5, 2018
33b0de8
test: update comments
waitingsong Apr 5, 2018
4c81525
test: disable case "stop --title"
waitingsong Apr 5, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 32 additions & 20 deletions lib/cmd/stop.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,30 @@ class StopCommand extends Command {

* run(context) {
/* istanbul ignore next */
if (process.platform === 'win32') {
this.logger.warn('Windows is not supported, try to kill master process which command contains `start-cluster` or `--type=egg-server` yourself, good luck.');
process.exit(0);
}
// if (process.platform === 'win32') {
// this.logger.warn('Windows is not supported, try to kill master process which command contains `start-cluster` or `--type=egg-server` yourself, good luck.');
// process.exit(0);
// }

const { argv } = context;

this.logger.info(`stopping egg application ${argv.title ? `with --title=${argv.title}` : ''}`);

// node /Users/tz/Workspaces/eggjs/egg-scripts/lib/start-cluster {"title":"egg-server","workers":4,"port":7001,"baseDir":"/Users/tz/Workspaces/eggjs/test/showcase","framework":"/Users/tz/Workspaces/eggjs/test/showcase/node_modules/egg"}
let processList = yield this.helper.findNodeProcess(item => {
const cmd = item.cmd;
return argv.title ?
cmd.includes('start-cluster') && cmd.includes(`"title":"${argv.title}"`) :
cmd.includes('start-cluster');
});
let pids = processList.map(x => x.pid);
let pids = [];

if (process.platform !== 'win32') {
// node /Users/tz/Workspaces/eggjs/egg-scripts/lib/start-cluster {"title":"egg-server","workers":4,"port":7001,"baseDir":"/Users/tz/Workspaces/eggjs/test/showcase","framework":"/Users/tz/Workspaces/eggjs/test/showcase/node_modules/egg"}
const processList = yield this.helper.findNodeProcess(item => {
const cmd = item.cmd;
return argv.title ?
cmd.includes('start-cluster') && cmd.includes(`"title":"${argv.title}"`) :
cmd.includes('start-cluster');
});

pids = processList.map(x => x.pid);
} else {
pids = yield this.helper.findfindNodeProcessWin(argv);
}

if (pids.length) {
this.logger.info('got master pid %j', pids);
Expand All @@ -49,18 +56,23 @@ class StopCommand extends Command {
this.logger.warn('can\'t detect any running egg process');
}

// wait for 5s to confirm whether any worker process did not kill by master
// wait for 5s to confirm whether every worker process killed by master or not
yield sleep('5s');

// node --debug-port=5856 /Users/tz/Workspaces/eggjs/test/showcase/node_modules/[email protected]@egg-cluster/lib/agent_worker.js {"framework":"/Users/tz/Workspaces/eggjs/test/showcase/node_modules/egg","baseDir":"/Users/tz/Workspaces/eggjs/test/showcase","port":7001,"workers":2,"plugins":null,"https":false,"key":"","cert":"","title":"egg-server","clusterPort":52406}
// node /Users/tz/Workspaces/eggjs/test/showcase/node_modules/[email protected]@egg-cluster/lib/app_worker.js {"framework":"/Users/tz/Workspaces/eggjs/test/showcase/node_modules/egg","baseDir":"/Users/tz/Workspaces/eggjs/test/showcase","port":7001,"workers":2,"plugins":null,"https":false,"key":"","cert":"","title":"egg-server","clusterPort":52406}
processList = yield this.helper.findNodeProcess(item => {
const cmd = item.cmd;
return argv.title ?
(cmd.includes('egg-cluster/lib/app_worker.js') || cmd.includes('egg-cluster/lib/agent_worker.js')) && cmd.includes(`"title":"${argv.title}"`) :
(cmd.includes('egg-cluster/lib/app_worker.js') || cmd.includes('egg-cluster/lib/agent_worker.js'));
});
pids = processList.map(x => x.pid);

if (process.platform !== 'win32') {
const processList = yield this.helper.findNodeProcess(item => {
const cmd = item.cmd;
return argv.title ?
(cmd.includes('egg-cluster/lib/app_worker.js') || cmd.includes('egg-cluster/lib/agent_worker.js')) && cmd.includes(`"title":"${argv.title}"`) :
(cmd.includes('egg-cluster/lib/app_worker.js') || cmd.includes('egg-cluster/lib/agent_worker.js'));
});
pids = processList.map(x => x.pid);
} else {
pids = yield this.helper.findfindNodeProcessWin(argv);
}

if (pids.length) {
this.logger.info('got worker/agent pids %j that is not killed by master', pids);
Expand Down
283 changes: 283 additions & 0 deletions lib/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const runScript = require('runscript');
const REGEX = /^\s*(\d+)\s+(.*)/;
const regexDim = /\\+/g;

exports.findNodeProcess = function* (filterFn) {
const command = 'ps -eo "pid,command"';
Expand Down Expand Up @@ -34,3 +35,285 @@ exports.kill = function(pids, signal) {
}
});
};


/* ------- below for win32 ------ */

/**
* retrieve master pid by worker's pid via ppid for win32
* Minimum supported client: Vista
* Minimum supported server: Server 2008
*
* @param {object} argv - context
* @return {number[]} - array pids of master and app/agent worker
*/
exports.findfindNodeProcessWin = function* (argv) {
const port = argv.port || (process.env && process.env.PORT);
const keyStr = argv.title ? argv.title : argv.baseDir;
let masterPids = [];
let workerPids = [];

if (keyStr) { // title match first
const mPidSet = new Set(); // master pid
workerPids = yield findWorkerPidsWin(keyStr); // order or pid random

for (const workerPid of workerPids) {
const ppid = yield findMasterPidByWorker(workerPid);
ppid > 0 && mPidSet.add(ppid);
}
if (mPidSet.size > 1) {
throw new Error('number of master pid should be One via title, but got:' + mPidSet.size);
}
masterPids = Array.from(mPidSet);
} else if (port) {
masterPids = yield findMasterPidsByPort(port);
if (masterPids.length > 1) {
throw new Error('number of master pid should be One via port, but got:' + masterPids.length);
}
workerPids = yield findWorkerPidsByMaster(masterPids[0]); // [agentPid, ...workerPids]
}

return masterPids.concat(workerPids); // master first
};

function parseKeyStr(str) {
const ret = str && typeof str === 'string' ? str : '';
return ret.replace(regexDim, '/');
}

/**
* retrieve master pid by worker's pid via ppid for win32
* Minimum supported client: Vista
* Minimum supported server: Server 2008
*
* @param {number} pid - process id
* @return {number} - pid of master
*/
function* findMasterPidByWorker(pid) {
const where = `process where processid="${pid}"`;
const str = yield retrieveProcessInfo(pid, where);
const info = parseProcessInfo(str);
const row = info && info.length === 1 && info[0];

return row && row.pid && row.ppid && row.pid === pid
? row.ppid
: 0;
}

/**
* retrieve master pid by worker's pid via ppid for win32
* Minimum supported client: Vista
* Minimum supported server: Server 2008
*
* @param {number} pid - master process id
* @return {number[]} - array pids of app/agent work, order: [agentPid, ...appPids]
*/
function* findWorkerPidsByMaster(pid) {
const where = `process where ParentProcessId="${pid}"`;
const str = yield retrieveProcessInfo(pid, where);
const info = parseProcessInfo(str);
const ret = [];

for (const row of info) {
if (row && row.ppid && row.ppid === pid) {
if (row.cmd.includes('agent_worker')) { // agent at first
ret.unshift(row.pid);
} else {
ret.push(row.pid);
}
}
}

return ret;
}

/**
* find master pids by listining port
* @param {number} port - port number of master listening
* @return {number[]} - array of pid
*/
function* findMasterPidsByPort(port) {
port = +port;
if (!Number.isSafeInteger(port)) {
return [];
}
const command = `netstat -aon|findstr ":${port}"`;
let stdio;

try {
stdio = yield runScript(command, { stdio: 'pipe' });
} catch (ex) {
return [];
}
const list = new Set();
const arr = stdio.stdout && stdio.stdout.toString().split('\n') || [];

arr.length && arr.forEach(line => {
if (line) {
// [ '', 'TCP', '0.0.0.0:7001', '0.0.0.0:0', 'LISTENING', '4580', '' ]
// [ '', 'TCP', '[::]:7001', '0.0.0.0:0', 'LISTENING', '4580', '' ]
const lineArr = line.split(/\s+/);

if (!lineArr[0] && lineArr[1] === 'TCP' && lineArr[2] && lineArr[5]) {
const pid = +lineArr[5];
const ipArr = lineArr[2].split(':');

if (!Number.isSafeInteger(pid) && list.has(pid)) {
return;
}

if (ipArr && ipArr.length >= 2) {
if (+ipArr[ipArr.length - 1] === port) { // ipv4/v6
list.add(pid);
}
}
}
}
});

return [ ...list ];
}

function* findWorkerPidsWin(str) {
const keyStr = parseKeyStr(str);
const command = `tasklist /NH /FI "WINDOWTITLE eq ${keyStr}"`;

if (!keyStr) {
return [];
}

let stdio;
try {
stdio = yield runScript(command, { stdio: 'pipe' });
} catch (ex) {
return [];
}
const list = new Set();
const arr = stdio.stdout && stdio.stdout.toString().split('\n') || [];

arr.length && arr.forEach(line => {
if (line && line.includes('node.exe')) {
// image name PID session name session# memory
// node.exe 1704 Console 1 31,540 K
// node.exe 6096 Console 1 33,380 K
const [ , id ] = line.split(/\s+/);
const pid = parseInt(id, 10);

if (typeof pid === 'number' && Number.isSafeInteger(pid)) {
if (list.has(pid)) {
return;
}
list.add(pid);
}
}
});

return [ ...list ];
}

/**
* retrieve process info by pid via wmic
* Minimum supported client: Vista
* Minimum supported server: Server 2008
*
* @param {number} processId - process id
* @param {string} where - wmic query string
* @return {Promise<string>} - raw data
*/
function* retrieveProcessInfo(processId, where) {
// invalid result of runScript(), so run by spawn
const spawn = require('child_process').spawn;

return new Promise((resolve, reject) => {
const pid = +processId;

if (Number.isNaN(pid) || !Number.isSafeInteger(pid)) {
return resolve('');
}
const wmic = spawn('wmic', []);
const stdout = [];

wmic.stdout.on('data', buf => {
stdout.push(buf.toString('utf8'));
});
wmic.stdout.on('error', err => {
reject(err);
});
wmic.on('close', () => {
resolve(stdout.join(''));
});

wmic.stdin.end(where + ' get CommandLine, Name, ParentProcessId, ProcessId');
});
}

/**
* parse process info for win32
*
* @param {string} data - data from retrieveProcessInfo()
* @return {processInfo[]} - <code>
* interface ProcessInfo {
* shell: string // 'D:/.../node.exe'
* cmd: string // 'E:/.../node_modules/egg-cluster/lib/(agent_worker|app_worker).js'
* options: ClusterOptions
* ppid: number
* pid: number
* }
* interface ClusterOptions {
* framework: string // delimite '\' be replaced to '/' -> 'E:/.../app/node_modules/yadan-ts'
* baseDir: string // delimite '\' be replaced to '/' -> 'E:/.../app'
* plugins: object | null
* workers: number
* port: number
* https: boolean
* key: string
* cert: string
* typescript: boolean
* title?: string
* [prop: string]: any
* }
* </code>
*/
function parseProcessInfo(data) {
const ret = [];

data && typeof data === 'string' && data.split('\n').forEach(line => {
if (!line) {
return;
}
// sh, cmd, optstr, imageName, ppid, pid
const arr = line.trim().split(/\s+/);

if (!arr[3] || !arr[3].includes('node')) { // node.exe
return;
}
let [ sh, cmd, optstr, , ppid, pid ] = arr;

if (typeof +ppid === 'number' && typeof +pid === 'number') {
optstr = optstr.trim().slice(1, -1).replace(/\\"/g, '"');

try {
const options = JSON.parse(optstr);

if (options.framework) {
options.framework = options.framework.trim().replace(regexDim, '/');
}
if (options.baseDir) {
options.baseDir = options.baseDir.trim().replace(regexDim, '/');
}

ret.push({
shell: sh && sh.trim().replace(regexDim, '/') || '',
cmd: cmd && cmd.trim().replace(regexDim, '/') || '',
options,
ppid: +ppid,
pid: +pid,
});
} catch (ex) {
return;
}
}
});

return ret;
}
Loading