app.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. 'use strict';
  2. const co = require('co');
  3. const path = require('path');
  4. const is = require('is-type-of');
  5. const { rimraf } = require('mz-modules');
  6. const sleep = require('ko-sleep');
  7. const ready = require('get-ready');
  8. const detectPort = require('detect-port');
  9. const debug = require('debug')('egg-mock');
  10. const EventEmitter = require('events');
  11. const os = require('os');
  12. const formatOptions = require('./format_options');
  13. const context = require('./context');
  14. const mockCustomLoader = require('./mock_custom_loader');
  15. const mockHttpServer = require('./mock_http_server');
  16. const ConsoleLogger = require('egg-logger').EggConsoleLogger;
  17. const consoleLogger = new ConsoleLogger({ level: 'INFO' });
  18. const apps = new Map();
  19. const INIT = Symbol('init');
  20. const APP_INIT = Symbol('appInit');
  21. const BIND_EVENT = Symbol('bindEvent');
  22. const INIT_ON_LISTENER = Symbol('initOnListener');
  23. const INIT_ONCE_LISTENER = Symbol('initOnceListener');
  24. const MESSENGER = Symbol('messenger');
  25. const MOCK_APP_METHOD = [
  26. 'ready',
  27. 'closed',
  28. 'close',
  29. '_agent',
  30. '_app',
  31. 'on',
  32. 'once',
  33. ];
  34. class MockApplication extends EventEmitter {
  35. constructor(options) {
  36. super();
  37. this.options = options;
  38. this.baseDir = options.baseDir;
  39. this.closed = false;
  40. this[APP_INIT] = false;
  41. this[INIT_ON_LISTENER] = new Set();
  42. this[INIT_ONCE_LISTENER] = new Set();
  43. ready.mixin(this);
  44. // listen once, otherwise will throw exception when emit error without listenr
  45. this.once('error', () => {});
  46. co(this[INIT].bind(this))
  47. .then(() => this.ready(true))
  48. .catch(err => {
  49. if (!this[APP_INIT]) {
  50. this.emit('error', err);
  51. }
  52. consoleLogger.error(err);
  53. this.ready(err);
  54. });
  55. }
  56. * [INIT]() {
  57. if (this.options.beforeInit) {
  58. yield this.options.beforeInit(this);
  59. delete this.options.beforeInit;
  60. }
  61. if (this.options.clean !== false) {
  62. const logDir = path.join(this.options.baseDir, 'logs');
  63. try {
  64. /* istanbul ignore if */
  65. if (os.platform() === 'win32') yield sleep(1000);
  66. yield rimraf(logDir);
  67. } catch (err) {
  68. /* istanbul ignore next */
  69. console.error(`remove log dir ${logDir} failed: ${err.stack}`);
  70. }
  71. }
  72. this.options.clusterPort = yield detectPort();
  73. debug('get clusterPort %s', this.options.clusterPort);
  74. const egg = require(this.options.framework);
  75. const Agent = egg.Agent;
  76. const agent = this._agent = new Agent(Object.assign({}, this.options));
  77. debug('agent instantiate');
  78. yield agent.ready();
  79. debug('agent ready');
  80. const Application = bindMessenger(egg.Application, agent);
  81. const app = this._app = new Application(Object.assign({}, this.options));
  82. // https://github.com/eggjs/egg/blob/8bb7c7e7d59d6aeca4b2ed1eb580368dcb731a4d/lib/egg.js#L125
  83. // egg single mode mount this at start(), so egg-mock should impel it.
  84. app.agent = agent;
  85. agent.app = app;
  86. // egg-mock plugin need to override egg context
  87. Object.assign(app.context, context);
  88. mockCustomLoader(app);
  89. debug('app instantiate');
  90. this[APP_INIT] = true;
  91. debug('this[APP_INIT] = true');
  92. this[BIND_EVENT]();
  93. debug('http server instantiate');
  94. mockHttpServer(app);
  95. yield app.ready();
  96. const msg = {
  97. action: 'egg-ready',
  98. data: this.options,
  99. };
  100. app.messenger._onMessage(msg);
  101. agent.messenger._onMessage(msg);
  102. debug('app ready');
  103. }
  104. [BIND_EVENT]() {
  105. for (const args of this[INIT_ON_LISTENER]) {
  106. debug('on(%s), use cache and pass to app', args);
  107. this._app.on(...args);
  108. this.removeListener(...args);
  109. }
  110. for (const args of this[INIT_ONCE_LISTENER]) {
  111. debug('once(%s), use cache and pass to app', args);
  112. this._app.on(...args);
  113. this.removeListener(...args);
  114. }
  115. }
  116. on(...args) {
  117. if (this[APP_INIT]) {
  118. debug('on(%s), pass to app', args);
  119. this._app.on(...args);
  120. } else {
  121. debug('on(%s), cache it because app has not init', args);
  122. this[INIT_ON_LISTENER].add(args);
  123. super.on(...args);
  124. }
  125. }
  126. once(...args) {
  127. if (this[APP_INIT]) {
  128. debug('once(%s), pass to app', args);
  129. this._app.once(...args);
  130. } else {
  131. debug('once(%s), cache it because app has not init', args);
  132. this[INIT_ONCE_LISTENER].add(args);
  133. super.on(...args);
  134. }
  135. }
  136. /**
  137. * close app
  138. * @return {Promise} promise
  139. */
  140. close() {
  141. this.closed = true;
  142. const self = this;
  143. const baseDir = this.baseDir;
  144. return co(function* () {
  145. if (self._app) {
  146. yield self._app.close();
  147. } else {
  148. // when app init throws an exception, must wait for app quit gracefully
  149. yield sleep(200);
  150. }
  151. if (self._agent) yield self._agent.close();
  152. apps.delete(baseDir);
  153. debug('delete app cache %s, remain %s', baseDir, [ ...apps.keys() ]);
  154. /* istanbul ignore if */
  155. if (os.platform() === 'win32') yield sleep(1000);
  156. });
  157. }
  158. }
  159. module.exports = function(options) {
  160. options = formatOptions(options);
  161. if (options.cache && apps.has(options.baseDir)) {
  162. const app = apps.get(options.baseDir);
  163. // return cache when it hasn't been killed
  164. if (!app.closed) {
  165. return app;
  166. }
  167. // delete the cache when it's closed
  168. apps.delete(options.baseDir);
  169. }
  170. let app = new MockApplication(options);
  171. app = new Proxy(app, {
  172. get(target, prop) {
  173. // don't delegate properties on MockApplication
  174. if (MOCK_APP_METHOD.includes(prop)) return getProperty(target, prop);
  175. if (!target[APP_INIT]) throw new Error(`can't get ${prop} before ready`);
  176. // it's asyncrounus when agent and app are loading,
  177. // so should get the properties after loader ready
  178. debug('proxy handler.get %s', prop);
  179. return target._app[prop];
  180. },
  181. set(target, prop, value) {
  182. if (MOCK_APP_METHOD.includes(prop)) return true;
  183. if (!target[APP_INIT]) throw new Error(`can't set ${prop} before ready`);
  184. debug('proxy handler.set %s', prop);
  185. target._app[prop] = value;
  186. return true;
  187. },
  188. defineProperty(target, prop, descriptor) {
  189. // can't define properties on MockApplication
  190. if (MOCK_APP_METHOD.includes(prop)) return true;
  191. if (!target[APP_INIT]) throw new Error(`can't defineProperty ${prop} before ready`);
  192. debug('proxy handler.defineProperty %s', prop);
  193. Object.defineProperty(target._app, prop, descriptor);
  194. return true;
  195. },
  196. deleteProperty(target, prop) {
  197. // can't delete properties on MockApplication
  198. if (MOCK_APP_METHOD.includes(prop)) return true;
  199. if (!target[APP_INIT]) throw new Error(`can't delete ${prop} before ready`);
  200. debug('proxy handler.deleteProperty %s', prop);
  201. delete target._app[prop];
  202. return true;
  203. },
  204. getOwnPropertyDescriptor(target, prop) {
  205. if (MOCK_APP_METHOD.includes(prop)) return Object.getOwnPropertyDescriptor(target, prop);
  206. if (!target[APP_INIT]) throw new Error(`can't getOwnPropertyDescriptor ${prop} before ready`);
  207. debug('proxy handler.getOwnPropertyDescriptor %s', prop);
  208. return Object.getOwnPropertyDescriptor(target._app, prop);
  209. },
  210. getPrototypeOf(target) {
  211. if (!target[APP_INIT]) throw new Error('can\'t getPrototypeOf before ready');
  212. debug('proxy handler.getPrototypeOf %s');
  213. return Object.getPrototypeOf(target._app);
  214. },
  215. });
  216. apps.set(options.baseDir, app);
  217. return app;
  218. };
  219. function getProperty(target, prop) {
  220. const member = target[prop];
  221. if (is.function(member)) {
  222. return member.bind(target);
  223. }
  224. return member;
  225. }
  226. function bindMessenger(Application, agent) {
  227. const agentMessenger = agent.messenger;
  228. return class MessengerApplication extends Application {
  229. constructor(options) {
  230. super(options);
  231. // enable app to send to a random agent
  232. this.messenger.sendRandom = (action, data) => {
  233. this.messenger.sendToAgent(action, data);
  234. };
  235. // enable agent to send to a random app
  236. agentMessenger.on('egg-ready', () => {
  237. agentMessenger.sendRandom = (action, data) => {
  238. agentMessenger.sendToApp(action, data);
  239. };
  240. });
  241. agentMessenger.send = new Proxy(agentMessenger.send, {
  242. apply: this._sendMessage.bind(this),
  243. });
  244. }
  245. _sendMessage(target, thisArg, [ action, data, to ]) {
  246. const appMessenger = this.messenger;
  247. setImmediate(() => {
  248. if (to === 'app') {
  249. appMessenger._onMessage({ action, data });
  250. } else {
  251. agentMessenger._onMessage({ action, data });
  252. }
  253. });
  254. }
  255. get messenger() {
  256. return this[MESSENGER];
  257. }
  258. set messenger(m) {
  259. m.send = new Proxy(m.send, {
  260. apply: this._sendMessage.bind(this),
  261. });
  262. this[MESSENGER] = m;
  263. }
  264. get [Symbol.for('egg#eggPath')]() { return path.join(__dirname, 'tmp'); }
  265. };
  266. }