start.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. 'use strict';
  2. const path = require('path');
  3. const Command = require('../command');
  4. const debug = require('debug')('egg-script:start');
  5. const { execFile } = require('mz/child_process');
  6. const fs = require('mz/fs');
  7. const homedir = require('node-homedir');
  8. const mkdirp = require('mz-modules/mkdirp');
  9. const moment = require('moment');
  10. const sleep = require('mz-modules/sleep');
  11. const spawn = require('child_process').spawn;
  12. const utils = require('egg-utils');
  13. class StartCommand extends Command {
  14. constructor(rawArgv) {
  15. super(rawArgv);
  16. this.usage = 'Usage: egg-scripts start [options] [baseDir]';
  17. this.serverBin = path.join(__dirname, '../start-cluster');
  18. this.options = {
  19. title: {
  20. description: 'process title description, use for kill grep, default to `egg-server-${APP_NAME}`',
  21. type: 'string',
  22. },
  23. workers: {
  24. description: 'numbers of app workers, default to `os.cpus().length`',
  25. type: 'number',
  26. alias: [ 'c', 'cluster' ],
  27. default: process.env.EGG_WORKERS,
  28. },
  29. port: {
  30. description: 'listening port, default to `process.env.PORT`',
  31. type: 'number',
  32. alias: 'p',
  33. default: process.env.PORT,
  34. },
  35. env: {
  36. description: 'server env, default to `process.env.EGG_SERVER_ENV`',
  37. default: process.env.EGG_SERVER_ENV,
  38. },
  39. framework: {
  40. description: 'specify framework that can be absolute path or npm package',
  41. type: 'string',
  42. },
  43. daemon: {
  44. description: 'whether run at background daemon mode',
  45. type: 'boolean',
  46. },
  47. stdout: {
  48. description: 'customize stdout file',
  49. type: 'string',
  50. },
  51. stderr: {
  52. description: 'customize stderr file',
  53. type: 'string',
  54. },
  55. timeout: {
  56. description: 'the maximum timeout when app starts',
  57. type: 'number',
  58. default: 300 * 1000,
  59. },
  60. 'ignore-stderr': {
  61. description: 'whether ignore stderr when app starts',
  62. type: 'boolean',
  63. },
  64. node: {
  65. description: 'customize node command path',
  66. type: 'string',
  67. },
  68. };
  69. }
  70. get description() {
  71. return 'Start server at prod mode';
  72. }
  73. * run(context) {
  74. context.execArgvObj = context.execArgvObj || {};
  75. const { argv, env, cwd, execArgvObj } = context;
  76. const HOME = homedir();
  77. const logDir = path.join(HOME, 'logs');
  78. // egg-script start
  79. // egg-script start ./server
  80. // egg-script start /opt/app
  81. let baseDir = argv._[0] || cwd;
  82. if (!path.isAbsolute(baseDir)) baseDir = path.join(cwd, baseDir);
  83. argv.baseDir = baseDir;
  84. const isDaemon = argv.daemon;
  85. argv.framework = yield this.getFrameworkPath({
  86. framework: argv.framework,
  87. baseDir,
  88. });
  89. this.frameworkName = yield this.getFrameworkName(argv.framework);
  90. const pkgInfo = require(path.join(baseDir, 'package.json'));
  91. argv.title = argv.title || `egg-server-${pkgInfo.name}`;
  92. argv.stdout = argv.stdout || path.join(logDir, 'master-stdout.log');
  93. argv.stderr = argv.stderr || path.join(logDir, 'master-stderr.log');
  94. // normalize env
  95. env.HOME = HOME;
  96. env.NODE_ENV = 'production';
  97. // it makes env big but more robust
  98. env.PATH = env.Path = [
  99. // for nodeinstall
  100. path.join(baseDir, 'node_modules/.bin'),
  101. // support `.node/bin`, due to npm5 will remove `node_modules/.bin`
  102. path.join(baseDir, '.node/bin'),
  103. // adjust env for win
  104. env.PATH || env.Path,
  105. ].filter(x => !!x).join(path.delimiter);
  106. // for alinode
  107. env.ENABLE_NODE_LOG = 'YES';
  108. env.NODE_LOG_DIR = env.NODE_LOG_DIR || path.join(logDir, 'alinode');
  109. yield mkdirp(env.NODE_LOG_DIR);
  110. // cli argv -> process.env.EGG_SERVER_ENV -> `undefined` then egg will use `prod`
  111. if (argv.env) {
  112. // if undefined, should not pass key due to `spwan`, https://github.com/nodejs/node/blob/master/lib/child_process.js#L470
  113. env.EGG_SERVER_ENV = argv.env;
  114. }
  115. // additional execArgv
  116. execArgvObj.deprecation = false; // --no-deprecation
  117. execArgvObj.traceWarnings = true; // --trace-warnings
  118. const command = argv.node || 'node';
  119. const options = {
  120. execArgv: context.execArgv, // getter for execArgvObj, see https://github.com/node-modules/common-bin/blob/master/lib/command.js#L332
  121. env,
  122. stdio: 'inherit',
  123. detached: false,
  124. };
  125. this.logger.info('Starting %s application at %s', this.frameworkName, baseDir);
  126. // remove unused properties from stringify, alias had been remove by `removeAlias`
  127. const ignoreKeys = [ '_', '$0', 'env', 'daemon', 'stdout', 'stderr', 'timeout', 'ignore-stderr', 'node' ];
  128. const clusterOptions = stringify(argv, ignoreKeys);
  129. // Note: `spawn` is not like `fork`, had to pass `execArgv` youself
  130. const eggArgs = [ ...(options.execArgv || []), this.serverBin, clusterOptions, `--title=${argv.title}` ];
  131. this.logger.info('Run node %s', eggArgs.join(' '));
  132. // whether run in the background.
  133. if (isDaemon) {
  134. this.logger.info(`Save log file to ${logDir}`);
  135. const [ stdout, stderr ] = yield [ getRotatelog(argv.stdout), getRotatelog(argv.stderr) ];
  136. options.stdio = [ 'ignore', stdout, stderr, 'ipc' ];
  137. options.detached = true;
  138. debug('Run spawn `%s %s`', command, eggArgs.join(' '));
  139. const child = this.child = spawn(command, eggArgs, options);
  140. this.isReady = false;
  141. child.on('message', msg => {
  142. /* istanbul ignore else */
  143. if (msg && msg.action === 'egg-ready') {
  144. this.isReady = true;
  145. this.logger.info('%s started on %s', this.frameworkName, msg.data.address);
  146. child.unref();
  147. child.disconnect();
  148. this.exit(0);
  149. }
  150. });
  151. // check start status
  152. yield this.checkStatus(argv);
  153. } else {
  154. options.stdio = [ 'inherit', 'inherit', 'inherit', 'ipc' ];
  155. debug('Run spawn `%s %s`', command, eggArgs.join(' '));
  156. const child = this.child = spawn(command, eggArgs, options);
  157. child.once('exit', code => {
  158. // command should exit after child process exit
  159. this.exit(code);
  160. });
  161. // attach master signal to child
  162. let signal;
  163. [ 'SIGINT', 'SIGQUIT', 'SIGTERM' ].forEach(event => {
  164. process.once(event, () => {
  165. debug('Kill child %s with %s', child.pid, signal);
  166. child.kill(event);
  167. });
  168. });
  169. }
  170. }
  171. * getFrameworkPath(params) {
  172. return utils.getFrameworkPath(params);
  173. }
  174. * getFrameworkName(framework) {
  175. const pkgPath = path.join(framework, 'package.json');
  176. let name = 'egg';
  177. try {
  178. const pkg = require(pkgPath);
  179. /* istanbul ignore else */
  180. if (pkg.name) name = pkg.name;
  181. } catch (_) {
  182. /* istanbul next */
  183. }
  184. return name;
  185. }
  186. * checkStatus({ stderr, timeout, 'ignore-stderr': ignoreStdErr }) {
  187. let count = 0;
  188. let hasError = false;
  189. let isSuccess = true;
  190. timeout = timeout / 1000;
  191. while (!this.isReady) {
  192. try {
  193. const stat = yield fs.stat(stderr);
  194. if (stat && stat.size > 0) {
  195. hasError = true;
  196. break;
  197. }
  198. } catch (_) {
  199. // nothing
  200. }
  201. if (count >= timeout) {
  202. this.logger.error('Start failed, %ds timeout', timeout);
  203. isSuccess = false;
  204. break;
  205. }
  206. yield sleep(1000);
  207. this.logger.log('Wait Start: %d...', ++count);
  208. }
  209. if (hasError) {
  210. try {
  211. const args = [ '-n', '100', stderr ];
  212. this.logger.error('tail %s', args.join(' '));
  213. const [ stdout ] = yield execFile('tail', args);
  214. this.logger.error('Got error when startup: ');
  215. this.logger.error(stdout);
  216. } catch (err) {
  217. this.logger.error('ignore tail error: %s', err);
  218. }
  219. isSuccess = ignoreStdErr;
  220. this.logger.error('Start got error, see %s', stderr);
  221. this.logger.error('Or use `--ignore-stderr` to ignore stderr at startup.');
  222. }
  223. if (!isSuccess) {
  224. this.child.kill('SIGTERM');
  225. yield sleep(1000);
  226. this.exit(1);
  227. }
  228. }
  229. }
  230. function* getRotatelog(logfile) {
  231. yield mkdirp(path.dirname(logfile));
  232. if (yield fs.exists(logfile)) {
  233. // format style: .20150602.193100
  234. const timestamp = moment().format('.YYYYMMDD.HHmmss');
  235. // Note: rename last log to next start time, not when last log file created
  236. yield fs.rename(logfile, logfile + timestamp);
  237. }
  238. return yield fs.open(logfile, 'a');
  239. }
  240. function stringify(obj, ignore) {
  241. const result = {};
  242. Object.keys(obj).forEach(key => {
  243. if (!ignore.includes(key)) {
  244. result[key] = obj[key];
  245. }
  246. });
  247. return JSON.stringify(result);
  248. }
  249. module.exports = StartCommand;