123 lines
6.1 KiB
JavaScript
123 lines
6.1 KiB
JavaScript
/** 验证真实原生助手的就绪、取消、篡改拒绝与失败恢复;不执行 NSIS。 */
|
|||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import crypto from 'node:crypto';
|
||
|
|
import fs from 'node:fs';
|
||
|
|
import path from 'node:path';
|
||
|
|
import { spawn, execFileSync } from 'node:child_process';
|
||
|
|
import { createRequire } from 'node:module';
|
||
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||
|
|
import { buildUpdateHelper } from '../packages/desktop-client/scripts/build-update-helper.mjs';
|
||
|
|
|
||
|
|
if (process.platform !== 'win32') throw new Error('原生更新助手验证仅支持 Windows');
|
||
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||
|
|
const dir = path.join(root, '.cache', 'update-installer-tests', `${Date.now()}-含空格 验证`);
|
||
|
|
fs.mkdirSync(path.join(dir, 'updates'), { recursive: true });
|
||
|
|
const clientRequire = createRequire(path.join(root, 'packages/desktop-client/package.json'));
|
||
|
|
const { build } = clientRequire('esbuild');
|
||
|
|
buildUpdateHelper(path.join(dir, 'update-helper.exe'));
|
||
|
|
await build({
|
||
|
|
entryPoints: [path.join(root, 'packages/desktop-client/src/main/update-installer.ts')],
|
||
|
|
outfile: path.join(dir, 'installer.cjs'),
|
||
|
|
bundle: true,
|
||
|
|
format: 'cjs',
|
||
|
|
platform: 'node',
|
||
|
|
logLevel: 'silent',
|
||
|
|
});
|
||
|
|
const { prepareUpdateInstaller } = await import(pathToFileURL(path.join(dir, 'installer.cjs')).href);
|
||
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||
|
|
async function waitFor(read, timeout = 10000) {
|
||
|
|
const end = Date.now() + timeout;
|
||
|
|
while (Date.now() < end) {
|
||
|
|
try {
|
||
|
|
if (read()) return;
|
||
|
|
} catch {
|
||
|
|
/* 等待写入 */
|
||
|
|
}
|
||
|
|
await sleep(50);
|
||
|
|
}
|
||
|
|
throw new Error('等待原生助手结果超时');
|
||
|
|
}
|
||
|
|
const installer = path.join(dir, 'updates', 'fixture.exe');
|
||
|
|
fs.writeFileSync(installer, '只验证准备与取消,不执行此文件');
|
||
|
|
const options = {
|
||
|
|
directory: path.dirname(installer),
|
||
|
|
installer,
|
||
|
|
version: '2.0.0',
|
||
|
|
sha256: crypto.createHash('sha256').update(fs.readFileSync(installer)).digest('hex'),
|
||
|
|
size: fs.statSync(installer).size,
|
||
|
|
serverUrl: 'http://127.0.0.1:1',
|
||
|
|
username: 'isolated-helper-test',
|
||
|
|
restartExe: process.execPath,
|
||
|
|
portable: false,
|
||
|
|
};
|
||
|
|
const helper = await prepareUpdateInstaller(options);
|
||
|
|
assert.match(fs.readFileSync(path.join(dir, 'updates/install-helper.log'), 'utf8'), /校验通过/);
|
||
|
|
helper.cancel();
|
||
|
|
await waitFor(() => !fs.readdirSync(path.dirname(installer)).some((name) => name.endsWith('.ready')));
|
||
|
|
assert.doesNotMatch(fs.readFileSync(path.join(dir, 'updates/install-helper.log'), 'utf8'), /开始安装/);
|
||
|
|
console.log('✓ 真实助手就绪,取消交接后不执行安装器');
|
||
|
|
|
||
|
|
fs.appendFileSync(installer, '篡改');
|
||
|
|
await assert.rejects(prepareUpdateInstaller(options), /长度不匹配/);
|
||
|
|
assert.match(JSON.parse(fs.readFileSync(path.join(dir, 'updates/install-result.json'))).error, /长度不匹配/);
|
||
|
|
console.log('✓ 安装前再次检查文件,拒绝已被篡改的安装包并记录原因');
|
||
|
|
await assert.rejects(
|
||
|
|
prepareUpdateInstaller({ ...options, size: fs.statSync(installer).size }),
|
||
|
|
/SHA256 校验失败/,
|
||
|
|
);
|
||
|
|
console.log('✓ 长度合法但摘要不符的安装包也被拒绝');
|
||
|
|
|
||
|
|
const compiler = path.join(process.env.SystemRoot, 'Microsoft.NET/Framework64/v4.0.30319/csc.exe');
|
||
|
|
function compile(name, code) {
|
||
|
|
const source = path.join(dir, name + '.cs'),
|
||
|
|
exe = path.join(dir, name + '.exe');
|
||
|
|
fs.writeFileSync(source, code);
|
||
|
|
execFileSync(compiler, ['/nologo', '/target:winexe', '/out:' + exe, source], {
|
||
|
|
windowsHide: true,
|
||
|
|
stdio: 'pipe',
|
||
|
|
});
|
||
|
|
return exe;
|
||
|
|
}
|
||
|
|
const failure = compile('failed-installer', 'class Fixture { static int Main() { return 37; } }');
|
||
|
|
const restoreMarker = path.join(dir, 'restored.txt');
|
||
|
|
const restore = compile(
|
||
|
|
'restore-client',
|
||
|
|
'using System.IO; [assembly:System.Reflection.AssemblyInformationalVersion("1.0.0")] class Fixture { static void Main() { File.WriteAllText(@"' +
|
||
|
|
restoreMarker.replaceAll('"', '""') +
|
||
|
|
'","restored"); } }',
|
||
|
|
);
|
||
|
|
fs.copyFileSync(failure, installer);
|
||
|
|
options.sha256 = crypto.createHash('sha256').update(fs.readFileSync(installer)).digest('hex');
|
||
|
|
options.size = fs.statSync(installer).size;
|
||
|
|
options.restartExe = restore;
|
||
|
|
const childFile = path.join(dir, 'parent.cjs');
|
||
|
|
fs.writeFileSync(
|
||
|
|
childFile,
|
||
|
|
`const {prepareUpdateInstaller}=require('./installer.cjs');(async()=>{const helper=await prepareUpdateInstaller(${JSON.stringify(options)});helper.commit();process.exit(0)})().catch(e=>{console.error(e);process.exit(1)});`,
|
||
|
|
);
|
||
|
|
const parent = spawn(process.execPath, [childFile], { windowsHide: true, stdio: 'inherit' });
|
||
|
|
assert.equal(await new Promise((resolve) => parent.once('exit', resolve)), 0);
|
||
|
|
await waitFor(() => fs.existsSync(restoreMarker));
|
||
|
|
const result = JSON.parse(fs.readFileSync(path.join(dir, 'updates/install-result.json')));
|
||
|
|
assert.equal(result.stage, 'failed');
|
||
|
|
assert.match(result.error, /退出码:37/);
|
||
|
|
assert.match(fs.readFileSync(path.join(dir, 'updates/install-helper.log'), 'utf8'), /旧进程已退出,开始安装/);
|
||
|
|
console.log('✓ 父进程退出后助手继续运行,非零退出被记录并恢复客户端');
|
||
|
|
const success = compile('successful-installer', 'class Fixture { static int Main() { return 0; } }');
|
||
|
|
fs.copyFileSync(success, installer);
|
||
|
|
options.sha256 = crypto.createHash('sha256').update(fs.readFileSync(installer)).digest('hex');
|
||
|
|
options.size = fs.statSync(installer).size;
|
||
|
|
options.version = 'v1.0.0';
|
||
|
|
fs.rmSync(restoreMarker);
|
||
|
|
fs.writeFileSync(
|
||
|
|
childFile,
|
||
|
|
`const {prepareUpdateInstaller}=require('./installer.cjs');(async()=>{const helper=await prepareUpdateInstaller(${JSON.stringify(options)});helper.commit();process.exit(0)})().catch(e=>{console.error(e);process.exit(1)});`,
|
||
|
|
);
|
||
|
|
const successfulParent = spawn(process.execPath, [childFile], { windowsHide: true, stdio: 'inherit' });
|
||
|
|
assert.equal(await new Promise((resolve) => successfulParent.once('exit', resolve)), 0);
|
||
|
|
await waitFor(() => fs.existsSync(restoreMarker));
|
||
|
|
await waitFor(
|
||
|
|
() => JSON.parse(fs.readFileSync(path.join(dir, 'updates/install-result.json'))).stage === 'installed',
|
||
|
|
);
|
||
|
|
console.log('✓ 支持 v 前缀版本号,安装器未启动新版时由助手补启客户端');
|
||
|
|
console.log(`原生更新助手验证通过,证据目录:${dir}`);
|