@push.rocks/smartpuppeteer 2.1.0 → 2.3.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/smartpuppeteer.classes.livebrowsersession.d.ts +53 -15
- package/dist_ts/smartpuppeteer.classes.livebrowsersession.js +1404 -60
- package/dist_ts/smartpuppeteer.classes.smartpuppeteer.d.ts +1 -0
- package/dist_ts/smartpuppeteer.classes.smartpuppeteer.js +23 -4
- package/dist_ts/smartpuppeteer.helpers.process.d.ts +14 -0
- package/dist_ts/smartpuppeteer.helpers.process.js +132 -0
- package/dist_ts/smartpuppeteer.interfaces.livebrowser.d.ts +47 -0
- package/dist_ts/smartpuppeteer.plugins.d.ts +4 -2
- package/dist_ts/smartpuppeteer.plugins.js +5 -3
- package/package.json +4 -4
- package/readme.md +52 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartpuppeteer.classes.livebrowsersession.ts +1768 -90
- package/ts/smartpuppeteer.classes.smartpuppeteer.ts +27 -4
- package/ts/smartpuppeteer.helpers.process.ts +169 -0
- package/ts/smartpuppeteer.interfaces.livebrowser.ts +59 -0
- package/ts/smartpuppeteer.plugins.ts +4 -2
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { getEnvAwareBrowserInstance } from './smartpuppeteer.classes.smartpuppeteer.js';
|
|
2
|
+
import {
|
|
3
|
+
delay,
|
|
4
|
+
type IOwnedProcessIdentity,
|
|
5
|
+
killFrozenOwnedProcessGroup,
|
|
6
|
+
listOwnedProcessGroupMembers,
|
|
7
|
+
readOwnedProcessIdentity,
|
|
8
|
+
signalOwnedProcessIdentity,
|
|
9
|
+
signalOwnedProcessGroup,
|
|
10
|
+
} from './smartpuppeteer.helpers.process.js';
|
|
2
11
|
import type {
|
|
3
12
|
ILiveBrowserClickOptions,
|
|
4
13
|
ILiveBrowserCreateTabOptions,
|
|
5
14
|
ILiveBrowserError,
|
|
15
|
+
ILiveBrowserEvaluateOptions,
|
|
6
16
|
ILiveBrowserFillOptions,
|
|
7
17
|
ILiveBrowserFrame,
|
|
8
18
|
ILiveBrowserFrameAcknowledgement,
|
|
@@ -15,17 +25,22 @@ import type {
|
|
|
15
25
|
ILiveBrowserNavigationOptions,
|
|
16
26
|
ILiveBrowserObservation,
|
|
17
27
|
ILiveBrowserObserveOptions,
|
|
28
|
+
ILiveBrowserOperationOptions,
|
|
18
29
|
ILiveBrowserPressOptions,
|
|
30
|
+
ILiveBrowserProcessState,
|
|
19
31
|
ILiveBrowserSessionOptions,
|
|
20
32
|
ILiveBrowserSnapshot,
|
|
21
33
|
ILiveBrowserSnapshotOptions,
|
|
22
34
|
ILiveBrowserState,
|
|
23
35
|
ILiveBrowserTabState,
|
|
36
|
+
ILiveBrowserTerminationOptions,
|
|
37
|
+
ILiveBrowserTerminationResult,
|
|
24
38
|
ILiveBrowserViewport,
|
|
25
39
|
ILiveBrowserWheelInput,
|
|
26
40
|
TLiveBrowserEvent,
|
|
27
41
|
TLiveBrowserEventListener,
|
|
28
42
|
TLiveBrowserImageFormat,
|
|
43
|
+
TLiveBrowserJsonValue,
|
|
29
44
|
TLiveBrowserWaitUntil,
|
|
30
45
|
} from './smartpuppeteer.interfaces.livebrowser.js';
|
|
31
46
|
import * as plugins from './smartpuppeteer.plugins.js';
|
|
@@ -47,10 +62,62 @@ const maxTimeoutMs = 60000;
|
|
|
47
62
|
const maxOutstandingFrames = 3;
|
|
48
63
|
const maxQueuedPublicOperations = 64;
|
|
49
64
|
const maxQueuedInternalOperations = 128;
|
|
65
|
+
const maxEvaluationScriptBytes = 262144;
|
|
66
|
+
const maxEvaluationOutputBytes = 1048576;
|
|
67
|
+
const maxEvaluationDepth = 32;
|
|
68
|
+
const maxEvaluationNodes = 50000;
|
|
69
|
+
const maxEvaluationStringBytes = 262144;
|
|
70
|
+
const maxEvaluationArrayLength = 10000;
|
|
71
|
+
const maxEvaluationObjectKeys = 10000;
|
|
72
|
+
const evaluationBootstrapKey = '__smartpuppeteerEvaluate';
|
|
73
|
+
const evaluationCancelKey = '__smartpuppeteerCancel';
|
|
74
|
+
const evaluationCleanupKey = '__smartpuppeteerCleanup';
|
|
75
|
+
const maxProxySecuritySessions = 256;
|
|
76
|
+
const maxTrackedProxyRequests = 1024;
|
|
77
|
+
const maxTotalTrackedProxyRequests = 4096;
|
|
78
|
+
const maxProxySecurityOperations = 2048;
|
|
79
|
+
const proxySeedTargetTypes = new Set([
|
|
80
|
+
'background_page',
|
|
81
|
+
'page',
|
|
82
|
+
'service_worker',
|
|
83
|
+
'shared_worker',
|
|
84
|
+
'webview',
|
|
85
|
+
]);
|
|
86
|
+
const proxyFetchUnsupportedTargetTypes = new Set(['tab', 'worker']);
|
|
50
87
|
|
|
51
88
|
type TScreencastFrameEvent = plugins.puppeteer.Protocol.Page.ScreencastFrameEvent;
|
|
52
89
|
type TScreencastFrameListener = (event: TScreencastFrameEvent) => void;
|
|
53
90
|
type TCdpSessionDetachedListener = (session: plugins.puppeteer.CDPSession) => void;
|
|
91
|
+
type TOwnedChildProcess = NonNullable<ReturnType<plugins.puppeteer.Browser['process']>>;
|
|
92
|
+
type TProxyAuthRequiredEvent = plugins.puppeteer.Protocol.Fetch.AuthRequiredEvent;
|
|
93
|
+
type TProxyRequestPausedEvent = plugins.puppeteer.Protocol.Fetch.RequestPausedEvent;
|
|
94
|
+
type TNetworkLoadingFinishedEvent = plugins.puppeteer.Protocol.Network.LoadingFinishedEvent;
|
|
95
|
+
type TNetworkLoadingFailedEvent = plugins.puppeteer.Protocol.Network.LoadingFailedEvent;
|
|
96
|
+
|
|
97
|
+
interface IProxySecuritySession {
|
|
98
|
+
session: plugins.puppeteer.CDPSession;
|
|
99
|
+
generation: number;
|
|
100
|
+
targetId?: string;
|
|
101
|
+
targetType?: string;
|
|
102
|
+
fetchEnabled: boolean;
|
|
103
|
+
allowLiveTargetDetach: boolean;
|
|
104
|
+
ownedByProxySecurity: boolean;
|
|
105
|
+
attemptedAuthentications: Set<string>;
|
|
106
|
+
requestIdsByNetworkId: Map<string, string>;
|
|
107
|
+
authRequiredListener: (event: TProxyAuthRequiredEvent) => void;
|
|
108
|
+
requestPausedListener: (event: TProxyRequestPausedEvent) => void;
|
|
109
|
+
loadingFinishedListener: (event: TNetworkLoadingFinishedEvent) => void;
|
|
110
|
+
loadingFailedListener: (event: TNetworkLoadingFailedEvent) => void;
|
|
111
|
+
setupPromise: Promise<void>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface IOwnedBrowserProcess {
|
|
115
|
+
generation: number;
|
|
116
|
+
childProcess: TOwnedChildProcess;
|
|
117
|
+
identity?: IOwnedProcessIdentity;
|
|
118
|
+
exitPromise: Promise<void>;
|
|
119
|
+
forceSignalled: boolean;
|
|
120
|
+
}
|
|
54
121
|
|
|
55
122
|
interface IPrivateLiveBrowserTab {
|
|
56
123
|
id: string;
|
|
@@ -67,6 +134,8 @@ interface IPrivateLiveBrowserTab {
|
|
|
67
134
|
stateUpdateQueued: boolean;
|
|
68
135
|
stateUpdatePending: boolean;
|
|
69
136
|
navigationResetPending: boolean;
|
|
137
|
+
evaluationExecutionContextId?: number;
|
|
138
|
+
securityCdpSession?: plugins.puppeteer.CDPSession;
|
|
70
139
|
cdpSession?: plugins.puppeteer.CDPSession;
|
|
71
140
|
cdpConnection?: plugins.puppeteer.Connection;
|
|
72
141
|
screencastFrameListener?: TScreencastFrameListener;
|
|
@@ -87,14 +156,34 @@ interface IImageDimensions {
|
|
|
87
156
|
height: number;
|
|
88
157
|
}
|
|
89
158
|
|
|
159
|
+
interface INormalizedEvaluationOptions {
|
|
160
|
+
timeoutMs: number;
|
|
161
|
+
maxOutputBytes: number;
|
|
162
|
+
maxDepth: number;
|
|
163
|
+
maxNodes: number;
|
|
164
|
+
maxStringBytes: number;
|
|
165
|
+
maxArrayLength: number;
|
|
166
|
+
maxObjectKeys: number;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface IEvaluationEnvelope {
|
|
170
|
+
ok: boolean;
|
|
171
|
+
json?: string;
|
|
172
|
+
error?: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
90
175
|
type TQueuedOperationKind = 'public' | 'internal' | 'shutdown';
|
|
176
|
+
type TQueuedOperationState = 'queued' | 'active' | 'settled';
|
|
91
177
|
|
|
92
178
|
interface IQueuedOperation {
|
|
93
179
|
kind: TQueuedOperationKind;
|
|
180
|
+
state: TQueuedOperationState;
|
|
94
181
|
controller: AbortController;
|
|
95
182
|
run: (signal: AbortSignal) => Promise<unknown>;
|
|
96
183
|
resolve: (value: unknown) => void;
|
|
97
184
|
reject: (error: unknown) => void;
|
|
185
|
+
capacityReleased: boolean;
|
|
186
|
+
removeCallerAbortListener?: () => void;
|
|
98
187
|
}
|
|
99
188
|
|
|
100
189
|
const validateBoundedString = (
|
|
@@ -163,6 +252,10 @@ const normalizeErrorMessage = (error: unknown): string => {
|
|
|
163
252
|
return truncate(String(error), 2048);
|
|
164
253
|
};
|
|
165
254
|
|
|
255
|
+
const normalizeAbortReason = (signal: AbortSignal): unknown => {
|
|
256
|
+
return signal.reason ?? new Error('The browser operation was aborted');
|
|
257
|
+
};
|
|
258
|
+
|
|
166
259
|
const readUint32 = (data: Uint8Array, offset: number): number => {
|
|
167
260
|
return (
|
|
168
261
|
data[offset]! * 0x1000000
|
|
@@ -279,6 +372,20 @@ export class LiveBrowserSession {
|
|
|
279
372
|
private browserContext?: plugins.puppeteer.BrowserContext;
|
|
280
373
|
private browserLifetimeController?: AbortController;
|
|
281
374
|
private browserDisconnectedListener?: () => void;
|
|
375
|
+
private browserTargetCreatedListener?: (target: plugins.puppeteer.Target) => void;
|
|
376
|
+
private browserSecurityCdpSession?: plugins.puppeteer.CDPSession;
|
|
377
|
+
private browserSecurityConnection?: plugins.puppeteer.Connection;
|
|
378
|
+
private browserSecuritySessionAttachedListener?: (session: plugins.puppeteer.CDPSession) => void;
|
|
379
|
+
private browserSecuritySessionDetachedListener?: TCdpSessionDetachedListener;
|
|
380
|
+
private readonly proxySecuritySessions = new Map<string, IProxySecuritySession>();
|
|
381
|
+
private readonly proxySecurityOperations = new Set<Promise<void>>();
|
|
382
|
+
private proxySecurityGeneration = 0;
|
|
383
|
+
private proxySecurityStopping = false;
|
|
384
|
+
private ownedBrowserProcess?: IOwnedBrowserProcess;
|
|
385
|
+
private processGeneration = 0;
|
|
386
|
+
private startRequestCount = 0;
|
|
387
|
+
private terminationSettled = true;
|
|
388
|
+
private terminationPromise?: Promise<ILiveBrowserTerminationResult>;
|
|
282
389
|
private readonly operationQueue: IQueuedOperation[] = [];
|
|
283
390
|
private activeOperation?: IQueuedOperation;
|
|
284
391
|
private operationRunning = false;
|
|
@@ -291,6 +398,7 @@ export class LiveBrowserSession {
|
|
|
291
398
|
private viewportRevision = 1;
|
|
292
399
|
private tabSequence = 0;
|
|
293
400
|
private frameSequence = 0;
|
|
401
|
+
private evaluationSequence = 0;
|
|
294
402
|
private normalStopRequested = false;
|
|
295
403
|
private lastError?: ILiveBrowserError;
|
|
296
404
|
|
|
@@ -310,6 +418,33 @@ export class LiveBrowserSession {
|
|
|
310
418
|
if (optionsArg.launchOptions && 'signal' in optionsArg.launchOptions) {
|
|
311
419
|
throw new Error('LiveBrowserSession owns launch cancellation; launchOptions.signal is unsupported');
|
|
312
420
|
}
|
|
421
|
+
validateOptionalBoolean(optionsArg.allowEvaluation, 'allowEvaluation');
|
|
422
|
+
for (const name of [
|
|
423
|
+
'denyDownloads',
|
|
424
|
+
'denyFileChoosers',
|
|
425
|
+
'denyPermissions',
|
|
426
|
+
'httpNavigationOnly',
|
|
427
|
+
] as const) {
|
|
428
|
+
validateOptionalBoolean(optionsArg.security?.[name], `security.${name}`);
|
|
429
|
+
}
|
|
430
|
+
const proxyCredentials = optionsArg.security?.proxyCredentials;
|
|
431
|
+
if (proxyCredentials !== undefined) {
|
|
432
|
+
if (!proxyCredentials || typeof proxyCredentials !== 'object') {
|
|
433
|
+
throw new Error('security.proxyCredentials must be an object');
|
|
434
|
+
}
|
|
435
|
+
validateBoundedString(
|
|
436
|
+
proxyCredentials.username,
|
|
437
|
+
'security.proxyCredentials.username',
|
|
438
|
+
1,
|
|
439
|
+
256,
|
|
440
|
+
);
|
|
441
|
+
validateBoundedString(
|
|
442
|
+
proxyCredentials.password,
|
|
443
|
+
'security.proxyCredentials.password',
|
|
444
|
+
1,
|
|
445
|
+
1024,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
313
448
|
const launchViewport = optionsArg.launchOptions?.defaultViewport;
|
|
314
449
|
const viewport = normalizeViewport(
|
|
315
450
|
optionsArg.viewport
|
|
@@ -327,9 +462,18 @@ export class LiveBrowserSession {
|
|
|
327
462
|
...optionsArg.launchOptions,
|
|
328
463
|
args: [...(optionsArg.launchOptions?.args ?? [])],
|
|
329
464
|
defaultViewport: createPuppeteerViewport(viewport),
|
|
465
|
+
...(optionsArg.security?.denyDownloads
|
|
466
|
+
? { downloadBehavior: { policy: 'deny' as const } }
|
|
467
|
+
: {}),
|
|
330
468
|
},
|
|
331
469
|
viewport,
|
|
332
470
|
screencast: optionsArg.screencast ? { ...optionsArg.screencast } : undefined,
|
|
471
|
+
security: optionsArg.security
|
|
472
|
+
? {
|
|
473
|
+
...optionsArg.security,
|
|
474
|
+
proxyCredentials: proxyCredentials ? { ...proxyCredentials } : undefined,
|
|
475
|
+
}
|
|
476
|
+
: undefined,
|
|
333
477
|
};
|
|
334
478
|
this.viewport = { ...viewport };
|
|
335
479
|
this.validateScreencastOptions();
|
|
@@ -356,12 +500,86 @@ export class LiveBrowserSession {
|
|
|
356
500
|
};
|
|
357
501
|
}
|
|
358
502
|
|
|
359
|
-
public
|
|
360
|
-
|
|
503
|
+
public getProcessState(): ILiveBrowserProcessState {
|
|
504
|
+
const ownedProcess = this.ownedBrowserProcess;
|
|
505
|
+
if (!ownedProcess) {
|
|
506
|
+
return {
|
|
507
|
+
generation: this.processGeneration,
|
|
508
|
+
pid: null,
|
|
509
|
+
processGroupId: null,
|
|
510
|
+
running: false,
|
|
511
|
+
exitCode: null,
|
|
512
|
+
signalCode: null,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
const childProcess = ownedProcess.childProcess;
|
|
516
|
+
return {
|
|
517
|
+
generation: ownedProcess.generation,
|
|
518
|
+
pid: childProcess.pid ?? null,
|
|
519
|
+
processGroupId: ownedProcess.identity?.processGroupId ?? null,
|
|
520
|
+
running: childProcess.exitCode === null && childProcess.signalCode === null,
|
|
521
|
+
exitCode: childProcess.exitCode,
|
|
522
|
+
signalCode: childProcess.signalCode,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
public terminate(
|
|
527
|
+
optionsArg: ILiveBrowserTerminationOptions = {},
|
|
528
|
+
): Promise<ILiveBrowserTerminationResult> {
|
|
529
|
+
if (this.terminationPromise) {
|
|
530
|
+
return this.terminationPromise;
|
|
531
|
+
}
|
|
532
|
+
const gracefulTimeoutMs = optionsArg.gracefulTimeoutMs === undefined
|
|
533
|
+
? 5000
|
|
534
|
+
: validateInteger(optionsArg.gracefulTimeoutMs, 'gracefulTimeoutMs', 1, 60000);
|
|
535
|
+
const forceTimeoutMs = optionsArg.forceTimeoutMs === undefined
|
|
536
|
+
? 5000
|
|
537
|
+
: validateInteger(optionsArg.forceTimeoutMs, 'forceTimeoutMs', 1, 60000);
|
|
538
|
+
let resolveTermination!: (result: ILiveBrowserTerminationResult) => void;
|
|
539
|
+
let rejectTermination!: (error: unknown) => void;
|
|
540
|
+
const terminationPromise = new Promise<ILiveBrowserTerminationResult>((resolve, reject) => {
|
|
541
|
+
resolveTermination = resolve;
|
|
542
|
+
rejectTermination = reject;
|
|
543
|
+
});
|
|
544
|
+
this.terminationPromise = terminationPromise;
|
|
545
|
+
this.terminationSettled = false;
|
|
546
|
+
void this.terminateInternal(gracefulTimeoutMs, forceTimeoutMs).then(
|
|
547
|
+
(result) => {
|
|
548
|
+
this.terminationSettled = true;
|
|
549
|
+
resolveTermination(result);
|
|
550
|
+
},
|
|
551
|
+
(error) => {
|
|
552
|
+
if (this.terminationPromise === terminationPromise) {
|
|
553
|
+
this.terminationPromise = undefined;
|
|
554
|
+
}
|
|
555
|
+
this.terminationSettled = true;
|
|
556
|
+
rejectTermination(error);
|
|
557
|
+
},
|
|
558
|
+
);
|
|
559
|
+
return terminationPromise;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
public start(operationOptions: ILiveBrowserOperationOptions = {}): Promise<void> {
|
|
563
|
+
if (!this.terminationSettled) {
|
|
564
|
+
return Promise.reject(new Error('A browser termination is still in progress'));
|
|
565
|
+
}
|
|
566
|
+
if (!this.ownedBrowserProcess && this.status === 'stopped') {
|
|
567
|
+
this.terminationPromise = undefined;
|
|
568
|
+
}
|
|
569
|
+
this.startRequestCount += 1;
|
|
570
|
+
const startPromise = this.enqueuePublicOperation(async (signal) => {
|
|
361
571
|
if (this.status === 'running' || this.status === 'starting') {
|
|
362
572
|
return;
|
|
363
573
|
}
|
|
364
574
|
|
|
575
|
+
await this.releaseExitedOwnedProcess();
|
|
576
|
+
if (signal.aborted) {
|
|
577
|
+
throw signal.reason;
|
|
578
|
+
}
|
|
579
|
+
if (this.ownedBrowserProcess) {
|
|
580
|
+
throw new Error('A previous owned Chromium process has not been confirmed dead');
|
|
581
|
+
}
|
|
582
|
+
|
|
365
583
|
this.normalStopRequested = false;
|
|
366
584
|
this.lastError = undefined;
|
|
367
585
|
this.viewportRevision = 1;
|
|
@@ -377,19 +595,77 @@ export class LiveBrowserSession {
|
|
|
377
595
|
if (browserLifetimeController.signal.aborted) {
|
|
378
596
|
throw browserLifetimeController.signal.reason;
|
|
379
597
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
598
|
+
const abortBrowserLaunch = (): void => {
|
|
599
|
+
if (!browserLifetimeController.signal.aborted) {
|
|
600
|
+
browserLifetimeController.abort(normalizeAbortReason(signal));
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
signal.addEventListener('abort', abortBrowserLaunch, { once: true });
|
|
604
|
+
try {
|
|
605
|
+
this.browser = await getEnvAwareBrowserInstance({
|
|
606
|
+
forceNoSandbox: this.options.forceNoSandbox,
|
|
607
|
+
requireSandbox: this.options.requireSandbox,
|
|
608
|
+
usePipe: this.options.usePipe,
|
|
609
|
+
launchOptions: {
|
|
610
|
+
...this.options.launchOptions,
|
|
611
|
+
protocol: 'cdp',
|
|
612
|
+
signal: browserLifetimeController.signal,
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
} finally {
|
|
616
|
+
signal.removeEventListener('abort', abortBrowserLaunch);
|
|
617
|
+
}
|
|
618
|
+
const childProcess = this.browser.process();
|
|
619
|
+
if (!childProcess?.pid) {
|
|
620
|
+
throw new Error('LiveBrowserSession did not receive an owned Chromium process');
|
|
621
|
+
}
|
|
622
|
+
const exitPromise = new Promise<void>((resolve) => {
|
|
623
|
+
if (childProcess.exitCode !== null || childProcess.signalCode !== null) {
|
|
624
|
+
resolve();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
childProcess.once('exit', () => resolve());
|
|
388
628
|
});
|
|
629
|
+
this.processGeneration += 1;
|
|
630
|
+
this.ownedBrowserProcess = {
|
|
631
|
+
generation: this.processGeneration,
|
|
632
|
+
childProcess,
|
|
633
|
+
exitPromise,
|
|
634
|
+
forceSignalled: false,
|
|
635
|
+
};
|
|
636
|
+
const identity = process.platform === 'linux'
|
|
637
|
+
? await readOwnedProcessIdentity(childProcess.pid)
|
|
638
|
+
: undefined;
|
|
639
|
+
if (process.platform === 'linux' && !identity) {
|
|
640
|
+
throw new Error('The owned Chromium process exited before its identity was captured');
|
|
641
|
+
}
|
|
642
|
+
if (
|
|
643
|
+
identity
|
|
644
|
+
&& (identity.processGroupId !== childProcess.pid || identity.sessionId !== childProcess.pid)
|
|
645
|
+
) {
|
|
646
|
+
throw new Error('Chromium was not launched as a dedicated process-group leader');
|
|
647
|
+
}
|
|
648
|
+
this.ownedBrowserProcess.identity = identity;
|
|
389
649
|
if (signal.aborted) {
|
|
390
650
|
throw signal.reason;
|
|
391
651
|
}
|
|
392
652
|
this.browserContext = this.browser.defaultBrowserContext();
|
|
653
|
+
this.browserTargetCreatedListener = (target) => {
|
|
654
|
+
if (target.type() !== 'page' || target.browserContext() !== this.browserContext) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
void target.page().then((page) => {
|
|
658
|
+
if (page) {
|
|
659
|
+
this.scheduleDiscoveredPageRegistration(page);
|
|
660
|
+
}
|
|
661
|
+
}).catch((error) => {
|
|
662
|
+
if (!this.normalStopRequested && this.status !== 'stopped' && this.status !== 'stopping') {
|
|
663
|
+
this.handleDiscoveredPageFailure(error);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
};
|
|
667
|
+
this.browser.on('targetcreated', this.browserTargetCreatedListener);
|
|
668
|
+
await this.configureBrowserSecurity(this.browser);
|
|
393
669
|
this.browserDisconnectedListener = () => {
|
|
394
670
|
if (this.normalStopRequested || this.status === 'stopped' || this.status === 'stopping') {
|
|
395
671
|
return;
|
|
@@ -432,9 +708,19 @@ export class LiveBrowserSession {
|
|
|
432
708
|
this.status = 'running';
|
|
433
709
|
this.emitState();
|
|
434
710
|
await this.startScreencast(firstTab);
|
|
711
|
+
if (signal.aborted) {
|
|
712
|
+
throw normalizeAbortReason(signal);
|
|
713
|
+
}
|
|
435
714
|
} catch (error) {
|
|
436
715
|
if (signal.aborted || this.normalStopRequested) {
|
|
437
|
-
|
|
716
|
+
try {
|
|
717
|
+
await this.stopInternal();
|
|
718
|
+
} catch (cleanupError) {
|
|
719
|
+
throw new AggregateError(
|
|
720
|
+
[error, cleanupError],
|
|
721
|
+
'Cancelled browser startup cleanup was incomplete',
|
|
722
|
+
);
|
|
723
|
+
}
|
|
438
724
|
throw error;
|
|
439
725
|
}
|
|
440
726
|
const startError: ILiveBrowserError = {
|
|
@@ -443,9 +729,19 @@ export class LiveBrowserSession {
|
|
|
443
729
|
fatal: true,
|
|
444
730
|
};
|
|
445
731
|
this.emitError(startError);
|
|
446
|
-
|
|
732
|
+
try {
|
|
733
|
+
await this.stopInternal(startError);
|
|
734
|
+
} catch (cleanupError) {
|
|
735
|
+
throw new AggregateError(
|
|
736
|
+
[error, cleanupError],
|
|
737
|
+
'Browser startup failed and cleanup was incomplete',
|
|
738
|
+
);
|
|
739
|
+
}
|
|
447
740
|
throw error;
|
|
448
741
|
}
|
|
742
|
+
}, operationOptions);
|
|
743
|
+
return startPromise.finally(() => {
|
|
744
|
+
this.startRequestCount -= 1;
|
|
449
745
|
});
|
|
450
746
|
}
|
|
451
747
|
|
|
@@ -508,6 +804,7 @@ export class LiveBrowserSession {
|
|
|
508
804
|
|
|
509
805
|
public async createTab(
|
|
510
806
|
optionsArg: ILiveBrowserCreateTabOptions = {},
|
|
807
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
511
808
|
): Promise<ILiveBrowserTabState> {
|
|
512
809
|
const url = optionsArg.url === undefined ? undefined : this.validateUrl(optionsArg.url);
|
|
513
810
|
const activate = optionsArg.activate ?? true;
|
|
@@ -563,16 +860,22 @@ export class LiveBrowserSession {
|
|
|
563
860
|
}
|
|
564
861
|
throw error;
|
|
565
862
|
}
|
|
566
|
-
});
|
|
863
|
+
}, operationOptions);
|
|
567
864
|
}
|
|
568
865
|
|
|
569
|
-
public async activateTab(
|
|
866
|
+
public async activateTab(
|
|
867
|
+
tabId: string,
|
|
868
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
869
|
+
): Promise<void> {
|
|
570
870
|
return this.enqueuePublicOperation(async () => {
|
|
571
871
|
await this.activateTabInternal(tabId);
|
|
572
|
-
});
|
|
872
|
+
}, operationOptions);
|
|
573
873
|
}
|
|
574
874
|
|
|
575
|
-
public async closeTab(
|
|
875
|
+
public async closeTab(
|
|
876
|
+
tabId: string,
|
|
877
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
878
|
+
): Promise<void> {
|
|
576
879
|
return this.enqueuePublicOperation(async () => {
|
|
577
880
|
const tab = this.requireTab(tabId);
|
|
578
881
|
const wasActive = this.activeTabId === tab.id;
|
|
@@ -609,10 +912,13 @@ export class LiveBrowserSession {
|
|
|
609
912
|
} else {
|
|
610
913
|
this.emitState();
|
|
611
914
|
}
|
|
612
|
-
});
|
|
915
|
+
}, operationOptions);
|
|
613
916
|
}
|
|
614
917
|
|
|
615
|
-
public async navigate(
|
|
918
|
+
public async navigate(
|
|
919
|
+
optionsArg: ILiveBrowserNavigateOptions,
|
|
920
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
921
|
+
): Promise<void> {
|
|
616
922
|
const url = this.validateUrl(optionsArg.url);
|
|
617
923
|
return this.enqueuePublicOperation(async (signal) => {
|
|
618
924
|
const tab = this.resolveNavigationTab(optionsArg.tabId);
|
|
@@ -623,10 +929,13 @@ export class LiveBrowserSession {
|
|
|
623
929
|
},
|
|
624
930
|
signal,
|
|
625
931
|
);
|
|
626
|
-
});
|
|
932
|
+
}, operationOptions);
|
|
627
933
|
}
|
|
628
934
|
|
|
629
|
-
public async back(
|
|
935
|
+
public async back(
|
|
936
|
+
optionsArg: ILiveBrowserNavigationOptions = {},
|
|
937
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
938
|
+
): Promise<void> {
|
|
630
939
|
return this.enqueuePublicOperation(async (signal) => {
|
|
631
940
|
const tab = this.resolveNavigationTab(optionsArg.tabId);
|
|
632
941
|
await this.navigateTab(
|
|
@@ -636,10 +945,13 @@ export class LiveBrowserSession {
|
|
|
636
945
|
},
|
|
637
946
|
signal,
|
|
638
947
|
);
|
|
639
|
-
});
|
|
948
|
+
}, operationOptions);
|
|
640
949
|
}
|
|
641
950
|
|
|
642
|
-
public async forward(
|
|
951
|
+
public async forward(
|
|
952
|
+
optionsArg: ILiveBrowserNavigationOptions = {},
|
|
953
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
954
|
+
): Promise<void> {
|
|
643
955
|
return this.enqueuePublicOperation(async (signal) => {
|
|
644
956
|
const tab = this.resolveNavigationTab(optionsArg.tabId);
|
|
645
957
|
await this.navigateTab(
|
|
@@ -649,10 +961,13 @@ export class LiveBrowserSession {
|
|
|
649
961
|
},
|
|
650
962
|
signal,
|
|
651
963
|
);
|
|
652
|
-
});
|
|
964
|
+
}, operationOptions);
|
|
653
965
|
}
|
|
654
966
|
|
|
655
|
-
public async reload(
|
|
967
|
+
public async reload(
|
|
968
|
+
optionsArg: ILiveBrowserNavigationOptions = {},
|
|
969
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
970
|
+
): Promise<void> {
|
|
656
971
|
return this.enqueuePublicOperation(async (signal) => {
|
|
657
972
|
const tab = this.resolveNavigationTab(optionsArg.tabId);
|
|
658
973
|
await this.navigateTab(
|
|
@@ -662,10 +977,13 @@ export class LiveBrowserSession {
|
|
|
662
977
|
},
|
|
663
978
|
signal,
|
|
664
979
|
);
|
|
665
|
-
});
|
|
980
|
+
}, operationOptions);
|
|
666
981
|
}
|
|
667
982
|
|
|
668
|
-
public async setViewport(
|
|
983
|
+
public async setViewport(
|
|
984
|
+
viewportArg: ILiveBrowserViewport,
|
|
985
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
986
|
+
): Promise<void> {
|
|
669
987
|
const viewport = normalizeViewport(viewportArg);
|
|
670
988
|
return this.enqueuePublicOperation(async (signal) => {
|
|
671
989
|
const tab = this.requireActiveTab();
|
|
@@ -684,7 +1002,7 @@ export class LiveBrowserSession {
|
|
|
684
1002
|
await this.startScreencast(tab);
|
|
685
1003
|
}
|
|
686
1004
|
}
|
|
687
|
-
});
|
|
1005
|
+
}, operationOptions);
|
|
688
1006
|
}
|
|
689
1007
|
|
|
690
1008
|
public async dispatchMouse(input: ILiveBrowserMouseInput): Promise<void> {
|
|
@@ -815,6 +1133,7 @@ export class LiveBrowserSession {
|
|
|
815
1133
|
|
|
816
1134
|
public async captureSnapshot(
|
|
817
1135
|
optionsArg: ILiveBrowserSnapshotOptions = {},
|
|
1136
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
818
1137
|
): Promise<ILiveBrowserSnapshot> {
|
|
819
1138
|
const format = optionsArg.format ?? 'jpeg';
|
|
820
1139
|
if (format !== 'jpeg' && format !== 'png') {
|
|
@@ -858,10 +1177,13 @@ export class LiveBrowserSession {
|
|
|
858
1177
|
...dimensions,
|
|
859
1178
|
data,
|
|
860
1179
|
};
|
|
861
|
-
});
|
|
1180
|
+
}, operationOptions);
|
|
862
1181
|
}
|
|
863
1182
|
|
|
864
|
-
public async observe(
|
|
1183
|
+
public async observe(
|
|
1184
|
+
optionsArg: ILiveBrowserObserveOptions = {},
|
|
1185
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
1186
|
+
): Promise<ILiveBrowserObservation> {
|
|
865
1187
|
const maxCharacters = optionsArg.maxCharacters === undefined
|
|
866
1188
|
? 12000
|
|
867
1189
|
: validateInteger(optionsArg.maxCharacters, 'maxCharacters', 256, 50000);
|
|
@@ -948,10 +1270,203 @@ export class LiveBrowserSession {
|
|
|
948
1270
|
text: truncatedText,
|
|
949
1271
|
truncated: reachedTraversalLimit || unboundedText.length > maxCharacters,
|
|
950
1272
|
};
|
|
951
|
-
});
|
|
1273
|
+
}, operationOptions);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
public async evaluate(
|
|
1277
|
+
expressionArg: string,
|
|
1278
|
+
optionsArg: ILiveBrowserEvaluateOptions = {},
|
|
1279
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
1280
|
+
): Promise<TLiveBrowserJsonValue> {
|
|
1281
|
+
if (!this.options.allowEvaluation) {
|
|
1282
|
+
throw new Error('LiveBrowserSession evaluation is disabled');
|
|
1283
|
+
}
|
|
1284
|
+
const expression = validateBoundedString(
|
|
1285
|
+
expressionArg,
|
|
1286
|
+
'expression',
|
|
1287
|
+
1,
|
|
1288
|
+
maxEvaluationScriptBytes,
|
|
1289
|
+
);
|
|
1290
|
+
const expressionBytes = new TextEncoder().encode(expression).byteLength;
|
|
1291
|
+
if (expressionBytes > maxEvaluationScriptBytes) {
|
|
1292
|
+
throw new Error(`expression must not exceed ${maxEvaluationScriptBytes} UTF-8 bytes`);
|
|
1293
|
+
}
|
|
1294
|
+
const evaluationOptions = this.normalizeEvaluationOptions(optionsArg);
|
|
1295
|
+
const evaluationId = ++this.evaluationSequence;
|
|
1296
|
+
const cancellationKey = `__smartpuppeteerCancel${evaluationId}`;
|
|
1297
|
+
const evaluationExpression = this.createEvaluationExpression(
|
|
1298
|
+
expression,
|
|
1299
|
+
evaluationOptions,
|
|
1300
|
+
cancellationKey,
|
|
1301
|
+
);
|
|
1302
|
+
|
|
1303
|
+
return this.enqueuePublicOperation(async (signal) => {
|
|
1304
|
+
const tab = this.resolveActionTab(optionsArg.tabId);
|
|
1305
|
+
const cdpSession = await tab.page.createCDPSession();
|
|
1306
|
+
this.allowOperationalCdpSessionDetach(cdpSession);
|
|
1307
|
+
let executionContextId: number | undefined;
|
|
1308
|
+
let cancellationPromise: Promise<void> | undefined;
|
|
1309
|
+
let terminationPromise: Promise<void> | undefined;
|
|
1310
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
1311
|
+
let evaluationTimedOut = false;
|
|
1312
|
+
const terminateEvaluation = (): void => {
|
|
1313
|
+
terminationPromise ??= cdpSession.send('Runtime.terminateExecution').then(() => undefined);
|
|
1314
|
+
};
|
|
1315
|
+
const cancelEvaluation = (): void => {
|
|
1316
|
+
if (executionContextId === undefined) {
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
cancellationPromise ??= cdpSession.send('Runtime.evaluate', {
|
|
1320
|
+
expression: `globalThis[${JSON.stringify(evaluationCancelKey)}]?.(${JSON.stringify(cancellationKey)})`,
|
|
1321
|
+
contextId: executionContextId,
|
|
1322
|
+
returnByValue: true,
|
|
1323
|
+
awaitPromise: false,
|
|
1324
|
+
includeCommandLineAPI: false,
|
|
1325
|
+
userGesture: false,
|
|
1326
|
+
disableBreaks: true,
|
|
1327
|
+
}).then(() => undefined);
|
|
1328
|
+
};
|
|
1329
|
+
signal.addEventListener('abort', cancelEvaluation, { once: true });
|
|
1330
|
+
let evaluationError: unknown;
|
|
1331
|
+
let evaluationFailed = false;
|
|
1332
|
+
try {
|
|
1333
|
+
if (signal.aborted) {
|
|
1334
|
+
throw normalizeAbortReason(signal);
|
|
1335
|
+
}
|
|
1336
|
+
executionContextId = tab.evaluationExecutionContextId;
|
|
1337
|
+
if (executionContextId === undefined) {
|
|
1338
|
+
const frameTreeResponse = await cdpSession.send('Page.getFrameTree');
|
|
1339
|
+
if (signal.aborted) {
|
|
1340
|
+
throw normalizeAbortReason(signal);
|
|
1341
|
+
}
|
|
1342
|
+
const isolatedWorld = await cdpSession.send('Page.createIsolatedWorld', {
|
|
1343
|
+
frameId: frameTreeResponse.frameTree.frame.id,
|
|
1344
|
+
worldName: 'smartpuppeteer-evaluation',
|
|
1345
|
+
grantUniveralAccess: false,
|
|
1346
|
+
});
|
|
1347
|
+
executionContextId = isolatedWorld.executionContextId;
|
|
1348
|
+
tab.evaluationExecutionContextId = executionContextId;
|
|
1349
|
+
}
|
|
1350
|
+
if (signal.aborted) {
|
|
1351
|
+
throw normalizeAbortReason(signal);
|
|
1352
|
+
}
|
|
1353
|
+
const bootstrapResponse = await cdpSession.send('Runtime.evaluate', {
|
|
1354
|
+
expression: this.createEvaluationBootstrapExpression(),
|
|
1355
|
+
contextId: executionContextId,
|
|
1356
|
+
returnByValue: true,
|
|
1357
|
+
awaitPromise: true,
|
|
1358
|
+
includeCommandLineAPI: false,
|
|
1359
|
+
userGesture: false,
|
|
1360
|
+
disableBreaks: true,
|
|
1361
|
+
});
|
|
1362
|
+
if (bootstrapResponse.exceptionDetails) {
|
|
1363
|
+
throw new Error(this.readCdpExceptionMessage(bootstrapResponse.exceptionDetails));
|
|
1364
|
+
}
|
|
1365
|
+
if (signal.aborted) {
|
|
1366
|
+
throw normalizeAbortReason(signal);
|
|
1367
|
+
}
|
|
1368
|
+
timeoutHandle = setTimeout(() => {
|
|
1369
|
+
evaluationTimedOut = true;
|
|
1370
|
+
terminateEvaluation();
|
|
1371
|
+
}, evaluationOptions.timeoutMs + 250);
|
|
1372
|
+
const evaluationResponse = await cdpSession.send('Runtime.evaluate', {
|
|
1373
|
+
expression: evaluationExpression,
|
|
1374
|
+
contextId: executionContextId,
|
|
1375
|
+
returnByValue: true,
|
|
1376
|
+
awaitPromise: true,
|
|
1377
|
+
timeout: evaluationOptions.timeoutMs + 250,
|
|
1378
|
+
includeCommandLineAPI: false,
|
|
1379
|
+
userGesture: false,
|
|
1380
|
+
disableBreaks: true,
|
|
1381
|
+
allowUnsafeEvalBlockedByCSP: true,
|
|
1382
|
+
});
|
|
1383
|
+
if (evaluationTimedOut) {
|
|
1384
|
+
throw new Error(`Evaluation timed out after ${evaluationOptions.timeoutMs}ms`);
|
|
1385
|
+
}
|
|
1386
|
+
if (signal.aborted) {
|
|
1387
|
+
throw normalizeAbortReason(signal);
|
|
1388
|
+
}
|
|
1389
|
+
if (evaluationResponse.exceptionDetails) {
|
|
1390
|
+
throw new Error(this.readCdpExceptionMessage(evaluationResponse.exceptionDetails));
|
|
1391
|
+
}
|
|
1392
|
+
const envelope = evaluationResponse.result.value as IEvaluationEnvelope | undefined;
|
|
1393
|
+
if (!envelope || typeof envelope !== 'object' || typeof envelope.ok !== 'boolean') {
|
|
1394
|
+
throw new Error('Evaluation returned an invalid result envelope');
|
|
1395
|
+
}
|
|
1396
|
+
if (!envelope.ok) {
|
|
1397
|
+
throw new Error(
|
|
1398
|
+
typeof envelope.error === 'string'
|
|
1399
|
+
? truncate(envelope.error, 2048)
|
|
1400
|
+
: 'Evaluation failed',
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
if (typeof envelope.json !== 'string') {
|
|
1404
|
+
throw new Error('Evaluation returned an invalid JSON result');
|
|
1405
|
+
}
|
|
1406
|
+
if (new TextEncoder().encode(envelope.json).byteLength > evaluationOptions.maxOutputBytes) {
|
|
1407
|
+
throw new Error('Evaluation result exceeded maxOutputBytes during transfer');
|
|
1408
|
+
}
|
|
1409
|
+
return JSON.parse(envelope.json) as TLiveBrowserJsonValue;
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
evaluationError = error;
|
|
1412
|
+
evaluationFailed = true;
|
|
1413
|
+
throw error;
|
|
1414
|
+
} finally {
|
|
1415
|
+
signal.removeEventListener('abort', cancelEvaluation);
|
|
1416
|
+
if (timeoutHandle) {
|
|
1417
|
+
clearTimeout(timeoutHandle);
|
|
1418
|
+
}
|
|
1419
|
+
const cleanupErrors: unknown[] = [];
|
|
1420
|
+
if (cancellationPromise) {
|
|
1421
|
+
try {
|
|
1422
|
+
await cancellationPromise;
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
cleanupErrors.push(error);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
if (terminationPromise) {
|
|
1428
|
+
try {
|
|
1429
|
+
await terminationPromise;
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
cleanupErrors.push(error);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
if (executionContextId !== undefined && !cdpSession.detached) {
|
|
1435
|
+
try {
|
|
1436
|
+
await cdpSession.send('Runtime.evaluate', {
|
|
1437
|
+
expression: `globalThis[${JSON.stringify(evaluationCleanupKey)}]?.(${JSON.stringify(cancellationKey)})`,
|
|
1438
|
+
contextId: executionContextId,
|
|
1439
|
+
returnByValue: true,
|
|
1440
|
+
awaitPromise: false,
|
|
1441
|
+
includeCommandLineAPI: false,
|
|
1442
|
+
userGesture: false,
|
|
1443
|
+
disableBreaks: true,
|
|
1444
|
+
});
|
|
1445
|
+
} catch {
|
|
1446
|
+
// Navigation destroys the old execution context and its cancellation registry.
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
if (!cdpSession.detached) {
|
|
1450
|
+
try {
|
|
1451
|
+
await cdpSession.detach();
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
cleanupErrors.push(error);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
if (cleanupErrors.length > 0) {
|
|
1457
|
+
throw new AggregateError(
|
|
1458
|
+
evaluationFailed ? [evaluationError, ...cleanupErrors] : cleanupErrors,
|
|
1459
|
+
'Evaluation cleanup was incomplete',
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}, operationOptions);
|
|
952
1464
|
}
|
|
953
1465
|
|
|
954
|
-
public async click(
|
|
1466
|
+
public async click(
|
|
1467
|
+
optionsArg: ILiveBrowserClickOptions,
|
|
1468
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
1469
|
+
): Promise<void> {
|
|
955
1470
|
const selector = validateBoundedString(
|
|
956
1471
|
optionsArg.selector,
|
|
957
1472
|
'selector',
|
|
@@ -977,10 +1492,13 @@ export class LiveBrowserSession {
|
|
|
977
1492
|
});
|
|
978
1493
|
await this.refreshTab(actionTab);
|
|
979
1494
|
this.emitState();
|
|
980
|
-
});
|
|
1495
|
+
}, operationOptions);
|
|
981
1496
|
}
|
|
982
1497
|
|
|
983
|
-
public async fill(
|
|
1498
|
+
public async fill(
|
|
1499
|
+
optionsArg: ILiveBrowserFillOptions,
|
|
1500
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
1501
|
+
): Promise<void> {
|
|
984
1502
|
const selector = validateBoundedString(
|
|
985
1503
|
optionsArg.selector,
|
|
986
1504
|
'selector',
|
|
@@ -996,10 +1514,13 @@ export class LiveBrowserSession {
|
|
|
996
1514
|
await actionTab.page.locator(selector).setTimeout(timeout).fill(text, { signal });
|
|
997
1515
|
await this.refreshTab(actionTab);
|
|
998
1516
|
this.emitState();
|
|
999
|
-
});
|
|
1517
|
+
}, operationOptions);
|
|
1000
1518
|
}
|
|
1001
1519
|
|
|
1002
|
-
public async press(
|
|
1520
|
+
public async press(
|
|
1521
|
+
optionsArg: ILiveBrowserPressOptions,
|
|
1522
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
1523
|
+
): Promise<void> {
|
|
1003
1524
|
const selector = validateBoundedString(
|
|
1004
1525
|
optionsArg.selector,
|
|
1005
1526
|
'selector',
|
|
@@ -1027,12 +1548,27 @@ export class LiveBrowserSession {
|
|
|
1027
1548
|
}
|
|
1028
1549
|
await this.refreshTab(actionTab);
|
|
1029
1550
|
this.emitState();
|
|
1030
|
-
});
|
|
1551
|
+
}, operationOptions);
|
|
1031
1552
|
}
|
|
1032
1553
|
|
|
1033
1554
|
private enqueuePublicOperation<T>(
|
|
1034
1555
|
operation: (signal: AbortSignal) => Promise<T>,
|
|
1556
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
1035
1557
|
): Promise<T> {
|
|
1558
|
+
const callerSignal = operationOptions.signal;
|
|
1559
|
+
if (
|
|
1560
|
+
callerSignal !== undefined
|
|
1561
|
+
&& (
|
|
1562
|
+
typeof callerSignal !== 'object'
|
|
1563
|
+
|| typeof callerSignal.addEventListener !== 'function'
|
|
1564
|
+
|| typeof callerSignal.removeEventListener !== 'function'
|
|
1565
|
+
)
|
|
1566
|
+
) {
|
|
1567
|
+
return Promise.reject(new Error('operationOptions.signal must be an AbortSignal'));
|
|
1568
|
+
}
|
|
1569
|
+
if (callerSignal?.aborted) {
|
|
1570
|
+
return Promise.reject(normalizeAbortReason(callerSignal));
|
|
1571
|
+
}
|
|
1036
1572
|
if (
|
|
1037
1573
|
this.status === 'stopping'
|
|
1038
1574
|
|| this.normalStopRequested
|
|
@@ -1044,7 +1580,25 @@ export class LiveBrowserSession {
|
|
|
1044
1580
|
return Promise.reject(new Error('LiveBrowserSession operation queue is full'));
|
|
1045
1581
|
}
|
|
1046
1582
|
this.admittedPublicOperations += 1;
|
|
1047
|
-
return this.enqueueQueuedOperation('public',
|
|
1583
|
+
return this.enqueueQueuedOperation('public', async (signal) => {
|
|
1584
|
+
this.assertProxySecurityReady();
|
|
1585
|
+
return operation(signal);
|
|
1586
|
+
}, false, callerSignal);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
private assertProxySecurityReady(): void {
|
|
1590
|
+
if (!this.options.security?.proxyCredentials || this.status !== 'running') {
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (
|
|
1594
|
+
!this.browserSecurityConnection
|
|
1595
|
+
|| !this.browserSecurityCdpSession
|
|
1596
|
+
|| this.browserSecurityCdpSession.detached
|
|
1597
|
+
) {
|
|
1598
|
+
const error = new Error('Authenticated proxy security is not attached');
|
|
1599
|
+
this.handleProxySecurityFailure('proxy_security_unavailable', error);
|
|
1600
|
+
throw error;
|
|
1601
|
+
}
|
|
1048
1602
|
}
|
|
1049
1603
|
|
|
1050
1604
|
private enqueueInternalOperation(
|
|
@@ -1062,15 +1616,45 @@ export class LiveBrowserSession {
|
|
|
1062
1616
|
kind: TQueuedOperationKind,
|
|
1063
1617
|
operation: (signal: AbortSignal) => Promise<T>,
|
|
1064
1618
|
priority = false,
|
|
1619
|
+
callerSignal?: AbortSignal,
|
|
1065
1620
|
): Promise<T> {
|
|
1066
1621
|
return new Promise<T>((resolve, reject) => {
|
|
1067
1622
|
const queuedOperation: IQueuedOperation = {
|
|
1068
1623
|
kind,
|
|
1624
|
+
state: 'queued',
|
|
1069
1625
|
controller: new AbortController(),
|
|
1070
1626
|
run: operation,
|
|
1071
1627
|
resolve: (value) => resolve(value as T),
|
|
1072
1628
|
reject,
|
|
1629
|
+
capacityReleased: false,
|
|
1073
1630
|
};
|
|
1631
|
+
if (callerSignal) {
|
|
1632
|
+
const onCallerAbort = (): void => {
|
|
1633
|
+
const abortReason = normalizeAbortReason(callerSignal);
|
|
1634
|
+
if (queuedOperation.state === 'queued') {
|
|
1635
|
+
const operationIndex = this.operationQueue.indexOf(queuedOperation);
|
|
1636
|
+
if (operationIndex >= 0) {
|
|
1637
|
+
this.operationQueue.splice(operationIndex, 1);
|
|
1638
|
+
}
|
|
1639
|
+
queuedOperation.controller.abort(abortReason);
|
|
1640
|
+
queuedOperation.reject(abortReason);
|
|
1641
|
+
this.finalizeQueuedOperation(queuedOperation);
|
|
1642
|
+
this.drainOperationQueue();
|
|
1643
|
+
} else if (queuedOperation.state === 'active') {
|
|
1644
|
+
queuedOperation.controller.abort(abortReason);
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
callerSignal.addEventListener('abort', onCallerAbort, { once: true });
|
|
1648
|
+
queuedOperation.removeCallerAbortListener = () => {
|
|
1649
|
+
callerSignal.removeEventListener('abort', onCallerAbort);
|
|
1650
|
+
};
|
|
1651
|
+
if (callerSignal.aborted) {
|
|
1652
|
+
onCallerAbort();
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
if (queuedOperation.state !== 'queued') {
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1074
1658
|
if (priority) {
|
|
1075
1659
|
this.operationQueue.unshift(queuedOperation);
|
|
1076
1660
|
} else {
|
|
@@ -1090,6 +1674,7 @@ export class LiveBrowserSession {
|
|
|
1090
1674
|
}
|
|
1091
1675
|
this.operationRunning = true;
|
|
1092
1676
|
this.activeOperation = queuedOperation;
|
|
1677
|
+
queuedOperation.state = 'active';
|
|
1093
1678
|
void this.executeQueuedOperation(queuedOperation).catch((error) => {
|
|
1094
1679
|
this.operationRunning = false;
|
|
1095
1680
|
this.activeOperation = undefined;
|
|
@@ -1107,15 +1692,15 @@ export class LiveBrowserSession {
|
|
|
1107
1692
|
if (queuedOperation.controller.signal.aborted) {
|
|
1108
1693
|
throw queuedOperation.controller.signal.reason;
|
|
1109
1694
|
}
|
|
1110
|
-
|
|
1695
|
+
const value = await queuedOperation.run(queuedOperation.controller.signal);
|
|
1696
|
+
if (queuedOperation.controller.signal.aborted) {
|
|
1697
|
+
throw queuedOperation.controller.signal.reason;
|
|
1698
|
+
}
|
|
1699
|
+
queuedOperation.resolve(value);
|
|
1111
1700
|
} catch (error) {
|
|
1112
1701
|
queuedOperation.reject(error);
|
|
1113
1702
|
} finally {
|
|
1114
|
-
|
|
1115
|
-
this.admittedPublicOperations -= 1;
|
|
1116
|
-
} else if (queuedOperation.kind === 'internal') {
|
|
1117
|
-
this.admittedInternalOperations -= 1;
|
|
1118
|
-
}
|
|
1703
|
+
this.finalizeQueuedOperation(queuedOperation);
|
|
1119
1704
|
if (this.activeOperation === queuedOperation) {
|
|
1120
1705
|
this.activeOperation = undefined;
|
|
1121
1706
|
}
|
|
@@ -1130,15 +1715,29 @@ export class LiveBrowserSession {
|
|
|
1130
1715
|
}
|
|
1131
1716
|
}
|
|
1132
1717
|
|
|
1718
|
+
private finalizeQueuedOperation(queuedOperation: IQueuedOperation): void {
|
|
1719
|
+
if (queuedOperation.state === 'settled') {
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
queuedOperation.state = 'settled';
|
|
1723
|
+
queuedOperation.removeCallerAbortListener?.();
|
|
1724
|
+
queuedOperation.removeCallerAbortListener = undefined;
|
|
1725
|
+
if (queuedOperation.capacityReleased) {
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
queuedOperation.capacityReleased = true;
|
|
1729
|
+
if (queuedOperation.kind === 'public') {
|
|
1730
|
+
this.admittedPublicOperations -= 1;
|
|
1731
|
+
} else if (queuedOperation.kind === 'internal') {
|
|
1732
|
+
this.admittedInternalOperations -= 1;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1133
1736
|
private cancelQueuedOperations(error: Error): void {
|
|
1134
1737
|
for (const queuedOperation of this.operationQueue.splice(0)) {
|
|
1135
1738
|
queuedOperation.controller.abort(error);
|
|
1136
|
-
if (queuedOperation.kind === 'public') {
|
|
1137
|
-
this.admittedPublicOperations -= 1;
|
|
1138
|
-
} else if (queuedOperation.kind === 'internal') {
|
|
1139
|
-
this.admittedInternalOperations -= 1;
|
|
1140
|
-
}
|
|
1141
1739
|
queuedOperation.reject(error);
|
|
1740
|
+
this.finalizeQueuedOperation(queuedOperation);
|
|
1142
1741
|
}
|
|
1143
1742
|
}
|
|
1144
1743
|
|
|
@@ -1162,7 +1761,8 @@ export class LiveBrowserSession {
|
|
|
1162
1761
|
}
|
|
1163
1762
|
|
|
1164
1763
|
private requestShutdown(error?: ILiveBrowserError): Promise<void> {
|
|
1165
|
-
if (this.status === 'stopped') {
|
|
1764
|
+
if (this.status === 'stopped' && !this.operationRunning && this.startRequestCount === 0) {
|
|
1765
|
+
this.normalStopRequested = false;
|
|
1166
1766
|
return Promise.resolve();
|
|
1167
1767
|
}
|
|
1168
1768
|
if (this.shutdownPromise) {
|
|
@@ -1275,57 +1875,325 @@ export class LiveBrowserSession {
|
|
|
1275
1875
|
};
|
|
1276
1876
|
}
|
|
1277
1877
|
|
|
1278
|
-
private async
|
|
1279
|
-
|
|
1878
|
+
private async releaseExitedOwnedProcess(): Promise<void> {
|
|
1879
|
+
const ownedProcess = this.ownedBrowserProcess;
|
|
1880
|
+
if (!ownedProcess) {
|
|
1280
1881
|
return;
|
|
1281
1882
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1883
|
+
if (
|
|
1884
|
+
ownedProcess.childProcess.exitCode === null
|
|
1885
|
+
&& ownedProcess.childProcess.signalCode === null
|
|
1886
|
+
) {
|
|
1887
|
+
await Promise.race([ownedProcess.exitPromise, delay(100)]);
|
|
1888
|
+
if (
|
|
1889
|
+
ownedProcess.childProcess.exitCode === null
|
|
1890
|
+
&& ownedProcess.childProcess.signalCode === null
|
|
1891
|
+
) {
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1285
1894
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
this.browserLifetimeController.abort(new Error('LiveBrowserSession is stopping'));
|
|
1895
|
+
if (process.platform === 'linux' && !ownedProcess.identity) {
|
|
1896
|
+
return;
|
|
1289
1897
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1898
|
+
if (ownedProcess.identity) {
|
|
1899
|
+
const members = await listOwnedProcessGroupMembers(ownedProcess.identity);
|
|
1900
|
+
if (members.length > 0) {
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1294
1903
|
}
|
|
1295
|
-
this.
|
|
1904
|
+
this.ownedBrowserProcess = undefined;
|
|
1905
|
+
}
|
|
1296
1906
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1907
|
+
private async terminateInternal(
|
|
1908
|
+
gracefulTimeoutMs: number,
|
|
1909
|
+
forceTimeoutMs: number,
|
|
1910
|
+
): Promise<ILiveBrowserTerminationResult> {
|
|
1911
|
+
const errors: string[] = [];
|
|
1912
|
+
let shutdownSettled = this.status === 'stopped';
|
|
1913
|
+
const shutdownObserved = this.stop().then(
|
|
1914
|
+
() => {
|
|
1915
|
+
shutdownSettled = true;
|
|
1916
|
+
},
|
|
1917
|
+
(error) => {
|
|
1918
|
+
errors.push(normalizeErrorMessage(error));
|
|
1919
|
+
shutdownSettled = true;
|
|
1920
|
+
},
|
|
1921
|
+
);
|
|
1922
|
+
const createConfirmedResult = (
|
|
1923
|
+
ownedProcess: IOwnedBrowserProcess | undefined,
|
|
1924
|
+
forced: boolean,
|
|
1925
|
+
): ILiveBrowserTerminationResult => {
|
|
1926
|
+
const childProcess = ownedProcess?.childProcess;
|
|
1927
|
+
const result: ILiveBrowserTerminationResult = {
|
|
1928
|
+
generation: ownedProcess?.generation ?? this.processGeneration,
|
|
1929
|
+
pid: childProcess?.pid ?? null,
|
|
1930
|
+
processGroupId: ownedProcess?.identity?.processGroupId ?? null,
|
|
1931
|
+
running: false,
|
|
1932
|
+
exitCode: childProcess?.exitCode ?? null,
|
|
1933
|
+
signalCode: childProcess?.signalCode ?? null,
|
|
1934
|
+
forced: forced || Boolean(ownedProcess?.forceSignalled),
|
|
1935
|
+
shutdownComplete: true,
|
|
1936
|
+
confirmedDead: true,
|
|
1937
|
+
errors,
|
|
1938
|
+
};
|
|
1939
|
+
if (this.ownedBrowserProcess === ownedProcess) {
|
|
1940
|
+
this.ownedBrowserProcess = undefined;
|
|
1304
1941
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1942
|
+
return result;
|
|
1943
|
+
};
|
|
1944
|
+
const isConfirmedDead = async (
|
|
1945
|
+
ownedProcess: IOwnedBrowserProcess | undefined,
|
|
1946
|
+
): Promise<boolean> => {
|
|
1947
|
+
if (!ownedProcess) {
|
|
1948
|
+
return true;
|
|
1309
1949
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1950
|
+
if (process.platform !== 'linux' || !ownedProcess.identity) {
|
|
1951
|
+
if (process.platform === 'linux' && this.startRequestCount > 0) {
|
|
1952
|
+
return false;
|
|
1953
|
+
}
|
|
1954
|
+
throw new Error('Confirmed browser process-group termination is supported on Linux only');
|
|
1955
|
+
}
|
|
1956
|
+
if (
|
|
1957
|
+
ownedProcess.childProcess.exitCode === null
|
|
1958
|
+
&& ownedProcess.childProcess.signalCode === null
|
|
1959
|
+
) {
|
|
1960
|
+
return false;
|
|
1961
|
+
}
|
|
1962
|
+
const members = await listOwnedProcessGroupMembers(ownedProcess.identity);
|
|
1963
|
+
if (members.length > 0) {
|
|
1964
|
+
return false;
|
|
1965
|
+
}
|
|
1966
|
+
return true;
|
|
1967
|
+
};
|
|
1316
1968
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1969
|
+
const gracefulDeadline = Date.now() + gracefulTimeoutMs;
|
|
1970
|
+
while (Date.now() < gracefulDeadline) {
|
|
1971
|
+
const ownedProcess = this.ownedBrowserProcess;
|
|
1972
|
+
if (
|
|
1973
|
+
await isConfirmedDead(ownedProcess)
|
|
1974
|
+
&& shutdownSettled
|
|
1975
|
+
&& this.status === 'stopped'
|
|
1976
|
+
) {
|
|
1977
|
+
return createConfirmedResult(ownedProcess, false);
|
|
1978
|
+
}
|
|
1979
|
+
if (shutdownSettled) {
|
|
1980
|
+
await delay(100);
|
|
1981
|
+
} else {
|
|
1982
|
+
await Promise.race([shutdownObserved, delay(100)]);
|
|
1322
1983
|
}
|
|
1323
1984
|
}
|
|
1324
1985
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1986
|
+
const ownedProcess = this.ownedBrowserProcess;
|
|
1987
|
+
let forced = false;
|
|
1988
|
+
if (ownedProcess) {
|
|
1989
|
+
if (process.platform !== 'linux') {
|
|
1990
|
+
throw new Error('Confirmed browser process-group termination is supported on Linux only');
|
|
1991
|
+
}
|
|
1992
|
+
if (ownedProcess.identity) {
|
|
1993
|
+
const membersBeforeFreeze = await listOwnedProcessGroupMembers(ownedProcess.identity);
|
|
1994
|
+
if (membersBeforeFreeze.length > 0) {
|
|
1995
|
+
forced = true;
|
|
1996
|
+
const groupStopped = await signalOwnedProcessGroup(ownedProcess.identity, 'SIGSTOP');
|
|
1997
|
+
if (groupStopped) {
|
|
1998
|
+
killFrozenOwnedProcessGroup(ownedProcess.identity);
|
|
1999
|
+
} else {
|
|
2000
|
+
await this.killOwnedProcessGroupSurvivors(ownedProcess.identity, membersBeforeFreeze);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
} else if (this.startRequestCount === 0) {
|
|
2004
|
+
throw new Error('Owned Chromium process identity was never confirmed');
|
|
2005
|
+
}
|
|
1327
2006
|
}
|
|
1328
|
-
|
|
2007
|
+
|
|
2008
|
+
const forceDeadline = Date.now() + forceTimeoutMs;
|
|
2009
|
+
while (Date.now() < forceDeadline) {
|
|
2010
|
+
const currentOwnedProcess = this.ownedBrowserProcess;
|
|
2011
|
+
if (
|
|
2012
|
+
await isConfirmedDead(currentOwnedProcess)
|
|
2013
|
+
&& shutdownSettled
|
|
2014
|
+
&& this.status === 'stopped'
|
|
2015
|
+
) {
|
|
2016
|
+
return createConfirmedResult(currentOwnedProcess, forced);
|
|
2017
|
+
}
|
|
2018
|
+
if (shutdownSettled) {
|
|
2019
|
+
await delay(100);
|
|
2020
|
+
} else {
|
|
2021
|
+
await Promise.race([shutdownObserved, delay(100)]);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
const remainingOwnedProcess = this.ownedBrowserProcess;
|
|
2026
|
+
if (remainingOwnedProcess?.identity) {
|
|
2027
|
+
const survivors = await listOwnedProcessGroupMembers(remainingOwnedProcess.identity);
|
|
2028
|
+
if (survivors.length > 0) {
|
|
2029
|
+
throw new Error(
|
|
2030
|
+
`Owned Chromium process group did not terminate: ${survivors.map((item) => item.pid).join(', ')}`,
|
|
2031
|
+
);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
if (!shutdownSettled || this.status !== 'stopped') {
|
|
2035
|
+
throw new Error(
|
|
2036
|
+
'Owned Chromium process group exited but LiveBrowserSession shutdown did not settle',
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
throw new Error('Owned Chromium process exit was not confirmed by Node.js');
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
private async killOwnedProcessGroupSurvivors(
|
|
2043
|
+
rootIdentity: IOwnedProcessIdentity,
|
|
2044
|
+
initialMembers: IOwnedProcessIdentity[],
|
|
2045
|
+
): Promise<void> {
|
|
2046
|
+
let previousKeys = new Set(initialMembers.map((member) => `${member.pid}:${member.startTime}`));
|
|
2047
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
2048
|
+
const members = await listOwnedProcessGroupMembers(rootIdentity);
|
|
2049
|
+
if (members.length === 0) {
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
for (const member of members) {
|
|
2053
|
+
await signalOwnedProcessIdentity(member, 'SIGSTOP');
|
|
2054
|
+
}
|
|
2055
|
+
await delay(10);
|
|
2056
|
+
const frozenMembers = await listOwnedProcessGroupMembers(rootIdentity);
|
|
2057
|
+
const currentKeys = new Set(
|
|
2058
|
+
frozenMembers.map((member) => `${member.pid}:${member.startTime}`),
|
|
2059
|
+
);
|
|
2060
|
+
const sameMembers = currentKeys.size === previousKeys.size
|
|
2061
|
+
&& [...currentKeys].every((key) => previousKeys.has(key));
|
|
2062
|
+
const allFrozen = frozenMembers.every((member) => (
|
|
2063
|
+
member.state === 'T' || member.state === 't' || member.state === 'Z'
|
|
2064
|
+
));
|
|
2065
|
+
if (sameMembers && allFrozen) {
|
|
2066
|
+
for (const member of frozenMembers) {
|
|
2067
|
+
await signalOwnedProcessIdentity(member, 'SIGKILL');
|
|
2068
|
+
}
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
previousKeys = currentKeys;
|
|
2072
|
+
}
|
|
2073
|
+
throw new Error('Unable to stabilize the surviving Chromium process group');
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
private async forceOwnedBrowserProcessGroup(
|
|
2077
|
+
ownedProcess: IOwnedBrowserProcess,
|
|
2078
|
+
): Promise<void> {
|
|
2079
|
+
ownedProcess.forceSignalled = true;
|
|
2080
|
+
if (process.platform !== 'linux') {
|
|
2081
|
+
ownedProcess.childProcess.kill('SIGKILL');
|
|
2082
|
+
await Promise.race([ownedProcess.exitPromise, delay(5000)]);
|
|
2083
|
+
if (
|
|
2084
|
+
ownedProcess.childProcess.exitCode === null
|
|
2085
|
+
&& ownedProcess.childProcess.signalCode === null
|
|
2086
|
+
) {
|
|
2087
|
+
throw new Error('Owned Chromium process did not exit after SIGKILL');
|
|
2088
|
+
}
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
if (!ownedProcess.identity) {
|
|
2092
|
+
throw new Error('Owned Chromium process identity was never confirmed');
|
|
2093
|
+
}
|
|
2094
|
+
const members = await listOwnedProcessGroupMembers(ownedProcess.identity);
|
|
2095
|
+
if (members.length > 0) {
|
|
2096
|
+
const groupStopped = await signalOwnedProcessGroup(ownedProcess.identity, 'SIGSTOP');
|
|
2097
|
+
if (groupStopped) {
|
|
2098
|
+
killFrozenOwnedProcessGroup(ownedProcess.identity);
|
|
2099
|
+
} else {
|
|
2100
|
+
await this.killOwnedProcessGroupSurvivors(ownedProcess.identity, members);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
const deadline = Date.now() + 5000;
|
|
2104
|
+
while (Date.now() < deadline) {
|
|
2105
|
+
const survivors = await listOwnedProcessGroupMembers(ownedProcess.identity);
|
|
2106
|
+
if (survivors.length === 0) {
|
|
2107
|
+
await Promise.race([ownedProcess.exitPromise, delay(100)]);
|
|
2108
|
+
if (
|
|
2109
|
+
ownedProcess.childProcess.exitCode !== null
|
|
2110
|
+
|| ownedProcess.childProcess.signalCode !== null
|
|
2111
|
+
) {
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
await delay(100);
|
|
2116
|
+
}
|
|
2117
|
+
throw new Error('Owned Chromium process group survived forced startup cleanup');
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
private async stopInternal(error?: ILiveBrowserError): Promise<void> {
|
|
2121
|
+
if (this.status === 'stopped') {
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
this.status = 'stopping';
|
|
2125
|
+
if (error) {
|
|
2126
|
+
this.lastError = { ...error };
|
|
2127
|
+
}
|
|
2128
|
+
this.emitState();
|
|
2129
|
+
if (this.browserLifetimeController && !this.browserLifetimeController.signal.aborted) {
|
|
2130
|
+
this.browserLifetimeController.abort(new Error('LiveBrowserSession is stopping'));
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
const browser = this.browser;
|
|
2134
|
+
if (browser && this.browserTargetCreatedListener) {
|
|
2135
|
+
browser.off('targetcreated', this.browserTargetCreatedListener);
|
|
2136
|
+
}
|
|
2137
|
+
this.browserTargetCreatedListener = undefined;
|
|
2138
|
+
if (browser && this.browserDisconnectedListener) {
|
|
2139
|
+
browser.off('disconnected', this.browserDisconnectedListener);
|
|
2140
|
+
}
|
|
2141
|
+
this.browserDisconnectedListener = undefined;
|
|
2142
|
+
|
|
2143
|
+
const shutdownErrors: unknown[] = [];
|
|
2144
|
+
try {
|
|
2145
|
+
await this.teardownProxySecurity();
|
|
2146
|
+
} catch (cleanupError) {
|
|
2147
|
+
shutdownErrors.push(cleanupError);
|
|
2148
|
+
}
|
|
2149
|
+
for (const tab of [...this.tabs.values()]) {
|
|
2150
|
+
tab.closing = true;
|
|
2151
|
+
const securityCdpSession = tab.securityCdpSession;
|
|
2152
|
+
tab.securityCdpSession = undefined;
|
|
2153
|
+
if (securityCdpSession && !securityCdpSession.detached) {
|
|
2154
|
+
try {
|
|
2155
|
+
await securityCdpSession.detach();
|
|
2156
|
+
} catch {
|
|
2157
|
+
// Browser lifetime cancellation may close the target before explicit detach settles.
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
try {
|
|
2161
|
+
await this.stopScreencast(tab);
|
|
2162
|
+
} catch (cleanupError) {
|
|
2163
|
+
shutdownErrors.push(cleanupError);
|
|
2164
|
+
}
|
|
2165
|
+
try {
|
|
2166
|
+
this.removePageListeners(tab);
|
|
2167
|
+
} catch (cleanupError) {
|
|
2168
|
+
shutdownErrors.push(cleanupError);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
try {
|
|
2172
|
+
await this.retireOutstandingFrames(() => true);
|
|
2173
|
+
} catch (cleanupError) {
|
|
2174
|
+
shutdownErrors.push(cleanupError);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
if (browser) {
|
|
2178
|
+
try {
|
|
2179
|
+
await browser.close();
|
|
2180
|
+
} catch (closeError) {
|
|
2181
|
+
shutdownErrors.push(closeError);
|
|
2182
|
+
const ownedProcess = this.ownedBrowserProcess;
|
|
2183
|
+
if (ownedProcess) {
|
|
2184
|
+
try {
|
|
2185
|
+
await this.forceOwnedBrowserProcessGroup(ownedProcess);
|
|
2186
|
+
} catch (forceError) {
|
|
2187
|
+
shutdownErrors.push(forceError);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
for (const tab of this.tabs.values()) {
|
|
2194
|
+
this.tabIdsByPage.delete(tab.page);
|
|
2195
|
+
}
|
|
2196
|
+
this.tabs.clear();
|
|
1329
2197
|
this.outstandingFrames.clear();
|
|
1330
2198
|
this.browser = undefined;
|
|
1331
2199
|
this.browserContext = undefined;
|
|
@@ -1333,11 +2201,452 @@ export class LiveBrowserSession {
|
|
|
1333
2201
|
this.activeTabId = null;
|
|
1334
2202
|
this.status = 'stopped';
|
|
1335
2203
|
this.emitState();
|
|
1336
|
-
if (shutdownErrors.length > 0
|
|
2204
|
+
if (shutdownErrors.length > 0) {
|
|
1337
2205
|
throw new AggregateError(shutdownErrors, 'LiveBrowserSession shutdown was incomplete');
|
|
1338
2206
|
}
|
|
1339
2207
|
}
|
|
1340
2208
|
|
|
2209
|
+
private async configureBrowserSecurity(
|
|
2210
|
+
browser: plugins.puppeteer.Browser,
|
|
2211
|
+
): Promise<void> {
|
|
2212
|
+
const denyPermissions = this.options.security?.denyPermissions;
|
|
2213
|
+
const proxyCredentials = this.options.security?.proxyCredentials;
|
|
2214
|
+
if (!denyPermissions && !proxyCredentials) {
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
const cdpSession = await browser.target().createCDPSession();
|
|
2218
|
+
if (!proxyCredentials) {
|
|
2219
|
+
try {
|
|
2220
|
+
if (denyPermissions) {
|
|
2221
|
+
await cdpSession.send('Browser.grantPermissions', { permissions: [] });
|
|
2222
|
+
}
|
|
2223
|
+
} finally {
|
|
2224
|
+
if (!cdpSession.detached) {
|
|
2225
|
+
await cdpSession.detach();
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
const generation = this.ownedBrowserProcess?.generation;
|
|
2232
|
+
if (generation === undefined) {
|
|
2233
|
+
throw new Error('Proxy security requires an owned browser generation');
|
|
2234
|
+
}
|
|
2235
|
+
this.proxySecurityStopping = false;
|
|
2236
|
+
this.proxySecurityGeneration = generation;
|
|
2237
|
+
this.browserSecurityCdpSession = cdpSession;
|
|
2238
|
+
try {
|
|
2239
|
+
if (denyPermissions) {
|
|
2240
|
+
await cdpSession.send('Browser.grantPermissions', { permissions: [] });
|
|
2241
|
+
}
|
|
2242
|
+
const browserSecurityConnection = cdpSession.connection();
|
|
2243
|
+
if (!browserSecurityConnection) {
|
|
2244
|
+
throw new Error('Browser security CDP session has no connection');
|
|
2245
|
+
}
|
|
2246
|
+
this.browserSecurityConnection = browserSecurityConnection;
|
|
2247
|
+
this.browserSecuritySessionAttachedListener = (attachedSession) => {
|
|
2248
|
+
this.trackProxySecurityOperation(
|
|
2249
|
+
this.configureProxySecuritySession(attachedSession, generation),
|
|
2250
|
+
generation,
|
|
2251
|
+
'proxy_security_session_setup_failed',
|
|
2252
|
+
);
|
|
2253
|
+
};
|
|
2254
|
+
this.browserSecuritySessionDetachedListener = (detachedSession) => {
|
|
2255
|
+
if (detachedSession === cdpSession) {
|
|
2256
|
+
this.handleProxySecurityFailure(
|
|
2257
|
+
'proxy_security_browser_session_detached',
|
|
2258
|
+
new Error('Browser security CDP session detached unexpectedly'),
|
|
2259
|
+
generation,
|
|
2260
|
+
);
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
this.trackProxySecurityOperation(
|
|
2264
|
+
this.verifyProxySecuritySessionDetached(detachedSession, generation),
|
|
2265
|
+
generation,
|
|
2266
|
+
'proxy_security_detach_verification_failed',
|
|
2267
|
+
);
|
|
2268
|
+
};
|
|
2269
|
+
browserSecurityConnection.on(
|
|
2270
|
+
'sessionattached',
|
|
2271
|
+
this.browserSecuritySessionAttachedListener,
|
|
2272
|
+
);
|
|
2273
|
+
browserSecurityConnection.on(
|
|
2274
|
+
'sessiondetached',
|
|
2275
|
+
this.browserSecuritySessionDetachedListener,
|
|
2276
|
+
);
|
|
2277
|
+
|
|
2278
|
+
for (const target of browser.targets()) {
|
|
2279
|
+
if (!proxySeedTargetTypes.has(target.type())) {
|
|
2280
|
+
continue;
|
|
2281
|
+
}
|
|
2282
|
+
const securitySession = await target.createCDPSession();
|
|
2283
|
+
const securityRecord = this.proxySecuritySessions.get(securitySession.id());
|
|
2284
|
+
if (!securityRecord) {
|
|
2285
|
+
throw new Error(`Proxy security did not observe session ${securitySession.id()}`);
|
|
2286
|
+
}
|
|
2287
|
+
securityRecord.ownedByProxySecurity = true;
|
|
2288
|
+
await securityRecord.setupPromise;
|
|
2289
|
+
}
|
|
2290
|
+
} catch (error) {
|
|
2291
|
+
await this.teardownProxySecurity();
|
|
2292
|
+
throw error;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
private configureProxySecuritySession(
|
|
2297
|
+
session: plugins.puppeteer.CDPSession,
|
|
2298
|
+
generation: number,
|
|
2299
|
+
): Promise<void> {
|
|
2300
|
+
const existing = this.proxySecuritySessions.get(session.id());
|
|
2301
|
+
if (existing) {
|
|
2302
|
+
if (existing.generation !== generation) {
|
|
2303
|
+
return Promise.reject(new Error(`CDP session ${session.id()} crossed browser generations`));
|
|
2304
|
+
}
|
|
2305
|
+
return existing.setupPromise;
|
|
2306
|
+
}
|
|
2307
|
+
if (this.proxySecuritySessions.size >= maxProxySecuritySessions) {
|
|
2308
|
+
return Promise.reject(
|
|
2309
|
+
new Error(`Proxy security exceeded ${maxProxySecuritySessions} CDP sessions`),
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
const attemptedAuthentications = new Set<string>();
|
|
2314
|
+
const requestIdsByNetworkId = new Map<string, string>();
|
|
2315
|
+
const handleProtocolFailure = (operation: string, error: unknown): void => {
|
|
2316
|
+
this.handleProxySecurityFailure(
|
|
2317
|
+
'proxy_security_protocol_failed',
|
|
2318
|
+
new Error(`${operation}: ${normalizeErrorMessage(error)}`),
|
|
2319
|
+
generation,
|
|
2320
|
+
);
|
|
2321
|
+
};
|
|
2322
|
+
const authRequiredListener = (event: TProxyAuthRequiredEvent): void => {
|
|
2323
|
+
const credentials = this.options.security?.proxyCredentials;
|
|
2324
|
+
const totalTrackedRequests = [...this.proxySecuritySessions.values()].reduce(
|
|
2325
|
+
(total, record) => total
|
|
2326
|
+
+ record.attemptedAuthentications.size
|
|
2327
|
+
+ record.requestIdsByNetworkId.size,
|
|
2328
|
+
0,
|
|
2329
|
+
);
|
|
2330
|
+
const hasTrackingCapacity = attemptedAuthentications.size < maxTrackedProxyRequests
|
|
2331
|
+
&& totalTrackedRequests < maxTotalTrackedProxyRequests;
|
|
2332
|
+
const isFirstProxyAttempt = event.authChallenge.source === 'Proxy'
|
|
2333
|
+
&& !attemptedAuthentications.has(event.requestId)
|
|
2334
|
+
&& hasTrackingCapacity;
|
|
2335
|
+
if (isFirstProxyAttempt) {
|
|
2336
|
+
attemptedAuthentications.add(event.requestId);
|
|
2337
|
+
} else if (
|
|
2338
|
+
event.authChallenge.source === 'Proxy'
|
|
2339
|
+
&& !attemptedAuthentications.has(event.requestId)
|
|
2340
|
+
&& !hasTrackingCapacity
|
|
2341
|
+
) {
|
|
2342
|
+
handleProtocolFailure(
|
|
2343
|
+
'Proxy authentication tracking failed',
|
|
2344
|
+
new Error(`A security session exceeded ${maxTrackedProxyRequests} tracked requests`),
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
const authChallengeResponse = isFirstProxyAttempt && credentials
|
|
2348
|
+
? {
|
|
2349
|
+
response: 'ProvideCredentials' as const,
|
|
2350
|
+
username: credentials.username,
|
|
2351
|
+
password: credentials.password,
|
|
2352
|
+
}
|
|
2353
|
+
: { response: 'CancelAuth' as const };
|
|
2354
|
+
this.trackProxySecurityOperation(
|
|
2355
|
+
session.send('Fetch.continueWithAuth', {
|
|
2356
|
+
requestId: event.requestId,
|
|
2357
|
+
authChallengeResponse,
|
|
2358
|
+
}).then(() => undefined),
|
|
2359
|
+
generation,
|
|
2360
|
+
'proxy_security_protocol_failed',
|
|
2361
|
+
);
|
|
2362
|
+
};
|
|
2363
|
+
const requestPausedListener = (event: TProxyRequestPausedEvent): void => {
|
|
2364
|
+
if (event.networkId) {
|
|
2365
|
+
const totalTrackedRequests = [...this.proxySecuritySessions.values()].reduce(
|
|
2366
|
+
(total, record) => total
|
|
2367
|
+
+ record.attemptedAuthentications.size
|
|
2368
|
+
+ record.requestIdsByNetworkId.size,
|
|
2369
|
+
0,
|
|
2370
|
+
);
|
|
2371
|
+
if (
|
|
2372
|
+
!requestIdsByNetworkId.has(event.networkId)
|
|
2373
|
+
&& (
|
|
2374
|
+
requestIdsByNetworkId.size >= maxTrackedProxyRequests
|
|
2375
|
+
|| totalTrackedRequests >= maxTotalTrackedProxyRequests
|
|
2376
|
+
)
|
|
2377
|
+
) {
|
|
2378
|
+
handleProtocolFailure(
|
|
2379
|
+
'Proxy request tracking failed',
|
|
2380
|
+
new Error(`A security session exceeded ${maxTrackedProxyRequests} tracked requests`),
|
|
2381
|
+
);
|
|
2382
|
+
} else {
|
|
2383
|
+
const previousRequestId = requestIdsByNetworkId.get(event.networkId);
|
|
2384
|
+
if (previousRequestId && previousRequestId !== event.requestId) {
|
|
2385
|
+
attemptedAuthentications.delete(previousRequestId);
|
|
2386
|
+
}
|
|
2387
|
+
requestIdsByNetworkId.set(event.networkId, event.requestId);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
this.trackProxySecurityOperation(
|
|
2391
|
+
session.send('Fetch.continueRequest', { requestId: event.requestId }).then(() => undefined),
|
|
2392
|
+
generation,
|
|
2393
|
+
'proxy_security_protocol_failed',
|
|
2394
|
+
);
|
|
2395
|
+
};
|
|
2396
|
+
const forgetAuthentication = (networkId: string): void => {
|
|
2397
|
+
const requestId = requestIdsByNetworkId.get(networkId);
|
|
2398
|
+
if (!requestId) {
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
requestIdsByNetworkId.delete(networkId);
|
|
2402
|
+
attemptedAuthentications.delete(requestId);
|
|
2403
|
+
};
|
|
2404
|
+
const loadingFinishedListener = (event: TNetworkLoadingFinishedEvent): void => {
|
|
2405
|
+
forgetAuthentication(event.requestId);
|
|
2406
|
+
};
|
|
2407
|
+
const loadingFailedListener = (event: TNetworkLoadingFailedEvent): void => {
|
|
2408
|
+
forgetAuthentication(event.requestId);
|
|
2409
|
+
};
|
|
2410
|
+
session.on('Fetch.authRequired', authRequiredListener);
|
|
2411
|
+
session.on('Fetch.requestPaused', requestPausedListener);
|
|
2412
|
+
session.on('Network.loadingFinished', loadingFinishedListener);
|
|
2413
|
+
session.on('Network.loadingFailed', loadingFailedListener);
|
|
2414
|
+
|
|
2415
|
+
const proxySecuritySession: IProxySecuritySession = {
|
|
2416
|
+
session,
|
|
2417
|
+
generation,
|
|
2418
|
+
fetchEnabled: false,
|
|
2419
|
+
allowLiveTargetDetach: false,
|
|
2420
|
+
ownedByProxySecurity: false,
|
|
2421
|
+
attemptedAuthentications,
|
|
2422
|
+
requestIdsByNetworkId,
|
|
2423
|
+
authRequiredListener,
|
|
2424
|
+
requestPausedListener,
|
|
2425
|
+
loadingFinishedListener,
|
|
2426
|
+
loadingFailedListener,
|
|
2427
|
+
setupPromise: Promise.resolve(),
|
|
2428
|
+
};
|
|
2429
|
+
const targetInfoPromise = session.send('Target.getTargetInfo');
|
|
2430
|
+
const networkEnablePromise = session.send('Network.enable');
|
|
2431
|
+
const fetchEnablePromise = session.send('Fetch.enable', {
|
|
2432
|
+
handleAuthRequests: true,
|
|
2433
|
+
patterns: [{ urlPattern: '*' }],
|
|
2434
|
+
});
|
|
2435
|
+
const commandResultsPromise = Promise.allSettled([
|
|
2436
|
+
targetInfoPromise,
|
|
2437
|
+
networkEnablePromise,
|
|
2438
|
+
fetchEnablePromise,
|
|
2439
|
+
]);
|
|
2440
|
+
const setupPromise = (async (): Promise<void> => {
|
|
2441
|
+
try {
|
|
2442
|
+
const [targetInfoResult, networkResult, fetchResult] = await commandResultsPromise;
|
|
2443
|
+
if (targetInfoResult.status === 'rejected') {
|
|
2444
|
+
throw targetInfoResult.reason;
|
|
2445
|
+
}
|
|
2446
|
+
const { targetInfo } = targetInfoResult.value;
|
|
2447
|
+
proxySecuritySession.targetId = targetInfo.targetId;
|
|
2448
|
+
proxySecuritySession.targetType = targetInfo.type;
|
|
2449
|
+
if (fetchResult.status === 'rejected') {
|
|
2450
|
+
const unsupportedTarget = proxyFetchUnsupportedTargetTypes.has(targetInfo.type)
|
|
2451
|
+
&& fetchResult.reason instanceof plugins.puppeteer.ProtocolError
|
|
2452
|
+
&& fetchResult.reason.originalMessage === "'Fetch.enable' wasn't found";
|
|
2453
|
+
if (unsupportedTarget) {
|
|
2454
|
+
this.removeProxySecuritySession(proxySecuritySession);
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2457
|
+
throw fetchResult.reason;
|
|
2458
|
+
}
|
|
2459
|
+
if (networkResult.status === 'rejected') {
|
|
2460
|
+
throw networkResult.reason;
|
|
2461
|
+
}
|
|
2462
|
+
proxySecuritySession.fetchEnabled = true;
|
|
2463
|
+
} catch (error) {
|
|
2464
|
+
this.removeProxySecuritySession(proxySecuritySession);
|
|
2465
|
+
if (session.detached || this.proxySecurityStopping || this.normalStopRequested) {
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
throw error;
|
|
2469
|
+
}
|
|
2470
|
+
})();
|
|
2471
|
+
proxySecuritySession.setupPromise = setupPromise;
|
|
2472
|
+
this.proxySecuritySessions.set(session.id(), proxySecuritySession);
|
|
2473
|
+
return setupPromise;
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
private async verifyProxySecuritySessionDetached(
|
|
2477
|
+
session: plugins.puppeteer.CDPSession,
|
|
2478
|
+
generation: number,
|
|
2479
|
+
): Promise<void> {
|
|
2480
|
+
const proxySecuritySession = this.proxySecuritySessions.get(session.id());
|
|
2481
|
+
if (!proxySecuritySession || proxySecuritySession.generation !== generation) {
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
this.removeProxySecuritySession(proxySecuritySession);
|
|
2485
|
+
if (this.proxySecurityStopping || this.normalStopRequested || this.status !== 'running') {
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
if (proxySecuritySession.allowLiveTargetDetach) {
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
if (!proxySecuritySession.targetId) {
|
|
2492
|
+
throw new Error(`Proxy security session detached before target identification: ${session.id()}`);
|
|
2493
|
+
}
|
|
2494
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
2495
|
+
const generationSessions = [...this.proxySecuritySessions.values()].filter((candidate) => (
|
|
2496
|
+
candidate.generation === generation
|
|
2497
|
+
));
|
|
2498
|
+
await Promise.allSettled(generationSessions.map((candidate) => candidate.setupPromise));
|
|
2499
|
+
const hasReplacement = [...this.proxySecuritySessions.values()].some((candidate) => (
|
|
2500
|
+
candidate.generation === generation
|
|
2501
|
+
&& candidate.targetId === proxySecuritySession.targetId
|
|
2502
|
+
&& candidate.fetchEnabled
|
|
2503
|
+
&& !candidate.session.detached
|
|
2504
|
+
));
|
|
2505
|
+
if (hasReplacement) {
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
if (attempt < 2) {
|
|
2509
|
+
await delay(25);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
const browserSecurityCdpSession = this.browserSecurityCdpSession;
|
|
2513
|
+
if (!browserSecurityCdpSession || browserSecurityCdpSession.detached) {
|
|
2514
|
+
throw new Error('Browser security CDP session detached unexpectedly');
|
|
2515
|
+
}
|
|
2516
|
+
const { targetInfos } = await browserSecurityCdpSession.send('Target.getTargets');
|
|
2517
|
+
if (targetInfos.some((targetInfo) => targetInfo.targetId === proxySecuritySession.targetId)) {
|
|
2518
|
+
throw new Error(
|
|
2519
|
+
`Proxy security detached from live target ${proxySecuritySession.targetId}`,
|
|
2520
|
+
);
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
private removeProxySecuritySession(proxySecuritySession: IProxySecuritySession): void {
|
|
2525
|
+
if (this.proxySecuritySessions.get(proxySecuritySession.session.id()) !== proxySecuritySession) {
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
proxySecuritySession.session.off('Fetch.authRequired', proxySecuritySession.authRequiredListener);
|
|
2529
|
+
proxySecuritySession.session.off('Fetch.requestPaused', proxySecuritySession.requestPausedListener);
|
|
2530
|
+
proxySecuritySession.session.off(
|
|
2531
|
+
'Network.loadingFinished',
|
|
2532
|
+
proxySecuritySession.loadingFinishedListener,
|
|
2533
|
+
);
|
|
2534
|
+
proxySecuritySession.session.off(
|
|
2535
|
+
'Network.loadingFailed',
|
|
2536
|
+
proxySecuritySession.loadingFailedListener,
|
|
2537
|
+
);
|
|
2538
|
+
proxySecuritySession.attemptedAuthentications.clear();
|
|
2539
|
+
proxySecuritySession.requestIdsByNetworkId.clear();
|
|
2540
|
+
this.proxySecuritySessions.delete(proxySecuritySession.session.id());
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
private allowOperationalCdpSessionDetach(session: plugins.puppeteer.CDPSession): void {
|
|
2544
|
+
const proxySecuritySession = this.proxySecuritySessions.get(session.id());
|
|
2545
|
+
if (proxySecuritySession) {
|
|
2546
|
+
proxySecuritySession.allowLiveTargetDetach = true;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
private trackProxySecurityOperation(
|
|
2551
|
+
operation: Promise<void>,
|
|
2552
|
+
generation: number,
|
|
2553
|
+
errorCode: string,
|
|
2554
|
+
): void {
|
|
2555
|
+
if (this.proxySecurityOperations.size >= maxProxySecurityOperations) {
|
|
2556
|
+
void operation.catch(() => undefined);
|
|
2557
|
+
this.handleProxySecurityFailure(
|
|
2558
|
+
'proxy_security_operation_capacity_exceeded',
|
|
2559
|
+
new Error(`Proxy security exceeded ${maxProxySecurityOperations} protocol operations`),
|
|
2560
|
+
generation,
|
|
2561
|
+
);
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
let trackedOperation: Promise<void>;
|
|
2565
|
+
trackedOperation = operation.catch((error) => {
|
|
2566
|
+
this.handleProxySecurityFailure(errorCode, error, generation);
|
|
2567
|
+
}).finally(() => {
|
|
2568
|
+
this.proxySecurityOperations.delete(trackedOperation);
|
|
2569
|
+
});
|
|
2570
|
+
this.proxySecurityOperations.add(trackedOperation);
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
private handleProxySecurityFailure(
|
|
2574
|
+
code: string,
|
|
2575
|
+
error: unknown,
|
|
2576
|
+
generation = this.proxySecurityGeneration,
|
|
2577
|
+
): void {
|
|
2578
|
+
if (
|
|
2579
|
+
generation !== this.proxySecurityGeneration
|
|
2580
|
+
|| this.proxySecurityStopping
|
|
2581
|
+
|| this.normalStopRequested
|
|
2582
|
+
|| this.status === 'stopped'
|
|
2583
|
+
|| this.status === 'stopping'
|
|
2584
|
+
) {
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
const liveBrowserError: ILiveBrowserError = {
|
|
2588
|
+
code,
|
|
2589
|
+
message: normalizeErrorMessage(error),
|
|
2590
|
+
fatal: true,
|
|
2591
|
+
};
|
|
2592
|
+
this.emitError(liveBrowserError);
|
|
2593
|
+
this.beginShutdown(true);
|
|
2594
|
+
void this.terminate().catch((terminationError) => {
|
|
2595
|
+
this.emitError({
|
|
2596
|
+
code: 'proxy_security_termination_failed',
|
|
2597
|
+
message: normalizeErrorMessage(terminationError),
|
|
2598
|
+
fatal: true,
|
|
2599
|
+
});
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
private async teardownProxySecurity(): Promise<void> {
|
|
2604
|
+
this.proxySecurityStopping = true;
|
|
2605
|
+
if (this.browserSecurityConnection && this.browserSecuritySessionAttachedListener) {
|
|
2606
|
+
this.browserSecurityConnection.off(
|
|
2607
|
+
'sessionattached',
|
|
2608
|
+
this.browserSecuritySessionAttachedListener,
|
|
2609
|
+
);
|
|
2610
|
+
}
|
|
2611
|
+
if (this.browserSecurityConnection && this.browserSecuritySessionDetachedListener) {
|
|
2612
|
+
this.browserSecurityConnection.off(
|
|
2613
|
+
'sessiondetached',
|
|
2614
|
+
this.browserSecuritySessionDetachedListener,
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
this.browserSecuritySessionAttachedListener = undefined;
|
|
2618
|
+
this.browserSecuritySessionDetachedListener = undefined;
|
|
2619
|
+
this.browserSecurityConnection = undefined;
|
|
2620
|
+
const ownedSessions = [...this.proxySecuritySessions.values()]
|
|
2621
|
+
.filter((record) => record.ownedByProxySecurity)
|
|
2622
|
+
.map((record) => record.session);
|
|
2623
|
+
for (const proxySecuritySession of [...this.proxySecuritySessions.values()]) {
|
|
2624
|
+
this.removeProxySecuritySession(proxySecuritySession);
|
|
2625
|
+
}
|
|
2626
|
+
for (const ownedSession of ownedSessions) {
|
|
2627
|
+
if (!ownedSession.detached) {
|
|
2628
|
+
try {
|
|
2629
|
+
await ownedSession.detach();
|
|
2630
|
+
} catch {
|
|
2631
|
+
// Browser shutdown can close a target before explicit detach settles.
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
const browserSecurityCdpSession = this.browserSecurityCdpSession;
|
|
2636
|
+
this.browserSecurityCdpSession = undefined;
|
|
2637
|
+
if (browserSecurityCdpSession && !browserSecurityCdpSession.detached) {
|
|
2638
|
+
try {
|
|
2639
|
+
await browserSecurityCdpSession.detach();
|
|
2640
|
+
} catch {
|
|
2641
|
+
// Browser shutdown can detach the browser target first.
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
while (this.proxySecurityOperations.size > 0) {
|
|
2645
|
+
await Promise.allSettled([...this.proxySecurityOperations]);
|
|
2646
|
+
}
|
|
2647
|
+
this.proxySecurityGeneration = 0;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
1341
2650
|
private requireBrowserContext(): plugins.puppeteer.BrowserContext {
|
|
1342
2651
|
if (this.status !== 'running' || !this.browserContext) {
|
|
1343
2652
|
throw new Error('LiveBrowserSession is not running');
|
|
@@ -1404,6 +2713,68 @@ export class LiveBrowserSession {
|
|
|
1404
2713
|
}
|
|
1405
2714
|
}
|
|
1406
2715
|
|
|
2716
|
+
private scheduleDiscoveredPageRegistration(page: plugins.puppeteer.Page): void {
|
|
2717
|
+
if (
|
|
2718
|
+
page.isClosed()
|
|
2719
|
+
|| this.tabIdsByPage.has(page)
|
|
2720
|
+
|| this.normalStopRequested
|
|
2721
|
+
|| this.status === 'stopped'
|
|
2722
|
+
|| this.status === 'stopping'
|
|
2723
|
+
) {
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
const wasScheduled = this.scheduleOperation(async () => {
|
|
2727
|
+
if (page.isClosed() || this.tabIdsByPage.has(page)) {
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
const previousActiveTabId = this.activeTabId;
|
|
2731
|
+
let discoveredTab: IPrivateLiveBrowserTab | undefined;
|
|
2732
|
+
try {
|
|
2733
|
+
discoveredTab = await this.registerPage(page);
|
|
2734
|
+
await this.activateTabInternal(discoveredTab.id);
|
|
2735
|
+
} catch (error) {
|
|
2736
|
+
const rollbackError = await this.rollbackCreatedPage(
|
|
2737
|
+
page,
|
|
2738
|
+
discoveredTab,
|
|
2739
|
+
previousActiveTabId,
|
|
2740
|
+
);
|
|
2741
|
+
if (rollbackError) {
|
|
2742
|
+
const aggregateError = new AggregateError(
|
|
2743
|
+
[error, rollbackError],
|
|
2744
|
+
'Discovered page registration rollback failed',
|
|
2745
|
+
);
|
|
2746
|
+
this.handleDiscoveredPageFailure(aggregateError);
|
|
2747
|
+
throw aggregateError;
|
|
2748
|
+
}
|
|
2749
|
+
throw error;
|
|
2750
|
+
}
|
|
2751
|
+
}, 'discovered_page_registration_failed', undefined, true);
|
|
2752
|
+
if (!wasScheduled) {
|
|
2753
|
+
this.handleDiscoveredPageFailure(
|
|
2754
|
+
new Error('A discovered page could not be admitted to the internal operation queue'),
|
|
2755
|
+
);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
private handleDiscoveredPageFailure(error: unknown): void {
|
|
2760
|
+
if (this.normalStopRequested || this.status === 'stopped' || this.status === 'stopping') {
|
|
2761
|
+
return;
|
|
2762
|
+
}
|
|
2763
|
+
const liveBrowserError: ILiveBrowserError = {
|
|
2764
|
+
code: 'untracked_page_detected',
|
|
2765
|
+
message: normalizeErrorMessage(error),
|
|
2766
|
+
fatal: true,
|
|
2767
|
+
};
|
|
2768
|
+
this.emitError(liveBrowserError);
|
|
2769
|
+
void this.requestShutdown(liveBrowserError).catch((shutdownError) => {
|
|
2770
|
+
this.emitError({
|
|
2771
|
+
code: 'untracked_page_shutdown_failed',
|
|
2772
|
+
message: normalizeErrorMessage(shutdownError),
|
|
2773
|
+
fatal: true,
|
|
2774
|
+
});
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
|
|
1407
2778
|
private async registerPage(page: plugins.puppeteer.Page): Promise<IPrivateLiveBrowserTab> {
|
|
1408
2779
|
const existingTabId = this.tabIdsByPage.get(page);
|
|
1409
2780
|
if (existingTabId) {
|
|
@@ -1433,6 +2804,18 @@ export class LiveBrowserSession {
|
|
|
1433
2804
|
this.tabs.set(tab.id, tab);
|
|
1434
2805
|
this.tabIdsByPage.set(page, tab.id);
|
|
1435
2806
|
try {
|
|
2807
|
+
if (this.options.security?.denyFileChoosers) {
|
|
2808
|
+
const securityCdpSession = await page.createCDPSession();
|
|
2809
|
+
this.allowOperationalCdpSessionDetach(securityCdpSession);
|
|
2810
|
+
tab.securityCdpSession = securityCdpSession;
|
|
2811
|
+
await securityCdpSession.send('Page.enable', {
|
|
2812
|
+
enableFileChooserOpenedEvent: true,
|
|
2813
|
+
});
|
|
2814
|
+
await securityCdpSession.send('Page.setInterceptFileChooserDialog', {
|
|
2815
|
+
enabled: true,
|
|
2816
|
+
cancel: true,
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
1436
2819
|
await this.ensureTabViewport(tab);
|
|
1437
2820
|
await this.refreshTab(tab);
|
|
1438
2821
|
|
|
@@ -1488,6 +2871,7 @@ export class LiveBrowserSession {
|
|
|
1488
2871
|
if (frame !== page.mainFrame() || tab.closing) {
|
|
1489
2872
|
return;
|
|
1490
2873
|
}
|
|
2874
|
+
tab.evaluationExecutionContextId = undefined;
|
|
1491
2875
|
const navigationReset = !tab.navigationInProgress;
|
|
1492
2876
|
if (navigationReset) {
|
|
1493
2877
|
tab.streamInvalidated = true;
|
|
@@ -1563,6 +2947,13 @@ export class LiveBrowserSession {
|
|
|
1563
2947
|
);
|
|
1564
2948
|
return tab;
|
|
1565
2949
|
} catch (error) {
|
|
2950
|
+
if (tab.securityCdpSession && !tab.securityCdpSession.detached) {
|
|
2951
|
+
try {
|
|
2952
|
+
await tab.securityCdpSession.detach();
|
|
2953
|
+
} catch {
|
|
2954
|
+
// The page may have closed while security setup was failing.
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
1566
2957
|
this.removePageListeners(tab);
|
|
1567
2958
|
this.tabs.delete(tab.id);
|
|
1568
2959
|
this.tabIdsByPage.delete(page);
|
|
@@ -1813,6 +3204,7 @@ export class LiveBrowserSession {
|
|
|
1813
3204
|
signal: AbortSignal,
|
|
1814
3205
|
): Promise<void> {
|
|
1815
3206
|
const isActive = this.activeTabId === tab.id;
|
|
3207
|
+
tab.evaluationExecutionContextId = undefined;
|
|
1816
3208
|
tab.navigationInProgress = true;
|
|
1817
3209
|
if (isActive) {
|
|
1818
3210
|
await this.stopScreencast(tab);
|
|
@@ -1832,7 +3224,6 @@ export class LiveBrowserSession {
|
|
|
1832
3224
|
&& this.activeTabId === tab.id
|
|
1833
3225
|
&& this.status === 'running'
|
|
1834
3226
|
&& tab.status === 'open'
|
|
1835
|
-
&& !signal.aborted
|
|
1836
3227
|
) {
|
|
1837
3228
|
await this.startScreencast(tab);
|
|
1838
3229
|
}
|
|
@@ -1879,6 +3270,7 @@ export class LiveBrowserSession {
|
|
|
1879
3270
|
await this.ensureTabViewport(tab);
|
|
1880
3271
|
|
|
1881
3272
|
const cdpSession = await tab.page.createCDPSession();
|
|
3273
|
+
this.allowOperationalCdpSessionDetach(cdpSession);
|
|
1882
3274
|
const generation = tab.generation + 1;
|
|
1883
3275
|
const frameListener: TScreencastFrameListener = (event) => {
|
|
1884
3276
|
this.handleScreencastFrame(tab, cdpSession, generation, event);
|
|
@@ -2242,14 +3634,300 @@ export class LiveBrowserSession {
|
|
|
2242
3634
|
|
|
2243
3635
|
private validateUrl(url: unknown): string {
|
|
2244
3636
|
const validatedUrl = validateBoundedString(url, 'url', 1, maxUrlLength);
|
|
3637
|
+
let parsedUrl: URL;
|
|
2245
3638
|
try {
|
|
2246
|
-
new URL(validatedUrl);
|
|
3639
|
+
parsedUrl = new URL(validatedUrl);
|
|
2247
3640
|
} catch {
|
|
2248
3641
|
throw new Error('url must be absolute');
|
|
2249
3642
|
}
|
|
3643
|
+
if (
|
|
3644
|
+
this.options.security?.httpNavigationOnly
|
|
3645
|
+
&& parsedUrl.protocol !== 'http:'
|
|
3646
|
+
&& parsedUrl.protocol !== 'https:'
|
|
3647
|
+
) {
|
|
3648
|
+
throw new Error('url protocol must be http or https');
|
|
3649
|
+
}
|
|
2250
3650
|
return validatedUrl;
|
|
2251
3651
|
}
|
|
2252
3652
|
|
|
3653
|
+
private normalizeEvaluationOptions(
|
|
3654
|
+
options: ILiveBrowserEvaluateOptions,
|
|
3655
|
+
): INormalizedEvaluationOptions {
|
|
3656
|
+
return {
|
|
3657
|
+
timeoutMs: options.timeoutMs === undefined
|
|
3658
|
+
? 5000
|
|
3659
|
+
: validateInteger(options.timeoutMs, 'timeoutMs', 1, 30000),
|
|
3660
|
+
maxOutputBytes: options.maxOutputBytes === undefined
|
|
3661
|
+
? 262144
|
|
3662
|
+
: validateInteger(
|
|
3663
|
+
options.maxOutputBytes,
|
|
3664
|
+
'maxOutputBytes',
|
|
3665
|
+
1,
|
|
3666
|
+
maxEvaluationOutputBytes,
|
|
3667
|
+
),
|
|
3668
|
+
maxDepth: options.maxDepth === undefined
|
|
3669
|
+
? 16
|
|
3670
|
+
: validateInteger(options.maxDepth, 'maxDepth', 1, maxEvaluationDepth),
|
|
3671
|
+
maxNodes: options.maxNodes === undefined
|
|
3672
|
+
? 10000
|
|
3673
|
+
: validateInteger(options.maxNodes, 'maxNodes', 1, maxEvaluationNodes),
|
|
3674
|
+
maxStringBytes: options.maxStringBytes === undefined
|
|
3675
|
+
? 65536
|
|
3676
|
+
: validateInteger(
|
|
3677
|
+
options.maxStringBytes,
|
|
3678
|
+
'maxStringBytes',
|
|
3679
|
+
0,
|
|
3680
|
+
maxEvaluationStringBytes,
|
|
3681
|
+
),
|
|
3682
|
+
maxArrayLength: options.maxArrayLength === undefined
|
|
3683
|
+
? 1000
|
|
3684
|
+
: validateInteger(
|
|
3685
|
+
options.maxArrayLength,
|
|
3686
|
+
'maxArrayLength',
|
|
3687
|
+
0,
|
|
3688
|
+
maxEvaluationArrayLength,
|
|
3689
|
+
),
|
|
3690
|
+
maxObjectKeys: options.maxObjectKeys === undefined
|
|
3691
|
+
? 1000
|
|
3692
|
+
: validateInteger(
|
|
3693
|
+
options.maxObjectKeys,
|
|
3694
|
+
'maxObjectKeys',
|
|
3695
|
+
0,
|
|
3696
|
+
maxEvaluationObjectKeys,
|
|
3697
|
+
),
|
|
3698
|
+
};
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
private readCdpExceptionMessage(
|
|
3702
|
+
exceptionDetails: plugins.puppeteer.Protocol.Runtime.ExceptionDetails,
|
|
3703
|
+
): string {
|
|
3704
|
+
const description = exceptionDetails.exception?.description;
|
|
3705
|
+
if (typeof description === 'string' && description.length > 0) {
|
|
3706
|
+
return truncate(description, 2048);
|
|
3707
|
+
}
|
|
3708
|
+
return truncate(exceptionDetails.text, 2048);
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
private createEvaluationBootstrapExpression(): string {
|
|
3712
|
+
const bootstrapKeyLiteral = JSON.stringify(evaluationBootstrapKey);
|
|
3713
|
+
const cancelKeyLiteral = JSON.stringify(evaluationCancelKey);
|
|
3714
|
+
const cleanupKeyLiteral = JSON.stringify(evaluationCleanupKey);
|
|
3715
|
+
return `(() => {
|
|
3716
|
+
'use strict';
|
|
3717
|
+
if (typeof globalThis[${bootstrapKeyLiteral}] === 'function') {
|
|
3718
|
+
return true;
|
|
3719
|
+
}
|
|
3720
|
+
const safeGlobalThis = globalThis;
|
|
3721
|
+
const SafeArray = Array;
|
|
3722
|
+
const SafeError = Error;
|
|
3723
|
+
const SafeFunction = Function;
|
|
3724
|
+
const SafeJSON = JSON;
|
|
3725
|
+
const SafeMap = Map;
|
|
3726
|
+
const SafeNumber = Number;
|
|
3727
|
+
const SafeObject = Object;
|
|
3728
|
+
const SafePromise = Promise;
|
|
3729
|
+
const SafeString = String;
|
|
3730
|
+
const SafeTextEncoder = TextEncoder;
|
|
3731
|
+
const SafeWeakSet = WeakSet;
|
|
3732
|
+
const SafeClearTimeout = clearTimeout;
|
|
3733
|
+
const SafeSetTimeout = setTimeout;
|
|
3734
|
+
const safeCreate = SafeObject.create;
|
|
3735
|
+
const safeDefineProperty = SafeObject.defineProperty;
|
|
3736
|
+
const safeGetOwnPropertyDescriptor = SafeObject.getOwnPropertyDescriptor;
|
|
3737
|
+
const safeGetOwnPropertySymbols = SafeObject.getOwnPropertySymbols;
|
|
3738
|
+
const safeGetPrototypeOf = SafeObject.getPrototypeOf;
|
|
3739
|
+
const safeIsArray = SafeArray.isArray;
|
|
3740
|
+
const safeIsFinite = SafeNumber.isFinite;
|
|
3741
|
+
const safeKeys = SafeObject.keys;
|
|
3742
|
+
const safeSetPrototypeOf = SafeObject.setPrototypeOf;
|
|
3743
|
+
const safeStringify = SafeJSON.stringify;
|
|
3744
|
+
const safeMapDelete = SafeFunction.prototype.call.bind(SafeMap.prototype.delete);
|
|
3745
|
+
const safeMapGet = SafeFunction.prototype.call.bind(SafeMap.prototype.get);
|
|
3746
|
+
const safeMapSet = SafeFunction.prototype.call.bind(SafeMap.prototype.set);
|
|
3747
|
+
const safePromiseThen = SafeFunction.prototype.call.bind(SafePromise.prototype.then);
|
|
3748
|
+
const safeWeakSetAdd = SafeFunction.prototype.call.bind(SafeWeakSet.prototype.add);
|
|
3749
|
+
const safeWeakSetHas = SafeFunction.prototype.call.bind(SafeWeakSet.prototype.has);
|
|
3750
|
+
const safeEncode = SafeFunction.prototype.call.bind(SafeTextEncoder.prototype.encode);
|
|
3751
|
+
const encoder = new SafeTextEncoder();
|
|
3752
|
+
const cancellationHandlers = new SafeMap();
|
|
3753
|
+
|
|
3754
|
+
const createEnvelope = (ok, value) => {
|
|
3755
|
+
const envelope = safeCreate(null);
|
|
3756
|
+
envelope.ok = ok;
|
|
3757
|
+
if (ok) {
|
|
3758
|
+
envelope.json = value;
|
|
3759
|
+
} else {
|
|
3760
|
+
envelope.error = value;
|
|
3761
|
+
}
|
|
3762
|
+
return envelope;
|
|
3763
|
+
};
|
|
3764
|
+
const byteLength = (value) => safeEncode(encoder, value).byteLength;
|
|
3765
|
+
const cancelEvaluation = (cancellationKey) => {
|
|
3766
|
+
const handler = safeMapGet(cancellationHandlers, cancellationKey);
|
|
3767
|
+
if (typeof handler === 'function') {
|
|
3768
|
+
handler();
|
|
3769
|
+
}
|
|
3770
|
+
return safeMapDelete(cancellationHandlers, cancellationKey);
|
|
3771
|
+
};
|
|
3772
|
+
const evaluate = async (source, options, cancellationKey) => {
|
|
3773
|
+
const seen = new SafeWeakSet();
|
|
3774
|
+
let visitedNodes = 0;
|
|
3775
|
+
const normalize = (value, depth) => {
|
|
3776
|
+
visitedNodes += 1;
|
|
3777
|
+
if (visitedNodes > options.maxNodes) {
|
|
3778
|
+
throw new SafeError('Evaluation result exceeded maxNodes');
|
|
3779
|
+
}
|
|
3780
|
+
if (depth > options.maxDepth) {
|
|
3781
|
+
throw new SafeError('Evaluation result exceeded maxDepth');
|
|
3782
|
+
}
|
|
3783
|
+
if (value === null || typeof value === 'boolean') {
|
|
3784
|
+
return value;
|
|
3785
|
+
}
|
|
3786
|
+
if (typeof value === 'number') {
|
|
3787
|
+
if (!safeIsFinite(value)) {
|
|
3788
|
+
throw new SafeError('Evaluation result contains a non-finite number');
|
|
3789
|
+
}
|
|
3790
|
+
return value;
|
|
3791
|
+
}
|
|
3792
|
+
if (typeof value === 'string') {
|
|
3793
|
+
if (byteLength(value) > options.maxStringBytes) {
|
|
3794
|
+
throw new SafeError('Evaluation result string exceeded maxStringBytes');
|
|
3795
|
+
}
|
|
3796
|
+
return value;
|
|
3797
|
+
}
|
|
3798
|
+
if (typeof value !== 'object') {
|
|
3799
|
+
throw new SafeError('Evaluation result contains a non-JSON value');
|
|
3800
|
+
}
|
|
3801
|
+
if (safeWeakSetHas(seen, value)) {
|
|
3802
|
+
throw new SafeError('Evaluation result contains a cycle or repeated object');
|
|
3803
|
+
}
|
|
3804
|
+
safeWeakSetAdd(seen, value);
|
|
3805
|
+
|
|
3806
|
+
if (safeIsArray(value)) {
|
|
3807
|
+
if (value.length > options.maxArrayLength) {
|
|
3808
|
+
throw new SafeError('Evaluation result array exceeded maxArrayLength');
|
|
3809
|
+
}
|
|
3810
|
+
const keys = safeKeys(value);
|
|
3811
|
+
if (keys.length !== value.length) {
|
|
3812
|
+
throw new SafeError('Evaluation result contains a sparse or extended array');
|
|
3813
|
+
}
|
|
3814
|
+
const output = SafeArray(value.length);
|
|
3815
|
+
safeSetPrototypeOf(output, null);
|
|
3816
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
3817
|
+
const descriptor = safeGetOwnPropertyDescriptor(value, SafeString(index));
|
|
3818
|
+
if (!descriptor || !('value' in descriptor)) {
|
|
3819
|
+
throw new SafeError('Evaluation result contains an array accessor');
|
|
3820
|
+
}
|
|
3821
|
+
output[index] = normalize(descriptor.value, depth + 1);
|
|
3822
|
+
}
|
|
3823
|
+
return output;
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
const prototype = safeGetPrototypeOf(value);
|
|
3827
|
+
if (prototype !== SafeObject.prototype && prototype !== null) {
|
|
3828
|
+
throw new SafeError('Evaluation result contains a non-plain object');
|
|
3829
|
+
}
|
|
3830
|
+
if (safeGetOwnPropertySymbols(value).length > 0) {
|
|
3831
|
+
throw new SafeError('Evaluation result contains symbol properties');
|
|
3832
|
+
}
|
|
3833
|
+
const keys = safeKeys(value);
|
|
3834
|
+
if (keys.length > options.maxObjectKeys) {
|
|
3835
|
+
throw new SafeError('Evaluation result object exceeded maxObjectKeys');
|
|
3836
|
+
}
|
|
3837
|
+
const output = safeCreate(null);
|
|
3838
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
3839
|
+
const key = keys[index];
|
|
3840
|
+
if (byteLength(key) > options.maxStringBytes) {
|
|
3841
|
+
throw new SafeError('Evaluation result key exceeded maxStringBytes');
|
|
3842
|
+
}
|
|
3843
|
+
const descriptor = safeGetOwnPropertyDescriptor(value, key);
|
|
3844
|
+
if (!descriptor || !('value' in descriptor)) {
|
|
3845
|
+
throw new SafeError('Evaluation result contains an object accessor');
|
|
3846
|
+
}
|
|
3847
|
+
output[key] = normalize(descriptor.value, depth + 1);
|
|
3848
|
+
}
|
|
3849
|
+
return output;
|
|
3850
|
+
};
|
|
3851
|
+
|
|
3852
|
+
try {
|
|
3853
|
+
const execute = SafeFunction(
|
|
3854
|
+
'\"use strict\"; return (async () => (' + source + '\\n))();',
|
|
3855
|
+
);
|
|
3856
|
+
const boundedExecution = new SafePromise((resolve, reject) => {
|
|
3857
|
+
let timeout;
|
|
3858
|
+
safeMapSet(cancellationHandlers, cancellationKey, () => {
|
|
3859
|
+
if (timeout !== undefined) {
|
|
3860
|
+
SafeClearTimeout(timeout);
|
|
3861
|
+
}
|
|
3862
|
+
reject(new SafeError('Evaluation cancelled'));
|
|
3863
|
+
});
|
|
3864
|
+
timeout = SafeSetTimeout(() => {
|
|
3865
|
+
reject(new SafeError('Evaluation timed out after ' + options.timeoutMs + 'ms'));
|
|
3866
|
+
}, options.timeoutMs);
|
|
3867
|
+
safePromiseThen(
|
|
3868
|
+
execute(),
|
|
3869
|
+
(value) => {
|
|
3870
|
+
SafeClearTimeout(timeout);
|
|
3871
|
+
resolve(value);
|
|
3872
|
+
},
|
|
3873
|
+
(error) => {
|
|
3874
|
+
SafeClearTimeout(timeout);
|
|
3875
|
+
reject(error);
|
|
3876
|
+
},
|
|
3877
|
+
);
|
|
3878
|
+
});
|
|
3879
|
+
let normalizedResult;
|
|
3880
|
+
try {
|
|
3881
|
+
normalizedResult = normalize(await boundedExecution, 0);
|
|
3882
|
+
} finally {
|
|
3883
|
+
safeMapDelete(cancellationHandlers, cancellationKey);
|
|
3884
|
+
}
|
|
3885
|
+
const json = safeStringify(normalizedResult);
|
|
3886
|
+
if (typeof json !== 'string' || byteLength(json) > options.maxOutputBytes) {
|
|
3887
|
+
throw new SafeError('Evaluation result exceeded maxOutputBytes');
|
|
3888
|
+
}
|
|
3889
|
+
return createEnvelope(true, json);
|
|
3890
|
+
} catch (error) {
|
|
3891
|
+
let message = 'Evaluation failed';
|
|
3892
|
+
if (typeof error === 'string') {
|
|
3893
|
+
message = error;
|
|
3894
|
+
} else if (error && typeof error === 'object') {
|
|
3895
|
+
const descriptor = safeGetOwnPropertyDescriptor(error, 'message');
|
|
3896
|
+
if (descriptor && 'value' in descriptor && typeof descriptor.value === 'string') {
|
|
3897
|
+
message = descriptor.value;
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
if (byteLength(message) > 2048) {
|
|
3901
|
+
message = 'Evaluation failed with an oversized error';
|
|
3902
|
+
}
|
|
3903
|
+
return createEnvelope(false, message);
|
|
3904
|
+
}
|
|
3905
|
+
};
|
|
3906
|
+
|
|
3907
|
+
for (const [key, value] of [
|
|
3908
|
+
[${bootstrapKeyLiteral}, evaluate],
|
|
3909
|
+
[${cancelKeyLiteral}, cancelEvaluation],
|
|
3910
|
+
[${cleanupKeyLiteral}, cancelEvaluation],
|
|
3911
|
+
]) {
|
|
3912
|
+
safeDefineProperty(safeGlobalThis, key, {
|
|
3913
|
+
value,
|
|
3914
|
+
configurable: false,
|
|
3915
|
+
enumerable: false,
|
|
3916
|
+
writable: false,
|
|
3917
|
+
});
|
|
3918
|
+
}
|
|
3919
|
+
return true;
|
|
3920
|
+
})()`;
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
private createEvaluationExpression(
|
|
3924
|
+
expression: string,
|
|
3925
|
+
options: INormalizedEvaluationOptions,
|
|
3926
|
+
cancellationKey: string,
|
|
3927
|
+
): string {
|
|
3928
|
+
return `globalThis[${JSON.stringify(evaluationBootstrapKey)}](${JSON.stringify(expression)}, ${JSON.stringify(options)}, ${JSON.stringify(cancellationKey)})`;
|
|
3929
|
+
}
|
|
3930
|
+
|
|
2253
3931
|
private validateTimeout(timeoutMs?: number, defaultValue = 5000): number {
|
|
2254
3932
|
if (timeoutMs === undefined) {
|
|
2255
3933
|
return defaultValue;
|