源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,552 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { access, mkdir, mkdtemp, readFile, rm, stat } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { basename, join, resolve } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import {
|
||||
feedServed,
|
||||
startDesktopUpdateFeed,
|
||||
verifyDesktopUpdateArtifacts,
|
||||
} from './desktop-update-contract.mjs';
|
||||
import {
|
||||
evaluateInRenderer,
|
||||
findRendererTarget,
|
||||
isolatedUserEnv,
|
||||
waitForDevToolsPort,
|
||||
waitForUsableRenderer,
|
||||
runCommand,
|
||||
stopChild,
|
||||
} from './verify-packaged-app.mjs';
|
||||
import {
|
||||
completeInstalledApplicationUninstall,
|
||||
installerVersion,
|
||||
readUninstallDisplayVersions,
|
||||
terminateInstalledProcesses,
|
||||
waitForInstalledProcessAppearance,
|
||||
waitForInstalledProcessesToExit,
|
||||
} from './verify-windows-installer-lifecycle.mjs';
|
||||
import {
|
||||
assertWindowsProductVersion,
|
||||
powerShellLiteral,
|
||||
verifyPackagedWindowsApp,
|
||||
} from './verify-windows-x64.mjs';
|
||||
import { compareProductReleaseVersions } from './release-version.mjs';
|
||||
|
||||
const uninstallExecutableName = 'Uninstall Maka.exe';
|
||||
const executableName = 'Maka.exe';
|
||||
|
||||
function delay(milliseconds) {
|
||||
return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
|
||||
}
|
||||
|
||||
function step(label) {
|
||||
console.log(`[verify-windows-autoupdate] ${label}`);
|
||||
}
|
||||
|
||||
export async function waitForInstalledProductVersion(
|
||||
executablePath,
|
||||
{
|
||||
run = runCommand,
|
||||
timeoutMs = 60_000,
|
||||
probeTimeoutMs = 10_000,
|
||||
pollIntervalMs = 1_000,
|
||||
sleep = delay,
|
||||
} = {},
|
||||
) {
|
||||
const script = `(Get-Item ${powerShellLiteral(executablePath)}).VersionInfo.ProductVersion`;
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastProbeError;
|
||||
for (;;) {
|
||||
try {
|
||||
const { stdout } = await run(
|
||||
'powershell',
|
||||
['-NoProfile', '-NonInteractive', '-Command', script],
|
||||
{ timeoutMs: Math.max(1, Math.min(probeTimeoutMs, deadline - Date.now())) },
|
||||
);
|
||||
return stdout;
|
||||
} catch (error) {
|
||||
lastProbeError = error;
|
||||
}
|
||||
if (Date.now() >= deadline) {
|
||||
throw new Error(
|
||||
`Could not read the installed ProductVersion within ${timeoutMs}ms: ${lastProbeError.message}`,
|
||||
{ cause: lastProbeError },
|
||||
);
|
||||
}
|
||||
await sleep(pollIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* End-to-end Windows automatic-update verification.
|
||||
*
|
||||
* Installs the candidate build, points its packaged updater at a loopback feed
|
||||
* serving a version-bumped installer, and asserts the complete in-app path:
|
||||
* check → background download (feed request log) → `downloaded` status →
|
||||
* `installUpdate` handoff → old process exit → NSIS upgrade → automatic
|
||||
* relaunch as the new version → full packaged smoke of the upgraded install →
|
||||
* silent uninstall. Every stage has its own deadline and named failure; the
|
||||
* overall shape fails closed rather than reporting a partial success.
|
||||
*/
|
||||
export async function verifyWindowsAutoupdate(
|
||||
candidateInputPath,
|
||||
nextDirectoryInput,
|
||||
{
|
||||
platform = process.platform,
|
||||
makeTemporaryDirectory = () => mkdtemp(join(tmpdir(), 'maka-autoupdate-')),
|
||||
run = runCommand,
|
||||
} = {},
|
||||
) {
|
||||
if (platform !== 'win32') {
|
||||
throw new Error('Windows auto-update verification requires Windows.');
|
||||
}
|
||||
if (!candidateInputPath || !nextDirectoryInput) {
|
||||
throw new Error(
|
||||
'Usage: npm run verify:windows-autoupdate -- <candidate-exe> <next-release-directory>',
|
||||
);
|
||||
}
|
||||
|
||||
const candidateInstaller = resolve(candidateInputPath);
|
||||
const nextDirectory = resolve(nextDirectoryInput);
|
||||
const candidateVersion = installerVersion(candidateInstaller);
|
||||
await access(candidateInstaller);
|
||||
const latestYml = await readFile(join(nextDirectory, 'latest.yml'), 'utf8');
|
||||
const nextVersion = /^version:\s*(.+)\s*$/m.exec(latestYml)?.[1]?.trim();
|
||||
if (!nextVersion) {
|
||||
throw new Error(`latest.yml in ${nextDirectory} does not advertise a version.`);
|
||||
}
|
||||
if (compareProductReleaseVersions(nextVersion, candidateVersion) <= 0) {
|
||||
throw new Error(
|
||||
`The served version ${nextVersion} must be newer than the candidate ${candidateVersion}.`,
|
||||
);
|
||||
}
|
||||
const nextInstallerName = `Maka-${nextVersion}-win-x64.exe`;
|
||||
installerVersion(join(nextDirectory, nextInstallerName));
|
||||
await verifyDesktopUpdateArtifacts({
|
||||
directory: nextDirectory,
|
||||
metadataName: 'latest.yml',
|
||||
version: nextVersion,
|
||||
artifactName: nextInstallerName,
|
||||
});
|
||||
|
||||
const temporaryDirectory = await makeTemporaryDirectory();
|
||||
const installDirectory = join(temporaryDirectory, 'installed');
|
||||
const uninstaller = join(installDirectory, uninstallExecutableName);
|
||||
const installedExecutable = join(installDirectory, executableName);
|
||||
let installationStarted = false;
|
||||
let uninstallCompleted = false;
|
||||
let feed;
|
||||
let child;
|
||||
let primaryError;
|
||||
|
||||
try {
|
||||
step('starting the loopback update feed');
|
||||
// The candidate's own blockmap is served too, exactly as a GitHub release
|
||||
// hosts the previous version's assets: the updater requests it to attempt
|
||||
// a differential download. If the file is missing next to the candidate
|
||||
// installer, the mapped-but-absent 404 makes the updater fall back to the
|
||||
// full download — both are valid production shapes.
|
||||
feed = await startDesktopUpdateFeed(
|
||||
new Map([
|
||||
['latest.yml', join(nextDirectory, 'latest.yml')],
|
||||
[nextInstallerName, join(nextDirectory, nextInstallerName)],
|
||||
[`${nextInstallerName}.blockmap`, join(nextDirectory, `${nextInstallerName}.blockmap`)],
|
||||
[`${basename(candidateInstaller)}.blockmap`, `${candidateInstaller}.blockmap`],
|
||||
]),
|
||||
);
|
||||
|
||||
step(`installing candidate ${candidateVersion} into ${installDirectory}`);
|
||||
installationStarted = true;
|
||||
await run(candidateInstaller, ['/S', `/D=${installDirectory}`], { timeoutMs: 120_000 });
|
||||
await access(uninstaller);
|
||||
|
||||
step('launching the installed candidate against the loopback feed');
|
||||
const home = join(temporaryDirectory, 'home');
|
||||
const userData = join(temporaryDirectory, 'user-data');
|
||||
const userEnv = isolatedUserEnv(home);
|
||||
await mkdir(home, { recursive: true });
|
||||
await mkdir(userData, { recursive: true });
|
||||
await mkdir(userEnv.APPDATA, { recursive: true });
|
||||
await mkdir(userEnv.LOCALAPPDATA, { recursive: true });
|
||||
const childEnv = {
|
||||
...process.env,
|
||||
MAKA_SKIP_SHELL_ENV: '1',
|
||||
...userEnv,
|
||||
MAKA_UPDATE_TEST_FEED: feed.url,
|
||||
};
|
||||
// The mocks short-circuit before the real updater; leaking them from the
|
||||
// caller's environment would turn this into a fixture test.
|
||||
delete childEnv.MAKA_UPDATE_MOCK_VERSION;
|
||||
delete childEnv.MAKA_UPDATE_MOCK_STATE;
|
||||
child = spawn(
|
||||
installedExecutable,
|
||||
['--remote-debugging-port=0', `--user-data-dir=${userData}`, '--enable-logging=stderr'],
|
||||
{ cwd: temporaryDirectory, env: childEnv, stdio: ['ignore', 'ignore', 'pipe'] },
|
||||
);
|
||||
let stderr = '';
|
||||
child.stderr.setEncoding('utf8');
|
||||
child.stderr.on('data', (chunk) => {
|
||||
stderr = `${stderr}${chunk}`.slice(-16_384);
|
||||
});
|
||||
|
||||
const cdpPort = await waitForDevToolsPort(child);
|
||||
// On a CDP attach failure the app's own stderr tail is the only evidence
|
||||
// of what the packaged process was doing (renderer crash, GPU fallback,
|
||||
// profile lock); the bare "fetch failed" without it has already burned
|
||||
// full CI cycles on this exact stage.
|
||||
const target = await findRendererTarget(cdpPort, child).catch((error) => {
|
||||
throw new Error(
|
||||
`${error.message}${stderr.trim() ? `\napp stderr tail:\n${stderr.trim()}` : ''}`,
|
||||
{ cause: error },
|
||||
);
|
||||
});
|
||||
await waitForUsableRenderer(target.webSocketDebuggerUrl, child, {
|
||||
description: 'Candidate renderer',
|
||||
}).catch((error) => {
|
||||
throw new Error(`${error.message}${stderr.trim() ? `\n${stderr.trim()}` : ''}`, {
|
||||
cause: error,
|
||||
});
|
||||
});
|
||||
|
||||
step('driving an update check through the renderer bridge');
|
||||
await evaluateInRenderer(target.webSocketDebuggerUrl, 'window.maka.app.checkForUpdates()', {
|
||||
awaitPromise: true,
|
||||
timeoutMs: 30_000,
|
||||
});
|
||||
|
||||
step('waiting for the update to reach the downloaded state');
|
||||
const downloadDeadline = Date.now() + 180_000;
|
||||
let status;
|
||||
for (;;) {
|
||||
status = await evaluateInRenderer(
|
||||
target.webSocketDebuggerUrl,
|
||||
'window.maka.app.updateStatus()',
|
||||
{
|
||||
awaitPromise: true,
|
||||
},
|
||||
);
|
||||
if (status?.state === 'error') {
|
||||
throw new Error(
|
||||
`Update ${status.operation ?? 'flow'} failed: ${status.message ?? '<no message>'}`,
|
||||
);
|
||||
}
|
||||
if (status?.state === 'downloaded') break;
|
||||
if (child.exitCode !== null) {
|
||||
throw new Error(`Candidate exited while downloading the update.\n${stderr.trim()}`);
|
||||
}
|
||||
if (Date.now() >= downloadDeadline) {
|
||||
throw new Error(`Update never reached 'downloaded': ${JSON.stringify(status)}`);
|
||||
}
|
||||
await delay(500);
|
||||
}
|
||||
if (status.currentVersion !== candidateVersion || status.latestVersion !== nextVersion) {
|
||||
throw new Error(
|
||||
`Downloaded update advertises ${status.currentVersion} -> ${status.latestVersion}, ` +
|
||||
`expected ${candidateVersion} -> ${nextVersion}.`,
|
||||
);
|
||||
}
|
||||
|
||||
step('asserting the download really came from the loopback feed');
|
||||
// Exact root-level paths, matching the server's own allowlist shape.
|
||||
if (!feedServed(feed, 'latest.yml')) {
|
||||
throw new Error(
|
||||
`The app never fetched latest.yml from the loopback feed: ${JSON.stringify(feed.requests)}`,
|
||||
);
|
||||
}
|
||||
if (!feedServed(feed, nextInstallerName)) {
|
||||
throw new Error(
|
||||
`The app never downloaded the installer from the loopback feed: ${JSON.stringify(feed.requests)}`,
|
||||
);
|
||||
}
|
||||
// The differential probe is a deterministic part of the flow: the updater
|
||||
// asks for the running version's blockmap before deciding between a
|
||||
// differential and a full download. Both outcomes are valid production
|
||||
// shapes; the probe itself must have happened. The mode is reported from
|
||||
// the installer transfer responses themselves — a served previous blockmap
|
||||
// alone proves nothing about which download the updater then performed.
|
||||
const oldBlockmapProbe = feed.requests.find(
|
||||
(request) => request.path === `/${basename(candidateInstaller)}.blockmap`,
|
||||
);
|
||||
if (!oldBlockmapProbe) {
|
||||
throw new Error(
|
||||
`The updater never probed the previous blockmap for a differential download: ${JSON.stringify(feed.requests)}`,
|
||||
);
|
||||
}
|
||||
const installerResponses = feed.requests
|
||||
.filter((request) => request.method === 'GET' && request.path === `/${nextInstallerName}`)
|
||||
.map((request) => request.status);
|
||||
step(
|
||||
`installer transfer observed: previous blockmap ${oldBlockmapProbe.status}, ` +
|
||||
`installer responses [${installerResponses.join(', ')}] ` +
|
||||
`(${installerResponses.includes(206) ? 'ranged/differential transfer' : 'single full download'})`,
|
||||
);
|
||||
if (feed.unexpectedCount() > 0) {
|
||||
throw new Error(`The app requested unexpected feed paths: ${JSON.stringify(feed.requests)}`);
|
||||
}
|
||||
|
||||
step('handing off to the installer via installUpdate');
|
||||
let installResult;
|
||||
try {
|
||||
installResult = await evaluateInRenderer(
|
||||
target.webSocketDebuggerUrl,
|
||||
'window.maka.app.installUpdate({ allowInterruptActiveTasks: true })',
|
||||
{ awaitPromise: true, timeoutMs: 30_000 },
|
||||
);
|
||||
} catch (error) {
|
||||
// The evaluation races the app quitting for the installer handoff; a
|
||||
// dropped socket here is the expected shape of success. A structured
|
||||
// failure is not.
|
||||
installResult = { racedAppExit: true, message: error.message };
|
||||
}
|
||||
if (installResult && installResult.ok === false) {
|
||||
throw new Error(`installUpdate refused: ${JSON.stringify(installResult)}`);
|
||||
}
|
||||
|
||||
step('waiting for the candidate process to exit for the installer');
|
||||
await new Promise((resolvePromise, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => reject(new Error('Candidate did not exit for the installer within 60s.')),
|
||||
60_000,
|
||||
);
|
||||
if (child.exitCode !== null) {
|
||||
clearTimeout(timeout);
|
||||
resolvePromise();
|
||||
return;
|
||||
}
|
||||
child.once('exit', () => {
|
||||
clearTimeout(timeout);
|
||||
resolvePromise();
|
||||
});
|
||||
});
|
||||
|
||||
step('waiting for the upgraded app to relaunch automatically');
|
||||
const relaunched = await waitForInstalledProcessAppearance(installDirectory, executableName, {
|
||||
timeoutMs: 120_000,
|
||||
});
|
||||
const productVersion = await waitForInstalledProductVersion(installedExecutable, { run });
|
||||
try {
|
||||
assertWindowsProductVersion(productVersion, nextVersion);
|
||||
} catch (error) {
|
||||
// Nondeterministic CI state observed once: a Maka.exe process is
|
||||
// running from the install directory while the on-disk executable is
|
||||
// still the previous version. The only code path in the NSIS template
|
||||
// that launches the app is StartApp at full Section success, which
|
||||
// contradicts old bytes on disk — so on mismatch, capture the raw
|
||||
// state needed to attribute the launch and the transaction stage:
|
||||
// each process's command line (StartApp passes `--updated`), the
|
||||
// pre-upgrade backup directory (exists ⇒ the upgrade quit between
|
||||
// customInit and customInstall), the uninstall registration, and the
|
||||
// executable timestamps (rewritten ⇒ extraction ran).
|
||||
const evidence = [`relaunched processes: ${JSON.stringify(relaunched)}`];
|
||||
try {
|
||||
const commandLines = await run(
|
||||
'powershell',
|
||||
[
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-Command',
|
||||
`Get-CimInstance Win32_Process -Filter "Name='${executableName}'" | Select-Object ProcessId,ExecutablePath,CommandLine,CreationDate | ConvertTo-Json -Compress`,
|
||||
],
|
||||
// Bounded: this probe runs on a machine already in a wedged state,
|
||||
// and an evidence collector that hangs destroys the evidence — the
|
||||
// catch below keeps a timeout from aborting the rest of the capture.
|
||||
{ timeoutMs: 30_000 },
|
||||
);
|
||||
evidence.push(`process command lines: ${commandLines.stdout.trim() || '<none>'}`);
|
||||
} catch (processError) {
|
||||
evidence.push(`process command lines unavailable: ${processError.message}`);
|
||||
}
|
||||
const backupDirectory = `${installDirectory}.pre-upgrade-backup`;
|
||||
evidence.push(
|
||||
`backup directory ${backupDirectory}: ${existsSync(backupDirectory) ? 'present' : 'absent'}`,
|
||||
);
|
||||
try {
|
||||
const registrations = await readUninstallDisplayVersions({ run });
|
||||
evidence.push(`uninstall registrations: ${JSON.stringify(registrations)}`);
|
||||
} catch (registryError) {
|
||||
evidence.push(`uninstall registrations unavailable: ${registryError.message}`);
|
||||
}
|
||||
for (const name of [executableName, uninstallExecutableName]) {
|
||||
const filePath = join(installDirectory, name);
|
||||
try {
|
||||
const fileStat = await stat(filePath);
|
||||
evidence.push(`${name}: mtime ${fileStat.mtime.toISOString()}, ${fileStat.size} bytes`);
|
||||
} catch (statError) {
|
||||
evidence.push(`${name}: ${statError.code ?? statError.message}`);
|
||||
}
|
||||
}
|
||||
throw new Error(`${error.message}\n${evidence.join('\n')}`, { cause: error });
|
||||
}
|
||||
|
||||
step('stopping the relaunched instance');
|
||||
// isForceRunAfter relaunches without our CDP/user-data arguments, so the
|
||||
// instance is observed (it exists, and the image on disk is the new
|
||||
// version) and then stopped before it can touch further state; the
|
||||
// environment it inherited still points at the isolated home. The
|
||||
// tolerant kill-then-prove policy lives in terminateInstalledProcesses,
|
||||
// shared with the cleanup path below.
|
||||
await terminateInstalledProcesses(installDirectory, { run });
|
||||
|
||||
step('running the full packaged smoke against the upgraded install');
|
||||
// The sandbox probe writes its manifest into workingDirectory before the
|
||||
// renderer smoke would have created any subdirectories, so the directory
|
||||
// must exist first.
|
||||
const smokeDirectory = join(temporaryDirectory, 'smoke');
|
||||
await mkdir(smokeDirectory, { recursive: true });
|
||||
const upgradedStatusExpression = 'window.maka.app.updateStatus()';
|
||||
await verifyPackagedWindowsApp(installDirectory, {
|
||||
workingDirectory: smokeDirectory,
|
||||
expectedVersion: nextVersion,
|
||||
smokeRenderer: async (executable, { workingDirectory }) => {
|
||||
const smokeHome = join(workingDirectory, 'home');
|
||||
const smokeUserData = join(workingDirectory, 'user-data');
|
||||
const smokeEnv = isolatedUserEnv(smokeHome);
|
||||
await mkdir(smokeHome, { recursive: true });
|
||||
await mkdir(smokeUserData, { recursive: true });
|
||||
await mkdir(smokeEnv.APPDATA, { recursive: true });
|
||||
await mkdir(smokeEnv.LOCALAPPDATA, { recursive: true });
|
||||
const smokeChild = spawn(
|
||||
executable,
|
||||
[
|
||||
'--remote-debugging-port=0',
|
||||
`--user-data-dir=${smokeUserData}`,
|
||||
'--enable-logging=stderr',
|
||||
],
|
||||
{
|
||||
cwd: workingDirectory,
|
||||
env: { ...process.env, MAKA_SKIP_SHELL_ENV: '1', ...smokeEnv },
|
||||
stdio: ['ignore', 'ignore', 'pipe'],
|
||||
},
|
||||
);
|
||||
smokeChild.stderr.setEncoding('utf8');
|
||||
// A persistent collector, like every sibling smoke: without one the
|
||||
// piped stream pauses once waitForDevToolsPort removes its listener,
|
||||
// Chromium's stderr logging can fill the pipe and block the child,
|
||||
// and the failure evidence this capture exists for is lost.
|
||||
let smokeStderr = '';
|
||||
smokeChild.stderr.on('data', (chunk) => {
|
||||
smokeStderr = `${smokeStderr}${chunk}`.slice(-16_384);
|
||||
});
|
||||
try {
|
||||
const smokePort = await waitForDevToolsPort(smokeChild);
|
||||
const smokeTarget = await findRendererTarget(smokePort, smokeChild);
|
||||
await waitForUsableRenderer(smokeTarget.webSocketDebuggerUrl, smokeChild, {
|
||||
description: 'Upgraded renderer',
|
||||
}).catch((error) => {
|
||||
throw new Error(
|
||||
`${error.message}${smokeStderr.trim() ? `\n${smokeStderr.trim()}` : ''}`,
|
||||
{ cause: error },
|
||||
);
|
||||
});
|
||||
const upgradedStatus = await evaluateInRenderer(
|
||||
smokeTarget.webSocketDebuggerUrl,
|
||||
upgradedStatusExpression,
|
||||
{ awaitPromise: true },
|
||||
);
|
||||
if (upgradedStatus?.currentVersion !== nextVersion) {
|
||||
throw new Error(
|
||||
`Upgraded app reports version ${upgradedStatus?.currentVersion}, expected ${nextVersion}.`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await stopChild(smokeChild);
|
||||
}
|
||||
},
|
||||
});
|
||||
await waitForInstalledProcessesToExit(installDirectory);
|
||||
|
||||
step('uninstalling the upgraded application');
|
||||
// Files gone is not uninstall over: the detached uninstaller deletes the
|
||||
// uninstall registry keys as its last action, tens of seconds later on a
|
||||
// busy runner. Declaring success on files alone let the next verify step
|
||||
// install inside that window and lose its fresh registration to this
|
||||
// step's stale uninstaller (see waitForUninstallRegistrationToClear).
|
||||
await completeInstalledApplicationUninstall(installDirectory, uninstaller, { run });
|
||||
uninstallCompleted = true;
|
||||
step(`verified automatic update ${candidateVersion} -> ${nextVersion}`);
|
||||
return { candidateVersion, nextVersion, installDirectory };
|
||||
} catch (error) {
|
||||
primaryError = error;
|
||||
throw error;
|
||||
} finally {
|
||||
const cleanupErrors = [];
|
||||
if (child) {
|
||||
try {
|
||||
await stopChild(child);
|
||||
} catch (error) {
|
||||
cleanupErrors.push(error);
|
||||
}
|
||||
}
|
||||
if (feed) {
|
||||
try {
|
||||
await feed.close();
|
||||
} catch (error) {
|
||||
cleanupErrors.push(error);
|
||||
}
|
||||
}
|
||||
if (installationStarted && !uninstallCompleted) {
|
||||
let exited = false;
|
||||
try {
|
||||
// Same stop-and-prove policy as the main path: a stale-PID exit 128
|
||||
// from cleanup's own taskkill (run 32378497920) must not skip the
|
||||
// authoritative exit wait and the uninstall/registration barrier.
|
||||
await terminateInstalledProcesses(installDirectory, { run });
|
||||
exited = true;
|
||||
} catch (error) {
|
||||
cleanupErrors.push(error);
|
||||
}
|
||||
if (exited) {
|
||||
try {
|
||||
await completeInstalledApplicationUninstall(installDirectory, uninstaller, { run });
|
||||
} catch (error) {
|
||||
cleanupErrors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
await rm(temporaryDirectory, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
maxRetries: 20,
|
||||
retryDelay: 250,
|
||||
});
|
||||
} catch (error) {
|
||||
cleanupErrors.push(error);
|
||||
}
|
||||
if (cleanupErrors.length > 0) {
|
||||
const cleanupFailure = new AggregateError(
|
||||
cleanupErrors,
|
||||
'Auto-update verifier cleanup failed.',
|
||||
);
|
||||
if (!primaryError) throw cleanupFailure;
|
||||
if (primaryError instanceof Error && primaryError.cause === undefined) {
|
||||
primaryError.cause = cleanupFailure;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const result = await verifyWindowsAutoupdate(process.argv[2], process.argv[3]);
|
||||
console.log(`Verified automatic update ${result.candidateVersion} -> ${result.nextVersion}`);
|
||||
}
|
||||
Reference in New Issue
Block a user