egg.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. 'use strict';
  2. const { performance } = require('perf_hooks');
  3. const path = require('path');
  4. const fs = require('fs');
  5. const ms = require('ms');
  6. const http = require('http');
  7. const EggCore = require('egg-core').EggCore;
  8. const cluster = require('cluster-client');
  9. const extend = require('extend2');
  10. const ContextLogger = require('egg-logger').EggContextLogger;
  11. const ContextCookies = require('egg-cookies');
  12. const CircularJSON = require('circular-json-for-egg');
  13. const ContextHttpClient = require('./core/context_httpclient');
  14. const Messenger = require('./core/messenger');
  15. const DNSCacheHttpClient = require('./core/dnscache_httpclient');
  16. const HttpClient = require('./core/httpclient');
  17. const createLoggers = require('./core/logger');
  18. const Singleton = require('./core/singleton');
  19. const utils = require('./core/utils');
  20. const BaseContextClass = require('./core/base_context_class');
  21. const BaseHookClass = require('./core/base_hook_class');
  22. const HTTPCLIENT = Symbol('EggApplication#httpclient');
  23. const LOGGERS = Symbol('EggApplication#loggers');
  24. const EGG_PATH = Symbol.for('egg#eggPath');
  25. const CLUSTER_CLIENTS = Symbol.for('egg#clusterClients');
  26. /**
  27. * Base on koa's Application
  28. * @see https://github.com/eggjs/egg-core
  29. * @see http://koajs.com/#application
  30. * @extends EggCore
  31. */
  32. class EggApplication extends EggCore {
  33. /**
  34. * @class
  35. * @param {Object} options
  36. * - {Object} [type] - type of instance, Agent and Application both extend koa, type can determine what it is.
  37. * - {String} [baseDir] - app root dir, default is `process.cwd()`
  38. * - {Object} [plugins] - custom plugin config, use it in unittest
  39. * - {String} [mode] - process mode, can be cluster / single, default is `cluster`
  40. */
  41. constructor(options = {}) {
  42. options.mode = options.mode || 'cluster';
  43. super(options);
  44. // export context base classes, let framework can impl sub class and over context extend easily.
  45. this.ContextCookies = ContextCookies;
  46. this.ContextLogger = ContextLogger;
  47. this.ContextHttpClient = ContextHttpClient;
  48. this.HttpClient = HttpClient;
  49. this.loader.loadConfig();
  50. /**
  51. * messenger instance
  52. * @member {Messenger}
  53. * @since 1.0.0
  54. */
  55. this.messenger = Messenger.create(this);
  56. // trigger serverDidReady hook when all app workers
  57. // and agent worker is ready
  58. this.messenger.once('egg-ready', () => {
  59. this.lifecycle.triggerServerDidReady();
  60. });
  61. // dump config after ready, ensure all the modifications during start will be recorded
  62. // make sure dumpConfig is the last ready callback
  63. this.ready(() => process.nextTick(() => {
  64. const dumpStartTime = Date.now();
  65. this.dumpConfig();
  66. this.dumpTiming();
  67. this.coreLogger.info('[egg:core] dump config after ready, %s', ms(Date.now() - dumpStartTime));
  68. }));
  69. this._setupTimeoutTimer();
  70. this.console.info('[egg:core] App root: %s', this.baseDir);
  71. this.console.info('[egg:core] All *.log files save on %j', this.config.logger.dir);
  72. this.console.info('[egg:core] Loaded enabled plugin %j', this.loader.orderPlugins);
  73. // Listen the error that promise had not catch, then log it in common-error
  74. this._unhandledRejectionHandler = this._unhandledRejectionHandler.bind(this);
  75. process.on('unhandledRejection', this._unhandledRejectionHandler);
  76. this[CLUSTER_CLIENTS] = [];
  77. /**
  78. * Wrap the Client with Leader/Follower Pattern
  79. *
  80. * @description almost the same as Agent.cluster API, the only different is that this method create Follower.
  81. *
  82. * @see https://github.com/node-modules/cluster-client
  83. * @param {Function} clientClass - client class function
  84. * @param {Object} [options]
  85. * - {Boolean} [autoGenerate] - whether generate delegate rule automatically, default is true
  86. * - {Function} [formatKey] - a method to tranform the subscription info into a string,default is JSON.stringify
  87. * - {Object} [transcode|JSON.stringify/parse]
  88. * - {Function} encode - custom serialize method
  89. * - {Function} decode - custom deserialize method
  90. * - {Boolean} [isBroadcast] - whether broadcast subscrption result to all followers or just one, default is true
  91. * - {Number} [responseTimeout] - response timeout, default is 3 seconds
  92. * - {Number} [maxWaitTime|30000] - leader startup max time, default is 30 seconds
  93. * @return {ClientWrapper} wrapper
  94. */
  95. this.cluster = (clientClass, options) => {
  96. options = Object.assign({}, this.config.clusterClient, options, {
  97. singleMode: this.options.mode === 'single',
  98. // cluster need a port that can't conflict on the environment
  99. port: this.options.clusterPort,
  100. // agent worker is leader, app workers are follower
  101. isLeader: this.type === 'agent',
  102. logger: this.coreLogger,
  103. });
  104. const client = cluster(clientClass, options);
  105. this._patchClusterClient(client);
  106. return client;
  107. };
  108. // register close function
  109. this.beforeClose(async () => {
  110. // single process mode will close agent before app close
  111. if (this.type === 'application' && this.options.mode === 'single') {
  112. await this.agent.close();
  113. }
  114. for (const logger of this.loggers.values()) {
  115. logger.close();
  116. }
  117. this.messenger.close();
  118. process.removeListener('unhandledRejection', this._unhandledRejectionHandler);
  119. });
  120. /**
  121. * Retreive base context class
  122. * @member {BaseContextClass} BaseContextClass
  123. * @since 1.0.0
  124. */
  125. this.BaseContextClass = BaseContextClass;
  126. /**
  127. * Retreive base controller
  128. * @member {Controller} Controller
  129. * @since 1.0.0
  130. */
  131. this.Controller = BaseContextClass;
  132. /**
  133. * Retreive base service
  134. * @member {Service} Service
  135. * @since 1.0.0
  136. */
  137. this.Service = BaseContextClass;
  138. /**
  139. * Retreive base subscription
  140. * @member {Subscription} Subscription
  141. * @since 2.12.0
  142. */
  143. this.Subscription = BaseContextClass;
  144. /**
  145. * Retreive base context class
  146. * @member {BaseHookClass} BaseHookClass
  147. */
  148. this.BaseHookClass = BaseHookClass;
  149. /**
  150. * Retreive base boot
  151. * @member {Boot}
  152. */
  153. this.Boot = BaseHookClass;
  154. }
  155. /**
  156. * print the information when console.log(app)
  157. * @return {Object} inspected app.
  158. * @since 1.0.0
  159. * @example
  160. * ```js
  161. * console.log(app);
  162. * =>
  163. * {
  164. * name: 'mockapp',
  165. * env: 'test',
  166. * subdomainOffset: 2,
  167. * config: '<egg config>',
  168. * controller: '<egg controller>',
  169. * service: '<egg service>',
  170. * middlewares: '<egg middlewares>',
  171. * urllib: '<egg urllib>',
  172. * loggers: '<egg loggers>'
  173. * }
  174. * ```
  175. */
  176. inspect() {
  177. const res = {
  178. env: this.config.env,
  179. };
  180. function delegate(res, app, keys) {
  181. for (const key of keys) {
  182. /* istanbul ignore else */
  183. if (app[key]) {
  184. res[key] = app[key];
  185. }
  186. }
  187. }
  188. function abbr(res, app, keys) {
  189. for (const key of keys) {
  190. /* istanbul ignore else */
  191. if (app[key]) {
  192. res[key] = `<egg ${key}>`;
  193. }
  194. }
  195. }
  196. delegate(res, this, [
  197. 'name',
  198. 'baseDir',
  199. 'subdomainOffset',
  200. ]);
  201. abbr(res, this, [
  202. 'config',
  203. 'controller',
  204. 'httpclient',
  205. 'loggers',
  206. 'middlewares',
  207. 'router',
  208. 'serviceClasses',
  209. ]);
  210. return res;
  211. }
  212. toJSON() {
  213. return this.inspect();
  214. }
  215. /**
  216. * http request helper base on {@link httpclient}, it will auto save httpclient log.
  217. * Keep the same api with `httpclient.request(url, args)`.
  218. *
  219. * See https://github.com/node-modules/urllib#api-doc for more details.
  220. *
  221. * @param {String} url request url address.
  222. * @param {Object} opts
  223. * - method {String} - Request method, defaults to GET. Could be GET, POST, DELETE or PUT. Alias 'type'.
  224. * - data {Object} - Data to be sent. Will be stringify automatically.
  225. * - dataType {String} - String - Type of response data. Could be `text` or `json`.
  226. * If it's `text`, the callbacked data would be a String.
  227. * If it's `json`, the data of callback would be a parsed JSON Object.
  228. * Default callbacked data would be a Buffer.
  229. * - headers {Object} - Request headers.
  230. * - timeout {Number} - Request timeout in milliseconds. Defaults to exports.TIMEOUT.
  231. * Include remote server connecting timeout and response timeout.
  232. * When timeout happen, will return ConnectionTimeout or ResponseTimeout.
  233. * - auth {String} - `username:password` used in HTTP Basic Authorization.
  234. * - followRedirect {Boolean} - follow HTTP 3xx responses as redirects. defaults to false.
  235. * - gzip {Boolean} - let you get the res object when request connected, default false. alias customResponse
  236. * - nestedQuerystring {Boolean} - urllib default use querystring to stringify form data which don't
  237. * support nested object, will use qs instead of querystring to support nested object by set this option to true.
  238. * - more options see https://www.npmjs.com/package/urllib
  239. * @return {Object}
  240. * - status {Number} - HTTP response status
  241. * - headers {Object} - HTTP response seaders
  242. * - res {Object} - HTTP response meta
  243. * - data {Object} - HTTP response body
  244. *
  245. * @example
  246. * ```js
  247. * const result = await app.curl('http://example.com/foo.json', {
  248. * method: 'GET',
  249. * dataType: 'json',
  250. * });
  251. * console.log(result.status, result.headers, result.data);
  252. * ```
  253. */
  254. curl(url, opts) {
  255. return this.httpclient.request(url, opts);
  256. }
  257. /**
  258. * HttpClient instance
  259. * @see https://github.com/node-modules/urllib
  260. * @member {HttpClient}
  261. */
  262. get httpclient() {
  263. if (!this[HTTPCLIENT]) {
  264. if (this.config.httpclient.enableDNSCache) {
  265. this[HTTPCLIENT] = new DNSCacheHttpClient(this);
  266. } else {
  267. this[HTTPCLIENT] = new this.HttpClient(this);
  268. }
  269. }
  270. return this[HTTPCLIENT];
  271. }
  272. /**
  273. * All loggers contain logger, coreLogger and customLogger
  274. * @member {Object}
  275. * @since 1.0.0
  276. */
  277. get loggers() {
  278. if (!this[LOGGERS]) {
  279. this[LOGGERS] = createLoggers(this);
  280. }
  281. return this[LOGGERS];
  282. }
  283. /**
  284. * Get logger by name, it's equal to app.loggers['name'],
  285. * but you can extend it with your own logical.
  286. * @param {String} name - logger name
  287. * @return {Logger} logger
  288. */
  289. getLogger(name) {
  290. return this.loggers[name] || null;
  291. }
  292. /**
  293. * application logger, log file is `$HOME/logs/{appname}/{appname}-web`
  294. * @member {Logger}
  295. * @since 1.0.0
  296. */
  297. get logger() {
  298. return this.getLogger('logger');
  299. }
  300. /**
  301. * core logger for framework and plugins, log file is `$HOME/logs/{appname}/egg-web`
  302. * @member {Logger}
  303. * @since 1.0.0
  304. */
  305. get coreLogger() {
  306. return this.getLogger('coreLogger');
  307. }
  308. _unhandledRejectionHandler(err) {
  309. if (!(err instanceof Error)) {
  310. const newError = new Error(String(err));
  311. // err maybe an object, try to copy the name, message and stack to the new error instance
  312. /* istanbul ignore else */
  313. if (err) {
  314. if (err.name) newError.name = err.name;
  315. if (err.message) newError.message = err.message;
  316. if (err.stack) newError.stack = err.stack;
  317. }
  318. err = newError;
  319. }
  320. /* istanbul ignore else */
  321. if (err.name === 'Error') {
  322. err.name = 'unhandledRejectionError';
  323. }
  324. this.coreLogger.error(err);
  325. }
  326. /**
  327. * dump out the config and meta object
  328. * @private
  329. * @return {Object} the result
  330. */
  331. dumpConfigToObject() {
  332. let ignoreList;
  333. try {
  334. // support array and set
  335. ignoreList = Array.from(this.config.dump.ignore);
  336. } catch (_) {
  337. ignoreList = [];
  338. }
  339. const json = extend(true, {}, { config: this.config, plugins: this.loader.allPlugins, appInfo: this.loader.appInfo });
  340. utils.convertObject(json, ignoreList);
  341. return {
  342. config: json,
  343. meta: this.loader.configMeta,
  344. };
  345. }
  346. /**
  347. * save app.config to `run/${type}_config.json`
  348. * @private
  349. */
  350. dumpConfig() {
  351. const rundir = this.config.rundir;
  352. try {
  353. /* istanbul ignore if */
  354. if (!fs.existsSync(rundir)) fs.mkdirSync(rundir);
  355. // get dumpped object
  356. const { config, meta } = this.dumpConfigToObject();
  357. // dump config
  358. const dumpFile = path.join(rundir, `${this.type}_config.json`);
  359. fs.writeFileSync(dumpFile, CircularJSON.stringify(config, null, 2));
  360. // dump config meta
  361. const dumpMetaFile = path.join(rundir, `${this.type}_config_meta.json`);
  362. fs.writeFileSync(dumpMetaFile, CircularJSON.stringify(meta, null, 2));
  363. } catch (err) {
  364. this.coreLogger.warn(`dumpConfig error: ${err.message}`);
  365. }
  366. }
  367. dumpTiming() {
  368. try {
  369. const json = this.timing.toJSON();
  370. const rundir = this.config.rundir;
  371. const dumpFile = path.join(rundir, `${this.type}_timing_${process.pid}.json`);
  372. fs.writeFileSync(dumpFile, CircularJSON.stringify(json, null, 2));
  373. // only disable, not clear bootstrap timing data.
  374. this.timing.disable();
  375. } catch (err) {
  376. this.coreLogger.warn(`dumpTiming error: ${err.message}`);
  377. }
  378. }
  379. get [EGG_PATH]() {
  380. return path.join(__dirname, '..');
  381. }
  382. _setupTimeoutTimer() {
  383. const startTimeoutTimer = setTimeout(() => {
  384. this.coreLogger.error(`${this.type} still doesn't ready after ${this.config.workerStartTimeout} ms.`);
  385. // log unfinished
  386. const json = this.timing.toJSON();
  387. for (const item of json) {
  388. if (item.end) continue;
  389. this.coreLogger.error(`unfinished timing item: ${CircularJSON.stringify(item)}`);
  390. }
  391. this.coreLogger.error(`check run/${this.type}_timing_${process.pid}.json for more details.`);
  392. this.emit('startTimeout');
  393. }, this.config.workerStartTimeout);
  394. this.ready(() => clearTimeout(startTimeoutTimer));
  395. }
  396. /**
  397. * app.env delegate app.config.env
  398. * @deprecated
  399. */
  400. get env() {
  401. this.deprecate('please use app.config.env instead');
  402. return this.config.env;
  403. }
  404. /* eslint no-empty-function: off */
  405. set env(_) {}
  406. /**
  407. * app.proxy delegate app.config.proxy
  408. * @deprecated
  409. */
  410. get proxy() {
  411. this.deprecate('please use app.config.proxy instead');
  412. return this.config.proxy;
  413. }
  414. /* eslint no-empty-function: off */
  415. set proxy(_) {}
  416. /**
  417. * create a singleton instance
  418. * @param {String} name - unique name for singleton
  419. * @param {Function|AsyncFunction} create - method will be invoked when singleton instance create
  420. */
  421. addSingleton(name, create) {
  422. const options = {};
  423. options.name = name;
  424. options.create = create;
  425. options.app = this;
  426. const singleton = new Singleton(options);
  427. const initPromise = singleton.init();
  428. if (initPromise) {
  429. this.beforeStart(async () => {
  430. await initPromise;
  431. });
  432. }
  433. }
  434. _patchClusterClient(client) {
  435. const create = client.create;
  436. client.create = (...args) => {
  437. const realClient = create.apply(client, args);
  438. this[CLUSTER_CLIENTS].push(realClient);
  439. this.beforeClose(() => cluster.close(realClient));
  440. return realClient;
  441. };
  442. }
  443. /**
  444. * Create an anonymous context, the context isn't request level, so the request is mocked.
  445. * then you can use context level API like `ctx.service`
  446. * @member {String} EggApplication#createAnonymousContext
  447. * @param {Request} req - if you want to mock request like querystring, you can pass an object to this function.
  448. * @return {Context} context
  449. */
  450. createAnonymousContext(req) {
  451. const request = {
  452. headers: {
  453. host: '127.0.0.1',
  454. 'x-forwarded-for': '127.0.0.1',
  455. },
  456. query: {},
  457. querystring: '',
  458. host: '127.0.0.1',
  459. hostname: '127.0.0.1',
  460. protocol: 'http',
  461. secure: 'false',
  462. method: 'GET',
  463. url: '/',
  464. path: '/',
  465. socket: {
  466. remoteAddress: '127.0.0.1',
  467. remotePort: 7001,
  468. },
  469. };
  470. if (req) {
  471. for (const key in req) {
  472. if (key === 'headers' || key === 'query' || key === 'socket') {
  473. Object.assign(request[key], req[key]);
  474. } else {
  475. request[key] = req[key];
  476. }
  477. }
  478. }
  479. const response = new http.ServerResponse(request);
  480. return this.createContext(request, response);
  481. }
  482. /**
  483. * Create egg context
  484. * @function EggApplication#createContext
  485. * @param {Req} req - node native Request object
  486. * @param {Res} res - node native Response object
  487. * @return {Context} context object
  488. */
  489. createContext(req, res) {
  490. const app = this;
  491. const context = Object.create(app.context);
  492. const request = context.request = Object.create(app.request);
  493. const response = context.response = Object.create(app.response);
  494. context.app = request.app = response.app = app;
  495. context.req = request.req = response.req = req;
  496. context.res = request.res = response.res = res;
  497. request.ctx = response.ctx = context;
  498. request.response = response;
  499. response.request = request;
  500. context.onerror = context.onerror.bind(context);
  501. context.originalUrl = request.originalUrl = req.url;
  502. /**
  503. * Request start time
  504. * @member {Number} Context#starttime
  505. */
  506. context.starttime = Date.now();
  507. if (this.config.logger.enablePerformanceTimer) {
  508. /**
  509. * Request start timer using `performance.now()`
  510. * @member {Number} Context#performanceStarttime
  511. */
  512. context.performanceStarttime = performance.now();
  513. }
  514. return context;
  515. }
  516. }
  517. module.exports = EggApplication;