diff --git a/electron/config/config.default.js b/electron/config/config.default.js index 5b866c3..a3fa67b 100644 --- a/electron/config/config.default.js +++ b/electron/config/config.default.js @@ -82,6 +82,18 @@ module.exports = (appInfo) => { url: 'https://discuz.chat/' // Any web url }; + /** + * 内置java服务 默认关闭 + */ + config.javaServer = { + enable: false, // 是否启用,true时,启动程序时,会自动启动 build/extraResources/app.jar 下的 java程序 + port: 18080, // 端口,端口被占用时随机一个端口,并通知前端修改请求地址。 + jreVersion: 'jre1.8.0_201', // build/extraResources/目录下 jre 文件夹名称 + // java 启动参数,该参数可以根据自己需求自由发挥 + opt: '-server -Xms512M -Xmx512M -Xss512k -Dspring.profiles.active=prod -Dserver.port=${port} -Dlogging.file.path="${path}" ', + name: 'app.jar' // build/extraResources/目录下 jar 名称 + } + /** * 内置socket服务 */ diff --git a/frontend/src/App.vue b/frontend/src/App.vue index e904022..6b84c49 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -5,6 +5,8 @@ diff --git a/main.js b/main.js index 0994e20..3467626 100644 --- a/main.js +++ b/main.js @@ -1,4 +1,6 @@ const Appliaction = require('ee-core').Appliaction; +const getPort = require('get-port'); +const { app } = require('electron'); class Main extends Appliaction { @@ -12,6 +14,26 @@ class Main extends Appliaction { */ async ready () { // do some things + + await this.createJavaPorts(); + await this.startJava(); + } + + async createJavaPorts() { + if (this.config.javaServer.enable) { + const javaPort = await getPort({ port: this.config.javaServer.port }); + process.env.EE_JAVA_PORT = javaPort; + this.config.javaServer.port = javaPort; + } + // 更新config配置 + this.getCoreDB().setItem("config", this.config); + } + + async startJava() { + this.logger.info("[main] startJava start"); + const javaServer = require("./public/lib/javaServer"); + javaServer.start(this); + this.logger.info("[main] startJava end"); } /** @@ -34,6 +56,16 @@ class Main extends Appliaction { win.show(); }) } + + const self = this; + this.electron.mainWindow.webContents.on("did-finish-load", () => { + const updateFrontend = require('./public/lib/updateFrontend'); + updateFrontend.install(self); + }); + + app.on("before-quit", async () => { + await this.killJava(); + }); } /** @@ -43,6 +75,13 @@ class Main extends Appliaction { // do some things } + + async killJava() { + if (this.config.javaServer.enable) { + const javaServer = require("./public/lib/javaServer"); + await javaServer.kill(this); + } + } } new Main(); diff --git a/package.json b/package.json index ba381ed..1cee481 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "dayjs": "^1.10.7", "ee-core": "^1.4.0", "electron-is": "^3.0.0", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "table-parser": "^0.1.3" } } diff --git a/public/lib/javaServer.js b/public/lib/javaServer.js new file mode 100644 index 0000000..aa6e53f --- /dev/null +++ b/public/lib/javaServer.js @@ -0,0 +1,124 @@ +"use strict"; + +const _ = require("lodash"); +const assert = require("assert"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { execSync } = require("child_process"); +const Utils = require("ee-core").Utils; +const ps = require("./ps"); + +function getCoreDB() { + const Storage = require("ee-core").Storage; + return Storage.JsonDB.connection("system"); +} + +function getJavaPort() { + const cdb = getCoreDB(); + const port = cdb.getItem("config").javaServer.port; + return port; +} + +function getJarName() { + const cdb = getCoreDB(); + return cdb.getItem("config").javaServer.name; +} + +function getOpt(port, path) { + const cdb = getCoreDB(); + const opt = cdb.getItem("config").javaServer.opt; + let javaOpt = _.replace(opt, "${port}", port); + return _.replace(javaOpt, "${path}", path); +} + +function getJreVersion() { + const cdb = getCoreDB(); + return cdb.getItem("config").javaServer.jreVersion; +} + +async function start(app) { + const options = app.config.javaServer; + if (!options.enable) { + return; + } + + let port = process.env.EE_JAVA_PORT ? parseInt(process.env.EE_JAVA_PORT) : parseInt(getJavaPort()); + assert(typeof port === "number", "java port required, and must be a number"); + app.logger.info("[javaServer] java server port is:", port); + + const jarName = getJarName(); + let softwarePath = path.join(Utils.getExtraResourcesDir(), jarName); + app.logger.info("[javaServer] jar存放路径:", softwarePath); + + const logPath = Utils.getLogDir() + + // 检查程序是否存在 + if (!fs.existsSync(softwarePath)) { + app.logger.info("[javaServer] java程序不存在", softwarePath); + } + + const JAVA_OPT = getOpt(port, logPath); + if (os.platform() === "win32") { + let jrePath = path.join( + Utils.getExtraResourcesDir(), + getJreVersion(), + "bin", + "javaw.exe" + ); + // 命令行字符串 并 执行 + let cmdStr = `start ${jrePath} -jar ${JAVA_OPT} ${softwarePath}`; + app.logger.info("[javaServer] cmdStr:", cmdStr); + await execSync(cmdStr); + } else { + // 不受信任请执行: sudo spctl --master-disable + let jrePath = path.join( + Utils.getExtraResourcesDir(), + getJreVersion(), + "Contents", + "Home", + "bin", + "java" + ); + // 命令行字符串 并 执行 + // let cmdStr = `${jrePath} -jar ${JAVA_OPT} ${softwarePath} > /Users/tangyh/Downloads/app.log`; + let cmdStr = `nohup ${jrePath} -jar ${JAVA_OPT} ${softwarePath} >/dev/null 2>&1 &`; + app.logger.info("[javaServer] cmdStr:", cmdStr); + await execSync(cmdStr); + } +} + +async function kill(app) { + const port = getJavaPort(); + const jarName = getJarName(); + app.logger.info("[javaServer] kill port: ", port); + + if (os.platform() === "win32") { + const resultList = ps.lookup({ + command: "java", + where: 'caption="javaw.exe"', + arguments: jarName, + }); + + app.logger.info("[javaServer] resultList:", resultList); + resultList.forEach((item) => { + ps.kill(item.pid, "SIGKILL", (err) => { + if (err) { + throw new Error(err); + } + app.logger.info("[javaServer] 已经退出后台程序: %O", item); + }); + }); + + // const cmd = `for /f "tokens=1-5" %i in ('netstat -ano ^| findstr ":${port}"') do taskkill /F /T /PID %m`; + // const a = await execSync(cmd, {encoding: 'utf-8'}); + // app.logger.info("[javaServer] kill:", a); + } else { + const cmd = `ps -ef | grep java | grep ${jarName} | grep -v grep | awk '{print $2}' | xargs kill -9`; + const result = await execSync(cmd); + app.logger.info("[javaServer] kill:", result != null ? result.toString(): ''); + } +} + +module.exports.start = start; +module.exports.kill = kill; diff --git a/public/lib/ps.js b/public/lib/ps.js new file mode 100644 index 0000000..ad096c9 --- /dev/null +++ b/public/lib/ps.js @@ -0,0 +1,264 @@ +/** + * 本文件修改至 https://github.com/neekey/ps + * 原因是: + * 1. lookup 只提供了异步方式 + * 2. lookup 性能太差 + */ + +var ChildProcess = require("child_process"); +var IS_WIN = process.platform === "win32"; +var TableParser = require("table-parser"); +/** + * End of line. + * Basically, the EOL should be: + * - windows: \r\n + * - *nix: \n + * But i'm trying to get every possibilities covered. + */ +var EOL = /(\r\n)|(\n\r)|\n|\r/; +var SystemEOL = require("os").EOL; + +/** + * Execute child process + * @type {Function} + * @param {String[]} args + * @param {String} where + * @param {Function} callback + * @param {Object=null} callback.err + * @param {Object[]} callback.stdout + */ +var Exec = function (args, where) { + var spawnSync = ChildProcess.spawnSync; + var execSync = ChildProcess.execSync; + + // on windows, if use ChildProcess.exec(`wmic process get`), the stdout will gives you nothing + // that's why I use `cmd` instead + if (IS_WIN) { + const cmd = `wmic process where ${where} get ProcessId,ParentProcessId,CommandLine \n`; + const result = execSync(cmd); + if (!result) { + throw new Error(result); + } + + var stdout = result.toString(); + + var beginRow; + stdout = stdout.split(EOL); + + // Find the line index for the titles + stdout.forEach(function (out, index) { + if ( + out && + typeof beginRow == "undefined" && + out.indexOf("CommandLine") === 0 + ) { + beginRow = index; + } + }); + + // get rid of the start (copyright) and the end (current pwd) + stdout.splice(stdout.length - 1, 1); + stdout.splice(0, beginRow); + + return stdout.join(SystemEOL) || false; + } else { + if (typeof args === "string") { + args = args.split(/\s+/); + } + const result = spawnSync("ps", args); + if (result.stderr && !!result.stderr.toString()) { + throw new Error(result.stderr); + } else { + return result.stdout.toString(); + } + } +}; + +/** + * Query Process: Focus on pid & cmd + * @param query + * @param {String|String[]} query.pid + * @param {String} query.command RegExp String + * @param {String} query.arguments RegExp String + * @param {String|array} query.psargs + * @param {String|array} query.where where 条件 + * @param {Function} callback + * @param {Object=null} callback.err + * @param {Object[]} callback.processList + * @return {Object} + */ + +exports.lookup = function (query) { + /** + * add 'lx' as default ps arguments, since the default ps output in linux like "ubuntu", wont include command arguments + */ + var exeArgs = query.psargs || ["lx"]; + var where = query.where || 'name="javaw.exe"'; + var filter = {}; + var idList; + + // Lookup by PID + if (query.pid) { + if (Array.isArray(query.pid)) { + idList = query.pid; + } else { + idList = [query.pid]; + } + + // Cast all PIDs as Strings + idList = idList.map(function (v) { + return String(v); + }); + } + + if (query.command) { + filter["command"] = new RegExp(query.command, "i"); + } + + if (query.arguments) { + filter["arguments"] = new RegExp(query.arguments, "i"); + } + + if (query.ppid) { + filter["ppid"] = new RegExp(query.ppid); + } + + const result = Exec(exeArgs, where); + + var processList = parseGrid(result); + var resultList = []; + + processList.forEach(function (p) { + var flt; + var type; + var result = true; + + if (idList && idList.indexOf(String(p.pid)) < 0) { + return; + } + + for (type in filter) { + flt = filter[type]; + result = flt.test(p[type]) ? result : false; + } + + if (result) { + resultList.push(p); + } + }); + + return resultList; +}; + +/** + * Kill process + * @param pid + * @param {Object|String} signal + * @param {String} signal.signal + * @param {number} signal.timeout + * @param next + */ + +exports.kill = function (pid, signal, next) { + //opts are optional + if (arguments.length == 2 && typeof signal == "function") { + next = signal; + signal = undefined; + } + + var checkTimeoutSeconds = (signal && signal.timeout) || 30; + + if (typeof signal === "object") { + signal = signal.signal; + } + + try { + process.kill(pid, signal); + } catch (e) { + return next && next(e); + } + + var checkConfident = 0; + var checkTimeoutTimer = null; + var checkIsTimeout = false; + + function checkKilled(finishCallback) { + exports.lookup({ pid: pid }, function (err, list) { + if (checkIsTimeout) return; + + if (err) { + clearTimeout(checkTimeoutTimer); + finishCallback && finishCallback(err); + } else if (list.length > 0) { + checkConfident = checkConfident - 1 || 0; + checkKilled(finishCallback); + } else { + checkConfident++; + if (checkConfident === 5) { + clearTimeout(checkTimeoutTimer); + finishCallback && finishCallback(); + } else { + checkKilled(finishCallback); + } + } + }); + } + + next && checkKilled(next); + + checkTimeoutTimer = + next && + setTimeout(function () { + checkIsTimeout = true; + next(new Error("Kill process timeout")); + }, checkTimeoutSeconds * 1000); +}; + +/** + * Parse the stdout into readable object. + * @param {String} output + */ + +function parseGrid(output) { + if (!output) { + return []; + } + return formatOutput(TableParser.parse(output)); +} + +/** + * format the structure, extract pid, command, arguments, ppid + * @param data + * @return {Array} + */ + +function formatOutput(data) { + var formatedData = []; + data.forEach(function (d) { + var pid = + (d.PID && d.PID[0]) || (d.ProcessId && d.ProcessId[0]) || undefined; + var cmd = d.CMD || d.CommandLine || d.COMMAND || undefined; + var ppid = + (d.PPID && d.PPID[0]) || + (d.ParentProcessId && d.ParentProcessId[0]) || + undefined; + + if (pid && cmd) { + var command = cmd[0]; + var args = ""; + + if (cmd.length > 1) { + args = cmd.slice(1); + } + + formatedData.push({ + pid: pid, + command: command, + arguments: args, + ppid: ppid, + }); + } + }); + + return formatedData; +} diff --git a/public/lib/updateFrontend.js b/public/lib/updateFrontend.js new file mode 100644 index 0000000..34ee1c7 --- /dev/null +++ b/public/lib/updateFrontend.js @@ -0,0 +1,13 @@ +/** 修改前端配置 */ +module.exports = { + install(eeApp) { + if (eeApp.config.javaServer.enable) { + let javaServerPrefix = `http://localhost:${eeApp.config.javaServer.port}`; + + const mainWindow = eeApp.electron.mainWindow; + const channel = "app.javaPort"; + mainWindow.webContents.send(channel, javaServerPrefix); + console.log('send'); + } + }, +};