Files
orca/config/scripts/nsis-process-check.test.mjs
T

125 lines
5.3 KiB
JavaScript

import { readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { runProcessSync } from '../../src/shared/child-process/run-process'
const hooks = readFileSync(new URL('../nsis/orca-installer-hooks.nsh', import.meta.url), 'utf8')
const processCheck = readFileSync(
new URL('../nsis/orca-process-check.nsh', import.meta.url),
'utf8'
)
const require = createRequire(import.meta.url)
const builderRequire = createRequire(require.resolve('electron-builder/package.json'))
const upstreamChecks = readFileSync(
join(
dirname(builderRequire.resolve('app-builder-lib/package.json')),
'templates/nsis/include/allowOnlyOneInstallerInstance.nsh'
),
'utf8'
)
function readPowerShellProbe(source = processCheck) {
const match = source.match(/nsExec::Exec `"\$PowerShellPath"(.*?) -(?:C|Command) "([^"\n]+)"`/)
if (!match) {
throw new Error('The NSIS PowerShell invocation was not found')
}
return { args: match[1].trim().split(/\s+/).filter(Boolean), command: match[2] }
}
describe('NSIS process-check integration', () => {
it('loads the capability hook through the installer and uninstaller include', () => {
expect(hooks).toContain('!include "${__FILEDIR__}\\orca-process-check.nsh"')
expect(processCheck).toMatch(/!macro customCheckAppRunning\b/)
expect(processCheck).toContain('!include "getProcessInfo.nsh"')
expect(processCheck).toMatch(/^Var pid$/m)
expect(processCheck).toMatch(/^Var \/GLOBAL IsPowerShellAvailable$/m)
})
it('keeps upstream process selection, retries, and installation-mode handling', () => {
expect(processCheck).toContain('!insertmacro _CHECK_APP_RUNNING')
expect(processCheck).not.toMatch(/!macro (?:FIND_PROCESS|KILL_PROCESS|_CHECK_APP_RUNNING)\b/)
expect(processCheck).not.toMatch(/\b(?:Stop-Process|taskkill|Set-ExecutionPolicy)\b/)
const findProcess = upstreamChecks.match(/!macro FIND_PROCESS\b[\s\S]*?!macroend/)?.[0]
if (!findProcess) {
throw new Error('The upstream process finder was not found')
}
expect(readPowerShellProbe().args).toEqual(readPowerShellProbe(findProcess).args)
})
})
describe.runIf(process.platform === 'win32')(
'NSIS capability probe under Restricted policy',
() => {
const policyReceipt = 'orca-nsis: restricted policy verified'
const queryFailureReceipt = 'orca-nsis: injected query failure'
const policyCheck = [
'function Test-OrcaRestrictedPolicy { param([string]$Scope)',
"try { $parameters = @{ ErrorAction = 'Stop' };",
'if ($Scope) { $parameters.Scope = $Scope };',
"return ((Get-ExecutionPolicy @parameters) -eq 'Restricted')",
'} catch { return $false } };',
// A failed getter must not fall through to the query's successful exit.
"if ((Test-OrcaRestrictedPolicy '__orca_invalid_scope__') -ne $false) { exit 11 };",
"if ((Test-OrcaRestrictedPolicy 'Process') -ne $true) { exit 10 };",
'if ((Test-OrcaRestrictedPolicy) -ne $true) { exit 10 };',
`[Console]::Out.WriteLine('${policyReceipt}');`
].join(' ')
// A watchdog against a wedged probe, not a latency budget: Windows PowerShell 5.1
// cold start under the packaged-CI process load has reached the former 20s cap. Matches
// the synchronous-probe ceiling in tests/tools/win-update-e2e/powershell-runner.mjs.
const PROBE_TIMEOUT_MS = 60_000
// Vitest's 30s default would otherwise cap the blocking spawn ahead of PROBE_TIMEOUT_MS.
const PROBE_TEST_TIMEOUT_MS = PROBE_TIMEOUT_MS + 15_000
function runProbe(arch, prefix = '') {
const { args, command } = readPowerShellProbe()
if (!process.env.SystemRoot) {
throw new Error('SystemRoot is required on Windows')
}
// The pwsh runner's module path points Windows PowerShell at incompatible PS7 modules.
const env = Object.fromEntries(
Object.entries(process.env).filter(([key]) => key.toLowerCase() !== 'psmodulepath')
)
return runProcessSync({
program: join(process.env.SystemRoot, arch, 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
args: [...args, '-Command', `${policyCheck} ${prefix}${command}`],
env: {
...env,
ORCA_BACKGROUND_LAUNCH: '1',
PSExecutionPolicyPreference: 'Restricted'
},
timeoutMs: PROBE_TIMEOUT_MS
})
}
it.each(['SysWOW64', 'System32'])(
'%s permits the real inline process query',
{ timeout: PROBE_TEST_TIMEOUT_MS },
(arch) => {
const result = runProbe(arch)
expect(result.code, JSON.stringify(result)).toBe(0)
expect(result.timedOut).toBe(false)
expect(result.stdout).toContain(policyReceipt)
}
)
it.each(['SysWOW64', 'System32'])(
'%s rejects a failed process query',
{ timeout: PROBE_TEST_TIMEOUT_MS },
(arch) => {
const result = runProbe(
arch,
'function Get-CimInstance { [CmdletBinding()] param([string]$ClassName); ' +
`[Console]::Out.WriteLine('${queryFailureReceipt}'); Write-Error 'CIM unavailable' }; `
)
expect(result.code, JSON.stringify(result)).toBe(1)
expect(result.timedOut).toBe(false)
expect(result.stdout).toContain(policyReceipt)
expect(result.stdout).toContain(queryFailureReceipt)
}
)
}
)