@push.rocks/smartpuppeteer 2.5.0 → 2.7.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 +20 -2
- package/dist_ts/smartpuppeteer.classes.livebrowsersession.js +627 -170
- package/dist_ts/smartpuppeteer.interfaces.livebrowser.d.ts +2 -0
- package/package.json +2 -2
- package/readme.hints.md +4 -0
- package/readme.md +22 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartpuppeteer.classes.livebrowsersession.ts +865 -174
- package/ts/smartpuppeteer.interfaces.livebrowser.ts +6 -0
|
@@ -33,6 +33,8 @@ import type {
|
|
|
33
33
|
ILiveBrowserOperationOptions,
|
|
34
34
|
ILiveBrowserPressOptions,
|
|
35
35
|
ILiveBrowserProcessState,
|
|
36
|
+
ILiveBrowserScreencastOptions,
|
|
37
|
+
ILiveBrowserScreencastUpdateOptions,
|
|
36
38
|
ILiveBrowserSessionOptions,
|
|
37
39
|
ILiveBrowserSnapshot,
|
|
38
40
|
ILiveBrowserSnapshotOptions,
|
|
@@ -64,6 +66,7 @@ const maxSelectorLength = 4096;
|
|
|
64
66
|
const maxTextLength = 32768;
|
|
65
67
|
const maxUrlLength = 16384;
|
|
66
68
|
const maxTimeoutMs = 60000;
|
|
69
|
+
const maxWheelDelta = 1000000;
|
|
67
70
|
const frameAcknowledgementTimeoutMs = 5000;
|
|
68
71
|
const defaultFirstFrameTimeoutMs = 5000;
|
|
69
72
|
const maxQueuedPublicOperations = 64;
|
|
@@ -82,10 +85,12 @@ const maxProxySecuritySessions = 256;
|
|
|
82
85
|
const maxTrackedProxyRequests = 1024;
|
|
83
86
|
const maxTotalTrackedProxyRequests = 4096;
|
|
84
87
|
const maxProxySecurityOperations = 2048;
|
|
88
|
+
const maxTrackedServiceWorkerVersions = 512;
|
|
89
|
+
const proxySecurityCommandTimeoutMs = 5000;
|
|
90
|
+
const proxySecurityCommandOptions = { timeout: proxySecurityCommandTimeoutMs } as const;
|
|
85
91
|
const proxySeedTargetTypes = new Set([
|
|
86
92
|
'background_page',
|
|
87
93
|
'page',
|
|
88
|
-
'service_worker',
|
|
89
94
|
'shared_worker',
|
|
90
95
|
'webview',
|
|
91
96
|
]);
|
|
@@ -99,6 +104,9 @@ type TProxyAuthRequiredEvent = plugins.puppeteer.Protocol.Fetch.AuthRequiredEven
|
|
|
99
104
|
type TProxyRequestPausedEvent = plugins.puppeteer.Protocol.Fetch.RequestPausedEvent;
|
|
100
105
|
type TNetworkLoadingFinishedEvent = plugins.puppeteer.Protocol.Network.LoadingFinishedEvent;
|
|
101
106
|
type TNetworkLoadingFailedEvent = plugins.puppeteer.Protocol.Network.LoadingFailedEvent;
|
|
107
|
+
type TServiceWorkerVersionUpdatedEvent = (
|
|
108
|
+
plugins.puppeteer.Protocol.ServiceWorker.WorkerVersionUpdatedEvent
|
|
109
|
+
);
|
|
102
110
|
|
|
103
111
|
interface IProxySecuritySession {
|
|
104
112
|
session: plugins.puppeteer.CDPSession;
|
|
@@ -108,6 +116,7 @@ interface IProxySecuritySession {
|
|
|
108
116
|
fetchEnabled: boolean;
|
|
109
117
|
allowLiveTargetDetach: boolean;
|
|
110
118
|
ownedByProxySecurity: boolean;
|
|
119
|
+
retireWhenIdle: boolean;
|
|
111
120
|
attemptedAuthentications: Set<string>;
|
|
112
121
|
requestIdsByNetworkId: Map<string, string>;
|
|
113
122
|
authRequiredListener: (event: TProxyAuthRequiredEvent) => void;
|
|
@@ -130,6 +139,34 @@ interface IScreencastAuthority {
|
|
|
130
139
|
controller: AbortController;
|
|
131
140
|
}
|
|
132
141
|
|
|
142
|
+
interface IPendingWheelDispatch {
|
|
143
|
+
cdpSession: plugins.puppeteer.CDPSession;
|
|
144
|
+
generation: number;
|
|
145
|
+
viewportRevision: number;
|
|
146
|
+
x: number;
|
|
147
|
+
y: number;
|
|
148
|
+
deltaX: number;
|
|
149
|
+
deltaY: number;
|
|
150
|
+
modifiers: number;
|
|
151
|
+
promise: Promise<void>;
|
|
152
|
+
resolve: () => void;
|
|
153
|
+
reject: (error: unknown) => void;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface IQueuedWheelInput {
|
|
157
|
+
kind: 'wheel';
|
|
158
|
+
dispatch: IPendingWheelDispatch;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface IQueuedEventInput {
|
|
162
|
+
kind: 'event';
|
|
163
|
+
send: () => Promise<void>;
|
|
164
|
+
resolve: () => void;
|
|
165
|
+
reject: (error: unknown) => void;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
type TQueuedTabInput = IQueuedWheelInput | IQueuedEventInput;
|
|
169
|
+
|
|
133
170
|
interface IPrivateLiveBrowserTab {
|
|
134
171
|
id: string;
|
|
135
172
|
page: plugins.puppeteer.Page;
|
|
@@ -146,13 +183,17 @@ interface IPrivateLiveBrowserTab {
|
|
|
146
183
|
closing: boolean;
|
|
147
184
|
stateUpdateQueued: boolean;
|
|
148
185
|
stateUpdatePending: boolean;
|
|
149
|
-
|
|
186
|
+
navigationResetRevision: number;
|
|
187
|
+
restoredNavigationRevision: number;
|
|
150
188
|
evaluationExecutionContextId?: number;
|
|
151
189
|
securityCdpSession?: plugins.puppeteer.CDPSession;
|
|
152
190
|
cdpSession?: plugins.puppeteer.CDPSession;
|
|
153
191
|
cdpConnection?: plugins.puppeteer.Connection;
|
|
154
192
|
screencastFrameListener?: TScreencastFrameListener;
|
|
155
193
|
cdpSessionDetachedListener?: TCdpSessionDetachedListener;
|
|
194
|
+
inputQueue: TQueuedTabInput[];
|
|
195
|
+
inputPumpActive: boolean;
|
|
196
|
+
wheelDispatchInFlight?: Promise<void>;
|
|
156
197
|
removeListeners: Array<() => void>;
|
|
157
198
|
}
|
|
158
199
|
|
|
@@ -379,6 +420,80 @@ const createPuppeteerViewport = (
|
|
|
379
420
|
hasTouch: false,
|
|
380
421
|
});
|
|
381
422
|
|
|
423
|
+
const validateScreencastOptions = (
|
|
424
|
+
options: ILiveBrowserScreencastOptions | undefined,
|
|
425
|
+
): void => {
|
|
426
|
+
if (!options) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (options.format !== undefined && options.format !== 'jpeg' && options.format !== 'png') {
|
|
430
|
+
throw new Error('screencast.format must be jpeg or png');
|
|
431
|
+
}
|
|
432
|
+
if (options.quality !== undefined) {
|
|
433
|
+
validateInteger(options.quality, 'screencast.quality', 0, 100);
|
|
434
|
+
}
|
|
435
|
+
if (options.maxWidth !== undefined) {
|
|
436
|
+
validateInteger(options.maxWidth, 'screencast.maxWidth', 1, maxViewportWidth);
|
|
437
|
+
}
|
|
438
|
+
if (options.maxHeight !== undefined) {
|
|
439
|
+
validateInteger(options.maxHeight, 'screencast.maxHeight', 1, maxViewportHeight);
|
|
440
|
+
}
|
|
441
|
+
if (
|
|
442
|
+
options.maxWidth !== undefined
|
|
443
|
+
&& options.maxHeight !== undefined
|
|
444
|
+
&& options.maxWidth * options.maxHeight > maxViewportPixelArea
|
|
445
|
+
) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
`screencast pixel area must not exceed ${maxViewportPixelArea} pixels`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
if (options.everyNthFrame !== undefined) {
|
|
451
|
+
validateInteger(options.everyNthFrame, 'screencast.everyNthFrame', 1, 100);
|
|
452
|
+
}
|
|
453
|
+
if (options.maxOutstandingFrames !== undefined) {
|
|
454
|
+
validateInteger(
|
|
455
|
+
options.maxOutstandingFrames,
|
|
456
|
+
'screencast.maxOutstandingFrames',
|
|
457
|
+
1,
|
|
458
|
+
liveBrowserMaxOutstandingFrames,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
if (options.firstFrameTimeoutMs !== undefined) {
|
|
462
|
+
validateInteger(
|
|
463
|
+
options.firstFrameTimeoutMs,
|
|
464
|
+
'screencast.firstFrameTimeoutMs',
|
|
465
|
+
100,
|
|
466
|
+
maxTimeoutMs,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const screencastUpdateKeys = ['quality', 'maxWidth', 'maxHeight', 'everyNthFrame'] as const;
|
|
472
|
+
|
|
473
|
+
const normalizeScreencastUpdate = (
|
|
474
|
+
optionsArg: ILiveBrowserScreencastUpdateOptions,
|
|
475
|
+
): ILiveBrowserScreencastUpdateOptions => {
|
|
476
|
+
if (!optionsArg || typeof optionsArg !== 'object' || Array.isArray(optionsArg)) {
|
|
477
|
+
throw new Error('screencast update options must be an object');
|
|
478
|
+
}
|
|
479
|
+
const update: ILiveBrowserScreencastUpdateOptions = {};
|
|
480
|
+
for (const key of Object.keys(optionsArg)) {
|
|
481
|
+
if (!(screencastUpdateKeys as readonly string[]).includes(key)) {
|
|
482
|
+
throw new Error(`Unknown screencast update option: ${key}`);
|
|
483
|
+
}
|
|
484
|
+
const value = optionsArg[key as keyof ILiveBrowserScreencastUpdateOptions];
|
|
485
|
+
if (value !== undefined) {
|
|
486
|
+
update[key as keyof ILiveBrowserScreencastUpdateOptions] = value;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
validateScreencastOptions(update);
|
|
490
|
+
return update;
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const clampWheelDelta = (value: number): number => {
|
|
494
|
+
return Math.max(-maxWheelDelta, Math.min(maxWheelDelta, value));
|
|
495
|
+
};
|
|
496
|
+
|
|
382
497
|
export class LiveBrowserSession {
|
|
383
498
|
private readonly options: ILiveBrowserSessionOptions;
|
|
384
499
|
private readonly eventListeners = new Set<TLiveBrowserEventListener>();
|
|
@@ -396,8 +511,18 @@ export class LiveBrowserSession {
|
|
|
396
511
|
private browserSecurityCdpSession?: plugins.puppeteer.CDPSession;
|
|
397
512
|
private browserSecurityConnection?: plugins.puppeteer.Connection;
|
|
398
513
|
private browserSecuritySessionAttachedListener?: (session: plugins.puppeteer.CDPSession) => void;
|
|
514
|
+
private browserSecurityOwnedSessionAttachedListener?: (
|
|
515
|
+
session: plugins.puppeteer.CDPSession,
|
|
516
|
+
) => void;
|
|
399
517
|
private browserSecuritySessionDetachedListener?: TCdpSessionDetachedListener;
|
|
518
|
+
private browserSecurityWorkerLifecycleCdpSession?: plugins.puppeteer.CDPSession;
|
|
519
|
+
private browserSecurityWorkerVersionUpdatedListener?: (
|
|
520
|
+
event: TServiceWorkerVersionUpdatedEvent,
|
|
521
|
+
) => void;
|
|
400
522
|
private readonly proxySecuritySessions = new Map<string, IProxySecuritySession>();
|
|
523
|
+
private readonly ownedProxySecuritySessions = new Set<plugins.puppeteer.CDPSession>();
|
|
524
|
+
private readonly browserSecurityParentOwnedSessions = new Set<plugins.puppeteer.CDPSession>();
|
|
525
|
+
private readonly serviceWorkerTargetIdsByVersionId = new Map<string, string>();
|
|
401
526
|
private readonly proxySecurityOperations = new Set<Promise<void>>();
|
|
402
527
|
private proxySecurityGeneration = 0;
|
|
403
528
|
private proxySecurityStopping = false;
|
|
@@ -507,7 +632,7 @@ export class LiveBrowserSession {
|
|
|
507
632
|
: undefined,
|
|
508
633
|
};
|
|
509
634
|
this.viewport = { ...viewport };
|
|
510
|
-
this.
|
|
635
|
+
validateScreencastOptions(this.options.screencast);
|
|
511
636
|
this.maxOutstandingFrames = this.options.screencast?.maxOutstandingFrames
|
|
512
637
|
?? liveBrowserDefaultMaxOutstandingFrames;
|
|
513
638
|
}
|
|
@@ -838,58 +963,95 @@ export class LiveBrowserSession {
|
|
|
838
963
|
public refreshScreencast(
|
|
839
964
|
operationOptions: ILiveBrowserOperationOptions = {},
|
|
840
965
|
): Promise<ILiveBrowserFrameIdentity> {
|
|
966
|
+
return this.enqueuePublicOperation(
|
|
967
|
+
(signal) => this.restartActiveScreencast(signal),
|
|
968
|
+
operationOptions,
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
public async updateScreencastOptions(
|
|
973
|
+
optionsArg: ILiveBrowserScreencastUpdateOptions,
|
|
974
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
975
|
+
): Promise<ILiveBrowserFrameIdentity | null> {
|
|
976
|
+
const update = normalizeScreencastUpdate(optionsArg);
|
|
841
977
|
return this.enqueuePublicOperation(async (signal) => {
|
|
842
978
|
signal.throwIfAborted();
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
979
|
+
const screencast: ILiveBrowserScreencastOptions = {
|
|
980
|
+
...this.options.screencast,
|
|
981
|
+
...update,
|
|
982
|
+
};
|
|
983
|
+
validateScreencastOptions(screencast);
|
|
984
|
+
this.options.screencast = screencast;
|
|
985
|
+
const activeTab = this.status === 'running' && this.activeTabId
|
|
986
|
+
? this.tabs.get(this.activeTabId)
|
|
987
|
+
: undefined;
|
|
988
|
+
if (!activeTab?.streaming) {
|
|
989
|
+
return null;
|
|
846
990
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
991
|
+
return this.restartActiveScreencast(signal);
|
|
992
|
+
}, operationOptions);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
private async restartActiveScreencast(
|
|
996
|
+
signal: AbortSignal,
|
|
997
|
+
): Promise<ILiveBrowserFrameIdentity> {
|
|
998
|
+
signal.throwIfAborted();
|
|
999
|
+
const tab = this.requireActiveTab();
|
|
1000
|
+
let firstFrame: ReturnType<LiveBrowserSession['waitForScreencastFrame']> | undefined;
|
|
1001
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
1002
|
+
let restartPromise: Promise<void> | undefined;
|
|
1003
|
+
try {
|
|
1004
|
+
let refreshLifecycleRevision = tab.streamLifecycleRevision;
|
|
1005
|
+
if (tab.streaming) {
|
|
851
1006
|
await this.stopScreencast(tab);
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
])
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1007
|
+
refreshLifecycleRevision += 1;
|
|
1008
|
+
}
|
|
1009
|
+
if (
|
|
1010
|
+
!this.canRestoreScreencast(tab)
|
|
1011
|
+
|| tab.streaming
|
|
1012
|
+
|| tab.streamLifecycleRevision !== refreshLifecycleRevision
|
|
1013
|
+
) {
|
|
1014
|
+
throw new Error(`Screencast lifecycle changed while refreshing tab: ${tab.id}`);
|
|
1015
|
+
}
|
|
1016
|
+
const authority = this.createScreencastAuthority(tab, refreshLifecycleRevision);
|
|
1017
|
+
const generation = tab.generation + 1;
|
|
1018
|
+
firstFrame = this.waitForScreencastFrame(
|
|
1019
|
+
tab,
|
|
1020
|
+
generation,
|
|
1021
|
+
this.viewportRevision,
|
|
1022
|
+
authority.controller.signal,
|
|
1023
|
+
);
|
|
1024
|
+
const refreshTimeoutMs = this.options.screencast?.firstFrameTimeoutMs
|
|
1025
|
+
?? defaultFirstFrameTimeoutMs;
|
|
1026
|
+
const refreshTimeout = new Promise<never>((_resolve, reject) => {
|
|
1027
|
+
timeout = setTimeout(() => {
|
|
1028
|
+
reject(new Error(
|
|
1029
|
+
`Screencast generation ${generation} did not restart and produce a frame within ${
|
|
1030
|
+
refreshTimeoutMs
|
|
1031
|
+
}ms`,
|
|
1032
|
+
));
|
|
1033
|
+
}, refreshTimeoutMs);
|
|
1034
|
+
});
|
|
1035
|
+
restartPromise = this.startScreencast(tab, authority);
|
|
1036
|
+
const [, identity] = await Promise.race([
|
|
1037
|
+
Promise.all([restartPromise, firstFrame.promise]),
|
|
1038
|
+
refreshTimeout,
|
|
1039
|
+
]);
|
|
1040
|
+
return identity;
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
if (!this.canRestoreScreencast(tab)) throw error;
|
|
1043
|
+
await this.abandonScreencastRestart(tab, restartPromise, error);
|
|
1044
|
+
// Browser loss is the only refresh failure that is not tab-recoverable; the
|
|
1045
|
+
// disconnect listener normally revokes restoration first, so this stays a guard.
|
|
1046
|
+
const fatal = this.browser !== undefined && !this.browser.connected;
|
|
1047
|
+
const refreshError: ILiveBrowserError = {
|
|
1048
|
+
code: 'screencast_refresh_failed',
|
|
1049
|
+
message: normalizeErrorMessage(error),
|
|
1050
|
+
fatal,
|
|
1051
|
+
tabId: tab.id,
|
|
1052
|
+
};
|
|
1053
|
+
this.emitError(refreshError);
|
|
1054
|
+
if (fatal) {
|
|
893
1055
|
void this.requestShutdown(refreshError).catch((shutdownError) => {
|
|
894
1056
|
this.emitError({
|
|
895
1057
|
code: 'screencast_refresh_shutdown_failed',
|
|
@@ -898,12 +1060,27 @@ export class LiveBrowserSession {
|
|
|
898
1060
|
tabId: tab.id,
|
|
899
1061
|
});
|
|
900
1062
|
});
|
|
901
|
-
throw error;
|
|
902
|
-
} finally {
|
|
903
|
-
if (timeout) clearTimeout(timeout);
|
|
904
|
-
firstFrame?.cancel();
|
|
905
1063
|
}
|
|
906
|
-
|
|
1064
|
+
throw error;
|
|
1065
|
+
} finally {
|
|
1066
|
+
if (timeout) clearTimeout(timeout);
|
|
1067
|
+
firstFrame?.cancel();
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
private async abandonScreencastRestart(
|
|
1072
|
+
tab: IPrivateLiveBrowserTab,
|
|
1073
|
+
restartPromise: Promise<void> | undefined,
|
|
1074
|
+
reason: unknown,
|
|
1075
|
+
): Promise<void> {
|
|
1076
|
+
// Revoke the restart authority so an in-flight startScreencast() unwinds, then
|
|
1077
|
+
// retire anything it already established. The tab stays open and invalidated so
|
|
1078
|
+
// a later refresh, viewport change, or activation can start a new generation.
|
|
1079
|
+
this.invalidateScreencast(tab, reason);
|
|
1080
|
+
if (restartPromise) {
|
|
1081
|
+
await restartPromise.catch(() => {});
|
|
1082
|
+
}
|
|
1083
|
+
await this.stopScreencast(tab);
|
|
907
1084
|
}
|
|
908
1085
|
|
|
909
1086
|
public async createTab(
|
|
@@ -1133,43 +1310,73 @@ export class LiveBrowserSession {
|
|
|
1133
1310
|
const clickCount = input.clickCount === undefined
|
|
1134
1311
|
? undefined
|
|
1135
1312
|
: validateInteger(input.clickCount, 'clickCount', 0, 3);
|
|
1313
|
+
const modifiers = this.createModifierMask(input.modifiers);
|
|
1136
1314
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1315
|
+
await this.dispatchTabInput(tab, cdpSession, () => cdpSession.send(
|
|
1316
|
+
'Input.dispatchMouseEvent',
|
|
1317
|
+
{
|
|
1139
1318
|
type,
|
|
1140
1319
|
x: input.x,
|
|
1141
1320
|
y: input.y,
|
|
1142
1321
|
button,
|
|
1143
1322
|
buttons,
|
|
1144
1323
|
clickCount,
|
|
1145
|
-
modifiers
|
|
1324
|
+
modifiers,
|
|
1146
1325
|
pointerType: 'mouse',
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
this.handlePossibleCdpDisconnection(tab, cdpSession, error);
|
|
1150
|
-
throw error;
|
|
1151
|
-
}
|
|
1326
|
+
},
|
|
1327
|
+
));
|
|
1152
1328
|
}
|
|
1153
1329
|
|
|
1154
1330
|
public async dispatchWheel(input: ILiveBrowserWheelInput): Promise<void> {
|
|
1155
1331
|
const { tab, cdpSession } = this.requireRawInputTarget(input);
|
|
1156
1332
|
this.validateCoordinates(input.x, input.y);
|
|
1157
|
-
validateFiniteNumber(input.deltaX, 'deltaX', -
|
|
1158
|
-
validateFiniteNumber(input.deltaY, 'deltaY', -
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1333
|
+
const deltaX = validateFiniteNumber(input.deltaX, 'deltaX', -maxWheelDelta, maxWheelDelta);
|
|
1334
|
+
const deltaY = validateFiniteNumber(input.deltaY, 'deltaY', -maxWheelDelta, maxWheelDelta);
|
|
1335
|
+
const modifiers = this.createModifierMask(input.modifiers);
|
|
1336
|
+
|
|
1337
|
+
// Queued input only waits while an earlier wheel dispatch is in flight; wheel
|
|
1338
|
+
// input for the same stream merges into a wheel entry still waiting at the tail.
|
|
1339
|
+
const lastQueuedInput = tab.inputQueue[tab.inputQueue.length - 1];
|
|
1340
|
+
if (
|
|
1341
|
+
lastQueuedInput?.kind === 'wheel'
|
|
1342
|
+
&& lastQueuedInput.dispatch.cdpSession === cdpSession
|
|
1343
|
+
&& lastQueuedInput.dispatch.generation === input.generation
|
|
1344
|
+
&& lastQueuedInput.dispatch.viewportRevision === input.viewportRevision
|
|
1345
|
+
) {
|
|
1346
|
+
const pendingDispatch = lastQueuedInput.dispatch;
|
|
1347
|
+
pendingDispatch.x = input.x;
|
|
1348
|
+
pendingDispatch.y = input.y;
|
|
1349
|
+
pendingDispatch.deltaX = clampWheelDelta(pendingDispatch.deltaX + deltaX);
|
|
1350
|
+
pendingDispatch.deltaY = clampWheelDelta(pendingDispatch.deltaY + deltaY);
|
|
1351
|
+
pendingDispatch.modifiers = modifiers;
|
|
1352
|
+
return pendingDispatch.promise;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
let resolveDispatch!: () => void;
|
|
1356
|
+
let rejectDispatch!: (error: unknown) => void;
|
|
1357
|
+
const promise = new Promise<void>((resolve, reject) => {
|
|
1358
|
+
resolveDispatch = resolve;
|
|
1359
|
+
rejectDispatch = reject;
|
|
1360
|
+
});
|
|
1361
|
+
void promise.catch(() => {});
|
|
1362
|
+
tab.inputQueue.push({
|
|
1363
|
+
kind: 'wheel',
|
|
1364
|
+
dispatch: {
|
|
1365
|
+
cdpSession,
|
|
1366
|
+
generation: input.generation,
|
|
1367
|
+
viewportRevision: input.viewportRevision,
|
|
1162
1368
|
x: input.x,
|
|
1163
1369
|
y: input.y,
|
|
1164
|
-
deltaX
|
|
1165
|
-
deltaY
|
|
1166
|
-
modifiers
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1370
|
+
deltaX,
|
|
1371
|
+
deltaY,
|
|
1372
|
+
modifiers,
|
|
1373
|
+
promise,
|
|
1374
|
+
resolve: resolveDispatch,
|
|
1375
|
+
reject: rejectDispatch,
|
|
1376
|
+
},
|
|
1377
|
+
});
|
|
1378
|
+
this.pumpTabInput(tab);
|
|
1379
|
+
return promise;
|
|
1173
1380
|
}
|
|
1174
1381
|
|
|
1175
1382
|
public async dispatchKey(input: ILiveBrowserKeyInput): Promise<void> {
|
|
@@ -1203,9 +1410,11 @@ export class LiveBrowserSession {
|
|
|
1203
1410
|
: validateInteger(input.location, 'location', 0, 3);
|
|
1204
1411
|
validateOptionalBoolean(input.autoRepeat, 'autoRepeat');
|
|
1205
1412
|
validateOptionalBoolean(input.isKeypad, 'isKeypad');
|
|
1413
|
+
const modifiers = this.createModifierMask(input.modifiers);
|
|
1206
1414
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1415
|
+
await this.dispatchTabInput(tab, cdpSession, () => cdpSession.send(
|
|
1416
|
+
'Input.dispatchKeyEvent',
|
|
1417
|
+
{
|
|
1209
1418
|
type,
|
|
1210
1419
|
key,
|
|
1211
1420
|
code,
|
|
@@ -1216,22 +1425,97 @@ export class LiveBrowserSession {
|
|
|
1216
1425
|
autoRepeat: input.autoRepeat,
|
|
1217
1426
|
isKeypad: input.isKeypad,
|
|
1218
1427
|
location,
|
|
1219
|
-
modifiers
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
this.handlePossibleCdpDisconnection(tab, cdpSession, error);
|
|
1223
|
-
throw error;
|
|
1224
|
-
}
|
|
1428
|
+
modifiers,
|
|
1429
|
+
},
|
|
1430
|
+
));
|
|
1225
1431
|
}
|
|
1226
1432
|
|
|
1227
1433
|
public async insertText(input: ILiveBrowserInsertTextInput): Promise<void> {
|
|
1228
1434
|
const { tab, cdpSession } = this.requireRawInputTarget(input);
|
|
1229
1435
|
const text = validateBoundedString(input.text, 'text', 0, maxTextLength);
|
|
1436
|
+
await this.dispatchTabInput(tab, cdpSession, () => cdpSession.send(
|
|
1437
|
+
'Input.insertText',
|
|
1438
|
+
{ text },
|
|
1439
|
+
));
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
private dispatchTabInput(
|
|
1443
|
+
tab: IPrivateLiveBrowserTab,
|
|
1444
|
+
cdpSession: plugins.puppeteer.CDPSession,
|
|
1445
|
+
send: () => Promise<unknown>,
|
|
1446
|
+
): Promise<void> {
|
|
1447
|
+
const guardedSend = async (): Promise<void> => {
|
|
1448
|
+
try {
|
|
1449
|
+
await send();
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
this.handlePossibleCdpDisconnection(tab, cdpSession, error);
|
|
1452
|
+
throw error;
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
// Chromium does not keep a pipelined mouse or key event behind an earlier wheel
|
|
1456
|
+
// command, so any input behind wheel input waits until that wheel dispatch settles.
|
|
1457
|
+
if (tab.inputQueue.length === 0 && !tab.wheelDispatchInFlight) {
|
|
1458
|
+
return guardedSend();
|
|
1459
|
+
}
|
|
1460
|
+
return new Promise<void>((resolve, reject) => {
|
|
1461
|
+
tab.inputQueue.push({ kind: 'event', send: guardedSend, resolve, reject });
|
|
1462
|
+
this.pumpTabInput(tab);
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
private pumpTabInput(tab: IPrivateLiveBrowserTab): void {
|
|
1467
|
+
if (tab.inputPumpActive) {
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
tab.inputPumpActive = true;
|
|
1471
|
+
void (async () => {
|
|
1472
|
+
try {
|
|
1473
|
+
while (tab.inputQueue.length > 0) {
|
|
1474
|
+
const wheelDispatchInFlight = tab.wheelDispatchInFlight;
|
|
1475
|
+
if (wheelDispatchInFlight) {
|
|
1476
|
+
await wheelDispatchInFlight;
|
|
1477
|
+
if (tab.wheelDispatchInFlight === wheelDispatchInFlight) {
|
|
1478
|
+
tab.wheelDispatchInFlight = undefined;
|
|
1479
|
+
}
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
const nextInput = tab.inputQueue.shift()!;
|
|
1483
|
+
if (nextInput.kind === 'wheel') {
|
|
1484
|
+
const inFlight = this.performWheelDispatch(tab, nextInput.dispatch);
|
|
1485
|
+
tab.wheelDispatchInFlight = inFlight;
|
|
1486
|
+
void inFlight.then(() => {
|
|
1487
|
+
if (tab.wheelDispatchInFlight === inFlight) {
|
|
1488
|
+
tab.wheelDispatchInFlight = undefined;
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
} else {
|
|
1492
|
+
nextInput.send().then(nextInput.resolve, nextInput.reject);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
} finally {
|
|
1496
|
+
tab.inputPumpActive = false;
|
|
1497
|
+
}
|
|
1498
|
+
})();
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
private async performWheelDispatch(
|
|
1502
|
+
tab: IPrivateLiveBrowserTab,
|
|
1503
|
+
dispatch: IPendingWheelDispatch,
|
|
1504
|
+
): Promise<void> {
|
|
1230
1505
|
try {
|
|
1231
|
-
await cdpSession.send('Input.
|
|
1506
|
+
await dispatch.cdpSession.send('Input.dispatchMouseEvent', {
|
|
1507
|
+
type: 'mouseWheel',
|
|
1508
|
+
x: dispatch.x,
|
|
1509
|
+
y: dispatch.y,
|
|
1510
|
+
deltaX: dispatch.deltaX,
|
|
1511
|
+
deltaY: dispatch.deltaY,
|
|
1512
|
+
modifiers: dispatch.modifiers,
|
|
1513
|
+
pointerType: 'mouse',
|
|
1514
|
+
});
|
|
1515
|
+
dispatch.resolve();
|
|
1232
1516
|
} catch (error) {
|
|
1233
|
-
this.handlePossibleCdpDisconnection(tab, cdpSession, error);
|
|
1234
|
-
|
|
1517
|
+
this.handlePossibleCdpDisconnection(tab, dispatch.cdpSession, error);
|
|
1518
|
+
dispatch.reject(error);
|
|
1235
1519
|
}
|
|
1236
1520
|
}
|
|
1237
1521
|
|
|
@@ -2276,7 +2560,10 @@ export class LiveBrowserSession {
|
|
|
2276
2560
|
tab.securityCdpSession = undefined;
|
|
2277
2561
|
if (securityCdpSession && !securityCdpSession.detached) {
|
|
2278
2562
|
try {
|
|
2279
|
-
await
|
|
2563
|
+
await this.detachCdpSessionWithTimeout(
|
|
2564
|
+
securityCdpSession,
|
|
2565
|
+
'page security session',
|
|
2566
|
+
);
|
|
2280
2567
|
} catch {
|
|
2281
2568
|
// Browser lifetime cancellation may close the target before explicit detach settles.
|
|
2282
2569
|
}
|
|
@@ -2346,11 +2633,18 @@ export class LiveBrowserSession {
|
|
|
2346
2633
|
if (!proxyCredentials) {
|
|
2347
2634
|
try {
|
|
2348
2635
|
if (denyPermissions) {
|
|
2349
|
-
await cdpSession.send(
|
|
2636
|
+
await cdpSession.send(
|
|
2637
|
+
'Browser.grantPermissions',
|
|
2638
|
+
{ permissions: [] },
|
|
2639
|
+
proxySecurityCommandOptions,
|
|
2640
|
+
);
|
|
2350
2641
|
}
|
|
2351
2642
|
} finally {
|
|
2352
2643
|
if (!cdpSession.detached) {
|
|
2353
|
-
await
|
|
2644
|
+
await this.detachCdpSessionWithTimeout(
|
|
2645
|
+
cdpSession,
|
|
2646
|
+
'browser permission session',
|
|
2647
|
+
);
|
|
2354
2648
|
}
|
|
2355
2649
|
}
|
|
2356
2650
|
return;
|
|
@@ -2365,7 +2659,11 @@ export class LiveBrowserSession {
|
|
|
2365
2659
|
this.browserSecurityCdpSession = cdpSession;
|
|
2366
2660
|
try {
|
|
2367
2661
|
if (denyPermissions) {
|
|
2368
|
-
await cdpSession.send(
|
|
2662
|
+
await cdpSession.send(
|
|
2663
|
+
'Browser.grantPermissions',
|
|
2664
|
+
{ permissions: [] },
|
|
2665
|
+
proxySecurityCommandOptions,
|
|
2666
|
+
);
|
|
2369
2667
|
}
|
|
2370
2668
|
const browserSecurityConnection = cdpSession.connection();
|
|
2371
2669
|
if (!browserSecurityConnection) {
|
|
@@ -2373,13 +2671,69 @@ export class LiveBrowserSession {
|
|
|
2373
2671
|
}
|
|
2374
2672
|
this.browserSecurityConnection = browserSecurityConnection;
|
|
2375
2673
|
this.browserSecuritySessionAttachedListener = (attachedSession) => {
|
|
2674
|
+
const setupPromise = this.configureProxySecuritySession(attachedSession, generation);
|
|
2675
|
+
queueMicrotask(() => {
|
|
2676
|
+
const securityRecord = this.proxySecuritySessions.get(attachedSession.id());
|
|
2677
|
+
if (
|
|
2678
|
+
securityRecord?.generation === generation
|
|
2679
|
+
&& securityRecord.ownedByProxySecurity
|
|
2680
|
+
) {
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
this.trackProxySecurityOperation(
|
|
2684
|
+
setupPromise,
|
|
2685
|
+
generation,
|
|
2686
|
+
'proxy_security_session_setup_failed',
|
|
2687
|
+
);
|
|
2688
|
+
});
|
|
2689
|
+
};
|
|
2690
|
+
this.browserSecurityOwnedSessionAttachedListener = (attachedSession) => {
|
|
2691
|
+
const securityRecord = this.proxySecuritySessions.get(attachedSession.id());
|
|
2692
|
+
if (!securityRecord || securityRecord.generation !== generation) {
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
securityRecord.ownedByProxySecurity = true;
|
|
2696
|
+
this.ownedProxySecuritySessions.add(attachedSession);
|
|
2697
|
+
this.browserSecurityParentOwnedSessions.add(attachedSession);
|
|
2698
|
+
const setupPromise = securityRecord.setupPromise;
|
|
2699
|
+
securityRecord.setupPromise = setupPromise.then(async () => {
|
|
2700
|
+
try {
|
|
2701
|
+
await attachedSession.send(
|
|
2702
|
+
'Runtime.runIfWaitingForDebugger',
|
|
2703
|
+
undefined,
|
|
2704
|
+
proxySecurityCommandOptions,
|
|
2705
|
+
);
|
|
2706
|
+
} catch (error) {
|
|
2707
|
+
this.removeProxySecuritySession(securityRecord);
|
|
2708
|
+
if (attachedSession.detached) {
|
|
2709
|
+
this.forgetOwnedProxySecuritySession(attachedSession);
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
if (this.proxySecurityStopping || this.normalStopRequested) {
|
|
2713
|
+
await this.detachOwnedProxySecuritySession(securityRecord);
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
if (
|
|
2717
|
+
securityRecord.targetId
|
|
2718
|
+
&& await this.waitForProxySecurityTargetCoverage(
|
|
2719
|
+
securityRecord.targetId,
|
|
2720
|
+
generation,
|
|
2721
|
+
)
|
|
2722
|
+
) {
|
|
2723
|
+
await this.detachOwnedProxySecuritySession(securityRecord);
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
throw error;
|
|
2727
|
+
}
|
|
2728
|
+
});
|
|
2376
2729
|
this.trackProxySecurityOperation(
|
|
2377
|
-
|
|
2730
|
+
securityRecord.setupPromise,
|
|
2378
2731
|
generation,
|
|
2379
2732
|
'proxy_security_session_setup_failed',
|
|
2380
2733
|
);
|
|
2381
2734
|
};
|
|
2382
2735
|
this.browserSecuritySessionDetachedListener = (detachedSession) => {
|
|
2736
|
+
this.forgetOwnedProxySecuritySession(detachedSession);
|
|
2383
2737
|
if (detachedSession === cdpSession) {
|
|
2384
2738
|
this.handleProxySecurityFailure(
|
|
2385
2739
|
'proxy_security_browser_session_detached',
|
|
@@ -2388,6 +2742,14 @@ export class LiveBrowserSession {
|
|
|
2388
2742
|
);
|
|
2389
2743
|
return;
|
|
2390
2744
|
}
|
|
2745
|
+
if (detachedSession === this.browserSecurityWorkerLifecycleCdpSession) {
|
|
2746
|
+
this.handleProxySecurityFailure(
|
|
2747
|
+
'proxy_security_worker_lifecycle_detached',
|
|
2748
|
+
new Error('Proxy security service-worker lifecycle session detached unexpectedly'),
|
|
2749
|
+
generation,
|
|
2750
|
+
);
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2391
2753
|
this.trackProxySecurityOperation(
|
|
2392
2754
|
this.verifyProxySecuritySessionDetached(detachedSession, generation),
|
|
2393
2755
|
generation,
|
|
@@ -2398,11 +2760,30 @@ export class LiveBrowserSession {
|
|
|
2398
2760
|
'sessionattached',
|
|
2399
2761
|
this.browserSecuritySessionAttachedListener,
|
|
2400
2762
|
);
|
|
2763
|
+
cdpSession.on(
|
|
2764
|
+
plugins.puppeteer.CDPSessionEvent.SessionAttached,
|
|
2765
|
+
this.browserSecurityOwnedSessionAttachedListener,
|
|
2766
|
+
);
|
|
2401
2767
|
browserSecurityConnection.on(
|
|
2402
2768
|
'sessiondetached',
|
|
2403
2769
|
this.browserSecuritySessionDetachedListener,
|
|
2404
2770
|
);
|
|
2405
2771
|
|
|
2772
|
+
await cdpSession.send('Target.setAutoAttach', {
|
|
2773
|
+
autoAttach: true,
|
|
2774
|
+
waitForDebuggerOnStart: true,
|
|
2775
|
+
flatten: true,
|
|
2776
|
+
filter: [
|
|
2777
|
+
{ type: 'service_worker' },
|
|
2778
|
+
{ exclude: true },
|
|
2779
|
+
],
|
|
2780
|
+
}, proxySecurityCommandOptions);
|
|
2781
|
+
await Promise.all(
|
|
2782
|
+
[...this.proxySecuritySessions.values()]
|
|
2783
|
+
.filter((record) => record.generation === generation && record.ownedByProxySecurity)
|
|
2784
|
+
.map((record) => record.setupPromise),
|
|
2785
|
+
);
|
|
2786
|
+
|
|
2406
2787
|
for (const target of browser.targets()) {
|
|
2407
2788
|
if (!proxySeedTargetTypes.has(target.type())) {
|
|
2408
2789
|
continue;
|
|
@@ -2413,14 +2794,121 @@ export class LiveBrowserSession {
|
|
|
2413
2794
|
throw new Error(`Proxy security did not observe session ${securitySession.id()}`);
|
|
2414
2795
|
}
|
|
2415
2796
|
securityRecord.ownedByProxySecurity = true;
|
|
2797
|
+
this.ownedProxySecuritySessions.add(securitySession);
|
|
2416
2798
|
await securityRecord.setupPromise;
|
|
2417
2799
|
}
|
|
2800
|
+
await this.configureProxySecurityWorkerLifecycle(cdpSession, generation);
|
|
2418
2801
|
} catch (error) {
|
|
2419
2802
|
await this.teardownProxySecurity();
|
|
2420
2803
|
throw error;
|
|
2421
2804
|
}
|
|
2422
2805
|
}
|
|
2423
2806
|
|
|
2807
|
+
private async configureProxySecurityWorkerLifecycle(
|
|
2808
|
+
browserSecurityCdpSession: plugins.puppeteer.CDPSession,
|
|
2809
|
+
generation: number,
|
|
2810
|
+
): Promise<void> {
|
|
2811
|
+
const lifecycleUrl = `data:text/html,<title>smartpuppeteer-proxy-security-${generation}</title>`;
|
|
2812
|
+
const { targetId } = await browserSecurityCdpSession.send('Target.createTarget', {
|
|
2813
|
+
url: lifecycleUrl,
|
|
2814
|
+
background: true,
|
|
2815
|
+
hidden: true,
|
|
2816
|
+
}, proxySecurityCommandOptions);
|
|
2817
|
+
const attachedSessions = new Map<string, plugins.puppeteer.CDPSession>();
|
|
2818
|
+
const captureAttachedSession = (session: plugins.puppeteer.CDPSession): void => {
|
|
2819
|
+
attachedSessions.set(session.id(), session);
|
|
2820
|
+
};
|
|
2821
|
+
browserSecurityCdpSession.on(
|
|
2822
|
+
plugins.puppeteer.CDPSessionEvent.SessionAttached,
|
|
2823
|
+
captureAttachedSession,
|
|
2824
|
+
);
|
|
2825
|
+
let lifecycleSession: plugins.puppeteer.CDPSession;
|
|
2826
|
+
try {
|
|
2827
|
+
const { sessionId } = await browserSecurityCdpSession.send('Target.attachToTarget', {
|
|
2828
|
+
targetId,
|
|
2829
|
+
flatten: true,
|
|
2830
|
+
}, proxySecurityCommandOptions);
|
|
2831
|
+
const attachedSession = attachedSessions.get(sessionId);
|
|
2832
|
+
if (!attachedSession) {
|
|
2833
|
+
throw new Error('Proxy security did not observe its hidden lifecycle session');
|
|
2834
|
+
}
|
|
2835
|
+
lifecycleSession = attachedSession;
|
|
2836
|
+
} finally {
|
|
2837
|
+
browserSecurityCdpSession.off(
|
|
2838
|
+
plugins.puppeteer.CDPSessionEvent.SessionAttached,
|
|
2839
|
+
captureAttachedSession,
|
|
2840
|
+
);
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
const securityRecord = this.proxySecuritySessions.get(lifecycleSession.id());
|
|
2844
|
+
if (!securityRecord || !securityRecord.ownedByProxySecurity) {
|
|
2845
|
+
throw new Error('Proxy security did not configure its hidden lifecycle session');
|
|
2846
|
+
}
|
|
2847
|
+
await securityRecord.setupPromise;
|
|
2848
|
+
if (lifecycleSession.detached) {
|
|
2849
|
+
throw new Error('Proxy security hidden lifecycle session detached during setup');
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
this.browserSecurityWorkerLifecycleCdpSession = lifecycleSession;
|
|
2853
|
+
this.browserSecurityWorkerVersionUpdatedListener = (event) => {
|
|
2854
|
+
const stoppedTargetIds = new Set<string>();
|
|
2855
|
+
const redundantTargetIds = new Set<string>();
|
|
2856
|
+
for (const version of event.versions) {
|
|
2857
|
+
if (version.targetId) {
|
|
2858
|
+
if (
|
|
2859
|
+
!this.serviceWorkerTargetIdsByVersionId.has(version.versionId)
|
|
2860
|
+
&& this.serviceWorkerTargetIdsByVersionId.size >= maxTrackedServiceWorkerVersions
|
|
2861
|
+
) {
|
|
2862
|
+
this.handleProxySecurityFailure(
|
|
2863
|
+
'proxy_security_worker_tracking_capacity_exceeded',
|
|
2864
|
+
new Error(
|
|
2865
|
+
`Proxy security exceeded ${maxTrackedServiceWorkerVersions} service-worker versions`,
|
|
2866
|
+
),
|
|
2867
|
+
generation,
|
|
2868
|
+
);
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
this.serviceWorkerTargetIdsByVersionId.set(version.versionId, version.targetId);
|
|
2872
|
+
}
|
|
2873
|
+
const knownTargetId = version.targetId
|
|
2874
|
+
?? this.serviceWorkerTargetIdsByVersionId.get(version.versionId);
|
|
2875
|
+
if (version.status === 'redundant' && knownTargetId) {
|
|
2876
|
+
redundantTargetIds.add(knownTargetId);
|
|
2877
|
+
}
|
|
2878
|
+
if (version.runningStatus === 'stopped') {
|
|
2879
|
+
if (knownTargetId) {
|
|
2880
|
+
stoppedTargetIds.add(knownTargetId);
|
|
2881
|
+
}
|
|
2882
|
+
this.serviceWorkerTargetIdsByVersionId.delete(version.versionId);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
for (const workerSecurityRecord of [...this.proxySecuritySessions.values()]) {
|
|
2886
|
+
if (
|
|
2887
|
+
!workerSecurityRecord.ownedByProxySecurity
|
|
2888
|
+
|| workerSecurityRecord.targetType !== 'service_worker'
|
|
2889
|
+
|| !workerSecurityRecord.targetId
|
|
2890
|
+
|| !this.browserSecurityParentOwnedSessions.has(workerSecurityRecord.session)
|
|
2891
|
+
) {
|
|
2892
|
+
continue;
|
|
2893
|
+
}
|
|
2894
|
+
if (stoppedTargetIds.has(workerSecurityRecord.targetId)) {
|
|
2895
|
+
this.retireOwnedProxySecuritySession(workerSecurityRecord, true);
|
|
2896
|
+
} else if (redundantTargetIds.has(workerSecurityRecord.targetId)) {
|
|
2897
|
+
this.retireOwnedProxySecuritySession(workerSecurityRecord, false);
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
};
|
|
2901
|
+
lifecycleSession.on(
|
|
2902
|
+
'ServiceWorker.workerVersionUpdated',
|
|
2903
|
+
this.browserSecurityWorkerVersionUpdatedListener,
|
|
2904
|
+
);
|
|
2905
|
+
await lifecycleSession.send(
|
|
2906
|
+
'ServiceWorker.enable',
|
|
2907
|
+
undefined,
|
|
2908
|
+
proxySecurityCommandOptions,
|
|
2909
|
+
);
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2424
2912
|
private configureProxySecuritySession(
|
|
2425
2913
|
session: plugins.puppeteer.CDPSession,
|
|
2426
2914
|
generation: number,
|
|
@@ -2483,7 +2971,7 @@ export class LiveBrowserSession {
|
|
|
2483
2971
|
session.send('Fetch.continueWithAuth', {
|
|
2484
2972
|
requestId: event.requestId,
|
|
2485
2973
|
authChallengeResponse,
|
|
2486
|
-
}).then(() => undefined),
|
|
2974
|
+
}, proxySecurityCommandOptions).then(() => undefined),
|
|
2487
2975
|
generation,
|
|
2488
2976
|
'proxy_security_protocol_failed',
|
|
2489
2977
|
);
|
|
@@ -2516,7 +3004,11 @@ export class LiveBrowserSession {
|
|
|
2516
3004
|
}
|
|
2517
3005
|
}
|
|
2518
3006
|
this.trackProxySecurityOperation(
|
|
2519
|
-
session.send(
|
|
3007
|
+
session.send(
|
|
3008
|
+
'Fetch.continueRequest',
|
|
3009
|
+
{ requestId: event.requestId },
|
|
3010
|
+
proxySecurityCommandOptions,
|
|
3011
|
+
).then(() => undefined),
|
|
2520
3012
|
generation,
|
|
2521
3013
|
'proxy_security_protocol_failed',
|
|
2522
3014
|
);
|
|
@@ -2528,6 +3020,13 @@ export class LiveBrowserSession {
|
|
|
2528
3020
|
}
|
|
2529
3021
|
requestIdsByNetworkId.delete(networkId);
|
|
2530
3022
|
attemptedAuthentications.delete(requestId);
|
|
3023
|
+
if (
|
|
3024
|
+
proxySecuritySession.retireWhenIdle
|
|
3025
|
+
&& attemptedAuthentications.size === 0
|
|
3026
|
+
&& requestIdsByNetworkId.size === 0
|
|
3027
|
+
) {
|
|
3028
|
+
this.retireOwnedProxySecuritySession(proxySecuritySession, false);
|
|
3029
|
+
}
|
|
2531
3030
|
};
|
|
2532
3031
|
const loadingFinishedListener = (event: TNetworkLoadingFinishedEvent): void => {
|
|
2533
3032
|
forgetAuthentication(event.requestId);
|
|
@@ -2546,6 +3045,7 @@ export class LiveBrowserSession {
|
|
|
2546
3045
|
fetchEnabled: false,
|
|
2547
3046
|
allowLiveTargetDetach: false,
|
|
2548
3047
|
ownedByProxySecurity: false,
|
|
3048
|
+
retireWhenIdle: false,
|
|
2549
3049
|
attemptedAuthentications,
|
|
2550
3050
|
requestIdsByNetworkId,
|
|
2551
3051
|
authRequiredListener,
|
|
@@ -2554,12 +3054,20 @@ export class LiveBrowserSession {
|
|
|
2554
3054
|
loadingFailedListener,
|
|
2555
3055
|
setupPromise: Promise.resolve(),
|
|
2556
3056
|
};
|
|
2557
|
-
const targetInfoPromise = session.send(
|
|
2558
|
-
|
|
3057
|
+
const targetInfoPromise = session.send(
|
|
3058
|
+
'Target.getTargetInfo',
|
|
3059
|
+
undefined,
|
|
3060
|
+
proxySecurityCommandOptions,
|
|
3061
|
+
);
|
|
3062
|
+
const networkEnablePromise = session.send(
|
|
3063
|
+
'Network.enable',
|
|
3064
|
+
undefined,
|
|
3065
|
+
proxySecurityCommandOptions,
|
|
3066
|
+
);
|
|
2559
3067
|
const fetchEnablePromise = session.send('Fetch.enable', {
|
|
2560
3068
|
handleAuthRequests: true,
|
|
2561
3069
|
patterns: [{ urlPattern: '*' }],
|
|
2562
|
-
});
|
|
3070
|
+
}, proxySecurityCommandOptions);
|
|
2563
3071
|
const commandResultsPromise = Promise.allSettled([
|
|
2564
3072
|
targetInfoPromise,
|
|
2565
3073
|
networkEnablePromise,
|
|
@@ -2590,10 +3098,28 @@ export class LiveBrowserSession {
|
|
|
2590
3098
|
proxySecuritySession.fetchEnabled = true;
|
|
2591
3099
|
} catch (error) {
|
|
2592
3100
|
this.removeProxySecuritySession(proxySecuritySession);
|
|
2593
|
-
if (session.detached
|
|
3101
|
+
if (session.detached) {
|
|
3102
|
+
this.forgetOwnedProxySecuritySession(session);
|
|
2594
3103
|
return;
|
|
2595
3104
|
}
|
|
2596
|
-
|
|
3105
|
+
if (this.proxySecurityStopping || this.normalStopRequested) {
|
|
3106
|
+
await this.detachOwnedProxySecuritySession(proxySecuritySession);
|
|
3107
|
+
return;
|
|
3108
|
+
}
|
|
3109
|
+
if (
|
|
3110
|
+
proxySecuritySession.targetId
|
|
3111
|
+
&& await this.waitForProxySecurityTargetCoverage(
|
|
3112
|
+
proxySecuritySession.targetId,
|
|
3113
|
+
generation,
|
|
3114
|
+
)
|
|
3115
|
+
) {
|
|
3116
|
+
await this.detachOwnedProxySecuritySession(proxySecuritySession);
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
throw new Error(
|
|
3120
|
+
`Proxy security ${proxySecuritySession.ownedByProxySecurity ? 'owned' : 'observed'} session setup failed for ${proxySecuritySession.targetType ?? 'unknown'} target ${proxySecuritySession.targetId ?? 'unknown'}: ${normalizeErrorMessage(error)}`,
|
|
3121
|
+
{ cause: error },
|
|
3122
|
+
);
|
|
2597
3123
|
}
|
|
2598
3124
|
})();
|
|
2599
3125
|
proxySecuritySession.setupPromise = setupPromise;
|
|
@@ -2619,6 +3145,18 @@ export class LiveBrowserSession {
|
|
|
2619
3145
|
if (!proxySecuritySession.targetId) {
|
|
2620
3146
|
throw new Error(`Proxy security session detached before target identification: ${session.id()}`);
|
|
2621
3147
|
}
|
|
3148
|
+
if (await this.waitForProxySecurityTargetCoverage(proxySecuritySession.targetId, generation)) {
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
throw new Error(
|
|
3152
|
+
`Proxy security detached from live target ${proxySecuritySession.targetId}`,
|
|
3153
|
+
);
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
private async waitForProxySecurityTargetCoverage(
|
|
3157
|
+
targetId: string,
|
|
3158
|
+
generation: number,
|
|
3159
|
+
): Promise<boolean> {
|
|
2622
3160
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
2623
3161
|
const generationSessions = [...this.proxySecuritySessions.values()].filter((candidate) => (
|
|
2624
3162
|
candidate.generation === generation
|
|
@@ -2626,12 +3164,12 @@ export class LiveBrowserSession {
|
|
|
2626
3164
|
await Promise.allSettled(generationSessions.map((candidate) => candidate.setupPromise));
|
|
2627
3165
|
const hasReplacement = [...this.proxySecuritySessions.values()].some((candidate) => (
|
|
2628
3166
|
candidate.generation === generation
|
|
2629
|
-
&& candidate.targetId ===
|
|
3167
|
+
&& candidate.targetId === targetId
|
|
2630
3168
|
&& candidate.fetchEnabled
|
|
2631
3169
|
&& !candidate.session.detached
|
|
2632
3170
|
));
|
|
2633
3171
|
if (hasReplacement) {
|
|
2634
|
-
return;
|
|
3172
|
+
return true;
|
|
2635
3173
|
}
|
|
2636
3174
|
if (attempt < 2) {
|
|
2637
3175
|
await delay(25);
|
|
@@ -2641,12 +3179,12 @@ export class LiveBrowserSession {
|
|
|
2641
3179
|
if (!browserSecurityCdpSession || browserSecurityCdpSession.detached) {
|
|
2642
3180
|
throw new Error('Browser security CDP session detached unexpectedly');
|
|
2643
3181
|
}
|
|
2644
|
-
const { targetInfos } = await browserSecurityCdpSession.send(
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
3182
|
+
const { targetInfos } = await browserSecurityCdpSession.send(
|
|
3183
|
+
'Target.getTargets',
|
|
3184
|
+
undefined,
|
|
3185
|
+
proxySecurityCommandOptions,
|
|
3186
|
+
);
|
|
3187
|
+
return !targetInfos.some((targetInfo) => targetInfo.targetId === targetId);
|
|
2650
3188
|
}
|
|
2651
3189
|
|
|
2652
3190
|
private removeProxySecuritySession(proxySecuritySession: IProxySecuritySession): void {
|
|
@@ -2668,6 +3206,113 @@ export class LiveBrowserSession {
|
|
|
2668
3206
|
this.proxySecuritySessions.delete(proxySecuritySession.session.id());
|
|
2669
3207
|
}
|
|
2670
3208
|
|
|
3209
|
+
private async detachOwnedProxySecuritySession(
|
|
3210
|
+
proxySecuritySession: IProxySecuritySession,
|
|
3211
|
+
): Promise<void> {
|
|
3212
|
+
if (!proxySecuritySession.ownedByProxySecurity) {
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
await this.detachTrackedOwnedProxySecuritySession(proxySecuritySession.session);
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
private forgetOwnedProxySecuritySession(session: plugins.puppeteer.CDPSession): void {
|
|
3219
|
+
this.ownedProxySecuritySessions.delete(session);
|
|
3220
|
+
this.browserSecurityParentOwnedSessions.delete(session);
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
private async detachCdpSessionWithTimeout(
|
|
3224
|
+
session: plugins.puppeteer.CDPSession,
|
|
3225
|
+
description: string,
|
|
3226
|
+
): Promise<void> {
|
|
3227
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
3228
|
+
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
|
3229
|
+
timeout = setTimeout(() => {
|
|
3230
|
+
reject(new Error(
|
|
3231
|
+
`${description} did not detach within ${proxySecurityCommandTimeoutMs}ms`,
|
|
3232
|
+
));
|
|
3233
|
+
}, proxySecurityCommandTimeoutMs);
|
|
3234
|
+
});
|
|
3235
|
+
try {
|
|
3236
|
+
await Promise.race([session.detach(), timeoutPromise]);
|
|
3237
|
+
} finally {
|
|
3238
|
+
if (timeout) {
|
|
3239
|
+
clearTimeout(timeout);
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
private retireOwnedProxySecuritySession(
|
|
3245
|
+
proxySecuritySession: IProxySecuritySession,
|
|
3246
|
+
force: boolean,
|
|
3247
|
+
): void {
|
|
3248
|
+
if (
|
|
3249
|
+
!proxySecuritySession.ownedByProxySecurity
|
|
3250
|
+
|| this.proxySecuritySessions.get(proxySecuritySession.session.id())
|
|
3251
|
+
!== proxySecuritySession
|
|
3252
|
+
) {
|
|
3253
|
+
return;
|
|
3254
|
+
}
|
|
3255
|
+
proxySecuritySession.retireWhenIdle = true;
|
|
3256
|
+
if (
|
|
3257
|
+
!force
|
|
3258
|
+
&& (
|
|
3259
|
+
proxySecuritySession.attemptedAuthentications.size > 0
|
|
3260
|
+
|| proxySecuritySession.requestIdsByNetworkId.size > 0
|
|
3261
|
+
)
|
|
3262
|
+
) {
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
if (proxySecuritySession.targetId) {
|
|
3266
|
+
for (const [versionId, targetId] of this.serviceWorkerTargetIdsByVersionId) {
|
|
3267
|
+
if (targetId === proxySecuritySession.targetId) {
|
|
3268
|
+
this.serviceWorkerTargetIdsByVersionId.delete(versionId);
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
proxySecuritySession.allowLiveTargetDetach = true;
|
|
3273
|
+
this.removeProxySecuritySession(proxySecuritySession);
|
|
3274
|
+
this.trackProxySecurityOperation(
|
|
3275
|
+
this.detachOwnedProxySecuritySession(proxySecuritySession),
|
|
3276
|
+
proxySecuritySession.generation,
|
|
3277
|
+
'proxy_security_worker_retirement_failed',
|
|
3278
|
+
);
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
private async detachTrackedOwnedProxySecuritySession(
|
|
3282
|
+
session: plugins.puppeteer.CDPSession,
|
|
3283
|
+
): Promise<void> {
|
|
3284
|
+
if (session.detached) {
|
|
3285
|
+
this.forgetOwnedProxySecuritySession(session);
|
|
3286
|
+
return;
|
|
3287
|
+
}
|
|
3288
|
+
try {
|
|
3289
|
+
if (this.browserSecurityParentOwnedSessions.has(session)) {
|
|
3290
|
+
const browserSecurityCdpSession = this.browserSecurityCdpSession;
|
|
3291
|
+
if (!browserSecurityCdpSession || browserSecurityCdpSession.detached) {
|
|
3292
|
+
throw new Error('Browser security parent session is unavailable');
|
|
3293
|
+
}
|
|
3294
|
+
await browserSecurityCdpSession.send('Target.detachFromTarget', {
|
|
3295
|
+
sessionId: session.id(),
|
|
3296
|
+
}, proxySecurityCommandOptions);
|
|
3297
|
+
} else {
|
|
3298
|
+
await this.detachCdpSessionWithTimeout(session, 'proxy security session');
|
|
3299
|
+
}
|
|
3300
|
+
this.forgetOwnedProxySecuritySession(session);
|
|
3301
|
+
} catch (error) {
|
|
3302
|
+
if (session.detached) {
|
|
3303
|
+
this.forgetOwnedProxySecuritySession(session);
|
|
3304
|
+
return;
|
|
3305
|
+
}
|
|
3306
|
+
if (this.proxySecurityStopping || this.normalStopRequested) {
|
|
3307
|
+
return;
|
|
3308
|
+
}
|
|
3309
|
+
throw new Error(
|
|
3310
|
+
`Failed to detach an owned proxy security session: ${normalizeErrorMessage(error)}`,
|
|
3311
|
+
{ cause: error },
|
|
3312
|
+
);
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
|
|
2671
3316
|
private allowOperationalCdpSessionDetach(session: plugins.puppeteer.CDPSession): void {
|
|
2672
3317
|
const proxySecuritySession = this.proxySecuritySessions.get(session.id());
|
|
2673
3318
|
if (proxySecuritySession) {
|
|
@@ -2730,12 +3375,51 @@ export class LiveBrowserSession {
|
|
|
2730
3375
|
|
|
2731
3376
|
private async teardownProxySecurity(): Promise<void> {
|
|
2732
3377
|
this.proxySecurityStopping = true;
|
|
3378
|
+
this.serviceWorkerTargetIdsByVersionId.clear();
|
|
3379
|
+
const browserSecurityCdpSession = this.browserSecurityCdpSession;
|
|
3380
|
+
if (browserSecurityCdpSession && !browserSecurityCdpSession.detached) {
|
|
3381
|
+
try {
|
|
3382
|
+
await browserSecurityCdpSession.send('Target.setAutoAttach', {
|
|
3383
|
+
autoAttach: false,
|
|
3384
|
+
waitForDebuggerOnStart: false,
|
|
3385
|
+
flatten: true,
|
|
3386
|
+
}, proxySecurityCommandOptions);
|
|
3387
|
+
} catch {
|
|
3388
|
+
// Browser shutdown may detach the browser target before auto-attach is disabled.
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
const workerLifecycleCdpSession = this.browserSecurityWorkerLifecycleCdpSession;
|
|
3392
|
+
if (workerLifecycleCdpSession && !workerLifecycleCdpSession.detached) {
|
|
3393
|
+
try {
|
|
3394
|
+
await workerLifecycleCdpSession.send(
|
|
3395
|
+
'ServiceWorker.disable',
|
|
3396
|
+
undefined,
|
|
3397
|
+
proxySecurityCommandOptions,
|
|
3398
|
+
);
|
|
3399
|
+
} catch {
|
|
3400
|
+
// Browser shutdown can close the hidden lifecycle target first.
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
if (workerLifecycleCdpSession && this.browserSecurityWorkerVersionUpdatedListener) {
|
|
3404
|
+
workerLifecycleCdpSession.off(
|
|
3405
|
+
'ServiceWorker.workerVersionUpdated',
|
|
3406
|
+
this.browserSecurityWorkerVersionUpdatedListener,
|
|
3407
|
+
);
|
|
3408
|
+
}
|
|
3409
|
+
this.browserSecurityWorkerVersionUpdatedListener = undefined;
|
|
3410
|
+
this.browserSecurityWorkerLifecycleCdpSession = undefined;
|
|
2733
3411
|
if (this.browserSecurityConnection && this.browserSecuritySessionAttachedListener) {
|
|
2734
3412
|
this.browserSecurityConnection.off(
|
|
2735
3413
|
'sessionattached',
|
|
2736
3414
|
this.browserSecuritySessionAttachedListener,
|
|
2737
3415
|
);
|
|
2738
3416
|
}
|
|
3417
|
+
if (browserSecurityCdpSession && this.browserSecurityOwnedSessionAttachedListener) {
|
|
3418
|
+
browserSecurityCdpSession.off(
|
|
3419
|
+
plugins.puppeteer.CDPSessionEvent.SessionAttached,
|
|
3420
|
+
this.browserSecurityOwnedSessionAttachedListener,
|
|
3421
|
+
);
|
|
3422
|
+
}
|
|
2739
3423
|
if (this.browserSecurityConnection && this.browserSecuritySessionDetachedListener) {
|
|
2740
3424
|
this.browserSecurityConnection.off(
|
|
2741
3425
|
'sessiondetached',
|
|
@@ -2743,28 +3427,31 @@ export class LiveBrowserSession {
|
|
|
2743
3427
|
);
|
|
2744
3428
|
}
|
|
2745
3429
|
this.browserSecuritySessionAttachedListener = undefined;
|
|
3430
|
+
this.browserSecurityOwnedSessionAttachedListener = undefined;
|
|
2746
3431
|
this.browserSecuritySessionDetachedListener = undefined;
|
|
2747
3432
|
this.browserSecurityConnection = undefined;
|
|
2748
|
-
const ownedSessions = [...this.
|
|
2749
|
-
.filter((record) => record.ownedByProxySecurity)
|
|
2750
|
-
.map((record) => record.session);
|
|
3433
|
+
const ownedSessions = [...this.ownedProxySecuritySessions];
|
|
2751
3434
|
for (const proxySecuritySession of [...this.proxySecuritySessions.values()]) {
|
|
2752
3435
|
this.removeProxySecuritySession(proxySecuritySession);
|
|
2753
3436
|
}
|
|
2754
3437
|
for (const ownedSession of ownedSessions) {
|
|
2755
3438
|
if (!ownedSession.detached) {
|
|
2756
3439
|
try {
|
|
2757
|
-
await
|
|
3440
|
+
await this.detachTrackedOwnedProxySecuritySession(ownedSession);
|
|
2758
3441
|
} catch {
|
|
2759
3442
|
// Browser shutdown can close a target before explicit detach settles.
|
|
2760
3443
|
}
|
|
2761
3444
|
}
|
|
2762
3445
|
}
|
|
2763
|
-
|
|
3446
|
+
this.ownedProxySecuritySessions.clear();
|
|
3447
|
+
this.browserSecurityParentOwnedSessions.clear();
|
|
2764
3448
|
this.browserSecurityCdpSession = undefined;
|
|
2765
3449
|
if (browserSecurityCdpSession && !browserSecurityCdpSession.detached) {
|
|
2766
3450
|
try {
|
|
2767
|
-
await
|
|
3451
|
+
await this.detachCdpSessionWithTimeout(
|
|
3452
|
+
browserSecurityCdpSession,
|
|
3453
|
+
'browser security session',
|
|
3454
|
+
);
|
|
2768
3455
|
} catch {
|
|
2769
3456
|
// Browser shutdown can detach the browser target first.
|
|
2770
3457
|
}
|
|
@@ -2927,7 +3614,10 @@ export class LiveBrowserSession {
|
|
|
2927
3614
|
closing: false,
|
|
2928
3615
|
stateUpdateQueued: false,
|
|
2929
3616
|
stateUpdatePending: false,
|
|
2930
|
-
|
|
3617
|
+
navigationResetRevision: 0,
|
|
3618
|
+
restoredNavigationRevision: 0,
|
|
3619
|
+
inputQueue: [],
|
|
3620
|
+
inputPumpActive: false,
|
|
2931
3621
|
removeListeners: [],
|
|
2932
3622
|
};
|
|
2933
3623
|
this.tabs.set(tab.id, tab);
|
|
@@ -2939,11 +3629,11 @@ export class LiveBrowserSession {
|
|
|
2939
3629
|
tab.securityCdpSession = securityCdpSession;
|
|
2940
3630
|
await securityCdpSession.send('Page.enable', {
|
|
2941
3631
|
enableFileChooserOpenedEvent: true,
|
|
2942
|
-
});
|
|
3632
|
+
}, proxySecurityCommandOptions);
|
|
2943
3633
|
await securityCdpSession.send('Page.setInterceptFileChooserDialog', {
|
|
2944
3634
|
enabled: true,
|
|
2945
3635
|
cancel: true,
|
|
2946
|
-
});
|
|
3636
|
+
}, proxySecurityCommandOptions);
|
|
2947
3637
|
}
|
|
2948
3638
|
await this.ensureTabViewport(tab);
|
|
2949
3639
|
await this.refreshTab(tab);
|
|
@@ -3078,7 +3768,10 @@ export class LiveBrowserSession {
|
|
|
3078
3768
|
} catch (error) {
|
|
3079
3769
|
if (tab.securityCdpSession && !tab.securityCdpSession.detached) {
|
|
3080
3770
|
try {
|
|
3081
|
-
await
|
|
3771
|
+
await this.detachCdpSessionWithTimeout(
|
|
3772
|
+
tab.securityCdpSession,
|
|
3773
|
+
'page security session',
|
|
3774
|
+
);
|
|
3082
3775
|
} catch {
|
|
3083
3776
|
// The page may have closed while security setup was failing.
|
|
3084
3777
|
}
|
|
@@ -3117,31 +3810,58 @@ export class LiveBrowserSession {
|
|
|
3117
3810
|
return;
|
|
3118
3811
|
}
|
|
3119
3812
|
tab.stateUpdatePending = true;
|
|
3120
|
-
|
|
3813
|
+
if (navigationReset) {
|
|
3814
|
+
tab.navigationResetRevision += 1;
|
|
3815
|
+
}
|
|
3121
3816
|
if (tab.stateUpdateQueued) {
|
|
3122
3817
|
return;
|
|
3123
3818
|
}
|
|
3124
3819
|
tab.stateUpdateQueued = true;
|
|
3125
3820
|
const wasScheduled = this.scheduleOperation(async () => {
|
|
3126
3821
|
try {
|
|
3127
|
-
const
|
|
3822
|
+
const navigationResetRevision = tab.navigationResetRevision;
|
|
3823
|
+
const shouldResetNavigation = (
|
|
3824
|
+
tab.restoredNavigationRevision < navigationResetRevision
|
|
3825
|
+
);
|
|
3128
3826
|
tab.stateUpdatePending = false;
|
|
3129
|
-
tab.navigationResetPending = false;
|
|
3130
3827
|
if (!this.tabs.has(tab.id) || tab.closing) {
|
|
3131
3828
|
return;
|
|
3132
3829
|
}
|
|
3133
3830
|
if (shouldResetNavigation && this.activeTabId === tab.id) {
|
|
3134
3831
|
await this.stopScreencast(tab);
|
|
3135
3832
|
}
|
|
3136
|
-
|
|
3137
|
-
|
|
3833
|
+
let stateUpdateError: unknown;
|
|
3834
|
+
try {
|
|
3835
|
+
await this.refreshTab(tab);
|
|
3836
|
+
this.emitState();
|
|
3837
|
+
} catch (error) {
|
|
3838
|
+
stateUpdateError = error;
|
|
3839
|
+
}
|
|
3138
3840
|
if (
|
|
3139
3841
|
shouldResetNavigation
|
|
3140
3842
|
&& this.activeTabId === tab.id
|
|
3141
3843
|
&& this.status === 'running'
|
|
3142
3844
|
&& tab.status === 'open'
|
|
3143
3845
|
) {
|
|
3144
|
-
|
|
3846
|
+
try {
|
|
3847
|
+
await this.startScreencast(tab);
|
|
3848
|
+
} catch (error) {
|
|
3849
|
+
throw stateUpdateError
|
|
3850
|
+
? new AggregateError(
|
|
3851
|
+
[stateUpdateError, error],
|
|
3852
|
+
'Page state refresh and screencast restart failed',
|
|
3853
|
+
)
|
|
3854
|
+
: error;
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
if (shouldResetNavigation) {
|
|
3858
|
+
tab.restoredNavigationRevision = Math.max(
|
|
3859
|
+
tab.restoredNavigationRevision,
|
|
3860
|
+
navigationResetRevision,
|
|
3861
|
+
);
|
|
3862
|
+
}
|
|
3863
|
+
if (stateUpdateError) {
|
|
3864
|
+
throw stateUpdateError;
|
|
3145
3865
|
}
|
|
3146
3866
|
} finally {
|
|
3147
3867
|
tab.stateUpdateQueued = false;
|
|
@@ -3346,15 +4066,32 @@ export class LiveBrowserSession {
|
|
|
3346
4066
|
} finally {
|
|
3347
4067
|
tab.navigationInProgress = false;
|
|
3348
4068
|
if (this.tabs.has(tab.id) && !tab.page.isClosed()) {
|
|
3349
|
-
|
|
3350
|
-
|
|
4069
|
+
let stateUpdateError: unknown;
|
|
4070
|
+
try {
|
|
4071
|
+
await this.refreshTab(tab);
|
|
4072
|
+
this.emitState();
|
|
4073
|
+
} catch (error) {
|
|
4074
|
+
stateUpdateError = error;
|
|
4075
|
+
}
|
|
3351
4076
|
if (
|
|
3352
4077
|
isActive
|
|
3353
4078
|
&& this.activeTabId === tab.id
|
|
3354
4079
|
&& this.status === 'running'
|
|
3355
4080
|
&& tab.status === 'open'
|
|
3356
4081
|
) {
|
|
3357
|
-
|
|
4082
|
+
try {
|
|
4083
|
+
await this.startScreencast(tab);
|
|
4084
|
+
} catch (error) {
|
|
4085
|
+
throw stateUpdateError
|
|
4086
|
+
? new AggregateError(
|
|
4087
|
+
[stateUpdateError, error],
|
|
4088
|
+
'Navigation state refresh and screencast restart failed',
|
|
4089
|
+
)
|
|
4090
|
+
: error;
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
if (stateUpdateError) {
|
|
4094
|
+
throw stateUpdateError;
|
|
3358
4095
|
}
|
|
3359
4096
|
}
|
|
3360
4097
|
}
|
|
@@ -4271,50 +5008,4 @@ export class LiveBrowserSession {
|
|
|
4271
5008
|
return validatedWaitUntil;
|
|
4272
5009
|
}
|
|
4273
5010
|
|
|
4274
|
-
private validateScreencastOptions(): void {
|
|
4275
|
-
const options = this.options.screencast;
|
|
4276
|
-
if (!options) {
|
|
4277
|
-
return;
|
|
4278
|
-
}
|
|
4279
|
-
if (options.format !== undefined && options.format !== 'jpeg' && options.format !== 'png') {
|
|
4280
|
-
throw new Error('screencast.format must be jpeg or png');
|
|
4281
|
-
}
|
|
4282
|
-
if (options.quality !== undefined) {
|
|
4283
|
-
validateInteger(options.quality, 'screencast.quality', 0, 100);
|
|
4284
|
-
}
|
|
4285
|
-
if (options.maxWidth !== undefined) {
|
|
4286
|
-
validateInteger(options.maxWidth, 'screencast.maxWidth', 1, maxViewportWidth);
|
|
4287
|
-
}
|
|
4288
|
-
if (options.maxHeight !== undefined) {
|
|
4289
|
-
validateInteger(options.maxHeight, 'screencast.maxHeight', 1, maxViewportHeight);
|
|
4290
|
-
}
|
|
4291
|
-
if (
|
|
4292
|
-
options.maxWidth !== undefined
|
|
4293
|
-
&& options.maxHeight !== undefined
|
|
4294
|
-
&& options.maxWidth * options.maxHeight > maxViewportPixelArea
|
|
4295
|
-
) {
|
|
4296
|
-
throw new Error(
|
|
4297
|
-
`screencast pixel area must not exceed ${maxViewportPixelArea} pixels`,
|
|
4298
|
-
);
|
|
4299
|
-
}
|
|
4300
|
-
if (options.everyNthFrame !== undefined) {
|
|
4301
|
-
validateInteger(options.everyNthFrame, 'screencast.everyNthFrame', 1, 100);
|
|
4302
|
-
}
|
|
4303
|
-
if (options.maxOutstandingFrames !== undefined) {
|
|
4304
|
-
validateInteger(
|
|
4305
|
-
options.maxOutstandingFrames,
|
|
4306
|
-
'screencast.maxOutstandingFrames',
|
|
4307
|
-
1,
|
|
4308
|
-
liveBrowserMaxOutstandingFrames,
|
|
4309
|
-
);
|
|
4310
|
-
}
|
|
4311
|
-
if (options.firstFrameTimeoutMs !== undefined) {
|
|
4312
|
-
validateInteger(
|
|
4313
|
-
options.firstFrameTimeoutMs,
|
|
4314
|
-
'screencast.firstFrameTimeoutMs',
|
|
4315
|
-
100,
|
|
4316
|
-
maxTimeoutMs,
|
|
4317
|
-
);
|
|
4318
|
-
}
|
|
4319
|
-
}
|
|
4320
5011
|
}
|