diff --git a/packages/core/src/expose.js b/packages/core/src/expose.js index ee3593b2cf..de190c5465 100644 --- a/packages/core/src/expose.js +++ b/packages/core/src/expose.js @@ -42,14 +42,17 @@ function newServerStart ({ mitmproxyPath }) { } server.start = newServerStart async function startup ({ mitmproxyPath }) { + let server const conf = config.get() + if (conf.server.enabled) { try { - await server.start({ mitmproxyPath }) + server = await server.start({ mitmproxyPath }) } catch (err) { log.error('代理服务启动失败:', err) } } + if (conf.proxy.enabled) { try { await proxy.start() @@ -57,8 +60,10 @@ async function startup ({ mitmproxyPath }) { log.error('开启系统代理失败:', err) } } + try { const plugins = [] + for (const key in plugin) { if (conf.plugin[key].enabled) { const start = async () => { @@ -72,12 +77,15 @@ async function startup ({ mitmproxyPath }) { plugins.push(start()) } } + if (plugins && plugins.length > 0) { await Promise.all(plugins) } } catch (err) { log.error('开启插件失败:', err) } + + return server } async function shutdown () { diff --git a/packages/core/src/modules/server/index.js b/packages/core/src/modules/server/index.js index fa0e4dbe5a..346f086253 100644 --- a/packages/core/src/modules/server/index.js +++ b/packages/core/src/modules/server/index.js @@ -30,7 +30,7 @@ const serverApi = { return this.close() } }, - async start ({ mitmproxyPath, plugins }) { + async start ({ mitmproxyPath, plugins, options }) { const allConfig = config.get() const serverConfig = lodash.cloneDeep(allConfig.server) @@ -78,7 +78,7 @@ const serverApi = { const runningConfigPath = path.join(basePath, '/running.json') fs.writeFileSync(runningConfigPath, jsonApi.stringify(serverConfig)) log.info('保存 running.json 运行时配置文件成功:', runningConfigPath) - const serverProcess = fork(mitmproxyPath, [runningConfigPath]) + const serverProcess = fork(mitmproxyPath, [runningConfigPath], options) server = { id: serverProcess.pid, process: serverProcess, @@ -86,6 +86,7 @@ const serverApi = { serverProcess.send({ type: 'action', event: { key: 'close' } }) }, } + serverProcess.on('beforeExit', (code) => { log.warn('server process beforeExit, code:', code) }) @@ -113,7 +114,8 @@ const serverApi = { event.fire('speed', msg.event) } }) - return { port: serverConfig.port } + + return { port: serverConfig.port, server } }, async kill () { if (server) { diff --git a/packages/core/src/utils/util.logger.js b/packages/core/src/utils/util.logger.js index 60697aa969..158aef802f 100644 --- a/packages/core/src/utils/util.logger.js +++ b/packages/core/src/utils/util.logger.js @@ -45,7 +45,7 @@ function log4jsConfigure (categories) { for (const category of categories) { config.appenders[category] = { ...appenderConfig, filename: path.join(basePath, `/${category}.log`) } - config.categories[category] = { appenders: [category, 'std'], level } + config.categories[category] = { appenders: [category, ...process.env.NO_CONSOLE_LOG ? [] : ['std']], level } } log4js.configure(config) diff --git a/packages/gui/extra/banner.txt b/packages/gui/extra/banner.txt new file mode 100644 index 0000000000..c025095feb --- /dev/null +++ b/packages/gui/extra/banner.txt @@ -0,0 +1,8 @@ + ____ _____ _ __ + / __ \___ _ __ / ___/(_)___/ /__ _________ ______ + / / / / _ \ | / /_____\__ \/ / __ / _ \/ ___/ __ `/ ___/ + / /_/ / __/ |/ /_____/__/ / / /_/ / __/ /__/ /_/ / / +/_____/\___/|___/ /____/_/\__,_/\___/\___/\__,_/_/ + + +==================== 开发者边车 ==================== diff --git a/packages/gui/package.json b/packages/gui/package.json index a125002993..4b895e9de8 100644 --- a/packages/gui/package.json +++ b/packages/gui/package.json @@ -13,6 +13,7 @@ "lint": "vue-cli-service lint", "electron:build": "vue-cli-service electron:build", "electron": "vue-cli-service electron:serve", + "electron:headless": "cross-env ELECTRON_RUN_AS_NODE=1 vue-cli-service electron:serve", "postinstall": "electron-builder install-app-deps", "postuninstall": "electron-builder install-app-deps", "electron:icons": "electron-icon-builder --input=./public/logo/win.png --output=build --flatten", @@ -27,6 +28,7 @@ "@vscode/sudo-prompt": "^9.3.1", "adm-zip": "^0.5.16", "ant-design-vue": "^1.7.8", + "cac": "^6.7.14", "electron-baidu-tongji": "^1.0.5", "electron-updater": "^6.3.9", "json5": "^2.2.3", @@ -44,6 +46,7 @@ "@vue/babel-preset-jsx": "^1.4.0", "@vue/cli-plugin-babel": "^5.0.8", "@vue/cli-service": "^5.0.8", + "cross-env": "^7.0.3", "electron": "^19.1.9", "electron-builder": "^25.1.8", "electron-icon-builder": "^2.0.1", diff --git a/packages/gui/src/background.js b/packages/gui/src/background.js index 512791296d..64b0964523 100644 --- a/packages/gui/src/background.js +++ b/packages/gui/src/background.js @@ -1,490 +1,38 @@ -'use strict' -/* global __static */ import path from 'node:path' -import DevSidecar from '@docmirror/dev-sidecar' -import { app, BrowserWindow, dialog, globalShortcut, ipcMain, Menu, nativeImage, nativeTheme, powerMonitor, protocol, Tray } from 'electron' -import minimist from 'minimist' -import { createProtocol } from 'vue-cli-plugin-electron-builder/lib' -import backend from './bridge/backend' -import jsonApi from '@docmirror/mitmproxy/src/json' -import log from './utils/util.log' +import { fork } from 'node:child_process' -const isWindows = process.platform === 'win32' -const isMac = process.platform === 'darwin' const isDevelopment = process.env.NODE_ENV !== 'production' +const RUN_AS_NODE = !!process.env.ELECTRON_RUN_AS_NODE -// 避免其他系统出现异常,只有 Windows 使用 './background/powerMonitor' -let _powerMonitor = powerMonitor -if (isWindows) { - try { - _powerMonitor = require('./background/powerMonitor').powerMonitor - } catch (e) { - log.error(`加载 './background/powerMonitor' 失败,现捕获异常并使用默认的 powerMonitor。\r\n目前,启动着DS重启电脑时,将无法正常关闭系统代理,届时请自行关闭系统代理!\r\n捕获的异常信息:`, e) - } -} - -// Keep a global reference of the window object, if you don't, the window will -// be closed automatically when the JavaScript object is garbage collected. -let win -let winIsHidden = false - -let tray // 防止被内存清理 -let forceClose = false -DevSidecar.api.config.reload() -let hideDockWhenWinClose = DevSidecar.api.config.get().app.dock.hideWhenWinClose || false -// Scheme must be registered before the app is ready -protocol.registerSchemesAsPrivileged([ - { scheme: 'app', privileges: { secure: true, standard: true } }, -]) - -function openDevTools () { - try { - log.debug('尝试打开 `开发者工具`') - win.webContents.openDevTools() - log.debug('打开 `开发者工具` 成功') - } catch (e) { - log.error('打开 `开发者工具` 失败:', e) - } -} - -function closeDevTools () { - try { - log.debug('尝试关闭 `开发者工具`') - win.webContents.closeDevTools() - log.debug('关闭 `开发者工具` 成功') - } catch (e) { - log.error('关闭 `开发者工具` 失败:', e) - } -} - -function switchDevTools () { - if (!win || !win.webContents) { - return - } - if (win.webContents.isDevToolsOpened()) { - closeDevTools() +;(async () => { + if (RUN_AS_NODE) { + await startHeadless() } else { - openDevTools() + await startGUI() } -} - -// 隐藏主窗口,并创建托盘,绑定关闭事件 -function setTray () { - // const topMenu = Menu.buildFromTemplate({}) - // Menu.setApplicationMenu(topMenu) - // 用一个 Tray 来表示一个图标,这个图标处于正在运行的系统的通知区 - // 通常被添加到一个 context menu 上. - // 系统托盘右键菜单 - const trayMenuTemplate = [ - { - // 系统托盘图标目录 - label: 'DevTools (F12)', - click: switchDevTools, - }, - { - // 系统托盘图标目录 - label: '退出', - click: () => { - log.info('force quit') - forceClose = true - quit() - }, - }, - ] - // 设置系统托盘图标 - const iconRootPath = path.join(__dirname, '../extra/icons/tray') - let iconPath = path.join(iconRootPath, 'icon.png') - const iconWhitePath = path.join(iconRootPath, 'icon-white.png') - const iconBlackPath = path.join(iconRootPath, 'icon-black.png') - if (isMac) { - iconPath = nativeTheme.shouldUseDarkColors ? iconWhitePath : iconBlackPath - } - - const trayIcon = nativeImage.createFromPath(iconPath) - const appTray = new Tray(trayIcon) +})() - // 当桌面主题更新时 - if (isMac) { - nativeTheme.on('updated', () => { - console.log('i am changed') - if (nativeTheme.shouldUseDarkColors) { - console.log('i am dark.') - tray.setImage(iconWhitePath) - } else { - console.log('i am light.') - tray.setImage(iconBlackPath) - // tray.setPressedImage(iconWhitePath) - } - }) - } - - // 图标的上下文菜单 - const contextMenu = Menu.buildFromTemplate(trayMenuTemplate) - - // 设置托盘悬浮提示 - appTray.setToolTip('DevSidecar-开发者边车辅助工具') - // 单击托盘小图标显示应用 - appTray.on('click', () => { - // 显示主程序 - showWin() - }) - - appTray.on('right-click', () => { - setTimeout(() => { - appTray.popUpContextMenu(contextMenu) - }, 200) - }) - - return appTray -} - -function isLinux () { - const platform = DevSidecar.api.shell.getSystemPlatform() - return platform === 'linux' -} - -function hideWin () { - if (win) { - if (isLinux()) { - quit() - return - } - win.hide() - if (isMac && hideDockWhenWinClose) { - app.dock.hide() - } - winIsHidden = true - } -} - -function showWin () { - if (win) { - win.show() - } - if (app.dock) { - app.dock.show() - } - winIsHidden = false -} - -function changeAppConfig (config) { - if (config.hideDockWhenWinClose != null) { - hideDockWhenWinClose = config.hideDockWhenWinClose - } -} - -function createWindow (startHideWindow) { - // Create the browser window. - const windowSize = DevSidecar.api.config.get().app.windowSize || {} - win = new BrowserWindow({ - width: windowSize.width || 900, - height: windowSize.height || 750, - title: 'DevSidecar', - webPreferences: { - enableRemoteModule: true, - contextIsolation: false, - nativeWindowOpen: true, // ADD THIS - // preload: path.join(__dirname, 'preload.js'), - // Use pluginOptions.nodeIntegration, leave this alone - // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info - nodeIntegration: true, // process.env.ELECTRON_NODE_INTEGRATION - }, - show: !startHideWindow, - icon: path.join(__static, 'icon.png'), - }) - winIsHidden = !!startHideWindow - - Menu.setApplicationMenu(null) - win.setMenu(null) - - // !!IMPORTANT - if (isWindows && typeof _powerMonitor.setupMainWindow === 'function') { - _powerMonitor.setupMainWindow(win) - } - - if (process.env.WEBPACK_DEV_SERVER_URL) { - // Load the url of the dev server if in development mode - win.loadURL(process.env.WEBPACK_DEV_SERVER_URL) - if (!process.env.IS_TEST) { - setTimeout(openDevTools, 2000) - } +async function startHeadless () { + const cli = path.join(__dirname, 'dev-sidecar-cli.js') + let argv + if (isDevelopment) { + argv = process.argv.splice(3) } else { - createProtocol('app') - // Load the index.html when not in development - win.loadURL('app://./index.html') - } - - if (startHideWindow) { - hideWin() + argv = process.argv } - win.on('closed', async (...args) => { - log.info('win closed:', ...args) - win = null - tray = null - }) - - ipcMain.on('close', async (event, message) => { - if (message.value === 1) { - quit() - } else { - hideWin() - } - }) - - win.on('close', (e, ...args) => { - log.info('win close:', e, ...args) - if (forceClose) { - return - } - e.preventDefault() - if (isLinux()) { - quit() - return - } - const config = DevSidecar.api.config.get() - const closeStrategy = config.app.closeStrategy - if (closeStrategy === 0) { - // 弹窗提示,选择关闭策略 - win.webContents.send('close.showTip', closeStrategy) - } else if (closeStrategy === 1) { - // 直接退出 - quit() - } else if (closeStrategy === 2) { - // 隐藏窗口 - hideWin() - } - }) - - win.on('session-end', async (e, ...args) => { - log.info('win session-end:', e, ...args) - await quit() - }) - - const shortcut = (event, input) => { - // 按 F12,打开/关闭 开发者工具 - if (input.key === 'F12') { - // 阻止默认的按键事件行为 - event.preventDefault() - // 切换开发者工具显示状态 - switchDevTools() - // eslint-disable-next-line style/brace-style - } - // 按 F5,刷新页面 - else if (input.key === 'F5') { - // 阻止默认的按键事件行为 - event.preventDefault() - // 刷新页面 - win.webContents.reload() - } - } - - // 监听键盘事件 - win.webContents.on('before-input-event', (event, input) => { - if (input.type !== 'keyUp' || input.control || input.alt || input.shift || input.meta) { - return - } - win.webContents.executeJavaScript('config') - .then((value) => { - console.info('window.config:', value, ', key:', input.key) - if (!value || (value.disableBeforeInputEvent !== true && value.disableBeforeInputEvent !== 'true')) { - shortcut(event, input) - } - }) - .catch(() => { - shortcut(event, input) - }) - }) - - // 监听渲染进程发送过来的消息 - win.webContents.on('ipc-message', (event, channel, message, ...args) => { - console.info('win ipc-message:', event, channel, message, ...args) - if (channel === 'change-showHideShortcut') { - registerShowHideShortcut(message) - } - }) -} - -async function beforeQuit () { - return DevSidecar.api.shutdown() -} -async function quit () { - if (tray) { - tray.displayBalloon({ title: '正在关闭', content: '关闭中,请稍候。。。' }) - } - await beforeQuit() - forceClose = true - app.quit() -} - -function registerShowHideShortcut (showHideShortcut) { - globalShortcut.unregisterAll() - if (showHideShortcut && showHideShortcut !== '无' && showHideShortcut.length > 1) { - try { - const registerSuccess = globalShortcut.register(DevSidecar.api.config.get().app.showHideShortcut, () => { - if (winIsHidden || !win.isFocused()) { - if (!win.isFocused()) { - win.focus() - } - if (winIsHidden) { - showWin() - } - } else { - // linux,快捷键不关闭窗口 - if (!isLinux()) { - hideWin() - } - } - }) - - if (registerSuccess) { - log.info('注册快捷键成功:', DevSidecar.api.config.get().app.showHideShortcut) - } else { - log.error('注册快捷键失败:', DevSidecar.api.config.get().app.showHideShortcut) - } - } catch (e) { - log.error('注册快捷键异常:', DevSidecar.api.config.get().app.showHideShortcut, ', error:', e) - } - } -} - -function initApp () { - if (isMac) { - app.whenReady().then(() => { - app.dock.setIcon(path.join(__dirname, '../build/mac/512x512.png')) - }) - } - - // 全局监听快捷键,用于 显示/隐藏 窗口 - app.whenReady().then(async () => { - registerShowHideShortcut(DevSidecar.api.config.get().app.showHideShortcut) - }) -} - -// -------------执行开始--------------- -app.disableHardwareAcceleration() // 禁用gpu - -// 开启后是否默认隐藏window -let startHideWindow = !DevSidecar.api.config.get().app.startShowWindow -if (app.getLoginItemSettings().wasOpenedAsHidden) { - startHideWindow = true -} else if (process.argv) { - const args = minimist(process.argv) - log.info('start args:', args) - - // 通过启动参数,判断是否隐藏窗口 - const hideWindowArg = `${args.hideWindow}` - if (hideWindowArg === 'true' || hideWindowArg === '1') { - startHideWindow = true - } else if (hideWindowArg === 'false' || hideWindowArg === '0') { - startHideWindow = false - } -} -log.info('startHideWindow = ', startHideWindow, ', app.getLoginItemSettings() = ', jsonApi.stringify2(app.getLoginItemSettings())) - -// 禁止双开 -const isFirstInstance = app.requestSingleInstanceLock() -if (!isFirstInstance) { - log.info('is second instance') - setTimeout(() => { - app.quit() - }, 1000) -} else { - app.on('before-quit', async () => { - log.info('before-quit') - if (process.platform === 'darwin') { - quit() - } - }) - app.on('will-quit', () => { - log.info('应用关闭,注销所有快捷键') - globalShortcut.unregisterAll() - }) - app.on('second-instance', (event, commandLine) => { - log.info('new app started, command:', commandLine) - if (win) { - showWin() - win.focus() - } - }) - - // Quit when all windows are closed. - app.on('window-all-closed', () => { - log.info('window-all-closed') - // On macOS it is common for applications and their menu bar - // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== 'darwin') { - quit() - } - }) - - app.on('activate', () => { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (win == null) { - createWindow(false) - } else { - showWin() - } + const cliProcess = fork(cli, argv, { + env: { + ...process.env, + NO_CONSOLE_LOG: true, + }, + detached: true, + stdio: 'inherit', }) - // initApp() - - // This method will be called when Electron has finished - // initialization and is ready to create browser windows. - // Some APIs can only be used after this event occurs. - app.on('ready', async () => { - if (isDevelopment && !process.env.IS_TEST) { - // Install Vue Devtools - // try { - // await installExtension(VUEJS_DEVTOOLS) - // } catch (e) { - // log.error('Vue Devtools failed to install:', e.toString()) - // } - } - try { - createWindow(startHideWindow) - const context = { win, app, beforeQuit, quit, ipcMain, dialog, log, api: DevSidecar.api, changeAppConfig } - backend.install(context) // 模块安装 - } catch (err) { - log.info('error:', err) - } - - try { - // 最小化到托盘 - tray = setTray() - } catch (err) { - log.info('error:', err) - } - - _powerMonitor.on('shutdown', async (e) => { - if (e) { - e.preventDefault() - } - log.info('系统关机,恢复代理设置') - await quit() - }) - }) + cliProcess.unref() } -initApp() +async function startGUI () { -// Exit cleanly on request from parent process in development mode. -if (isDevelopment) { - if (process.platform === 'win32') { - process.on('message', (data) => { - if (data === 'graceful-exit') { - quit() - } - }) - } else { - process.on('SIGINT', () => { - quit() - }) - } } -// 系统关机和重启时的操作 -process.on('exit', () => { - log.info('进程结束,退出app') - quit() -}) diff --git a/packages/gui/src/background/cli.js b/packages/gui/src/background/cli.js new file mode 100644 index 0000000000..248cf385a0 --- /dev/null +++ b/packages/gui/src/background/cli.js @@ -0,0 +1,81 @@ +import DevSidecar from '@docmirror/dev-sidecar' +import { getExtraPath, mitmproxyPath } from './config' +import path from 'node:path' +import fs from 'node:fs' +import os from 'node:os' +import cac from 'cac' + +const cli = cac('dev-sidecar') +const pk = require('../../package.json') + +async function startup () { + console.log('启动 DevSidecar 服务') + const config = DevSidecar.api.config.get() + + if (config.server.pid) { + process.kill(config.server.pid, os.constants.signals.SIGINT) + config.server.pid = null + DevSidecar.api.config.save(config) + } + + // 开启自动下载远程配置 + await DevSidecar.api.config.startAutoDownloadRemoteConfig() + + const { server } = await DevSidecar.api.server.start({ + mitmproxyPath, + options: { + stdio: 'ignore', + }, + }) + + // 写入server进程pid + config.server.pid = server.id + DevSidecar.api.config.save(config) +} + +async function stop () { + console.log('关闭 DevSidecar 服务') + const config = DevSidecar.api.config.get() + + if (config.server.pid) { + process.kill(config.server.pid, os.constants.signals.SIGINT) + config.server.pid = null + DevSidecar.api.config.save(config) + } +} + +async function restart () { + console.log('重启 DevSidecar 服务') + const config = DevSidecar.api.config.get() + + if (config.server.pid) { + process.kill(config.server.pid, 'SIGINT') + } + + await startup() +} + +const banner = fs.readFileSync(path.join(getExtraPath(), 'banner.txt')) +console.log(banner.toString()) + +cli + .help() + .usage('start') + .version(pk.version) + +cli + .command('start', '启动 DevSidecar 服务') + .action(async () => { + await startup() + process.exit(0) + }) + +cli + .command('stop', '关闭 DevSidecar 服务') + .action(stop) + +cli + .command('restart', '重启 DevSidecar 服务') + .action(restart) + +cli.parse() diff --git a/packages/gui/src/background/config.js b/packages/gui/src/background/config.js new file mode 100644 index 0000000000..e698aa89fd --- /dev/null +++ b/packages/gui/src/background/config.js @@ -0,0 +1,93 @@ +import path from 'node:path' +import DevSidecar from '@docmirror/dev-sidecar' +import fs from 'node:fs' + +const jsonApi = require('@docmirror/mitmproxy/src/json') +const log = require('../utils/util.log') +const configFromFiles = require('@docmirror/dev-sidecar/src/config/index.js').configFromFiles + +export const mitmproxyPath = path.join(__dirname, 'mitmproxy.js') +process.env.DS_EXTRA_PATH = path.join(__dirname, '../extra/') + +function getDefaultConfigBasePath () { + return DevSidecar.api.config.get().server.setting.userBasePath +} + +export function getSettingsPath () { + const dir = getDefaultConfigBasePath() + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir) + } else { + // 兼容1.7.3及以下版本的配置文件处理逻辑 + const newFilePath = path.join(dir, '/setting.json') + const oldFilePath = path.join(dir, '/setting.json5') + if (!fs.existsSync(newFilePath) && fs.existsSync(oldFilePath)) { + return oldFilePath // 如果新文件不存在,且旧文件存在,则返回旧文件路径 + } + return newFilePath + } + return path.join(dir, '/setting.json') +} + +export function getLogDir () { + return configFromFiles.app.logFileSavePath || path.join(getDefaultConfigBasePath(), '/logs/') +} + +export function loadConfig () { + const settingPath = getSettingsPath() + let setting = {} + if (fs.existsSync(settingPath)) { + const file = fs.readFileSync(settingPath) + try { + setting = jsonApi.parse(file.toString()) + log.info('读取 setting.json 成功:', settingPath) + } catch (e) { + log.error('读取 setting.json 失败:', settingPath, ', error:', e) + } + if (setting == null) { + setting = {} + } + } + if (setting.overwall == null) { + setting.overwall = false + } + + if (setting.installTime == null) { + // 设置安装时间 + setting.installTime = getDateTimeStr() + + // 初始化 rootCa.setuped + if (setting.rootCa == null) { + setting.rootCa = { + setuped: false, + desc: '根证书未安装', + } + } + + // 保存 setting.json + saveConfig(setting) + } + return setting +} + +export function saveConfig (setting) { + const settingPath = getSettingsPath() + fs.writeFileSync(settingPath, jsonApi.stringify(setting)) + log.info('保存 setting.json 配置文件成功:', settingPath) +} + +export function getDateTimeStr () { + const date = new Date() // 创建一个表示当前日期和时间的 Date 对象 + const year = date.getFullYear() // 获取年份 + const month = String(date.getMonth() + 1).padStart(2, '0') // 获取月份(注意月份从 0 开始计数) + const day = String(date.getDate()).padStart(2, '0') // 获取天数 + const hours = String(date.getHours()).padStart(2, '0') // 获取小时 + const minutes = String(date.getMinutes()).padStart(2, '0') // 获取分钟 + const seconds = String(date.getSeconds()).padStart(2, '0') // 获取秒数 + const milliseconds = String(date.getMilliseconds()).padStart(3, '0') // 获取毫秒 + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}` +} + +export function getExtraPath () { + return path.join(__dirname, '../extra/') +} diff --git a/packages/gui/src/background/gui.js b/packages/gui/src/background/gui.js new file mode 100644 index 0000000000..5e3e76762a --- /dev/null +++ b/packages/gui/src/background/gui.js @@ -0,0 +1,493 @@ +/* global __static */ + +import path from 'node:path' +import DevSidecar from '@docmirror/dev-sidecar' +import { app, BrowserWindow, dialog, globalShortcut, ipcMain, Menu, nativeImage, nativeTheme, powerMonitor, protocol, Tray } from 'electron' +import minimist from 'minimist' +import { createProtocol } from 'vue-cli-plugin-electron-builder/lib' +import backend from './bridge/backend' +import jsonApi from '@docmirror/mitmproxy/src/json' +import log from './utils/util.log' + +DevSidecar.api.config.reload() + +// Scheme must be registered before the app is ready +protocol.registerSchemesAsPrivileged([ + { scheme: 'app', privileges: { secure: true, standard: true } }, +]) + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let win +let winIsHidden = false + +let tray // 防止被内存清理 +let forceClose = false + +let hideDockWhenWinClose = DevSidecar.api.config.get().app.dock.hideWhenWinClose || false + +const isWindows = process.platform === 'win32' +const isMac = process.platform === 'darwin' +const isDevelopment = process.env.NODE_ENV !== 'production' + +// 避免其他系统出现异常,只有 Windows 使用 './background/powerMonitor' +let _powerMonitor = powerMonitor +if (isWindows) { + try { + _powerMonitor = require('./background/powerMonitor').powerMonitor + } catch (e) { + log.error(`加载 './background/powerMonitor' 失败,现捕获异常并使用默认的 powerMonitor。\r\n目前,启动着DS重启电脑时,将无法正常关闭系统代理,届时请自行关闭系统代理!\r\n捕获的异常信息:`, e) + } +} + +function openDevTools () { + try { + log.debug('尝试打开 `开发者工具`') + win.webContents.openDevTools() + log.debug('打开 `开发者工具` 成功') + } catch (e) { + log.error('打开 `开发者工具` 失败:', e) + } +} + +function closeDevTools () { + try { + log.debug('尝试关闭 `开发者工具`') + win.webContents.closeDevTools() + log.debug('关闭 `开发者工具` 成功') + } catch (e) { + log.error('关闭 `开发者工具` 失败:', e) + } +} + +function switchDevTools () { + if (!win || !win.webContents) { + return + } + if (win.webContents.isDevToolsOpened()) { + closeDevTools() + } else { + openDevTools() + } +} + +// 隐藏主窗口,并创建托盘,绑定关闭事件 +function setTray () { + // const topMenu = Menu.buildFromTemplate({}) + // Menu.setApplicationMenu(topMenu) + // 用一个 Tray 来表示一个图标,这个图标处于正在运行的系统的通知区 + // 通常被添加到一个 context menu 上. + // 系统托盘右键菜单 + const trayMenuTemplate = [ + { + // 系统托盘图标目录 + label: 'DevTools (F12)', + click: switchDevTools, + }, + { + // 系统托盘图标目录 + label: '退出', + click: () => { + log.info('force quit') + forceClose = true + quit() + }, + }, + ] + // 设置系统托盘图标 + const iconRootPath = path.join(__dirname, '../extra/icons/tray') + let iconPath = path.join(iconRootPath, 'icon.png') + const iconWhitePath = path.join(iconRootPath, 'icon-white.png') + const iconBlackPath = path.join(iconRootPath, 'icon-black.png') + if (isMac) { + iconPath = nativeTheme.shouldUseDarkColors ? iconWhitePath : iconBlackPath + } + + const trayIcon = nativeImage.createFromPath(iconPath) + const appTray = new Tray(trayIcon) + + // 当桌面主题更新时 + if (isMac) { + nativeTheme.on('updated', () => { + console.log('i am changed') + if (nativeTheme.shouldUseDarkColors) { + console.log('i am dark.') + tray.setImage(iconWhitePath) + } else { + console.log('i am light.') + tray.setImage(iconBlackPath) + // tray.setPressedImage(iconWhitePath) + } + }) + } + + // 图标的上下文菜单 + const contextMenu = Menu.buildFromTemplate(trayMenuTemplate) + + // 设置托盘悬浮提示 + appTray.setToolTip('DevSidecar-开发者边车辅助工具') + // 单击托盘小图标显示应用 + appTray.on('click', () => { + // 显示主程序 + showWin() + }) + + appTray.on('right-click', () => { + setTimeout(() => { + appTray.popUpContextMenu(contextMenu) + }, 200) + }) + + return appTray +} + +function isLinux () { + const platform = DevSidecar.api.shell.getSystemPlatform() + return platform === 'linux' +} + +function hideWin () { + if (win) { + if (isLinux()) { + quit() + return + } + win.hide() + if (isMac && hideDockWhenWinClose) { + app.dock.hide() + } + winIsHidden = true + } +} + +function showWin () { + if (win) { + win.show() + } + if (app.dock) { + app.dock.show() + } + winIsHidden = false +} + +function changeAppConfig (config) { + if (config.hideDockWhenWinClose != null) { + hideDockWhenWinClose = config.hideDockWhenWinClose + } +} + +function createWindow (startHideWindow) { + // Create the browser window. + const windowSize = DevSidecar.api.config.get().app.windowSize || {} + win = new BrowserWindow({ + width: windowSize.width || 900, + height: windowSize.height || 750, + title: 'DevSidecar', + webPreferences: { + enableRemoteModule: true, + contextIsolation: false, + nativeWindowOpen: true, // ADD THIS + // preload: path.join(__dirname, 'preload.js'), + // Use pluginOptions.nodeIntegration, leave this alone + // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info + nodeIntegration: true, // process.env.ELECTRON_NODE_INTEGRATION + }, + show: !startHideWindow, + icon: path.join(__static, 'icon.png'), + }) + winIsHidden = !!startHideWindow + + Menu.setApplicationMenu(null) + win.setMenu(null) + + // !!IMPORTANT + if (isWindows && typeof _powerMonitor.setupMainWindow === 'function') { + _powerMonitor.setupMainWindow(win) + } + + if (process.env.WEBPACK_DEV_SERVER_URL) { + // Load the url of the dev server if in development mode + win.loadURL(process.env.WEBPACK_DEV_SERVER_URL) + if (!process.env.IS_TEST) { + setTimeout(openDevTools, 2000) + } + } else { + createProtocol('app') + // Load the index.html when not in development + win.loadURL('app://./index.html') + } + + if (startHideWindow) { + hideWin() + } + + win.on('closed', async (...args) => { + log.info('win closed:', ...args) + win = null + tray = null + }) + + ipcMain.on('close', async (event, message) => { + if (message.value === 1) { + quit() + } else { + hideWin() + } + }) + + win.on('close', (e, ...args) => { + log.info('win close:', e, ...args) + if (forceClose) { + return + } + e.preventDefault() + if (isLinux()) { + quit() + return + } + const config = DevSidecar.api.config.get() + const closeStrategy = config.app.closeStrategy + if (closeStrategy === 0) { + // 弹窗提示,选择关闭策略 + win.webContents.send('close.showTip', closeStrategy) + } else if (closeStrategy === 1) { + // 直接退出 + quit() + } else if (closeStrategy === 2) { + // 隐藏窗口 + hideWin() + } + }) + + win.on('session-end', async (e, ...args) => { + log.info('win session-end:', e, ...args) + await quit() + }) + + const shortcut = (event, input) => { + // 按 F12,打开/关闭 开发者工具 + if (input.key === 'F12') { + // 阻止默认的按键事件行为 + event.preventDefault() + // 切换开发者工具显示状态 + switchDevTools() + // eslint-disable-next-line style/brace-style + } + // 按 F5,刷新页面 + else if (input.key === 'F5') { + // 阻止默认的按键事件行为 + event.preventDefault() + // 刷新页面 + win.webContents.reload() + } + } + + // 监听键盘事件 + win.webContents.on('before-input-event', (event, input) => { + if (input.type !== 'keyUp' || input.control || input.alt || input.shift || input.meta) { + return + } + win.webContents.executeJavaScript('config') + .then((value) => { + console.info('window.config:', value, ', key:', input.key) + if (!value || (value.disableBeforeInputEvent !== true && value.disableBeforeInputEvent !== 'true')) { + shortcut(event, input) + } + }) + .catch(() => { + shortcut(event, input) + }) + }) + + // 监听渲染进程发送过来的消息 + win.webContents.on('ipc-message', (event, channel, message, ...args) => { + console.info('win ipc-message:', event, channel, message, ...args) + if (channel === 'change-showHideShortcut') { + registerShowHideShortcut(message) + } + }) +} + +async function beforeQuit () { + return DevSidecar.api.shutdown() +} +async function quit () { + if (tray) { + tray.displayBalloon({ title: '正在关闭', content: '关闭中,请稍候。。。' }) + } + await beforeQuit() + forceClose = true + app.quit() +} + +function registerShowHideShortcut (showHideShortcut) { + globalShortcut.unregisterAll() + if (showHideShortcut && showHideShortcut !== '无' && showHideShortcut.length > 1) { + try { + const registerSuccess = globalShortcut.register(DevSidecar.api.config.get().app.showHideShortcut, () => { + if (winIsHidden || !win.isFocused()) { + if (!win.isFocused()) { + win.focus() + } + if (winIsHidden) { + showWin() + } + } else { + // linux,快捷键不关闭窗口 + if (!isLinux()) { + hideWin() + } + } + }) + + if (registerSuccess) { + log.info('注册快捷键成功:', DevSidecar.api.config.get().app.showHideShortcut) + } else { + log.error('注册快捷键失败:', DevSidecar.api.config.get().app.showHideShortcut) + } + } catch (e) { + log.error('注册快捷键异常:', DevSidecar.api.config.get().app.showHideShortcut, ', error:', e) + } + } +} + +function initApp () { + if (isMac) { + app.whenReady().then(() => { + app.dock.setIcon(path.join(__dirname, '../build/mac/512x512.png')) + }) + } + + // 全局监听快捷键,用于 显示/隐藏 窗口 + app.whenReady().then(async () => { + registerShowHideShortcut(DevSidecar.api.config.get().app.showHideShortcut) + }) +} + +// -------------执行开始--------------- +app.disableHardwareAcceleration() // 禁用gpu + +// 开启后是否默认隐藏window +let startHideWindow = !DevSidecar.api.config.get().app.startShowWindow +if (app.getLoginItemSettings().wasOpenedAsHidden) { + startHideWindow = true +} else if (process.argv) { + const args = minimist(process.argv) + log.info('start args:', args) + + // 通过启动参数,判断是否隐藏窗口 + const hideWindowArg = `${args.hideWindow}` + if (hideWindowArg === 'true' || hideWindowArg === '1') { + startHideWindow = true + } else if (hideWindowArg === 'false' || hideWindowArg === '0') { + startHideWindow = false + } +} +log.info('startHideWindow = ', startHideWindow, ', app.getLoginItemSettings() = ', jsonApi.stringify2(app.getLoginItemSettings())) + +// 禁止双开 +const isFirstInstance = app.requestSingleInstanceLock() +if (!isFirstInstance) { + log.info('is second instance') + setTimeout(() => { + app.quit() + }, 1000) +} else { + app.on('before-quit', async () => { + log.info('before-quit') + if (process.platform === 'darwin') { + quit() + } + }) + app.on('will-quit', () => { + log.info('应用关闭,注销所有快捷键') + globalShortcut.unregisterAll() + }) + app.on('second-instance', (event, commandLine) => { + log.info('new app started, command:', commandLine) + if (win) { + showWin() + win.focus() + } + }) + + // Quit when all windows are closed. + app.on('window-all-closed', () => { + log.info('window-all-closed') + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + quit() + } + }) + + app.on('activate', () => { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (win == null) { + createWindow(false) + } else { + showWin() + } + }) + + // initApp() + + // This method will be called when Electron has finished + // initialization and is ready to create browser windows. + // Some APIs can only be used after this event occurs. + app.on('ready', async () => { + if (isDevelopment && !process.env.IS_TEST) { + // Install Vue Devtools + // try { + // await installExtension(VUEJS_DEVTOOLS) + // } catch (e) { + // log.error('Vue Devtools failed to install:', e.toString()) + // } + } + try { + createWindow(startHideWindow) + const context = { win, app, beforeQuit, quit, ipcMain, dialog, log, api: DevSidecar.api, changeAppConfig } + backend.install(context) // 模块安装 + } catch (err) { + log.info('error:', err) + } + + try { + // 最小化到托盘 + tray = setTray() + } catch (err) { + log.info('error:', err) + } + + _powerMonitor.on('shutdown', async (e) => { + if (e) { + e.preventDefault() + } + log.info('系统关机,恢复代理设置') + await quit() + }) + }) +} + +initApp() + +// Exit cleanly on request from parent process in development mode. +if (isDevelopment) { + if (process.platform === 'win32') { + process.on('message', (data) => { + if (data === 'graceful-exit') { + quit() + } + }) + } else { + process.on('SIGINT', () => { + quit() + }) + } +} +// 系统关机和重启时的操作 +process.on('exit', () => { + log.info('进程结束,退出app') + quit() +}) diff --git a/packages/gui/src/bridge/api/backend.js b/packages/gui/src/bridge/api/backend.js index 6cf37bf607..4ef2a5656c 100644 --- a/packages/gui/src/bridge/api/backend.js +++ b/packages/gui/src/bridge/api/backend.js @@ -1,33 +1,11 @@ -import fs from 'node:fs' -import path from 'node:path' import DevSidecar from '@docmirror/dev-sidecar' import { ipcMain } from 'electron' import lodash from 'lodash' +import { getDateTimeStr, getDefaultConfigBasePath, getLogDir, loadConfig, mitmproxyPath, saveConfig } from '../../background/config' -const jsonApi = require('@docmirror/mitmproxy/src/json') const pk = require('../../../package.json') -const configFromFiles = require('@docmirror/dev-sidecar/src/config/index.js').configFromFiles const log = require('../../utils/util.log') -const mitmproxyPath = path.join(__dirname, 'mitmproxy.js') -process.env.DS_EXTRA_PATH = path.join(__dirname, '../extra/') - -const getDefaultConfigBasePath = function () { - return DevSidecar.api.config.get().server.setting.userBasePath -} - -const getDateTimeStr = function () { - const date = new Date() // 创建一个表示当前日期和时间的 Date 对象 - const year = date.getFullYear() // 获取年份 - const month = String(date.getMonth() + 1).padStart(2, '0') // 获取月份(注意月份从 0 开始计数) - const day = String(date.getDate()).padStart(2, '0') // 获取天数 - const hours = String(date.getHours()).padStart(2, '0') // 获取小时 - const minutes = String(date.getMinutes()).padStart(2, '0') // 获取分钟 - const seconds = String(date.getSeconds()).padStart(2, '0') // 获取秒数 - const milliseconds = String(date.getMilliseconds()).padStart(3, '0') // 获取毫秒 - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}` -} - const localApi = { /** * 返回所有api列表,供vue来ipc调用 @@ -52,7 +30,7 @@ const localApi = { return getDefaultConfigBasePath() }, getLogDir () { - return configFromFiles.app.logFileSavePath || path.join(getDefaultConfigBasePath(), '/logs/') + return getLogDir() }, getSystemPlatform (throwIfUnknown = false) { return DevSidecar.api.shell.getSystemPlatform(throwIfUnknown) @@ -63,45 +41,10 @@ const localApi = { */ setting: { load () { - const settingPath = _getSettingsPath() - let setting = {} - if (fs.existsSync(settingPath)) { - const file = fs.readFileSync(settingPath) - try { - setting = jsonApi.parse(file.toString()) - log.info('读取 setting.json 成功:', settingPath) - } catch (e) { - log.error('读取 setting.json 失败:', settingPath, ', error:', e) - } - if (setting == null) { - setting = {} - } - } - if (setting.overwall == null) { - setting.overwall = false - } - - if (setting.installTime == null) { - // 设置安装时间 - setting.installTime = getDateTimeStr() - - // 初始化 rootCa.setuped - if (setting.rootCa == null) { - setting.rootCa = { - setuped: false, - desc: '根证书未安装', - } - } - - // 保存 setting.json - localApi.setting.save(setting) - } - return setting + return loadConfig() }, save (setting = {}) { - const settingPath = _getSettingsPath() - fs.writeFileSync(settingPath, jsonApi.stringify(setting)) - log.info('保存 setting.json 配置文件成功:', settingPath) + saveConfig(setting) }, }, /** @@ -140,22 +83,6 @@ function _deepFindFunction (list, parent, parentKey) { } } -function _getSettingsPath () { - const dir = getDefaultConfigBasePath() - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir) - } else { - // 兼容1.7.3及以下版本的配置文件处理逻辑 - const newFilePath = path.join(dir, '/setting.json') - const oldFilePath = path.join(dir, '/setting.json5') - if (!fs.existsSync(newFilePath) && fs.existsSync(oldFilePath)) { - return oldFilePath // 如果新文件不存在,且旧文件存在,则返回旧文件路径 - } - return newFilePath - } - return path.join(dir, '/setting.json') -} - function invoke (api, param) { let target = lodash.get(localApi, api) if (target == null) { diff --git a/packages/gui/vue.config.js b/packages/gui/vue.config.js index 7bfd991caf..7efa2453fe 100644 --- a/packages/gui/vue.config.js +++ b/packages/gui/vue.config.js @@ -122,6 +122,7 @@ module.exports = defineConfig({ }, chainWebpackMainProcess (config) { config.entry('mitmproxy').add(path.join(__dirname, 'src/bridge/mitmproxy.js')) + config.entry('dev-sidecar-cli').add(path.join(__dirname, 'src/background/cli.js')) }, }, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74d087b334..878c42fdb5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,6 +87,9 @@ importers: ant-design-vue: specifier: ^1.7.8 version: 1.7.8(vue-template-compiler@2.7.16)(vue@2.7.16) + cac: + specifier: ^6.7.14 + version: 6.7.14 electron-baidu-tongji: specifier: ^1.0.5 version: 1.0.5 @@ -133,6 +136,9 @@ importers: '@vue/cli-service': specifier: ^5.0.8 version: 5.0.8(@vue/compiler-sfc@3.5.12)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.8)(lodash@4.17.21)(sass-loader@16.0.3(sass@1.81.0)(webpack@5.96.1))(underscore@1.13.7)(vue-template-compiler@2.7.16)(vue@2.7.16)(webpack-sources@3.2.3) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 electron: specifier: ^19.1.9 version: 19.1.9 @@ -2239,6 +2245,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + cacache@16.1.3: resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -2771,6 +2781,11 @@ packages: crc@3.8.0: resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + cross-spawn@4.0.2: resolution: {integrity: sha512-yAXz/pA1tD8Gtg2S98Ekf/sewp3Lcp3YoFKJ4Hkp5h5yLWnKVTDU0kwjKJ8NDCYcfTLfyGkzTikst+jWypT1iA==} @@ -9841,6 +9856,8 @@ snapshots: bytes@3.1.2: {} + cac@6.7.14: {} + cacache@16.1.3: dependencies: '@npmcli/fs': 2.1.2 @@ -10262,6 +10279,10 @@ snapshots: buffer: 5.7.1 optional: true + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.5 + cross-spawn@4.0.2: dependencies: lru-cache: 4.1.5