Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support win32 #11

Closed
wants to merge 12 commits into from
14 changes: 11 additions & 3 deletions lib/cmd/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,17 @@ class StartCommand extends Command {

if (hasError) {
try {
const [ stdout ] = yield exec('tail -n 100 ' + stderr);
this.logger.error('Got error when startup: ');
this.logger.error(stdout);
if (process.platform !== 'win32') {
const [ stdout ] = yield exec('tail -n 100 ' + stderr);
this.logger.error('Got error when startup: ');
this.logger.error(stdout);
} else {
const str = yield fs.readFile(stderr, 'utf-8');
let stdout = str ? str.slice(0, 50000).replace(/\r/g, '\n') : '';
stdout = stdout.split(/\n+/g).splice(0, 100).join('\n');
this.logger.error('Got error when startup: ');
this.logger.error(stdout);
}
} catch (_) {
// nothing
}
Expand Down
11 changes: 3 additions & 8 deletions lib/cmd/stop.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,8 @@ 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);
}

const { argv } = context;
const port = argv.port || (process.env && process.env.PORT);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there other way to stop without port

Copy link
Author

@waitingsong waitingsong Nov 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

有其他方法。但相当复杂
1、修改egg-cluster, 在启动 worker 进程时,更新窗口的 title
2、在关闭服务时借助node-ffi 找到任何一个匹配 title 的 worker 窗口获取其pid
3、借助node-ffi 调用 ntdll.NtQueryInformationProcess() 方法,根据pid找到父级pid(即master进程),然后kill。

我计划实现(1, 2),并且可根据参数隐藏worker的窗口。
因为master进程没有窗口,也就无法设置其窗口 title,所以只有在 worker 上面设置了。 否则就可以跳过(3)直接找到 master 进程窗口然后 kill 之。

(3)这个 NtQueryInformationProcess 需要的 参数结构Struct极其复杂,目前搞不定。

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we can store port when app starts, so it’s unnecessary to give the port argument.

Copy link
Author

@waitingsong waitingsong Nov 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

能记录所有进程 pid 那当然就方便了


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

Expand All @@ -39,7 +34,7 @@ class StopCommand extends Command {
return argv.title ?
cmd.includes('start-cluster') && cmd.includes(`"title":"${argv.title}"`) :
cmd.includes('start-cluster');
});
}, port);
let pids = processList.map(x => x.pid);

if (pids.length) {
Expand All @@ -59,7 +54,7 @@ class StopCommand extends Command {
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'));
});
}, port);
pids = processList.map(x => x.pid);

if (pids.length) {
Expand Down
52 changes: 50 additions & 2 deletions lib/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
const runScript = require('runscript');
const REGEX = /^\s*(\d+)\s+(.*)/;

exports.findNodeProcess = function* (filterFn) {
exports.findNodeProcess = function* (filterFn, port) {
if (process.platform === 'win32') {
return yield findNodeProcessWin(port);
}

const command = 'ps -eo "pid,command"';
const stdio = yield runScript(command, { stdio: 'pipe' });
const processList = stdio.stdout.toString().split('\n')
Expand All @@ -21,12 +25,56 @@ exports.findNodeProcess = function* (filterFn) {
return arr;
}, []);
return processList;

};

function* findNodeProcessWin(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 map = new Map();

stdio.stdout.toString().split('\n')
.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) && map.has(pid)) {
return;
}

if (ipArr && ipArr.length >= 2) {
if (+ipArr[ipArr.length - 1] === port) { // ipv4/v6
map.set(pid, { pid, cmd: '' });
}
}
}
}
});

return Array.from(map.values());
}
exports.findNodeProcessWin = findNodeProcessWin;

