mirror of
https://github.com/electron/electron.git
synced 2026-03-19 03:02:02 -04:00
Compare commits
7 Commits
nikwen/ext
...
fix-trace-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0b644e80f | ||
|
|
378659c535 | ||
|
|
6be775ad83 | ||
|
|
11f28ac3ac | ||
|
|
5ec589a1de | ||
|
|
4fe3752fae | ||
|
|
c8dd0b99ee |
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"plugins": [
|
||||
["remark-lint-code-block-style", "fenced"],
|
||||
["remark-lint-fenced-code-flag"]
|
||||
]
|
||||
}
|
||||
@@ -2,7 +2,7 @@ is_electron_build = true
|
||||
root_extra_deps = [ "//electron" ]
|
||||
|
||||
# Registry of NMVs --> https://github.com/nodejs/node/blob/main/doc/abi_version_registry.json
|
||||
node_module_version = 145
|
||||
node_module_version = 146
|
||||
|
||||
v8_promise_internal_field_count = 1
|
||||
v8_embedder_string = "-electron.0"
|
||||
|
||||
@@ -245,6 +245,10 @@ static_library("chrome") {
|
||||
"//chrome/browser/ui/views/dark_mode_manager_linux.cc",
|
||||
"//chrome/browser/ui/views/dark_mode_manager_linux.h",
|
||||
]
|
||||
sources += [
|
||||
"//chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.cc",
|
||||
"//chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.h",
|
||||
]
|
||||
public_deps += [ "//components/dbus" ]
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ etc.
|
||||
|
||||
## Documentation
|
||||
|
||||
* Write [remark](https://github.com/remarkjs/remark) markdown style.
|
||||
* Write prose according to our [documentation style guide](./style-guide.md).
|
||||
|
||||
You can run `npm run lint:docs` to ensure that your documentation changes are
|
||||
formatted correctly.
|
||||
|
||||
@@ -45,8 +45,6 @@
|
||||
"null-loader": "^4.0.1",
|
||||
"pre-flight": "^2.0.0",
|
||||
"process": "^0.11.10",
|
||||
"remark-cli": "^12.0.1",
|
||||
"remark-preset-lint-markdown-style-guide": "^6.0.1",
|
||||
"semver": "^7.6.3",
|
||||
"stream-json": "^1.9.1",
|
||||
"tap-xunit": "^2.4.1",
|
||||
@@ -73,7 +71,7 @@
|
||||
"lint:objc": "node ./script/lint.js --objc",
|
||||
"lint:py": "node ./script/lint.js --py",
|
||||
"lint:gn": "node ./script/lint.js --gn",
|
||||
"lint:docs": "remark docs -qf && npm run lint:js-in-markdown && npm run create-typescript-definitions && npm run lint:ts-check-js-in-markdown && npm run lint:docs-fiddles && npm run lint:docs-relative-links && npm run lint:markdown && npm run lint:api-history",
|
||||
"lint:docs": "npm run lint:js-in-markdown && npm run create-typescript-definitions && npm run lint:ts-check-js-in-markdown && npm run lint:docs-fiddles && npm run lint:docs-relative-links && npm run lint:markdown && npm run lint:api-history",
|
||||
"lint:docs-fiddles": "standard \"docs/fiddles/**/*.js\"",
|
||||
"lint:docs-relative-links": "lint-roller-markdown-links --resource-root . --root docs \"**/*.md\"",
|
||||
"lint:markdown": "node ./script/lint.js --md",
|
||||
|
||||
@@ -8,10 +8,10 @@ electron objects that extend gin::Wrappable and gets
|
||||
allocated on the cpp heap
|
||||
|
||||
diff --git a/gin/public/wrappable_pointer_tags.h b/gin/public/wrappable_pointer_tags.h
|
||||
index c29e8554933994ff56ccea394af34e17c4e9fc2c..42512541b36ceb353483a29eca2c858b9628854b 100644
|
||||
index c29e8554933994ff56ccea394af34e17c4e9fc2c..6befb717f83d93d97033c240aa281e0bcb94e69c 100644
|
||||
--- a/gin/public/wrappable_pointer_tags.h
|
||||
+++ b/gin/public/wrappable_pointer_tags.h
|
||||
@@ -76,7 +76,19 @@ enum WrappablePointerTag : uint16_t {
|
||||
@@ -76,7 +76,20 @@ enum WrappablePointerTag : uint16_t {
|
||||
kTextInputControllerBindings, // content::TextInputControllerBindings
|
||||
kWebAXObjectProxy, // content::WebAXObjectProxy
|
||||
kWrappedExceptionHandler, // extensions::WrappedExceptionHandler
|
||||
@@ -27,6 +27,7 @@ index c29e8554933994ff56ccea394af34e17c4e9fc2c..42512541b36ceb353483a29eca2c858b
|
||||
+ kElectronReplyChannel, // gin_helper::internal::ReplyChannel
|
||||
+ kElectronScreen, // electron::api::Screen
|
||||
+ kElectronSession, // electron::api::Session
|
||||
+ kElectronTray, // electron::api::Tray
|
||||
+ kElectronWebRequest, // electron::api::WebRequest
|
||||
+ kLastPointerTag = kElectronWebRequest,
|
||||
};
|
||||
|
||||
@@ -63,6 +63,31 @@ index f8b4fd7c4ca5a0907806c7e804de8c951675a36a..209e3bcf8be5a23ac528dcd673bed82c
|
||||
}
|
||||
|
||||
function ipToInt(ip) {
|
||||
diff --git a/lib/internal/trace_events_async_hooks.js b/lib/internal/trace_events_async_hooks.js
|
||||
index a9f517ffc9e4eea5bc68997ffadc85d43dde2a52..d3db6bf119a6bb9cea1d069957f89cc7f99512b7 100644
|
||||
--- a/lib/internal/trace_events_async_hooks.js
|
||||
+++ b/lib/internal/trace_events_async_hooks.js
|
||||
@@ -11,15 +11,13 @@ const { trace } = internalBinding('trace_events');
|
||||
const async_wrap = internalBinding('async_wrap');
|
||||
const async_hooks = require('async_hooks');
|
||||
const {
|
||||
- CHAR_LOWERCASE_B,
|
||||
- CHAR_LOWERCASE_E,
|
||||
+ CHAR_UPPERCASE_B,
|
||||
+ CHAR_UPPERCASE_E,
|
||||
} = require('internal/constants');
|
||||
|
||||
-// Use small letters such that chrome://tracing groups by the name.
|
||||
-// The behavior is not only useful but the same as the events emitted using
|
||||
-// the specific C++ macros.
|
||||
-const kBeforeEvent = CHAR_LOWERCASE_B;
|
||||
-const kEndEvent = CHAR_LOWERCASE_E;
|
||||
+// See v8/src/builtins/builtins-trace.cc - must be uppercase for perfetto
|
||||
+const kBeforeEvent = CHAR_UPPERCASE_B;
|
||||
+const kEndEvent = CHAR_UPPERCASE_E;
|
||||
const kTraceEventCategory = 'node,node.async_hooks';
|
||||
|
||||
const kEnabled = Symbol('enabled');
|
||||
diff --git a/node.gyp b/node.gyp
|
||||
index f5cd416b5fe7a51084bc4af9a4427a8e62599fd8..5eb70ce3820f2b82121bc102c5182ab768cbef36 100644
|
||||
--- a/node.gyp
|
||||
|
||||
@@ -9,5 +9,5 @@ refactor_use_non-deprecated_nskeyedarchiver_apis.patch
|
||||
chore_turn_off_launchapplicationaturl_deprecation_errors_in_squirrel.patch
|
||||
fix_crash_when_process_to_extract_zip_cannot_be_launched.patch
|
||||
use_uttype_class_instead_of_deprecated_uttypeconformsto.patch
|
||||
fix_clean_up_old_staged_updates_before_downloading_new_update.patch
|
||||
fix_clean_up_orphaned_staged_updates_before_downloading_new_update.patch
|
||||
fix_add_explicit_json_property_mappings_for_shipit_request_model.patch
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Andy Locascio <loc@anthropic.com>
|
||||
Date: Tue, 6 Jan 2026 08:23:03 -0800
|
||||
Subject: fix: clean up old staged updates before downloading new update
|
||||
|
||||
When checkForUpdates() is called while an update is already staged,
|
||||
Squirrel creates a new temporary directory for the download without
|
||||
cleaning up the old one. This can lead to significant disk usage if
|
||||
the app keeps checking for updates without restarting.
|
||||
|
||||
This change adds a force parameter to pruneUpdateDirectories that
|
||||
bypasses the AwaitingRelaunch state check. This is called before
|
||||
creating a new temp directory, ensuring old staged updates are
|
||||
cleaned up when a new download starts.
|
||||
|
||||
diff --git a/Squirrel/SQRLUpdater.m b/Squirrel/SQRLUpdater.m
|
||||
index d156616e81e6f25a3bded30e6216b8fc311f31bc..6cd4346bf43b191147aff819cb93387e71275a46 100644
|
||||
--- a/Squirrel/SQRLUpdater.m
|
||||
+++ b/Squirrel/SQRLUpdater.m
|
||||
@@ -543,11 +543,17 @@ - (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL
|
||||
#pragma mark File Management
|
||||
|
||||
- (RACSignal *)uniqueTemporaryDirectoryForUpdate {
|
||||
- return [[[RACSignal
|
||||
+ // Clean up any old staged update directories before creating a new one.
|
||||
+ // This prevents disk usage from growing when checkForUpdates() is called
|
||||
+ // multiple times without the app restarting.
|
||||
+ return [[[[[self
|
||||
+ pruneUpdateDirectoriesWithForce:YES]
|
||||
+ ignoreValues]
|
||||
+ concat:[RACSignal
|
||||
defer:^{
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
|
||||
return [directoryManager storageURL];
|
||||
- }]
|
||||
+ }]]
|
||||
flattenMap:^(NSURL *storageURL) {
|
||||
NSURL *updateDirectoryTemplate = [storageURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
|
||||
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
|
||||
@@ -643,7 +649,7 @@ - (BOOL)isRunningOnReadOnlyVolume {
|
||||
|
||||
- (RACSignal *)performHousekeeping {
|
||||
return [[RACSignal
|
||||
- merge:@[ [self pruneUpdateDirectories], [self truncateLogs] ]]
|
||||
+ merge:@[ [self pruneUpdateDirectoriesWithForce:NO], [self truncateLogs] ]]
|
||||
catch:^(NSError *error) {
|
||||
NSLog(@"Error doing housekeeping: %@", error);
|
||||
return [RACSignal empty];
|
||||
@@ -658,11 +664,12 @@ - (RACSignal *)performHousekeeping {
|
||||
///
|
||||
/// Sends each removed directory then completes, or errors, on an unspecified
|
||||
/// thread.
|
||||
-- (RACSignal *)pruneUpdateDirectories {
|
||||
+- (RACSignal *)pruneUpdateDirectoriesWithForce:(BOOL)force {
|
||||
return [[[RACSignal
|
||||
defer:^{
|
||||
- // If we already have updates downloaded we don't wanna prune them.
|
||||
- if (self.state == SQRLUpdaterStateAwaitingRelaunch) return [RACSignal empty];
|
||||
+ // If we already have updates downloaded we don't wanna prune them,
|
||||
+ // unless force is YES (used when starting a new download).
|
||||
+ if (!force && self.state == SQRLUpdaterStateAwaitingRelaunch) return [RACSignal empty];
|
||||
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
|
||||
return [directoryManager storageURL];
|
||||
@@ -0,0 +1,130 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Andy Locascio <loc@anthropic.com>
|
||||
Date: Tue, 6 Jan 2026 08:23:03 -0800
|
||||
Subject: fix: clean up orphaned staged updates before downloading new update
|
||||
|
||||
When checkForUpdates() is called while an update is already staged,
|
||||
Squirrel creates a new temporary directory for the download without
|
||||
cleaning up the old one. This can lead to significant disk usage if
|
||||
the app keeps checking for updates without restarting.
|
||||
|
||||
This change adds a pruneOrphanedUpdateDirectories step before creating
|
||||
a new temp directory. Unlike a blanket prune, this reads the current
|
||||
ShipItState.plist and preserves the directory it references, deleting
|
||||
only truly orphaned update directories. This keeps the on-disk
|
||||
footprint bounded (at most 2 dirs) while ensuring quitAndInstall
|
||||
remains safe to call even when a new check is in progress.
|
||||
|
||||
Refs https://github.com/electron/electron/issues/50200
|
||||
|
||||
diff --git a/Squirrel/SQRLUpdater.m b/Squirrel/SQRLUpdater.m
|
||||
index d156616e81e6f25a3bded30e6216b8fc311f31bc..41856e5754228d33982db72f97f2ff241615a357 100644
|
||||
--- a/Squirrel/SQRLUpdater.m
|
||||
+++ b/Squirrel/SQRLUpdater.m
|
||||
@@ -543,11 +543,19 @@ - (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL
|
||||
#pragma mark File Management
|
||||
|
||||
- (RACSignal *)uniqueTemporaryDirectoryForUpdate {
|
||||
- return [[[RACSignal
|
||||
+ // Clean up any orphaned update directories before creating a new one.
|
||||
+ // This prevents disk usage from growing when checkForUpdates() is called
|
||||
+ // multiple times without the app restarting. The currently staged update
|
||||
+ // (referenced by ShipItState.plist) is always preserved so quitAndInstall
|
||||
+ // remains safe to call while a new check is in progress.
|
||||
+ return [[[[[self
|
||||
+ pruneOrphanedUpdateDirectories]
|
||||
+ ignoreValues]
|
||||
+ concat:[RACSignal
|
||||
defer:^{
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
|
||||
return [directoryManager storageURL];
|
||||
- }]
|
||||
+ }]]
|
||||
flattenMap:^(NSURL *storageURL) {
|
||||
NSURL *updateDirectoryTemplate = [storageURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
|
||||
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
|
||||
@@ -668,25 +676,68 @@ - (RACSignal *)pruneUpdateDirectories {
|
||||
return [directoryManager storageURL];
|
||||
}]
|
||||
flattenMap:^(NSURL *storageURL) {
|
||||
- NSFileManager *manager = [[NSFileManager alloc] init];
|
||||
- NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:storageURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
|
||||
- NSLog(@"Error enumerating item %@ within directory %@: %@", URL, storageURL, error);
|
||||
- return YES;
|
||||
- }];
|
||||
+ return [self removeUpdateDirectoriesInStorageURL:storageURL excludingURL:nil];
|
||||
+ }]
|
||||
+ setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
|
||||
+}
|
||||
|
||||
- return [[enumerator.rac_sequence.signal
|
||||
- filter:^(NSURL *enumeratedURL) {
|
||||
- NSString *name = enumeratedURL.lastPathComponent;
|
||||
- return [name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix];
|
||||
- }]
|
||||
- doNext:^(NSURL *directoryURL) {
|
||||
- NSError *error = nil;
|
||||
- if (![manager removeItemAtURL:directoryURL error:&error]) {
|
||||
- NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
|
||||
- }
|
||||
+/// Lazily removes orphaned temporary directories upon subscription, always
|
||||
+/// preserving the directory currently referenced by ShipItState.plist so that
|
||||
+/// quitAndInstall remains safe to call mid-check.
|
||||
+///
|
||||
+/// Safe to call in any state. Sends each removed directory then completes on
|
||||
+/// an unspecified thread. Errors reading the staged request are swallowed
|
||||
+/// (treated as "nothing staged").
|
||||
+- (RACSignal *)pruneOrphanedUpdateDirectories {
|
||||
+ return [[[[[SQRLShipItRequest
|
||||
+ readUsingURL:self.shipItStateURL]
|
||||
+ map:^(SQRLShipItRequest *request) {
|
||||
+ // The request holds the URL to the staged .app bundle; its parent
|
||||
+ // is the update.XXXXXXX directory we must preserve.
|
||||
+ return [request.updateBundleURL URLByDeletingLastPathComponent];
|
||||
+ }]
|
||||
+ catch:^(NSError *error) {
|
||||
+ // No staged request (or unreadable) — nothing to preserve.
|
||||
+ return [RACSignal return:nil];
|
||||
+ }]
|
||||
+ flattenMap:^(NSURL *stagedDirectoryURL) {
|
||||
+ SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
|
||||
+ return [[directoryManager storageURL]
|
||||
+ flattenMap:^(NSURL *storageURL) {
|
||||
+ return [self removeUpdateDirectoriesInStorageURL:storageURL excludingURL:stagedDirectoryURL];
|
||||
}];
|
||||
}]
|
||||
- setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
|
||||
+ setNameWithFormat:@"%@ -pruneOrphanedUpdateDirectories", self];
|
||||
+}
|
||||
+
|
||||
+/// Shared enumerate-and-delete logic for update temp directories.
|
||||
+///
|
||||
+/// storageURL - The Squirrel storage root to enumerate. Must not be nil.
|
||||
+/// excludedURL - Directory to skip (compared by standardized path). May be nil.
|
||||
+- (RACSignal *)removeUpdateDirectoriesInStorageURL:(NSURL *)storageURL excludingURL:(NSURL *)excludedURL {
|
||||
+ NSParameterAssert(storageURL != nil);
|
||||
+
|
||||
+ NSFileManager *manager = [[NSFileManager alloc] init];
|
||||
+ NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:storageURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
|
||||
+ NSLog(@"Error enumerating item %@ within directory %@: %@", URL, storageURL, error);
|
||||
+ return YES;
|
||||
+ }];
|
||||
+
|
||||
+ NSString *excludedPath = excludedURL.URLByStandardizingPath.path;
|
||||
+
|
||||
+ return [[enumerator.rac_sequence.signal
|
||||
+ filter:^(NSURL *enumeratedURL) {
|
||||
+ NSString *name = enumeratedURL.lastPathComponent;
|
||||
+ if (![name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix]) return NO;
|
||||
+ if (excludedPath != nil && [enumeratedURL.URLByStandardizingPath.path isEqualToString:excludedPath]) return NO;
|
||||
+ return YES;
|
||||
+ }]
|
||||
+ doNext:^(NSURL *directoryURL) {
|
||||
+ NSError *error = nil;
|
||||
+ if (![manager removeItemAtURL:directoryURL error:&error]) {
|
||||
+ NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
|
||||
+ }
|
||||
+ }];
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#include "shell/common/gin_helper/error_thrower.h"
|
||||
#include "shell/common/gin_helper/handle.h"
|
||||
#include "shell/common/node_includes.h"
|
||||
#include "v8/include/cppgc/allocation.h"
|
||||
#include "v8/include/v8-cppgc.h"
|
||||
|
||||
namespace gin {
|
||||
|
||||
@@ -48,12 +50,13 @@ struct Converter<electron::TrayIcon::IconType> {
|
||||
|
||||
namespace electron::api {
|
||||
|
||||
gin::DeprecatedWrapperInfo Tray::kWrapperInfo = {gin::kEmbedderNativeGin};
|
||||
const gin::WrapperInfo Tray::kWrapperInfo = {{gin::kEmbedderNativeGin},
|
||||
gin::kElectronTray};
|
||||
|
||||
Tray::Tray(v8::Isolate* isolate,
|
||||
v8::Local<v8::Value> image,
|
||||
std::optional<base::Uuid> guid)
|
||||
: guid_(guid), tray_icon_(TrayIcon::Create(guid)) {
|
||||
: guid_{guid}, tray_icon_{TrayIcon::Create(guid)} {
|
||||
SetImage(isolate, image);
|
||||
tray_icon_->AddObserver(this);
|
||||
if (guid.has_value())
|
||||
@@ -63,10 +66,10 @@ Tray::Tray(v8::Isolate* isolate,
|
||||
Tray::~Tray() = default;
|
||||
|
||||
// static
|
||||
gin_helper::Handle<Tray> Tray::New(gin_helper::ErrorThrower thrower,
|
||||
v8::Local<v8::Value> image,
|
||||
std::optional<base::Uuid> guid,
|
||||
gin::Arguments* args) {
|
||||
Tray* Tray::New(gin_helper::ErrorThrower thrower,
|
||||
v8::Local<v8::Value> image,
|
||||
std::optional<base::Uuid> guid,
|
||||
gin::Arguments* args) {
|
||||
if (!Browser::Get()->is_ready()) {
|
||||
thrower.ThrowError("Cannot create Tray before app is ready");
|
||||
return {};
|
||||
@@ -80,17 +83,17 @@ gin_helper::Handle<Tray> Tray::New(gin_helper::ErrorThrower thrower,
|
||||
// Error thrown by us will be dropped when entering V8.
|
||||
// Make sure to abort early and propagate the error to JS.
|
||||
// Refs https://chromium-review.googlesource.com/c/v8/v8/+/5050065
|
||||
v8::TryCatch try_catch(args->isolate());
|
||||
auto* tray = new Tray(args->isolate(), image, guid);
|
||||
v8::Isolate* isolate = args->isolate();
|
||||
v8::TryCatch try_catch{isolate};
|
||||
Tray* tray = cppgc::MakeGarbageCollected<Tray>(
|
||||
isolate->GetCppHeap()->GetAllocationHandle(), isolate, image, guid);
|
||||
if (try_catch.HasCaught()) {
|
||||
delete tray;
|
||||
tray->keep_alive_.Clear();
|
||||
try_catch.ReThrow();
|
||||
return {};
|
||||
}
|
||||
|
||||
auto handle = gin_helper::CreateHandle(args->isolate(), tray);
|
||||
handle->Pin(args->isolate());
|
||||
return handle;
|
||||
return tray;
|
||||
}
|
||||
|
||||
void Tray::OnClicked(const gfx::Rect& bounds,
|
||||
@@ -186,9 +189,9 @@ void Tray::OnDragEnded() {
|
||||
}
|
||||
|
||||
void Tray::Destroy() {
|
||||
Unpin();
|
||||
menu_.Reset();
|
||||
menu_.Clear();
|
||||
tray_icon_.reset();
|
||||
keep_alive_.Clear();
|
||||
}
|
||||
|
||||
bool Tray::IsDestroyed() {
|
||||
@@ -374,12 +377,13 @@ void Tray::SetContextMenu(gin_helper::ErrorThrower thrower,
|
||||
v8::Local<v8::Value> arg) {
|
||||
if (!CheckAlive())
|
||||
return;
|
||||
gin_helper::Handle<Menu> menu;
|
||||
|
||||
if (arg->IsNull()) {
|
||||
menu_.Reset();
|
||||
menu_.Clear();
|
||||
tray_icon_->SetContextMenu(nullptr);
|
||||
} else if (gin::ConvertFromV8(thrower.isolate(), arg, &menu)) {
|
||||
menu_.Reset(thrower.isolate(), menu.ToV8());
|
||||
} else if (Menu* menu = nullptr;
|
||||
gin::ConvertFromV8(thrower.isolate(), arg, &menu)) {
|
||||
menu_ = menu;
|
||||
tray_icon_->SetContextMenu(menu->model());
|
||||
} else {
|
||||
thrower.ThrowTypeError("Must pass Menu or null");
|
||||
@@ -437,12 +441,17 @@ void Tray::FillObjectTemplate(v8::Isolate* isolate,
|
||||
.Build();
|
||||
}
|
||||
|
||||
const char* Tray::GetTypeName() {
|
||||
return GetClassName();
|
||||
void Tray::Trace(cppgc::Visitor* visitor) const {
|
||||
gin::Wrappable<Tray>::Trace(visitor);
|
||||
visitor->Trace(menu_);
|
||||
}
|
||||
|
||||
void Tray::WillBeDestroyed() {
|
||||
ClearWeak();
|
||||
const gin::WrapperInfo* Tray::wrapper_info() const {
|
||||
return &kWrapperInfo;
|
||||
}
|
||||
|
||||
const char* Tray::GetHumanReadableName() const {
|
||||
return "Electron / Tray";
|
||||
}
|
||||
|
||||
} // namespace electron::api
|
||||
@@ -457,7 +466,7 @@ void Initialize(v8::Local<v8::Object> exports,
|
||||
void* priv) {
|
||||
v8::Isolate* const isolate = electron::JavascriptEnvironment::GetIsolate();
|
||||
gin::Dictionary dict{isolate, exports};
|
||||
dict.Set("Tray", Tray::GetConstructor(isolate, context));
|
||||
dict.Set("Tray", Tray::GetConstructor(isolate, context, &Tray::kWrapperInfo));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -10,14 +10,13 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "gin/wrappable.h"
|
||||
#include "shell/browser/event_emitter_mixin.h"
|
||||
#include "shell/browser/ui/tray_icon.h"
|
||||
#include "shell/browser/ui/tray_icon_observer.h"
|
||||
#include "shell/common/gin_converters/guid_converter.h"
|
||||
#include "shell/common/gin_helper/cleaned_up_at_exit.h"
|
||||
#include "shell/common/gin_helper/constructible.h"
|
||||
#include "shell/common/gin_helper/pinnable.h"
|
||||
#include "shell/common/gin_helper/wrappable.h"
|
||||
#include "shell/common/gin_helper/self_keep_alive.h"
|
||||
|
||||
namespace gfx {
|
||||
class Image;
|
||||
@@ -27,47 +26,43 @@ class Image;
|
||||
namespace gin_helper {
|
||||
class Dictionary;
|
||||
class ErrorThrower;
|
||||
template <typename T>
|
||||
class Handle;
|
||||
} // namespace gin_helper
|
||||
|
||||
namespace electron::api {
|
||||
|
||||
class Menu;
|
||||
|
||||
class Tray final : public gin_helper::DeprecatedWrappable<Tray>,
|
||||
class Tray final : public gin::Wrappable<Tray>,
|
||||
public gin_helper::EventEmitterMixin<Tray>,
|
||||
public gin_helper::Constructible<Tray>,
|
||||
public gin_helper::CleanedUpAtExit,
|
||||
public gin_helper::Pinnable<Tray>,
|
||||
private TrayIconObserver {
|
||||
public:
|
||||
// gin_helper::Constructible
|
||||
static gin_helper::Handle<Tray> New(gin_helper::ErrorThrower thrower,
|
||||
v8::Local<v8::Value> image,
|
||||
std::optional<base::Uuid> guid,
|
||||
gin::Arguments* args);
|
||||
static Tray* New(gin_helper::ErrorThrower thrower,
|
||||
v8::Local<v8::Value> image,
|
||||
std::optional<base::Uuid> guid,
|
||||
gin::Arguments* args);
|
||||
|
||||
static void FillObjectTemplate(v8::Isolate*, v8::Local<v8::ObjectTemplate>);
|
||||
static const char* GetClassName() { return "Tray"; }
|
||||
|
||||
// gin_helper::Wrappable
|
||||
static gin::DeprecatedWrapperInfo kWrapperInfo;
|
||||
const char* GetTypeName() override;
|
||||
// gin::Wrappable
|
||||
static const gin::WrapperInfo kWrapperInfo;
|
||||
void Trace(cppgc::Visitor*) const override;
|
||||
const gin::WrapperInfo* wrapper_info() const override;
|
||||
const char* GetHumanReadableName() const override;
|
||||
|
||||
// gin_helper::CleanedUpAtExit
|
||||
void WillBeDestroyed() override;
|
||||
// Make public for cppgc::MakeGarbageCollected.
|
||||
Tray(v8::Isolate* isolate,
|
||||
v8::Local<v8::Value> image,
|
||||
std::optional<base::Uuid> guid);
|
||||
~Tray() override;
|
||||
|
||||
// disable copy
|
||||
Tray(const Tray&) = delete;
|
||||
Tray& operator=(const Tray&) = delete;
|
||||
|
||||
private:
|
||||
Tray(v8::Isolate* isolate,
|
||||
v8::Local<v8::Value> image,
|
||||
std::optional<base::Uuid> guid);
|
||||
~Tray() override;
|
||||
|
||||
// TrayIconObserver:
|
||||
void OnClicked(const gfx::Rect& bounds,
|
||||
const gfx::Point& location,
|
||||
@@ -115,9 +110,10 @@ class Tray final : public gin_helper::DeprecatedWrappable<Tray>,
|
||||
|
||||
bool CheckAlive();
|
||||
|
||||
v8::Global<v8::Value> menu_;
|
||||
cppgc::Member<Menu> menu_;
|
||||
std::optional<base::Uuid> guid_;
|
||||
std::unique_ptr<TrayIcon> tray_icon_;
|
||||
gin_helper::SelfKeepAlive<Tray> keep_alive_{this};
|
||||
};
|
||||
|
||||
} // namespace electron::api
|
||||
|
||||
@@ -172,6 +172,12 @@ class NativeWindowMac : public NativeWindow,
|
||||
void NotifyWindowDidFailToEnterFullScreen();
|
||||
void NotifyWindowWillLeaveFullScreen();
|
||||
|
||||
// Hide/show traffic light buttons around miniaturize/deminiaturize to
|
||||
// prevent them from flashing at the default position during the restore
|
||||
// animation when a custom trafficLightPosition is configured.
|
||||
void HideTrafficLights();
|
||||
void RestoreTrafficLights();
|
||||
|
||||
// Cleanup observers when window is getting closed. Note that the destructor
|
||||
// can be called much later after window gets closed, so we should not do
|
||||
// cleanup in destructor.
|
||||
|
||||
@@ -1535,6 +1535,18 @@ void NativeWindowMac::RedrawTrafficLights() {
|
||||
[buttons_proxy_ redraw];
|
||||
}
|
||||
|
||||
void NativeWindowMac::HideTrafficLights() {
|
||||
if (buttons_proxy_)
|
||||
[buttons_proxy_ setVisible:NO];
|
||||
}
|
||||
|
||||
void NativeWindowMac::RestoreTrafficLights() {
|
||||
if (buttons_proxy_ && window_button_visibility_.value_or(true)) {
|
||||
[buttons_proxy_ redraw];
|
||||
[buttons_proxy_ setVisible:YES];
|
||||
}
|
||||
}
|
||||
|
||||
// In simpleFullScreen mode, update the frame for new bounds.
|
||||
void NativeWindowMac::UpdateFrame() {
|
||||
NSWindow* window = GetNativeWindow().GetNativeNSWindow();
|
||||
|
||||
@@ -256,6 +256,10 @@ using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle;
|
||||
shell_->SetWindowLevel(NSNormalWindowLevel);
|
||||
shell_->UpdateWindowOriginalFrame();
|
||||
shell_->DetachChildren();
|
||||
// Hide the traffic light buttons container before miniaturize so that
|
||||
// when the window is restored, macOS does not render the buttons at
|
||||
// their default position during the deminiaturize animation.
|
||||
shell_->HideTrafficLights();
|
||||
}
|
||||
|
||||
- (void)windowDidMiniaturize:(NSNotification*)notification {
|
||||
@@ -273,6 +277,10 @@ using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle;
|
||||
shell_->set_wants_to_be_visible(true);
|
||||
shell_->AttachChildren();
|
||||
shell_->SetWindowLevel(level_);
|
||||
// Reposition traffic light buttons and make them visible again.
|
||||
// They were hidden in windowWillMiniaturize to prevent a flash at
|
||||
// the default (0,0) position during the restore animation.
|
||||
shell_->RestoreTrafficLights();
|
||||
shell_->NotifyWindowRestore();
|
||||
}
|
||||
|
||||
|
||||
@@ -243,8 +243,8 @@ void ElectronDesktopWindowTreeHostLinux::UpdateFrameHints() {
|
||||
// The opaque region is a list of rectangles that contain only fully
|
||||
// opaque pixels of the window. We need to convert the clipping
|
||||
// rounded-rect into this format.
|
||||
SkRRect rrect = layout->GetRoundedWindowContentBounds();
|
||||
gfx::RectF rectf(layout->GetWindowContentBounds());
|
||||
SkRRect rrect = layout->GetRoundedWindowBounds();
|
||||
gfx::RectF rectf(layout->GetWindowBounds());
|
||||
rectf.Scale(scale);
|
||||
// It is acceptable to omit some pixels that are opaque, but the region
|
||||
// must not include any translucent pixels. Therefore, we must
|
||||
|
||||
@@ -112,7 +112,7 @@ ClientFrameViewLinux::~ClientFrameViewLinux() {
|
||||
void ClientFrameViewLinux::Init(NativeWindowViews* window,
|
||||
views::Widget* frame) {
|
||||
FramelessView::Init(window, frame);
|
||||
linux_frame_layout_ = std::make_unique<LinuxCSDFrameLayout>(window);
|
||||
linux_frame_layout_ = std::make_unique<LinuxCSDNativeFrameLayout>(window);
|
||||
|
||||
// Unretained() is safe because the subscription is saved into an instance
|
||||
// member and thus will be cancelled upon the instance's destruction.
|
||||
@@ -156,7 +156,8 @@ void ClientFrameViewLinux::OnWindowButtonOrderingChange() {
|
||||
}
|
||||
|
||||
int ClientFrameViewLinux::ResizingBorderHitTest(const gfx::Point& point) {
|
||||
return ResizingBorderHitTestImpl(point, RestoredFrameBorderInsets());
|
||||
return ResizingBorderHitTestImpl(
|
||||
point, linux_frame_layout_->GetResizeBorderInsets());
|
||||
}
|
||||
|
||||
gfx::Rect ClientFrameViewLinux::GetBoundsForClientView() const {
|
||||
@@ -235,8 +236,11 @@ void ClientFrameViewLinux::Layout(PassKey) {
|
||||
}
|
||||
|
||||
void ClientFrameViewLinux::OnPaint(gfx::Canvas* canvas) {
|
||||
linux_frame_layout_->PaintWindowFrame(
|
||||
canvas, GetLocalBounds(), GetTitlebarBounds(), ShouldPaintAsActive());
|
||||
if (auto* frame_provider = linux_frame_layout_->GetFrameProvider()) {
|
||||
frame_provider->PaintWindowFrame(
|
||||
canvas, GetLocalBounds(), GetTitlebarBounds().bottom(),
|
||||
ShouldPaintAsActive(), linux_frame_layout_->GetInputInsets());
|
||||
}
|
||||
}
|
||||
|
||||
void ClientFrameViewLinux::PaintAsActiveChanged() {
|
||||
@@ -267,7 +271,7 @@ void ClientFrameViewLinux::UpdateThemeValues() {
|
||||
}
|
||||
|
||||
theme_values_.window_border_radius =
|
||||
linux_frame_layout_->GetFrameProvider()->GetTopCornerRadiusDip();
|
||||
linux_frame_layout_->GetTopCornerRadiusDip();
|
||||
|
||||
gtk::GtkStyleContextGet(headerbar_context, "min-height",
|
||||
&theme_values_.titlebar_min_height, nullptr);
|
||||
|
||||
@@ -112,7 +112,7 @@ class ClientFrameViewLinux : public FramelessView,
|
||||
gfx::Insets GetTitlebarContentInsets() const;
|
||||
gfx::Rect GetTitlebarContentBounds() const;
|
||||
|
||||
std::unique_ptr<LinuxFrameLayout> linux_frame_layout_;
|
||||
std::unique_ptr<LinuxCSDNativeFrameLayout> linux_frame_layout_;
|
||||
|
||||
raw_ptr<ui::NativeTheme> theme_;
|
||||
ThemeValues theme_values_;
|
||||
|
||||
@@ -4,14 +4,20 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/ui/views/linux_frame_layout.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "base/i18n/rtl.h"
|
||||
#include "chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.h" // nogncheck
|
||||
#include "shell/browser/linux/x11_util.h"
|
||||
#include "shell/browser/native_window_views.h"
|
||||
#include "shell/browser/ui/electron_desktop_window_tree_host_linux.h"
|
||||
#include "ui/gfx/canvas.h"
|
||||
#include "third_party/skia/include/core/SkRRect.h"
|
||||
#include "ui/gfx/geometry/insets.h"
|
||||
#include "ui/gfx/geometry/skia_conversions.h"
|
||||
#include "ui/linux/linux_ui.h"
|
||||
#include "ui/native_theme/native_theme.h"
|
||||
#include "ui/linux/window_frame_provider.h"
|
||||
#include "ui/views/layout/layout_provider.h"
|
||||
#include "ui/views/widget/widget.h"
|
||||
|
||||
namespace electron {
|
||||
@@ -21,151 +27,174 @@ namespace {
|
||||
constexpr int kResizeBorder = 10;
|
||||
// This should match FramelessView's inside resize band.
|
||||
constexpr int kResizeInsideBoundsSize = 5;
|
||||
// These should match Chromium's restored frame edge thickness.
|
||||
constexpr gfx::Insets kDefaultCustomFrameBorder = gfx::Insets::TLBR(2, 1, 1, 1);
|
||||
|
||||
bool CheckClientFrameShadowSupport(NativeWindowViews* window) {
|
||||
auto* tree_host = static_cast<ElectronDesktopWindowTreeHostLinux*>(
|
||||
ElectronDesktopWindowTreeHostLinux::GetHostForWidget(
|
||||
window->GetAcceleratedWidget()));
|
||||
return tree_host && tree_host->SupportsClientFrameShadow();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// static
|
||||
std::unique_ptr<LinuxFrameLayout> LinuxFrameLayout::Create(
|
||||
NativeWindowViews* window,
|
||||
bool wants_shadow) {
|
||||
bool wants_shadow,
|
||||
CSDStyle csd_style) {
|
||||
if (x11_util::IsX11() || window->IsTranslucent() || !wants_shadow) {
|
||||
return std::make_unique<LinuxUndecoratedFrameLayout>(window);
|
||||
return std::make_unique<LinuxFrameLayout>(window);
|
||||
} else if (csd_style == CSDStyle::kCustom) {
|
||||
return std::make_unique<LinuxCSDCustomFrameLayout>(window);
|
||||
} else {
|
||||
return std::make_unique<LinuxCSDFrameLayout>(window);
|
||||
return std::make_unique<LinuxCSDNativeFrameLayout>(window);
|
||||
}
|
||||
}
|
||||
|
||||
LinuxCSDFrameLayout::LinuxCSDFrameLayout(NativeWindowViews* window)
|
||||
: window_(window) {
|
||||
host_supports_client_frame_shadow_ = SupportsClientFrameShadow();
|
||||
gfx::Insets LinuxFrameLayout::GetResizeBorderInsets() const {
|
||||
gfx::Insets insets = RestoredFrameBorderInsets();
|
||||
return insets.IsEmpty() ? GetInputInsets() : insets;
|
||||
}
|
||||
|
||||
bool LinuxCSDFrameLayout::tiled() const {
|
||||
return tiled_;
|
||||
}
|
||||
|
||||
void LinuxCSDFrameLayout::set_tiled(bool tiled) {
|
||||
tiled_ = tiled;
|
||||
}
|
||||
|
||||
gfx::Insets LinuxCSDFrameLayout::RestoredFrameBorderInsets() const {
|
||||
gfx::Insets insets = GetFrameProvider()->GetFrameThicknessDip();
|
||||
const gfx::Insets input = GetInputInsets();
|
||||
|
||||
auto expand_if_visible = [](int side_thickness, int min_band) {
|
||||
return side_thickness > 0 ? std::max(side_thickness, min_band) : 0;
|
||||
};
|
||||
|
||||
gfx::Insets merged;
|
||||
merged.set_top(expand_if_visible(insets.top(), input.top()));
|
||||
merged.set_left(expand_if_visible(insets.left(), input.left()));
|
||||
merged.set_bottom(expand_if_visible(insets.bottom(), input.bottom()));
|
||||
merged.set_right(expand_if_visible(insets.right(), input.right()));
|
||||
|
||||
return base::i18n::IsRTL() ? gfx::Insets::TLBR(merged.top(), merged.right(),
|
||||
merged.bottom(), merged.left())
|
||||
: merged;
|
||||
}
|
||||
|
||||
gfx::Insets LinuxCSDFrameLayout::GetInputInsets() const {
|
||||
bool showing_shadow = host_supports_client_frame_shadow_ &&
|
||||
!window_->IsMaximized() && !window_->IsFullscreen();
|
||||
return gfx::Insets(showing_shadow ? kResizeBorder : 0);
|
||||
}
|
||||
|
||||
bool LinuxCSDFrameLayout::SupportsClientFrameShadow() const {
|
||||
auto* tree_host = static_cast<ElectronDesktopWindowTreeHostLinux*>(
|
||||
ElectronDesktopWindowTreeHostLinux::GetHostForWidget(
|
||||
window_->GetAcceleratedWidget()));
|
||||
return tree_host->SupportsClientFrameShadow();
|
||||
}
|
||||
|
||||
void LinuxCSDFrameLayout::PaintWindowFrame(gfx::Canvas* canvas,
|
||||
gfx::Rect local_bounds,
|
||||
gfx::Rect titlebar_bounds,
|
||||
bool active) {
|
||||
GetFrameProvider()->PaintWindowFrame(
|
||||
canvas, local_bounds, titlebar_bounds.bottom(), active, GetInputInsets());
|
||||
}
|
||||
|
||||
gfx::Rect LinuxCSDFrameLayout::GetWindowContentBounds() const {
|
||||
gfx::Rect content_bounds = window_->widget()->GetWindowBoundsInScreen();
|
||||
content_bounds.Inset(RestoredFrameBorderInsets());
|
||||
return content_bounds;
|
||||
}
|
||||
|
||||
SkRRect LinuxCSDFrameLayout::GetRoundedWindowContentBounds() const {
|
||||
SkRect rect = gfx::RectToSkRect(GetWindowContentBounds());
|
||||
SkRRect LinuxFrameLayout::GetRoundedWindowBounds() const {
|
||||
SkRect rect = gfx::RectToSkRect(GetWindowBounds());
|
||||
SkRRect rrect;
|
||||
|
||||
if (!window_->IsMaximized()) {
|
||||
float radius = GetFrameProvider()->GetTopCornerRadiusDip();
|
||||
float radius = GetTopCornerRadiusDip();
|
||||
if (radius > 0) {
|
||||
SkPoint round_point{radius, radius};
|
||||
SkPoint radii[] = {round_point, round_point, {}, {}};
|
||||
rrect.setRectRadii(rect, radii);
|
||||
} else {
|
||||
rrect.setRect(rect);
|
||||
}
|
||||
|
||||
return rrect;
|
||||
}
|
||||
|
||||
int LinuxCSDFrameLayout::GetTranslucentTopAreaHeight() const {
|
||||
// Base implementation is suitable for X11/views without shadows
|
||||
LinuxFrameLayout::LinuxFrameLayout(NativeWindowViews* window)
|
||||
: window_(window) {
|
||||
host_supports_client_frame_shadow_ = false;
|
||||
}
|
||||
|
||||
LinuxFrameLayout::~LinuxFrameLayout() = default;
|
||||
|
||||
gfx::Insets LinuxFrameLayout::RestoredFrameBorderInsets() const {
|
||||
return gfx::Insets();
|
||||
}
|
||||
|
||||
gfx::Insets LinuxFrameLayout::GetInputInsets() const {
|
||||
return gfx::Insets(kResizeInsideBoundsSize);
|
||||
}
|
||||
|
||||
bool LinuxFrameLayout::IsShowingShadow() const {
|
||||
return host_supports_client_frame_shadow_ && !window_->IsMaximized() &&
|
||||
!window_->IsFullscreen();
|
||||
}
|
||||
|
||||
bool LinuxFrameLayout::SupportsClientFrameShadow() const {
|
||||
return host_supports_client_frame_shadow_;
|
||||
}
|
||||
|
||||
bool LinuxFrameLayout::tiled() const {
|
||||
return tiled_;
|
||||
}
|
||||
|
||||
void LinuxFrameLayout::set_tiled(bool tiled) {
|
||||
tiled_ = tiled;
|
||||
}
|
||||
|
||||
gfx::Rect LinuxFrameLayout::GetWindowBounds() const {
|
||||
gfx::Rect bounds = window_->widget()->GetWindowBoundsInScreen();
|
||||
bounds.Inset(RestoredFrameBorderInsets());
|
||||
return bounds;
|
||||
}
|
||||
|
||||
float LinuxFrameLayout::GetTopCornerRadiusDip() const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ui::WindowFrameProvider* LinuxCSDFrameLayout::GetFrameProvider() const {
|
||||
int LinuxFrameLayout::GetTranslucentTopAreaHeight() const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
gfx::Insets LinuxFrameLayout::NormalizeBorderInsets(
|
||||
const gfx::Insets& frame_insets,
|
||||
const gfx::Insets& input_insets) const {
|
||||
auto expand_if_visible = [](int side_thickness, int min_band) {
|
||||
return side_thickness > 0 ? std::max(side_thickness, min_band) : 0;
|
||||
};
|
||||
|
||||
// Ensure hit testing for resize targets works
|
||||
// even if borders/shadows are absent on some edges.
|
||||
gfx::Insets merged;
|
||||
merged.set_top(expand_if_visible(frame_insets.top(), input_insets.top()));
|
||||
merged.set_left(expand_if_visible(frame_insets.left(), input_insets.left()));
|
||||
merged.set_bottom(
|
||||
expand_if_visible(frame_insets.bottom(), input_insets.bottom()));
|
||||
merged.set_right(
|
||||
expand_if_visible(frame_insets.right(), input_insets.right()));
|
||||
|
||||
return base::i18n::IsRTL() ? gfx::Insets::TLBR(merged.top(), merged.right(),
|
||||
merged.bottom(), merged.left())
|
||||
: merged;
|
||||
}
|
||||
|
||||
// Used for a native-like frame with a FrameProvider
|
||||
LinuxCSDNativeFrameLayout::LinuxCSDNativeFrameLayout(NativeWindowViews* window)
|
||||
: LinuxFrameLayout(window) {
|
||||
host_supports_client_frame_shadow_ = CheckClientFrameShadowSupport(window);
|
||||
}
|
||||
|
||||
LinuxCSDNativeFrameLayout::~LinuxCSDNativeFrameLayout() = default;
|
||||
|
||||
gfx::Insets LinuxCSDNativeFrameLayout::RestoredFrameBorderInsets() const {
|
||||
const gfx::Insets input_insets = GetInputInsets();
|
||||
const gfx::Insets frame_insets = GetFrameProvider()->GetFrameThicknessDip();
|
||||
return NormalizeBorderInsets(frame_insets, input_insets);
|
||||
}
|
||||
|
||||
gfx::Insets LinuxCSDNativeFrameLayout::GetInputInsets() const {
|
||||
return gfx::Insets(IsShowingShadow() ? kResizeBorder : 0);
|
||||
}
|
||||
|
||||
float LinuxCSDNativeFrameLayout::GetTopCornerRadiusDip() const {
|
||||
return window_->IsMaximized() ? 0
|
||||
: GetFrameProvider()->GetTopCornerRadiusDip();
|
||||
}
|
||||
|
||||
ui::WindowFrameProvider* LinuxCSDNativeFrameLayout::GetFrameProvider() const {
|
||||
return ui::LinuxUiTheme::GetForProfile(nullptr)->GetWindowFrameProvider(
|
||||
!host_supports_client_frame_shadow_, tiled(), window_->IsMaximized());
|
||||
}
|
||||
|
||||
LinuxUndecoratedFrameLayout::LinuxUndecoratedFrameLayout(
|
||||
NativeWindowViews* window)
|
||||
: window_(window) {}
|
||||
|
||||
gfx::Insets LinuxUndecoratedFrameLayout::RestoredFrameBorderInsets() const {
|
||||
return gfx::Insets();
|
||||
// Used for Chromium-like custom CSD
|
||||
LinuxCSDCustomFrameLayout::LinuxCSDCustomFrameLayout(NativeWindowViews* window)
|
||||
: LinuxFrameLayout(window) {
|
||||
host_supports_client_frame_shadow_ = CheckClientFrameShadowSupport(window);
|
||||
}
|
||||
|
||||
gfx::Insets LinuxUndecoratedFrameLayout::GetInputInsets() const {
|
||||
return gfx::Insets(kResizeInsideBoundsSize);
|
||||
LinuxCSDCustomFrameLayout::~LinuxCSDCustomFrameLayout() = default;
|
||||
|
||||
gfx::Insets LinuxCSDCustomFrameLayout::RestoredFrameBorderInsets() const {
|
||||
const gfx::Insets input_insets = GetInputInsets();
|
||||
const bool showing_shadow = IsShowingShadow();
|
||||
const auto shadow_values = (showing_shadow && !tiled())
|
||||
? GetFrameShadowValuesLinux(/*active=*/true)
|
||||
: gfx::ShadowValues();
|
||||
const gfx::Insets frame_insets = GetRestoredFrameBorderInsetsLinux(
|
||||
showing_shadow, kDefaultCustomFrameBorder, shadow_values, input_insets);
|
||||
return NormalizeBorderInsets(frame_insets, input_insets);
|
||||
}
|
||||
|
||||
bool LinuxUndecoratedFrameLayout::SupportsClientFrameShadow() const {
|
||||
return false;
|
||||
gfx::Insets LinuxCSDCustomFrameLayout::GetInputInsets() const {
|
||||
return gfx::Insets(IsShowingShadow() ? kResizeBorder : 0);
|
||||
}
|
||||
|
||||
bool LinuxUndecoratedFrameLayout::tiled() const {
|
||||
return tiled_;
|
||||
}
|
||||
|
||||
void LinuxUndecoratedFrameLayout::set_tiled(bool tiled) {
|
||||
tiled_ = tiled;
|
||||
}
|
||||
|
||||
void LinuxUndecoratedFrameLayout::PaintWindowFrame(gfx::Canvas* canvas,
|
||||
gfx::Rect local_bounds,
|
||||
gfx::Rect titlebar_bounds,
|
||||
bool active) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
gfx::Rect LinuxUndecoratedFrameLayout::GetWindowContentBounds() const {
|
||||
// With no transparent insets, widget bounds and logical bounds match.
|
||||
return window_->widget()->GetWindowBoundsInScreen();
|
||||
}
|
||||
|
||||
SkRRect LinuxUndecoratedFrameLayout::GetRoundedWindowContentBounds() const {
|
||||
SkRRect rrect;
|
||||
rrect.setRect(gfx::RectToSkRect(GetWindowContentBounds()));
|
||||
return rrect;
|
||||
}
|
||||
|
||||
int LinuxUndecoratedFrameLayout::GetTranslucentTopAreaHeight() const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ui::WindowFrameProvider* LinuxUndecoratedFrameLayout::GetFrameProvider() const {
|
||||
return nullptr;
|
||||
gfx::ShadowValues GetFrameShadowValuesLinux(bool active) {
|
||||
const int elevation = views::LayoutProvider::Get()->GetShadowElevationMetric(
|
||||
active ? views::Emphasis::kMaximum : views::Emphasis::kMedium);
|
||||
return gfx::ShadowValue::MakeMdShadowValues(elevation);
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
|
||||
@@ -8,110 +8,96 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "base/i18n/rtl.h"
|
||||
#include "shell/browser/linux/x11_util.h"
|
||||
#include "shell/browser/native_window_views.h"
|
||||
#include "shell/browser/ui/electron_desktop_window_tree_host_linux.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "third_party/skia/include/core/SkRRect.h"
|
||||
#include "ui/base/ozone_buildflags.h"
|
||||
#include "ui/gfx/canvas.h"
|
||||
#include "ui/gfx/geometry/insets.h"
|
||||
#include "ui/linux/linux_ui.h"
|
||||
#include "ui/gfx/shadow_value.h"
|
||||
#include "ui/linux/window_frame_provider.h"
|
||||
|
||||
namespace gfx {
|
||||
class Insets;
|
||||
class Rect;
|
||||
} // namespace gfx
|
||||
|
||||
namespace electron {
|
||||
|
||||
class NativeWindowViews;
|
||||
|
||||
// Shared helper for CSD layout and frame painting on Linux (shadows, resize
|
||||
// regions, titlebars, etc.). Also helps views determine insets and perform
|
||||
// bounds conversions between widget and logical coordinates.
|
||||
// Shared helper for CSD layout on Linux (shadows, resize regions, titlebars,
|
||||
// etc.). Also helps views determine insets and perform bounds conversions
|
||||
// between widget and logical coordinates.
|
||||
//
|
||||
// The base class is concrete and suitable as-is for the undecorated case (X11,
|
||||
// translucent windows, or windows without shadows). CSD subclasses override
|
||||
// the methods that differ.
|
||||
class LinuxFrameLayout {
|
||||
public:
|
||||
virtual ~LinuxFrameLayout() = default;
|
||||
enum class CSDStyle {
|
||||
kNativeFrame,
|
||||
kCustom,
|
||||
};
|
||||
|
||||
explicit LinuxFrameLayout(NativeWindowViews* window);
|
||||
virtual ~LinuxFrameLayout();
|
||||
|
||||
static std::unique_ptr<LinuxFrameLayout> Create(NativeWindowViews* window,
|
||||
bool wants_shadow);
|
||||
bool wants_shadow,
|
||||
CSDStyle csd_style);
|
||||
|
||||
// Insets from the transparent widget border to the opaque part of the window
|
||||
virtual gfx::Insets RestoredFrameBorderInsets() const = 0;
|
||||
// Insets for parts of the surface that should be counted for user input
|
||||
virtual gfx::Insets GetInputInsets() const = 0;
|
||||
// Insets from the transparent widget border to the opaque part of the window.
|
||||
virtual gfx::Insets RestoredFrameBorderInsets() const;
|
||||
// Insets for parts of the surface that should be counted for user input.
|
||||
virtual gfx::Insets GetInputInsets() const;
|
||||
// Insets to use for non-client resize hit-testing.
|
||||
gfx::Insets GetResizeBorderInsets() const;
|
||||
|
||||
virtual bool SupportsClientFrameShadow() const = 0;
|
||||
bool IsShowingShadow() const;
|
||||
bool SupportsClientFrameShadow() const;
|
||||
|
||||
virtual bool tiled() const = 0;
|
||||
virtual void set_tiled(bool tiled) = 0;
|
||||
bool tiled() const;
|
||||
void set_tiled(bool tiled);
|
||||
|
||||
virtual void PaintWindowFrame(gfx::Canvas* canvas,
|
||||
gfx::Rect local_bounds,
|
||||
gfx::Rect titlebar_bounds,
|
||||
bool active) = 0;
|
||||
// The logical bounds of the window interior.
|
||||
gfx::Rect GetWindowBounds() const;
|
||||
// The logical window bounds as a rounded rect with corner radii applied.
|
||||
SkRRect GetRoundedWindowBounds() const;
|
||||
// The corner radius of the top corners of the window, in DIPs.
|
||||
virtual float GetTopCornerRadiusDip() const;
|
||||
|
||||
// The logical bounds of the window
|
||||
virtual gfx::Rect GetWindowContentBounds() const = 0;
|
||||
// The logical bounds as a rounded rect with corner radii applied
|
||||
virtual SkRRect GetRoundedWindowContentBounds() const = 0;
|
||||
int GetTranslucentTopAreaHeight() const;
|
||||
|
||||
virtual int GetTranslucentTopAreaHeight() const = 0;
|
||||
protected:
|
||||
gfx::Insets NormalizeBorderInsets(const gfx::Insets& frame_insets,
|
||||
const gfx::Insets& input_insets) const;
|
||||
|
||||
virtual ui::WindowFrameProvider* GetFrameProvider() const = 0;
|
||||
};
|
||||
|
||||
// Client-side decoration (CSD) Linux frame layout implementation.
|
||||
class LinuxCSDFrameLayout : public LinuxFrameLayout {
|
||||
public:
|
||||
explicit LinuxCSDFrameLayout(NativeWindowViews* window);
|
||||
~LinuxCSDFrameLayout() override = default;
|
||||
|
||||
gfx::Insets RestoredFrameBorderInsets() const override;
|
||||
gfx::Insets GetInputInsets() const override;
|
||||
bool SupportsClientFrameShadow() const override;
|
||||
bool tiled() const override;
|
||||
void set_tiled(bool tiled) override;
|
||||
void PaintWindowFrame(gfx::Canvas* canvas,
|
||||
gfx::Rect local_bounds,
|
||||
gfx::Rect titlebar_bounds,
|
||||
bool active) override;
|
||||
gfx::Rect GetWindowContentBounds() const override;
|
||||
SkRRect GetRoundedWindowContentBounds() const override;
|
||||
int GetTranslucentTopAreaHeight() const override;
|
||||
ui::WindowFrameProvider* GetFrameProvider() const override;
|
||||
|
||||
private:
|
||||
raw_ptr<NativeWindowViews> window_;
|
||||
bool tiled_ = false;
|
||||
bool host_supports_client_frame_shadow_ = false;
|
||||
};
|
||||
|
||||
// No-decoration Linux frame layout implementation.
|
||||
//
|
||||
// Intended for cases where we do not allocate a transparent inset area around
|
||||
// the window (e.g. X11 / server-side decorations, or when insets are disabled).
|
||||
// All inset math returns 0 and frame painting is skipped.
|
||||
class LinuxUndecoratedFrameLayout : public LinuxFrameLayout {
|
||||
// CSD strategy that uses the GTK window frame provider for metrics.
|
||||
class LinuxCSDNativeFrameLayout : public LinuxFrameLayout {
|
||||
public:
|
||||
explicit LinuxUndecoratedFrameLayout(NativeWindowViews* window);
|
||||
~LinuxUndecoratedFrameLayout() override = default;
|
||||
explicit LinuxCSDNativeFrameLayout(NativeWindowViews* window);
|
||||
~LinuxCSDNativeFrameLayout() override;
|
||||
|
||||
gfx::Insets RestoredFrameBorderInsets() const override;
|
||||
gfx::Insets GetInputInsets() const override;
|
||||
bool SupportsClientFrameShadow() const override;
|
||||
bool tiled() const override;
|
||||
void set_tiled(bool tiled) override;
|
||||
void PaintWindowFrame(gfx::Canvas* canvas,
|
||||
gfx::Rect local_bounds,
|
||||
gfx::Rect titlebar_bounds,
|
||||
bool active) override;
|
||||
gfx::Rect GetWindowContentBounds() const override;
|
||||
SkRRect GetRoundedWindowContentBounds() const override;
|
||||
int GetTranslucentTopAreaHeight() const override;
|
||||
ui::WindowFrameProvider* GetFrameProvider() const override;
|
||||
|
||||
private:
|
||||
raw_ptr<NativeWindowViews> window_;
|
||||
bool tiled_ = false;
|
||||
float GetTopCornerRadiusDip() const override;
|
||||
ui::WindowFrameProvider* GetFrameProvider() const;
|
||||
};
|
||||
|
||||
// CSD strategy that uses custom metrics, similar to those used in Chromium.
|
||||
class LinuxCSDCustomFrameLayout : public LinuxFrameLayout {
|
||||
public:
|
||||
explicit LinuxCSDCustomFrameLayout(NativeWindowViews* window);
|
||||
~LinuxCSDCustomFrameLayout() override;
|
||||
|
||||
gfx::Insets RestoredFrameBorderInsets() const override;
|
||||
gfx::Insets GetInputInsets() const override;
|
||||
};
|
||||
|
||||
gfx::ShadowValues GetFrameShadowValuesLinux(bool active);
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // ELECTRON_SHELL_BROWSER_UI_VIEWS_LINUX_FRAME_LAYOUT_H_
|
||||
|
||||
@@ -5,22 +5,24 @@
|
||||
#include "shell/browser/ui/views/opaque_frame_view.h"
|
||||
|
||||
#include "base/containers/adapters.h"
|
||||
#include "base/i18n/rtl.h"
|
||||
#include "chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.h" // nogncheck
|
||||
#include "chrome/browser/ui/views/frame/opaque_browser_frame_view_layout.h" // nogncheck
|
||||
#include "chrome/grit/generated_resources.h"
|
||||
#include "components/strings/grit/components_strings.h"
|
||||
#include "shell/browser/native_window_views.h"
|
||||
#include "shell/browser/ui/views/caption_button_placeholder_container.h"
|
||||
#include "third_party/skia/include/core/SkRRect.h"
|
||||
#include "ui/base/hit_test.h"
|
||||
#include "ui/base/l10n/l10n_util.h"
|
||||
#include "ui/base/metadata/metadata_impl_macros.h"
|
||||
#include "ui/compositor/layer.h"
|
||||
#include "ui/gfx/font_list.h"
|
||||
#include "ui/linux/linux_ui.h"
|
||||
#include "ui/gfx/geometry/insets_f.h"
|
||||
#include "ui/gfx/geometry/skia_conversions.h"
|
||||
#include "ui/views/accessibility/view_accessibility.h"
|
||||
#include "ui/views/background.h"
|
||||
#include "ui/views/widget/widget.h"
|
||||
#include "ui/views/widget/widget_delegate.h"
|
||||
#include "ui/views/window/frame_background.h"
|
||||
#include "ui/views/window/frame_caption_button.h"
|
||||
#include "ui/views/window/vector_icons/vector_icons.h"
|
||||
|
||||
@@ -55,12 +57,14 @@ const int kCaptionButtonBottomPadding = 3;
|
||||
// The content edge images have a shadow built into them.
|
||||
const int OpaqueFrameView::kContentEdgeShadowThickness = 2;
|
||||
|
||||
OpaqueFrameView::OpaqueFrameView() = default;
|
||||
OpaqueFrameView::OpaqueFrameView()
|
||||
: frame_background_(std::make_unique<views::FrameBackground>()) {}
|
||||
OpaqueFrameView::~OpaqueFrameView() = default;
|
||||
|
||||
void OpaqueFrameView::Init(NativeWindowViews* window, views::Widget* frame) {
|
||||
FramelessView::Init(window, frame);
|
||||
linux_frame_layout_ = LinuxFrameLayout::Create(window, window->HasShadow());
|
||||
linux_frame_layout_ = LinuxFrameLayout::Create(
|
||||
window, window->HasShadow(), LinuxFrameLayout::CSDStyle::kCustom);
|
||||
|
||||
// Unretained() is safe because the subscription is saved into an instance
|
||||
// member and thus will be cancelled upon the instance's destruction.
|
||||
@@ -98,9 +102,8 @@ void OpaqueFrameView::Init(NativeWindowViews* window, views::Widget* frame) {
|
||||
}
|
||||
|
||||
int OpaqueFrameView::ResizingBorderHitTest(const gfx::Point& point) {
|
||||
auto insets = RestoredFrameBorderInsets();
|
||||
return ResizingBorderHitTestImpl(
|
||||
point, insets.IsEmpty() ? linux_frame_layout_->GetInputInsets() : insets);
|
||||
point, linux_frame_layout_->GetResizeBorderInsets());
|
||||
}
|
||||
|
||||
void OpaqueFrameView::InvalidateCaptionButtons() {
|
||||
@@ -200,14 +203,31 @@ void OpaqueFrameView::OnPaint(gfx::Canvas* canvas) {
|
||||
if (frame()->IsFullscreen())
|
||||
return;
|
||||
|
||||
// Titlebar height must be at least the frame border insets to avoid
|
||||
// a negative height calculation in the GTK frame provider. We add 1 to
|
||||
// ensure it's always positive even when insets are 0.
|
||||
int top_area_height = RestoredFrameBorderInsets().top() + 1;
|
||||
const bool active = ShouldPaintAsActive();
|
||||
const gfx::Insets border = RestoredFrameBorderInsets();
|
||||
const bool showing_shadow = linux_frame_layout_->IsShowingShadow();
|
||||
gfx::RectF bounds_dip(GetLocalBounds());
|
||||
if (showing_shadow) {
|
||||
bounds_dip.Inset(gfx::InsetsF(border));
|
||||
}
|
||||
|
||||
linux_frame_layout_->PaintWindowFrame(
|
||||
canvas, GetLocalBounds(), gfx::Rect(0, 0, width(), top_area_height),
|
||||
ShouldPaintAsActive());
|
||||
// TODO: support roundedCorners.
|
||||
float radius_dip = 0;
|
||||
SkVector radii[4]{{radius_dip, radius_dip}, {radius_dip, radius_dip}, {}, {}};
|
||||
SkRRect clip;
|
||||
clip.setRectRadii(gfx::RectFToSkRect(bounds_dip), radii);
|
||||
|
||||
frame_background_->set_frame_color(GetFrameColor());
|
||||
frame_background_->set_use_custom_frame(true);
|
||||
frame_background_->set_is_active(active);
|
||||
frame_background_->set_top_area_height(GetTopAreaHeight());
|
||||
|
||||
const bool draw_shadow = showing_shadow && !linux_frame_layout_->tiled();
|
||||
auto shadow_values =
|
||||
draw_shadow ? GetFrameShadowValuesLinux(active) : gfx::ShadowValues();
|
||||
::PaintRestoredFrameBorderLinux(*canvas, *this, frame_background_.get(), clip,
|
||||
showing_shadow, active, border, shadow_values,
|
||||
linux_frame_layout_->tiled());
|
||||
|
||||
if (!window()->IsWindowControlsOverlayEnabled())
|
||||
return;
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
|
||||
class CaptionButtonPlaceholderContainer;
|
||||
|
||||
namespace views {
|
||||
class FrameBackground;
|
||||
}
|
||||
|
||||
namespace electron {
|
||||
|
||||
class NativeWindowViews;
|
||||
@@ -166,6 +170,7 @@ class OpaqueFrameView : public FramelessView {
|
||||
bool is_leading_button) const;
|
||||
|
||||
std::unique_ptr<LinuxFrameLayout> linux_frame_layout_;
|
||||
std::unique_ptr<views::FrameBackground> frame_background_;
|
||||
|
||||
// Window controls.
|
||||
raw_ptr<views::Button> minimize_button_;
|
||||
|
||||
@@ -403,7 +403,7 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should clean up old staged update directories when a new update is downloaded', async () => {
|
||||
it('should preserve the staged update directory and prune orphaned ones when a new update is downloaded', async () => {
|
||||
// Clean up any existing update directories before the test
|
||||
await cleanSquirrelCache();
|
||||
|
||||
@@ -419,16 +419,23 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
|
||||
}, async (_, updateZipPath3) => {
|
||||
let updateCount = 0;
|
||||
let downloadCount = 0;
|
||||
let directoriesDuringSecondDownload: string[] = [];
|
||||
let dirsDuringFirstDownload: string[] = [];
|
||||
let dirsDuringSecondDownload: string[] = [];
|
||||
|
||||
server.get('/update-file', async (req, res) => {
|
||||
downloadCount++;
|
||||
// When the second download request arrives, Squirrel has already
|
||||
// called uniqueTemporaryDirectoryForUpdate which (with our patch)
|
||||
// cleans up old directories before creating the new one.
|
||||
// Without the patch, both directories would exist at this point.
|
||||
if (downloadCount === 2) {
|
||||
directoriesDuringSecondDownload = await getUpdateDirectoriesInCache();
|
||||
// Snapshot update directories at the moment each download begins.
|
||||
// By this point uniqueTemporaryDirectoryForUpdate has already run
|
||||
// (prune + mkdtemp). We want to verify:
|
||||
// 1st download: 1 dir (nothing to preserve, nothing to prune)
|
||||
// 2nd download: 2 dirs (staged dir from 1st check is preserved
|
||||
// so quitAndInstall stays safe, + new temp dir)
|
||||
// The count never exceeds 2 across repeated checks — orphaned dirs
|
||||
// (no longer referenced by ShipItState.plist) get pruned.
|
||||
if (downloadCount === 1) {
|
||||
dirsDuringFirstDownload = await getUpdateDirectoriesInCache();
|
||||
} else if (downloadCount === 2) {
|
||||
dirsDuringSecondDownload = await getUpdateDirectoriesInCache();
|
||||
}
|
||||
res.download(updateCount > 1 ? updateZipPath3 : updateZipPath2);
|
||||
});
|
||||
@@ -455,15 +462,181 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
|
||||
|
||||
await relaunchPromise;
|
||||
|
||||
// During the second download, the old staged update directory should
|
||||
// have been cleaned up. With our patch, there should be exactly 1
|
||||
// directory (the new one). Without the patch, there would be 2.
|
||||
expect(directoriesDuringSecondDownload).to.have.lengthOf(1,
|
||||
`Expected 1 update directory during second download but found ${directoriesDuringSecondDownload.length}: ${directoriesDuringSecondDownload.join(', ')}`);
|
||||
// First download: exactly one temp dir (the first update).
|
||||
expect(dirsDuringFirstDownload).to.have.lengthOf(1,
|
||||
`Expected 1 update directory during first download but found ${dirsDuringFirstDownload.length}: ${dirsDuringFirstDownload.join(', ')}`);
|
||||
|
||||
// Second download: exactly two — the staged one preserved + the new
|
||||
// one. Crucially the first download's directory must still be present,
|
||||
// otherwise a mid-download quitAndInstall would find a dangling
|
||||
// ShipItState.plist.
|
||||
expect(dirsDuringSecondDownload).to.have.lengthOf(2,
|
||||
`Expected 2 update directories during second download (staged + new) but found ${dirsDuringSecondDownload.length}: ${dirsDuringSecondDownload.join(', ')}`);
|
||||
expect(dirsDuringSecondDownload).to.include(dirsDuringFirstDownload[0],
|
||||
'The staged update directory from the first download must be preserved during the second download');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep the update directory count bounded across repeated checks', async () => {
|
||||
// Verifies the orphan prune actually fires: after a second download
|
||||
// completes and rewrites ShipItState.plist, the first directory is no
|
||||
// longer referenced and must be removed when a third check begins.
|
||||
// Without this, directories would accumulate forever.
|
||||
await cleanSquirrelCache();
|
||||
|
||||
await withUpdatableApp({
|
||||
nextVersion: '2.0.0',
|
||||
startFixture: 'update-triple-stack',
|
||||
endFixture: 'update-triple-stack'
|
||||
}, async (appPath, updateZipPath2) => {
|
||||
await withUpdatableApp({
|
||||
nextVersion: '3.0.0',
|
||||
startFixture: 'update-triple-stack',
|
||||
endFixture: 'update-triple-stack'
|
||||
}, async (_, updateZipPath3) => {
|
||||
await withUpdatableApp({
|
||||
nextVersion: '4.0.0',
|
||||
startFixture: 'update-triple-stack',
|
||||
endFixture: 'update-triple-stack'
|
||||
}, async (__, updateZipPath4) => {
|
||||
let downloadCount = 0;
|
||||
const dirsPerDownload: string[][] = [];
|
||||
|
||||
server.get('/update-file', async (req, res) => {
|
||||
downloadCount++;
|
||||
// Snapshot after prune+mkdtemp but before the payload transfers.
|
||||
dirsPerDownload.push(await getUpdateDirectoriesInCache());
|
||||
const zips = [updateZipPath2, updateZipPath3, updateZipPath4];
|
||||
res.download(zips[Math.min(downloadCount, zips.length) - 1]);
|
||||
});
|
||||
server.get('/update-check', (req, res) => {
|
||||
res.json({
|
||||
url: `http://localhost:${port}/update-file`,
|
||||
name: 'My Release Name',
|
||||
notes: 'Theses are some release notes innit',
|
||||
pub_date: (new Date()).toString()
|
||||
});
|
||||
});
|
||||
const relaunchPromise = new Promise<void>((resolve) => {
|
||||
server.get('/update-check/updated/:version', (req, res) => {
|
||||
res.status(204).send();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
|
||||
logOnError(launchResult, () => {
|
||||
expect(launchResult).to.have.property('code', 0);
|
||||
expect(launchResult.out).to.include('Update Downloaded');
|
||||
});
|
||||
|
||||
await relaunchPromise;
|
||||
expect(requests[requests.length - 1].url).to.equal('/update-check/updated/4.0.0');
|
||||
|
||||
expect(dirsPerDownload).to.have.lengthOf(3);
|
||||
|
||||
// 1st: fresh cache, 1 dir.
|
||||
expect(dirsPerDownload[0]).to.have.lengthOf(1,
|
||||
`1st download: ${dirsPerDownload[0].join(', ')}`);
|
||||
|
||||
// 2nd: staged (1st) preserved + new = 2 dirs.
|
||||
expect(dirsPerDownload[1]).to.have.lengthOf(2,
|
||||
`2nd download: ${dirsPerDownload[1].join(', ')}`);
|
||||
expect(dirsPerDownload[1]).to.include(dirsPerDownload[0][0]);
|
||||
|
||||
// 3rd: 1st is now orphaned (plist points to 2nd) — must be pruned.
|
||||
// Staged (2nd) preserved + new = still 2 dirs. Bounded.
|
||||
expect(dirsPerDownload[2]).to.have.lengthOf(2,
|
||||
`3rd download: ${dirsPerDownload[2].join(', ')}`);
|
||||
expect(dirsPerDownload[2]).to.not.include(dirsPerDownload[0][0],
|
||||
'The first (now orphaned) update directory must be pruned on the third check');
|
||||
const secondDir = dirsPerDownload[1].find(d => d !== dirsPerDownload[0][0]);
|
||||
expect(dirsPerDownload[2]).to.include(secondDir,
|
||||
'The second (currently staged) update directory must be preserved on the third check');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/electron/electron/issues/50200
|
||||
//
|
||||
// When checkForUpdates() is called again after an update has been staged,
|
||||
// Squirrel creates a new temporary directory and prunes old ones. If the
|
||||
// prune removes the directory that ShipItState.plist references while the
|
||||
// second download is still in flight, a subsequent quitAndInstall() will
|
||||
// fail with ENOENT and the app will never relaunch.
|
||||
it('should install the staged update when quitAndInstall is called while a second check is in flight', async () => {
|
||||
await cleanSquirrelCache();
|
||||
|
||||
await withUpdatableApp({
|
||||
nextVersion: '2.0.0',
|
||||
startFixture: 'update-race',
|
||||
endFixture: 'update-race'
|
||||
}, async (appPath, updateZipPath) => {
|
||||
let downloadCount = 0;
|
||||
let stalledResponse: express.Response | null = null;
|
||||
|
||||
server.get('/update-file', (req, res) => {
|
||||
downloadCount++;
|
||||
if (downloadCount === 1) {
|
||||
// First download completes normally and stages the update.
|
||||
res.download(updateZipPath);
|
||||
} else {
|
||||
// Second download: stall indefinitely to simulate a slow
|
||||
// network. This keeps the second check "in progress" when
|
||||
// quitAndInstall() fires. Hold onto the response so we can
|
||||
// clean it up later.
|
||||
stalledResponse = res;
|
||||
}
|
||||
});
|
||||
server.get('/update-check', (req, res) => {
|
||||
res.json({
|
||||
url: `http://localhost:${port}/update-file`,
|
||||
name: 'My Release Name',
|
||||
notes: 'Theses are some release notes innit',
|
||||
pub_date: (new Date()).toString()
|
||||
});
|
||||
});
|
||||
const relaunchPromise = new Promise<void>((resolve) => {
|
||||
server.get('/update-check/updated/:version', (req, res) => {
|
||||
res.status(204).send();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
|
||||
logOnError(launchResult, () => {
|
||||
expect(launchResult).to.have.property('code', 0);
|
||||
expect(launchResult.out).to.include('Update Downloaded');
|
||||
expect(launchResult.out).to.include('Calling quitAndInstall mid-download');
|
||||
// First check + first download + second check + stalled second download.
|
||||
expect(requests).to.have.lengthOf(4);
|
||||
expect(requests[0]).to.have.property('url', '/update-check');
|
||||
expect(requests[1]).to.have.property('url', '/update-file');
|
||||
expect(requests[2]).to.have.property('url', '/update-check');
|
||||
expect(requests[3]).to.have.property('url', '/update-file');
|
||||
// The second download must have been in flight (never completed)
|
||||
// when quitAndInstall was called.
|
||||
expect(launchResult.out).to.not.include('Unexpected second download completion');
|
||||
});
|
||||
|
||||
// Unblock the stalled response now that the initial app has exited
|
||||
// so the express server can shut down cleanly.
|
||||
if (stalledResponse) {
|
||||
(stalledResponse as express.Response).status(500).end();
|
||||
}
|
||||
|
||||
// The originally staged update (2.0.0) must have been applied and
|
||||
// the app must relaunch, proving the staged update directory was
|
||||
// not pruned out from under ShipItState.plist.
|
||||
await relaunchPromise;
|
||||
expect(requests).to.have.lengthOf(5);
|
||||
expect(requests[4].url).to.equal('/update-check/updated/2.0.0');
|
||||
expect(requests[4].header('user-agent')).to.include('Electron/');
|
||||
});
|
||||
});
|
||||
|
||||
it('should update to lower version numbers', async () => {
|
||||
await withUpdatableApp({
|
||||
nextVersion: '0.0.1',
|
||||
|
||||
@@ -567,14 +567,7 @@ describe('command line switches', () => {
|
||||
});
|
||||
|
||||
it('creates startup trace', async () => {
|
||||
// node.async_hooks relies on %trace builtin to log trace points from JS
|
||||
// https://github.com/nodejs/node/blob/8b199eef3dd4de910a6521adc42ae611a62a19e1/lib/internal/trace_events_async_hooks.js#L48-L53
|
||||
// The phase event arg TRACE_EVENT_PHASE_NESTABLE_ASYNC_(BEGIN | END) is not supported in v8_use_perfetto mode
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/builtins/builtins-trace.cc;l=201-216
|
||||
// and leads to the following error: TypeError: Trace event phase must be a number.
|
||||
// TODO: Identify why the error started appearing with roll https://github.com/electron/electron/pull/47561
|
||||
// given both v8_use_perfetto has been enabled before the roll and builtins-trace macro hasn't changed.
|
||||
const rc = await startRemoteControlApp(['--trace-startup="*,-node.async_hooks"', `--trace-startup-file=${outputFilePath}`, '--trace-startup-duration=1', '--enable-logging']);
|
||||
const rc = await startRemoteControlApp(['--trace-startup=*', `--trace-startup-file=${outputFilePath}`, '--trace-startup-duration=1', '--enable-logging']);
|
||||
const stderrComplete = new Promise<string>(resolve => {
|
||||
let stderr = '';
|
||||
rc.process.stderr!.on('data', (chunk) => {
|
||||
|
||||
82
spec/fixtures/auto-update/update-race/index.js
vendored
Normal file
82
spec/fixtures/auto-update/update-race/index.js
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
const { app, autoUpdater } = require('electron');
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
let installInvoked = false;
|
||||
|
||||
autoUpdater.on('error', (err) => {
|
||||
// Once quitAndInstall() has been invoked the second in-flight check may
|
||||
// surface a cancellation/network error as the process tears down; ignore
|
||||
// errors after that point so we test the actual install race, not teardown.
|
||||
if (installInvoked) {
|
||||
console.log('Ignoring post-install error:', err && err.message);
|
||||
return;
|
||||
}
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const urlPath = path.resolve(__dirname, '../../../../url.txt');
|
||||
let feedUrl = process.argv[1];
|
||||
|
||||
if (feedUrl === 'remain-open') {
|
||||
// Hold the event loop
|
||||
setInterval(() => {});
|
||||
} else {
|
||||
if (!feedUrl || !feedUrl.startsWith('http')) {
|
||||
feedUrl = `${fs.readFileSync(urlPath, 'utf8')}/${app.getVersion()}`;
|
||||
} else {
|
||||
fs.writeFileSync(urlPath, `${feedUrl}/updated`);
|
||||
}
|
||||
|
||||
autoUpdater.setFeedURL({
|
||||
url: feedUrl
|
||||
});
|
||||
|
||||
autoUpdater.checkForUpdates();
|
||||
|
||||
autoUpdater.on('update-available', () => {
|
||||
console.log('Update Available');
|
||||
});
|
||||
|
||||
let downloadedOnce = false;
|
||||
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
console.log('Update Downloaded');
|
||||
if (!downloadedOnce) {
|
||||
downloadedOnce = true;
|
||||
// Simulate a periodic update check firing after an update was already
|
||||
// staged. The test server is expected to stall this second download so
|
||||
// that it remains in flight while we call quitAndInstall().
|
||||
// The short delay lets checkForUpdatesCommand's RACCommand executing
|
||||
// state settle; calling immediately would hit the command's "disabled"
|
||||
// guard since RACCommand disallows concurrent execution.
|
||||
setTimeout(() => {
|
||||
autoUpdater.checkForUpdates();
|
||||
// Give Squirrel enough time to enter the second check (creating a new
|
||||
// temporary directory, which with the regression prunes the directory
|
||||
// that the staged update lives in) before invoking the install.
|
||||
setTimeout(() => {
|
||||
console.log('Calling quitAndInstall mid-download');
|
||||
installInvoked = true;
|
||||
autoUpdater.quitAndInstall();
|
||||
}, 3000);
|
||||
}, 1000);
|
||||
} else {
|
||||
// Should not reach here — the second download is stalled on purpose.
|
||||
console.log('Unexpected second download completion');
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
console.error('No update available');
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
5
spec/fixtures/auto-update/update-race/package.json
vendored
Normal file
5
spec/fixtures/auto-update/update-race/package.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "electron-test-update-race",
|
||||
"version": "1.0.0",
|
||||
"main": "./index.js"
|
||||
}
|
||||
57
spec/fixtures/auto-update/update-triple-stack/index.js
vendored
Normal file
57
spec/fixtures/auto-update/update-triple-stack/index.js
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
const { app, autoUpdater } = require('electron');
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
autoUpdater.on('error', (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const urlPath = path.resolve(__dirname, '../../../../url.txt');
|
||||
let feedUrl = process.argv[1];
|
||||
|
||||
if (feedUrl === 'remain-open') {
|
||||
// Hold the event loop
|
||||
setInterval(() => {});
|
||||
} else {
|
||||
if (!feedUrl || !feedUrl.startsWith('http')) {
|
||||
feedUrl = `${fs.readFileSync(urlPath, 'utf8')}/${app.getVersion()}`;
|
||||
} else {
|
||||
fs.writeFileSync(urlPath, `${feedUrl}/updated`);
|
||||
}
|
||||
|
||||
autoUpdater.setFeedURL({
|
||||
url: feedUrl
|
||||
});
|
||||
|
||||
autoUpdater.checkForUpdates();
|
||||
|
||||
autoUpdater.on('update-available', () => {
|
||||
console.log('Update Available');
|
||||
});
|
||||
|
||||
let updateStackCount = 0;
|
||||
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
updateStackCount++;
|
||||
console.log('Update Downloaded');
|
||||
if (updateStackCount > 2) {
|
||||
autoUpdater.quitAndInstall();
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
autoUpdater.checkForUpdates();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
console.error('No update available');
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
5
spec/fixtures/auto-update/update-triple-stack/package.json
vendored
Normal file
5
spec/fixtures/auto-update/update-triple-stack/package.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "electron-test-update-triple-stack",
|
||||
"version": "1.0.0",
|
||||
"main": "./index.js"
|
||||
}
|
||||
Reference in New Issue
Block a user