Jelajahi Sumber

:sparkles: https://github.com/siyuan-note/siyuan/issues/4567

Vanessa 2 tahun lalu
induk
melakukan
910a600d5d

+ 4 - 2
app/electron/init.html

@@ -358,8 +358,10 @@
           if (!fs.existsSync(initPath)) {
             fs.mkdirSync(initPath, {mode: 0o755, recursive: true})
           }
-          const lang = document.querySelector('.lang').value
-          ipcRenderer.send('siyuan-first-init', `${initPath}-${lang}`)
+          ipcRenderer.send('siyuan-first-init', {
+            workspace: initPath,
+            lang: document.querySelector('.lang').value
+          })
         }
       })
     })

+ 346 - 359
app/electron/main.js

@@ -27,7 +27,7 @@ const {
 } = require('electron')
 const path = require('path')
 const fs = require('fs')
-const net = require("net");
+const net = require('net')
 const fetch = require('electron-fetch').default
 process.noAsar = true
 const appDir = path.dirname(app.getAppPath())
@@ -35,14 +35,13 @@ 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 tray // 托盘必须使用全局变量,以防止被垃圾回收 https://www.electronjs.org/docs/faq#my-apps-windowtray-disappeared-after-a-few-minutes
-let mainWindow // 从托盘处激活报错 https://github.com/siyuan-note/siyuan/issues/769
 let firstOpenWindow, bootWindow
-let closeButtonBehavior = 0
 let siyuanOpenURL
 let firstOpen = false
 let resetWindowStateOnRestart = false
