diff --git a/.eslintignore b/.eslintignore index a24e501..4ebc8ae 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1 @@ -test/fixtures coverage diff --git a/.gitignore b/.gitignore index f18c766..446bbe1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,8 @@ logs/ npm-debug.log -node_modules/ +/node_modules coverage/ .idea/ run/ .DS_Store *.swp -!test/fixtures/example/node_modules - diff --git a/README.md b/README.md index 04dabf7..a1b3b66 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ deploy tool for egg project. +**Note: Windows is not supported** + ## Install ```bash @@ -54,8 +56,6 @@ $ egg-scripts start [options] [baseDir] Stop egg gracefull. -**Note:** **Windows is not supported yet**, try to kill master process which command contains `start-cluster` or `--title=egg-server` yourself, good luck. - ```bash # stop egg $ egg-scripts stop [baseDir] @@ -63,4 +63,4 @@ $ egg-scripts stop [baseDir] ``` - **Arguments** - - `baseDir` - directory of application, default to `process.cwd()`. \ No newline at end of file + - `baseDir` - directory of application, default to `process.cwd()`. diff --git a/lib/cmd/start.js b/lib/cmd/start.js index 0e3ed6a..12dc48f 100644 --- a/lib/cmd/start.js +++ b/lib/cmd/start.js @@ -2,9 +2,11 @@ const path = require('path'); const mkdirp = require('mz-modules/mkdirp'); +const sleep = require('mz-modules/sleep'); const homedir = require('node-homedir'); const utils = require('egg-utils'); const fs = require('mz/fs'); +const { exec } = require('mz/child_process'); const moment = require('moment'); const spawn = require('child_process').spawn; const Command = require('../command'); @@ -33,7 +35,7 @@ class StartCommand extends Command { default: process.env.PORT, }, env: { - description: 'egg server env, default to `process.env.EGG_SERVER_ENV`', + description: 'server env, default to `process.env.EGG_SERVER_ENV`', default: process.env.EGG_SERVER_ENV, }, framework: { @@ -52,6 +54,11 @@ class StartCommand extends Command { description: 'A file that stderr redirect to', type: 'string', }, + timeout: { + description: 'a timeout for start when daemon', + type: 'number', + default: 300 * 1000, + }, }; } @@ -78,6 +85,8 @@ class StartCommand extends Command { baseDir, }); + this.frameworkName = yield this.getFrameworkName(argv.framework); + const pkgInfo = require(path.join(baseDir, 'package.json')); argv.title = argv.title || `egg-server-${pkgInfo.name}`; @@ -120,27 +129,32 @@ class StartCommand extends Command { detached: false, }; - this.logger.info(`starting egg application at ${baseDir}`); + this.logger.info('Starting %s application at %s', this.frameworkName, baseDir); const eggArgs = [ this.serverBin, JSON.stringify(argv), `--title=${argv.title}` ]; - this.logger.info('run node %s', eggArgs.join(' ')); + this.logger.info('Run node %s', eggArgs.join(' ')); // whether run in the background. if (isDaemon) { - this.logger.info(`save log file to ${logDir}`); + this.logger.info(`Save log file to ${logDir}`); const [ stdout, stderr ] = yield [ getRotatelog(argv.stdout), getRotatelog(argv.stderr) ]; options.stdio = [ 'ignore', stdout, stderr, 'ipc' ]; options.detached = true; const child = this.child = spawn('node', eggArgs, options); + this.isReady = false; child.on('message', msg => { if (msg && msg.action === 'egg-ready') { - this.logger.info(`egg started on ${msg.data.address}`); + this.isReady = true; + this.logger.info('%s started on %s', this.frameworkName, msg.data.address); child.unref(); child.disconnect(); process.exit(0); } }); + + // check start status + yield this.checkStatus(argv); } else { // signal event had been handler at common-bin helper this.helper.spawn('node', eggArgs, options); @@ -151,6 +165,52 @@ class StartCommand extends Command { return utils.getFrameworkPath(params); } + * getFrameworkName(framework) { + const pkgPath = path.join(framework, 'package.json'); + let name = 'egg'; + try { + const pkg = require(pkgPath); + if (pkg.name) name = pkg.name; + } catch (_) { + /* istanbul next */ + } + return name; + } + + * checkStatus({ stderr, timeout }) { + let count = 0; + let isSuccess = true; + timeout = timeout / 1000; + while (!this.isReady) { + try { + const stat = yield fs.stat(stderr); + if (stat && stat.size > 0) { + const [ stdout ] = yield exec('tail -n 100 ' + stderr); + this.logger.error(stdout); + this.logger.error('Start failed, see %s', stderr); + isSuccess = false; + break; + } + } catch (_) { + // nothing + } + + if (count >= timeout) { + this.logger.error('Start failed, %ds timeout', timeout); + isSuccess = false; + break; + } + + yield sleep(1000); + this.logger.log('Wait Start: %d...', ++count); + } + + if (!isSuccess) { + this.child.kill('SIGTERM'); + yield sleep(1000); + process.exit(1); + } + } } function* getRotatelog(logfile) { diff --git a/package.json b/package.json index 59f1435..c39c68f 100644 --- a/package.json +++ b/package.json @@ -7,25 +7,25 @@ "egg-scripts": "bin/egg-scripts.js" }, "dependencies": { - "common-bin": "^2.5.0", + "common-bin": "^2.7.1", "egg-utils": "^2.2.0", - "moment": "^2.18.1", - "mz": "^2.6.0", - "mz-modules": "^1.0.0", - "node-homedir": "^1.0.0", + "moment": "^2.19.1", + "mz": "^2.7.0", + "mz-modules": "^2.0.0", + "node-homedir": "^1.1.0", "runscript": "^1.3.0", "zlogger": "^1.1.0" }, "devDependencies": { - "autod": "^2.9.0", + "autod": "^2.10.1", "coffee": "^4.1.0", - "egg": "^1.7.0", - "egg-bin": "^4.1.0", + "egg": "^1.9.0", + "egg-bin": "^4.3.5", "egg-ci": "^1.8.0", - "eslint": "^4.4.1", - "eslint-config-egg": "^5.0.0", - "mm": "^2.1.0", - "urllib": "^2.24.0", + "eslint": "^4.8.0", + "eslint-config-egg": "^5.1.1", + "mm": "^2.2.0", + "urllib": "^2.25.0", "webstorm-disable-index": "^1.2.0" }, "engines": { diff --git a/test/fixtures/cluster-config/config/config.prod.js b/test/fixtures/cluster-config/config/config.prod.js index fa189b2..e7523db 100644 --- a/test/fixtures/cluster-config/config/config.prod.js +++ b/test/fixtures/cluster-config/config/config.prod.js @@ -3,5 +3,5 @@ exports.cluster = { listen: { port: 8000, - } -} + }, +}; diff --git a/test/fixtures/egg-app/config/config.default.js b/test/fixtures/egg-app/config/config.default.js new file mode 100644 index 0000000..98de4f0 --- /dev/null +++ b/test/fixtures/egg-app/config/config.default.js @@ -0,0 +1,8 @@ +'use strict'; + +exports.keys = '123456'; + +exports.logger = { + level: 'WARN', + consoleLevel: 'WARN', +}; diff --git a/test/fixtures/egg-app/node_modules/egg/index.js b/test/fixtures/egg-app/node_modules/egg/index.js new file mode 100644 index 0000000..7a1482f --- /dev/null +++ b/test/fixtures/egg-app/node_modules/egg/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('../../../../../node_modules/egg'); diff --git a/test/fixtures/egg-app/node_modules/egg/package.json b/test/fixtures/egg-app/node_modules/egg/package.json new file mode 100644 index 0000000..034e266 --- /dev/null +++ b/test/fixtures/egg-app/node_modules/egg/package.json @@ -0,0 +1,4 @@ +{ + "name": "egg", + "version": "1.0.0" +} diff --git a/test/fixtures/egg-app/package.json b/test/fixtures/egg-app/package.json new file mode 100644 index 0000000..f833722 --- /dev/null +++ b/test/fixtures/egg-app/package.json @@ -0,0 +1,6 @@ +{ + "name": "example", + "dependencies": { + "egg": "^1.0.0" + } +} diff --git a/test/fixtures/example/node_modules/custom-framework/index.js b/test/fixtures/example/node_modules/custom-framework/index.js index 50b41a0..071acea 100644 --- a/test/fixtures/example/node_modules/custom-framework/index.js +++ b/test/fixtures/example/node_modules/custom-framework/index.js @@ -17,5 +17,5 @@ module.exports = Object.assign(egg, { process.env.EGG_SERVER_ENV = process.env.CUSTOM_ENV; } return originStartCluster(...args); - } + }, }); diff --git a/test/fixtures/example/node_modules/yadan/index.js b/test/fixtures/example/node_modules/yadan/index.js index 5ce51a8..246811a 100644 --- a/test/fixtures/example/node_modules/yadan/index.js +++ b/test/fixtures/example/node_modules/yadan/index.js @@ -2,8 +2,6 @@ const egg = require('../../../../../node_modules/egg'); -const originStartCluster = egg.startCluster; - module.exports = Object.assign(egg, { Application: class CustomApplication extends egg.Application { get [Symbol.for('egg#eggPath')]() { diff --git a/test/fixtures/status/app.js b/test/fixtures/status/app.js new file mode 100644 index 0000000..7ba4259 --- /dev/null +++ b/test/fixtures/status/app.js @@ -0,0 +1,13 @@ +'use strict'; + +const sleep = require('mz-modules/sleep'); + +module.exports = app => { + if (process.env.ERROR) { + app.logger.error(new Error(process.env.ERROR)); + } + + app.beforeStart(function* () { + yield sleep(process.env.WAIT_TIME); + }); +}; diff --git a/test/fixtures/status/config/config.default.js b/test/fixtures/status/config/config.default.js new file mode 100644 index 0000000..98de4f0 --- /dev/null +++ b/test/fixtures/status/config/config.default.js @@ -0,0 +1,8 @@ +'use strict'; + +exports.keys = '123456'; + +exports.logger = { + level: 'WARN', + consoleLevel: 'WARN', +}; diff --git a/test/fixtures/status/node_modules/custom-framework/index.js b/test/fixtures/status/node_modules/custom-framework/index.js new file mode 100644 index 0000000..071acea --- /dev/null +++ b/test/fixtures/status/node_modules/custom-framework/index.js @@ -0,0 +1,21 @@ +'use strict'; + +const egg = require('../../../../../node_modules/egg'); + +const originStartCluster = egg.startCluster; + +module.exports = Object.assign(egg, { + Application: class CustomApplication extends egg.Application { + get [Symbol.for('egg#eggPath')]() { + return __dirname; + } + }, + startCluster(...args) { + if (process.env.CUSTOM_ENV && !process.env.EGG_SERVER_ENV) { + console.log('## EGG_SERVER_ENV is not pass'); + console.log('## CUSTOM_ENV:', process.env.CUSTOM_ENV); + process.env.EGG_SERVER_ENV = process.env.CUSTOM_ENV; + } + return originStartCluster(...args); + }, +}); diff --git a/test/fixtures/status/node_modules/custom-framework/package.json b/test/fixtures/status/node_modules/custom-framework/package.json new file mode 100644 index 0000000..a9328f7 --- /dev/null +++ b/test/fixtures/status/node_modules/custom-framework/package.json @@ -0,0 +1,7 @@ +{ + "name": "custom-framework", + "version": "1.0.0", + "dependencies": { + "egg": "*" + } +} \ No newline at end of file diff --git a/test/fixtures/status/package.json b/test/fixtures/status/package.json new file mode 100644 index 0000000..5fc5476 --- /dev/null +++ b/test/fixtures/status/package.json @@ -0,0 +1,10 @@ +{ + "name": "example", + "version": "1.0.0", + "dependencies": { + "egg": "^1.0.0" + }, + "egg": { + "framework": "custom-framework" + } +} diff --git a/test/start.test.js b/test/start.test.js index a034ac9..f00ce80 100644 --- a/test/start.test.js +++ b/test/start.test.js @@ -25,7 +25,7 @@ describe('test/start.test.js', () => { yield rimraf(homePath); }); beforeEach(() => mm(process.env, 'MOCK_HOME_DIR', homePath)); - afterEach(() => mm.restore); + afterEach(mm.restore); describe('start without daemon', () => { describe('full path', () => { @@ -338,30 +338,29 @@ describe('test/start.test.js', () => { }); describe('start with daemon', () => { - let app; - - before(function* () { - yield utils.cleanup(fixturePath); + let cwd; + beforeEach(function* () { + yield utils.cleanup(cwd); yield rimraf(logDir); yield mkdirp(logDir); yield fs.writeFile(path.join(logDir, 'master-stdout.log'), 'just for test'); yield fs.writeFile(path.join(logDir, 'master-stderr.log'), 'just for test'); }); - - after(function* () { - app.proc.kill('SIGTERM'); - yield utils.cleanup(fixturePath); + afterEach(function* () { + yield coffee.fork(eggBin, [ 'stop', cwd ]) + .debug() + .end(); + yield utils.cleanup(cwd); }); - it('should start', function* () { - app = coffee.fork(eggBin, [ 'start', '--daemon', '--workers=2', '--port=7002', fixturePath ]); - // app.debug(); - app.expect('code', 0); - - yield sleep(waitTime); - - assert(app.stdout.match(/starting egg.*example/)); - assert(app.stdout.match(/egg started on http:\/\/127\.0\.0\.1:7002/)); + it('should start custom-framework', function* () { + cwd = fixturePath; + yield coffee.fork(eggBin, [ 'start', '--daemon', '--workers=2', '--port=7002', cwd ]) + // .debug() + .expect('stdout', /Starting custom-framework application/) + .expect('stdout', /custom-framework started on http:\/\/127\.0\.0\.1:7002/) + .expect('code', 0) + .end(); // master log const stdout = yield fs.readFile(path.join(logDir, 'master-stdout.log'), 'utf-8'); @@ -377,6 +376,62 @@ describe('test/start.test.js', () => { const result = yield httpclient.request('http://127.0.0.1:7002'); assert(result.data.toString() === 'hi, egg'); }); + + it('should start default egg', function* () { + cwd = path.join(__dirname, 'fixtures/egg-app'); + yield coffee.fork(eggBin, [ 'start', '--daemon', '--workers=2', cwd ]) + .debug() + .expect('stdout', /Starting egg application/) + .expect('stdout', /egg started on http:\/\/127\.0\.0\.1:7001/) + .expect('code', 0) + .end(); + }); }); + describe('check status', () => { + const cwd = path.join(__dirname, 'fixtures/status'); + + after(function* () { + yield coffee.fork(eggBin, [ 'stop', cwd ]) + // .debug() + .end(); + yield utils.cleanup(cwd); + }); + + it('should status check success, exit with 0', function* () { + mm(process.env, 'WAIT_TIME', 5000); + yield coffee.fork(eggBin, [ 'start', '--daemon', '--workers=1' ], { cwd }) + // .debug() + .expect('stdout', /Wait Start: 5.../) + .expect('stdout', /custom-framework started/) + .expect('code', 0) + .end(); + }); + + it('should status check fail, exit with 1', function* () { + mm(process.env, 'WAIT_TIME', 5000); + mm(process.env, 'ERROR', 'error message'); + + const stderr = path.join(homePath, 'logs/master-stderr.log'); + + yield coffee.fork(eggBin, [ 'start', '--daemon', '--workers=1' ], { cwd }) + // .debug() + .expect('stderr', /nodejs.Error: error message/) + .expect('stderr', new RegExp(`Start failed, see ${stderr}`)) + .expect('code', 1) + .end(); + }); + + it('should status check timeout and exit with code 1', function* () { + mm(process.env, 'WAIT_TIME', 10000); + + yield coffee.fork(eggBin, [ 'start', '--daemon', '--workers=1', '--timeout=5000' ], { cwd }) + // .debug() + .expect('stdout', /Wait Start: 1.../) + .expect('stderr', /Start failed, 5s timeout/) + .expect('code', 1) + .end(); + }); + + }); }); diff --git a/test/stop.test.js b/test/stop.test.js index 2fd109f..bb79773 100644 --- a/test/stop.test.js +++ b/test/stop.test.js @@ -114,32 +114,29 @@ describe('test/stop.test.js', () => { }); describe('stop with daemon', () => { - let app; - let killer; - - before(function* () { + beforeEach(function* () { yield utils.cleanup(fixturePath); yield rimraf(logDir); - app = coffee.fork(eggBin, [ 'start', '--daemon', '--workers=2', fixturePath ]); - // app.debug(); - app.expect('code', 0); - yield sleep('10s'); + yield coffee.fork(eggBin, [ 'start', '--daemon', '--workers=2', fixturePath ]) + .debug() + .expect('code', 0) + .end(); const result = yield httpclient.request('http://127.0.0.1:7001'); assert(result.data.toString() === 'hi, egg'); }); - - after(function* () { - app.proc.kill('SIGTERM'); + afterEach(function* () { yield utils.cleanup(fixturePath); }); it('should stop', function* () { - killer = coffee.fork(eggBin, [ 'stop', fixturePath ]); - killer.debug(); - killer.expect('code', 0); + 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 killer.end(); yield sleep(waitTime); // master log @@ -148,25 +145,24 @@ describe('test/stop.test.js', () => { 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')); - // assert(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)); + + yield coffee.fork(eggBin, [ 'stop', fixturePath ]) + .debug() + .expect('stderr', /can't detect any running egg process/) + .expect('code', 0) + .end(); }); }); describe('stop with not exist', () => { - let killer; - it('should work', function* () { yield utils.cleanup(fixturePath); - killer = coffee.fork(eggBin, [ 'stop', fixturePath ]); - killer.debug(); - killer.expect('code', 0); - - yield sleep('5s'); - - assert(killer.stdout.includes(`[egg-scripts] stopping egg application at ${fixturePath}`)); - assert(killer.stderr.includes('can\'t detect any running egg process')); + yield coffee.fork(eggBin, [ 'stop', fixturePath ]) + .debug() + .expect('stdout', new RegExp(`\\[egg-scripts] stopping egg application at ${fixturePath}`)) + .expect('stderr', /can't detect any running egg process/) + .expect('code', 0) + .end(); }); }); });