feat: msix auto-updater (#49585)

* feat: native auto updater for MSIX on Windows

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* doc: added MSIX debug documentation

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: allow downgrade with json release file and emit update-available

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* test: msix auot-update tests

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* doc: API documentation

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* test: add package version validation

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: docs typo

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: don't allow auto-updating when using appinstaller manifest

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: getPackageInfo interface implementation

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: review feedback, add comment

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: missed filename commit

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: install test cert on demand

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: time stamp mismatch in tests

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: feedback - rename to MSIXPackageInfo

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: update and reference windowsStore property

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: remove getPackagInfo from public API

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: type error bcause of removed API

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: fix Windows MSIX release build errors (#49613)

* fix: fix MSIX release build

* fix: add C++/WinRT headers

* build: modify include paths

* fix: compile msix as seperate source set

* build: add additional needed deps for msix

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>
Co-authored-by: Keeley Hammond <vertedinde@electronjs.org>
This commit is contained in:
trop[bot]
2026-02-02 09:33:46 +01:00
committed by GitHub
parent fd4f835f37
commit e6b53033dd
20 changed files with 1738 additions and 8 deletions

View File

@@ -420,6 +420,37 @@ action("electron_generate_node_defines") {
args = [ rebase_path(target_gen_dir) ] + rebase_path(inputs)
}
# MSIX updater needs to be in a separate source_set because it uses C++/WinRT
# headers that require exceptions to be enabled.
source_set("electron_msix_updater") {
sources = [
"shell/browser/api/electron_api_msix_updater.cc",
"shell/browser/api/electron_api_msix_updater.h",
]
configs += [ "//third_party/electron_node:node_external_config" ]
public_configs = [ ":electron_lib_config" ]
if (is_win) {
cflags_cc = [
"/EHsc", # Enable C++ exceptions for C++/WinRT
"-Wno-c++98-compat-extra-semi", #Suppress C++98 compatibility warnings
]
include_dirs = [ "//third_party/nearby/src/internal/platform/implementation/windows/generated" ]
}
deps = [
"//base",
"//content/public/browser",
"//gin",
"//third_party/electron_node/deps/simdjson",
"//third_party/electron_node/deps/uv",
"//v8",
]
}
source_set("electron_lib") {
configs += [
"//v8:external_startup_data",
@@ -435,6 +466,7 @@ source_set("electron_lib") {
":electron_fuses",
":electron_generate_node_defines",
":electron_js2c",
":electron_msix_updater",
":electron_version_header",
":resources",
"buildflags",

View File

@@ -32,9 +32,19 @@ update process. Apps that need to disable ATS can add the
### Windows
On Windows, you have to install your app into a user's machine before you can
use the `autoUpdater`, so it is recommended that you use
[electron-winstaller][installer-lib] or [Electron Forge's Squirrel.Windows maker][electron-forge-lib] to generate a Windows installer.
On Windows, the `autoUpdater` module automatically selects the appropriate update mechanism
based on how your app is packaged:
* **MSIX packages**: If your app is running as an MSIX package (created with [electron-windows-msix][msix-lib] and detected via [`process.windowsStore`](process.md#processwindowsstore-readonly)),
the module uses the MSIX updater, which supports direct MSIX file links and JSON update feeds.
* **Squirrel.Windows**: For apps installed via traditional installers (created with
[electron-winstaller][installer-lib] or [Electron Forge's Squirrel.Windows maker][electron-forge-lib]),
the module uses Squirrel.Windows for updates.
You don't need to configure which updater to use; Electron automatically detects the packaging
format and uses the appropriate one.
#### Squirrel.Windows
Apps built with Squirrel.Windows will trigger [custom launch events](https://github.com/Squirrel/Squirrel.Windows/blob/51f5e2cb01add79280a53d51e8d0cfa20f8c9f9f/docs/using/custom-squirrel-events-non-cs.md#application-startup-commands)
that must be handled by your Electron application to ensure proper setup and teardown.
@@ -55,6 +65,14 @@ The installer generated with Squirrel.Windows will create a shortcut icon with a
same ID for your app with `app.setAppUserModelId` API, otherwise Windows will
not be able to pin your app properly in task bar.
#### MSIX Packages
When your app is packaged as an MSIX, the `autoUpdater` module provides additional
functionality:
* Use the `allowAnyVersion` option in `setFeedURL()` to allow updates to older versions (downgrades)
* Support for direct MSIX file links or JSON update feeds (similar to Squirrel.Mac format)
## Events
The `autoUpdater` object emits the following events:
@@ -92,7 +110,7 @@ Returns:
Emitted when an update has been downloaded.
On Windows only `releaseName` is available.
With Squirrel.Windows only `releaseName` is available.
> [!NOTE]
> It is not strictly necessary to handle this event. A successfully
@@ -111,10 +129,12 @@ The `autoUpdater` object has the following methods:
### `autoUpdater.setFeedURL(options)`
* `options` Object
* `url` string
* `url` string - The update server URL. For _Windows_ MSIX, this can be either a direct link to an MSIX file (e.g., `https://example.com/update.msix`) or a JSON endpoint that returns update information (see the [Squirrel.Mac][squirrel-mac] README for more information).
* `headers` Record\<string, string\> (optional) _macOS_ - HTTP request headers.
* `serverType` string (optional) _macOS_ - Can be `json` or `default`, see the [Squirrel.Mac][squirrel-mac]
README for more information.
* `allowAnyVersion` boolean (optional) _Windows_ - If `true`, allows downgrades to older versions for MSIX packages.
Defaults to `false`.
Sets the `url` and initialize the auto updater.
@@ -151,3 +171,4 @@ closed.
[electron-forge-lib]: https://www.electronforge.io/config/makers/squirrel.windows
[app-user-model-id]: https://learn.microsoft.com/en-us/windows/win32/shell/appids
[event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter
[msix-lib]: https://github.com/electron-userland/electron-windows-msix

View File

@@ -159,6 +159,22 @@ Notification activated (com.github.Electron:notification:EAF7B87C-A113-43D7-8E76
Notification replied to (com.github.Electron:notification:EAF7B87C-A113-43D7-8E76-F88EC9D73D44)
```
### `ELECTRON_DEBUG_MSIX_UPDATER`
Adds extra logs to MSIX updater operations on Windows to aid in debugging. Extra logging will be displayed when MSIX update operations are initiated, including package updates, package registration, and restart registration. This helps diagnose issues with MSIX package updates and deployments.
Sample output:
```sh
UpdateMsix called with URI: https://example.com/app.msix
DoUpdateMsix: Starting
Calling AddPackageByUriAsync... URI: https://example.com/app.msix
Update options - deferRegistration: true, developerMode: false, forceShutdown: false, forceTargetShutdown: false, forceUpdateFromAnyVersion: false
Waiting for deployment...
Deployment finished.
MSIX Deployment completed.
```
### `ELECTRON_LOG_ASAR_READS`
When Electron reads from an ASAR file, log the read offset and file path to

View File

@@ -128,8 +128,8 @@ A `string` representing Electron's version string.
### `process.windowsStore` _Readonly_
A `boolean`. If the app is running as a Windows Store app (appx), this property is `true`,
for otherwise it is `undefined`.
A `boolean`. If the app is running as an MSIX package (including AppX for Windows Store),
this property is `true`, otherwise it is `undefined`.
### `process.contextId` _Readonly_

View File

@@ -222,8 +222,10 @@ auto_filenames = {
browser_bundle_deps = [
"lib/browser/api/app.ts",
"lib/browser/api/auto-updater.ts",
"lib/browser/api/auto-updater/auto-updater-msix.ts",
"lib/browser/api/auto-updater/auto-updater-native.ts",
"lib/browser/api/auto-updater/auto-updater-win.ts",
"lib/browser/api/auto-updater/msix-update-win.ts",
"lib/browser/api/auto-updater/squirrel-update-win.ts",
"lib/browser/api/base-window.ts",
"lib/browser/api/browser-view.ts",

View File

@@ -1,5 +1,10 @@
if (process.platform === 'win32') {
module.exports = require('./auto-updater/auto-updater-win');
// windowsStore indicates whether the app is running as a packaged app (MSIX), even outside of the store
if (process.windowsStore) {
module.exports = require('./auto-updater/auto-updater-msix');
} else {
module.exports = require('./auto-updater/auto-updater-win');
}
} else {
module.exports = require('./auto-updater/auto-updater-native');
}

View File

@@ -0,0 +1,449 @@
import * as msixUpdate from '@electron/internal/browser/api/auto-updater/msix-update-win';
import { app, net } from 'electron/main';
import { EventEmitter } from 'events';
interface UpdateInfo {
ok: boolean; // False if error encountered
available?: boolean; // True if the update is available, false if not
updateUrl?: string; // The URL of the update
releaseNotes?: string; // The release notes of the update
releaseName?: string; // The release name of the update
releaseDate?: Date; // The release date of the update
}
interface MSIXPackageInfo {
id: string;
familyName: string;
developmentMode: boolean;
version: string;
signatureKind: 'developer' | 'enterprise' | 'none' | 'store' | 'system';
appInstallerUri?: string;
}
/**
* Options for updating an MSIX package.
* Used with `updateMsix()` to control how the package update behaves.
*
* These options correspond to the Windows.Management.Deployment.AddPackageOptions class properties.
*
* @see https://learn.microsoft.com/en-us/uwp/api/windows.management.deployment.addpackageoptions?view=winrt-26100
*/
export interface UpdateMsixOptions {
/**
* Gets or sets a value that indicates whether to delay registration of the main package
* or dependency packages if the packages are currently in use.
*
* Corresponds to `AddPackageOptions.DeferRegistrationWhenPackagesAreInUse`
*
* @default false
*/
deferRegistration?: boolean;
/**
* Gets or sets a value that indicates whether the app is installed in developer mode.
* When set, the app is installed in development mode which allows for a more rapid
* development cycle. The BlockMap.xml, [Content_Types].xml, and digital signature
* files are not required for app installation.
*
* Corresponds to `AddPackageOptions.DeveloperMode`
*
* @default false
*/
developerMode?: boolean;
/**
* Gets or sets a value that indicates whether the processes associated with the package
* will be shut down forcibly so that registration can continue if the package, or any
* package that depends on the package, is currently in use.
*
* Corresponds to `AddPackageOptions.ForceAppShutdown`
*
* @default false
*/
forceShutdown?: boolean;
/**
* Gets or sets a value that indicates whether the processes associated with the package
* will be shut down forcibly so that registration can continue if the package is
* currently in use.
*
* Corresponds to `AddPackageOptions.ForceTargetAppShutdown`
*
* @default false
*/
forceTargetShutdown?: boolean;
/**
* Gets or sets a value that indicates whether to force a specific version of a package
* to be added, regardless of if a higher version is already added.
*
* Corresponds to `AddPackageOptions.ForceUpdateFromAnyVersion`
*
* @default false
*/
forceUpdateFromAnyVersion?: boolean;
}
/**
* Options for registering an MSIX package.
* Used with `registerPackage()` to control how the package registration behaves.
*
* These options correspond to the Windows.Management.Deployment.DeploymentOptions enum.
*
* @see https://learn.microsoft.com/en-us/uwp/api/windows.management.deployment.deploymentoptions?view=winrt-26100
*/
interface RegisterPackageOptions {
/**
* Force shutdown of the application if it's currently running.
* If this package, or any package that depends on this package, is currently in use,
* the processes associated with the package are shut down forcibly so that registration can continue.
*
* Corresponds to `DeploymentOptions.ForceApplicationShutdown` (value: 1)
*
* @default false
*/
forceShutdown?: boolean;
/**
* Force shutdown of the target application if it's currently running.
* If this package is currently in use, the processes associated with the package
* are shut down forcibly so that registration can continue.
*
* Corresponds to `DeploymentOptions.ForceTargetApplicationShutdown` (value: 64)
*
* @default false
*/
forceTargetShutdown?: boolean;
/**
* Force a specific version of a package to be staged/registered, regardless of if
* a higher version is already staged/registered.
*
* Corresponds to `DeploymentOptions.ForceUpdateFromAnyVersion` (value: 262144)
*
* @default false
*/
forceUpdateFromAnyVersion?: boolean;
}
class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
updateAvailable: boolean = false;
updateURL: string | null = null;
updateHeaders: Record<string, string> | null = null;
allowAnyVersion: boolean = false;
// Private: Validate that the URL points to an MSIX file (following redirects)
private async validateMsixUrl (url: string): Promise<void> {
try {
// Make a HEAD request to follow redirects and get the final URL
const response = await net.fetch(url, {
method: 'HEAD',
headers: this.updateHeaders ? new Headers(this.updateHeaders) : undefined,
redirect: 'follow' // Follow redirects to get the final URL
});
// Get the final URL after redirects (response.url contains the final URL)
const finalUrl = response.url || url;
const urlObj = new URL(finalUrl);
const pathname = urlObj.pathname.toLowerCase();
// Check if final URL ends with .msix or .msixbundle extension
const hasMsixExtension = pathname.endsWith('.msix') || pathname.endsWith('.msixbundle');
if (!hasMsixExtension) {
throw new Error(`Update URL does not point to an MSIX file. Expected .msix or .msixbundle extension, got final URL: ${finalUrl}`);
}
} catch (error) {
if (error instanceof TypeError) {
throw new Error(`Invalid MSIX URL: ${url}`);
}
throw error;
}
}
// Private: Check if URL is a direct MSIX file (following redirects)
private async isDirectMsixUrl (url: string, emitError: boolean = false): Promise<boolean> {
try {
await this.validateMsixUrl(url);
return true;
} catch (error) {
if (emitError) {
this.emitError(error as Error);
}
return false;
}
}
// Supports both versioning (x.y.z) and Windows version format (x.y.z.a)
// Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if v1 === v2
private compareVersions (v1: string, v2: string): number {
const parts1 = v1.split('.').map(part => {
const parsed = parseInt(part, 10);
return isNaN(parsed) ? 0 : parsed;
});
const parts2 = v2.split('.').map(part => {
const parsed = parseInt(part, 10);
return isNaN(parsed) ? 0 : parsed;
});
const maxLength = Math.max(parts1.length, parts2.length);
for (let i = 0; i < maxLength; i++) {
const part1 = parts1[i] ?? 0;
const part2 = parts2[i] ?? 0;
if (part1 > part2) return 1;
if (part1 < part2) return -1;
}
return 0;
}
// Private: Parse the static releases array format
// This is a static JSON file containing all releases
private parseStaticReleasFile (json: any, currentVersion: string): { ok: boolean; available: boolean; url?: string; name?: string; notes?: string; pub_date?: string } {
if (!Array.isArray(json.releases) || !json.currentRelease || typeof json.currentRelease !== 'string') {
this.emitError(new Error('Invalid releases format. Expected \'releases\' array and \'currentRelease\' string.'));
return { ok: false, available: false };
}
// Use currentRelease property to determine if update is available
const currentReleaseVersion = json.currentRelease;
// Compare current version with currentRelease
const versionComparison = this.compareVersions(currentReleaseVersion, currentVersion);
// If versions match, we're up to date
if (versionComparison === 0) {
return { ok: true, available: false };
}
// If currentRelease is older than current version, check allowAnyVersion
if (versionComparison < 0) {
// If allowAnyVersion is true, allow downgrades
if (this.allowAnyVersion) {
// Continue to find the release entry for downgrade
} else {
return { ok: true, available: false };
}
}
// currentRelease is newer, find the release entry
const releaseEntry = json.releases.find((r: any) => r.version === currentReleaseVersion);
if (!releaseEntry || !releaseEntry.updateTo) {
this.emitError(new Error(`Release entry for version '${currentReleaseVersion}' not found or missing 'updateTo' property.`));
return { ok: false, available: false };
}
const updateTo = releaseEntry.updateTo;
if (!updateTo.url) {
this.emitError(new Error(`Invalid release entry. 'updateTo.url' is missing for version ${currentReleaseVersion}.`));
return { ok: false, available: false };
}
return {
ok: true,
available: true,
url: updateTo.url,
name: updateTo.name,
notes: updateTo.notes,
pub_date: updateTo.pub_date
};
}
private parseDynamicReleasFile (json: any): { ok: boolean; available: boolean; url?: string; name?: string; notes?: string; pub_date?: string } {
if (!json.url) {
this.emitError(new Error('Invalid releases format. Expected \'url\' string property.'));
return { ok: false, available: false };
}
return { ok: true, available: true, url: json.url, name: json.name, notes: json.notes, pub_date: json.pub_date };
}
private async fetchSquirrelJson (url: string) {
const headers: Record<string, string> = {
...this.updateHeaders,
Accept: 'application/json' // Always set Accept header, overriding any user-provided Accept
};
const response = await net.fetch(url, {
headers
});
if (response.status === 204) {
return { ok: true, available: false };
} else if (response.status === 200) {
const updateJson = await response.json();
// Check if this is the static releases array format
if (Array.isArray(updateJson.releases)) {
// Get current package version
const packageInfo = msixUpdate.getPackageInfo();
const currentVersion = packageInfo.version;
if (!currentVersion) {
this.emitError(new Error('Cannot determine current package version.'));
return { ok: false, available: false };
}
return this.parseStaticReleasFile(updateJson, currentVersion);
} else {
// Dynamic format: server returns JSON with update info for current version
return this.parseDynamicReleasFile(updateJson);
}
} else {
this.emitError(new Error(`Unexpected response status: ${response.status}`));
return { ok: false, available: false };
}
}
private async getUpdateInfo (url: string): Promise<UpdateInfo> {
if (url && await this.isDirectMsixUrl(url)) {
return { ok: true, available: true, updateUrl: url, releaseDate: new Date() };
} else {
const updateJson = await this.fetchSquirrelJson(url);
if (!updateJson.ok) {
return { ok: false };
} else if (updateJson.ok && !updateJson.available) {
return { ok: true, available: false };
} else {
// updateJson.ok && updateJson.available must be true here
// Parse the publication date if present (ISO 8601 format)
let releaseDate: Date | null = null;
if (updateJson.pub_date) {
releaseDate = new Date(updateJson.pub_date);
}
const updateUrl = updateJson.url ?? '';
const releaseNotes = updateJson.notes ?? '';
const releaseName = updateJson.name ?? '';
releaseDate = releaseDate ?? new Date();
if (!await this.isDirectMsixUrl(updateUrl, true)) {
return { ok: false };
} else {
return {
ok: true,
available: true,
updateUrl,
releaseNotes,
releaseName,
releaseDate
};
}
}
}
}
getFeedURL () {
return this.updateURL ?? '';
}
setFeedURL (options: { url: string; headers?: Record<string, string>; allowAnyVersion?: boolean } | string) {
let updateURL: string;
let headers: Record<string, string> | undefined;
let allowAnyVersion: boolean | undefined;
if (typeof options === 'object') {
if (typeof options.url === 'string') {
updateURL = options.url;
headers = options.headers;
allowAnyVersion = options.allowAnyVersion;
} else {
throw new TypeError('Expected options object to contain a \'url\' string property in setFeedUrl call');
}
} else if (typeof options === 'string') {
updateURL = options;
} else {
throw new TypeError('Expected an options object with a \'url\' property to be provided');
}
this.updateURL = updateURL;
this.updateHeaders = headers ?? null;
this.allowAnyVersion = allowAnyVersion ?? false;
}
getPackageInfo (): MSIXPackageInfo {
return msixUpdate.getPackageInfo() as MSIXPackageInfo;
}
async checkForUpdates () {
const url = this.updateURL;
if (!url) {
return this.emitError(new Error('Update URL is not set'));
}
// Check if running in MSIX package
const packageInfo = msixUpdate.getPackageInfo();
if (!packageInfo.familyName) {
return this.emitError(new Error('MSIX updates are not supported'));
}
// If appInstallerUri is set, Windows App Installer manages updates automatically
// Prevent updates here to avoid conflicts
if (packageInfo.appInstallerUri) {
return this.emitError(new Error('Auto-updates are managed by Windows App Installer. Updates are not allowed when installed via Application Manifest.'));
}
this.emit('checking-for-update');
try {
const msixUrlInfo = await this.getUpdateInfo(url);
if (!msixUrlInfo.ok) {
return this.emitError(new Error('Invalid update or MSIX URL. See previous errors.'));
}
if (!msixUrlInfo.available) {
this.emit('update-not-available');
} else {
this.updateAvailable = true;
this.emit('update-available');
await msixUpdate.updateMsix(msixUrlInfo.updateUrl, {
deferRegistration: true,
developerMode: false,
forceShutdown: false,
forceTargetShutdown: false,
forceUpdateFromAnyVersion: this.allowAnyVersion
} as UpdateMsixOptions);
this.emit('update-downloaded', {}, msixUrlInfo.releaseNotes, msixUrlInfo.releaseName, msixUrlInfo.releaseDate, msixUrlInfo.updateUrl, () => {
this.quitAndInstall();
});
}
} catch (error) {
this.emitError(error as Error);
}
}
async quitAndInstall () {
if (!this.updateAvailable) {
this.emitError(new Error('No update available, can\'t quit and install'));
app.quit();
return;
}
try {
// Get package info to get family name
const packageInfo = msixUpdate.getPackageInfo();
if (!packageInfo.familyName) {
return this.emitError(new Error('MSIX updates are not supported'));
}
msixUpdate.registerRestartOnUpdate('');
this.emit('before-quit-for-update');
// force shutdown of the application and register the package to be installed on restart
await msixUpdate.registerPackage(packageInfo.familyName, {
forceShutdown: true
} as RegisterPackageOptions);
} catch (error) {
this.emitError(error as Error);
}
}
// Private: Emit both error object and message, this is to keep compatibility
// with Old APIs.
emitError (error: Error) {
this.emit('error', error, error.message);
}
}
export default new AutoUpdater();

View File

@@ -20,6 +20,11 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
return this.updateURL ?? '';
}
getPackageInfo () {
// Squirrel-based Windows apps don't have MSIX package information
return undefined;
}
setFeedURL (options: { url: string } | string) {
let updateURL: string;
if (typeof options === 'object') {

View File

@@ -0,0 +1,4 @@
const { updateMsix, registerPackage, registerRestartOnUpdate, getPackageInfo } =
process._linkedBinding('electron_browser_msix_updater');
export { updateMsix, registerPackage, registerRestartOnUpdate, getPackageInfo };

View 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)

View 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_

View File

@@ -66,6 +66,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) \

View File

@@ -0,0 +1,336 @@
import { expect } from 'chai';
import * as express from 'express';
import * as http from 'node:http';
import { AddressInfo } from 'node:net';
import {
getElectronExecutable,
getMainJsFixturePath,
getMsixFixturePath,
getMsixPackageVersion,
installMsixCertificate,
installMsixPackage,
registerExecutableWithIdentity,
shouldRunMsixTests,
spawn,
uninstallMsixPackage,
unregisterExecutableWithIdentity
} from './lib/msix-helpers';
import { ifdescribe } from './lib/spec-helpers';
const ELECTRON_MSIX_ALIAS = 'ElectronMSIX.exe';
const MAIN_JS_PATH = getMainJsFixturePath();
const MSIX_V1 = getMsixFixturePath('v1');
const MSIX_V2 = getMsixFixturePath('v2');
// We can only test the MSIX updater on Windows
ifdescribe(shouldRunMsixTests)('autoUpdater MSIX behavior', function () {
this.timeout(120000);
before(async function () {
await installMsixCertificate();
const electronExec = getElectronExecutable();
await registerExecutableWithIdentity(electronExec);
});
after(async function () {
await unregisterExecutableWithIdentity();
});
const launchApp = (executablePath: string, args: string[] = []) => {
return spawn(executablePath, args);
};
const logOnError = (what: any, fn: () => void) => {
try {
fn();
} catch (err) {
console.error(what);
throw err;
}
};
it('should launch Electron via MSIX alias', async () => {
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, ['--version']);
logOnError(launchResult, () => {
expect(launchResult.code).to.equal(0);
});
});
it('should print package identity information', async () => {
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, [MAIN_JS_PATH, '--printPackageId']);
logOnError(launchResult, () => {
expect(launchResult.code).to.equal(0);
expect(launchResult.out).to.include('Family Name: Electron.Dev.MSIX_rdjwn13tdj8dy');
expect(launchResult.out).to.include('Package ID: Electron.Dev.MSIX_1.0.0.0_x64__rdjwn13tdj8dy');
expect(launchResult.out).to.include('Version: 1.0.0.0');
});
});
describe('with update server', () => {
let port = 0;
let server: express.Application = null as any;
let httpServer: http.Server = null as any;
let requests: express.Request[] = [];
beforeEach((done) => {
requests = [];
server = express();
server.use((req, res, next) => {
requests.push(req);
next();
});
httpServer = server.listen(0, '127.0.0.1', () => {
port = (httpServer.address() as AddressInfo).port;
done();
});
});
afterEach(async () => {
if (httpServer) {
await new Promise<void>(resolve => {
httpServer.close(() => {
httpServer = null as any;
server = null as any;
resolve();
});
});
}
await uninstallMsixPackage('com.electron.myapp');
});
it('should not update when no update is available', async () => {
server.get('/update-check', (req, res) => {
res.status(204).send();
});
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, [MAIN_JS_PATH, '--checkUpdate', `http://localhost:${port}/update-check`]);
logOnError(launchResult, () => {
expect(launchResult.code).to.equal(0);
expect(requests.length).to.be.greaterThan(0);
expect(launchResult.out).to.include('Checking for update...');
expect(launchResult.out).to.include('Update not available');
});
});
it('should hit the update endpoint with custom headers when checkForUpdates is called', async () => {
server.get('/update-check', (req, res) => {
expect(req.headers['x-appversion']).to.equal('1.0.0');
expect(req.headers.authorization).to.equal('Bearer test-token');
res.status(204).send();
});
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, [
MAIN_JS_PATH,
'--checkUpdate',
`http://localhost:${port}/update-check`,
'--useCustomHeaders'
]);
logOnError(launchResult, () => {
expect(launchResult.code).to.equal(0);
expect(requests.length).to.be.greaterThan(0);
expect(launchResult.out).to.include('Checking for update...');
expect(launchResult.out).to.include('Update not available');
});
});
it('should update successfully with direct link to MSIX file', async () => {
await installMsixPackage(MSIX_V1);
const initialVersion = await getMsixPackageVersion('com.electron.myapp');
expect(initialVersion).to.equal('1.0.0.0');
server.get('/update.msix', (req, res) => {
res.download(MSIX_V2);
});
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, [
MAIN_JS_PATH,
'--checkUpdate',
`http://localhost:${port}/update.msix`
]);
logOnError(launchResult, () => {
expect(launchResult.code).to.equal(0);
expect(requests.length).to.be.greaterThan(0);
expect(launchResult.out).to.include('Checking for update...');
expect(launchResult.out).to.include('Update available');
expect(launchResult.out).to.include('Update downloaded');
expect(launchResult.out).to.include('Release Name: N/A');
expect(launchResult.out).to.include('Release Notes: N/A');
expect(launchResult.out).to.include(`Update URL: http://localhost:${port}/update.msix`);
});
const updatedVersion = await getMsixPackageVersion('com.electron.myapp');
expect(updatedVersion).to.equal('2.0.0.0');
});
it('should update successfully with JSON response', async () => {
await installMsixPackage(MSIX_V1);
const initialVersion = await getMsixPackageVersion('com.electron.myapp');
expect(initialVersion).to.equal('1.0.0.0');
const fixedPubDate = '2011-11-11T11:11:11.000Z';
const expectedDateStr = new Date(fixedPubDate).toDateString();
server.get('/update-check', (req, res) => {
res.json({
url: `http://localhost:${port}/update.msix`,
name: '2.0.0',
notes: 'Test release notes',
pub_date: fixedPubDate
});
});
server.get('/update.msix', (req, res) => {
res.download(MSIX_V2);
});
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, [MAIN_JS_PATH, '--checkUpdate', `http://localhost:${port}/update-check`]);
logOnError(launchResult, () => {
expect(launchResult.code).to.equal(0);
expect(requests.length).to.be.greaterThan(0);
expect(launchResult.out).to.include('Checking for update...');
expect(launchResult.out).to.include('Update available');
expect(launchResult.out).to.include('Update downloaded');
expect(launchResult.out).to.include('Release Name: 2.0.0');
expect(launchResult.out).to.include('Release Notes: Test release notes');
expect(launchResult.out).to.include(`Release Date: ${expectedDateStr}`);
expect(launchResult.out).to.include(`Update URL: http://localhost:${port}/update.msix`);
});
const updatedVersion = await getMsixPackageVersion('com.electron.myapp');
expect(updatedVersion).to.equal('2.0.0.0');
});
it('should update successfully with static JSON releases file', async () => {
await installMsixPackage(MSIX_V1);
const initialVersion = await getMsixPackageVersion('com.electron.myapp');
expect(initialVersion).to.equal('1.0.0.0');
const fixedPubDate = '2011-11-11T11:11:11.000Z';
const expectedDateStr = new Date(fixedPubDate).toDateString();
server.get('/update-check', (req, res) => {
res.json({
currentRelease: '2.0.0',
releases: [
{
version: '1.0.0',
updateTo: {
version: '1.0.0',
url: `http://localhost:${port}/update-v1.msix`,
name: '1.0.0',
notes: 'Initial release',
pub_date: '2010-10-10T10:10:10.000Z'
}
},
{
version: '2.0.0',
updateTo: {
version: '2.0.0',
url: `http://localhost:${port}/update-v2.msix`,
name: '2.0.0',
notes: 'Test release notes for static format',
pub_date: fixedPubDate
}
}
]
});
});
server.get('/update-v2.msix', (req, res) => {
res.download(MSIX_V2);
});
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, [MAIN_JS_PATH, '--checkUpdate', `http://localhost:${port}/update-check`]);
logOnError(launchResult, () => {
expect(launchResult.code).to.equal(0);
expect(requests.length).to.be.greaterThan(0);
expect(launchResult.out).to.include('Checking for update...');
expect(launchResult.out).to.include('Update available');
expect(launchResult.out).to.include('Update downloaded');
expect(launchResult.out).to.include('Release Name: 2.0.0');
expect(launchResult.out).to.include('Release Notes: Test release notes for static format');
expect(launchResult.out).to.include(`Release Date: ${expectedDateStr}`);
expect(launchResult.out).to.include(`Update URL: http://localhost:${port}/update-v2.msix`);
});
const updatedVersion = await getMsixPackageVersion('com.electron.myapp');
expect(updatedVersion).to.equal('2.0.0.0');
});
it('should not update with update File JSON Format if currentRelease is older than installed version', async () => {
await installMsixPackage(MSIX_V2);
server.get('/update-check', (req, res) => {
res.json({
currentRelease: '1.0.0',
releases: [
{
version: '1.0.0',
updateTo: {
version: '1.0.0',
url: `http://localhost:${port}/update-v1.msix`,
name: '1.0.0',
notes: 'Initial release',
pub_date: '2010-10-10T10:10:10.000Z'
}
}
]
});
});
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, [MAIN_JS_PATH, '--checkUpdate', `http://localhost:${port}/update-check`]);
logOnError(launchResult, () => {
expect(launchResult.code).to.equal(0);
expect(requests.length).to.be.greaterThan(0);
expect(launchResult.out).to.include('Checking for update...');
expect(launchResult.out).to.include('Update not available');
});
});
it('should downgrade to older version with JSON server format and allowAnyVersion is true', async () => {
await installMsixPackage(MSIX_V2);
const initialVersion = await getMsixPackageVersion('com.electron.myapp');
expect(initialVersion).to.equal('2.0.0.0');
const fixedPubDate = '2010-10-10T10:10:10.000Z';
const expectedDateStr = new Date(fixedPubDate).toDateString();
server.get('/update-check', (req, res) => {
res.json({
url: `http://localhost:${port}/update-v1.msix`,
name: '1.0.0',
notes: 'Initial release',
pub_date: fixedPubDate
});
});
server.get('/update-v1.msix', (req, res) => {
res.download(MSIX_V1);
});
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, [MAIN_JS_PATH, '--checkUpdate', `http://localhost:${port}/update-check`, '--allowAnyVersion']);
logOnError(launchResult, () => {
expect(launchResult.code).to.equal(0);
expect(requests.length).to.be.greaterThan(0);
expect(launchResult.out).to.include('Checking for update...');
expect(launchResult.out).to.include('Update available');
expect(launchResult.out).to.include('Update downloaded');
expect(launchResult.out).to.include('Release Name: 1.0.0');
expect(launchResult.out).to.include('Release Notes: Initial release');
expect(launchResult.out).to.include(`Release Date: ${expectedDateStr}`);
expect(launchResult.out).to.include(`Update URL: http://localhost:${port}/update-v1.msix`);
});
const downgradedVersion = await getMsixPackageVersion('com.electron.myapp');
expect(downgradedVersion).to.equal('1.0.0.0');
});
});
});

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
xmlns:desktop2="http://schemas.microsoft.com/appx/manifest/desktop/windows10/2"
IgnorableNamespaces="uap uap3 desktop2">
<Identity Name="Electron.Dev.MSIX"
ProcessorArchitecture="x64"
Version="1.0.0.0"
Publisher="CN=Electron"/>
<Properties>
<DisplayName>Electron Dev MSIX</DisplayName>
<PublisherDisplayName>Electron</PublisherDisplayName>
<Logo>assets\icon.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.17763.0" />
</Dependencies>
<Resources>
<Resource Language="en-US" />
</Resources>
<Applications>
<Application Id="ElectronMSIX" Executable="Electron.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="Electron Dev MSIX"
Description="Electron running with Identity"
Square44x44Logo="assets\Square44x44Logo.png"
Square150x150Logo="assets\Square150x150Logo.png"
BackgroundColor="transparent">
</uap:VisualElements>
<Extensions>
<uap3:Extension
Category="windows.appExecutionAlias"
Executable="Electron.exe"
EntryPoint="Windows.FullTrustApplication">
<uap3:AppExecutionAlias>
<desktop:ExecutionAlias Alias="ElectronMSIX.exe" />
</uap3:AppExecutionAlias>
</uap3:Extension>
</Extensions>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
<Capability Name="internetClient" />
</Capabilities>
</Package>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,22 @@
Add-Type -AssemblyName System.Security.Cryptography.X509Certificates
# Path to cert file one folder up relative to script location
$scriptDir = Split-Path -Parent $PSCommandPath
$certPath = Join-Path $scriptDir "MSIXDevCert.cer" | Resolve-Path
# Load the certificate from file
$cert = [System.Security.Cryptography.X509Certificates.X509CertificateLoader]::LoadCertificateFromFile($certPath)
$trustedStore = Get-ChildItem -Path "cert:\LocalMachine\TrustedPeople" | Where-Object { $_.Thumbprint -eq $cert.Thumbprint }
if (-not $trustedStore) {
# We gonna need admin privileges to install the cert
if (-Not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Start-Process powershell -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs
exit
}
# Install the public cert to LocalMachine\TrustedPeople (for MSIX trust)
Import-Certificate -FilePath $certPath -CertStoreLocation "cert:\LocalMachine\TrustedPeople" | Out-Null
Write-Host " 🏛️ Installed to: cert:\LocalMachine\TrustedPeople"
} else {
Write-Host " ✅ Certificate already trusted in: cert:\LocalMachine\TrustedPeople"
}

View File

@@ -0,0 +1,96 @@
const { app, autoUpdater } = require('electron');
// Parse command-line arguments
const args = process.argv.slice(2);
const command = args[0];
const commandArg = args[1];
app.whenReady().then(() => {
try {
// Debug: log received arguments
if (process.env.DEBUG) {
console.log('Command:', command);
console.log('Command arg:', commandArg);
console.log('All args:', JSON.stringify(args));
}
if (command === '--printPackageId') {
const packageInfo = autoUpdater.getPackageInfo();
if (packageInfo.familyName) {
console.log(`Family Name: ${packageInfo.familyName}`);
console.log(`Package ID: ${packageInfo.id || 'N/A'}`);
console.log(`Version: ${packageInfo.version || 'N/A'}`);
console.log(`Development Mode: ${packageInfo.developmentMode ? 'Yes' : 'No'}`);
console.log(`Signature Kind: ${packageInfo.signatureKind || 'N/A'}`);
if (packageInfo.appInstallerUri) {
console.log(`App Installer URI: ${packageInfo.appInstallerUri}`);
}
app.quit();
} else {
console.error('No package identity found. Process is not running in an MSIX package context.');
app.exit(1);
}
} else if (command === '--checkUpdate') {
if (!commandArg) {
console.error('Update URL is required for --checkUpdate');
app.exit(1);
return;
}
// Use hardcoded headers if --useCustomHeaders flag is provided
let headers;
let allowAnyVersion = false;
if (args[2] === '--useCustomHeaders') {
headers = {
'X-AppVersion': '1.0.0',
Authorization: 'Bearer test-token'
};
} else if (args[2] === '--allowAnyVersion') {
allowAnyVersion = true;
}
// Set up event listeners
autoUpdater.on('checking-for-update', () => {
console.log('Checking for update...');
});
autoUpdater.on('update-available', () => {
console.log('Update available');
});
autoUpdater.on('update-not-available', () => {
console.log('Update not available');
app.quit();
});
autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName, releaseDate, updateUrl) => {
console.log('Update downloaded');
console.log(`Release Name: ${releaseName || 'N/A'}`);
console.log(`Release Notes: ${releaseNotes || 'N/A'}`);
console.log(`Release Date: ${releaseDate || 'N/A'}`);
console.log(`Update URL: ${updateUrl || 'N/A'}`);
app.quit();
});
autoUpdater.on('error', (error, message) => {
console.error(`Update error: ${message || error.message || 'Unknown error'}`);
app.exit(1);
});
// Set the feed URL with optional headers and allowAnyVersion, then check for updates
if (headers || allowAnyVersion) {
autoUpdater.setFeedURL({ url: commandArg, headers, allowAnyVersion });
} else {
autoUpdater.setFeedURL(commandArg);
}
autoUpdater.checkForUpdates();
} else {
console.error(`Unknown command: ${command || '(none)'}`);
app.exit(1);
}
} catch (error) {
console.error('Unhandled error:', error.message);
console.error(error.stack);
app.exit(1);
}
});

