mirror of
https://github.com/electron/electron.git
synced 2026-02-19 03:14:51 -05:00
Compare commits
19 Commits
v34.0.0-be
...
v27.0.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43d0dcfc27 | ||
|
|
056c343c04 | ||
|
|
5707be53af | ||
|
|
e1465f6723 | ||
|
|
c406384e8c | ||
|
|
c5ce6de82b | ||
|
|
d89b7f0a4e | ||
|
|
dfbd4c4335 | ||
|
|
d79189056d | ||
|
|
6fd069231f | ||
|
|
56e749782e | ||
|
|
864dd4af40 | ||
|
|
3d31570f8d | ||
|
|
3411959886 | ||
|
|
b0b1f2c727 | ||
|
|
4128e9f0e0 | ||
|
|
0bd5e36411 | ||
|
|
c97a4ce691 | ||
|
|
fbb982350b |
@@ -38,7 +38,7 @@ For more installation options and troubleshooting tips, see
|
||||
|
||||
Each Electron release provides binaries for macOS, Windows, and Linux.
|
||||
|
||||
* macOS (High Sierra and up): Electron provides 64-bit Intel and ARM binaries for macOS. Apple Silicon support was added in Electron 11.
|
||||
* macOS (Catalina and up): Electron provides 64-bit Intel and ARM binaries for macOS. Apple Silicon support was added in Electron 11.
|
||||
* Windows (Windows 10 and up): Electron provides `ia32` (`x86`), `x64` (`amd64`), and `arm64` binaries for Windows. Windows on ARM support was added in Electron 5.0.8. Support for Windows 7, 8 and 8.1 was [removed in Electron 23, in line with Chromium's Windows deprecation policy](https://www.electronjs.org/blog/windows-7-to-8-1-deprecation-notice).
|
||||
* Linux: The prebuilt binaries of Electron are built on Ubuntu 20.04. They have also been verified to work on:
|
||||
* Ubuntu 14.04 and newer
|
||||
|
||||
@@ -118,7 +118,7 @@ for:
|
||||
- ps: .\src\electron\script\start-goma.ps1 -gomaDir $env:LOCAL_GOMA_DIR
|
||||
- ps: >-
|
||||
if (Test-Path 'env:RAW_GOMA_AUTH') {
|
||||
$goma_login = python $env:LOCAL_GOMA_DIR\goma_auth.py info
|
||||
$goma_login = python3 $env:LOCAL_GOMA_DIR\goma_auth.py info
|
||||
if ($goma_login -eq 'Login as Fermi Planck') {
|
||||
Write-warning "Goma authentication is correct";
|
||||
} else {
|
||||
@@ -168,7 +168,7 @@ for:
|
||||
- ninja -C out/Default electron:hunspell_dictionaries_zip
|
||||
- ninja -C out/Default electron:electron_chromedriver_zip
|
||||
- ninja -C out/Default third_party/electron_node:headers
|
||||
- python %LOCAL_GOMA_DIR%\goma_ctl.py stat
|
||||
- python3 %LOCAL_GOMA_DIR%\goma_ctl.py stat
|
||||
- ps: >-
|
||||
Get-CimInstance -Namespace root\cimv2 -Class Win32_product | Select vendor, description, @{l='install_location';e='InstallLocation'}, @{l='install_date';e='InstallDate'}, @{l='install_date_2';e='InstallDate2'}, caption, version, name, @{l='sku_number';e='SKUNumber'} | ConvertTo-Json | Out-File -Encoding utf8 -FilePath .\installed_software.json
|
||||
- python3 electron/build/profile_toolchain.py --output-json=out/Default/windows_toolchain_profile.json
|
||||
|
||||
@@ -116,7 +116,7 @@ for:
|
||||
- ps: .\src\electron\script\start-goma.ps1 -gomaDir $env:LOCAL_GOMA_DIR
|
||||
- ps: >-
|
||||
if (Test-Path 'env:RAW_GOMA_AUTH') {
|
||||
$goma_login = python $env:LOCAL_GOMA_DIR\goma_auth.py info
|
||||
$goma_login = python3 $env:LOCAL_GOMA_DIR\goma_auth.py info
|
||||
if ($goma_login -eq 'Login as Fermi Planck') {
|
||||
Write-warning "Goma authentication is correct";
|
||||
} else {
|
||||
@@ -166,7 +166,7 @@ for:
|
||||
- ninja -C out/Default electron:hunspell_dictionaries_zip
|
||||
- ninja -C out/Default electron:electron_chromedriver_zip
|
||||
- ninja -C out/Default third_party/electron_node:headers
|
||||
- python %LOCAL_GOMA_DIR%\goma_ctl.py stat
|
||||
- python3 %LOCAL_GOMA_DIR%\goma_ctl.py stat
|
||||
- ps: >-
|
||||
Get-CimInstance -Namespace root\cimv2 -Class Win32_product | Select vendor, description, @{l='install_location';e='InstallLocation'}, @{l='install_date';e='InstallDate'}, @{l='install_date_2';e='InstallDate2'}, caption, version, name, @{l='sku_number';e='SKUNumber'} | ConvertTo-Json | Out-File -Encoding utf8 -FilePath .\installed_software.json
|
||||
- python3 electron/build/profile_toolchain.py --output-json=out/Default/windows_toolchain_profile.json
|
||||
|
||||
@@ -412,18 +412,7 @@ Returns:
|
||||
|
||||
* `event` Event
|
||||
* `webContents` [WebContents](web-contents.md)
|
||||
* `details` Object
|
||||
* `reason` string - The reason the render process is gone. Possible values:
|
||||
* `clean-exit` - Process exited with an exit code of zero
|
||||
* `abnormal-exit` - Process exited with a non-zero exit code
|
||||
* `killed` - Process was sent a SIGTERM or otherwise killed externally
|
||||
* `crashed` - Process crashed
|
||||
* `oom` - Process ran out of memory
|
||||
* `launch-failed` - Process never successfully launched
|
||||
* `integrity-failure` - Windows code integrity checks failed
|
||||
* `exitCode` Integer - The exit code of the process, unless `reason` is
|
||||
`launch-failed`, in which case `exitCode` will be a platform-specific
|
||||
launch failure error code.
|
||||
* `details` [RenderProcessGoneDetails](structures/render-process-gone-details.md)
|
||||
|
||||
Emitted when the renderer process unexpectedly disappears. This is normally
|
||||
because it was crashed or killed.
|
||||
|
||||
@@ -820,10 +820,14 @@ win.setBounds({ width: 100 })
|
||||
console.log(win.getBounds())
|
||||
```
|
||||
|
||||
**Note:** On macOS, the y-coordinate value cannot be smaller than the [Tray](tray.md) height. The tray height has changed over time and depends on the operating system, but is between 20-40px. Passing a value lower than the tray height will result in a window that is flush to the tray.
|
||||
|
||||
#### `win.getBounds()`
|
||||
|
||||
Returns [`Rectangle`](structures/rectangle.md) - The `bounds` of the window as `Object`.
|
||||
|
||||
**Note:** On macOS, the y-coordinate value returned will be at minimum the [Tray](tray.md) height. For example, calling `win.setBounds({ x: 25, y: 20, width: 800, height: 600 })` with a tray height of 38 means that `win.getBounds()` will return `{ x: 25, y: 38, width: 800, height: 600 }`.
|
||||
|
||||
#### `win.getBackgroundColor()`
|
||||
|
||||
Returns `string` - Gets the background color of the window in Hex (`#RRGGBB`) format.
|
||||
|
||||
@@ -1311,7 +1311,7 @@ The API will generate a [DownloadItem](download-item.md) that can be accessed
|
||||
with the [will-download](#event-will-download) event.
|
||||
|
||||
**Note:** This does not perform any security checks that relate to a page's origin,
|
||||
unlike [`webContents.downloadURL`](web-contents.md#contentsdownloadurlurl).
|
||||
unlike [`webContents.downloadURL`](web-contents.md#contentsdownloadurlurl-options).
|
||||
|
||||
#### `ses.createInterruptedDownload(options)`
|
||||
|
||||
|
||||
13
docs/api/structures/render-process-gone-details.md
Normal file
13
docs/api/structures/render-process-gone-details.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# RenderProcessGoneDetails Object
|
||||
|
||||
* `reason` string - The reason the render process is gone. Possible values:
|
||||
* `clean-exit` - Process exited with an exit code of zero
|
||||
* `abnormal-exit` - Process exited with a non-zero exit code
|
||||
* `killed` - Process was sent a SIGTERM or otherwise killed externally
|
||||
* `crashed` - Process crashed
|
||||
* `oom` - Process ran out of memory
|
||||
* `launch-failed` - Process never successfully launched
|
||||
* `integrity-failure` - Windows code integrity checks failed
|
||||
* `exitCode` Integer - The exit code of the process, unless `reason` is
|
||||
`launch-failed`, in which case `exitCode` will be a platform-specific
|
||||
launch failure error code.
|
||||
@@ -479,18 +479,7 @@ checking `reason === 'killed'` when you switch to that event.
|
||||
Returns:
|
||||
|
||||
* `event` Event
|
||||
* `details` Object
|
||||
* `reason` string - The reason the render process is gone. Possible values:
|
||||
* `clean-exit` - Process exited with an exit code of zero
|
||||
* `abnormal-exit` - Process exited with a non-zero exit code
|
||||
* `killed` - Process was sent a SIGTERM or otherwise killed externally
|
||||
* `crashed` - Process crashed
|
||||
* `oom` - Process ran out of memory
|
||||
* `launch-failed` - Process never successfully launched
|
||||
* `integrity-failure` - Windows code integrity checks failed
|
||||
* `exitCode` Integer - The exit code of the process, unless `reason` is
|
||||
`launch-failed`, in which case `exitCode` will be a platform-specific
|
||||
launch failure error code.
|
||||
* `details` [RenderProcessGoneDetails](structures/render-process-gone-details.md)
|
||||
|
||||
Emitted when the renderer process unexpectedly disappears. This is normally
|
||||
because it was crashed or killed.
|
||||
@@ -1057,9 +1046,11 @@ const win = new BrowserWindow()
|
||||
win.loadFile('src/index.html')
|
||||
```
|
||||
|
||||
#### `contents.downloadURL(url)`
|
||||
#### `contents.downloadURL(url[, options])`
|
||||
|
||||
* `url` string
|
||||
* `options` Object (optional)
|
||||
* `headers` Record<string, string> (optional) - HTTP request headers.
|
||||
|
||||
Initiates a download of the resource at `url` without navigating. The
|
||||
`will-download` event of `session` will be triggered.
|
||||
|
||||
@@ -280,9 +280,11 @@ if the page fails to load (see
|
||||
Loads the `url` in the webview, the `url` must contain the protocol prefix,
|
||||
e.g. the `http://` or `file://`.
|
||||
|
||||
### `<webview>.downloadURL(url)`
|
||||
### `<webview>.downloadURL(url[, options])`
|
||||
|
||||
* `url` string
|
||||
* `options` Object (optional)
|
||||
* `headers` Record<string, string> (optional) - HTTP request headers.
|
||||
|
||||
Initiates a download of the resource at `url` without navigating.
|
||||
|
||||
@@ -983,9 +985,22 @@ ipcRenderer.on('ping', () => {
|
||||
})
|
||||
```
|
||||
|
||||
### Event: 'crashed'
|
||||
### Event: 'crashed' _Deprecated_
|
||||
|
||||
Fired when the renderer process is crashed.
|
||||
Fired when the renderer process crashes or is killed.
|
||||
|
||||
**Deprecated:** This event is superceded by the `render-process-gone` event
|
||||
which contains more information about why the render process disappeared. It
|
||||
isn't always because it crashed.
|
||||
|
||||
### Event: 'render-process-gone'
|
||||
|
||||
Returns:
|
||||
|
||||
* `details` [RenderProcessGoneDetails](structures/render-process-gone-details.md)
|
||||
|
||||
Fired when the renderer process unexpectedly disappears. This is normally
|
||||
because it was crashed or killed.
|
||||
|
||||
### Event: 'plugin-crashed'
|
||||
|
||||
|
||||
@@ -4,15 +4,48 @@ Electron Forge is a tool for packaging and publishing Electron applications.
|
||||
It unifies Electron's build tooling ecosystem into
|
||||
a single extensible interface so that anyone can jump right into making Electron apps.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Alternative tooling</summary>
|
||||
|
||||
If you do not want to use Electron Forge for your project, there are other
|
||||
third-party tools you can use to distribute your app.
|
||||
|
||||
These tools are maintained by members of the Electron community,
|
||||
and do not come with official support from the Electron project.
|
||||
|
||||
**Electron Builder**
|
||||
|
||||
A "complete solution to package and build a ready-for-distribution Electron app"
|
||||
that focuses on an integrated experience. [`electron-builder`](https://github.com/electron-userland/electron-builder) adds a single dependency and manages all further requirements internally.
|
||||
|
||||
`electron-builder` replaces features and modules used by the Electron
|
||||
maintainers (such as the auto-updater) with custom ones.
|
||||
|
||||
**Hydraulic Conveyor**
|
||||
|
||||
A [desktop app deployment tool](https://hydraulic.dev) that supports
|
||||
cross-building/signing of all packages from any OS without the need for
|
||||
multi-platform CI, can do synchronous web-style updates on each start
|
||||
of the app, requires no code changes, can use plain HTTP servers for updates and
|
||||
which focuses on ease of use. Conveyor replaces the Electron auto-updaters
|
||||
with Sparkle on macOS, MSIX on Windows, and Linux package repositories.
|
||||
|
||||
Conveyor is a commercial tool that is free for open source projects. There's
|
||||
an example of [how to package GitHub Desktop](https://hydraulic.dev/blog/8-packaging-electron-apps.html)
|
||||
which can be used for learning.
|
||||
|
||||
</details>
|
||||
|
||||
## Getting started
|
||||
|
||||
The [Electron Forge docs][] contain detailed information on taking your application
|
||||
from source code to your end users' machines.
|
||||
This includes:
|
||||
|
||||
* Packaging your application [(package)][]
|
||||
* Generating executables and installers for each OS [(make)][], and,
|
||||
* Publishing these files to online platforms to download [(publish)][].
|
||||
- Packaging your application [(package)][]
|
||||
- Generating executables and installers for each OS [(make)][], and,
|
||||
- Publishing these files to online platforms to download [(publish)][].
|
||||
|
||||
For beginners, we recommend following through Electron's [tutorial][] to develop, build,
|
||||
package and publish your first Electron app. If you have already developed an app on your machine
|
||||
@@ -20,11 +53,11 @@ and want to start on packaging and distribution, start from [step 5][] of the tu
|
||||
|
||||
## Getting help
|
||||
|
||||
* If you need help with developing your app, our [community Discord server][discord] is a great place
|
||||
to get advice from other Electron app developers.
|
||||
* If you suspect you're running into a bug with Forge, please check the [GitHub issue tracker][]
|
||||
to see if any existing issues match your problem. If not, feel free to fill out our bug report
|
||||
template and submit a new issue.
|
||||
- If you need help with developing your app, our [community Discord server][discord] is a great place
|
||||
to get advice from other Electron app developers.
|
||||
- If you suspect you're running into a bug with Forge, please check the [GitHub issue tracker][]
|
||||
to see if any existing issues match your problem. If not, feel free to fill out our bug report
|
||||
template and submit a new issue.
|
||||
|
||||
[Electron Forge Docs]: https://www.electronforge.io/
|
||||
[step 5]: ./tutorial-5-packaging.md
|
||||
|
||||
@@ -61,6 +61,7 @@ template("electron_extra_paks") {
|
||||
"$root_gen_dir/content/browser/tracing/tracing_resources.pak",
|
||||
"$root_gen_dir/content/browser/webrtc/resources/webrtc_internals_resources.pak",
|
||||
"$root_gen_dir/content/content_resources.pak",
|
||||
"$root_gen_dir/content/gpu_resources.pak",
|
||||
"$root_gen_dir/mojo/public/js/mojo_bindings_resources.pak",
|
||||
"$root_gen_dir/net/net_resources.pak",
|
||||
"$root_gen_dir/third_party/blink/public/resources/blink_resources.pak",
|
||||
@@ -74,6 +75,7 @@ template("electron_extra_paks") {
|
||||
"//chrome/common:resources",
|
||||
"//components/resources",
|
||||
"//content:content_resources",
|
||||
"//content/browser/resources/gpu:resources",
|
||||
"//content/browser/resources/media:resources",
|
||||
"//content/browser/tracing:resources",
|
||||
"//content/browser/webrtc/resources",
|
||||
|
||||
@@ -115,6 +115,7 @@ auto_filenames = {
|
||||
"docs/api/structures/protocol-response.md",
|
||||
"docs/api/structures/rectangle.md",
|
||||
"docs/api/structures/referrer.md",
|
||||
"docs/api/structures/render-process-gone-details.md",
|
||||
"docs/api/structures/resolved-endpoint.md",
|
||||
"docs/api/structures/resolved-host.md",
|
||||
"docs/api/structures/scrubber-item.md",
|
||||
|
||||
@@ -446,6 +446,7 @@ WebContents.prototype.loadURL = function (url, options) {
|
||||
};
|
||||
|
||||
let navigationStarted = false;
|
||||
let browserInitiatedInPageNavigation = false;
|
||||
const navigationListener = (event: Electron.Event, url: string, isSameDocument: boolean, isMainFrame: boolean) => {
|
||||
if (isMainFrame) {
|
||||
if (navigationStarted && !isSameDocument) {
|
||||
@@ -460,6 +461,7 @@ WebContents.prototype.loadURL = function (url, options) {
|
||||
// as the routing does not leave the document
|
||||
return rejectAndCleanup(-3, 'ERR_ABORTED', url);
|
||||
}
|
||||
browserInitiatedInPageNavigation = navigationStarted && isSameDocument;
|
||||
navigationStarted = true;
|
||||
}
|
||||
};
|
||||
@@ -474,17 +476,22 @@ WebContents.prototype.loadURL = function (url, options) {
|
||||
// would be more appropriate.
|
||||
rejectAndCleanup(-2, 'ERR_FAILED', url);
|
||||
};
|
||||
const finishListenerWhenUserInitiatedNavigation = () => {
|
||||
if (!browserInitiatedInPageNavigation) {
|
||||
finishListener();
|
||||
}
|
||||
};
|
||||
const removeListeners = () => {
|
||||
this.removeListener('did-finish-load', finishListener);
|
||||
this.removeListener('did-fail-load', failListener);
|
||||
this.removeListener('did-navigate-in-page', finishListener);
|
||||
this.removeListener('did-navigate-in-page', finishListenerWhenUserInitiatedNavigation);
|
||||
this.removeListener('did-start-navigation', navigationListener);
|
||||
this.removeListener('did-stop-loading', stopLoadingListener);
|
||||
this.removeListener('destroyed', stopLoadingListener);
|
||||
};
|
||||
this.on('did-finish-load', finishListener);
|
||||
this.on('did-fail-load', failListener);
|
||||
this.on('did-navigate-in-page', finishListener);
|
||||
this.on('did-navigate-in-page', finishListenerWhenUserInitiatedNavigation);
|
||||
this.on('did-start-navigation', navigationListener);
|
||||
this.on('did-stop-loading', stopLoadingListener);
|
||||
this.on('destroyed', stopLoadingListener);
|
||||
|
||||
@@ -98,7 +98,6 @@ make_gtk_getlibgtk_public.patch
|
||||
build_disable_print_content_analysis.patch
|
||||
custom_protocols_plzserviceworker.patch
|
||||
feat_filter_out_non-shareable_windows_in_the_current_application_in.patch
|
||||
fix_allow_guest_webcontents_to_enter_fullscreen.patch
|
||||
disable_freezing_flags_after_init_in_node.patch
|
||||
short-circuit_permissions_checks_in_mediastreamdevicescontroller.patch
|
||||
chore_add_electron_deps_to_gitignores.patch
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Samuel Attard <sattard@salesforce.com>
|
||||
Date: Mon, 6 Jun 2022 14:25:15 -0700
|
||||
Subject: fix: allow guest webcontents to enter fullscreen
|
||||
|
||||
This can be upstreamed, a guest webcontents can't technically become the focused webContents. This DCHECK should allow all guest webContents to request fullscreen entrance.
|
||||
|
||||
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
|
||||
index 8d5a0e6aa6ea3a0965ed6abb76368d1f8e3ea91e..36cd52f18c83adc007d6f896a4136a93f0fa1c83 100644
|
||||
--- a/content/browser/web_contents/web_contents_impl.cc
|
||||
+++ b/content/browser/web_contents/web_contents_impl.cc
|
||||
@@ -3715,7 +3715,7 @@ void WebContentsImpl::EnterFullscreenMode(
|
||||
OPTIONAL_TRACE_EVENT0("content", "WebContentsImpl::EnterFullscreenMode");
|
||||
DCHECK(CanEnterFullscreenMode(requesting_frame, options));
|
||||
DCHECK(requesting_frame->IsActive());
|
||||
- DCHECK(ContainsOrIsFocusedWebContents());
|
||||
+ DCHECK(ContainsOrIsFocusedWebContents() || IsGuest());
|
||||
|
||||
// When WebView is the `delegate_` we can end up with VisualProperties changes
|
||||
// synchronously. Notify the view ahead so it can handle the transition.
|
||||
@@ -45,10 +45,10 @@ index 1e9dcbb9f0a5e401a50d63be490755bc3eeaa1a3..1e2e0a321df6fe9dea4401ed327c9373
|
||||
// RenderFrameMetadataProvider::Observer implementation.
|
||||
void OnRenderFrameMetadataChangedBeforeActivation(
|
||||
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
|
||||
index 36cd52f18c83adc007d6f896a4136a93f0fa1c83..99f6e45fe48449c2dbe88514648a4d907999e282 100644
|
||||
index 61d985829d54e2f98351f39d1b58e5702f10fa79..6ee703fc50459901068c364a3a0ccd3a60ce28bb 100644
|
||||
--- a/content/browser/web_contents/web_contents_impl.cc
|
||||
+++ b/content/browser/web_contents/web_contents_impl.cc
|
||||
@@ -8319,7 +8319,7 @@ void WebContentsImpl::OnFocusedElementChangedInFrame(
|
||||
@@ -8323,7 +8323,7 @@ void WebContentsImpl::OnFocusedElementChangedInFrame(
|
||||
"WebContentsImpl::OnFocusedElementChangedInFrame",
|
||||
"render_frame_host", frame);
|
||||
RenderWidgetHostViewBase* root_view =
|
||||
|
||||
@@ -8,7 +8,8 @@ the parent frame should also enter fullscreen mode too. Chromium handles
|
||||
this for iframes, but not for webviews as they are essentially main
|
||||
frames instead of child frames.
|
||||
|
||||
This patch makes webviews propagate the fullscreen state to embedder.
|
||||
This patch makes webviews propagate the fullscreen state to embedder.It also handles a
|
||||
DCHECK preventing guest webcontents from becoming the focused webContents.
|
||||
|
||||
Note that we also need to manually update embedder's
|
||||
`api::WebContents::IsFullscreenForTabOrPending` value.
|
||||
@@ -35,3 +36,67 @@ index 01eb116b51037bff5da7a87d119b78387f5e72c6..7ab5609215ce352b2595b4953e1b9ca0
|
||||
// Focus the window if another frame may have delegated the capability.
|
||||
if (had_fullscreen_token && !GetView()->HasFocus())
|
||||
GetView()->Focus();
|
||||
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
|
||||
index 8d5a0e6aa6ea3a0965ed6abb76368d1f8e3ea91e..61d985829d54e2f98351f39d1b58e5702f10fa79 100644
|
||||
--- a/content/browser/web_contents/web_contents_impl.cc
|
||||
+++ b/content/browser/web_contents/web_contents_impl.cc
|
||||
@@ -3569,21 +3569,25 @@ KeyboardEventProcessingResult WebContentsImpl::PreHandleKeyboardEvent(
|
||||
const NativeWebKeyboardEvent& event) {
|
||||
OPTIONAL_TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("content.verbose"),
|
||||
"WebContentsImpl::PreHandleKeyboardEvent");
|
||||
- auto* outermost_contents = GetOutermostWebContents();
|
||||
- // TODO(wjmaclean): Generalize this to forward all key events to the outermost
|
||||
- // delegate's handler.
|
||||
- if (outermost_contents != this && IsFullscreen() &&
|
||||
- event.windows_key_code == ui::VKEY_ESCAPE) {
|
||||
- // When an inner WebContents has focus and is fullscreen, redirect <esc>
|
||||
- // key events to the outermost WebContents so it can be handled by that
|
||||
- // WebContents' delegate.
|
||||
- if (outermost_contents->PreHandleKeyboardEvent(event) ==
|
||||
- KeyboardEventProcessingResult::HANDLED) {
|
||||
+
|
||||
+ auto handled = delegate_ ? delegate_->PreHandleKeyboardEvent(this, event)
|
||||
+ : KeyboardEventProcessingResult::NOT_HANDLED;
|
||||
+
|
||||
+ if (IsFullscreen() && event.windows_key_code == ui::VKEY_ESCAPE) {
|
||||
+ if (handled == KeyboardEventProcessingResult::HANDLED)
|
||||
return KeyboardEventProcessingResult::HANDLED;
|
||||
+
|
||||
+ // When an inner WebContents has focus and is fullscreen, traverse through
|
||||
+ // containing webcontents to any that may handle the escape key.
|
||||
+ while (auto* outer_web_contents = GetOuterWebContents()) {
|
||||
+ auto result = outer_web_contents->PreHandleKeyboardEvent(event);
|
||||
+ if (result == KeyboardEventProcessingResult::HANDLED) {
|
||||
+ return KeyboardEventProcessingResult::HANDLED;
|
||||
+ }
|
||||
}
|
||||
}
|
||||
- return delegate_ ? delegate_->PreHandleKeyboardEvent(this, event)
|
||||
- : KeyboardEventProcessingResult::NOT_HANDLED;
|
||||
+
|
||||
+ return handled;
|
||||
}
|
||||
|
||||
bool WebContentsImpl::HandleMouseEvent(const blink::WebMouseEvent& event) {
|
||||
@@ -3715,7 +3719,7 @@ void WebContentsImpl::EnterFullscreenMode(
|
||||
OPTIONAL_TRACE_EVENT0("content", "WebContentsImpl::EnterFullscreenMode");
|
||||
DCHECK(CanEnterFullscreenMode(requesting_frame, options));
|
||||
DCHECK(requesting_frame->IsActive());
|
||||
- DCHECK(ContainsOrIsFocusedWebContents());
|
||||
+ DCHECK(ContainsOrIsFocusedWebContents() || IsGuest());
|
||||
|
||||
// When WebView is the `delegate_` we can end up with VisualProperties changes
|
||||
// synchronously. Notify the view ahead so it can handle the transition.
|
||||
diff --git a/third_party/blink/renderer/core/fullscreen/fullscreen.cc b/third_party/blink/renderer/core/fullscreen/fullscreen.cc
|
||||
index f58ade7ed652ea0d97c123dc78715c049cb5e500..6619207f2f8731a7d8e88d6c1613e77ca4abc558 100644
|
||||
--- a/third_party/blink/renderer/core/fullscreen/fullscreen.cc
|
||||
+++ b/third_party/blink/renderer/core/fullscreen/fullscreen.cc
|
||||
@@ -99,7 +99,7 @@ void FullscreenElementChanged(Document& document,
|
||||
// is the iframe element for the out-of-process frame that contains the
|
||||
// fullscreen element. Hence, it must match :-webkit-full-screen-ancestor.
|
||||
if (new_request_type & FullscreenRequestType::kForCrossProcessDescendant) {
|
||||
- DCHECK(IsA<HTMLIFrameElement>(new_element));
|
||||
+ // DCHECK(IsA<HTMLIFrameElement>(new_element));
|
||||
new_element->SetContainsFullScreenElement(true);
|
||||
}
|
||||
new_element->SetContainsFullScreenElementOnAncestorsCrossingFrameBoundaries(
|
||||
|
||||
@@ -41,3 +41,4 @@ fix_ftbfs_werror_wextra-semi.patch
|
||||
fix_isurl_implementation.patch
|
||||
ci_ensure_node_tests_set_electron_run_as_node.patch
|
||||
chore_update_fixtures_errors_force_colors_snapshot.patch
|
||||
fix_assert_module_in_the_renderer_process.patch
|
||||
|
||||
75
patches/node/fix_assert_module_in_the_renderer_process.patch
Normal file
75
patches/node/fix_assert_module_in_the_renderer_process.patch
Normal file
@@ -0,0 +1,75 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Shelley Vohr <shelley.vohr@gmail.com>
|
||||
Date: Wed, 16 Aug 2023 19:15:29 +0200
|
||||
Subject: fix: assert module in the renderer process
|
||||
|
||||
When creating a Node.js Environment, embedders have the option to disable Node.js'
|
||||
default overriding of Error.prepareStackTrace. However, the assert module depends on
|
||||
a WeakMap that is populated with the error stacktraces in the overridden function.
|
||||
|
||||
This adds handling to fall back to the default implementation if Error.prepareStackTrace
|
||||
if the override has been disabled.
|
||||
|
||||
This will be upstreamed.
|
||||
|
||||
diff --git a/lib/assert.js b/lib/assert.js
|
||||
index 04c2dd3bfcfdfbb4b8079c306e1d80aa48027787..34658819d09cc20f372798caec79e19c4a36565d 100644
|
||||
--- a/lib/assert.js
|
||||
+++ b/lib/assert.js
|
||||
@@ -66,6 +66,7 @@ const { inspect } = require('internal/util/inspect');
|
||||
const { isPromise, isRegExp } = require('internal/util/types');
|
||||
const { EOL } = require('internal/constants');
|
||||
const { BuiltinModule } = require('internal/bootstrap/loaders');
|
||||
+const { getEmbedderOptions } = require('internal/options');
|
||||
const { isError } = require('internal/util');
|
||||
|
||||
const errorCache = new SafeMap();
|
||||
@@ -293,8 +294,16 @@ function getErrMessage(message, fn) {
|
||||
ErrorCaptureStackTrace(err, fn);
|
||||
if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = tmpLimit;
|
||||
|
||||
- overrideStackTrace.set(err, (_, stack) => stack);
|
||||
- const call = err.stack[0];
|
||||
+ let call;
|
||||
+ if (getEmbedderOptions().hasPrepareStackTraceCallback) {
|
||||
+ overrideStackTrace.set(err, (_, stack) => stack);
|
||||
+ call = err.stack[0];
|
||||
+ } else {
|
||||
+ const tmpPrepare = Error.prepareStackTrace;
|
||||
+ Error.prepareStackTrace = (_, stack) => stack;
|
||||
+ call = err.stack[0];
|
||||
+ Error.prepareStackTrace = tmpPrepare;
|
||||
+ }
|
||||
|
||||
const filename = call.getFileName();
|
||||
const line = call.getLineNumber() - 1;
|
||||
diff --git a/src/api/environment.cc b/src/api/environment.cc
|
||||
index d5a03d5e10faaa204b3f9f290fed79be824c78b1..c4caef25af670658965fc740ce03c2d2c4ed3e66 100644
|
||||
--- a/src/api/environment.cc
|
||||
+++ b/src/api/environment.cc
|
||||
@@ -263,6 +263,9 @@ void SetIsolateErrorHandlers(v8::Isolate* isolate, const IsolateSettings& s) {
|
||||
auto* prepare_stack_trace_cb = s.prepare_stack_trace_callback ?
|
||||
s.prepare_stack_trace_callback : PrepareStackTraceCallback;
|
||||
isolate->SetPrepareStackTraceCallback(prepare_stack_trace_cb);
|
||||
+ } else {
|
||||
+ auto env = Environment::GetCurrent(isolate);
|
||||
+ env->set_prepare_stack_trace_callback(Local<Function>());
|
||||
}
|
||||
}
|
||||
|
||||
diff --git a/src/node_options.cc b/src/node_options.cc
|
||||
index 499d048fcb82e50a302d74e2c7f87f3696103a40..7ad8d80faee840e4dd224d946871b2ff08b0c23c 100644
|
||||
--- a/src/node_options.cc
|
||||
+++ b/src/node_options.cc
|
||||
@@ -1205,6 +1205,11 @@ void GetEmbedderOptions(const FunctionCallbackInfo<Value>& args) {
|
||||
Local<Context> context = env->context();
|
||||
Local<Object> ret = Object::New(isolate);
|
||||
|
||||
+ if (ret->Set(context,
|
||||
+ FIXED_ONE_BYTE_STRING(env->isolate(), "hasPrepareStackTraceCallback"),
|
||||
+ Boolean::New(isolate, !env->prepare_stack_trace_callback().IsEmpty()))
|
||||
+ .IsNothing()) return;
|
||||
+
|
||||
if (ret->Set(context,
|
||||
FIXED_ONE_BYTE_STRING(env->isolate(), "shouldNotRegisterESMLoader"),
|
||||
Boolean::New(isolate, env->should_not_register_esm_loader()))
|
||||
@@ -1,6 +1,6 @@
|
||||
param([string]$gomaDir=$PWD)
|
||||
$cmdPath = Join-Path -Path $gomaDir -ChildPath "goma_ctl.py"
|
||||
Start-Process -FilePath cmd -ArgumentList "/C", "python", "$cmdPath", "ensure_start"
|
||||
Start-Process -FilePath cmd -ArgumentList "/C", "python3", "$cmdPath", "ensure_start"
|
||||
$timedOut = $false; $waitTime = 0; $waitIncrement = 5; $timeout=120;
|
||||
Do { sleep $waitIncrement; $timedOut = (($waitTime+=$waitIncrement) -gt $timeout); iex "$gomaDir\gomacc.exe port 2" > $null; } Until(($LASTEXITCODE -eq 0) -or $timedOut)
|
||||
if ($timedOut) {
|
||||
|
||||
@@ -54,13 +54,14 @@ namespace {
|
||||
// See https://nodejs.org/api/cli.html#cli_options
|
||||
void ExitIfContainsDisallowedFlags(const std::vector<std::string>& argv) {
|
||||
// Options that are unilaterally disallowed.
|
||||
static constexpr auto disallowed = base::MakeFixedFlatSet<base::StringPiece>({
|
||||
"--enable-fips",
|
||||
"--force-fips",
|
||||
"--openssl-config",
|
||||
"--use-bundled-ca",
|
||||
"--use-openssl-ca",
|
||||
});
|
||||
static constexpr auto disallowed =
|
||||
base::MakeFixedFlatSetSorted<base::StringPiece>({
|
||||
"--enable-fips",
|
||||
"--force-fips",
|
||||
"--openssl-config",
|
||||
"--use-bundled-ca",
|
||||
"--use-openssl-ca",
|
||||
});
|
||||
|
||||
for (const auto& arg : argv) {
|
||||
const auto key = base::StringPiece(arg).substr(0, arg.find('='));
|
||||
|
||||
@@ -768,10 +768,20 @@ void BaseWindow::AddBrowserView(gin::Handle<BrowserView> browser_view) {
|
||||
browser_view->SetOwnerWindow(nullptr);
|
||||
}
|
||||
|
||||
// If the user set the BrowserView's bounds before adding it to the window,
|
||||
// we need to get those initial bounds *before* adding it to the window
|
||||
// so bounds isn't returned relative despite not being correctly positioned
|
||||
// relative to the window.
|
||||
auto bounds = browser_view->GetBounds();
|
||||
|
||||
window_->AddBrowserView(browser_view->view());
|
||||
window_->AddDraggableRegionProvider(browser_view.get());
|
||||
browser_view->SetOwnerWindow(this);
|
||||
browser_views_.emplace_back().Reset(isolate(), browser_view.ToV8());
|
||||
|
||||
// Recalibrate bounds relative to the containing window.
|
||||
if (!bounds.IsEmpty())
|
||||
browser_view->SetBounds(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,9 @@ class BrowserView : public gin::Wrappable<BrowserView>,
|
||||
BrowserView(const BrowserView&) = delete;
|
||||
BrowserView& operator=(const BrowserView&) = delete;
|
||||
|
||||
gfx::Rect GetBounds();
|
||||
void SetBounds(const gfx::Rect& bounds);
|
||||
|
||||
protected:
|
||||
BrowserView(gin::Arguments* args, const gin_helper::Dictionary& options);
|
||||
~BrowserView() override;
|
||||
@@ -78,8 +81,6 @@ class BrowserView : public gin::Wrappable<BrowserView>,
|
||||
|
||||
private:
|
||||
void SetAutoResize(AutoResizeFlags flags);
|
||||
void SetBounds(const gfx::Rect& bounds);
|
||||
gfx::Rect GetBounds();
|
||||
void SetBackgroundColor(const std::string& color_name);
|
||||
v8::Local<v8::Value> GetWebContents(v8::Isolate*);
|
||||
|
||||
|
||||
@@ -2444,12 +2444,25 @@ void WebContents::ReloadIgnoringCache() {
|
||||
/* check_for_repost */ true);
|
||||
}
|
||||
|
||||
void WebContents::DownloadURL(const GURL& url) {
|
||||
auto* browser_context = web_contents()->GetBrowserContext();
|
||||
auto* download_manager = browser_context->GetDownloadManager();
|
||||
void WebContents::DownloadURL(const GURL& url, gin::Arguments* args) {
|
||||
std::map<std::string, std::string> headers;
|
||||
gin_helper::Dictionary options;
|
||||
if (args->GetNext(&options)) {
|
||||
if (options.Has("headers") && !options.Get("headers", &headers)) {
|
||||
args->ThrowTypeError("Invalid value for headers - must be an object");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<download::DownloadUrlParameters> download_params(
|
||||
content::DownloadRequestUtils::CreateDownloadForWebContentsMainFrame(
|
||||
web_contents(), url, MISSING_TRAFFIC_ANNOTATION));
|
||||
for (const auto& [name, value] : headers) {
|
||||
download_params->add_request_header(name, value);
|
||||
}
|
||||
|
||||
auto* download_manager =
|
||||
web_contents()->GetBrowserContext()->GetDownloadManager();
|
||||
download_manager->DownloadUrl(std::move(download_params));
|
||||
}
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ class WebContents : public ExclusiveAccessContext,
|
||||
void LoadURL(const GURL& url, const gin_helper::Dictionary& options);
|
||||
void Reload();
|
||||
void ReloadIgnoringCache();
|
||||
void DownloadURL(const GURL& url);
|
||||
void DownloadURL(const GURL& url, gin::Arguments* args);
|
||||
GURL GetURL() const;
|
||||
std::u16string GetTitle() const;
|
||||
bool IsLoading() const;
|
||||
|
||||
@@ -580,7 +580,7 @@ void ElectronBrowserContext::DisplayMediaDeviceChosen(
|
||||
blink::MediaStreamDevice video_device(request.video_type, id, name);
|
||||
video_device.display_media_info = DesktopMediaIDToDisplayMediaInformation(
|
||||
nullptr, url::Origin::Create(request.security_origin),
|
||||
content::DesktopMediaID::Parse(request.requested_video_device_id));
|
||||
content::DesktopMediaID::Parse(video_device.id));
|
||||
devices.video_device = video_device;
|
||||
} else if (result_dict.Get("video", &rfh)) {
|
||||
auto* web_contents = content::WebContents::FromRenderFrameHost(rfh);
|
||||
@@ -592,7 +592,7 @@ void ElectronBrowserContext::DisplayMediaDeviceChosen(
|
||||
base::UTF16ToUTF8(web_contents->GetTitle()));
|
||||
video_device.display_media_info = DesktopMediaIDToDisplayMediaInformation(
|
||||
web_contents, url::Origin::Create(request.security_origin),
|
||||
content::DesktopMediaID::Parse(request.requested_video_device_id));
|
||||
content::DesktopMediaID::Parse(video_device.id));
|
||||
devices.video_device = video_device;
|
||||
} else {
|
||||
gin_helper::ErrorThrower(args->isolate())
|
||||
|
||||
@@ -204,11 +204,11 @@ ElectronBrowserMainParts* ElectronBrowserMainParts::self_ = nullptr;
|
||||
|
||||
ElectronBrowserMainParts::ElectronBrowserMainParts()
|
||||
: fake_browser_process_(std::make_unique<BrowserProcessImpl>()),
|
||||
browser_(std::make_unique<Browser>()),
|
||||
node_bindings_(
|
||||
NodeBindings::Create(NodeBindings::BrowserEnvironment::kBrowser)),
|
||||
electron_bindings_(
|
||||
std::make_unique<ElectronBindings>(node_bindings_->uv_loop())) {
|
||||
node_bindings_{
|
||||
NodeBindings::Create(NodeBindings::BrowserEnvironment::kBrowser)},
|
||||
electron_bindings_{
|
||||
std::make_unique<ElectronBindings>(node_bindings_->uv_loop())},
|
||||
browser_{std::make_unique<Browser>()} {
|
||||
DCHECK(!self_) << "Cannot have two ElectronBrowserMainParts";
|
||||
self_ = this;
|
||||
}
|
||||
@@ -266,26 +266,25 @@ void ElectronBrowserMainParts::PostEarlyInitialization() {
|
||||
|
||||
node_bindings_->Initialize(js_env_->isolate()->GetCurrentContext());
|
||||
// Create the global environment.
|
||||
node::Environment* env = node_bindings_->CreateEnvironment(
|
||||
node_env_ = node_bindings_->CreateEnvironment(
|
||||
js_env_->isolate()->GetCurrentContext(), js_env_->platform());
|
||||
node_env_ = std::make_unique<NodeEnvironment>(env);
|
||||
|
||||
env->set_trace_sync_io(env->options()->trace_sync_io);
|
||||
node_env_->set_trace_sync_io(node_env_->options()->trace_sync_io);
|
||||
|
||||
// We do not want to crash the main process on unhandled rejections.
|
||||
env->options()->unhandled_rejections = "warn-with-error-code";
|
||||
node_env_->options()->unhandled_rejections = "warn-with-error-code";
|
||||
|
||||
// Add Electron extended APIs.
|
||||
electron_bindings_->BindTo(js_env_->isolate(), env->process_object());
|
||||
electron_bindings_->BindTo(js_env_->isolate(), node_env_->process_object());
|
||||
|
||||
// Create explicit microtasks runner.
|
||||
js_env_->CreateMicrotasksRunner();
|
||||
|
||||
// Wrap the uv loop with global env.
|
||||
node_bindings_->set_uv_env(env);
|
||||
node_bindings_->set_uv_env(node_env_.get());
|
||||
|
||||
// Load everything.
|
||||
node_bindings_->LoadEnvironment(env);
|
||||
node_bindings_->LoadEnvironment(node_env_.get());
|
||||
|
||||
// We already initialized the feature list in PreEarlyInitialization(), but
|
||||
// the user JS script would not have had a chance to alter the command-line
|
||||
@@ -627,9 +626,9 @@ void ElectronBrowserMainParts::PostMainMessageLoopRun() {
|
||||
|
||||
// Destroy node platform after all destructors_ are executed, as they may
|
||||
// invoke Node/V8 APIs inside them.
|
||||
node_env_->env()->set_trace_sync_io(false);
|
||||
node_env_->set_trace_sync_io(false);
|
||||
js_env_->DestroyMicrotasksRunner();
|
||||
node::Stop(node_env_->env(), node::StopFlags::kDoNotTerminateIsolate);
|
||||
node::Stop(node_env_.get(), node::StopFlags::kDoNotTerminateIsolate);
|
||||
node_env_.reset();
|
||||
|
||||
auto default_context_key = ElectronBrowserContext::PartitionKey("", false);
|
||||
|
||||
@@ -37,6 +37,10 @@ class Screen;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace node {
|
||||
class Environment;
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
class LinuxUiGetter;
|
||||
}
|
||||
@@ -47,7 +51,6 @@ class Browser;
|
||||
class ElectronBindings;
|
||||
class JavascriptEnvironment;
|
||||
class NodeBindings;
|
||||
class NodeEnvironment;
|
||||
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
class ElectronExtensionsClient;
|
||||
@@ -157,11 +160,20 @@ class ElectronBrowserMainParts : public content::BrowserMainParts {
|
||||
// Before then, we just exit() without any intermediate steps.
|
||||
absl::optional<int> exit_code_;
|
||||
|
||||
std::unique_ptr<JavascriptEnvironment> js_env_;
|
||||
std::unique_ptr<Browser> browser_;
|
||||
std::unique_ptr<NodeBindings> node_bindings_;
|
||||
|
||||
// depends-on: node_bindings_
|
||||
std::unique_ptr<ElectronBindings> electron_bindings_;
|
||||
std::unique_ptr<NodeEnvironment> node_env_;
|
||||
|
||||
// depends-on: node_bindings_
|
||||
std::unique_ptr<JavascriptEnvironment> js_env_;
|
||||
|
||||
// depends-on: js_env_'s isolate
|
||||
std::shared_ptr<node::Environment> node_env_;
|
||||
|
||||
// depends-on: js_env_'s isolate
|
||||
std::unique_ptr<Browser> browser_;
|
||||
|
||||
std::unique_ptr<IconManager> icon_manager_;
|
||||
std::unique_ptr<base::FieldTrialList> field_trial_list_;
|
||||
|
||||
|
||||
@@ -301,6 +301,7 @@ ExtensionFunction::ResponseAction TabsQueryFunction::Run() {
|
||||
|
||||
tabs::Tab tab;
|
||||
tab.id = contents->ID();
|
||||
tab.title = base::UTF16ToUTF8(wc->GetTitle());
|
||||
tab.url = wc->GetLastCommittedURL().spec();
|
||||
tab.active = contents->IsFocused();
|
||||
tab.audible = contents->IsCurrentlyAudible();
|
||||
@@ -322,12 +323,18 @@ ExtensionFunction::ResponseAction TabsGetFunction::Run() {
|
||||
return RespondNow(Error("No such tab"));
|
||||
|
||||
tabs::Tab tab;
|
||||
|
||||
tab.id = tab_id;
|
||||
// TODO(nornagon): in Chrome, the tab URL is only available to extensions
|
||||
// that have the "tabs" (or "activeTab") permission. We should do the same
|
||||
// permission check here.
|
||||
tab.url = contents->web_contents()->GetLastCommittedURL().spec();
|
||||
|
||||
// "title" and "url" properties are considered privileged data and can
|
||||
// only be checked if the extension has the "tabs" permission or it has
|
||||
// access to the WebContents's origin.
|
||||
auto* wc = contents->web_contents();
|
||||
if (extension()->permissions_data()->HasAPIPermissionForTab(
|
||||
contents->ID(), mojom::APIPermissionID::kTab) ||
|
||||
extension()->permissions_data()->HasHostPermission(wc->GetURL())) {
|
||||
tab.url = wc->GetLastCommittedURL().spec();
|
||||
tab.title = base::UTF16ToUTF8(wc->GetTitle());
|
||||
}
|
||||
|
||||
tab.active = contents->IsFocused();
|
||||
|
||||
@@ -609,10 +616,16 @@ ExtensionFunction::ResponseValue TabsUpdateFunction::GetResult() {
|
||||
auto* api_web_contents = electron::api::WebContents::From(web_contents_);
|
||||
tab.id = (api_web_contents ? api_web_contents->ID() : -1);
|
||||
|
||||
// TODO(nornagon): in Chrome, the tab URL is only available to extensions
|
||||
// that have the "tabs" (or "activeTab") permission. We should do the same
|
||||
// permission check here.
|
||||
tab.url = web_contents_->GetLastCommittedURL().spec();
|
||||
// "title" and "url" properties are considered privileged data and can
|
||||
// only be checked if the extension has the "tabs" permission or it has
|
||||
// access to the WebContents's origin.
|
||||
if (extension()->permissions_data()->HasAPIPermissionForTab(
|
||||
api_web_contents->ID(), mojom::APIPermissionID::kTab) ||
|
||||
extension()->permissions_data()->HasHostPermission(
|
||||
web_contents_->GetURL())) {
|
||||
tab.url = web_contents_->GetLastCommittedURL().spec();
|
||||
tab.title = base::UTF16ToUTF8(web_contents_->GetTitle());
|
||||
}
|
||||
|
||||
if (api_web_contents)
|
||||
tab.active = api_web_contents->IsFocused();
|
||||
|
||||
@@ -104,9 +104,10 @@ gin::IsolateHolder CreateIsolateHolder(v8::Isolate* isolate) {
|
||||
|
||||
JavascriptEnvironment::JavascriptEnvironment(uv_loop_t* event_loop,
|
||||
bool setup_wasm_streaming)
|
||||
: isolate_(Initialize(event_loop, setup_wasm_streaming)),
|
||||
isolate_holder_(CreateIsolateHolder(isolate_)),
|
||||
locker_(isolate_) {
|
||||
: isolate_holder_{CreateIsolateHolder(
|
||||
Initialize(event_loop, setup_wasm_streaming))},
|
||||
isolate_{isolate_holder_.isolate()},
|
||||
locker_{isolate_} {
|
||||
isolate_->Enter();
|
||||
|
||||
v8::HandleScope scope(isolate_);
|
||||
@@ -339,12 +340,4 @@ void JavascriptEnvironment::DestroyMicrotasksRunner() {
|
||||
base::CurrentThread::Get()->RemoveTaskObserver(microtasks_runner_.get());
|
||||
}
|
||||
|
||||
NodeEnvironment::NodeEnvironment(node::Environment* env) : env_(env) {}
|
||||
|
||||
NodeEnvironment::~NodeEnvironment() {
|
||||
auto* isolate_data = env_->isolate_data();
|
||||
node::FreeEnvironment(env_);
|
||||
node::FreeIsolateData(isolate_data);
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
|
||||
@@ -43,29 +43,17 @@ class JavascriptEnvironment {
|
||||
v8::Isolate* Initialize(uv_loop_t* event_loop, bool setup_wasm_streaming);
|
||||
std::unique_ptr<node::MultiIsolatePlatform> platform_;
|
||||
|
||||
raw_ptr<v8::Isolate> isolate_;
|
||||
gin::IsolateHolder isolate_holder_;
|
||||
|
||||
// owned-by: isolate_holder_
|
||||
const raw_ptr<v8::Isolate> isolate_;
|
||||
|
||||
// depends-on: isolate_
|
||||
v8::Locker locker_;
|
||||
|
||||
std::unique_ptr<MicrotasksRunner> microtasks_runner_;
|
||||
};
|
||||
|
||||
// Manage the Node Environment automatically.
|
||||
class NodeEnvironment {
|
||||
public:
|
||||
explicit NodeEnvironment(node::Environment* env);
|
||||
~NodeEnvironment();
|
||||
|
||||
// disable copy
|
||||
NodeEnvironment(const NodeEnvironment&) = delete;
|
||||
NodeEnvironment& operator=(const NodeEnvironment&) = delete;
|
||||
|
||||
node::Environment* env() { return env_; }
|
||||
|
||||
private:
|
||||
raw_ptr<node::Environment> env_;
|
||||
};
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // ELECTRON_SHELL_BROWSER_JAVASCRIPT_ENVIRONMENT_H_
|
||||
|
||||
@@ -55,25 +55,27 @@ void NativeBrowserViewMac::SetBounds(const gfx::Rect& bounds) {
|
||||
auto* iwc_view = GetInspectableWebContentsView();
|
||||
if (!iwc_view)
|
||||
return;
|
||||
|
||||
auto* view = iwc_view->GetNativeView().GetNativeNSView();
|
||||
auto* superview = view.superview;
|
||||
const auto superview_height = superview ? superview.frame.size.height : 0;
|
||||
view.frame =
|
||||
NSMakeRect(bounds.x(), superview_height - bounds.y() - bounds.height(),
|
||||
bounds.width(), bounds.height());
|
||||
const auto superview_height =
|
||||
view.superview ? view.superview.frame.size.height : 0;
|
||||
int y_coord = superview_height - bounds.y() - bounds.height();
|
||||
|
||||
view.frame = NSMakeRect(bounds.x(), y_coord, bounds.width(), bounds.height());
|
||||
}
|
||||
|
||||
gfx::Rect NativeBrowserViewMac::GetBounds() {
|
||||
auto* iwc_view = GetInspectableWebContentsView();
|
||||
if (!iwc_view)
|
||||
return gfx::Rect();
|
||||
|
||||
NSView* view = iwc_view->GetNativeView().GetNativeNSView();
|
||||
const int superview_height =
|
||||
(view.superview) ? view.superview.frame.size.height : 0;
|
||||
return gfx::Rect(
|
||||
view.frame.origin.x,
|
||||
superview_height - view.frame.origin.y - view.frame.size.height,
|
||||
view.frame.size.width, view.frame.size.height);
|
||||
view.superview ? view.superview.frame.size.height : 0;
|
||||
int y_coord = superview_height - view.frame.origin.y - view.frame.size.height;
|
||||
|
||||
return gfx::Rect(view.frame.origin.x, y_coord, view.frame.size.width,
|
||||
view.frame.size.height);
|
||||
}
|
||||
|
||||
void NativeBrowserViewMac::SetBackgroundColor(SkColor color) {
|
||||
|
||||
@@ -834,23 +834,19 @@ void NativeWindowMac::SetResizable(bool resizable) {
|
||||
ScopedDisableResize disable_resize;
|
||||
SetStyleMask(resizable, NSWindowStyleMaskResizable);
|
||||
|
||||
bool was_fullscreenable = IsFullScreenable();
|
||||
|
||||
// Right now, resizable and fullscreenable are decoupled in
|
||||
// documentation and on Windows/Linux. Chromium disables
|
||||
// fullscreenability if resizability is false on macOS as well
|
||||
// as disabling the maximize traffic light unless the window
|
||||
// is both resizable and maximizable. To work around this, we want
|
||||
// to match behavior on other platforms by disabiliting the maximize
|
||||
// button but keeping fullscreenability enabled.
|
||||
// TODO(codebytere): refactor this once we have a better solution.
|
||||
// fullscreen collection behavior as well as the maximize traffic
|
||||
// light in SetCanResize if resizability is false on macOS unless
|
||||
// the window is both resizable and maximizable. We want consistent
|
||||
// cross-platform behavior, so if resizability is disabled we disable
|
||||
// the maximize button and ensure fullscreenability matches user setting.
|
||||
SetCanResize(resizable);
|
||||
if (!resizable) {
|
||||
SetFullScreenable(true);
|
||||
[[window_ standardWindowButton:NSWindowZoomButton] setEnabled:false];
|
||||
} else {
|
||||
SetFullScreenable(true);
|
||||
[[window_ standardWindowButton:NSWindowZoomButton]
|
||||
setEnabled:IsFullScreenable()];
|
||||
}
|
||||
SetFullScreenable(was_fullscreenable);
|
||||
[[window_ standardWindowButton:NSWindowZoomButton]
|
||||
setEnabled:resizable ? was_fullscreenable : false];
|
||||
}
|
||||
|
||||
bool NativeWindowMac::IsResizable() {
|
||||
|
||||
@@ -150,7 +150,13 @@ void ClientFrameViewLinux::Init(NativeWindowViews* window,
|
||||
}
|
||||
|
||||
gfx::Insets ClientFrameViewLinux::GetBorderDecorationInsets() const {
|
||||
return frame_provider_->GetFrameThicknessDip();
|
||||
const auto insets = frame_provider_->GetFrameThicknessDip();
|
||||
// We shouldn't draw frame decorations for the tiled edges.
|
||||
// See https://wayland.app/protocols/xdg-shell#xdg_toplevel:enum:state
|
||||
return gfx::Insets::TLBR(tiled_edges().top ? 0 : insets.top(),
|
||||
tiled_edges().left ? 0 : insets.left(),
|
||||
tiled_edges().bottom ? 0 : insets.bottom(),
|
||||
tiled_edges().right ? 0 : insets.right());
|
||||
}
|
||||
|
||||
gfx::Insets ClientFrameViewLinux::GetInputInsets() const {
|
||||
|
||||
@@ -45,7 +45,7 @@ electron::UsbChooserContext* GetChooserContext(
|
||||
// These extensions can claim the smart card USB class and automatically gain
|
||||
// permissions for devices that have an interface with this class.
|
||||
constexpr auto kSmartCardPrivilegedExtensionIds =
|
||||
base::MakeFixedFlatSet<base::StringPiece>({
|
||||
base::MakeFixedFlatSetSorted<base::StringPiece>({
|
||||
// Smart Card Connector Extension and its Beta version, see
|
||||
// crbug.com/1233881.
|
||||
"khpfeaanjngmcnplbdlpegiifgpfgdco",
|
||||
|
||||
@@ -20,5 +20,11 @@
|
||||
"extension_types": [
|
||||
"extension"
|
||||
]
|
||||
},
|
||||
"tabs": {
|
||||
"channel": "stable",
|
||||
"extension_types": [
|
||||
"extension"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,8 @@ constexpr APIPermissionInfo::InitInfo permissions_to_register[] = {
|
||||
{mojom::APIPermissionID::kPdfViewerPrivate, "pdfViewerPrivate"},
|
||||
#endif
|
||||
{mojom::APIPermissionID::kManagement, "management"},
|
||||
{mojom::APIPermissionID::kTab, "tabs",
|
||||
APIPermissionInfo::kFlagRequiresManagementUIWarning},
|
||||
};
|
||||
base::span<const APIPermissionInfo::InitInfo> GetPermissionInfos() {
|
||||
return base::make_span(permissions_to_register);
|
||||
|
||||
@@ -232,7 +232,7 @@ void ErrorMessageListener(v8::Local<v8::Message> message,
|
||||
// If node CLI inspect support is disabled, allow no debug options.
|
||||
bool IsAllowedOption(base::StringPiece option) {
|
||||
static constexpr auto debug_options =
|
||||
base::MakeFixedFlatSet<base::StringPiece>({
|
||||
base::MakeFixedFlatSetSorted<base::StringPiece>({
|
||||
"--debug",
|
||||
"--debug-brk",
|
||||
"--debug-port",
|
||||
@@ -244,13 +244,14 @@ bool IsAllowedOption(base::StringPiece option) {
|
||||
});
|
||||
|
||||
// This should be aligned with what's possible to set via the process object.
|
||||
static constexpr auto options = base::MakeFixedFlatSet<base::StringPiece>({
|
||||
"--trace-warnings",
|
||||
"--trace-deprecation",
|
||||
"--throw-deprecation",
|
||||
"--no-deprecation",
|
||||
"--dns-result-order",
|
||||
});
|
||||
static constexpr auto options =
|
||||
base::MakeFixedFlatSetSorted<base::StringPiece>({
|
||||
"--dns-result-order",
|
||||
"--no-deprecation",
|
||||
"--throw-deprecation",
|
||||
"--trace-deprecation",
|
||||
"--trace-warnings",
|
||||
});
|
||||
|
||||
if (debug_options.contains(option))
|
||||
return electron::fuses::IsNodeCliInspectEnabled();
|
||||
@@ -262,14 +263,21 @@ bool IsAllowedOption(base::StringPiece option) {
|
||||
// See https://nodejs.org/api/cli.html#cli_node_options_options
|
||||
void SetNodeOptions(base::Environment* env) {
|
||||
// Options that are unilaterally disallowed
|
||||
static constexpr auto disallowed = base::MakeFixedFlatSet<base::StringPiece>(
|
||||
{"--enable-fips", "--force-fips", "--openssl-config", "--use-bundled-ca",
|
||||
"--use-openssl-ca", "--experimental-policy"});
|
||||
static constexpr auto disallowed =
|
||||
base::MakeFixedFlatSetSorted<base::StringPiece>({
|
||||
"--enable-fips",
|
||||
"--experimental-policy",
|
||||
"--force-fips",
|
||||
"--openssl-config",
|
||||
"--use-bundled-ca",
|
||||
"--use-openssl-ca",
|
||||
});
|
||||
|
||||
static constexpr auto pkg_opts = base::MakeFixedFlatSet<base::StringPiece>({
|
||||
"--http-parser",
|
||||
"--max-http-header-size",
|
||||
});
|
||||
static constexpr auto pkg_opts =
|
||||
base::MakeFixedFlatSetSorted<base::StringPiece>({
|
||||
"--http-parser",
|
||||
"--max-http-header-size",
|
||||
});
|
||||
|
||||
if (env->HasVar("NODE_OPTIONS")) {
|
||||
if (electron::fuses::IsNodeOptionsEnabled()) {
|
||||
@@ -469,7 +477,7 @@ void NodeBindings::Initialize(v8::Local<v8::Context> context) {
|
||||
g_is_initialized = true;
|
||||
}
|
||||
|
||||
node::Environment* NodeBindings::CreateEnvironment(
|
||||
std::shared_ptr<node::Environment> NodeBindings::CreateEnvironment(
|
||||
v8::Handle<v8::Context> context,
|
||||
node::MultiIsolatePlatform* platform,
|
||||
std::vector<std::string> args,
|
||||
@@ -636,10 +644,25 @@ node::Environment* NodeBindings::CreateEnvironment(
|
||||
base::PathService::Get(content::CHILD_PROCESS_EXE, &helper_exec_path);
|
||||
process.Set("helperExecPath", helper_exec_path);
|
||||
|
||||
return env;
|
||||
auto env_deleter = [isolate, isolate_data,
|
||||
context = v8::Global<v8::Context>{isolate, context}](
|
||||
node::Environment* nenv) mutable {
|
||||
// When `isolate_data` was created above, a pointer to it was kept
|
||||
// in context's embedder_data[kElectronContextEmbedderDataIndex].
|
||||
// Since we're about to free `isolate_data`, clear that entry
|
||||
v8::HandleScope handle_scope{isolate};
|
||||
context.Get(isolate)->SetAlignedPointerInEmbedderData(
|
||||
kElectronContextEmbedderDataIndex, nullptr);
|
||||
context.Reset();
|
||||
|
||||
node::FreeEnvironment(nenv);
|
||||
node::FreeIsolateData(isolate_data);
|
||||
};
|
||||
|
||||
return {env, std::move(env_deleter)};
|
||||
}
|
||||
|
||||
node::Environment* NodeBindings::CreateEnvironment(
|
||||
std::shared_ptr<node::Environment> NodeBindings::CreateEnvironment(
|
||||
v8::Handle<v8::Context> context,
|
||||
node::MultiIsolatePlatform* platform) {
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#ifndef ELECTRON_SHELL_COMMON_NODE_BINDINGS_H_
|
||||
#define ELECTRON_SHELL_COMMON_NODE_BINDINGS_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
@@ -25,12 +26,6 @@ class SingleThreadTaskRunner;
|
||||
|
||||
namespace electron {
|
||||
|
||||
// Choose a reasonable unique index that's higher than any Blink uses
|
||||
// and thus unlikely to collide with an existing index.
|
||||
static constexpr int kElectronContextEmbedderDataIndex =
|
||||
static_cast<int>(gin::kPerContextDataStartIndex) +
|
||||
static_cast<int>(gin::kEmbedderElectron);
|
||||
|
||||
// A helper class to manage uv_handle_t types, e.g. uv_async_t.
|
||||
//
|
||||
// As per the uv docs: "uv_close() MUST be called on each handle before
|
||||
@@ -95,12 +90,15 @@ class NodeBindings {
|
||||
std::vector<std::string> ParseNodeCliFlags();
|
||||
|
||||
// Create the environment and load node.js.
|
||||
node::Environment* CreateEnvironment(v8::Handle<v8::Context> context,
|
||||
node::MultiIsolatePlatform* platform,
|
||||
std::vector<std::string> args,
|
||||
std::vector<std::string> exec_args);
|
||||
node::Environment* CreateEnvironment(v8::Handle<v8::Context> context,
|
||||
node::MultiIsolatePlatform* platform);
|
||||
std::shared_ptr<node::Environment> CreateEnvironment(
|
||||
v8::Handle<v8::Context> context,
|
||||
node::MultiIsolatePlatform* platform,
|
||||
std::vector<std::string> args,
|
||||
std::vector<std::string> exec_args);
|
||||
|
||||
std::shared_ptr<node::Environment> CreateEnvironment(
|
||||
v8::Handle<v8::Context> context,
|
||||
node::MultiIsolatePlatform* platform);
|
||||
|
||||
// Load node.js in the environment.
|
||||
void LoadEnvironment(node::Environment* env);
|
||||
@@ -111,12 +109,6 @@ class NodeBindings {
|
||||
// Notify embed thread to start polling after environment is loaded.
|
||||
void StartPolling();
|
||||
|
||||
// Clears the PerIsolateData.
|
||||
void clear_isolate_data(v8::Local<v8::Context> context) {
|
||||
context->SetAlignedPointerInEmbedderData(kElectronContextEmbedderDataIndex,
|
||||
nullptr);
|
||||
}
|
||||
|
||||
node::IsolateData* isolate_data(v8::Local<v8::Context> context) const {
|
||||
if (context->GetNumberOfEmbedderDataFields() <=
|
||||
kElectronContextEmbedderDataIndex) {
|
||||
@@ -167,6 +159,12 @@ class NodeBindings {
|
||||
raw_ptr<uv_loop_t> uv_loop_;
|
||||
|
||||
private:
|
||||
// Choose a reasonable unique index that's higher than any Blink uses
|
||||
// and thus unlikely to collide with an existing index.
|
||||
static constexpr int kElectronContextEmbedderDataIndex =
|
||||
static_cast<int>(gin::kPerContextDataStartIndex) +
|
||||
static_cast<int>(gin::kEmbedderElectron);
|
||||
|
||||
// Thread to poll uv events.
|
||||
static void EmbedThreadRunner(void* arg);
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ void ElectronRendererClient::DidCreateScriptContext(
|
||||
v8::Maybe<bool> initialized = node::InitializeContext(renderer_context);
|
||||
CHECK(!initialized.IsNothing() && initialized.FromJust());
|
||||
|
||||
node::Environment* env =
|
||||
std::shared_ptr<node::Environment> env =
|
||||
node_bindings_->CreateEnvironment(renderer_context, nullptr);
|
||||
|
||||
// If we have disabled the site instance overrides we should prevent loading
|
||||
@@ -106,11 +106,11 @@ void ElectronRendererClient::DidCreateScriptContext(
|
||||
BindProcess(env->isolate(), &process_dict, render_frame);
|
||||
|
||||
// Load everything.
|
||||
node_bindings_->LoadEnvironment(env);
|
||||
node_bindings_->LoadEnvironment(env.get());
|
||||
|
||||
if (node_bindings_->uv_env() == nullptr) {
|
||||
// Make uv loop being wrapped by window context.
|
||||
node_bindings_->set_uv_env(env);
|
||||
node_bindings_->set_uv_env(env.get());
|
||||
|
||||
// Give the node loop a run to make sure everything is ready.
|
||||
node_bindings_->StartPolling();
|
||||
@@ -124,7 +124,9 @@ void ElectronRendererClient::WillReleaseScriptContext(
|
||||
return;
|
||||
|
||||
node::Environment* env = node::Environment::GetCurrent(context);
|
||||
if (environments_.erase(env) == 0)
|
||||
const auto iter = base::ranges::find_if(
|
||||
environments_, [env](auto& item) { return env == item.get(); });
|
||||
if (iter == environments_.end())
|
||||
return;
|
||||
|
||||
gin_helper::EmitEvent(env->isolate(), env->process_object(), "exit");
|
||||
@@ -143,9 +145,7 @@ void ElectronRendererClient::WillReleaseScriptContext(
|
||||
DCHECK_EQ(microtask_queue->GetMicrotasksScopeDepth(), 0);
|
||||
microtask_queue->set_microtasks_policy(v8::MicrotasksPolicy::kExplicit);
|
||||
|
||||
node::FreeEnvironment(env);
|
||||
node::FreeIsolateData(node_bindings_->isolate_data(context));
|
||||
node_bindings_->clear_isolate_data(context);
|
||||
environments_.erase(iter);
|
||||
|
||||
microtask_queue->set_microtasks_policy(old_policy);
|
||||
|
||||
@@ -201,7 +201,11 @@ node::Environment* ElectronRendererClient::GetEnvironment(
|
||||
auto context =
|
||||
GetContext(render_frame->GetWebFrame(), v8::Isolate::GetCurrent());
|
||||
node::Environment* env = node::Environment::GetCurrent(context);
|
||||
return base::Contains(environments_, env) ? env : nullptr;
|
||||
|
||||
return base::Contains(environments_, env,
|
||||
[](auto const& item) { return item.get(); })
|
||||
? env
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
|
||||
@@ -55,7 +55,7 @@ class ElectronRendererClient : public RendererClientBase {
|
||||
// The node::Environment::GetCurrent API does not return nullptr when it
|
||||
// is called for a context without node::Environment, so we have to keep
|
||||
// a book of the environments created.
|
||||
std::set<node::Environment*> environments_;
|
||||
std::set<std::shared_ptr<node::Environment>> environments_;
|
||||
|
||||
// Getting main script context from web frame would lazily initializes
|
||||
// its script context. Doing so in a web page without scripts would trigger
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "base/containers/cxx20_erase_set.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "base/ranges/algorithm.h"
|
||||
#include "base/threading/thread_local.h"
|
||||
#include "shell/common/api/electron_bindings.h"
|
||||
#include "shell/common/gin_helper/event_emitter_caller.h"
|
||||
@@ -61,20 +63,23 @@ void WebWorkerObserver::WorkerScriptReadyForEvaluation(
|
||||
// Setup node environment for each window.
|
||||
v8::Maybe<bool> initialized = node::InitializeContext(worker_context);
|
||||
CHECK(!initialized.IsNothing() && initialized.FromJust());
|
||||
node::Environment* env =
|
||||
std::shared_ptr<node::Environment> env =
|
||||
node_bindings_->CreateEnvironment(worker_context, nullptr);
|
||||
|
||||
// Add Electron extended APIs.
|
||||
electron_bindings_->BindTo(env->isolate(), env->process_object());
|
||||
|
||||
// Load everything.
|
||||
node_bindings_->LoadEnvironment(env);
|
||||
node_bindings_->LoadEnvironment(env.get());
|
||||
|
||||
// Make uv loop being wrapped by window context.
|
||||
node_bindings_->set_uv_env(env);
|
||||
node_bindings_->set_uv_env(env.get());
|
||||
|
||||
// Give the node loop a run to make sure everything is ready.
|
||||
node_bindings_->StartPolling();
|
||||
|
||||
// Keep the environment alive until we free it in ContextWillDestroy()
|
||||
environments_.insert(std::move(env));
|
||||
}
|
||||
|
||||
void WebWorkerObserver::ContextWillDestroy(v8::Local<v8::Context> context) {
|
||||
@@ -91,9 +96,8 @@ void WebWorkerObserver::ContextWillDestroy(v8::Local<v8::Context> context) {
|
||||
DCHECK_EQ(microtask_queue->GetMicrotasksScopeDepth(), 0);
|
||||
microtask_queue->set_microtasks_policy(v8::MicrotasksPolicy::kExplicit);
|
||||
|
||||
node::FreeEnvironment(env);
|
||||
node::FreeIsolateData(node_bindings_->isolate_data(context));
|
||||
node_bindings_->clear_isolate_data(context);
|
||||
base::EraseIf(environments_,
|
||||
[env](auto const& item) { return item.get() == env; });
|
||||
|
||||
microtask_queue->set_microtasks_policy(old_policy);
|
||||
|
||||
|
||||
@@ -6,9 +6,16 @@
|
||||
#define ELECTRON_SHELL_RENDERER_WEB_WORKER_OBSERVER_H_
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#include "v8/include/v8.h"
|
||||
|
||||
namespace node {
|
||||
|
||||
class Environment;
|
||||
|
||||
} // namespace node
|
||||
|
||||
namespace electron {
|
||||
|
||||
class ElectronBindings;
|
||||
@@ -35,6 +42,7 @@ class WebWorkerObserver {
|
||||
private:
|
||||
std::unique_ptr<NodeBindings> node_bindings_;
|
||||
std::unique_ptr<ElectronBindings> electron_bindings_;
|
||||
std::set<std::shared_ptr<node::Environment>> environments_;
|
||||
};
|
||||
|
||||
} // namespace electron
|
||||
|
||||
@@ -30,9 +30,9 @@ NodeService::NodeService(
|
||||
|
||||
NodeService::~NodeService() {
|
||||
if (!node_env_stopped_) {
|
||||
node_env_->env()->set_trace_sync_io(false);
|
||||
node_env_->set_trace_sync_io(false);
|
||||
js_env_->DestroyMicrotasksRunner();
|
||||
node::Stop(node_env_->env(), node::StopFlags::kDoNotTerminateIsolate);
|
||||
node::Stop(node_env_.get(), node::StopFlags::kDoNotTerminateIsolate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,13 +57,12 @@ void NodeService::Initialize(node::mojom::NodeServiceParamsPtr params) {
|
||||
#endif
|
||||
|
||||
// Create the global environment.
|
||||
node::Environment* env = node_bindings_->CreateEnvironment(
|
||||
node_env_ = node_bindings_->CreateEnvironment(
|
||||
js_env_->isolate()->GetCurrentContext(), js_env_->platform(),
|
||||
params->args, params->exec_args);
|
||||
node_env_ = std::make_unique<NodeEnvironment>(env);
|
||||
|
||||
node::SetProcessExitHandler(
|
||||
env, [this](node::Environment* env, int exit_code) {
|
||||
node_env_.get(), [this](node::Environment* env, int exit_code) {
|
||||
// Destroy node platform.
|
||||
env->set_trace_sync_io(false);
|
||||
js_env_->DestroyMicrotasksRunner();
|
||||
@@ -72,20 +71,21 @@ void NodeService::Initialize(node::mojom::NodeServiceParamsPtr params) {
|
||||
receiver_.ResetWithReason(exit_code, "");
|
||||
});
|
||||
|
||||
env->set_trace_sync_io(env->options()->trace_sync_io);
|
||||
node_env_->set_trace_sync_io(node_env_->options()->trace_sync_io);
|
||||
|
||||
// Add Electron extended APIs.
|
||||
electron_bindings_->BindTo(env->isolate(), env->process_object());
|
||||
electron_bindings_->BindTo(node_env_->isolate(), node_env_->process_object());
|
||||
|
||||
// Add entry script to process object.
|
||||
gin_helper::Dictionary process(env->isolate(), env->process_object());
|
||||
gin_helper::Dictionary process(node_env_->isolate(),
|
||||
node_env_->process_object());
|
||||
process.SetHidden("_serviceStartupScript", params->script);
|
||||
|
||||
// Setup microtask runner.
|
||||
js_env_->CreateMicrotasksRunner();
|
||||
|
||||
// Wrap the uv loop with global env.
|
||||
node_bindings_->set_uv_env(env);
|
||||
node_bindings_->set_uv_env(node_env_.get());
|
||||
|
||||
// LoadEnvironment should be called after setting up
|
||||
// JavaScriptEnvironment including the microtask runner
|
||||
@@ -94,7 +94,7 @@ void NodeService::Initialize(node::mojom::NodeServiceParamsPtr params) {
|
||||
// the exit handler set above will be triggered and it expects
|
||||
// both Node Env and JavaScriptEnviroment are setup to perform
|
||||
// a clean shutdown of this process.
|
||||
node_bindings_->LoadEnvironment(env);
|
||||
node_bindings_->LoadEnvironment(node_env_.get());
|
||||
|
||||
// Run entry script.
|
||||
node_bindings_->PrepareEmbedThread();
|
||||
|
||||
@@ -11,12 +11,17 @@
|
||||
#include "mojo/public/cpp/bindings/receiver.h"
|
||||
#include "shell/services/node/public/mojom/node_service.mojom.h"
|
||||
|
||||
namespace node {
|
||||
|
||||
class Environment;
|
||||
|
||||
} // namespace node
|
||||
|
||||
namespace electron {
|
||||
|
||||
class ElectronBindings;
|
||||
class JavascriptEnvironment;
|
||||
class NodeBindings;
|
||||
class NodeEnvironment;
|
||||
|
||||
class NodeService : public node::mojom::NodeService {
|
||||
public:
|
||||
@@ -35,7 +40,7 @@ class NodeService : public node::mojom::NodeService {
|
||||
std::unique_ptr<JavascriptEnvironment> js_env_;
|
||||
std::unique_ptr<NodeBindings> node_bindings_;
|
||||
std::unique_ptr<ElectronBindings> electron_bindings_;
|
||||
std::unique_ptr<NodeEnvironment> node_env_;
|
||||
std::shared_ptr<node::Environment> node_env_;
|
||||
mojo::Receiver<node::mojom::NodeService> receiver_{this};
|
||||
};
|
||||
|
||||
|
||||
@@ -147,6 +147,41 @@ describe('BrowserView module', () => {
|
||||
view.setBounds({} as any);
|
||||
}).to.throw(/conversion failure/);
|
||||
});
|
||||
|
||||
it('can set bounds after view is added to window', () => {
|
||||
view = new BrowserView();
|
||||
|
||||
const bounds = { x: 0, y: 0, width: 50, height: 50 };
|
||||
|
||||
w.addBrowserView(view);
|
||||
view.setBounds(bounds);
|
||||
|
||||
expect(view.getBounds()).to.deep.equal(bounds);
|
||||
});
|
||||
|
||||
it('can set bounds before view is added to window', () => {
|
||||
view = new BrowserView();
|
||||
|
||||
const bounds = { x: 0, y: 0, width: 50, height: 50 };
|
||||
|
||||
view.setBounds(bounds);
|
||||
w.addBrowserView(view);
|
||||
|
||||
expect(view.getBounds()).to.deep.equal(bounds);
|
||||
});
|
||||
|
||||
it('can update bounds', () => {
|
||||
view = new BrowserView();
|
||||
w.addBrowserView(view);
|
||||
|
||||
const bounds1 = { x: 0, y: 0, width: 50, height: 50 };
|
||||
view.setBounds(bounds1);
|
||||
expect(view.getBounds()).to.deep.equal(bounds1);
|
||||
|
||||
const bounds2 = { x: 0, y: 150, width: 50, height: 50 };
|
||||
view.setBounds(bounds2);
|
||||
expect(view.getBounds()).to.deep.equal(bounds2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BrowserView.getBounds()', () => {
|
||||
@@ -156,6 +191,16 @@ describe('BrowserView module', () => {
|
||||
view.setBounds(bounds);
|
||||
expect(view.getBounds()).to.deep.equal(bounds);
|
||||
});
|
||||
|
||||
it('does not changer after being added to a window', () => {
|
||||
view = new BrowserView();
|
||||
const bounds = { x: 10, y: 20, width: 30, height: 40 };
|
||||
view.setBounds(bounds);
|
||||
expect(view.getBounds()).to.deep.equal(bounds);
|
||||
|
||||
w.addBrowserView(view);
|
||||
expect(view.getBounds()).to.deep.equal(bounds);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BrowserWindow.setBrowserView()', () => {
|
||||
|
||||
@@ -5456,6 +5456,42 @@ describe('BrowserWindow module', () => {
|
||||
expect(w.isFullScreenable()).to.be.true('isFullScreenable');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not open non-fullscreenable child windows in fullscreen if parent is fullscreen', async () => {
|
||||
const w = new BrowserWindow();
|
||||
|
||||
const enterFS = once(w, 'enter-full-screen');
|
||||
w.setFullScreen(true);
|
||||
await enterFS;
|
||||
|
||||
const child = new BrowserWindow({ parent: w, resizable: false, fullscreenable: false });
|
||||
const shown = once(child, 'show');
|
||||
await shown;
|
||||
|
||||
expect(child.resizable).to.be.false('resizable');
|
||||
expect(child.fullScreen).to.be.false('fullscreen');
|
||||
expect(child.fullScreenable).to.be.false('fullscreenable');
|
||||
});
|
||||
|
||||
it('is set correctly with different resizable values', async () => {
|
||||
const w1 = new BrowserWindow({
|
||||
resizable: false,
|
||||
fullscreenable: false
|
||||
});
|
||||
|
||||
const w2 = new BrowserWindow({
|
||||
resizable: true,
|
||||
fullscreenable: false
|
||||
});
|
||||
|
||||
const w3 = new BrowserWindow({
|
||||
fullscreenable: false
|
||||
});
|
||||
|
||||
expect(w1.isFullScreenable()).to.be.false('isFullScreenable');
|
||||
expect(w2.isFullScreenable()).to.be.false('isFullScreenable');
|
||||
expect(w3.isFullScreenable()).to.be.false('isFullScreenable');
|
||||
});
|
||||
});
|
||||
|
||||
ifdescribe(process.platform === 'darwin')('isHiddenInMissionControl state', () => {
|
||||
|
||||
@@ -283,6 +283,29 @@ describe('setDisplayMediaRequestHandler', () => {
|
||||
expect(ok).to.be.true(message);
|
||||
});
|
||||
|
||||
it('returns a MediaStream with BrowserCaptureMediaStreamTrack when the current tab is selected', async () => {
|
||||
const ses = session.fromPartition('' + Math.random());
|
||||
let requestHandlerCalled = false;
|
||||
ses.setDisplayMediaRequestHandler((request, callback) => {
|
||||
requestHandlerCalled = true;
|
||||
callback({ video: w.webContents.mainFrame });
|
||||
});
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
|
||||
await w.loadURL(serverUrl);
|
||||
const { ok, message } = await w.webContents.executeJavaScript(`
|
||||
navigator.mediaDevices.getDisplayMedia({
|
||||
preferCurrentTab: true,
|
||||
video: true,
|
||||
audio: false,
|
||||
}).then(stream => {
|
||||
const [videoTrack] = stream.getVideoTracks();
|
||||
return { ok: videoTrack instanceof BrowserCaptureMediaStreamTrack, message: null };
|
||||
}, e => ({ok: false, message: e.message}))
|
||||
`, true);
|
||||
expect(requestHandlerCalled).to.be.true();
|
||||
expect(ok).to.be.true(message);
|
||||
});
|
||||
|
||||
ifit(!(process.platform === 'darwin' && process.arch === 'x64'))('can supply a screen response to preferCurrentTab', async () => {
|
||||
const ses = session.fromPartition('' + Math.random());
|
||||
let requestHandlerCalled = false;
|
||||
|
||||
@@ -827,277 +827,383 @@ describe('session module', () => {
|
||||
fs.unlinkSync(downloadFilePath);
|
||||
};
|
||||
|
||||
it('can download using session.downloadURL', (done) => {
|
||||
session.defaultSession.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
assertDownload(state, item);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
session.defaultSession.downloadURL(`${url}:${port}`);
|
||||
});
|
||||
|
||||
it('can download using session.downloadURL with a valid auth header', async () => {
|
||||
const server = http.createServer((req, res) => {
|
||||
const { authorization } = req.headers;
|
||||
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
|
||||
res.statusCode = 401;
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {
|
||||
'Content-Length': mockPDF.length,
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
|
||||
});
|
||||
res.end(mockPDF);
|
||||
}
|
||||
});
|
||||
|
||||
const { port } = await listen(server);
|
||||
|
||||
const downloadDone: Promise<Electron.DownloadItem> = new Promise((resolve) => {
|
||||
session.defaultSession.once('will-download', (e, item) => {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', () => {
|
||||
try {
|
||||
resolve(item);
|
||||
} catch {}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
session.defaultSession.downloadURL(`${url}:${port}`, {
|
||||
headers: {
|
||||
Authorization: 'Basic i-am-an-auth-header'
|
||||
}
|
||||
});
|
||||
|
||||
const item = await downloadDone;
|
||||
expect(item.getState()).to.equal('completed');
|
||||
expect(item.getFilename()).to.equal('mock.pdf');
|
||||
expect(item.getMimeType()).to.equal('application/pdf');
|
||||
expect(item.getReceivedBytes()).to.equal(mockPDF.length);
|
||||
expect(item.getTotalBytes()).to.equal(mockPDF.length);
|
||||
expect(item.getContentDisposition()).to.equal(contentDisposition);
|
||||
});
|
||||
|
||||
it('throws when session.downloadURL is called with invalid headers', () => {
|
||||
expect(() => {
|
||||
session.defaultSession.downloadURL(`${url}:${port}`, {
|
||||
// @ts-ignore this line is intentionally incorrect
|
||||
headers: 'i-am-a-bad-header'
|
||||
});
|
||||
}).to.throw(/Invalid value for headers - must be an object/);
|
||||
});
|
||||
|
||||
it('can download using session.downloadURL with an invalid auth header', async () => {
|
||||
const server = http.createServer((req, res) => {
|
||||
const { authorization } = req.headers;
|
||||
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
|
||||
res.statusCode = 401;
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {
|
||||
'Content-Length': mockPDF.length,
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
|
||||
});
|
||||
res.end(mockPDF);
|
||||
}
|
||||
});
|
||||
|
||||
const { port } = await listen(server);
|
||||
|
||||
const downloadFailed: Promise<Electron.DownloadItem> = new Promise((resolve) => {
|
||||
session.defaultSession.once('will-download', (_, item) => {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', (e, state) => {
|
||||
console.log(state);
|
||||
try {
|
||||
resolve(item);
|
||||
} catch {}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
session.defaultSession.downloadURL(`${url}:${port}`, {
|
||||
headers: {
|
||||
Authorization: 'wtf-is-this'
|
||||
}
|
||||
});
|
||||
|
||||
const item = await downloadFailed;
|
||||
expect(item.getState()).to.equal('interrupted');
|
||||
expect(item.getReceivedBytes()).to.equal(0);
|
||||
expect(item.getTotalBytes()).to.equal(0);
|
||||
});
|
||||
|
||||
it('can download using WebContents.downloadURL', (done) => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
assertDownload(state, item);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
w.webContents.downloadURL(`${url}:${port}`);
|
||||
});
|
||||
|
||||
it('can download from custom protocols using WebContents.downloadURL', (done) => {
|
||||
const protocol = session.defaultSession.protocol;
|
||||
const handler = (ignoredError: any, callback: Function) => {
|
||||
callback({ url: `${url}:${port}` });
|
||||
};
|
||||
protocol.registerHttpProtocol(protocolName, handler);
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
assertDownload(state, item, true);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
w.webContents.downloadURL(`${protocolName}://item`);
|
||||
});
|
||||
|
||||
it('can download using WebView.downloadURL', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } });
|
||||
await w.loadURL('about:blank');
|
||||
function webviewDownload ({ fixtures, url, port }: {fixtures: string, url: string, port: string}) {
|
||||
const webview = new (window as any).WebView();
|
||||
webview.addEventListener('did-finish-load', () => {
|
||||
webview.downloadURL(`${url}:${port}/`);
|
||||
});
|
||||
webview.src = `file://${fixtures}/api/blank.html`;
|
||||
document.body.appendChild(webview);
|
||||
}
|
||||
const done: Promise<[string, Electron.DownloadItem]> = new Promise(resolve => {
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
describe('session.downloadURL', () => {
|
||||
it('can perform a download', (done) => {
|
||||
session.defaultSession.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
resolve([state, item]);
|
||||
});
|
||||
});
|
||||
});
|
||||
await w.webContents.executeJavaScript(`(${webviewDownload})(${JSON.stringify({ fixtures, url, port })})`);
|
||||
const [state, item] = await done;
|
||||
assertDownload(state, item);
|
||||
});
|
||||
|
||||
it('can cancel download', (done) => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
expect(state).to.equal('cancelled');
|
||||
expect(item.getFilename()).to.equal('mock.pdf');
|
||||
expect(item.getMimeType()).to.equal('application/pdf');
|
||||
expect(item.getReceivedBytes()).to.equal(0);
|
||||
expect(item.getTotalBytes()).to.equal(mockPDF.length);
|
||||
expect(item.getContentDisposition()).to.equal(contentDisposition);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
item.cancel();
|
||||
});
|
||||
w.webContents.downloadURL(`${url}:${port}/`);
|
||||
});
|
||||
|
||||
it('can generate a default filename', function (done) {
|
||||
if (process.env.APPVEYOR === 'True') {
|
||||
// FIXME(alexeykuzmin): Skip the test.
|
||||
// this.skip()
|
||||
return done();
|
||||
}
|
||||
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function () {
|
||||
try {
|
||||
expect(item.getFilename()).to.equal('download.pdf');
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
item.cancel();
|
||||
});
|
||||
w.webContents.downloadURL(`${url}:${port}/?testFilename`);
|
||||
});
|
||||
|
||||
it('can set options for the save dialog', (done) => {
|
||||
const filePath = path.join(__dirname, 'fixtures', 'mock.pdf');
|
||||
const options = {
|
||||
window: null,
|
||||
title: 'title',
|
||||
message: 'message',
|
||||
buttonLabel: 'buttonLabel',
|
||||
nameFieldLabel: 'nameFieldLabel',
|
||||
defaultPath: '/',
|
||||
filters: [{
|
||||
name: '1', extensions: ['.1', '.2']
|
||||
}, {
|
||||
name: '2', extensions: ['.3', '.4', '.5']
|
||||
}],
|
||||
showsTagField: true,
|
||||
securityScopedBookmarks: true
|
||||
};
|
||||
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.setSavePath(filePath);
|
||||
item.setSaveDialogOptions(options);
|
||||
item.on('done', function () {
|
||||
try {
|
||||
expect(item.getSaveDialogOptions()).to.deep.equal(options);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
item.cancel();
|
||||
});
|
||||
w.webContents.downloadURL(`${url}:${port}`);
|
||||
});
|
||||
|
||||
describe('when a save path is specified and the URL is unavailable', () => {
|
||||
it('does not display a save dialog and reports the done state as interrupted', (done) => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
if (item.getState() === 'interrupted') {
|
||||
item.resume();
|
||||
}
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
expect(state).to.equal('interrupted');
|
||||
assertDownload(state, item);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
w.webContents.downloadURL(`file://${path.join(__dirname, 'does-not-exist.txt')}`);
|
||||
session.defaultSession.downloadURL(`${url}:${port}`);
|
||||
});
|
||||
|
||||
it('can perform a download with a valid auth header', async () => {
|
||||
const server = http.createServer((req, res) => {
|
||||
const { authorization } = req.headers;
|
||||
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
|
||||
res.statusCode = 401;
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {
|
||||
'Content-Length': mockPDF.length,
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
|
||||
});
|
||||
res.end(mockPDF);
|
||||
}
|
||||
});
|
||||
|
||||
const { port } = await listen(server);
|
||||
|
||||
const downloadDone: Promise<Electron.DownloadItem> = new Promise((resolve) => {
|
||||
session.defaultSession.once('will-download', (e, item) => {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', () => {
|
||||
try {
|
||||
resolve(item);
|
||||
} catch { }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
session.defaultSession.downloadURL(`${url}:${port}`, {
|
||||
headers: {
|
||||
Authorization: 'Basic i-am-an-auth-header'
|
||||
}
|
||||
});
|
||||
|
||||
const item = await downloadDone;
|
||||
expect(item.getState()).to.equal('completed');
|
||||
expect(item.getFilename()).to.equal('mock.pdf');
|
||||
expect(item.getMimeType()).to.equal('application/pdf');
|
||||
expect(item.getReceivedBytes()).to.equal(mockPDF.length);
|
||||
expect(item.getTotalBytes()).to.equal(mockPDF.length);
|
||||
expect(item.getContentDisposition()).to.equal(contentDisposition);
|
||||
});
|
||||
|
||||
it('throws when called with invalid headers', () => {
|
||||
expect(() => {
|
||||
session.defaultSession.downloadURL(`${url}:${port}`, {
|
||||
// @ts-ignore this line is intentionally incorrect
|
||||
headers: 'i-am-a-bad-header'
|
||||
});
|
||||
}).to.throw(/Invalid value for headers - must be an object/);
|
||||
});
|
||||
|
||||
it('correctly handles a download with an invalid auth header', async () => {
|
||||
const server = http.createServer((req, res) => {
|
||||
const { authorization } = req.headers;
|
||||
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
|
||||
res.statusCode = 401;
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {
|
||||
'Content-Length': mockPDF.length,
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
|
||||
});
|
||||
res.end(mockPDF);
|
||||
}
|
||||
});
|
||||
|
||||
const { port } = await listen(server);
|
||||
|
||||
const downloadFailed: Promise<Electron.DownloadItem> = new Promise((resolve) => {
|
||||
session.defaultSession.once('will-download', (_, item) => {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', (e, state) => {
|
||||
console.log(state);
|
||||
try {
|
||||
resolve(item);
|
||||
} catch { }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
session.defaultSession.downloadURL(`${url}:${port}`, {
|
||||
headers: {
|
||||
Authorization: 'wtf-is-this'
|
||||
}
|
||||
});
|
||||
|
||||
const item = await downloadFailed;
|
||||
expect(item.getState()).to.equal('interrupted');
|
||||
expect(item.getReceivedBytes()).to.equal(0);
|
||||
expect(item.getTotalBytes()).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('webContents.downloadURL', () => {
|
||||
it('can perform a download', (done) => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
assertDownload(state, item);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
w.webContents.downloadURL(`${url}:${port}`);
|
||||
});
|
||||
|
||||
it('can perform a download with a valid auth header', async () => {
|
||||
const server = http.createServer((req, res) => {
|
||||
const { authorization } = req.headers;
|
||||
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
|
||||
res.statusCode = 401;
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {
|
||||
'Content-Length': mockPDF.length,
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
|
||||
});
|
||||
res.end(mockPDF);
|
||||
}
|
||||
});
|
||||
|
||||
const { port } = await listen(server);
|
||||
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const downloadDone: Promise<Electron.DownloadItem> = new Promise((resolve) => {
|
||||
w.webContents.session.once('will-download', (e, item) => {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', () => {
|
||||
try {
|
||||
resolve(item);
|
||||
} catch { }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
w.webContents.downloadURL(`${url}:${port}`, {
|
||||
headers: {
|
||||
Authorization: 'Basic i-am-an-auth-header'
|
||||
}
|
||||
});
|
||||
|
||||
const item = await downloadDone;
|
||||
expect(item.getState()).to.equal('completed');
|
||||
expect(item.getFilename()).to.equal('mock.pdf');
|
||||
expect(item.getMimeType()).to.equal('application/pdf');
|
||||
expect(item.getReceivedBytes()).to.equal(mockPDF.length);
|
||||
expect(item.getTotalBytes()).to.equal(mockPDF.length);
|
||||
expect(item.getContentDisposition()).to.equal(contentDisposition);
|
||||
});
|
||||
|
||||
it('throws when called with invalid headers', () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
expect(() => {
|
||||
w.webContents.downloadURL(`${url}:${port}`, {
|
||||
// @ts-ignore this line is intentionally incorrect
|
||||
headers: 'i-am-a-bad-header'
|
||||
});
|
||||
}).to.throw(/Invalid value for headers - must be an object/);
|
||||
});
|
||||
|
||||
it('correctly handles a download and an invalid auth header', async () => {
|
||||
const server = http.createServer((req, res) => {
|
||||
const { authorization } = req.headers;
|
||||
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
|
||||
res.statusCode = 401;
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {
|
||||
'Content-Length': mockPDF.length,
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
|
||||
});
|
||||
res.end(mockPDF);
|
||||
}
|
||||
});
|
||||
|
||||
const { port } = await listen(server);
|
||||
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const downloadFailed: Promise<Electron.DownloadItem> = new Promise((resolve) => {
|
||||
w.webContents.session.once('will-download', (_, item) => {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', (e, state) => {
|
||||
console.log(state);
|
||||
try {
|
||||
resolve(item);
|
||||
} catch { }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
w.webContents.downloadURL(`${url}:${port}`, {
|
||||
headers: {
|
||||
Authorization: 'wtf-is-this'
|
||||
}
|
||||
});
|
||||
|
||||
const item = await downloadFailed;
|
||||
expect(item.getState()).to.equal('interrupted');
|
||||
expect(item.getReceivedBytes()).to.equal(0);
|
||||
expect(item.getTotalBytes()).to.equal(0);
|
||||
});
|
||||
|
||||
it('can download from custom protocols', (done) => {
|
||||
const protocol = session.defaultSession.protocol;
|
||||
const handler = (ignoredError: any, callback: Function) => {
|
||||
callback({ url: `${url}:${port}` });
|
||||
};
|
||||
protocol.registerHttpProtocol(protocolName, handler);
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
assertDownload(state, item, true);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
w.webContents.downloadURL(`${protocolName}://item`);
|
||||
});
|
||||
|
||||
it('can cancel download', (done) => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
expect(state).to.equal('cancelled');
|
||||
expect(item.getFilename()).to.equal('mock.pdf');
|
||||
expect(item.getMimeType()).to.equal('application/pdf');
|
||||
expect(item.getReceivedBytes()).to.equal(0);
|
||||
expect(item.getTotalBytes()).to.equal(mockPDF.length);
|
||||
expect(item.getContentDisposition()).to.equal(contentDisposition);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
item.cancel();
|
||||
});
|
||||
w.webContents.downloadURL(`${url}:${port}/`);
|
||||
});
|
||||
|
||||
it('can generate a default filename', function (done) {
|
||||
if (process.env.APPVEYOR === 'True') {
|
||||
// FIXME(alexeykuzmin): Skip the test.
|
||||
// this.skip()
|
||||
return done();
|
||||
}
|
||||
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function () {
|
||||
try {
|
||||
expect(item.getFilename()).to.equal('download.pdf');
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
item.cancel();
|
||||
});
|
||||
w.webContents.downloadURL(`${url}:${port}/?testFilename`);
|
||||
});
|
||||
|
||||
it('can set options for the save dialog', (done) => {
|
||||
const filePath = path.join(__dirname, 'fixtures', 'mock.pdf');
|
||||
const options = {
|
||||
window: null,
|
||||
title: 'title',
|
||||
message: 'message',
|
||||
buttonLabel: 'buttonLabel',
|
||||
nameFieldLabel: 'nameFieldLabel',
|
||||
defaultPath: '/',
|
||||
filters: [{
|
||||
name: '1', extensions: ['.1', '.2']
|
||||
}, {
|
||||
name: '2', extensions: ['.3', '.4', '.5']
|
||||
}],
|
||||
showsTagField: true,
|
||||
securityScopedBookmarks: true
|
||||
};
|
||||
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.setSavePath(filePath);
|
||||
item.setSaveDialogOptions(options);
|
||||
item.on('done', function () {
|
||||
try {
|
||||
expect(item.getSaveDialogOptions()).to.deep.equal(options);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
item.cancel();
|
||||
});
|
||||
w.webContents.downloadURL(`${url}:${port}`);
|
||||
});
|
||||
|
||||
describe('when a save path is specified and the URL is unavailable', () => {
|
||||
it('does not display a save dialog and reports the done state as interrupted', (done) => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
if (item.getState() === 'interrupted') {
|
||||
item.resume();
|
||||
}
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
expect(state).to.equal('interrupted');
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
w.webContents.downloadURL(`file://${path.join(__dirname, 'does-not-exist.txt')}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('WebView.downloadURL', () => {
|
||||
it('can perform a download', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } });
|
||||
await w.loadURL('about:blank');
|
||||
function webviewDownload ({ fixtures, url, port }: { fixtures: string, url: string, port: string }) {
|
||||
const webview = new (window as any).WebView();
|
||||
webview.addEventListener('did-finish-load', () => {
|
||||
webview.downloadURL(`${url}:${port}/`);
|
||||
});
|
||||
webview.src = `file://${fixtures}/api/blank.html`;
|
||||
document.body.appendChild(webview);
|
||||
}
|
||||
const done: Promise<[string, Electron.DownloadItem]> = new Promise(resolve => {
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
resolve([state, item]);
|
||||
});
|
||||
});
|
||||
});
|
||||
await w.webContents.executeJavaScript(`(${webviewDownload})(${JSON.stringify({ fixtures, url, port })})`);
|
||||
const [state, item] = await done;
|
||||
assertDownload(state, item);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -375,6 +375,16 @@ describe('webContents module', () => {
|
||||
await expect(w.loadURL(w.getURL() + '#foo')).to.eventually.be.fulfilled();
|
||||
});
|
||||
|
||||
it('resolves after browser initiated navigation', async () => {
|
||||
let finishedLoading = false;
|
||||
w.webContents.on('did-finish-load', function () {
|
||||
finishedLoading = true;
|
||||
});
|
||||
|
||||
await w.loadFile(path.join(fixturesPath, 'pages', 'navigate_in_page_and_wait.html'));
|
||||
expect(finishedLoading).to.be.true();
|
||||
});
|
||||
|
||||
it('rejects when failing to load a file URL', async () => {
|
||||
await expect(w.loadURL('file:non-existent')).to.eventually.be.rejected()
|
||||
.and.have.property('code', 'ERR_FILE_NOT_FOUND');
|
||||
|
||||
@@ -2050,10 +2050,32 @@ describe('chromium features', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('chrome://accessibility', () => {
|
||||
it('loads the page successfully', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
await w.loadURL('chrome://accessibility');
|
||||
const pageExists = await w.webContents.executeJavaScript(
|
||||
"window.hasOwnProperty('chrome') && window.chrome.hasOwnProperty('send')"
|
||||
);
|
||||
expect(pageExists).to.be.true();
|
||||
});
|
||||
});
|
||||
|
||||
describe('chrome://gpu', () => {
|
||||
it('loads the page successfully', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
await w.loadURL('chrome://gpu');
|
||||
const pageExists = await w.webContents.executeJavaScript(
|
||||
"window.hasOwnProperty('chrome') && window.chrome.hasOwnProperty('send')"
|
||||
);
|
||||
expect(pageExists).to.be.true();
|
||||
});
|
||||
});
|
||||
|
||||
describe('chrome://media-internals', () => {
|
||||
it('loads the page successfully', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.loadURL('chrome://media-internals');
|
||||
await w.loadURL('chrome://media-internals');
|
||||
const pageExists = await w.webContents.executeJavaScript(
|
||||
"window.hasOwnProperty('chrome') && window.chrome.hasOwnProperty('send')"
|
||||
);
|
||||
@@ -2064,7 +2086,7 @@ describe('chromium features', () => {
|
||||
describe('chrome://webrtc-internals', () => {
|
||||
it('loads the page successfully', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.loadURL('chrome://webrtc-internals');
|
||||
await w.loadURL('chrome://webrtc-internals');
|
||||
const pageExists = await w.webContents.executeJavaScript(
|
||||
"window.hasOwnProperty('chrome') && window.chrome.hasOwnProperty('send')"
|
||||
);
|
||||
|
||||
@@ -842,15 +842,14 @@ describe('chrome extensions', () => {
|
||||
|
||||
before(async () => {
|
||||
customSession = session.fromPartition(`persist:${uuid.v4()}`);
|
||||
await customSession.loadExtension(path.join(fixtures, 'extensions', 'tabs-api-async'));
|
||||
await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-tabs', 'api-async'));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
w = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
session: customSession,
|
||||
nodeIntegration: true
|
||||
session: customSession
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -913,27 +912,55 @@ describe('chrome extensions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('get', async () => {
|
||||
await w.loadURL(url);
|
||||
describe('get', () => {
|
||||
it('returns tab properties', async () => {
|
||||
await w.loadURL(url);
|
||||
|
||||
const message = { method: 'get' };
|
||||
w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
|
||||
const message = { method: 'get' };
|
||||
w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
|
||||
|
||||
const [,, responseString] = await once(w.webContents, 'console-message');
|
||||
const [,, responseString] = await once(w.webContents, 'console-message');
|
||||
|
||||
const response = JSON.parse(responseString);
|
||||
expect(response).to.have.property('active').that.is.a('boolean');
|
||||
expect(response).to.have.property('autoDiscardable').that.is.a('boolean');
|
||||
expect(response).to.have.property('discarded').that.is.a('boolean');
|
||||
expect(response).to.have.property('groupId').that.is.a('number');
|
||||
expect(response).to.have.property('highlighted').that.is.a('boolean');
|
||||
expect(response).to.have.property('id').that.is.a('number');
|
||||
expect(response).to.have.property('incognito').that.is.a('boolean');
|
||||
expect(response).to.have.property('index').that.is.a('number');
|
||||
expect(response).to.have.property('pinned').that.is.a('boolean');
|
||||
expect(response).to.have.property('selected').that.is.a('boolean');
|
||||
expect(response).to.have.property('url').that.is.a('string');
|
||||
expect(response).to.have.property('windowId').that.is.a('number');
|
||||
const response = JSON.parse(responseString);
|
||||
expect(response).to.have.property('url').that.is.a('string');
|
||||
expect(response).to.have.property('title').that.is.a('string');
|
||||
expect(response).to.have.property('active').that.is.a('boolean');
|
||||
expect(response).to.have.property('autoDiscardable').that.is.a('boolean');
|
||||
expect(response).to.have.property('discarded').that.is.a('boolean');
|
||||
expect(response).to.have.property('groupId').that.is.a('number');
|
||||
expect(response).to.have.property('highlighted').that.is.a('boolean');
|
||||
expect(response).to.have.property('id').that.is.a('number');
|
||||
expect(response).to.have.property('incognito').that.is.a('boolean');
|
||||
expect(response).to.have.property('index').that.is.a('number');
|
||||
expect(response).to.have.property('pinned').that.is.a('boolean');
|
||||
expect(response).to.have.property('selected').that.is.a('boolean');
|
||||
expect(response).to.have.property('windowId').that.is.a('number');
|
||||
});
|
||||
|
||||
it('does not return privileged properties without tabs permission', async () => {
|
||||
const noPrivilegeSes = session.fromPartition(`persist:${uuid.v4()}`);
|
||||
await noPrivilegeSes.loadExtension(path.join(fixtures, 'extensions', 'chrome-tabs', 'no-privileges'));
|
||||
|
||||
w = new BrowserWindow({ show: false, webPreferences: { session: noPrivilegeSes } });
|
||||
await w.loadURL(url);
|
||||
|
||||
w.webContents.executeJavaScript('window.postMessage(\'{}\', \'*\')');
|
||||
const [,, responseString] = await once(w.webContents, 'console-message');
|
||||
const response = JSON.parse(responseString);
|
||||
expect(response).not.to.have.property('url');
|
||||
expect(response).not.to.have.property('title');
|
||||
expect(response).to.have.property('active').that.is.a('boolean');
|
||||
expect(response).to.have.property('autoDiscardable').that.is.a('boolean');
|
||||
expect(response).to.have.property('discarded').that.is.a('boolean');
|
||||
expect(response).to.have.property('groupId').that.is.a('number');
|
||||
expect(response).to.have.property('highlighted').that.is.a('boolean');
|
||||
expect(response).to.have.property('id').that.is.a('number');
|
||||
expect(response).to.have.property('incognito').that.is.a('boolean');
|
||||
expect(response).to.have.property('index').that.is.a('number');
|
||||
expect(response).to.have.property('pinned').that.is.a('boolean');
|
||||
expect(response).to.have.property('selected').that.is.a('boolean');
|
||||
expect(response).to.have.property('windowId').that.is.a('number');
|
||||
});
|
||||
});
|
||||
|
||||
it('reload', async () => {
|
||||
@@ -960,6 +987,19 @@ describe('chrome extensions', () => {
|
||||
const [,, responseString] = await once(w.webContents, 'console-message');
|
||||
const response = JSON.parse(responseString);
|
||||
|
||||
expect(response).to.have.property('url').that.is.a('string');
|
||||
expect(response).to.have.property('title').that.is.a('string');
|
||||
expect(response).to.have.property('active').that.is.a('boolean');
|
||||
expect(response).to.have.property('autoDiscardable').that.is.a('boolean');
|
||||
expect(response).to.have.property('discarded').that.is.a('boolean');
|
||||
expect(response).to.have.property('groupId').that.is.a('number');
|
||||
expect(response).to.have.property('highlighted').that.is.a('boolean');
|
||||
expect(response).to.have.property('id').that.is.a('number');
|
||||
expect(response).to.have.property('incognito').that.is.a('boolean');
|
||||
expect(response).to.have.property('index').that.is.a('number');
|
||||
expect(response).to.have.property('pinned').that.is.a('boolean');
|
||||
expect(response).to.have.property('selected').that.is.a('boolean');
|
||||
expect(response).to.have.property('windowId').that.is.a('number');
|
||||
expect(response).to.have.property('mutedInfo').that.is.a('object');
|
||||
const { mutedInfo } = response;
|
||||
expect(mutedInfo).to.deep.eq({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "tabs-api-async",
|
||||
"name": "api-async",
|
||||
"version": "1.0",
|
||||
"content_scripts": [
|
||||
{
|
||||
@@ -8,6 +8,7 @@
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
"permissions": ["tabs"],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
6
spec/fixtures/extensions/chrome-tabs/no-privileges/background.js
vendored
Normal file
6
spec/fixtures/extensions/chrome-tabs/no-privileges/background.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/* global chrome */
|
||||
|
||||
chrome.runtime.onMessage.addListener((_request, sender, sendResponse) => {
|
||||
chrome.tabs.get(sender.tab.id).then(sendResponse);
|
||||
return true;
|
||||
});
|
||||
11
spec/fixtures/extensions/chrome-tabs/no-privileges/main.js
vendored
Normal file
11
spec/fixtures/extensions/chrome-tabs/no-privileges/main.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/* global chrome */
|
||||
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
sendResponse(request);
|
||||
});
|
||||
|
||||
window.addEventListener('message', () => {
|
||||
chrome.runtime.sendMessage({}, response => {
|
||||
console.log(JSON.stringify(response));
|
||||
});
|
||||
}, false);
|
||||
19
spec/fixtures/extensions/chrome-tabs/no-privileges/manifest.json
vendored
Normal file
19
spec/fixtures/extensions/chrome-tabs/no-privileges/manifest.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "no-privileges",
|
||||
"version": "1.0",
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": [
|
||||
"main.js"
|
||||
],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"manifest_version": 3
|
||||
}
|
||||
15
spec/fixtures/pages/navigate_in_page_and_wait.html
vendored
Normal file
15
spec/fixtures/pages/navigate_in_page_and_wait.html
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<html>
|
||||
<header>
|
||||
<script type="text/javascript">
|
||||
window.history.replaceState(window.location.href, "Sample Title", window.location.href);
|
||||
// Simulate that we load web page.
|
||||
let d = new Date();
|
||||
const endTime = new Date(d.getTime() + (10 * 1000));
|
||||
while(d.getTime() < endTime) {
|
||||
d = new Date();
|
||||
}
|
||||
</script>
|
||||
</header>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user