mirror of
https://github.com/electron/electron.git
synced 2026-02-26 03:01:17 -05:00
Compare commits
19 Commits
test/osr-c
...
v35.0.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04f5fe6a1c | ||
|
|
08b6bb1712 | ||
|
|
813efbcdf7 | ||
|
|
d34fa2e301 | ||
|
|
724744af16 | ||
|
|
91bb748eaa | ||
|
|
d77c2d75ed | ||
|
|
f4c3eb4391 | ||
|
|
9aca9e9fb6 | ||
|
|
e0fa647601 | ||
|
|
6b0fd02c0a | ||
|
|
1017ac821f | ||
|
|
91b53b633a | ||
|
|
8da9572592 | ||
|
|
291bbff5d8 | ||
|
|
d704a3fc5b | ||
|
|
fb70b81ee6 | ||
|
|
97fa059e1f | ||
|
|
8a64cdc0b1 |
@@ -16,5 +16,5 @@ runs:
|
||||
e auto-update disable
|
||||
if [ "$(expr substr $(uname -s) 1 10)" == "MSYS_NT-10" ]; then
|
||||
e d cipd.bat --version
|
||||
cp "C:\Python37\python.exe" "C:\Python37\python3.exe"
|
||||
cp "C:\Python311\python.exe" "C:\Python311\python3.exe"
|
||||
fi
|
||||
|
||||
2
.github/workflows/archaeologist-dig.yml
vendored
2
.github/workflows/archaeologist-dig.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
sha-file: .dig-old
|
||||
filename: electron.old.d.ts
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 #v4.6.0
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b #v4.5.0
|
||||
with:
|
||||
name: artifacts
|
||||
path: electron/artifacts
|
||||
|
||||
6
.github/workflows/branch-created.yml
vendored
6
.github/workflows/branch-created.yml
vendored
@@ -94,7 +94,7 @@ jobs:
|
||||
}))
|
||||
- name: Create Release Project Board
|
||||
if: ${{ steps.check-major-version.outputs.MAJOR }}
|
||||
uses: dsanders11/project-actions/copy-project@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/copy-project@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
id: create-release-board
|
||||
with:
|
||||
drafts: true
|
||||
@@ -114,14 +114,14 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
- name: Find Previous Release Project Board
|
||||
if: ${{ steps.check-major-version.outputs.MAJOR }}
|
||||
uses: dsanders11/project-actions/find-project@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/find-project@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
id: find-prev-release-board
|
||||
with:
|
||||
title: ${{ steps.generate-project-metadata.outputs.prev-prev-major }}-x-y
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
- name: Close Previous Release Project Board
|
||||
if: ${{ steps.check-major-version.outputs.MAJOR }}
|
||||
uses: dsanders11/project-actions/close-project@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/close-project@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
with:
|
||||
project-number: ${{ steps.find-prev-release-board.outputs.number }}
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
6
.github/workflows/issue-labeled.yml
vendored
6
.github/workflows/issue-labeled.yml
vendored
@@ -20,13 +20,12 @@ jobs:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Set status
|
||||
uses: dsanders11/project-actions/edit-item@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/edit-item@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
project-number: 90
|
||||
field: Status
|
||||
field-value: ✅ Triaged
|
||||
fail-if-item-not-found: false
|
||||
issue-labeled-blocked:
|
||||
name: blocked/* label added
|
||||
if: startsWith(github.event.label.name, 'blocked/')
|
||||
@@ -39,13 +38,12 @@ jobs:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Set status
|
||||
uses: dsanders11/project-actions/edit-item@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/edit-item@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
project-number: 90
|
||||
field: Status
|
||||
field-value: 🛑 Blocked
|
||||
fail-if-item-not-found: false
|
||||
issue-labeled-blocked-need-repro:
|
||||
name: blocked/need-repro label added
|
||||
if: github.event.label.name == 'blocked/need-repro'
|
||||
|
||||
2
.github/workflows/issue-opened.yml
vendored
2
.github/workflows/issue-opened.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Add to Issue Triage
|
||||
uses: dsanders11/project-actions/add-item@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/add-item@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
with:
|
||||
field: Reporter
|
||||
field-value: ${{ github.event.issue.user.login }}
|
||||
|
||||
5
.github/workflows/issue-transferred.yml
vendored
5
.github/workflows/issue-transferred.yml
vendored
@@ -10,7 +10,6 @@ jobs:
|
||||
issue-transferred:
|
||||
name: Issue Transferred
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !github.event.changes.new_repository.private }}
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
uses: electron/github-app-auth-action@384fd19694fe7b6dcc9a684746c6976ad78228ae # v1.1.1
|
||||
@@ -19,9 +18,7 @@ jobs:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Remove from issue triage
|
||||
uses: dsanders11/project-actions/delete-item@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/delete-item@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
project-number: 90
|
||||
item: ${{ github.event.changes.new_issue.html_url }}
|
||||
fail-if-item-not-found: false
|
||||
|
||||
3
.github/workflows/issue-unlabeled.yml
vendored
3
.github/workflows/issue-unlabeled.yml
vendored
@@ -30,10 +30,9 @@ jobs:
|
||||
org: electron
|
||||
- name: Set status
|
||||
if: ${{ steps.check-for-blocked-labels.outputs.NOT_BLOCKED }}
|
||||
uses: dsanders11/project-actions/edit-item@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/edit-item@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
project-number: 90
|
||||
field: Status
|
||||
field-value: 📥 Was Blocked
|
||||
fail-if-item-not-found: false
|
||||
|
||||
@@ -239,7 +239,7 @@ jobs:
|
||||
if: always() && !cancelled()
|
||||
- name: Upload Test Artifacts
|
||||
if: always() && !cancelled()
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b
|
||||
with:
|
||||
name: test_artifacts_${{ env.ARTIFACT_KEY }}_${{ matrix.shard }}
|
||||
path: src/electron/spec/artifacts
|
||||
|
||||
2
.github/workflows/pull-request-labeled.yml
vendored
2
.github/workflows/pull-request-labeled.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
creds: ${{ secrets.RELEASE_BOARD_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Set status
|
||||
uses: dsanders11/project-actions/edit-item@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/edit-item@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
project-number: 94
|
||||
|
||||
4
.github/workflows/scorecards.yml
vendored
4
.github/workflows/scorecards.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
@@ -50,6 +50,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
|
||||
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
2
.github/workflows/stable-prep-items.yml
vendored
2
.github/workflows/stable-prep-items.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
PROJECT_NUMBER=$(gh project list --owner electron --format json | jq -r '.projects | map(select(.title | test("^[0-9]+-x-y$"))) | max_by(.number) | .number')
|
||||
echo "PROJECT_NUMBER=$PROJECT_NUMBER" >> "$GITHUB_OUTPUT"
|
||||
- name: Update Completed Stable Prep Items
|
||||
uses: dsanders11/project-actions/completed-by@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/completed-by@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
with:
|
||||
field: Prep Status
|
||||
field-value: ✅ Complete
|
||||
|
||||
2
.github/workflows/update_appveyor_image.yml
vendored
2
.github/workflows/update_appveyor_image.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
fi
|
||||
- name: (Optionally) Update Appveyor Image
|
||||
if: ${{ env.APPVEYOR_IMAGE_VERSION }}
|
||||
uses: mikefarah/yq@8bf425b4d1344db7cd469a8d10a390876e0c77fd # v4.45.1
|
||||
uses: mikefarah/yq@4839dbbf80445070a31c7a9c1055da527db2d5ee # v4.44.6
|
||||
with:
|
||||
cmd: |
|
||||
yq '.image = "${{ env.APPVEYOR_IMAGE_VERSION }}"' "appveyor.yml" > "appveyor2.yml"
|
||||
|
||||
@@ -12,17 +12,9 @@ shortcuts.
|
||||
not have the keyboard focus. This module cannot be used before the `ready`
|
||||
event of the app module is emitted.
|
||||
|
||||
Please also note that it is also possible to use Chromium's
|
||||
`GlobalShortcutsPortal` implementation, which allows apps to bind global
|
||||
shortcuts when running within a Wayland session.
|
||||
|
||||
```js
|
||||
const { app, globalShortcut } = require('electron')
|
||||
|
||||
// Enable usage of Portal's globalShortcuts. This is essential for cases when
|
||||
// the app runs in a Wayland session.
|
||||
app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal')
|
||||
|
||||
app.whenReady().then(() => {
|
||||
// Register a 'CommandOrControl+X' shortcut listener.
|
||||
const ret = globalShortcut.register('CommandOrControl+X', () => {
|
||||
|
||||
@@ -22,12 +22,9 @@ In `preload.js` use the [`contextBridge`][] to inject a method `window.electron.
|
||||
|
||||
```js
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
const path = require('node:path')
|
||||
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
startDrag: (fileName) => {
|
||||
ipcRenderer.send('ondragstart', path.join(process.cwd(), fileName))
|
||||
}
|
||||
startDrag: (fileName) => ipcRenderer.send('ondragstart', fileName)
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ You should at least follow these steps to improve the security of your applicati
|
||||
17. [Validate the `sender` of all IPC messages](#17-validate-the-sender-of-all-ipc-messages)
|
||||
18. [Avoid usage of the `file://` protocol and prefer usage of custom protocols](#18-avoid-usage-of-the-file-protocol-and-prefer-usage-of-custom-protocols)
|
||||
19. [Check which fuses you can change](#19-check-which-fuses-you-can-change)
|
||||
20. [Do not expose Electron APIs to untrusted web content](#20-do-not-expose-electron-apis-to-untrusted-web-content)
|
||||
|
||||
To automate the detection of misconfigurations and insecure patterns, it is
|
||||
possible to use
|
||||
@@ -229,7 +230,7 @@ API to remotely loaded content via the [contextBridge API](../api/context-bridge
|
||||
### 3. Enable Context Isolation
|
||||
|
||||
:::info
|
||||
This recommendation is the default behavior in Electron since 12.0.0.
|
||||
Context Isolation is the default behavior in Electron since 12.0.0.
|
||||
:::
|
||||
|
||||
Context isolation is an Electron feature that allows developers to run code
|
||||
@@ -804,6 +805,48 @@ flipping these fuses easy. Check out the README of that module for more details
|
||||
potential error cases, and refer to
|
||||
[How do I flip the fuses?](./fuses.md#how-do-i-flip-the-fuses) in our documentation.
|
||||
|
||||
### 20. Do not expose Electron APIs to untrusted web content
|
||||
|
||||
You should not directly expose Electron's APIs, especially IPC, to untrusted web content in your
|
||||
preload scripts.
|
||||
|
||||
### Why?
|
||||
|
||||
Exposing raw APIs like `ipcRenderer.on` is dangerous because it gives renderer processes direct
|
||||
access to the entire IPC event system, allowing them to listen for any IPC events, not just the ones
|
||||
intended for them.
|
||||
|
||||
To avoid that exposure, we also cannot pass callbacks directly through: The first
|
||||
argument to IPC event callbacks is an `IpcRendererEvent` object, which includes properties like `sender`
|
||||
that provide access to the underlying `ipcRenderer` instance. Even if you only listen for specific
|
||||
events, passing the callback directly means the renderer gets access to this event object.
|
||||
|
||||
In short, we want the untrusted web content to only have access to necessary information and APIs.
|
||||
|
||||
### How?
|
||||
|
||||
```js title='preload'.js'
|
||||
// Bad
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
on: ipcRenderer.on
|
||||
})
|
||||
|
||||
// Also bad
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback)
|
||||
})
|
||||
|
||||
// Good
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value))
|
||||
})
|
||||
```
|
||||
|
||||
:::info
|
||||
For more information on what `contextIsolation` is and how to use it to secure your app,
|
||||
please see the [Context Isolation](context-isolation.md) document.
|
||||
:::
|
||||
|
||||
[breaking-changes]: ../breaking-changes.md
|
||||
[browser-window]: ../api/browser-window.md
|
||||
[webview-tag]: ../api/webview-tag.md
|
||||
|
||||
@@ -139,4 +139,3 @@ refactor_unfilter_unresponsive_events.patch
|
||||
build_disable_thin_lto_mac.patch
|
||||
build_add_public_config_simdutf_config.patch
|
||||
revert_code_health_clean_up_stale_macwebcontentsocclusion.patch
|
||||
check_for_unit_to_activate_before_notifying_about_success.patch
|
||||
|
||||
@@ -1,497 +0,0 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Maksim Sisov <msisov@igalia.com>
|
||||
Date: Tue, 7 Jan 2025 23:46:56 -0800
|
||||
Subject: Check for unit to activate before notifying about success
|
||||
|
||||
Portal's globalShortcuts interface relies on the unit name to
|
||||
properly assign a client for the bound commands. However, in
|
||||
some scenarious, there is a race between the service to be
|
||||
created, changed its name and activated. As a result, shortcuts
|
||||
might be bound before the name is changed. As a result, shortcuts
|
||||
might not correctly work and the client will not receive any
|
||||
signals.
|
||||
|
||||
This is mostly not an issue for Chromium as it creates the
|
||||
global shortcuts portal linux object way earlier than it gets
|
||||
commands from the command service. But downstream project, which
|
||||
try to bind shortcuts at the same time as that instance is created,
|
||||
may experience this issue. As a result, they might not have
|
||||
shortcuts working correctly after system reboot or app restart as
|
||||
there is a race between those operations.
|
||||
|
||||
Bug: None
|
||||
Change-Id: I8346d65e051d9587850c76ca0b8807669c161667
|
||||
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6110782
|
||||
Reviewed-by: Thomas Anderson <thomasanderson@chromium.org>
|
||||
Commit-Queue: Maksim Sisov <msisov@igalia.com>
|
||||
Cr-Commit-Position: refs/heads/main@{#1403434}
|
||||
|
||||
diff --git a/components/dbus/xdg/systemd.cc b/components/dbus/xdg/systemd.cc
|
||||
index 362a16447bf578923cb8a84674c277ae6c98228f..3cd9a55d540c07a4c53f5a62bec5cbea37c11838 100644
|
||||
--- a/components/dbus/xdg/systemd.cc
|
||||
+++ b/components/dbus/xdg/systemd.cc
|
||||
@@ -4,9 +4,12 @@
|
||||
|
||||
#include "components/dbus/xdg/systemd.h"
|
||||
|
||||
+#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "base/environment.h"
|
||||
+#include "base/functional/bind.h"
|
||||
+#include "base/functional/callback_helpers.h"
|
||||
#include "base/memory/scoped_refptr.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "base/sequence_checker.h"
|
||||
@@ -17,7 +20,9 @@
|
||||
#include "components/dbus/utils/name_has_owner.h"
|
||||
#include "dbus/bus.h"
|
||||
#include "dbus/message.h"
|
||||
+#include "dbus/object_path.h"
|
||||
#include "dbus/object_proxy.h"
|
||||
+#include "dbus/property.h"
|
||||
#include "third_party/abseil-cpp/absl/types/variant.h"
|
||||
|
||||
namespace dbus_xdg {
|
||||
@@ -37,6 +42,10 @@ constexpr char kServiceNameSystemd[] = "org.freedesktop.systemd1";
|
||||
constexpr char kObjectPathSystemd[] = "/org/freedesktop/systemd1";
|
||||
constexpr char kInterfaceSystemdManager[] = "org.freedesktop.systemd1.Manager";
|
||||
constexpr char kMethodStartTransientUnit[] = "StartTransientUnit";
|
||||
+constexpr char kMethodGetUnit[] = "GetUnit";
|
||||
+
|
||||
+constexpr char kInterfaceSystemdUnit[] = "org.freedesktop.systemd1.Unit";
|
||||
+constexpr char kActiveStateProp[] = "ActiveState";
|
||||
|
||||
constexpr char kUnitNameFormat[] = "app-$1$2-$3.scope";
|
||||
|
||||
@@ -67,6 +76,81 @@ const char* GetAppNameSuffix(const std::string& channel) {
|
||||
return "";
|
||||
}
|
||||
|
||||
+// Declare this helper for SystemdUnitActiveStateWatcher to be used.
|
||||
+void SetStateAndRunCallbacks(SystemdUnitStatus result);
|
||||
+
|
||||
+// Watches the object to become active and fires callbacks via
|
||||
+// SetStateAndRunCallbacks. The callbacks are fired whenever a response with the
|
||||
+// state being "active" or "failed" (or similar) comes.
|
||||
+//
|
||||
+// PS firing callbacks results in destroying this object. So any references
|
||||
+// to this become invalid.
|
||||
+class SystemdUnitActiveStateWatcher : public dbus::PropertySet {
|
||||
+ public:
|
||||
+ SystemdUnitActiveStateWatcher(scoped_refptr<dbus::Bus> bus,
|
||||
+ dbus::ObjectProxy* object_proxy)
|
||||
+ : dbus::PropertySet(object_proxy,
|
||||
+ kInterfaceSystemdUnit,
|
||||
+ base::BindRepeating(
|
||||
+ &SystemdUnitActiveStateWatcher::OnPropertyChanged,
|
||||
+ base::Unretained(this))),
|
||||
+ bus_(bus) {
|
||||
+ RegisterProperty(kActiveStateProp, &active_state_);
|
||||
+ ConnectSignals();
|
||||
+ GetAll();
|
||||
+ }
|
||||
+
|
||||
+ ~SystemdUnitActiveStateWatcher() override {
|
||||
+ bus_->RemoveObjectProxy(kServiceNameSystemd, object_proxy()->object_path(),
|
||||
+ base::DoNothing());
|
||||
+ }
|
||||
+
|
||||
+ private:
|
||||
+ void OnPropertyChanged(const std::string& property_name) {
|
||||
+ DCHECK(active_state_.is_valid());
|
||||
+ const std::string state_value = active_state_.value();
|
||||
+ if (callbacks_called_ || state_value == "activating" ||
|
||||
+ state_value == "reloading") {
|
||||
+ // Ignore if callbacks have already been fired or continue waiting until
|
||||
+ // the state changes to something else.
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ // There are other states as failed, inactive, and deactivating. Treat all
|
||||
+ // of them as failed.
|
||||
+ callbacks_called_ = true;
|
||||
+ SetStateAndRunCallbacks(state_value == "active"
|
||||
+ ? SystemdUnitStatus::kUnitStarted
|
||||
+ : SystemdUnitStatus::kFailedToStart);
|
||||
+ MaybeDeleteSelf();
|
||||
+ }
|
||||
+
|
||||
+ void OnGetAll(dbus::Response* response) override {
|
||||
+ dbus::PropertySet::OnGetAll(response);
|
||||
+ keep_alive_ = false;
|
||||
+ MaybeDeleteSelf();
|
||||
+ }
|
||||
+
|
||||
+ void MaybeDeleteSelf() {
|
||||
+ if (!keep_alive_ && callbacks_called_) {
|
||||
+ delete this;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // Registered property that this listens updates to.
|
||||
+ dbus::Property<std::string> active_state_;
|
||||
+
|
||||
+ // Indicates whether callbacks for the unit's state have been called.
|
||||
+ bool callbacks_called_ = false;
|
||||
+
|
||||
+ // Control variable that helps to defer the destruction of |this| as deleting
|
||||
+ // self when the state changes to active during |OnGetAll| will result in a
|
||||
+ // segfault.
|
||||
+ bool keep_alive_ = true;
|
||||
+
|
||||
+ scoped_refptr<dbus::Bus> bus_;
|
||||
+};
|
||||
+
|
||||
// Global state for cached result or pending callbacks.
|
||||
StatusOrCallbacks& GetUnitNameState() {
|
||||
static base::NoDestructor<StatusOrCallbacks> state(
|
||||
@@ -83,10 +167,52 @@ void SetStateAndRunCallbacks(SystemdUnitStatus result) {
|
||||
}
|
||||
}
|
||||
|
||||
-void OnStartTransientUnitResponse(dbus::Response* response) {
|
||||
+void OnGetPathResponse(scoped_refptr<dbus::Bus> bus, dbus::Response* response) {
|
||||
+ dbus::MessageReader reader(response);
|
||||
+ dbus::ObjectPath obj_path;
|
||||
+ if (!response || !reader.PopObjectPath(&obj_path) || !obj_path.IsValid()) {
|
||||
+ // We didn't get a valid response. Treat this as failed service.
|
||||
+ SetStateAndRunCallbacks(SystemdUnitStatus::kFailedToStart);
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ dbus::ObjectProxy* unit_proxy =
|
||||
+ bus->GetObjectProxy(kServiceNameSystemd, obj_path);
|
||||
+ // Create the active state property watcher. It will destroy itself once
|
||||
+ // it gets notified about the state change.
|
||||
+ std::unique_ptr<SystemdUnitActiveStateWatcher> active_state_watcher =
|
||||
+ std::make_unique<SystemdUnitActiveStateWatcher>(bus, unit_proxy);
|
||||
+ active_state_watcher.release();
|
||||
+}
|
||||
+
|
||||
+void WaitUnitActivateAndRunCallbacks(scoped_refptr<dbus::Bus> bus,
|
||||
+ std::string unit_name) {
|
||||
+ // Get the path of the unit, which looks similar to
|
||||
+ // /org/freedesktop/systemd1/unit/app_2dorg_2echromium_2eChromium_2d3182191_2escope
|
||||
+ // and then wait for it activation.
|
||||
+ dbus::ObjectProxy* systemd = bus->GetObjectProxy(
|
||||
+ kServiceNameSystemd, dbus::ObjectPath(kObjectPathSystemd));
|
||||
+
|
||||
+ dbus::MethodCall method_call(kInterfaceSystemdManager, kMethodGetUnit);
|
||||
+ dbus::MessageWriter writer(&method_call);
|
||||
+ writer.AppendString(unit_name);
|
||||
+
|
||||
+ systemd->CallMethod(&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
|
||||
+ base::BindOnce(&OnGetPathResponse, std::move(bus)));
|
||||
+}
|
||||
+
|
||||
+void OnStartTransientUnitResponse(scoped_refptr<dbus::Bus> bus,
|
||||
+ std::string unit_name,
|
||||
+ dbus::Response* response) {
|
||||
SystemdUnitStatus result = response ? SystemdUnitStatus::kUnitStarted
|
||||
: SystemdUnitStatus::kFailedToStart;
|
||||
- SetStateAndRunCallbacks(result);
|
||||
+ // If the start of the unit failed, immediately notify the client. Otherwise,
|
||||
+ // wait for its activation.
|
||||
+ if (result == SystemdUnitStatus::kFailedToStart) {
|
||||
+ SetStateAndRunCallbacks(result);
|
||||
+ } else {
|
||||
+ WaitUnitActivateAndRunCallbacks(std::move(bus), unit_name);
|
||||
+ }
|
||||
}
|
||||
|
||||
void OnNameHasOwnerResponse(scoped_refptr<dbus::Bus> bus,
|
||||
@@ -128,8 +254,9 @@ void OnNameHasOwnerResponse(scoped_refptr<dbus::Bus> bus,
|
||||
properties.Write(&writer);
|
||||
// No auxiliary units.
|
||||
Dict<VarDict>().Write(&writer);
|
||||
- systemd->CallMethod(&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
|
||||
- base::BindOnce(&OnStartTransientUnitResponse));
|
||||
+ systemd->CallMethod(
|
||||
+ &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
|
||||
+ base::BindOnce(&OnStartTransientUnitResponse, std::move(bus), unit_name));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
diff --git a/components/dbus/xdg/systemd_unittest.cc b/components/dbus/xdg/systemd_unittest.cc
|
||||
index 2e3baecabc4b479000c78d4f6bd30cb1f6e61d2e..67278d7033664d52fbbda02749a2aaa43352f402 100644
|
||||
--- a/components/dbus/xdg/systemd_unittest.cc
|
||||
+++ b/components/dbus/xdg/systemd_unittest.cc
|
||||
@@ -16,7 +16,9 @@
|
||||
#include "dbus/message.h"
|
||||
#include "dbus/mock_bus.h"
|
||||
#include "dbus/mock_object_proxy.h"
|
||||
+#include "dbus/object_path.h"
|
||||
#include "dbus/object_proxy.h"
|
||||
+#include "dbus/property.h"
|
||||
#include "testing/gmock/include/gmock/gmock.h"
|
||||
#include "testing/gtest/include/gtest/gtest.h"
|
||||
|
||||
@@ -32,6 +34,27 @@ constexpr char kServiceNameSystemd[] = "org.freedesktop.systemd1";
|
||||
constexpr char kObjectPathSystemd[] = "/org/freedesktop/systemd1";
|
||||
constexpr char kInterfaceSystemdManager[] = "org.freedesktop.systemd1.Manager";
|
||||
constexpr char kMethodStartTransientUnit[] = "StartTransientUnit";
|
||||
+constexpr char kMethodGetUnit[] = "GetUnit";
|
||||
+
|
||||
+constexpr char kFakeUnitPath[] = "/fake/unit/path";
|
||||
+constexpr char kActiveState[] = "ActiveState";
|
||||
+constexpr char kStateActive[] = "active";
|
||||
+constexpr char kStateInactive[] = "inactive";
|
||||
+
|
||||
+std::unique_ptr<dbus::Response> CreateActiveStateGetAllResponse(
|
||||
+ const std::string& state) {
|
||||
+ auto response = dbus::Response::CreateEmpty();
|
||||
+ dbus::MessageWriter writer(response.get());
|
||||
+ dbus::MessageWriter array_writer(nullptr);
|
||||
+ dbus::MessageWriter dict_entry_writer(nullptr);
|
||||
+ writer.OpenArray("{sv}", &array_writer);
|
||||
+ array_writer.OpenDictEntry(&dict_entry_writer);
|
||||
+ dict_entry_writer.AppendString(kActiveState);
|
||||
+ dict_entry_writer.AppendVariantOfString(state);
|
||||
+ array_writer.CloseContainer(&dict_entry_writer);
|
||||
+ writer.CloseContainer(&array_writer);
|
||||
+ return response;
|
||||
+}
|
||||
|
||||
class SetSystemdScopeUnitNameForXdgPortalTest : public ::testing::Test {
|
||||
public:
|
||||
@@ -124,17 +147,48 @@ TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, StartTransientUnitSuccess) {
|
||||
|
||||
EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
|
||||
dbus::ObjectPath(kObjectPathSystemd)))
|
||||
- .WillOnce(Return(mock_systemd_proxy.get()));
|
||||
+ .Times(2)
|
||||
+ .WillRepeatedly(Return(mock_systemd_proxy.get()));
|
||||
+
|
||||
+ auto mock_dbus_unit_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
|
||||
+ bus.get(), kServiceNameSystemd, dbus::ObjectPath(kFakeUnitPath));
|
||||
+ EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
|
||||
+ dbus::ObjectPath(kFakeUnitPath)))
|
||||
+ .WillOnce(Return(mock_dbus_unit_proxy.get()));
|
||||
|
||||
EXPECT_CALL(*mock_systemd_proxy, DoCallMethod(_, _, _))
|
||||
.WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ // Expect kMethodStartTransientUnit first.
|
||||
EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
|
||||
EXPECT_EQ(method_call->GetMember(), kMethodStartTransientUnit);
|
||||
|
||||
// Simulate a successful response
|
||||
auto response = dbus::Response::CreateEmpty();
|
||||
std::move(*callback).Run(response.get());
|
||||
+ }))
|
||||
+ .WillOnce(Invoke([obj_path = kFakeUnitPath](
|
||||
+ dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ // Then expect kMethodGetUnit. A valid path must be provided.
|
||||
+ EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
|
||||
+ EXPECT_EQ(method_call->GetMember(), kMethodGetUnit);
|
||||
+
|
||||
+ // Simulate a successful response and provide a fake path to the object.
|
||||
+ auto response = dbus::Response::CreateEmpty();
|
||||
+ dbus::MessageWriter writer(response.get());
|
||||
+ writer.AppendObjectPath(dbus::ObjectPath(obj_path));
|
||||
+ std::move(*callback).Run(response.get());
|
||||
+ }));
|
||||
+
|
||||
+ EXPECT_CALL(*mock_dbus_unit_proxy, DoCallMethod(_, _, _))
|
||||
+ .WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ EXPECT_EQ(method_call->GetInterface(), dbus::kPropertiesInterface);
|
||||
+ EXPECT_EQ(method_call->GetMember(), dbus::kPropertiesGetAll);
|
||||
+ // Simulate a successful response with "active" state.
|
||||
+ auto response = CreateActiveStateGetAllResponse(kStateActive);
|
||||
+ std::move(*callback).Run(response.get());
|
||||
}));
|
||||
|
||||
std::optional<SystemdUnitStatus> status;
|
||||
@@ -189,6 +243,142 @@ TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, StartTransientUnitFailure) {
|
||||
EXPECT_EQ(status, SystemdUnitStatus::kFailedToStart);
|
||||
}
|
||||
|
||||
+TEST_F(SetSystemdScopeUnitNameForXdgPortalTest,
|
||||
+ StartTransientUnitInvalidUnitPath) {
|
||||
+ scoped_refptr<dbus::MockBus> bus =
|
||||
+ base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
|
||||
+
|
||||
+ auto mock_dbus_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
|
||||
+ bus.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));
|
||||
+
|
||||
+ EXPECT_CALL(
|
||||
+ *bus, GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS)))
|
||||
+ .WillRepeatedly(Return(mock_dbus_proxy.get()));
|
||||
+
|
||||
+ EXPECT_CALL(*mock_dbus_proxy, DoCallMethod(_, _, _))
|
||||
+ .WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ auto response = dbus::Response::CreateEmpty();
|
||||
+ dbus::MessageWriter writer(response.get());
|
||||
+ writer.AppendBool(true);
|
||||
+ std::move(*callback).Run(response.get());
|
||||
+ }));
|
||||
+
|
||||
+ auto mock_systemd_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
|
||||
+ bus.get(), kServiceNameSystemd, dbus::ObjectPath(kObjectPathSystemd));
|
||||
+
|
||||
+ EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
|
||||
+ dbus::ObjectPath(kObjectPathSystemd)))
|
||||
+ .Times(2)
|
||||
+ .WillRepeatedly(Return(mock_systemd_proxy.get()));
|
||||
+
|
||||
+ EXPECT_CALL(*mock_systemd_proxy, DoCallMethod(_, _, _))
|
||||
+ .WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
|
||||
+ EXPECT_EQ(method_call->GetMember(), kMethodStartTransientUnit);
|
||||
+
|
||||
+ // Simulate a successful response
|
||||
+ auto response = dbus::Response::CreateEmpty();
|
||||
+ std::move(*callback).Run(response.get());
|
||||
+ }))
|
||||
+ .WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
|
||||
+ EXPECT_EQ(method_call->GetMember(), kMethodGetUnit);
|
||||
+
|
||||
+ // Simulate a failure response.
|
||||
+ std::move(*callback).Run(nullptr);
|
||||
+ }));
|
||||
+
|
||||
+ std::optional<SystemdUnitStatus> status;
|
||||
+
|
||||
+ SetSystemdScopeUnitNameForXdgPortal(
|
||||
+ bus.get(), base::BindLambdaForTesting(
|
||||
+ [&](SystemdUnitStatus result) { status = result; }));
|
||||
+
|
||||
+ EXPECT_EQ(status, SystemdUnitStatus::kFailedToStart);
|
||||
+}
|
||||
+
|
||||
+TEST_F(SetSystemdScopeUnitNameForXdgPortalTest,
|
||||
+ StartTransientUnitFailToActivate) {
|
||||
+ scoped_refptr<dbus::MockBus> bus =
|
||||
+ base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
|
||||
+
|
||||
+ auto mock_dbus_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
|
||||
+ bus.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));
|
||||
+
|
||||
+ EXPECT_CALL(
|
||||
+ *bus, GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS)))
|
||||
+ .WillRepeatedly(Return(mock_dbus_proxy.get()));
|
||||
+
|
||||
+ EXPECT_CALL(*mock_dbus_proxy, DoCallMethod(_, _, _))
|
||||
+ .WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ auto response = dbus::Response::CreateEmpty();
|
||||
+ dbus::MessageWriter writer(response.get());
|
||||
+ writer.AppendBool(true);
|
||||
+ std::move(*callback).Run(response.get());
|
||||
+ }));
|
||||
+
|
||||
+ auto mock_systemd_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
|
||||
+ bus.get(), kServiceNameSystemd, dbus::ObjectPath(kObjectPathSystemd));
|
||||
+
|
||||
+ EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
|
||||
+ dbus::ObjectPath(kObjectPathSystemd)))
|
||||
+ .Times(2)
|
||||
+ .WillRepeatedly(Return(mock_systemd_proxy.get()));
|
||||
+
|
||||
+ auto mock_dbus_unit_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
|
||||
+ bus.get(), kServiceNameSystemd, dbus::ObjectPath(kFakeUnitPath));
|
||||
+ EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
|
||||
+ dbus::ObjectPath(kFakeUnitPath)))
|
||||
+ .WillOnce(Return(mock_dbus_unit_proxy.get()));
|
||||
+
|
||||
+ EXPECT_CALL(*mock_systemd_proxy, DoCallMethod(_, _, _))
|
||||
+ .WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
|
||||
+ EXPECT_EQ(method_call->GetMember(), kMethodStartTransientUnit);
|
||||
+
|
||||
+ // Simulate a successful response
|
||||
+ auto response = dbus::Response::CreateEmpty();
|
||||
+ std::move(*callback).Run(response.get());
|
||||
+ }))
|
||||
+ .WillOnce(Invoke([obj_path = kFakeUnitPath](
|
||||
+ dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
|
||||
+ EXPECT_EQ(method_call->GetMember(), kMethodGetUnit);
|
||||
+
|
||||
+ // Simulate a successful response
|
||||
+ auto response = dbus::Response::CreateEmpty();
|
||||
+ dbus::MessageWriter writer(response.get());
|
||||
+ writer.AppendObjectPath(dbus::ObjectPath(obj_path));
|
||||
+ std::move(*callback).Run(response.get());
|
||||
+ }));
|
||||
+
|
||||
+ EXPECT_CALL(*mock_dbus_unit_proxy, DoCallMethod(_, _, _))
|
||||
+ .WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ // Then expect kMethodGetUnit. A valid path must be provided.
|
||||
+ EXPECT_EQ(method_call->GetInterface(), dbus::kPropertiesInterface);
|
||||
+ EXPECT_EQ(method_call->GetMember(), dbus::kPropertiesGetAll);
|
||||
+
|
||||
+ // Simulate a successful response, but with inactive state.
|
||||
+ auto response = CreateActiveStateGetAllResponse(kStateInactive);
|
||||
+ std::move(*callback).Run(response.get());
|
||||
+ }));
|
||||
+
|
||||
+ std::optional<SystemdUnitStatus> status;
|
||||
+
|
||||
+ SetSystemdScopeUnitNameForXdgPortal(
|
||||
+ bus.get(), base::BindLambdaForTesting(
|
||||
+ [&](SystemdUnitStatus result) { status = result; }));
|
||||
+
|
||||
+ EXPECT_EQ(status, SystemdUnitStatus::kFailedToStart);
|
||||
+}
|
||||
+
|
||||
TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, UnitNameConstruction) {
|
||||
scoped_refptr<dbus::MockBus> bus =
|
||||
base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
|
||||
@@ -220,7 +410,14 @@ TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, UnitNameConstruction) {
|
||||
|
||||
EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
|
||||
dbus::ObjectPath(kObjectPathSystemd)))
|
||||
- .WillOnce(Return(mock_systemd_proxy.get()));
|
||||
+ .Times(2)
|
||||
+ .WillRepeatedly(Return(mock_systemd_proxy.get()));
|
||||
+
|
||||
+ auto mock_dbus_unit_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
|
||||
+ bus.get(), kServiceNameSystemd, dbus::ObjectPath(kFakeUnitPath));
|
||||
+ EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
|
||||
+ dbus::ObjectPath(kFakeUnitPath)))
|
||||
+ .WillOnce(Return(mock_dbus_unit_proxy.get()));
|
||||
|
||||
EXPECT_CALL(*mock_systemd_proxy, DoCallMethod(_, _, _))
|
||||
.WillOnce(Invoke([&](dbus::MethodCall* method_call, int timeout_ms,
|
||||
@@ -256,6 +453,30 @@ TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, UnitNameConstruction) {
|
||||
|
||||
auto response = dbus::Response::CreateEmpty();
|
||||
std::move(*callback).Run(response.get());
|
||||
+ }))
|
||||
+ .WillOnce(Invoke([obj_path = kFakeUnitPath](
|
||||
+ dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
|
||||
+ EXPECT_EQ(method_call->GetMember(), kMethodGetUnit);
|
||||
+
|
||||
+ // Simulate a successful response
|
||||
+ auto response = dbus::Response::CreateEmpty();
|
||||
+ dbus::MessageWriter writer(response.get());
|
||||
+ writer.AppendObjectPath(dbus::ObjectPath(obj_path));
|
||||
+ std::move(*callback).Run(response.get());
|
||||
+ }));
|
||||
+
|
||||
+ EXPECT_CALL(*mock_dbus_unit_proxy, DoCallMethod(_, _, _))
|
||||
+ .WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ // Then expect kMethodGetUnit. A valid path must be provided.
|
||||
+ EXPECT_EQ(method_call->GetInterface(), dbus::kPropertiesInterface);
|
||||
+ EXPECT_EQ(method_call->GetMember(), dbus::kPropertiesGetAll);
|
||||
+
|
||||
+ // Simulate a successful response
|
||||
+ auto response = CreateActiveStateGetAllResponse(kStateActive);
|
||||
+ std::move(*callback).Run(response.get());
|
||||
}));
|
||||
|
||||
std::optional<SystemdUnitStatus> status;
|
||||
@@ -250,8 +250,8 @@ int NodeMain() {
|
||||
|
||||
uint64_t env_flags = node::EnvironmentFlags::kDefaultFlags |
|
||||
node::EnvironmentFlags::kHideConsoleWindows;
|
||||
env = node::CreateEnvironment(
|
||||
isolate_data, isolate->GetCurrentContext(), result->args(),
|
||||
env = electron::util::CreateEnvironment(
|
||||
isolate, isolate_data, isolate->GetCurrentContext(), result->args(),
|
||||
result->exec_args(),
|
||||
static_cast<node::EnvironmentFlags::Flags>(env_flags));
|
||||
CHECK_NE(nullptr, env);
|
||||
|
||||
@@ -4,15 +4,9 @@
|
||||
|
||||
#include "shell/browser/api/electron_api_global_shortcut.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "base/containers/contains.h"
|
||||
#include "base/strings/utf_string_conversions.h"
|
||||
#include "base/uuid.h"
|
||||
#include "components/prefs/pref_service.h"
|
||||
#include "electron/shell/browser/electron_browser_context.h"
|
||||
#include "electron/shell/common/electron_constants.h"
|
||||
#include "extensions/common/command.h"
|
||||
#include "gin/dictionary.h"
|
||||
#include "gin/handle.h"
|
||||
@@ -68,12 +62,7 @@ void GlobalShortcut::OnKeyPressed(const ui::Accelerator& accelerator) {
|
||||
|
||||
void GlobalShortcut::ExecuteCommand(const extensions::ExtensionId& extension_id,
|
||||
const std::string& command_id) {
|
||||
if (!base::Contains(command_callback_map_, command_id)) {
|
||||
// This should never occur, because if it does, GlobalShortcutListener
|
||||
// notifies us with wrong command.
|
||||
NOTREACHED();
|
||||
}
|
||||
command_callback_map_[command_id].Run();
|
||||
// Ignore extension commands
|
||||
}
|
||||
|
||||
bool GlobalShortcut::RegisterAll(
|
||||
@@ -114,56 +103,13 @@ bool GlobalShortcut::Register(const ui::Accelerator& accelerator,
|
||||
}
|
||||
#endif
|
||||
|
||||
auto* instance = GlobalShortcutListener::GetInstance();
|
||||
if (!instance) {
|
||||
if (!GlobalShortcutListener::GetInstance()->RegisterAccelerator(accelerator,
|
||||
this)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instance->IsRegistrationHandledExternally()) {
|
||||
auto* context = ElectronBrowserContext::From("", false);
|
||||
PrefService* prefs = context->prefs();
|
||||
|
||||
// Need a unique profile id. Set one if not generated yet, otherwise re-use
|
||||
// the same so that the session for the globalShortcuts is able to get
|
||||
// already registered shortcuts from the previous session. This will be used
|
||||
// by GlobalShortcutListenerLinux as a session key.
|
||||
std::string profile_id = prefs->GetString(kElectronGlobalShortcutsUuid);
|
||||
if (profile_id.empty()) {
|
||||
profile_id = base::Uuid::GenerateRandomV4().AsLowercaseString();
|
||||
prefs->SetString(kElectronGlobalShortcutsUuid, profile_id);
|
||||
}
|
||||
|
||||
// There is no way to get command id for the accelerator as it's extensions'
|
||||
// thing. Instead, we can convert it to string in a following example form
|
||||
// - std::string("Alt+Shift+K"). That must be sufficient enough for us to
|
||||
// map this accelerator with registered commands.
|
||||
const std::string command_str =
|
||||
extensions::Command::AcceleratorToString(accelerator);
|
||||
ui::CommandMap commands;
|
||||
extensions::Command command(
|
||||
command_str, base::UTF8ToUTF16("Electron shortcut " + command_str),
|
||||
/*accelerator=*/std::string(), /*global=*/true);
|
||||
command.set_accelerator(accelerator);
|
||||
commands[command_str] = command;
|
||||
|
||||
// In order to distinguish the shortcuts, we must register multiple commands
|
||||
// as different extensions. Otherwise, each shortcut will be an alternative
|
||||
// for the very first registered and we'll not be able to distinguish them.
|
||||
// For example, if Alt+Shift+K is registered first, registering and pressing
|
||||
// Alt+Shift+M will trigger global shortcuts, but the command id that is
|
||||
// received by GlobalShortcut will correspond to Alt+Shift+K as our command
|
||||
// id is basically a stringified accelerator.
|
||||
const std::string fake_extension_id = command_str + "+" + profile_id;
|
||||
instance->OnCommandsChanged(fake_extension_id, profile_id, commands, this);
|
||||
command_callback_map_[command_str] = callback;
|
||||
return true;
|
||||
} else {
|
||||
if (instance->RegisterAccelerator(accelerator, this)) {
|
||||
accelerator_callback_map_[accelerator] = callback;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
accelerator_callback_map_[accelerator] = callback;
|
||||
return true;
|
||||
}
|
||||
|
||||
void GlobalShortcut::Unregister(const ui::Accelerator& accelerator) {
|
||||
@@ -181,10 +127,8 @@ void GlobalShortcut::Unregister(const ui::Accelerator& accelerator) {
|
||||
}
|
||||
#endif
|
||||
|
||||
if (GlobalShortcutListener::GetInstance()) {
|
||||
GlobalShortcutListener::GetInstance()->UnregisterAccelerator(accelerator,
|
||||
this);
|
||||
}
|
||||
GlobalShortcutListener::GetInstance()->UnregisterAccelerator(accelerator,
|
||||
this);
|
||||
}
|
||||
|
||||
void GlobalShortcut::UnregisterSome(
|
||||
@@ -195,12 +139,7 @@ void GlobalShortcut::UnregisterSome(
|
||||
}
|
||||
|
||||
bool GlobalShortcut::IsRegistered(const ui::Accelerator& accelerator) {
|
||||
if (base::Contains(accelerator_callback_map_, accelerator)) {
|
||||
return true;
|
||||
}
|
||||
const std::string command_str =
|
||||
extensions::Command::AcceleratorToString(accelerator);
|
||||
return base::Contains(command_callback_map_, command_str);
|
||||
return base::Contains(accelerator_callback_map_, accelerator);
|
||||
}
|
||||
|
||||
void GlobalShortcut::UnregisterAll() {
|
||||
@@ -210,9 +149,7 @@ void GlobalShortcut::UnregisterAll() {
|
||||
return;
|
||||
}
|
||||
accelerator_callback_map_.clear();
|
||||
if (GlobalShortcutListener::GetInstance()) {
|
||||
GlobalShortcutListener::GetInstance()->UnregisterAccelerators(this);
|
||||
}
|
||||
GlobalShortcutListener::GetInstance()->UnregisterAccelerators(this);
|
||||
}
|
||||
|
||||
// static
|
||||
|
||||
@@ -44,7 +44,6 @@ class GlobalShortcut final
|
||||
private:
|
||||
typedef std::map<ui::Accelerator, base::RepeatingClosure>
|
||||
AcceleratorCallbackMap;
|
||||
typedef std::map<std::string, base::RepeatingClosure> CommandCallbackMap;
|
||||
|
||||
bool RegisterAll(const std::vector<ui::Accelerator>& accelerators,
|
||||
const base::RepeatingClosure& callback);
|
||||
@@ -61,7 +60,6 @@ class GlobalShortcut final
|
||||
const std::string& command_id) override;
|
||||
|
||||
AcceleratorCallbackMap accelerator_callback_map_;
|
||||
CommandCallbackMap command_callback_map_;
|
||||
};
|
||||
|
||||
} // namespace electron::api
|
||||
|
||||
@@ -458,10 +458,6 @@ void ElectronBrowserContext::InitPrefs() {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Unique uuid for global shortcuts.
|
||||
registry->RegisterStringPref(electron::kElectronGlobalShortcutsUuid,
|
||||
std::string());
|
||||
}
|
||||
|
||||
void ElectronBrowserContext::SetUserAgent(const std::string& user_agent) {
|
||||
|
||||
@@ -64,5 +64,7 @@
|
||||
<string>${DEFAULT_APP_ASAR_HEADER_SHA}</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSPrefersDisplaySafeAreaCompatibilityMode</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -23,13 +23,6 @@ inline constexpr std::string_view kDeviceSerialNumberKey = "serialNumber";
|
||||
|
||||
inline constexpr base::cstring_view kRunAsNode = "ELECTRON_RUN_AS_NODE";
|
||||
|
||||
// Per-profile UUID to distinguish global shortcut sessions for
|
||||
// org.freedesktop.portal.GlobalShortcuts. This is a counterpart to
|
||||
// extensions::pref_names::kGlobalShortcutsUuid, which may be not defined
|
||||
// if extensions are disabled.
|
||||
inline constexpr char kElectronGlobalShortcutsUuid[] =
|
||||
"electron.global_shortcuts.uuid";
|
||||
|
||||
#if BUILDFLAG(ENABLE_PDF_VIEWER)
|
||||
inline constexpr std::string_view kPDFExtensionPluginName =
|
||||
"Chromium PDF Viewer";
|
||||
|
||||
@@ -649,7 +649,6 @@ std::shared_ptr<node::Environment> NodeBindings::CreateEnvironment(
|
||||
context->SetAlignedPointerInEmbedderData(kElectronContextEmbedderDataIndex,
|
||||
static_cast<void*>(isolate_data));
|
||||
|
||||
node::Environment* env;
|
||||
uint64_t env_flags = node::EnvironmentFlags::kDefaultFlags |
|
||||
node::EnvironmentFlags::kHideConsoleWindows |
|
||||
node::EnvironmentFlags::kNoGlobalSearchPaths |
|
||||
@@ -675,24 +674,10 @@ std::shared_ptr<node::Environment> NodeBindings::CreateEnvironment(
|
||||
env_flags |= node::EnvironmentFlags::kNoStartDebugSignalHandler;
|
||||
}
|
||||
|
||||
{
|
||||
v8::TryCatch try_catch(isolate);
|
||||
env = node::CreateEnvironment(
|
||||
static_cast<node::IsolateData*>(isolate_data), context, args, exec_args,
|
||||
static_cast<node::EnvironmentFlags::Flags>(env_flags));
|
||||
|
||||
if (try_catch.HasCaught()) {
|
||||
std::string err_msg =
|
||||
"Failed to initialize node environment in process: " + process_type;
|
||||
v8::Local<v8::Message> message = try_catch.Message();
|
||||
std::string msg;
|
||||
if (!message.IsEmpty() &&
|
||||
gin::ConvertFromV8(isolate, message->Get(), &msg))
|
||||
err_msg += " , with error: " + msg;
|
||||
LOG(ERROR) << err_msg;
|
||||
}
|
||||
}
|
||||
|
||||
node::Environment* env = electron::util::CreateEnvironment(
|
||||
isolate, static_cast<node::IsolateData*>(isolate_data), context, args,
|
||||
exec_args, static_cast<node::EnvironmentFlags::Flags>(env_flags),
|
||||
process_type);
|
||||
DCHECK(env);
|
||||
|
||||
node::IsolateSettings is;
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
#include "shell/common/node_util.h"
|
||||
|
||||
#include "base/compiler_specific.h"
|
||||
#include "base/containers/to_value_list.h"
|
||||
#include "base/json/json_writer.h"
|
||||
#include "base/logging.h"
|
||||
#include "base/strings/strcat.h"
|
||||
#include "base/strings/string_number_conversions.h"
|
||||
#include "base/values.h"
|
||||
#include "gin/converter.h"
|
||||
#include "gin/dictionary.h"
|
||||
#include "shell/browser/javascript_environment.h"
|
||||
@@ -76,4 +81,51 @@ base::span<uint8_t> as_byte_span(v8::Local<v8::Value> node_buffer) {
|
||||
return UNSAFE_BUFFERS(base::span{data, size});
|
||||
}
|
||||
|
||||
node::Environment* CreateEnvironment(v8::Isolate* isolate,
|
||||
node::IsolateData* isolate_data,
|
||||
v8::Local<v8::Context> context,
|
||||
const std::vector<std::string>& args,
|
||||
const std::vector<std::string>& exec_args,
|
||||
node::EnvironmentFlags::Flags env_flags,
|
||||
std::string_view process_type) {
|
||||
v8::TryCatch try_catch{isolate};
|
||||
node::Environment* env = node::CreateEnvironment(isolate_data, context, args,
|
||||
exec_args, env_flags);
|
||||
if (auto message = try_catch.Message(); !message.IsEmpty()) {
|
||||
base::Value::Dict dict;
|
||||
|
||||
if (std::string str; gin::ConvertFromV8(isolate, message->Get(), &str))
|
||||
dict.Set("message", std::move(str));
|
||||
|
||||
if (std::string str; gin::ConvertFromV8(
|
||||
isolate, message->GetScriptOrigin().ResourceName(), &str)) {
|
||||
const auto line_num = message->GetLineNumber(context).FromJust();
|
||||
const auto line_str = base::NumberToString(line_num);
|
||||
dict.Set("location", base::StrCat({", at ", str, ":", line_str}));
|
||||
}
|
||||
|
||||
if (std::string str; gin::ConvertFromV8(
|
||||
isolate, message->GetSourceLine(context).ToLocalChecked(), &str))
|
||||
dict.Set("source_line", std::move(str));
|
||||
|
||||
if (!std::empty(process_type))
|
||||
dict.Set("process_type", process_type);
|
||||
|
||||
if (auto list = base::ToValueList(args); !std::empty(list))
|
||||
dict.Set("args", std::move(list));
|
||||
|
||||
if (auto list = base::ToValueList(exec_args); !std::empty(list))
|
||||
dict.Set("exec_args", std::move(list));
|
||||
|
||||
std::string errstr = "Failed to initialize Node.js.";
|
||||
if (std::optional<std::string> jsonstr = base::WriteJsonWithOptions(
|
||||
dict, base::JsonOptions::OPTIONS_PRETTY_PRINT))
|
||||
errstr += base::StrCat({" ", *jsonstr});
|
||||
|
||||
LOG(ERROR) << errstr;
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
} // namespace electron::util
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#ifndef ELECTRON_SHELL_COMMON_NODE_UTIL_H_
|
||||
#define ELECTRON_SHELL_COMMON_NODE_UTIL_H_
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
@@ -13,7 +14,13 @@
|
||||
|
||||
namespace node {
|
||||
class Environment;
|
||||
class IsolateData;
|
||||
struct ThreadId;
|
||||
|
||||
namespace EnvironmentFlags {
|
||||
enum Flags : uint64_t;
|
||||
}
|
||||
} // namespace node
|
||||
|
||||
namespace electron::util {
|
||||
|
||||
@@ -37,6 +44,15 @@ v8::MaybeLocal<v8::Value> CompileAndCall(
|
||||
std::vector<v8::Local<v8::String>>* parameters,
|
||||
std::vector<v8::Local<v8::Value>>* arguments);
|
||||
|
||||
// Wrapper for node::CreateEnvironment that logs failure
|
||||
node::Environment* CreateEnvironment(v8::Isolate* isolate,
|
||||
node::IsolateData* isolate_data,
|
||||
v8::Local<v8::Context> context,
|
||||
const std::vector<std::string>& args,
|
||||
const std::vector<std::string>& exec_args,
|
||||
node::EnvironmentFlags::Flags env_flags,
|
||||
std::string_view process_type = "");
|
||||
|
||||
// Convenience function to view a Node buffer's data as a base::span().
|
||||
// Analogous to base::as_byte_span()
|
||||
[[nodiscard]] base::span<uint8_t> as_byte_span(
|
||||
|
||||
@@ -16,8 +16,7 @@ import { setTimeout } from 'node:timers/promises';
|
||||
import * as nodeUrl from 'node:url';
|
||||
|
||||
import { emittedUntil, emittedNTimes } from './lib/events-helpers';
|
||||
import { debugPreviewImage } from './lib/image-helpers';
|
||||
import { HexColors, hasCapturableScreen, ScreenCapture, getPixelColor } from './lib/screen-helpers';
|
||||
import { HexColors, hasCapturableScreen, ScreenCapture } from './lib/screen-helpers';
|
||||
import { ifit, ifdescribe, defer, listen, waitUntil } from './lib/spec-helpers';
|
||||
import { closeWindow, closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
@@ -1273,7 +1272,6 @@ describe('BrowserWindow module', () => {
|
||||
// We first need to resign app focus for this test to work
|
||||
const isInactive = once(app, 'did-resign-active');
|
||||
childProcess.execSync('osascript -e \'tell application "Finder" to activate\'');
|
||||
defer(() => childProcess.execSync('osascript -e \'tell application "Finder" to quit\''));
|
||||
await isInactive;
|
||||
|
||||
// Create new window
|
||||
@@ -6460,51 +6458,12 @@ describe('BrowserWindow module', () => {
|
||||
const [, , data] = await paint;
|
||||
expect(data.constructor.name).to.equal('NativeImage');
|
||||
expect(data.isEmpty()).to.be.false('data is empty');
|
||||
await debugPreviewImage(data);
|
||||
const size = data.getSize();
|
||||
const { scaleFactor } = screen.getPrimaryDisplay();
|
||||
expect(size.width).to.be.closeTo(100 * scaleFactor, 2);
|
||||
expect(size.height).to.be.closeTo(100 * scaleFactor, 2);
|
||||
});
|
||||
|
||||
it('creates offscreen window with opaque background', async () => {
|
||||
w.setBackgroundColor(HexColors.RED);
|
||||
await waitUntil(async () => {
|
||||
const paint = once(w.webContents, 'paint') as Promise<[any, Electron.Rectangle, Electron.NativeImage]>;
|
||||
w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html'));
|
||||
const [, , data] = await paint;
|
||||
await debugPreviewImage(data);
|
||||
expect(getPixelColor(data, { x: 0, y: 0 }, true)).to.equal('#ff0000ff');
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
it('creates offscreen window with transparent background', async () => {
|
||||
w.setBackgroundColor(HexColors.TRANSPARENT);
|
||||
await waitUntil(async () => {
|
||||
const paint = once(w.webContents, 'paint') as Promise<[any, Electron.Rectangle, Electron.NativeImage]>;
|
||||
w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html'));
|
||||
const [, , data] = await paint;
|
||||
await debugPreviewImage(data);
|
||||
expect(getPixelColor(data, { x: 0, y: 0 }, true)).to.equal(HexColors.TRANSPARENT);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// Semi-transparent background is not supported
|
||||
it.skip('creates offscreen window with semi-transparent background', async () => {
|
||||
const bgColor = '#66ffffff'; // ARGB
|
||||
w.setBackgroundColor(bgColor);
|
||||
await waitUntil(async () => {
|
||||
const paint = once(w.webContents, 'paint') as Promise<[any, Electron.Rectangle, Electron.NativeImage]>;
|
||||
w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html'));
|
||||
const [, , data] = await paint;
|
||||
await debugPreviewImage(data);
|
||||
expect(getPixelColor(data, { x: 0, y: 0 }, true)).to.equal(bgColor);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
it('does not crash after navigation', () => {
|
||||
w.webContents.loadURL('about:blank');
|
||||
w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html'));
|
||||
|
||||
@@ -99,8 +99,7 @@ ifdescribe(!(['arm', 'arm64'].includes(process.arch)) || (process.platform !== '
|
||||
this.timeout(5e3);
|
||||
}
|
||||
|
||||
// FIXME(samuelmaddock): this test regularly flakes
|
||||
it.skip('does not crash on empty string', async () => {
|
||||
it('does not crash on empty string', async () => {
|
||||
const options = {
|
||||
categoryFilter: '*',
|
||||
traceOptions: 'record-until-full,enable-sampling'
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { BaseWindow, NativeImage } from 'electron';
|
||||
|
||||
import { once } from 'node:events';
|
||||
|
||||
/**
|
||||
* Opens a window to display a native image. Useful for quickly debugging tests
|
||||
* rather than writing a file and opening manually.
|
||||
*
|
||||
* Set the `DEBUG_PREVIEW_IMAGE` environment variable to show previews.
|
||||
*/
|
||||
export async function debugPreviewImage (image: NativeImage) {
|
||||
if (!process.env.DEBUG_PREVIEW_IMAGE) return;
|
||||
const previewWindow = new BaseWindow({
|
||||
title: 'NativeImage preview',
|
||||
backgroundColor: '#444444'
|
||||
});
|
||||
const ImageView = (require('electron') as any).ImageView;
|
||||
const imgView = new ImageView();
|
||||
imgView.setImage(image);
|
||||
previewWindow.contentView.addChildView(imgView);
|
||||
imgView.setBounds({ x: 0, y: 0, ...image.getSize() });
|
||||
await once(previewWindow, 'close');
|
||||
};
|
||||
@@ -10,7 +10,6 @@ export enum HexColors {
|
||||
RED = '#ff0000',
|
||||
BLUE = '#0000ff',
|
||||
WHITE = '#ffffff',
|
||||
TRANSPARENT = '#00000000'
|
||||
}
|
||||
|
||||
function hexToRgba (
|
||||
@@ -36,10 +35,9 @@ function formatHexByte (val: number): string {
|
||||
/**
|
||||
* Get the hex color at the given pixel coordinate in an image.
|
||||
*/
|
||||
export function getPixelColor (
|
||||
function getPixelColor (
|
||||
image: Electron.NativeImage,
|
||||
point: Electron.Point,
|
||||
includeAlpha: boolean = false
|
||||
point: Electron.Point
|
||||
): string {
|
||||
// image.crop crashes if point is fractional, so round to prevent that crash
|
||||
const pixel = image.crop({
|
||||
@@ -50,10 +48,8 @@ export function getPixelColor (
|
||||
});
|
||||
// TODO(samuelmaddock): NativeImage.toBitmap() should return the raw pixel
|
||||
// color, but it sometimes differs. Why is that?
|
||||
const [b, g, r, a] = pixel.toBitmap();
|
||||
let hex = `#${formatHexByte(r)}${formatHexByte(g)}${formatHexByte(b)}`;
|
||||
if (includeAlpha) hex += `${formatHexByte(a)}`;
|
||||
return hex;
|
||||
const [b, g, r] = pixel.toBitmap();
|
||||
return `#${formatHexByte(r)}${formatHexByte(g)}${formatHexByte(b)}`;
|
||||
}
|
||||
|
||||
/** Calculate euclidean distance between colors. */
|
||||
@@ -72,7 +68,7 @@ function colorDistance (hexColorA: string, hexColorB: string): number {
|
||||
* Determine if colors are similar based on distance. This can be useful when
|
||||
* comparing colors which may differ based on lossy compression.
|
||||
*/
|
||||
export function areColorsSimilar (
|
||||
function areColorsSimilar (
|
||||
hexColorA: string,
|
||||
hexColorB: string,
|
||||
distanceThreshold = 90
|
||||
|
||||
Reference in New Issue
Block a user