2022-05-26 15:18:53 +08:00
// SiYuan - Build Your Eternal Digital Garden
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const {
2023-02-11 23:42:51 +08:00
app ,
BrowserWindow ,
shell ,
Menu ,
screen ,
ipcMain ,
globalShortcut ,
Tray ,
} = require ( "electron" ) ;
const path = require ( "path" ) ;
const fs = require ( "fs" ) ;
const net = require ( "net" ) ;
const fetch = require ( "electron-fetch" ) . default ;
process . noAsar = true ;
const appDir = path . dirname ( app . getAppPath ( ) ) ;
const isDevEnv = process . env . NODE _ENV === "development" ;
const appVer = app . getVersion ( ) ;
const confDir = path . join ( app . getPath ( "home" ) , ".config" , "siyuan" ) ;
const windowStatePath = path . join ( confDir , "windowState.json" ) ;
let bootWindow ;
let firstOpen = false ;
let workspaces = [ ] ; // workspaceDir, id, browserWindow, tray
let kernelPort = 6806 ;
2023-03-18 23:38:27 +08:00
let resetWindowStateOnRestart = false ;
2023-02-11 23:42:51 +08:00
require ( "@electron/remote/main" ) . initialize ( ) ;
2022-05-26 15:18:53 +08:00
if ( ! app . requestSingleInstanceLock ( ) ) {
2023-02-11 23:42:51 +08:00
app . quit ( ) ;
return ;
2022-05-26 15:18:53 +08:00
}
2023-01-04 23:03:08 +08:00
try {
2023-02-11 23:42:51 +08:00
firstOpen = ! fs . existsSync ( path . join ( confDir , "workspace.json" ) ) ;
if ( ! fs . existsSync ( confDir ) ) {
fs . mkdirSync ( confDir , { mode : 0o755 , recursive : true } ) ;
}
2023-01-04 23:03:08 +08:00
} catch ( e ) {
2023-02-11 23:42:51 +08:00
console . error ( e ) ;
require ( "electron" ) . dialog . showErrorBox ( "创建配置目录失败 Failed to create config directory" ,
"思源需要在用户家目录下创建配置文件夹(~/.config/siyuan) , 请确保该路径具有写入权限。\n\nSiYuan needs to create a configuration folder (~/.config/siyuan) in the user's home directory. Please make sure that the path has write permissions." ) ;
app . exit ( ) ;
2023-01-04 23:03:08 +08:00
}
2023-03-18 23:38:27 +08:00
// type: port/id
const exitApp = ( type , id ) => {
let tray ;
2023-03-18 23:54:58 +08:00
let mainWindow ;
2023-03-18 23:38:27 +08:00
workspaces . find ( ( item , index ) => {
if ( type === "id" ) {
if ( item . id === id ) {
2023-03-18 23:54:58 +08:00
mainWindow = item . browserWindow ;
2023-03-18 23:38:27 +08:00
if ( workspaces . length > 1 ) {
item . browserWindow . destroy ( ) ;
}
2023-03-18 23:54:58 +08:00
workspaces . splice ( index , 1 ) ;
2023-03-18 23:38:27 +08:00
tray = item . tray ;
return true ;
}
} else {
const currentURL = new URL ( item . browserWindow . getURL ( ) ) ;
2023-03-19 10:24:05 +08:00
if ( currentURL . port === id || currentURL . port === id . toString ( ) ) {
2023-03-18 23:54:58 +08:00
mainWindow = item . browserWindow ;
2023-03-18 23:38:27 +08:00
if ( workspaces . length > 1 ) {
item . browserWindow . destroy ( ) ;
}
2023-03-18 23:54:58 +08:00
workspaces . splice ( index , 1 ) ;
2023-03-18 23:38:27 +08:00
tray = item . tray ;
return true ;
}
}
} ) ;
if ( tray && ( "win32" === process . platform || "linux" === process . platform ) ) {
tray . destroy ( ) ;
}
2023-03-18 23:54:58 +08:00
if ( workspaces . length === 0 && mainWindow ) {
2023-03-18 23:38:27 +08:00
try {
if ( resetWindowStateOnRestart ) {
fs . writeFileSync ( windowStatePath , "{}" ) ;
} else {
const bounds = mainWindow . getBounds ( ) ;
fs . writeFileSync ( windowStatePath , JSON . stringify ( {
isMaximized : mainWindow . isMaximized ( ) ,
fullscreen : mainWindow . isFullScreen ( ) ,
isDevToolsOpened : mainWindow . webContents . isDevToolsOpened ( ) ,
x : bounds . x ,
y : bounds . y ,
width : bounds . width ,
height : bounds . height ,
} ) ) ;
}
} catch ( e ) {
writeLog ( e ) ;
}
app . exit ( ) ;
globalShortcut . unregisterAll ( ) ;
writeLog ( "exited ui" ) ;
}
}
2023-01-06 16:44:54 +08:00
const getServer = ( port = kernelPort ) => {
2023-02-11 23:42:51 +08:00
return "http://127.0.0.1:" + port ;
} ;
2023-01-04 23:03:08 +08:00
const sleep = ( ms ) => {
2023-02-11 23:42:51 +08:00
return new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
} ;
2023-01-04 23:03:08 +08:00
2022-05-26 15:18:53 +08:00
const showErrorWindow = ( title , content ) => {
2023-02-11 23:42:51 +08:00
let errorHTMLPath = path . join ( appDir , "app" , "electron" , "error.html" ) ;
if ( isDevEnv ) {
errorHTMLPath = path . join ( appDir , "electron" , "error.html" ) ;
}
const errWindow = new BrowserWindow ( {
width : screen . getPrimaryDisplay ( ) . size . width / 2 ,
height : screen . getPrimaryDisplay ( ) . workAreaSize . height / 2 ,
frame : false ,
icon : path . join ( appDir , "stage" , "icon-large.png" ) ,
webPreferences : {
nodeIntegration : true ,
webviewTag : true ,
webSecurity : false ,
contextIsolation : false ,
} ,
} ) ;
require ( "@electron/remote/main" ) . enable ( errWindow . webContents ) ;
errWindow . loadFile ( errorHTMLPath , {
query : {
home : app . getPath ( "home" ) ,
v : appVer ,
title : title ,
content : content ,
icon : path . join ( appDir , "stage" , "icon-large.png" ) ,
} ,
} ) ;
errWindow . show ( ) ;
} ;
2022-05-26 15:18:53 +08:00
const writeLog = ( out ) => {
2023-02-11 23:42:51 +08:00
console . log ( out ) ;
const logFile = path . join ( confDir , "app.log" ) ;
let log = "" ;
const maxLogLines = 1024 ;
try {
if ( fs . existsSync ( logFile ) ) {
log = fs . readFileSync ( logFile ) . toString ( ) ;
let lines = log . split ( "\n" ) ;
if ( maxLogLines < lines . length ) {
log = lines . slice ( maxLogLines / 2 , maxLogLines ) . join ( "\n" ) + "\n" ;
}
}
out = out . toString ( ) ;
out = new Date ( ) . toISOString ( ) . replace ( /T/ , " " ) . replace ( /\..+/ , "" ) + " " +
out ;
log += out + "\n" ;
fs . writeFileSync ( logFile , log ) ;
} catch ( e ) {
console . error ( e ) ;
2022-05-26 15:18:53 +08:00
}
2023-02-11 23:42:51 +08:00
} ;
2022-05-26 15:18:53 +08:00
const boot = ( ) => {
2023-02-11 23:42:51 +08:00
// 恢复主窗体状态
let oldWindowState = { } ;
try {
oldWindowState = JSON . parse ( fs . readFileSync ( windowStatePath , "utf8" ) ) ;
} catch ( e ) {
fs . writeFileSync ( windowStatePath , "{}" ) ;
}
let defaultWidth ;
let defaultHeight ;
let workArea ;
try {
defaultWidth = screen . getPrimaryDisplay ( ) . size . width ;
defaultHeight = screen . getPrimaryDisplay ( ) . workAreaSize . height ;
workArea = screen . getPrimaryDisplay ( ) . workArea ;
} catch ( e ) {
console . error ( e ) ;
}
const windowState = Object . assign ( { } , {
isMaximized : true ,
fullscreen : false ,
isDevToolsOpened : false ,
x : 0 , y : 0 ,
width : defaultWidth ,
height : defaultHeight ,
} , oldWindowState ) ;
2022-05-26 15:18:53 +08:00
2023-02-11 23:42:51 +08:00
let x = windowState . x ;
let y = windowState . y ;
if ( workArea ) {
// 窗口大小等同于或大于 workArea 时,缩小会隐藏到左下角
if ( windowState . width >= workArea . width || windowState . height >=
workArea . height ) {
windowState . width = Math . min ( defaultWidth , workArea . width ) ;
windowState . height = Math . min ( defaultHeight , workArea . height ) ;
}
if ( x > workArea . width ) {
x = 0 ;
}
if ( y > workArea . height ) {
y = 0 ;
}
2022-07-19 21:24:49 +08:00
}
2023-02-11 23:42:51 +08:00
if ( windowState . width < 400 ) {
windowState . width = 400 ;
2022-07-19 21:24:49 +08:00
}
2023-02-11 23:42:51 +08:00
if ( windowState . height < 300 ) {
windowState . height = 300 ;
2022-07-19 21:24:49 +08:00
}
2023-02-11 23:42:51 +08:00
if ( x < 0 ) {
x = 0 ;
}
if ( y < 0 ) {
y = 0 ;
}
// 创建主窗体
const currentWindow = new BrowserWindow ( {
show : false ,
backgroundColor : "#FFF" , // 桌面端主窗体背景色设置为 `#FFF` Fix https://github.com/siyuan-note/siyuan/issues/4544
width : windowState . width ,
height : windowState . height ,
minWidth : 493 ,
minHeight : 376 ,
x ,
y ,
fullscreenable : true ,
fullscreen : windowState . fullscreen ,
trafficLightPosition : { x : 8 , y : 8 } ,
webPreferences : {
nodeIntegration : true ,
webviewTag : true ,
webSecurity : false ,
contextIsolation : false ,
2023-03-07 15:06:50 +08:00
autoplayPolicy : "user-gesture-required" // 桌面端禁止自动播放多媒体 https://github.com/siyuan-note/siyuan/issues/7587
2023-02-11 23:42:51 +08:00
} ,
frame : "darwin" === process . platform ,
titleBarStyle : "hidden" ,
icon : path . join ( appDir , "stage" , "icon-large.png" ) ,
} ) ;
require ( "@electron/remote/main" ) . enable ( currentWindow . webContents ) ;
currentWindow . webContents . userAgent = "SiYuan/" + appVer +
" https://b3log.org/siyuan Electron" ;
2022-05-26 15:18:53 +08:00
2023-02-11 23:42:51 +08:00
currentWindow . webContents . session . setSpellCheckerLanguages ( [ "en-US" ] ) ;
2022-07-25 22:51:01 +08:00
2023-02-11 23:42:51 +08:00
// 发起互联网服务请求时绕过安全策略 https://github.com/siyuan-note/siyuan/issues/5516
currentWindow . webContents . session . webRequest . onBeforeSendHeaders (
( details , cb ) => {
if ( - 1 < details . url . indexOf ( "bili" ) ) {
// B 站不移除 Referer https://github.com/siyuan-note/siyuan/issues/94
cb ( { requestHeaders : details . requestHeaders } ) ;
return ;
}
2022-12-30 11:27:26 +08:00
2023-02-11 23:42:51 +08:00
for ( let key in details . requestHeaders ) {
if ( "referer" === key . toLowerCase ( ) ) {
delete details . requestHeaders [ key ] ;
}
}
cb ( { requestHeaders : details . requestHeaders } ) ;
} ) ;
currentWindow . webContents . session . webRequest . onHeadersReceived (
( details , cb ) => {
for ( let key in details . responseHeaders ) {
if ( "x-frame-options" === key . toLowerCase ( ) ) {
delete details . responseHeaders [ key ] ;
} else if ( "content-security-policy" === key . toLowerCase ( ) ) {
delete details . responseHeaders [ key ] ;
} else if ( "access-control-allow-origin" === key . toLowerCase ( ) ) {
delete details . responseHeaders [ key ] ;
}
}
cb ( { responseHeaders : details . responseHeaders } ) ;
} ) ;
2022-07-25 22:51:01 +08:00
2023-02-11 23:42:51 +08:00
currentWindow . webContents . on ( "did-finish-load" , ( ) => {
let siyuanOpenURL ;
if ( "win32" === process . platform || "linux" === process . platform ) {
siyuanOpenURL = process . argv . find ( ( arg ) => arg . startsWith ( "siyuan://" ) ) ;
2022-07-25 22:51:01 +08:00
}
2023-02-11 23:42:51 +08:00
if ( siyuanOpenURL ) {
if ( currentWindow . isMinimized ( ) ) {
currentWindow . restore ( ) ;
}
if ( ! currentWindow . isVisible ( ) ) {
currentWindow . show ( ) ;
}
currentWindow . focus ( ) ;
setTimeout ( ( ) => { // 等待界面js执行完毕
writeLog ( siyuanOpenURL ) ;
currentWindow . webContents . send ( "siyuan-openurl" , siyuanOpenURL ) ;
} , 2000 ) ;
2023-01-04 23:03:08 +08:00
}
2023-02-11 23:42:51 +08:00
} ) ;
2022-07-25 22:51:01 +08:00
2023-02-11 23:42:51 +08:00
if ( windowState . isDevToolsOpened ) {
currentWindow . webContents . openDevTools ( { mode : "bottom" } ) ;
2022-05-26 15:18:53 +08:00
}
2023-02-11 23:42:51 +08:00
// 主界面事件监听
currentWindow . once ( "ready-to-show" , ( ) => {
currentWindow . show ( ) ;
if ( windowState . isMaximized ) {
currentWindow . maximize ( ) ;
} else {
currentWindow . unmaximize ( ) ;
}
if ( bootWindow && ! bootWindow . isDestroyed ( ) ) {
bootWindow . destroy ( ) ;
}
} ) ;
2022-05-26 15:18:53 +08:00
2023-02-11 23:42:51 +08:00
// 加载主界面
currentWindow . loadURL ( getServer ( ) + "/stage/build/app/index.html?v=" +
new Date ( ) . getTime ( ) ) ;
2022-05-26 15:18:53 +08:00
2023-02-11 23:42:51 +08:00
// 菜单
const productName = "SiYuan" ;
const template = [
2022-05-26 15:18:53 +08:00
{
2023-02-11 23:42:51 +08:00
label : productName ,
submenu : [
{
label : ` About ${ productName } ` ,
role : "about" ,
} ,
{ type : "separator" } ,
{ role : "services" } ,
{ type : "separator" } ,
{
label : ` Hide ${ productName } ` ,
role : "hide" ,
} ,
{ role : "hideOthers" } ,
{ role : "unhide" } ,
{ type : "separator" } ,
{
label : ` Quit ${ productName } ` ,
role : "quit" ,
} ,
] ,
2022-05-26 15:18:53 +08:00
} ,
{
2023-02-11 23:42:51 +08:00
role : "editMenu" ,
submenu : [
{ role : "cut" } ,
{ role : "copy" } ,
{ role : "paste" } ,
{ role : "pasteAndMatchStyle" , accelerator : "CmdOrCtrl+Shift+C" } ,
{ role : "selectAll" } ,
] ,
2022-05-26 15:18:53 +08:00
} ,
{
2023-02-11 23:42:51 +08:00
role : "windowMenu" ,
submenu : [
{ role : "minimize" } ,
{ role : "zoom" } ,
{ role : "togglefullscreen" } ,
{ type : "separator" } ,
{ role : "toggledevtools" } ,
{ type : "separator" } ,
{ role : "front" } ,
] ,
2022-05-26 15:18:53 +08:00
} ,
2023-02-11 23:42:51 +08:00
] ;
const menu = Menu . buildFromTemplate ( template ) ;
Menu . setApplicationMenu ( menu ) ;
// 当前页面链接使用浏览器打开
currentWindow . webContents . on ( "will-navigate" , ( event , url ) => {
const currentURL = new URL ( event . sender . getURL ( ) ) ;
if ( url . startsWith ( getServer ( currentURL . port ) ) ) {
return ;
}
2022-10-24 12:17:54 +08:00
2023-02-11 23:42:51 +08:00
event . preventDefault ( ) ;
shell . openExternal ( url ) ;
} ) ;
2022-05-26 15:18:53 +08:00
2023-02-11 23:42:51 +08:00
currentWindow . on ( "close" , ( event ) => {
if ( currentWindow && ! currentWindow . isDestroyed ( ) ) {
currentWindow . webContents . send ( "siyuan-save-close" , false ) ;
}
event . preventDefault ( ) ;
} ) ;
workspaces . push ( {
browserWindow : currentWindow ,
id : currentWindow . id ,
} ) ;
} ;
2022-12-13 00:00:49 +08:00
2023-01-13 23:01:16 +08:00
const showWindow = ( wnd ) => {
2023-02-11 23:42:51 +08:00
if ( ! wnd || wnd . isDestroyed ( ) ) {
return ;
}
2023-01-13 23:01:16 +08:00
2023-02-11 23:42:51 +08:00
if ( wnd . isMinimized ( ) ) {
wnd . restore ( ) ;
}
if ( ! wnd . isVisible ( ) ) {
wnd . show ( ) ;
}
wnd . focus ( ) ;
} ;
2023-01-13 23:01:16 +08:00
2023-01-17 00:02:15 +08:00
const initKernel = ( workspace , port , lang ) => {
2023-02-11 23:42:51 +08:00
return new Promise ( async ( resolve ) => {
bootWindow = new BrowserWindow ( {
width : screen . getPrimaryDisplay ( ) . size . width / 2 ,
height : screen . getPrimaryDisplay ( ) . workAreaSize . height / 2 ,
frame : false ,
icon : path . join ( appDir , "stage" , "icon-large.png" ) ,
transparent : "linux" !== process . platform ,
} ) ;
2022-05-26 15:18:53 +08:00
2023-02-11 23:42:51 +08:00
const kernelName = "win32" === process . platform
? "SiYuan-Kernel.exe"
: "SiYuan-Kernel" ;
const kernelPath = path . join ( appDir , "kernel" , kernelName ) ;
if ( ! fs . existsSync ( kernelPath ) ) {
showErrorWindow ( "⚠️ 内核文件丢失 Kernel is missing" ,
"<div>内核可执行文件丢失,请重新安装思源,并将思源加入杀毒软件信任列表。</div><div>The kernel binary is not found, please reinstall SiYuan and add SiYuan into the trust list of your antivirus software.</div>" ) ;
bootWindow . destroy ( ) ;
resolve ( false ) ;
return ;
}
2023-01-06 10:08:29 +08:00
2023-02-11 23:42:51 +08:00
if ( ! isDevEnv || workspaces . length > 0 ) {
if ( port && "" !== port ) {
kernelPort = port ;
} else {
const getAvailablePort = ( ) => {
// https://gist.github.com/mikeal/1840641
return new Promise ( ( portResolve , portReject ) => {
const server = net . createServer ( ) ;
server . on ( "error" , error => {
writeLog ( error ) ;
kernelPort = "" ;
portReject ( ) ;
} ) ;
server . listen ( 0 , ( ) => {
kernelPort = server . address ( ) . port ;
server . close ( ( ) => portResolve ( kernelPort ) ) ;
} ) ;
} ) ;
} ;
await getAvailablePort ( ) ;
}
2023-01-17 00:02:15 +08:00
}
2023-02-11 23:42:51 +08:00
writeLog ( "got kernel port [" + kernelPort + "]" ) ;
if ( ! kernelPort ) {
bootWindow . destroy ( ) ;
resolve ( false ) ;
return ;
}
const cmds = [ "--port" , kernelPort , "--wd" , appDir ] ;
if ( isDevEnv && workspaces . length === 0 ) {
cmds . push ( "--mode" , "dev" ) ;
}
if ( workspace && "" !== workspace ) {
cmds . push ( "--workspace" , workspace ) ;
}
if ( port && "" !== port ) {
cmds . push ( "--port" , port ) ;
}
if ( lang && "" !== lang ) {
cmds . push ( "--lang" , lang ) ;
}
let cmd = ` ui version [ ${ appVer } ], booting kernel [ ${ kernelPath } ${ cmds . join (
" " ) } ] ` ;
writeLog ( cmd ) ;
if ( ! isDevEnv || workspaces . length > 0 ) {
const cp = require ( "child_process" ) ;
const kernelProcess = cp . spawn ( kernelPath ,
cmds , {
detached : false , // 桌面端内核进程不再以游离模式拉起 https://github.com/siyuan-note/siyuan/issues/6336
stdio : "ignore" ,
} ,
) ;
2023-03-19 10:24:05 +08:00
const currentKernelPid = kernelProcess . pid ;
const currentKernelPort = kernelPort ;
writeLog ( "booted kernel process [pid=" + currentKernelPid + ", port=" + currentKernelPort + "]" ) ;
2022-10-25 11:15:09 +08:00
2023-02-11 23:42:51 +08:00
kernelProcess . on ( "close" , ( code ) => {
2023-03-19 10:24:05 +08:00
writeLog ( ` kernel [pid= ${ currentKernelPid } , port= ${ currentKernelPort } ] exited with code [ ${ code } ] ` ) ;
2023-02-11 23:42:51 +08:00
if ( 0 !== code ) {
switch ( code ) {
case 20 :
showErrorWindow ( "⚠️ 数据库被锁定 The database is locked" ,
"<div>数据库文件正在被其他进程占用, 请检查是否同时存在多个内核进程( SiYuan Kernel) 服务相同的工作空间。</div><div>The database file is being occupied by other processes, please check whether there are multiple kernel processes (SiYuan Kernel) serving the same workspace at the same time.</div>" ) ;
break ;
case 21 :
2023-03-19 10:26:30 +08:00
showErrorWindow ( "⚠️ 监听端口 " + kernelPort + " 失败 Failed to listen to port " + kernelPort ,
"<div>监听 " + kernelPort + " 端口失败,请确保程序拥有网络权限并不受防火墙和杀毒软件阻止。</div><div>Failed to listen to port " + kernelPort + ", please make sure the program has network permissions and is not blocked by firewalls and antivirus software.</div>" ) ;
2023-02-11 23:42:51 +08:00
break ;
case 22 :
2023-03-19 10:26:30 +08:00
showErrorWindow ( "⚠️ 创建配置目录失败 Failed to create config directory" ,
2023-02-11 23:42:51 +08:00
"<div>思源需要在用户家目录下创建配置文件夹(~/.config/siyuan) , 请确保该路径具有写入权限。</div><div>SiYuan needs to create a configuration folder (~/.config/siyuan) in the user\'s home directory. Please make sure that the path has write permissions.</div>" ) ;
break ;
case 24 : // 工作空间已被锁定,尝试切换到第一个打开的工作空间
if ( workspaces && 0 < workspaces . length ) {
showWindow ( workspaces [ 0 ] . browserWindow ) ;
}
2023-01-13 23:01:16 +08:00
2023-03-19 10:26:30 +08:00
showErrorWindow ( "⚠️ 工作空间已被锁定 The workspace is locked" ,
2023-02-11 23:42:51 +08:00
"<div>该工作空间正在被使用。</div><div>The workspace is in use.</div>" ) ;
break ;
case 25 :
2023-03-19 10:26:30 +08:00
showErrorWindow ( "⚠️ 创建工作空间目录失败 Failed to create workspace directory" ,
2023-02-11 23:42:51 +08:00
"<div>创建工作空间目录失败。</div><div>Failed to create workspace directory.</div>" ) ;
break ;
2023-03-16 22:55:32 +08:00
case 26 :
2023-03-19 10:26:30 +08:00
showErrorWindow ( "⚠️ 文件系统读写错误 File system access error" ,
2023-03-19 10:27:22 +08:00
"<div>请检查文件系统权限,并确保没有其他程序正在读写文件;<br>请勿使用第三方同步盘进行数据同步, 否则数据会被损坏( iCloud/OneDrive/Dropbox/Google Drive/坚果云/百度网盘/腾讯微云等)</div><div>Please check file system permissions and make sure no other programs are reading or writing to the file;<br>Do not use a third-party sync disk for data sync, otherwise the data will be damaged (OneDrive/Dropbox/Google Drive/Nutstore/Baidu Netdisk/Tencent Weiyun, etc.)</div>" ) ;
2023-03-16 22:55:32 +08:00
break ;
2023-02-11 23:42:51 +08:00
case 0 :
case 1 : // Fatal error
break ;
default :
2023-03-19 10:26:30 +08:00
showErrorWindow ( "⚠️ 内核因未知原因退出 The kernel exited for unknown reasons" ,
2023-02-11 23:42:51 +08:00
` <div>思源内核因未知原因退出 [code= ${ code } ],请尝试重启操作系统后再启动思源。如果该问题依然发生,请检查杀毒软件是否阻止思源内核启动。</div>
< div > SiYuan Kernel exited for unknown reasons [ code = $ { code } ] , please try to reboot your operating system and then start SiYuan again . If occurs this problem still , please check your anti - virus software whether kill the SiYuan Kernel . < / d i v > ` ) ;
break ;
}
2023-03-19 09:59:16 +08:00
2023-03-19 10:24:05 +08:00
exitApp ( "port" , currentKernelPort ) ;
2023-02-11 23:42:51 +08:00
bootWindow . destroy ( ) ;
resolve ( false ) ;
}
} ) ;
2022-10-25 11:15:09 +08:00
}
2022-05-26 15:18:53 +08:00
2023-02-11 23:42:51 +08:00
let gotVersion = false ;
let apiData ;
let count = 0 ;
writeLog ( "checking kernel version" ) ;
while ( ! gotVersion && count < 15 ) {
try {
const apiResult = await fetch ( getServer ( ) + "/api/system/version" ) ;
apiData = await apiResult . json ( ) ;
gotVersion = true ;
bootWindow . setResizable ( false ) ;
bootWindow . loadURL ( getServer ( ) + "/appearance/boot/index.html" ) ;
bootWindow . show ( ) ;
} catch ( e ) {
writeLog ( "get kernel version failed: " + e . message ) ;
await sleep ( 100 ) ;
} finally {
count ++ ;
if ( 14 < count ) {
writeLog ( "get kernel ver failed" ) ;
2023-03-19 10:26:30 +08:00
showErrorWindow ( "⚠️ 获取内核服务端口失败 Failed to get kernel serve port" ,
2023-02-11 23:42:51 +08:00
"<div>获取内核服务端口失败,请确保程序拥有网络权限并不受防火墙和杀毒软件阻止。</div><div>Failed to get kernel serve port, please make sure the program has network permissions and is not blocked by firewalls and antivirus software.</div>" ) ;
bootWindow . destroy ( ) ;
resolve ( false ) ;
}
}
}
2022-12-20 19:40:01 +08:00
2023-02-11 23:42:51 +08:00
if ( ! gotVersion ) {
return ;
2022-05-26 15:18:53 +08:00
}
2023-02-11 23:42:51 +08:00
if ( 0 === apiData . code ) {
writeLog ( "got kernel version [" + apiData . data + "]" ) ;
if ( ! isDevEnv && apiData . data !== appVer ) {
writeLog (
` kernel [ ${ apiData . data } ] is running, shutdown it now and then start kernel [ ${ appVer } ] ` ) ;
fetch ( getServer ( ) + "/api/system/exit" , { method : "POST" } ) ;
bootWindow . destroy ( ) ;
resolve ( false ) ;
2022-05-26 15:18:53 +08:00
} else {
2023-02-11 23:42:51 +08:00
let progressing = false ;
while ( ! progressing ) {
try {
2023-03-19 10:26:30 +08:00
const progressResult = await fetch ( getServer ( ) + "/api/system/bootProgress" ) ;
2023-02-11 23:42:51 +08:00
const progressData = await progressResult . json ( ) ;
if ( progressData . data . progress >= 100 ) {
resolve ( true ) ;
progressing = true ;
} else {
await sleep ( 100 ) ;
}
} catch ( e ) {
writeLog ( "get boot progress failed: " + e . message ) ;
fetch ( getServer ( ) + "/api/system/exit" , { method : "POST" } ) ;
bootWindow . destroy ( ) ;
resolve ( false ) ;
progressing = true ;
}
}
2022-05-26 15:18:53 +08:00
}
2023-02-11 23:42:51 +08:00
} else {
writeLog ( ` get kernel version failed: ${ apiData . code } , ${ apiData . msg } ` ) ;
resolve ( false ) ;
2022-05-26 15:18:53 +08:00
}
2023-02-11 23:42:51 +08:00
} ) ;
} ;
2022-05-26 15:18:53 +08:00
2023-02-11 23:42:51 +08:00
app . setAsDefaultProtocolClient ( "siyuan" ) ;
2022-10-30 13:53:38 +08:00
2023-02-11 23:42:51 +08:00
app . commandLine . appendSwitch ( "disable-web-security" ) ;
app . commandLine . appendSwitch ( "auto-detect" , "false" ) ;
app . commandLine . appendSwitch ( "no-proxy-server" ) ;
app . commandLine . appendSwitch ( "enable-features" , "PlatformHEVCDecoderSupport" ) ;
2022-10-30 13:53:38 +08:00
2023-02-11 23:42:51 +08:00
app . setPath ( "userData" , app . getPath ( "userData" ) + "-Electron" ) ; // `~/.config` 下 Electron 相关文件夹名称改为 `SiYuan-Electron` https://github.com/siyuan-note/siyuan/issues/3349
2022-05-26 15:18:53 +08:00
app . whenReady ( ) . then ( ( ) => {
2023-02-11 23:42:51 +08:00
const resetTrayMenu = ( tray , lang , mainWindow ) => {
const trayMenuTemplate = [
{
2023-03-19 10:26:30 +08:00
label : mainWindow . isVisible ( ) ? lang . hideWindow : lang . showWindow ,
2023-02-11 23:42:51 +08:00
click : ( ) => {
showHideWindow ( tray , lang , mainWindow ) ;
} ,
} ,
{
label : lang . officialWebsite ,
click : ( ) => {
shell . openExternal ( "https://b3log.org/siyuan/" ) ;
} ,
} ,
{
label : lang . openSource ,
click : ( ) => {
shell . openExternal ( "https://github.com/siyuan-note/siyuan" ) ;
} ,
} ,
{
label : lang . resetWindow ,
type : "checkbox" ,
click : v => {
resetWindowStateOnRestart = v . checked ;
mainWindow . webContents . send ( "siyuan-save-close" , true ) ;
} ,
} ,
{
label : lang . quit ,
click : ( ) => {
mainWindow . webContents . send ( "siyuan-save-close" , true ) ;
} ,
} ,
] ;
2023-01-04 23:03:08 +08:00
2023-02-11 23:42:51 +08:00
if ( "win32" === process . platform ) {
// Windows 端支持窗口置顶 https://github.com/siyuan-note/siyuan/issues/6860
trayMenuTemplate . splice ( 1 , 0 , {
2023-03-19 10:26:30 +08:00
label : mainWindow . isAlwaysOnTop ( ) ? lang . cancelWindowTop : lang . setWindowTop ,
2023-02-11 23:42:51 +08:00
click : ( ) => {
if ( ! mainWindow . isAlwaysOnTop ( ) ) {
mainWindow . setAlwaysOnTop ( true ) ;
} else {
mainWindow . setAlwaysOnTop ( false ) ;
}
resetTrayMenu ( tray , lang , mainWindow ) ;
} ,
} ) ;
}
const contextMenu = Menu . buildFromTemplate ( trayMenuTemplate ) ;
tray . setContextMenu ( contextMenu ) ;
} ;
2023-01-13 23:01:16 +08:00
2023-02-11 23:42:51 +08:00
const hideWindow = ( wnd ) => {
// 通过 `Alt+M` 最小化后焦点回到先前的窗口 https://github.com/siyuan-note/siyuan/issues/7275
wnd . minimize ( ) ;
wnd . hide ( ) ;
} ;
2023-02-07 17:35:53 +08:00
2023-02-11 23:42:51 +08:00
const showHideWindow = ( tray , lang , mainWindow ) => {
if ( ! mainWindow . isVisible ( ) ) {
if ( mainWindow . isMinimized ( ) ) {
mainWindow . restore ( ) ;
}
mainWindow . show ( ) ;
} else {
hideWindow ( mainWindow ) ;
}
2023-01-04 23:03:08 +08:00
2023-02-11 23:42:51 +08:00
resetTrayMenu ( tray , lang , mainWindow ) ;
} ;
2023-01-04 23:03:08 +08:00
2023-02-11 23:42:51 +08:00
ipcMain . on ( "siyuan-first-quit" , ( ) => {
app . exit ( ) ;
} ) ;
ipcMain . on ( "siyuan-show" , ( event , id ) => {
showWindow ( BrowserWindow . fromId ( id ) ) ;
} ) ;
ipcMain . on ( "siyuan-config-tray" , ( event , data ) => {
workspaces . find ( item => {
if ( item . id === data . id ) {
hideWindow ( item . browserWindow ) ;
if ( "win32" === process . platform || "linux" === process . platform ) {
resetTrayMenu ( item . tray , data . languages , item . browserWindow ) ;
}
return true ;
}
} ) ;
} ) ;
ipcMain . on ( "siyuan-export-pdf" , ( event , data ) => {
BrowserWindow . fromId ( data . id ) . webContents . send ( "siyuan-export-pdf" , data ) ;
} ) ;
ipcMain . on ( "siyuan-export-close" , ( event , id ) => {
BrowserWindow . fromId ( id ) . webContents . send ( "siyuan-export-close" , id ) ;
} ) ;
ipcMain . on ( "siyuan-export-prevent" , ( event , id ) => {
BrowserWindow . fromId ( id ) . webContents . on ( "will-navigate" , ( event , url ) => {
const currentURL = new URL ( event . sender . getURL ( ) ) ;
2023-03-05 14:55:53 +08:00
event . preventDefault ( ) ;
2023-02-11 23:42:51 +08:00
if ( url . startsWith ( getServer ( currentURL . port ) ) ) {
return ;
}
shell . openExternal ( url ) ;
} ) ;
} ) ;
ipcMain . on ( "siyuan-quit" , ( event , id ) => {
2023-03-18 23:38:27 +08:00
exitApp ( "id" , id )
2023-02-11 23:42:51 +08:00
} ) ;
ipcMain . on ( "siyuan-openwindow" , ( event , data ) => {
2023-02-26 10:19:47 +08:00
const mainWindow = BrowserWindow . fromId ( data . id ) ;
const mainBounds = mainWindow . getBounds ( ) ;
const mainScreen = screen . getDisplayNearestPoint ( { x : mainBounds . x , y : mainBounds . y } ) ;
2023-02-11 23:42:51 +08:00
const win = new BrowserWindow ( {
show : true ,
backgroundColor : "#FFF" ,
trafficLightPosition : { x : 8 , y : 13 } ,
2023-02-26 10:19:47 +08:00
width : mainScreen . size . width * 0.7 ,
height : mainScreen . size . height * 0.9 ,
2023-02-11 23:42:51 +08:00
minWidth : 493 ,
2023-02-26 10:25:39 +08:00
center : true ,
2023-02-11 23:42:51 +08:00
minHeight : 376 ,
fullscreenable : true ,
frame : "darwin" === process . platform ,
icon : path . join ( appDir , "stage" , "icon-large.png" ) ,
titleBarStyle : "hidden" ,
webPreferences : {
contextIsolation : false ,
nodeIntegration : true ,
webviewTag : true ,
webSecurity : false ,
2023-03-07 15:06:50 +08:00
autoplayPolicy : "user-gesture-required" // 桌面端禁止自动播放多媒体 https://github.com/siyuan-note/siyuan/issues/7587
2023-02-11 23:42:51 +08:00
} ,
} ) ;
2023-02-26 10:19:47 +08:00
win . loadURL ( data . url ) ;
2023-02-26 11:45:50 +08:00
const targetScreen = screen . getDisplayNearestPoint ( screen . getCursorScreenPoint ( ) ) ;
2023-02-26 10:19:47 +08:00
if ( mainScreen . id !== targetScreen . id ) {
2023-02-26 11:45:50 +08:00
win . setBounds ( targetScreen . workArea ) ;
2023-02-26 10:19:47 +08:00
}
2023-02-11 23:42:51 +08:00
require ( "@electron/remote/main" ) . enable ( win . webContents ) ;
} ) ;
ipcMain . on ( "siyuan-open-workspace" , ( event , data ) => {
const foundWorkspace = workspaces . find ( ( item ) => {
if ( item . workspaceDir === data . workspace ) {
showWindow ( item . browserWindow ) ;
return true ;
}
} ) ;
if ( ! foundWorkspace ) {
initKernel ( data . workspace , "" , data . lang ) . then ( ( isSucc ) => {
if ( isSucc ) {
boot ( ) ;
}
} ) ;
2023-01-05 11:48:03 +08:00
}
2023-02-11 23:42:51 +08:00
} ) ;
ipcMain . on ( "siyuan-init" , async ( event , data ) => {
const exitWS = workspaces . find ( item => {
if ( data . id === item . id && item . workspaceDir ) {
return true ;
2023-02-05 10:36:11 +08:00
}
2023-02-11 23:42:51 +08:00
} ) ;
if ( exitWS ) {
return ;
}
let tray ;
if ( "win32" === process . platform || "linux" === process . platform ) {
// 系统托盘
tray = new Tray ( path . join ( appDir , "stage" , "icon-large.png" ) ) ;
tray . setToolTip ( ` ${ path . basename ( data . workspaceDir ) } - SiYuan v ${ appVer } ` ) ;
const mainWindow = BrowserWindow . fromId ( data . id ) ;
resetTrayMenu ( tray , data . languages , mainWindow ) ;
tray . on ( "click" , ( ) => {
showHideWindow ( tray , data . languages , mainWindow ) ;
} ) ;
2023-01-05 22:49:53 +08:00
}
2023-02-11 23:42:51 +08:00
workspaces . find ( item => {
if ( data . id === item . id ) {
item . workspaceDir = data . workspaceDir ;
item . tray = tray ;
return true ;
}
} ) ;
await fetch ( getServer ( data . port ) + "/api/system/uiproc?pid=" + process . pid ,
{ method : "POST" } ) ;
} ) ;
ipcMain . on ( "siyuan-hotkey" , ( event , data ) => {
globalShortcut . unregisterAll ( ) ;
if ( ! data . hotkey ) {
return ;
}
globalShortcut . register ( data . hotkey , ( ) => {
workspaces . forEach ( item => {
const mainWindow = item . browserWindow ;
if ( mainWindow . isMinimized ( ) ) {
mainWindow . restore ( ) ;
if ( ! mainWindow . isVisible ( ) ) {
mainWindow . show ( ) ;
}
} else {
if ( mainWindow . isVisible ( ) ) {
if ( 1 === workspaces . length ) { // 改进 `Alt+M` 激活窗口 https://github.com/siyuan-note/siyuan/issues/7258
if ( ! mainWindow . isFocused ( ) ) {
mainWindow . show ( ) ;
} else {
hideWindow ( mainWindow ) ;
}
} else {
hideWindow ( mainWindow ) ;
}
} else {
mainWindow . show ( ) ;
}
}
2023-02-05 10:36:11 +08:00
2023-02-11 23:42:51 +08:00
if ( "win32" === process . platform || "linux" === process . platform ) {
resetTrayMenu ( item . tray , data . languages , mainWindow ) ;
}
} ) ;
} ) ;
} ) ;
2023-02-27 10:58:07 +08:00
ipcMain . on ( "siyuan-send_windows" , ( event , data ) => {
2023-02-11 23:42:51 +08:00
BrowserWindow . getAllWindows ( ) . forEach ( item => {
2023-02-27 10:58:07 +08:00
item . webContents . send ( "siyuan-send_windows" , data ) ;
2023-02-27 09:54:36 +08:00
} ) ;
} ) ;
2023-02-11 23:42:51 +08:00
if ( firstOpen ) {
const firstOpenWindow = new BrowserWindow ( {
width : screen . getPrimaryDisplay ( ) . size . width * 0.6 ,
height : screen . getPrimaryDisplay ( ) . workAreaSize . height * 0.8 ,
frame : false ,
icon : path . join ( appDir , "stage" , "icon-large.png" ) ,
transparent : "linux" !== process . platform ,
webPreferences : {
nodeIntegration : true ,
webviewTag : true ,
webSecurity : false ,
contextIsolation : false ,
} ,
} ) ;
require ( "@electron/remote/main" ) . enable ( firstOpenWindow . webContents ) ;
let initHTMLPath = path . join ( appDir , "app" , "electron" , "init.html" ) ;
if ( isDevEnv ) {
initHTMLPath = path . join ( appDir , "electron" , "init.html" ) ;
2023-01-05 22:49:53 +08:00
}
2023-01-12 15:53:57 +08:00
2023-02-11 23:42:51 +08:00
// 改进桌面端初始化时使用的外观语言 https://github.com/siyuan-note/siyuan/issues/6803
let languages = app . getPreferredSystemLanguages ( ) ;
let language = languages && 0 < languages . length && "zh-Hans-CN" ===
languages [ 0 ] ? "zh_CN" : "en_US" ;
firstOpenWindow . loadFile (
initHTMLPath , {
query : {
lang : language ,
home : app . getPath ( "home" ) ,
v : appVer ,
icon : path . join ( appDir , "stage" , "icon-large.png" ) ,
} ,
} ) ;
firstOpenWindow . show ( ) ;
// 初始化启动
ipcMain . on ( "siyuan-first-init" , ( event , data ) => {
initKernel ( data . workspace , "" , data . lang ) . then ( ( isSucc ) => {
if ( isSucc ) {
boot ( ) ;
}
} ) ;
firstOpenWindow . destroy ( ) ;
} ) ;
} else {
const getArg = ( name ) => {
for ( let i = 0 ; i < process . argv . length ; i ++ ) {
if ( process . argv [ i ] . startsWith ( name ) ) {
return process . argv [ i ] . split ( "=" ) [ 1 ] ;
}
}
} ;
2022-12-07 11:01:29 +08:00
2023-02-11 23:42:51 +08:00
const workspace = getArg ( "--workspace" ) ;
if ( workspace ) {
writeLog ( "got arg [--workspace=" + workspace + "]" ) ;
2022-05-26 15:18:53 +08:00
}
2023-02-11 23:42:51 +08:00
const port = getArg ( "--port" ) ;
if ( port ) {
writeLog ( "got arg [--port=" + port + "]" ) ;
2023-01-06 09:49:31 +08:00
}
2023-02-11 23:42:51 +08:00
initKernel ( workspace , port , "" ) . then ( ( isSucc ) => {
if ( isSucc ) {
boot ( ) ;
}
} ) ;
}
} ) ;
app . on ( "open-url" , ( event , url ) => { // for macOS
if ( url . startsWith ( "siyuan://" ) ) {
workspaces . forEach ( item => {
if ( item . browserWindow && ! item . browserWindow . isDestroyed ( ) ) {
item . browserWindow . webContents . send ( "siyuan-openurl" , url ) ;
}
} ) ;
2023-01-06 09:49:31 +08:00
}
2023-02-11 23:42:51 +08:00
} ) ;
2023-01-06 09:49:31 +08:00
2023-02-11 23:42:51 +08:00
app . on ( "second-instance" , ( event , argv ) => {
writeLog ( "second-instance [" + argv + "]" ) ;
let workspace = argv . find ( ( arg ) => arg . startsWith ( "--workspace=" ) ) ;
2023-01-06 09:49:31 +08:00
if ( workspace ) {
2023-02-11 23:42:51 +08:00
workspace = workspace . split ( "=" ) [ 1 ] ;
writeLog ( "got second-instance arg [--workspace=" + workspace + "]" ) ;
2023-01-06 09:49:31 +08:00
}
2023-02-11 23:42:51 +08:00
let port = argv . find ( ( arg ) => arg . startsWith ( "--port=" ) ) ;
2023-01-17 00:02:15 +08:00
if ( port ) {
2023-02-11 23:42:51 +08:00
port = port . split ( "=" ) [ 1 ] ;
writeLog ( "got second-instance arg [--port=" + port + "]" ) ;
} else {
port = 0 ;
}
const foundWorkspace = workspaces . find ( item => {
if ( item . browserWindow && ! item . browserWindow . isDestroyed ( ) ) {
if ( workspace && workspace === item . workspaceDir ) {
showWindow ( item . browserWindow ) ;
return true ;
}
}
} ) ;
if ( foundWorkspace ) {
return ;
}
if ( workspace ) {
initKernel ( workspace , port , "" ) . then ( ( isSucc ) => {
if ( isSucc ) {
boot ( ) ;
}
} ) ;
return ;
2023-01-17 00:02:15 +08:00
}
2022-05-26 15:18:53 +08:00
2023-02-11 23:42:51 +08:00
const siyuanURL = argv . find ( ( arg ) => arg . startsWith ( "siyuan://" ) ) ;
2023-01-04 23:03:08 +08:00
workspaces . forEach ( item => {
2023-02-11 23:42:51 +08:00
if ( item . browserWindow && ! item . browserWindow . isDestroyed ( ) && siyuanURL ) {
item . browserWindow . webContents . send ( "siyuan-openurl" , siyuanURL ) ;
}
} ) ;
2022-05-26 15:18:53 +08:00
2023-02-11 23:42:51 +08:00
if ( ! siyuanURL && 0 < workspaces . length ) {
showWindow ( workspaces [ 0 ] . browserWindow ) ;
2022-05-26 15:18:53 +08:00
}
2023-02-11 23:42:51 +08:00
} ) ;
2023-01-14 11:13:02 +08:00
2023-02-11 23:42:51 +08:00
app . on ( "activate" , ( ) => {
if ( workspaces . length > 0 ) {
const mainWindow = workspaces [ 0 ] . browserWindow ;
if ( mainWindow && ! mainWindow . isDestroyed ( ) ) {
mainWindow . show ( ) ;
}
2023-01-14 10:48:22 +08:00
}
2023-02-11 23:42:51 +08:00
if ( BrowserWindow . getAllWindows ( ) . length === 0 ) {
boot ( ) ;
2023-01-05 09:44:18 +08:00
}
2023-02-11 23:42:51 +08:00
} ) ;
2022-05-26 15:18:53 +08:00
// 在编辑器内打开链接的处理,比如 iframe 上的打开链接。
2023-02-11 23:42:51 +08:00
app . on ( "web-contents-created" , ( webContentsCreatedEvent , contents ) => {
contents . setWindowOpenHandler ( ( details ) => {
shell . openExternal ( details . url ) ;
return { action : "deny" } ;
} ) ;
} ) ;
2022-05-26 15:18:53 +08:00
2023-02-11 23:42:51 +08:00
app . on ( "before-quit" , ( event ) => {
workspaces . forEach ( item => {
if ( item . browserWindow && ! item . browserWindow . isDestroyed ( ) ) {
event . preventDefault ( ) ;
item . browserWindow . webContents . send ( "siyuan-save-close" , true ) ;
}
} ) ;
} ) ;
2022-05-26 15:18:53 +08:00
2023-02-11 23:42:51 +08:00
const { powerMonitor } = require ( "electron" ) ;
2022-05-26 15:18:53 +08:00
2023-02-11 23:42:51 +08:00
powerMonitor . on ( "suspend" , ( ) => {
writeLog ( "system suspend" ) ;
} ) ;
2022-05-26 15:18:53 +08:00
2023-02-11 23:42:51 +08:00
powerMonitor . on ( "resume" , async ( ) => {
// 桌面端系统休眠唤醒后判断网络连通性后再执行数据同步 https://github.com/siyuan-note/siyuan/issues/6687
writeLog ( "system resume" ) ;
const isOnline = async ( ) => {
try {
const result = await fetch ( "https://www.baidu.com" , { timeout : 1000 } ) ;
return 200 === result . status ;
} catch ( e ) {
try {
const result = await fetch ( "https://icanhazip.com" , { timeout : 1000 } ) ;
return 200 === result . status ;
} catch ( e ) {
return false ;
}
}
} ;
let online = false ;
for ( let i = 0 ; i < 7 ; i ++ ) {
if ( await isOnline ( ) ) {
online = true ;
break ;
}
2022-11-23 20:41:14 +08:00
2023-02-11 23:42:51 +08:00
writeLog ( "network is offline" ) ;
await sleep ( 1000 ) ;
}
2022-11-23 20:41:14 +08:00
2023-02-11 23:42:51 +08:00
if ( ! online ) {
writeLog ( "network is offline, do not sync after system resume" ) ;
return ;
}
2022-11-23 20:41:14 +08:00
2023-02-11 23:42:51 +08:00
workspaces . forEach ( item => {
const currentURL = new URL ( item . browserWindow . getURL ( ) ) ;
const server = getServer ( currentURL . port ) ;
writeLog (
"sync after system resume [" + server + "/api/sync/performSync" + "]" ) ;
fetch ( server + "/api/sync/performSync" , { method : "POST" } ) ;
} ) ;
} ) ;
2022-05-26 15:18:53 +08:00
2023-02-11 23:42:51 +08:00
powerMonitor . on ( "shutdown" , ( ) => {
writeLog ( "system shutdown" ) ;
workspaces . forEach ( item => {
const currentURL = new URL ( item . browserWindow . getURL ( ) ) ;
fetch ( getServer ( currentURL . port ) + "/api/system/exit" , { method : "POST" } ) ;
} ) ;
} ) ;