86 lines
3.2 KiB
JavaScript
86 lines
3.2 KiB
JavaScript
// Probes a running daemon's loaded modules (via loaded-modules.ps1) and flags
|
|
// any that resolve under the original app dir — those would be locked during an
|
|
// NSIS update and defeat the relocation.
|
|
|
|
import { spawnSync } from 'node:child_process'
|
|
import { join, win32 } from 'node:path'
|
|
|
|
const HERE = import.meta.dirname
|
|
const POWERSHELL = 'powershell.exe'
|
|
const PS_ARGS = ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass']
|
|
|
|
// Case-insensitive, separator-normalized containment test. The inputs are always
|
|
// Windows module paths, so use win32 semantics explicitly — otherwise --selftest
|
|
// on a non-Windows host would treat `\` as a literal and every check would fail.
|
|
function normalizeForCompare(p) {
|
|
return win32
|
|
.normalize(p)
|
|
.replace(/[\\/]+$/, '')
|
|
.toLowerCase()
|
|
}
|
|
|
|
/**
|
|
* Pure filter: which of `modulePaths` live under `appDir`. Exported so selftest
|
|
* can validate the containment logic against synthetic inputs without a launch.
|
|
* Matches on a path-segment boundary so `C:\App` does not match `C:\Application`.
|
|
*/
|
|
export function findAppDirResidentModules(modulePaths, appDir) {
|
|
const needle = normalizeForCompare(appDir)
|
|
const prefix = `${needle}${win32.sep}`
|
|
return modulePaths.filter((raw) => {
|
|
const candidate = normalizeForCompare(raw)
|
|
return candidate === needle || candidate.startsWith(prefix)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Run the PowerShell probe for `pid`. Returns
|
|
* { found, mainModule, modules } or throws if PowerShell itself fails.
|
|
*/
|
|
export function probeLoadedModules(pid) {
|
|
const script = join(HERE, 'loaded-modules.ps1')
|
|
const result = spawnSync(POWERSHELL, [...PS_ARGS, '-File', script, '-ProcessId', String(pid)], {
|
|
encoding: 'utf8',
|
|
maxBuffer: 32 * 1024 * 1024
|
|
})
|
|
if (result.error) {
|
|
throw new Error(`failed to spawn PowerShell probe: ${result.error.message}`)
|
|
}
|
|
// Check exit status before parsing: a non-zero exit that still wrote to stdout
|
|
// would otherwise surface as a bare JSON SyntaxError instead of the real error.
|
|
if (result.status !== 0) {
|
|
throw new Error(`loaded-modules.ps1 exited with status ${result.status}: ${result.stderr}`)
|
|
}
|
|
const trimmed = (result.stdout ?? '').trim()
|
|
if (!trimmed) {
|
|
throw new Error(
|
|
`loaded-modules.ps1 produced no output (exit ${result.status}): ${result.stderr}`
|
|
)
|
|
}
|
|
return JSON.parse(trimmed)
|
|
}
|
|
|
|
/**
|
|
* Full handle assessment for a running daemon: whether its main module is the
|
|
* copied host exe, and which loaded modules (if any) still live in the app dir.
|
|
* Returns a structured verdict; never throws for a clean/empty module list.
|
|
*/
|
|
export function assessDaemonHandles(pid, appDir, expectedHostExePath) {
|
|
const probe = probeLoadedModules(pid)
|
|
if (!probe.found) {
|
|
return { found: false, mainModuleOk: false, appDirModules: [], mainModule: null }
|
|
}
|
|
const modules = Array.isArray(probe.modules) ? probe.modules : []
|
|
const appDirModules = findAppDirResidentModules(modules, appDir)
|
|
const mainModuleOk =
|
|
typeof probe.mainModule === 'string' &&
|
|
normalizeForCompare(probe.mainModule) === normalizeForCompare(expectedHostExePath)
|
|
return {
|
|
found: true,
|
|
mainModule: probe.mainModule,
|
|
mainModuleOk,
|
|
moduleCount: modules.length,
|
|
appDirModules
|
|
}
|
|
}
|