processLauncher.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.envArrayToObject = envArrayToObject;
  6. exports.gracefullyCloseAll = gracefullyCloseAll;
  7. exports.gracefullyCloseSet = void 0;
  8. exports.gracefullyProcessExitDoNotHang = gracefullyProcessExitDoNotHang;
  9. exports.launchProcess = launchProcess;
  10. var _fs = _interopRequireDefault(require("fs"));
  11. var childProcess = _interopRequireWildcard(require("child_process"));
  12. var readline = _interopRequireWildcard(require("readline"));
  13. var _ = require("./");
  14. var _fileUtils = require("./fileUtils");
  15. function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
  16. function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
  17. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  18. /**
  19. * Copyright 2017 Google Inc. All rights reserved.
  20. * Modifications copyright (c) Microsoft Corporation.
  21. *
  22. * Licensed under the Apache License, Version 2.0 (the "License");
  23. * you may not use this file except in compliance with the License.
  24. * You may obtain a copy of the License at
  25. *
  26. * http://www.apache.org/licenses/LICENSE-2.0
  27. *
  28. * Unless required by applicable law or agreed to in writing, software
  29. * distributed under the License is distributed on an "AS IS" BASIS,
  30. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31. * See the License for the specific language governing permissions and
  32. * limitations under the License.
  33. */
  34. const gracefullyCloseSet = exports.gracefullyCloseSet = new Set();
  35. const killSet = new Set();
  36. async function gracefullyCloseAll() {
  37. await Promise.all(Array.from(gracefullyCloseSet).map(gracefullyClose => gracefullyClose().catch(e => {})));
  38. }
  39. function gracefullyProcessExitDoNotHang(code) {
  40. // Force exit after 30 seconds.
  41. // eslint-disable-next-line no-restricted-properties
  42. setTimeout(() => process.exit(code), 30000);
  43. // Meanwhile, try to gracefully close all browsers.
  44. gracefullyCloseAll().then(() => {
  45. // eslint-disable-next-line no-restricted-properties
  46. process.exit(code);
  47. });
  48. }
  49. function exitHandler() {
  50. for (const kill of killSet) kill();
  51. }
  52. let sigintHandlerCalled = false;
  53. function sigintHandler() {
  54. const exitWithCode130 = () => {
  55. // Give tests a chance to see that launched process did exit and dispatch any async calls.
  56. if ((0, _.isUnderTest)()) {
  57. // eslint-disable-next-line no-restricted-properties
  58. setTimeout(() => process.exit(130), 1000);
  59. } else {
  60. // eslint-disable-next-line no-restricted-properties
  61. process.exit(130);
  62. }
  63. };
  64. if (sigintHandlerCalled) {
  65. // Resort to default handler from this point on, just in case we hang/stall.
  66. process.off('SIGINT', sigintHandler);
  67. // Upon second Ctrl+C, immediately kill browsers and exit.
  68. // This prevents hanging in the case where closing the browser takes a lot of time or is buggy.
  69. for (const kill of killSet) kill();
  70. exitWithCode130();
  71. } else {
  72. sigintHandlerCalled = true;
  73. gracefullyCloseAll().then(() => exitWithCode130());
  74. }
  75. }
  76. function sigtermHandler() {
  77. gracefullyCloseAll();
  78. }
  79. function sighupHandler() {
  80. gracefullyCloseAll();
  81. }
  82. const installedHandlers = new Set();
  83. const processHandlers = {
  84. exit: exitHandler,
  85. SIGINT: sigintHandler,
  86. SIGTERM: sigtermHandler,
  87. SIGHUP: sighupHandler
  88. };
  89. function addProcessHandlerIfNeeded(name) {
  90. if (!installedHandlers.has(name)) {
  91. installedHandlers.add(name);
  92. process.on(name, processHandlers[name]);
  93. }
  94. }
  95. async function launchProcess(options) {
  96. const stdio = options.stdio === 'pipe' ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'];
  97. options.log(`<launching> ${options.command} ${options.args ? options.args.join(' ') : ''}`);
  98. const spawnOptions = {
  99. // On non-windows platforms, `detached: true` makes child process a leader of a new
  100. // process group, making it possible to kill child process tree with `.kill(-pid)` command.
  101. // @see https://nodejs.org/api/child_process.html#child_process_options_detached
  102. detached: process.platform !== 'win32',
  103. env: options.env,
  104. cwd: options.cwd,
  105. shell: options.shell,
  106. stdio
  107. };
  108. const spawnedProcess = childProcess.spawn(options.command, options.args || [], spawnOptions);
  109. const cleanup = async () => {
  110. options.log(`[pid=${spawnedProcess.pid || 'N/A'}] starting temporary directories cleanup`);
  111. const errors = await (0, _fileUtils.removeFolders)(options.tempDirectories);
  112. for (let i = 0; i < options.tempDirectories.length; ++i) {
  113. if (errors[i]) options.log(`[pid=${spawnedProcess.pid || 'N/A'}] exception while removing ${options.tempDirectories[i]}: ${errors[i]}`);
  114. }
  115. options.log(`[pid=${spawnedProcess.pid || 'N/A'}] finished temporary directories cleanup`);
  116. };
  117. // Prevent Unhandled 'error' event.
  118. spawnedProcess.on('error', () => {});
  119. if (!spawnedProcess.pid) {
  120. let failed;
  121. const failedPromise = new Promise((f, r) => failed = f);
  122. spawnedProcess.once('error', error => {
  123. failed(new Error('Failed to launch: ' + error));
  124. });
  125. return cleanup().then(() => failedPromise).then(e => Promise.reject(e));
  126. }
  127. options.log(`<launched> pid=${spawnedProcess.pid}`);
  128. const stdout = readline.createInterface({
  129. input: spawnedProcess.stdout
  130. });
  131. stdout.on('line', data => {
  132. options.log(`[pid=${spawnedProcess.pid}][out] ` + data);
  133. });
  134. const stderr = readline.createInterface({
  135. input: spawnedProcess.stderr
  136. });
  137. stderr.on('line', data => {
  138. options.log(`[pid=${spawnedProcess.pid}][err] ` + data);
  139. });
  140. let processClosed = false;
  141. let fulfillCleanup = () => {};
  142. const waitForCleanup = new Promise(f => fulfillCleanup = f);
  143. spawnedProcess.once('exit', (exitCode, signal) => {
  144. options.log(`[pid=${spawnedProcess.pid}] <process did exit: exitCode=${exitCode}, signal=${signal}>`);
  145. processClosed = true;
  146. gracefullyCloseSet.delete(gracefullyClose);
  147. killSet.delete(killProcessAndCleanup);
  148. options.onExit(exitCode, signal);
  149. // Cleanup as process exits.
  150. cleanup().then(fulfillCleanup);
  151. });
  152. addProcessHandlerIfNeeded('exit');
  153. if (options.handleSIGINT) addProcessHandlerIfNeeded('SIGINT');
  154. if (options.handleSIGTERM) addProcessHandlerIfNeeded('SIGTERM');
  155. if (options.handleSIGHUP) addProcessHandlerIfNeeded('SIGHUP');
  156. gracefullyCloseSet.add(gracefullyClose);
  157. killSet.add(killProcessAndCleanup);
  158. let gracefullyClosing = false;
  159. async function gracefullyClose() {
  160. // We keep listeners until we are done, to handle 'exit' and 'SIGINT' while
  161. // asynchronously closing to prevent zombie processes. This might introduce
  162. // reentrancy to this function, for example user sends SIGINT second time.
  163. // In this case, let's forcefully kill the process.
  164. if (gracefullyClosing) {
  165. options.log(`[pid=${spawnedProcess.pid}] <forcefully close>`);
  166. killProcess();
  167. await waitForCleanup; // Ensure the process is dead and we have cleaned up.
  168. return;
  169. }
  170. gracefullyClosing = true;
  171. options.log(`[pid=${spawnedProcess.pid}] <gracefully close start>`);
  172. await options.attemptToGracefullyClose().catch(() => killProcess());
  173. await waitForCleanup; // Ensure the process is dead and we have cleaned up.
  174. options.log(`[pid=${spawnedProcess.pid}] <gracefully close end>`);
  175. }
  176. // This method has to be sync to be used in the 'exit' event handler.
  177. function killProcess() {
  178. gracefullyCloseSet.delete(gracefullyClose);
  179. killSet.delete(killProcessAndCleanup);
  180. options.log(`[pid=${spawnedProcess.pid}] <kill>`);
  181. if (spawnedProcess.pid && !spawnedProcess.killed && !processClosed) {
  182. options.log(`[pid=${spawnedProcess.pid}] <will force kill>`);
  183. // Force kill the browser.
  184. try {
  185. if (process.platform === 'win32') {
  186. const taskkillProcess = childProcess.spawnSync(`taskkill /pid ${spawnedProcess.pid} /T /F`, {
  187. shell: true
  188. });
  189. const [stdout, stderr] = [taskkillProcess.stdout.toString(), taskkillProcess.stderr.toString()];
  190. if (stdout) options.log(`[pid=${spawnedProcess.pid}] taskkill stdout: ${stdout}`);
  191. if (stderr) options.log(`[pid=${spawnedProcess.pid}] taskkill stderr: ${stderr}`);
  192. } else {
  193. process.kill(-spawnedProcess.pid, 'SIGKILL');
  194. }
  195. } catch (e) {
  196. options.log(`[pid=${spawnedProcess.pid}] exception while trying to kill process: ${e}`);
  197. // the process might have already stopped
  198. }
  199. } else {
  200. options.log(`[pid=${spawnedProcess.pid}] <skipped force kill spawnedProcess.killed=${spawnedProcess.killed} processClosed=${processClosed}>`);
  201. }
  202. }
  203. function killProcessAndCleanup() {
  204. killProcess();
  205. options.log(`[pid=${spawnedProcess.pid || 'N/A'}] starting temporary directories cleanup`);
  206. for (const dir of options.tempDirectories) {
  207. try {
  208. _fs.default.rmSync(dir, {
  209. force: true,
  210. recursive: true,
  211. maxRetries: 5
  212. });
  213. } catch (e) {
  214. options.log(`[pid=${spawnedProcess.pid || 'N/A'}] exception while removing ${dir}: ${e}`);
  215. }
  216. }
  217. options.log(`[pid=${spawnedProcess.pid || 'N/A'}] finished temporary directories cleanup`);
  218. }
  219. function killAndWait() {
  220. killProcess();
  221. return waitForCleanup;
  222. }
  223. return {
  224. launchedProcess: spawnedProcess,
  225. gracefullyClose,
  226. kill: killAndWait
  227. };
  228. }
  229. function envArrayToObject(env) {
  230. const result = {};
  231. for (const {
  232. name,
  233. value
  234. } of env) result[name] = value;
  235. return result;
  236. }