single-file-cli 2.7.2 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/bidi.js ADDED
@@ -0,0 +1,141 @@
1
+ /*
2
+ * Copyright 2010-2026 Gildas Lormeau
3
+ * contact : gildas.lormeau <at> gmail.com
4
+ *
5
+ * This file is part of SingleFile.
6
+ *
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
+ * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
+ * of the License, or (at your option) any later version.
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
+ * General Public License for more details.
16
+ *
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
+ * Source.
22
+ */
23
+
24
+ /* global WebSocket, setTimeout, clearTimeout */
25
+
26
+ const SUCCESS_TYPE = "success";
27
+ const EVENT_TYPE = "event";
28
+ const OPEN_ATTEMPT_TIMEOUT = 500;
29
+ const OPEN_RETRY_DELAY = 200;
30
+ const DEFAULT_COMMAND_TIMEOUT = 30000;
31
+
32
+ export { connect };
33
+
34
+ async function connect(url, { timeout, commandTimeout = DEFAULT_COMMAND_TIMEOUT, isClosed = () => false } = {}) {
35
+ const socket = await openSocket(url, timeout, isClosed);
36
+ const pendingCommands = new Map();
37
+ const listeners = new Map();
38
+ let nextId = 1, closeError;
39
+ socket.addEventListener("close", () => {
40
+ closeError = new Error("The BiDi connection was closed");
41
+ pendingCommands.forEach(({ reject }) => reject(closeError));
42
+ pendingCommands.clear();
43
+ });
44
+ socket.addEventListener("message", event => {
45
+ const message = JSON.parse(event.data);
46
+ if (message.id !== undefined && pendingCommands.has(message.id)) {
47
+ const { resolve, reject } = pendingCommands.get(message.id);
48
+ pendingCommands.delete(message.id);
49
+ if (message.type == SUCCESS_TYPE) {
50
+ resolve(message.result);
51
+ } else {
52
+ const error = new Error(message.message);
53
+ error.code = message.error;
54
+ reject(error);
55
+ }
56
+ } else if (message.type == EVENT_TYPE) {
57
+ const eventListeners = listeners.get(message.method);
58
+ if (eventListeners) {
59
+ Array.from(eventListeners).forEach(listener => listener(message.params));
60
+ }
61
+ }
62
+ });
63
+ return { send, addEventListener, removeEventListener, close };
64
+
65
+ function send(method, params = {}, { timeout = commandTimeout } = {}) {
66
+ if (closeError) {
67
+ return Promise.reject(closeError);
68
+ }
69
+ const id = nextId++;
70
+ socket.send(JSON.stringify({ id, method, params }));
71
+ return new Promise((resolve, reject) => {
72
+ let timeoutId;
73
+ if (timeout) {
74
+ timeoutId = setTimeout(() => {
75
+ pendingCommands.delete(id);
76
+ reject(new Error(method + " timed out after " + timeout + " ms"));
77
+ }, timeout);
78
+ }
79
+ pendingCommands.set(id, {
80
+ resolve: value => {
81
+ clearTimeout(timeoutId);
82
+ resolve(value);
83
+ },
84
+ reject: error => {
85
+ clearTimeout(timeoutId);
86
+ reject(error);
87
+ }
88
+ });
89
+ });
90
+ }
91
+
92
+ function addEventListener(eventName, listener) {
93
+ if (!listeners.has(eventName)) {
94
+ listeners.set(eventName, new Set());
95
+ }
96
+ listeners.get(eventName).add(listener);
97
+ }
98
+
99
+ function removeEventListener(eventName, listener) {
100
+ const eventListeners = listeners.get(eventName);
101
+ if (eventListeners) {
102
+ eventListeners.delete(listener);
103
+ }
104
+ }
105
+
106
+ function close() {
107
+ try {
108
+ socket.close();
109
+ } catch {
110
+ // ignored
111
+ }
112
+ }
113
+ }
114
+
115
+ async function openSocket(url, timeout, isClosed) {
116
+ const timeoutTime = Date.now() + timeout;
117
+ while (Date.now() < timeoutTime && !isClosed()) {
118
+ const socket = new WebSocket(url);
119
+ const opened = await new Promise(resolve => {
120
+ const timeoutId = setTimeout(() => resolve(false), OPEN_ATTEMPT_TIMEOUT);
121
+ socket.addEventListener("open", () => {
122
+ clearTimeout(timeoutId);
123
+ resolve(true);
124
+ });
125
+ socket.addEventListener("error", () => {
126
+ clearTimeout(timeoutId);
127
+ resolve(false);
128
+ });
129
+ });
130
+ if (opened) {
131
+ return socket;
132
+ }
133
+ try {
134
+ socket.close();
135
+ } catch {
136
+ // ignored
137
+ }
138
+ await new Promise(resolve => setTimeout(resolve, OPEN_RETRY_DELAY));
139
+ }
140
+ throw new Error(isClosed() ? "The browser exited unexpectedly" : "The browser is not responding");
141
+ }
package/lib/browser.js CHANGED
@@ -1,38 +1,33 @@
1
1
  /*
2
2
  * Copyright 2010-2024 Gildas Lormeau
3
3
  * contact : gildas.lormeau <at> gmail.com
4
- *
4
+ *
5
5
  * This file is part of SingleFile.
6
6
  *
7
- * The code in this file is free software: you can redistribute it and/or
8
- * modify it under the terms of the GNU Affero General Public License
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
9
  * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
10
  * of the License, or (at your option) any later version.
11
- *
12
- * The code in this file is distributed in the hope that it will be useful,
13
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
15
  * General Public License for more details.
16
16
  *
17
- * As additional permission under GNU AGPL version 3 section 7, you may
18
- * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
- * AGPL normally required by section 4, provided you include this license
20
- * notice and a URL through which recipients can access the Corresponding
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
21
  * Source.
22
22
  */
