main.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818
  1. // SiYuan - Build Your Eternal Digital Garden
  2. // Copyright (c) 2020-present, b3log.org
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. const {
  17. app,
  18. BrowserWindow,
  19. shell,
  20. Menu,
  21. screen,
  22. nativeTheme,
  23. ipcMain,
  24. globalShortcut,
  25. Tray,
  26. } = require('electron')
  27. const path = require('path')
  28. const fs = require('fs')
  29. const fetch = require('electron-fetch').default
  30. process.noAsar = true
  31. const appDir = path.dirname(app.getAppPath())
  32. const isDevEnv = process.env.NODE_ENV === 'development'
  33. const appVer = app.getVersion()
  34. const confDir = path.join(app.getPath('home'), '.config', 'siyuan')
  35. const windowStatePath = path.join(confDir, 'windowState.json')
  36. const portJSONPath = path.join(confDir, 'port.json')
  37. let tray // 托盘必须使用全局变量,以防止被垃圾回收 https://www.electronjs.org/docs/faq#my-apps-windowtray-disappeared-after-a-few-minutes
  38. let mainWindow // 从托盘处激活报错 https://github.com/siyuan-note/siyuan/issues/769
  39. let firstOpenWindow, bootWindow
  40. let closeButtonBehavior = 0
  41. let siyuanOpenURL
  42. let firstOpen = false
  43. let resetWindowStateOnRestart = false
  44. let kernelPort = "6806"
  45. require('@electron/remote/main').initialize()
  46. if (!app.requestSingleInstanceLock()) {
  47. app.quit()
  48. return
  49. }
  50. const showErrorWindow = (title, content) => {
  51. let errorHTMLPath = path.join(appDir, 'app', 'electron', 'error.html')
  52. if (isDevEnv) {
  53. errorHTMLPath = path.join(appDir, 'electron', 'error.html')
  54. }
  55. const errWindow = new BrowserWindow({
  56. width: screen.getPrimaryDisplay().size.width / 2,
  57. height: screen.getPrimaryDisplay().workAreaSize.height / 2,
  58. frame: false,
  59. icon: path.join(appDir, 'stage', 'icon-large.png'),
  60. webPreferences: {
  61. nativeWindowOpen: true,
  62. nodeIntegration: true,
  63. webviewTag: true,
  64. webSecurity: false,
  65. contextIsolation: false,
  66. },
  67. })
  68. require('@electron/remote/main').enable(errWindow.webContents)
  69. errWindow.loadFile(errorHTMLPath, {
  70. query: {
  71. home: app.getPath('home'),
  72. v: appVer,
  73. title: title,
  74. content: content,
  75. icon: path.join(appDir, 'stage', 'icon-large.png'),
  76. },
  77. })
  78. errWindow.show()
  79. }
  80. try {
  81. firstOpen = !fs.existsSync(path.join(confDir, 'workspace.json'))
  82. if (!fs.existsSync(confDir)) {
  83. fs.mkdirSync(confDir, {mode: 0o755, recursive: true})
  84. }
  85. } catch (e) {
  86. console.error(e)
  87. require('electron').dialog.showErrorBox('创建配置目录失败 Failed to create config directory',
  88. '思源需要在用户家目录下创建配置文件夹(~/.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.')
  89. app.exit()
  90. }
  91. const writeLog = (out) => {
  92. console.log(out)
  93. const logFile = path.join(confDir, 'app.log')
  94. let log = ''
  95. const maxLogLines = 1024
  96. try {
  97. if (fs.existsSync(logFile)) {
  98. log = fs.readFileSync(logFile).toString()
  99. let lines = log.split('\n')
  100. if (maxLogLines < lines.length) {
  101. log = lines.slice(maxLogLines / 2, maxLogLines).join('\n') + '\n'
  102. }
  103. }
  104. out = out.toString()
  105. out = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '') + ' ' +
  106. out
  107. log += out + '\n'
  108. fs.writeFileSync(logFile, log)
  109. } catch (e) {
  110. console.error(e)
  111. }
  112. }
  113. const boot = () => {
  114. // 恢复主窗体状态
  115. let oldWindowState = {}
  116. try {
  117. oldWindowState = JSON.parse(fs.readFileSync(windowStatePath, 'utf8'))
  118. } catch (e) {
  119. fs.writeFileSync(windowStatePath, '{}')
  120. }
  121. let defaultWidth
  122. let defaultHeight
  123. let workArea
  124. try {
  125. defaultWidth = screen.getPrimaryDisplay().size.width * 4 / 5
  126. defaultHeight = screen.getPrimaryDisplay().workAreaSize.height * 4 / 5
  127. workArea = screen.getPrimaryDisplay().workArea
  128. } catch (e) {
  129. console.error(e)
  130. }
  131. const windowState = Object.assign({}, {
  132. isMaximized: true,
  133. fullscreen: false,
  134. isDevToolsOpened: false,
  135. x: 0, y: 0,
  136. width: defaultWidth,
  137. height: defaultHeight,
  138. }, oldWindowState)
  139. let x = windowState.x
  140. let y = windowState.y
  141. if (workArea) {
  142. // 窗口大小等同于或大于 workArea 时,缩小会隐藏到左下角
  143. if (windowState.width >= workArea.width || windowState.height >=
  144. workArea.height) {
  145. windowState.width = Math.min(defaultWidth, workArea.width)
  146. windowState.height = Math.min(defaultHeight, workArea.height)
  147. }
  148. if (x > workArea.width) {
  149. x = 0
  150. }
  151. if (y > workArea.height) {
  152. y = 0
  153. }
  154. }
  155. if (windowState.width < 400) {
  156. windowState.width = 400
  157. }
  158. if (windowState.height < 300) {
  159. windowState.height = 300
  160. }
  161. if (x < 0) {
  162. x = 0
  163. }
  164. if (y < 0) {
  165. y = 0
  166. }
  167. // 创建主窗体
  168. mainWindow = new BrowserWindow({
  169. show: false,
  170. backgroundColor: '#FFF', // 桌面端主窗体背景色设置为 `#FFF` Fix https://github.com/siyuan-note/siyuan/issues/4544
  171. width: windowState.width,
  172. height: windowState.height,
  173. x,
  174. y,
  175. fullscreenable: true,
  176. fullscreen: windowState.fullscreen,
  177. trafficLightPosition: {x: 8, y: 8},
  178. webPreferences: {
  179. nodeIntegration: true,
  180. nativeWindowOpen: true,
  181. webviewTag: true,
  182. webSecurity: false,
  183. contextIsolation: false,
  184. },
  185. frame: 'darwin' === process.platform,
  186. titleBarStyle: 'hidden',
  187. icon: path.join(appDir, 'stage', 'icon-large.png'),
  188. })
  189. require('@electron/remote/main').enable(mainWindow.webContents)
  190. mainWindow.webContents.userAgent = 'SiYuan/' + appVer +
  191. ' https://b3log.org/siyuan Electron'
  192. // 发起互联网服务请求时绕过安全策略 https://github.com/siyuan-note/siyuan/issues/5516
  193. mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
  194. (details, cb) => {
  195. if (-1 < details.url.indexOf('bili')) {
  196. // B 站不移除 Referer https://github.com/siyuan-note/siyuan/issues/94
  197. cb({requestHeaders: details.requestHeaders})
  198. return
  199. }
  200. for (let key in details.requestHeaders) {
  201. if ('referer' === key.toLowerCase()) {
  202. delete details.requestHeaders[key]
  203. }
  204. }
  205. cb({requestHeaders: details.requestHeaders})
  206. })
  207. mainWindow.webContents.session.webRequest.onHeadersReceived((details, cb) => {
  208. for (let key in details.responseHeaders) {
  209. if ('x-frame-options' === key.toLowerCase()) {
  210. delete details.responseHeaders[key]
  211. } else if ('content-security-policy' === key.toLowerCase()) {
  212. delete details.responseHeaders[key]
  213. } else if ('access-control-allow-origin' === key.toLowerCase()) {
  214. delete details.responseHeaders[key]
  215. }
  216. }
  217. cb({responseHeaders: details.responseHeaders})
  218. })
  219. mainWindow.webContents.on('did-finish-load', () => {
  220. if ('win32' === process.platform || 'linux' === process.platform) {
  221. siyuanOpenURL = process.argv.find((arg) => arg.startsWith('siyuan://'))
  222. }
  223. if (siyuanOpenURL) {
  224. if (mainWindow.isMinimized()) {
  225. mainWindow.restore()
  226. }
  227. if (!mainWindow.isVisible()) {
  228. mainWindow.show()
  229. }
  230. mainWindow.focus()
  231. setTimeout(() => { // 等待界面js执行完毕
  232. writeLog(siyuanOpenURL)
  233. mainWindow.webContents.send('siyuan-openurl', siyuanOpenURL)
  234. siyuanOpenURL = null
  235. }, 2000)
  236. }
  237. })
  238. if (windowState.isDevToolsOpened) {
  239. mainWindow.webContents.openDevTools({mode: 'bottom'})
  240. }
  241. // 主界面事件监听
  242. mainWindow.once('ready-to-show', () => {
  243. mainWindow.show()
  244. if (windowState.isMaximized) {
  245. mainWindow.maximize()
  246. } else {
  247. mainWindow.unmaximize()
  248. }
  249. if (bootWindow && !bootWindow.isDestroyed()) {
  250. bootWindow.destroy()
  251. }
  252. })
  253. // 加载主界面
  254. mainWindow.loadURL('http://127.0.0.1:' + kernelPort + '/stage/build/app/index.html?v=' +
  255. new Date().getTime())
  256. // 菜单
  257. const productName = 'SiYuan'
  258. const template = [
  259. {
  260. label: productName,
  261. submenu: [
  262. {
  263. label: `About ${productName}`,
  264. role: 'about',
  265. },
  266. {type: 'separator'},
  267. {role: 'services'},
  268. {type: 'separator'},
  269. {
  270. label: `Hide ${productName}`,
  271. role: 'hide',
  272. },
  273. {role: 'hideOthers'},
  274. {role: 'unhide'},
  275. {type: 'separator'},
  276. {
  277. label: `Quit ${productName}`,
  278. role: 'quit',
  279. },
  280. ],
  281. },
  282. {
  283. role: 'editMenu',
  284. submenu: [
  285. {role: 'cut'},
  286. {role: 'copy'},
  287. {role: 'paste'},
  288. {role: 'pasteAndMatchStyle', accelerator: 'CmdOrCtrl+Shift+C'},
  289. {role: 'selectAll'},
  290. ],
  291. },
  292. {
  293. role: 'viewMenu',
  294. submenu: [
  295. {role: 'resetZoom'},
  296. {role: 'zoomIn', accelerator: 'CommandOrControl+='},
  297. {role: 'zoomOut'},
  298. ],
  299. },
  300. {
  301. role: 'windowMenu',
  302. submenu: [
  303. {role: 'minimize'},
  304. {role: 'zoom'},
  305. {role: 'togglefullscreen'},
  306. {type: 'separator'},
  307. {role: 'toggledevtools'},
  308. {type: 'separator'},
  309. {role: 'front'},
  310. ],
  311. },
  312. ]
  313. const menu = Menu.buildFromTemplate(template)
  314. Menu.setApplicationMenu(menu)
  315. // 当前页面链接使用浏览器打开
  316. mainWindow.webContents.on('will-navigate', (event, url) => {
  317. if (url.startsWith('http://127.0.0.1:' + kernelPort)) {
  318. return
  319. }
  320. event.preventDefault()
  321. shell.openExternal(url)
  322. })
  323. mainWindow.on('close', (event) => {
  324. if (mainWindow && !mainWindow.isDestroyed()) {
  325. mainWindow.webContents.send('siyuan-save-close', false)
  326. }
  327. event.preventDefault()
  328. })
  329. nativeTheme.on('updated', () => {
  330. mainWindow.webContents.send('siyuan-update-theme', {
  331. theme: nativeTheme.shouldUseDarkColors ? 'dark' : 'light',
  332. init: false,
  333. })
  334. })
  335. // 监听主题切换
  336. ipcMain.on('siyuan-config-theme', (event, theme) => {
  337. nativeTheme.themeSource = theme
  338. })
  339. ipcMain.on('siyuan-config-close', (event, close) => {
  340. closeButtonBehavior = close
  341. })
  342. ipcMain.on('siyuan-config-tray', () => {
  343. mainWindow.hide()
  344. })
  345. ipcMain.on('siyuan-config-closetray', () => {
  346. if ('win32' === process.platform) {
  347. tray.destroy()
  348. }
  349. })
  350. ipcMain.on('siyuan-export-pdf', (event, data) => {
  351. mainWindow.webContents.send('siyuan-export-pdf', data)
  352. })
  353. ipcMain.on('siyuan-export-close', (event, data) => {
  354. mainWindow.webContents.send('siyuan-export-close', data)
  355. })
  356. ipcMain.on('siyuan-quit', () => {
  357. try {
  358. if (resetWindowStateOnRestart) {
  359. fs.writeFileSync(windowStatePath, '{}')
  360. } else {
  361. const bounds = mainWindow.getBounds()
  362. fs.writeFileSync(windowStatePath, JSON.stringify({
  363. isMaximized: mainWindow.isMaximized(),
  364. fullscreen: mainWindow.isFullScreen(),
  365. isDevToolsOpened: mainWindow.webContents.isDevToolsOpened(),
  366. x: bounds.x,
  367. y: bounds.y,
  368. width: bounds.width,
  369. height: bounds.height,
  370. }))
  371. }
  372. } catch (e) {
  373. writeLog(e)
  374. }
  375. app.exit()
  376. globalShortcut.unregisterAll()
  377. writeLog('exited ui')
  378. })
  379. ipcMain.on('siyuan-init', async () => {
  380. mainWindow.webContents.send('siyuan-update-theme', {
  381. theme: nativeTheme.shouldUseDarkColors ? 'dark' : 'light',
  382. init: true,
  383. })
  384. await fetch('http://127.0.0.1:' + kernelPort + '/api/system/uiproc?pid=' + process.pid,
  385. {method: 'POST'})
  386. })
  387. ipcMain.on('siyuan-hotkey', (event, hotkey) => {
  388. globalShortcut.unregisterAll()
  389. if (!hotkey) {
  390. return
  391. }
  392. globalShortcut.register(hotkey, () => {
  393. if (mainWindow.isMinimized()) {
  394. mainWindow.restore()
  395. if (!mainWindow.isVisible()) {
  396. mainWindow.show()
  397. }
  398. } else {
  399. if (mainWindow.isVisible()) {
  400. if (!mainWindow.isFocused()) {
  401. mainWindow.show()
  402. } else {
  403. mainWindow.hide()
  404. }
  405. } else {
  406. mainWindow.show()
  407. }
  408. }
  409. })
  410. })
  411. if ('win32' === process.platform || 'linux' === process.platform) {
  412. // 系统托盘
  413. tray = new Tray(path.join(appDir, 'stage', 'icon-large.png'))
  414. tray.setToolTip('SiYuan v' + appVer)
  415. const trayMenuTemplate = [
  416. {
  417. label: 'Show Window',
  418. click: () => {
  419. if (mainWindow.isMinimized()) {
  420. mainWindow.restore()
  421. }
  422. mainWindow.show()
  423. },
  424. },
  425. {
  426. label: 'Hide Window',
  427. click: () => {
  428. mainWindow.hide()
  429. },
  430. },
  431. {
  432. label: 'Official Website',
  433. click: () => {
  434. shell.openExternal('https://b3log.org/siyuan/')
  435. },
  436. },
  437. {
  438. label: 'Open Source',
  439. click: () => {
  440. shell.openExternal('https://github.com/siyuan-note/siyuan')
  441. },
  442. },
  443. {
  444. label: '中文反馈',
  445. click: () => {
  446. shell.openExternal('https://ld246.com/article/1649901726096')
  447. },
  448. },
  449. {
  450. label: 'Reset Window on restart',
  451. type: 'checkbox',
  452. click: v => {
  453. resetWindowStateOnRestart = v.checked
  454. },
  455. },
  456. {
  457. label: 'Quit',
  458. click: () => {
  459. mainWindow.webContents.send('siyuan-save-close', true)
  460. },
  461. }]
  462. const contextMenu = Menu.buildFromTemplate(trayMenuTemplate)
  463. tray.setContextMenu(contextMenu)
  464. tray.on('click', () => {
  465. if (mainWindow.isMinimized()) {
  466. mainWindow.restore()
  467. if (!mainWindow.isVisible()) {
  468. mainWindow.show()
  469. }
  470. } else {
  471. if (mainWindow.isVisible()) {
  472. if (!mainWindow.isFocused()) {
  473. mainWindow.show()
  474. } else {
  475. mainWindow.hide()
  476. }
  477. } else {
  478. mainWindow.show()
  479. }
  480. }
  481. })
  482. }
  483. }
  484. const initKernel = (initData) => {
  485. return new Promise(async (resolve) => {
  486. bootWindow = new BrowserWindow({
  487. width: screen.getPrimaryDisplay().size.width / 2,
  488. height: screen.getPrimaryDisplay().workAreaSize.height / 2,
  489. frame: false,
  490. icon: path.join(appDir, 'stage', 'icon-large.png'),
  491. transparent: 'linux' !== process.platform,
  492. webPreferences: {
  493. nativeWindowOpen: true,
  494. },
  495. })
  496. const kernelName = 'win32' === process.platform
  497. ? 'SiYuan-Kernel.exe'
  498. : 'SiYuan-Kernel'
  499. const kernelPath = path.join(appDir, 'kernel', kernelName)
  500. if (!fs.existsSync(kernelPath)) {
  501. showErrorWindow('⚠️ 内核文件丢失 Kernel is missing',
  502. `<div>内核可执行文件丢失,请重新安装思源,并将思源加入杀毒软件信任列表。</div><div>The kernel binary is not found, please reinstall SiYuan and add SiYuan into the trust list of your antivirus software.</div>`)
  503. bootWindow.destroy()
  504. resolve(false)
  505. return
  506. }
  507. const cmds = ['--wd', appDir]
  508. cmds.push('--resident', 'false')
  509. if (isDevEnv) {
  510. cmds.push('--mode', 'dev')
  511. }
  512. if (initData) {
  513. const initDatas = initData.split('-')
  514. cmds.push('--workspace', initDatas[0])
  515. cmds.push('--lang', initDatas[1])
  516. }
  517. let cmd = `ui version [${appVer}], booting kernel [${kernelPath} ${cmds.join(' ')}]`
  518. writeLog(cmd)
  519. let kernelProcessPid = ""
  520. if (!isDevEnv) {
  521. const cp = require('child_process')
  522. const kernelProcess = cp.spawn(kernelPath,
  523. cmds, {
  524. detached: false, // 桌面端内核进程不再以游离模式拉起 https://github.com/siyuan-note/siyuan/issues/6336
  525. stdio: 'ignore',
  526. },
  527. )
  528. kernelProcessPid = kernelProcess.pid
  529. writeLog('booted kernel process [pid=' + kernelProcessPid + ']')
  530. kernelProcess.on('close', (code) => {
  531. writeLog(`kernel exited with code [${code}]`)
  532. if (0 !== code) {
  533. switch (code) {
  534. case 20:
  535. showErrorWindow('⚠️ 数据库被锁定 The database is locked',
  536. `<div>数据库文件正在被其他程序锁定。如果你使用了第三方同步盘,请在思源运行期间关闭同步。</div><div>The database file is being locked by another program. If you use a third-party sync disk, please turn off sync while SiYuan is running.</div>`)
  537. break
  538. case 21:
  539. showErrorWindow('⚠️ 监听端口 ' + kernelPort + ' 失败 Failed to listen to port ' + kernelPort,
  540. '<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>')
  541. break
  542. case 22:
  543. showErrorWindow(
  544. '⚠️ 创建配置目录失败 Failed to create config directory',
  545. `<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>`)
  546. break
  547. case 23:
  548. showErrorWindow(
  549. '⚠️ 无法读写块树文件 Failed to access blocktree file',
  550. `<div>块树文件正在被其他程序锁定或者已经损坏,请删除 工作空间/temp/ 文件夹后重启</div><div>The block tree file is being locked by another program or is corrupted, please delete the workspace/temp/ folder and restart.</div>`)
  551. break
  552. case 0:
  553. case 1: // Fatal error
  554. break
  555. default:
  556. showErrorWindow(
  557. '⚠️ 内核因未知原因退出 The kernel exited for unknown reasons',
  558. `<div>思源内核因未知原因退出 [code=${code}],请尝试重启操作系统后再启动思源。如果该问题依然发生,请检查杀毒软件是否阻止思源内核启动。</div>
  559. <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.</div>`)
  560. break
  561. }
  562. bootWindow.destroy()
  563. resolve(false)
  564. }
  565. })
  566. }
  567. const sleep = (ms) => {
  568. return new Promise(resolve => setTimeout(resolve, ms))
  569. }
  570. const getKernelPort = async () => {
  571. if (isDevEnv) {
  572. return kernelPort
  573. }
  574. await sleep(200)
  575. let gotPort = false
  576. let count = 0
  577. while (!gotPort) {
  578. try {
  579. const portJSON = JSON.parse(fs.readFileSync(portJSONPath, 'utf8'))
  580. const ret = portJSON[kernelProcessPid]
  581. if (ret) {
  582. gotPort = true
  583. return ret
  584. }
  585. await sleep(100)
  586. } catch (e) {
  587. await sleep(100)
  588. } finally {
  589. count++
  590. if (64 < count) {
  591. writeLog('get kernel port failed [pid=' + kernelProcessPid + ']')
  592. bootWindow.destroy()
  593. resolve(false)
  594. }
  595. }
  596. }
  597. }
  598. kernelPort = await getKernelPort()
  599. writeLog("got kernel port [" + kernelPort + "]")
  600. let gotVersion = false
  601. let apiData
  602. let count = 0
  603. writeLog('checking kernel version')
  604. while (!gotVersion) {
  605. try {
  606. const apiResult = await fetch('http://127.0.0.1:' + kernelPort + '/api/system/version')
  607. apiData = await apiResult.json()
  608. gotVersion = true
  609. bootWindow.setResizable(false)
  610. bootWindow.loadURL('http://127.0.0.1:' + kernelPort + '/appearance/boot/index.html')
  611. bootWindow.show()
  612. } catch (e) {
  613. writeLog('get kernel version failed: ' + e.message)
  614. await sleep(100)
  615. } finally {
  616. count++
  617. if (14 < count) {
  618. writeLog('get kernel ver failed')
  619. bootWindow.destroy()
  620. resolve(false)
  621. }
  622. }
  623. }
  624. if (0 === apiData.code) {
  625. writeLog('got kernel version [' + apiData.data + ']')
  626. if (!isDevEnv && apiData.data !== appVer) {
  627. writeLog(
  628. `kernel [${apiData.data}] is running, shutdown it now and then start kernel [${appVer}]`)
  629. fetch('http://127.0.0.1:' + kernelPort + '/api/system/exit', {method: 'POST'})
  630. bootWindow.destroy()
  631. resolve(false)
  632. } else {
  633. let progressing = false
  634. while (!progressing) {
  635. try {
  636. const progressResult = await fetch('http://127.0.0.1:' + kernelPort + '/api/system/bootProgress')
  637. const progressData = await progressResult.json()
  638. if (progressData.data.progress >= 100) {
  639. resolve(true)
  640. progressing = true
  641. } else {
  642. await sleep(100)
  643. }
  644. } catch (e) {
  645. writeLog('get boot progress failed: ' + e.message)
  646. fetch('http://127.0.0.1:' + kernelPort + '/api/system/exit', {method: 'POST'})
  647. bootWindow.destroy()
  648. resolve(false)
  649. progressing = true
  650. }
  651. }
  652. }
  653. } else {
  654. writeLog(`get kernel version failed: ${apiData.code}, ${apiData.msg}`)
  655. resolve(false)
  656. }
  657. })
  658. }
  659. app.setAsDefaultProtocolClient('siyuan')
  660. app.commandLine.appendSwitch('disable-web-security')
  661. app.commandLine.appendSwitch('auto-detect', 'false')
  662. app.commandLine.appendSwitch('no-proxy-server')
  663. app.setPath('userData', app.getPath('userData') + '-Electron') // `~/.config` 下 Electron 相关文件夹名称改为 `SiYuan-Electron` https://github.com/siyuan-note/siyuan/issues/3349
  664. app.whenReady().then(() => {
  665. ipcMain.on('siyuan-first-quit', () => {
  666. app.exit()
  667. })
  668. if (firstOpen) {
  669. firstOpenWindow = new BrowserWindow({
  670. width: screen.getPrimaryDisplay().size.width / 2,
  671. height: screen.getPrimaryDisplay().workAreaSize.height / 2,
  672. frame: false,
  673. icon: path.join(appDir, 'stage', 'icon-large.png'),
  674. transparent: 'linux' !== process.platform,
  675. webPreferences: {
  676. nativeWindowOpen: true,
  677. nodeIntegration: true,
  678. webviewTag: true,
  679. webSecurity: false,
  680. contextIsolation: false,
  681. },
  682. })
  683. require('@electron/remote/main').enable(firstOpenWindow.webContents)
  684. let initHTMLPath = path.join(appDir, 'app', 'electron', 'init.html')
  685. if (isDevEnv) {
  686. initHTMLPath = path.join(appDir, 'electron', 'init.html')
  687. }
  688. firstOpenWindow.loadFile(
  689. initHTMLPath, {
  690. query: {
  691. home: app.getPath('home'),
  692. v: appVer,
  693. icon: path.join(appDir, 'stage', 'icon-large.png'),
  694. },
  695. })
  696. firstOpenWindow.show()
  697. // 初始化启动
  698. ipcMain.on('siyuan-first-init', (event, initData) => {
  699. initKernel(initData).then((isSucc) => {
  700. if (isSucc) {
  701. boot()
  702. }
  703. })
  704. firstOpenWindow.destroy()
  705. })
  706. } else {
  707. initKernel().then((isSucc) => {
  708. if (isSucc) {
  709. boot()
  710. }
  711. })
  712. }
  713. })
  714. app.on('open-url', (event, url) => { // for macOS
  715. if (url.startsWith('siyuan://')) {
  716. siyuanOpenURL = url
  717. if (mainWindow && !mainWindow.isDestroyed()) {
  718. if (mainWindow.isMinimized()) {
  719. mainWindow.restore()
  720. }
  721. if (!mainWindow.isVisible()) {
  722. mainWindow.show()
  723. }
  724. mainWindow.focus()
  725. mainWindow.webContents.send('siyuan-openurl', url)
  726. }
  727. }
  728. })
  729. app.on('second-instance', (event, commandLine) => {
  730. if (mainWindow && !mainWindow.isDestroyed()) {
  731. if (mainWindow.isMinimized()) {
  732. mainWindow.restore()
  733. }
  734. if (!mainWindow.isVisible()) {
  735. mainWindow.show()
  736. }
  737. mainWindow.focus()
  738. mainWindow.webContents.send('siyuan-openurl',
  739. commandLine.find((arg) => arg.startsWith('siyuan://')))
  740. }
  741. })
  742. app.on('activate', () => {
  743. if (mainWindow && !mainWindow.isDestroyed()) {
  744. mainWindow.show()
  745. }
  746. if (BrowserWindow.getAllWindows().length === 0) {
  747. boot()
  748. }
  749. })
  750. // 在编辑器内打开链接的处理,比如 iframe 上的打开链接。
  751. app.on('web-contents-created', (webContentsCreatedEvent, contents) => {
  752. contents.on('new-window', (newWindowEvent, url) => {
  753. newWindowEvent.preventDefault()
  754. shell.openExternal(url)
  755. })
  756. })
  757. app.on('before-quit', (event) => {
  758. if (mainWindow && !mainWindow.isDestroyed()) {
  759. event.preventDefault()
  760. mainWindow.webContents.send('siyuan-save-close', true)
  761. }
  762. })
  763. const {powerMonitor} = require('electron')
  764. const cp = require("child_process");
  765. powerMonitor.on('suspend', () => {
  766. writeLog('system suspend')
  767. fetch('http://127.0.0.1:' + kernelPort + '/api/sync/performSync', {method: 'POST'})
  768. })
  769. powerMonitor.on('resume', () => {
  770. writeLog('system resume')
  771. fetch('http://127.0.0.1:' + kernelPort + '/api/sync/performSync', {method: 'POST'})
  772. })
  773. powerMonitor.on('shutdown', () => {
  774. writeLog('system shutdown')
  775. fetch('http://127.0.0.1:' + kernelPort + '/api/system/exit', {method: 'POST'})
  776. })