123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- 'use strict';
- const path = require('path');
- const Command = require('../command');
- const debug = require('debug')('egg-script:start');
- const { execFile } = require('mz/child_process');
- const fs = require('mz/fs');
- const homedir = require('node-homedir');
- const mkdirp = require('mz-modules/mkdirp');
- const moment = require('moment');
- const sleep = require('mz-modules/sleep');
- const spawn = require('child_process').spawn;
- const utils = require('egg-utils');
- class StartCommand extends Command {
- constructor(rawArgv) {
- super(rawArgv);
- this.usage = 'Usage: egg-scripts start [options] [baseDir]';
- this.serverBin = path.join(__dirname, '../start-cluster');
- this.options = {
- title: {
- description: 'process title description, use for kill grep, default to `egg-server-${APP_NAME}`',
- type: 'string',
- },
- workers: {
- description: 'numbers of app workers, default to `os.cpus().length`',
- type: 'number',
- alias: [ 'c', 'cluster' ],
- default: process.env.EGG_WORKERS,
- },
- port: {
- description: 'listening port, default to `process.env.PORT`',
- type: 'number',
- alias: 'p',
- default: process.env.PORT,
- },
- env: {
- description: 'server env, default to `process.env.EGG_SERVER_ENV`',
- default: process.env.EGG_SERVER_ENV,
- },
- framework: {
- description: 'specify framework that can be absolute path or npm package',
- type: 'string',
- },
- daemon: {
- description: 'whether run at background daemon mode',
- type: 'boolean',
- },
- stdout: {
- description: 'customize stdout file',
- type: 'string',
- },
- stderr: {
- description: 'customize stderr file',
- type: 'string',
- },
- timeout: {
- description: 'the maximum timeout when app starts',
- type: 'number',
- default: 300 * 1000,
- },
- 'ignore-stderr': {
- description: 'whether ignore stderr when app starts',
- type: 'boolean',
- },
- node: {
- description: 'customize node command path',
- type: 'string',
- },
- };
- }
- get description() {
- return 'Start server at prod mode';
- }
- * run(context) {
- context.execArgvObj = context.execArgvObj || {};
- const { argv, env, cwd, execArgvObj } = context;
- const HOME = homedir();
- const logDir = path.join(HOME, 'logs');
- // egg-script start
- // egg-script start ./server
- // egg-script start /opt/app
- let baseDir = argv._[0] || cwd;
- if (!path.isAbsolute(baseDir)) baseDir = path.join(cwd, baseDir);
- argv.baseDir = baseDir;
- const isDaemon = argv.daemon;
- argv.framework = yield this.getFrameworkPath({
- framework: argv.framework,
- baseDir,
- });
- this.frameworkName = yield this.getFrameworkName(argv.framework);
- const pkgInfo = require(path.join(baseDir, 'package.json'));
- argv.title = argv.title || `egg-server-${pkgInfo.name}`;
- argv.stdout = argv.stdout || path.join(logDir, 'master-stdout.log');
- argv.stderr = argv.stderr || path.join(logDir, 'master-stderr.log');
- // normalize env
- env.HOME = HOME;
- env.NODE_ENV = 'production';
- // it makes env big but more robust
- env.PATH = env.Path = [
- // for nodeinstall
- path.join(baseDir, 'node_modules/.bin'),
- // support `.node/bin`, due to npm5 will remove `node_modules/.bin`
- path.join(baseDir, '.node/bin'),
- // adjust env for win
- env.PATH || env.Path,
- ].filter(x => !!x).join(path.delimiter);
- // for alinode
- env.ENABLE_NODE_LOG = 'YES';
- env.NODE_LOG_DIR = env.NODE_LOG_DIR || path.join(logDir, 'alinode');
- yield mkdirp(env.NODE_LOG_DIR);
- // cli argv -> process.env.EGG_SERVER_ENV -> `undefined` then egg will use `prod`
- if (argv.env) {
- // if undefined, should not pass key due to `spwan`, https://github.com/nodejs/node/blob/master/lib/child_process.js#L470
- env.EGG_SERVER_ENV = argv.env;
- }
- // additional execArgv
- execArgvObj.deprecation = false; // --no-deprecation
- execArgvObj.traceWarnings = true; // --trace-warnings
- const command = argv.node || 'node';
- const options = {
- execArgv: context.execArgv, // getter for execArgvObj, see https://github.com/node-modules/common-bin/blob/master/lib/command.js#L332
- env,
- stdio: 'inherit',
- detached: false,
- };
- this.logger.info('Starting %s application at %s', this.frameworkName, baseDir);
- // remove unused properties from stringify, alias had been remove by `removeAlias`
- const ignoreKeys = [ '_', '$0', 'env', 'daemon', 'stdout', 'stderr', 'timeout', 'ignore-stderr', 'node' ];
- const clusterOptions = stringify(argv, ignoreKeys);
- // Note: `spawn` is not like `fork`, had to pass `execArgv` youself
- const eggArgs = [ ...(options.execArgv || []), this.serverBin, clusterOptions, `--title=${argv.title}` ];
- this.logger.info('Run node %s', eggArgs.join(' '));
- // whether run in the background.
- if (isDaemon) {
- 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;
- debug('Run spawn `%s %s`', command, eggArgs.join(' '));
- const child = this.child = spawn(command, eggArgs, options);
- this.isReady = false;
- child.on('message', msg => {
- /* istanbul ignore else */
- if (msg && msg.action === 'egg-ready') {
- this.isReady = true;
- this.logger.info('%s started on %s', this.frameworkName, msg.data.address);
- child.unref();
- child.disconnect();
- this.exit(0);
- }
- });
- // check start status
- yield this.checkStatus(argv);
- } else {
- options.stdio = [ 'inherit', 'inherit', 'inherit', 'ipc' ];
- debug('Run spawn `%s %s`', command, eggArgs.join(' '));
- const child = this.child = spawn(command, eggArgs, options);
- child.once('exit', code => {
- // command should exit after child process exit
- this.exit(code);
- });
- // attach master signal to child
- let signal;
- [ 'SIGINT', 'SIGQUIT', 'SIGTERM' ].forEach(event => {
- process.once(event, () => {
- debug('Kill child %s with %s', child.pid, signal);
- child.kill(event);
- });
- });
- }
- }
- * getFrameworkPath(params) {
- return utils.getFrameworkPath(params);
- }
- * getFrameworkName(framework) {
- const pkgPath = path.join(framework, 'package.json');
- let name = 'egg';
- try {
- const pkg = require(pkgPath);
- /* istanbul ignore else */
- if (pkg.name) name = pkg.name;
- } catch (_) {
- /* istanbul next */
- }
- return name;
- }
- * checkStatus({ stderr, timeout, 'ignore-stderr': ignoreStdErr }) {
- let count = 0;
- let hasError = false;
- let isSuccess = true;
- timeout = timeout / 1000;
- while (!this.isReady) {
- try {
- const stat = yield fs.stat(stderr);
- if (stat && stat.size > 0) {
- hasError = true;
- 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 (hasError) {
- try {
- const args = [ '-n', '100', stderr ];
- this.logger.error('tail %s', args.join(' '));
- const [ stdout ] = yield execFile('tail', args);
- this.logger.error('Got error when startup: ');
- this.logger.error(stdout);
- } catch (err) {
- this.logger.error('ignore tail error: %s', err);
- }
- isSuccess = ignoreStdErr;
- this.logger.error('Start got error, see %s', stderr);
- this.logger.error('Or use `--ignore-stderr` to ignore stderr at startup.');
- }
- if (!isSuccess) {
- this.child.kill('SIGTERM');
- yield sleep(1000);
- this.exit(1);
- }
- }
- }
- function* getRotatelog(logfile) {
- yield mkdirp(path.dirname(logfile));
- if (yield fs.exists(logfile)) {
- // format style: .20150602.193100
- const timestamp = moment().format('.YYYYMMDD.HHmmss');
- // Note: rename last log to next start time, not when last log file created
- yield fs.rename(logfile, logfile + timestamp);
- }
- return yield fs.open(logfile, 'a');
- }
- function stringify(obj, ignore) {
- const result = {};
- Object.keys(obj).forEach(key => {
- if (!ignore.includes(key)) {
- result[key] = obj[key];
- }
- });
- return JSON.stringify(result);
- }
- module.exports = StartCommand;
|