23
23
 
24
- /* global fetch, setTimeout, WebSocket */
24
+ /* global fetch */
25
25
 
26
- import { BROWSER_PATHS, BROWSER_ARGS } from "./constants.js";
27
26
  import { Deno, path } from "./deno-polyfill.js";
28
27
 
29
28
  const NULL_STD_CONFIG = "null";
30
29
  const DEBUG_PORT_MIN = 9222;
31
30
  const DEBUG_PORT_RANGE = 256;
32
- const READY_TIMEOUT = 30000;
33
- const READY_RETRY_DELAY = 250;
34
- const CLOSE_TIMEOUT = 15000;
35
- const LOCALHOST = "http://localhost:";
36
31
  const PROFILE_LOCK_ENTRY_PREFIX = "Singleton";
37
32
  const PROFILE_IGNORED_ENTRY_NAMES = [
38
33
  "AutofillAiModelCache",
@@ -53,13 +48,8 @@ const PROFILE_IGNORED_ENTRY_NAMES = [
53
48
  "optimization_guide_hint_cache_store",
54
49
  "optimization_guide_model_store"
55
50
  ];
56
- const PROFILE_INCOMPATIBLE_ARGS = [
57
- "--no-startup-window",
58
- "--bwsi",
59
- "--deny-permission-prompts"
60
- ];
61
51
 
62
- const { build, makeTempDir, readDir, copyFile, mkdir, Command, errors, remove, stat } = Deno;
52
+ const { build, readDir, copyFile, mkdir, Command, errors, remove, stat } = Deno;
63
53
  const WATCHDOG_SHELL = "/bin/sh";
64
54
  const WATCHDOG_SCRIPT = [
65
55
  "trap 'kill -TERM $browser 2>/dev/null' TERM INT",
@@ -77,116 +67,7 @@ const WATCHDOG_SCRIPT = [
77
67
  "wait $browser 2>/dev/null"
78
68
  ].join("\n");
79
69
  const { join } = path;
80
- let child, profilePath, childExited, keepProfilePath, browserDebugPort;
81
- export { launchBrowser, createBrowserProfile, getBrowserOptions, copyProfile, pruneProfile, closeBrowser, browserExited };
82
-
83
- function getBrowserOptions(options) {
84
- return {
85
- args: options.browserArgs,
86
- headless: options.browserHeadless,
87
- executablePath: options.browserExecutablePath,
88
- debug: options.browserDebug,
89
- singleProcess: options.browserSingleProcess,
90
- width: options.browserWidth,
91
- height: options.browserHeight,
92
- userAgent: options.userAgent,
93
- httpProxyServer: options.httpProxyServer,
94
- profile: options.browserProfile
95
- };
96
- }
97
-
98
- async function createBrowserProfile(options = {}) {
99
- await mkdir(options.profile, { recursive: true });
100
- await launchBrowser(Object.assign({}, options, { headless: false, persistProfile: true }));
101
- if (child !== undefined) {
102
- await child.status;
103
- }
104
- await closeBrowser();
105
- }
106
-
107
- async function launchBrowser(options = {}, indexPath = 0) {
108
- const executablePath = options.executablePath || BROWSER_PATHS[build.os][indexPath];
109
- let args = Array.from(BROWSER_ARGS);
110
- const debugPort = await getDebugPort();
111
- browserDebugPort = debugPort;
112
- args.push("--remote-debugging-port=" + debugPort);
113
- keepProfilePath = Boolean(options.persistProfile);
114
- if (keepProfilePath) {
115
- profilePath = options.profile;
116
- args = args.filter(arg => !PROFILE_INCOMPATIBLE_ARGS.includes(arg));
117
- } else {
118
- profilePath = await makeTempDir();
119
- if (options.profile) {
120
- await copyProfile(options.profile, profilePath);
121
- }
122
- }
123
- if (options.headless && !options.debug) {
124
- args.push("--headless");
125
- } else {
126
- args.push("--start-maximized");
127
- }
128
- if (options.debug) {
129
- args.push("--auto-open-devtools-for-tabs");
130
- }
131
- if (options.width && options.height) {
132
- args.push("--window-size=" + options.width + "," + options.height);
133
- }
134
- if (options.userAgent) {
135
- args.push("--user-agent=" + options.userAgent);
136
- }
137
- if (options.httpProxyServer) {
138
- args.push("--proxy-server=" + options.httpProxyServer);
139
- }
140
- args.push("--user-data-dir=" + profilePath);
141
- if (options.singleProcess) {
142
- args.push("--single-process");
143
- }
144
- if (options.args) {
145
- const argNames = options.args.map(arg => arg.split("=")[0]);
146
- args = args.filter(arg => !argNames.includes(arg.split("=")[0]));
147
- args.push(...options.args);
148
- }
149
- if (args.includes("--headless=new") ||
150
- args.includes("--auto-open-devtools-for-tabs") ||
151
- args.includes("--start-maximized") ||
152
- !args.includes("--headless")) {
153
- args.push("--disable-site-isolation-trials");
154
- }
155
- if (options.startUrl) {
156
- args.push(options.startUrl);
157
- }
158
- try {
159
- child = await spawnBrowser(executablePath, args, keepProfilePath ? "" : profilePath);
160
- } catch (error) {
161
- if (!keepProfilePath) {
162
- await remove(profilePath, { recursive: true }).catch(() => { });
163
- }
164
- profilePath = undefined;
165
- if (error instanceof errors.NotFound) {
166
- if (!options.executablePath && indexPath + 1 < BROWSER_PATHS[build.os].length) {
167
- return launchBrowser(options, indexPath + 1);
168
- }
169
- throw new Error(options.executablePath ?
170
- `The browser executable was not found at ${JSON.stringify(executablePath)}` :
171
- "The browser executable was not found, use --browser-executable-path to set its location");
172
- }
173
- throw error;
174
- }
175
- child.ref();
176
- childExited = false;
177
- child.status.then(() => childExited = true);
178
- try {
179
- await waitUntilReady(debugPort);
180
- } catch (error) {
181
- await closeBrowser();
182
- if (options.singleProcess) {
183
- console.warn("Warning: the browser exited when using --browser-single-process, retrying without it"); // eslint-disable-line no-console
184
- return launchBrowser(Object.assign({}, options, { singleProcess: false }), indexPath);
185
- }
186
- throw error;
187
- }
188
- return debugPort;
189
- }
70
+ export { spawnBrowser, getDebugPort, copyProfile, pruneProfile, removeProfileLockEntries };
190
71
 
191
72
  async function spawnBrowser(executablePath, args, temporaryProfilePath) {
192
73
  if (build.os == "windows") {
@@ -200,32 +81,6 @@ async function spawnBrowser(executablePath, args, temporaryProfilePath) {
200
81
  return new Command(WATCHDOG_SHELL, { args: ["-c", WATCHDOG_SCRIPT, executablePath, temporaryProfilePath, ...args], stdout: NULL_STD_CONFIG, stderr: NULL_STD_CONFIG }).spawn();
201
82
  }
202
83
 
203
- async function waitUntilReady(debugPort) {
204
- const timeoutTime = Date.now() + READY_TIMEOUT;
205
- while (!childExited && Date.now() < timeoutTime) {
206
- try {
207
- await fetch("http://localhost:" + debugPort + "/json/version");
208
- return;
209
- } catch {
210
- await new Promise(resolve => setTimeout(resolve, READY_RETRY_DELAY));
211
- }
212
- }
213
- throw new Error(childExited ? "The browser exited unexpectedly" : "The browser is not responding");
214
- }
215
-
216
- async function browserExited(maxDelay = 0) {
217
- if (child === undefined) {
218
- return false;
219
- }
220
- if (childExited || !maxDelay) {
221
- return childExited;
222
- }
223
- return Promise.race([
224
- child.status.then(() => true),
225
- new Promise(resolve => setTimeout(() => resolve(childExited), maxDelay))
226
- ]);
227
- }
228
-
229
84
  async function getDebugPort(port = getRandomDebugPort(), usedPorts = []) {
230
85
  try {
231
86
  await fetch("http://localhost:" + port + "/json/version");
@@ -243,60 +98,10 @@ async function getDebugPort(port = getRandomDebugPort(), usedPorts = []) {
243
98
  }
244
99
  }
245
100
 
246
-
247
101
  function getRandomDebugPort() {
248
102
  return Math.floor(Math.random() * DEBUG_PORT_RANGE) + DEBUG_PORT_MIN;
249
103
  }
250
104
 
251
- async function closeBrowser() {
252
- const closedChild = child;
253
- const closedProfilePath = profilePath;
254
- child = undefined;
255
- profilePath = undefined;
256
- if (closedChild !== undefined) {
257
- if (keepProfilePath && !childExited) {
258
- await closeBrowserGracefully(closedChild);
259
- }
260
- try {
261
- closedChild.kill();
262
- } catch {
263
- // ignored
264
- }
265
- await closedChild.status;
266
- }
267
- if (closedProfilePath !== undefined) {
268
- if (keepProfilePath) {
269
- await removeProfileLockEntries(closedProfilePath).catch(() => { });
270
- await pruneProfile(closedProfilePath).catch(() => { });
271
- } else {
272
- try {
273
- await remove(closedProfilePath, { recursive: true });
274
- } catch {
275
- console.log("Warning: failed to remove profile directory: " + closedProfilePath); // eslint-disable-line no-console
276
- }
277
- }
278
- }
279
- }
280
-
281
- async function closeBrowserGracefully(closedChild) {
282
- try {
283
- const response = await fetch(LOCALHOST + browserDebugPort + "/json/version");
284
- const { webSocketDebuggerUrl } = await response.json();
285
- const socket = new WebSocket(webSocketDebuggerUrl);
286
- await new Promise((resolve, reject) => {
287
- socket.addEventListener("open", resolve);
288
- socket.addEventListener("error", reject);
289
- });
290
- socket.send(JSON.stringify({ id: 0, method: "Browser.close" }));
291
- await Promise.race([
292
- closedChild.status,
293
- new Promise(resolve => setTimeout(resolve, CLOSE_TIMEOUT))
294
- ]);
295
- } catch {
296
- // ignored
297
- }
298
- }
299
-
300
105
  async function copyProfile(sourcePath, destinationPath) {
301
106
  let entries;
302
107
  try {
package/lib/cdp-client.js CHANGED
@@ -24,11 +24,11 @@
24
24
  /* global setTimeout, clearTimeout, URL, AbortController */
25
25
 
26
26
  import {
27
- launchBrowser,
28
- getBrowserOptions,
29
- closeBrowser,
30
- browserExited
31
- } from "./browser.js";
27
+ launchChromium,
28
+ getChromiumOptions,
29
+ closeChromium,
30
+ chromiumExited
31
+ } from "./chromium.js";
32
32
  import {
33
33
  CDP,
34
34
  options as cdpOptions
@@ -73,15 +73,15 @@ let browserOptions, relaunchBrowserPromise;
73
73
  export {
74
74
  initialize,
75
75
  getPageData,
76
- closeBrowser
76
+ closeChromium as closeBrowser
77
77
  };
78
78
 
79
79
  async function initialize(singleFileOptions) {
80
80
  if (singleFileOptions.browserServer) {
81
81
  cdpOptions.apiUrl = singleFileOptions.browserServer;
82
82
  } else {
83
- browserOptions = getBrowserOptions(singleFileOptions);
84
- cdpOptions.apiUrl = LOCALHOST + (await launchBrowser(browserOptions));
83
+ browserOptions = getChromiumOptions(singleFileOptions);
84
+ cdpOptions.apiUrl = LOCALHOST + (await launchChromium(browserOptions));
85
85
  }
86
86
  }
87
87
 
@@ -89,9 +89,9 @@ function relaunchBrowser() {
89
89
  if (!relaunchBrowserPromise) {
90
90
  console.warn("Warning: the browser exited when using --browser-single-process, retrying without it"); // eslint-disable-line no-console
91
91
  relaunchBrowserPromise = (async () => {
92
- await closeBrowser();
92
+ await closeChromium();
93
93
  browserOptions.singleProcess = false;
94
- cdpOptions.apiUrl = LOCALHOST + (await launchBrowser(browserOptions));
94
+ cdpOptions.apiUrl = LOCALHOST + (await launchChromium(browserOptions));
95
95
  })();
96
96
  }
97
97
  return relaunchBrowserPromise;
@@ -135,7 +135,7 @@ async function getPageData(options) {
135
135
  }
136
136
 
137
137
  async function shouldRelaunchBrowser() {
138
- return Boolean(browserOptions && browserOptions.singleProcess) && await browserExited(BROWSER_EXITED_MAX_DELAY);
138
+ return Boolean(browserOptions && browserOptions.singleProcess) && await chromiumExited(BROWSER_EXITED_MAX_DELAY);
139
139
  }
140
140
 
141
141
  async function relaunchBrowserAndRetry() {
@@ -0,0 +1,225 @@
1
+ /*
2
+ * Copyright 2010-2024 Gildas Lormeau
3
+ * contact : gildas.lormeau <at> gmail.com
4
+ *
5
+ * This file is part of SingleFile.
6
+ *
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
+ * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
+ * of the License, or (at your option) any later version.
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
+ * General Public License for more details.
16
+ *
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
+ * Source.
22
+ */
23
+
24
+ /* global fetch, setTimeout, WebSocket */
25
+
26
+ import { CHROMIUM_PATHS, CHROMIUM_ARGS } from "./constants.js";
27
+ import { spawnBrowser, getDebugPort, copyProfile, pruneProfile, removeProfileLockEntries } from "./browser.js";
28
+ import { Deno } from "./deno-polyfill.js";
29
+
30
+ const READY_TIMEOUT = 30000;
31
+ const READY_RETRY_DELAY = 250;
32
+ const CLOSE_TIMEOUT = 15000;
33
+ const LOCALHOST = "http://localhost:";
34
+ const PROFILE_INCOMPATIBLE_ARGS = [
35
+ "--no-startup-window",
36
+ "--bwsi",
37
+ "--deny-permission-prompts"
38
+ ];
39
+
40
+ const { build, makeTempDir, mkdir, errors, remove } = Deno;
41
+ let child, profilePath, childExited, keepProfilePath, browserDebugPort;
42
+ export { launchChromium, createBrowserProfile, getChromiumOptions, closeChromium, chromiumExited };
43
+
44
+ function getChromiumOptions(options) {
45
+ return {
46
+ args: options.browserArgs,
47
+ headless: options.browserHeadless,
48
+ executablePath: options.browserExecutablePath,
49
+ debug: options.browserDebug,
50
+ singleProcess: options.browserSingleProcess,
51
+ width: options.browserWidth,
52
+ height: options.browserHeight,
53
+ userAgent: options.userAgent,
54
+ httpProxyServer: options.httpProxyServer,
55
+ profile: options.browserProfile
56
+ };
57
+ }
58
+
59
+ async function createBrowserProfile(options = {}) {
60
+ await mkdir(options.profile, { recursive: true });
61
+ await launchChromium(Object.assign({}, options, { headless: false, persistProfile: true }));
62
+ if (child !== undefined) {
63
+ await child.status;
64
+ }
65
+ await closeChromium();
66
+ }
67
+
68
+ async function launchChromium(options = {}, indexPath = 0) {
69
+ const executablePath = options.executablePath || CHROMIUM_PATHS[build.os][indexPath];
70
+ let args = Array.from(CHROMIUM_ARGS);
71
+ const debugPort = await getDebugPort();
72
+ browserDebugPort = debugPort;
73
+ args.push("--remote-debugging-port=" + debugPort);
74
+ keepProfilePath = Boolean(options.persistProfile);
75
+ if (keepProfilePath) {
76
+ profilePath = options.profile;
77
+ args = args.filter(arg => !PROFILE_INCOMPATIBLE_ARGS.includes(arg));
78
+ } else {
79
+ profilePath = await makeTempDir();
80
+ if (options.profile) {
81
+ await copyProfile(options.profile, profilePath);
82
+ }
83
+ }
84
+ if (options.headless && !options.debug) {
85
+ args.push("--headless");
86
+ } else {
87
+ args.push("--start-maximized");
88
+ }
89
+ if (options.debug) {
90
+ args.push("--auto-open-devtools-for-tabs");
91
+ }
92
+ if (options.width && options.height) {
93
+ args.push("--window-size=" + options.width + "," + options.height);
94
+ }
95
+ if (options.userAgent) {
96
+ args.push("--user-agent=" + options.userAgent);
97
+ }
98
+ if (options.httpProxyServer) {
99
+ args.push("--proxy-server=" + options.httpProxyServer);
100
+ }
101
+ args.push("--user-data-dir=" + profilePath);
102
+ if (options.singleProcess) {
103
+ args.push("--single-process");
104
+ }
105
+ if (options.args) {
106
+ const argNames = options.args.map(arg => arg.split("=")[0]);
107
+ args = args.filter(arg => !argNames.includes(arg.split("=")[0]));
108
+ args.push(...options.args);
109
+ }
110
+ if (args.includes("--headless=new") ||
111
+ args.includes("--auto-open-devtools-for-tabs") ||
112
+ args.includes("--start-maximized") ||
113
+ !args.includes("--headless")) {
114
+ args.push("--disable-site-isolation-trials");
115
+ }
116
+ if (options.startUrl) {
117
+ args.push(options.startUrl);
118
+ }
119
+ try {
120
+ child = await spawnBrowser(executablePath, args, keepProfilePath ? "" : profilePath);
121
+ } catch (error) {
122
+ if (!keepProfilePath) {
123
+ await remove(profilePath, { recursive: true }).catch(() => { });
124
+ }
125
+ profilePath = undefined;
126
+ if (error instanceof errors.NotFound) {
127
+ if (!options.executablePath && indexPath + 1 < CHROMIUM_PATHS[build.os].length) {
128
+ return launchChromium(options, indexPath + 1);
129
+ }
130
+ throw new Error(options.executablePath ?
131
+ `The browser executable was not found at ${JSON.stringify(executablePath)}` :
132
+ "The browser executable was not found, use --browser-executable-path to set its location");
133
+ }
134
+ throw error;
135
+ }
136
+ child.ref();
137
+ childExited = false;
138
+ child.status.then(() => childExited = true);
139
+ try {
140
+ await waitUntilReady(debugPort);
141
+ } catch (error) {
142
+ await closeChromium();
143
+ if (options.singleProcess) {
144
+ console.warn("Warning: the browser exited when using --browser-single-process, retrying without it"); // eslint-disable-line no-console
145
+ return launchChromium(Object.assign({}, options, { singleProcess: false }), indexPath);
146
+ }
147
+ throw error;
148
+ }
149
+ return debugPort;
150
+ }
151
+
152
+ async function waitUntilReady(debugPort) {
153
+ const timeoutTime = Date.now() + READY_TIMEOUT;
154
+ while (!childExited && Date.now() < timeoutTime) {
155
+ try {
156
+ await fetch("http://localhost:" + debugPort + "/json/version");
157
+ return;
158
+ } catch {
159
+ await new Promise(resolve => setTimeout(resolve, READY_RETRY_DELAY));
160
+ }
161
+ }
162
+ throw new Error(childExited ? "The browser exited unexpectedly" : "The browser is not responding");
163
+ }
164
+
165
+ async function chromiumExited(maxDelay = 0) {
166
+ if (child === undefined) {
167
+ return false;
168
+ }
169
+ if (childExited || !maxDelay) {
170
+ return childExited;
171
+ }
172
+ return Promise.race([
173
+ child.status.then(() => true),
174
+ new Promise(resolve => setTimeout(() => resolve(childExited), maxDelay))
175
+ ]);
176
+ }
177
+
178
+ async function closeChromium() {
179
+ const closedChild = child;
180
+ const closedProfilePath = profilePath;
181
+ child = undefined;
182
+ profilePath = undefined;
183
+ if (closedChild !== undefined) {
184
+ if (keepProfilePath && !childExited) {
185
+ await closeBrowserGracefully(closedChild);
186
+ }
187
+ try {
188
+ closedChild.kill();
189
+ } catch {
190
+ // ignored
191
+ }
192
+ await closedChild.status;
193
+ }
194
+ if (closedProfilePath !== undefined) {
195
+ if (keepProfilePath) {
196
+ await removeProfileLockEntries(closedProfilePath).catch(() => { });
197
+ await pruneProfile(closedProfilePath).catch(() => { });
198
+ } else {
199
+ try {
200
+ await remove(closedProfilePath, { recursive: true });
201
+ } catch {
202
+ console.log("Warning: failed to remove profile directory: " + closedProfilePath); // eslint-disable-line no-console
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ async function closeBrowserGracefully(closedChild) {
209
+ try {
210
+ const response = await fetch(LOCALHOST + browserDebugPort + "/json/version");
211
+ const { webSocketDebuggerUrl } = await response.json();
212
+ const socket = new WebSocket(webSocketDebuggerUrl);
213
+ await new Promise((resolve, reject) => {
214
+ socket.addEventListener("open", resolve);
215
+ socket.addEventListener("error", reject);
216
+ });
217
+ socket.send(JSON.stringify({ id: 0, method: "Browser.close" }));
218
+ await Promise.race([
219
+ closedChild.status,
220
+ new Promise(resolve => setTimeout(resolve, CLOSE_TIMEOUT))
221
+ ]);
222
+ } catch {
223
+ // ignored
224
+ }
225
+ }