Files
iofficeai--aionui/scripts/smoke-installer-self-lock.js
2026-07-13 13:20:22 +08:00

179 lines
5.9 KiB
JavaScript

#!/usr/bin/env node
const { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } = require('node:fs');
const { tmpdir } = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const repoRoot = path.resolve(__dirname, '..');
function nsisQuote(value) {
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '$\\"').replace(/\$/g, '$$');
}
function findMakensis() {
if (process.env.MAKENSIS && existsSync(process.env.MAKENSIS)) {
return process.env.MAKENSIS;
}
const candidates = [];
const cacheRoot = process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'electron-builder', 'Cache') : '';
function walk(dir, depth = 0) {
if (!dir || depth > 5 || !existsSync(dir)) {
return;
}
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(full, depth + 1);
} else if (entry.isFile() && entry.name.toLowerCase() === 'makensis.exe') {
candidates.push(full);
}
}
}
walk(cacheRoot);
candidates.sort((a, b) => b.localeCompare(a));
if (candidates[0]) {
return candidates[0];
}
const fromPath = spawnSync('where.exe', ['makensis.exe'], { encoding: 'utf8' });
if (fromPath.status === 0) {
const first = fromPath.stdout.split(/\r?\n/).find(Boolean);
if (first && existsSync(first)) {
return first;
}
}
throw new Error('makensis.exe not found. Run a Windows build once or set MAKENSIS=C:\\path\\to\\makensis.exe');
}
function readJsonl(logPath) {
return readFileSync(logPath, 'utf8')
.split(/\r?\n/)
.filter((line) => line.trim())
.map((line) => JSON.parse(line.replace(/^\uFEFF/, '')));
}
function normalizeWinPath(value) {
return path.win32.normalize(String(value).replace(/\\\\/g, '\\'));
}
function main() {
if (process.platform !== 'win32') {
throw new Error('This smoke test only runs on Windows.');
}
const makensis = findMakensis();
const root = mkdtempSync(path.join(tmpdir(), 'aionui-self-lock-'));
const installDir = path.join(root, 'install-dir');
mkdirSync(installDir, { recursive: true });
writeFileSync(path.join(installDir, 'existing-file.txt'), 'self-lock smoke\n', 'utf8');
const nsiPath = path.join(root, 'aionui-self-lock-smoke.nsi');
const exePath = path.join(root, 'aionui-self-lock-smoke.exe');
const logPath = path.join(
process.env.TEMP || tmpdir(),
`aionui-installer-self-lock-${new Date()
.toISOString()
.replace(/[-:]/g, '')
.replace(/\..+$/, '')
.replace('T', '-')}-log.jsonl`
);
const resultPath = path.join(process.env.TEMP || tmpdir(), `aionui-installer-self-lock-${process.pid}-result.txt`);
const processControlPath = path.join(repoRoot, 'resources', 'windows', 'installer-process-control.nsh');
const nsi = `
Unicode true
Name "AionUi Installer Self Lock Smoke"
OutFile "${nsisQuote(exePath)}"
RequestExecutionLevel user
SilentInstall silent
!define VERSION "self-lock-smoke"
!define AIONUI_TARGET_ARCH "x64"
!define AIONUI_FALLBACK_LOG "aionui-installer-self-lock-fallback.log"
!define AIONUI_APP_EXECUTABLE_FILENAME "AionUi.exe"
!define UNINSTALL_FILENAME "Uninstall AionUi.exe"
!define PROJECT_DIR "${nsisQuote(repoRoot)}"
!include LogicLib.nsh
!include "${nsisQuote(processControlPath)}"
Var AionUiSessionId
Var AionUiIsUpdated
Var AionUiSessionLogPath
Var ResultFile
Section
StrCpy $INSTDIR "${nsisQuote(installDir)}"
StrCpy $AionUiSessionId "selflock"
StrCpy $AionUiIsUpdated "1"
StrCpy $AionUiSessionLogPath "${nsisQuote(logPath)}"
StrCpy $ResultFile "${nsisQuote(resultPath)}"
InitPluginsDir
SetOutPath $INSTDIR
StrCpy $AionUiCurrentOutDir "$INSTDIR"
!insertmacro AIONUI_QUERY_LOCKERS "$INSTDIR" $AionUiLockerResult
FileOpen $0 "$ResultFile" w
FileWrite $0 "$AionUiLockerResult"
FileWrite $0 "|$AionUiCurrentOutDir|$AionUiSessionLogPath"
FileClose $0
\${If} $AionUiLockerResult != 0
SetErrorLevel 10
Quit
\${EndIf}
SectionEnd
`;
try {
writeFileSync(nsiPath, nsi, 'utf8');
console.log(`[self-lock] makensis: ${makensis}`);
const compile = spawnSync(makensis, [nsiPath], { encoding: 'utf8' });
if (compile.status !== 0) {
process.stdout.write(compile.stdout || '');
process.stderr.write(compile.stderr || '');
throw new Error(`makensis failed with exit ${compile.status}`);
}
const run = spawnSync(exePath, [], { encoding: 'utf8' });
if (run.status !== 0) {
process.stdout.write(run.stdout || '');
process.stderr.write(run.stderr || '');
const result = existsSync(resultPath) ? readFileSync(resultPath, 'utf8') : '<missing>';
throw new Error(`self-lock harness exited with ${run.status}; locker result=${result}`);
}
const events = readJsonl(logPath);
const lockers =
events.findLast?.((event) => event.event === 'rm-lockers') ??
events.filter((event) => event.event === 'rm-lockers').at(-1);
if (!lockers) {
throw new Error(`rm-lockers event missing: ${logPath}`);
}
if (lockers.fallbackReason !== 'installer-self-lock') {
throw new Error(`expected installer-self-lock, got ${lockers.fallbackReason || '<empty>'}`);
}
if (normalizeWinPath(lockers.currentOutDir) !== normalizeWinPath(installDir)) {
throw new Error(`expected currentOutDir ${installDir}, got ${lockers.currentOutDir}`);
}
const blocking = lockers.blockingProcesses || [];
if (!blocking.some((process) => process.name === 'AionUi installer' && Number(process.pid) > 0)) {
throw new Error(`expected AionUi installer blocker, got ${JSON.stringify(blocking)}`);
}
console.log(`[self-lock] ok: ${logPath}`);
} finally {
rmSync(resultPath, { force: true });
rmSync(root, { recursive: true, force: true });
}
}
try {
main();
} catch (err) {
console.error(`[self-lock] ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}