Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ Run fiddles from anywhere, on any Electron release
# fiddle-core test ver (gist | repo URL | folder)
# fiddle-core bisect ver1 ver2 (gist | repo URL | folder)
#
# Run with Windows MSIX identity (Windows only):
# fiddle-core run:msix ver (gist | repo URL | folder)
# fiddle-core test:msix ver (gist | repo URL | folder)
# fiddle-core start:msix ver (gist | repo URL | folder)
#
# Examples:

$ fiddle-core run 12.0.0 /path/to/fiddle
$ fiddle-core test 12.0.0 642fa8daaebea6044c9079e3f8a46390
$ fiddle-core bisect 8.0.0 13.0.0 https://github.com/my/testcase.git

# Run with Windows MSIX identity (gives Electron a Windows app identity)
$ fiddle-core run:msix 30.0.0 /path/to/fiddle


$ fiddle-core bisect 8.0.0 13.0.0 642fa8daaebea6044c9079e3f8a46390
...
Expand Down Expand Up @@ -75,9 +83,34 @@ const result = await runner.run('15.0.0-alpha.1', files);
// bisect a regression test across a range of Electron versions
const result = await runner.bisect('10.0.0', '13.1.7', path_or_gist_or_git_repo);

// run with Windows MSIX identity (Windows only)
// This registers Electron as a sparse MSIX package, giving it a Windows app identity
const result = await runner.run('30.0.0', fiddle, { runWithIdentity: true });

// see also `Runner.spawn()` in Advanced Use
```

### Running with Windows MSIX Identity

On Windows, you can run Electron with a [sparse MSIX package](https://learn.microsoft.com/en-us/windows/msix/overview) identity. This gives Electron a Windows app identity, which is required for certain Windows features.

```ts
import { Runner } from '@electron/fiddle-core';

const runner = await Runner.create();

