🏗️ refactor FrpcProcessService and enhance process management

This commit is contained in:
刘嘉伟 2025-02-24 14:35:11 +08:00
parent 9f46ea781d
commit 6d9f9269b7
13 changed files with 214 additions and 136 deletions

View File

@ -1,5 +1,6 @@
import BaseController from "./BaseController"; import BaseController from "./BaseController";
import FrpcProcessService from "../service/FrpcProcessService"; import FrpcProcessService from "../service/FrpcProcessService";
import { success } from "../utils/response";
class LaunchController extends BaseController { class LaunchController extends BaseController {
private readonly _frpcProcessService: FrpcProcessService; private readonly _frpcProcessService: FrpcProcessService;
@ -10,7 +11,20 @@ class LaunchController extends BaseController {
} }
launch(req: ControllerParam) { launch(req: ControllerParam) {
this._frpcProcessService.startFrpcProcess().then(r => {}); this._frpcProcessService.startFrpcProcess().then(r => {
req.event.reply(req.channel, success());
});
}
terminate(req: ControllerParam) {
this._frpcProcessService.stopFrpcProcess().then(r => {
req.event.reply(req.channel, success());
});
}
getStatus(req: ControllerParam) {
const running = this._frpcProcessService.isRunning();
req.event.reply(req.channel, success(running));
} }
} }

View File

@ -63,8 +63,15 @@ class VersionController extends BaseController {
}); });
} }
importLocalFrpcVersion (){ importLocalFrpcVersion(req: ControllerParam) {
this._versionService
.importLocalFrpcVersion(req.win)
.then(data => {
req.event.reply(req.channel, success());
})
.catch(err => {
req.event.reply(req.channel, fail());
});
} }
} }

View File

