diff --git a/docs/api/app.md b/docs/api/app.md index e1455d6a2e1e..e424f43856d6 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -1216,6 +1216,13 @@ Returns `Object`: should restore the state from the previous session. This indicates that the app should restore the windows that were open the last time the app was closed. This setting is not available on [MAS builds][mas-builds]. +* `executableWillLaunchAtLogin` Boolean _Windows_ - `true` if app is set to open at login and its run key is not deactivated. This differs from `openAtLogin` as it ignores the `args` option, this property will be true if the given executable would be launched at login with **any** arguments. +* `launchItems` Object[] _Windows_ + * `name` String _Windows_ - name value of a registry entry. + * `path` String _Windows_ - The executable to an app that corresponds to a registry entry. + * `args` String[] _Windows_ - the command-line arguments to pass to the executable. + * `scope` String _Windows_ - one of `user` or `machine`. Indicates whether the registry entry is under `HKEY_CURRENT USER` or `HKEY_LOCAL_MACHINE`. + * `enabled` Boolean _Windows_ - `true` if the app registry key is startup approved and therfore shows as `enabled` in Task Manager and Windows settings. ### `app.setLoginItemSettings(settings)` _macOS_ _Windows_ @@ -1231,7 +1238,9 @@ Returns `Object`: * `args` String[] (optional) _Windows_ - The command-line arguments to pass to the executable. Defaults to an empty array. Take care to wrap paths in quotes. - + * `enabled` Boolean (optional) _Windows_ - `true` will change the startup approved registry key and `enable / disable` the App in Task Manager and Windows Settings. + Defaults to `true`. + * `name` String (optional) _Windows_ - value name to write into registry. Defaults to the app's AppUserModelId(). Set the app's login item settings. To work with Electron's `autoUpdater` on Windows, which uses [Squirrel][Squirrel-Windows], diff --git a/shell/browser/api/electron_api_app.cc b/shell/browser/api/electron_api_app.cc index a39b41d4f329..a43013809025 100644 --- a/shell/browser/api/electron_api_app.cc +++ b/shell/browser/api/electron_api_app.cc @@ -334,6 +334,37 @@ struct Converter { }; #endif +#if defined(OS_WIN) +template <> +struct Converter { + static bool FromV8(v8::Isolate* isolate, + v8::Local val, + Browser::LaunchItem* out) { + gin_helper::Dictionary dict; + if (!ConvertFromV8(isolate, val, &dict)) + return false; + + dict.Get("name", &(out->name)); + dict.Get("path", &(out->path)); + dict.Get("args", &(out->args)); + dict.Get("scope", &(out->scope)); + dict.Get("enabled", &(out->enabled)); + return true; + } + + static v8::Local ToV8(v8::Isolate* isolate, + Browser::LaunchItem val) { + gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(isolate); + dict.Set("name", val.name); + dict.Set("path", val.path); + dict.Set("args", val.args); + dict.Set("scope", val.scope); + dict.Set("enabled", val.enabled); + return dict.GetHandle(); + } +}; +#endif + template <> struct Converter { static bool FromV8(v8::Isolate* isolate, @@ -347,6 +378,10 @@ struct Converter { dict.Get("openAsHidden", &(out->open_as_hidden)); dict.Get("path", &(out->path)); dict.Get("args", &(out->args)); +#if defined(OS_WIN) + dict.Get("enabled", &(out->enabled)); + dict.Get("name", &(out->name)); +#endif return true; } @@ -358,6 +393,11 @@ struct Converter { dict.Set("restoreState", val.restore_state); dict.Set("wasOpenedAtLogin", val.opened_at_login); dict.Set("wasOpenedAsHidden", val.opened_as_hidden); +#if defined(OS_WIN) + dict.Set("launchItems", val.launch_items); + dict.Set("executableWillLaunchAtLogin", + val.executable_will_launch_at_login); +#endif return dict.GetHandle(); } }; diff --git a/shell/browser/browser.cc b/shell/browser/browser.cc index 6340f56b52de..b9e0ccfde110 100644 --- a/shell/browser/browser.cc +++ b/shell/browser/browser.cc @@ -40,6 +40,12 @@ void RunQuitClosure(base::OnceClosure quit) { } // namespace +#if defined(OS_WIN) +Browser::LaunchItem::LaunchItem() = default; +Browser::LaunchItem::~LaunchItem() = default; +Browser::LaunchItem::LaunchItem(const LaunchItem& other) = default; +#endif + Browser::LoginItemSettings::LoginItemSettings() = default; Browser::LoginItemSettings::~LoginItemSettings() = default; Browser::LoginItemSettings::LoginItemSettings(const LoginItemSettings& other) = diff --git a/shell/browser/browser.h b/shell/browser/browser.h index 9228700a4ac1..3c9e52c95d07 100644 --- a/shell/browser/browser.h +++ b/shell/browser/browser.h @@ -110,6 +110,20 @@ class Browser : public WindowListObserver { bool SetBadgeCount(int count); int GetBadgeCount(); +#if defined(OS_WIN) + struct LaunchItem { + base::string16 name; + base::string16 path; + base::string16 scope; + std::vector args; + bool enabled = true; + + LaunchItem(); + ~LaunchItem(); + LaunchItem(const LaunchItem&); + }; +#endif + // Set/Get the login item settings of the app struct LoginItemSettings { bool open_at_login = false; @@ -120,6 +134,16 @@ class Browser : public WindowListObserver { base::string16 path; std::vector args; +#if defined(OS_WIN) + // used in browser::setLoginItemSettings + bool enabled = true; + base::string16 name = base::string16(); + + // used in browser::getLoginItemSettings + bool executable_will_launch_at_login = false; + std::vector launch_items; +#endif + LoginItemSettings(); ~LoginItemSettings(); LoginItemSettings(const LoginItemSettings&); diff --git a/shell/browser/browser_win.cc b/shell/browser/browser_win.cc index d790dbde9ef1..fa2f3c480b4e 100644 --- a/shell/browser/browser_win.cc +++ b/shell/browser/browser_win.cc @@ -16,6 +16,7 @@ #include "base/base_paths.h" #include "base/file_version_info.h" #include "base/files/file_path.h" +#include "base/logging.h" #include "base/path_service.h" #include "base/strings/string_util.h" #include "base/strings/stringprintf.h" @@ -183,6 +184,87 @@ bool FormatCommandLineString(base::string16* exe, return true; } +// Helper for GetLoginItemSettings(). +// iterates over all the entries in a windows registry path and returns +// a list of launchItem with matching paths to our application. +// if a launchItem with a matching path also has a matching entry within the +// startup_approved_key_path, set executable_will_launch_at_login to be `true` +std::vector GetLoginItemSettingsHelper( + base::win::RegistryValueIterator* it, + boolean* executable_will_launch_at_login, + base::string16 scope, + const Browser::LoginItemSettings& options) { + std::vector launch_items; + + while (it->Valid()) { + base::string16 exe = options.path; + if (FormatCommandLineString(&exe, options.args)) { + // add launch item to vector if it has a matching path (case-insensitive) + if ((base::CompareCaseInsensitiveASCII(it->Value(), exe.c_str())) == 0) { + Browser::LaunchItem launch_item; + base::string16 launch_path = options.path; + if (!(launch_path.size() > 0)) { + GetProcessExecPath(&launch_path); + } + launch_item.name = it->Name(); + launch_item.path = launch_path; + launch_item.args = options.args; + launch_item.scope = scope; + launch_item.enabled = true; + + // attempt to update launch_item.enabled if there is a matching key + // value entry in the StartupApproved registry + HKEY hkey; + // StartupApproved registry path + LPCTSTR path = TEXT( + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApp" + "roved\\Run"); + LONG res; + if (scope == L"user") { + res = + RegOpenKeyEx(HKEY_CURRENT_USER, path, 0, KEY_QUERY_VALUE, &hkey); + } else { + res = + RegOpenKeyEx(HKEY_LOCAL_MACHINE, path, 0, KEY_QUERY_VALUE, &hkey); + } + if (res == ERROR_SUCCESS) { + DWORD type, size; + wchar_t startup_binary[12]; + LONG result = + RegQueryValueEx(hkey, it->Name(), nullptr, &type, + reinterpret_cast(&startup_binary), + &(size = sizeof(startup_binary))); + if (result == ERROR_SUCCESS) { + if (type == REG_BINARY) { + // any other binary other than this indicates that the program is + // not set to launch at login + wchar_t binary_accepted[12] = {0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00}; + wchar_t binary_accepted_alt[12] = {0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00}; + std::string reg_binary(reinterpret_cast(binary_accepted)); + std::string reg_binary_alt( + reinterpret_cast(binary_accepted_alt)); + std::string reg_startup_binary( + reinterpret_cast(startup_binary)); + launch_item.enabled = (reg_binary == reg_startup_binary) || + (reg_binary == reg_binary_alt); + } + } + } + + *executable_will_launch_at_login = + *executable_will_launch_at_login || launch_item.enabled; + launch_items.push_back(launch_item); + } + } + it->operator++(); + } + return launch_items; +} + std::unique_ptr FetchFileVersionInfo() { base::FilePath path; @@ -515,16 +597,46 @@ bool Browser::SetBadgeCount(int count) { } void Browser::SetLoginItemSettings(LoginItemSettings settings) { - base::string16 keyPath = L"Software\\Microsoft\\Windows\\CurrentVersion\\Run"; - base::win::RegKey key(HKEY_CURRENT_USER, keyPath.c_str(), KEY_ALL_ACCESS); + base::string16 key_path = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Run"; + base::win::RegKey key(HKEY_CURRENT_USER, key_path.c_str(), KEY_ALL_ACCESS); + + base::string16 startup_approved_key_path = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApproved" + L"\\Run"; + base::win::RegKey startup_approved_key( + HKEY_CURRENT_USER, startup_approved_key_path.c_str(), KEY_ALL_ACCESS); + PCWSTR key_name = + settings.name.size() > 0 ? settings.name.c_str() : GetAppUserModelID(); if (settings.open_at_login) { base::string16 exe = settings.path; if (FormatCommandLineString(&exe, settings.args)) { - key.WriteValue(GetAppUserModelID(), exe.c_str()); + key.WriteValue(key_name, exe.c_str()); + + if (settings.enabled) { + startup_approved_key.DeleteValue(key_name); + } else { + HKEY hard_key; + LPCTSTR path = TEXT( + "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApp" + "roved\\Run"); + LONG res = + RegOpenKeyEx(HKEY_CURRENT_USER, path, 0, KEY_ALL_ACCESS, &hard_key); + + if (res == ERROR_SUCCESS) { + UCHAR disable_startup_binary[] = {0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + RegSetValueEx(hard_key, key_name, 0, REG_BINARY, + reinterpret_cast(disable_startup_binary), + sizeof(disable_startup_binary)); + } + } } } else { - key.DeleteValue(GetAppUserModelID()); + // if open at login is false, delete both values + startup_approved_key.DeleteValue(key_name); + key.DeleteValue(key_name); } } @@ -535,6 +647,7 @@ Browser::LoginItemSettings Browser::GetLoginItemSettings( base::win::RegKey key(HKEY_CURRENT_USER, keyPath.c_str(), KEY_ALL_ACCESS); base::string16 keyVal; + // keep old openAtLogin behaviour if (!FAILED(key.ReadValue(GetAppUserModelID(), &keyVal))) { base::string16 exe = options.path; if (FormatCommandLineString(&exe, options.args)) { @@ -542,6 +655,27 @@ Browser::LoginItemSettings Browser::GetLoginItemSettings( } } + // iterate over current user and machine registries and populate launch items + // if there exists a launch entry with property enabled=='true', + // set executable_will_launch_at_login to 'true'. + boolean executable_will_launch_at_login = false; + std::vector launch_items; + base::win::RegistryValueIterator hkcu_iterator(HKEY_CURRENT_USER, + keyPath.c_str()); + base::win::RegistryValueIterator hklm_iterator(HKEY_LOCAL_MACHINE, + keyPath.c_str()); + + launch_items = GetLoginItemSettingsHelper( + &hkcu_iterator, &executable_will_launch_at_login, L"user", options); + std::vector launch_items_hklm = + GetLoginItemSettingsHelper(&hklm_iterator, + &executable_will_launch_at_login, L"machine", + options); + launch_items.insert(launch_items.end(), launch_items_hklm.begin(), + launch_items_hklm.end()); + + settings.executable_will_launch_at_login = executable_will_launch_at_login; + settings.launch_items = launch_items; return settings; } diff --git a/spec-main/api-app-spec.ts b/spec-main/api-app-spec.ts index 4b5797ab9a75..11dab023be47 100644 --- a/spec-main/api-app-spec.ts +++ b/spec-main/api-app-spec.ts @@ -619,7 +619,7 @@ describe('app module', () => { app.setLoginItemSettings({ openAtLogin: false, path: updateExe, args: processStartArgs }); }); - it('sets and returns the app as a login item', done => { + ifit(process.platform !== 'win32')('sets and returns the app as a login item', function () { app.setLoginItemSettings({ openAtLogin: true }); expect(app.getLoginItemSettings()).to.deep.equal({ openAtLogin: true, @@ -628,10 +628,28 @@ describe('app module', () => { wasOpenedAsHidden: false, restoreState: false }); - done(); }); - it('adds a login item that loads in hidden mode', done => { + ifit(process.platform === 'win32')('sets and returns the app as a login item (windows)', function () { + app.setLoginItemSettings({ openAtLogin: true }); + expect(app.getLoginItemSettings()).to.deep.equal({ + openAtLogin: true, + openAsHidden: false, + wasOpenedAtLogin: false, + wasOpenedAsHidden: false, + restoreState: false, + executableWillLaunchAtLogin: true, + launchItems: [{ + name: 'electron.app.Electron', + path: process.execPath, + args: [], + scope: 'user', + enabled: true + }] + }); + }); + + ifit(process.platform !== 'win32')('adds a login item that loads in hidden mode', function () { app.setLoginItemSettings({ openAtLogin: true, openAsHidden: true }); expect(app.getLoginItemSettings()).to.deep.equal({ openAtLogin: true, @@ -640,7 +658,25 @@ describe('app module', () => { wasOpenedAsHidden: false, restoreState: false }); - done(); + }); + + ifit(process.platform === 'win32')('adds a login item that loads in hidden mode (windows)', function () { + app.setLoginItemSettings({ openAtLogin: true, openAsHidden: true }); + expect(app.getLoginItemSettings()).to.deep.equal({ + openAtLogin: true, + openAsHidden: false, + wasOpenedAtLogin: false, + wasOpenedAsHidden: false, + restoreState: false, + executableWillLaunchAtLogin: true, + launchItems: [{ + name: 'electron.app.Electron', + path: process.execPath, + args: [], + scope: 'user', + enabled: true + }] + }); }); it('correctly sets and unsets the LoginItem', function () { @@ -668,16 +704,77 @@ describe('app module', () => { expect(app.getLoginItemSettings().openAsHidden).to.equal(false); }); - it('allows you to pass a custom executable and arguments', function () { - if (process.platform !== 'win32') this.skip(); - - app.setLoginItemSettings({ openAtLogin: true, path: updateExe, args: processStartArgs }); - + ifit(process.platform === 'win32')('allows you to pass a custom executable and arguments', function () { + app.setLoginItemSettings({ openAtLogin: true, path: updateExe, args: processStartArgs, enabled: true }); expect(app.getLoginItemSettings().openAtLogin).to.equal(false); - expect(app.getLoginItemSettings({ + const openAtLoginTrueEnabledTrue = app.getLoginItemSettings({ path: updateExe, args: processStartArgs - }).openAtLogin).to.equal(true); + }); + + expect(openAtLoginTrueEnabledTrue.openAtLogin).to.equal(true); + expect(openAtLoginTrueEnabledTrue.executableWillLaunchAtLogin).to.equal(true); + + app.setLoginItemSettings({ openAtLogin: true, path: updateExe, args: processStartArgs, enabled: false }); + const openAtLoginTrueEnabledFalse = app.getLoginItemSettings({ + path: updateExe, + args: processStartArgs + }); + + expect(openAtLoginTrueEnabledFalse.openAtLogin).to.equal(true); + expect(openAtLoginTrueEnabledFalse.executableWillLaunchAtLogin).to.equal(false); + + app.setLoginItemSettings({ openAtLogin: false, path: updateExe, args: processStartArgs, enabled: false }); + const openAtLoginFalseEnabledFalse = app.getLoginItemSettings({ + path: updateExe, + args: processStartArgs + }); + + expect(openAtLoginFalseEnabledFalse.openAtLogin).to.equal(false); + expect(openAtLoginFalseEnabledFalse.executableWillLaunchAtLogin).to.equal(false); + }); + + ifit(process.platform === 'win32')('allows you to pass a custom name', function () { + app.setLoginItemSettings({ openAtLogin: true }); + app.setLoginItemSettings({ openAtLogin: true, name: 'additionalEntry', enabled: false }); + expect(app.getLoginItemSettings()).to.deep.equal({ + openAtLogin: true, + openAsHidden: false, + wasOpenedAtLogin: false, + wasOpenedAsHidden: false, + restoreState: false, + executableWillLaunchAtLogin: true, + launchItems: [{ + name: 'additionalEntry', + path: process.execPath, + args: [], + scope: 'user', + enabled: false + }, { + name: 'electron.app.Electron', + path: process.execPath, + args: [], + scope: 'user', + enabled: true + }] + }); + + app.setLoginItemSettings({ openAtLogin: false, name: 'additionalEntry' }); + expect(app.getLoginItemSettings()).to.deep.equal({ + openAtLogin: true, + openAsHidden: false, + wasOpenedAtLogin: false, + wasOpenedAsHidden: false, + restoreState: false, + executableWillLaunchAtLogin: true, + launchItems: [{ + name: 'electron.app.Electron', + path: process.execPath, + args: [], + scope: 'user', + enabled: true + }] + }); }); });