exports.kill = function(pids, signal) {
pids.forEach(pid => {
try {
process.kill(pid, signal);
process.kill(pid, 0) && process.kill(pid, signal);
} catch (err) { /* istanbul ignore next */
if (err.code !== 'ESRCH') {
throw err;
Expand Down
13 changes: 8 additions & 5 deletions test/start.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ describe('test/start.test.js', () => {

after(function* () {
app.proc.kill('SIGTERM');
yield utils.cleanup(fixturePath);
yield utils.cleanup(fixturePath, 7002);
});

it('should start', function* () {
Expand All @@ -201,7 +201,7 @@ describe('test/start.test.js', () => {

after(function* () {
app.proc.kill('SIGTERM');
yield utils.cleanup(fixturePath);
yield utils.cleanup(fixturePath, 7002);
});

it('should start', function* () {
Expand Down Expand Up @@ -271,7 +271,8 @@ describe('test/start.test.js', () => {
let result = yield httpclient.request('http://127.0.0.1:7001/env');
assert(result.data.toString() === 'pre, true');
result = yield httpclient.request('http://127.0.0.1:7001/path');
assert(result.data.toString().match(new RegExp(`^${fixturePath}/node_modules/.bin${path.delimiter}`)));
const p = path.normalize(`${fixturePath}/node_modules/.bin${path.delimiter}`).replace(/\\/g, '\\\\');
assert(result.data.toString().match(new RegExp(`^${p}`)));
});
});

Expand Down Expand Up @@ -318,7 +319,7 @@ describe('test/start.test.js', () => {

after(function* () {
app.proc.kill('SIGTERM');
yield utils.cleanup(fixturePath);
yield utils.cleanup(fixturePath, 8000);
});

it('should start', function* () {
Expand Down Expand Up @@ -411,6 +412,7 @@ describe('test/start.test.js', () => {
.debug()
.end();
yield utils.cleanup(cwd);
yield utils.cleanup(cwd, 7002);
});

it('should start custom-framework', function* () {
Expand Down Expand Up @@ -486,7 +488,7 @@ describe('test/start.test.js', () => {
mm(process.env, 'WAIT_TIME', 5000);
mm(process.env, 'ERROR', 'error message');

const stderr = path.join(homePath, 'logs/master-stderr.log');
const stderr = path.join(homePath, 'logs/master-stderr.log').replace(/\\/g, '\\\\');

yield coffee.fork(eggBin, [ 'start', '--daemon', '--workers=1' ], { cwd })
// .debug()
Expand All @@ -509,3 +511,4 @@ describe('test/start.test.js', () => {

});
});

78 changes: 57 additions & 21 deletions test/stop.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ const coffee = require('coffee');
const httpclient = require('urllib');
const mm = require('mm');
const utils = require('./utils');
const helper = require('../lib/helper');

describe('test/stop.test.js', () => {
const eggBin = require.resolve('../bin/egg-scripts.js');
const fixturePath = path.join(__dirname, 'fixtures/example');
const homePath = path.join(__dirname, 'fixtures/home');
const logDir = path.join(homePath, 'logs');
const waitTime = '10s';
const fixturePathR = path.normalize(fixturePath).replace(/\\/g, '\\\\'); // for win32

before(function* () {
yield mkdirp(homePath);
Expand Down Expand Up @@ -57,15 +59,19 @@ describe('test/stop.test.js', () => {
// yield killer.end();
yield sleep(waitTime);

// make sure is kill not auto exist
assert(!app.stdout.includes('exist by env'));

assert(app.stdout.includes('[master] receive signal SIGTERM, closing'));
assert(app.stdout.includes('[master] exit with code:0'));
assert(app.stdout.includes('[app_worker] exit with code:0'));
// assert(app.stdout.includes('[agent_worker] exit with code:0'));
assert(killer.stdout.includes('[egg-scripts] stopping egg application'));
assert(killer.stdout.match(/got master pid \["\d+\"\]/i));
if (process.platform !== 'win32') {
// make sure is kill not auto exist
assert(!app.stdout.includes('exist by env'));

assert(app.stdout.includes('[master] receive signal SIGTERM, closing'));
assert(app.stdout.includes('[master] exit with code:0'));
assert(app.stdout.includes('[app_worker] exit with code:0'));
// assert(app.stdout.includes('[agent_worker] exit with code:0'));
assert(killer.stdout.includes(`[egg-scripts] stopping egg application at ${fixturePath}`));
assert(killer.stdout.match(/got master pid \["\d+\"\]/i));
}
});
});
});

Expand All @@ -86,36 +92,66 @@ describe('test/stop.test.js', () => {
});

it('should stop', function* () {
yield coffee.fork(eggBin, [ 'stop', fixturePath ])
.debug()
.expect('stdout', /\[egg-scripts] stopping egg application/)
.expect('stdout', /got master pid \["\d+\"\]/i)
.expect('code', 0)
.end();
if (process.platform !== 'win32') {
yield coffee.fork(eggBin, [ 'stop', fixturePath ])
.debug()
.expect('stdout', new RegExp(`\\[egg-scripts] stopping egg application at ${fixturePath}`))
.expect('stdout', /got master pid \["\d+\"\]/i)
.expect('code', 0)
.end();

yield sleep(waitTime);
yield sleep(waitTime);

// master log
const stdout = yield fs.readFile(path.join(logDir, 'master-stdout.log'), 'utf-8');

assert(stdout.includes('[master] receive signal SIGTERM, closing'));
assert(stdout.includes('[master] exit with code:0'));
assert(stdout.includes('[app_worker] exit with code:0'));

} else {
yield coffee.fork(eggBin, [ 'stop', fixturePath ])
.debug()
.expect('stdout', new RegExp(`\\[egg-scripts] stopping egg application at ${fixturePathR}`))
.expect('stdout', /(got master pid \["\d+\"\])|(\[egg-scripts\] stopped)/i)
.expect('code', 0)
.end();

// master log
const stdout = yield fs.readFile(path.join(logDir, 'master-stdout.log'), 'utf-8');
yield sleep(waitTime);

assert(stdout.includes('[master] receive signal SIGTERM, closing'));
assert(stdout.includes('[master] exit with code:0'));
assert(stdout.includes('[app_worker] exit with code:0'));
}

yield coffee.fork(eggBin, [ 'stop', fixturePath ])
.debug()
.expect('stderr', /can't detect any running egg process/)
.expect('code', 0)
.end();
});

if (process.platform === 'win32') {
it('should got pid', function* () {
const port = 7001;
const processList = yield helper.findNodeProcessWin(port);

assert(Array.isArray(processList) && processList.length);
});

it('should got empty pid', function* () {
const port = 0;
const processList = yield helper.findNodeProcessWin(port);

assert(Array.isArray(processList) && !processList.length);
});
}

});

describe('stop with not exist', () => {
it('should work', function* () {
yield utils.cleanup(fixturePath);
yield coffee.fork(eggBin, [ 'stop', fixturePath ])
.debug()
.expect('stdout', /\[egg-scripts] stopping egg application/)
.expect('stdout', new RegExp(`\\[egg-scripts] stopping egg application at ${fixturePathR}`))
.expect('stderr', /can't detect any running egg process/)
.expect('code', 0)
.end();
Expand Down
5 changes: 3 additions & 2 deletions test/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
const helper = require('../lib/helper');
const sleep = require('mz-modules/sleep');

exports.cleanup = function* (baseDir) {
const processList = yield helper.findNodeProcess(x => x.cmd.includes(`"baseDir":"${baseDir}"`));
exports.cleanup = function* (baseDir, port) {
port = port ? port : 7001;
const processList = yield helper.findNodeProcess(x => x.cmd.includes(`"baseDir":"${baseDir}"`), port);

if (processList.length) {
console.log(`cleanup: ${processList.length} to kill`);
Expand Down