@ -52,12 +52,24 @@ export const ipcRouters: IpcRouters = {
deleteDownloadedVersion: { deleteDownloadedVersion: {
path: "version/deleteDownloadedVersion", path: "version/deleteDownloadedVersion",
controller: "versionController.deleteDownloadedVersion" controller: "versionController.deleteDownloadedVersion"
},
importLocalFrpcVersion: {
path: "version/importLocalFrpcVersion",
controller: "versionController.importLocalFrpcVersion"
} }
}, },
LAUNCH: { LAUNCH: {
launch: { launch: {
path: "launch/launch", path: "launch/launch",
controller: "launchController.launch" controller: "launchController.launch"
},
terminate: {
path: "launch/terminate",
controller: "launchController.terminate"
},
getStatus: {
path: "launch/getStatus",
controller: "launchController.getStatus"
} }
}, },
PROXY: { PROXY: {
@ -92,6 +104,10 @@ export const listeners: Listeners = {
watchFrpcLog: { watchFrpcLog: {
listenerMethod: "logService.watchFrpcLog", listenerMethod: "logService.watchFrpcLog",
channel: "log:watchFrpcLog" channel: "log:watchFrpcLog"
},
watchFrpcProcess: {
listenerMethod: "frpcProcessService.watchFrpcProcess",
channel: "frpcProcess:watchFrpcLog"
} }
}; };

View File

@ -24,7 +24,7 @@ class BaseDao<T> {
protected readonly db: Datastore; protected readonly db: Datastore;
constructor(dbName: string) { constructor(dbName: string) {
const dbFilename = path.join(PathUtils.getAppData(), `${dbName}-v2.db`); const dbFilename = path.join(PathUtils.getDataBaseStoragePath(), `${dbName}-v2.db`);
this.db = new Datastore({ this.db = new Datastore({
autoload: true, autoload: true,
filename: dbFilename filename: dbFilename

View File

@ -1,6 +1,6 @@
import BaseDao from "./BaseDao"; import BaseDao from "./BaseDao";
class ServerDao extends BaseDao<FrpcDesktopServer> { class ServerDao extends BaseDao<OpenSourceFrpcDesktopServer> {
constructor() { constructor() {
super("server"); super("server");
} }

View File

@ -199,19 +199,12 @@ app.whenReady().then(() => {
initConfigApi(win); initConfigApi(win);
logInfo(LogModule.APP, `Config API initialized.`); logInfo(LogModule.APP, `Config API initialized.`);
initFrpcApi();
logInfo(LogModule.APP, `FRPC API initialized.`);
// logInfo(LogModule.APP, `Logger API initialized.`);
initFileApi(); initFileApi();
logInfo(LogModule.APP, `File API initialized.`); logInfo(LogModule.APP, `File API initialized.`);
initCommonApi(); initCommonApi();
logInfo(LogModule.APP, `Common API initialized.`); logInfo(LogModule.APP, `Common API initialized.`);
// initUpdaterApi(win); // initUpdaterApi(win);
logInfo(LogModule.APP, `Updater API initialization skipped.`); logInfo(LogModule.APP, `Updater API initialization skipped.`);
} catch (error) { } catch (error) {

View File

@ -3,15 +3,14 @@ import VersionDao from "../dao/VersionDao";
import PathUtils from "../utils/PathUtils"; import PathUtils from "../utils/PathUtils";
import GlobalConstant from "../core/GlobalConstant"; import GlobalConstant from "../core/GlobalConstant";
import { Notification } from "electron"; import { Notification } from "electron";
import { logDebug, LogModule } from "../utils/log"; import { success } from "../utils/response";
import treeKill from "tree-kill";
const { exec, spawn } = require("child_process");
class FrpcProcessService { class FrpcProcessService {
private readonly _serverService: ServerService; private readonly _serverService: ServerService;
private readonly _versionDao: VersionDao; private readonly _versionDao: VersionDao;
private _frpcProcess: any; private _frpcProcess: any;
private _FrpcProcessListener: any; private _frpcProcessListener: any;
constructor(serverService: ServerService, versionDao: VersionDao) { constructor(serverService: ServerService, versionDao: VersionDao) {
this._serverService = serverService; this._serverService = serverService;
@ -19,8 +18,16 @@ class FrpcProcessService {
} }
isRunning(): boolean { isRunning(): boolean {
if (!this._frpcProcess) {
return false; return false;
} }
try {
process.kill(this._frpcProcess.pid, 0);
return true;
} catch (err) {
return false;
}
}
async startFrpcProcess() { async startFrpcProcess() {
const config = await this._serverService.getServerConfig(); const config = await this._serverService.getServerConfig();
@ -28,35 +35,47 @@ class FrpcProcessService {
config.frpcVersion config.frpcVersion
); );
// todo genConfigfile. // todo genConfigfile.
await this._serverService.genTomlConfig(); const configPath = await this._serverService.genTomlConfig();
const configPath = ""; const command = `./${PathUtils.getFrpcFilename()} -c "${configPath}"`;
const command = `${PathUtils.getFrpcFilename()} -c ${configPath}`; this._frpcProcess = require("child_process").spawn(command, {
this._frpcProcess = spawn(command, {
cwd: version.localPath, cwd: version.localPath,
shell: true shell: true
}); });
this._frpcProcess.stdout.on("data", data => { this._frpcProcess.stdout.on("data", data => {
logDebug(LogModule.FRP_CLIENT, `Frpc process output: ${data}`); console.log(`stdout: ${data}`);
}); });
this._frpcProcess.stdout.on("error", data => { this._frpcProcess.stderr.on("data", data => {
// logError(LogModule.FRP_CLIENT, `Frpc process error: ${data}`); console.error(`stderr: ${data}`);
// stopFrpcProcess(() => {});
this.stopFrpcProcess();
}); });
// this._frpcProcess.on("close",function(code){
// console.log("out code:" + code)
// })
} }
stopFrpcProcess() {} async stopFrpcProcess() {
if (this._frpcProcess) {
treeKill(this._frpcProcess.pid, (error: Error) => {
if (error) {
throw error;
} else {
this._frpcProcess = null;
// clearInterval(this._frpcProcessListener);
}
});
} else {
}
}
watchFrpcProcess(listenerParam: ListenerParam) { watchFrpcProcess(listenerParam: ListenerParam) {
this._FrpcProcessListener = setInterval(() => { this._frpcProcessListener = setInterval(() => {
const running = this.isRunning(); const running = this.isRunning();
// todo return status to view. // todo return status to view.
// logDebug( // logDebug(
// LogModule.FRP_CLIENT, // LogModule.FRP_CLIENT,
// `Monitoring frpc process status: ${status}, Listener ID: ${frpcStatusListener}` // `Monitoring frpc process status: ${status}, Listener ID: ${frpcStatusListener}`
// ); // );
console.log("running", running);
if (!running) { if (!running) {
new Notification({ new Notification({
title: GlobalConstant.APP_NAME, title: GlobalConstant.APP_NAME,
@ -66,8 +85,12 @@ class FrpcProcessService {
// LogModule.FRP_CLIENT, // LogModule.FRP_CLIENT,
// "Frpc process status check failed. Connection lost." // "Frpc process status check failed. Connection lost."
// ); // );
clearInterval(this._FrpcProcessListener); // clearInterval(this._frpcProcessListener);
} }
listenerParam.win.webContents.send(
listenerParam.channel,
success(running)
);
}, GlobalConstant.FRPC_PROCESS_STATUS_CHECK_INTERVAL); }, GlobalConstant.FRPC_PROCESS_STATUS_CHECK_INTERVAL);
} }
} }

View File

@ -5,7 +5,7 @@ import fs from "fs";
import PathUtils from "../utils/PathUtils"; import PathUtils from "../utils/PathUtils";
import ProxyDao from "../dao/ProxyDao"; import ProxyDao from "../dao/ProxyDao";
class ServerService extends BaseService<FrpcDesktopServer> { class ServerService extends BaseService<OpenSourceFrpcDesktopServer> {
private readonly _serverDao: ServerDao; private readonly _serverDao: ServerDao;
private readonly _proxyDao: ProxyDao; private readonly _proxyDao: ProxyDao;
private readonly _serverId: string = "1"; private readonly _serverId: string = "1";
@ -17,13 +17,13 @@ class ServerService extends BaseService<FrpcDesktopServer> {
} }
async saveServerConfig( async saveServerConfig(
frpcServer: FrpcDesktopServer frpcServer: OpenSourceFrpcDesktopServer
): Promise<FrpcDesktopServer> { ): Promise<OpenSourceFrpcDesktopServer> {
frpcServer._id = this._serverId; frpcServer._id = this._serverId;
return await this._serverDao.updateById(this._serverId, frpcServer); return await this._serverDao.updateById(this._serverId, frpcServer);
} }
async getServerConfig(): Promise<FrpcDesktopServer> { async getServerConfig(): Promise<OpenSourceFrpcDesktopServer> {
return await this._serverDao.findById(this._serverId); return await this._serverDao.findById(this._serverId);
} }
@ -45,23 +45,19 @@ class ServerService extends BaseService<FrpcDesktopServer> {
.filter(f => f.status === 1) .filter(f => f.status === 1)
.map(proxy => { .map(proxy => {
const { _id, status, ...frpProxyConfig } = proxy; const { _id, status, ...frpProxyConfig } = proxy;
return frpProxyConfig return frpProxyConfig;
}); });
const { frpcVersion, _id, system, ...commonConfig } = server; const { frpcVersion, _id, system, ...commonConfig } = server;
const frpcConfig = { ...commonConfig }; const frpcConfig = { ...commonConfig };
frpcConfig.log.to = PathUtils.getFrpcLogFilePath(); frpcConfig.log.to = PathUtils.getFrpcLogFilePath();
const toml = TOML.stringify({ ...frpcConfig, enabledProxies }); const toml = TOML.stringify({ ...frpcConfig, proxies: enabledProxies });
fs.writeFile( const tomlPath = PathUtils.getTomlConfigFilePath();
PathUtils.getTomlConfigFilePath(), // 配置文件目录 fs.writeFileSync(
tomlPath, // 配置文件目录
toml, // 配置文件内容 toml, // 配置文件内容
{ flag: "w" }, { flag: "w" }
err => {
if (err) {
} else {
// callback(filename);
}
}
); );
return tomlPath;
} }
} }

View File

@ -63,52 +63,13 @@ class VersionService extends BaseService<FrpcVersion> {
onProgress(progress); onProgress(progress);
}, },
onCompleted: () => { onCompleted: () => {
const ext = path.extname(version.assetName); this.decompressFrp(version, downloadedFilePath)
if (ext === GlobalConstant.ZIP_EXT) {
this._fileService.decompressZipFile(
downloadedFilePath,
versionFilePath
);
// todo delete frps and other file.
} else if (
ext === GlobalConstant.GZ_EXT &&
version.assetName.includes(GlobalConstant.TAR_GZ_EXT)
) {
this._fileService.decompressTarGzFile(
downloadedFilePath,
versionFilePath,
() => {
// rename frpc.
const frpcFilePath = path.join(versionFilePath, "frpc");
if (fs.existsSync(frpcFilePath)) {
const newFrpcFilePath = path.join(
versionFilePath,
PathUtils.getFrpcFilename()
);
fs.renameSync(frpcFilePath, newFrpcFilePath);
}
// delete downloaded file.
// todo has bug.
const downloadedFile = path.join(
PathUtils.getDownloadStoragePath(),
version.assetName
);
if (fs.existsSync(downloadedFile)) {
fs.rmSync(downloadedFile, { recursive: true, force: true });
}
}
);
}
// todo 2025-02-23 delete downloaded file.
version.localPath = versionFilePath;
version.downloaded = true;
this._versionDao
.insert(version)
.then(data => { .then(data => {
resolve(data); resolve(data);
}) })
.catch(err => reject(err)); .catch(err => {
reject(err);
});
} }
}); });
}); });
@ -219,7 +180,18 @@ class VersionService extends BaseService<FrpcVersion> {
if (frpName) { if (frpName) {
if (this._currFrpArch.every(item => frpName.includes(item))) { if (this._currFrpArch.every(item => frpName.includes(item))) {
const version = this.getFrpVersionByAssetName(frpName); const version = this.getFrpVersionByAssetName(frpName);
const existsVersion = await this._versionDao.findByGithubReleaseId(
version.githubReleaseId
);
if (existsVersion) {
throw new Error("导入失败,版本已存在");
} }
return this.decompressFrp(version, filePath);
} else {
throw new Error(`导入失败,所选 frp 架构与操作系统不符`);
}
} else {
throw new Error("导入失败,无法识别文件");
} }
} }
} }
@ -227,6 +199,51 @@ class VersionService extends BaseService<FrpcVersion> {
getFrpVersionByAssetName(assetName: string) { getFrpVersionByAssetName(assetName: string) {
return this._versions.find(f => f.assetName === assetName); return this._versions.find(f => f.assetName === assetName);
} }
async decompressFrp(version: FrpcVersion, compressedPath: string) {
const versionFilePath = path.join(
PathUtils.getVersionStoragePath(),
SecureUtils.calculateMD5(version.name)
);
const ext = path.extname(version.assetName);
if (ext === GlobalConstant.ZIP_EXT) {
this._fileService.decompressZipFile(compressedPath, versionFilePath);
// todo delete frps and other file.
} else if (
ext === GlobalConstant.GZ_EXT &&
version.assetName.includes(GlobalConstant.TAR_GZ_EXT)
) {
this._fileService.decompressTarGzFile(
compressedPath,
versionFilePath,
() => {
// rename frpc.
const frpcFilePath = path.join(versionFilePath, "frpc");
if (fs.existsSync(frpcFilePath)) {
const newFrpcFilePath = path.join(
versionFilePath,
PathUtils.getFrpcFilename()
);
fs.renameSync(frpcFilePath, newFrpcFilePath);
}
// delete downloaded file.
// todo has bug.
const downloadedFile = path.join(
PathUtils.getDownloadStoragePath(),
version.assetName
);
if (fs.existsSync(downloadedFile)) {
fs.rmSync(downloadedFile, { recursive: true, force: true });
}
}
);
}
// todo 2025-02-23 delete downloaded file.
version.localPath = versionFilePath;
version.downloaded = true;
return await this._versionDao.insert(version);
}
} }
export default VersionService; export default VersionService;

View File

@ -38,6 +38,12 @@ class PathUtils {
return app.getPath("userData"); return app.getPath("userData");
} }
public static getDataBaseStoragePath() {
const result = path.join(PathUtils.getAppData(), "db");
FileUtils.mkdir(result);
return result;
}
public static getTomlConfigFilePath() { public static getTomlConfigFilePath() {
return path.join( return path.join(
PathUtils.getConfigStoragePath(), PathUtils.getConfigStoragePath(),

View File

@ -7,7 +7,7 @@ import { useDebounceFn } from "@vueuse/core";
import { clone } from "@/utils/clone"; import { clone } from "@/utils/clone";
import { Base64 } from "js-base64"; import { Base64 } from "js-base64";
import IconifyIconOffline from "@/components/IconifyIcon/src/iconifyIconOffline"; import IconifyIconOffline from "@/components/IconifyIcon/src/iconifyIconOffline";
import { on, send } from "@/utils/ipcUtils"; import { on, removeRouterListeners, send } from "@/utils/ipcUtils";
import { ipcRouters } from "../../../electron/core/IpcRouter"; import { ipcRouters } from "../../../electron/core/IpcRouter";
defineComponent({ defineComponent({
@ -25,7 +25,8 @@ type ShareLinkConfig = {
metaToken: string; metaToken: string;
}; };
const defaultFormData = ref<OpenSourceFrpcDesktopServer>({ const defaultFormData: OpenSourceFrpcDesktopServer = {
_id: "",
frpcVersion: null, frpcVersion: null,
loginFailExit: false, loginFailExit: false,
udpPacketSize: 1500, udpPacketSize: 1500,
@ -77,9 +78,9 @@ const defaultFormData = ref<OpenSourceFrpcDesktopServer>({
autoConnectOnStartup: false autoConnectOnStartup: false
}, },
user: "" user: ""
}); };
const formData = ref<OpenSourceFrpcDesktopServer>(defaultFormData.value); const formData = ref<OpenSourceFrpcDesktopServer>(defaultFormData);
const loading = ref(1); const loading = ref(1);
@ -201,7 +202,6 @@ const handleSubmit = useDebounceFn(() => {
loading.value = 1; loading.value = 1;
const data = clone(formData.value); const data = clone(formData.value);
send(ipcRouters.SERVER.saveConfig, data); send(ipcRouters.SERVER.saveConfig, data);
// ipcRenderer.send("server/saveConfig", data);
} }
}); });
}, 300); }, 300);
@ -222,12 +222,12 @@ const handleAuthMethodChange = e => {
}; };
const checkAndResetVersion = () => { const checkAndResetVersion = () => {
const currentVersion = formData.value.currentVersion; const currentVersion = formData.value.frpcVersion;
if ( if (
currentVersion && currentVersion &&
!versions.value.some(item => item.id === currentVersion) !versions.value.some(item => item.githubReleaseId === currentVersion)
) { ) {
formData.value.currentVersion = null; formData.value.frpcVersion = null;
} }
}; };
@ -239,14 +239,15 @@ const handleLoadSavedConfig = () => {
send(ipcRouters.SERVER.getServerConfig); send(ipcRouters.SERVER.getServerConfig);
}; };
onMounted(() => { onMounted(() => {
handleLoadDownloadedVersion(); handleLoadDownloadedVersion();
handleLoadSavedConfig(); handleLoadSavedConfig();
on(ipcRouters.SERVER.getServerConfig, data => { on(ipcRouters.SERVER.getServerConfig, data => {
console.log("data", data); console.log("data", data);
if (data) {
formData.value = data; formData.value = data;
}
loading.value--; loading.value--;
}); });
@ -263,8 +264,6 @@ onMounted(() => {
}); });
loading.value--; loading.value--;
}); });
// ipcRenderer.send("config.getConfig");
// handleLoadVersions();
// ipcRenderer.on("Config.getConfig.hook", (event, args) => { // ipcRenderer.on("Config.getConfig.hook", (event, args) => {
// const { err, data } = args; // const { err, data } = args;
// if (!err) { // if (!err) {
@ -448,7 +447,6 @@ const handlePasteServerConfigBase64 = useDebounceFn(() => {
} }
const ciphertext = pasteServerConfigBase64.value.replace("frp://", ""); const ciphertext = pasteServerConfigBase64.value.replace("frp://", "");
const plaintext = Base64.decode(ciphertext); const plaintext = Base64.decode(ciphertext);
console.log("plain", plaintext);
let serverConfig: ShareLinkConfig = null; let serverConfig: ShareLinkConfig = null;
try { try {
serverConfig = JSON.parse(plaintext); serverConfig = JSON.parse(plaintext);
@ -512,12 +510,14 @@ const handleOpenDataFolder = useDebounceFn(() => {
}, 1000); }, 1000);
onUnmounted(() => { onUnmounted(() => {
ipcRenderer.removeAllListeners("Config.getConfig.hook"); removeRouterListeners(ipcRouters.SERVER.saveConfig);
ipcRenderer.removeAllListeners("Config.saveConfig.hook"); removeRouterListeners(ipcRouters.VERSION.getDownloadedVersions);
ipcRenderer.removeAllListeners("Config.versions.hook"); removeRouterListeners(ipcRouters.SERVER.getServerConfig);
ipcRenderer.removeAllListeners("Config.exportConfig.hook"); removeRouterListeners(ipcRouters.SERVER.saveConfig);
ipcRenderer.removeAllListeners("Config.clearAll.hook"); removeRouterListeners(ipcRouters.VERSION.getDownloadedVersions);
ipcRenderer.removeAllListeners("Config.openDataFolder.hook"); // ipcRenderer.removeAllListeners("Config.exportConfig.hook");
// ipcRenderer.removeAllListeners("Config.clearAll.hook");
// ipcRenderer.removeAllListeners("Config.openDataFolder.hook");
}); });
</script> </script>
<template> <template>