-const localhost = "127.0.0.1"
+let workspaces = []
+const localhost = '127.0.0.1'
+let kernelPort = 6806
 require('@electron/remote/main').initialize()
 
 if (!app.requestSingleInstanceLock()) {
@@ -50,6 +49,28 @@ if (!app.requestSingleInstanceLock()) {
   return
 }
 
+try {
+  firstOpen = !fs.existsSync(path.join(confDir, 'workspace.json'))
+  if (!fs.existsSync(confDir)) {
+    fs.mkdirSync(confDir, {mode: 0o755, recursive: true})
+  }
+} catch (e) {
+  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()
+}
+
+const getServer = () => {
+  return 'http://' + localhost + ':' + kernelPort
+}
+
+const sleep = (ms) => {
+  return new Promise(resolve => setTimeout(resolve, ms))
+}
+
 const showErrorWindow = (title, content) => {
   let errorHTMLPath = path.join(appDir, 'app', 'electron', 'error.html')
   if (isDevEnv) {
@@ -81,18 +102,6 @@ const showErrorWindow = (title, content) => {
   errWindow.show()
 }
 
-try {
-  firstOpen = !fs.existsSync(path.join(confDir, 'workspace.json'))
-  if (!fs.existsSync(confDir)) {
-    fs.mkdirSync(confDir, {mode: 0o755, recursive: true})
-  }
-} catch (e) {
-  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()
-}
-
 const writeLog = (out) => {
   console.log(out)
   const logFile = path.join(confDir, 'app.log')
@@ -173,7 +182,7 @@ const boot = () => {
   }
 
   // 创建主窗体
-  mainWindow = new BrowserWindow({
+  const currentWindow = new BrowserWindow({
     show: false,
     backgroundColor: '#FFF', // 桌面端主窗体背景色设置为 `#FFF` Fix https://github.com/siyuan-note/siyuan/issues/4544
     width: windowState.width,
@@ -196,15 +205,14 @@ const boot = () => {
     titleBarStyle: 'hidden',
     icon: path.join(appDir, 'stage', 'icon-large.png'),
   })
-
-  require('@electron/remote/main').enable(mainWindow.webContents)
-  mainWindow.webContents.userAgent = 'SiYuan/' + appVer +
+  require('@electron/remote/main').enable(currentWindow.webContents)
+  currentWindow.webContents.userAgent = 'SiYuan/' + appVer +
     ' https://b3log.org/siyuan Electron'
 
-  mainWindow.webContents.session.setSpellCheckerLanguages(['en-US'])
+  currentWindow.webContents.session.setSpellCheckerLanguages(['en-US'])
 
   // 发起互联网服务请求时绕过安全策略 https://github.com/siyuan-note/siyuan/issues/5516
-  mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
+  currentWindow.webContents.session.webRequest.onBeforeSendHeaders(
     (details, cb) => {
       if (-1 < details.url.indexOf('bili')) {
         // B 站不移除 Referer https://github.com/siyuan-note/siyuan/issues/94
@@ -219,50 +227,51 @@ const boot = () => {
       }
       cb({requestHeaders: details.requestHeaders})
     })
-  mainWindow.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]
+  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})
-  })
+      cb({responseHeaders: details.responseHeaders})
+    })
 
-  mainWindow.webContents.on('did-finish-load', () => {
+  currentWindow.webContents.on('did-finish-load', () => {
     if ('win32' === process.platform || 'linux' === process.platform) {
       siyuanOpenURL = process.argv.find((arg) => arg.startsWith('siyuan://'))
     }
     if (siyuanOpenURL) {
-      if (mainWindow.isMinimized()) {
-        mainWindow.restore()
+      if (currentWindow.isMinimized()) {
+        currentWindow.restore()
       }
-      if (!mainWindow.isVisible()) {
-        mainWindow.show()
+      if (!currentWindow.isVisible()) {
+        currentWindow.show()
       }
-      mainWindow.focus()
+      currentWindow.focus()
       setTimeout(() => { // 等待界面js执行完毕
         writeLog(siyuanOpenURL)
-        mainWindow.webContents.send('siyuan-openurl', siyuanOpenURL)
+        currentWindow.webContents.send('siyuan-openurl', siyuanOpenURL)
         siyuanOpenURL = null
       }, 2000)
     }
   })
 
   if (windowState.isDevToolsOpened) {
-    mainWindow.webContents.openDevTools({mode: 'bottom'})
+    currentWindow.webContents.openDevTools({mode: 'bottom'})
   }
 
   // 主界面事件监听
-  mainWindow.once('ready-to-show', () => {
-    mainWindow.show()
+  currentWindow.once('ready-to-show', () => {
+    currentWindow.show()
     if (windowState.isMaximized) {
-      mainWindow.maximize()
+      currentWindow.maximize()
     } else {
-      mainWindow.unmaximize()
+      currentWindow.unmaximize()
     }
     if (bootWindow && !bootWindow.isDestroyed()) {
       bootWindow.destroy()
@@ -270,7 +279,7 @@ const boot = () => {
   })
 
   // 加载主界面
-  mainWindow.loadURL(getServer() + '/stage/build/app/index.html?v=' +
+  currentWindow.loadURL(getServer() + '/stage/build/app/index.html?v=' +
     new Date().getTime())
 
   // 菜单
@@ -333,7 +342,7 @@ const boot = () => {
   const menu = Menu.buildFromTemplate(template)
   Menu.setApplicationMenu(menu)
   // 当前页面链接使用浏览器打开
-  mainWindow.webContents.on('will-navigate', (event, url) => {
+  currentWindow.webContents.on('will-navigate', (event, url) => {
     if (url.startsWith(getServer())) {
       return
     }
@@ -342,218 +351,48 @@ const boot = () => {
     shell.openExternal(url)
   })
 
-  mainWindow.on('close', (event) => {
-    if (mainWindow && !mainWindow.isDestroyed()) {
-      mainWindow.webContents.send('siyuan-save-close', false)
+  currentWindow.on('close', (event) => {
+    if (currentWindow && !currentWindow.isDestroyed()) {
+      currentWindow.webContents.send('siyuan-save-close', false)
     }
     event.preventDefault()
   })
-  // 监听主题切换
-  ipcMain.on('siyuan-config-theme', (event, theme) => {
-    nativeTheme.themeSource = theme
-  })
-  ipcMain.on('siyuan-config-close', (event, close) => {
-    closeButtonBehavior = close
+  workspaces.push({
+    browserWindow: currentWindow,
+    id: currentWindow.id,
   })
-  ipcMain.on('siyuan-config-tray', () => {
-    mainWindow.hide()
-  })
-  ipcMain.on('siyuan-config-closetray', () => {
-    if ('win32' === process.platform) {
-      tray.destroy()
-    }
-  })
-  ipcMain.on('siyuan-export-pdf', (event, data) => {
-    mainWindow.webContents.send('siyuan-export-pdf', data)
-  })
-  ipcMain.on('siyuan-export-close', (event, data) => {
-    mainWindow.webContents.send('siyuan-export-close', data)
-  })
-  ipcMain.on('siyuan-quit', () => {
-    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')
-  })
-
-  let trayMenu = {
-    "showWindow": "Show Window",
-    "hideWindow": "Hide Window",
-    "setWindowTop": "Set Window top",
-    "cancelWindowTop": "Cancel Window top",
-    "officialWebsite": "Visit official website",
-    "openSource": "Visit Project on Github",
-    "resetWindow": "Reset Window on restart",
-    "quit": "Quit application"
-  }
-  ipcMain.on('siyuan-init', async (event, languages) => {
-    trayMenu = languages['_trayMenu'];
-    resetTrayMenu()
-    await fetch(getServer() + '/api/system/uiproc?pid=' + process.pid,
-      {method: 'POST'})
-  })
-
-  const resetTrayMenu = () => {
-    if ('win32' !== process.platform && 'linux' !== process.platform) {
-      return
-    }
-
-    const trayMenuTemplate = buildTrayMenuTemplate()
-    const contextMenu = Menu.buildFromTemplate(trayMenuTemplate)
-    tray.setContextMenu(contextMenu)
-  }
-
-  const buildShowWndMenu = () => {
-    const ret = {
-      label: trayMenu.hideWindow,
-      click: () => {
-        showHideWnd()
-      },
-    }
-
-    if (mainWindow.isVisible()) {
-      ret.label = trayMenu.hideWindow
-    } else {
-      ret.label = trayMenu.showWindow
-    }
-    return ret
-  }
-
-  const showHideWnd = () => {
-    if (!mainWindow.isVisible()) {
-      if (mainWindow.isMinimized()) {
-        mainWindow.restore()
-      }
-      mainWindow.show()
-    } else {
-      mainWindow.hide()
-    }
-
-    resetTrayMenu()
-  }
-
-  const buildSetWndTopMenu = () => {
-    const ret = {
-      label: trayMenu.setWindowTop,
-      click: () => {
-        setCancelWndTop()
-      },
-    }
-    if (mainWindow.isAlwaysOnTop()) {
-      ret.label = trayMenu.cancelWindowTop
-    } else {
-      ret.label = trayMenu.setWindowTop
-    }
-    return ret;
-  }
-  const setCancelWndTop = () => {
-    if (!mainWindow.isAlwaysOnTop()) {
-      mainWindow.setAlwaysOnTop(true)
-    } else {
-      mainWindow.setAlwaysOnTop(false)
-    }
-
-    resetTrayMenu()
-  }
-
-  const buildTrayMenuTemplate = () => {
-    let ret = [
-      buildShowWndMenu(),
-      {
-        label: trayMenu.officialWebsite,
-        click: () => {
-          shell.openExternal('https://b3log.org/siyuan/')
-        },
-      },
-      {
-        label: trayMenu.openSource,
-        click: () => {
-          shell.openExternal('https://github.com/siyuan-note/siyuan')
-        },
-      },
-      {
-        label: trayMenu.resetWindow,
-        type: 'checkbox',
-        click: v => {
-          resetWindowStateOnRestart = v.checked
-          mainWindow.webContents.send('siyuan-save-close', true)
-        },
-      },
-      {
-        label: trayMenu.quit,
-        click: () => {
-          mainWindow.webContents.send('siyuan-save-close', true)
-        },
-      }
-    ]
-
-    if ('win32' === process.platform) {
-      // Windows 端支持窗口置顶 https://github.com/siyuan-note/siyuan/issues/6860
-      ret.splice(1, 0, buildSetWndTopMenu())
-    }
-    return ret;
-  }
+}
 
-  ipcMain.on('siyuan-hotkey', (event, hotkey) => {
-    globalShortcut.unregisterAll()
-    if (!hotkey) {
-      return
-    }
-    globalShortcut.register(hotkey, () => {
-      if (mainWindow.isMinimized()) {
-        mainWindow.restore()
-        if (!mainWindow.isVisible()) {
-          mainWindow.show()
-        }
-      } else {
-        if (mainWindow.isVisible()) {
-          if (!mainWindow.isFocused()) {
-            mainWindow.show()
-          } else {
-            mainWindow.hide()
-          }
-        } else {
-          mainWindow.show()
+const initKernel = (workspace, lang) => {
+  const getKernelPort = async () => {
+    // TODO if (isDevEnv) {
+    //   writeLog("got kernel port [" + kernelPort + "]")
+    //   return kernelPort
+    // }
+
+    // 改进桌面端拉起内核 https://github.com/siyuan-note/siyuan/issues/6894
+    const getAvailablePort = (port = kernelPort) => {
+      // https://gist.github.com/mikeal/1840641
+      let tryGetPortCount = 0
+      const server = net.createServer()
+      return new Promise((resolve, reject) => server.on('error', error => {
+        writeLog(error)
+        if (2048 < ++tryGetPortCount) {
+          writeLog('failed to get available port [tryCount=' + tryGetPortCount +
+            ', port=' + port + ']')
+          reject(error)
+          return
         }
-      }
-
-      resetTrayMenu()
-    })
-  })
-
-  if ('win32' === process.platform || 'linux' === process.platform) {
-    // 系统托盘
-
-    tray = new Tray(path.join(appDir, 'stage', 'icon-large.png'))
-    tray.setToolTip('SiYuan v' + appVer)
-
-    const trayMenuTemplate = buildTrayMenuTemplate()
-    const contextMenu = Menu.buildFromTemplate(trayMenuTemplate)
-    tray.setContextMenu(contextMenu)
-    tray.on('click', () => {
-      showHideWnd()
-    })
+        server.listen(++port)
+      }).on('listening', () => {
+        writeLog('found an available port [' + port + ']')
+        server.close(() => resolve(port))
+      }).listen(port, '127.0.0.1'))
+    }
+    kernelPort = await getAvailablePort()
+    writeLog('got kernel available port [' + kernelPort + ']')
+    return kernelPort
   }
-}
-
-const initKernel = (initData) => {
   return new Promise(async (resolve) => {
     bootWindow = new BrowserWindow({
       width: screen.getPrimaryDisplay().size.width / 2,
@@ -566,7 +405,9 @@ const initKernel = (initData) => {
       },
     })
 
-    const kernelName = 'win32' === process.platform ? 'SiYuan-Kernel.exe' : 'SiYuan-Kernel'
+    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',
@@ -578,18 +419,19 @@ const initKernel = (initData) => {
 
     const availablePort = await getKernelPort()
     const cmds = ['--port', availablePort, '--wd', appDir]
-    if (isDevEnv) {
-      cmds.push('--mode', 'dev')
-    }
-    if (initData) {
-      const initDatas = initData.split('-')
-      cmds.push('--workspace', initDatas[0])
-      cmds.push('--lang', initDatas[1])
-    }
-    let cmd = `ui version [${appVer}], booting kernel [${kernelPath} ${cmds.join(' ')}]`
+    // TODO if (isDevEnv) {
+    //   cmds.push('--mode', 'dev')
+    // }
+    if (workspace) {
+      cmds.push('--workspace', workspace)
+      cmds.push('--lang', lang)
+    }
+    let cmd = `ui version [${appVer}], booting kernel [${kernelPath} ${cmds.join(
+      ' ')}]`
     writeLog(cmd)
-    let kernelProcessPid = ""
-    if (!isDevEnv) {
+    let kernelProcessPid = ''
+    // TODO
+    if (isDevEnv) {
       const cp = require('child_process')
       const kernelProcess = cp.spawn(kernelPath,
         cmds, {
@@ -609,8 +451,12 @@ const initKernel = (initData) => {
                 `<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:
-              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>')
+              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>')
               break
             case 22:
               showErrorWindow(
@@ -659,7 +505,8 @@ const initKernel = (initData) => {
         if (14 < count) {
           writeLog('get kernel ver failed')
 
-          showErrorWindow('⚠️ 获取内核服务端口失败 Failed to get kernel serve port',
+          showErrorWindow(
+            '⚠️ 获取内核服务端口失败 Failed to get kernel serve port',
             '<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)
@@ -680,7 +527,8 @@ const initKernel = (initData) => {
         let progressing = false
         while (!progressing) {
           try {
-            const progressResult = await fetch(getServer() + '/api/system/bootProgress')
+            const progressResult = await fetch(
+              getServer() + '/api/system/bootProgress')
             const progressData = await progressResult.json()
             if (progressData.data.progress >= 100) {
               resolve(true)
@@ -714,9 +562,199 @@ app.commandLine.appendSwitch('enable-features', 'PlatformHEVCDecoderSupport')
 app.setPath('userData', app.getPath('userData') + '-Electron') // `~/.config` 下 Electron 相关文件夹名称改为 `SiYuan-Electron` https://github.com/siyuan-note/siyuan/issues/3349
 
 app.whenReady().then(() => {
+  const resetTrayMenu = (tray, lang, mainWindow) => {
+    const trayMenuTemplate = [
+      {
+        label: mainWindow.isVisible()
+          ? lang.hideWindow
+          : lang.showWindow,
+        click: () => {
+          showHideWnd(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)
+        },
+      },
+    ]
+
+    if ('win32' === process.platform) {
+      // Windows 端支持窗口置顶 https://github.com/siyuan-note/siyuan/issues/6860
+      trayMenuTemplate.splice(1, 0, {
+        label: mainWindow.isAlwaysOnTop()
+          ? lang.cancelWindowTop
+          : lang.setWindowTop,
+        click: () => {
+          if (!mainWindow.isAlwaysOnTop()) {
+            mainWindow.setAlwaysOnTop(true)
+          } else {
+            mainWindow.setAlwaysOnTop(false)
+          }
+          resetTrayMenu(tray, lang, mainWindow)
+        },
+      })
+    }
+    const contextMenu = Menu.buildFromTemplate(trayMenuTemplate)
+    tray.setContextMenu(contextMenu)
+  }
+  const showHideWnd = (tray, lang, mainWindow) => {
+    if (!mainWindow.isVisible()) {
+      if (mainWindow.isMinimized()) {
+        mainWindow.restore()
+      }
+      mainWindow.show()
+    } else {
+      mainWindow.hide()
+    }
+
+    resetTrayMenu(tray, lang, mainWindow)
+  }
+
   ipcMain.on('siyuan-first-quit', () => {
     app.exit()
   })
+  ipcMain.on('siyuan-show', (event, id) => {
+    const mainWindow = BrowserWindow.fromId(id)
+    if (mainWindow.isMinimized()) {
+      mainWindow.restore()
+    }
+    if (!mainWindow.isVisible()) {
+      mainWindow.show()
+    }
+    mainWindow.focus()
+  })
+  ipcMain.on('siyuan-config-theme', (event, theme) => {
+    nativeTheme.themeSource = theme
+  })
+  ipcMain.on('siyuan-config-tray', (event, id) => {
+    BrowserWindow.fromId(id).hide()
+  })
+  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', data)
+  })
+  ipcMain.on('siyuan-quit', (id) => {
+    const mainWindow = BrowserWindow.fromId(id)
+    let tray
+    workspaces.find((item, index) => {
+      if (item.id === id) {
+        mainWindow.destroy()
+        tray = item.tray
+        workspaces.splice(index, 1)
+        return true
+      }
+    })
+    if (tray && 'win32' === process.platform) {
+      tray.destroy()
+    }
+    if (workspaces.length === 0) {
+      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')
+    }
+  })
+  ipcMain.on('siyuan-open-workspace', (event, data) => {
+    initKernel(data.workspace, data.lang).then((isSucc) => {
+      if (isSucc) {
+        boot()
+      }
+    })
+  })
+  ipcMain.on('siyuan-init', async (event, data) => {
+    let tray
+    if ('win32' === process.platform || 'linux' === process.platform) {
+      // 系统托盘
+      tray = new Tray(path.join(appDir, 'stage', 'icon-large.png'))
+      tray.setToolTip('SiYuan v' + appVer)
+      const mainWindow = BrowserWindow.fromId(data.id)
+      resetTrayMenu(tray, data.languages, mainWindow)
+      tray.on('click', () => {
+        showHideWnd(tray, data.languages, mainWindow)
+      })
+    }
+    workspaces.find(item => {
+      if (data.id === item.id) {
+        item.workspaceDir = data.workspaceDir
+        item.tray = tray
+        return true
+      }
+    })
+    await fetch(getServer() + '/api/system/uiproc?pid=' + process.pid,
+      {method: 'POST'})
+  })
+  ipcMain.on('siyuan-hotkey', (event, data) => {
+    globalShortcut.unregisterAll()
+    if (!data.hotkey) {
+      return
+    }
+    globalShortcut.register(data.hotkey, () => {
+      const mainWindow = BrowserWindow.fromId(data.id)
+      if (mainWindow.isMinimized()) {
+        mainWindow.restore()
+        if (!mainWindow.isVisible()) {
+          mainWindow.show()
+        }
+      } else {
+        if (mainWindow.isVisible()) {
+          if (!mainWindow.isFocused()) {
+            mainWindow.show()
+          } else {
+            mainWindow.hide()
+          }
+        } else {
+          mainWindow.show()
+        }
+      }
+      workspaces.find(item => {
+        if (item.id === data.id) {
+          resetTrayMenu(item.tray, data.languages, mainWindow)
+          return true
+        }
+      })
+    })
+  })
 
   if (firstOpen) {
     firstOpenWindow = new BrowserWindow({
@@ -740,8 +778,9 @@ app.whenReady().then(() => {
     }
 
     // 改进桌面端初始化时使用的外观语言 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";
+    let languages = app.getPreferredSystemLanguages()
+    let language = languages && 0 < languages.length && 'zh-Hans-CN' ===
+    languages[0] ? 'zh_CN' : 'en_US'
     firstOpenWindow.loadFile(
       initHTMLPath, {
         query: {
@@ -753,8 +792,8 @@ app.whenReady().then(() => {
       })
     firstOpenWindow.show()
     // 初始化启动
-    ipcMain.on('siyuan-first-init', (event, initData) => {
-      initKernel(initData).then((isSucc) => {
+    ipcMain.on('siyuan-first-init', (event, data) => {
+      initKernel(data.workspace, data.lang).then((isSucc) => {
         if (isSucc) {
           boot()
         }
@@ -773,34 +812,25 @@ app.whenReady().then(() => {
 app.on('open-url', (event, url) => { // for macOS
   if (url.startsWith('siyuan://')) {
     siyuanOpenURL = url
-    if (mainWindow && !mainWindow.isDestroyed()) {
-      if (mainWindow.isMinimized()) {
-        mainWindow.restore()
-      }
-      if (!mainWindow.isVisible()) {
-        mainWindow.show()
+    workspaces.forEach(item => {
+      if (item.browserWindow && !item.browserWindow.isDestroyed()) {
+        item.browserWindow.webContents.send('siyuan-openurl', url)
       }
-      mainWindow.focus()
-      mainWindow.webContents.send('siyuan-openurl', url)
-    }
+    })
   }
 })
 
 app.on('second-instance', (event, commandLine) => {
-  if (mainWindow && !mainWindow.isDestroyed()) {
-    if (mainWindow.isMinimized()) {
-      mainWindow.restore()
-    }
-    if (!mainWindow.isVisible()) {
-      mainWindow.show()
+  workspaces.forEach(item => {
+    if (item.browserWindow && !item.browserWindow.isDestroyed()) {
+      item.browserWindow.webContents.send('siyuan-openurl',
+        commandLine.find((arg) => arg.startsWith('siyuan://')))
     }
-    mainWindow.focus()
-    mainWindow.webContents.send('siyuan-openurl',
-      commandLine.find((arg) => arg.startsWith('siyuan://')))
-  }
+  })
 })
 
 app.on('activate', () => {
+  const mainWindow = workspaces[0].browserWindow
   if (mainWindow && !mainWindow.isDestroyed()) {
     mainWindow.show()
   }
@@ -818,10 +848,12 @@ app.on('web-contents-created', (webContentsCreatedEvent, contents) => {
 })
 
 app.on('before-quit', (event) => {
-  if (mainWindow && !mainWindow.isDestroyed()) {
-    event.preventDefault()
-    mainWindow.webContents.send('siyuan-save-close', true)
-  }
+  workspaces.forEach(item => {
+    if (item.browserWindow && !item.browserWindow.isDestroyed()) {
+      event.preventDefault()
+      item.browserWindow.webContents.send('siyuan-save-close', true)
+    }
+  })
 })
 
 const {powerMonitor} = require('electron')
@@ -832,23 +864,36 @@ powerMonitor.on('suspend', () => {
 
 powerMonitor.on('resume', async () => {
   writeLog('system resume')
+  const isOnline = async () => {
+    try {
+      const result = await fetch('https://icanhazip.com', {timeout: 1000})
+      return 200 === result.status
+    } catch (e) {
+      try {
+        const result = await fetch('https://www.baidu.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;
+      break
     }
 
-    writeLog("network is offline")
+    writeLog('network is offline')
     await sleep(1000)
   }
 
   if (!online) {
-    writeLog("network is offline, do not sync after system resume")
-    return;
+    writeLog('network is offline, do not sync after system resume')
+    return
   }
 
-  writeLog("sync after system resume")
+  writeLog('sync after system resume')
   // 桌面端系统休眠唤醒后同步延时 7s 后再执行 https://github.com/siyuan-note/siyuan/issues/6687
   fetch(getServer() + '/api/sync/performSync', {method: 'POST'})
 })
@@ -857,61 +902,3 @@ powerMonitor.on('shutdown', () => {
   writeLog('system shutdown')
   fetch(getServer() + '/api/system/exit', {method: 'POST'})
 })
-
-const sleep = (ms) => {
-  return new Promise(resolve => setTimeout(resolve, ms))
-}
-
-const isOnline = async () => {
-  try {
-    const result = await fetch("https://icanhazip.com", {timeout: 1000})
-    return 200 === result.status
-  } catch (e) {
-    try {
-      const result = await fetch("https://www.baidu.com", {timeout: 1000})
-      return 200 === result.status
-    } catch (e) {
-      return false;
-    }
-  }
-}
-
-let kernelPort = 6806
-
-const getKernelPort = async () => {
-  if (isDevEnv) {
-    writeLog("got kernel port [" + kernelPort + "]")
-    return kernelPort
-  }
-
-  // 改进桌面端拉起内核 https://github.com/siyuan-note/siyuan/issues/6894
-  kernelPort = await getAvailablePort()
-  writeLog("got kernel available port [" + kernelPort + "]")
-  return kernelPort
-}
-
-let tryGetPortCount = 0
-const getAvailablePort = (port = kernelPort) => {
-  // https://gist.github.com/mikeal/1840641
-
-  const server = net.createServer()
-  return new Promise((resolve, reject) => server
-    .on('error', error => {
-      writeLog(error)
-      if (2048 < ++tryGetPortCount) {
-        writeLog('failed to get available port [tryCount=' + tryGetPortCount + ', port=' + port + ']')
-        reject(error)
-        return
-      }
-      server.listen(++port)
-    })
-    .on('listening', () => {
-      writeLog('found an available port [' + port + ']')
-      server.close(() => resolve(port))
-    })
-    .listen(port, '127.0.0.1'))
-}
-
-const getServer = () => {
-  return "http://" + localhost + ":" + kernelPort
-}

+ 4 - 3
app/src/config/about.ts

@@ -1,6 +1,6 @@
 import {Constants} from "../constants";
 /// #if !BROWSER
-import {app, shell} from "electron";
+import {app, ipcRenderer, shell} from "electron";
 import {dialog} from "@electron/remote";
 /// #endif
 import {isBrowser} from "../util/functions";
@@ -242,8 +242,9 @@ export const about = {
             fetchPost("/api/system/setWorkspaceDir", {
                 path: workspace
             }, () => {
-                exportLayout(false, () => {
-                    exitSiYuan();
+                ipcRenderer.send(Constants.SIYUAN_OPEN_WORKSPACE, {
+                    workspace,
+                    lang: window.siyuan.config.appearance.lang
                 });
             });
         });

+ 0 - 1
app/src/config/appearance.ts

@@ -274,7 +274,6 @@ export const appearance = {
         }
         /// #if !BROWSER
         ipcRenderer.send(Constants.SIYUAN_CONFIG_THEME, data.modeOS ? "system" : (data.mode === 1 ? "dark" : "light"));
-        ipcRenderer.send(Constants.SIYUAN_CONFIG_CLOSE, data.closeButtonBehavior);
         /// #endif
         if (needLoadAsset) {
             loadAssets(data);

+ 14 - 3
app/src/config/keymap.ts

@@ -2,8 +2,11 @@ import {hotKey2Electron, isCtrl, isMac, updateHotkeyTip} from "../protyle/util/c
 import {Constants} from "../constants";
 import {showMessage} from "../dialog/message";
 import {fetchPost} from "../util/fetch";
-import {ipcRenderer} from "electron";
 import {exportLayout} from "../layout/util";
+/// #if !BROWSER
+import {getCurrentWindow} from "@electron/remote";
+import {ipcRenderer} from "electron";
+/// #endif
 import {confirmDialog} from "../dialog/confirmDialog";
 
 export const keymap = {
@@ -132,7 +135,11 @@ export const keymap = {
             data
         }, () => {
             /// #if !BROWSER
-            ipcRenderer.send(Constants.SIYUAN_HOTKEY, hotKey2Electron(window.siyuan.config.keymap.general.toggleWin.custom));
+            ipcRenderer.send(Constants.SIYUAN_HOTKEY, {
+                languages: window.siyuan.languages["_trayMenu"],
+                id: getCurrentWindow().id,
+                hotkey: hotKey2Electron(window.siyuan.config.keymap.general.toggleWin.custom)
+            });
             /// #endif
         });
     },
@@ -219,7 +226,11 @@ export const keymap = {
                 }, () => {
                     window.location.reload();
                     /// #if !BROWSER
-                    ipcRenderer.send(Constants.SIYUAN_HOTKEY, hotKey2Electron(window.siyuan.config.keymap.general.toggleWin.custom));
+                    ipcRenderer.send(Constants.SIYUAN_HOTKEY, {
+                        languages: window.siyuan.languages["_trayMenu"],
+                        id: getCurrentWindow().id,
+                        hotkey: hotKey2Electron(window.siyuan.config.keymap.general.toggleWin.custom)
+                    });
                     /// #endif
                 });
             });

+ 2 - 2
app/src/constants.ts

@@ -20,10 +20,10 @@ export abstract class Constants {
     public static readonly SIYUAN_DROP_EDITOR: string = "application/siyuan-editor";
 
     // 渲染进程调主进程
+    public static readonly SIYUAN_SHOW: string = "siyuan-show";
     public static readonly SIYUAN_CONFIG_THEME: string = "siyuan-config-theme";
-    public static readonly SIYUAN_CONFIG_CLOSE: string = "siyuan-config-close";
     public static readonly SIYUAN_CONFIG_TRAY: string = "siyuan-config-tray";
-    public static readonly SIYUAN_CONFIG_CLOSETRAY: string = "siyuan-config-closetray";
+    public static readonly SIYUAN_OPEN_WORKSPACE: string = "siyuan-open-workspace";
     public static readonly SIYUAN_QUIT: string = "siyuan-quit";
     public static readonly SIYUAN_HOTKEY: string = "siyuan-hotkey";
     public static readonly SIYUAN_INIT: string = "siyuan-init";

+ 0 - 4
app/src/dialog/processSystem.ts

@@ -93,7 +93,6 @@ export const exitSiYuan = () => {
                 buttonElement.addEventListener("click", () => {
                     fetchPost("/api/system/exit", {force: true}, () => {
                         /// #if !BROWSER
-                        ipcRenderer.send(Constants.SIYUAN_CONFIG_CLOSETRAY);
                         ipcRenderer.send(Constants.SIYUAN_QUIT);
                         /// #else
                         if (["ios", "android"].includes(window.siyuan.config.system.container) && (window.webkit?.messageHandlers || window.JSAndroid)) {
@@ -118,7 +117,6 @@ export const exitSiYuan = () => {
                     }, 2000);
                     // 然后等待一段时间后再退出,避免界面主进程退出以后内核子进程被杀死
                     setTimeout(() => {
-                        ipcRenderer.send(Constants.SIYUAN_CONFIG_CLOSETRAY);
                         ipcRenderer.send(Constants.SIYUAN_QUIT);
                     }, 4000);
                     /// #endif
@@ -129,14 +127,12 @@ export const exitSiYuan = () => {
                     execInstallPkg: 1 //  0:默认检查新版本,1:不执行新版本安装,2:执行新版本安装
                 }, () => {
                     /// #if !BROWSER
-                    ipcRenderer.send(Constants.SIYUAN_CONFIG_CLOSETRAY);
                     ipcRenderer.send(Constants.SIYUAN_QUIT);
                     /// #endif
                 });
             });
         } else { // 正常退出
             /// #if !BROWSER
-            ipcRenderer.send(Constants.SIYUAN_CONFIG_CLOSETRAY);
             ipcRenderer.send(Constants.SIYUAN_QUIT);
             /// #else
             if (["ios", "android"].includes(window.siyuan.config.system.container) && (window.webkit?.messageHandlers || window.JSAndroid)) {

+ 2 - 1
app/src/protyle/export/index.ts

@@ -416,11 +416,12 @@ const renderPDF = (id: string) => {
         });
         actionElement.querySelector('.b3-button--cancel').addEventListener('click', () => {
             const {ipcRenderer}  = require("electron");
-            ipcRenderer.send("${Constants.SIYUAN_EXPORT_CLOSE}")
+            ipcRenderer.send("${Constants.SIYUAN_EXPORT_CLOSE}", getCurrentWindow().id)
         });
         actionElement.querySelector('.b3-button--text').addEventListener('click', () => {
             const {ipcRenderer}  = require("electron");
             ipcRenderer.send("${Constants.SIYUAN_EXPORT_PDF}", {
+              id: getCurrentWindow().id,
               pdfOptions:{
                 printBackground: true,
                 landscape: actionElement.querySelector("#landscape").checked,

+ 0 - 1
app/src/util/assets.ts

@@ -235,7 +235,6 @@ export const setMode = (modeElementValue: number) => {
         window.siyuan.config.appearance = response.data;
         /// #if !BROWSER
         ipcRenderer.send(Constants.SIYUAN_CONFIG_THEME, response.data.modeOS ? "system" : (response.data.mode === 1 ? "dark" : "light"));
-        ipcRenderer.send(Constants.SIYUAN_CONFIG_CLOSE, response.data.closeButtonBehavior);
         /// #endif
         loadAssets(response.data);
         document.querySelector("#barMode use").setAttribute("xlink:href", `#icon${window.siyuan.config.appearance.modeOS ? "Mode" : (window.siyuan.config.appearance.mode === 0 ? "Light" : "Dark")}`);

+ 0 - 1
app/src/util/fetch.ts

@@ -48,7 +48,6 @@ export const fetchPost = (url: string, data?: any, cb?: (response: IWebSocketDat
         if (url === "/api/system/exit" || url === "/api/system/setWorkspaceDir" || (
             ["/api/system/setUILayout"].includes(url) && data.exit // 内核中断,点关闭处理
         )) {
-            ipcRenderer.send(Constants.SIYUAN_CONFIG_CLOSETRAY);
             ipcRenderer.send(Constants.SIYUAN_QUIT);
         }
         /// #endif

+ 26 - 9
app/src/util/onGetConfig.ts

@@ -126,14 +126,25 @@ export const onGetConfig = (isStart: boolean) => {
             data: window.siyuan.config.keymap
         }, () => {
             /// #if !BROWSER
-            ipcRenderer.send(Constants.SIYUAN_HOTKEY, hotKey2Electron(window.siyuan.config.keymap.general.toggleWin.custom));
+            ipcRenderer.send(Constants.SIYUAN_HOTKEY, {
+                languages: window.siyuan.languages["_trayMenu"],
+                id: getCurrentWindow().id,
+                hotkey: hotKey2Electron(window.siyuan.config.keymap.general.toggleWin.custom)
+            });
             /// #endif
         });
     }
     /// #if !BROWSER
-    ipcRenderer.send(Constants.SIYUAN_CONFIG_CLOSE, window.siyuan.config.appearance.closeButtonBehavior);
-    ipcRenderer.send(Constants.SIYUAN_INIT, window.siyuan.languages);
-    ipcRenderer.send(Constants.SIYUAN_HOTKEY, hotKey2Electron(window.siyuan.config.keymap.general.toggleWin.custom));
+    ipcRenderer.send(Constants.SIYUAN_INIT, {
+        languages: window.siyuan.languages["_trayMenu"],
+        workspaceDir: window.siyuan.config.system.workspaceDir,
+        id: getCurrentWindow().id,
+    });
+    ipcRenderer.send(Constants.SIYUAN_HOTKEY, {
+        languages: window.siyuan.languages["_trayMenu"],
+        id: getCurrentWindow().id,
+        hotkey: hotKey2Electron(window.siyuan.config.keymap.general.toggleWin.custom)
+    });
     /// #endif
     if (!window.siyuan.config.uiLayout || (window.siyuan.config.uiLayout && !window.siyuan.config.uiLayout.left)) {
         window.siyuan.config.uiLayout = Constants.SIYUAN_EMPTY_LAYOUT;
@@ -332,7 +343,7 @@ const winOnClose = (currentWindow: Electron.BrowserWindow, close = false) => {
         if (window.siyuan.config.appearance.closeButtonBehavior === 1 && !close) {
             // 最小化
             if ("windows" === window.siyuan.config.system.os) {
-                ipcRenderer.send(Constants.SIYUAN_CONFIG_TRAY);
+                ipcRenderer.send(Constants.SIYUAN_CONFIG_TRAY, getCurrentWindow().id);
             } else {
                 if (currentWindow.isFullScreen()) {
                     currentWindow.once("leave-full-screen", () => currentWindow.hide());
@@ -359,10 +370,16 @@ const initWindow = () => {
         if (!/^siyuan:\/\/blocks\/\d{14}-\w{7}/.test(url)) {
             return;
         }
-        openFileById({
-            id: url.substr(16, 22),
-            action: [Constants.CB_GET_FOCUS, Constants.CB_GET_CONTEXT],
-            zoomIn: getSearch("focus", url) === "1"
+        const id = url.substr(16, 22);
+        fetchPost("/api/block/checkBlockExist", {id}, existResponse => {
+            if (existResponse.data) {
+                openFileById({
+                    id,
+                    action: [Constants.CB_GET_FOCUS, Constants.CB_GET_CONTEXT],
+                    zoomIn: getSearch("focus", url) === "1"
+                });
+                ipcRenderer.send(Constants.SIYUAN_SHOW, getCurrentWindow().id);
+            }
         });
     });
     ipcRenderer.on(Constants.SIYUAN_SAVE_CLOSE, (event, close) => {

+ 2 - 2
app/stage/auth.html

@@ -432,8 +432,8 @@
   const exitApp = () => {
     try {
       const {ipcRenderer} = require('electron')
-      ipcRenderer.send('siyuan-config-closetray')
-      ipcRenderer.send('siyuan-quit')
+      const {getCurrentWindow} = require('@electron/remote');
+      ipcRenderer.send('siyuan-quit', getCurrentWindow().id)
     } catch (e) {
       if ((window.webkit && window.webkit.messageHandlers) || window.JSAndroid) {
         window.location.href = 'siyuan://api/system/exit'