mirror of
https://github.com/electron/electron.git
synced 2026-02-19 03:14:51 -05:00
feat: msix auto-updater (#49230)
* feat: native auto updater for MSIX on Windows * doc: added MSIX debug documentation * fix: allow downgrade with json release file and emit update-available * test: msix auot-update tests * doc: API documentation * test: add package version validation * fix: docs typo * fix: don't allow auto-updating when using appinstaller manifest * fix: getPackageInfo interface implementation * fix: review feedback, add comment * fix: missed filename commit * fix: install test cert on demand * fix: time stamp mismatch in tests * fix: feedback - rename to MSIXPackageInfo * fix: update and reference windowsStore property * fix: remove getPackagInfo from public API * fix: type error bcause of removed API
This commit is contained in:
336
spec/api-autoupdater-msix-spec.ts
Normal file
336
spec/api-autoupdater-msix-spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
50
spec/fixtures/api/autoupdater/msix/ElectronDevAppxManifest.xml
vendored
Normal file
50
spec/fixtures/api/autoupdater/msix/ElectronDevAppxManifest.xml
vendored
Normal 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>
|
||||
BIN
spec/fixtures/api/autoupdater/msix/HelloMSIX_V1.msix
vendored
Normal file
BIN
spec/fixtures/api/autoupdater/msix/HelloMSIX_V1.msix
vendored
Normal file
Binary file not shown.
BIN
spec/fixtures/api/autoupdater/msix/HelloMSIX_V2.msix
vendored
Normal file
BIN
spec/fixtures/api/autoupdater/msix/HelloMSIX_V2.msix
vendored
Normal file
Binary file not shown.
BIN
spec/fixtures/api/autoupdater/msix/MSIXDevCert.cer
vendored
Normal file
BIN
spec/fixtures/api/autoupdater/msix/MSIXDevCert.cer
vendored
Normal file
Binary file not shown.
22
spec/fixtures/api/autoupdater/msix/install_test_cert.ps1
vendored
Normal file
22
spec/fixtures/api/autoupdater/msix/install_test_cert.ps1
vendored
Normal 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"
|
||||
}
|
||||
96
spec/fixtures/api/autoupdater/msix/main.js
vendored
Normal file
96
spec/fixtures/api/autoupdater/msix/main.js
vendored
Normal 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
149
spec/lib/msix-helpers.ts
Normal 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
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user