diff --git a/README.md b/README.md index 261fcbb..f559c77 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,10 @@
+## 里程碑 +- 2023-12-01: 发布v1.0.1版本 +- 2023-11-28: 发布v1.0版本 + ## 演示 ![connect server](https://github.com/luckjiawei/frpc-desktop/blob/main/demo/conn.png?raw=true) @@ -31,11 +35,6 @@ ![log](https://github.com/luckjiawei/frpc-desktop/blob/main/demo/log.png?raw=true) -[//]: # (## 里程碑) - -[//]: # (- 2023-11-28: 发布v1.0版本) - - ## License [MIT](LICENSE) diff --git a/electron/api/file.ts b/electron/api/file.ts new file mode 100644 index 0000000..e546f66 --- /dev/null +++ b/electron/api/file.ts @@ -0,0 +1,13 @@ +import {dialog, ipcMain} from "electron"; + +export const initFileApi = () => { + ipcMain.handle("file.selectFile", async (event, args) => { + const result = dialog.showOpenDialogSync({ + properties: ['openFile'], + filters: [ + {name: 'Text Files', extensions: args}, + ] + }) + return result; + }); +} diff --git a/electron/api/frpc.ts b/electron/api/frpc.ts index 7dabac8..09cbbda 100644 --- a/electron/api/frpc.ts +++ b/electron/api/frpc.ts @@ -1,25 +1,18 @@ -import { app, ipcMain } from "electron"; -import { Config, getConfig } from "../storage/config"; -import { listProxy } from "../storage/proxy"; -import { getVersionById } from "../storage/version"; +import {app, ipcMain, Notification} from "electron"; +import {Config, getConfig} from "../storage/config"; +import {listProxy} from "../storage/proxy"; +import {getVersionById} from "../storage/version"; +import treeKill from "tree-kill"; const fs = require("fs"); const path = require("path"); - -const { exec, spawn } = require("child_process"); +const {exec, spawn} = require("child_process"); export let frpcProcess = null; - const runningCmd = { - commandPath: null, - configPath: null + commandPath: null, + configPath: null }; - -// const getFrpc = (config: Config) => { -// getVersionById(config.currentVersion, (err, document) => { -// if (!err) { -// } -// }); -// }; +let frpcStatusListener = null; /** * 获取选择版本的工作目录 @@ -27,75 +20,85 @@ const runningCmd = { * @param callback */ const getFrpcVersionWorkerPath = ( - versionId: string, - callback: (workerPath: string) => void + versionId: string, + callback: (workerPath: string) => void ) => { - getVersionById(versionId, (err2, version) => { - if (!err2) { - callback(version["frpcVersionPath"]); - } - }); + getVersionById(versionId, (err2, version) => { + if (!err2) { + callback(version["frpcVersionPath"]); + } + }); }; /** * 生成配置文件 */ export const generateConfig = ( - config: Config, - callback: (configPath: string) => void + config: Config, + callback: (configPath: string) => void ) => { - listProxy((err3, proxys) => { - if (!err3) { - const proxyToml = proxys.map(m => { - let toml = ` + listProxy((err3, proxys) => { + if (!err3) { + const proxyToml = proxys.map(m => { + let toml = ` [[proxies]] name = "${m.name}" type = "${m.type}" localIP = "${m.localIp}" localPort = ${m.localPort} `; - switch (m.type) { - case "tcp": - toml += `remotePort = ${m.remotePort}`; - break; - case "http": - case "https": - toml += `customDomains=[${m.customDomains.map(m => `"${m}"`)}]`; - break; - default: - break; - } + switch (m.type) { + case "tcp": + toml += `remotePort = ${m.remotePort}`; + break; + case "http": + case "https": + toml += `customDomains=[${m.customDomains.map(m => `"${m}"`)}]`; + break; + default: + break; + } - return toml; - }); - let toml = ` + return toml; + }); + let toml = ` serverAddr = "${config.serverAddr}" serverPort = ${config.serverPort} auth.method = "${config.authMethod}" auth.token = "${config.authToken}" log.to = "frpc.log" -log.level = "debug" -log.maxDays = 3 +log.level = "${config.logLevel}" +log.maxDays = ${config.logMaxDays} webServer.addr = "127.0.0.1" webServer.port = 57400 +transport.tls.enable = ${config.tlsConfigEnable} +${config.tlsConfigEnable ? ` +transport.tls.certFile = "${config.tlsConfigCertFile}" +transport.tls.keyFile = "${config.tlsConfigKeyFile}" +transport.tls.trustedCaFile = "${config.tlsConfigTrustedCaFile}" +transport.tls.serverName = "${config.tlsConfigServerName}" +` : ""} -${proxyToml} + +${proxyToml.join("")} `; - // const configPath = path.join("frp.toml"); - const filename = "frp.toml"; - fs.writeFile( - path.join(app.getPath("userData"), filename), // 配置文件目录 - toml, // 配置文件内容 - { flag: "w" }, - err => { - if (!err) { - callback(filename); - } + // const configPath = path.join("frp.toml"); + const filename = "frp.toml"; + const configPath = path.join(app.getPath("userData"), filename) + console.debug("生成配置成功", configPath) + fs.writeFile( + configPath, // 配置文件目录 + toml, // 配置文件内容 + {flag: "w"}, + err => { + if (!err) { + callback(filename); + } + } + ); } - ); - } - }); + }); }; /** @@ -105,87 +108,133 @@ ${proxyToml} * @param configPath */ const startFrpcProcess = (commandPath: string, configPath: string) => { - const command = `${commandPath} -c ${configPath}`; - frpcProcess = spawn(command, { - cwd: app.getPath("userData"), - shell: true - }); - runningCmd.commandPath = commandPath; - runningCmd.configPath = configPath; - frpcProcess.stdout.on("data", data => { - console.log(`命令输出: ${data}`); - }); - frpcProcess.stdout.on("error", data => { - console.log(`执行错误: ${data}`); - frpcProcess.kill("SIGINT"); - }); + const command = `${commandPath} -c ${configPath}`; + console.info("启动", command) + frpcProcess = spawn(command, { + cwd: app.getPath("userData"), + shell: true + }); + runningCmd.commandPath = commandPath; + runningCmd.configPath = configPath; + frpcProcess.stdout.on("data", data => { + console.debug(`命令输出: ${data}`); + }); + frpcProcess.stdout.on("error", data => { + console.log("启动错误", data) + stopFrpcProcess() + }); + frpcStatusListener = setInterval(() => { + const status = frpcProcessStatus() + if (!status) { + console.log("连接已断开") + new Notification({ + title: "Frpc Desktop", + body: "连接已断开,请前往日志查看原因" + }).show() + clearInterval(frpcStatusListener) + } + }, 3000) }; +/** + * 重载frpc配置 + */ export const reloadFrpcProcess = () => { - if (frpcProcess && !frpcProcess.killed) { - getConfig((err1, config) => { - if (!err1) { - if (config) { - generateConfig(config, configPath => { - const command = `${runningCmd.commandPath} reload -c ${configPath}`; - console.log("重启", command); - exec(command, { - cwd: app.getPath("userData"), - shell: true - }); - }); - } - } - }); - } + if (frpcProcess && !frpcProcess.killed) { + getConfig((err1, config) => { + if (!err1) { + if (config) { + generateConfig(config, configPath => { + const command = `${runningCmd.commandPath} reload -c ${configPath}`; + console.info("重启", command); + exec(command, { + cwd: app.getPath("userData"), + shell: true + }); + }); + } + } + }); + } }; -export const initFrpcApi = () => { - ipcMain.handle("frpc.running", async (event, args) => { +/** + * 停止frpc子进程 + */ +export const stopFrpcProcess = (callback?:() => void) => { + if (frpcProcess) { + treeKill(frpcProcess.pid, (error: Error) => { + if (error) { + console.log("关闭失败", frpcProcess.pid, error) + } else { + console.log('关闭成功') + frpcProcess = null + clearInterval(frpcStatusListener) + } + callback() + }) + } +} + +/** + * 获取frpc子进程状态 + */ +export const frpcProcessStatus = () => { if (!frpcProcess) { - return false; - } else { - return !frpcProcess.killed; + return false; + } + try { + // 发送信号给进程,如果进程存在,会正常返回 + process.kill(frpcProcess.pid, 0); + return true; + } catch (error) { + // 进程不存在,抛出异常 + return false; } - }); +} + + +export const initFrpcApi = () => { + ipcMain.handle("frpc.running", async (event, args) => { + return frpcProcessStatus() + }); - ipcMain.on("frpc.start", async (event, args) => { - getConfig((err1, config) => { - if (!err1) { - if (config) { - getFrpcVersionWorkerPath( - config.currentVersion, - (frpcVersionPath: string) => { - generateConfig(config, configPath => { - const platform = process.platform; - if (platform === 'win32') { - startFrpcProcess( - path.join(frpcVersionPath, "frpc.exe"), - configPath - ); - }else { - startFrpcProcess( - path.join(frpcVersionPath, "frpc"), - configPath - ); + ipcMain.on("frpc.start", async (event, args) => { + getConfig((err1, config) => { + if (!err1) { + if (config) { + getFrpcVersionWorkerPath( + config.currentVersion, + (frpcVersionPath: string) => { + generateConfig(config, configPath => { + const platform = process.platform; + if (platform === 'win32') { + startFrpcProcess( + path.join(frpcVersionPath, "frpc.exe"), + configPath + ); + } else { + startFrpcProcess( + path.join(frpcVersionPath, "frpc"), + configPath + ); + } + }); + } + ); + } else { + event.reply( + "Home.frpc.start.error.hook", + "请先前往设置页面,修改配置后再启动" + ); } - }); } - ); - } else { - event.reply( - "Home.frpc.start.error.hook", - "请先前往设置页面,修改配置后再启动" - ); - } - } + }); }); - }); - ipcMain.on("frpc.stop", () => { - if (frpcProcess && !frpcProcess.killed) { - console.log("关闭"); - frpcProcess.kill(); - } - }); + ipcMain.on("frpc.stop", () => { + if (frpcProcess && !frpcProcess.killed) { + stopFrpcProcess() + } + }); }; diff --git a/electron/api/github.ts b/electron/api/github.ts index 383c986..46e2624 100644 --- a/electron/api/github.ts +++ b/electron/api/github.ts @@ -1,4 +1,4 @@ -import {app, BrowserWindow, ipcMain, net} from "electron"; +import {app, BrowserWindow, ipcMain, net, shell} from "electron"; import {insertVersion} from "../storage/version"; const fs = require("fs"); @@ -162,4 +162,11 @@ export const initGitHubApi = () => { } }); }); + + /** + * 打开GitHub + */ + ipcMain.on("github.open", () => { + shell.openExternal("https://github.com/luckjiawei/frpc-desktop"); + }) }; diff --git a/electron/main/index.ts b/electron/main/index.ts index afe01ba..3a59204 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1,11 +1,12 @@ -import { app, BrowserWindow, ipcMain, shell } from "electron"; -import { release } from "node:os"; -import { join } from "node:path"; -import { initGitHubApi } from "../api/github"; -import { initConfigApi } from "../api/config"; -import { initProxyApi } from "../api/proxy"; -import { initFrpcApi } from "../api/frpc"; -import { initLoggerApi } from "../api/logger"; +import {app, BrowserWindow, ipcMain, Menu, MenuItem, MenuItemConstructorOptions, shell, Tray} from "electron"; +import {release} from "node:os"; +import node_path, {join} from "node:path"; +import {initGitHubApi} from "../api/github"; +import {initConfigApi} from "../api/config"; +import {initProxyApi} from "../api/proxy"; +import {initFrpcApi, stopFrpcProcess} from "../api/frpc"; +import {initLoggerApi} from "../api/logger"; +import {initFileApi} from "../api/file"; // The built directory structure // // ├─┬ dist-electron @@ -19,8 +20,8 @@ import { initLoggerApi } from "../api/logger"; process.env.DIST_ELECTRON = join(__dirname, ".."); process.env.DIST = join(process.env.DIST_ELECTRON, "../dist"); process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL - ? join(process.env.DIST_ELECTRON, "../public") - : process.env.DIST; + ? join(process.env.DIST_ELECTRON, "../public") + : process.env.DIST; // Disable GPU Acceleration for Windows 7 if (release().startsWith("6.1")) app.disableHardwareAcceleration(); @@ -29,8 +30,8 @@ if (release().startsWith("6.1")) app.disableHardwareAcceleration(); if (process.platform === "win32") app.setAppUserModelId(app.getName()); if (!app.requestSingleInstanceLock()) { - app.quit(); - process.exit(0); + app.quit(); + process.exit(0); } // Remove electron security warnings @@ -39,98 +40,159 @@ if (!app.requestSingleInstanceLock()) { // process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true' let win: BrowserWindow | null = null; +let tray = null; // Here, you can also use other preload const preload = join(__dirname, "../preload/index.js"); const url = process.env.VITE_DEV_SERVER_URL; const indexHtml = join(process.env.DIST, "index.html"); +let isQuiting; async function createWindow() { - win = new BrowserWindow({ - title: "Main window", - icon: join(process.env.VITE_PUBLIC, "favicon.ico"), - webPreferences: { - preload, - // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production - // Consider using contextBridge.exposeInMainWorld - // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation - nodeIntegration: true, - contextIsolation: false + win = new BrowserWindow({ + title: "Frpc Desktop", + icon: join(process.env.VITE_PUBLIC, "logo/16x16.png"), + webPreferences: { + preload, + // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production + // Consider using contextBridge.exposeInMainWorld + // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation + nodeIntegration: true, + contextIsolation: false + } + }); + if (process.env.VITE_DEV_SERVER_URL) { + // electron-vite-vue#298 + win.loadURL(url); + // Open devTool if the app is not packaged + win.webContents.openDevTools(); + } else { + win.loadFile(indexHtml); } - }); - - if (process.env.VITE_DEV_SERVER_URL) { - // electron-vite-vue#298 - win.loadURL(url); - // Open devTool if the app is not packaged - win.webContents.openDevTools(); - } else { - win.loadFile(indexHtml); - } - - // Test actively push message to the Electron-Renderer - win.webContents.on("did-finish-load", () => { - win?.webContents.send("main-process-message", new Date().toLocaleString()); - }); - - // Make all links open with the browser, not with the application - win.webContents.setWindowOpenHandler(({ url }) => { - if (url.startsWith("https:")) shell.openExternal(url); - return { action: "deny" }; - }); - - // 隐藏菜单栏 - const { Menu } = require("electron"); - Menu.setApplicationMenu(null); - // hide menu for Mac - if (process.platform !== "darwin") { - app.dock.hide(); - } - // win.webContents.on('will-navigate', (event, url) => { }) #344 + + // Test actively push message to the Electron-Renderer + win.webContents.on("did-finish-load", () => { + win?.webContents.send("main-process-message", new Date().toLocaleString()); + }); + + // Make all links open with the browser, not with the application + win.webContents.setWindowOpenHandler(({url}) => { + if (url.startsWith("https:")) shell.openExternal(url); + return {action: "deny"}; + }); + + // 隐藏菜单栏 + const {Menu} = require("electron"); + Menu.setApplicationMenu(null); + // hide menu for Mac + // if (process.platform !== "darwin") { + // app.dock.hide(); + // } + + win.on('minimize', function (event) { + event.preventDefault(); + win.hide(); + }); + + win.on('close', function (event) { + if (!isQuiting) { + event.preventDefault(); + win.hide(); + if (process.platform === "darwin") { + app.dock.hide(); + } + } + return false; + }); + } -app.whenReady().then(createWindow); +export const createTray = () => { + let menu: Array<(MenuItemConstructorOptions) | (MenuItem)> = [ + { + label: '显示主窗口', click: function () { + win.show(); + if (process.platform === "darwin") { + app.dock.show(); + } + } + }, + { + label: '退出', + click: () => { + isQuiting = true; + stopFrpcProcess(() => { + app.quit(); + }) + } + } + ]; + tray = new Tray(node_path.join(process.env.VITE_PUBLIC, "logo/16x16.png")) + tray.setToolTip('Frpc Desktop') + const contextMenu = Menu.buildFromTemplate(menu) + tray.setContextMenu(contextMenu) + + // 托盘双击打开 + tray.on('double-click', () => { + win.show(); + }) +} + +app.whenReady().then(() => { + createWindow().then(r => { + createTray() + }) +}); app.on("window-all-closed", () => { - win = null; - if (process.platform !== "darwin") app.quit(); + win = null; + if (process.platform !== "darwin") { + stopFrpcProcess(() => { + app.quit(); + }) + } }); app.on("second-instance", () => { - if (win) { - // Focus on the main window if the user tried to open another - if (win.isMinimized()) win.restore(); - win.focus(); - } + if (win) { + // Focus on the main window if the user tried to open another + if (win.isMinimized()) win.restore(); + win.focus(); + } }); app.on("activate", () => { - const allWindows = BrowserWindow.getAllWindows(); - if (allWindows.length) { - allWindows[0].focus(); - } else { - createWindow(); - } + const allWindows = BrowserWindow.getAllWindows(); + if (allWindows.length) { + allWindows[0].focus(); + } else { + createWindow(); + } }); +app.on('before-quit', () => { + isQuiting = true; +}) + // New window example arg: new windows url ipcMain.handle("open-win", (_, arg) => { - const childWindow = new BrowserWindow({ - webPreferences: { - preload, - nodeIntegration: true, - contextIsolation: false + const childWindow = new BrowserWindow({ + webPreferences: { + preload, + nodeIntegration: true, + contextIsolation: false + } + }); + + if (process.env.VITE_DEV_SERVER_URL) { + childWindow.loadURL(`${url}#${arg}`); + } else { + childWindow.loadFile(indexHtml, {hash: arg}); } - }); - - if (process.env.VITE_DEV_SERVER_URL) { - childWindow.loadURL(`${url}#${arg}`); - } else { - childWindow.loadFile(indexHtml, { hash: arg }); - } }); ipcMain.on('open-url', (event, url) => { - shell.openExternal(url).then(r => {}); + shell.openExternal(url).then(r => { + }); }); initGitHubApi(); @@ -138,3 +200,4 @@ initConfigApi(); initProxyApi(); initFrpcApi(); initLoggerApi(); +initFileApi(); diff --git a/electron/storage/config.ts b/electron/storage/config.ts index 625e427..9e486d0 100644 --- a/electron/storage/config.ts +++ b/electron/storage/config.ts @@ -13,6 +13,13 @@ export type Config = { serverPort: number; authMethod: string; authToken: string; + logLevel: string; + logMaxDays: number; + tlsConfigEnable: boolean; + tlsConfigCertFile: string; + tlsConfigKeyFile: string; + tlsConfigTrustedCaFile: string; + tlsConfigServerName: string; }; /** diff --git a/package.json b/package.json index 6b77207..11650e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Frpc-Desktop", - "version": "1.0.0", + "version": "1.0.1", "main": "dist-electron/main/index.js", "description": "一个frpc桌面客户端", "author": "刘嘉伟 <8473136@qq.com>", diff --git a/src/layout/compoenets/LeftMenu.vue b/src/layout/compoenets/LeftMenu.vue index 917bd25..3dd3ecc 100644 --- a/src/layout/compoenets/LeftMenu.vue +++ b/src/layout/compoenets/LeftMenu.vue @@ -3,6 +3,7 @@ import { computed, defineComponent, onMounted, ref } from "vue"; import { Icon } from "@iconify/vue"; import router from "@/router"; import { RouteRecordRaw } from "vue-router"; +import {ipcRenderer} from "electron"; defineComponent({ name: "AppMain" @@ -27,6 +28,10 @@ const handleMenuChange = (route: RouteRecordRaw) => { }); }; +const handleOpenGitHub = () => { + ipcRenderer.send("github.open") +} + onMounted(() => { routes.value = router.options.routes[0].children?.filter( f => !f.meta?.hidden @@ -49,6 +54,12 @@ onMounted(() => { >