149
spec/lib/msix-helpers.ts Normal file
View File

@@ -0,0 +1,149 @@
import { expect } from 'chai';
import * as cp from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
const fixturesPath = path.resolve(__dirname, '..', 'fixtures', 'api', 'autoupdater', 'msix');
const manifestFixturePath = path.resolve(fixturesPath, 'ElectronDevAppxManifest.xml');
const installCertScriptPath = path.resolve(fixturesPath, 'install_test_cert.ps1');
// Install the signing certificate for MSIX test packages to the Trusted People store
// This is required to install self-signed MSIX packages
export async function installMsixCertificate (): Promise<void> {
const result = cp.spawnSync('powershell', [
'-NoProfile',
'-ExecutionPolicy', 'Bypass',
'-File', installCertScriptPath
]);
if (result.status !== 0) {
throw new Error(`Failed to install MSIX certificate: ${result.stderr.toString() || result.stdout.toString()}`);
}
}
// Check if we should run MSIX tests
export const shouldRunMsixTests =
process.platform === 'win32';
// Get the Electron executable path
export function getElectronExecutable (): string {
return process.execPath;
}
// Get path to main.js fixture
export function getMainJsFixturePath (): string {
return path.resolve(fixturesPath, 'main.js');
}
// Register executable with identity
export async function registerExecutableWithIdentity (executablePath: string): Promise<void> {
if (!fs.existsSync(manifestFixturePath)) {
throw new Error(`Manifest fixture not found: ${manifestFixturePath}`);
}
const executableDir = path.dirname(executablePath);
const manifestPath = path.join(executableDir, 'AppxManifest.xml');
const escapedManifestPath = manifestPath.replace(/'/g, "''").replace(/\\/g, '\\\\');
const psCommand = `
$ErrorActionPreference = 'Stop';
try {
Add-AppxPackage -Register '${escapedManifestPath}' -ForceUpdateFromAnyVersion
} catch {
Write-Error $_.Exception.Message
exit 1
}
`;
fs.copyFileSync(manifestFixturePath, manifestPath);
const result = cp.spawnSync('powershell', ['-NoProfile', '-Command', psCommand]);
if (result.status !== 0) {
const errorMsg = result.stderr.toString() || result.stdout.toString();
try {
fs.unlinkSync(manifestPath);
} catch {
// Ignore cleanup errors
}
throw new Error(`Failed to register executable with identity: ${errorMsg}`);
}
}
// Unregister the Electron Dev MSIX package
// This removes the sparse package registration created by registerExecutableWithIdentity
export async function unregisterExecutableWithIdentity (): Promise<void> {
const result = cp.spawnSync('powershell', ['-NoProfile', '-Command', ' Get-AppxPackage Electron.Dev.MSIX | Remove-AppxPackage']);
// Don't throw if package doesn't exist
if (result.status !== 0) {
throw new Error(`Failed to unregister executable with identity: ${result.stderr.toString() || result.stdout.toString()}`);
}
const electronExec = getElectronExecutable();
const executableDir = path.dirname(electronExec);
const manifestPath = path.join(executableDir, 'AppxManifest.xml');
try {
if (fs.existsSync(manifestPath)) {
fs.unlinkSync(manifestPath);
}
} catch {
// Ignore cleanup errors
}
}
// Get path to MSIX fixture package
export function getMsixFixturePath (version: 'v1' | 'v2'): string {
const filename = `HelloMSIX_${version}.msix`;
return path.resolve(fixturesPath, filename);
}
// Install MSIX package
export async function installMsixPackage (msixPath: string): Promise<void> {
// Use Add-AppxPackage PowerShell cmdlet
const result = cp.spawnSync('powershell', [
'-Command',
`Add-AppxPackage -Path "${msixPath}" -ForceApplicationShutdown`
]);
if (result.status !== 0) {
throw new Error(`Failed to install MSIX package: ${result.stderr.toString()}`);
}
}
// Uninstall MSIX package by name
export async function uninstallMsixPackage (name: string): Promise<void> {
const result = cp.spawnSync('powershell', ['-NoProfile', '-Command', `Get-AppxPackage ${name} | Remove-AppxPackage`]);
// Don't throw if package doesn't exist
if (result.status !== 0) {
throw new Error(`Failed to uninstall MSIX package: ${result.stderr.toString() || result.stdout.toString()}`);
}
}
// Get version of installed MSIX package by name
export async function getMsixPackageVersion (name: string): Promise<string | null> {
const psCommand = `(Get-AppxPackage -Name '${name}').Version`;
const result = cp.spawnSync('powershell', ['-NoProfile', '-Command', psCommand]);
if (result.status !== 0) {
return null;
}
const version = result.stdout.toString().trim();
return version || null;
}
export function spawn (cmd: string, args: string[], opts: any = {}): Promise<{ code: number, out: string }> {
let out = '';
const child = cp.spawn(cmd, args, opts);
child.stdout.on('data', (chunk: Buffer) => {
out += chunk.toString();
});
child.stderr.on('data', (chunk: Buffer) => {
out += chunk.toString();
});
return new Promise<{ code: number, out: string }>((resolve) => {
child.on('exit', (code, signal) => {
expect(signal).to.equal(null);
resolve({
code: code!,
out
});
});
});
}