child.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import { fork } from 'child_process';
  2. import { createServer } from 'net';
  3. import { Worker } from 'worker_threads';
  4. import { ChildCommand, ParentCommand } from '../enums';
  5. import { EventEmitter } from 'events';
  6. /**
  7. * @see https://nodejs.org/api/process.html#process_exit_codes
  8. */
  9. const exitCodesErrors = {
  10. 1: 'Uncaught Fatal Exception',
  11. 2: 'Unused',
  12. 3: 'Internal JavaScript Parse Error',
  13. 4: 'Internal JavaScript Evaluation Failure',
  14. 5: 'Fatal Error',
  15. 6: 'Non-function Internal Exception Handler',
  16. 7: 'Internal Exception Handler Run-Time Failure',
  17. 8: 'Unused',
  18. 9: 'Invalid Argument',
  19. 10: 'Internal JavaScript Run-Time Failure',
  20. 12: 'Invalid Debug Argument',
  21. 13: 'Unfinished Top-Level Await',
  22. };
  23. /**
  24. * Child class
  25. *
  26. * This class is used to create a child process or worker thread, and allows using
  27. * isolated processes or threads for processing jobs.
  28. *
  29. */
  30. export class Child extends EventEmitter {
  31. constructor(mainFile, processFile, opts = {
  32. useWorkerThreads: false,
  33. }) {
  34. super();
  35. this.mainFile = mainFile;
  36. this.processFile = processFile;
  37. this.opts = opts;
  38. this._exitCode = null;
  39. this._signalCode = null;
  40. this._killed = false;
  41. }
  42. get pid() {
  43. if (this.childProcess) {
  44. return this.childProcess.pid;
  45. }
  46. else if (this.worker) {
  47. // Worker threads pids can become negative when they are terminated
  48. // so we need to use the absolute value to index the retained object
  49. return Math.abs(this.worker.threadId);
  50. }
  51. else {
  52. throw new Error('No child process or worker thread');
  53. }
  54. }
  55. get exitCode() {
  56. return this._exitCode;
  57. }
  58. get signalCode() {
  59. return this._signalCode;
  60. }
  61. get killed() {
  62. if (this.childProcess) {
  63. return this.childProcess.killed;
  64. }
  65. return this._killed;
  66. }
  67. async init() {
  68. const execArgv = await convertExecArgv(process.execArgv);
  69. let parent;
  70. if (this.opts.useWorkerThreads) {
  71. this.worker = parent = new Worker(this.mainFile, Object.assign({ execArgv, stdin: true, stdout: true, stderr: true }, (this.opts.workerThreadsOptions
  72. ? this.opts.workerThreadsOptions
  73. : {})));
  74. }
  75. else {
  76. this.childProcess = parent = fork(this.mainFile, [], Object.assign({ execArgv, stdio: 'pipe' }, (this.opts.workerForkOptions ? this.opts.workerForkOptions : {})));
  77. }
  78. parent.on('exit', (exitCode, signalCode) => {
  79. this._exitCode = exitCode;
  80. // Coerce to null if undefined for backwards compatibility
  81. signalCode = typeof signalCode === 'undefined' ? null : signalCode;
  82. this._signalCode = signalCode;
  83. this._killed = true;
  84. this.emit('exit', exitCode, signalCode);
  85. // Clean all listeners, we do not expect any more events after "exit"
  86. parent.removeAllListeners();
  87. this.removeAllListeners();
  88. });
  89. parent.on('error', (...args) => this.emit('error', ...args));
  90. parent.on('message', (...args) => this.emit('message', ...args));
  91. parent.on('close', (...args) => this.emit('close', ...args));
  92. parent.stdout.pipe(process.stdout);
  93. parent.stderr.pipe(process.stderr);
  94. await this.initChild();
  95. }
  96. async send(msg) {
  97. return new Promise((resolve, reject) => {
  98. if (this.childProcess) {
  99. this.childProcess.send(msg, (err) => {
  100. if (err) {
  101. reject(err);
  102. }
  103. else {
  104. resolve();
  105. }
  106. });
  107. }
  108. else if (this.worker) {
  109. resolve(this.worker.postMessage(msg));
  110. }
  111. else {
  112. resolve();
  113. }
  114. });
  115. }
  116. killProcess(signal = 'SIGKILL') {
  117. if (this.childProcess) {
  118. this.childProcess.kill(signal);
  119. }
  120. else if (this.worker) {
  121. this.worker.terminate();
  122. }
  123. }
  124. async kill(signal = 'SIGKILL', timeoutMs) {
  125. if (this.hasProcessExited()) {
  126. return;
  127. }
  128. const onExit = onExitOnce(this.childProcess || this.worker);
  129. this.killProcess(signal);
  130. if (timeoutMs !== undefined && (timeoutMs === 0 || isFinite(timeoutMs))) {
  131. const timeoutHandle = setTimeout(() => {
  132. if (!this.hasProcessExited()) {
  133. this.killProcess('SIGKILL');
  134. }
  135. }, timeoutMs);
  136. await onExit;
  137. clearTimeout(timeoutHandle);
  138. }
  139. await onExit;
  140. }
  141. async initChild() {
  142. const onComplete = new Promise((resolve, reject) => {
  143. const onMessageHandler = (msg) => {
  144. if (!Object.values(ParentCommand).includes(msg.cmd)) {
  145. return;
  146. }
  147. if (msg.cmd === ParentCommand.InitCompleted) {
  148. resolve();
  149. }
  150. else if (msg.cmd === ParentCommand.InitFailed) {
  151. const err = new Error();
  152. err.stack = msg.err.stack;
  153. err.message = msg.err.message;
  154. reject(err);
  155. }
  156. this.off('message', onMessageHandler);
  157. this.off('close', onCloseHandler);
  158. };
  159. const onCloseHandler = (code, signal) => {
  160. if (code > 128) {
  161. code -= 128;
  162. }
  163. const msg = exitCodesErrors[code] || `Unknown exit code ${code}`;
  164. reject(new Error(`Error initializing child: ${msg} and signal ${signal}`));
  165. this.off('message', onMessageHandler);
  166. this.off('close', onCloseHandler);
  167. };
  168. this.on('message', onMessageHandler);
  169. this.on('close', onCloseHandler);
  170. });
  171. await this.send({
  172. cmd: ChildCommand.Init,
  173. value: this.processFile,
  174. });
  175. await onComplete;
  176. }
  177. hasProcessExited() {
  178. return !!(this.exitCode !== null || this.signalCode);
  179. }
  180. }
  181. function onExitOnce(child) {
  182. return new Promise(resolve => {
  183. child.once('exit', () => resolve());
  184. });
  185. }
  186. const getFreePort = async () => {
  187. return new Promise(resolve => {
  188. const server = createServer();
  189. server.listen(0, () => {
  190. const { port } = server.address();
  191. server.close(() => resolve(port));
  192. });
  193. });
  194. };
  195. const convertExecArgv = async (execArgv) => {
  196. const standard = [];
  197. const convertedArgs = [];
  198. for (let i = 0; i < execArgv.length; i++) {
  199. const arg = execArgv[i];
  200. if (arg.indexOf('--inspect') === -1) {
  201. standard.push(arg);
  202. }
  203. else {
  204. const argName = arg.split('=')[0];
  205. const port = await getFreePort();
  206. convertedArgs.push(`${argName}=${port}`);
  207. }
  208. }
  209. return standard.concat(convertedArgs);
  210. };
  211. //# sourceMappingURL=child.js.map