|
@@ -1,7 +1,7 @@
|
|
|
-import { app, BrowserWindow, ipcMain, Menu, Tray, nativeImage, shell, utilityProcess, powerMonitor, systemPreferences, dialog, safeStorage, net } from 'electron'
|
|
|
|
|
|
|
+import { app, BrowserWindow, ipcMain, Menu, Tray, nativeImage, shell, utilityProcess, powerMonitor, systemPreferences, dialog, safeStorage, net, session as electronSession } from 'electron'
|
|
|
import { appendFileSync, mkdirSync, readFileSync, realpathSync } from 'node:fs'
|
|
import { appendFileSync, mkdirSync, readFileSync, realpathSync } from 'node:fs'
|
|
|
import { join, dirname } from 'node:path'
|
|
import { join, dirname } from 'node:path'
|
|
|
-import { homedir } from 'node:os'
|
|
|
|
|
|
|
+import { homedir, hostname } from 'node:os'
|
|
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
|
import { DesktopServiceManager } from './service-manager.mjs'
|
|
import { DesktopServiceManager } from './service-manager.mjs'
|
|
|
import { DesktopRunnerManager } from './runner-manager.mjs'
|
|
import { DesktopRunnerManager } from './runner-manager.mjs'
|
|
@@ -11,6 +11,7 @@ import { synchronizeLocalService, stopLocalService, migrateLocalData } from './s
|
|
|
import { resolveDataHome } from './data-home.mjs'
|
|
import { resolveDataHome } from './data-home.mjs'
|
|
|
import { DesktopEnvironmentHost, DesktopHostCore } from './environment-host.mjs'
|
|
import { DesktopEnvironmentHost, DesktopHostCore } from './environment-host.mjs'
|
|
|
import { DesktopHostManager } from './host-manager.mjs'
|
|
import { DesktopHostManager } from './host-manager.mjs'
|
|
|
|
|
+import { remoteSession, enrollDesktopHost, normalizeServerURL } from './remote-login.mjs'
|
|
|
import { DesktopUpdateManager } from './update-manager.mjs'
|
|
import { DesktopUpdateManager } from './update-manager.mjs'
|
|
|
import { installComputerViewer } from './computer-viewer.mjs'
|
|
import { installComputerViewer } from './computer-viewer.mjs'
|
|
|
|
|
|
|
@@ -22,7 +23,7 @@ app.setName('夭夭')
|
|
|
if (!app.requestSingleInstanceLock()) { app.quit() }
|
|
if (!app.requestSingleInstanceLock()) { app.quit() }
|
|
|
else {
|
|
else {
|
|
|
let window, tray, manager, runnerManager, quitting = false, closing = false, timer
|
|
let window, tray, manager, runnerManager, quitting = false, closing = false, timer
|
|
|
- let updateWindow, updater, environmentHost, hostManager
|
|
|
|
|
|
|
+ let updateWindow, updater, environmentHost, hostManager, loginWindow
|
|
|
let remoteMode = false, remoteURL = ''
|
|
let remoteMode = false, remoteURL = ''
|
|
|
const serviceURL = () => remoteMode ? remoteURL : manager?.state.url
|
|
const serviceURL = () => remoteMode ? remoteURL : manager?.state.url
|
|
|
const serviceOrigins = () => {
|
|
const serviceOrigins = () => {
|
|
@@ -183,7 +184,7 @@ else {
|
|
|
function requestQuit(stopBackground = false) {
|
|
function requestQuit(stopBackground = false) {
|
|
|
if (closing || quitting) return
|
|
if (closing || quitting) return
|
|
|
closing = true; clearInterval(timer)
|
|
closing = true; clearInterval(timer)
|
|
|
- updater?.cancel(); updateWindow?.close()
|
|
|
|
|
|
|
+ updater?.cancel(); updateWindow?.close(); loginWindow?.close()
|
|
|
window?.hide()
|
|
window?.hide()
|
|
|
// Native Cmd+Q can enter before-quit from Cocoa's termination callback.
|
|
// Native Cmd+Q can enter before-quit from Cocoa's termination callback.
|
|
|
// Both cleanup and the final quit must run after that callback unwinds.
|
|
// Both cleanup and the final quit must run after that callback unwinds.
|
|
@@ -273,18 +274,38 @@ else {
|
|
|
:await dialog.showOpenDialog({title:'导入桌面主机配置',properties:['openFile'],filters:[{name:'桌面主机配置',extensions:['json']}]})
|
|
:await dialog.showOpenDialog({title:'导入桌面主机配置',properties:['openFile'],filters:[{name:'桌面主机配置',extensions:['json']}]})
|
|
|
if(!selection.canceled&&selection.filePaths[0]&&!closing)await hostAction(()=>hostManager.importFile(selection.filePaths[0]))
|
|
if(!selection.canceled&&selection.filePaths[0]&&!closing)await hostAction(()=>hostManager.importFile(selection.filePaths[0]))
|
|
|
}
|
|
}
|
|
|
|
|
+ function openRemoteLogin(){
|
|
|
|
|
+ if(closing||quitting)return
|
|
|
|
|
+ if(loginWindow&&!loginWindow.isDestroyed()){loginWindow.show();loginWindow.focus();return}
|
|
|
|
|
+ loginWindow=new BrowserWindow({width:420,height:580,minWidth:360,minHeight:440,show:false,title:'登录远程',backgroundColor:'#f7f7f7',
|
|
|
|
|
+ webPreferences:{contextIsolation:true,nodeIntegration:false,sandbox:true,webSecurity:true,preload:join(desktopRoot,'remote-login-preload.cjs')}})
|
|
|
|
|
+ loginWindow.once('ready-to-show',()=>{if(loginWindow)loginWindow.show()})
|
|
|
|
|
+ loginWindow.on('closed',()=>{loginWindow=undefined})
|
|
|
|
|
+ const stored=preferences.value.remoteServer
|
|
|
|
|
+ void loginWindow.loadFile(join(desktopRoot,'remote-login.html'),stored?{query:{server:stored}}:undefined)
|
|
|
|
|
+ }
|
|
|
async function enterRemoteMode(){
|
|
async function enterRemoteMode(){
|
|
|
- let config=await hostManager.read().catch(error=>{log(error.message);return undefined})
|
|
|
|
|
- if(!config){await importDesktopHost();config=await hostManager.read().catch(()=>undefined)}
|
|
|
|
|
- if(!config)return false
|
|
|
|
|
- remoteMode=true;remoteURL=config.serverURL
|
|
|
|
|
|
|
+ const enrolled=await hostManager.read().catch(error=>{log(error.message);return undefined})
|
|
|
|
|
+ let server=''
|
|
|
|
|
+ const candidate=preferences.value.remoteServer||enrolled?.serverURL
|
|
|
|
|
+ if(candidate){try{server=normalizeServerURL(candidate)}catch(error){log(error.message)}}
|
|
|
|
|
+ if(!server){openRemoteLogin();return false}
|
|
|
|
|
+ remoteMode=true;remoteURL=server
|
|
|
|
|
+ await preferences.setRemoteServer(server).catch(error=>log(error.message))
|
|
|
await preferences.setStartupChoice('remote').catch(error=>log(error.message))
|
|
await preferences.setStartupChoice('remote').catch(error=>log(error.message))
|
|
|
- await hostManager.start().catch(error=>{hostManager.publish(error.message)})
|
|
|
|
|
|
|
+ if(enrolled)await hostManager.start().catch(error=>{hostManager.publish(error.message)})
|
|
|
const status=Menu.getApplicationMenu()?.getMenuItemById('desktop-service-status')
|
|
const status=Menu.getApplicationMenu()?.getMenuItemById('desktop-service-status')
|
|
|
- if(status)status.label=`已登录远程 · ${new URL(remoteURL).host}`
|
|
|
|
|
- tray?.setToolTip(`夭夭 · 远程 ${new URL(remoteURL).host}`)
|
|
|
|
|
|
|
+ if(status)status.label=`已登录远程 · ${new URL(server).host}`
|
|
|
|
|
+ tray?.setToolTip(`夭夭 · 远程 ${new URL(server).host}`)
|
|
|
syncModeMenu()
|
|
syncModeMenu()
|
|
|
- log(`远程模式:${remoteURL}`)
|
|
|
|
|
|
|
+ log(`远程模式:${server}`)
|
|
|
|
|
+ if(!fixtureHome){
|
|
|
|
|
+ try{
|
|
|
|
|
+ const probe=await net.fetch(new URL('/api/app/bootstrap',server),{headers:{accept:'application/json'},redirect:'error'})
|
|
|
|
|
+ const info=await probe.json().catch(()=>undefined)
|
|
|
|
|
+ if(info?.authenticated!==true)openRemoteLogin()
|
|
|
|
|
+ }catch{openRemoteLogin()}
|
|
|
|
|
+ }
|
|
|
return true
|
|
return true
|
|
|
}
|
|
}
|
|
|
function syncModeMenu(){
|
|
function syncModeMenu(){
|
|
@@ -317,12 +338,43 @@ else {
|
|
|
syncModeMenu()
|
|
syncModeMenu()
|
|
|
}catch(error){await appDialog({type:'error',message:'切换使用方式未完成',detail:error.message})}
|
|
}catch(error){await appDialog({type:'error',message:'切换使用方式未完成',detail:error.message})}
|
|
|
}
|
|
}
|
|
|
|
|
+ ipcMain.handle('desktop-remote-login:submit',async(_event,value)=>{
|
|
|
|
|
+ if(closing||quitting)return {ok:false,error:'App 正在退出'}
|
|
|
|
|
+ try{
|
|
|
|
|
+ const server=normalizeServerURL(value?.serverURL)
|
|
|
|
|
+ const auth=await remoteSession(server,{username:value?.username,password:value?.password})
|
|
|
|
|
+ let registered=false
|
|
|
|
|
+ if(auth.user?.role==='admin'){
|
|
|
|
|
+ const registration=await enrollDesktopHost(auth,{name:hostname()})
|
|
|
|
|
+ await hostManager.adopt({protocol:1,serverURL:server,hostId:registration.host.id,token:registration.token,...(server.startsWith('http://')?{allowInsecureLan:true}:{})})
|
|
|
|
|
+ registered=true
|
|
|
|
|
+ }
|
|
|
|
|
+ try{
|
|
|
|
|
+ for(const [name,cookie] of auth.cookies)
|
|
|
|
|
+ await electronSession.defaultSession.cookies.set({url:server,name,value:cookie,httpOnly:true,secure:server.startsWith('https://')})
|
|
|
|
|
+ }catch(error){log(`远程会话 Cookie 写入失败:${error.message}`)}
|
|
|
|
|
+ await enterRemoteMode()
|
|
|
|
|
+ if(!window||window.isDestroyed())createWindow()
|
|
|
|
|
+ else void window.loadURL(server)
|
|
|
|
|
+ loginWindow?.close()
|
|
|
|
|
+ if(!registered)await appDialog({type:'info',message:'已登录远程服务器',detail:'当前账号不是管理员:窗口将使用远程夭夭。要把这台 Mac 注册为可远程控制的桌面主机,请使用管理员账号重新登录。'})
|
|
|
|
|
+ return {ok:true,hostRegistered:registered,user:{username:auth.user.username,role:auth.user.role}}
|
|
|
|
|
+ }catch(error){return {ok:false,error:error instanceof Error?error.message:'登录失败'}}
|
|
|
|
|
+ })
|
|
|
|
|
+ ipcMain.handle('desktop-remote-login:use-local',async()=>{
|
|
|
|
|
+ loginWindow?.close()
|
|
|
|
|
+ if(!remoteMode){
|
|
|
|
|
+ if(!window||window.isDestroyed())createWindow()
|
|
|
|
|
+ await switchMode('local')
|
|
|
|
|
+ }
|
|
|
|
|
+ return {ok:true}
|
|
|
|
|
+ })
|
|
|
async function chooseStartupMode(){
|
|
async function chooseStartupMode(){
|
|
|
if(fixtureHome)return 'local'
|
|
if(fixtureHome)return 'local'
|
|
|
const stored=preferences.value.startupChoice
|
|
const stored=preferences.value.startupChoice
|
|
|
if(stored==='local'||stored==='remote')return stored
|
|
if(stored==='local'||stored==='remote')return stored
|
|
|
const choice=await dialog.showMessageBox({type:'question',title:'夭夭',message:'这台 Mac 如何使用夭夭?',
|
|
const choice=await dialog.showMessageBox({type:'question',title:'夭夭',message:'这台 Mac 如何使用夭夭?',
|
|
|
- detail:'「登录远程」连接另一台电脑上的夭夭服务器:本机不运行 Web 服务,窗口直接打开远程夭夭,本机桌面也可供远程服务器的机器人操作(需要服务器下发的桌面主机配置文件)。「进入本地」使用这台 Mac 自己的夭夭服务。',
|
|
|
|
|
|
|
+ detail:'「登录远程」用夭夭服务器的地址和账号密码登录另一台电脑上的夭夭:本机不运行 Web 服务,窗口直接使用远程夭夭;管理员账号还会把这台 Mac 注册为可远程控制的桌面主机。「进入本地」使用这台 Mac 自己的夭夭服务。',
|
|
|
buttons:['登录远程…','进入本地'],defaultId:1,cancelId:1,checkboxLabel:'记住选择,之后启动不再询问',checkboxChecked:false,noLink:true})
|
|
buttons:['登录远程…','进入本地'],defaultId:1,cancelId:1,checkboxLabel:'记住选择,之后启动不再询问',checkboxChecked:false,noLink:true})
|
|
|
const mode=choice.response===0?'remote':'local'
|
|
const mode=choice.response===0?'remote':'local'
|
|
|
if(choice.checkboxChecked)await preferences.setStartupChoice(mode).catch(error=>log(error.message))
|
|
if(choice.checkboxChecked)await preferences.setStartupChoice(mode).catch(error=>log(error.message))
|
|
@@ -375,15 +427,21 @@ else {
|
|
|
tray.setToolTip('夭夭')
|
|
tray.setToolTip('夭夭')
|
|
|
tray.setContextMenu(Menu.buildFromTemplate([{ label: '打开夭夭', click: show }, { label: '查看日志', click: () => shell.showItemInFolder(logFile) }, { type: 'separator' }, { label: '退出夭夭(后台继续运行)', click: () => requestQuit() }, { label: '停止后台服务并退出', click: () => requestQuit(true) }]))
|
|
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)
|
|
|
- const startupRemote=(await chooseStartupMode())==='remote'&&await enterRemoteMode()
|
|
|
|
|
- createWindow()
|
|
|
|
|
|
|
+ let startupRemote=false,startupLogin=false
|
|
|
|
|
+ if((await chooseStartupMode())==='remote'){
|
|
|
|
|
+ const enrolled=await hostManager.read().catch(()=>undefined)
|
|
|
|
|
+ if(preferences.value.remoteServer||enrolled)startupRemote=await enterRemoteMode()
|
|
|
|
|
+ else startupLogin=true
|
|
|
|
|
+ }
|
|
|
|
|
+ if(!startupLogin)createWindow()
|
|
|
powerMonitor.on('resume', () => { if(!closing&&!quitting)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()
|
|
|
- if(!startupRemote){
|
|
|
|
|
|
|
+ if(startupLogin)openRemoteLogin()
|
|
|
|
|
+ else if(!startupRemote){
|
|
|
syncModeMenu()
|
|
syncModeMenu()
|
|
|
await manager.start().catch(error => log(error.message))
|
|
await manager.start().catch(error => log(error.message))
|
|
|
if(!closing)environmentHost.start()
|
|
if(!closing)environmentHost.start()
|
|
|
}
|
|
}
|
|
|
- if(!closing&&!startupRemote&&manager.state.phase==='ready')await runnerManager.start().catch(error=>{runnerManager.publish(error.message)})
|
|
|
|
|
|
|
+ if(!startupLogin&&!startupRemote&&manager.state.phase==='ready')await runnerManager.start().catch(error=>{runnerManager.publish(error.message)})
|
|
|
}).catch(error => { log(error.stack || error.message); quitting = true; app.quit() })
|
|
}).catch(error => { log(error.stack || error.message); quitting = true; app.quit() })
|
|
|
}
|
|
}
|