@push.rocks/smartpuppeteer 2.4.0 → 2.6.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 +21 -1
- package/dist_ts/smartpuppeteer.classes.livebrowsersession.js +611 -86
- package/dist_ts/smartpuppeteer.interfaces.livebrowser.d.ts +8 -5
- package/package.json +1 -1
- package/readme.hints.md +3 -0
- package/readme.md +5 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartpuppeteer.classes.livebrowsersession.ts +840 -103
- package/ts/smartpuppeteer.interfaces.livebrowser.ts +9 -6
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
ILiveBrowserFrame,
|
|
22
22
|
ILiveBrowserFrameAcknowledgement,
|
|
23
23
|
ILiveBrowserFrameAcknowledgementRequest,
|
|
24
|
+
ILiveBrowserFrameIdentity,
|
|
24
25
|
ILiveBrowserInsertTextInput,
|
|
25
26
|
ILiveBrowserKeyInput,
|
|
26
27
|
ILiveBrowserModifierState,
|
|
@@ -64,6 +65,7 @@ const maxTextLength = 32768;
|
|
|
64
65
|
const maxUrlLength = 16384;
|
|
65
66
|
const maxTimeoutMs = 60000;
|
|
66
67
|
const frameAcknowledgementTimeoutMs = 5000;
|
|
68
|
+
const defaultFirstFrameTimeoutMs = 5000;
|
|
67
69
|
const maxQueuedPublicOperations = 64;
|
|
68
70
|
const maxQueuedInternalOperations = 128;
|
|
69
71
|
const maxEvaluationScriptBytes = 262144;
|
|
@@ -80,10 +82,12 @@ const maxProxySecuritySessions = 256;
|
|
|
80
82
|
const maxTrackedProxyRequests = 1024;
|
|
81
83
|
const maxTotalTrackedProxyRequests = 4096;
|
|
82
84
|
const maxProxySecurityOperations = 2048;
|
|
85
|
+
const maxTrackedServiceWorkerVersions = 512;
|
|
86
|
+
const proxySecurityCommandTimeoutMs = 5000;
|
|
87
|
+
const proxySecurityCommandOptions = { timeout: proxySecurityCommandTimeoutMs } as const;
|
|
83
88
|
const proxySeedTargetTypes = new Set([
|
|
84
89
|
'background_page',
|
|
85
90
|
'page',
|
|
86
|
-
'service_worker',
|
|
87
91
|
'shared_worker',
|
|
88
92
|
'webview',
|
|
89
93
|
]);
|
|
@@ -97,6 +101,9 @@ type TProxyAuthRequiredEvent = plugins.puppeteer.Protocol.Fetch.AuthRequiredEven
|
|
|
97
101
|
type TProxyRequestPausedEvent = plugins.puppeteer.Protocol.Fetch.RequestPausedEvent;
|
|
98
102
|
type TNetworkLoadingFinishedEvent = plugins.puppeteer.Protocol.Network.LoadingFinishedEvent;
|
|
99
103
|
type TNetworkLoadingFailedEvent = plugins.puppeteer.Protocol.Network.LoadingFailedEvent;
|
|
104
|
+
type TServiceWorkerVersionUpdatedEvent = (
|
|
105
|
+
plugins.puppeteer.Protocol.ServiceWorker.WorkerVersionUpdatedEvent
|
|
106
|
+
);
|
|
100
107
|
|
|
101
108
|
interface IProxySecuritySession {
|
|
102
109
|
session: plugins.puppeteer.CDPSession;
|
|
@@ -106,6 +113,7 @@ interface IProxySecuritySession {
|
|
|
106
113
|
fetchEnabled: boolean;
|
|
107
114
|
allowLiveTargetDetach: boolean;
|
|
108
115
|
ownedByProxySecurity: boolean;
|
|
116
|
+
retireWhenIdle: boolean;
|
|
109
117
|
attemptedAuthentications: Set<string>;
|
|
110
118
|
requestIdsByNetworkId: Map<string, string>;
|
|
111
119
|
authRequiredListener: (event: TProxyAuthRequiredEvent) => void;
|
|
@@ -123,6 +131,11 @@ interface IOwnedBrowserProcess {
|
|
|
123
131
|
forceSignalled: boolean;
|
|
124
132
|
}
|
|
125
133
|
|
|
134
|
+
interface IScreencastAuthority {
|
|
135
|
+
revision: number;
|
|
136
|
+
controller: AbortController;
|
|
137
|
+
}
|
|
138
|
+
|
|
126
139
|
interface IPrivateLiveBrowserTab {
|
|
127
140
|
id: string;
|
|
128
141
|
page: plugins.puppeteer.Page;
|
|
@@ -133,11 +146,14 @@ interface IPrivateLiveBrowserTab {
|
|
|
133
146
|
appliedViewportRevision: number;
|
|
134
147
|
streaming: boolean;
|
|
135
148
|
streamInvalidated: boolean;
|
|
149
|
+
streamLifecycleRevision: number;
|
|
150
|
+
screencastAuthority?: IScreencastAuthority;
|
|
136
151
|
navigationInProgress: boolean;
|
|
137
152
|
closing: boolean;
|
|
138
153
|
stateUpdateQueued: boolean;
|
|
139
154
|
stateUpdatePending: boolean;
|
|
140
|
-
|
|
155
|
+
navigationResetRevision: number;
|
|
156
|
+
restoredNavigationRevision: number;
|
|
141
157
|
evaluationExecutionContextId?: number;
|
|
142
158
|
securityCdpSession?: plugins.puppeteer.CDPSession;
|
|
143
159
|
cdpSession?: plugins.puppeteer.CDPSession;
|
|
@@ -387,8 +403,18 @@ export class LiveBrowserSession {
|
|
|
387
403
|
private browserSecurityCdpSession?: plugins.puppeteer.CDPSession;
|
|
388
404
|
private browserSecurityConnection?: plugins.puppeteer.Connection;
|
|
389
405
|
private browserSecuritySessionAttachedListener?: (session: plugins.puppeteer.CDPSession) => void;
|
|
406
|
+
private browserSecurityOwnedSessionAttachedListener?: (
|
|
407
|
+
session: plugins.puppeteer.CDPSession,
|
|
408
|
+
) => void;
|
|
390
409
|
private browserSecuritySessionDetachedListener?: TCdpSessionDetachedListener;
|
|
410
|
+
private browserSecurityWorkerLifecycleCdpSession?: plugins.puppeteer.CDPSession;
|
|
411
|
+
private browserSecurityWorkerVersionUpdatedListener?: (
|
|
412
|
+
event: TServiceWorkerVersionUpdatedEvent,
|
|
413
|
+
) => void;
|
|
391
414
|
private readonly proxySecuritySessions = new Map<string, IProxySecuritySession>();
|
|
415
|
+
private readonly ownedProxySecuritySessions = new Set<plugins.puppeteer.CDPSession>();
|
|
416
|
+
private readonly browserSecurityParentOwnedSessions = new Set<plugins.puppeteer.CDPSession>();
|
|
417
|
+
private readonly serviceWorkerTargetIdsByVersionId = new Map<string, string>();
|
|
392
418
|
private readonly proxySecurityOperations = new Set<Promise<void>>();
|
|
393
419
|
private proxySecurityGeneration = 0;
|
|
394
420
|
private proxySecurityStopping = false;
|
|
@@ -826,6 +852,77 @@ export class LiveBrowserSession {
|
|
|
826
852
|
};
|
|
827
853
|
}
|
|
828
854
|
|
|
855
|
+
public refreshScreencast(
|
|
856
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
857
|
+
): Promise<ILiveBrowserFrameIdentity> {
|
|
858
|
+
return this.enqueuePublicOperation(async (signal) => {
|
|
859
|
+
signal.throwIfAborted();
|
|
860
|
+
const tab = this.requireActiveTab();
|
|
861
|
+
if (!tab.streaming || tab.streamInvalidated) {
|
|
862
|
+
throw new Error(`Tab input transport is not available: ${tab.id}`);
|
|
863
|
+
}
|
|
864
|
+
let firstFrame: ReturnType<LiveBrowserSession['waitForScreencastFrame']> | undefined;
|
|
865
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
866
|
+
try {
|
|
867
|
+
const previousLifecycleRevision = tab.streamLifecycleRevision;
|
|
868
|
+
await this.stopScreencast(tab);
|
|
869
|
+
const refreshLifecycleRevision = previousLifecycleRevision + 1;
|
|
870
|
+
if (
|
|
871
|
+
!this.canRestoreScreencast(tab)
|
|
872
|
+
|| tab.streamLifecycleRevision !== refreshLifecycleRevision
|
|
873
|
+
) {
|
|
874
|
+
throw new Error(`Screencast lifecycle changed while refreshing tab: ${tab.id}`);
|
|
875
|
+
}
|
|
876
|
+
const authority = this.createScreencastAuthority(tab, refreshLifecycleRevision);
|
|
877
|
+
const generation = tab.generation + 1;
|
|
878
|
+
firstFrame = this.waitForScreencastFrame(
|
|
879
|
+
tab,
|
|
880
|
+
generation,
|
|
881
|
+
this.viewportRevision,
|
|
882
|
+
authority.controller.signal,
|
|
883
|
+
);
|
|
884
|
+
const refreshTimeoutMs = this.options.screencast?.firstFrameTimeoutMs
|
|
885
|
+
?? defaultFirstFrameTimeoutMs;
|
|
886
|
+
const refreshTimeout = new Promise<never>((_resolve, reject) => {
|
|
887
|
+
timeout = setTimeout(() => {
|
|
888
|
+
reject(new Error(
|
|
889
|
+
`Screencast generation ${generation} did not restart and produce a frame within ${
|
|
890
|
+
refreshTimeoutMs
|
|
891
|
+
}ms`,
|
|
892
|
+
));
|
|
893
|
+
}, refreshTimeoutMs);
|
|
894
|
+
});
|
|
895
|
+
const restartPromise = this.startScreencast(tab, authority);
|
|
896
|
+
const [, identity] = await Promise.race([
|
|
897
|
+
Promise.all([restartPromise, firstFrame.promise]),
|
|
898
|
+
refreshTimeout,
|
|
899
|
+
]);
|
|
900
|
+
return identity;
|
|
901
|
+
} catch (error) {
|
|
902
|
+
if (!this.canRestoreScreencast(tab)) throw error;
|
|
903
|
+
const refreshError: ILiveBrowserError = {
|
|
904
|
+
code: 'screencast_refresh_failed',
|
|
905
|
+
message: normalizeErrorMessage(error),
|
|
906
|
+
fatal: true,
|
|
907
|
+
tabId: tab.id,
|
|
908
|
+
};
|
|
909
|
+
this.emitError(refreshError);
|
|
910
|
+
void this.requestShutdown(refreshError).catch((shutdownError) => {
|
|
911
|
+
this.emitError({
|
|
912
|
+
code: 'screencast_refresh_shutdown_failed',
|
|
913
|
+
message: normalizeErrorMessage(shutdownError),
|
|
914
|
+
fatal: true,
|
|
915
|
+
tabId: tab.id,
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
throw error;
|
|
919
|
+
} finally {
|
|
920
|
+
if (timeout) clearTimeout(timeout);
|
|
921
|
+
firstFrame?.cancel();
|
|
922
|
+
}
|
|
923
|
+
}, operationOptions);
|
|
924
|
+
}
|
|
925
|
+
|
|
829
926
|
public async createTab(
|
|
830
927
|
optionsArg: ILiveBrowserCreateTabOptions = {},
|
|
831
928
|
operationOptions: ILiveBrowserOperationOptions = {},
|
|
@@ -1771,6 +1868,9 @@ export class LiveBrowserSession {
|
|
|
1771
1868
|
this.emitState();
|
|
1772
1869
|
}
|
|
1773
1870
|
const shutdownError = new Error('LiveBrowserSession is stopping');
|
|
1871
|
+
for (const tab of this.tabs.values()) {
|
|
1872
|
+
this.invalidateScreencast(tab, shutdownError);
|
|
1873
|
+
}
|
|
1774
1874
|
const activeOperation = this.activeOperation;
|
|
1775
1875
|
const shouldAbortActiveOperation = Boolean(
|
|
1776
1876
|
abortActiveOperation
|
|
@@ -2193,7 +2293,10 @@ export class LiveBrowserSession {
|
|
|
2193
2293
|
tab.securityCdpSession = undefined;
|
|
2194
2294
|
if (securityCdpSession && !securityCdpSession.detached) {
|
|
2195
2295
|
try {
|
|
2196
|
-
await
|
|
2296
|
+
await this.detachCdpSessionWithTimeout(
|
|
2297
|
+
securityCdpSession,
|
|
2298
|
+
'page security session',
|
|
2299
|
+
);
|
|
2197
2300
|
} catch {
|
|
2198
2301
|
// Browser lifetime cancellation may close the target before explicit detach settles.
|
|
2199
2302
|
}
|
|
@@ -2263,11 +2366,18 @@ export class LiveBrowserSession {
|
|
|
2263
2366
|
if (!proxyCredentials) {
|
|
2264
2367
|
try {
|
|
2265
2368
|
if (denyPermissions) {
|
|
2266
|
-
await cdpSession.send(
|
|
2369
|
+
await cdpSession.send(
|
|
2370
|
+
'Browser.grantPermissions',
|
|
2371
|
+
{ permissions: [] },
|
|
2372
|
+
proxySecurityCommandOptions,
|
|
2373
|
+
);
|
|
2267
2374
|
}
|
|
2268
2375
|
} finally {
|
|
2269
2376
|
if (!cdpSession.detached) {
|
|
2270
|
-
await
|
|
2377
|
+
await this.detachCdpSessionWithTimeout(
|
|
2378
|
+
cdpSession,
|
|
2379
|
+
'browser permission session',
|
|
2380
|
+
);
|
|
2271
2381
|
}
|
|
2272
2382
|
}
|
|
2273
2383
|
return;
|
|
@@ -2282,7 +2392,11 @@ export class LiveBrowserSession {
|
|
|
2282
2392
|
this.browserSecurityCdpSession = cdpSession;
|
|
2283
2393
|
try {
|
|
2284
2394
|
if (denyPermissions) {
|
|
2285
|
-
await cdpSession.send(
|
|
2395
|
+
await cdpSession.send(
|
|
2396
|
+
'Browser.grantPermissions',
|
|
2397
|
+
{ permissions: [] },
|
|
2398
|
+
proxySecurityCommandOptions,
|
|
2399
|
+
);
|
|
2286
2400
|
}
|
|
2287
2401
|
const browserSecurityConnection = cdpSession.connection();
|
|
2288
2402
|
if (!browserSecurityConnection) {
|
|
@@ -2290,13 +2404,69 @@ export class LiveBrowserSession {
|
|
|
2290
2404
|
}
|
|
2291
2405
|
this.browserSecurityConnection = browserSecurityConnection;
|
|
2292
2406
|
this.browserSecuritySessionAttachedListener = (attachedSession) => {
|
|
2407
|
+
const setupPromise = this.configureProxySecuritySession(attachedSession, generation);
|
|
2408
|
+
queueMicrotask(() => {
|
|
2409
|
+
const securityRecord = this.proxySecuritySessions.get(attachedSession.id());
|
|
2410
|
+
if (
|
|
2411
|
+
securityRecord?.generation === generation
|
|
2412
|
+
&& securityRecord.ownedByProxySecurity
|
|
2413
|
+
) {
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
this.trackProxySecurityOperation(
|
|
2417
|
+
setupPromise,
|
|
2418
|
+
generation,
|
|
2419
|
+
'proxy_security_session_setup_failed',
|
|
2420
|
+
);
|
|
2421
|
+
});
|
|
2422
|
+
};
|
|
2423
|
+
this.browserSecurityOwnedSessionAttachedListener = (attachedSession) => {
|
|
2424
|
+
const securityRecord = this.proxySecuritySessions.get(attachedSession.id());
|
|
2425
|
+
if (!securityRecord || securityRecord.generation !== generation) {
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
securityRecord.ownedByProxySecurity = true;
|
|
2429
|
+
this.ownedProxySecuritySessions.add(attachedSession);
|
|
2430
|
+
this.browserSecurityParentOwnedSessions.add(attachedSession);
|
|
2431
|
+
const setupPromise = securityRecord.setupPromise;
|
|
2432
|
+
securityRecord.setupPromise = setupPromise.then(async () => {
|
|
2433
|
+
try {
|
|
2434
|
+
await attachedSession.send(
|
|
2435
|
+
'Runtime.runIfWaitingForDebugger',
|
|
2436
|
+
undefined,
|
|
2437
|
+
proxySecurityCommandOptions,
|
|
2438
|
+
);
|
|
2439
|
+
} catch (error) {
|
|
2440
|
+
this.removeProxySecuritySession(securityRecord);
|
|
2441
|
+
if (attachedSession.detached) {
|
|
2442
|
+
this.forgetOwnedProxySecuritySession(attachedSession);
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
if (this.proxySecurityStopping || this.normalStopRequested) {
|
|
2446
|
+
await this.detachOwnedProxySecuritySession(securityRecord);
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
if (
|
|
2450
|
+
securityRecord.targetId
|
|
2451
|
+
&& await this.waitForProxySecurityTargetCoverage(
|
|
2452
|
+
securityRecord.targetId,
|
|
2453
|
+
generation,
|
|
2454
|
+
)
|
|
2455
|
+
) {
|
|
2456
|
+
await this.detachOwnedProxySecuritySession(securityRecord);
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
throw error;
|
|
2460
|
+
}
|
|
2461
|
+
});
|
|
2293
2462
|
this.trackProxySecurityOperation(
|
|
2294
|
-
|
|
2463
|
+
securityRecord.setupPromise,
|
|
2295
2464
|
generation,
|
|
2296
2465
|
'proxy_security_session_setup_failed',
|
|
2297
2466
|
);
|
|
2298
2467
|
};
|
|
2299
2468
|
this.browserSecuritySessionDetachedListener = (detachedSession) => {
|
|
2469
|
+
this.forgetOwnedProxySecuritySession(detachedSession);
|
|
2300
2470
|
if (detachedSession === cdpSession) {
|
|
2301
2471
|
this.handleProxySecurityFailure(
|
|
2302
2472
|
'proxy_security_browser_session_detached',
|
|
@@ -2305,6 +2475,14 @@ export class LiveBrowserSession {
|
|
|
2305
2475
|
);
|
|
2306
2476
|
return;
|
|
2307
2477
|
}
|
|
2478
|
+
if (detachedSession === this.browserSecurityWorkerLifecycleCdpSession) {
|
|
2479
|
+
this.handleProxySecurityFailure(
|
|
2480
|
+
'proxy_security_worker_lifecycle_detached',
|
|
2481
|
+
new Error('Proxy security service-worker lifecycle session detached unexpectedly'),
|
|
2482
|
+
generation,
|
|
2483
|
+
);
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2308
2486
|
this.trackProxySecurityOperation(
|
|
2309
2487
|
this.verifyProxySecuritySessionDetached(detachedSession, generation),
|
|
2310
2488
|
generation,
|
|
@@ -2315,11 +2493,30 @@ export class LiveBrowserSession {
|
|
|
2315
2493
|
'sessionattached',
|
|
2316
2494
|
this.browserSecuritySessionAttachedListener,
|
|
2317
2495
|
);
|
|
2496
|
+
cdpSession.on(
|
|
2497
|
+
plugins.puppeteer.CDPSessionEvent.SessionAttached,
|
|
2498
|
+
this.browserSecurityOwnedSessionAttachedListener,
|
|
2499
|
+
);
|
|
2318
2500
|
browserSecurityConnection.on(
|
|
2319
2501
|
'sessiondetached',
|
|
2320
2502
|
this.browserSecuritySessionDetachedListener,
|
|
2321
2503
|
);
|
|
2322
2504
|
|
|
2505
|
+
await cdpSession.send('Target.setAutoAttach', {
|
|
2506
|
+
autoAttach: true,
|
|
2507
|
+
waitForDebuggerOnStart: true,
|
|
2508
|
+
flatten: true,
|
|
2509
|
+
filter: [
|
|
2510
|
+
{ type: 'service_worker' },
|
|
2511
|
+
{ exclude: true },
|
|
2512
|
+
],
|
|
2513
|
+
}, proxySecurityCommandOptions);
|
|
2514
|
+
await Promise.all(
|
|
2515
|
+
[...this.proxySecuritySessions.values()]
|
|
2516
|
+
.filter((record) => record.generation === generation && record.ownedByProxySecurity)
|
|
2517
|
+
.map((record) => record.setupPromise),
|
|
2518
|
+
);
|
|
2519
|
+
|
|
2323
2520
|
for (const target of browser.targets()) {
|
|
2324
2521
|
if (!proxySeedTargetTypes.has(target.type())) {
|
|
2325
2522
|
continue;
|
|
@@ -2330,14 +2527,121 @@ export class LiveBrowserSession {
|
|
|
2330
2527
|
throw new Error(`Proxy security did not observe session ${securitySession.id()}`);
|
|
2331
2528
|
}
|
|
2332
2529
|
securityRecord.ownedByProxySecurity = true;
|
|
2530
|
+
this.ownedProxySecuritySessions.add(securitySession);
|
|
2333
2531
|
await securityRecord.setupPromise;
|
|
2334
2532
|
}
|
|
2533
|
+
await this.configureProxySecurityWorkerLifecycle(cdpSession, generation);
|
|
2335
2534
|
} catch (error) {
|
|
2336
2535
|
await this.teardownProxySecurity();
|
|
2337
2536
|
throw error;
|
|
2338
2537
|
}
|
|
2339
2538
|
}
|
|
2340
2539
|
|
|
2540
|
+
private async configureProxySecurityWorkerLifecycle(
|
|
2541
|
+
browserSecurityCdpSession: plugins.puppeteer.CDPSession,
|
|
2542
|
+
generation: number,
|
|
2543
|
+
): Promise<void> {
|
|
2544
|
+
const lifecycleUrl = `data:text/html,<title>smartpuppeteer-proxy-security-${generation}</title>`;
|
|
2545
|
+
const { targetId } = await browserSecurityCdpSession.send('Target.createTarget', {
|
|
2546
|
+
url: lifecycleUrl,
|
|
2547
|
+
background: true,
|
|
2548
|
+
hidden: true,
|
|
2549
|
+
}, proxySecurityCommandOptions);
|
|
2550
|
+
const attachedSessions = new Map<string, plugins.puppeteer.CDPSession>();
|
|
2551
|
+
const captureAttachedSession = (session: plugins.puppeteer.CDPSession): void => {
|
|
2552
|
+
attachedSessions.set(session.id(), session);
|
|
2553
|
+
};
|
|
2554
|
+
browserSecurityCdpSession.on(
|
|
2555
|
+
plugins.puppeteer.CDPSessionEvent.SessionAttached,
|
|
2556
|
+
captureAttachedSession,
|
|
2557
|
+
);
|
|
2558
|
+
let lifecycleSession: plugins.puppeteer.CDPSession;
|
|
2559
|
+
try {
|
|
2560
|
+
const { sessionId } = await browserSecurityCdpSession.send('Target.attachToTarget', {
|
|
2561
|
+
targetId,
|
|
2562
|
+
flatten: true,
|
|
2563
|
+
}, proxySecurityCommandOptions);
|
|
2564
|
+
const attachedSession = attachedSessions.get(sessionId);
|
|
2565
|
+
if (!attachedSession) {
|
|
2566
|
+
throw new Error('Proxy security did not observe its hidden lifecycle session');
|
|
2567
|
+
}
|
|
2568
|
+
lifecycleSession = attachedSession;
|
|
2569
|
+
} finally {
|
|
2570
|
+
browserSecurityCdpSession.off(
|
|
2571
|
+
plugins.puppeteer.CDPSessionEvent.SessionAttached,
|
|
2572
|
+
captureAttachedSession,
|
|
2573
|
+
);
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
const securityRecord = this.proxySecuritySessions.get(lifecycleSession.id());
|
|
2577
|
+
if (!securityRecord || !securityRecord.ownedByProxySecurity) {
|
|
2578
|
+
throw new Error('Proxy security did not configure its hidden lifecycle session');
|
|
2579
|
+
}
|
|
2580
|
+
await securityRecord.setupPromise;
|
|
2581
|
+
if (lifecycleSession.detached) {
|
|
2582
|
+
throw new Error('Proxy security hidden lifecycle session detached during setup');
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
this.browserSecurityWorkerLifecycleCdpSession = lifecycleSession;
|
|
2586
|
+
this.browserSecurityWorkerVersionUpdatedListener = (event) => {
|
|
2587
|
+
const stoppedTargetIds = new Set<string>();
|
|
2588
|
+
const redundantTargetIds = new Set<string>();
|
|
2589
|
+
for (const version of event.versions) {
|
|
2590
|
+
if (version.targetId) {
|
|
2591
|
+
if (
|
|
2592
|
+
!this.serviceWorkerTargetIdsByVersionId.has(version.versionId)
|
|
2593
|
+
&& this.serviceWorkerTargetIdsByVersionId.size >= maxTrackedServiceWorkerVersions
|
|
2594
|
+
) {
|
|
2595
|
+
this.handleProxySecurityFailure(
|
|
2596
|
+
'proxy_security_worker_tracking_capacity_exceeded',
|
|
2597
|
+
new Error(
|
|
2598
|
+
`Proxy security exceeded ${maxTrackedServiceWorkerVersions} service-worker versions`,
|
|
2599
|
+
),
|
|
2600
|
+
generation,
|
|
2601
|
+
);
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
this.serviceWorkerTargetIdsByVersionId.set(version.versionId, version.targetId);
|
|
2605
|
+
}
|
|
2606
|
+
const knownTargetId = version.targetId
|
|
2607
|
+
?? this.serviceWorkerTargetIdsByVersionId.get(version.versionId);
|
|
2608
|
+
if (version.status === 'redundant' && knownTargetId) {
|
|
2609
|
+
redundantTargetIds.add(knownTargetId);
|
|
2610
|
+
}
|
|
2611
|
+
if (version.runningStatus === 'stopped') {
|
|
2612
|
+
if (knownTargetId) {
|
|
2613
|
+
stoppedTargetIds.add(knownTargetId);
|
|
2614
|
+
}
|
|
2615
|
+
this.serviceWorkerTargetIdsByVersionId.delete(version.versionId);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
for (const workerSecurityRecord of [...this.proxySecuritySessions.values()]) {
|
|
2619
|
+
if (
|
|
2620
|
+
!workerSecurityRecord.ownedByProxySecurity
|
|
2621
|
+
|| workerSecurityRecord.targetType !== 'service_worker'
|
|
2622
|
+
|| !workerSecurityRecord.targetId
|
|
2623
|
+
|| !this.browserSecurityParentOwnedSessions.has(workerSecurityRecord.session)
|
|
2624
|
+
) {
|
|
2625
|
+
continue;
|
|
2626
|
+
}
|
|
2627
|
+
if (stoppedTargetIds.has(workerSecurityRecord.targetId)) {
|
|
2628
|
+
this.retireOwnedProxySecuritySession(workerSecurityRecord, true);
|
|
2629
|
+
} else if (redundantTargetIds.has(workerSecurityRecord.targetId)) {
|
|
2630
|
+
this.retireOwnedProxySecuritySession(workerSecurityRecord, false);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
};
|
|
2634
|
+
lifecycleSession.on(
|
|
2635
|
+
'ServiceWorker.workerVersionUpdated',
|
|
2636
|
+
this.browserSecurityWorkerVersionUpdatedListener,
|
|
2637
|
+
);
|
|
2638
|
+
await lifecycleSession.send(
|
|
2639
|
+
'ServiceWorker.enable',
|
|
2640
|
+
undefined,
|
|
2641
|
+
proxySecurityCommandOptions,
|
|
2642
|
+
);
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2341
2645
|
private configureProxySecuritySession(
|
|
2342
2646
|
session: plugins.puppeteer.CDPSession,
|
|
2343
2647
|
generation: number,
|
|
@@ -2400,7 +2704,7 @@ export class LiveBrowserSession {
|
|
|
2400
2704
|
session.send('Fetch.continueWithAuth', {
|
|
2401
2705
|
requestId: event.requestId,
|
|
2402
2706
|
authChallengeResponse,
|
|
2403
|
-
}).then(() => undefined),
|
|
2707
|
+
}, proxySecurityCommandOptions).then(() => undefined),
|
|
2404
2708
|
generation,
|
|
2405
2709
|
'proxy_security_protocol_failed',
|
|
2406
2710
|
);
|
|
@@ -2433,7 +2737,11 @@ export class LiveBrowserSession {
|
|
|
2433
2737
|
}
|
|
2434
2738
|
}
|
|
2435
2739
|
this.trackProxySecurityOperation(
|
|
2436
|
-
session.send(
|
|
2740
|
+
session.send(
|
|
2741
|
+
'Fetch.continueRequest',
|
|
2742
|
+
{ requestId: event.requestId },
|
|
2743
|
+
proxySecurityCommandOptions,
|
|
2744
|
+
).then(() => undefined),
|
|
2437
2745
|
generation,
|
|
2438
2746
|
'proxy_security_protocol_failed',
|
|
2439
2747
|
);
|
|
@@ -2445,6 +2753,13 @@ export class LiveBrowserSession {
|
|
|
2445
2753
|
}
|
|
2446
2754
|
requestIdsByNetworkId.delete(networkId);
|
|
2447
2755
|
attemptedAuthentications.delete(requestId);
|
|
2756
|
+
if (
|
|
2757
|
+
proxySecuritySession.retireWhenIdle
|
|
2758
|
+
&& attemptedAuthentications.size === 0
|
|
2759
|
+
&& requestIdsByNetworkId.size === 0
|
|
2760
|
+
) {
|
|
2761
|
+
this.retireOwnedProxySecuritySession(proxySecuritySession, false);
|
|
2762
|
+
}
|
|
2448
2763
|
};
|
|
2449
2764
|
const loadingFinishedListener = (event: TNetworkLoadingFinishedEvent): void => {
|
|
2450
2765
|
forgetAuthentication(event.requestId);
|
|
@@ -2463,6 +2778,7 @@ export class LiveBrowserSession {
|
|
|
2463
2778
|
fetchEnabled: false,
|
|
2464
2779
|
allowLiveTargetDetach: false,
|
|
2465
2780
|
ownedByProxySecurity: false,
|
|
2781
|
+
retireWhenIdle: false,
|
|
2466
2782
|
attemptedAuthentications,
|
|
2467
2783
|
requestIdsByNetworkId,
|
|
2468
2784
|
authRequiredListener,
|
|
@@ -2471,12 +2787,20 @@ export class LiveBrowserSession {
|
|
|
2471
2787
|
loadingFailedListener,
|
|
2472
2788
|
setupPromise: Promise.resolve(),
|
|
2473
2789
|
};
|
|
2474
|
-
const targetInfoPromise = session.send(
|
|
2475
|
-
|
|
2790
|
+
const targetInfoPromise = session.send(
|
|
2791
|
+
'Target.getTargetInfo',
|
|
2792
|
+
undefined,
|
|
2793
|
+
proxySecurityCommandOptions,
|
|
2794
|
+
);
|
|
2795
|
+
const networkEnablePromise = session.send(
|
|
2796
|
+
'Network.enable',
|
|
2797
|
+
undefined,
|
|
2798
|
+
proxySecurityCommandOptions,
|
|
2799
|
+
);
|
|
2476
2800
|
const fetchEnablePromise = session.send('Fetch.enable', {
|
|
2477
2801
|
handleAuthRequests: true,
|
|
2478
2802
|
patterns: [{ urlPattern: '*' }],
|
|
2479
|
-
});
|
|
2803
|
+
}, proxySecurityCommandOptions);
|
|
2480
2804
|
const commandResultsPromise = Promise.allSettled([
|
|
2481
2805
|
targetInfoPromise,
|
|
2482
2806
|
networkEnablePromise,
|
|
@@ -2507,10 +2831,28 @@ export class LiveBrowserSession {
|
|
|
2507
2831
|
proxySecuritySession.fetchEnabled = true;
|
|
2508
2832
|
} catch (error) {
|
|
2509
2833
|
this.removeProxySecuritySession(proxySecuritySession);
|
|
2510
|
-
if (session.detached
|
|
2834
|
+
if (session.detached) {
|
|
2835
|
+
this.forgetOwnedProxySecuritySession(session);
|
|
2511
2836
|
return;
|
|
2512
2837
|
}
|
|
2513
|
-
|
|
2838
|
+
if (this.proxySecurityStopping || this.normalStopRequested) {
|
|
2839
|
+
await this.detachOwnedProxySecuritySession(proxySecuritySession);
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
if (
|
|
2843
|
+
proxySecuritySession.targetId
|
|
2844
|
+
&& await this.waitForProxySecurityTargetCoverage(
|
|
2845
|
+
proxySecuritySession.targetId,
|
|
2846
|
+
generation,
|
|
2847
|
+
)
|
|
2848
|
+
) {
|
|
2849
|
+
await this.detachOwnedProxySecuritySession(proxySecuritySession);
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
throw new Error(
|
|
2853
|
+
`Proxy security ${proxySecuritySession.ownedByProxySecurity ? 'owned' : 'observed'} session setup failed for ${proxySecuritySession.targetType ?? 'unknown'} target ${proxySecuritySession.targetId ?? 'unknown'}: ${normalizeErrorMessage(error)}`,
|
|
2854
|
+
{ cause: error },
|
|
2855
|
+
);
|
|
2514
2856
|
}
|
|
2515
2857
|
})();
|
|
2516
2858
|
proxySecuritySession.setupPromise = setupPromise;
|
|
@@ -2536,6 +2878,18 @@ export class LiveBrowserSession {
|
|
|
2536
2878
|
if (!proxySecuritySession.targetId) {
|
|
2537
2879
|
throw new Error(`Proxy security session detached before target identification: ${session.id()}`);
|
|
2538
2880
|
}
|
|
2881
|
+
if (await this.waitForProxySecurityTargetCoverage(proxySecuritySession.targetId, generation)) {
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
throw new Error(
|
|
2885
|
+
`Proxy security detached from live target ${proxySecuritySession.targetId}`,
|
|
2886
|
+
);
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
private async waitForProxySecurityTargetCoverage(
|
|
2890
|
+
targetId: string,
|
|
2891
|
+
generation: number,
|
|
2892
|
+
): Promise<boolean> {
|
|
2539
2893
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
2540
2894
|
const generationSessions = [...this.proxySecuritySessions.values()].filter((candidate) => (
|
|
2541
2895
|
candidate.generation === generation
|
|
@@ -2543,12 +2897,12 @@ export class LiveBrowserSession {
|
|
|
2543
2897
|
await Promise.allSettled(generationSessions.map((candidate) => candidate.setupPromise));
|
|
2544
2898
|
const hasReplacement = [...this.proxySecuritySessions.values()].some((candidate) => (
|
|
2545
2899
|
candidate.generation === generation
|
|
2546
|
-
&& candidate.targetId ===
|
|
2900
|
+
&& candidate.targetId === targetId
|
|
2547
2901
|
&& candidate.fetchEnabled
|
|
2548
2902
|
&& !candidate.session.detached
|
|
2549
2903
|
));
|
|
2550
2904
|
if (hasReplacement) {
|
|
2551
|
-
return;
|
|
2905
|
+
return true;
|
|
2552
2906
|
}
|
|
2553
2907
|
if (attempt < 2) {
|
|
2554
2908
|
await delay(25);
|
|
@@ -2558,12 +2912,12 @@ export class LiveBrowserSession {
|
|
|
2558
2912
|
if (!browserSecurityCdpSession || browserSecurityCdpSession.detached) {
|
|
2559
2913
|
throw new Error('Browser security CDP session detached unexpectedly');
|
|
2560
2914
|
}
|
|
2561
|
-
const { targetInfos } = await browserSecurityCdpSession.send(
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2915
|
+
const { targetInfos } = await browserSecurityCdpSession.send(
|
|
2916
|
+
'Target.getTargets',
|
|
2917
|
+
undefined,
|
|
2918
|
+
proxySecurityCommandOptions,
|
|
2919
|
+
);
|
|
2920
|
+
return !targetInfos.some((targetInfo) => targetInfo.targetId === targetId);
|
|
2567
2921
|
}
|
|
2568
2922
|
|
|
2569
2923
|
private removeProxySecuritySession(proxySecuritySession: IProxySecuritySession): void {
|
|
@@ -2585,6 +2939,113 @@ export class LiveBrowserSession {
|
|
|
2585
2939
|
this.proxySecuritySessions.delete(proxySecuritySession.session.id());
|
|
2586
2940
|
}
|
|
2587
2941
|
|
|
2942
|
+
private async detachOwnedProxySecuritySession(
|
|
2943
|
+
proxySecuritySession: IProxySecuritySession,
|
|
2944
|
+
): Promise<void> {
|
|
2945
|
+
if (!proxySecuritySession.ownedByProxySecurity) {
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
await this.detachTrackedOwnedProxySecuritySession(proxySecuritySession.session);
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
private forgetOwnedProxySecuritySession(session: plugins.puppeteer.CDPSession): void {
|
|
2952
|
+
this.ownedProxySecuritySessions.delete(session);
|
|
2953
|
+
this.browserSecurityParentOwnedSessions.delete(session);
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
private async detachCdpSessionWithTimeout(
|
|
2957
|
+
session: plugins.puppeteer.CDPSession,
|
|
2958
|
+
description: string,
|
|
2959
|
+
): Promise<void> {
|
|
2960
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
2961
|
+
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
|
2962
|
+
timeout = setTimeout(() => {
|
|
2963
|
+
reject(new Error(
|
|
2964
|
+
`${description} did not detach within ${proxySecurityCommandTimeoutMs}ms`,
|
|
2965
|
+
));
|
|
2966
|
+
}, proxySecurityCommandTimeoutMs);
|
|
2967
|
+
});
|
|
2968
|
+
try {
|
|
2969
|
+
await Promise.race([session.detach(), timeoutPromise]);
|
|
2970
|
+
} finally {
|
|
2971
|
+
if (timeout) {
|
|
2972
|
+
clearTimeout(timeout);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
private retireOwnedProxySecuritySession(
|
|
2978
|
+
proxySecuritySession: IProxySecuritySession,
|
|
2979
|
+
force: boolean,
|
|
2980
|
+
): void {
|
|
2981
|
+
if (
|
|
2982
|
+
!proxySecuritySession.ownedByProxySecurity
|
|
2983
|
+
|| this.proxySecuritySessions.get(proxySecuritySession.session.id())
|
|
2984
|
+
!== proxySecuritySession
|
|
2985
|
+
) {
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
proxySecuritySession.retireWhenIdle = true;
|
|
2989
|
+
if (
|
|
2990
|
+
!force
|
|
2991
|
+
&& (
|
|
2992
|
+
proxySecuritySession.attemptedAuthentications.size > 0
|
|
2993
|
+
|| proxySecuritySession.requestIdsByNetworkId.size > 0
|
|
2994
|
+
)
|
|
2995
|
+
) {
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
if (proxySecuritySession.targetId) {
|
|
2999
|
+
for (const [versionId, targetId] of this.serviceWorkerTargetIdsByVersionId) {
|
|
3000
|
+
if (targetId === proxySecuritySession.targetId) {
|
|
3001
|
+
this.serviceWorkerTargetIdsByVersionId.delete(versionId);
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
proxySecuritySession.allowLiveTargetDetach = true;
|
|
3006
|
+
this.removeProxySecuritySession(proxySecuritySession);
|
|
3007
|
+
this.trackProxySecurityOperation(
|
|
3008
|
+
this.detachOwnedProxySecuritySession(proxySecuritySession),
|
|
3009
|
+
proxySecuritySession.generation,
|
|
3010
|
+
'proxy_security_worker_retirement_failed',
|
|
3011
|
+
);
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
private async detachTrackedOwnedProxySecuritySession(
|
|
3015
|
+
session: plugins.puppeteer.CDPSession,
|
|
3016
|
+
): Promise<void> {
|
|
3017
|
+
if (session.detached) {
|
|
3018
|
+
this.forgetOwnedProxySecuritySession(session);
|
|
3019
|
+
return;
|
|
3020
|
+
}
|
|
3021
|
+
try {
|
|
3022
|
+
if (this.browserSecurityParentOwnedSessions.has(session)) {
|
|
3023
|
+
const browserSecurityCdpSession = this.browserSecurityCdpSession;
|
|
3024
|
+
if (!browserSecurityCdpSession || browserSecurityCdpSession.detached) {
|
|
3025
|
+
throw new Error('Browser security parent session is unavailable');
|
|
3026
|
+
}
|
|
3027
|
+
await browserSecurityCdpSession.send('Target.detachFromTarget', {
|
|
3028
|
+
sessionId: session.id(),
|
|
3029
|
+
}, proxySecurityCommandOptions);
|
|
3030
|
+
} else {
|
|
3031
|
+
await this.detachCdpSessionWithTimeout(session, 'proxy security session');
|
|
3032
|
+
}
|
|
3033
|
+
this.forgetOwnedProxySecuritySession(session);
|
|
3034
|
+
} catch (error) {
|
|
3035
|
+
if (session.detached) {
|
|
3036
|
+
this.forgetOwnedProxySecuritySession(session);
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
if (this.proxySecurityStopping || this.normalStopRequested) {
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
throw new Error(
|
|
3043
|
+
`Failed to detach an owned proxy security session: ${normalizeErrorMessage(error)}`,
|
|
3044
|
+
{ cause: error },
|
|
3045
|
+
);
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
|
|
2588
3049
|
private allowOperationalCdpSessionDetach(session: plugins.puppeteer.CDPSession): void {
|
|
2589
3050
|
const proxySecuritySession = this.proxySecuritySessions.get(session.id());
|
|
2590
3051
|
if (proxySecuritySession) {
|
|
@@ -2647,12 +3108,51 @@ export class LiveBrowserSession {
|
|
|
2647
3108
|
|
|
2648
3109
|
private async teardownProxySecurity(): Promise<void> {
|
|
2649
3110
|
this.proxySecurityStopping = true;
|
|
3111
|
+
this.serviceWorkerTargetIdsByVersionId.clear();
|
|
3112
|
+
const browserSecurityCdpSession = this.browserSecurityCdpSession;
|
|
3113
|
+
if (browserSecurityCdpSession && !browserSecurityCdpSession.detached) {
|
|
3114
|
+
try {
|
|
3115
|
+
await browserSecurityCdpSession.send('Target.setAutoAttach', {
|
|
3116
|
+
autoAttach: false,
|
|
3117
|
+
waitForDebuggerOnStart: false,
|
|
3118
|
+
flatten: true,
|
|
3119
|
+
}, proxySecurityCommandOptions);
|
|
3120
|
+
} catch {
|
|
3121
|
+
// Browser shutdown may detach the browser target before auto-attach is disabled.
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
const workerLifecycleCdpSession = this.browserSecurityWorkerLifecycleCdpSession;
|
|
3125
|
+
if (workerLifecycleCdpSession && !workerLifecycleCdpSession.detached) {
|
|
3126
|
+
try {
|
|
3127
|
+
await workerLifecycleCdpSession.send(
|
|
3128
|
+
'ServiceWorker.disable',
|
|
3129
|
+
undefined,
|
|
3130
|
+
proxySecurityCommandOptions,
|
|
3131
|
+
);
|
|
3132
|
+
} catch {
|
|
3133
|
+
// Browser shutdown can close the hidden lifecycle target first.
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
if (workerLifecycleCdpSession && this.browserSecurityWorkerVersionUpdatedListener) {
|
|
3137
|
+
workerLifecycleCdpSession.off(
|
|
3138
|
+
'ServiceWorker.workerVersionUpdated',
|
|
3139
|
+
this.browserSecurityWorkerVersionUpdatedListener,
|
|
3140
|
+
);
|
|
3141
|
+
}
|
|
3142
|
+
this.browserSecurityWorkerVersionUpdatedListener = undefined;
|
|
3143
|
+
this.browserSecurityWorkerLifecycleCdpSession = undefined;
|
|
2650
3144
|
if (this.browserSecurityConnection && this.browserSecuritySessionAttachedListener) {
|
|
2651
3145
|
this.browserSecurityConnection.off(
|
|
2652
3146
|
'sessionattached',
|
|
2653
3147
|
this.browserSecuritySessionAttachedListener,
|
|
2654
3148
|
);
|
|
2655
3149
|
}
|
|
3150
|
+
if (browserSecurityCdpSession && this.browserSecurityOwnedSessionAttachedListener) {
|
|
3151
|
+
browserSecurityCdpSession.off(
|
|
3152
|
+
plugins.puppeteer.CDPSessionEvent.SessionAttached,
|
|
3153
|
+
this.browserSecurityOwnedSessionAttachedListener,
|
|
3154
|
+
);
|
|
3155
|
+
}
|
|
2656
3156
|
if (this.browserSecurityConnection && this.browserSecuritySessionDetachedListener) {
|
|
2657
3157
|
this.browserSecurityConnection.off(
|
|
2658
3158
|
'sessiondetached',
|
|
@@ -2660,28 +3160,31 @@ export class LiveBrowserSession {
|
|
|
2660
3160
|
);
|
|
2661
3161
|
}
|
|
2662
3162
|
this.browserSecuritySessionAttachedListener = undefined;
|
|
3163
|
+
this.browserSecurityOwnedSessionAttachedListener = undefined;
|
|
2663
3164
|
this.browserSecuritySessionDetachedListener = undefined;
|
|
2664
3165
|
this.browserSecurityConnection = undefined;
|
|
2665
|
-
const ownedSessions = [...this.
|
|
2666
|
-
.filter((record) => record.ownedByProxySecurity)
|
|
2667
|
-
.map((record) => record.session);
|
|
3166
|
+
const ownedSessions = [...this.ownedProxySecuritySessions];
|
|
2668
3167
|
for (const proxySecuritySession of [...this.proxySecuritySessions.values()]) {
|
|
2669
3168
|
this.removeProxySecuritySession(proxySecuritySession);
|
|
2670
3169
|
}
|
|
2671
3170
|
for (const ownedSession of ownedSessions) {
|
|
2672
3171
|
if (!ownedSession.detached) {
|
|
2673
3172
|
try {
|
|
2674
|
-
await
|
|
3173
|
+
await this.detachTrackedOwnedProxySecuritySession(ownedSession);
|
|
2675
3174
|
} catch {
|
|
2676
3175
|
// Browser shutdown can close a target before explicit detach settles.
|
|
2677
3176
|
}
|
|
2678
3177
|
}
|
|
2679
3178
|
}
|
|
2680
|
-
|
|
3179
|
+
this.ownedProxySecuritySessions.clear();
|
|
3180
|
+
this.browserSecurityParentOwnedSessions.clear();
|
|
2681
3181
|
this.browserSecurityCdpSession = undefined;
|
|
2682
3182
|
if (browserSecurityCdpSession && !browserSecurityCdpSession.detached) {
|
|
2683
3183
|
try {
|
|
2684
|
-
await
|
|
3184
|
+
await this.detachCdpSessionWithTimeout(
|
|
3185
|
+
browserSecurityCdpSession,
|
|
3186
|
+
'browser security session',
|
|
3187
|
+
);
|
|
2685
3188
|
} catch {
|
|
2686
3189
|
// Browser shutdown can detach the browser target first.
|
|
2687
3190
|
}
|
|
@@ -2839,11 +3342,13 @@ export class LiveBrowserSession {
|
|
|
2839
3342
|
appliedViewportRevision: 0,
|
|
2840
3343
|
streaming: false,
|
|
2841
3344
|
streamInvalidated: true,
|
|
3345
|
+
streamLifecycleRevision: 0,
|
|
2842
3346
|
navigationInProgress: false,
|
|
2843
3347
|
closing: false,
|
|
2844
3348
|
stateUpdateQueued: false,
|
|
2845
3349
|
stateUpdatePending: false,
|
|
2846
|
-
|
|
3350
|
+
navigationResetRevision: 0,
|
|
3351
|
+
restoredNavigationRevision: 0,
|
|
2847
3352
|
removeListeners: [],
|
|
2848
3353
|
};
|
|
2849
3354
|
this.tabs.set(tab.id, tab);
|
|
@@ -2855,11 +3360,11 @@ export class LiveBrowserSession {
|
|
|
2855
3360
|
tab.securityCdpSession = securityCdpSession;
|
|
2856
3361
|
await securityCdpSession.send('Page.enable', {
|
|
2857
3362
|
enableFileChooserOpenedEvent: true,
|
|
2858
|
-
});
|
|
3363
|
+
}, proxySecurityCommandOptions);
|
|
2859
3364
|
await securityCdpSession.send('Page.setInterceptFileChooserDialog', {
|
|
2860
3365
|
enabled: true,
|
|
2861
3366
|
cancel: true,
|
|
2862
|
-
});
|
|
3367
|
+
}, proxySecurityCommandOptions);
|
|
2863
3368
|
}
|
|
2864
3369
|
await this.ensureTabViewport(tab);
|
|
2865
3370
|
await this.refreshTab(tab);
|
|
@@ -2919,7 +3424,7 @@ export class LiveBrowserSession {
|
|
|
2919
3424
|
tab.evaluationExecutionContextId = undefined;
|
|
2920
3425
|
const navigationReset = !tab.navigationInProgress;
|
|
2921
3426
|
if (navigationReset) {
|
|
2922
|
-
tab
|
|
3427
|
+
this.invalidateScreencast(tab, new Error('Page navigation invalidated the screencast'));
|
|
2923
3428
|
this.retireFramesForTabInBackground(tab.id);
|
|
2924
3429
|
}
|
|
2925
3430
|
this.requestTabStateUpdate(tab, navigationReset);
|
|
@@ -2928,7 +3433,7 @@ export class LiveBrowserSession {
|
|
|
2928
3433
|
this.requestTabStateUpdate(tab, false);
|
|
2929
3434
|
};
|
|
2930
3435
|
const onClose = (): void => {
|
|
2931
|
-
tab
|
|
3436
|
+
this.invalidateScreencast(tab, new Error('Page closure invalidated the screencast'));
|
|
2932
3437
|
this.retireFramesForTabInBackground(tab.id);
|
|
2933
3438
|
if (tab.closing || this.normalStopRequested) {
|
|
2934
3439
|
return;
|
|
@@ -2938,7 +3443,7 @@ export class LiveBrowserSession {
|
|
|
2938
3443
|
}, 'page_close_cleanup_failed', tab.id, true);
|
|
2939
3444
|
};
|
|
2940
3445
|
const onCrash = (error: Error): void => {
|
|
2941
|
-
tab
|
|
3446
|
+
this.invalidateScreencast(tab, error);
|
|
2942
3447
|
this.retireFramesForTabInBackground(tab.id);
|
|
2943
3448
|
if (tab.closing || this.normalStopRequested) {
|
|
2944
3449
|
return;
|
|
@@ -2994,7 +3499,10 @@ export class LiveBrowserSession {
|
|
|
2994
3499
|
} catch (error) {
|
|
2995
3500
|
if (tab.securityCdpSession && !tab.securityCdpSession.detached) {
|
|
2996
3501
|
try {
|
|
2997
|
-
await
|
|
3502
|
+
await this.detachCdpSessionWithTimeout(
|
|
3503
|
+
tab.securityCdpSession,
|
|
3504
|
+
'page security session',
|
|
3505
|
+
);
|
|
2998
3506
|
} catch {
|
|
2999
3507
|
// The page may have closed while security setup was failing.
|
|
3000
3508
|
}
|
|
@@ -3033,31 +3541,58 @@ export class LiveBrowserSession {
|
|
|
3033
3541
|
return;
|
|
3034
3542
|
}
|
|
3035
3543
|
tab.stateUpdatePending = true;
|
|
3036
|
-
|
|
3544
|
+
if (navigationReset) {
|
|
3545
|
+
tab.navigationResetRevision += 1;
|
|
3546
|
+
}
|
|
3037
3547
|
if (tab.stateUpdateQueued) {
|
|
3038
3548
|
return;
|
|
3039
3549
|
}
|
|
3040
3550
|
tab.stateUpdateQueued = true;
|
|
3041
3551
|
const wasScheduled = this.scheduleOperation(async () => {
|
|
3042
3552
|
try {
|
|
3043
|
-
const
|
|
3553
|
+
const navigationResetRevision = tab.navigationResetRevision;
|
|
3554
|
+
const shouldResetNavigation = (
|
|
3555
|
+
tab.restoredNavigationRevision < navigationResetRevision
|
|
3556
|
+
);
|
|
3044
3557
|
tab.stateUpdatePending = false;
|
|
3045
|
-
tab.navigationResetPending = false;
|
|
3046
3558
|
if (!this.tabs.has(tab.id) || tab.closing) {
|
|
3047
3559
|
return;
|
|
3048
3560
|
}
|
|
3049
3561
|
if (shouldResetNavigation && this.activeTabId === tab.id) {
|
|
3050
3562
|
await this.stopScreencast(tab);
|
|
3051
3563
|
}
|
|
3052
|
-
|
|
3053
|
-
|
|
3564
|
+
let stateUpdateError: unknown;
|
|
3565
|
+
try {
|
|
3566
|
+
await this.refreshTab(tab);
|
|
3567
|
+
this.emitState();
|
|
3568
|
+
} catch (error) {
|
|
3569
|
+
stateUpdateError = error;
|
|
3570
|
+
}
|
|
3054
3571
|
if (
|
|
3055
3572
|
shouldResetNavigation
|
|
3056
3573
|
&& this.activeTabId === tab.id
|
|
3057
3574
|
&& this.status === 'running'
|
|
3058
3575
|
&& tab.status === 'open'
|
|
3059
3576
|
) {
|
|
3060
|
-
|
|
3577
|
+
try {
|
|
3578
|
+
await this.startScreencast(tab);
|
|
3579
|
+
} catch (error) {
|
|
3580
|
+
throw stateUpdateError
|
|
3581
|
+
? new AggregateError(
|
|
3582
|
+
[stateUpdateError, error],
|
|
3583
|
+
'Page state refresh and screencast restart failed',
|
|
3584
|
+
)
|
|
3585
|
+
: error;
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
if (shouldResetNavigation) {
|
|
3589
|
+
tab.restoredNavigationRevision = Math.max(
|
|
3590
|
+
tab.restoredNavigationRevision,
|
|
3591
|
+
navigationResetRevision,
|
|
3592
|
+
);
|
|
3593
|
+
}
|
|
3594
|
+
if (stateUpdateError) {
|
|
3595
|
+
throw stateUpdateError;
|
|
3061
3596
|
}
|
|
3062
3597
|
} finally {
|
|
3063
3598
|
tab.stateUpdateQueued = false;
|
|
@@ -3262,15 +3797,32 @@ export class LiveBrowserSession {
|
|
|
3262
3797
|
} finally {
|
|
3263
3798
|
tab.navigationInProgress = false;
|
|
3264
3799
|
if (this.tabs.has(tab.id) && !tab.page.isClosed()) {
|
|
3265
|
-
|
|
3266
|
-
|
|
3800
|
+
let stateUpdateError: unknown;
|
|
3801
|
+
try {
|
|
3802
|
+
await this.refreshTab(tab);
|
|
3803
|
+
this.emitState();
|
|
3804
|
+
} catch (error) {
|
|
3805
|
+
stateUpdateError = error;
|
|
3806
|
+
}
|
|
3267
3807
|
if (
|
|
3268
3808
|
isActive
|
|
3269
3809
|
&& this.activeTabId === tab.id
|
|
3270
3810
|
&& this.status === 'running'
|
|
3271
3811
|
&& tab.status === 'open'
|
|
3272
3812
|
) {
|
|
3273
|
-
|
|
3813
|
+
try {
|
|
3814
|
+
await this.startScreencast(tab);
|
|
3815
|
+
} catch (error) {
|
|
3816
|
+
throw stateUpdateError
|
|
3817
|
+
? new AggregateError(
|
|
3818
|
+
[stateUpdateError, error],
|
|
3819
|
+
'Navigation state refresh and screencast restart failed',
|
|
3820
|
+
)
|
|
3821
|
+
: error;
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
if (stateUpdateError) {
|
|
3825
|
+
throw stateUpdateError;
|
|
3274
3826
|
}
|
|
3275
3827
|
}
|
|
3276
3828
|
}
|
|
@@ -3301,7 +3853,10 @@ export class LiveBrowserSession {
|
|
|
3301
3853
|
}
|
|
3302
3854
|
}
|
|
3303
3855
|
|
|
3304
|
-
private async startScreencast(
|
|
3856
|
+
private async startScreencast(
|
|
3857
|
+
tab: IPrivateLiveBrowserTab,
|
|
3858
|
+
authorityArg?: IScreencastAuthority,
|
|
3859
|
+
): Promise<void> {
|
|
3305
3860
|
if (
|
|
3306
3861
|
this.status !== 'running'
|
|
3307
3862
|
|| this.activeTabId !== tab.id
|
|
@@ -3312,80 +3867,254 @@ export class LiveBrowserSession {
|
|
|
3312
3867
|
return;
|
|
3313
3868
|
}
|
|
3314
3869
|
|
|
3315
|
-
|
|
3870
|
+
const authority = authorityArg
|
|
3871
|
+
?? this.createScreencastAuthority(tab, tab.streamLifecycleRevision);
|
|
3872
|
+
let cdpSession: plugins.puppeteer.CDPSession | undefined;
|
|
3873
|
+
let cdpConnection: plugins.puppeteer.Connection | undefined;
|
|
3874
|
+
let frameListener: TScreencastFrameListener | undefined;
|
|
3875
|
+
let cdpSessionDetachedListener: TCdpSessionDetachedListener | undefined;
|
|
3876
|
+
try {
|
|
3877
|
+
this.assertScreencastAuthority(tab, authority);
|
|
3878
|
+
await this.waitForScreencastAuthority(this.ensureTabViewport(tab), authority);
|
|
3879
|
+
this.assertScreencastAuthority(tab, authority);
|
|
3316
3880
|
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3881
|
+
const cdpSessionPromise = tab.page.createCDPSession();
|
|
3882
|
+
void cdpSessionPromise.then(async (createdSession) => {
|
|
3883
|
+
if (
|
|
3884
|
+
tab.screencastAuthority === authority
|
|
3885
|
+
&& !authority.controller.signal.aborted
|
|
3886
|
+
) {
|
|
3887
|
+
return;
|
|
3888
|
+
}
|
|
3889
|
+
this.allowOperationalCdpSessionDetach(createdSession);
|
|
3890
|
+
if (!createdSession.detached) {
|
|
3891
|
+
try {
|
|
3892
|
+
await createdSession.detach();
|
|
3893
|
+
} catch {
|
|
3894
|
+
// Browser shutdown may detach a late-created session first.
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
}).catch(() => {});
|
|
3898
|
+
cdpSession = await this.waitForScreencastAuthority(cdpSessionPromise, authority);
|
|
3899
|
+
this.allowOperationalCdpSessionDetach(cdpSession);
|
|
3900
|
+
this.assertScreencastAuthority(tab, authority);
|
|
3901
|
+
const generation = tab.generation + 1;
|
|
3902
|
+
frameListener = (event) => {
|
|
3903
|
+
this.handleScreencastFrame(tab, cdpSession!, generation, event);
|
|
3904
|
+
};
|
|
3905
|
+
cdpConnection = cdpSession.connection();
|
|
3906
|
+
cdpSessionDetachedListener = (detachedSession) => {
|
|
3907
|
+
if (detachedSession !== cdpSession) {
|
|
3908
|
+
return;
|
|
3909
|
+
}
|
|
3910
|
+
this.handlePossibleCdpDisconnection(
|
|
3911
|
+
tab,
|
|
3912
|
+
cdpSession!,
|
|
3913
|
+
new Error('The tab CDP session disconnected'),
|
|
3914
|
+
);
|
|
3915
|
+
};
|
|
3916
|
+
tab.cdpSession = cdpSession;
|
|
3917
|
+
tab.cdpConnection = cdpConnection;
|
|
3918
|
+
tab.screencastFrameListener = frameListener;
|
|
3919
|
+
tab.cdpSessionDetachedListener = cdpSessionDetachedListener;
|
|
3920
|
+
tab.generation = generation;
|
|
3921
|
+
tab.streaming = true;
|
|
3922
|
+
tab.streamInvalidated = false;
|
|
3923
|
+
cdpSession.on('Page.screencastFrame', frameListener);
|
|
3924
|
+
cdpConnection?.on(
|
|
3925
|
+
plugins.puppeteer.CDPSessionEvent.SessionDetached,
|
|
3926
|
+
cdpSessionDetachedListener,
|
|
3332
3927
|
);
|
|
3333
|
-
};
|
|
3334
|
-
tab.cdpSession = cdpSession;
|
|
3335
|
-
tab.cdpConnection = cdpConnection;
|
|
3336
|
-
tab.screencastFrameListener = frameListener;
|
|
3337
|
-
tab.cdpSessionDetachedListener = cdpSessionDetachedListener;
|
|
3338
|
-
tab.generation = generation;
|
|
3339
|
-
tab.streaming = true;
|
|
3340
|
-
tab.streamInvalidated = false;
|
|
3341
|
-
cdpSession.on('Page.screencastFrame', frameListener);
|
|
3342
|
-
cdpConnection?.on(
|
|
3343
|
-
plugins.puppeteer.CDPSessionEvent.SessionDetached,
|
|
3344
|
-
cdpSessionDetachedListener,
|
|
3345
|
-
);
|
|
3346
3928
|
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3929
|
+
const format = this.options.screencast?.format ?? 'jpeg';
|
|
3930
|
+
await this.waitForScreencastAuthority(
|
|
3931
|
+
cdpSession.send('Page.startScreencast', {
|
|
3932
|
+
format,
|
|
3933
|
+
quality: this.options.screencast?.quality ?? 80,
|
|
3934
|
+
maxWidth: this.options.screencast?.maxWidth,
|
|
3935
|
+
maxHeight: this.options.screencast?.maxHeight,
|
|
3936
|
+
everyNthFrame: this.options.screencast?.everyNthFrame ?? 1,
|
|
3937
|
+
}),
|
|
3938
|
+
authority,
|
|
3939
|
+
);
|
|
3940
|
+
this.assertScreencastAuthority(tab, authority);
|
|
3356
3941
|
this.emitState();
|
|
3357
3942
|
} catch (error) {
|
|
3358
|
-
tab.
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
)
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3943
|
+
if (tab.cdpSession === cdpSession) {
|
|
3944
|
+
tab.streaming = false;
|
|
3945
|
+
tab.cdpSession = undefined;
|
|
3946
|
+
tab.cdpConnection = undefined;
|
|
3947
|
+
tab.screencastFrameListener = undefined;
|
|
3948
|
+
tab.cdpSessionDetachedListener = undefined;
|
|
3949
|
+
}
|
|
3950
|
+
if (tab.screencastAuthority === authority) {
|
|
3951
|
+
this.invalidateScreencast(tab, error);
|
|
3952
|
+
}
|
|
3953
|
+
if (cdpSession) {
|
|
3954
|
+
if (frameListener) {
|
|
3955
|
+
cdpSession.off('Page.screencastFrame', frameListener);
|
|
3956
|
+
}
|
|
3957
|
+
if (cdpConnection && cdpSessionDetachedListener) {
|
|
3958
|
+
cdpConnection.off(
|
|
3959
|
+
plugins.puppeteer.CDPSessionEvent.SessionDetached,
|
|
3960
|
+
cdpSessionDetachedListener,
|
|
3961
|
+
);
|
|
3962
|
+
}
|
|
3963
|
+
await this.retireOutstandingFrames((frame) => frame.cdpSession === cdpSession);
|
|
3964
|
+
await this.waitForCdpFrameAcknowledgements(cdpSession);
|
|
3965
|
+
if (!cdpSession.detached) {
|
|
3966
|
+
try {
|
|
3967
|
+
await cdpSession.detach();
|
|
3968
|
+
} catch {
|
|
3969
|
+
// The target may have closed while screencast startup was failing.
|
|
3970
|
+
}
|
|
3376
3971
|
}
|
|
3377
3972
|
}
|
|
3378
3973
|
throw error;
|
|
3379
3974
|
}
|
|
3380
3975
|
}
|
|
3381
3976
|
|
|
3977
|
+
private canRestoreScreencast(tab: IPrivateLiveBrowserTab): boolean {
|
|
3978
|
+
return this.status === 'running'
|
|
3979
|
+
&& !this.normalStopRequested
|
|
3980
|
+
&& this.activeTabId === tab.id
|
|
3981
|
+
&& this.tabs.get(tab.id) === tab
|
|
3982
|
+
&& tab.status === 'open'
|
|
3983
|
+
&& !tab.closing
|
|
3984
|
+
&& !tab.page.isClosed();
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
private createScreencastAuthority(
|
|
3988
|
+
tab: IPrivateLiveBrowserTab,
|
|
3989
|
+
expectedLifecycleRevision: number,
|
|
3990
|
+
): IScreencastAuthority {
|
|
3991
|
+
if (
|
|
3992
|
+
!this.canRestoreScreencast(tab)
|
|
3993
|
+
|| tab.streaming
|
|
3994
|
+
|| tab.streamLifecycleRevision !== expectedLifecycleRevision
|
|
3995
|
+
) {
|
|
3996
|
+
throw new Error(`Screencast lifecycle authority is unavailable for tab: ${tab.id}`);
|
|
3997
|
+
}
|
|
3998
|
+
const authority: IScreencastAuthority = {
|
|
3999
|
+
revision: expectedLifecycleRevision,
|
|
4000
|
+
controller: new AbortController(),
|
|
4001
|
+
};
|
|
4002
|
+
tab.screencastAuthority = authority;
|
|
4003
|
+
return authority;
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
private assertScreencastAuthority(
|
|
4007
|
+
tab: IPrivateLiveBrowserTab,
|
|
4008
|
+
authority: IScreencastAuthority,
|
|
4009
|
+
): void {
|
|
4010
|
+
if (
|
|
4011
|
+
this.canRestoreScreencast(tab)
|
|
4012
|
+
&& tab.streamLifecycleRevision === authority.revision
|
|
4013
|
+
&& tab.screencastAuthority === authority
|
|
4014
|
+
&& !authority.controller.signal.aborted
|
|
4015
|
+
) {
|
|
4016
|
+
return;
|
|
4017
|
+
}
|
|
4018
|
+
throw authority.controller.signal.aborted
|
|
4019
|
+
? normalizeAbortReason(authority.controller.signal)
|
|
4020
|
+
: new Error(`Screencast lifecycle authority was revoked for tab: ${tab.id}`);
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
private waitForScreencastAuthority<T>(
|
|
4024
|
+
operation: Promise<T>,
|
|
4025
|
+
authority: IScreencastAuthority,
|
|
4026
|
+
): Promise<T> {
|
|
4027
|
+
const signal = authority.controller.signal;
|
|
4028
|
+
if (signal.aborted) {
|
|
4029
|
+
return Promise.reject(normalizeAbortReason(signal));
|
|
4030
|
+
}
|
|
4031
|
+
return new Promise<T>((resolve, reject) => {
|
|
4032
|
+
let settled = false;
|
|
4033
|
+
const finish = (actionArg: () => void): void => {
|
|
4034
|
+
if (settled) return;
|
|
4035
|
+
settled = true;
|
|
4036
|
+
signal.removeEventListener('abort', handleAbort);
|
|
4037
|
+
actionArg();
|
|
4038
|
+
};
|
|
4039
|
+
const handleAbort = (): void => {
|
|
4040
|
+
finish(() => reject(normalizeAbortReason(signal)));
|
|
4041
|
+
};
|
|
4042
|
+
signal.addEventListener('abort', handleAbort, { once: true });
|
|
4043
|
+
operation.then(
|
|
4044
|
+
(value) => finish(() => resolve(value)),
|
|
4045
|
+
(error) => finish(() => reject(error)),
|
|
4046
|
+
);
|
|
4047
|
+
if (signal.aborted) handleAbort();
|
|
4048
|
+
});
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
private invalidateScreencast(tab: IPrivateLiveBrowserTab, reason: unknown): void {
|
|
4052
|
+
tab.streamInvalidated = true;
|
|
4053
|
+
tab.streamLifecycleRevision += 1;
|
|
4054
|
+
const authority = tab.screencastAuthority;
|
|
4055
|
+
tab.screencastAuthority = undefined;
|
|
4056
|
+
if (authority && !authority.controller.signal.aborted) {
|
|
4057
|
+
authority.controller.abort(reason);
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
private waitForScreencastFrame(
|
|
4062
|
+
tab: IPrivateLiveBrowserTab,
|
|
4063
|
+
generation: number,
|
|
4064
|
+
viewportRevision: number,
|
|
4065
|
+
invalidationSignal: AbortSignal,
|
|
4066
|
+
): {
|
|
4067
|
+
promise: Promise<ILiveBrowserFrameIdentity>;
|
|
4068
|
+
cancel: () => void;
|
|
4069
|
+
} {
|
|
4070
|
+
let settled = false;
|
|
4071
|
+
let resolvePromise!: (identityArg: ILiveBrowserFrameIdentity) => void;
|
|
4072
|
+
let rejectPromise!: (errorArg: unknown) => void;
|
|
4073
|
+
const promise = new Promise<ILiveBrowserFrameIdentity>((resolve, reject) => {
|
|
4074
|
+
resolvePromise = resolve;
|
|
4075
|
+
rejectPromise = reject;
|
|
4076
|
+
});
|
|
4077
|
+
void promise.catch(() => {});
|
|
4078
|
+
let unsubscribe: () => void = () => {};
|
|
4079
|
+
const finish = (actionArg: () => void) => {
|
|
4080
|
+
if (settled) return;
|
|
4081
|
+
settled = true;
|
|
4082
|
+
invalidationSignal.removeEventListener('abort', handleAbort);
|
|
4083
|
+
unsubscribe();
|
|
4084
|
+
actionArg();
|
|
4085
|
+
};
|
|
4086
|
+
const handleAbort = () => {
|
|
4087
|
+
finish(() => rejectPromise(normalizeAbortReason(invalidationSignal)));
|
|
4088
|
+
};
|
|
4089
|
+
unsubscribe = this.onEvent((eventArg) => {
|
|
4090
|
+
if (
|
|
4091
|
+
eventArg.type !== 'frame'
|
|
4092
|
+
|| eventArg.frame.tabId !== tab.id
|
|
4093
|
+
|| eventArg.frame.generation !== generation
|
|
4094
|
+
|| eventArg.frame.viewportRevision !== viewportRevision
|
|
4095
|
+
) return;
|
|
4096
|
+
finish(() => resolvePromise({
|
|
4097
|
+
tabId: eventArg.frame.tabId,
|
|
4098
|
+
sequence: eventArg.frame.sequence,
|
|
4099
|
+
generation: eventArg.frame.generation,
|
|
4100
|
+
viewportRevision: eventArg.frame.viewportRevision,
|
|
4101
|
+
}));
|
|
4102
|
+
});
|
|
4103
|
+
invalidationSignal.addEventListener('abort', handleAbort, { once: true });
|
|
4104
|
+
if (invalidationSignal.aborted) handleAbort();
|
|
4105
|
+
return {
|
|
4106
|
+
promise,
|
|
4107
|
+
cancel: () => finish(() => rejectPromise(new Error('Screencast frame wait was cancelled'))),
|
|
4108
|
+
};
|
|
4109
|
+
}
|
|
4110
|
+
|
|
3382
4111
|
private async stopScreencast(tab: IPrivateLiveBrowserTab): Promise<void> {
|
|
3383
4112
|
const cdpSession = tab.cdpSession;
|
|
3384
4113
|
const cdpConnection = tab.cdpConnection;
|
|
3385
4114
|
const frameListener = tab.screencastFrameListener;
|
|
3386
4115
|
const cdpSessionDetachedListener = tab.cdpSessionDetachedListener;
|
|
3387
4116
|
tab.streaming = false;
|
|
3388
|
-
tab
|
|
4117
|
+
this.invalidateScreencast(tab, new Error('Screencast stopped'));
|
|
3389
4118
|
tab.cdpSession = undefined;
|
|
3390
4119
|
tab.cdpConnection = undefined;
|
|
3391
4120
|
tab.screencastFrameListener = undefined;
|
|
@@ -3595,7 +4324,7 @@ export class LiveBrowserSession {
|
|
|
3595
4324
|
) {
|
|
3596
4325
|
return;
|
|
3597
4326
|
}
|
|
3598
|
-
tab
|
|
4327
|
+
this.invalidateScreencast(tab, error);
|
|
3599
4328
|
this.retireFramesForTabInBackground(tab.id);
|
|
3600
4329
|
this.scheduleOperation(async () => {
|
|
3601
4330
|
if (!this.tabs.has(tab.id) || tab.cdpSession !== cdpSession) {
|
|
@@ -4047,5 +4776,13 @@ export class LiveBrowserSession {
|
|
|
4047
4776
|
liveBrowserMaxOutstandingFrames,
|
|
4048
4777
|
);
|
|
4049
4778
|
}
|
|
4779
|
+
if (options.firstFrameTimeoutMs !== undefined) {
|
|
4780
|
+
validateInteger(
|
|
4781
|
+
options.firstFrameTimeoutMs,
|
|
4782
|
+
'screencast.firstFrameTimeoutMs',
|
|
4783
|
+
100,
|
|
4784
|
+
maxTimeoutMs,
|
|
4785
|
+
);
|
|
4786
|
+
}
|
|
4050
4787
|
}
|
|
4051
4788
|
}
|