workspace-updates-interactive.mjs 4.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. /** Operator-controlled qualification using the real workspace; synthetic payloads never execute. */
  2. import { app, BrowserWindow, Menu, dialog } from 'electron'
  3. import { writeFile } from 'node:fs/promises'
  4. import { join } from 'node:path'
  5. /**
  6. * Keep the private workspace open until the operator closes the control window or exits the application.
  7. * @param {object} options Real workspace and fixture-owned delivery, task, and installation controls.
  8. * @returns {Promise<void>} Resolves after operator exit and removal of control listeners.
  9. */
  10. export async function runInteractiveUpdates({ mainWindow, server, fixture, checkMenu, control, root }) {
  11. const panel = new BrowserWindow({ title: '本地升级验收控制', width: 640, height: 460,
  12. webPreferences: { nodeIntegration: false, contextIsolation: true, sandbox: true } })
  13. const finished = Promise.withResolvers()
  14. const finish = () => finished.resolve()
  15. panel.once('closed', finish)
  16. app.once('before-quit', finish)
  17. let closing = false
  18. const originalInstall = fixture.updater.quitAndInstall
  19. fixture.updater.quitAndInstall = (...args) => {
  20. fixture.installations.push(args)
  21. void dialog.showMessageBox(panel, { type: 'info', title: '本地验收结束',
  22. message: '已完成下载、校验、安装确认和任务收尾。',
  23. detail: '安装器调用已拦截,未安装或重启到新版本。确认后退出本轮演练;重新运行命令可开始下一轮。',
  24. buttons: ['结束演练'] }).then(finish).catch(finish)
  25. }
  26. const action = (label, operation) => ({ label, click: () => {
  27. if (closing) return
  28. void Promise.resolve().then(operation).catch(error => {
  29. console.error(error)
  30. if (!panel.isDestroyed()) dialog.showErrorBox('本地验收操作失败', String(error))
  31. })
  32. } })
  33. const select = mode => server.select(mode, '0.1.6-nightly.1')
  34. select('hold-download')
  35. const check = () => checkMenu.click()
  36. panel.setMenu(Menu.buildFromTemplate([
  37. { label: '更新场景', submenu: [
  38. action('普通更新(点击下载后保持进度)', () => { server.policy('clear'); select('hold-download'); check() }),
  39. action('强制更新(点击下载后保持进度)', () => { server.policy('force'); select('hold-download'); check() }),
  40. action('放行当前下载 → 校验与安装确认', () => server.release()),
  41. { type: 'separator' },
  42. action('下一次下载:校验失败', () => select('corrupt')),
  43. action('下一次下载:404 失败', () => select('download-404')),
  44. action('下一次下载:恢复正常', () => select('healthy')),
  45. action('检查失败', () => { select('feed-404'); check() }),
  46. action('没有可用更新(未下载前使用)', () => { server.select('healthy', '0.1.5-rc.1'); check() }),
  47. action('解除强更阻塞', () => { server.policy('clear'); check() }),
  48. ] },
  49. { label: '任务状态', submenu: [
  50. action('添加排队任务', () => control('queue')),
  51. action('清空排队任务', () => control('clear')),
  52. action('模拟任务停止失败(本轮有效)', () => control('hold-shutdown')),
  53. ] },
  54. { label: '窗口', submenu: [
  55. action('返回应用', () => { mainWindow.restore(); mainWindow.show(); mainWindow.focus() }),
  56. action('结束演练', finish),
  57. ] },
  58. ]))
  59. try {
  60. await panel.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(`<!doctype html><html lang="zh-CN">
  61. <meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'">
  62. <style>body{font:16px/1.8 system-ui;padding:24px;color:#222}h2{margin-top:0}strong{color:#165dff}</style>
  63. <h2>手动升级验收 · 不执行安装器</h2>
  64. <p>使用本窗口顶部的<strong>更新场景</strong>菜单选择普通更新或强制更新,再到应用里点击下载。</p>
  65. <p>下载会保持进度,供你查看。回到这里选择<strong>放行当前下载</strong>,才会进入校验和安装确认。</p>
  66. <p>用<strong>任务状态</strong>菜单添加排队任务,查看安装前的任务警告。失败模式须在下载前选择。</p>
  67. <p>数据和更新服务器均为本地隔离测试。下载地址为示例地址。关闭此窗口结束演练;重跑命令重置状态。</p>
  68. </html>`)}`)
  69. console.log(`Interactive updater ready: ${root}`)
  70. await finished.promise
  71. closing = true
  72. await writeFile(join(root, 'interactive-result.json'), JSON.stringify({ installerExecuted: false,
  73. interceptedInstallations: fixture.installations.length, phases: fixture.states.map(state => state.phase) }, null, 2) + '\n')
  74. } finally {
  75. closing = true
  76. fixture.updater.quitAndInstall = originalInstall
  77. app.off('before-quit', finish)
  78. panel.off('closed', finish)
  79. if (!panel.isDestroyed()) panel.destroy()
  80. }
  81. }