|
@@ -7,7 +7,7 @@ import { DesktopServiceManager } from './service-manager.mjs'
|
|
|
import { DesktopRunnerManager } from './runner-manager.mjs'
|
|
import { DesktopRunnerManager } from './runner-manager.mjs'
|
|
|
import { DesktopPreferences } from './preferences.mjs'
|
|
import { DesktopPreferences } from './preferences.mjs'
|
|
|
import { DesktopCredentials } from './credentials.mjs'
|
|
import { DesktopCredentials } from './credentials.mjs'
|
|
|
-import { synchronizeLocalService } from './service-sync.mjs'
|
|
|
|
|
|
|
+import { synchronizeLocalService, stopLocalService } from './service-sync.mjs'
|
|
|
|
|
|
|
|
const desktopRoot = dirname(fileURLToPath(import.meta.url))
|
|
const desktopRoot = dirname(fileURLToPath(import.meta.url))
|
|
|
const fixtureHome = process.env.HERMES_YAOYAO_DESKTOP_TEST_HOME
|
|
const fixtureHome = process.env.HERMES_YAOYAO_DESKTOP_TEST_HOME
|
|
@@ -45,8 +45,10 @@ else {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
function show() {
|
|
function show() {
|
|
|
|
|
+ if (closing || quitting) return
|
|
|
if (!window || window.isDestroyed()) createWindow()
|
|
if (!window || window.isDestroyed()) createWindow()
|
|
|
window.show(); window.focus()
|
|
window.show(); window.focus()
|
|
|
|
|
+ if (manager) void manager.reconnect().then(() => stateChanged(manager.state)).catch(error => log(error.message))
|
|
|
}
|
|
}
|
|
|
function safeExternal(url) {
|
|
function safeExternal(url) {
|
|
|
try { const parsed = new URL(url); if (['https:', 'http:', 'mailto:'].includes(parsed.protocol)) void shell.openExternal(url) }
|
|
try { const parsed = new URL(url); if (['https:', 'http:', 'mailto:'].includes(parsed.protocol)) void shell.openExternal(url) }
|
|
@@ -90,6 +92,11 @@ else {
|
|
|
}
|
|
}
|
|
|
function stateChanged(state) {
|
|
function stateChanged(state) {
|
|
|
log(`${state.phase}: ${state.message}`)
|
|
log(`${state.phase}: ${state.message}`)
|
|
|
|
|
+ // Quit owns the window from this point on. A detached connection is not a
|
|
|
|
|
+ // stopped server and must never navigate the closing window to boot.html.
|
|
|
|
|
+ if (closing || quitting) return
|
|
|
|
|
+ const serviceStatus=Menu.getApplicationMenu()?.getMenuItemById('desktop-service-status')
|
|
|
|
|
+ if(serviceStatus)serviceStatus.label=state.phase==='ready'?'后台服务运行中':state.message
|
|
|
const localVmMenu=Menu.getApplicationMenu()?.getMenuItemById('desktop-local-vm')
|
|
const localVmMenu=Menu.getApplicationMenu()?.getMenuItemById('desktop-local-vm')
|
|
|
if(localVmMenu)localVmMenu.enabled=state.phase==='ready'
|
|
if(localVmMenu)localVmMenu.enabled=state.phase==='ready'
|
|
|
const notice = Menu.getApplicationMenu()?.getMenuItemById('desktop-sync-notice')
|
|
const notice = Menu.getApplicationMenu()?.getMenuItemById('desktop-sync-notice')
|
|
@@ -106,19 +113,39 @@ else {
|
|
|
app.on('second-instance', show)
|
|
app.on('second-instance', show)
|
|
|
app.on('activate', show)
|
|
app.on('activate', show)
|
|
|
app.on('window-all-closed', () => {})
|
|
app.on('window-all-closed', () => {})
|
|
|
- app.on('before-quit', event => {
|
|
|
|
|
- if (quitting) return
|
|
|
|
|
- event.preventDefault()
|
|
|
|
|
- if (closing) return
|
|
|
|
|
|
|
+ function requestQuit(stopBackground = false) {
|
|
|
|
|
+ if (closing || quitting) return
|
|
|
closing = true; clearInterval(timer)
|
|
closing = true; clearInterval(timer)
|
|
|
- void (async()=>{
|
|
|
|
|
- try{await runnerManager?.stop();await manager?.stop();quitting=true;app.quit()}
|
|
|
|
|
- catch(error){
|
|
|
|
|
|
|
+ window?.hide()
|
|
|
|
|
+ // Native Cmd+Q can enter before-quit from Cocoa's termination callback.
|
|
|
|
|
+ // Both cleanup and the final quit must run after that callback unwinds.
|
|
|
|
|
+ setImmediate(() => { void (async()=>{
|
|
|
|
|
+ try {
|
|
|
|
|
+ await runnerManager?.stop()
|
|
|
|
|
+ if (stopBackground) await manager?.stopBackground()
|
|
|
|
|
+ else await manager?.stop()
|
|
|
|
|
+ quitting = true
|
|
|
|
|
+ setImmediate(() => app.quit())
|
|
|
|
|
+ } catch(error) {
|
|
|
log(error.message)
|
|
log(error.message)
|
|
|
- const choice=await dialog.showMessageBox(window,{type:'warning',message:'服务清理尚未完成',detail:error.message,buttons:['继续运行','仍然退出'],defaultId:0,cancelId:0})
|
|
|
|
|
- if(choice.response===1){quitting=true;app.quit()}else{closing=false;timer=setInterval(()=>{void manager.check()},10000);timer.unref()}
|
|
|
|
|
|
|
+ window?.show()
|
|
|
|
|
+ const choice=await dialog.showMessageBox(window,{type:'warning',message:stopBackground?'后台服务停止未完成':'App 退出清理未完成',detail:error.message,buttons:['返回应用','仅退出 App'],defaultId:0,cancelId:0})
|
|
|
|
|
+ if(choice.response===1){quitting=true;setImmediate(()=>app.quit())}
|
|
|
|
|
+ else {
|
|
|
|
|
+ closing=false
|
|
|
|
|
+ await manager?.reconnect().catch(error=>log(error.message))
|
|
|
|
|
+ await runnerManager?.start().catch(error=>log(error.message))
|
|
|
|
|
+ stateChanged(manager.state)
|
|
|
|
|
+ timer=setInterval(()=>{void manager.check()},10000);timer.unref()
|
|
|
|
|
+ show()
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- })()
|
|
|
|
|
|
|
+ })() })
|
|
|
|
|
+ }
|
|
|
|
|
+ app.on('before-quit', event => {
|
|
|
|
|
+ if (quitting) return
|
|
|
|
|
+ event.preventDefault()
|
|
|
|
|
+ requestQuit()
|
|
|
})
|
|
})
|
|
|
app.whenReady().then(async () => {
|
|
app.whenReady().then(async () => {
|
|
|
await preferences.load()
|
|
await preferences.load()
|
|
@@ -127,6 +154,8 @@ else {
|
|
|
const buildInfo=JSON.parse(readFileSync(join(root,'build-info.json'),'utf8'))
|
|
const buildInfo=JSON.parse(readFileSync(join(root,'build-info.json'),'utf8'))
|
|
|
app.setAboutPanelOptions({applicationName:'夭夭',applicationVersion:packageVersion,version:`${String(buildInfo.commit).slice(0,12)}${buildInfo.dirty?' · 工作区快照':''}`})
|
|
app.setAboutPanelOptions({applicationName:'夭夭',applicationVersion:packageVersion,version:`${String(buildInfo.commit).slice(0,12)}${buildInfo.dirty?' · 工作区快照':''}`})
|
|
|
manager = new DesktopServiceManager({ home, port, version: packageVersion, log, onState: stateChanged,
|
|
manager = new DesktopServiceManager({ home, port, version: packageVersion, log, onState: stateChanged,
|
|
|
|
|
+ stopBackground: (app.isPackaged && !fixtureHome) || (fixtureHome && process.env.HERMES_YAOYAO_DESKTOP_TEST_SYNC === '1')
|
|
|
|
|
+ ? onProgress => stopLocalService({home,port,root,fixture:Boolean(fixtureHome),onProgress}) : undefined,
|
|
|
synchronize: (app.isPackaged && !fixtureHome) || (fixtureHome && process.env.HERMES_YAOYAO_DESKTOP_TEST_SYNC === '1')
|
|
synchronize: (app.isPackaged && !fixtureHome) || (fixtureHome && process.env.HERMES_YAOYAO_DESKTOP_TEST_SYNC === '1')
|
|
|
? onProgress => synchronizeLocalService({ home, port, root, fixture: Boolean(fixtureHome), onProgress,
|
|
? onProgress => synchronizeLocalService({ home, port, root, fixture: Boolean(fixtureHome), onProgress,
|
|
|
environment: { HERMES_YAOYAO_UPSTREAM: process.env.HERMES_YAOYAO_UPSTREAM || 'http://127.0.0.1:9119', HERMES_YAOYAO_SUPERVISE_DASHBOARD: fixtureHome ? '0' : '1' } })
|
|
environment: { HERMES_YAOYAO_UPSTREAM: process.env.HERMES_YAOYAO_UPSTREAM || 'http://127.0.0.1:9119', HERMES_YAOYAO_SUPERVISE_DASHBOARD: fixtureHome ? '0' : '1' } })
|
|
@@ -159,6 +188,7 @@ else {
|
|
|
}
|
|
}
|
|
|
Menu.setApplicationMenu(Menu.buildFromTemplate([
|
|
Menu.setApplicationMenu(Menu.buildFromTemplate([
|
|
|
{ label: '夭夭', submenu: [{ role: 'about', label: '关于夭夭' }, { type: 'separator' },
|
|
{ label: '夭夭', submenu: [{ role: 'about', label: '关于夭夭' }, { type: 'separator' },
|
|
|
|
|
+ { id:'desktop-service-status',label:'正在连接后台服务…',enabled:false },
|
|
|
{ label: '显示窗口', click: show }, { label: '在浏览器中打开', click: () => { if (manager.state.url) safeExternal(manager.state.url) } },
|
|
{ label: '显示窗口', click: show }, { label: '在浏览器中打开', click: () => { if (manager.state.url) safeExternal(manager.state.url) } },
|
|
|
{ id:'desktop-local-vm',label:'本地虚拟机设置…',enabled:false,click:()=>{if(manager.state.phase==='ready'){show();void window.webContents.executeJavaScript("window.dispatchEvent(new CustomEvent('yaoyao:local-vm-settings'))")}} },
|
|
{ id:'desktop-local-vm',label:'本地虚拟机设置…',enabled:false,click:()=>{if(manager.state.phase==='ready'){show();void window.webContents.executeJavaScript("window.dispatchEvent(new CustomEvent('yaoyao:local-vm-settings'))")}} },
|
|
|
{ label: '查看服务日志', click: () => shell.showItemInFolder(logFile) },
|
|
{ label: '查看服务日志', click: () => shell.showItemInFolder(logFile) },
|
|
@@ -172,7 +202,9 @@ else {
|
|
|
{ id:'desktop-update-help', label:'App 更新与回退…', click:()=>dialog.showMessageBox(window,{type:'info',message:`夭夭 App ${packageVersion}`,detail:`App 构建:${String(buildInfo.commit).slice(0,12)}\n当前 Web:${manager.state.version||'尚未就绪'}${manager.state.build?.commit?' · '+manager.state.build.commit.slice(0,12):''}${manager.state.external?'(独立后台服务)':'(开发模式)'}\n\n安装或更新 App 后首次启动,会将较旧的本机 Web 同步到配套构建。已有更新的 Web 会保留;同一个 App 构建不会重复同步。\n\nWeb 可以继续自行升级。独立 Web 服务在退出 App 后继续运行;App 管理的执行节点会停止。\n\n降级通过 Web 的回滚入口单独处理。数据库结构不兼容时,需要独立的数据恢复流程。\n\n数据目录:${home}`,buttons:['知道了','打开数据目录']}).then(result=>{if(result.response===1)shell.showItemInFolder(home)}) },
|
|
{ id:'desktop-update-help', label:'App 更新与回退…', click:()=>dialog.showMessageBox(window,{type:'info',message:`夭夭 App ${packageVersion}`,detail:`App 构建:${String(buildInfo.commit).slice(0,12)}\n当前 Web:${manager.state.version||'尚未就绪'}${manager.state.build?.commit?' · '+manager.state.build.commit.slice(0,12):''}${manager.state.external?'(独立后台服务)':'(开发模式)'}\n\n安装或更新 App 后首次启动,会将较旧的本机 Web 同步到配套构建。已有更新的 Web 会保留;同一个 App 构建不会重复同步。\n\nWeb 可以继续自行升级。独立 Web 服务在退出 App 后继续运行;App 管理的执行节点会停止。\n\n降级通过 Web 的回滚入口单独处理。数据库结构不兼容时,需要独立的数据恢复流程。\n\n数据目录:${home}`,buttons:['知道了','打开数据目录']}).then(result=>{if(result.response===1)shell.showItemInFolder(home)}) },
|
|
|
{ type: 'separator' },
|
|
{ type: 'separator' },
|
|
|
{ role: 'hide', label: '隐藏夭夭' }, { role: 'hideOthers', label: '隐藏其他' }, { role: 'unhide', label: '显示全部' },
|
|
{ role: 'hide', label: '隐藏夭夭' }, { role: 'hideOthers', label: '隐藏其他' }, { role: 'unhide', label: '显示全部' },
|
|
|
- { type: 'separator' }, { role: 'quit', label: '退出夭夭' }] },
|
|
|
|
|
|
|
+ { type: 'separator' },
|
|
|
|
|
+ { id:'desktop-quit',label:'退出夭夭(后台继续运行)',accelerator:'CommandOrControl+Q',click:()=>requestQuit() },
|
|
|
|
|
+ { id:'desktop-stop-and-quit',label:'停止后台服务并退出',click:()=>requestQuit(true) }] },
|
|
|
{label:'执行节点',submenu:[{id:'runner-status',label:'未配置执行节点',enabled:false},{type:'separator'},
|
|
{label:'执行节点',submenu:[{id:'runner-status',label:'未配置执行节点',enabled:false},{type:'separator'},
|
|
|
{id:'runner-import',label:'导入节点配置…',click:importRunner},
|
|
{id:'runner-import',label:'导入节点配置…',click:importRunner},
|
|
|
{id:'runner-reconnect',label:'重新连接',click:()=>runnerAction(()=>runnerManager.start())},
|
|
{id:'runner-reconnect',label:'重新连接',click:()=>runnerAction(()=>runnerManager.start())},
|
|
@@ -183,10 +215,10 @@ else {
|
|
|
icon.setTemplateImage(true)
|
|
icon.setTemplateImage(true)
|
|
|
tray = new Tray(icon)
|
|
tray = new Tray(icon)
|
|
|
tray.setToolTip('夭夭')
|
|
tray.setToolTip('夭夭')
|
|
|
- tray.setContextMenu(Menu.buildFromTemplate([{ label: '打开夭夭', click: show }, { label: '查看日志', click: () => shell.showItemInFolder(logFile) }, { type: 'separator' }, { label: '退出夭夭', click: () => app.quit() }]))
|
|
|
|
|
|
|
+ tray.setContextMenu(Menu.buildFromTemplate([{ label: '打开夭夭', click: show }, { label: '查看日志', click: () => shell.showItemInFolder(logFile) }, { type: 'separator' }, { label: '退出夭夭(后台继续运行)', click: () => requestQuit() }, { label: '停止后台服务并退出', click: () => requestQuit(true) }]))
|
|
|
tray.on('click', show)
|
|
tray.on('click', show)
|
|
|
createWindow()
|
|
createWindow()
|
|
|
- powerMonitor.on('resume', () => { void manager.check() })
|
|
|
|
|
|
|
+ powerMonitor.on('resume', () => { if(!closing&&!quitting)void manager.check() })
|
|
|
timer = setInterval(() => { void manager.check() }, 10_000); timer.unref()
|
|
timer = setInterval(() => { void manager.check() }, 10_000); timer.unref()
|
|
|
await manager.start().catch(error => log(error.message))
|
|
await manager.start().catch(error => log(error.message))
|
|
|
if(!closing)await runnerManager.start().catch(error=>{runnerManager.publish(error.message)})
|
|
if(!closing)await runnerManager.start().catch(error=>{runnerManager.publish(error.message)})
|