mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
Runs `yarn format` across lib/, spec/, script/, build/, default_app/, and npm/ to bring the codebase in line with the .oxfmtrc.json settings added in the previous commit. This is a pure formatting pass: import statements are sorted into the groups defined by the config, method chains longer than printWidth are broken, single-quoted strings containing apostrophes are switched to double quotes, and a handful of single-statement `if` bodies are re-wrapped and get braces added by `oxlint --fix` to satisfy the `curly: multi-line` rule. No behavior changes.
478 lines
16 KiB
TypeScript
478 lines
16 KiB
TypeScript
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();
|