Files
ycode/scripts/verify-updates-installer-marker.cjs

254 lines
9.6 KiB
JavaScript

#!/usr/bin/env node
'use strict'
const assert = require('node:assert/strict')
const fs = require('node:fs')
const fsp = require('node:fs/promises')
const path = require('node:path')
const { execFileSync, spawnSync } = require('node:child_process')
const ROOT = path.resolve(__dirname, '..')
const REGISTRY_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\8a4ee941-d893-569b-8bcb-2a55d270c04b'
const STAMP = new Date().toISOString().replace(/[:.]/g, '-')
const RUN_ROOT = path.join(ROOT, '.tmp', 'updates-installer-marker', STAMP)
const LATEST_REPORT = path.join(ROOT, '.tmp', 'updates-installer-marker', 'latest-report.json')
function parseRegistryValue(output, name) {
const expression = new RegExp(`^\\s*${name}\\s+REG_\\w+\\s+(.+?)\\s*$`, 'mi')
const match = expression.exec(output)
return match ? match[1].replace(/^"(.*)"$/, '$1') : null
}
function queryRegistryValue(name) {
const output = execFileSync('reg.exe', ['query', REGISTRY_KEY, '/v', name], {
encoding: 'utf8',
windowsHide: true,
timeout: 10_000,
maxBuffer: 64 * 1024
})
return parseRegistryValue(output, name)
}
function registryExecutablePath(value) {
const quoted = /^\s*"([^"]+\.exe)"/i.exec(value)
if (quoted) return quoted[1]
const plain = /^\s*(.+?\.exe)(?:\s|$|,)/i.exec(value)
return plain ? plain[1] : null
}
function relativeImport(from, to) {
return `./${path.relative(path.dirname(from), to).replace(/\\/g, '/')}`
}
function createRunnerSource(filename) {
const installer = path.join(ROOT, 'src', 'main', 'updates', 'installer.ts')
const physical = path.join(ROOT, 'src', 'main', 'updates', 'physical-files.ts')
return `
import { app } from 'electron'
import fsPromises from 'node:fs/promises'
import path from 'node:path'
import { createNsisInstaller, queryNsisRegistryValue } from ${JSON.stringify(relativeImport(filename, installer))}
import { physicalRealPath, physicalStat } from ${JSON.stringify(relativeImport(filename, physical))}
function decodeConfig() {
return JSON.parse(Buffer.from(process.env.YCODE_MARKER_CONFIG || '', 'base64').toString('utf8'))
}
async function captureError(operation) {
try {
await operation()
return null
} catch (error) {
return {
code: error && typeof error === 'object' && 'code' in error ? error.code : null,
message: error instanceof Error ? error.message : String(error)
}
}
}
async function main() {
const config = decodeConfig()
const patched = await fsPromises.stat(config.appPath)
const physicalAsar = await physicalStat(config.appPath)
const physicalResources = await physicalStat(path.join(config.installationDirectory, 'resources'))
const validAdapter = createNsisInstaller({
platform: 'win32',
isPackaged: true,
execPath: config.executablePath,
appPath: config.appPath,
cacheDirectory: config.cacheDirectory,
resolveRealPath: physicalRealPath,
readStat: physicalStat,
queryRegistryValue: queryNsisRegistryValue
})
const environment = await validAdapter.validateEnvironment(1)
const directoryFixtureAdapter = createNsisInstaller({
platform: 'win32',
isPackaged: true,
execPath: config.directoryFixture.executablePath,
appPath: config.directoryFixture.appPath,
cacheDirectory: config.cacheDirectory,
resolveRealPath: physicalRealPath,
readStat: physicalStat,
queryRegistryValue: async name => {
if (name === 'UninstallString') return '"' + config.directoryFixture.uninstallerPath + '" /currentuser'
if (name === 'DisplayIcon') return config.directoryFixture.executablePath + ',0'
return null
}
})
const directoryError = await captureError(() => directoryFixtureAdapter.validateEnvironment(0))
const wrongRegistryAdapter = createNsisInstaller({
platform: 'win32',
isPackaged: true,
execPath: config.executablePath,
appPath: config.appPath,
cacheDirectory: config.cacheDirectory,
resolveRealPath: physicalRealPath,
readStat: physicalStat,
queryRegistryValue: async name => {
if (name === 'UninstallString') return '"C:\\\\Wrong\\\\Uninstall YCode.exe" /currentuser'
if (name === 'DisplayIcon') return config.executablePath + ',0'
return null
}
})
const wrongRegistryError = await captureError(() => wrongRegistryAdapter.validateEnvironment(0))
const result = {
electronVersion: process.versions.electron,
nodeVersion: process.versions.node,
installationDirectory: environment.installationDirectory,
executablePath: environment.executablePath,
appPath: config.appPath,
patchedFs: { isFile: patched.isFile(), isDirectory: patched.isDirectory() },
physicalAsar: { isFile: physicalAsar.isFile(), isDirectory: physicalAsar.isDirectory() },
physicalResources: { isFile: physicalResources.isFile(), isDirectory: physicalResources.isDirectory() },
directoryError,
wrongRegistryError,
installerLaunched: false
}
console.log('YCODE_MARKER_RESULT=' + Buffer.from(JSON.stringify(result)).toString('base64'))
app.exit(0)
}
main().catch(error => {
console.error(error)
app.exit(1)
})
`
}
async function main() {
if (process.platform !== 'win32') throw new Error('Installer marker verification requires Windows')
await fsp.mkdir(RUN_ROOT, { recursive: true })
const uninstallString = queryRegistryValue('UninstallString')
const displayIcon = queryRegistryValue('DisplayIcon')
const displayVersion = queryRegistryValue('DisplayVersion')
const uninstallerPath = uninstallString ? registryExecutablePath(uninstallString) : null
const executablePath = displayIcon
? registryExecutablePath(displayIcon.replace(/,\s*-?\d+\s*$/, ''))
: null
if (!uninstallerPath || !executablePath) throw new Error('Current HKCU YCode registry paths are incomplete')
const installationDirectory = path.dirname(executablePath)
if (path.dirname(uninstallerPath).toLowerCase() !== installationDirectory.toLowerCase()) {
throw new Error('Current HKCU uninstaller and icon paths disagree')
}
const appPath = path.join(installationDirectory, 'resources', 'app.asar')
const cacheDirectory = path.join(RUN_ROOT, 'cache')
await fsp.mkdir(cacheDirectory, { recursive: true })
const directoryFixtureRoot = path.join(RUN_ROOT, 'directory-marker-fixture')
const directoryFixtureResources = path.join(directoryFixtureRoot, 'resources')
await fsp.mkdir(path.join(directoryFixtureResources, 'app.asar'), { recursive: true })
const directoryFixtureExe = path.join(directoryFixtureRoot, 'YCode.exe')
const directoryFixtureUninstaller = path.join(directoryFixtureRoot, 'Uninstall YCode.exe')
await fsp.writeFile(directoryFixtureExe, 'fixture')
await fsp.writeFile(directoryFixtureUninstaller, 'fixture')
const runnerEntry = path.join(RUN_ROOT, 'runner-entry.ts')
const runnerOutput = path.join(RUN_ROOT, 'runner.cjs')
await fsp.writeFile(runnerEntry, createRunnerSource(runnerEntry), 'utf8')
const esbuild = path.join(ROOT, 'node_modules', 'esbuild', 'bin', 'esbuild')
execFileSync(process.execPath, [
esbuild,
runnerEntry,
'--bundle',
'--platform=node',
'--format=cjs',
'--external:electron',
`--outfile=${runnerOutput}`,
'--log-level=warning'
], { cwd: ROOT, stdio: 'inherit' })
const electronBinary = require('electron')
const config = {
installationDirectory,
executablePath,
appPath,
cacheDirectory,
directoryFixture: {
executablePath: directoryFixtureExe,
uninstallerPath: directoryFixtureUninstaller,
appPath: path.join(directoryFixtureResources, 'app.asar')
}
}
const env = {
...process.env,
YCODE_MARKER_CONFIG: Buffer.from(JSON.stringify(config)).toString('base64')
}
delete env.ELECTRON_RUN_AS_NODE
delete env.ELECTRON_NO_ASAR
const child = spawnSync(electronBinary, [runnerOutput], {
cwd: RUN_ROOT,
env,
encoding: 'utf8',
windowsHide: true,
timeout: 45_000,
maxBuffer: 4 * 1024 * 1024
})
if (child.error) throw child.error
if (child.status !== 0) {
throw new Error(`Electron marker runner failed (${child.status}): ${child.stderr || child.stdout}`)
}
const marker = child.stdout.split(/\r?\n/).find(line => line.startsWith('YCODE_MARKER_RESULT='))
if (!marker) throw new Error(`Electron marker runner returned no result: ${child.stderr || child.stdout}`)
const result = JSON.parse(Buffer.from(marker.slice('YCODE_MARKER_RESULT='.length), 'base64').toString('utf8'))
assert.equal(result.installationDirectory.toLowerCase(), installationDirectory.toLowerCase())
assert.equal(result.patchedFs.isFile, false)
assert.equal(result.patchedFs.isDirectory, true)
assert.equal(result.physicalAsar.isFile, true)
assert.equal(result.physicalAsar.isDirectory, false)
assert.equal(result.physicalResources.isFile, false)
assert.equal(result.physicalResources.isDirectory, true)
assert.equal(result.directoryError?.code, 'installer-marker')
assert.match(result.directoryError?.message || '', /app\.asar/)
assert.equal(result.wrongRegistryError?.code, 'installer-registry')
assert.equal(result.installerLaunched, false)
const report = {
schemaVersion: 1,
passed: true,
registryDisplayVersion: displayVersion,
uninstallerPath,
executablePath,
appPath,
runnerOutput,
result
}
const reportPath = path.join(RUN_ROOT, 'report.json')
await fsp.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8')
await fsp.mkdir(path.dirname(LATEST_REPORT), { recursive: true })
await fsp.writeFile(LATEST_REPORT, `${JSON.stringify(report, null, 2)}\n`, 'utf8')
console.log(`PASS installer marker: patched fs directory, original-fs file, directory and registry rejection`)
console.log(`report: ${reportPath}`)
}
main().catch(error => {
console.error(error)
process.exitCode = 1
})