View File

@ -1,12 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { defineComponent, onMounted, onUnmounted, ref } from "vue"; import { defineComponent, onMounted, onUnmounted, ref } from "vue";
import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue"; import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue";
import { ipcRenderer } from "electron"; import { useDebounceFn } from "@vueuse/core";
import { ElMessageBox } from "element-plus"; import { on, onListener, removeRouterListeners2, send } from "@/utils/ipcUtils";
import router from "@/router"; import { ipcRouters, listeners } from "../../../electron/core/IpcRouter";
import { useDebounceFn, useIntervalFn } from "@vueuse/core";
import { send } from "@/utils/ipcUtils";
import { ipcRouters } from "../../../electron/core/IpcRouter";
defineComponent({ defineComponent({
name: "Home" name: "Home"
@ -15,12 +12,11 @@ defineComponent({
const running = ref(false); const running = ref(false);
const handleStartFrpc = () => { const handleStartFrpc = () => {
// ipcRenderer.send("frpc.start");
send(ipcRouters.LAUNCH.launch); send(ipcRouters.LAUNCH.launch);
}; };
const handleStopFrpc = () => { const handleStopFrpc = () => {
ipcRenderer.send("frpc.stop"); send(ipcRouters.LAUNCH.terminate);
}; };
const handleButtonClick = useDebounceFn(() => { const handleButtonClick = useDebounceFn(() => {
@ -32,30 +28,40 @@ const handleButtonClick = useDebounceFn(() => {
}, 300); }, 300);
onMounted(() => { onMounted(() => {
useIntervalFn(() => { onListener(listeners.watchFrpcProcess, data => {
ipcRenderer.invoke("frpc.running").then(data => { console.log("watchFrpcProcess", data);
running.value = data; running.value = data;
console.log("进程状态", data);
}); });
}, 500);
ipcRenderer.on("Home.frpc.start.error.hook", (event, args) => { on(ipcRouters.LAUNCH.getStatus, data => {
if (args) { running.value = data;
ElMessageBox.alert(args, "提示", {
showCancelButton: true,
cancelButtonText: "取消",
confirmButtonText: "去设置"
}).then(() => {
router.replace({
name: "Config"
}); });
on(ipcRouters.LAUNCH.launch, () => {
send(ipcRouters.LAUNCH.getStatus);
}); });
}
on(ipcRouters.LAUNCH.terminate, () => {
send(ipcRouters.LAUNCH.getStatus);
}); });
// ipcRenderer.on("Home.frpc.start.error.hook", (event, args) => {
// if (args) {
// ElMessageBox.alert(args, "", {
// showCancelButton: true,
// cancelButtonText: "",
// confirmButtonText: ""
// }).then(() => {
// router.replace({
// name: "Config"
// });
// });
// }
// });
}); });
onUnmounted(() => { onUnmounted(() => {
ipcRenderer.removeAllListeners("Home.frpc.start.error.hook"); // ipcRenderer.removeAllListeners("Home.frpc.start.error.hook");
removeRouterListeners2(listeners.watchFrpcProcess);
}); });
</script> </script>

View File

@ -13,7 +13,7 @@ interface FrpcSystemConfiguration {
type FrpcDesktopServer = BaseEntity & type FrpcDesktopServer = BaseEntity &
FrpcCommonConfig & { FrpcCommonConfig & {
frpcVersion: number; frpcVersion: number;
system: any; // system: any;
}; };
type FrpcVersion = BaseEntity & { type FrpcVersion = BaseEntity & {