Skip to content

Commit b924db3

Browse files
committed
playwright support
1 parent 9863faf commit b924db3

File tree

5 files changed

+274
-12
lines changed

5 files changed

+274
-12
lines changed

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ If you don't want to mess with the patches and all possible errors, there is a d
1818

1919
Puppeteer: [rebrowser-puppeteer](https://www.npmjs.com/package/rebrowser-puppeteer) and [rebrowser-puppeteer-core](https://www.npmjs.com/package/rebrowser-puppeteer-core)
2020

21-
Playwright: *coming soon*
21+
Playwright: [rebrowser-playwright](https://www.npmjs.com/package/rebrowser-playwright) and [rebrowser-playwright-core](https://www.npmjs.com/package/rebrowser-playwright-core)
2222

2323
The easiest way to start using it is to fix your `package.json` to use new packages but keep the old name as an alias. This way, you don't need to change any source code of your automation. Here is how to do that:
2424
1. Open `package.json` and replace `"puppeteer": "^23.3.1"` and `"puppeteer-core": "^23.3.1"` with `"puppeteer": "npm:rebrowser-puppeteer@^23.3.1"` and `"puppeteer-core": "npm:rebrowser-puppeteer-core@^23.3.1"`.
@@ -136,7 +136,7 @@ You can see all command-line options by running `npx rebrowser-patches@latest --
136136
## How to update the patches?
137137
If you already have your package patched and want to update to the latest version of rebrowser-patches, the easiest way would be to delete `node_modules/puppeteer-core`, then run `npm install` or `yarn install --check-files`, and then run `npx rebrowser-patches@latest patch`.
138138

139-
## Supported versions
139+
## Puppeteer support
140140

141141
| Pptr Ver | Release Date | Chrome Ver | Patch Support |
142142
|--------------------------------------|--------------|------------|---------------|
@@ -149,10 +149,16 @@ If you already have your package patched and want to update to the latest versio
149149
| 22.13.x | 2024-07-11 | 126 ||
150150
| 22.12.x<br/><small>and below</small> | 2024-06-21 | 126 ||
151151

152-
## What about Playwright support?
153-
Currently, this repo contains only patches for the latest Puppeteer version. Creating these patches is time-consuming as it requires digging into someone else's code and changing it in ways it wasn't designed for.
152+
## Playwright support
153+
Playwright patches support `Runtime.enable` leak (only alwaysIsolated mode) and ability to change utility world name via `REBROWSER_PATCHES_UTILITY_WORLD_NAME` env variable.
154+
155+
Only JS version of Playwright is supported. Python is coming soon.
156+
157+
| Playwright Ver | Release Date | Chrome Ver | Patch Support |
158+
|-------------------------------------|--------------|------------|---------------|
159+
| 1.47.2 | 2024-09-20 | 129 ||
160+
| 1.47.1<br/><small>and below</small> | 2024-09-13 | 129 ||
154161

155-
📣 If we see **demand from the community** for Playwright support, we'll be happy to allocate more resources to this mission. Please provide your feedback in the [issues section](https://github.com/rebrowser/rebrowser-patches/issues).
156162

157163
## Follow the project
158164
We're currently developing more patches to improve web automation transparency, which will be released in this repo soon. Please support the project by clicking ⭐️ star or watch button.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
--- a/lib/server/chromium/crDevTools.js
2+
+++ b/lib/server/chromium/crDevTools.js
3+
@@ -66,7 +66,11 @@
4+
contextId: event.executionContextId
5+
}).catch(e => null);
6+
});
7+
- Promise.all([session.send('Runtime.enable'), session.send('Runtime.addBinding', {
8+
+ Promise.all([(() => {
9+
+ if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
10+
+ return session.send('Runtime.enable', {});
11+
+ }
12+
+ })(), session.send('Runtime.addBinding', {
13+
name: kBindingName
14+
}), session.send('Page.enable'), session.send('Page.addScriptToEvaluateOnNewDocument', {
15+
source: `
16+
17+
--- a/lib/server/chromium/crPage.js
18+
+++ b/lib/server/chromium/crPage.js
19+
@@ -445,7 +445,11 @@
20+
}
21+
}), this._client.send('Log.enable', {}), lifecycleEventsEnabled = this._client.send('Page.setLifecycleEventsEnabled', {
22+
enabled: true
23+
- }), this._client.send('Runtime.enable', {}), this._client.send('Runtime.addBinding', {
24+
+ }), (() => {
25+
+ if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
26+
+ return this._client.send('Runtime.enable', {});
27+
+ }
28+
+ })(), this._client.send('Runtime.addBinding', {
29+
name: _page.PageBinding.kPlaywrightBinding
30+
}), this._client.send('Page.addScriptToEvaluateOnNewDocument', {
31+
source: '',
32+
@@ -624,8 +628,11 @@
33+
session.once('Runtime.executionContextCreated', async event => {
34+
worker._createExecutionContext(new _crExecutionContext.CRExecutionContext(session, event.context));
35+
});
36+
- // This might fail if the target is closed before we initialize.
37+
- session._sendMayFail('Runtime.enable');
38+
+ if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
39+
+ // This might fail if the target is closed before we initialize.
40+
+ session._sendMayFail('Runtime.enable');
41+
+ }
42+
+
43+
// TODO: attribute workers to the right frame.
44+
this._crPage._networkManager.addSession(session, (_this$_page$_frameMan = this._page._frameManager.frame(this._targetId)) !== null && _this$_page$_frameMan !== void 0 ? _this$_page$_frameMan : undefined).catch(() => {});
45+
session._sendMayFail('Runtime.runIfWaitingForDebugger');
46+
47+
--- a/lib/server/chromium/crServiceWorker.js
48+
+++ b/lib/server/chromium/crServiceWorker.js
49+
@@ -46,7 +46,9 @@
50+
this.updateOffline();
51+
this._networkManager.addSession(session, undefined, true /* isMain */).catch(() => {});
52+
}
53+
- session.send('Runtime.enable', {}).catch(e => {});
54+
+ if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
55+
+ session.send('Runtime.enable', {}).catch(e => {});
56+
+ }
57+
session.send('Runtime.runIfWaitingForDebugger').catch(e => {});
58+
session.on('Inspector.targetReloadedAfterCrash', () => {
59+
// Resume service worker after restart.
60+
61+
--- a/lib/server/frames.js
62+
+++ b/lib/server/frames.js
63+
@@ -22,6 +22,7 @@
64+
var _frameSelectors = require("./frameSelectors");
65+
var _errors = require("./errors");
66+
var _fileUploadUtils = require("./fileUploadUtils");
67+
+var _crExecutionContext = require("./chromium/crExecutionContext");
68+
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
69+
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
70+
/**
71+
@@ -60,6 +61,7 @@
72+
this._webSockets = new Map();
73+
this._openedDialogs = new Set();
74+
this._closeAllOpeningDialogs = false;
75+
+ this._isolatedContext = undefined;
76+
this._page = page;
77+
this._mainFrame = undefined;
78+
}
79+
@@ -427,6 +429,7 @@
80+
if (this._inflightRequests.size === 0) this._startNetworkIdleTimer();
81+
this._page.mainFrame()._recalculateNetworkIdle(this);
82+
this._onLifecycleEvent('commit');
83+
+ this._isolatedContext = undefined;
84+
}
85+
setPendingDocument(documentInfo) {
86+
this._pendingDocument = documentInfo;
87+
@@ -582,6 +585,34 @@
88+
return this._page._delegate.getFrameElement(this);
89+
}
90+
_context(world) {
91+
+ if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
92+
+ // rebrowser-patches: use only utility context, create it on demand and cache
93+
+ if (this._isolatedContext !== undefined) {
94+
+ return this._isolatedContext;
95+
+ }
96+
+ const utilityWorldName = process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] !== '0' ? process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] || 'util' : '__playwright_utility_world__';
97+
+ return this._page._delegate._mainFrameSession._client.send('Page.createIsolatedWorld', {
98+
+ frameId: this._id,
99+
+ grantUniveralAccess: true,
100+
+ worldName: utilityWorldName
101+
+ }).then(createIsolatedWorldResult => {
102+
+ const contextPayload = {
103+
+ id: createIsolatedWorldResult.executionContextId,
104+
+ name: utilityWorldName,
105+
+ auxData: {
106+
+ frameId: this._id,
107+
+ isDefault: false
108+
+ }
109+
+ };
110+
+ const delegate = new _crExecutionContext.CRExecutionContext(this._page._delegate._mainFrameSession._client, contextPayload);
111+
+ this._isolatedContext = new dom.FrameExecutionContext(delegate, this, 'utility');
112+
+ return this._isolatedContext;
113+
+ }).catch(error => {
114+
+ // probably target is closed
115+
+ _debugLogger.debugLogger.log('error', error);
116+
+ console.error('[rebrowser-patches][frames._context] cannot create utility world');
117+
+ });
118+
+ }
119+
return this._contextData.get(world).contextPromise.then(contextOrDestroyedReason => {
120+
if (contextOrDestroyedReason instanceof js.ExecutionContext) return contextOrDestroyedReason;
121+
throw new Error(contextOrDestroyedReason.destroyedReason);
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
--- a/src/server/chromium/crDevTools.ts
2+
+++ b/src/server/chromium/crDevTools.ts
3+
@@ -66,7 +66,11 @@
4+
}).catch(e => null);
5+
});
6+
Promise.all([
7+
- session.send('Runtime.enable'),
8+
+ (() => {
9+
+ if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
10+
+ return session.send('Runtime.enable', {})
11+
+ }
12+
+ })(),
13+
session.send('Runtime.addBinding', { name: kBindingName }),
14+
session.send('Page.enable'),
15+
session.send('Page.addScriptToEvaluateOnNewDocument', { source: `
16+
17+
--- a/src/server/chromium/crPage.ts
18+
+++ b/src/server/chromium/crPage.ts
19+
@@ -528,7 +528,11 @@
20+
}),
21+
this._client.send('Log.enable', {}),
22+
lifecycleEventsEnabled = this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
23+
- this._client.send('Runtime.enable', {}),
24+
+ (() => {
25+
+ if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
26+
+ return this._client.send('Runtime.enable', {})
27+
+ }
28+
+ })(),
29+
this._client.send('Runtime.addBinding', { name: PageBinding.kPlaywrightBinding }),
30+
this._client.send('Page.addScriptToEvaluateOnNewDocument', {
31+
source: '',
32+
@@ -744,8 +748,11 @@
33+
session.once('Runtime.executionContextCreated', async event => {
34+
worker._createExecutionContext(new CRExecutionContext(session, event.context));
35+
});
36+
- // This might fail if the target is closed before we initialize.
37+
- session._sendMayFail('Runtime.enable');
38+
+ if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
39+
+ // This might fail if the target is closed before we initialize.
40+
+ session._sendMayFail('Runtime.enable');
41+
+ }
42+
+
43+
// TODO: attribute workers to the right frame.
44+
this._crPage._networkManager.addSession(session, this._page._frameManager.frame(this._targetId) ?? undefined).catch(() => {});
45+
session._sendMayFail('Runtime.runIfWaitingForDebugger');
46+
47+
--- a/src/server/chromium/crServiceWorker.ts
48+
+++ b/src/server/chromium/crServiceWorker.ts
49+
@@ -44,7 +44,9 @@
50+
this._networkManager.addSession(session, undefined, true /* isMain */).catch(() => {});
51+
}
52+
53+
- session.send('Runtime.enable', {}).catch(e => { });
54+
+ if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
55+
+ session.send('Runtime.enable', {}).catch(e => { });
56+
+ }
57+
session.send('Runtime.runIfWaitingForDebugger').catch(e => { });
58+
session.on('Inspector.targetReloadedAfterCrash', () => {
59+
// Resume service worker after restart.
60+
61+
--- a/src/server/frames.ts
62+
+++ b/src/server/frames.ts
63+
@@ -41,6 +41,7 @@
64+
import { FrameSelectors } from './frameSelectors';
65+
import { TimeoutError } from './errors';
66+
import { prepareFilesForUpload } from './fileUploadUtils';
67+
+import { CRExecutionContext } from './chromium/crExecutionContext';
68+
69+
type ContextData = {
70+
contextPromise: ManualPromise<dom.FrameExecutionContext | { destroyedReason: string }>;
71+
@@ -100,6 +101,7 @@
72+
private _webSockets = new Map<string, network.WebSocket>();
73+
_openedDialogs: Set<Dialog> = new Set();
74+
private _closeAllOpeningDialogs = false;
75+
+ _isolatedContext = undefined;
76+
77+
constructor(page: Page) {
78+
this._page = page;
79+
@@ -531,6 +533,7 @@
80+
this._startNetworkIdleTimer();
81+
this._page.mainFrame()._recalculateNetworkIdle(this);
82+
this._onLifecycleEvent('commit');
83+
+ this._isolatedContext = undefined
84+
}
85+
86+
setPendingDocument(documentInfo: DocumentInfo | undefined) {
87+
@@ -735,6 +738,38 @@
88+
}
89+
90+
_context(world: types.World): Promise<dom.FrameExecutionContext> {
91+
+ if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
92+
+ // rebrowser-patches: use only utility context, create it on demand and cache
93+
+ if (this._isolatedContext !== undefined) {
94+
+ return this._isolatedContext
95+
+ }
96+
+
97+
+ const utilityWorldName = process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] !== '0' ? (process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] || 'util') : '__playwright_utility_world__';
98+
+ return this._page._delegate._mainFrameSession._client.send('Page.createIsolatedWorld', {
99+
+ frameId: this._id,
100+
+ grantUniveralAccess: true,
101+
+ worldName: utilityWorldName,
102+
+ })
103+
+ .then((createIsolatedWorldResult) => {
104+
+ const contextPayload = {
105+
+ id: createIsolatedWorldResult.executionContextId,
106+
+ name: utilityWorldName,
107+
+ auxData: {
108+
+ frameId: this._id,
109+
+ isDefault: false
110+
+ }
111+
+ }
112+
+ const delegate = new CRExecutionContext(this._page._delegate._mainFrameSession._client, contextPayload);
113+
+ this._isolatedContext = new dom.FrameExecutionContext(delegate, this, 'utility');
114+
+ return this._isolatedContext
115+
+ })
116+
+ .catch(error => {
117+
+ // probably target is closed
118+
+ debugLogger.log('error', error)
119+
+ console.error('[rebrowser-patches][frames._context] cannot create utility world')
120+
+ })
121+
+ }
122+
+
123+
return this._contextData.get(world)!.contextPromise.then(contextOrDestroyedReason => {
124+
if (contextOrDestroyedReason instanceof js.ExecutionContext)
125+
return contextOrDestroyedReason;

scripts/patcher.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getPatchBaseCmd,
1313
getPatcherPackagePath,
1414
log,
15+
validPackagesNames,
1516
} from './utils/index.js';
1617

1718
(async () => {
@@ -21,8 +22,7 @@ import {
2122
.command('patch', 'Apply patch')
2223
.command('unpatch', 'Reverse patch')
2324
.command('check', 'Check if patch is already applied')
24-
.describe('packageName', 'Target package name: puppeteer-core, playwright')
25-
.default('packageName', 'puppeteer-core')
25+
.describe('packageName', `Target package name: ${validPackagesNames.join(', ')}`)
2626
.describe('packagePath', 'Path to the target package')
2727
.boolean('debug')
2828
.describe('debug', 'Enable debugging mode')
@@ -42,6 +42,10 @@ import {
4242
const command = cliArgs._[0]
4343
let commandResult
4444

45+
if (!packagePath && !packageName) {
46+
fatalError('You need to specify either packageName or packagePath.')
47+
}
48+
4549
if (!packagePath) {
4650
packagePath = `${process.cwd()}/node_modules/${packageName}`
4751
}
@@ -50,12 +54,9 @@ import {
5054
fatalError(`Unknown command: ${command}`)
5155
}
5256

53-
const patchFilePath = resolve(getPatcherPackagePath(), `./patches/${packageName}/22.13.x.patch`)
54-
5557
log('Config:')
5658
log(`command = ${command}, packageName = ${packageName}`)
5759
log(`packagePath = ${packagePath}`)
58-
log(`patchFilePath = ${patchFilePath}`)
5960
log('------')
6061

6162
// find package
@@ -67,11 +68,19 @@ import {
6768
} catch (err) {
6869
fatalError('Cannot read package.json', err)
6970
}
70-
if (packageJson.name !== packageName) {
71+
if (!packageName) {
72+
if (!validPackagesNames.includes(packageJson.name)) {
73+
fatalError(`Package name is "${packageJson.name}", but we only support ${validPackagesNames.join(', ')}.`)
74+
} else {
75+
packageName = packageJson.name
76+
}
77+
} else if (packageJson.name !== packageName) {
7178
fatalError(`Package name is "${packageJson.name}", but we're looking for "${packageName}". Check your package path.`)
7279
}
7380
log(`Found package "${packageJson.name}", version ${packageJson.version}`)
7481

82+
const patchFilePath = resolve(getPatcherPackagePath(), `./patches/${packageName}/${packageName === 'puppeteer-core' ? '22.13.x' : '1.47.x-lib'}.patch`)
83+
7584
// check patch status
7685
let patchStatus
7786
try {

scripts/utils/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { exec as execNative } from 'node:child_process'
2-
import { readdir } from 'node:fs/promises'
32
import { dirname, resolve } from 'node:path'
43
import { fileURLToPath } from 'node:url'
54
import { promisify } from 'node:util'
65

76
const promisifiedExec = promisify(execNative)
87

8+
export const validPackagesNames = ['puppeteer-core', 'playwright-core']
9+
910
export const exec = async (...args) => {
1011
if (isDebug()) {
1112
log('[debug][exec]', args)

0 commit comments

Comments
 (0)