test-signed-updates.mjs 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. /** Run real Electron downloads from local executable inputs without installing or publishing them. */
  2. import assert from 'node:assert/strict'
  3. import { spawn } from 'node:child_process'
  4. import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises'
  5. import { join } from 'node:path'
  6. import { fileURLToPath } from 'node:url'
  7. import electron from 'electron'
  8. assert.equal(process.platform, 'win32', 'Signed NSIS download qualification requires Windows')
  9. const [certificate, signed, unsigned, old] = process.argv.slice(2)
  10. assert.ok(certificate && signed && unsigned,
  11. 'Usage: node apps/desktop/scripts/test-signed-updates.mjs <public.cer> <signed.exe> <unsigned.exe> [old.exe]')
  12. const paths = await Promise.all([certificate, signed, unsigned, ...(old ? [old, `${signed}.blockmap`, `${old}.blockmap`] : [])]
  13. .map(file => realpath(file)))
  14. const evidence = fileURLToPath(new URL('../.desktop-build/qualification/', import.meta.url))
  15. await mkdir(evidence, { recursive: true })
  16. const root = await mkdtemp(join(evidence, 'signed-downloads-'))
  17. await mkdir(join(root, 'runtime'))
  18. const environment = Object.fromEntries(Object.entries(process.env).filter(([name]) =>
  19. !/KEY|SECRET|TOKEN|PASSWORD|^NODE_OPTIONS$|^ELECTRON_RUN_AS_NODE$/iu.test(name)))
  20. let timedOut = false
  21. const timeoutMs = Number(process.env.DSH_SIGNED_UPDATE_TEST_TIMEOUT_MS ?? 180_000)
  22. assert.ok(Number.isSafeInteger(timeoutMs) && timeoutMs >= 1000 && timeoutMs <= 180_000,
  23. 'DSH_SIGNED_UPDATE_TEST_TIMEOUT_MS must be an integer from 1000 to 180000')
  24. let termination = Promise.resolve()
  25. let terminationError
  26. let launchError
  27. const startedAt = new Date().toISOString()
  28. const child = spawn(electron, [fileURLToPath(new URL('../tests/fixtures/signed-updates.mjs', import.meta.url))], {
  29. env: { ...environment, DSH_SIGNED_UPDATE_TEST_ROOT: root,
  30. DSH_SIGNED_UPDATE_CERTIFICATE: paths[0], DSH_SIGNED_UPDATE_SIGNED: paths[1], DSH_SIGNED_UPDATE_UNSIGNED: paths[2],
  31. DSH_SIGNED_UPDATE_OLD: paths[3] ?? '' },
  32. stdio: 'inherit', windowsHide: true,
  33. })
  34. child.once('error', error => { launchError = error.message })
  35. const deadline = setTimeout(() => {
  36. timedOut = true
  37. // Authenticode starts PowerShell; stop the complete owned process tree before deleting downloads.
  38. termination = new Promise((resolve) => {
  39. const killer = spawn('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], {
  40. env: environment, stdio: 'ignore', windowsHide: true,
  41. })
  42. killer.once('error', error => { terminationError = error.message })
  43. killer.once('close', (code) => {
  44. if (code !== 0) terminationError ??= `taskkill exited ${code}`
  45. resolve()
  46. })
  47. })
  48. }, timeoutMs)
  49. try {
  50. const result = await new Promise((resolve) => {
  51. child.once('close', (code, signal) => resolve({ code, signal }))
  52. })
  53. clearTimeout(deadline)
  54. await termination
  55. await writeFile(join(root, 'process.json'), `${JSON.stringify({ ...result, timedOut, pid: child.pid,
  56. startedAt, completedAt: new Date().toISOString(), launchError, terminationError }, null, 2)}\n`)
  57. assert.equal(launchError, undefined)
  58. assert.equal(terminationError, undefined)
  59. assert.equal(timedOut, false, `Signed download qualification exceeded ${timeoutMs} ms`)
  60. assert.equal(result.signal, null, `Electron exited on ${result.signal}`)
  61. assert.equal(result.code, 0, `Signed download qualification failed; see ${root}`)
  62. const report = JSON.parse(await readFile(join(root, 'result.json'), 'utf8'))
  63. assert.equal(report.passed, true)
  64. }
  65. finally {
  66. clearTimeout(deadline)
  67. await termination
  68. // Only this invocation's runtime directory is removed after process exit; reports remain.
  69. await rm(join(root, 'runtime'), { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
  70. console.log(`SIGNED_UPDATE_DOWNLOAD_RESULT ${root}`)
  71. }