arn-browser 0.1.30 → 0.1.32
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/bin/cli.js +7 -3
- package/bin/install.js +65 -191
- package/package.json +2 -1
- package/src/utility/playwright/findBrowserPath.js +211 -0
- package/src/utility/playwright/pwLaunch.js +11 -71
- package/src/utility/proxy-utility/proxy-chain.js +96 -79
- package/src/utility/puppeteer/deleteDirectory.js +104 -0
- package/src/utility/puppeteer/findBrowserPath.js +211 -0
- package/src/utility/puppeteer/ppLaunch.js +20 -37
- package/src/utility/puppeteer/routes/ppRoute.js +3 -0
- /package/src/utility/{deleteDirectory.js → playwright/deleteDirectory.js} +0 -0
|
@@ -18,7 +18,8 @@ import { FingerprintGenerator } from "fingerprint-generator";
|
|
|
18
18
|
|
|
19
19
|
// Internal Utilities
|
|
20
20
|
import { getMultiloginToken } from "../mlx_token.js";
|
|
21
|
-
import { deleteDirectoryWithRetries } from "
|
|
21
|
+
import { deleteDirectoryWithRetries } from "./deleteDirectory.js";
|
|
22
|
+
import { findBrowserPath } from "./findBrowserPath.js";
|
|
22
23
|
|
|
23
24
|
// Human Cursor - for human-like mouse movements
|
|
24
25
|
import { createCursor } from "./human-cursor/index.js";
|
|
@@ -78,70 +79,8 @@ function resolveProfilePath(nameOrPath, browserName) {
|
|
|
78
79
|
return path.join(PERSISTENT_DIR, folderName);
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
* Uses ~/.arn-browser/browsers/{browser}/{executable}
|
|
84
|
-
*/
|
|
85
|
-
function getBinaryPath(browserName) {
|
|
86
|
-
const isWindows = process.platform === "win32";
|
|
87
|
-
const browsersDir = path.join(ARN_BROWSER_DIR, "browsers");
|
|
88
|
-
|
|
89
|
-
const binaryMap = {
|
|
90
|
-
brave: isWindows
|
|
91
|
-
? path.join(browsersDir, "brave", "brave.exe")
|
|
92
|
-
: path.join(browsersDir, "brave", "brave"),
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const binaryPath = binaryMap[browserName];
|
|
96
|
-
if (!binaryPath) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`░░░░░ [LaunchBrowser] Unknown browser for getBinaryPath: ${browserName}`
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (!fs.existsSync(binaryPath)) {
|
|
103
|
-
throw new Error(
|
|
104
|
-
`░░░░░ [LaunchBrowser] ${browserName} binary not found at: ${binaryPath}\n` +
|
|
105
|
-
` Run "node bin/cli.js install" to install browsers.`
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return binaryPath;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Gets the executable path for Playwright-managed browsers (Chromium, Firefox).
|
|
114
|
-
* Uses ~/.arn-browser/browsers/{browser}/{executable}
|
|
115
|
-
*/
|
|
116
|
-
function getPlaywrightExePath(browserName) {
|
|
117
|
-
const isWindows = process.platform === "win32";
|
|
118
|
-
const browsersDir = path.join(ARN_BROWSER_DIR, "browsers");
|
|
119
|
-
|
|
120
|
-
const binaryMap = {
|
|
121
|
-
chromium: isWindows
|
|
122
|
-
? path.join(browsersDir, "chromium", "chrome.exe")
|
|
123
|
-
: path.join(browsersDir, "chromium", "chrome"),
|
|
124
|
-
firefox: isWindows
|
|
125
|
-
? path.join(browsersDir, "firefox", "firefox.exe")
|
|
126
|
-
: path.join(browsersDir, "firefox", "firefox"),
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const binaryPath = binaryMap[browserName];
|
|
130
|
-
if (!binaryPath) {
|
|
131
|
-
throw new Error(
|
|
132
|
-
`░░░░░ [LaunchBrowser] Unknown browser: ${browserName}`
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (!fs.existsSync(binaryPath)) {
|
|
137
|
-
throw new Error(
|
|
138
|
-
`░░░░░ [LaunchBrowser] ${browserName} binary not found at: ${binaryPath}\n` +
|
|
139
|
-
` Run "node bin/cli.js install" to install browsers.`
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return binaryPath;
|
|
144
|
-
}
|
|
82
|
+
// Browser path resolution now uses OS-installed browsers via findBrowserPath.
|
|
83
|
+
// See src/utility/findBrowserPath.js for detection logic.
|
|
145
84
|
|
|
146
85
|
const PROFILE_META_FILE = "_profile_meta.json";
|
|
147
86
|
|
|
@@ -385,6 +324,7 @@ export async function pwLaunch({
|
|
|
385
324
|
humanize_options: effectiveHumanizeOptions,
|
|
386
325
|
spoof_fingerprint,
|
|
387
326
|
cleanupMinutes: effectiveCleanupMinutes,
|
|
327
|
+
browserType: which_browser, // "chrome" or "chromium"
|
|
388
328
|
});
|
|
389
329
|
break;
|
|
390
330
|
case "firefox":
|
|
@@ -442,7 +382,7 @@ export async function pwLaunch({
|
|
|
442
382
|
// ==========================================================================
|
|
443
383
|
// 4. ENGINE: CHROMIUM
|
|
444
384
|
// ==========================================================================
|
|
445
|
-
async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, humanize_options, spoof_fingerprint, cleanupMinutes }) {
|
|
385
|
+
async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, humanize_options, spoof_fingerprint, cleanupMinutes, browserType = "chromium" }) {
|
|
446
386
|
const isPersistent = !!profilePath;
|
|
447
387
|
|
|
448
388
|
// 1. Determine Path (Temp needs it for fingerprint storage, Persistent needs it for data)
|
|
@@ -504,7 +444,7 @@ async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, hum
|
|
|
504
444
|
// Launch standard browser (not persistent context)
|
|
505
445
|
const browser = await chromium.launch({
|
|
506
446
|
headless: false,
|
|
507
|
-
executablePath:
|
|
447
|
+
executablePath: findBrowserPath(browserType),
|
|
508
448
|
proxy: proxyObj,
|
|
509
449
|
args: args,
|
|
510
450
|
ignoreDefaultArgs: ignoreDefaultArgs,
|
|
@@ -571,7 +511,7 @@ async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, hum
|
|
|
571
511
|
// Logic: Native Persistent Launch. No fingerprint-injector.
|
|
572
512
|
const context = await chromium.launchPersistentContext(activePath, {
|
|
573
513
|
headless: false,
|
|
574
|
-
executablePath:
|
|
514
|
+
executablePath: findBrowserPath(browserType),
|
|
575
515
|
proxy: proxyObj,
|
|
576
516
|
args: args,
|
|
577
517
|
timezoneId: tz,
|
|
@@ -625,7 +565,7 @@ async function firefoxLauncher({ profilePath, proxy, timezoneId, humanize_option
|
|
|
625
565
|
|
|
626
566
|
const browser = await firefox.launch({
|
|
627
567
|
headless: false,
|
|
628
|
-
executablePath:
|
|
568
|
+
executablePath: findBrowserPath("firefox"),
|
|
629
569
|
proxy: proxyObj,
|
|
630
570
|
ignoreDefaultArgs: ["--enable-automation"],
|
|
631
571
|
firefoxUserPrefs: firefoxUserPrefs,
|
|
@@ -667,7 +607,7 @@ async function firefoxLauncher({ profilePath, proxy, timezoneId, humanize_option
|
|
|
667
607
|
|
|
668
608
|
const context = await firefox.launchPersistentContext(activePath, {
|
|
669
609
|
headless: false,
|
|
670
|
-
executablePath:
|
|
610
|
+
executablePath: findBrowserPath("firefox"),
|
|
671
611
|
proxy: proxyObj,
|
|
672
612
|
timezoneId: tz,
|
|
673
613
|
ignoreDefaultArgs: ["--enable-automation"],
|
|
@@ -744,7 +684,7 @@ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humani
|
|
|
744
684
|
}
|
|
745
685
|
}
|
|
746
686
|
|
|
747
|
-
const braveBin =
|
|
687
|
+
const braveBin = findBrowserPath("brave");
|
|
748
688
|
|
|
749
689
|
// ======================================================
|
|
750
690
|
// Disable Brave Sidebar via Preferences
|
|
@@ -208,14 +208,7 @@ export async function startProxyServer({
|
|
|
208
208
|
proxy2: createHostMatcher([...PROXY_2_HOSTS, "proxy.multilogin.com", "multilogin.com"]),
|
|
209
209
|
};
|
|
210
210
|
|
|
211
|
-
// 2.
|
|
212
|
-
const selectedPort = await findAvailablePort(50001, 50010);
|
|
213
|
-
if (!selectedPort) {
|
|
214
|
-
console.error("░░ Critical Error: No available ports.");
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// 3. Build URLs
|
|
211
|
+
// 2. Build URLs
|
|
219
212
|
const buildURL = (data) => {
|
|
220
213
|
if (!data) return null; // Returns null if no config
|
|
221
214
|
const { type = "http", host, port, user, pass } = data;
|
|
@@ -230,7 +223,7 @@ export async function startProxyServer({
|
|
|
230
223
|
p2: buildURL(PROXY_2_DATA),
|
|
231
224
|
};
|
|
232
225
|
|
|
233
|
-
//
|
|
226
|
+
// 3. Fetch Details (Simplified Logic)
|
|
234
227
|
// We pass the URL (or null). The function handles the "Local" logic.
|
|
235
228
|
const [defaultDetails, p1Details, p2Details] = await Promise.all([
|
|
236
229
|
fetchProxyDetails(upstreamProxies.default, ip2LocationKey, retryDelayMs),
|
|
@@ -243,7 +236,7 @@ export async function startProxyServer({
|
|
|
243
236
|
if (upstreamProxies.p1 && !p1Details) { console.warn("░░ Warning: PROXY_1 configured but unreachable."); return null; }
|
|
244
237
|
if (upstreamProxies.p2 && !p2Details) { console.warn("░░ Warning: PROXY_2 configured but unreachable."); return null; }
|
|
245
238
|
|
|
246
|
-
//
|
|
239
|
+
// 4. Stats
|
|
247
240
|
const stats = {
|
|
248
241
|
DEFAULT_PROXY: { request: 0, srcTx: 0, srcRx: 0, trgTx: 0, trgRx: 0 },
|
|
249
242
|
NO_PROXY: { request: 0, srcTx: 0, srcRx: 0, trgTx: 0, trgRx: 0 },
|
|
@@ -261,72 +254,105 @@ export async function startProxyServer({
|
|
|
261
254
|
const connectionMap = {}; // Maps connectionId -> { type: "..." }
|
|
262
255
|
let serverRunning = false;
|
|
263
256
|
|
|
264
|
-
//
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
verbose: debug,
|
|
269
|
-
prepareRequestFunction: ({ hostname, connectionId }) => {
|
|
270
|
-
let proxyType = "DEFAULT_PROXY";
|
|
271
|
-
let upstreamUrl = upstreamProxies.default;
|
|
272
|
-
let isCustomResponse = false;
|
|
273
|
-
let customResponseData = null;
|
|
274
|
-
|
|
275
|
-
// Logic to determine Proxy Type
|
|
276
|
-
if (matchers.noProxy(hostname)) {
|
|
277
|
-
// A. Direct
|
|
278
|
-
proxyType = "NO_PROXY";
|
|
279
|
-
upstreamUrl = null;
|
|
280
|
-
} else if (matchers.proxy1(hostname) && upstreamProxies.p1) {
|
|
281
|
-
// C1. Proxy 1
|
|
282
|
-
proxyType = "PROXY_1";
|
|
283
|
-
upstreamUrl = upstreamProxies.p1;
|
|
284
|
-
} else if (matchers.proxy2(hostname) && upstreamProxies.p2) {
|
|
285
|
-
// C2. Proxy 2
|
|
286
|
-
proxyType = "PROXY_2";
|
|
287
|
-
upstreamUrl = upstreamProxies.p2;
|
|
288
|
-
}
|
|
257
|
+
// 5. Create server and bind with retry (handles race conditions between concurrent processes)
|
|
258
|
+
const PORT_RANGE_START = 50001;
|
|
259
|
+
const PORT_RANGE_END = 50200;
|
|
260
|
+
const MAX_BIND_RETRIES = 10;
|
|
289
261
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
else displayedIP = defaultDetails?.ip;
|
|
300
|
-
|
|
301
|
-
customResponseData = {
|
|
302
|
-
statusCode: 200,
|
|
303
|
-
headers: { "Content-Type": "text/plain", Connection: "close" },
|
|
304
|
-
body: displayedIP || "Unknown IP",
|
|
305
|
-
};
|
|
306
|
-
}
|
|
262
|
+
let selectedPort = null;
|
|
263
|
+
let server = null;
|
|
264
|
+
|
|
265
|
+
for (let attempt = 1; attempt <= MAX_BIND_RETRIES; attempt++) {
|
|
266
|
+
const candidatePort = await findAvailablePort(PORT_RANGE_START, PORT_RANGE_END);
|
|
267
|
+
if (!candidatePort) {
|
|
268
|
+
console.error("░░ Critical Error: No available ports in range " + PORT_RANGE_START + "-" + PORT_RANGE_END);
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
307
271
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
272
|
+
server = new ProxyChain.Server({
|
|
273
|
+
port: candidatePort,
|
|
274
|
+
host: "127.0.0.1",
|
|
275
|
+
verbose: debug,
|
|
276
|
+
prepareRequestFunction: ({ hostname, connectionId }) => {
|
|
277
|
+
let proxyType = "DEFAULT_PROXY";
|
|
278
|
+
let upstreamUrl = upstreamProxies.default;
|
|
279
|
+
let isCustomResponse = false;
|
|
280
|
+
let customResponseData = null;
|
|
281
|
+
|
|
282
|
+
// Logic to determine Proxy Type
|
|
283
|
+
if (matchers.noProxy(hostname)) {
|
|
284
|
+
// A. Direct
|
|
285
|
+
proxyType = "NO_PROXY";
|
|
286
|
+
upstreamUrl = null;
|
|
287
|
+
} else if (matchers.proxy1(hostname) && upstreamProxies.p1) {
|
|
288
|
+
// C1. Proxy 1
|
|
289
|
+
proxyType = "PROXY_1";
|
|
290
|
+
upstreamUrl = upstreamProxies.p1;
|
|
291
|
+
} else if (matchers.proxy2(hostname) && upstreamProxies.p2) {
|
|
292
|
+
// C2. Proxy 2
|
|
293
|
+
proxyType = "PROXY_2";
|
|
294
|
+
upstreamUrl = upstreamProxies.p2;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// B. IP Check Interception (Overrules standard routing for specific domain)
|
|
298
|
+
if (hostname === "ip.bablosoft.com") {
|
|
299
|
+
isCustomResponse = true;
|
|
300
|
+
let displayedIP;
|
|
301
|
+
if (proxyType === "PROXY_1") displayedIP = p1Details?.ip;
|
|
302
|
+
else if (proxyType === "PROXY_2") displayedIP = p2Details?.ip;
|
|
303
|
+
else if (proxyType === "NO_PROXY") displayedIP = "127.0.0.1";
|
|
304
|
+
else displayedIP = defaultDetails?.ip;
|
|
305
|
+
|
|
306
|
+
customResponseData = {
|
|
307
|
+
statusCode: 200,
|
|
308
|
+
headers: { "Content-Type": "text/plain", Connection: "close" },
|
|
309
|
+
body: displayedIP || "Unknown IP",
|
|
310
|
+
};
|
|
315
311
|
}
|
|
316
|
-
hostStatsMap[proxyType][hostname].req++;
|
|
317
|
-
}
|
|
318
312
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
313
|
+
// Record Stats
|
|
314
|
+
connectionMap[connectionId] = { type: proxyType, hostname: hostname };
|
|
315
|
+
if (host_stats && hostname) {
|
|
316
|
+
if (!hostStatsMap[proxyType]) hostStatsMap[proxyType] = {};
|
|
317
|
+
if (!hostStatsMap[proxyType][hostname]) {
|
|
318
|
+
hostStatsMap[proxyType][hostname] = { req: 0, srcTx: 0, srcRx: 0, trgTx: 0, trgRx: 0 };
|
|
319
|
+
}
|
|
320
|
+
hostStatsMap[proxyType][hostname].req++;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Return Decision
|
|
324
|
+
if (isCustomResponse) {
|
|
325
|
+
return { customResponseFunction: () => customResponseData };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
upstreamProxyUrl: upstreamUrl,
|
|
330
|
+
requestAuthentication: false,
|
|
331
|
+
};
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
await server.listen();
|
|
337
|
+
selectedPort = candidatePort;
|
|
338
|
+
serverRunning = true;
|
|
339
|
+
console.log(`░░ Local Proxy Started: http://127.0.0.1:${selectedPort}`);
|
|
340
|
+
break; // Successfully bound — exit retry loop
|
|
341
|
+
} catch (err) {
|
|
342
|
+
if (err.code === "EADDRINUSE" && attempt < MAX_BIND_RETRIES) {
|
|
343
|
+
console.warn(`░░ Port ${candidatePort} taken (race), retrying... (${attempt}/${MAX_BIND_RETRIES})`);
|
|
344
|
+
await sleep(50 + Math.random() * 100); // Small random delay to de-sync concurrent processes
|
|
345
|
+
continue;
|
|
322
346
|
}
|
|
347
|
+
console.error("░░ Failed to start proxy server:", err);
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
323
351
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
},
|
|
329
|
-
});
|
|
352
|
+
if (!selectedPort || !server) {
|
|
353
|
+
console.error("░░ Critical Error: Could not bind to any port after " + MAX_BIND_RETRIES + " attempts.");
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
330
356
|
|
|
331
357
|
server.on("connectionClosed", ({ connectionId, stats: connStats }) => {
|
|
332
358
|
const connectionInfo = connectionMap[connectionId];
|
|
@@ -355,15 +381,6 @@ export async function startProxyServer({
|
|
|
355
381
|
delete connectionMap[connectionId];
|
|
356
382
|
});
|
|
357
383
|
|
|
358
|
-
try {
|
|
359
|
-
await server.listen();
|
|
360
|
-
serverRunning = true;
|
|
361
|
-
console.log(`░░ Local Proxy Started: http://127.0.0.1:${selectedPort}`);
|
|
362
|
-
} catch (err) {
|
|
363
|
-
console.error("░░ Failed to start proxy server:", err);
|
|
364
|
-
return null;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
384
|
const formatBytes = (bytes) => {
|
|
368
385
|
if (bytes < 1024) return bytes + " B";
|
|
369
386
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Deletes a directory with built-in retry logic.
|
|
7
|
+
* Useful for browser profiles where files might still be locked by the OS process.
|
|
8
|
+
* * @param {string} targetPath - The absolute path to delete.
|
|
9
|
+
* @param {number} maxRetries - How many times to retry (default: 5).
|
|
10
|
+
* @param {number} retryDelayMs - Time to wait between retries (default: 2000ms).
|
|
11
|
+
*/
|
|
12
|
+
export async function deleteDirectoryWithRetries(targetPath, maxRetries = 5, retryDelayMs = 2000) {
|
|
13
|
+
if (!fs.existsSync(targetPath)) {
|
|
14
|
+
return true; // Already gone
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let attempt = 0;
|
|
18
|
+
|
|
19
|
+
while (attempt < maxRetries) {
|
|
20
|
+
try {
|
|
21
|
+
// Force: true allows deleting files even if read-only
|
|
22
|
+
// Recursive: true deletes inner folders
|
|
23
|
+
await fs.promises.rm(targetPath, { recursive: true, force: true });
|
|
24
|
+
|
|
25
|
+
// Double check if it's actually gone
|
|
26
|
+
if (!fs.existsSync(targetPath)) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const isLastAttempt = attempt === maxRetries - 1;
|
|
31
|
+
|
|
32
|
+
// If it's the last attempt, log error
|
|
33
|
+
if (isLastAttempt) {
|
|
34
|
+
console.error(`░░░░░ Failed to delete directory after ${maxRetries} attempts: ${targetPath}`);
|
|
35
|
+
console.error(` Error: ${error.message}`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Log warning and wait
|
|
40
|
+
console.warn(
|
|
41
|
+
`░░░░░ Delete failed (Attempt ${attempt + 1}/${maxRetries}). File might be locked. Retrying in ${retryDelayMs / 1000
|
|
42
|
+
}s...`
|
|
43
|
+
);
|
|
44
|
+
await sleep(retryDelayMs);
|
|
45
|
+
}
|
|
46
|
+
attempt++;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Deletes specific contents inside a directory but keeps the parent folder.
|
|
54
|
+
* (Optional utility if you need to clear cache without removing the profile folder itself)
|
|
55
|
+
*/
|
|
56
|
+
export async function deleteAllItemsInDirectory(directoryPath) {
|
|
57
|
+
try {
|
|
58
|
+
if (!fs.existsSync(directoryPath)) return;
|
|
59
|
+
|
|
60
|
+
const files = await fs.promises.readdir(directoryPath);
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const curPath = path.join(directoryPath, file);
|
|
63
|
+
await deleteDirectoryWithRetries(curPath);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(`Error emptying directory ${directoryPath}:`, error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Legacy utility: Scan a directory and delete folders older than X minutes.
|
|
72
|
+
* (Note: launchBrowser.js now has its own internal version of this, but keeping this
|
|
73
|
+
* here is useful if you want to run cleanup scripts independently).
|
|
74
|
+
*/
|
|
75
|
+
export function deleteOldDirectories({ directoryPath, ageLimitMinutes }) {
|
|
76
|
+
if (!fs.existsSync(directoryPath)) return;
|
|
77
|
+
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
const limit = ageLimitMinutes * 60 * 1000;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const files = fs.readdirSync(directoryPath);
|
|
83
|
+
|
|
84
|
+
files.forEach((file) => {
|
|
85
|
+
const curPath = path.join(directoryPath, file);
|
|
86
|
+
try {
|
|
87
|
+
const stats = fs.statSync(curPath);
|
|
88
|
+
if (stats.isDirectory()) {
|
|
89
|
+
const age = now - stats.mtimeMs;
|
|
90
|
+
if (age > limit) {
|
|
91
|
+
console.log(`Cleaning up old directory: ${file}`);
|
|
92
|
+
// We use the sync version of retry logic or just fire-and-forget async here
|
|
93
|
+
// For simplicity in a sync loop, we often just do a force removal:
|
|
94
|
+
fs.rmSync(curPath, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {
|
|
98
|
+
// Ignore access errors on specific files
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error("Error during old directory cleanup:", err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file findBrowserPath.js
|
|
3
|
+
* @description Cross-platform utility to find OS-installed browser executables.
|
|
4
|
+
* Supports: chrome, chromium, firefox, brave
|
|
5
|
+
* Works on: Linux (x64/arm64), macOS (arm64), Windows (x64)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
|
|
12
|
+
// ==========================================================================
|
|
13
|
+
// KNOWN BINARY PATHS PER PLATFORM
|
|
14
|
+
// ==========================================================================
|
|
15
|
+
|
|
16
|
+
const BROWSER_PATHS = {
|
|
17
|
+
chrome: {
|
|
18
|
+
linux: [
|
|
19
|
+
"google-chrome-stable",
|
|
20
|
+
"google-chrome",
|
|
21
|
+
"/opt/google/chrome/google-chrome",
|
|
22
|
+
"/usr/bin/google-chrome-stable",
|
|
23
|
+
"/usr/bin/google-chrome",
|
|
24
|
+
],
|
|
25
|
+
darwin: [
|
|
26
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
27
|
+
],
|
|
28
|
+
win32: [
|
|
29
|
+
path.join(process.env.PROGRAMFILES || "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe"),
|
|
30
|
+
path.join(process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)", "Google", "Chrome", "Application", "chrome.exe"),
|
|
31
|
+
path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
chromium: {
|
|
35
|
+
linux: [
|
|
36
|
+
"chromium-browser",
|
|
37
|
+
"chromium",
|
|
38
|
+
"/usr/bin/chromium-browser",
|
|
39
|
+
"/usr/bin/chromium",
|
|
40
|
+
"/snap/bin/chromium",
|
|
41
|
+
],
|
|
42
|
+
darwin: [
|
|
43
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
44
|
+
],
|
|
45
|
+
win32: [
|
|
46
|
+
path.join(process.env.PROGRAMFILES || "C:\\Program Files", "Chromium", "Application", "chrome.exe"),
|
|
47
|
+
path.join(process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)", "Chromium", "Application", "chrome.exe"),
|
|
48
|
+
path.join(process.env.LOCALAPPDATA || "", "Chromium", "Application", "chrome.exe"),
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
firefox: {
|
|
52
|
+
linux: [
|
|
53
|
+
"firefox",
|
|
54
|
+
"/usr/bin/firefox",
|
|
55
|
+
"/snap/bin/firefox",
|
|
56
|
+
"/usr/lib/firefox/firefox",
|
|
57
|
+
],
|
|
58
|
+
darwin: [
|
|
59
|
+
"/Applications/Firefox.app/Contents/MacOS/firefox",
|
|
60
|
+
],
|
|
61
|
+
win32: [
|
|
62
|
+
path.join(process.env.PROGRAMFILES || "C:\\Program Files", "Mozilla Firefox", "firefox.exe"),
|
|
63
|
+
path.join(process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)", "Mozilla Firefox", "firefox.exe"),
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
brave: {
|
|
67
|
+
linux: [
|
|
68
|
+
"brave-browser",
|
|
69
|
+
"brave-browser-stable",
|
|
70
|
+
"/opt/brave.com/brave/brave-browser",
|
|
71
|
+
"/usr/bin/brave-browser",
|
|
72
|
+
"/usr/bin/brave-browser-stable",
|
|
73
|
+
"/snap/bin/brave",
|
|
74
|
+
],
|
|
75
|
+
darwin: [
|
|
76
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
77
|
+
],
|
|
78
|
+
win32: [
|
|
79
|
+
path.join(process.env.PROGRAMFILES || "C:\\Program Files", "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
|
|
80
|
+
path.join(process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)", "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
|
|
81
|
+
path.join(process.env.LOCALAPPDATA || "", "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Friendly display names for error messages
|
|
87
|
+
const BROWSER_DISPLAY_NAMES = {
|
|
88
|
+
chrome: "Google Chrome",
|
|
89
|
+
chromium: "Chromium",
|
|
90
|
+
firefox: "Mozilla Firefox",
|
|
91
|
+
brave: "Brave Browser",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Install hints per platform
|
|
95
|
+
const INSTALL_HINTS = {
|
|
96
|
+
chrome: {
|
|
97
|
+
linux: "sudo apt install google-chrome-stable OR download from https://www.google.com/chrome/",
|
|
98
|
+
darwin: "Download from https://www.google.com/chrome/",
|
|
99
|
+
win32: "Download from https://www.google.com/chrome/",
|
|
100
|
+
},
|
|
101
|
+
chromium: {
|
|
102
|
+
linux: "sudo apt install chromium-browser OR sudo snap install chromium",
|
|
103
|
+
darwin: "brew install --cask chromium OR download from https://www.chromium.org/",
|
|
104
|
+
win32: "Download from https://www.chromium.org/getting-involved/download-chromium/",
|
|
105
|
+
},
|
|
106
|
+
firefox: {
|
|
107
|
+
linux: "sudo apt install firefox OR sudo snap install firefox",
|
|
108
|
+
darwin: "brew install --cask firefox OR download from https://www.mozilla.org/firefox/",
|
|
109
|
+
win32: "Download from https://www.mozilla.org/firefox/",
|
|
110
|
+
},
|
|
111
|
+
brave: {
|
|
112
|
+
linux: "See https://brave.com/linux/ OR sudo apt install brave-browser",
|
|
113
|
+
darwin: "brew install --cask brave-browser OR download from https://brave.com/",
|
|
114
|
+
win32: "Download from https://brave.com/",
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// ==========================================================================
|
|
119
|
+
// DETECTION LOGIC
|
|
120
|
+
// ==========================================================================
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Try to resolve a command name using `which` (Unix) or `where` (Windows).
|
|
124
|
+
* Returns the absolute path if found, null otherwise.
|
|
125
|
+
*/
|
|
126
|
+
function resolveFromPath(command) {
|
|
127
|
+
const isWindows = process.platform === "win32";
|
|
128
|
+
try {
|
|
129
|
+
const cmd = isWindows ? `where "${command}"` : `which "${command}"`;
|
|
130
|
+
const result = execSync(cmd, { stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8" }).trim();
|
|
131
|
+
// `where` on Windows may return multiple lines — take the first
|
|
132
|
+
const firstLine = result.split("\n")[0].trim();
|
|
133
|
+
if (firstLine && fs.existsSync(firstLine)) {
|
|
134
|
+
return firstLine;
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// Command not found in PATH
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Find the OS-installed executable path for a given browser.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} browserName - One of: "chrome", "chromium", "firefox", "brave"
|
|
146
|
+
* @returns {string} Absolute path to the browser executable
|
|
147
|
+
* @throws {Error} If the browser is not found on the system
|
|
148
|
+
*/
|
|
149
|
+
export function findBrowserPath(browserName) {
|
|
150
|
+
const name = browserName.toLowerCase();
|
|
151
|
+
const candidates = BROWSER_PATHS[name];
|
|
152
|
+
|
|
153
|
+
if (!candidates) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`❌ [findBrowserPath] Unknown browser: "${browserName}"\n` +
|
|
156
|
+
` Supported: chrome, chromium, firefox, brave`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const platform = process.platform; // 'linux', 'darwin', 'win32'
|
|
161
|
+
const paths = candidates[platform];
|
|
162
|
+
|
|
163
|
+
if (!paths || paths.length === 0) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`❌ [findBrowserPath] ${BROWSER_DISPLAY_NAMES[name]} is not supported on platform: ${platform}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Strategy 1: Try PATH lookup first (works for short names like "google-chrome", "firefox")
|
|
170
|
+
for (const candidate of paths) {
|
|
171
|
+
// If it's not an absolute path, try resolving from PATH
|
|
172
|
+
if (!path.isAbsolute(candidate)) {
|
|
173
|
+
const resolved = resolveFromPath(candidate);
|
|
174
|
+
if (resolved) return resolved;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Strategy 2: Check known absolute paths
|
|
179
|
+
for (const candidate of paths) {
|
|
180
|
+
if (path.isAbsolute(candidate) && fs.existsSync(candidate)) {
|
|
181
|
+
return candidate;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Not found — throw descriptive error
|
|
186
|
+
const displayName = BROWSER_DISPLAY_NAMES[name];
|
|
187
|
+
const hint = INSTALL_HINTS[name]?.[platform] || `Install ${displayName} from its official website.`;
|
|
188
|
+
|
|
189
|
+
throw new Error(
|
|
190
|
+
`❌ [findBrowserPath] ${displayName} not found on this system.\n` +
|
|
191
|
+
` Platform: ${platform}\n` +
|
|
192
|
+
` Searched: ${paths.filter(p => path.isAbsolute(p)).join(", ") || "(PATH lookup only)"}\n\n` +
|
|
193
|
+
` To install:\n` +
|
|
194
|
+
` ${hint}`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check if a browser is available without throwing.
|
|
200
|
+
*
|
|
201
|
+
* @param {string} browserName - One of: "chrome", "chromium", "firefox", "brave"
|
|
202
|
+
* @returns {{ found: boolean, path: string|null, error: string|null }}
|
|
203
|
+
*/
|
|
204
|
+
export function checkBrowserAvailability(browserName) {
|
|
205
|
+
try {
|
|
206
|
+
const p = findBrowserPath(browserName);
|
|
207
|
+
return { found: true, path: p, error: null };
|
|
208
|
+
} catch (err) {
|
|
209
|
+
return { found: false, path: null, error: err.message };
|
|
210
|
+
}
|
|
211
|
+
}
|