mirror of
https://github.com/electron/electron.git
synced 2026-02-19 03:14:51 -05:00
chore: handle patch merge conflicts
This commit is contained in:
@@ -43,13 +43,10 @@
|
||||
#include "shell/browser/electron_gpu_client.h"
|
||||
#include "shell/browser/feature_list.h"
|
||||
#include "shell/browser/relauncher.h"
|
||||
#include "shell/common/application_info.h"
|
||||
#include "shell/common/electron_paths.h"
|
||||
#include "shell/common/logging.h"
|
||||
#include "shell/common/options_switches.h"
|
||||
#include "shell/common/platform_util.h"
|
||||
#include "shell/common/process_util.h"
|
||||
#include "shell/common/thread_restrictions.h"
|
||||
#include "shell/renderer/electron_renderer_client.h"
|
||||
#include "shell/renderer/electron_sandboxed_renderer_client.h"
|
||||
#include "shell/utility/electron_content_utility_client.h"
|
||||
@@ -121,100 +118,6 @@ void InvalidParameterHandler(const wchar_t*,
|
||||
}
|
||||
#endif
|
||||
|
||||
// TODO(nornagon): move path provider overriding to its own file in
|
||||
// shell/common
|
||||
bool ElectronPathProvider(int key, base::FilePath* result) {
|
||||
bool create_dir = false;
|
||||
base::FilePath cur;
|
||||
switch (key) {
|
||||
case chrome::DIR_USER_DATA:
|
||||
if (!base::PathService::Get(DIR_APP_DATA, &cur))
|
||||
return false;
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe(
|
||||
GetPossiblyOverriddenApplicationName()));
|
||||
create_dir = true;
|
||||
break;
|
||||
case DIR_CRASH_DUMPS:
|
||||
if (!base::PathService::Get(chrome::DIR_USER_DATA, &cur))
|
||||
return false;
|
||||
cur = cur.Append(FILE_PATH_LITERAL("Crashpad"));
|
||||
create_dir = true;
|
||||
break;
|
||||
case chrome::DIR_APP_DICTIONARIES:
|
||||
// TODO(nornagon): can we just default to using Chrome's logic here?
|
||||
if (!base::PathService::Get(DIR_SESSION_DATA, &cur))
|
||||
return false;
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe("Dictionaries"));
|
||||
create_dir = true;
|
||||
break;
|
||||
case DIR_SESSION_DATA:
|
||||
// By default and for backward, equivalent to DIR_USER_DATA.
|
||||
return base::PathService::Get(chrome::DIR_USER_DATA, result);
|
||||
case DIR_USER_CACHE: {
|
||||
#if BUILDFLAG(IS_POSIX)
|
||||
int parent_key = base::DIR_CACHE;
|
||||
#else
|
||||
// On Windows, there's no OS-level centralized location for caches, so
|
||||
// store the cache in the app data directory.
|
||||
int parent_key = base::DIR_ROAMING_APP_DATA;
|
||||
#endif
|
||||
if (!base::PathService::Get(parent_key, &cur))
|
||||
return false;
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe(
|
||||
GetPossiblyOverriddenApplicationName()));
|
||||
create_dir = true;
|
||||
break;
|
||||
}
|
||||
#if BUILDFLAG(IS_LINUX)
|
||||
case DIR_APP_DATA: {
|
||||
auto env = base::Environment::Create();
|
||||
cur = base::nix::GetXDGDirectory(
|
||||
env.get(), base::nix::kXdgConfigHomeEnvVar, base::nix::kDotConfigDir);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
case DIR_RECENT:
|
||||
if (!platform_util::GetFolderPath(DIR_RECENT, &cur))
|
||||
return false;
|
||||
create_dir = true;
|
||||
break;
|
||||
#endif
|
||||
case DIR_APP_LOGS:
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
if (!base::PathService::Get(base::DIR_HOME, &cur))
|
||||
return false;
|
||||
cur = cur.Append(FILE_PATH_LITERAL("Library"));
|
||||
cur = cur.Append(FILE_PATH_LITERAL("Logs"));
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe(
|
||||
GetPossiblyOverriddenApplicationName()));
|
||||
#else
|
||||
if (!base::PathService::Get(chrome::DIR_USER_DATA, &cur))
|
||||
return false;
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe("logs"));
|
||||
#endif
|
||||
create_dir = true;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO(bauerb): http://crbug.com/259796
|
||||
ScopedAllowBlockingForElectron allow_blocking;
|
||||
if (create_dir && !base::PathExists(cur) && !base::CreateDirectory(cur)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*result = cur;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void RegisterPathProvider() {
|
||||
base::PathService::RegisterProvider(ElectronPathProvider, PATH_START,
|
||||
PATH_END);
|
||||
}
|
||||
|
||||
void ValidateV8Snapshot(v8::StartupData* data) {
|
||||
if (data->data &&
|
||||
electron::fuses::IsEmbeddedAsarIntegrityValidationEnabled()) {
|
||||
|
||||
@@ -80,6 +80,11 @@ struct Converter<net::CookieChangeCause> {
|
||||
const net::CookieChangeCause& val) {
|
||||
switch (val) {
|
||||
case net::CookieChangeCause::INSERTED:
|
||||
return gin::StringToV8(isolate, "inserted");
|
||||
case net::CookieChangeCause::INSERTED_NO_CHANGE_OVERWRITE:
|
||||
return gin::StringToV8(isolate, "inserted-no-change-overwrite");
|
||||
case net::CookieChangeCause::INSERTED_NO_VALUE_CHANGE_OVERWRITE:
|
||||
return gin::StringToV8(isolate, "inserted-no-value-change-overwrite");
|
||||
case net::CookieChangeCause::EXPLICIT:
|
||||
return gin::StringToV8(isolate, "explicit");
|
||||
case net::CookieChangeCause::OVERWRITE:
|
||||
@@ -274,6 +279,17 @@ std::string StringToCookieSameSite(const std::string* str_ptr,
|
||||
return "";
|
||||
}
|
||||
|
||||
bool IsDeletion(net::CookieChangeCause cause) {
|
||||
switch (cause) {
|
||||
case net::CookieChangeCause::INSERTED:
|
||||
case net::CookieChangeCause::INSERTED_NO_CHANGE_OVERWRITE:
|
||||
case net::CookieChangeCause::INSERTED_NO_VALUE_CHANGE_OVERWRITE:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
gin::DeprecatedWrapperInfo Cookies::kWrapperInfo = {gin::kEmbedderNativeGin};
|
||||
@@ -437,10 +453,10 @@ v8::Local<v8::Promise> Cookies::FlushStore(v8::Isolate* isolate) {
|
||||
void Cookies::OnCookieChanged(const net::CookieChangeInfo& change) {
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
bool is_deletion = IsDeletion(change.cause);
|
||||
Emit("changed", gin::ConvertToV8(isolate, change.cookie),
|
||||
gin::ConvertToV8(isolate, change.cause),
|
||||
gin::ConvertToV8(isolate,
|
||||
change.cause != net::CookieChangeCause::INSERTED));
|
||||
gin::ConvertToV8(isolate, is_deletion));
|
||||
}
|
||||
|
||||
// static
|
||||
|
||||
@@ -68,6 +68,10 @@ Menu::Menu(gin::Arguments* args)
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
RemoveModelObserver();
|
||||
}
|
||||
|
||||
void Menu::RemoveModelObserver() {
|
||||
if (model_) {
|
||||
model_->RemoveObserver(this);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@ class Menu : public gin::Wrappable<Menu>,
|
||||
ElectronMenuModel* model() const { return model_.get(); }
|
||||
|
||||
protected:
|
||||
// Remove this instance as an observer from the model. Called by derived
|
||||
// class destructors to ensure observer is removed before platform-specific
|
||||
// cleanup that may trigger model callbacks.
|
||||
void RemoveModelObserver();
|
||||
// Returns a new callback which keeps references of the JS wrapper until the
|
||||
// passed |callback| is called.
|
||||
base::OnceClosure BindSelfToClosure(base::OnceClosure callback);
|
||||
|
||||
@@ -51,7 +51,11 @@ namespace electron::api {
|
||||
|
||||
MenuMac::MenuMac(gin::Arguments* args) : Menu{args} {}
|
||||
|
||||
MenuMac::~MenuMac() = default;
|
||||
MenuMac::~MenuMac() {
|
||||
// Must remove observer before destroying menu_controller_, which holds
|
||||
// a weak reference to model_
|
||||
RemoveModelObserver();
|
||||
}
|
||||
|
||||
void MenuMac::PopupAt(BaseWindow* window,
|
||||
std::optional<WebFrameMain*> frame,
|
||||
|
||||
528
shell/browser/api/electron_api_msix_updater.cc
Normal file
528
shell/browser/api/electron_api_msix_updater.cc
Normal file
@@ -0,0 +1,528 @@
|
||||
// Copyright (c) 2025 GitHub, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/api/electron_api_msix_updater.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
#include "base/environment.h"
|
||||
#include "base/functional/bind.h"
|
||||
#include "base/logging.h"
|
||||
#include "base/strings/utf_string_conversions.h"
|
||||
#include "base/task/single_thread_task_runner.h"
|
||||
#include "base/task/task_traits.h"
|
||||
#include "base/task/thread_pool.h"
|
||||
#include "content/public/browser/browser_task_traits.h"
|
||||
#include "content/public/browser/browser_thread.h"
|
||||
#include "shell/browser/browser.h"
|
||||
#include "shell/browser/javascript_environment.h"
|
||||
#include "shell/browser/native_window.h"
|
||||
#include "shell/browser/window_list.h"
|
||||
#include "shell/common/gin_helper/dictionary.h"
|
||||
#include "shell/common/gin_helper/error_thrower.h"
|
||||
#include "shell/common/gin_helper/promise.h"
|
||||
#include "shell/common/node_includes.h"
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
#include <appmodel.h>
|
||||
#include <roapi.h>
|
||||
#include <windows.applicationmodel.h>
|
||||
#include <windows.foundation.collections.h>
|
||||
#include <windows.foundation.h>
|
||||
#include <windows.foundation.metadata.h>
|
||||
#include <windows.h>
|
||||
#include <windows.management.deployment.h>
|
||||
// Use pre-generated C++/WinRT headers from //third_party/nearby instead of the
|
||||
// SDK's cppwinrt headers, which are missing implementation files.
|
||||
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/Windows.ApplicationModel.h"
|
||||
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/Windows.Foundation.Collections.h"
|
||||
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/Windows.Foundation.Metadata.h"
|
||||
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/Windows.Foundation.h"
|
||||
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/Windows.Management.Deployment.h"
|
||||
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/base.h"
|
||||
|
||||
#include "base/win/scoped_com_initializer.h"
|
||||
#endif
|
||||
|
||||
namespace electron {
|
||||
|
||||
const bool debug_msix_updater =
|
||||
base::Environment::Create()->HasVar("ELECTRON_DEBUG_MSIX_UPDATER");
|
||||
|
||||
} // namespace electron
|
||||
|
||||
namespace {
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
// Helper function for debug logging
|
||||
void DebugLog(std::string_view log_msg) {
|
||||
if (electron::debug_msix_updater)
|
||||
LOG(INFO) << std::string(log_msg);
|
||||
}
|
||||
|
||||
// Check if the process has a package identity
|
||||
bool HasPackageIdentity() {
|
||||
UINT32 length = 0;
|
||||
LONG rc = GetCurrentPackageFullName(&length, NULL);
|
||||
return rc != APPMODEL_ERROR_NO_PACKAGE;
|
||||
}
|
||||
|
||||
// POD struct to hold MSIX update options
|
||||
struct UpdateMsixOptions {
|
||||
bool defer_registration = false;
|
||||
bool developer_mode = false;
|
||||
bool force_shutdown = false;
|
||||
bool force_target_shutdown = false;
|
||||
bool force_update_from_any_version = false;
|
||||
};
|
||||
|
||||
// POD struct to hold package registration options
|
||||
struct RegisterPackageOptions {
|
||||
bool force_shutdown = false;
|
||||
bool force_target_shutdown = false;
|
||||
bool force_update_from_any_version = false;
|
||||
};
|
||||
|
||||
// Performs MSIX update on IO thread
|
||||
void DoUpdateMsix(const std::string& package_uri,
|
||||
UpdateMsixOptions opts,
|
||||
scoped_refptr<base::SingleThreadTaskRunner> reply_runner,
|
||||
gin_helper::Promise<void> promise) {
|
||||
DebugLog("DoUpdateMsix: Starting");
|
||||
|
||||
using winrt::Windows::Foundation::AsyncStatus;
|
||||
using winrt::Windows::Foundation::Uri;
|
||||
using winrt::Windows::Management::Deployment::AddPackageOptions;
|
||||
using winrt::Windows::Management::Deployment::DeploymentResult;
|
||||
using winrt::Windows::Management::Deployment::PackageManager;
|
||||
|
||||
std::string error;
|
||||
std::wstring packageUriString =
|
||||
std::wstring(package_uri.begin(), package_uri.end());
|
||||
Uri uri{packageUriString};
|
||||
PackageManager packageManager;
|
||||
AddPackageOptions packageOptions;
|
||||
|
||||
// Use the pre-parsed options
|
||||
packageOptions.DeferRegistrationWhenPackagesAreInUse(opts.defer_registration);
|
||||
packageOptions.DeveloperMode(opts.developer_mode);
|
||||
packageOptions.ForceAppShutdown(opts.force_shutdown);
|
||||
packageOptions.ForceTargetAppShutdown(opts.force_target_shutdown);
|
||||
packageOptions.ForceUpdateFromAnyVersion(opts.force_update_from_any_version);
|
||||
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "Calling AddPackageByUriAsync... URI: " << package_uri;
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "Update options - deferRegistration: " << opts.defer_registration
|
||||
<< ", developerMode: " << opts.developer_mode
|
||||
<< ", forceShutdown: " << opts.force_shutdown
|
||||
<< ", forceTargetShutdown: " << opts.force_target_shutdown
|
||||
<< ", forceUpdateFromAnyVersion: "
|
||||
<< opts.force_update_from_any_version;
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
|
||||
auto deploymentOperation =
|
||||
packageManager.AddPackageByUriAsync(uri, packageOptions);
|
||||
|
||||
if (!deploymentOperation) {
|
||||
DebugLog("Deployment operation is null");
|
||||
error =
|
||||
"Deployment is NULL. See "
|
||||
"http://go.microsoft.com/fwlink/?LinkId=235160 for diagnosing.";
|
||||
} else {
|
||||
if (!opts.force_shutdown && !opts.force_target_shutdown) {
|
||||
DebugLog("Waiting for deployment...");
|
||||
deploymentOperation.get();
|
||||
DebugLog("Deployment finished.");
|
||||
|
||||
if (deploymentOperation.Status() == AsyncStatus::Error) {
|
||||
auto deploymentResult{deploymentOperation.GetResults()};
|
||||
std::string errorText = winrt::to_string(deploymentResult.ErrorText());
|
||||
std::string errorCode =
|
||||
std::to_string(static_cast<int>(deploymentOperation.ErrorCode()));
|
||||
error = errorText + " (" + errorCode + ")";
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "Deployment failed: " << error;
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
} else if (deploymentOperation.Status() == AsyncStatus::Canceled) {
|
||||
DebugLog("Deployment canceled");
|
||||
error = "Deployment canceled";
|
||||
} else if (deploymentOperation.Status() == AsyncStatus::Completed) {
|
||||
DebugLog("MSIX Deployment completed.");
|
||||
} else {
|
||||
error = "Deployment status unknown";
|
||||
DebugLog("Deployment status unknown");
|
||||
}
|
||||
} else {
|
||||
// At this point, we can not await the deployment because we require a
|
||||
// shutdown of the app to continue, so we do a fire and forget. When the
|
||||
// deployment process tries ot shutdown the app, the process waits for us
|
||||
// to finish here. But to finish we need to shutdow. That leads to a 30s
|
||||
// dealock, till we forcefully get shutdown by the OS.
|
||||
DebugLog(
|
||||
"Deployment initiated. Force shutdown or target shutdown requested. "
|
||||
"Good bye!");
|
||||
}
|
||||
}
|
||||
|
||||
// Post result back
|
||||
reply_runner->PostTask(
|
||||
FROM_HERE, base::BindOnce(
|
||||
[](gin_helper::Promise<void> promise, std::string error) {
|
||||
if (error.empty()) {
|
||||
promise.Resolve();
|
||||
} else {
|
||||
promise.RejectWithErrorMessage(error);
|
||||
}
|
||||
},
|
||||
std::move(promise), error));
|
||||
}
|
||||
|
||||
// Performs package registration on IO thread
|
||||
void DoRegisterPackage(const std::string& family_name,
|
||||
RegisterPackageOptions opts,
|
||||
scoped_refptr<base::SingleThreadTaskRunner> reply_runner,
|
||||
gin_helper::Promise<void> promise) {
|
||||
DebugLog("DoRegisterPackage: Starting");
|
||||
|
||||
using winrt::Windows::Foundation::AsyncStatus;
|
||||
using winrt::Windows::Foundation::Collections::IIterable;
|
||||
using winrt::Windows::Management::Deployment::DeploymentOptions;
|
||||
using winrt::Windows::Management::Deployment::PackageManager;
|
||||
|
||||
std::string error;
|
||||
auto familyNameH = winrt::to_hstring(family_name);
|
||||
PackageManager packageManager;
|
||||
DeploymentOptions deploymentOptions = DeploymentOptions::None;
|
||||
|
||||
// Use the pre-parsed options (no V8 access needed)
|
||||
if (opts.force_shutdown) {
|
||||
deploymentOptions |= DeploymentOptions::ForceApplicationShutdown;
|
||||
}
|
||||
if (opts.force_target_shutdown) {
|
||||
deploymentOptions |= DeploymentOptions::ForceTargetApplicationShutdown;
|
||||
}
|
||||
if (opts.force_update_from_any_version) {
|
||||
deploymentOptions |= DeploymentOptions::ForceUpdateFromAnyVersion;
|
||||
}
|
||||
|
||||
// Create empty collections for dependency and optional packages
|
||||
IIterable<winrt::hstring> emptyDependencies{nullptr};
|
||||
IIterable<winrt::hstring> emptyOptional{nullptr};
|
||||
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "Calling RegisterPackageByFamilyNameAsync... FamilyName: "
|
||||
<< family_name;
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "Registration options - forceShutdown: " << opts.force_shutdown
|
||||
<< ", forceTargetShutdown: " << opts.force_target_shutdown
|
||||
<< ", forceUpdateFromAnyVersion: "
|
||||
<< opts.force_update_from_any_version;
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
|
||||
auto deploymentOperation = packageManager.RegisterPackageByFamilyNameAsync(
|
||||
familyNameH, emptyDependencies, deploymentOptions, nullptr,
|
||||
emptyOptional);
|
||||
|
||||
if (!deploymentOperation) {
|
||||
error =
|
||||
"Deployment is NULL. See "
|
||||
"http://go.microsoft.com/fwlink/?LinkId=235160 for diagnosing.";
|
||||
} else {
|
||||
if (!opts.force_shutdown && !opts.force_target_shutdown) {
|
||||
DebugLog("Waiting for registration...");
|
||||
deploymentOperation.get();
|
||||
DebugLog("Registration finished.");
|
||||
|
||||
if (deploymentOperation.Status() == AsyncStatus::Error) {
|
||||
auto deploymentResult{deploymentOperation.GetResults()};
|
||||
std::string errorText = winrt::to_string(deploymentResult.ErrorText());
|
||||
std::string errorCode =
|
||||
std::to_string(static_cast<int>(deploymentOperation.ErrorCode()));
|
||||
error = errorText + " (" + errorCode + ")";
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "Registration failed: " << error;
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
} else if (deploymentOperation.Status() == AsyncStatus::Canceled) {
|
||||
DebugLog("Registration canceled");
|
||||
error = "Registration canceled";
|
||||
} else if (deploymentOperation.Status() == AsyncStatus::Completed) {
|
||||
DebugLog("MSIX Registration completed.");
|
||||
} else {
|
||||
error = "Registration status unknown";
|
||||
DebugLog("Registration status unknown");
|
||||
}
|
||||
} else {
|
||||
// At this point, we can not await the registration because we require a
|
||||
// shutdown of the app to continue, so we do a fire and forget. When the
|
||||
// registration process tries ot shutdown the app, the process waits for
|
||||
// us to finish here. But to finish we need to shutdown. That leads to a
|
||||
// 30s dealock, till we forcefully get shutdown by the OS.
|
||||
DebugLog(
|
||||
"Registration initiated. Force shutdown or target shutdown "
|
||||
"requested. Good bye!");
|
||||
}
|
||||
}
|
||||
|
||||
// Post result back to UI thread
|
||||
reply_runner->PostTask(
|
||||
FROM_HERE, base::BindOnce(
|
||||
[](gin_helper::Promise<void> promise, std::string error) {
|
||||
if (error.empty()) {
|
||||
promise.Resolve();
|
||||
} else {
|
||||
promise.RejectWithErrorMessage(error);
|
||||
}
|
||||
},
|
||||
std::move(promise), error));
|
||||
}
|
||||
#endif
|
||||
|
||||
// Update MSIX package
|
||||
v8::Local<v8::Promise> UpdateMsix(const std::string& package_uri,
|
||||
gin_helper::Dictionary options) {
|
||||
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
|
||||
gin_helper::Promise<void> promise(isolate);
|
||||
v8::Local<v8::Promise> handle = promise.GetHandle();
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
if (!HasPackageIdentity()) {
|
||||
DebugLog("UpdateMsix: The process has no package identity");
|
||||
promise.RejectWithErrorMessage("The process has no package identity.");
|
||||
return handle;
|
||||
}
|
||||
|
||||
// Parse options on UI thread (where V8 is available)
|
||||
UpdateMsixOptions opts;
|
||||
options.Get("deferRegistration", &opts.defer_registration);
|
||||
options.Get("developerMode", &opts.developer_mode);
|
||||
options.Get("forceShutdown", &opts.force_shutdown);
|
||||
options.Get("forceTargetShutdown", &opts.force_target_shutdown);
|
||||
options.Get("forceUpdateFromAnyVersion", &opts.force_update_from_any_version);
|
||||
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "UpdateMsix called with URI: " << package_uri;
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
|
||||
// Post to IO thread
|
||||
content::GetIOThreadTaskRunner({})->PostTask(
|
||||
FROM_HERE,
|
||||
base::BindOnce(&DoUpdateMsix, package_uri, opts,
|
||||
base::SingleThreadTaskRunner::GetCurrentDefault(),
|
||||
std::move(promise)));
|
||||
#else
|
||||
promise.RejectWithErrorMessage(
|
||||
"MSIX updates are only supported on Windows with identity.");
|
||||
#endif
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
// Register MSIX package
|
||||
v8::Local<v8::Promise> RegisterPackage(const std::string& family_name,
|
||||
gin_helper::Dictionary options) {
|
||||
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
|
||||
gin_helper::Promise<void> promise(isolate);
|
||||
v8::Local<v8::Promise> handle = promise.GetHandle();
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
if (!HasPackageIdentity()) {
|
||||
DebugLog("RegisterPackage: The process has no package identity");
|
||||
promise.RejectWithErrorMessage("The process has no package identity.");
|
||||
return handle;
|
||||
}
|
||||
|
||||
// Parse options on UI thread (where V8 is available)
|
||||
RegisterPackageOptions opts;
|
||||
options.Get("forceShutdown", &opts.force_shutdown);
|
||||
options.Get("forceTargetShutdown", &opts.force_target_shutdown);
|
||||
options.Get("forceUpdateFromAnyVersion", &opts.force_update_from_any_version);
|
||||
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "RegisterPackage called with family name: " << family_name;
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
|
||||
// Post to IO thread with POD options (no V8 objects)
|
||||
content::GetIOThreadTaskRunner({})->PostTask(
|
||||
FROM_HERE,
|
||||
base::BindOnce(&DoRegisterPackage, family_name, opts,
|
||||
base::SingleThreadTaskRunner::GetCurrentDefault(),
|
||||
std::move(promise)));
|
||||
#else
|
||||
promise.RejectWithErrorMessage(
|
||||
"MSIX package registration is only supported on Windows.");
|
||||
#endif
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
// Register application restart
|
||||
// Only registers for update restarts (not crashes, hangs, or reboots)
|
||||
bool RegisterRestartOnUpdate(const std::string& command_line) {
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
if (!HasPackageIdentity()) {
|
||||
DebugLog("Cannot restart: no package identity");
|
||||
return false;
|
||||
}
|
||||
|
||||
const wchar_t* commandLine = nullptr;
|
||||
// Flags: RESTART_NO_CRASH | RESTART_NO_HANG | RESTART_NO_REBOOT
|
||||
// This means: only restart on updates (RESTART_NO_PATCH is NOT set)
|
||||
const DWORD dwFlags = 1 | 2 | 8; // 11
|
||||
|
||||
if (!command_line.empty()) {
|
||||
std::wstring commandLineW =
|
||||
std::wstring(command_line.begin(), command_line.end());
|
||||
commandLine = commandLineW.c_str();
|
||||
}
|
||||
|
||||
HRESULT hr = RegisterApplicationRestart(commandLine, dwFlags);
|
||||
if (FAILED(hr)) {
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "RegisterApplicationRestart failed with error code: " << hr;
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "RegisterApplicationRestart succeeded"
|
||||
<< (command_line.empty() ? "" : " with command line");
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Get package information
|
||||
v8::Local<v8::Value> GetPackageInfo() {
|
||||
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
// Check if running in a package
|
||||
if (!HasPackageIdentity()) {
|
||||
DebugLog("GetPackageInfo: The process has no package identity");
|
||||
gin_helper::ErrorThrower thrower(isolate);
|
||||
thrower.ThrowTypeError("The process has no package identity.");
|
||||
return v8::Null(isolate);
|
||||
}
|
||||
|
||||
DebugLog("GetPackageInfo: Retrieving package information");
|
||||
|
||||
gin_helper::Dictionary result(isolate, v8::Object::New(isolate));
|
||||
|
||||
// Check API contract version (Windows 10 version 1703 or later)
|
||||
if (winrt::Windows::Foundation::Metadata::ApiInformation::
|
||||
IsApiContractPresent(L"Windows.Foundation.UniversalApiContract", 7)) {
|
||||
using winrt::Windows::ApplicationModel::Package;
|
||||
using winrt::Windows::ApplicationModel::PackageSignatureKind;
|
||||
Package package = Package::Current();
|
||||
|
||||
// Get package ID and family name
|
||||
std::string packageId = winrt::to_string(package.Id().FullName());
|
||||
std::string familyName = winrt::to_string(package.Id().FamilyName());
|
||||
|
||||
result.Set("id", packageId);
|
||||
result.Set("familyName", familyName);
|
||||
result.Set("developmentMode", package.IsDevelopmentMode());
|
||||
|
||||
// Get package version
|
||||
auto packageVersion = package.Id().Version();
|
||||
std::string version = std::to_string(packageVersion.Major) + "." +
|
||||
std::to_string(packageVersion.Minor) + "." +
|
||||
std::to_string(packageVersion.Build) + "." +
|
||||
std::to_string(packageVersion.Revision);
|
||||
result.Set("version", version);
|
||||
|
||||
// Convert signature kind to string
|
||||
std::string signatureKind;
|
||||
switch (package.SignatureKind()) {
|
||||
case PackageSignatureKind::Developer:
|
||||
signatureKind = "developer";
|
||||
break;
|
||||
case PackageSignatureKind::Enterprise:
|
||||
signatureKind = "enterprise";
|
||||
break;
|
||||
case PackageSignatureKind::None:
|
||||
signatureKind = "none";
|
||||
break;
|
||||
case PackageSignatureKind::Store:
|
||||
signatureKind = "store";
|
||||
break;
|
||||
case PackageSignatureKind::System:
|
||||
signatureKind = "system";
|
||||
break;
|
||||
default:
|
||||
signatureKind = "none";
|
||||
break;
|
||||
}
|
||||
result.Set("signatureKind", signatureKind);
|
||||
|
||||
// Get app installer info if available
|
||||
auto appInstallerInfo = package.GetAppInstallerInfo();
|
||||
if (appInstallerInfo != nullptr) {
|
||||
std::string uriStr = winrt::to_string(appInstallerInfo.Uri().ToString());
|
||||
result.Set("appInstallerUri", uriStr);
|
||||
}
|
||||
} else {
|
||||
// Windows version doesn't meet minimum API requirements
|
||||
result.Set("familyName", "");
|
||||
result.Set("id", "");
|
||||
result.Set("developmentMode", false);
|
||||
result.Set("signatureKind", "none");
|
||||
result.Set("version", "");
|
||||
}
|
||||
|
||||
return result.GetHandle();
|
||||
#else
|
||||
// Non-Windows platforms
|
||||
gin_helper::Dictionary result(isolate, v8::Object::New(isolate));
|
||||
result.Set("familyName", "");
|
||||
result.Set("id", "");
|
||||
result.Set("developmentMode", false);
|
||||
result.Set("signatureKind", "none");
|
||||
result.Set("version", "");
|
||||
return result.GetHandle();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Initialize(v8::Local<v8::Object> exports,
|
||||
v8::Local<v8::Value> unused,
|
||||
v8::Local<v8::Context> context,
|
||||
void* priv) {
|
||||
v8::Isolate* const isolate = electron::JavascriptEnvironment::GetIsolate();
|
||||
gin_helper::Dictionary dict(isolate, exports);
|
||||
|
||||
dict.SetMethod("updateMsix", base::BindRepeating(&UpdateMsix));
|
||||
dict.SetMethod("registerPackage", base::BindRepeating(&RegisterPackage));
|
||||
dict.SetMethod("registerRestartOnUpdate",
|
||||
base::BindRepeating(&RegisterRestartOnUpdate));
|
||||
dict.SetMethod("getPackageInfo",
|
||||
base::BindRepeating([]() { return GetPackageInfo(); }));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_msix_updater, Initialize)
|
||||
14
shell/browser/api/electron_api_msix_updater.h
Normal file
14
shell/browser/api/electron_api_msix_updater.h
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2013 GitHub, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef ELECTRON_SHELL_BROWSER_API_ELECTRON_API_MSIX_UPDATER_H_
|
||||
#define ELECTRON_SHELL_BROWSER_API_ELECTRON_API_MSIX_UPDATER_H_
|
||||
|
||||
namespace electron {
|
||||
|
||||
extern const bool debug_msix_updater;
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // ELECTRON_SHELL_BROWSER_API_ELECTRON_API_MSIX_UPDATER_H_
|
||||
@@ -70,16 +70,26 @@ void DelayEmitWithMetrics(Screen* screen,
|
||||
screen->Emit(name, display, metrics);
|
||||
}
|
||||
|
||||
// Calls the one-liner `display::Screen::Get()` to get ui's global screen.
|
||||
// NOTE: during shutdown, that screen can be destroyed before us. This means:
|
||||
// 1. Call this instead of keeping a possibly-dangling raw_ptr in api::Screen.
|
||||
// 2. Always check this function's return value for nullptr before use.
|
||||
[[nodiscard]] auto* GetDisplayScreen() {
|
||||
return display::Screen::Get();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto GetFallbackDisplay() {
|
||||
return display::Display::GetDefaultDisplay();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Screen::Screen(display::Screen* screen) : screen_{screen} {
|
||||
screen_->AddObserver(this);
|
||||
Screen::Screen() {
|
||||
if (auto* screen = GetDisplayScreen())
|
||||
screen->AddObserver(this);
|
||||
}
|
||||
|
||||
Screen::~Screen() {
|
||||
// Use `display::Screen::Get()` here, not our cached `screen_`:
|
||||
// during shutdown, it can get torn down before us.
|
||||
if (auto* screen = display::Screen::Get())
|
||||
if (auto* screen = GetDisplayScreen())
|
||||
screen->RemoveObserver(this);
|
||||
}
|
||||
|
||||
@@ -95,7 +105,33 @@ gfx::Point Screen::GetCursorScreenPoint(v8::Isolate* isolate) {
|
||||
return {};
|
||||
}
|
||||
#endif
|
||||
return screen_->GetCursorScreenPoint();
|
||||
auto* screen = GetDisplayScreen();
|
||||
return screen ? screen->GetCursorScreenPoint() : gfx::Point{};
|
||||
}
|
||||
|
||||
display::Display Screen::GetPrimaryDisplay() const {
|
||||
const auto* screen = GetDisplayScreen();
|
||||
return screen ? screen->GetPrimaryDisplay() : GetFallbackDisplay();
|
||||
}
|
||||
|
||||
std::vector<display::Display> Screen::GetAllDisplays() const {
|
||||
if (const auto* screen = GetDisplayScreen())
|
||||
return screen->GetAllDisplays();
|
||||
|
||||
// Even though this is only reached during shutdown by Screen::Get() failing,
|
||||
// display::Screen::GetAllDisplays() is guaranteed to return >= 1 display.
|
||||
// For consistency with that API, let's return a nonempty vector here.
|
||||
return {GetFallbackDisplay()};
|
||||
}
|
||||
|
||||
display::Display Screen::GetDisplayNearestPoint(const gfx::Point& point) const {
|
||||
const auto* screen = GetDisplayScreen();
|
||||
return screen ? screen->GetDisplayNearestPoint(point) : GetFallbackDisplay();
|
||||
}
|
||||
|
||||
display::Display Screen::GetDisplayMatching(const gfx::Rect& match_rect) const {
|
||||
const auto* screen = GetDisplayScreen();
|
||||
return screen ? screen->GetDisplayMatching(match_rect) : GetFallbackDisplay();
|
||||
}
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
@@ -182,14 +218,14 @@ Screen* Screen::Create(gin_helper::ErrorThrower error_thrower) {
|
||||
return {};
|
||||
}
|
||||
|
||||
display::Screen* screen = display::Screen::Get();
|
||||
display::Screen* screen = GetDisplayScreen();
|
||||
if (!screen) {
|
||||
error_thrower.ThrowError("Failed to get screen information");
|
||||
return {};
|
||||
}
|
||||
|
||||
return cppgc::MakeGarbageCollected<Screen>(
|
||||
error_thrower.isolate()->GetCppHeap()->GetAllocationHandle(), screen);
|
||||
error_thrower.isolate()->GetCppHeap()->GetAllocationHandle());
|
||||
}
|
||||
|
||||
gin::ObjectTemplateBuilder Screen::GetObjectTemplateBuilder(
|
||||
@@ -218,7 +254,6 @@ const gin::WrapperInfo* Screen::wrapper_info() const {
|
||||
const char* Screen::GetHumanReadableName() const {
|
||||
return "Electron / Screen";
|
||||
}
|
||||
|
||||
} // namespace electron::api
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "gin/wrappable.h"
|
||||
#include "shell/browser/event_emitter_mixin.h"
|
||||
#include "ui/display/display_observer.h"
|
||||
@@ -43,22 +42,16 @@ class Screen final : public gin::Wrappable<Screen>,
|
||||
Screen& operator=(const Screen&) = delete;
|
||||
|
||||
// Make public for cppgc::MakeGarbageCollected.
|
||||
explicit Screen(display::Screen* screen);
|
||||
Screen();
|
||||
~Screen() override;
|
||||
|
||||
gfx::Point GetCursorScreenPoint(v8::Isolate* isolate);
|
||||
display::Display GetPrimaryDisplay() const {
|
||||
return screen_->GetPrimaryDisplay();
|
||||
}
|
||||
const std::vector<display::Display>& GetAllDisplays() const {
|
||||
return screen_->GetAllDisplays();
|
||||
}
|
||||
display::Display GetDisplayNearestPoint(const gfx::Point& point) const {
|
||||
return screen_->GetDisplayNearestPoint(point);
|
||||
}
|
||||
display::Display GetDisplayMatching(const gfx::Rect& match_rect) const {
|
||||
return screen_->GetDisplayMatching(match_rect);
|
||||
}
|
||||
[[nodiscard]] gfx::Point GetCursorScreenPoint(v8::Isolate* isolate);
|
||||
[[nodiscard]] display::Display GetPrimaryDisplay() const;
|
||||
[[nodiscard]] std::vector<display::Display> GetAllDisplays() const;
|
||||
[[nodiscard]] display::Display GetDisplayNearestPoint(
|
||||
const gfx::Point& point) const;
|
||||
[[nodiscard]] display::Display GetDisplayMatching(
|
||||
const gfx::Rect& match_rect) const;
|
||||
|
||||
gfx::PointF ScreenToDIPPoint(const gfx::PointF& point_px);
|
||||
gfx::Point DIPToScreenPoint(const gfx::Point& point_dip);
|
||||
@@ -68,9 +61,6 @@ class Screen final : public gin::Wrappable<Screen>,
|
||||
void OnDisplaysRemoved(const display::Displays& removed_displays) override;
|
||||
void OnDisplayMetricsChanged(const display::Display& display,
|
||||
uint32_t changed_metrics) override;
|
||||
|
||||
private:
|
||||
raw_ptr<display::Screen> screen_;
|
||||
};
|
||||
|
||||
} // namespace electron::api
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#include "shell/common/gin_helper/handle.h"
|
||||
#include "shell/common/gin_helper/object_template_builder.h"
|
||||
#include "shell/common/node_includes.h"
|
||||
#include "ui/compositor/layer.h"
|
||||
#include "ui/views/animation/animation_builder.h"
|
||||
#include "ui/views/background.h"
|
||||
#include "ui/views/layout/flex_layout.h"
|
||||
#include "ui/views/layout/layout_manager_base.h"
|
||||
@@ -144,6 +146,27 @@ struct Converter<views::SizeBounds> {
|
||||
.Build();
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct Converter<gfx::Tween::Type> {
|
||||
static bool FromV8(v8::Isolate* isolate,
|
||||
v8::Local<v8::Value> val,
|
||||
gfx::Tween::Type* out) {
|
||||
std::string easing = base::ToLowerASCII(gin::V8ToString(isolate, val));
|
||||
if (easing == "linear") {
|
||||
*out = gfx::Tween::LINEAR;
|
||||
} else if (easing == "ease-in") {
|
||||
*out = gfx::Tween::EASE_IN;
|
||||
} else if (easing == "ease-out") {
|
||||
*out = gfx::Tween::EASE_OUT;
|
||||
} else if (easing == "ease-in-out") {
|
||||
*out = gfx::Tween::EASE_IN_OUT;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
} // namespace gin
|
||||
|
||||
namespace electron::api {
|
||||
@@ -280,10 +303,116 @@ void View::RemoveChildView(gin_helper::Handle<View> child) {
|
||||
}
|
||||
}
|
||||
|
||||
void View::SetBounds(const gfx::Rect& bounds) {
|
||||
ui::Layer* View::GetLayer() {
|
||||
if (!view_)
|
||||
return nullptr;
|
||||
|
||||
if (view_->layer())
|
||||
return view_->layer();
|
||||
|
||||
view_->SetPaintToLayer();
|
||||
|
||||
ui::Layer* layer = view_->layer();
|
||||
|
||||
layer->SetFillsBoundsOpaquely(false);
|
||||
|
||||
return layer;
|
||||
}
|
||||
|
||||
void View::SetBounds(const gfx::Rect& bounds, gin::Arguments* const args) {
|
||||
bool animate = false;
|
||||
int duration = 250;
|
||||
gfx::Tween::Type easing = gfx::Tween::LINEAR;
|
||||
|
||||
gin_helper::Dictionary dict;
|
||||
if (args->GetNext(&dict)) {
|
||||
v8::Local<v8::Value> animate_value;
|
||||
|
||||
if (dict.Get("animate", &animate_value)) {
|
||||
if (animate_value->IsBoolean()) {
|
||||
animate = animate_value->BooleanValue(isolate());
|
||||
} else {
|
||||
animate = true;
|
||||
|
||||
gin_helper::Dictionary animate_dict;
|
||||
if (gin::ConvertFromV8(isolate(), animate_value, &animate_dict)) {
|
||||
animate_dict.Get("duration", &duration);
|
||||
animate_dict.Get("easing", &easing);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (duration < 0)
|
||||
duration = 0;
|
||||
|
||||
if (!view_)
|
||||
return;
|
||||
view_->SetBoundsRect(bounds);
|
||||
|
||||
if (!animate) {
|
||||
view_->SetBoundsRect(bounds);
|
||||
return;
|
||||
}
|
||||
|
||||
ui::Layer* layer = GetLayer();
|
||||
|
||||
gfx::Rect current_bounds = view_->bounds();
|
||||
|
||||
if (bounds.size() == current_bounds.size()) {
|
||||
// If the size isn't changing, we can just animate the bounds directly.
|
||||
|
||||
views::AnimationBuilder()
|
||||
.SetPreemptionStrategy(
|
||||
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
|
||||
.OnEnded(base::BindOnce(
|
||||
[](views::View* view, const gfx::Rect& final_bounds) {
|
||||
view->SetBoundsRect(final_bounds);
|
||||
},
|
||||
view_, bounds))
|
||||
.Once()
|
||||
.SetDuration(base::Milliseconds(duration))
|
||||
.SetBounds(view_, bounds, easing);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
gfx::Rect target_size = gfx::Rect(0, 0, bounds.width(), bounds.height());
|
||||
gfx::Rect max_size =
|
||||
gfx::Rect(current_bounds.x(), current_bounds.y(),
|
||||
std::max(current_bounds.width(), bounds.width()),
|
||||
std::max(current_bounds.height(), bounds.height()));
|
||||
|
||||
// if the view's size is smaller than the target size, we need to set the
|
||||
// view's bounds immediatley to the new size (not position) and set the
|
||||
// layer's clip rect to animate from there.
|
||||
if (view_->width() < bounds.width() || view_->height() < bounds.height()) {
|
||||
view_->SetBoundsRect(max_size);
|
||||
|
||||
if (layer) {
|
||||
layer->SetClipRect(
|
||||
gfx::Rect(0, 0, current_bounds.width(), current_bounds.height()));
|
||||
}
|
||||
}
|
||||
|
||||
views::AnimationBuilder()
|
||||
.SetPreemptionStrategy(
|
||||
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
|
||||
.OnEnded(base::BindOnce(
|
||||
[](views::View* view, const gfx::Rect& final_bounds,
|
||||
ui::Layer* layer) {
|
||||
view->SetBoundsRect(final_bounds);
|
||||
if (layer)
|
||||
layer->SetClipRect(gfx::Rect());
|
||||
},
|
||||
view_, bounds, layer))
|
||||
.Once()
|
||||
.SetDuration(base::Milliseconds(duration))
|
||||
.SetBounds(view_, bounds, easing)
|
||||
.SetClipRect(
|
||||
view_, target_size,
|
||||
easing); // We have to set the clip rect independently of the
|
||||
// bounds, because animating the bounds of the layer
|
||||
// will not animate the underlying view's bounds.
|
||||
}
|
||||
|
||||
gfx::Rect View::GetBounds() const {
|
||||
|
||||
@@ -37,7 +37,7 @@ class View : public gin_helper::EventEmitter<View>,
|
||||
std::optional<size_t> index);
|
||||
void RemoveChildView(gin_helper::Handle<View> child);
|
||||
|
||||
void SetBounds(const gfx::Rect& bounds);
|
||||
void SetBounds(const gfx::Rect& bounds, gin::Arguments* args);
|
||||
gfx::Rect GetBounds() const;
|
||||
void SetLayout(v8::Isolate* isolate, v8::Local<v8::Object> value);
|
||||
std::vector<v8::Local<v8::Value>> GetChildren();
|
||||
@@ -70,6 +70,7 @@ class View : public gin_helper::EventEmitter<View>,
|
||||
void OnChildViewRemoved(views::View* observed_view,
|
||||
views::View* child) override;
|
||||
|
||||
ui::Layer* GetLayer();
|
||||
void ApplyBorderRadius();
|
||||
void ReorderChildView(gin_helper::Handle<View> child, size_t index);
|
||||
|
||||
|
||||
@@ -400,31 +400,47 @@ class FileSystemAccessPermissionContext::PermissionGrantImpl
|
||||
}
|
||||
}
|
||||
|
||||
// Updates the in-memory permission grant for the `new_path` in the `grants`
|
||||
// map using the same grant from the `old_path`, and removes the grant entry
|
||||
// for the `old_path`.
|
||||
// If `allow_overwrite` is true, this will replace any pre-existing grant at
|
||||
// `new_path`.
|
||||
static void UpdateGrantPath(
|
||||
std::map<base::FilePath, PermissionGrantImpl*>& grants,
|
||||
const content::PathInfo& old_path,
|
||||
const content::PathInfo& new_path) {
|
||||
const content::PathInfo& new_path,
|
||||
bool allow_overwrite) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
auto entry_it =
|
||||
auto old_path_it =
|
||||
std::ranges::find_if(grants, [&old_path](const auto& entry) {
|
||||
return entry.first == old_path.path;
|
||||
});
|
||||
|
||||
if (entry_it == grants.end()) {
|
||||
// There must be an entry for an ancestor of this entry. Nothing to do
|
||||
// here.
|
||||
if (old_path_it == grants.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
DCHECK_EQ(entry_it->second->GetActivePermissionStatus(),
|
||||
DCHECK_EQ(old_path_it->second->GetActivePermissionStatus(),
|
||||
PermissionStatus::GRANTED);
|
||||
|
||||
auto* const grant_impl = entry_it->second;
|
||||
grant_impl->SetPath(new_path);
|
||||
auto* const grant_to_move = old_path_it->second;
|
||||
|
||||
// Update the permission grant's key in the map of active permissions.
|
||||
grants.erase(entry_it);
|
||||
grants.emplace(new_path.path, grant_impl);
|
||||
// See https://chromium-review.googlesource.com/4803165
|
||||
if (allow_overwrite) {
|
||||
auto new_path_it = grants.find(new_path.path);
|
||||
if (new_path_it != grants.end() && new_path_it->second != grant_to_move) {
|
||||
new_path_it->second->SetStatus(PermissionStatus::DENIED);
|
||||
}
|
||||
}
|
||||
|
||||
grant_to_move->SetPath(new_path);
|
||||
|
||||
grants.erase(old_path_it);
|
||||
if (allow_overwrite) {
|
||||
grants.insert_or_assign(new_path.path, grant_to_move);
|
||||
} else {
|
||||
grants.emplace(new_path.path, grant_to_move);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -930,12 +946,17 @@ void FileSystemAccessPermissionContext::NotifyEntryMoved(
|
||||
return;
|
||||
}
|
||||
|
||||
// It's possible `new_path` already has existing persistent permission.
|
||||
// See crbug.com/423663220.
|
||||
bool allow_overwrite = base::FeatureList::IsEnabled(
|
||||
features::kFileSystemAccessMoveWithOverwrite);
|
||||
|
||||
auto it = active_permissions_map_.find(origin);
|
||||
if (it != active_permissions_map_.end()) {
|
||||
PermissionGrantImpl::UpdateGrantPath(it->second.write_grants, old_path,
|
||||
new_path);
|
||||
new_path, allow_overwrite);
|
||||
PermissionGrantImpl::UpdateGrantPath(it->second.read_grants, old_path,
|
||||
new_path);
|
||||
new_path, allow_overwrite);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,11 +56,6 @@ static NSDictionary* UNNotificationResponseToNSDictionary(
|
||||
}
|
||||
|
||||
- (void)applicationWillFinishLaunching:(NSNotification*)notify {
|
||||
// Don't add the "Enter Full Screen" menu item automatically.
|
||||
[[NSUserDefaults standardUserDefaults]
|
||||
setBool:NO
|
||||
forKey:@"NSFullScreenMenuItemEverywhere"];
|
||||
|
||||
[[[NSWorkspace sharedWorkspace] notificationCenter]
|
||||
addObserver:self
|
||||
selector:@selector(willPowerOff:)
|
||||
@@ -114,7 +109,14 @@ static NSDictionary* UNNotificationResponseToNSDictionary(
|
||||
}
|
||||
|
||||
- (NSMenu*)applicationDockMenu:(NSApplication*)sender {
|
||||
return menu_controller_ ? menu_controller_.menu : nil;
|
||||
if (!menu_controller_)
|
||||
return nil;
|
||||
|
||||
// Manually refresh menu state since menuWillOpen: is not called
|
||||
// by macOS for dock menus for some reason before they are displayed.
|
||||
NSMenu* menu = menu_controller_.menu;
|
||||
[menu_controller_ refreshMenuTree:menu];
|
||||
return menu;
|
||||
}
|
||||
|
||||
- (BOOL)application:(NSApplication*)sender openFile:(NSString*)filename {
|
||||
|
||||
@@ -57,6 +57,10 @@ class ElectronMenuModel;
|
||||
// Whether the menu is currently open.
|
||||
- (BOOL)isMenuOpen;
|
||||
|
||||
// Recursively refreshes the menu tree starting from |menu|, applying the
|
||||
// model state (enabled, checked, hidden etc) to each menu item.
|
||||
- (void)refreshMenuTree:(NSMenu*)menu;
|
||||
|
||||
// NSMenuDelegate methods this class implements. Subclasses should call super
|
||||
// if extending the behavior.
|
||||
- (void)menuWillOpen:(NSMenu*)menu;
|
||||
|
||||
@@ -484,14 +484,15 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
|
||||
if (index < 0 || index >= count)
|
||||
return;
|
||||
|
||||
// When the menu is closed, we need to allow shortcuts to be triggered even
|
||||
// if the menu item is disabled. So we only disable the menu item when the
|
||||
// menu is open. This matches behavior of |validateUserInterfaceItem|.
|
||||
item.enabled = model->IsEnabledAt(index) || !isMenuOpen_;
|
||||
item.hidden = !model->IsVisibleAt(index);
|
||||
item.enabled = model->IsEnabledAt(index);
|
||||
item.state = model->IsItemCheckedAt(index) ? NSControlStateValueOn
|
||||
: NSControlStateValueOff;
|
||||
}
|
||||
|
||||
// Recursively refreshes the menu tree starting from |menu|, applying the
|
||||
// model state to each menu item.
|
||||
- (void)refreshMenuTree:(NSMenu*)menu {
|
||||
for (NSMenuItem* item in menu.itemArray) {
|
||||
[self applyStateToMenuItem:item];
|
||||
@@ -557,6 +558,14 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
|
||||
|
||||
- (void)menuWillOpen:(NSMenu*)menu {
|
||||
isMenuOpen_ = YES;
|
||||
|
||||
// macOS automatically injects a duplicate "Toggle Full Screen" menu item
|
||||
// when we set menu.delegate on submenus. Remove hidden duplicates.
|
||||
for (NSMenuItem* item in menu.itemArray) {
|
||||
if (item.isHidden && item.action == @selector(toggleFullScreenMode:))
|
||||
[menu removeItem:item];
|
||||
}
|
||||
|
||||
[self refreshMenuTree:menu];
|
||||
if (model_)
|
||||
model_->MenuWillShow();
|
||||
@@ -567,18 +576,19 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
|
||||
if (!isMenuOpen_)
|
||||
return;
|
||||
|
||||
bool has_close_cb = !popupCloseCallback.is_null();
|
||||
isMenuOpen_ = NO;
|
||||
[self refreshMenuTree:menu];
|
||||
|
||||
// There are two scenarios where we should emit menu-did-close:
|
||||
// 1. It's a popup and the top level menu is closed.
|
||||
// 2. It's an application menu, and the current menu's supermenu
|
||||
// is the top-level menu.
|
||||
bool has_close_cb = !popupCloseCallback.is_null();
|
||||
if (menu != menu_) {
|
||||
if (has_close_cb || menu.supermenu != menu_)
|
||||
return;
|
||||
}
|
||||
|
||||
isMenuOpen_ = NO;
|
||||
if (model_)
|
||||
model_->MenuWillClose();
|
||||
// Post async task so that itemSelected runs before the close callback
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
#include "ui/views/widget/widget.h"
|
||||
#include "ui/views/widget/widget_delegate.h"
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
#include "ui/accessibility/platform/ax_platform_node_win.h"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view kTargetsDataFile = "targets-data.json";
|
||||
@@ -151,6 +155,28 @@ bool ShouldHandleAccessibilityRequestCallback(const std::string& path) {
|
||||
return path == kTargetsDataFile;
|
||||
}
|
||||
|
||||
// Sets boolean values in `data` for each bit in `new_ax_mode` that differs from
|
||||
// that in `last_ax_mode`. Returns `true` if `data` was modified.
|
||||
void SetProcessModeBools(ui::AXMode ax_mode, base::Value::Dict& data) {
|
||||
data.Set(kNative, ax_mode.has_mode(ui::AXMode::kNativeAPIs));
|
||||
data.Set(kWeb, ax_mode.has_mode(ui::AXMode::kWebContents));
|
||||
data.Set(kText, ax_mode.has_mode(ui::AXMode::kInlineTextBoxes));
|
||||
data.Set(kExtendedProperties,
|
||||
ax_mode.has_mode(ui::AXMode::kExtendedProperties));
|
||||
data.Set(kHTML, ax_mode.has_mode(ui::AXMode::kHTML));
|
||||
data.Set(kScreenReader, ax_mode.has_mode(ui::AXMode::kScreenReader));
|
||||
}
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
// Sets values in `data` for the platform node counts in `counts`.
|
||||
void SetNodeCounts(const ui::AXPlatformNodeWin::Counts& counts,
|
||||
base::Value::Dict& data) {
|
||||
data.Set("dormantCount", base::NumberToString(counts.dormant_nodes));
|
||||
data.Set("liveCount", base::NumberToString(counts.live_nodes));
|
||||
data.Set("ghostCount", base::NumberToString(counts.ghost_nodes));
|
||||
}
|
||||
#endif
|
||||
|
||||
void HandleAccessibilityRequestCallback(
|
||||
content::BrowserContext* current_context,
|
||||
ui::AXMode initial_process_mode,
|
||||
@@ -294,6 +320,10 @@ void HandleAccessibilityRequestCallback(
|
||||
}
|
||||
data.Set(kBrowsersField, std::move(window_list));
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
SetNodeCounts(ui::AXPlatformNodeWin::GetCounts(), data);
|
||||
#endif
|
||||
|
||||
std::move(callback).Run(base::MakeRefCounted<base::RefCountedString>(
|
||||
base::WriteJson(data).value_or("")));
|
||||
}
|
||||
@@ -381,8 +411,13 @@ ElectronAccessibilityUI::ElectronAccessibilityUI(content::WebUI* web_ui)
|
||||
|
||||
ElectronAccessibilityUI::~ElectronAccessibilityUI() = default;
|
||||
|
||||
ElectronAccessibilityUIMessageHandler::ElectronAccessibilityUIMessageHandler() =
|
||||
default;
|
||||
ElectronAccessibilityUIMessageHandler::ElectronAccessibilityUIMessageHandler()
|
||||
: update_display_timer_(
|
||||
FROM_HERE,
|
||||
base::Seconds(1),
|
||||
base::BindRepeating(
|
||||
&ElectronAccessibilityUIMessageHandler::OnUpdateDisplayTimer,
|
||||
base::Unretained(this))) {}
|
||||
|
||||
void ElectronAccessibilityUIMessageHandler::GetRequestTypeAndFilters(
|
||||
const base::DictValue& data,
|
||||
@@ -472,6 +507,10 @@ void ElectronAccessibilityUIMessageHandler::RegisterMessages() {
|
||||
base::BindRepeating(
|
||||
&AccessibilityUIMessageHandler::RequestAccessibilityEvents,
|
||||
base::Unretained(this)));
|
||||
|
||||
auto* web_contents = web_ui()->GetWebContents();
|
||||
Observe(web_contents);
|
||||
OnVisibilityChanged(web_contents->GetVisibility());
|
||||
}
|
||||
|
||||
// static
|
||||
@@ -482,3 +521,45 @@ void ElectronAccessibilityUIMessageHandler::RegisterPrefs(
|
||||
registry->RegisterStringPref(prefs::kShownAccessibilityApiType,
|
||||
std::string(default_api_type));
|
||||
}
|
||||
|
||||
void ElectronAccessibilityUIMessageHandler::OnVisibilityChanged(
|
||||
content::Visibility visibility) {
|
||||
if (visibility == content::Visibility::HIDDEN) {
|
||||
update_display_timer_.Stop();
|
||||
} else {
|
||||
update_display_timer_.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
void ElectronAccessibilityUIMessageHandler::OnUpdateDisplayTimer() {
|
||||
// Collect the current state.
|
||||
base::Value::Dict data;
|
||||
|
||||
SetProcessModeBools(
|
||||
content::BrowserAccessibilityState::GetInstance()->GetAccessibilityMode(),
|
||||
data);
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
SetNodeCounts(ui::AXPlatformNodeWin::GetCounts(), data);
|
||||
#endif // BUILDFLAG(IS_WIN)
|
||||
|
||||
// Compute the delta from the last transmission.
|
||||
for (auto scan = data.begin(); scan != data.end();) {
|
||||
const auto& [new_key, new_value] = *scan;
|
||||
if (const auto* old_value = last_data_.Find(new_key);
|
||||
!old_value || *old_value != new_value) {
|
||||
// This is a new value; remember it for the future.
|
||||
last_data_.Set(new_key, new_value.Clone());
|
||||
++scan;
|
||||
} else {
|
||||
// This is the same as the last value; forget about it.
|
||||
scan = data.erase(scan);
|
||||
}
|
||||
}
|
||||
|
||||
// Transmit any new values to the UI.
|
||||
if (!data.empty()) {
|
||||
AllowJavascript();
|
||||
FireWebUIListener("updateDisplay", data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,21 @@ class ElectronAccessibilityUIMessageHandler
|
||||
std::string& allow,
|
||||
std::string& allow_empty,
|
||||
std::string& deny);
|
||||
|
||||
void RequestNativeUITree(const base::ListValue& args);
|
||||
|
||||
// content::WebContentsObserver:
|
||||
void OnVisibilityChanged(content::Visibility visibility) override;
|
||||
|
||||
// Updates the UI with new data. Called periodically to keep the UI up-to-date
|
||||
// while it is visible.
|
||||
void OnUpdateDisplayTimer();
|
||||
|
||||
// The last data for display sent to the UI by OnUpdateDisplayTimer.
|
||||
base::Value::Dict last_data_;
|
||||
|
||||
// A timer that runs while the UI is visible to call OnUpdateDisplayTimer.
|
||||
base::RepeatingTimer update_display_timer_;
|
||||
};
|
||||
|
||||
#endif // ELECTRON_SHELL_BROWSER_UI_WEBUI_ACCESSIBILITY_UI_H_
|
||||
|
||||
@@ -150,6 +150,31 @@ bool ElectronDesktopWindowTreeHostWin::HandleMouseEvent(ui::MouseEvent* event) {
|
||||
return views::DesktopWindowTreeHostWin::HandleMouseEvent(event);
|
||||
}
|
||||
|
||||
bool ElectronDesktopWindowTreeHostWin::HandleIMEMessage(UINT message,
|
||||
WPARAM w_param,
|
||||
LPARAM l_param,
|
||||
LRESULT* result) {
|
||||
if ((message == WM_SYSCHAR) && (w_param == VK_SPACE)) {
|
||||
if (native_window_view_->widget() &&
|
||||
native_window_view_->widget()->non_client_view()) {
|
||||
const auto* frame =
|
||||
native_window_view_->widget()->non_client_view()->frame_view();
|
||||
auto location = frame->GetSystemMenuScreenPixelLocation();
|
||||
|
||||
bool prevent_default = false;
|
||||
native_window_view_->NotifyWindowSystemContextMenu(
|
||||
location.x(), location.y(), &prevent_default);
|
||||
|
||||
return prevent_default ||
|
||||
views::DesktopWindowTreeHostWin::HandleIMEMessage(message, w_param,
|
||||
l_param, result);
|
||||
}
|
||||
}
|
||||
|
||||
return views::DesktopWindowTreeHostWin::HandleIMEMessage(message, w_param,
|
||||
l_param, result);
|
||||
}
|
||||
|
||||
void ElectronDesktopWindowTreeHostWin::HandleVisibilityChanged(bool visible) {
|
||||
if (native_window_view_->widget())
|
||||
native_window_view_->widget()->OnNativeWidgetVisibilityChanged(visible);
|
||||
|
||||
@@ -44,6 +44,10 @@ class ElectronDesktopWindowTreeHostWin : public views::DesktopWindowTreeHostWin,
|
||||
int frame_thickness) const override;
|
||||
bool HandleMouseEventForCaption(UINT message) const override;
|
||||
bool HandleMouseEvent(ui::MouseEvent* event) override;
|
||||
bool HandleIMEMessage(UINT message,
|
||||
WPARAM w_param,
|
||||
LPARAM l_param,
|
||||
LRESULT* result) override;
|
||||
void HandleVisibilityChanged(bool visible) override;
|
||||
void SetAllowScreenshots(bool allow) override;
|
||||
|
||||
|
||||
117
shell/common/electron_paths.cc
Normal file
117
shell/common/electron_paths.cc
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2026 Microsoft GmbH.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/common/electron_paths.h"
|
||||
|
||||
#include "base/environment.h"
|
||||
#include "base/files/file_util.h"
|
||||
#include "base/path_service.h"
|
||||
#include "chrome/common/chrome_paths.h"
|
||||
#include "shell/common/application_info.h"
|
||||
#include "shell/common/platform_util.h"
|
||||
#include "shell/common/thread_restrictions.h"
|
||||
|
||||
#if BUILDFLAG(IS_LINUX)
|
||||
#include "base/nix/xdg_util.h"
|
||||
#endif
|
||||
|
||||
namespace electron {
|
||||
|
||||
namespace {
|
||||
|
||||
bool ElectronPathProvider(int key, base::FilePath* result) {
|
||||
bool create_dir = false;
|
||||
base::FilePath cur;
|
||||
switch (key) {
|
||||
case chrome::DIR_USER_DATA:
|
||||
if (!base::PathService::Get(DIR_APP_DATA, &cur))
|
||||
return false;
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe(
|
||||
GetPossiblyOverriddenApplicationName()));
|
||||
create_dir = true;
|
||||
break;
|
||||
case DIR_CRASH_DUMPS:
|
||||
if (!base::PathService::Get(chrome::DIR_USER_DATA, &cur))
|
||||
return false;
|
||||
cur = cur.Append(FILE_PATH_LITERAL("Crashpad"));
|
||||
create_dir = true;
|
||||
break;
|
||||
case chrome::DIR_APP_DICTIONARIES:
|
||||
// TODO(nornagon): can we just default to using Chrome's logic here?
|
||||
if (!base::PathService::Get(DIR_SESSION_DATA, &cur))
|
||||
return false;
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe("Dictionaries"));
|
||||
create_dir = true;
|
||||
break;
|
||||
case DIR_SESSION_DATA:
|
||||
// By default and for backward, equivalent to DIR_USER_DATA.
|
||||
return base::PathService::Get(chrome::DIR_USER_DATA, result);
|
||||
case DIR_USER_CACHE: {
|
||||
#if BUILDFLAG(IS_POSIX)
|
||||
int parent_key = base::DIR_CACHE;
|
||||
#else
|
||||
// On Windows, there's no OS-level centralized location for caches, so
|
||||
// store the cache in the app data directory.
|
||||
int parent_key = base::DIR_ROAMING_APP_DATA;
|
||||
#endif
|
||||
if (!base::PathService::Get(parent_key, &cur))
|
||||
return false;
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe(
|
||||
GetPossiblyOverriddenApplicationName()));
|
||||
create_dir = true;
|
||||
break;
|
||||
}
|
||||
#if BUILDFLAG(IS_LINUX)
|
||||
case DIR_APP_DATA: {
|
||||
auto env = base::Environment::Create();
|
||||
cur = base::nix::GetXDGDirectory(
|
||||
env.get(), base::nix::kXdgConfigHomeEnvVar, base::nix::kDotConfigDir);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
case DIR_RECENT:
|
||||
if (!platform_util::GetFolderPath(DIR_RECENT, &cur))
|
||||
return false;
|
||||
create_dir = true;
|
||||
break;
|
||||
#endif
|
||||
case DIR_APP_LOGS:
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
if (!base::PathService::Get(base::DIR_HOME, &cur))
|
||||
return false;
|
||||
cur = cur.Append(FILE_PATH_LITERAL("Library"));
|
||||
cur = cur.Append(FILE_PATH_LITERAL("Logs"));
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe(
|
||||
GetPossiblyOverriddenApplicationName()));
|
||||
#else
|
||||
if (!base::PathService::Get(chrome::DIR_USER_DATA, &cur))
|
||||
return false;
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe("logs"));
|
||||
#endif
|
||||
create_dir = true;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO(bauerb): http://crbug.com/259796
|
||||
ScopedAllowBlockingForElectron allow_blocking;
|
||||
if (create_dir && !base::PathExists(cur) && !base::CreateDirectory(cur)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*result = cur;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RegisterPathProvider() {
|
||||
base::PathService::RegisterProvider(ElectronPathProvider, PATH_START,
|
||||
PATH_END);
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
@@ -6,6 +6,7 @@
|
||||
#define ELECTRON_SHELL_COMMON_ELECTRON_PATHS_H_
|
||||
|
||||
#include "base/base_paths.h"
|
||||
#include "base/files/file_path.h"
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
#include "base/base_paths_win.h"
|
||||
@@ -47,6 +48,9 @@ enum {
|
||||
|
||||
static_assert(PATH_START < PATH_END, "invalid PATH boundaries");
|
||||
|
||||
// Register the path provider with the base::PathService.
|
||||
void RegisterPathProvider();
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // ELECTRON_SHELL_COMMON_ELECTRON_PATHS_H_
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
V(electron_browser_in_app_purchase) \
|
||||
V(electron_browser_menu) \
|
||||
V(electron_browser_message_port) \
|
||||
V(electron_browser_msix_updater) \
|
||||
V(electron_browser_native_theme) \
|
||||
V(electron_browser_notification) \
|
||||
V(electron_browser_power_monitor) \
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -27,8 +29,13 @@
|
||||
#include "base/run_loop.h"
|
||||
#include "base/strings/escape.h"
|
||||
#include "base/strings/string_util.h"
|
||||
#include "base/task/thread_pool.h"
|
||||
#include "base/threading/thread_restrictions.h"
|
||||
#include "base/types/expected.h"
|
||||
#include "components/dbus/thread_linux/dbus_thread_linux.h"
|
||||
#include "components/dbus/utils/call_method.h"
|
||||
#include "components/dbus/utils/check_for_service_and_start.h"
|
||||
#include "components/dbus/xdg/request.h"
|
||||
#include "content/public/browser/browser_thread.h"
|
||||
#include "dbus/bus.h"
|
||||
#include "dbus/message.h"
|
||||
@@ -45,9 +52,6 @@ void OpenFolder(const base::FilePath& full_path);
|
||||
|
||||
namespace {
|
||||
|
||||
const char kMethodListActivatableNames[] = "ListActivatableNames";
|
||||
const char kMethodNameHasOwner[] = "NameHasOwner";
|
||||
|
||||
const char kFreedesktopFileManagerName[] = "org.freedesktop.FileManager1";
|
||||
const char kFreedesktopFileManagerPath[] = "/org/freedesktop/FileManager1";
|
||||
|
||||
@@ -58,6 +62,7 @@ const char kFreedesktopPortalPath[] = "/org/freedesktop/portal/desktop";
|
||||
const char kFreedesktopPortalOpenURI[] = "org.freedesktop.portal.OpenURI";
|
||||
|
||||
const char kMethodOpenDirectory[] = "OpenDirectory";
|
||||
const char kActivationTokenKey[] = "activation_token";
|
||||
|
||||
class ShowItemHelper {
|
||||
public:
|
||||
@@ -72,179 +77,216 @@ class ShowItemHelper {
|
||||
ShowItemHelper& operator=(const ShowItemHelper&) = delete;
|
||||
|
||||
void ShowItemInFolder(const base::FilePath& full_path) {
|
||||
if (!bus_)
|
||||
if (!bus_) {
|
||||
bus_ = dbus_thread_linux::GetSharedSessionBus();
|
||||
|
||||
if (!dbus_proxy_) {
|
||||
dbus_proxy_ = bus_->GetObjectProxy(DBUS_SERVICE_DBUS,
|
||||
dbus::ObjectPath(DBUS_PATH_DBUS));
|
||||
}
|
||||
|
||||
if (prefer_filemanager_interface_.has_value()) {
|
||||
if (prefer_filemanager_interface_.value()) {
|
||||
ShowItemUsingFileManager(full_path);
|
||||
} else {
|
||||
ShowItemUsingFreedesktopPortal(full_path);
|
||||
}
|
||||
} else {
|
||||
CheckFileManagerRunning(full_path);
|
||||
if (api_type_.has_value()) {
|
||||
ShowItemInFolderOnApiTypeSet(full_path);
|
||||
return;
|
||||
}
|
||||
|
||||
bool api_availability_check_in_progress = !pending_requests_.empty();
|
||||
pending_requests_.push(full_path);
|
||||
if (!api_availability_check_in_progress) {
|
||||
// Initiate check to determine if portal or the FileManager API should
|
||||
// be used. The portal API is always preferred if available.
|
||||
dbus_utils::CheckForServiceAndStart(
|
||||
bus_.get(), kFreedesktopPortalName,
|
||||
base::BindOnce(&ShowItemHelper::CheckPortalRunningResponse,
|
||||
// Unretained is safe, the ShowItemHelper instance is
|
||||
// never destroyed.
|
||||
base::Unretained(this)));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void CheckFileManagerRunning(const base::FilePath& full_path) {
|
||||
dbus::MethodCall method_call(DBUS_INTERFACE_DBUS, kMethodNameHasOwner);
|
||||
dbus::MessageWriter writer(&method_call);
|
||||
writer.AppendString(kFreedesktopFileManagerName);
|
||||
enum class ApiType { kNone, kPortal, kFileManager };
|
||||
|
||||
dbus_proxy_->CallMethod(
|
||||
&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
|
||||
base::BindOnce(&ShowItemHelper::CheckFileManagerRunningResponse,
|
||||
base::Unretained(this), full_path));
|
||||
void ShowItemInFolderOnApiTypeSet(const base::FilePath& full_path) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
CHECK(api_type_.has_value());
|
||||
switch (*api_type_) {
|
||||
case ApiType::kPortal:
|
||||
ShowItemUsingPortal(full_path);
|
||||
break;
|
||||
case ApiType::kFileManager:
|
||||
ShowItemUsingFileManager(full_path);
|
||||
break;
|
||||
case ApiType::kNone:
|
||||
OpenParentFolderFallback(full_path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void CheckFileManagerRunningResponse(const base::FilePath& full_path,
|
||||
dbus::Response* response) {
|
||||
if (prefer_filemanager_interface_.has_value()) {
|
||||
ShowItemInFolder(full_path);
|
||||
void ProcessPendingRequests() {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
if (!bus_) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool is_running = false;
|
||||
|
||||
if (!response) {
|
||||
LOG(ERROR) << "Failed to call " << kMethodNameHasOwner;
|
||||
} else {
|
||||
dbus::MessageReader reader(response);
|
||||
bool owned = false;
|
||||
|
||||
if (!reader.PopBool(&owned)) {
|
||||
LOG(ERROR) << "Failed to read " << kMethodNameHasOwner << " response";
|
||||
} else if (owned) {
|
||||
is_running = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_running) {
|
||||
prefer_filemanager_interface_ = true;
|
||||
ShowItemInFolder(full_path);
|
||||
} else {
|
||||
CheckFileManagerActivatable(full_path);
|
||||
CHECK(!pending_requests_.empty());
|
||||
while (!pending_requests_.empty()) {
|
||||
ShowItemInFolderOnApiTypeSet(pending_requests_.front());
|
||||
pending_requests_.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void CheckFileManagerActivatable(const base::FilePath& full_path) {
|
||||
dbus::MethodCall method_call(DBUS_INTERFACE_DBUS,
|
||||
kMethodListActivatableNames);
|
||||
dbus_proxy_->CallMethod(
|
||||
&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
|
||||
base::BindOnce(&ShowItemHelper::CheckFileManagerActivatableResponse,
|
||||
void CheckPortalRunningResponse(std::optional<bool> is_running) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
if (is_running.value_or(false)) {
|
||||
api_type_ = ApiType::kPortal;
|
||||
ProcessPendingRequests();
|
||||
} else {
|
||||
// Portal is unavailable.
|
||||
// Check if FileManager is available.
|
||||
dbus_utils::CheckForServiceAndStart(
|
||||
bus_.get(), kFreedesktopFileManagerName,
|
||||
base::BindOnce(&ShowItemHelper::CheckFileManagerRunningResponse,
|
||||
// Unretained is safe, the ShowItemHelper instance is
|
||||
// never destroyed.
|
||||
base::Unretained(this)));
|
||||
}
|
||||
}
|
||||
|
||||
void CheckFileManagerRunningResponse(std::optional<bool> is_running) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
if (is_running.value_or(false)) {
|
||||
api_type_ = ApiType::kFileManager;
|
||||
} else {
|
||||
// Neither portal nor FileManager is available.
|
||||
api_type_ = ApiType::kNone;
|
||||
}
|
||||
ProcessPendingRequests();
|
||||
}
|
||||
|
||||
void ShowItemUsingPortal(const base::FilePath& full_path) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
CHECK(api_type_.has_value());
|
||||
CHECK_EQ(*api_type_, ApiType::kPortal);
|
||||
base::ThreadPool::PostTaskAndReplyWithResult(
|
||||
FROM_HERE, {base::MayBlock()},
|
||||
base::BindOnce(
|
||||
[](const base::FilePath& full_path) {
|
||||
base::ScopedFD fd(HANDLE_EINTR(
|
||||
open(full_path.value().c_str(), O_RDONLY | O_CLOEXEC)));
|
||||
return fd;
|
||||
},
|
||||
full_path),
|
||||
base::BindOnce(&ShowItemHelper::ShowItemUsingPortalFdOpened,
|
||||
// Unretained is safe, the ShowItemHelper instance is
|
||||
// never destroyed.
|
||||
base::Unretained(this), full_path));
|
||||
}
|
||||
|
||||
void CheckFileManagerActivatableResponse(const base::FilePath& full_path,
|
||||
dbus::Response* response) {
|
||||
if (prefer_filemanager_interface_.has_value()) {
|
||||
ShowItemInFolder(full_path);
|
||||
void ShowItemUsingPortalFdOpened(const base::FilePath& full_path,
|
||||
base::ScopedFD fd) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
if (!bus_) {
|
||||
return;
|
||||
}
|
||||
if (!fd.is_valid()) {
|
||||
// At least open the parent folder, as long as we're not in the unit
|
||||
// tests.
|
||||
OpenParentFolderFallback(full_path);
|
||||
return;
|
||||
}
|
||||
base::nix::CreateXdgActivationToken(base::BindOnce(
|
||||
&ShowItemHelper::ShowItemUsingPortalWithToken,
|
||||
// Unretained is safe, the ShowItemHelper instance is never destroyed.
|
||||
base::Unretained(this), full_path, std::move(fd)));
|
||||
}
|
||||
|
||||
void ShowItemUsingPortalWithToken(const base::FilePath& full_path,
|
||||
base::ScopedFD fd,
|
||||
std::string activation_token) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
if (!bus_) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool is_activatable = false;
|
||||
|
||||
if (!response) {
|
||||
LOG(ERROR) << "Failed to call " << kMethodListActivatableNames;
|
||||
} else {
|
||||
dbus::MessageReader reader(response);
|
||||
std::vector<std::string> names;
|
||||
if (!reader.PopArrayOfStrings(&names)) {
|
||||
LOG(ERROR) << "Failed to read " << kMethodListActivatableNames
|
||||
<< " response";
|
||||
} else if (std::ranges::contains(names, kFreedesktopFileManagerName)) {
|
||||
is_activatable = true;
|
||||
}
|
||||
}
|
||||
|
||||
prefer_filemanager_interface_ = is_activatable;
|
||||
ShowItemInFolder(full_path);
|
||||
}
|
||||
|
||||
void ShowItemUsingFreedesktopPortal(const base::FilePath& full_path) {
|
||||
if (!object_proxy_) {
|
||||
object_proxy_ = bus_->GetObjectProxy(
|
||||
if (!portal_object_proxy_) {
|
||||
portal_object_proxy_ = bus_->GetObjectProxy(
|
||||
kFreedesktopPortalName, dbus::ObjectPath(kFreedesktopPortalPath));
|
||||
}
|
||||
|
||||
base::ScopedFD fd(
|
||||
HANDLE_EINTR(open(full_path.value().c_str(), O_RDONLY | O_CLOEXEC)));
|
||||
if (!fd.is_valid()) {
|
||||
LOG(ERROR) << "Failed to open " << full_path << " for URI portal";
|
||||
dbus_xdg::Dictionary options;
|
||||
options[kActivationTokenKey] =
|
||||
dbus_utils::Variant::Wrap<"s">(activation_token);
|
||||
// In the rare occasion that another request comes in before the response is
|
||||
// received, we will end up overwriting this request object with the new one
|
||||
// and the response from the first request will not be handled in that case.
|
||||
// This should be acceptable as it means the two requests were received too
|
||||
// close to each other from the user and the first one was handled on a best
|
||||
// effort basis.
|
||||
portal_open_directory_request_ = std::make_unique<dbus_xdg::Request>(
|
||||
bus_, portal_object_proxy_, kFreedesktopPortalOpenURI,
|
||||
kMethodOpenDirectory, std::move(options),
|
||||
base::BindOnce(&ShowItemHelper::ShowItemUsingPortalResponse,
|
||||
// Unretained is safe, the ShowItemHelper instance is
|
||||
// never destroyed.
|
||||
base::Unretained(this), full_path),
|
||||
std::string(), std::move(fd));
|
||||
}
|
||||
|
||||
// If the call fails, at least open the parent folder.
|
||||
platform_util::OpenFolder(full_path.DirName());
|
||||
|
||||
return;
|
||||
void ShowItemUsingPortalResponse(
|
||||
const base::FilePath& full_path,
|
||||
base::expected<dbus_xdg::Dictionary, dbus_xdg::ResponseError> results) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
portal_open_directory_request_.reset();
|
||||
if (!results.has_value()) {
|
||||
OpenParentFolderFallback(full_path);
|
||||
}
|
||||
|
||||
dbus::MethodCall open_directory_call(kFreedesktopPortalOpenURI,
|
||||
kMethodOpenDirectory);
|
||||
dbus::MessageWriter writer(&open_directory_call);
|
||||
|
||||
writer.AppendString("");
|
||||
|
||||
// Note that AppendFileDescriptor() duplicates the fd, so we shouldn't
|
||||
// release ownership of it here.
|
||||
writer.AppendFileDescriptor(fd.get());
|
||||
|
||||
dbus::MessageWriter options_writer(nullptr);
|
||||
writer.OpenArray("{sv}", &options_writer);
|
||||
writer.CloseContainer(&options_writer);
|
||||
|
||||
ShowItemUsingBusCall(&open_directory_call, full_path);
|
||||
}
|
||||
|
||||
void ShowItemUsingFileManager(const base::FilePath& full_path) {
|
||||
if (!object_proxy_) {
|
||||
object_proxy_ =
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
if (!bus_) {
|
||||
return;
|
||||
}
|
||||
CHECK(api_type_.has_value());
|
||||
CHECK_EQ(*api_type_, ApiType::kFileManager);
|
||||
if (!file_manager_object_proxy_) {
|
||||
file_manager_object_proxy_ =
|
||||
bus_->GetObjectProxy(kFreedesktopFileManagerName,
|
||||
dbus::ObjectPath(kFreedesktopFileManagerPath));
|
||||
}
|
||||
|
||||
dbus::MethodCall show_items_call(kFreedesktopFileManagerName,
|
||||
kMethodShowItems);
|
||||
dbus::MessageWriter writer(&show_items_call);
|
||||
|
||||
writer.AppendArrayOfStrings(
|
||||
{"file://" + base::EscapePath(
|
||||
full_path.value())}); // List of file(s) to highlight.
|
||||
writer.AppendString({}); // startup-id
|
||||
|
||||
ShowItemUsingBusCall(&show_items_call, full_path);
|
||||
std::vector<std::string> file_to_highlight{"file://" + full_path.value()};
|
||||
dbus_utils::CallMethod<"ass", "">(
|
||||
file_manager_object_proxy_, kFreedesktopFileManagerName,
|
||||
kMethodShowItems,
|
||||
base::BindOnce(&ShowItemHelper::ShowItemUsingFileManagerResponse,
|
||||
// Unretained is safe, the ShowItemHelper instance is
|
||||
// never destroyed.
|
||||
base::Unretained(this), full_path),
|
||||
std::move(file_to_highlight), /*startup-id=*/"");
|
||||
}
|
||||
|
||||
void ShowItemUsingBusCall(dbus::MethodCall* call,
|
||||
const base::FilePath& full_path) {
|
||||
object_proxy_->CallMethod(
|
||||
call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
|
||||
base::BindOnce(&ShowItemHelper::ShowItemInFolderResponse,
|
||||
base::Unretained(this), full_path, call->GetMember()));
|
||||
void ShowItemUsingFileManagerResponse(
|
||||
const base::FilePath& full_path,
|
||||
dbus_utils::CallMethodResultSig<""> response) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
if (!response.has_value()) {
|
||||
// If the bus call fails, at least open the parent folder.
|
||||
OpenParentFolderFallback(full_path);
|
||||
}
|
||||
}
|
||||
|
||||
void ShowItemInFolderResponse(const base::FilePath& full_path,
|
||||
const std::string& method,
|
||||
dbus::Response* response) {
|
||||
if (response)
|
||||
return;
|
||||
|
||||
LOG(ERROR) << "Error calling " << method;
|
||||
// If the bus call fails, at least open the parent folder.
|
||||
void OpenParentFolderFallback(const base::FilePath& full_path) {
|
||||
platform_util::OpenFolder(full_path.DirName());
|
||||
}
|
||||
|
||||
scoped_refptr<dbus::Bus> bus_;
|
||||
raw_ptr<dbus::ObjectProxy> dbus_proxy_ = nullptr;
|
||||
raw_ptr<dbus::ObjectProxy> object_proxy_ = nullptr;
|
||||
|
||||
std::optional<bool> prefer_filemanager_interface_;
|
||||
std::optional<ApiType> api_type_;
|
||||
// The proxy objects are owned by `bus_`.
|
||||
raw_ptr<dbus::ObjectProxy> portal_object_proxy_ = nullptr;
|
||||
raw_ptr<dbus::ObjectProxy> file_manager_object_proxy_ = nullptr;
|
||||
std::unique_ptr<dbus_xdg::Request> portal_open_directory_request_;
|
||||
|
||||
// Requests that are queued until the API availability is determined.
|
||||
std::queue<base::FilePath> pending_requests_;
|
||||
};
|
||||
|
||||
// Descriptions pulled from https://linux.die.net/man/1/xdg-open
|
||||
|
||||
Reference in New Issue
Block a user