arn-browser 0.1.12 → 0.1.14
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/package.json +2 -2
- package/src/index.d.ts +12 -4
- package/src/index.js +12 -4
- package/src/utility/playwright/human-cursor/HumanCursor.js +28 -21
- package/src/utility/playwright/human-cursor/index.d.ts +1 -1
- package/src/utility/playwright/{playwright-helper.d.ts → pwHelper.d.ts} +1 -1
- package/src/utility/playwright/{playwright-helper.js → pwHelper.js} +85 -5
- package/src/utility/playwright/pwLaunch.d.ts +1 -1
- package/src/utility/playwright/pwLaunch.js +67 -2
- package/src/utility/playwright/routes/pwRoute.d.ts +1 -1
- package/src/utility/puppeteer/ppHelper.d.ts +70 -0
- package/src/utility/puppeteer/ppHelper.js +227 -0
- package/src/utility/puppeteer/ppLaunch.js +73 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arn-browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "A lightweight, browser autmation helper.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"https-proxy-agent": "^7.0.6",
|
|
23
23
|
"node-cache": "^5.1.2",
|
|
24
24
|
"node-fetch": "^3.3.2",
|
|
25
|
-
"playwright": "
|
|
25
|
+
"playwright-core": "1.42.1",
|
|
26
26
|
"proxy-chain": "^2.6.0",
|
|
27
27
|
"puppeteer-core": "^24.38.0",
|
|
28
28
|
"randomstring": "^1.3.1",
|
package/src/index.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { ppLaunch } from "./utility/puppeteer/ppLaunch";
|
|
|
4
4
|
import { ppRoute, ppCacheLogs } from "./utility/puppeteer/routes/ppRoute";
|
|
5
5
|
import { pwLaunch } from "./utility/playwright/pwLaunch";
|
|
6
6
|
import { pwRoute, pwCacheLogs } from "./utility/playwright/routes/pwRoute";
|
|
7
|
-
import { retryNavigation, retryClick, checkPageConditions } from "./utility/playwright/
|
|
7
|
+
import { retryNavigation, retryClick, checkPageConditions, exportSession, importSession } from "./utility/playwright/pwHelper";
|
|
8
|
+
import { retryNavigation as ppRetryNavigation, retryClick as ppRetryClick, checkPageConditions as ppCheckPageConditions, exportSession as ppExportSession, importSession as ppImportSession } from "./utility/puppeteer/ppHelper";
|
|
8
9
|
import { startProxyServer, fetchPublicIP, fetchProxyDetails } from "./utility/proxy-utility/proxy-chain";
|
|
9
10
|
import {
|
|
10
11
|
fetchAwsProxy, getInstanceStatus, getPublicIpAddress,
|
|
@@ -20,15 +21,22 @@ export declare const ppBrowser: {
|
|
|
20
21
|
launch: typeof ppLaunch;
|
|
21
22
|
route: typeof ppRoute;
|
|
22
23
|
cacheLogs: typeof ppCacheLogs;
|
|
24
|
+
Goto: typeof ppRetryNavigation;
|
|
25
|
+
Click: typeof ppRetryClick;
|
|
26
|
+
Conditions: typeof ppCheckPageConditions;
|
|
27
|
+
exportSession: typeof ppExportSession;
|
|
28
|
+
importSession: typeof ppImportSession;
|
|
23
29
|
};
|
|
24
30
|
|
|
25
31
|
export declare const pwBrowser: {
|
|
26
32
|
launch: typeof pwLaunch;
|
|
27
33
|
route: typeof pwRoute;
|
|
28
34
|
cacheLogs: typeof pwCacheLogs;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
Goto: typeof retryNavigation;
|
|
36
|
+
Click: typeof retryClick;
|
|
37
|
+
Conditions: typeof checkPageConditions;
|
|
38
|
+
exportSession: typeof exportSession;
|
|
39
|
+
importSession: typeof importSession;
|
|
32
40
|
};
|
|
33
41
|
|
|
34
42
|
export declare const proxyUtil: {
|
package/src/index.js
CHANGED
|
@@ -5,25 +5,33 @@
|
|
|
5
5
|
// --- Puppeteer ---
|
|
6
6
|
import { ppLaunch } from "./utility/puppeteer/ppLaunch.js";
|
|
7
7
|
import { ppRoute, ppCacheLogs } from "./utility/puppeteer/routes/ppRoute.js";
|
|
8
|
+
import { retryNavigation as ppRetryNavigation, retryClick as ppRetryClick, checkPageConditions as ppCheckPageConditions, exportSession as ppExportSession, importSession as ppImportSession } from "./utility/puppeteer/ppHelper.js";
|
|
8
9
|
|
|
9
10
|
export const ppBrowser = {
|
|
10
11
|
launch: ppLaunch,
|
|
11
12
|
route: ppRoute,
|
|
12
13
|
cacheLogs: ppCacheLogs,
|
|
14
|
+
Goto: ppRetryNavigation,
|
|
15
|
+
Click: ppRetryClick,
|
|
16
|
+
Conditions: ppCheckPageConditions,
|
|
17
|
+
exportSession: ppExportSession,
|
|
18
|
+
importSession: ppImportSession,
|
|
13
19
|
};
|
|
14
20
|
|
|
15
21
|
// --- Playwright ---
|
|
16
22
|
import { pwLaunch } from "./utility/playwright/pwLaunch.js";
|
|
17
23
|
import { pwRoute, pwCacheLogs } from "./utility/playwright/routes/pwRoute.js";
|
|
18
|
-
import { retryNavigation, retryClick, checkPageConditions } from "./utility/playwright/
|
|
24
|
+
import { retryNavigation, retryClick, checkPageConditions, exportSession, importSession } from "./utility/playwright/pwHelper.js";
|
|
19
25
|
|
|
20
26
|
export const pwBrowser = {
|
|
21
27
|
launch: pwLaunch,
|
|
22
28
|
route: pwRoute,
|
|
23
29
|
cacheLogs: pwCacheLogs,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
Goto: retryNavigation,
|
|
31
|
+
Click: retryClick,
|
|
32
|
+
Conditions: checkPageConditions,
|
|
33
|
+
exportSession,
|
|
34
|
+
importSession,
|
|
27
35
|
};
|
|
28
36
|
|
|
29
37
|
// --- Proxy ---
|
|
@@ -43,21 +43,7 @@ function gaussianRandom(mean = 0.5, stdDev = 0.15) {
|
|
|
43
43
|
return Math.max(0.1, Math.min(0.9, z0 * stdDev + mean));
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
* Simple mutex for serializing keyboard operations
|
|
48
|
-
*/
|
|
49
|
-
let typingLock = Promise.resolve();
|
|
50
|
-
async function withTypingLock(fn) {
|
|
51
|
-
const previousLock = typingLock;
|
|
52
|
-
let releaseLock;
|
|
53
|
-
typingLock = new Promise(resolve => { releaseLock = resolve; });
|
|
54
|
-
await previousLock;
|
|
55
|
-
try {
|
|
56
|
-
return await fn();
|
|
57
|
-
} finally {
|
|
58
|
-
releaseLock();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
46
|
+
|
|
61
47
|
|
|
62
48
|
/**
|
|
63
49
|
* Creates a HumanLocator that wraps a Playwright Locator
|
|
@@ -144,7 +130,7 @@ function createHumanLocator(cursor, locator) {
|
|
|
144
130
|
if (!cursor.config.humanize) {
|
|
145
131
|
return await locator.fill(value, options);
|
|
146
132
|
}
|
|
147
|
-
return await
|
|
133
|
+
return await cursor._withTypingLock(async () => {
|
|
148
134
|
await this.click(options);
|
|
149
135
|
// Clear field reliably using fill('') before human typing
|
|
150
136
|
await locator.fill('');
|
|
@@ -158,7 +144,7 @@ function createHumanLocator(cursor, locator) {
|
|
|
158
144
|
if (!cursor.config.humanize) {
|
|
159
145
|
return await locator.type(text, options);
|
|
160
146
|
}
|
|
161
|
-
return await
|
|
147
|
+
return await cursor._withTypingLock(async () => {
|
|
162
148
|
await this.click(options);
|
|
163
149
|
await cursor._type(text, options);
|
|
164
150
|
});
|
|
@@ -212,7 +198,7 @@ function createHumanLocator(cursor, locator) {
|
|
|
212
198
|
if (!cursor.config.humanize) {
|
|
213
199
|
return await locator.pressSequentially(text, options);
|
|
214
200
|
}
|
|
215
|
-
return await
|
|
201
|
+
return await cursor._withTypingLock(async () => {
|
|
216
202
|
await this.click(options);
|
|
217
203
|
await locator.clear();
|
|
218
204
|
await cursor._type(text, options);
|
|
@@ -273,9 +259,9 @@ function createHumanLocator(cursor, locator) {
|
|
|
273
259
|
* Create a HumanCursor that acts as a drop-in replacement for Page
|
|
274
260
|
* All page methods work, but click/fill/type/check/hover use human cursor
|
|
275
261
|
*
|
|
276
|
-
* @param {import('playwright').Page} page - Playwright Page object
|
|
262
|
+
* @param {import('playwright-core').Page} page - Playwright Page object
|
|
277
263
|
* @param {Object} options - Configuration options
|
|
278
|
-
* @returns {import('playwright').Page & HumanCursorMethods}
|
|
264
|
+
* @returns {import('playwright-core').Page & HumanCursorMethods}
|
|
279
265
|
*/
|
|
280
266
|
export function createCursor(page, options = {}) {
|
|
281
267
|
const config = {
|
|
@@ -292,9 +278,26 @@ export function createCursor(page, options = {}) {
|
|
|
292
278
|
// Internal state
|
|
293
279
|
const cursor = {
|
|
294
280
|
_page: page,
|
|
281
|
+
_typingLock: Promise.resolve(),
|
|
282
|
+
_viewportSize: null,
|
|
295
283
|
originCoordinates: [0, 0],
|
|
296
284
|
config,
|
|
297
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Per-cursor mutex for serializing keyboard operations
|
|
288
|
+
*/
|
|
289
|
+
async _withTypingLock(fn) {
|
|
290
|
+
const previousLock = this._typingLock;
|
|
291
|
+
let releaseLock;
|
|
292
|
+
this._typingLock = new Promise(resolve => { releaseLock = resolve; });
|
|
293
|
+
await previousLock;
|
|
294
|
+
try {
|
|
295
|
+
return await fn();
|
|
296
|
+
} finally {
|
|
297
|
+
releaseLock();
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
|
|
298
301
|
/**
|
|
299
302
|
* Move cursor to locator with human-like behavior
|
|
300
303
|
* Includes overshoot/correction and pre-movement jitter
|
|
@@ -367,7 +370,11 @@ export function createCursor(page, options = {}) {
|
|
|
367
370
|
* Move cursor to a specific point with variable speed and micro-pauses
|
|
368
371
|
*/
|
|
369
372
|
async _moveToPoint(destination, options = {}) {
|
|
370
|
-
|
|
373
|
+
if (!this._viewportSize) {
|
|
374
|
+
this._viewportSize = page.viewportSize()
|
|
375
|
+
|| await page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight }));
|
|
376
|
+
}
|
|
377
|
+
const viewport = this._viewportSize;
|
|
371
378
|
|
|
372
379
|
const params = generateRandomCurveParameters(
|
|
373
380
|
viewport,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* TypeScript declarations for human-cursor-playwright
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { Page, Locator, Response, PageScreenshotOptions, WaitForURLOptions } from 'playwright';
|
|
5
|
+
import type { Page, Locator, Response, PageScreenshotOptions, WaitForURLOptions } from 'playwright-core';
|
|
6
6
|
|
|
7
7
|
// ============= Tweening Functions =============
|
|
8
8
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* If navigation fails, it temporarily goes to "about:blank" before retrying.
|
|
4
4
|
*
|
|
5
5
|
* @param {Object} options
|
|
6
|
-
* @param {import('playwright').Page} options.page - Playwright Page object
|
|
6
|
+
* @param {import('playwright-core').Page} options.page - Playwright Page object
|
|
7
7
|
* @param {string} options.url - Target URL
|
|
8
8
|
* @param {number} [options.maxRetries=5] - Maximum number of attempts
|
|
9
9
|
* @param {string|null} [options.referer=null] - Referer header
|
|
@@ -54,7 +54,7 @@ export const retryNavigation = async ({
|
|
|
54
54
|
* EXPECTATION: The element should disappear (become hidden) after clicking.
|
|
55
55
|
*
|
|
56
56
|
* @param {Object} options
|
|
57
|
-
* @param {import('playwright').Locator} options.locator - The element to click
|
|
57
|
+
* @param {import('playwright-core').Locator} options.locator - The element to click
|
|
58
58
|
* @param {number} [options.maxRetries=3] - Max retries
|
|
59
59
|
* @param {number} [options.timeout=15000] - Timeout for visibility checks
|
|
60
60
|
*/
|
|
@@ -80,8 +80,8 @@ export const retryClick = async ({ locator, maxRetries = 3, timeout = 15000 }) =
|
|
|
80
80
|
* Races multiple conditions (URL or Element) to see which happens first.
|
|
81
81
|
* Modifies the input object by deleting the matched key.
|
|
82
82
|
*
|
|
83
|
-
* @param {import('playwright').Page} page
|
|
84
|
-
* @param {Object.<string, string|import('playwright').Locator|null>} checksToPerform - Map of names to URL strings or Locators
|
|
83
|
+
* @param {import('playwright-core').Page} page
|
|
84
|
+
* @param {Object.<string, string|import('playwright-core').Locator|null>} checksToPerform - Map of names to URL strings or Locators
|
|
85
85
|
* @param {number} timeout - Timeout in ms
|
|
86
86
|
* @returns {Promise<string|null>} The key of the matched condition
|
|
87
87
|
*/
|
|
@@ -94,7 +94,7 @@ export const checkPageConditions = async (page, checksToPerform, timeout) => {
|
|
|
94
94
|
try {
|
|
95
95
|
const promises = Object.entries(checksToPerform)
|
|
96
96
|
.filter(([_, value]) => value !== null) // Needed when setting value null after match
|
|
97
|
-
.map(([key, value]) => {
|
|
97
|
+
.map(async ([key, value]) => {
|
|
98
98
|
if (typeof value === "string") {
|
|
99
99
|
// Handle URL check
|
|
100
100
|
return page
|
|
@@ -127,3 +127,83 @@ export const checkPageConditions = async (page, checksToPerform, timeout) => {
|
|
|
127
127
|
return null; // Return null if no match is found
|
|
128
128
|
}
|
|
129
129
|
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Exports browser session (cookies + localStorage) via CDP.
|
|
133
|
+
* Silent operation — no browser blinking.
|
|
134
|
+
*
|
|
135
|
+
* @param {import('playwright-core').BrowserContext} context - Playwright BrowserContext
|
|
136
|
+
* @returns {Promise<{cookies: Array, localStorage: Array}>} Session data for JSONB storage
|
|
137
|
+
*/
|
|
138
|
+
export const exportSession = async (context) => {
|
|
139
|
+
const pages = context.pages();
|
|
140
|
+
const page = pages[0];
|
|
141
|
+
if (!page) throw new Error("No page available for exportSession");
|
|
142
|
+
|
|
143
|
+
const client = await context.newCDPSession(page);
|
|
144
|
+
const { cookies } = await client.send("Network.getAllCookies");
|
|
145
|
+
await client.detach();
|
|
146
|
+
|
|
147
|
+
const localStorage = [];
|
|
148
|
+
for (const p of pages) {
|
|
149
|
+
const url = p.url();
|
|
150
|
+
if (!url || url === "about:blank" || url.startsWith("chrome")) continue;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const origin = new URL(url).origin;
|
|
154
|
+
const items = await p.evaluate(() => {
|
|
155
|
+
const data = [];
|
|
156
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
157
|
+
const key = window.localStorage.key(i);
|
|
158
|
+
data.push({ name: key, value: window.localStorage.getItem(key) });
|
|
159
|
+
}
|
|
160
|
+
return data;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (items.length > 0 && !localStorage.some((o) => o.origin === origin)) {
|
|
164
|
+
localStorage.push({ origin, items });
|
|
165
|
+
}
|
|
166
|
+
} catch (e) {
|
|
167
|
+
// Skip pages that can't be evaluated
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { cookies, localStorage };
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Imports browser session (cookies + localStorage) via CDP.
|
|
176
|
+
* Silent operation — no browser blinking.
|
|
177
|
+
*
|
|
178
|
+
* @param {import('playwright-core').BrowserContext} context - Playwright BrowserContext
|
|
179
|
+
* @param {Object} data - Session data { cookies, localStorage }
|
|
180
|
+
*/
|
|
181
|
+
export const importSession = async (context, data) => {
|
|
182
|
+
const pages = context.pages();
|
|
183
|
+
const page = pages[0];
|
|
184
|
+
if (!page) throw new Error("No page available for importSession");
|
|
185
|
+
|
|
186
|
+
const client = await context.newCDPSession(page);
|
|
187
|
+
|
|
188
|
+
if (data.cookies?.length) {
|
|
189
|
+
await client.send("Network.setCookies", { cookies: data.cookies });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (data.localStorage?.length) {
|
|
193
|
+
await client.send("DOMStorage.enable");
|
|
194
|
+
for (const entry of data.localStorage) {
|
|
195
|
+
if (!entry.items?.length) continue;
|
|
196
|
+
|
|
197
|
+
const storageId = { securityOrigin: entry.origin, isLocalStorage: true };
|
|
198
|
+
for (const { name, value } of entry.items) {
|
|
199
|
+
await client.send("DOMStorage.setDOMStorageItem", {
|
|
200
|
+
storageId,
|
|
201
|
+
key: name,
|
|
202
|
+
value: value,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await client.detach();
|
|
209
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Browser, BrowserContext, Page } from "playwright";
|
|
1
|
+
import { Browser, BrowserContext, Page } from "playwright-core";
|
|
2
2
|
import type { FingerprintGeneratorOptions } from "fingerprint-generator";
|
|
3
3
|
import type { HumanPage, CreateCursorOptions } from "./human-cursor/index";
|
|
4
4
|
type Screen = FingerprintGeneratorOptions["screen"];
|
|
@@ -9,7 +9,7 @@ import fs from "fs";
|
|
|
9
9
|
import path from "path";
|
|
10
10
|
import os from "os";
|
|
11
11
|
import crypto from "node:crypto";
|
|
12
|
-
import { chromium, firefox } from "playwright";
|
|
12
|
+
import { chromium, firefox } from "playwright-core";
|
|
13
13
|
import { setTimeout as sleep } from "timers/promises";
|
|
14
14
|
|
|
15
15
|
// Fingerprint management
|
|
@@ -494,6 +494,31 @@ async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, hum
|
|
|
494
494
|
try {
|
|
495
495
|
if (!fs.existsSync(activePath)) fs.mkdirSync(activePath, { recursive: true });
|
|
496
496
|
|
|
497
|
+
// ======================================================
|
|
498
|
+
// Prevent Tab Restore via Preferences
|
|
499
|
+
// ======================================================
|
|
500
|
+
try {
|
|
501
|
+
const prefsFilePath = path.join(activePath, "Default", "Preferences");
|
|
502
|
+
const prefsDir = path.dirname(prefsFilePath);
|
|
503
|
+
if (!fs.existsSync(prefsDir)) fs.mkdirSync(prefsDir, { recursive: true });
|
|
504
|
+
|
|
505
|
+
let prefs = {};
|
|
506
|
+
if (fs.existsSync(prefsFilePath)) {
|
|
507
|
+
prefs = JSON.parse(fs.readFileSync(prefsFilePath, "utf-8"));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (!prefs.profile) prefs.profile = {};
|
|
511
|
+
prefs.profile.exit_type = "Normal";
|
|
512
|
+
|
|
513
|
+
if (!prefs.session) prefs.session = {};
|
|
514
|
+
prefs.session.restore_on_startup = 4;
|
|
515
|
+
prefs.session.startup_urls = ["about:blank"];
|
|
516
|
+
|
|
517
|
+
fs.writeFileSync(prefsFilePath, JSON.stringify(prefs, null, 2), "utf-8");
|
|
518
|
+
} catch (e) {
|
|
519
|
+
// Non-critical
|
|
520
|
+
}
|
|
521
|
+
|
|
497
522
|
// Logic: Native Persistent Launch. No fingerprint-injector.
|
|
498
523
|
const context = await chromium.launchPersistentContext(activePath, {
|
|
499
524
|
headless: false,
|
|
@@ -690,6 +715,15 @@ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humani
|
|
|
690
715
|
if (!prefs.brave) prefs.brave = {};
|
|
691
716
|
if (!prefs.brave.sidebar) prefs.brave.sidebar = {};
|
|
692
717
|
prefs.brave.sidebar.sidebar_show_option = 3;
|
|
718
|
+
|
|
719
|
+
// Prevent tab restore (saves proxy bandwidth)
|
|
720
|
+
if (!prefs.profile) prefs.profile = {};
|
|
721
|
+
prefs.profile.exit_type = "Normal";
|
|
722
|
+
|
|
723
|
+
if (!prefs.session) prefs.session = {};
|
|
724
|
+
prefs.session.restore_on_startup = 4;
|
|
725
|
+
prefs.session.startup_urls = ["about:blank"];
|
|
726
|
+
|
|
693
727
|
fs.writeFileSync(prefsFilePath, JSON.stringify(prefs, null, 2), "utf-8");
|
|
694
728
|
} catch (e) {
|
|
695
729
|
console.warn("░░░░░ Could not modify Brave preferences:", e.message);
|
|
@@ -729,7 +763,6 @@ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humani
|
|
|
729
763
|
// --- Brave Specific Silence ---
|
|
730
764
|
"--disable-features=Translate,BraveRewards,BraveWallet,BraveNews,Sidebar,SidePanel,BraveNTPBrandedWallpaper,NTPBackgroundImages",
|
|
731
765
|
"--disable-infobars",
|
|
732
|
-
"about:blank",
|
|
733
766
|
];
|
|
734
767
|
const ignoreDefaultArgs = ["--enable-automation"];
|
|
735
768
|
if (CapSolver) {
|
|
@@ -966,11 +999,27 @@ async function launchExistingMultiloginProfile(profileId, humanize_options = nul
|
|
|
966
999
|
page = createCursor(page, humanize_options);
|
|
967
1000
|
}
|
|
968
1001
|
|
|
1002
|
+
let closing = false;
|
|
969
1003
|
const closeBrowser = async () => {
|
|
1004
|
+
if (closing) return false;
|
|
1005
|
+
closing = true;
|
|
1006
|
+
if (signalHandler) {
|
|
1007
|
+
process.off("SIGINT", signalHandler);
|
|
1008
|
+
process.off("SIGTERM", signalHandler);
|
|
1009
|
+
process.off("SIGHUP", signalHandler);
|
|
1010
|
+
}
|
|
970
1011
|
if (browser) await browser.close().catch(() => { });
|
|
971
1012
|
return await stopMultiloginProfile(profileId);
|
|
972
1013
|
};
|
|
973
1014
|
|
|
1015
|
+
let signalHandler = async () => {
|
|
1016
|
+
await closeBrowser();
|
|
1017
|
+
process.exit(0);
|
|
1018
|
+
};
|
|
1019
|
+
process.once("SIGINT", signalHandler);
|
|
1020
|
+
process.once("SIGTERM", signalHandler);
|
|
1021
|
+
process.once("SIGHUP", signalHandler);
|
|
1022
|
+
|
|
974
1023
|
return { browser, context, page, isBrowserRunning: () => !!browser, closeBrowser, launchError: null };
|
|
975
1024
|
} catch (error) {
|
|
976
1025
|
console.error("Multilogin Launch Error:", error.message);
|
|
@@ -1040,11 +1089,27 @@ async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, medi
|
|
|
1040
1089
|
page = createCursor(page, humanize_options);
|
|
1041
1090
|
}
|
|
1042
1091
|
|
|
1092
|
+
let closing = false;
|
|
1043
1093
|
const closeBrowser = async () => {
|
|
1094
|
+
if (closing) return false;
|
|
1095
|
+
closing = true;
|
|
1096
|
+
if (signalHandler) {
|
|
1097
|
+
process.off("SIGINT", signalHandler);
|
|
1098
|
+
process.off("SIGTERM", signalHandler);
|
|
1099
|
+
process.off("SIGHUP", signalHandler);
|
|
1100
|
+
}
|
|
1044
1101
|
if (browser) await browser.close().catch(() => { });
|
|
1045
1102
|
return await stopMultiloginProfile(profileId);
|
|
1046
1103
|
};
|
|
1047
1104
|
|
|
1105
|
+
let signalHandler = async () => {
|
|
1106
|
+
await closeBrowser();
|
|
1107
|
+
process.exit(0);
|
|
1108
|
+
};
|
|
1109
|
+
process.once("SIGINT", signalHandler);
|
|
1110
|
+
process.once("SIGTERM", signalHandler);
|
|
1111
|
+
process.once("SIGHUP", signalHandler);
|
|
1112
|
+
|
|
1048
1113
|
return { browser, context, page, isBrowserRunning: () => !!browser, closeBrowser, launchError: null };
|
|
1049
1114
|
} catch (error) {
|
|
1050
1115
|
console.error("Quick Profile Error:", error);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Page, Locator } from "puppeteer-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for the retryNavigation function.
|
|
5
|
+
*/
|
|
6
|
+
export interface RetryNavigationOptions {
|
|
7
|
+
/** The Puppeteer Page object. */
|
|
8
|
+
page: Page;
|
|
9
|
+
/** The target URL to navigate to. */
|
|
10
|
+
url: string;
|
|
11
|
+
/** Maximum number of retry attempts. Default: 5 */
|
|
12
|
+
maxRetries?: number;
|
|
13
|
+
/** Custom Referer header. Default: null */
|
|
14
|
+
referer?: string | null;
|
|
15
|
+
/** Base timeout in milliseconds. Default: 30000 */
|
|
16
|
+
timeout?: number;
|
|
17
|
+
/** When to consider operation succeeded. Default: "load" */
|
|
18
|
+
waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for the retryClick function.
|
|
23
|
+
*/
|
|
24
|
+
export interface RetryClickOptions {
|
|
25
|
+
/** The Puppeteer Page object. Required when using selector. */
|
|
26
|
+
page?: Page;
|
|
27
|
+
/** CSS/aria/xpath selector string (old-style). Provide either selector or locator. */
|
|
28
|
+
selector?: string;
|
|
29
|
+
/** Puppeteer Locator (new-style). Provide either selector or locator. */
|
|
30
|
+
locator?: Locator<Node>;
|
|
31
|
+
/** Maximum number of retry attempts. Default: 3 */
|
|
32
|
+
maxRetries?: number;
|
|
33
|
+
/** Timeout for waiting for element visibility/invisibility. Default: 15000 */
|
|
34
|
+
timeout?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Navigates to a URL with retry logic and incremental timeouts.
|
|
39
|
+
* If navigation fails, it temporarily goes to "about:blank" before retrying.
|
|
40
|
+
* @returns True if navigation succeeded, throws error otherwise.
|
|
41
|
+
*/
|
|
42
|
+
export function retryNavigation(options: RetryNavigationOptions): Promise<boolean>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Retries clicking an element (by selector or locator).
|
|
46
|
+
* Warning: This function waits for the element to become HIDDEN after clicking.
|
|
47
|
+
* Use this for actions that close modals or navigate away.
|
|
48
|
+
*/
|
|
49
|
+
export function retryClick(options: RetryClickOptions): Promise<void>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Races multiple conditions (URL match or Locator visibility) to see which happens first.
|
|
53
|
+
*
|
|
54
|
+
* Value types in checksToPerform:
|
|
55
|
+
* - string → URL check (exact match, startsWith, or glob with *)
|
|
56
|
+
* - Locator → Element check via page.locator() (new-style API)
|
|
57
|
+
* - null → skipped
|
|
58
|
+
*
|
|
59
|
+
* NOTE: This function MUTATES the `checksToPerform` object by deleting the matched key.
|
|
60
|
+
*
|
|
61
|
+
* @param page - The Puppeteer Page object.
|
|
62
|
+
* @param checksToPerform - An object mapping keys (names) to URL strings, Locators, or null.
|
|
63
|
+
* @param timeout - Maximum time to wait for a condition in milliseconds.
|
|
64
|
+
* @returns The key name of the matched condition, or null if timed out.
|
|
65
|
+
*/
|
|
66
|
+
export function checkPageConditions(
|
|
67
|
+
page: Page,
|
|
68
|
+
checksToPerform: Record<string, string | Locator<Node> | null>,
|
|
69
|
+
timeout: number
|
|
70
|
+
): Promise<string | null>;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigates to a URL with retry logic and incremental timeouts.
|
|
3
|
+
* If navigation fails, it temporarily goes to "about:blank" before retrying.
|
|
4
|
+
*
|
|
5
|
+
* @param {Object} options
|
|
6
|
+
* @param {import('puppeteer-core').Page} options.page - Puppeteer Page object
|
|
7
|
+
* @param {string} options.url - Target URL
|
|
8
|
+
* @param {number} [options.maxRetries=5] - Maximum number of attempts
|
|
9
|
+
* @param {string|null} [options.referer=null] - Referer header
|
|
10
|
+
* @param {number} [options.timeout=30000] - Base timeout in ms
|
|
11
|
+
* @param {"load"|"domcontentloaded"|"networkidle0"|"networkidle2"} [options.waitUntil="load"] - Wait condition
|
|
12
|
+
* @returns {Promise<boolean>} True if successful
|
|
13
|
+
*/
|
|
14
|
+
export const retryNavigation = async ({
|
|
15
|
+
page,
|
|
16
|
+
url,
|
|
17
|
+
maxRetries = 5,
|
|
18
|
+
referer = null,
|
|
19
|
+
timeout = 30000,
|
|
20
|
+
waitUntil = "load",
|
|
21
|
+
}) => {
|
|
22
|
+
for (let retryCount = 0; retryCount < maxRetries; retryCount++) {
|
|
23
|
+
// Apply incremental timeout only if the initial timeout is <= 30,000
|
|
24
|
+
const currentTimeout = timeout <= 30000 ? timeout + retryCount * 15000 : timeout;
|
|
25
|
+
try {
|
|
26
|
+
const gotoOptions = {
|
|
27
|
+
waitUntil: waitUntil,
|
|
28
|
+
timeout: currentTimeout,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (referer) {
|
|
32
|
+
gotoOptions.referer = referer;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await page.goto(url, gotoOptions);
|
|
36
|
+
|
|
37
|
+
return true;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.log(`Navigation attempt ${retryCount + 1} failed.`, url);
|
|
40
|
+
await page.goto("about:blank", { waitUntil: "load", timeout: timeout });
|
|
41
|
+
if (retryCount === maxRetries - 1) {
|
|
42
|
+
console.log("All retry attempts failed");
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Retries clicking an element (by selector or locator).
|
|
52
|
+
* EXPECTATION: The element should disappear (become hidden/detached) after clicking.
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} options
|
|
55
|
+
* @param {import('puppeteer-core').Page} options.page - Puppeteer Page object (required when using selector)
|
|
56
|
+
* @param {string} [options.selector] - CSS/aria/xpath selector string (old-style)
|
|
57
|
+
* @param {import('puppeteer-core').Locator} [options.locator] - Puppeteer Locator (new-style)
|
|
58
|
+
* @param {number} [options.maxRetries=3] - Max retries
|
|
59
|
+
* @param {number} [options.timeout=15000] - Timeout for visibility checks
|
|
60
|
+
*/
|
|
61
|
+
export const retryClick = async ({ page, selector, locator, maxRetries = 3, timeout = 15000 }) => {
|
|
62
|
+
if (!selector && !locator) throw new Error("Either selector or locator is required");
|
|
63
|
+
if (selector && !page) throw new Error("Page is required when using selector");
|
|
64
|
+
|
|
65
|
+
for (let retryCount = 0; retryCount < maxRetries; retryCount++) {
|
|
66
|
+
try {
|
|
67
|
+
if (locator) {
|
|
68
|
+
// New-style: use Locator API
|
|
69
|
+
await locator.setTimeout(timeout).setVisibility("visible").click();
|
|
70
|
+
// Wait for element to disappear
|
|
71
|
+
await locator.setTimeout(timeout).setVisibility("hidden").wait().catch(() => {});
|
|
72
|
+
} else {
|
|
73
|
+
// Old-style: use waitForSelector
|
|
74
|
+
const el = await page.waitForSelector(selector, { visible: true, timeout });
|
|
75
|
+
await el.click();
|
|
76
|
+
// Wait for element to disappear
|
|
77
|
+
await page.waitForSelector(selector, { hidden: true, timeout }).catch(() => {});
|
|
78
|
+
}
|
|
79
|
+
return; // Exit if successful
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (retryCount === maxRetries - 1) {
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Races multiple conditions (URL or Locator) to see which happens first.
|
|
90
|
+
* Modifies the input object by deleting the matched key.
|
|
91
|
+
*
|
|
92
|
+
* Value types in checksToPerform:
|
|
93
|
+
* - string → URL check (exact match, startsWith, or glob with *)
|
|
94
|
+
* - Locator → Element check via page.locator() (new-style API)
|
|
95
|
+
* - null → skipped
|
|
96
|
+
*
|
|
97
|
+
* @param {import('puppeteer-core').Page} page
|
|
98
|
+
* @param {Object.<string, string|import('puppeteer-core').Locator|null>} checksToPerform - Map of names to URL strings or Locators
|
|
99
|
+
* @param {number} timeout - Timeout in ms
|
|
100
|
+
* @returns {Promise<string|null>} The key of the matched condition
|
|
101
|
+
*/
|
|
102
|
+
export const checkPageConditions = async (page, checksToPerform, timeout) => {
|
|
103
|
+
// Validate required parameters
|
|
104
|
+
if (!page) throw new Error("Page parameter is required");
|
|
105
|
+
if (!timeout) throw new Error("Timeout parameter is required");
|
|
106
|
+
|
|
107
|
+
let matchedKey = null;
|
|
108
|
+
try {
|
|
109
|
+
const promises = Object.entries(checksToPerform)
|
|
110
|
+
.filter(([_, value]) => value !== null) // Needed when setting value null after match
|
|
111
|
+
.map(async ([key, value]) => {
|
|
112
|
+
if (typeof value === "string") {
|
|
113
|
+
// Handle URL check — poll with waitForFunction
|
|
114
|
+
const urlPattern = value;
|
|
115
|
+
return page
|
|
116
|
+
.waitForFunction(
|
|
117
|
+
(pattern) => {
|
|
118
|
+
const url = window.location.href;
|
|
119
|
+
if (pattern.includes("*")) {
|
|
120
|
+
// Convert glob to regex: ** → .*, * → [^/]*
|
|
121
|
+
const regexStr = pattern.replace(/\*\*/g, ".*").replace(/(?<!\.\*)\*/g, "[^/]*");
|
|
122
|
+
return new RegExp("^" + regexStr + "$").test(url);
|
|
123
|
+
}
|
|
124
|
+
return url === pattern || url.startsWith(pattern);
|
|
125
|
+
},
|
|
126
|
+
{ timeout },
|
|
127
|
+
urlPattern
|
|
128
|
+
)
|
|
129
|
+
.then(() => key);
|
|
130
|
+
} else {
|
|
131
|
+
// Handle Locator check (from page.locator())
|
|
132
|
+
return value
|
|
133
|
+
.setTimeout(timeout)
|
|
134
|
+
.setVisibility("visible")
|
|
135
|
+
.wait()
|
|
136
|
+
.then(() => key);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
matchedKey = await Promise.race(promises);
|
|
141
|
+
if (matchedKey) {
|
|
142
|
+
// Remove the matched key from the original object
|
|
143
|
+
delete checksToPerform[matchedKey];
|
|
144
|
+
console.log(`${matchedKey} matched!`);
|
|
145
|
+
}
|
|
146
|
+
return matchedKey; // Return the matched key if found
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.log(`No match found - ${error.message}`);
|
|
149
|
+
return null; // Return null if no match is found
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Exports browser session (cookies + localStorage) via CDP.
|
|
155
|
+
* Silent operation — no browser blinking.
|
|
156
|
+
*
|
|
157
|
+
* @param {import('puppeteer-core').Page} page - Any Puppeteer Page
|
|
158
|
+
* @returns {Promise<{cookies: Array, localStorage: Array}>} Session data for JSONB storage
|
|
159
|
+
*/
|
|
160
|
+
export const exportSession = async (page) => {
|
|
161
|
+
const client = await page.createCDPSession();
|
|
162
|
+
const { cookies } = await client.send("Network.getAllCookies");
|
|
163
|
+
await client.detach();
|
|
164
|
+
|
|
165
|
+
const localStorage = [];
|
|
166
|
+
const browser = page.browser();
|
|
167
|
+
const pages = await browser.pages();
|
|
168
|
+
|
|
169
|
+
for (const p of pages) {
|
|
170
|
+
const url = p.url();
|
|
171
|
+
if (!url || url === "about:blank" || url.startsWith("chrome")) continue;
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const origin = new URL(url).origin;
|
|
175
|
+
const items = await p.evaluate(() => {
|
|
176
|
+
const data = [];
|
|
177
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
178
|
+
const key = window.localStorage.key(i);
|
|
179
|
+
data.push({ name: key, value: window.localStorage.getItem(key) });
|
|
180
|
+
}
|
|
181
|
+
return data;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (items.length > 0 && !localStorage.some((o) => o.origin === origin)) {
|
|
185
|
+
localStorage.push({ origin, items });
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {
|
|
188
|
+
// Skip pages that can't be evaluated
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { cookies, localStorage };
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Imports browser session (cookies + localStorage) into a Puppeteer Browser.
|
|
197
|
+
*
|
|
198
|
+
* @param {import('puppeteer-core').Page} page - Any Puppeteer Page
|
|
199
|
+
* @param {Object} data - Session data { cookies, localStorage }
|
|
200
|
+
*/
|
|
201
|
+
export const importSession = async (page, data) => {
|
|
202
|
+
if (data.cookies?.length) {
|
|
203
|
+
const client = await page.createCDPSession();
|
|
204
|
+
await client.send("Network.setCookies", { cookies: data.cookies });
|
|
205
|
+
await client.detach();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (data.localStorage?.length) {
|
|
209
|
+
const browser = page.browser();
|
|
210
|
+
|
|
211
|
+
for (const entry of data.localStorage) {
|
|
212
|
+
if (!entry.items?.length) continue;
|
|
213
|
+
|
|
214
|
+
const p = await browser.newPage();
|
|
215
|
+
try {
|
|
216
|
+
await p.goto(entry.origin, { waitUntil: "domcontentloaded", timeout: 15000 });
|
|
217
|
+
await p.evaluate((items) => {
|
|
218
|
+
for (const { name, value } of items) {
|
|
219
|
+
window.localStorage.setItem(name, value);
|
|
220
|
+
}
|
|
221
|
+
}, entry.items);
|
|
222
|
+
} finally {
|
|
223
|
+
await p.close();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
@@ -348,6 +348,15 @@ async function braveLauncher({ profilePath, proxy, extraArgs, spoof_fingerprint,
|
|
|
348
348
|
if (!prefs.brave) prefs.brave = {};
|
|
349
349
|
if (!prefs.brave.sidebar) prefs.brave.sidebar = {};
|
|
350
350
|
prefs.brave.sidebar.sidebar_show_option = 3;
|
|
351
|
+
|
|
352
|
+
// Prevent tab restore (saves proxy bandwidth)
|
|
353
|
+
if (!prefs.profile) prefs.profile = {};
|
|
354
|
+
prefs.profile.exit_type = "Normal";
|
|
355
|
+
|
|
356
|
+
if (!prefs.session) prefs.session = {};
|
|
357
|
+
prefs.session.restore_on_startup = 4;
|
|
358
|
+
prefs.session.startup_urls = ["about:blank"];
|
|
359
|
+
|
|
351
360
|
fs.writeFileSync(prefsFilePath, JSON.stringify(prefs, null, 2), "utf-8");
|
|
352
361
|
} catch (e) {
|
|
353
362
|
console.warn("░░░░░ Could not modify Brave preferences:", e.message);
|
|
@@ -380,6 +389,35 @@ async function spawnAndConnect({ binaryPath, profilePath, isPersistent, proxy, e
|
|
|
380
389
|
let closing = false;
|
|
381
390
|
let signalHandler;
|
|
382
391
|
|
|
392
|
+
// ======================================================
|
|
393
|
+
// Prevent Tab Restore via Preferences
|
|
394
|
+
// Sets exit_type to "Normal" and startup to open about:blank
|
|
395
|
+
// This prevents old tabs from loading (saves proxy bandwidth)
|
|
396
|
+
// ======================================================
|
|
397
|
+
try {
|
|
398
|
+
const prefsFilePath = path.join(profilePath, "Default", "Preferences");
|
|
399
|
+
const prefsDir = path.dirname(prefsFilePath);
|
|
400
|
+
if (!fs.existsSync(prefsDir)) fs.mkdirSync(prefsDir, { recursive: true });
|
|
401
|
+
|
|
402
|
+
let prefs = {};
|
|
403
|
+
if (fs.existsSync(prefsFilePath)) {
|
|
404
|
+
prefs = JSON.parse(fs.readFileSync(prefsFilePath, "utf-8"));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Prevent "Restore pages?" prompt after crash
|
|
408
|
+
if (!prefs.profile) prefs.profile = {};
|
|
409
|
+
prefs.profile.exit_type = "Normal";
|
|
410
|
+
|
|
411
|
+
// Set startup to open about:blank instead of restoring tabs
|
|
412
|
+
if (!prefs.session) prefs.session = {};
|
|
413
|
+
prefs.session.restore_on_startup = 4;
|
|
414
|
+
prefs.session.startup_urls = ["about:blank"];
|
|
415
|
+
|
|
416
|
+
fs.writeFileSync(prefsFilePath, JSON.stringify(prefs, null, 2), "utf-8");
|
|
417
|
+
} catch (e) {
|
|
418
|
+
// Non-critical — don't break the launch
|
|
419
|
+
}
|
|
420
|
+
|
|
383
421
|
// Random 5-digit port (10000–65535)
|
|
384
422
|
const debugPort = Math.floor(Math.random() * 55535) + 10000;
|
|
385
423
|
|
|
@@ -394,8 +432,10 @@ async function spawnAndConnect({ binaryPath, profilePath, isPersistent, proxy, e
|
|
|
394
432
|
// Very Important for Vps
|
|
395
433
|
"--no-sandbox",
|
|
396
434
|
// --- Stealth & Anti-Detection ---
|
|
435
|
+
// Suppresses "unsupported command-line flag" warning bar
|
|
397
436
|
"--test-type",
|
|
398
|
-
|
|
437
|
+
// Not needed for CDP — navigator.webdriver is not set when using --remote-debugging-port
|
|
438
|
+
// "--disable-blink-features=AutomationControlled",
|
|
399
439
|
|
|
400
440
|
// --- PREVENT EXTRA TABS ---
|
|
401
441
|
"--disable-restore-session-state",
|
|
@@ -612,13 +652,29 @@ async function launchExistingMultiloginProfile(profileId) {
|
|
|
612
652
|
const pages = await browser.pages();
|
|
613
653
|
const page = pages[0] ?? (await browser.newPage());
|
|
614
654
|
|
|
655
|
+
let closing = false;
|
|
615
656
|
const closeBrowser = async () => {
|
|
657
|
+
if (closing) return false;
|
|
658
|
+
closing = true;
|
|
659
|
+
if (signalHandler) {
|
|
660
|
+
process.off("SIGINT", signalHandler);
|
|
661
|
+
process.off("SIGTERM", signalHandler);
|
|
662
|
+
process.off("SIGHUP", signalHandler);
|
|
663
|
+
}
|
|
616
664
|
try {
|
|
617
665
|
if (browser?.connected) await browser.close();
|
|
618
666
|
} catch { }
|
|
619
667
|
return await stopMultiloginProfile(profileId);
|
|
620
668
|
};
|
|
621
669
|
|
|
670
|
+
let signalHandler = async () => {
|
|
671
|
+
await closeBrowser();
|
|
672
|
+
process.exit(0);
|
|
673
|
+
};
|
|
674
|
+
process.once("SIGINT", signalHandler);
|
|
675
|
+
process.once("SIGTERM", signalHandler);
|
|
676
|
+
process.once("SIGHUP", signalHandler);
|
|
677
|
+
|
|
622
678
|
return { browser, context: browser, page, isBrowserRunning: () => !!browser?.connected, closeBrowser, launchError: null };
|
|
623
679
|
} catch (error) {
|
|
624
680
|
console.error("Multilogin Launch Error:", error.message);
|
|
@@ -685,13 +741,29 @@ async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, medi
|
|
|
685
741
|
const pages = await browser.pages();
|
|
686
742
|
const page = pages[0] ?? (await browser.newPage());
|
|
687
743
|
|
|
744
|
+
let closing = false;
|
|
688
745
|
const closeBrowser = async () => {
|
|
746
|
+
if (closing) return false;
|
|
747
|
+
closing = true;
|
|
748
|
+
if (signalHandler) {
|
|
749
|
+
process.off("SIGINT", signalHandler);
|
|
750
|
+
process.off("SIGTERM", signalHandler);
|
|
751
|
+
process.off("SIGHUP", signalHandler);
|
|
752
|
+
}
|
|
689
753
|
try {
|
|
690
754
|
if (browser?.connected) await browser.close();
|
|
691
755
|
} catch { }
|
|
692
756
|
return await stopMultiloginProfile(profileId);
|
|
693
757
|
};
|
|
694
758
|
|
|
759
|
+
let signalHandler = async () => {
|
|
760
|
+
await closeBrowser();
|
|
761
|
+
process.exit(0);
|
|
762
|
+
};
|
|
763
|
+
process.once("SIGINT", signalHandler);
|
|
764
|
+
process.once("SIGTERM", signalHandler);
|
|
765
|
+
process.once("SIGHUP", signalHandler);
|
|
766
|
+
|
|
695
767
|
return { browser, context: browser, page, isBrowserRunning: () => !!browser?.connected, closeBrowser, launchError: null };
|
|
696
768
|
} catch (error) {
|
|
697
769
|
console.error("Quick Profile Error:", error);
|