feat: try no gui mode

pull/437/head
starknt 2025-01-20 01:21:08 +08:00
parent 3ae6282870
commit 164532f3b7
12 changed files with 742 additions and 557 deletions

View File

@ -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 () {

View File

@ -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) {

View File

@ -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)

View File

@ -0,0 +1,8 @@
____ _____ _ __
/ __ \___ _ __ / ___/(_)___/ /__ _________ ______
/ / / / _ \ | / /_____\__ \/ / __ / _ \/ ___/ __ `/ ___/
/ /_/ / __/ |/ /_____/__/ / / /_/ / __/ /__/ /_/ / /
/_____/\___/|___/ /____/_/\__,_/\___/\___/\__,_/_/
==================== 开发者边车 ====================

View File

@ -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",

View File

@ -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')
argv = process.argv
}
if (startHideWindow) {
hideWin()
}
win.on('closed', async (...args) => {
log.info('win closed:', ...args)
win = null
tray = null
const cliProcess = fork(cli, argv, {
env: {
...process.env,
NO_CONSOLE_LOG: true,
},
detached: true,
stdio: 'inherit',
})
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)
}
})
cliProcess.unref()
}
async function beforeQuit () {
return DevSidecar.api.shutdown()
async function startGUI () {
}
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()
})

View File

@ -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()

View File

@ -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/')
}

View File

@ -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()
})

View File

@ -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) {

View File

@ -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'))
},
},
},

View File

@ -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