blob: 840b2448b0c174a26f45f97ff95c7759e36d6e3c [file] [log] [blame]
Randolf Jungbcb3bc82023-06-26 16:30:141/**
2 * Copyright 2023 Google Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
17 if (kind === "m") throw new TypeError("Private method is not writable");
18 if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
19 if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
20 return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
21};
22var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
23 if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
24 if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
25 return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
26};
27var _Process_instances, _Process_executablePath, _Process_args, _Process_browserProcess, _Process_exited, _Process_hooksRan, _Process_onExitHook, _Process_browserProcessExiting, _Process_runHooks, _Process_configureStdio, _Process_clearListeners, _Process_onDriverProcessExit, _Process_onDriverProcessSignal;
28import childProcess from 'child_process';
29import { accessSync } from 'fs';
30import os from 'os';
31import path from 'path';
32import readline from 'readline';
33import { executablePathByBrowser, resolveSystemExecutablePath, } from './browser-data/browser-data.js';
34import { Cache } from './Cache.js';
35import { debug } from './debug.js';
36import { detectBrowserPlatform } from './detectPlatform.js';
37const debugLaunch = debug('puppeteer:browsers:launcher');
38/**
39 * @public
40 */
41export function computeExecutablePath(options) {
42 options.platform ??= detectBrowserPlatform();
43 if (!options.platform) {
44 throw new Error(`Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})`);
45 }
46 const installationDir = new Cache(options.cacheDir).installationDir(options.browser, options.platform, options.buildId);
47 return path.join(installationDir, executablePathByBrowser[options.browser](options.platform, options.buildId));
48}
49/**
50 * @public
51 */
52export function computeSystemExecutablePath(options) {
53 options.platform ??= detectBrowserPlatform();
54 if (!options.platform) {
55 throw new Error(`Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})`);
56 }
57 const path = resolveSystemExecutablePath(options.browser, options.platform, options.channel);
58 try {
59 accessSync(path);
60 }
61 catch (error) {
62 throw new Error(`Could not find Google Chrome executable for channel '${options.channel}' at '${path}'.`);
63 }
64 return path;
65}
66/**
67 * @public
68 */
69export function launch(opts) {
70 return new Process(opts);
71}
72/**
73 * @public
74 */
75export const CDP_WEBSOCKET_ENDPOINT_REGEX = /^DevTools listening on (ws:\/\/.*)$/;
76/**
77 * @public
78 */
79export const WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX = /^WebDriver BiDi listening on (ws:\/\/.*)$/;
80/**
81 * @public
82 */
83export class Process {
84 constructor(opts) {
85 _Process_instances.add(this);
86 _Process_executablePath.set(this, void 0);
87 _Process_args.set(this, void 0);
88 _Process_browserProcess.set(this, void 0);
89 _Process_exited.set(this, false);
90 // The browser process can be closed externally or from the driver process. We
91 // need to invoke the hooks only once though but we don't know how many times
92 // we will be invoked.
93 _Process_hooksRan.set(this, false);
94 _Process_onExitHook.set(this, async () => { });
95 _Process_browserProcessExiting.set(this, void 0);
96 _Process_onDriverProcessExit.set(this, (_code) => {
97 this.kill();
98 });
99 _Process_onDriverProcessSignal.set(this, (signal) => {
100 switch (signal) {
101 case 'SIGINT':
102 this.kill();
103 process.exit(130);
104 case 'SIGTERM':
105 case 'SIGHUP':
106 void this.close();
107 break;
108 }
109 });
110 __classPrivateFieldSet(this, _Process_executablePath, opts.executablePath, "f");
111 __classPrivateFieldSet(this, _Process_args, opts.args ?? [], "f");
112 opts.pipe ??= false;
113 opts.dumpio ??= false;
114 opts.handleSIGINT ??= true;
115 opts.handleSIGTERM ??= true;
116 opts.handleSIGHUP ??= true;
117 // On non-windows platforms, `detached: true` makes child process a
118 // leader of a new process group, making it possible to kill child
119 // process tree with `.kill(-pid)` command. @see
120 // https://nodejs.org/api/child_process.html#child_process_options_detached
121 opts.detached ??= process.platform !== 'win32';
122 const stdio = __classPrivateFieldGet(this, _Process_instances, "m", _Process_configureStdio).call(this, {
123 pipe: opts.pipe,
124 dumpio: opts.dumpio,
125 });
126 debugLaunch(`Launching ${__classPrivateFieldGet(this, _Process_executablePath, "f")} ${__classPrivateFieldGet(this, _Process_args, "f").join(' ')}`, {
127 detached: opts.detached,
128 env: opts.env,
129 stdio,
130 });
131 __classPrivateFieldSet(this, _Process_browserProcess, childProcess.spawn(__classPrivateFieldGet(this, _Process_executablePath, "f"), __classPrivateFieldGet(this, _Process_args, "f"), {
132 detached: opts.detached,
133 env: opts.env,
134 stdio,
135 }), "f");
136 debugLaunch(`Launched ${__classPrivateFieldGet(this, _Process_browserProcess, "f").pid}`);
137 if (opts.dumpio) {
138 __classPrivateFieldGet(this, _Process_browserProcess, "f").stderr?.pipe(process.stderr);
139 __classPrivateFieldGet(this, _Process_browserProcess, "f").stdout?.pipe(process.stdout);
140 }
141 process.on('exit', __classPrivateFieldGet(this, _Process_onDriverProcessExit, "f"));
142 if (opts.handleSIGINT) {
143 process.on('SIGINT', __classPrivateFieldGet(this, _Process_onDriverProcessSignal, "f"));
144 }
145 if (opts.handleSIGTERM) {
146 process.on('SIGTERM', __classPrivateFieldGet(this, _Process_onDriverProcessSignal, "f"));
147 }
148 if (opts.handleSIGHUP) {
149 process.on('SIGHUP', __classPrivateFieldGet(this, _Process_onDriverProcessSignal, "f"));
150 }
151 if (opts.onExit) {
152 __classPrivateFieldSet(this, _Process_onExitHook, opts.onExit, "f");
153 }
154 __classPrivateFieldSet(this, _Process_browserProcessExiting, new Promise((resolve, reject) => {
155 __classPrivateFieldGet(this, _Process_browserProcess, "f").once('exit', async () => {
156 debugLaunch(`Browser process ${__classPrivateFieldGet(this, _Process_browserProcess, "f").pid} onExit`);
157 __classPrivateFieldGet(this, _Process_instances, "m", _Process_clearListeners).call(this);
158 __classPrivateFieldSet(this, _Process_exited, true, "f");
159 try {
160 await __classPrivateFieldGet(this, _Process_instances, "m", _Process_runHooks).call(this);
161 }
162 catch (err) {
163 reject(err);
164 return;
165 }
166 resolve();
167 });
168 }), "f");
169 }
170 get nodeProcess() {
171 return __classPrivateFieldGet(this, _Process_browserProcess, "f");
172 }
173 async close() {
174 await __classPrivateFieldGet(this, _Process_instances, "m", _Process_runHooks).call(this);
175 if (!__classPrivateFieldGet(this, _Process_exited, "f")) {
176 this.kill();
177 }
178 return __classPrivateFieldGet(this, _Process_browserProcessExiting, "f");
179 }
180 hasClosed() {
181 return __classPrivateFieldGet(this, _Process_browserProcessExiting, "f");
182 }
183 kill() {
184 debugLaunch(`Trying to kill ${__classPrivateFieldGet(this, _Process_browserProcess, "f").pid}`);
185 // If the process failed to launch (for example if the browser executable path
186 // is invalid), then the process does not get a pid assigned. A call to
187 // `proc.kill` would error, as the `pid` to-be-killed can not be found.
188 if (__classPrivateFieldGet(this, _Process_browserProcess, "f") &&
189 __classPrivateFieldGet(this, _Process_browserProcess, "f").pid &&
190 pidExists(__classPrivateFieldGet(this, _Process_browserProcess, "f").pid)) {
191 try {
192 debugLaunch(`Browser process ${__classPrivateFieldGet(this, _Process_browserProcess, "f").pid} exists`);
193 if (process.platform === 'win32') {
194 try {
195 childProcess.execSync(`taskkill /pid ${__classPrivateFieldGet(this, _Process_browserProcess, "f").pid} /T /F`);
196 }
197 catch (error) {
198 debugLaunch(`Killing ${__classPrivateFieldGet(this, _Process_browserProcess, "f").pid} using taskkill failed`, error);
199 // taskkill can fail to kill the process e.g. due to missing permissions.
200 // Let's kill the process via Node API. This delays killing of all child
201 // processes of `this.proc` until the main Node.js process dies.
202 __classPrivateFieldGet(this, _Process_browserProcess, "f").kill();
203 }
204 }
205 else {
206 // on linux the process group can be killed with the group id prefixed with
207 // a minus sign. The process group id is the group leader's pid.
208 const processGroupId = -__classPrivateFieldGet(this, _Process_browserProcess, "f").pid;
209 try {
210 process.kill(processGroupId, 'SIGKILL');
211 }
212 catch (error) {
213 debugLaunch(`Killing ${__classPrivateFieldGet(this, _Process_browserProcess, "f").pid} using process.kill failed`, error);
214 // Killing the process group can fail due e.g. to missing permissions.
215 // Let's kill the process via Node API. This delays killing of all child
216 // processes of `this.proc` until the main Node.js process dies.
217 __classPrivateFieldGet(this, _Process_browserProcess, "f").kill('SIGKILL');
218 }
219 }
220 }
221 catch (error) {
222 throw new Error(`${PROCESS_ERROR_EXPLANATION}\nError cause: ${isErrorLike(error) ? error.stack : error}`);
223 }
224 }
225 __classPrivateFieldGet(this, _Process_instances, "m", _Process_clearListeners).call(this);
226 }
Alex Rudenko1552f2b2023-07-11 11:18:32227 waitForLineOutput(regex, timeout = 0) {
Randolf Jungbcb3bc82023-06-26 16:30:14228 if (!__classPrivateFieldGet(this, _Process_browserProcess, "f").stderr) {
229 throw new Error('`browserProcess` does not have stderr.');
230 }
231 const rl = readline.createInterface(__classPrivateFieldGet(this, _Process_browserProcess, "f").stderr);
232 let stderr = '';
233 return new Promise((resolve, reject) => {
234 rl.on('line', onLine);
235 rl.on('close', onClose);
236 __classPrivateFieldGet(this, _Process_browserProcess, "f").on('exit', onClose);
237 __classPrivateFieldGet(this, _Process_browserProcess, "f").on('error', onClose);
Alex Rudenko1552f2b2023-07-11 11:18:32238 const timeoutId = timeout > 0 ? setTimeout(onTimeout, timeout) : undefined;
Randolf Jungbcb3bc82023-06-26 16:30:14239 const cleanup = () => {
240 if (timeoutId) {
241 clearTimeout(timeoutId);
242 }
243 rl.off('line', onLine);
244 rl.off('close', onClose);
245 __classPrivateFieldGet(this, _Process_browserProcess, "f").off('exit', onClose);
246 __classPrivateFieldGet(this, _Process_browserProcess, "f").off('error', onClose);
247 };
248 function onClose(error) {
249 cleanup();
250 reject(new Error([
251 `Failed to launch the browser process!${error ? ' ' + error.message : ''}`,
252 stderr,
253 '',
254 'TROUBLESHOOTING: https://pptr.dev/troubleshooting',
255 '',
256 ].join('\n')));
257 }
258 function onTimeout() {
259 cleanup();
260 reject(new TimeoutError(`Timed out after ${timeout} ms while waiting for the WS endpoint URL to appear in stdout!`));
261 }
262 function onLine(line) {
263 stderr += line + '\n';
264 const match = line.match(regex);
265 if (!match) {
266 return;
267 }
268 cleanup();
269 // The RegExp matches, so this will obviously exist.
270 resolve(match[1]);
271 }
272 });
273 }
274}
275_Process_executablePath = new WeakMap(), _Process_args = new WeakMap(), _Process_browserProcess = new WeakMap(), _Process_exited = new WeakMap(), _Process_hooksRan = new WeakMap(), _Process_onExitHook = new WeakMap(), _Process_browserProcessExiting = new WeakMap(), _Process_onDriverProcessExit = new WeakMap(), _Process_onDriverProcessSignal = new WeakMap(), _Process_instances = new WeakSet(), _Process_runHooks = async function _Process_runHooks() {
276 if (__classPrivateFieldGet(this, _Process_hooksRan, "f")) {
277 return;
278 }
279 __classPrivateFieldSet(this, _Process_hooksRan, true, "f");
280 await __classPrivateFieldGet(this, _Process_onExitHook, "f").call(this);
281}, _Process_configureStdio = function _Process_configureStdio(opts) {
282 if (opts.pipe) {
283 if (opts.dumpio) {
284 return ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'];
285 }
286 else {
287 return ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'];
288 }
289 }
290 else {
291 if (opts.dumpio) {
292 return ['pipe', 'pipe', 'pipe'];
293 }
294 else {
295 return ['pipe', 'ignore', 'pipe'];
296 }
297 }
298}, _Process_clearListeners = function _Process_clearListeners() {
299 process.off('exit', __classPrivateFieldGet(this, _Process_onDriverProcessExit, "f"));
300 process.off('SIGINT', __classPrivateFieldGet(this, _Process_onDriverProcessSignal, "f"));
301 process.off('SIGTERM', __classPrivateFieldGet(this, _Process_onDriverProcessSignal, "f"));
302 process.off('SIGHUP', __classPrivateFieldGet(this, _Process_onDriverProcessSignal, "f"));
303};
304const PROCESS_ERROR_EXPLANATION = `Puppeteer was unable to kill the process which ran the browser binary.
305This means that, on future Puppeteer launches, Puppeteer might not be able to launch the browser.
306Please check your open processes and ensure that the browser processes that Puppeteer launched have been killed.
307If you think this is a bug, please report it on the Puppeteer issue tracker.`;
308/**
309 * @internal
310 */
311function pidExists(pid) {
312 try {
313 return process.kill(pid, 0);
314 }
315 catch (error) {
316 if (isErrnoException(error)) {
317 if (error.code && error.code === 'ESRCH') {
318 return false;
319 }
320 }
321 throw error;
322 }
323}
324/**
325 * @internal
326 */
327export function isErrorLike(obj) {
328 return (typeof obj === 'object' && obj !== null && 'name' in obj && 'message' in obj);
329}
330/**
331 * @internal
332 */
333export function isErrnoException(obj) {
334 return (isErrorLike(obj) &&
335 ('errno' in obj || 'code' in obj || 'path' in obj || 'syscall' in obj));
336}
337/**
338 * @public
339 */
340export class TimeoutError extends Error {
341 /**
342 * @internal
343 */
344 constructor(message) {
345 super(message);
346 this.name = this.constructor.name;
347 Error.captureStackTrace(this, this.constructor);
348 }
349}
350//# sourceMappingURL=launch.js.map