// Run fiddle with Windows MSIX identity
const result = await runner.run('30.0.0', fiddle, {
runWithIdentity: true,
});
```

When `runWithIdentity` is `true`, fiddle-core will:
1. Generate an AppxManifest.xml in the Electron installation directory
2. Register Electron as a sparse MSIX package using `Add-AppxPackage`
3. Run Electron via its registered execution alias


### Managing Electron Installations

```ts
Expand Down
2 changes: 2 additions & 0 deletions etc/fiddle-core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ export interface RunnerOptions {
// (undocumented)
runFromAsar?: boolean;
// (undocumented)
runWithIdentity?: boolean;
// (undocumented)
showConfig?: boolean;
}

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"bin": "dist/cli.js",
"files": [
"dist",
"static",
"README.md"
],
"publishConfig": {
Expand Down
9 changes: 9 additions & 0 deletions src/command-line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export async function runFromCommandLine(argv: string[]): Promise<void> {

type Cmd = 'bisect' | 'test' | undefined;
let cmd: Cmd = undefined;
let runWithIdentity: boolean = false;
let fiddle: Fiddle | undefined = undefined;

d('argv', inspect(argv));
Expand All @@ -27,6 +28,13 @@ export async function runFromCommandLine(argv: string[]): Promise<void> {
} else if (param === 'test' || param === 'start' || param === 'run') {
d('it is test');
cmd = 'test';
} else if (
param === 'test:msix' ||
param === 'start:msix' ||
param === 'run:msix'
) {
cmd = 'test';
runWithIdentity = true;
} else if (versions.isVersion(param)) {
versionArgs.push(param);
} else {
Expand Down Expand Up @@ -56,6 +64,7 @@ export async function runFromCommandLine(argv: string[]): Promise<void> {
if (cmd === 'test' && versionArgs.length === 1) {
const result = await runner.run(versionArgs[0], fiddle, {
out: process.stdout,
runWithIdentity: runWithIdentity,
});
const vals = ['test_passed', 'test_failed', 'test_error', 'system_error'];
process.exitCode = vals.indexOf(result.status);
Expand Down
18 changes: 17 additions & 1 deletion src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { Installer } from './installer.js';
import { ElectronVersions, Versions } from './versions.js';
import { Fiddle, FiddleFactory, FiddleSource } from './fiddle.js';
import { DefaultPaths, Paths } from './paths.js';
import { registerElectronIdentity } from './windows-identity.js';

const MSIX_EXEC_ALIAS = 'ElectronFiddleMSIX.exe';

export interface RunnerOptions {
// extra arguments to be appended to the electron invocation
Expand All @@ -25,6 +28,8 @@ export interface RunnerOptions {
showConfig?: boolean;
// whether to run the fiddle from asar
runFromAsar?: boolean;
// whether to run Electron with Windows MSIX identity (Windows only).
runWithIdentity?: boolean;
}

const DefaultRunnerOpts: RunnerOptions = {
Expand Down Expand Up @@ -153,7 +158,10 @@ export class Runner {

// set up the electron binary and the fiddle
const electronExec = await this.getExec(version);
let exec = electronExec;
let exec =
process.platform === 'win32' && opts.runWithIdentity
? MSIX_EXEC_ALIAS
: electronExec;
let args = [...(opts.args || []), fiddle.mainPath];
if (opts.headless) ({ exec, args } = Runner.headless(exec, args));

Expand Down Expand Up @@ -204,6 +212,14 @@ export class Runner {
fiddle: FiddleSource,
opts: RunnerSpawnOptions = DefaultRunnerOpts,
): Promise<TestResult> {
if (process.platform === 'win32' && opts.runWithIdentity) {
const electronVersion =
version instanceof SemVer ? version.version : version;
const electronExec = await this.getExec(electronVersion);
const electronDir = path.dirname(electronExec);
await registerElectronIdentity(electronVersion, electronDir);
}

const subprocess = await this.spawn(version, fiddle, opts);

return new Promise((resolve) => {
Expand Down
148 changes: 148 additions & 0 deletions src/windows-identity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { spawn } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';

const SOURCE_MANIFEST_FILENAME = 'FiddleAppxManifest.xml';
const TARGET_MANIFEST_FILENAME = 'AppxManifest.xml';
const SPARSE_PACKAGE_NAME = 'Electron.Fiddle.MSIX';

/**
* Map Node.js os.arch() to Windows AppxManifest ProcessorArchitecture values.
*/
function getAppxArchitecture(): string {
const arch = os.arch();
switch (arch) {
case 'x64':
return 'x64';
case 'ia32':
return 'x86';
case 'arm64':
return 'arm64';
default:
return 'x64';
}
}

/**
* Execute a PowerShell command and return the result.
*/
function executePowerShell(command: string): Promise<string> {
return new Promise((resolve, reject) => {
const ps = spawn('powershell.exe', [
'-NoProfile',
'-ExecutionPolicy',
'Bypass',
'-Command',
command,
]);

let stdout = '';
let stderr = '';

ps.stdout.on('data', (data: Buffer) => {
stdout += data.toString();
});

ps.stderr.on('data', (data: Buffer) => {
stderr += data.toString();
});

ps.on('close', (code: number) => {
if (code === 0) {
resolve(stdout.trim());
} else {
reject(new Error(`PowerShell command failed: ${stderr || stdout}`));
}
});

ps.on('error', (err: Error) => {
reject(err);
});
});
}

/**
* Unregister any previously registered sparse packages with our package name.
*/
async function unregisterSparsePackage(): Promise<void> {
try {
const result = await executePowerShell(
`Get-AppxPackage -Name "${SPARSE_PACKAGE_NAME}" | Select-Object -ExpandProperty PackageFullName`,
);

const packages = result
.trim()
.split('\n')
.map((p) => p.trim())
.filter((p) => p.length > 0);

for (const pkg of packages) {
console.log(`Unregistering sparse package: ${pkg}`);
await executePowerShell(`Remove-AppxPackage -Package "${pkg}"`);
console.log(`Successfully unregistered: ${pkg}`);
}
} catch {
console.log('No existing sparse package to unregister');
}
}

/**
* Register the sparse package for an Electron installation.
* This gives Electron a Windows app identity. Same as an MSIX package.
*
* @param version - The Electron version string to display in the manifest.
* @param electronDir - The directory containing the Electron executable.
*/
export async function registerElectronIdentity(
version: string,
electronDir: string,
): Promise<void> {
if (process.platform !== 'win32') {
return;
}

const electronExe = path.join(electronDir, 'electron.exe');

// Check if Electron is actually installed
if (!fs.existsSync(electronExe)) {
console.log(
`Electron not found at ${electronDir}, skipping identity registration`,
);
return;
}

try {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const sourcePath = path.join(
__dirname,
'..',
'static',
SOURCE_MANIFEST_FILENAME,
);
const targetPath = path.join(electronDir, TARGET_MANIFEST_FILENAME);

// Read manifest and replace placeholders
let manifest = fs.readFileSync(sourcePath, 'utf8');
const displayName = `Electron (${version}) MSIX`;
const architecture = getAppxArchitecture();
manifest = manifest.replace(/\$DISPLAY_NAME\$/g, displayName);
manifest = manifest.replace(/\$ARCHITECTURE\$/g, architecture);

console.log(`Writing manifest with version ${version} to ${targetPath}`);
fs.writeFileSync(targetPath, manifest, 'utf8');

await unregisterSparsePackage();

console.log(`Registering sparse package from: ${electronDir}`);
await executePowerShell(
`Add-AppxPackage -ExternalLocation "${electronDir}" -Register "${targetPath}"`,
);

console.log('Sparse package registered successfully');
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.error('Failed to register sparse package:', message);
}
}
52 changes: 52 additions & 0 deletions static/FiddleAppxManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?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"
xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
IgnorableNamespaces="uap uap3 desktop2">
<Identity Name="Electron.Fiddle.MSIX"
ProcessorArchitecture="$ARCHITECTURE$"
Version="1.0.0.0"
Publisher="CN=Electron"/>
<Properties>
<uap10:AllowExternalContent>true</uap10:AllowExternalContent>
<DisplayName>$DISPLAY_NAME$</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="$DISPLAY_NAME$"
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="ElectronFiddleMSIX.exe" />
</uap3:AppExecutionAlias>
</uap3:Extension>
</Extensions>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
<Capability Name="internetClient" />
</Capabilities>
</Package>
Loading