1 | const debug = require('util').debuglog('egg-mock:cluster');
|
2 | const path = require('path');
|
3 | const os = require('os');
|
4 | const childProcess = require('child_process');
|
5 | const Coffee = require('coffee').Coffee;
|
6 | const ready = require('get-ready');
|
7 | const co = require('co');
|
8 | const awaitEvent = require('await-event');
|
9 | const supertestRequest = require('./supertest');
|
10 | const { sleep, rimrafSync } = require('./utils');
|
11 | const formatOptions = require('./format_options');
|
12 |
|
13 | const clusters = new Map();
|
14 | const serverBin = path.join(__dirname, 'start-cluster');
|
15 | const requestCallFunctionFile = path.join(__dirname, 'request_call_function.js');
|
16 | let masterPort = 17000;
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 | class ClusterApplication extends Coffee {
|
44 | |
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 | constructor(options) {
|
57 | const opt = options.opt;
|
58 | delete options.opt;
|
59 |
|
60 |
|
61 | options.port = options.port || ++masterPort;
|
62 |
|
63 | if (!options.workers) options.workers = 1;
|
64 |
|
65 | const args = [ JSON.stringify(options) ];
|
66 | debug('fork %s, args: %s, opt: %j', serverBin, args.join(' '), opt);
|
67 | super({
|
68 | method: 'fork',
|
69 | cmd: serverBin,
|
70 | args,
|
71 | opt,
|
72 | });
|
73 |
|
74 | ready.mixin(this);
|
75 |
|
76 | this.port = options.port;
|
77 | this.baseDir = options.baseDir;
|
78 |
|
79 |
|
80 | this.debug(process.env.DEBUG ? 0 : 2);
|
81 |
|
82 |
|
83 | if (options.coverage === false) {
|
84 | this.coverage(false);
|
85 | }
|
86 |
|
87 | process.nextTick(() => {
|
88 | this.proc.on('message', msg => {
|
89 |
|
90 | const action = msg && msg.action ? msg.action : msg;
|
91 | switch (action) {
|
92 | case 'egg-ready':
|
93 | this.emit('close', 0);
|
94 | break;
|
95 | case 'app-worker-died':
|
96 | case 'agent-worker-died':
|
97 | this.emit('close', 1);
|
98 | break;
|
99 | default:
|
100 |
|
101 | break;
|
102 | }
|
103 | });
|
104 | });
|
105 |
|
106 | this.end(() => this.ready(true));
|
107 | }
|
108 |
|
109 | |
110 |
|
111 |
|
112 |
|
113 | get process() {
|
114 | return this.proc;
|
115 | }
|
116 |
|
117 | |
118 |
|
119 |
|
120 |
|
121 | callback() {
|
122 | return this;
|
123 | }
|
124 |
|
125 | |
126 |
|
127 |
|
128 |
|
129 |
|
130 | get url() {
|
131 | return 'http://127.0.0.1:' + this.port;
|
132 | }
|
133 |
|
134 | |
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 | address() {
|
141 | return {
|
142 | port: this.port,
|
143 | };
|
144 | }
|
145 |
|
146 | |
147 |
|
148 |
|
149 |
|
150 |
|
151 | listen() {
|
152 | return this;
|
153 | }
|
154 |
|
155 | |
156 |
|
157 |
|
158 |
|
159 | close() {
|
160 | this.closed = true;
|
161 |
|
162 | const proc = this.proc;
|
163 | const baseDir = this.baseDir;
|
164 | return co(function* () {
|
165 | if (proc.connected) {
|
166 | proc.kill('SIGTERM');
|
167 | yield awaitEvent.call(proc, 'exit');
|
168 | }
|
169 |
|
170 | clusters.delete(baseDir);
|
171 | debug('delete cluster cache %s, remain %s', baseDir, [ ...clusters.keys() ]);
|
172 |
|
173 |
|
174 | if (os.platform() === 'win32') yield sleep(1000);
|
175 | });
|
176 | }
|
177 |
|
178 |
|
179 | get router() {
|
180 | const that = this;
|
181 | return {
|
182 | pathFor(url) {
|
183 | return that._callFunctionOnAppWorker('pathFor', [ url ], 'router', true);
|
184 | },
|
185 | };
|
186 | }
|
187 |
|
188 | |
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 | mockLog(logger) {
|
196 | logger = logger || 'logger';
|
197 | this._callFunctionOnAppWorker('mockLog', [ logger ], null, true);
|
198 | }
|
199 |
|
200 | |
201 |
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 |
|
208 | expectLog(str, logger) {
|
209 | logger = logger || 'logger';
|
210 | this._callFunctionOnAppWorker('expectLog', [ str, logger ], null, true);
|
211 | }
|
212 |
|
213 | |
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 | notExpectLog(str, logger) {
|
222 | logger = logger || 'logger';
|
223 | this._callFunctionOnAppWorker('notExpectLog', [ str, logger ], null, true);
|
224 | }
|
225 |
|
226 | httpRequest() {
|
227 | return supertestRequest(this);
|
228 | }
|
229 |
|
230 | _callFunctionOnAppWorker(method, args = [], property = undefined, needResult = false) {
|
231 | for (let i = 0; i < args.length; i++) {
|
232 | const arg = args[i];
|
233 | if (typeof arg === 'function') {
|
234 | args[i] = {
|
235 | __egg_mock_type: 'function',
|
236 | value: arg.toString(),
|
237 | };
|
238 | } else if (arg instanceof Error) {
|
239 | const errObject = {
|
240 | __egg_mock_type: 'error',
|
241 | name: arg.name,
|
242 | message: arg.message,
|
243 | stack: arg.stack,
|
244 | };
|
245 | for (const key in arg) {
|
246 | if (key !== 'name' && key !== 'message' && key !== 'stack') {
|
247 | errObject[key] = arg[key];
|
248 | }
|
249 | }
|
250 | args[i] = errObject;
|
251 | }
|
252 | }
|
253 | const data = {
|
254 | port: this.port,
|
255 | method,
|
256 | args,
|
257 | property,
|
258 | needResult,
|
259 | };
|
260 | const child = childProcess.spawnSync(process.execPath, [
|
261 | requestCallFunctionFile,
|
262 | JSON.stringify(data),
|
263 | ], {
|
264 | stdio: 'pipe',
|
265 | });
|
266 | if (child.stderr && child.stderr.length > 0) {
|
267 | console.error(child.stderr.toString());
|
268 | }
|
269 |
|
270 | let result;
|
271 | if (child.stdout && child.stdout.length > 0) {
|
272 | if (needResult) {
|
273 | result = JSON.parse(child.stdout.toString());
|
274 | } else {
|
275 | console.error(child.stdout.toString());
|
276 | }
|
277 | }
|
278 |
|
279 | if (child.status !== 0) {
|
280 | throw new Error(child.stderr.toString());
|
281 | }
|
282 | if (child.error) {
|
283 | throw child.error;
|
284 | }
|
285 |
|
286 | return result;
|
287 | }
|
288 | }
|
289 |
|
290 | module.exports = options => {
|
291 | options = formatOptions(options);
|
292 | if (options.cache && clusters.has(options.baseDir)) {
|
293 | const clusterApp = clusters.get(options.baseDir);
|
294 |
|
295 | if (!clusterApp.closed) {
|
296 | return clusterApp;
|
297 | }
|
298 |
|
299 |
|
300 | clusters.delete(options.baseDir);
|
301 | }
|
302 |
|
303 | if (options.clean !== false) {
|
304 | const logDir = path.join(options.baseDir, 'logs');
|
305 | try {
|
306 | rimrafSync(logDir);
|
307 | } catch (err) {
|
308 |
|
309 | console.error(`remove log dir ${logDir} failed: ${err.stack}`);
|
310 | }
|
311 | }
|
312 |
|
313 | let clusterApp = new ClusterApplication(options);
|
314 | clusterApp = new Proxy(clusterApp, {
|
315 | get(target, prop) {
|
316 | debug('proxy handler.get %s', prop);
|
317 |
|
318 | const method = prop;
|
319 | if (typeof method === 'string' && /^mock\w+$/.test(method) && target[method] === undefined) {
|
320 | return function mockProxy(...args) {
|
321 | return target._callFunctionOnAppWorker(method, args, null, true);
|
322 | };
|
323 | }
|
324 |
|
325 | return target[prop];
|
326 | },
|
327 | });
|
328 |
|
329 | clusters.set(options.baseDir, clusterApp);
|
330 | return clusterApp;
|
331 | };
|
332 |
|
333 |
|
334 | module.exports.restore = () => {
|
335 | for (const clusterApp of clusters.values()) {
|
336 | clusterApp.mockRestore();
|
337 | }
|
338 | };
|
339 |
|
340 |
|
341 | process.on('exit', () => {
|
342 | for (const clusterApp of clusters.values()) {
|
343 | clusterApp.close();
|
344 | }
|
345 | });
|