├── .gitignore
├── README.md
├── package.json
├── patches
├── playwright-core
│ ├── lib.patch
│ └── src.patch
└── puppeteer-core
│ ├── lib.patch
│ └── src.patch
└── scripts
├── patcher.js
└── utils
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | yarn.lock
3 | yarn-error.log*
4 | .DS_Store
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🪄 Patches for undetectable browser automation
2 |
3 | This repo contains patches to enhance popular web automation libraries. Specifically, it targets the [`puppeteer`](https://github.com/puppeteer/puppeteer) and [`playwright`](https://github.com/microsoft/playwright) packages.
4 |
5 | Some aspects of automation libraries or browser behavior cannot be adjusted through settings or command-line switches. Therefore, we fix these issues by patching the library's source code. While this approach is fragile and may break as the libraries' source code changes over time, the goal is to maintain this repo with community help to keep the patches up to date.
6 |
7 | ## Do I really need any patches?
8 | Out of the box Puppeteer and Playwright come with some significant leaks that are easy to detect. It doesn't matter how good your proxies, fingeprints, and behaviour scripts, if you don't have it patched, you're just a big red flag for any major website.
9 |
10 | 🕵️ You can easily test your automation setup for major modern detections with [rebrowser-bot-detector](https://bot-detector.rebrowser.net/) ([sources and details](https://github.com/rebrowser/rebrowser-bot-detector))
11 |
12 | | Before the patches 👎 | After the patches 👍 |
13 | |--------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|
14 | |  |  |
15 |
16 | ## Is there an easy drop-in replacement?
17 | If you don't want to mess with the patches and all possible errors, there is a drop-in solution for you. These packages have simply applied rebrowser-patches on top of the original code, nothing more.
18 |
19 | Puppeteer: [rebrowser-puppeteer](https://www.npmjs.com/package/rebrowser-puppeteer) ([src](https://github.com/rebrowser/rebrowser-puppeteer)) and [rebrowser-puppeteer-core](https://www.npmjs.com/package/rebrowser-puppeteer-core) ([src](https://github.com/rebrowser/rebrowser-puppeteer-core))
20 |
21 | Playwright (Node.js): [rebrowser-playwright](https://www.npmjs.com/package/rebrowser-playwright) ([src](https://github.com/rebrowser/rebrowser-playwright)) and [rebrowser-playwright-core](https://www.npmjs.com/package/rebrowser-playwright-core) ([src](https://github.com/rebrowser/rebrowser-playwright-core))
22 |
23 | Playwright (Python): [rebrowser-playwright](https://pypi.org/project/rebrowser-playwright/) ([src](https://github.com/rebrowser/rebrowser-playwright-python))
24 |
25 | 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:
26 | 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"`. Note: 23.3.1 is just an example, check the latest version on [npm](https://www.npmjs.com/package/rebrowser-puppeteer-core).
27 | 2. Run `npm install` (or `yarn install`)
28 |
29 | Another way is to actually use new packages instead of the original one. Here are the steps you need to follow:
30 | 1. Open `package.json` and replace `puppeteer` and `puppeteer-core` packages with `rebrowser-puppeteer` and `rebrowser-puppeteer-core`. Don't change versions of the packages, just replace the names.
31 | 2. Run `npm install` (or `yarn install`)
32 | 3. Find and replace in your scripts any mentions of `puppeteer` and `puppeteer-core` with `rebrowser-puppeteer` and `rebrowser-puppeteer-core`
33 |
34 | 🚀 That's it! Just visit the [rebrowser-bot-detector](https://bot-detector.rebrowser.net/) page and test your patched browser.
35 |
36 | Our goal is to maintain and support these drop-in replacement packages with the latest versions, but we mainly focus on fresh versions, so if you're still using puppeteer 13.3.7 from the early 90s, it might be a good time to upgrade. There's a high chance that it won't really break anything as the API is quite stable over time.
37 |
38 | ## Available patches
39 | ### Fix `Runtime.Enable` leak
40 | Popular automation libraries rely on the CDP command `Runtime.Enable`, which allows receiving events from the `Runtime.` domain. This is crucial for managing execution contexts used to evaluate JavaScript on pages, a key feature for any automation process.
41 |
42 | However, there's a technique that detects the usage of this command, revealing that the browser is controlled by automation software like Puppeteer or Playwright. This technique is **used by all major anti-bot software** such as Cloudflare, DataDome, and others.
43 |
44 | > We've prepared a full article about our investigation on this leak, which you can read in [our blog](https://rebrowser.net/blog/how-to-fix-runtime-enable-cdp-detection-of-puppeteer-playwright-and-other-automation-libraries-61740).
45 |
46 | For more details on this technique, read DataDome's blog post: [How New Headless Chrome & the CDP Signal Are Impacting Bot Detection](https://datadome.co/threat-research/how-new-headless-chrome-the-cdp-signal-are-impacting-bot-detection/).
47 | In brief, it's a few lines of JavaScript on the page that are automatically called if `Runtime.Enable` was used.
48 |
49 | Our fix disables the automatic `Runtime.Enable` command on every frame. Instead, we manually create contexts with unknown IDs when a frame is created. Then, when code needs to be executed, there are multiple ways to get the context ID.
50 |
51 | #### 1. Create a new binding in the main world, call it and save its context ID.
52 | 🟢 Pros: The ultimate approach that keeps access to the main world and works with web workers and iframes. You don't need to change any of your existing codebase.
53 |
54 | 🔴 Cons: None are discovered so far.
55 |
56 | #### 2. Create a new isolated context via `Page.createIsolatedWorld` and save its ID.
57 | 🟢 Pros: All your code will be executed in a separate isolated world, preventing page scripts from detecting your changes via MutationObserver and other techniques.
58 |
59 | 🔴 Cons: You won't be able to access main context variables and code. While this is necessary for some use cases, the isolated context generally works fine for most scenarios. Also, web workers don't allow creating new worlds, so you can't execute your code inside a worker. This is a niche use case but may matter in some situations. There is a workaround for this issue, please read [How to Access Main Context Objects from Isolated Context in Puppeteer & Playwright](https://rebrowser.net/blog/how-to-access-main-context-objects-from-isolated-context-in-puppeteer-and-playwright-23741).
60 |
61 | #### 3. Call `Runtime.Enable` and then immediately call `Runtime.Disable`.
62 | This triggers `Runtime.executionContextCreated` events, allowing us to catch the proper context ID.
63 |
64 | 🟢 Pros: You will have full access to the main context.
65 |
66 | 🔴 Cons: There's a slight chance that during this short timeframe, the page will call code that leads to the leak. The risk is low, as detection code is usually called during specific actions like CAPTCHA pages or login/registration forms, typically right after the page loads. Your business logic is usually called a bit later.
67 |
68 | > 🎉 Our tests show that all these approaches are currently undetectable by Cloudflare or DataDome.
69 |
70 | Note: you can change settings for this patch on the fly using an environment variable. This allows you to easily switch between patched and non-patched versions based on your business logic.
71 |
72 | - `REBROWSER_PATCHES_RUNTIME_FIX_MODE=addBinding` — addBinding technique (default)
73 | - `REBROWSER_PATCHES_RUNTIME_FIX_MODE=alwaysIsolated` — always run all scripts in isolated context
74 | - `REBROWSER_PATCHES_RUNTIME_FIX_MODE=enableDisable` — use Enable/Disable technique
75 | - `REBROWSER_PATCHES_RUNTIME_FIX_MODE=0` — completely disable fix for this leak
76 | - `REBROWSER_PATCHES_DEBUG=1` — enable some debugging messages
77 |
78 | Remember, you can set these variables in different ways, for example, in code:
79 | ```js
80 | process.env.REBROWSER_PATCHES_RUNTIME_FIX_MODE = "alwaysIsolated"
81 | ```
82 | or in command line:
83 | ```shell
84 | REBROWSER_PATCHES_RUNTIME_FIX_MODE=alwaysIsolated node app.js
85 | ```
86 |
87 | ### Change sourceURL to generic script name
88 | By default, Puppeteer adds `//# sourceURL=pptr:...` to every script in `page.evaluate()`. A remote website can detect this behavior and raise red flags.
89 | This patch changes it to `//# sourceURL=app.js`. You can also adjust it via environment variable:
90 | ```shell
91 | # use any generic filename
92 | REBROWSER_PATCHES_SOURCE_URL=jquery.min.js
93 | # use 0 to completely disable this patch
94 | REBROWSER_PATCHES_SOURCE_URL=0
95 | ```
96 |
97 | ### Method to access browser CDP connection
98 | Sometimes, it could be very useful to access a CDP session at a browser level. For example, when you want to implement some custom CDP command. There is a method `page._client()` that returns CDP session for the current page instance, but there is no such method for browser instance.
99 | This patch adds a new method `_connection()` to Browser class, so you can use it like this:
100 | ```js
101 | browser._connection().on('Rebrowser.addRunEvent', (params) => { ... })
102 | ```
103 | *Note: it's not detectable by external website scripts, it's just for your convenience.*
104 |
105 | ### Change default utility world name
106 | The default utility world name is `'__puppeteer_utility_world__' + packageVersion`. Sometimes you might want to change it to something else. This patch changes it to `util` and allows you to customize it via env variable:
107 | ```shell
108 | REBROWSER_PATCHES_UTILITY_WORLD_NAME=customUtilityWorld
109 | # use 0 to completely disable this patch
110 | REBROWSER_PATCHES_UTILITY_WORLD_NAME=0
111 | ```
112 | This env variable cannot be changed on the fly, you have to set it before running your script because it's used at the moment when the module is getting imported.
113 |
114 | | Before patch 👎 | After patch 👍 |
115 | |--------| --- |
116 | |  |  |
117 |
118 |
119 | *Note: it's not detectable by external website scripts, but Google might use this information in their proprietary Chrome; we never know.*
120 |
121 | ## Usage
122 | This package is designed to be run against an installed library. Install the library, then call the patcher, and it's ready to go.
123 |
124 | In the root folder of your project, run:
125 | ```
126 | npx rebrowser-patches@latest patch --packageName puppeteer-core
127 | ```
128 |
129 | You can easily revert all changes with this command:
130 | ```
131 | npx rebrowser-patches@latest unpatch --packageName puppeteer-core
132 | ```
133 |
134 | You can also patch a package by providing the full path to its folder, for example:
135 |
136 | ```
137 | npx rebrowser-patches@latest patch --packagePath /web/app/node_modules/puppeteer-core-custom
138 | ```
139 |
140 | You can see all command-line options by running `npx rebrowser-patches@latest --help`, but currently, there's just one patch for one library, so you don't need to configure anything.
141 |
142 | ⚠️ Be aware that after running `npm install` or `yarn install` in your project folder, it might override all the changes from the patches. You'll need to run the patcher again to keep the patches in place.
143 |
144 | ## How to update the patches?
145 | 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`.
146 |
147 | ## How to patch Java/Python/.NET versions of Playwright?
148 | All these versions are just wrappers around Node.js version of Playwright. You need to find `driver` folder inside your Playwright package and run this patch with `--packagePath=$yourDriverFolder/$yourPlatform/package`.
149 |
150 | ## Puppeteer support
151 | ✅ Latest fully tested version: 24.8.1 (released 2025-05-06)
152 |
153 | ## Playwright support
154 | Playwright patches include:
155 | - `Runtime.enable` leak: `addBinding` and `alwaysIsolated`modes.
156 | - Ability to change utility world name via `REBROWSER_PATCHES_UTILITY_WORLD_NAME` env variable.
157 | - More patches are coming, star and follow the repo.
158 |
159 | Important: `page.pause()` method doesn't work with the enabled fix, it needs more investigation. You can just disable the fix completely while debugging using `REBROWSER_PATCHES_RUNTIME_FIX_MODE=0` env variable.
160 |
161 | These patches work only for Chrome for now. If you really want to use it with WebKit or Firefox, please open a new issue.
162 |
163 | ✅ Latest fully tested version: 1.52.0 (released 2025-04-17)
164 |
165 | ## How to use `rebrowser-puppeteer` with `puppeteer-extra`?
166 | Use `addExtra` method, here is the example:
167 | ```
168 | // before
169 | import puppeteer from 'puppeteer-extra'
170 |
171 | // after
172 | import { addExtra } from 'puppeteer-extra'
173 | import rebrowserPuppeteer from 'rebrowser-puppeteer-core'
174 | const puppeteer = addExtra(rebrowserPuppeteer)
175 | ```
176 |
177 | ## Follow the project
178 | 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.
179 |
180 | 💭 If you have any ideas, thoughts, or questions, feel free to reach out to our team by [email](mailto:info@rebrowser.net) or use the [issues section](https://github.com/rebrowser/rebrowser-patches/issues).
181 |
182 | ## The fix doesn't help, I'm still getting blocked 🤯
183 | ⚠️ It's important to know that this fix alone won't make your browser bulletproof and undetectable. You need to address **many other aspects** such as proxies, proper user-agent and fingerprints (canvas, WebGL), and more.
184 |
185 | Always keep in mind: the less you manipulate browser internals via JS injections, the better. There are ways to detect that internal objects such as console, navigator, and others were affected by Proxy objects or Object.defineProperty. It's tricky, but it's always a cat-and-mouse game.
186 |
187 | If you've tried everything and still face issues, try asking a question in the issues section or consider using cloud solutions from Rebrowser.
188 |
189 | ## What is Rebrowser?
190 | This package is sponsored and maintained by [Rebrowser](https://rebrowser.net). We allow you to scale your browser automation and web scraping in the cloud with hundreds of unique fingerprints.
191 |
192 | Our cloud browsers have great success rates and come with nice features such as notifications if your library uses `Runtime.Enable` during execution or has other red flags that could be improved. [Create an account](https://rebrowser.net) today to get invited to test our bleeding-edge platform and take your automation business to the next level.
193 |
194 | [](https://rebrowser.net/docs/sensitive-cdp-methods)
195 |
196 | ## Patch command on Windows
197 | When you try to run this patcher on a Windows machine, you will probably encounter an error because the patch command is not found. To fix this, you need to install [Git](https://git-scm.com/download/win), which includes patch.exe. After you have installed it, you need to add it to your PATH:
198 |
199 | ```
200 | set PATH=%PATH%;C:\Program Files\Git\usr\bin\
201 | ```
202 |
203 | You can check that patch.exe is installed correctly by using next command:
204 | ```
205 | patch -v
206 | ```
207 |
208 | ### Special thanks
209 | [zfcsoftware/puppeteer-real-browser](https://github.com/zfcsoftware/puppeteer-real-browser) - general ideas and contribution to the automation community
210 |
211 | [Kaliiiiiiiiii-Vinyzu/patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright) - set of patches to fix Playwright leaks
212 |
213 | [kaliiiiiiiiii/brotector](https://github.com/kaliiiiiiiiii/brotector) - some modern tests, algorithm to distinguish CDP vs devtools
214 |
215 | [prescience-data/harden-puppeteer](https://github.com/prescience-data/harden-puppeteer) - one of the pioneers of the execution in an isolated world
216 |
217 | [puppeteer-extra-plugin-stealth](https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth) - where it all started, big props to all the contributors and the community 🙏 berstend and co are the goats
218 |
219 |
220 | ### Disclaimer
221 |
222 | No responsibility is accepted for the use of this software. This software is intended for educational and informational purposes only. Users should use this software at their own risk. The developers of the software cannot be held liable for any damages that may result from the use of this software. This software is not intended to bypass any security measures, including but not limited to CAPTCHAs, anti-bot systems, or other protective mechanisms employed by websites. The software must not be used for malicious purposes. By using this software, you agree to this disclaimer and acknowledge that you are using the software responsibly and in compliance with all applicable laws and regulations.
223 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rebrowser-patches",
3 | "version": "1.0.19",
4 | "description": "Collection of patches for puppeteer and playwright to avoid automation detection and leaks. Helps to avoid Cloudflare and DataDome CAPTCHA pages. Easy to patch/unpatch, can be enabled/disabled on demand.",
5 | "keywords": [
6 | "automation",
7 | "bot",
8 | "bot-detection",
9 | "crawler",
10 | "crawling",
11 | "chromedriver",
12 | "webdriver",
13 | "headless",
14 | "headless-chrome",
15 | "stealth",
16 | "captcha",
17 | "scraping",
18 | "web-scraping",
19 | "cloudflare",
20 | "datadome",
21 | "chrome",
22 | "puppeteer",
23 | "puppeteer-extra",
24 | "selenium",
25 | "playwright",
26 | "rebrowser",
27 | "rebrowser-patches"
28 | ],
29 | "author": {
30 | "name": "Rebrowser",
31 | "email": "info@rebrowser.net",
32 | "url": "https://rebrowser.net"
33 | },
34 | "contributors": [
35 | "Nick Webson "
36 | ],
37 | "repository": {
38 | "type": "git",
39 | "url": "git+https://github.com/rebrowser/rebrowser-patches"
40 | },
41 | "bugs": {
42 | "url": "https://github.com/rebrowser/rebrowser-patches/issues"
43 | },
44 | "files": [
45 | "/patches",
46 | "/scripts"
47 | ],
48 | "homepage": "https://rebrowser.net",
49 | "main": "scripts/patcher.js",
50 | "bin": "./scripts/patcher.js",
51 | "license": "MIT",
52 | "type": "module",
53 | "scripts": {
54 | "start": "node ./scripts/patcher.js"
55 | },
56 | "dependencies": {
57 | "yargs": "^17.7.2"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/patches/playwright-core/lib.patch:
--------------------------------------------------------------------------------
1 | --- a/lib/server/chromium/crConnection.js
2 | +++ b/lib/server/chromium/crConnection.js
3 | @@ -159,6 +159,127 @@
4 | }
5 | this._callbacks.clear();
6 | }
7 | + async __re__emitExecutionContext({
8 | + world,
9 | + targetId,
10 | + frame = null
11 | + }) {
12 | + const fixMode = process.env["REBROWSER_PATCHES_RUNTIME_FIX_MODE"] || "addBinding";
13 | + const utilityWorldName = process.env["REBROWSER_PATCHES_UTILITY_WORLD_NAME"] !== "0" ? process.env["REBROWSER_PATCHES_UTILITY_WORLD_NAME"] || "util" : "__playwright_utility_world__";
14 | + process.env["REBROWSER_PATCHES_DEBUG"] && console.log(`[rebrowser-patches][crSession] targetId = ${targetId}, world = ${world}, frame = ${frame ? "Y" : "N"}, fixMode = ${fixMode}`);
15 | + let getWorldPromise;
16 | + if (fixMode === "addBinding") {
17 | + if (world === "utility") {
18 | + getWorldPromise = this.__re__getIsolatedWorld({
19 | + client: this,
20 | + frameId: targetId,
21 | + worldName: utilityWorldName
22 | + }).then((contextId) => {
23 | + return {
24 | + id: contextId,
25 | + // use UTILITY_WORLD_NAME value from crPage.ts otherwise _onExecutionContextCreated will ignore it
26 | + name: "__playwright_utility_world__",
27 | + auxData: {
28 | + frameId: targetId,
29 | + isDefault: false
30 | + }
31 | + };
32 | + });
33 | + } else if (world === "main") {
34 | + getWorldPromise = this.__re__getMainWorld({
35 | + client: this,
36 | + frameId: targetId,
37 | + isWorker: frame === null
38 | + }).then((contextId) => {
39 | + return {
40 | + id: contextId,
41 | + name: "",
42 | + auxData: {
43 | + frameId: targetId,
44 | + isDefault: true
45 | + }
46 | + };
47 | + });
48 | + }
49 | + } else if (fixMode === "alwaysIsolated") {
50 | + getWorldPromise = this.__re__getIsolatedWorld({
51 | + client: this,
52 | + frameId: targetId,
53 | + worldName: utilityWorldName
54 | + }).then((contextId) => {
55 | + return {
56 | + id: contextId,
57 | + name: "",
58 | + auxData: {
59 | + frameId: targetId,
60 | + isDefault: true
61 | + }
62 | + };
63 | + });
64 | + }
65 | + const contextPayload = await getWorldPromise;
66 | + this.emit("Runtime.executionContextCreated", {
67 | + context: contextPayload
68 | + });
69 | + }
70 | + async __re__getMainWorld({ client, frameId, isWorker = false }) {
71 | + let contextId;
72 | + const randomName = [...Array(Math.floor(Math.random() * (10 + 1)) + 10)].map(() => Math.random().toString(36)[2]).join("");
73 | + process.env["REBROWSER_PATCHES_DEBUG"] && console.log(`[rebrowser-patches][getMainWorld] binding name = ${randomName}`);
74 | + await client.send("Runtime.addBinding", {
75 | + name: randomName
76 | + });
77 | + const bindingCalledHandler = ({ name, payload, executionContextId }) => {
78 | + process.env["REBROWSER_PATCHES_DEBUG"] && console.log("[rebrowser-patches][bindingCalledHandler]", {
79 | + name,
80 | + payload,
81 | + executionContextId
82 | + });
83 | + if (contextId > 0) {
84 | + return;
85 | + }
86 | + if (name !== randomName) {
87 | + return;
88 | + }
89 | + if (payload !== frameId) {
90 | + return;
91 | + }
92 | + contextId = executionContextId;
93 | + client.off("Runtime.bindingCalled", bindingCalledHandler);
94 | + };
95 | + client.on("Runtime.bindingCalled", bindingCalledHandler);
96 | + if (isWorker) {
97 | + await client.send("Runtime.evaluate", {
98 | + expression: `this['${randomName}']('${frameId}')`
99 | + });
100 | + } else {
101 | + await client.send("Page.addScriptToEvaluateOnNewDocument", {
102 | + source: `document.addEventListener('${randomName}', (e) => self['${randomName}'](e.detail.frameId))`,
103 | + runImmediately: true
104 | + });
105 | + const createIsolatedWorldResult = await client.send("Page.createIsolatedWorld", {
106 | + frameId,
107 | + // use randomName for worldName to distinguish from normal utility world
108 | + worldName: randomName,
109 | + grantUniveralAccess: true
110 | + });
111 | + await client.send("Runtime.evaluate", {
112 | + expression: `document.dispatchEvent(new CustomEvent('${randomName}', { detail: { frameId: '${frameId}' } }))`,
113 | + contextId: createIsolatedWorldResult.executionContextId
114 | + });
115 | + }
116 | + process.env["REBROWSER_PATCHES_DEBUG"] && console.log(`[rebrowser-patches][getMainWorld] result:`, { contextId });
117 | + return contextId;
118 | + }
119 | + async __re__getIsolatedWorld({ client, frameId, worldName }) {
120 | + const createIsolatedWorldResult = await client.send("Page.createIsolatedWorld", {
121 | + frameId,
122 | + worldName,
123 | + grantUniveralAccess: true
124 | + });
125 | + process.env["REBROWSER_PATCHES_DEBUG"] && console.log(`[rebrowser-patches][getIsolatedWorld] result:`, createIsolatedWorldResult);
126 | + return createIsolatedWorldResult.executionContextId;
127 | + }
128 | }
129 | class CDPSession extends import_events.EventEmitter {
130 | constructor(parentSession, sessionId) {
131 |
132 | --- a/lib/server/chromium/crDevTools.js
133 | +++ b/lib/server/chromium/crDevTools.js
134 | @@ -72,7 +72,11 @@
135 | }).catch((e) => null);
136 | });
137 | Promise.all([
138 | - session.send("Runtime.enable"),
139 | + (() => {
140 | + if (process.env["REBROWSER_PATCHES_RUNTIME_FIX_MODE"] === "0") {
141 | + return session.send("Runtime.enable", {});
142 | + }
143 | + })(),
144 | session.send("Runtime.addBinding", { name: kBindingName }),
145 | session.send("Page.enable"),
146 | session.send("Page.addScriptToEvaluateOnNewDocument", { source: `
147 |
148 | --- a/lib/server/chromium/crPage.js
149 | +++ b/lib/server/chromium/crPage.js
150 | @@ -425,7 +425,11 @@
151 | }),
152 | this._client.send("Log.enable", {}),
153 | lifecycleEventsEnabled = this._client.send("Page.setLifecycleEventsEnabled", { enabled: true }),
154 | - this._client.send("Runtime.enable", {}),
155 | + (() => {
156 | + if (process.env["REBROWSER_PATCHES_RUNTIME_FIX_MODE"] === "0") {
157 | + return this._client.send("Runtime.enable", {});
158 | + }
159 | + })(),
160 | this._client.send("Runtime.addBinding", { name: import_page.PageBinding.kPlaywrightBinding }),
161 | this._client.send("Page.addScriptToEvaluateOnNewDocument", {
162 | source: "",
163 | @@ -601,13 +605,15 @@
164 | return;
165 | }
166 | const url = event.targetInfo.url;
167 | - const worker = new import_page2.Worker(this._page, url);
168 | + const worker = new import_page2.Worker(this._page, url, event.targetInfo.targetId, session);
169 | this._page._addWorker(event.sessionId, worker);
170 | this._workerSessions.set(event.sessionId, session);
171 | session.once("Runtime.executionContextCreated", async (event2) => {
172 | worker._createExecutionContext(new import_crExecutionContext.CRExecutionContext(session, event2.context));
173 | });
174 | - session._sendMayFail("Runtime.enable");
175 | + if (process.env["REBROWSER_PATCHES_RUNTIME_FIX_MODE"] === "0") {
176 | + session._sendMayFail("Runtime.enable");
177 | + }
178 | this._crPage._networkManager.addSession(session, this._page._frameManager.frame(this._targetId) ?? void 0).catch(() => {
179 | });
180 | session._sendMayFail("Runtime.runIfWaitingForDebugger");
181 |
182 | --- a/lib/server/chromium/crServiceWorker.js
183 | +++ b/lib/server/chromium/crServiceWorker.js
184 | @@ -59,8 +59,10 @@
185 | ).catch(() => {
186 | });
187 | }
188 | - session.send("Runtime.enable", {}).catch((e) => {
189 | - });
190 | + if (process.env["REBROWSER_PATCHES_RUNTIME_FIX_MODE"] === "0") {
191 | + session.send("Runtime.enable", {}).catch((e) => {
192 | + });
193 | + }
194 | session.send("Runtime.runIfWaitingForDebugger").catch((e) => {
195 | });
196 | session.on("Inspector.targetReloadedAfterCrash", () => {
197 |
198 | --- a/lib/server/frames.js
199 | +++ b/lib/server/frames.js
200 | @@ -434,6 +434,8 @@
201 | this._startNetworkIdleTimer();
202 | this._page.mainFrame()._recalculateNetworkIdle(this);
203 | this._onLifecycleEvent("commit");
204 | + const crSession = (this._page._delegate._sessions.get(this._id) || this._page._delegate._mainFrameSession)._client;
205 | + crSession.emit("Runtime.executionContextsCleared");
206 | }
207 | setPendingDocument(documentInfo) {
208 | this._pendingDocument = documentInfo;
209 | @@ -605,11 +607,29 @@
210 | async frameElement() {
211 | return this._page._delegate.getFrameElement(this);
212 | }
213 | - _context(world) {
214 | - return this._contextData.get(world).contextPromise.then((contextOrDestroyedReason) => {
215 | - if (contextOrDestroyedReason instanceof js.ExecutionContext)
216 | - return contextOrDestroyedReason;
217 | - throw new Error(contextOrDestroyedReason.destroyedReason);
218 | + _context(world, useContextPromise = false) {
219 | + if (process.env["REBROWSER_PATCHES_RUNTIME_FIX_MODE"] === "0" || this._contextData.get(world).context || useContextPromise) {
220 | + return this._contextData.get(world).contextPromise.then((contextOrDestroyedReason) => {
221 | + if (contextOrDestroyedReason instanceof js.ExecutionContext)
222 | + return contextOrDestroyedReason;
223 | + throw new Error(contextOrDestroyedReason.destroyedReason);
224 | + });
225 | + }
226 | + const crSession = (this._page._delegate._sessions.get(this._id) || this._page._delegate._mainFrameSession)._client;
227 | + return crSession.__re__emitExecutionContext({
228 | + world,
229 | + targetId: this._id,
230 | + frame: this
231 | + }).then(() => {
232 | + return this._context(world, true);
233 | + }).catch((error) => {
234 | + if (error.message.includes("No frame for given id found")) {
235 | + return {
236 | + destroyedReason: "Frame was detached"
237 | + };
238 | + }
239 | + import_debugLogger.debugLogger.log("error", error);
240 | + console.error("[rebrowser-patches][frames._context] cannot get world, error:", error);
241 | });
242 | }
243 | _mainContext() {
244 |
245 | --- a/lib/server/page.js
246 | +++ b/lib/server/page.js
247 | @@ -624,7 +624,7 @@
248 | }
249 | }
250 | class Worker extends import_instrumentation.SdkObject {
251 | - constructor(parent, url) {
252 | + constructor(parent, url, targetId, session) {
253 | super(parent, "worker");
254 | this._existingExecutionContext = null;
255 | this.openScope = new import_utils.LongStandingScope();
256 | @@ -632,6 +632,8 @@
257 | this._executionContextCallback = () => {
258 | };
259 | this._executionContextPromise = new Promise((x) => this._executionContextCallback = x);
260 | + this._targetId = targetId;
261 | + this._session = session;
262 | }
263 | static {
264 | this.Events = {
265 | @@ -652,11 +654,20 @@
266 | this.emit(Worker.Events.Close, this);
267 | this.openScope.close(new Error("Worker closed"));
268 | }
269 | + async getExecutionContext() {
270 | + if (process.env["REBROWSER_PATCHES_RUNTIME_FIX_MODE"] !== "0" && !this._existingExecutionContext) {
271 | + await this._session.__re__emitExecutionContext({
272 | + world: "main",
273 | + targetId: this._targetId
274 | + });
275 | + }
276 | + return this._executionContextPromise;
277 | + }
278 | async evaluateExpression(expression, isFunction, arg) {
279 | - return js.evaluateExpression(await this._executionContextPromise, expression, { returnByValue: true, isFunction }, arg);
280 | + return js.evaluateExpression(await this.getExecutionContext(), expression, { returnByValue: true, isFunction }, arg);
281 | }
282 | async evaluateExpressionHandle(expression, isFunction, arg) {
283 | - return js.evaluateExpression(await this._executionContextPromise, expression, { returnByValue: false, isFunction }, arg);
284 | + return js.evaluateExpression(await this.getExecutionContext(), expression, { returnByValue: false, isFunction }, arg);
285 | }
286 | }
287 | class PageBinding {
288 | @@ -675,6 +686,9 @@
289 | this.internal = name.startsWith("__pw");
290 | }
291 | static async dispatch(page, payload, context) {
292 | + if (process.env["REBROWSER_PATCHES_RUNTIME_FIX_MODE"] !== "0" && !payload.includes("{")) {
293 | + return;
294 | + }
295 | const { name, seq, serializedArgs } = JSON.parse(payload);
296 | try {
297 | (0, import_utils.assert)(context.world);
298 |
--------------------------------------------------------------------------------
/patches/playwright-core/src.patch:
--------------------------------------------------------------------------------
1 |
2 | --- a/src/server/chromium/crConnection.ts
3 | +++ b/src/server/chromium/crConnection.ts
4 | @@ -201,6 +201,153 @@
5 | }
6 | this._callbacks.clear();
7 | }
8 | +
9 | + async __re__emitExecutionContext({
10 | + world,
11 | + targetId,
12 | + frame = null
13 | + }) {
14 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding'
15 | + const utilityWorldName = process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] !== '0' ? (process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] || 'util') : '__playwright_utility_world__';
16 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][crSession] targetId = ${targetId}, world = ${world}, frame = ${frame ? 'Y' : 'N'}, fixMode = ${fixMode}`)
17 | +
18 | + let getWorldPromise
19 | + if (fixMode === 'addBinding') {
20 | + if (world === 'utility') {
21 | + getWorldPromise = this.__re__getIsolatedWorld({
22 | + client: this,
23 | + frameId: targetId,
24 | + worldName: utilityWorldName,
25 | + })
26 | + .then((contextId) => {
27 | + return {
28 | + id: contextId,
29 | + // use UTILITY_WORLD_NAME value from crPage.ts otherwise _onExecutionContextCreated will ignore it
30 | + name: '__playwright_utility_world__',
31 | + auxData: {
32 | + frameId: targetId,
33 | + isDefault: false
34 | + }
35 | + }
36 | + })
37 | + } else if (world === 'main') {
38 | + getWorldPromise = this.__re__getMainWorld({
39 | + client: this,
40 | + frameId: targetId,
41 | + isWorker: frame === null
42 | + })
43 | + .then((contextId) => {
44 | + return {
45 | + id: contextId,
46 | + name: '',
47 | + auxData: {
48 | + frameId: targetId,
49 | + isDefault: true
50 | + }
51 | + }
52 | + })
53 | + }
54 | + } else if (fixMode === 'alwaysIsolated') {
55 | + // use only utility context
56 | + getWorldPromise = this.__re__getIsolatedWorld({
57 | + client: this,
58 | + frameId: targetId,
59 | + worldName: utilityWorldName,
60 | + })
61 | + .then((contextId) => {
62 | + // make it look as main world
63 | + return {
64 | + id: contextId,
65 | + name: '',
66 | + auxData: {
67 | + frameId: targetId,
68 | + isDefault: true
69 | + }
70 | + }
71 | + })
72 | + }
73 | +
74 | + const contextPayload = await getWorldPromise
75 | + this.emit('Runtime.executionContextCreated', {
76 | + context: contextPayload
77 | + })
78 | + }
79 | + async __re__getMainWorld({ client, frameId, isWorker = false }: any) {
80 | + let contextId: any;
81 | +
82 | + // random name to make it harder to detect for any 3rd party script by watching window object and events
83 | + const randomName = [...Array(Math.floor(Math.random() * (10 + 1)) + 10)].map(() => Math.random().toString(36)[2]).join('');
84 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] binding name = ${randomName}`);
85 | +
86 | + // add the binding
87 | + await client.send('Runtime.addBinding', {
88 | + name: randomName,
89 | + });
90 | +
91 | + // listen for 'Runtime.bindingCalled' event
92 | + const bindingCalledHandler = ({ name, payload, executionContextId }: any) => {
93 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][bindingCalledHandler]', {
94 | + name,
95 | + payload,
96 | + executionContextId
97 | + });
98 | + if (contextId > 0) {
99 | + // already acquired the id
100 | + return;
101 | + }
102 | + if (name !== randomName) {
103 | + // ignore irrelevant bindings
104 | + return;
105 | + }
106 | + if (payload !== frameId) {
107 | + // ignore irrelevant frames
108 | + return;
109 | + }
110 | + contextId = executionContextId;
111 | + // remove this listener
112 | + client.off('Runtime.bindingCalled', bindingCalledHandler);
113 | + };
114 | + client.on('Runtime.bindingCalled', bindingCalledHandler);
115 | +
116 | + if (isWorker) {
117 | + // workers don't support `Page.addScriptToEvaluateOnNewDocument` and `Page.createIsolatedWorld`, but there are no iframes inside of them, so it's safe to just use Runtime.evaluate
118 | + await client.send('Runtime.evaluate', {
119 | + expression: `this['${randomName}']('${frameId}')`,
120 | + });
121 | + } else {
122 | + // we could call the binding right from `addScriptToEvaluateOnNewDocument`, but this way it will be called in all existing frames and it's hard to distinguish children from the parent
123 | + await client.send('Page.addScriptToEvaluateOnNewDocument', {
124 | + source: `document.addEventListener('${randomName}', (e) => self['${randomName}'](e.detail.frameId))`,
125 | + runImmediately: true,
126 | + });
127 | +
128 | + // create new isolated world for this frame
129 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', {
130 | + frameId,
131 | + // use randomName for worldName to distinguish from normal utility world
132 | + worldName: randomName,
133 | + grantUniveralAccess: true,
134 | + });
135 | +
136 | + // emit event in the specific frame from the isolated world
137 | + await client.send('Runtime.evaluate', {
138 | + expression: `document.dispatchEvent(new CustomEvent('${randomName}', { detail: { frameId: '${frameId}' } }))`,
139 | + contextId: createIsolatedWorldResult.executionContextId,
140 | + });
141 | + }
142 | +
143 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] result:`, { contextId });
144 | + return contextId;
145 | + }
146 | + async __re__getIsolatedWorld({ client, frameId, worldName }: any) {
147 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', {
148 | + frameId,
149 | + worldName,
150 | + grantUniveralAccess: true,
151 | + });
152 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getIsolatedWorld] result:`, createIsolatedWorldResult);
153 | + return createIsolatedWorldResult.executionContextId;
154 | + }
155 | }
156 |
157 | export class CDPSession extends EventEmitter {
158 |
159 | --- a/src/server/chromium/crDevTools.ts
160 | +++ b/src/server/chromium/crDevTools.ts
161 | @@ -67,7 +67,11 @@
162 | }).catch(e => null);
163 | });
164 | Promise.all([
165 | - session.send('Runtime.enable'),
166 | + (() => {
167 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
168 | + return session.send('Runtime.enable', {})
169 | + }
170 | + })(),
171 | session.send('Runtime.addBinding', { name: kBindingName }),
172 | session.send('Page.enable'),
173 | session.send('Page.addScriptToEvaluateOnNewDocument', { source: `
174 |
175 | --- a/src/server/chromium/crPage.ts
176 | +++ b/src/server/chromium/crPage.ts
177 | @@ -507,7 +507,11 @@
178 | }),
179 | this._client.send('Log.enable', {}),
180 | lifecycleEventsEnabled = this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
181 | - this._client.send('Runtime.enable', {}),
182 | + (() => {
183 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
184 | + return this._client.send('Runtime.enable', {})
185 | + }
186 | + })(),
187 | this._client.send('Runtime.addBinding', { name: PageBinding.kPlaywrightBinding }),
188 | this._client.send('Page.addScriptToEvaluateOnNewDocument', {
189 | source: '',
190 | @@ -719,14 +723,17 @@
191 | }
192 |
193 | const url = event.targetInfo.url;
194 | - const worker = new Worker(this._page, url);
195 | + const worker = new Worker(this._page, url, event.targetInfo.targetId, session);
196 | this._page._addWorker(event.sessionId, worker);
197 | this._workerSessions.set(event.sessionId, session);
198 | session.once('Runtime.executionContextCreated', async event => {
199 | worker._createExecutionContext(new CRExecutionContext(session, event.context));
200 | });
201 | - // This might fail if the target is closed before we initialize.
202 | - session._sendMayFail('Runtime.enable');
203 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
204 | + // This might fail if the target is closed before we initialize.
205 | + session._sendMayFail('Runtime.enable');
206 | + }
207 | +
208 | // TODO: attribute workers to the right frame.
209 | this._crPage._networkManager.addSession(session, this._page._frameManager.frame(this._targetId) ?? undefined).catch(() => {});
210 | session._sendMayFail('Runtime.runIfWaitingForDebugger');
211 |
212 | --- a/src/server/chromium/crServiceWorker.ts
213 | +++ b/src/server/chromium/crServiceWorker.ts
214 | @@ -45,7 +45,9 @@
215 | this._networkManager.addSession(session, undefined, true /* isMain */).catch(() => {});
216 | }
217 |
218 | - session.send('Runtime.enable', {}).catch(e => { });
219 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
220 | + session.send('Runtime.enable', {}).catch(e => { });
221 | + }
222 | session.send('Runtime.runIfWaitingForDebugger').catch(e => { });
223 | session.on('Inspector.targetReloadedAfterCrash', () => {
224 | // Resume service worker after restart.
225 |
226 | --- a/src/server/frames.ts
227 | +++ b/src/server/frames.ts
228 | @@ -540,6 +540,8 @@
229 | this._startNetworkIdleTimer();
230 | this._page.mainFrame()._recalculateNetworkIdle(this);
231 | this._onLifecycleEvent('commit');
232 | + const crSession = (this._page._delegate._sessions.get(this._id) || this._page._delegate._mainFrameSession)._client
233 | + crSession.emit('Runtime.executionContextsCleared')
234 | }
235 |
236 | setPendingDocument(documentInfo: DocumentInfo | undefined) {
237 | @@ -743,12 +745,34 @@
238 | return this._page._delegate.getFrameElement(this);
239 | }
240 |
241 | - _context(world: types.World): Promise {
242 | - return this._contextData.get(world)!.contextPromise.then(contextOrDestroyedReason => {
243 | - if (contextOrDestroyedReason instanceof js.ExecutionContext)
244 | - return contextOrDestroyedReason;
245 | - throw new Error(contextOrDestroyedReason.destroyedReason);
246 | - });
247 | + _context(world: types.World, useContextPromise = false): Promise {
248 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0' || this._contextData.get(world).context || useContextPromise) {
249 | + return this._contextData.get(world)!.contextPromise.then(contextOrDestroyedReason => {
250 | + if (contextOrDestroyedReason instanceof js.ExecutionContext)
251 | + return contextOrDestroyedReason;
252 | + throw new Error(contextOrDestroyedReason.destroyedReason);
253 | + });
254 | + }
255 | +
256 | + const crSession = (this._page._delegate._sessions.get(this._id) || this._page._delegate._mainFrameSession)._client
257 | + return crSession.__re__emitExecutionContext({
258 | + world,
259 | + targetId: this._id,
260 | + frame: this
261 | + })
262 | + .then(() => {
263 | + return this._context(world, true)
264 | + })
265 | + .catch(error => {
266 | + if (error.message.includes('No frame for given id found')) {
267 | + // ignore, frame is already gone
268 | + return {
269 | + destroyedReason: 'Frame was detached'
270 | + }
271 | + }
272 | + debugLogger.log('error', error)
273 | + console.error('[rebrowser-patches][frames._context] cannot get world, error:', error)
274 | + })
275 | }
276 |
277 | _mainContext(): Promise {
278 |
279 | --- a/src/server/page.ts
280 | +++ b/src/server/page.ts
281 | @@ -823,12 +823,16 @@
282 | private _executionContextCallback: (value: js.ExecutionContext) => void;
283 | _existingExecutionContext: js.ExecutionContext | null = null;
284 | readonly openScope = new LongStandingScope();
285 | + private _targetId: string;
286 | + private _session: any;
287 |
288 | - constructor(parent: SdkObject, url: string) {
289 | + constructor(parent: SdkObject, url: string, targetId: string, session: any) {
290 | super(parent, 'worker');
291 | this._url = url;
292 | this._executionContextCallback = () => {};
293 | this._executionContextPromise = new Promise(x => this._executionContextCallback = x);
294 | + this._targetId = targetId
295 | + this._session = session
296 | }
297 |
298 | _createExecutionContext(delegate: js.ExecutionContextDelegate) {
299 | @@ -848,12 +852,23 @@
300 | this.openScope.close(new Error('Worker closed'));
301 | }
302 |
303 | + async getExecutionContext() {
304 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0' && !this._existingExecutionContext) {
305 | + await this._session.__re__emitExecutionContext({
306 | + world: 'main',
307 | + targetId: this._targetId,
308 | + })
309 | + }
310 | +
311 | + return this._executionContextPromise
312 | + }
313 | +
314 | async evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any): Promise {
315 | - return js.evaluateExpression(await this._executionContextPromise, expression, { returnByValue: true, isFunction }, arg);
316 | + return js.evaluateExpression(await this.getExecutionContext(), expression, { returnByValue: true, isFunction }, arg);
317 | }
318 |
319 | async evaluateExpressionHandle(expression: string, isFunction: boolean | undefined, arg: any): Promise {
320 | - return js.evaluateExpression(await this._executionContextPromise, expression, { returnByValue: false, isFunction }, arg);
321 | + return js.evaluateExpression(await this.getExecutionContext(), expression, { returnByValue: false, isFunction }, arg);
322 | }
323 | }
324 |
325 | @@ -875,6 +890,10 @@
326 | }
327 |
328 | static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) {
329 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0' && !payload.includes('{')) {
330 | + // ignore as it's not a JSON but a string from addBinding method
331 | + return
332 | + }
333 | const { name, seq, serializedArgs } = JSON.parse(payload) as BindingPayload;
334 | try {
335 | assert(context.world);
336 |
--------------------------------------------------------------------------------
/patches/puppeteer-core/lib.patch:
--------------------------------------------------------------------------------
1 | --- a/lib/cjs/puppeteer/cdp/Browser.d.ts
2 | +++ b/lib/cjs/puppeteer/cdp/Browser.d.ts
3 | @@ -32,6 +32,7 @@
4 | _disposeContext(contextId?: string): Promise;
5 | wsEndpoint(): string;
6 | newPage(): Promise;
7 | + _connection(): Connection;
8 | _createPageInContext(contextId?: string): Promise;
9 | installExtension(path: string): Promise;
10 | uninstallExtension(id: string): Promise;
11 |
12 | --- a/lib/cjs/puppeteer/cdp/Browser.js
13 | +++ b/lib/cjs/puppeteer/cdp/Browser.js
14 | @@ -175,6 +175,10 @@
15 | async newPage() {
16 | return await this.#defaultContext.newPage();
17 | }
18 | + // rebrowser-patches: expose browser CDP session
19 | + _connection() {
20 | + return this.#connection;
21 | + }
22 | async _createPageInContext(contextId) {
23 | const { targetId } = await this.#connection.send('Target.createTarget', {
24 | url: 'about:blank',
25 |
26 | --- a/lib/cjs/puppeteer/cdp/ExecutionContext.d.ts
27 | +++ b/lib/cjs/puppeteer/cdp/ExecutionContext.d.ts
28 | @@ -22,6 +22,7 @@
29 | bindingcalled: Protocol.Runtime.BindingCalledEvent;
30 | }> implements Disposable {
31 | #private;
32 | + _frameId: any;
33 | constructor(client: CDPSession, contextPayload: Protocol.Runtime.ExecutionContextDescription, world: IsolatedWorld);
34 | get id(): number;
35 | get puppeteerUtil(): Promise>;
36 | @@ -116,6 +117,10 @@
37 | * {@link ElementHandle | element handle}.
38 | */
39 | evaluateHandle = EvaluateFunc>(pageFunction: Func | string, ...args: Params): Promise>>>;
40 | + clear(newId: any): void;
41 | + __re__getMainWorld({ client, frameId, isWorker }: any): Promise;
42 | + __re__getIsolatedWorld({ client, frameId, worldName }: any): Promise;
43 | + acquireContextId(tryCount?: number): Promise;
44 | [disposeSymbol](): void;
45 | }
46 | //# sourceMappingURL=ExecutionContext.d.ts.map
47 | \ No newline at end of file
48 |
49 | --- a/lib/cjs/puppeteer/cdp/ExecutionContext.js
50 | +++ b/lib/cjs/puppeteer/cdp/ExecutionContext.js
51 | @@ -86,6 +86,7 @@
52 | #client;
53 | #world;
54 | #id;
55 | + _frameId;
56 | #name;
57 | #disposables = new disposable_js_1.DisposableStack();
58 | constructor(client, contextPayload, world) {
59 | @@ -96,16 +97,22 @@
60 | if (contextPayload.name) {
61 | this.#name = contextPayload.name;
62 | }
63 | + // rebrowser-patches: keep frameId to use later
64 | + if (contextPayload.auxData?.frameId) {
65 | + this._frameId = contextPayload.auxData?.frameId;
66 | + }
67 | const clientEmitter = this.#disposables.use(new EventEmitter_js_1.EventEmitter(this.#client));
68 | clientEmitter.on('Runtime.bindingCalled', this.#onBindingCalled.bind(this));
69 | - clientEmitter.on('Runtime.executionContextDestroyed', async (event) => {
70 | - if (event.executionContextId === this.#id) {
71 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
72 | + clientEmitter.on('Runtime.executionContextDestroyed', async (event) => {
73 | + if (event.executionContextId === this.#id) {
74 | + this[disposable_js_1.disposeSymbol]();
75 | + }
76 | + });
77 | + clientEmitter.on('Runtime.executionContextsCleared', async () => {
78 | this[disposable_js_1.disposeSymbol]();
79 | - }
80 | - });
81 | - clientEmitter.on('Runtime.executionContextsCleared', async () => {
82 | - this[disposable_js_1.disposeSymbol]();
83 | - });
84 | + });
85 | + }
86 | clientEmitter.on('Runtime.consoleAPICalled', this.#onConsoleAPI.bind(this));
87 | clientEmitter.on(CDPSession_js_1.CDPSessionEvent.Disconnected, () => {
88 | this[disposable_js_1.disposeSymbol]();
89 | @@ -328,7 +335,181 @@
90 | async evaluateHandle(pageFunction, ...args) {
91 | return await this.#evaluate(false, pageFunction, ...args);
92 | }
93 | + // rebrowser-patches: alternative to dispose
94 | + clear(newId) {
95 | + this.#id = newId;
96 | + this.#bindings = new Map();
97 | + this.#bindingsInstalled = false;
98 | + this.#puppeteerUtil = undefined;
99 | + }
100 | + async __re__getMainWorld({ client, frameId, isWorker = false }) {
101 | + let contextId;
102 | + // random name to make it harder to detect for any 3rd party script by watching window object and events
103 | + const randomName = [...Array(Math.floor(Math.random() * (10 + 1)) + 10)].map(() => Math.random().toString(36)[2]).join('');
104 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] binding name = ${randomName}`);
105 | + // add the binding
106 | + await client.send('Runtime.addBinding', {
107 | + name: randomName,
108 | + });
109 | + // listen for 'Runtime.bindingCalled' event
110 | + const bindingCalledHandler = ({ name, payload, executionContextId }) => {
111 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][bindingCalledHandler]', {
112 | + name,
113 | + payload,
114 | + executionContextId
115 | + });
116 | + if (contextId > 0) {
117 | + // already acquired the id
118 | + return;
119 | + }
120 | + if (name !== randomName) {
121 | + // ignore irrelevant bindings
122 | + return;
123 | + }
124 | + if (payload !== frameId) {
125 | + // ignore irrelevant frames
126 | + return;
127 | + }
128 | + contextId = executionContextId;
129 | + // remove this listener
130 | + client.off('Runtime.bindingCalled', bindingCalledHandler);
131 | + };
132 | + client.on('Runtime.bindingCalled', bindingCalledHandler);
133 | + if (isWorker) {
134 | + // workers don't support `Page.addScriptToEvaluateOnNewDocument` and `Page.createIsolatedWorld`, but there are no iframes inside of them, so it's safe to just use Runtime.evaluate
135 | + await client.send('Runtime.evaluate', {
136 | + expression: `this['${randomName}']('${frameId}')`,
137 | + });
138 | + }
139 | + else {
140 | + // we could call the binding right from `addScriptToEvaluateOnNewDocument`, but this way it will be called in all existing frames and it's hard to distinguish children from the parent
141 | + await client.send('Page.addScriptToEvaluateOnNewDocument', {
142 | + source: `document.addEventListener('${randomName}', (e) => self['${randomName}'](e.detail.frameId))`,
143 | + runImmediately: true,
144 | + });
145 | + // create new isolated world for this frame
146 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', {
147 | + frameId,
148 | + // use randomName for worldName to distinguish from normal utility world
149 | + worldName: randomName,
150 | + grantUniveralAccess: true,
151 | + });
152 | + // emit event in the specific frame from the isolated world
153 | + await client.send('Runtime.evaluate', {
154 | + expression: `document.dispatchEvent(new CustomEvent('${randomName}', { detail: { frameId: '${frameId}' } }))`,
155 | + contextId: createIsolatedWorldResult.executionContextId,
156 | + });
157 | + }
158 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] result:`, { contextId });
159 | + return contextId;
160 | + }
161 | + async __re__getIsolatedWorld({ client, frameId, worldName }) {
162 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', {
163 | + frameId,
164 | + worldName,
165 | + grantUniveralAccess: true,
166 | + });
167 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getIsolatedWorld] result:`, createIsolatedWorldResult);
168 | + return createIsolatedWorldResult.executionContextId;
169 | + }
170 | + // rebrowser-patches: get context id if it's missing
171 | + async acquireContextId(tryCount = 1) {
172 | + if (this.#id > 0) {
173 | + return;
174 | + }
175 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding';
176 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][acquireContextId] id = ${this.#id}, name = ${this.#name}, fixMode = ${fixMode}, tryCount = ${tryCount}`);
177 | + let contextId;
178 | + let tryAgain = true;
179 | + let errorMessage = 'N/A';
180 | + if (fixMode === 'addBinding') {
181 | + try {
182 | + if (this.#id === -2) {
183 | + // isolated world
184 | + contextId = await this.__re__getIsolatedWorld({
185 | + client: this.#client,
186 | + frameId: this._frameId,
187 | + worldName: this.#name,
188 | + });
189 | + }
190 | + else {
191 | + // main world
192 | + contextId = await this.__re__getMainWorld({
193 | + client: this.#client,
194 | + frameId: this._frameId,
195 | + isWorker: this.#id === -3,
196 | + });
197 | + }
198 | + }
199 | + catch (error) {
200 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.error('[rebrowser-patches][acquireContextId] error:', error);
201 | + errorMessage = error.message;
202 | + if (error instanceof Error) {
203 | + if (error.message.includes('No frame for given id found') ||
204 | + error.message.includes('Target closed') ||
205 | + error.message.includes('Session closed')) {
206 | + // target doesn't exist anymore, don't try again
207 | + tryAgain = false;
208 | + }
209 | + }
210 | + (0, util_js_1.debugError)(error);
211 | + }
212 | + }
213 | + else if (fixMode === 'alwaysIsolated') {
214 | + if (this.#id === -3) {
215 | + throw new Error('[rebrowser-patches] web workers are not supported in alwaysIsolated mode');
216 | + }
217 | + contextId = await this.__re__getIsolatedWorld({
218 | + client: this.#client,
219 | + frameId: this._frameId,
220 | + worldName: this.#name,
221 | + });
222 | + }
223 | + else if (fixMode === 'enableDisable') {
224 | + const executionContextCreatedHandler = ({ context }) => {
225 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][executionContextCreated] this.#id = ${this.#id}, name = ${this.#name}, contextId = ${contextId}, event.context.id = ${context.id}`);
226 | + if (contextId > 0) {
227 | + // already acquired the id
228 | + return;
229 | + }
230 | + if (this.#id === -1) {
231 | + // main world
232 | + if (context.auxData && context.auxData['isDefault']) {
233 | + contextId = context.id;
234 | + }
235 | + }
236 | + else if (this.#id === -2) {
237 | + // utility world
238 | + if (this.#name === context.name) {
239 | + contextId = context.id;
240 | + }
241 | + }
242 | + else if (this.#id === -3) {
243 | + // web worker
244 | + contextId = context.id;
245 | + }
246 | + };
247 | + this.#client.on('Runtime.executionContextCreated', executionContextCreatedHandler);
248 | + await this.#client.send('Runtime.enable');
249 | + await this.#client.send('Runtime.disable');
250 | + this.#client.off('Runtime.executionContextCreated', executionContextCreatedHandler);
251 | + }
252 | + if (!contextId) {
253 | + if (!tryAgain || tryCount >= 3) {
254 | + throw new Error(`[rebrowser-patches] acquireContextId failed (tryAgain = ${tryAgain}, tryCount = ${tryCount}), errorMessage: ${errorMessage}`);
255 | + }
256 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][acquireContextId] failed, try again (tryCount = ${tryCount})`);
257 | + return this.acquireContextId(tryCount + 1);
258 | + }
259 | + this.#id = contextId;
260 | + }
261 | async #evaluate(returnByValue, pageFunction, ...args) {
262 | + // rebrowser-patches: context id is missing, acquire it and try again
263 | + if (this.#id < 0) {
264 | + await this.acquireContextId();
265 | + // @ts-ignore
266 | + return this.#evaluate(returnByValue, pageFunction, ...args);
267 | + }
268 | const sourceUrlComment = (0, util_js_1.getSourceUrlComment)((0, util_js_1.getSourcePuppeteerURLIfAvailable)(pageFunction)?.toString() ??
269 | util_js_1.PuppeteerURL.INTERNAL_URL);
270 | if ((0, util_js_1.isString)(pageFunction)) {
271 |
272 | --- a/lib/cjs/puppeteer/cdp/FrameManager.js
273 | +++ b/lib/cjs/puppeteer/cdp/FrameManager.js
274 | @@ -154,6 +154,10 @@
275 | this.#onFrameStoppedLoading(event.frameId);
276 | });
277 | session.on('Runtime.executionContextCreated', async (event) => {
278 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
279 | + // rebrowser-patches: ignore default logic
280 | + return;
281 | + }
282 | await this.#frameTreeHandled?.valueOrThrow();
283 | this.#onExecutionContextCreated(event.context, session);
284 | });
285 | @@ -178,9 +182,16 @@
286 | this.#frameTreeHandled?.resolve();
287 | }),
288 | client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
289 | - client.send('Runtime.enable').then(() => {
290 | - return this.#createIsolatedWorld(client, util_js_1.UTILITY_WORLD_NAME);
291 | - }),
292 | + (() => {
293 | + // rebrowser-patches: skip Runtime.enable
294 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
295 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][FrameManager] initialize');
296 | + return this.#createIsolatedWorld(client, util_js_1.UTILITY_WORLD_NAME);
297 | + }
298 | + return client.send('Runtime.enable').then(() => {
299 | + return this.#createIsolatedWorld(client, util_js_1.UTILITY_WORLD_NAME);
300 | + });
301 | + })(),
302 | ...(frame
303 | ? Array.from(this.#scriptsToEvaluateOnNewDocument.values())
304 | : []).map(script => {
305 | @@ -190,6 +201,26 @@
306 | return frame?.addExposedFunctionBinding(binding);
307 | }),
308 | ]);
309 | + // rebrowser-patches: manually create main world context
310 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
311 | + this.frames()
312 | + .filter(frame => {
313 | + return frame.client === client;
314 | + }).map(frame => {
315 | + const world = frame.worlds[IsolatedWorlds_js_1.MAIN_WORLD];
316 | + const contextPayload = {
317 | + id: -1,
318 | + name: '',
319 | + auxData: {
320 | + frameId: frame._id,
321 | + }
322 | + };
323 | + const context = new ExecutionContext_js_1.ExecutionContext(frame.client,
324 | + // @ts-ignore
325 | + contextPayload, world);
326 | + world.setContext(context);
327 | + });
328 | + }
329 | }
330 | catch (error) {
331 | this.#frameTreeHandled?.resolve();
332 | @@ -358,6 +389,23 @@
333 | }
334 | this._frameTree.addFrame(frame);
335 | }
336 | + // rebrowser-patches: we cannot fully dispose contexts as they won't be recreated as we don't have Runtime events,
337 | + // instead, just mark it all empty
338 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
339 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches] onFrameNavigated, navigationType = ${navigationType}, id = ${framePayload.id}, url = ${framePayload.url}`);
340 | + for (const worldSymbol of [IsolatedWorlds_js_1.MAIN_WORLD, IsolatedWorlds_js_1.PUPPETEER_WORLD]) {
341 | + // @ts-ignore
342 | + if (frame?.worlds[worldSymbol].context) {
343 | + // @ts-ignore
344 | + const frameOrWorker = frame.worlds[worldSymbol].environment;
345 | + if ('clearDocumentHandle' in frameOrWorker) {
346 | + frameOrWorker.clearDocumentHandle();
347 | + }
348 | + // @ts-ignore
349 | + frame.worlds[worldSymbol].context?.clear(worldSymbol === IsolatedWorlds_js_1.MAIN_WORLD ? -1 : -2);
350 | + }
351 | + }
352 | + }
353 | frame = await this._frameTree.waitForFrame(frameId);
354 | frame._navigated(framePayload);
355 | this.emit(FrameManagerEvents_js_1.FrameManagerEvent.FrameNavigated, frame);
356 | @@ -385,6 +433,24 @@
357 | worldName: name,
358 | grantUniveralAccess: true,
359 | })
360 | + .then((createIsolatedWorldResult) => {
361 | + // rebrowser-patches: save created context id
362 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
363 | + return;
364 | + }
365 | + if (!createIsolatedWorldResult?.executionContextId) {
366 | + // probably "Target closed" error, just ignore it
367 | + return;
368 | + }
369 | + // @ts-ignore
370 | + this.#onExecutionContextCreated({
371 | + id: createIsolatedWorldResult.executionContextId,
372 | + name,
373 | + auxData: {
374 | + frameId: frame._id,
375 | + }
376 | + }, frame.client);
377 | + })
378 | .catch(util_js_1.debugError);
379 | }));
380 | this.#isolatedWorlds.add(key);
381 |
382 | --- a/lib/cjs/puppeteer/cdp/IsolatedWorld.d.ts
383 | +++ b/lib/cjs/puppeteer/cdp/IsolatedWorld.d.ts
384 | @@ -12,9 +12,9 @@
385 | import type { TimeoutSettings } from '../common/TimeoutSettings.js';
386 | import type { EvaluateFunc, HandleFor } from '../common/types.js';
387 | import { disposeSymbol } from '../util/disposable.js';
388 | -import type { ExecutionContext } from './ExecutionContext.js';
389 | +import { ExecutionContext } from './ExecutionContext.js';
390 | import type { CdpFrame } from './Frame.js';
391 | -import type { MAIN_WORLD, PUPPETEER_WORLD } from './IsolatedWorlds.js';
392 | +import { MAIN_WORLD, PUPPETEER_WORLD } from './IsolatedWorlds.js';
393 | import type { CdpWebWorker } from './WebWorker.js';
394 | /**
395 | * @internal
396 |
397 | --- a/lib/cjs/puppeteer/cdp/IsolatedWorld.js
398 | +++ b/lib/cjs/puppeteer/cdp/IsolatedWorld.js
399 | @@ -1,4 +1,5 @@
400 | "use strict";
401 | +//@ts-nocheck
402 | /**
403 | * @license
404 | * Copyright 2019 Google Inc.
405 | @@ -12,6 +13,8 @@
406 | const util_js_1 = require("../common/util.js");
407 | const disposable_js_1 = require("../util/disposable.js");
408 | const ElementHandle_js_1 = require("./ElementHandle.js");
409 | +const ExecutionContext_js_1 = require("./ExecutionContext.js");
410 | +const IsolatedWorlds_js_1 = require("./IsolatedWorlds.js");
411 | const JSHandle_js_1 = require("./JSHandle.js");
412 | /**
413 | * @internal
414 | @@ -70,6 +73,21 @@
415 | * Waits for the next context to be set on the isolated world.
416 | */
417 | async #waitForExecutionContext() {
418 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding';
419 | + if (fixMode === 'addBinding') {
420 | + const isMainWorld = this.#frameOrWorker.worlds[IsolatedWorlds_js_1.MAIN_WORLD] === this;
421 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][waitForExecutionContext] frameId = ${this.#frameOrWorker._id}, isMainWorld = ${isMainWorld}`);
422 | + const contextPayload = {
423 | + id: isMainWorld ? -1 : -2,
424 | + name: isMainWorld ? '' : util_js_1.UTILITY_WORLD_NAME,
425 | + auxData: {
426 | + frameId: this.#frameOrWorker._id,
427 | + }
428 | + };
429 | + const context = new ExecutionContext_js_1.ExecutionContext(this.client, contextPayload, this);
430 | + this.setContext(context);
431 | + return context;
432 | + }
433 | const error = new Error('Execution context was destroyed');
434 | const result = await (0, rxjs_js_1.firstValueFrom)((0, util_js_1.fromEmitterEvent)(this.#emitter, 'context').pipe((0, rxjs_js_1.raceWith)((0, util_js_1.fromEmitterEvent)(this.#emitter, 'disposed').pipe((0, rxjs_js_1.map)(() => {
435 | // The message has to match the CDP message expected by the WaitTask class.
436 | @@ -107,6 +125,8 @@
437 | if (!context) {
438 | context = await this.#waitForExecutionContext();
439 | }
440 | + // rebrowser-patches: make sure id is acquired
441 | + await context.acquireContextId();
442 | const { object } = await this.client.send('DOM.resolveNode', {
443 | backendNodeId: backendNodeId,
444 | executionContextId: context.id,
445 |
446 | --- a/lib/cjs/puppeteer/cdp/WebWorker.js
447 | +++ b/lib/cjs/puppeteer/cdp/WebWorker.js
448 | @@ -24,6 +24,10 @@
449 | this.#targetType = targetType;
450 | this.#world = new IsolatedWorld_js_1.IsolatedWorld(this, new TimeoutSettings_js_1.TimeoutSettings());
451 | this.#client.once('Runtime.executionContextCreated', async (event) => {
452 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
453 | + // rebrowser-patches: ignore default logic
454 | + return;
455 | + }
456 | this.#world.setContext(new ExecutionContext_js_1.ExecutionContext(client, event.context, this.#world));
457 | });
458 | this.#world.emitter.on('consoleapicalled', async (event) => {
459 | @@ -42,7 +46,22 @@
460 | });
461 | // This might fail if the target is closed before we receive all execution contexts.
462 | networkManager?.addClient(this.#client).catch(util_js_1.debugError);
463 | - this.#client.send('Runtime.enable').catch(util_js_1.debugError);
464 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
465 | + // @ts-ignore
466 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][WebWorker] initialize', targetType, targetId, client._target(), client._target()._getTargetInfo());
467 | + // rebrowser-patches: manually create context
468 | + const contextPayload = {
469 | + id: -3,
470 | + auxData: {
471 | + frameId: targetId,
472 | + }
473 | + };
474 | + // @ts-ignore
475 | + this.#world.setContext(new ExecutionContext_js_1.ExecutionContext(client, contextPayload, this.#world));
476 | + }
477 | + else {
478 | + this.#client.send('Runtime.enable').catch(util_js_1.debugError);
479 | + }
480 | }
481 | mainRealm() {
482 | return this.#world;
483 |
484 | --- a/lib/cjs/puppeteer/common/util.js
485 | +++ b/lib/cjs/puppeteer/common/util.js
486 | @@ -252,7 +252,10 @@
487 | /**
488 | * @internal
489 | */
490 | -exports.UTILITY_WORLD_NAME = '__puppeteer_utility_world__' + version_js_1.packageVersion;
491 | +exports.UTILITY_WORLD_NAME =
492 | +// rebrowser-patches: change utility world name
493 | +process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] !== '0' ? (process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] || 'util') :
494 | + '__puppeteer_utility_world__' + version_js_1.packageVersion;
495 | /**
496 | * @internal
497 | */
498 | @@ -261,6 +264,10 @@
499 | * @internal
500 | */
501 | function getSourceUrlComment(url) {
502 | + // rebrowser-patches: change sourceUrl to generic script name
503 | + if (process.env['REBROWSER_PATCHES_SOURCE_URL'] !== '0') {
504 | + url = process.env['REBROWSER_PATCHES_SOURCE_URL'] || 'app.js';
505 | + }
506 | return `//# sourceURL=${url}`;
507 | }
508 | /**
509 |
510 | --- a/lib/es5-iife/puppeteer-core-browser.js
511 | +++ b/lib/es5-iife/puppeteer-core-browser.js
512 | @@ -3561,7 +3561,9 @@
513 | /**
514 | * @internal
515 | */
516 | - const UTILITY_WORLD_NAME = '__puppeteer_utility_world__' + packageVersion;
517 | + const UTILITY_WORLD_NAME =
518 | + // rebrowser-patches: change utility world name
519 | + process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] !== '0' ? process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] || 'util' : '__puppeteer_utility_world__' + packageVersion;
520 | /**
521 | * @internal
522 | */
523 | @@ -3570,6 +3572,10 @@
524 | * @internal
525 | */
526 | function getSourceUrlComment(url) {
527 | + // rebrowser-patches: change sourceUrl to generic script name
528 | + if (process.env['REBROWSER_PATCHES_SOURCE_URL'] !== '0') {
529 | + url = process.env['REBROWSER_PATCHES_SOURCE_URL'] || 'app.js';
530 | + }
531 | return `//# sourceURL=${url}`;
532 | }
533 | /**
534 | @@ -15625,6 +15631,7 @@
535 | _classPrivateFieldInitSpec(this, _client8, void 0);
536 | _classPrivateFieldInitSpec(this, _world3, void 0);
537 | _classPrivateFieldInitSpec(this, _id5, void 0);
538 | + _defineProperty(this, "_frameId", void 0);
539 | _classPrivateFieldInitSpec(this, _name3, void 0);
540 | _classPrivateFieldInitSpec(this, _disposables, new DisposableStack());
541 | // Contains mapping from functions that should be bound to Puppeteer functions.
542 | @@ -15640,16 +15647,22 @@
543 | if (contextPayload.name) {
544 | _classPrivateFieldSet(_name3, this, contextPayload.name);
545 | }
546 | + // rebrowser-patches: keep frameId to use later
547 | + if (contextPayload.auxData?.frameId) {
548 | + this._frameId = contextPayload.auxData?.frameId;
549 | + }
550 | const clientEmitter = _classPrivateFieldGet(_disposables, this).use(new EventEmitter(_classPrivateFieldGet(_client8, this)));
551 | clientEmitter.on('Runtime.bindingCalled', _assertClassBrand(_ExecutionContext_brand, this, _onBindingCalled).bind(this));
552 | - clientEmitter.on('Runtime.executionContextDestroyed', async event => {
553 | - if (event.executionContextId === _classPrivateFieldGet(_id5, this)) {
554 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
555 | + clientEmitter.on('Runtime.executionContextDestroyed', async event => {
556 | + if (event.executionContextId === _classPrivateFieldGet(_id5, this)) {
557 | + this[disposeSymbol]();
558 | + }
559 | + });
560 | + clientEmitter.on('Runtime.executionContextsCleared', async () => {
561 | this[disposeSymbol]();
562 | - }
563 | - });
564 | - clientEmitter.on('Runtime.executionContextsCleared', async () => {
565 | - this[disposeSymbol]();
566 | - });
567 | + });
568 | + }
569 | clientEmitter.on('Runtime.consoleAPICalled', _assertClassBrand(_ExecutionContext_brand, this, _onConsoleAPI).bind(this));
570 | clientEmitter.on(exports.CDPSessionEvent.Disconnected, () => {
571 | this[disposeSymbol]();
572 | @@ -15771,6 +15784,181 @@
573 | async evaluateHandle(pageFunction, ...args) {
574 | return await _assertClassBrand(_ExecutionContext_brand, this, _evaluate).call(this, false, pageFunction, ...args);
575 | }
576 | + // rebrowser-patches: alternative to dispose
577 | + clear(newId) {
578 | + _classPrivateFieldSet(_id5, this, newId);
579 | + _classPrivateFieldSet(_bindings, this, new Map());
580 | + _classPrivateFieldSet(_bindingsInstalled, this, false);
581 | + _classPrivateFieldSet(_puppeteerUtil, this, undefined);
582 | + }
583 | + async __re__getMainWorld({
584 | + client,
585 | + frameId,
586 | + isWorker = false
587 | + }) {
588 | + let contextId;
589 | + // random name to make it harder to detect for any 3rd party script by watching window object and events
590 | + const randomName = [...Array(Math.floor(Math.random() * (10 + 1)) + 10)].map(() => Math.random().toString(36)[2]).join('');
591 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] binding name = ${randomName}`);
592 | + // add the binding
593 | + await client.send('Runtime.addBinding', {
594 | + name: randomName
595 | + });
596 | + // listen for 'Runtime.bindingCalled' event
597 | + const bindingCalledHandler = ({
598 | + name,
599 | + payload,
600 | + executionContextId
601 | + }) => {
602 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][bindingCalledHandler]', {
603 | + name,
604 | + payload,
605 | + executionContextId
606 | + });
607 | + if (contextId > 0) {
608 | + // already acquired the id
609 | + return;
610 | + }
611 | + if (name !== randomName) {
612 | + // ignore irrelevant bindings
613 | + return;
614 | + }
615 | + if (payload !== frameId) {
616 | + // ignore irrelevant frames
617 | + return;
618 | + }
619 | + contextId = executionContextId;
620 | + // remove this listener
621 | + client.off('Runtime.bindingCalled', bindingCalledHandler);
622 | + };
623 | + client.on('Runtime.bindingCalled', bindingCalledHandler);
624 | + if (isWorker) {
625 | + // workers don't support `Page.addScriptToEvaluateOnNewDocument` and `Page.createIsolatedWorld`, but there are no iframes inside of them, so it's safe to just use Runtime.evaluate
626 | + await client.send('Runtime.evaluate', {
627 | + expression: `this['${randomName}']('${frameId}')`
628 | + });
629 | + } else {
630 | + // we could call the binding right from `addScriptToEvaluateOnNewDocument`, but this way it will be called in all existing frames and it's hard to distinguish children from the parent
631 | + await client.send('Page.addScriptToEvaluateOnNewDocument', {
632 | + source: `document.addEventListener('${randomName}', (e) => self['${randomName}'](e.detail.frameId))`,
633 | + runImmediately: true
634 | + });
635 | + // create new isolated world for this frame
636 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', {
637 | + frameId,
638 | + // use randomName for worldName to distinguish from normal utility world
639 | + worldName: randomName,
640 | + grantUniveralAccess: true
641 | + });
642 | + // emit event in the specific frame from the isolated world
643 | + await client.send('Runtime.evaluate', {
644 | + expression: `document.dispatchEvent(new CustomEvent('${randomName}', { detail: { frameId: '${frameId}' } }))`,
645 | + contextId: createIsolatedWorldResult.executionContextId
646 | + });
647 | + }
648 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] result:`, {
649 | + contextId
650 | + });
651 | + return contextId;
652 | + }
653 | + async __re__getIsolatedWorld({
654 | + client,
655 | + frameId,
656 | + worldName
657 | + }) {
658 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', {
659 | + frameId,
660 | + worldName,
661 | + grantUniveralAccess: true
662 | + });
663 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getIsolatedWorld] result:`, createIsolatedWorldResult);
664 | + return createIsolatedWorldResult.executionContextId;
665 | + }
666 | + // rebrowser-patches: get context id if it's missing
667 | + async acquireContextId(tryCount = 1) {
668 | + if (_classPrivateFieldGet(_id5, this) > 0) {
669 | + return;
670 | + }
671 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding';
672 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][acquireContextId] id = ${_classPrivateFieldGet(_id5, this)}, name = ${_classPrivateFieldGet(_name3, this)}, fixMode = ${fixMode}, tryCount = ${tryCount}`);
673 | + let contextId;
674 | + let tryAgain = true;
675 | + let errorMessage = 'N/A';
676 | + if (fixMode === 'addBinding') {
677 | + try {
678 | + if (_classPrivateFieldGet(_id5, this) === -2) {
679 | + // isolated world
680 | + contextId = await this.__re__getIsolatedWorld({
681 | + client: _classPrivateFieldGet(_client8, this),
682 | + frameId: this._frameId,
683 | + worldName: _classPrivateFieldGet(_name3, this)
684 | + });
685 | + } else {
686 | + // main world
687 | + contextId = await this.__re__getMainWorld({
688 | + client: _classPrivateFieldGet(_client8, this),
689 | + frameId: this._frameId,
690 | + isWorker: _classPrivateFieldGet(_id5, this) === -3
691 | + });
692 | + }
693 | + } catch (error) {
694 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.error('[rebrowser-patches][acquireContextId] error:', error);
695 | + errorMessage = error.message;
696 | + if (error instanceof Error) {
697 | + if (error.message.includes('No frame for given id found') || error.message.includes('Target closed') || error.message.includes('Session closed')) {
698 | + // target doesn't exist anymore, don't try again
699 | + tryAgain = false;
700 | + }
701 | + }
702 | + debugError(error);
703 | + }
704 | + } else if (fixMode === 'alwaysIsolated') {
705 | + if (_classPrivateFieldGet(_id5, this) === -3) {
706 | + throw new Error('[rebrowser-patches] web workers are not supported in alwaysIsolated mode');
707 | + }
708 | + contextId = await this.__re__getIsolatedWorld({
709 | + client: _classPrivateFieldGet(_client8, this),
710 | + frameId: this._frameId,
711 | + worldName: _classPrivateFieldGet(_name3, this)
712 | + });
713 | + } else if (fixMode === 'enableDisable') {
714 | + const executionContextCreatedHandler = ({
715 | + context
716 | + }) => {
717 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][executionContextCreated] this.#id = ${_classPrivateFieldGet(_id5, this)}, name = ${_classPrivateFieldGet(_name3, this)}, contextId = ${contextId}, event.context.id = ${context.id}`);
718 | + if (contextId > 0) {
719 | + // already acquired the id
720 | + return;
721 | + }
722 | + if (_classPrivateFieldGet(_id5, this) === -1) {
723 | + // main world
724 | + if (context.auxData && context.auxData['isDefault']) {
725 | + contextId = context.id;
726 | + }
727 | + } else if (_classPrivateFieldGet(_id5, this) === -2) {
728 | + // utility world
729 | + if (_classPrivateFieldGet(_name3, this) === context.name) {
730 | + contextId = context.id;
731 | + }
732 | + } else if (_classPrivateFieldGet(_id5, this) === -3) {
733 | + // web worker
734 | + contextId = context.id;
735 | + }
736 | + };
737 | + _classPrivateFieldGet(_client8, this).on('Runtime.executionContextCreated', executionContextCreatedHandler);
738 | + await _classPrivateFieldGet(_client8, this).send('Runtime.enable');
739 | + await _classPrivateFieldGet(_client8, this).send('Runtime.disable');
740 | + _classPrivateFieldGet(_client8, this).off('Runtime.executionContextCreated', executionContextCreatedHandler);
741 | + }
742 | + if (!contextId) {
743 | + if (!tryAgain || tryCount >= 3) {
744 | + throw new Error(`[rebrowser-patches] acquireContextId failed (tryAgain = ${tryAgain}, tryCount = ${tryCount}), errorMessage: ${errorMessage}`);
745 | + }
746 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][acquireContextId] failed, try again (tryCount = ${tryCount})`);
747 | + return this.acquireContextId(tryCount + 1);
748 | + }
749 | + _classPrivateFieldSet(_id5, this, contextId);
750 | + }
751 | [disposeSymbol]() {
752 | _classPrivateFieldGet(_disposables, this).dispose();
753 | this.emit('disposed', undefined);
754 | @@ -15870,6 +16058,12 @@
755 | }
756 | }
757 | async function _evaluate(returnByValue, pageFunction, ...args) {
758 | + // rebrowser-patches: context id is missing, acquire it and try again
759 | + if (_classPrivateFieldGet(_id5, this) < 0) {
760 | + await this.acquireContextId();
761 | + // @ts-ignore
762 | + return _assertClassBrand(_ExecutionContext_brand, this, _evaluate).call(this, returnByValue, pageFunction, ...args);
763 | + }
764 | const sourceUrlComment = getSourceUrlComment(getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ?? PuppeteerURL.INTERNAL_URL);
765 | if (isString(pageFunction)) {
766 | const contextId = _classPrivateFieldGet(_id5, this);
767 | @@ -16036,6 +16230,27 @@
768 |
769 | /**
770 | * @license
771 | + * Copyright 2022 Google Inc.
772 | + * SPDX-License-Identifier: Apache-2.0
773 | + */
774 | + /**
775 | + * A unique key for {@link IsolatedWorldChart} to denote the default world.
776 | + * Execution contexts are automatically created in the default world.
777 | + *
778 | + * @internal
779 | + */
780 | + const MAIN_WORLD = Symbol('mainWorld');
781 | + /**
782 | + * A unique key for {@link IsolatedWorldChart} to denote the puppeteer world.
783 | + * This world contains all puppeteer-internal bindings/code.
784 | + *
785 | + * @internal
786 | + */
787 | + const PUPPETEER_WORLD = Symbol('puppeteerWorld');
788 | +
789 | + //@ts-nocheck
790 | + /**
791 | + * @license
792 | * Copyright 2019 Google Inc.
793 | * SPDX-License-Identifier: Apache-2.0
794 | */
795 | @@ -16109,6 +16324,8 @@
796 | if (!context) {
797 | context = await _assertClassBrand(_IsolatedWorld_brand, this, _waitForExecutionContext).call(this);
798 | }
799 | + // rebrowser-patches: make sure id is acquired
800 | + await context.acquireContextId();
801 | const {
802 | object
803 | } = await this.client.send('DOM.resolveNode', {
804 | @@ -16164,15 +16381,9 @@
805 |
806 | /**
807 | * @license
808 | - * Copyright 2022 Google Inc.
809 | + * Copyright 2019 Google Inc.
810 | * SPDX-License-Identifier: Apache-2.0
811 | */
812 | - /**
813 | - * A unique key for {@link IsolatedWorldChart} to denote the default world.
814 | - * Execution contexts are automatically created in the default world.
815 | - *
816 | - * @internal
817 | - */
818 | function _onContextDisposed() {
819 | _classPrivateFieldSet(_context, this, undefined);
820 | if ('clearDocumentHandle' in _classPrivateFieldGet(_frameOrWorker, this)) {
821 | @@ -16195,6 +16406,21 @@
822 | * Waits for the next context to be set on the isolated world.
823 | */
824 | async function _waitForExecutionContext() {
825 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding';
826 | + if (fixMode === 'addBinding') {
827 | + const isMainWorld = _classPrivateFieldGet(_frameOrWorker, this).worlds[MAIN_WORLD] === this;
828 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][waitForExecutionContext] frameId = ${_classPrivateFieldGet(_frameOrWorker, this)._id}, isMainWorld = ${isMainWorld}`);
829 | + const contextPayload = {
830 | + id: isMainWorld ? -1 : -2,
831 | + name: isMainWorld ? '' : UTILITY_WORLD_NAME,
832 | + auxData: {
833 | + frameId: _classPrivateFieldGet(_frameOrWorker, this)._id
834 | + }
835 | + };
836 | + const context = new ExecutionContext(this.client, contextPayload, this);
837 | + this.setContext(context);
838 | + return context;
839 | + }
840 | const error = new Error('Execution context was destroyed');
841 | const result = await firstValueFrom(fromEmitterEvent(_classPrivateFieldGet(_emitter2, this), 'context').pipe(raceWith(fromEmitterEvent(_classPrivateFieldGet(_emitter2, this), 'disposed').pipe(map(() => {
842 | // The message has to match the CDP message expected by the WaitTask class.
843 | @@ -16202,20 +16428,6 @@
844 | })), timeout(this.timeoutSettings.timeout()))));
845 | return result;
846 | }
847 | - const MAIN_WORLD = Symbol('mainWorld');
848 | - /**
849 | - * A unique key for {@link IsolatedWorldChart} to denote the puppeteer world.
850 | - * This world contains all puppeteer-internal bindings/code.
851 | - *
852 | - * @internal
853 | - */
854 | - const PUPPETEER_WORLD = Symbol('puppeteerWorld');
855 | -
856 | - /**
857 | - * @license
858 | - * Copyright 2019 Google Inc.
859 | - * SPDX-License-Identifier: Apache-2.0
860 | - */
861 | const puppeteerToProtocolLifecycle = new Map([['load', 'load'], ['domcontentloaded', 'DOMContentLoaded'], ['networkidle0', 'networkIdle'], ['networkidle2', 'networkAlmostIdle']]);
862 | /**
863 | * @internal
864 | @@ -18012,6 +18224,10 @@
865 | _assertClassBrand(_FrameManager_brand, this, _onFrameStoppedLoading).call(this, event.frameId);
866 | });
867 | session.on('Runtime.executionContextCreated', async event => {
868 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
869 | + // rebrowser-patches: ignore default logic
870 | + return;
871 | + }
872 | await _classPrivateFieldGet(_frameTreeHandled, this)?.valueOrThrow();
873 | _assertClassBrand(_FrameManager_brand, this, _onExecutionContextCreated).call(this, event.context, session);
874 | });
875 | @@ -18035,13 +18251,39 @@
876 | _classPrivateFieldGet(_frameTreeHandled, this)?.resolve();
877 | }), client.send('Page.setLifecycleEventsEnabled', {
878 | enabled: true
879 | - }), client.send('Runtime.enable').then(() => {
880 | - return _assertClassBrand(_FrameManager_brand, this, _createIsolatedWorld).call(this, client, UTILITY_WORLD_NAME);
881 | - }), ...(frame ? Array.from(_classPrivateFieldGet(_scriptsToEvaluateOnNewDocument, this).values()) : []).map(script => {
882 | + }), (() => {
883 | + // rebrowser-patches: skip Runtime.enable
884 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
885 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][FrameManager] initialize');
886 | + return _assertClassBrand(_FrameManager_brand, this, _createIsolatedWorld).call(this, client, UTILITY_WORLD_NAME);
887 | + }
888 | + return client.send('Runtime.enable').then(() => {
889 | + return _assertClassBrand(_FrameManager_brand, this, _createIsolatedWorld).call(this, client, UTILITY_WORLD_NAME);
890 | + });
891 | + })(), ...(frame ? Array.from(_classPrivateFieldGet(_scriptsToEvaluateOnNewDocument, this).values()) : []).map(script => {
892 | return frame?.addPreloadScript(script);
893 | }), ...(frame ? Array.from(_classPrivateFieldGet(_bindings2, this).values()) : []).map(binding => {
894 | return frame?.addExposedFunctionBinding(binding);
895 | })]);
896 | + // rebrowser-patches: manually create main world context
897 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
898 | + this.frames().filter(frame => {
899 | + return frame.client === client;
900 | + }).map(frame => {
901 | + const world = frame.worlds[MAIN_WORLD];
902 | + const contextPayload = {
903 | + id: -1,
904 | + name: '',
905 | + auxData: {
906 | + frameId: frame._id
907 | + }
908 | + };
909 | + const context = new ExecutionContext(frame.client,
910 | + // @ts-ignore
911 | + contextPayload, world);
912 | + world.setContext(context);
913 | + });
914 | + }
915 | } catch (error) {
916 | _classPrivateFieldGet(_frameTreeHandled, this)?.resolve();
917 | // The target might have been closed before the initialization finished.
918 | @@ -18243,6 +18485,23 @@
919 | }
920 | this._frameTree.addFrame(frame);
921 | }
922 | + // rebrowser-patches: we cannot fully dispose contexts as they won't be recreated as we don't have Runtime events,
923 | + // instead, just mark it all empty
924 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
925 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches] onFrameNavigated, navigationType = ${navigationType}, id = ${framePayload.id}, url = ${framePayload.url}`);
926 | + for (const worldSymbol of [MAIN_WORLD, PUPPETEER_WORLD]) {
927 | + // @ts-ignore
928 | + if (frame?.worlds[worldSymbol].context) {
929 | + // @ts-ignore
930 | + const frameOrWorker = frame.worlds[worldSymbol].environment;
931 | + if ('clearDocumentHandle' in frameOrWorker) {
932 | + frameOrWorker.clearDocumentHandle();
933 | + }
934 | + // @ts-ignore
935 | + frame.worlds[worldSymbol].context?.clear(worldSymbol === MAIN_WORLD ? -1 : -2);
936 | + }
937 | + }
938 | + }
939 | frame = await this._frameTree.waitForFrame(frameId);
940 | frame._navigated(framePayload);
941 | this.emit(exports.FrameManagerEvent.FrameNavigated, frame);
942 | @@ -18266,6 +18525,23 @@
943 | frameId: frame._id,
944 | worldName: name,
945 | grantUniveralAccess: true
946 | + }).then(createIsolatedWorldResult => {
947 | + // rebrowser-patches: save created context id
948 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
949 | + return;
950 | + }
951 | + if (!createIsolatedWorldResult?.executionContextId) {
952 | + // probably "Target closed" error, just ignore it
953 | + return;
954 | + }
955 | + // @ts-ignore
956 | + _assertClassBrand(_FrameManager_brand, this, _onExecutionContextCreated).call(this, {
957 | + id: createIsolatedWorldResult.executionContextId,
958 | + name,
959 | + auxData: {
960 | + frameId: frame._id
961 | + }
962 | + }, frame.client);
963 | }).catch(debugError);
964 | }));
965 | _classPrivateFieldGet(_isolatedWorlds, this).add(key);
966 | @@ -20394,6 +20670,10 @@
967 | _classPrivateFieldSet(_targetType2, this, targetType);
968 | _classPrivateFieldSet(_world4, this, new IsolatedWorld(this, new TimeoutSettings()));
969 | _classPrivateFieldGet(_client17, this).once('Runtime.executionContextCreated', async event => {
970 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
971 | + // rebrowser-patches: ignore default logic
972 | + return;
973 | + }
974 | _classPrivateFieldGet(_world4, this).setContext(new ExecutionContext(client, event.context, _classPrivateFieldGet(_world4, this)));
975 | });
976 | _classPrivateFieldGet(_world4, this).emitter.on('consoleapicalled', async event => {
977 | @@ -20411,7 +20691,21 @@
978 | });
979 | // This might fail if the target is closed before we receive all execution contexts.
980 | networkManager?.addClient(_classPrivateFieldGet(_client17, this)).catch(debugError);
981 | - _classPrivateFieldGet(_client17, this).send('Runtime.enable').catch(debugError);
982 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
983 | + // @ts-ignore
984 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][WebWorker] initialize', targetType, targetId, client._target(), client._target()._getTargetInfo());
985 | + // rebrowser-patches: manually create context
986 | + const contextPayload = {
987 | + id: -3,
988 | + auxData: {
989 | + frameId: targetId
990 | + }
991 | + };
992 | + // @ts-ignore
993 | + _classPrivateFieldGet(_world4, this).setContext(new ExecutionContext(client, contextPayload, _classPrivateFieldGet(_world4, this)));
994 | + } else {
995 | + _classPrivateFieldGet(_client17, this).send('Runtime.enable').catch(debugError);
996 | + }
997 | }
998 | mainRealm() {
999 | return _classPrivateFieldGet(_world4, this);
1000 | @@ -22313,6 +22607,10 @@
1001 | async newPage() {
1002 | return await _classPrivateFieldGet(_defaultContext, this).newPage();
1003 | }
1004 | + // rebrowser-patches: expose browser CDP session
1005 | + _connection() {
1006 | + return _classPrivateFieldGet(_connection4, this);
1007 | + }
1008 | async _createPageInContext(contextId) {
1009 | const {
1010 | targetId
1011 |
1012 | --- a/lib/esm/puppeteer/cdp/Browser.d.ts
1013 | +++ b/lib/esm/puppeteer/cdp/Browser.d.ts
1014 | @@ -32,6 +32,7 @@
1015 | _disposeContext(contextId?: string): Promise;
1016 | wsEndpoint(): string;
1017 | newPage(): Promise;
1018 | + _connection(): Connection;
1019 | _createPageInContext(contextId?: string): Promise;
1020 | installExtension(path: string): Promise;
1021 | uninstallExtension(id: string): Promise;
1022 |
1023 | --- a/lib/esm/puppeteer/cdp/Browser.js
1024 | +++ b/lib/esm/puppeteer/cdp/Browser.js
1025 | @@ -172,6 +172,10 @@
1026 | async newPage() {
1027 | return await this.#defaultContext.newPage();
1028 | }
1029 | + // rebrowser-patches: expose browser CDP session
1030 | + _connection() {
1031 | + return this.#connection;
1032 | + }
1033 | async _createPageInContext(contextId) {
1034 | const { targetId } = await this.#connection.send('Target.createTarget', {
1035 | url: 'about:blank',
1036 |
1037 | --- a/lib/esm/puppeteer/cdp/ExecutionContext.d.ts
1038 | +++ b/lib/esm/puppeteer/cdp/ExecutionContext.d.ts
1039 | @@ -22,6 +22,7 @@
1040 | bindingcalled: Protocol.Runtime.BindingCalledEvent;
1041 | }> implements Disposable {
1042 | #private;
1043 | + _frameId: any;
1044 | constructor(client: CDPSession, contextPayload: Protocol.Runtime.ExecutionContextDescription, world: IsolatedWorld);
1045 | get id(): number;
1046 | get puppeteerUtil(): Promise>;
1047 | @@ -116,6 +117,10 @@
1048 | * {@link ElementHandle | element handle}.
1049 | */
1050 | evaluateHandle = EvaluateFunc>(pageFunction: Func | string, ...args: Params): Promise>>>;
1051 | + clear(newId: any): void;
1052 | + __re__getMainWorld({ client, frameId, isWorker }: any): Promise;
1053 | + __re__getIsolatedWorld({ client, frameId, worldName }: any): Promise;
1054 | + acquireContextId(tryCount?: number): Promise;
1055 | [disposeSymbol](): void;
1056 | }
1057 | //# sourceMappingURL=ExecutionContext.d.ts.map
1058 | \ No newline at end of file
1059 |
1060 | --- a/lib/esm/puppeteer/cdp/ExecutionContext.js
1061 | +++ b/lib/esm/puppeteer/cdp/ExecutionContext.js
1062 | @@ -83,6 +83,7 @@
1063 | #client;
1064 | #world;
1065 | #id;
1066 | + _frameId;
1067 | #name;
1068 | #disposables = new DisposableStack();
1069 | constructor(client, contextPayload, world) {
1070 | @@ -93,16 +94,22 @@
1071 | if (contextPayload.name) {
1072 | this.#name = contextPayload.name;
1073 | }
1074 | + // rebrowser-patches: keep frameId to use later
1075 | + if (contextPayload.auxData?.frameId) {
1076 | + this._frameId = contextPayload.auxData?.frameId;
1077 | + }
1078 | const clientEmitter = this.#disposables.use(new EventEmitter(this.#client));
1079 | clientEmitter.on('Runtime.bindingCalled', this.#onBindingCalled.bind(this));
1080 | - clientEmitter.on('Runtime.executionContextDestroyed', async (event) => {
1081 | - if (event.executionContextId === this.#id) {
1082 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
1083 | + clientEmitter.on('Runtime.executionContextDestroyed', async (event) => {
1084 | + if (event.executionContextId === this.#id) {
1085 | + this[disposeSymbol]();
1086 | + }
1087 | + });
1088 | + clientEmitter.on('Runtime.executionContextsCleared', async () => {
1089 | this[disposeSymbol]();
1090 | - }
1091 | - });
1092 | - clientEmitter.on('Runtime.executionContextsCleared', async () => {
1093 | - this[disposeSymbol]();
1094 | - });
1095 | + });
1096 | + }
1097 | clientEmitter.on('Runtime.consoleAPICalled', this.#onConsoleAPI.bind(this));
1098 | clientEmitter.on(CDPSessionEvent.Disconnected, () => {
1099 | this[disposeSymbol]();
1100 | @@ -325,7 +332,181 @@
1101 | async evaluateHandle(pageFunction, ...args) {
1102 | return await this.#evaluate(false, pageFunction, ...args);
1103 | }
1104 | + // rebrowser-patches: alternative to dispose
1105 | + clear(newId) {
1106 | + this.#id = newId;
1107 | + this.#bindings = new Map();
1108 | + this.#bindingsInstalled = false;
1109 | + this.#puppeteerUtil = undefined;
1110 | + }
1111 | + async __re__getMainWorld({ client, frameId, isWorker = false }) {
1112 | + let contextId;
1113 | + // random name to make it harder to detect for any 3rd party script by watching window object and events
1114 | + const randomName = [...Array(Math.floor(Math.random() * (10 + 1)) + 10)].map(() => Math.random().toString(36)[2]).join('');
1115 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] binding name = ${randomName}`);
1116 | + // add the binding
1117 | + await client.send('Runtime.addBinding', {
1118 | + name: randomName,
1119 | + });
1120 | + // listen for 'Runtime.bindingCalled' event
1121 | + const bindingCalledHandler = ({ name, payload, executionContextId }) => {
1122 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][bindingCalledHandler]', {
1123 | + name,
1124 | + payload,
1125 | + executionContextId
1126 | + });
1127 | + if (contextId > 0) {
1128 | + // already acquired the id
1129 | + return;
1130 | + }
1131 | + if (name !== randomName) {
1132 | + // ignore irrelevant bindings
1133 | + return;
1134 | + }
1135 | + if (payload !== frameId) {
1136 | + // ignore irrelevant frames
1137 | + return;
1138 | + }
1139 | + contextId = executionContextId;
1140 | + // remove this listener
1141 | + client.off('Runtime.bindingCalled', bindingCalledHandler);
1142 | + };
1143 | + client.on('Runtime.bindingCalled', bindingCalledHandler);
1144 | + if (isWorker) {
1145 | + // workers don't support `Page.addScriptToEvaluateOnNewDocument` and `Page.createIsolatedWorld`, but there are no iframes inside of them, so it's safe to just use Runtime.evaluate
1146 | + await client.send('Runtime.evaluate', {
1147 | + expression: `this['${randomName}']('${frameId}')`,
1148 | + });
1149 | + }
1150 | + else {
1151 | + // we could call the binding right from `addScriptToEvaluateOnNewDocument`, but this way it will be called in all existing frames and it's hard to distinguish children from the parent
1152 | + await client.send('Page.addScriptToEvaluateOnNewDocument', {
1153 | + source: `document.addEventListener('${randomName}', (e) => self['${randomName}'](e.detail.frameId))`,
1154 | + runImmediately: true,
1155 | + });
1156 | + // create new isolated world for this frame
1157 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', {
1158 | + frameId,
1159 | + // use randomName for worldName to distinguish from normal utility world
1160 | + worldName: randomName,
1161 | + grantUniveralAccess: true,
1162 | + });
1163 | + // emit event in the specific frame from the isolated world
1164 | + await client.send('Runtime.evaluate', {
1165 | + expression: `document.dispatchEvent(new CustomEvent('${randomName}', { detail: { frameId: '${frameId}' } }))`,
1166 | + contextId: createIsolatedWorldResult.executionContextId,
1167 | + });
1168 | + }
1169 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] result:`, { contextId });
1170 | + return contextId;
1171 | + }
1172 | + async __re__getIsolatedWorld({ client, frameId, worldName }) {
1173 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', {
1174 | + frameId,
1175 | + worldName,
1176 | + grantUniveralAccess: true,
1177 | + });
1178 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getIsolatedWorld] result:`, createIsolatedWorldResult);
1179 | + return createIsolatedWorldResult.executionContextId;
1180 | + }
1181 | + // rebrowser-patches: get context id if it's missing
1182 | + async acquireContextId(tryCount = 1) {
1183 | + if (this.#id > 0) {
1184 | + return;
1185 | + }
1186 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding';
1187 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][acquireContextId] id = ${this.#id}, name = ${this.#name}, fixMode = ${fixMode}, tryCount = ${tryCount}`);
1188 | + let contextId;
1189 | + let tryAgain = true;
1190 | + let errorMessage = 'N/A';
1191 | + if (fixMode === 'addBinding') {
1192 | + try {
1193 | + if (this.#id === -2) {
1194 | + // isolated world
1195 | + contextId = await this.__re__getIsolatedWorld({
1196 | + client: this.#client,
1197 | + frameId: this._frameId,
1198 | + worldName: this.#name,
1199 | + });
1200 | + }
1201 | + else {
1202 | + // main world
1203 | + contextId = await this.__re__getMainWorld({
1204 | + client: this.#client,
1205 | + frameId: this._frameId,
1206 | + isWorker: this.#id === -3,
1207 | + });
1208 | + }
1209 | + }
1210 | + catch (error) {
1211 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.error('[rebrowser-patches][acquireContextId] error:', error);
1212 | + errorMessage = error.message;
1213 | + if (error instanceof Error) {
1214 | + if (error.message.includes('No frame for given id found') ||
1215 | + error.message.includes('Target closed') ||
1216 | + error.message.includes('Session closed')) {
1217 | + // target doesn't exist anymore, don't try again
1218 | + tryAgain = false;
1219 | + }
1220 | + }
1221 | + debugError(error);
1222 | + }
1223 | + }
1224 | + else if (fixMode === 'alwaysIsolated') {
1225 | + if (this.#id === -3) {
1226 | + throw new Error('[rebrowser-patches] web workers are not supported in alwaysIsolated mode');
1227 | + }
1228 | + contextId = await this.__re__getIsolatedWorld({
1229 | + client: this.#client,
1230 | + frameId: this._frameId,
1231 | + worldName: this.#name,
1232 | + });
1233 | + }
1234 | + else if (fixMode === 'enableDisable') {
1235 | + const executionContextCreatedHandler = ({ context }) => {
1236 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][executionContextCreated] this.#id = ${this.#id}, name = ${this.#name}, contextId = ${contextId}, event.context.id = ${context.id}`);
1237 | + if (contextId > 0) {
1238 | + // already acquired the id
1239 | + return;
1240 | + }
1241 | + if (this.#id === -1) {
1242 | + // main world
1243 | + if (context.auxData && context.auxData['isDefault']) {
1244 | + contextId = context.id;
1245 | + }
1246 | + }
1247 | + else if (this.#id === -2) {
1248 | + // utility world
1249 | + if (this.#name === context.name) {
1250 | + contextId = context.id;
1251 | + }
1252 | + }
1253 | + else if (this.#id === -3) {
1254 | + // web worker
1255 | + contextId = context.id;
1256 | + }
1257 | + };
1258 | + this.#client.on('Runtime.executionContextCreated', executionContextCreatedHandler);
1259 | + await this.#client.send('Runtime.enable');
1260 | + await this.#client.send('Runtime.disable');
1261 | + this.#client.off('Runtime.executionContextCreated', executionContextCreatedHandler);
1262 | + }
1263 | + if (!contextId) {
1264 | + if (!tryAgain || tryCount >= 3) {
1265 | + throw new Error(`[rebrowser-patches] acquireContextId failed (tryAgain = ${tryAgain}, tryCount = ${tryCount}), errorMessage: ${errorMessage}`);
1266 | + }
1267 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][acquireContextId] failed, try again (tryCount = ${tryCount})`);
1268 | + return this.acquireContextId(tryCount + 1);
1269 | + }
1270 | + this.#id = contextId;
1271 | + }
1272 | async #evaluate(returnByValue, pageFunction, ...args) {
1273 | + // rebrowser-patches: context id is missing, acquire it and try again
1274 | + if (this.#id < 0) {
1275 | + await this.acquireContextId();
1276 | + // @ts-ignore
1277 | + return this.#evaluate(returnByValue, pageFunction, ...args);
1278 | + }
1279 | const sourceUrlComment = getSourceUrlComment(getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ??
1280 | PuppeteerURL.INTERNAL_URL);
1281 | if (isString(pageFunction)) {
1282 |
1283 | --- a/lib/esm/puppeteer/cdp/FrameManager.js
1284 | +++ b/lib/esm/puppeteer/cdp/FrameManager.js
1285 | @@ -151,6 +151,10 @@
1286 | this.#onFrameStoppedLoading(event.frameId);
1287 | });
1288 | session.on('Runtime.executionContextCreated', async (event) => {
1289 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
1290 | + // rebrowser-patches: ignore default logic
1291 | + return;
1292 | + }
1293 | await this.#frameTreeHandled?.valueOrThrow();
1294 | this.#onExecutionContextCreated(event.context, session);
1295 | });
1296 | @@ -175,9 +179,16 @@
1297 | this.#frameTreeHandled?.resolve();
1298 | }),
1299 | client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
1300 | - client.send('Runtime.enable').then(() => {
1301 | - return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME);
1302 | - }),
1303 | + (() => {
1304 | + // rebrowser-patches: skip Runtime.enable
1305 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
1306 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][FrameManager] initialize');
1307 | + return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME);
1308 | + }
1309 | + return client.send('Runtime.enable').then(() => {
1310 | + return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME);
1311 | + });
1312 | + })(),
1313 | ...(frame
1314 | ? Array.from(this.#scriptsToEvaluateOnNewDocument.values())
1315 | : []).map(script => {
1316 | @@ -187,6 +198,26 @@
1317 | return frame?.addExposedFunctionBinding(binding);
1318 | }),
1319 | ]);
1320 | + // rebrowser-patches: manually create main world context
1321 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
1322 | + this.frames()
1323 | + .filter(frame => {
1324 | + return frame.client === client;
1325 | + }).map(frame => {
1326 | + const world = frame.worlds[MAIN_WORLD];
1327 | + const contextPayload = {
1328 | + id: -1,
1329 | + name: '',
1330 | + auxData: {
1331 | + frameId: frame._id,
1332 | + }
1333 | + };
1334 | + const context = new ExecutionContext(frame.client,
1335 | + // @ts-ignore
1336 | + contextPayload, world);
1337 | + world.setContext(context);
1338 | + });
1339 | + }
1340 | }
1341 | catch (error) {
1342 | this.#frameTreeHandled?.resolve();
1343 | @@ -355,6 +386,23 @@
1344 | }
1345 | this._frameTree.addFrame(frame);
1346 | }
1347 | + // rebrowser-patches: we cannot fully dispose contexts as they won't be recreated as we don't have Runtime events,
1348 | + // instead, just mark it all empty
1349 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
1350 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches] onFrameNavigated, navigationType = ${navigationType}, id = ${framePayload.id}, url = ${framePayload.url}`);
1351 | + for (const worldSymbol of [MAIN_WORLD, PUPPETEER_WORLD]) {
1352 | + // @ts-ignore
1353 | + if (frame?.worlds[worldSymbol].context) {
1354 | + // @ts-ignore
1355 | + const frameOrWorker = frame.worlds[worldSymbol].environment;
1356 | + if ('clearDocumentHandle' in frameOrWorker) {
1357 | + frameOrWorker.clearDocumentHandle();
1358 | + }
1359 | + // @ts-ignore
1360 | + frame.worlds[worldSymbol].context?.clear(worldSymbol === MAIN_WORLD ? -1 : -2);
1361 | + }
1362 | + }
1363 | + }
1364 | frame = await this._frameTree.waitForFrame(frameId);
1365 | frame._navigated(framePayload);
1366 | this.emit(FrameManagerEvent.FrameNavigated, frame);
1367 | @@ -382,6 +430,24 @@
1368 | worldName: name,
1369 | grantUniveralAccess: true,
1370 | })
1371 | + .then((createIsolatedWorldResult) => {
1372 | + // rebrowser-patches: save created context id
1373 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
1374 | + return;
1375 | + }
1376 | + if (!createIsolatedWorldResult?.executionContextId) {
1377 | + // probably "Target closed" error, just ignore it
1378 | + return;
1379 | + }
1380 | + // @ts-ignore
1381 | + this.#onExecutionContextCreated({
1382 | + id: createIsolatedWorldResult.executionContextId,
1383 | + name,
1384 | + auxData: {
1385 | + frameId: frame._id,
1386 | + }
1387 | + }, frame.client);
1388 | + })
1389 | .catch(debugError);
1390 | }));
1391 | this.#isolatedWorlds.add(key);
1392 |
1393 | --- a/lib/esm/puppeteer/cdp/IsolatedWorld.d.ts
1394 | +++ b/lib/esm/puppeteer/cdp/IsolatedWorld.d.ts
1395 | @@ -12,9 +12,9 @@
1396 | import type { TimeoutSettings } from '../common/TimeoutSettings.js';
1397 | import type { EvaluateFunc, HandleFor } from '../common/types.js';
1398 | import { disposeSymbol } from '../util/disposable.js';
1399 | -import type { ExecutionContext } from './ExecutionContext.js';
1400 | +import { ExecutionContext } from './ExecutionContext.js';
1401 | import type { CdpFrame } from './Frame.js';
1402 | -import type { MAIN_WORLD, PUPPETEER_WORLD } from './IsolatedWorlds.js';
1403 | +import { MAIN_WORLD, PUPPETEER_WORLD } from './IsolatedWorlds.js';
1404 | import type { CdpWebWorker } from './WebWorker.js';
1405 | /**
1406 | * @internal
1407 |
1408 | --- a/lib/esm/puppeteer/cdp/IsolatedWorld.js
1409 | +++ b/lib/esm/puppeteer/cdp/IsolatedWorld.js
1410 | @@ -1,3 +1,4 @@
1411 | +//@ts-nocheck
1412 | /**
1413 | * @license
1414 | * Copyright 2019 Google Inc.
1415 | @@ -6,9 +7,11 @@
1416 | import { firstValueFrom, map, raceWith } from '../../third_party/rxjs/rxjs.js';
1417 | import { Realm } from '../api/Realm.js';
1418 | import { EventEmitter } from '../common/EventEmitter.js';
1419 | -import { fromEmitterEvent, timeout, withSourcePuppeteerURLIfNone, } from '../common/util.js';
1420 | +import { fromEmitterEvent, timeout, withSourcePuppeteerURLIfNone, UTILITY_WORLD_NAME, } from '../common/util.js';
1421 | import { disposeSymbol } from '../util/disposable.js';
1422 | import { CdpElementHandle } from './ElementHandle.js';
1423 | +import { ExecutionContext } from './ExecutionContext.js';
1424 | +import { MAIN_WORLD, PUPPETEER_WORLD } from './IsolatedWorlds.js';
1425 | import { CdpJSHandle } from './JSHandle.js';
1426 | /**
1427 | * @internal
1428 | @@ -67,6 +70,21 @@
1429 | * Waits for the next context to be set on the isolated world.
1430 | */
1431 | async #waitForExecutionContext() {
1432 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding';
1433 | + if (fixMode === 'addBinding') {
1434 | + const isMainWorld = this.#frameOrWorker.worlds[MAIN_WORLD] === this;
1435 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][waitForExecutionContext] frameId = ${this.#frameOrWorker._id}, isMainWorld = ${isMainWorld}`);
1436 | + const contextPayload = {
1437 | + id: isMainWorld ? -1 : -2,
1438 | + name: isMainWorld ? '' : UTILITY_WORLD_NAME,
1439 | + auxData: {
1440 | + frameId: this.#frameOrWorker._id,
1441 | + }
1442 | + };
1443 | + const context = new ExecutionContext(this.client, contextPayload, this);
1444 | + this.setContext(context);
1445 | + return context;
1446 | + }
1447 | const error = new Error('Execution context was destroyed');
1448 | const result = await firstValueFrom(fromEmitterEvent(this.#emitter, 'context').pipe(raceWith(fromEmitterEvent(this.#emitter, 'disposed').pipe(map(() => {
1449 | // The message has to match the CDP message expected by the WaitTask class.
1450 | @@ -104,6 +122,8 @@
1451 | if (!context) {
1452 | context = await this.#waitForExecutionContext();
1453 | }
1454 | + // rebrowser-patches: make sure id is acquired
1455 | + await context.acquireContextId();
1456 | const { object } = await this.client.send('DOM.resolveNode', {
1457 | backendNodeId: backendNodeId,
1458 | executionContextId: context.id,
1459 |
1460 | --- a/lib/esm/puppeteer/cdp/WebWorker.js
1461 | +++ b/lib/esm/puppeteer/cdp/WebWorker.js
1462 | @@ -21,6 +21,10 @@
1463 | this.#targetType = targetType;
1464 | this.#world = new IsolatedWorld(this, new TimeoutSettings());
1465 | this.#client.once('Runtime.executionContextCreated', async (event) => {
1466 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
1467 | + // rebrowser-patches: ignore default logic
1468 | + return;
1469 | + }
1470 | this.#world.setContext(new ExecutionContext(client, event.context, this.#world));
1471 | });
1472 | this.#world.emitter.on('consoleapicalled', async (event) => {
1473 | @@ -39,7 +43,22 @@
1474 | });
1475 | // This might fail if the target is closed before we receive all execution contexts.
1476 | networkManager?.addClient(this.#client).catch(debugError);
1477 | - this.#client.send('Runtime.enable').catch(debugError);
1478 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
1479 | + // @ts-ignore
1480 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][WebWorker] initialize', targetType, targetId, client._target(), client._target()._getTargetInfo());
1481 | + // rebrowser-patches: manually create context
1482 | + const contextPayload = {
1483 | + id: -3,
1484 | + auxData: {
1485 | + frameId: targetId,
1486 | + }
1487 | + };
1488 | + // @ts-ignore
1489 | + this.#world.setContext(new ExecutionContext(client, contextPayload, this.#world));
1490 | + }
1491 | + else {
1492 | + this.#client.send('Runtime.enable').catch(debugError);
1493 | + }
1494 | }
1495 | mainRealm() {
1496 | return this.#world;
1497 |
1498 | --- a/lib/esm/puppeteer/common/util.js
1499 | +++ b/lib/esm/puppeteer/common/util.js
1500 | @@ -231,7 +231,10 @@
1501 | /**
1502 | * @internal
1503 | */
1504 | -export const UTILITY_WORLD_NAME = '__puppeteer_utility_world__' + packageVersion;
1505 | +export const UTILITY_WORLD_NAME =
1506 | +// rebrowser-patches: change utility world name
1507 | +process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] !== '0' ? (process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] || 'util') :
1508 | + '__puppeteer_utility_world__' + packageVersion;
1509 | /**
1510 | * @internal
1511 | */
1512 | @@ -240,6 +243,10 @@
1513 | * @internal
1514 | */
1515 | export function getSourceUrlComment(url) {
1516 | + // rebrowser-patches: change sourceUrl to generic script name
1517 | + if (process.env['REBROWSER_PATCHES_SOURCE_URL'] !== '0') {
1518 | + url = process.env['REBROWSER_PATCHES_SOURCE_URL'] || 'app.js';
1519 | + }
1520 | return `//# sourceURL=${url}`;
1521 | }
1522 | /**
1523 |
--------------------------------------------------------------------------------
/patches/puppeteer-core/src.patch:
--------------------------------------------------------------------------------
1 | --- a/src/cdp/Browser.ts
2 | +++ b/src/cdp/Browser.ts
3 | @@ -334,6 +334,11 @@
4 | return await this.#defaultContext.newPage();
5 | }
6 |
7 | + // rebrowser-patches: expose browser CDP session
8 | + _connection(): Connection {
9 | + return this.#connection;
10 | + }
11 | +
12 | async _createPageInContext(contextId?: string): Promise {
13 | const {targetId} = await this.#connection.send('Target.createTarget', {
14 | url: 'about:blank',
15 |
16 | --- a/src/cdp/ExecutionContext.ts
17 | +++ b/src/cdp/ExecutionContext.ts
18 | @@ -78,6 +78,7 @@
19 | #client: CDPSession;
20 | #world: IsolatedWorld;
21 | #id: number;
22 | + _frameId: any;
23 | #name?: string;
24 |
25 | readonly #disposables = new DisposableStack();
26 | @@ -94,16 +95,22 @@
27 | if (contextPayload.name) {
28 | this.#name = contextPayload.name;
29 | }
30 | + // rebrowser-patches: keep frameId to use later
31 | + if (contextPayload.auxData?.frameId) {
32 | + this._frameId = contextPayload.auxData?.frameId;
33 | + }
34 | const clientEmitter = this.#disposables.use(new EventEmitter(this.#client));
35 | clientEmitter.on('Runtime.bindingCalled', this.#onBindingCalled.bind(this));
36 | - clientEmitter.on('Runtime.executionContextDestroyed', async event => {
37 | - if (event.executionContextId === this.#id) {
38 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
39 | + clientEmitter.on('Runtime.executionContextDestroyed', async event => {
40 | + if (event.executionContextId === this.#id) {
41 | + this[disposeSymbol]();
42 | + }
43 | + });
44 | + clientEmitter.on('Runtime.executionContextsCleared', async () => {
45 | this[disposeSymbol]();
46 | - }
47 | - });
48 | - clientEmitter.on('Runtime.executionContextsCleared', async () => {
49 | - this[disposeSymbol]();
50 | - });
51 | + });
52 | + }
53 | clientEmitter.on('Runtime.consoleAPICalled', this.#onConsoleAPI.bind(this));
54 | clientEmitter.on(CDPSessionEvent.Disconnected, () => {
55 | this[disposeSymbol]();
56 | @@ -350,6 +357,186 @@
57 | return await this.#evaluate(false, pageFunction, ...args);
58 | }
59 |
60 | + // rebrowser-patches: alternative to dispose
61 | + clear(newId: any) {
62 | + this.#id = newId;
63 | + this.#bindings = new Map();
64 | + this.#bindingsInstalled = false;
65 | + this.#puppeteerUtil = undefined;
66 | + }
67 | + async __re__getMainWorld({ client, frameId, isWorker = false }: any) {
68 | + let contextId: any;
69 | +
70 | + // random name to make it harder to detect for any 3rd party script by watching window object and events
71 | + const randomName = [...Array(Math.floor(Math.random() * (10 + 1)) + 10)].map(() => Math.random().toString(36)[2]).join('');
72 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] binding name = ${randomName}`);
73 | +
74 | + // add the binding
75 | + await client.send('Runtime.addBinding', {
76 | + name: randomName,
77 | + });
78 | +
79 | + // listen for 'Runtime.bindingCalled' event
80 | + const bindingCalledHandler = ({ name, payload, executionContextId }: any) => {
81 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][bindingCalledHandler]', {
82 | + name,
83 | + payload,
84 | + executionContextId
85 | + });
86 | + if (contextId > 0) {
87 | + // already acquired the id
88 | + return;
89 | + }
90 | + if (name !== randomName) {
91 | + // ignore irrelevant bindings
92 | + return;
93 | + }
94 | + if (payload !== frameId) {
95 | + // ignore irrelevant frames
96 | + return;
97 | + }
98 | + contextId = executionContextId;
99 | + // remove this listener
100 | + client.off('Runtime.bindingCalled', bindingCalledHandler);
101 | + };
102 | + client.on('Runtime.bindingCalled', bindingCalledHandler);
103 | +
104 | + if (isWorker) {
105 | + // workers don't support `Page.addScriptToEvaluateOnNewDocument` and `Page.createIsolatedWorld`, but there are no iframes inside of them, so it's safe to just use Runtime.evaluate
106 | + await client.send('Runtime.evaluate', {
107 | + expression: `this['${randomName}']('${frameId}')`,
108 | + });
109 | + } else {
110 | + // we could call the binding right from `addScriptToEvaluateOnNewDocument`, but this way it will be called in all existing frames and it's hard to distinguish children from the parent
111 | + await client.send('Page.addScriptToEvaluateOnNewDocument', {
112 | + source: `document.addEventListener('${randomName}', (e) => self['${randomName}'](e.detail.frameId))`,
113 | + runImmediately: true,
114 | + });
115 | +
116 | + // create new isolated world for this frame
117 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', {
118 | + frameId,
119 | + // use randomName for worldName to distinguish from normal utility world
120 | + worldName: randomName,
121 | + grantUniveralAccess: true,
122 | + });
123 | +
124 | + // emit event in the specific frame from the isolated world
125 | + await client.send('Runtime.evaluate', {
126 | + expression: `document.dispatchEvent(new CustomEvent('${randomName}', { detail: { frameId: '${frameId}' } }))`,
127 | + contextId: createIsolatedWorldResult.executionContextId,
128 | + });
129 | + }
130 | +
131 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] result:`, { contextId });
132 | + return contextId;
133 | + }
134 | + async __re__getIsolatedWorld({ client, frameId, worldName }: any) {
135 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', {
136 | + frameId,
137 | + worldName,
138 | + grantUniveralAccess: true,
139 | + });
140 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getIsolatedWorld] result:`, createIsolatedWorldResult);
141 | + return createIsolatedWorldResult.executionContextId;
142 | + }
143 | + // rebrowser-patches: get context id if it's missing
144 | + async acquireContextId(tryCount = 1): Promise {
145 | + if (this.#id > 0) {
146 | + return
147 | + }
148 | +
149 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding'
150 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][acquireContextId] id = ${this.#id}, name = ${this.#name}, fixMode = ${fixMode}, tryCount = ${tryCount}`)
151 | +
152 | + let contextId: any
153 | + let tryAgain = true;
154 | + let errorMessage = 'N/A'
155 | + if (fixMode === 'addBinding') {
156 | + try {
157 | + if (this.#id === -2) {
158 | + // isolated world
159 | + contextId = await this.__re__getIsolatedWorld({
160 | + client: this.#client,
161 | + frameId: this._frameId,
162 | + worldName: this.#name,
163 | + })
164 | + } else {
165 | + // main world
166 | + contextId = await this.__re__getMainWorld({
167 | + client: this.#client,
168 | + frameId: this._frameId,
169 | + isWorker: this.#id === -3,
170 | + })
171 | + }
172 | + } catch (error: any) {
173 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.error('[rebrowser-patches][acquireContextId] error:', error)
174 | + errorMessage = error.message
175 | + if (error instanceof Error) {
176 | + if (
177 | + error.message.includes('No frame for given id found') ||
178 | + error.message.includes('Target closed') ||
179 | + error.message.includes('Session closed')
180 | + ) {
181 | + // target doesn't exist anymore, don't try again
182 | + tryAgain = false
183 | + }
184 | + }
185 | +
186 | + debugError(error);
187 | + }
188 | + } else if (fixMode === 'alwaysIsolated') {
189 | + if (this.#id === -3) {
190 | + throw new Error('[rebrowser-patches] web workers are not supported in alwaysIsolated mode')
191 | + }
192 | +
193 | + contextId = await this.__re__getIsolatedWorld({
194 | + client: this.#client,
195 | + frameId: this._frameId,
196 | + worldName: this.#name,
197 | + })
198 | + } else if (fixMode === 'enableDisable') {
199 | + const executionContextCreatedHandler = ({ context }: any) => {
200 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][executionContextCreated] this.#id = ${this.#id}, name = ${this.#name}, contextId = ${contextId}, event.context.id = ${context.id}`)
201 | +
202 | + if (contextId > 0) {
203 | + // already acquired the id
204 | + return
205 | + }
206 | +
207 | + if (this.#id === -1) {
208 | + // main world
209 | + if (context.auxData && context.auxData['isDefault']) {
210 | + contextId = context.id
211 | + }
212 | + } else if (this.#id === -2) {
213 | + // utility world
214 | + if (this.#name === context.name) {
215 | + contextId = context.id
216 | + }
217 | + } else if (this.#id === -3) {
218 | + // web worker
219 | + contextId = context.id
220 | + }
221 | + }
222 | +
223 | + this.#client.on('Runtime.executionContextCreated', executionContextCreatedHandler)
224 | + await this.#client.send('Runtime.enable')
225 | + await this.#client.send('Runtime.disable')
226 | + this.#client.off('Runtime.executionContextCreated', executionContextCreatedHandler)
227 | + }
228 | +
229 | + if (!contextId) {
230 | + if (!tryAgain || tryCount >= 3) {
231 | + throw new Error(`[rebrowser-patches] acquireContextId failed (tryAgain = ${tryAgain}, tryCount = ${tryCount}), errorMessage: ${errorMessage}`)
232 | + }
233 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][acquireContextId] failed, try again (tryCount = ${tryCount})`)
234 | + return this.acquireContextId(tryCount + 1)
235 | + }
236 | +
237 | + this.#id = contextId
238 | + }
239 | +
240 | async #evaluate<
241 | Params extends unknown[],
242 | Func extends EvaluateFunc = EvaluateFunc,
243 | @@ -374,6 +561,13 @@
244 | pageFunction: Func | string,
245 | ...args: Params
246 | ): Promise>> | Awaited>> {
247 | + // rebrowser-patches: context id is missing, acquire it and try again
248 | + if (this.#id < 0) {
249 | + await this.acquireContextId()
250 | + // @ts-ignore
251 | + return this.#evaluate(returnByValue, pageFunction, ...args)
252 | + }
253 | +
254 | const sourceUrlComment = getSourceUrlComment(
255 | getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ??
256 | PuppeteerURL.INTERNAL_URL,
257 |
258 | --- a/src/cdp/FrameManager.ts
259 | +++ b/src/cdp/FrameManager.ts
260 | @@ -191,6 +191,10 @@
261 | this.#onFrameStoppedLoading(event.frameId);
262 | });
263 | session.on('Runtime.executionContextCreated', async event => {
264 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
265 | + // rebrowser-patches: ignore default logic
266 | + return
267 | + }
268 | await this.#frameTreeHandled?.valueOrThrow();
269 | this.#onExecutionContextCreated(event.context, session);
270 | });
271 | @@ -216,9 +220,17 @@
272 | this.#frameTreeHandled?.resolve();
273 | }),
274 | client.send('Page.setLifecycleEventsEnabled', {enabled: true}),
275 | - client.send('Runtime.enable').then(() => {
276 | - return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME);
277 | - }),
278 | + (() => {
279 | + // rebrowser-patches: skip Runtime.enable
280 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
281 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][FrameManager] initialize')
282 | + return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME)
283 | + }
284 | +
285 | + return client.send('Runtime.enable').then(() => {
286 | + return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME);
287 | + })
288 | + })(),
289 | ...(frame
290 | ? Array.from(this.#scriptsToEvaluateOnNewDocument.values())
291 | : []
292 | @@ -229,6 +241,30 @@
293 | return frame?.addExposedFunctionBinding(binding);
294 | }),
295 | ]);
296 | +
297 | + // rebrowser-patches: manually create main world context
298 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
299 | + this.frames()
300 | + .filter(frame => {
301 | + return frame.client === client;
302 | + }).map(frame => {
303 | + const world = frame.worlds[MAIN_WORLD]
304 | + const contextPayload = {
305 | + id: -1,
306 | + name: '',
307 | + auxData: {
308 | + frameId: frame._id,
309 | + }
310 | + }
311 | + const context = new ExecutionContext(
312 | + frame.client,
313 | + // @ts-ignore
314 | + contextPayload,
315 | + world
316 | + );
317 | + world.setContext(context);
318 | + })
319 | + }
320 | } catch (error) {
321 | this.#frameTreeHandled?.resolve();
322 | // The target might have been closed before the initialization finished.
323 | @@ -455,6 +491,24 @@
324 | this._frameTree.addFrame(frame);
325 | }
326 |
327 | + // rebrowser-patches: we cannot fully dispose contexts as they won't be recreated as we don't have Runtime events,
328 | + // instead, just mark it all empty
329 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
330 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches] onFrameNavigated, navigationType = ${navigationType}, id = ${framePayload.id}, url = ${framePayload.url}`)
331 | + for (const worldSymbol of [MAIN_WORLD, PUPPETEER_WORLD]) {
332 | + // @ts-ignore
333 | + if (frame?.worlds[worldSymbol].context) {
334 | + // @ts-ignore
335 | + const frameOrWorker = frame.worlds[worldSymbol].environment
336 | + if ('clearDocumentHandle' in frameOrWorker) {
337 | + frameOrWorker.clearDocumentHandle();
338 | + }
339 | + // @ts-ignore
340 | + frame.worlds[worldSymbol].context?.clear(worldSymbol === MAIN_WORLD ? -1 : -2)
341 | + }
342 | + }
343 | + }
344 | +
345 | frame = await this._frameTree.waitForFrame(frameId);
346 | frame._navigated(framePayload);
347 | this.emit(FrameManagerEvent.FrameNavigated, frame);
348 | @@ -487,6 +541,24 @@
349 | worldName: name,
350 | grantUniveralAccess: true,
351 | })
352 | + .then((createIsolatedWorldResult: any) => {
353 | + // rebrowser-patches: save created context id
354 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') {
355 | + return
356 | + }
357 | + if (!createIsolatedWorldResult?.executionContextId) {
358 | + // probably "Target closed" error, just ignore it
359 | + return
360 | + }
361 | + // @ts-ignore
362 | + this.#onExecutionContextCreated({
363 | + id: createIsolatedWorldResult.executionContextId,
364 | + name,
365 | + auxData: {
366 | + frameId: frame._id,
367 | + }
368 | + }, frame.client)
369 | + })
370 | .catch(debugError);
371 | }),
372 | );
373 |
374 | --- a/src/cdp/IsolatedWorld.ts
375 | +++ b/src/cdp/IsolatedWorld.ts
376 | @@ -1,3 +1,4 @@
377 | +//@ts-nocheck
378 | /**
379 | * @license
380 | * Copyright 2019 Google Inc.
381 | @@ -18,13 +19,14 @@
382 | fromEmitterEvent,
383 | timeout,
384 | withSourcePuppeteerURLIfNone,
385 | + UTILITY_WORLD_NAME,
386 | } from '../common/util.js';
387 | import {disposeSymbol} from '../util/disposable.js';
388 |
389 | import {CdpElementHandle} from './ElementHandle.js';
390 | -import type {ExecutionContext} from './ExecutionContext.js';
391 | +import {ExecutionContext} from './ExecutionContext.js';
392 | import type {CdpFrame} from './Frame.js';
393 | -import type {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
394 | +import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
395 | import {CdpJSHandle} from './JSHandle.js';
396 | import type {CdpWebWorker} from './WebWorker.js';
397 |
398 | @@ -137,6 +139,23 @@
399 | * Waits for the next context to be set on the isolated world.
400 | */
401 | async #waitForExecutionContext(): Promise {
402 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding';
403 | + if (fixMode === 'addBinding') {
404 | + const isMainWorld = this.#frameOrWorker.worlds[MAIN_WORLD] === this;
405 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][waitForExecutionContext] frameId = ${this.#frameOrWorker._id}, isMainWorld = ${isMainWorld}`);
406 | +
407 | + const contextPayload = {
408 | + id: isMainWorld ? -1 : -2,
409 | + name: isMainWorld ? '' : UTILITY_WORLD_NAME,
410 | + auxData: {
411 | + frameId: this.#frameOrWorker._id,
412 | + }
413 | + };
414 | + const context = new ExecutionContext(this.client, contextPayload, this);
415 | + this.setContext(context);
416 | + return context;
417 | + }
418 | +
419 | const error = new Error('Execution context was destroyed');
420 | const result = await firstValueFrom(
421 | fromEmitterEvent(this.#emitter, 'context').pipe(
422 | @@ -206,6 +225,8 @@
423 | if (!context) {
424 | context = await this.#waitForExecutionContext();
425 | }
426 | + // rebrowser-patches: make sure id is acquired
427 | + await context.acquireContextId()
428 | const {object} = await this.client.send('DOM.resolveNode', {
429 | backendNodeId: backendNodeId,
430 | executionContextId: context.id,
431 |
432 | --- a/src/cdp/WebWorker.ts
433 | +++ b/src/cdp/WebWorker.ts
434 | @@ -58,6 +58,10 @@
435 | this.#world = new IsolatedWorld(this, new TimeoutSettings());
436 |
437 | this.#client.once('Runtime.executionContextCreated', async event => {
438 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
439 | + // rebrowser-patches: ignore default logic
440 | + return
441 | + }
442 | this.#world.setContext(
443 | new ExecutionContext(client, event.context, this.#world),
444 | );
445 | @@ -82,7 +86,22 @@
446 |
447 | // This might fail if the target is closed before we receive all execution contexts.
448 | networkManager?.addClient(this.#client).catch(debugError);
449 | - this.#client.send('Runtime.enable').catch(debugError);
450 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') {
451 | + // @ts-ignore
452 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][WebWorker] initialize', targetType, targetId, client._target(), client._target()._getTargetInfo())
453 | +
454 | + // rebrowser-patches: manually create context
455 | + const contextPayload = {
456 | + id: -3,
457 | + auxData: {
458 | + frameId: targetId,
459 | + }
460 | + }
461 | + // @ts-ignore
462 | + this.#world.setContext(new ExecutionContext(client, contextPayload, this.#world));
463 | + } else {
464 | + this.#client.send('Runtime.enable').catch(debugError);
465 | + }
466 | }
467 |
468 | mainRealm(): Realm {
469 |
470 | --- a/src/common/util.ts
471 | +++ b/src/common/util.ts
472 | @@ -299,7 +299,9 @@
473 | * @internal
474 | */
475 | export const UTILITY_WORLD_NAME =
476 | - '__puppeteer_utility_world__' + packageVersion;
477 | + // rebrowser-patches: change utility world name
478 | + process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] !== '0' ? (process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] || 'util') :
479 | + '__puppeteer_utility_world__' + packageVersion;
480 |
481 | /**
482 | * @internal
483 | @@ -310,6 +312,10 @@
484 | * @internal
485 | */
486 | export function getSourceUrlComment(url: string): string {
487 | + // rebrowser-patches: change sourceUrl to generic script name
488 | + if (process.env['REBROWSER_PATCHES_SOURCE_URL'] !== '0') {
489 | + url = process.env['REBROWSER_PATCHES_SOURCE_URL'] || 'app.js'
490 | + }
491 | return `//# sourceURL=${url}`;
492 | }
493 |
494 |
--------------------------------------------------------------------------------
/scripts/patcher.js:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 |
3 | import { readFile } from 'node:fs/promises'
4 | import { resolve } from 'node:path'
5 |
6 | import yargs from 'yargs'
7 | import { hideBin } from 'yargs/helpers'
8 |
9 | import {
10 | exec,
11 | fatalError,
12 | getPatchBaseCmd,
13 | getPatcherPackagePath,
14 | log,
15 | validPackagesNames,
16 | } from './utils/index.js';
17 |
18 | (async () => {
19 | // config and preparations
20 | const cliArgs = yargs(hideBin(process.argv))
21 | .usage('Usage: [options]')
22 | .command('patch', 'Apply patch')
23 | .command('unpatch', 'Reverse patch')
24 | .command('check', 'Check if patch is already applied')
25 | .describe('packageName', `Target package name: ${validPackagesNames.join(', ')}`)
26 | .describe('packagePath', 'Path to the target package')
27 | .describe('codeTarget', 'What code to patch: src or lib')
28 | .boolean('debug')
29 | .describe('debug', 'Enable debugging mode')
30 | .demandCommand(1, 1, 'Error: choose a command (patch, unpatch, check)')
31 | .parse()
32 |
33 | let {
34 | packageName,
35 | packagePath,
36 | codeTarget = 'lib',
37 | debug,
38 | } = cliArgs
39 |
40 | if (debug) {
41 | process.env.REBROWSER_PATCHES_DEBUG = 1
42 | }
43 |
44 | const command = cliArgs._[0]
45 | let commandResult
46 |
47 | if (!packagePath && !packageName) {
48 | fatalError('You need to specify either packageName or packagePath.')
49 | }
50 |
51 | if (!packagePath) {
52 | packagePath = `${process.cwd()}/node_modules/${packageName}`
53 | }
54 |
55 | if (!['patch', 'unpatch', 'check'].includes(command)) {
56 | fatalError(`Unknown command: ${command}`)
57 | }
58 |
59 | if (!['src', 'lib'].includes(codeTarget)) {
60 | fatalError(`Unknown codeTarget: ${codeTarget}`)
61 | }
62 |
63 | log('Config:')
64 | log(`command = ${command}, packageName = ${packageName}, codeTarget = ${codeTarget}`)
65 | log(`packagePath = ${packagePath}`)
66 | log('------')
67 |
68 | // find package
69 | let packageJson
70 | const packageJsonPath = resolve(packagePath, 'package.json')
71 | try {
72 | const packageJsonText = await readFile(packageJsonPath, { encoding: 'utf8' })
73 | packageJson = JSON.parse(packageJsonText)
74 | } catch (err) {
75 | fatalError('Cannot read package.json', err)
76 | }
77 | if (!packageName) {
78 | if (!validPackagesNames.includes(packageJson.name)) {
79 | fatalError(`Package name is "${packageJson.name}", but we only support ${validPackagesNames.join(', ')}.`)
80 | } else {
81 | packageName = packageJson.name
82 | }
83 | } else if (packageJson.name !== packageName) {
84 | fatalError(`Package name is "${packageJson.name}", but we're looking for "${packageName}". Check your package path.`)
85 | }
86 | log(`Found package "${packageJson.name}", version ${packageJson.version}`)
87 |
88 | const patchFilePath = resolve(getPatcherPackagePath(), `./patches/${packageName}/${codeTarget}.patch`)
89 |
90 | // check patch status
91 | let patchStatus
92 | try {
93 | const { stdout, stderr } = await exec(`${getPatchBaseCmd(patchFilePath)} --dry-run`, {
94 | cwd: packagePath,
95 | })
96 | patchStatus = 'unpatched'
97 | } catch (e) {
98 | if (e.stdout.includes('No file to patch')) {
99 | fatalError('Internal error, patch command cannot find file to patch')
100 | } else if (e.stdout.includes('Ignoring previously applied (or reversed) patch')) {
101 | patchStatus = 'patched'
102 | } else if (e.stderr.includes('is not recognized')) {
103 | let message = 'patch command not found!'
104 | if (process.platform === 'win32') {
105 | message += '\nCheck README for how to install patch.exe on Windows.'
106 | }
107 | fatalError(message)
108 | } else {
109 | log('[debug] patch error:', e)
110 | throw e
111 | }
112 | }
113 | log(`Current patch status = ${patchStatus === 'patched' ? '🟩' : '🟧'} ${patchStatus}`)
114 |
115 | // run command
116 | let execCmd
117 | if (command === 'patch') {
118 | if (patchStatus === 'patched') {
119 | log('Package already patched.')
120 | } else {
121 | execCmd = getPatchBaseCmd(patchFilePath)
122 | }
123 | } else if (command === 'unpatch') {
124 | if (patchStatus === 'unpatched') {
125 | log('Package already unpatched.')
126 | } else {
127 | execCmd = `${getPatchBaseCmd(patchFilePath)} --reverse`
128 | }
129 | }
130 |
131 | if (execCmd) {
132 | try {
133 | const { stdout, stderr } = await exec(execCmd, {
134 | cwd: packagePath,
135 | })
136 | commandResult = 'success'
137 | } catch (e) {
138 | log('patch exec error:', e)
139 | commandResult = 'error'
140 | }
141 | }
142 |
143 | // process results
144 | let exitCode = 0
145 | let resultText
146 | if (!commandResult) {
147 | resultText = '🟡 nothing changed'
148 | } else if (commandResult === 'success') {
149 | resultText = '🟢 success'
150 | } else if (commandResult === 'error') {
151 | resultText = '🔴 error'
152 | exitCode = 1
153 | }
154 | log(`Result: ${resultText}`)
155 |
156 | process.exit(exitCode)
157 | })()
158 |
--------------------------------------------------------------------------------
/scripts/utils/index.js:
--------------------------------------------------------------------------------
1 | import { exec as execNative } from 'node:child_process'
2 | import { dirname, resolve } from 'node:path'
3 | import { fileURLToPath } from 'node:url'
4 | import { promisify } from 'node:util'
5 |
6 | const promisifiedExec = promisify(execNative)
7 |
8 | export const validPackagesNames = ['puppeteer-core', 'playwright-core']
9 |
10 | export const exec = async (...args) => {
11 | if (isDebug()) {
12 | log('[debug][exec]', args)
13 | }
14 |
15 | const execRes = await promisifiedExec(...args)
16 |
17 | if (isDebug()) {
18 | log('[debug][execRes]', execRes)
19 | }
20 |
21 | return execRes
22 | }
23 | export const log = console.log
24 |
25 | export const getPatcherPackagePath = () => {
26 | return resolve(dirname(fileURLToPath(import.meta.url)), '../..')
27 | }
28 |
29 | export const fatalError = (...args) => {
30 | console.error('❌ FATAL ERROR:', ...args)
31 | process.exit(1)
32 | }
33 |
34 | export const getPatchBaseCmd = (patchFilePath) => {
35 | return `patch --batch -p1 --input=${patchFilePath} --verbose --no-backup-if-mismatch --reject-file=- --forward --silent`
36 | }
37 |
38 | export const isDebug = () => {
39 | return !!process.env.REBROWSER_PATCHES_DEBUG
40 | }
41 |
--------------------------------------------------------------------------------