diff --git a/src/electron/config/config.default.ts b/src/electron/config/config.default.ts new file mode 100644 index 0000000..ac87532 --- /dev/null +++ b/src/electron/config/config.default.ts @@ -0,0 +1,78 @@ +'use strict'; + +const path = require('path'); +const { getBaseDir } = require('ee-core/ps'); + +/** + * 默认配置 + */ +module.exports = () => { + return { + openDevTools: false, + singleLock: true, + windowsOption: { + title: 'electron-egg', + width: 980, + height: 650, + minWidth: 400, + minHeight: 300, + webPreferences: { + //webSecurity: false, + contextIsolation: false, // false -> 可在渲染进程中使用electron的api,true->需要bridge.js(contextBridge) + nodeIntegration: true, + //preload: path.join(getElectronDir(), 'preload', 'bridge.js'), + }, + frame: true, + show: true, + icon: path.join(getBaseDir(), 'public', 'images', 'logo-32.png'), + }, + logger: { + level: 'INFO', + outputJSON: false, + appLogName: 'ee.log', + coreLogName: 'ee-core.log', + errorLogName: 'ee-error.log' + }, + remote: { + enable: false, + url: 'http://electron-egg.kaka996.com/' + }, + socketServer: { + enable: false, + port: 7070, + path: "/socket.io/", + connectTimeout: 45000, + pingTimeout: 30000, + pingInterval: 25000, + maxHttpBufferSize: 1e8, + transports: ["polling", "websocket"], + cors: { + origin: true, + }, + channel: 'c1' + }, + httpServer: { + enable: false, + https: { + enable: false, + key: '/public/ssl/localhost+1.key', + cert: '/public/ssl/localhost+1.pem' + }, + host: '127.0.0.1', + port: 7071, + }, + mainServer: { + indexPath: '/public/dist/index.html', + }, + customize: { + tray: { + title: 'EE程序', + icon: '/public/images/tray.png' + }, + awaken: { + protocol: 'ee', + args: [] + } + }, + } +} diff --git a/src/electron/config/config.local.ts b/src/electron/config/config.local.ts new file mode 100644 index 0000000..20ecc4b --- /dev/null +++ b/src/electron/config/config.local.ts @@ -0,0 +1,15 @@ +'use strict'; + +/** + * Development environment configuration, coverage config.default.js + */ +module.exports = () => { + return { + openDevTools: { + mode: 'bottom' + }, + jobs: { + messageLog: true + } + }; +}; diff --git a/src/electron/config/config.prod.ts b/src/electron/config/config.prod.ts new file mode 100644 index 0000000..db1ce01 --- /dev/null +++ b/src/electron/config/config.prod.ts @@ -0,0 +1,10 @@ +'use strict'; + +/** + * coverage config.default.js + */ +module.exports = () => { + return { + openDevTools: false, + }; +}; diff --git a/src/electron/controller/child/friend/school.ts b/src/electron/controller/child/friend/school.ts new file mode 100644 index 0000000..d89582b --- /dev/null +++ b/src/electron/controller/child/friend/school.ts @@ -0,0 +1,18 @@ +'use strict'; + +/** + * school + * @class + */ +class SchoolController { + + /** + * test + */ + async test () { + return 'hello electron-egg'; + } +} + +SchoolController.toString = () => '[class SchoolController]'; +module.exports = SchoolController; \ No newline at end of file diff --git a/src/electron/controller/child/tool.ts b/src/electron/controller/child/tool.ts new file mode 100644 index 0000000..143d960 --- /dev/null +++ b/src/electron/controller/child/tool.ts @@ -0,0 +1,18 @@ +'use strict'; + +/** + * tool + * @class + */ +class ToolController { + + /** + * test + */ + async test () { + return 'hello electron-egg'; + } +} + +ToolController.toString = () => '[class ToolController]'; +module.exports = ToolController; \ No newline at end of file diff --git a/src/electron/controller/child/user.ts b/src/electron/controller/child/user.ts new file mode 100644 index 0000000..169f11f --- /dev/null +++ b/src/electron/controller/child/user.ts @@ -0,0 +1,18 @@ +'use strict'; + +/** + * user + * @class + */ +class UserController { + + /** + * test + */ + async test () { + return 'hello electron-egg'; + } +} + +UserController.toString = () => '[class UserController]'; +module.exports = UserController; \ No newline at end of file diff --git a/src/electron/controller/cross.ts b/src/electron/controller/cross.ts new file mode 100644 index 0000000..a289912 --- /dev/null +++ b/src/electron/controller/cross.ts @@ -0,0 +1,65 @@ +'use strict'; + +const { crossService } = require('../service/cross'); + +/** + * Cross + * @class + */ +class CrossController { + + /** + * View process service information + */ + info() { + crossService.info(); + return 'hello electron-egg'; + } + + /** + * Get service url + */ + async getUrl(args) { + const { name } = args; + const serverUrl = crossService.getUrl(name); + return serverUrl; + } + + /** + * kill service + * By default (modifiable), killing the process will exit the electron application. + */ + async killServer(args) { + const { type, name } = args; + crossService.killServer(type, name); + return; + } + + /** + * create service + */ + async createServer(args) { + const { program } = args; + if (program == 'go') { + crossService.createGoServer(); + } else if (program == 'java') { + crossService.createJavaServer(); + } else if (program == 'python') { + crossService.createPythonServer(); + } + + return; + } + + /** + * Access the api for the cross service + */ + async requestApi(args) { + const { name, urlPath, params} = args; + const data = await crossService.requestApi(name, urlPath, params); + return data; + } +} + +CrossController.toString = () => '[class CrossController]'; +module.exports = CrossController; \ No newline at end of file diff --git a/src/electron/controller/effect.ts b/src/electron/controller/effect.ts new file mode 100644 index 0000000..f4c692a --- /dev/null +++ b/src/electron/controller/effect.ts @@ -0,0 +1,66 @@ +'use strict'; + +const { dialog } = require('electron'); +const _ = require('lodash'); +const { getMainWindow } = require('ee-core/electron/window'); + +/** + * effect - demo + * @class + */ +class EffectController { + + /** + * select file + */ + selectFile() { + const filePaths = dialog.showOpenDialogSync({ + properties: ['openFile'] + }); + + if (_.isEmpty(filePaths)) { + return null + } + + return filePaths[0]; + } + + /** + * login window + */ + loginWindow(args) { + const { width, height } = args; + const win = getMainWindow(); + + const size = { + width: width || 400, + height: height || 300 + } + win.setSize(size.width, size.height); + win.setResizable(true); + win.center(); + win.show(); + win.focus(); + } + + /** + * restore window + */ + restoreWindow(args) { + const { width, height } = args; + const win = getMainWindow(); + + const size = { + width: width || 980, + height: height || 650 + } + win.setSize(size.width, size.height); + win.setResizable(true); + win.center(); + win.show(); + win.focus(); + } +} + +EffectController.toString = () => '[class EffectController]'; +module.exports = EffectController; \ No newline at end of file diff --git a/src/electron/controller/example.ts b/src/electron/controller/example.ts new file mode 100644 index 0000000..7ec8da5 --- /dev/null +++ b/src/electron/controller/example.ts @@ -0,0 +1,18 @@ +'use strict'; + +/** + * example + * @class + */ +class ExampleController { + + /** + * test + */ + async test () { + return 'hello electron-egg'; + } +} + +ExampleController.toString = () => '[class ExampleController]'; +module.exports = ExampleController; \ No newline at end of file diff --git a/src/electron/controller/framework.ts b/src/electron/controller/framework.ts new file mode 100644 index 0000000..1033f8a --- /dev/null +++ b/src/electron/controller/framework.ts @@ -0,0 +1,253 @@ +'use strict'; + +const dayjs = require('dayjs'); +const path = require('path'); +const fs = require('fs'); +const { exec } = require('child_process'); +const { app: electronApp, shell } = require('electron'); +const { getExtraResourcesDir } = require('ee-core/ps'); +const { logger } = require('ee-core/log'); +const { getConfig } = require('ee-core/config'); +const { frameworkService } = require('../service/framework'); +const { sqlitedbService } = require('../service/database/sqlitedb'); + +/** + * framework - demo + * @class + */ +class FrameworkController { + + /** + * 所有方法接收两个参数 + * @param args 前端传的参数 + * @param event - ipc通信时才有值。详情见:控制器文档 + */ + + /** + * sqlite数据库操作 + */ + async sqlitedbOperation(args) { + const { action, info, delete_name, update_name, update_age, search_age, data_dir } = args; + + const data = { + action, + result: null, + all_list: [], + code: 0 + }; + + try { + // test + sqlitedbService.getDataDir(); + } catch (err) { + console.log(err); + data.code = -1; + return data; + } + + switch (action) { + case 'add' : + data.result = await sqlitedbService.addTestDataSqlite(info);; + break; + case 'del' : + data.result = await sqlitedbService.delTestDataSqlite(delete_name);; + break; + case 'update' : + data.result = await sqlitedbService.updateTestDataSqlite(update_name, update_age); + break; + case 'get' : + data.result = await sqlitedbService.getTestDataSqlite(search_age); + break; + case 'getDataDir' : + data.result = await sqlitedbService.getDataDir(); + break; + case 'setDataDir' : + data.result = await sqlitedbService.setCustomDataDir(data_dir); + break; + } + + data.all_list = await sqlitedbService.getAllTestDataSqlite(); + + return data; + } + + /** + * 调用其它程序(exe、bash等可执行程序) + * + */ + openSoftware(args) { + const { softName } = args; + const softwarePath = path.join(getExtraResourcesDir(), softName); + logger.info('[openSoftware] softwarePath:', softwarePath); + + // 检查程序是否存在 + if (!fs.existsSync(softwarePath)) { + return false; + } + // 命令行字符串 并 执行, start 命令后面的路径要加双引号 + const cmdStr = `start "${softwarePath}"`; + exec(cmdStr); + + // 方法二 + // 使用cross模块 + + return true; + } + + /** + * 检测http服务是否开启 + */ + async checkHttpServer() { + const { enable, protocol, host, port } = getConfig().httpServer; + const url = protocol + host + ':' + port; + console.log('[checkHttpServer] url:', url); + const data = { + enable: enable, + server: url + } + return data; + } + + /** + * 一个 http 请求 + * args 是 前端传的参数 + * ctx 是 koa 的 ctx 对象 + */ + async doHttpRequest(args, ctx) { + const httpInfo = { + args, + method: ctx.request.method, + query: ctx.request.query, + body: ctx.request.body + } + logger.info('httpInfo:', httpInfo); + + const { id } = args; + if (!id) { + return false; + } + const dir = electronApp.getPath(id); + shell.openPath(dir); + + return true; + } + + /** + * 一个socket io请求访问此方法 + */ + async doSocketRequest(args) { + const { id } = args; + if (!id) { + return false; + } + const dir = electronApp.getPath(id); + shell.openPath(dir); + + return true; + } + + /** + * 异步消息类型 + */ + async ipcInvokeMsg(args) { + let timeNow = dayjs().format('YYYY-MM-DD HH:mm:ss'); + const data = args + ' - ' + timeNow; + + return data; + } + + /** + * 同步消息类型 + */ + async ipcSendSyncMsg(args) { + let timeNow = dayjs().format('YYYY-MM-DD HH:mm:ss'); + const data = args + ' - ' + timeNow; + + return data; + } + + /** + * 双向异步通信 + */ + ipcSendMsg(args, event) { + const { type, content } = args; + const data = frameworkService.bothWayMessage(type, content, event); + + return data; + } + + /** + * 任务 + */ + someJob(args, event) { + const { jobId, action} = args; + let result; + + switch (action) { + case 'create': + result = frameworkService.doJob(jobId, action, event); + break; + case 'close': + frameworkService.doJob(jobId, action, event); + break; + case 'pause': + frameworkService.doJob(jobId, action, event); + break; + case 'resume': + frameworkService.doJob(jobId, action, event); + break; + default: + } + + let data = { + jobId, + action, + result + } + return data; + } + + /** + * 创建任务池 + */ + async createPool(args, event) { + let num = args.number; + frameworkService.doCreatePool(num, event); + + // test monitor + frameworkService.monitorJob(); + + return; + } + + /** + * 通过进程池执行任务 + */ + someJobByPool(args, event) { + const { jobId, action } = args; + let result; + switch (action) { + case 'run': + result = frameworkService.doJobByPool(jobId, action, event); + break; + default: + } + + let data = { + jobId, + action, + result + } + return data; + } + + /** + * 测试接口 + */ + hello(args) { + logger.info('hello ', args); + } +} + +FrameworkController.toString = () => '[class FrameworkController]'; +module.exports = FrameworkController; \ No newline at end of file diff --git a/src/electron/controller/os.ts b/src/electron/controller/os.ts new file mode 100644 index 0000000..3ebfd30 --- /dev/null +++ b/src/electron/controller/os.ts @@ -0,0 +1,176 @@ +'use strict'; + +const _ = require('lodash'); +const fs = require('fs'); +const path = require('path'); +const { + app: electronApp, dialog, shell, Notification, +} = require('electron'); +const { windowService } = require('../service/os/window'); + +/** + * example + * @class + */ +class OsController { + + /** + * All methods receive two parameters + * @param args Parameters transmitted by the frontend + * @param event - Event are only available during IPC communication. For details, please refer to the controller documentation + */ + + /** + * Message prompt dialog box + */ + messageShow() { + dialog.showMessageBoxSync({ + type: 'info', // "none", "info", "error", "question" 或者 "warning" + title: 'Custom Title', + message: 'Customize message content', + detail: 'Other additional information' + }) + + return 'Opened the message box'; + } + + /** + * Message prompt and confirmation dialog box + */ + messageShowConfirm() { + const res = dialog.showMessageBoxSync({ + type: 'info', + title: 'Custom Title', + message: 'Customize message content', + detail: 'Other additional information', + cancelId: 1, // Index of buttons used to cancel dialog boxes + defaultId: 0, // Set default selected button + buttons: ['confirm', 'cancel'], + }) + let data = (res === 0) ? 'click the confirm button' : 'click the cancel button'; + + return data; + } + + /** + * Select Directory + */ + selectFolder() { + const filePaths = dialog.showOpenDialogSync({ + properties: ['openDirectory', 'createDirectory'] + }); + + if (_.isEmpty(filePaths)) { + return null + } + + return filePaths[0]; + } + + /** + * open directory + */ + openDirectory(args) { + const { id } = args; + if (!id) { + return false; + } + let dir = ''; + if (path.isAbsolute(id)) { + dir = id; + } else { + dir = electronApp.getPath(id); + } + + shell.openPath(dir); + return true; + } + + /** + * Select Picture + */ + selectPic() { + const filePaths = dialog.showOpenDialogSync({ + title: 'select pic', + properties: ['openFile'], + filters: [ + { name: 'Images', extensions: ['jpg', 'png', 'gif'] }, + ] + }); + if (_.isEmpty(filePaths)) { + return null + } + + try { + const data = fs.readFileSync(filePaths[0]); + const pic = 'data:image/jpeg;base64,' + data.toString('base64'); + return pic; + } catch (err) { + console.error(err); + return null; + } + } + + /** + * Open a new window + */ + createWindow(args) { + const wcid = windowService.createWindow(args); + return wcid; + } + + /** + * Get Window contents id + */ + getWCid(args) { + const wcid = windowService.getWCid(args); + return wcid; + } + + /** + * Realize communication between two windows through the transfer of the main process + */ + window1ToWindow2(args, event) { + windowService.communicate(args, event); + return; + } + + /** + * Realize communication between two windows through the transfer of the main process + */ + window2ToWindow1(args, event) { + windowService.communicate(args, event); + return; + } + + /** + * Create system notifications + */ + sendNotification(args, event) { + const { title, subtitle, body, silent} = args; + + if (!Notification.isSupported()) { + return '当前系统不支持通知'; + } + + let options = {}; + if (!_.isEmpty(title)) { + options.title = title; + } + if (!_.isEmpty(subtitle)) { + options.subtitle = subtitle; + } + if (!_.isEmpty(body)) { + options.body = body; + } + if (!_.isEmpty(silent)) { + options.silent = silent; + } + windowService.createNotification(options, event); + + return true + } +} + +OsController.toString = () => '[class OsController]'; +module.exports = OsController; \ No newline at end of file diff --git a/src/electron/jobs/example/hello.ts b/src/electron/jobs/example/hello.ts new file mode 100644 index 0000000..a4b0250 --- /dev/null +++ b/src/electron/jobs/example/hello.ts @@ -0,0 +1,11 @@ +'use strict'; + +const { logger } = require('ee-core/log'); + +function welcome() { + logger.info('[child-process] [jobs/example/hello] welcome ! '); +} + +module.exports = { + welcome +}; \ No newline at end of file diff --git a/src/electron/jobs/example/timer.ts b/src/electron/jobs/example/timer.ts new file mode 100644 index 0000000..ed70364 --- /dev/null +++ b/src/electron/jobs/example/timer.ts @@ -0,0 +1,88 @@ +'use strict'; + +const { logger } = require('ee-core/log'); +const { isChildJob, exit } = require('ee-core/ps'); +const { childMessage } = require('ee-core/message'); +const { welcome } = require('./hello'); +const { UserService } = require('../../service/job/user'); + +/** + * example - TimerJob + * @class + */ +class TimerJob { + + constructor(params) { + this.params = params; + this.timer = undefined; + this.timeoutTimer = undefined; + this.number = 0; + this.countdown = 10; // 倒计时 + } + + /** + * handle()方法是必要的,且会被自动调用 + */ + async handle () { + logger.info("[child-process] TimerJob params: ", this.params); + const { jobId } = this.params; + + // 子进程中使用service + // 1. 确保引入的 service 中不能有electron 的 api或依赖, electron 不支持 + const userService = new UserService(); + userService.hello('job'); + + // 执行任务 + this.doTimer(jobId); + } + + /** + * 暂停任务运行 + */ + async pause(jobId) { + logger.info("[child-process] Pause timerJob, jobId: ", jobId); + clearInterval(this.timer); + clearInterval(this.timeoutTimer); + } + + /** + * 恢复任务运行 + */ + async resume(jobId, pid) { + logger.info("[child-process] Resume timerJob, jobId: ", jobId, ", pid: ", pid); + this.doTimer(jobId); + } + + /** + * 运行任务 + */ + async doTimer(jobId) { + // 计时器模拟任务 + const eventName = 'job-timer-progress-' + jobId; + this.timer = setInterval(() => { + welcome(); + + childMessage.send(eventName, {jobId, number: this.number, end: false}); + this.number++; + this.countdown--; + }, 1000); + + // 用 setTimeout 模拟任务运行时长 + this.timeoutTimer = setTimeout(() => { + // 关闭计时器模拟任务 + clearInterval(this.timer); + + // 任务结束,重置前端显示 + childMessage.send(eventName, {jobId, number:0, pid:0, end: true}); + + // 如果是childJob任务,必须调用 exit() 方法,让进程退出,否则会常驻内存 + // 如果是childPoolJob任务,常驻内存,等待下一个业务 + if (isChildJob()) { + exit(); + } + }, this.countdown * 1000) + } +} + +TimerJob.toString = () => '[class TimerJob]'; +module.exports = TimerJob; diff --git a/src/electron/main.ts b/src/electron/main.ts new file mode 100644 index 0000000..b005896 --- /dev/null +++ b/src/electron/main.ts @@ -0,0 +1,19 @@ +const { ElectronEgg } = require('ee-core'); +const { Lifecycle } = require('./preload/lifecycle'); +const { preload } = require('./preload'); + +// new app +const app = new ElectronEgg(); + +// register lifecycle +const life = new Lifecycle(); +app.register("ready", life.ready); +app.register("electron-app-ready", life.electronAppReady); +app.register("window-ready", life.windowReady); +app.register("before-close", life.beforeClose); + +// register preload +app.register("preload", preload); + +// run +app.run(); \ No newline at end of file diff --git a/src/electron/preload/bridge.ts b/src/electron/preload/bridge.ts new file mode 100644 index 0000000..c9b86db --- /dev/null +++ b/src/electron/preload/bridge.ts @@ -0,0 +1,10 @@ +/* + * 如果启用了上下文隔离,渲染进程无法使用electron的api, + * 可通过contextBridge 导出api给渲染进程使用 + */ + +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('electron', { + ipcRenderer: ipcRenderer, +}) \ No newline at end of file diff --git a/src/electron/preload/index.ts b/src/electron/preload/index.ts new file mode 100644 index 0000000..d2c53a7 --- /dev/null +++ b/src/electron/preload/index.ts @@ -0,0 +1,15 @@ +/************************************************* + ** preload为预加载模块,该文件将会在程序启动时加载 ** + *************************************************/ + +function preload() { + // 示例功能模块,可选择性使用和修改 + console.log('preload/index.js'); +} + +/** +* 预加载模块入口 +*/ +module.exports = { + preload +} \ No newline at end of file diff --git a/src/electron/preload/lifecycle.ts b/src/electron/preload/lifecycle.ts new file mode 100644 index 0000000..396be77 --- /dev/null +++ b/src/electron/preload/lifecycle.ts @@ -0,0 +1,51 @@ +'use strict'; + +const { getConfig } = require('ee-core/config'); +const { getMainWindow } = require('ee-core/electron'); + +class Lifecycle { + + /** + * core app have been loaded + */ + async ready() { + // do some things + console.log('[lifecycle] ready'); + } + + /** + * electron app ready + */ + async electronAppReady() { + // do some things + console.log('[lifecycle] electron-app-ready'); + } + + /** + * main window have been loaded + */ + async windowReady() { + console.log('[lifecycle] window-ready'); + // 延迟加载,无白屏 + const { windowsOption } = getConfig(); + if (windowsOption.show == false) { + const win = getMainWindow(); + win.once('ready-to-show', () => { + win.show(); + win.focus(); + }) + } + } + + /** + * before app close + */ + async beforeClose() { + console.log('[lifecycle] before-close'); + } +} + +Lifecycle.toString = () => '[class Lifecycle]'; +module.exports = { + Lifecycle +}; \ No newline at end of file diff --git a/src/electron/service/cross.ts b/src/electron/service/cross.ts new file mode 100644 index 0000000..c850d06 --- /dev/null +++ b/src/electron/service/cross.ts @@ -0,0 +1,151 @@ +'use strict'; + +const { logger } = require('ee-core/log'); +const { getExtraResourcesDir } = require('ee-core/ps'); +const path = require("path"); +const axios = require('axios'); +const { is } = require('ee-core/utils'); +const { cross } = require('ee-core/cross'); + +/** + * cross + * @class + */ +class CrossService { + + info() { + const pids = cross.getPids(); + logger.info('cross pids:', pids); + + let num = 1; + pids.forEach(pid => { + let entity = cross.getProc(pid); + logger.info(`server-${num} name:${entity.name}`); + logger.info(`server-${num} config:`, entity.config); + num++; + }) + + return 'hello electron-egg'; + } + + getUrl(name) { + const serverUrl = cross.getUrl(name); + return serverUrl; + } + + killServer(type, name) { + if (type == 'all') { + cross.killAll(); + } else { + cross.killByName(name); + } + } + + /** + * create go service + * In the default configuration, services can be started with applications. + * Developers can turn off the configuration and create it manually. + */ + async createGoServer() { + // method 1: Use the default Settings + //const entity = await cross.run(serviceName); + + // method 2: Use custom configuration + const serviceName = "go"; + const opt = { + name: 'goapp', + cmd: path.join(getExtraResourcesDir(), 'goapp'), + directory: getExtraResourcesDir(), + args: ['--port=7073'], + appExit: true, + } + const entity = await cross.run(serviceName, opt); + logger.info('server name:', entity.name); + logger.info('server config:', entity.config); + logger.info('server url:', entity.getUrl()); + + return; + } + + /** + * create java server + */ + async createJavaServer() { + const serviceName = "java"; + const jarPath = path.join(getExtraResourcesDir(), 'java-app.jar'); + const opt = { + name: 'javaapp', + cmd: path.join(getExtraResourcesDir(), 'jre1.8.0_201/bin/javaw.exe'), + directory: getExtraResourcesDir(), + args: ['-jar', '-server', '-Xms512M', '-Xmx512M', '-Xss512k', '-Dspring.profiles.active=prod', `-Dserver.port=18080`, `-Dlogging.file.path=${Ps.getLogDir()}`, `${jarPath}`], + appExit: false, + } + if (is.macOS()) { + // Setup Java program + opt.cmd = path.join(getExtraResourcesDir(), 'jre1.8.0_201.jre/Contents/Home/bin/java'); + } + if (is.linux()) { + // Setup Java program + } + + const entity = await cross.run(serviceName, opt); + logger.info('server name:', entity.name); + logger.info('server config:', entity.config); + logger.info('server url:', cross.getUrl(entity.name)); + + return; + } + + /** + * create python service + * In the default configuration, services can be started with applications. + * Developers can turn off the configuration and create it manually. + */ + async createPythonServer() { + // method 1: Use the default Settings + //const entity = await cross.run(serviceName); + + // method 2: Use custom configuration + const serviceName = "python"; + const opt = { + name: 'pyapp', + cmd: path.join(getExtraResourcesDir(), 'py', 'pyapp'), + directory: path.join(getExtraResourcesDir(), 'py'), + args: ['--port=7074'], + windowsExtname: true, + appExit: true, + } + const entity = await cross.run(serviceName, opt); + logger.info('server name:', entity.name); + logger.info('server config:', entity.config); + logger.info('server url:', entity.getUrl()); + + return; + } + + async requestApi(name, urlPath, params) { + const serverUrl = cross.getUrl(name); + const apiHello = serverUrl + urlPath; + console.log('Server Url:', serverUrl); + + const response = await axios({ + method: 'get', + url: apiHello, + timeout: 1000, + params, + proxy: false, + }); + if (response.status == 200) { + const { data } = response; + return data; + } + + return null; + } +} + +CrossService.toString = () => '[class CrossService]'; +module.exports = { + CrossService, + crossService: new CrossService() +}; \ No newline at end of file diff --git a/src/electron/service/database/basedb.ts b/src/electron/service/database/basedb.ts new file mode 100644 index 0000000..0d5d05a --- /dev/null +++ b/src/electron/service/database/basedb.ts @@ -0,0 +1,52 @@ +'use strict'; + +const { SqliteStorage } = require('ee-core/storage'); +const { getDataDir } = require('ee-core/ps'); +const path = require('path'); + +/** + * sqlite数据存储 + * @class + */ +class BasedbService { + + constructor(options) { + const { dbname } = options; + this.dbname = dbname; + this.db = undefined; + this._init(); + } + + /* + * 初始化 + */ + _init() { + // 定义数据文件 + const dbFile = path.join(getDataDir(), "db", this.dbname); + const sqliteOptions = { + timeout: 6000, + verbose: console.log + } + this.storage = new SqliteStorage(dbFile, sqliteOptions); + this.db = this.storage.db; + } + + /* + * change data dir (sqlite) + */ + changeDataDir(dir) { + // the absolute path of the db file + const dbFile = path.join(dir, this.dbname); + const sqliteOptions = { + timeout: 6000, + verbose: console.log + } + this.storage = new SqliteStorage(dbFile, sqliteOptions); + this.db = this.storage.db; + } +} + +BasedbService.toString = () => '[class BasedbService]'; +module.exports = { + BasedbService, +}; \ No newline at end of file diff --git a/src/electron/service/database/sqlitedb.ts b/src/electron/service/database/sqlitedb.ts new file mode 100644 index 0000000..3707e06 --- /dev/null +++ b/src/electron/service/database/sqlitedb.ts @@ -0,0 +1,112 @@ +'use strict'; + +const { BasedbService } = require('./basedb'); +const _ = require('lodash'); + +/** + * sqlite数据存储 + * @class + */ +class SqlitedbService extends BasedbService { + + constructor () { + const options = { + dbname: 'sqlite-demo.db', + } + super(options); + this.userTableName = 'user'; + this._initTable(); + } + + /* + * 初始化表 + */ + _initTable() { + // 检查表是否存在 + const masterStmt = this.db.prepare('SELECT * FROM sqlite_master WHERE type=? AND name = ?'); + let tableExists = masterStmt.get('table', this.userTableName); + if (!tableExists) { + // 创建表 + const create_user_table_sql = + `CREATE TABLE ${this.userTableName} + ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name CHAR(50) NOT NULL, + age INT + );` + this.db.exec(create_user_table_sql); + } + } + + /* + * 增 Test data (sqlite) + */ + async addTestDataSqlite(data) { + const insert = this.db.prepare(`INSERT INTO ${this.userTableName} (name, age) VALUES (@name, @age)`); + insert.run(data); + return true; + } + + /* + * 删 Test data (sqlite) + */ + async delTestDataSqlite(name = '') { + const delUser = this.db.prepare(`DELETE FROM ${this.userTableName} WHERE name = ?`); + delUser.run(name); + return true; + } + + /* + * 改 Test data (sqlite) + */ + async updateTestDataSqlite(name= '', age = 0) { + const updateUser = this.db.prepare(`UPDATE ${this.userTableName} SET age = ? WHERE name = ?`); + updateUser.run(age, name); + return true; + } + + /* + * 查 Test data (sqlite) + */ + async getTestDataSqlite(age = 0) { + const selectUser = this.db.prepare(`SELECT * FROM ${this.userTableName} WHERE age = @age`); + const users = selectUser.all({age: age}); + return users; + } + + /* + * all Test data (sqlite) + */ + async getAllTestDataSqlite() { + const selectAllUser = this.db.prepare(`SELECT * FROM ${this.userTableName} `); + const allUser = selectAllUser.all(); + return allUser; + } + + /* + * get data dir (sqlite) + */ + async getDataDir() { + const dir = this.storage.getStorageDir(); + return dir; + } + + /* + * set custom data dir (sqlite) + */ + async setCustomDataDir(dir) { + if (_.isEmpty(dir)) { + return; + } + + this.changeDataDir(dir); + this._initTable(); + return; + } +} + +SqlitedbService.toString = () => '[class SqlitedbService]'; +module.exports = { + SqlitedbService, + sqlitedbService: new SqlitedbService() +}; diff --git a/src/electron/service/effect.ts b/src/electron/service/effect.ts new file mode 100644 index 0000000..06bee6e --- /dev/null +++ b/src/electron/service/effect.ts @@ -0,0 +1,30 @@ +'use strict'; + +const { logger } = require('ee-core/log'); + +/** + * effect + * @class + */ +class EffectService { + + /** + * hello + */ + async hello(args) { + let obj = { + status:'ok', + params: args + } + logger.info('EffectService obj:', obj); + + return obj; + } + +} + +EffectService.toString = () => '[class EffectService]'; +module.exports = { + EffectService, + effectService: new EffectService() +}; \ No newline at end of file diff --git a/src/electron/service/example.ts b/src/electron/service/example.ts new file mode 100644 index 0000000..d7fab41 --- /dev/null +++ b/src/electron/service/example.ts @@ -0,0 +1,32 @@ +'use strict'; + +const { logger } = require('ee-core/log'); + +/** + * 示例服务 + * @class + */ +class ExampleService { + + /** + * test + */ + async test(args) { + let obj = { + status:'ok', + params: args + } + + logger.info('ExampleService obj:', obj); + + //Services.get('framework').test('egg'); + + return obj; + } +} + +ExampleService.toString = () => '[class ExampleService]'; +module.exports = { + ExampleService, + exampleService: new ExampleService() +}; \ No newline at end of file diff --git a/src/electron/service/framework.ts b/src/electron/service/framework.ts new file mode 100644 index 0000000..9c219f5 --- /dev/null +++ b/src/electron/service/framework.ts @@ -0,0 +1,163 @@ +'use strict'; + +const { logger } = require('ee-core/log'); +const { ChildJob, ChildPoolJob } = require('ee-core/jobs'); + +/** + * framework + * @class + */ +class FrameworkService { + + constructor() { + // 在构造函数中初始化一些变量 + this.myTimer = null; + this.myJob = new ChildJob(); + this.myJobPool = new ChildPoolJob(); + this.taskForJob = {}; + } + + /** + * test + */ + async test(args) { + let obj = { + status:'ok', + params: args + } + logger.info('FrameworkService obj:', obj); + return obj; + } + + /** + * ipc通信(双向) + */ + bothWayMessage(type, content, event) { + // 前端ipc频道 channel + const channel = 'controller.framework.ipcSendMsg'; + + if (type == 'start') { + // 每隔1秒,向前端页面发送消息 + // 用定时器模拟 + this.myTimer = setInterval(function(e, c, msg) { + let timeNow = Date.now(); + let data = msg + ':' + timeNow; + e.reply(`${c}`, data) + }, 1000, event, channel, content) + + return '开始了' + } else if (type == 'end') { + clearInterval(this.myTimer); + return '停止了' + } else { + return 'ohther' + } + } + + /** + * 执行任务 + */ + doJob(jobId, action, event) { + let res = {}; + let oneTask; + const channel = 'controller.framework.timerJobProgress'; + if (action == 'create') { + // 执行任务及监听进度 + let eventName = 'job-timer-progress-' + jobId; + const timerTask = this.myJob.exec('./jobs/example/timer', {jobId}); + timerTask.emitter.on(eventName, (data) => { + logger.info('[main-process] timerTask, from TimerJob data:', data); + // 发送数据到渲染进程 + event.sender.send(`${channel}`, data) + }) + + // 执行任务及监听进度 异步 + // myjob.execPromise('./jobs/example/timer', {jobId}).then(task => { + // task.emitter.on(eventName, (data) => { + // Log.info('[main-process] timerTask, from TimerJob data:', data); + // // 发送数据到渲染进程 + // event.sender.send(`${channel}`, data) + // }) + // }); + + res.pid = timerTask.pid; + this.taskForJob[jobId] = timerTask; + } + if (action == 'close') { + oneTask = this.taskForJob[jobId]; + oneTask.kill(); + event.sender.send(`${channel}`, {jobId, number:0, pid:0}); + } + if (action == 'pause') { + oneTask = this.taskForJob[jobId]; + oneTask.callFunc('./jobs/example/timer', 'pause', jobId); + } + if (action == 'resume') { + oneTask = this.taskForJob[jobId]; + oneTask.callFunc('./jobs/example/timer', 'resume', jobId, oneTask.pid); + } + + return res; + } + + + + /** + * 创建pool + */ + doCreatePool(num, event) { + const channel = 'controller.framework.createPoolNotice'; + this.myJobPool.create(num).then(pids => { + event.reply(`${channel}`, pids); + }); + } + + /** + * 通过进程池执行任务 + */ + doJobByPool(jobId, action, event) { + let res = {}; + const channel = 'controller.framework.timerJobProgress'; + if (action == 'run') { + // 异步-执行任务及监听进度 + this.myJobPool.runPromise('./jobs/example/timer', {jobId}).then(task => { + + // 监听器名称唯一,否则会出现重复监听。 + // 任务完成时,需要移除监听器,防止内存泄漏 + let eventName = 'job-timer-progress-' + jobId; + task.emitter.on(eventName, (data) => { + logger.info('[main-process] [ChildPoolJob] timerTask, from TimerJob data:', data); + + // 发送数据到渲染进程 + event.sender.send(`${channel}`, data) + + // 如果收到任务完成的消息,移除监听器 + if (data.end) { + task.emitter.removeAllListeners(eventName); + } + }); + + res.pid = task.pid; + }); + } + return res; + } + + /** + * 获取正在运行的 job 进程 + */ + monitorJob() { + setInterval(() => { + let jobPids = this.myJob.getPids(); + let jobPoolPids = this.myJobPool.getPids(); + logger.info(`[main-process] [monitorJob] jobPids: ${jobPids}, jobPoolPids: ${jobPoolPids}`); + }, 5000) + } + +} + +FrameworkService.toString = () => '[class FrameworkService]'; +module.exports = { + FrameworkService, + frameworkService: new FrameworkService() +}; \ No newline at end of file diff --git a/src/electron/service/job/user.ts b/src/electron/service/job/user.ts new file mode 100644 index 0000000..e463365 --- /dev/null +++ b/src/electron/service/job/user.ts @@ -0,0 +1,26 @@ +'use strict'; + +const { logger } = require('ee-core/log'); + +// The service used in the job should not rely on Electron's API, as it may cause errors +class UserService { + + /** + * hello + */ + async hello(args) { + let obj = { + status:'ok', + params: args + } + logger.info('UserService obj:', obj); + + return obj; + } + +} + +UserService.toString = () => '[class UserService]'; +module.exports = { + UserService +}; \ No newline at end of file diff --git a/src/electron/service/os/auto_updater.ts b/src/electron/service/os/auto_updater.ts new file mode 100644 index 0000000..84135f1 --- /dev/null +++ b/src/electron/service/os/auto_updater.ts @@ -0,0 +1,166 @@ +const { app: electronApp } = require('electron'); +const { autoUpdater } = require("electron-updater"); +const { is } = require('ee-core/utils'); +const { logger } = require('ee-core/log'); +const { getConfig } = require('ee-core/config'); +const { getMainWindow, setCloseAndQuit } = require('ee-core/electron/window'); + +/** + * 自动升级 + * @class + */ +class AutoUpdater { + + constructor() { + } + + /** + * 创建 + */ + create () { + logger.info('[addon:autoUpdater] load'); + const cfg = getConfig().customize.autoUpdater; + if ((is.windows() && cfg.windows) + || (is.macOS() && cfg.macOS) + || (is.linux() && cfg.linux)) + { + // continue + } else { + return + } + + // 是否检查更新 + if (cfg.force) { + this.checkUpdate(); + } + + const status = { + error: -1, + available: 1, + noAvailable: 2, + downloading: 3, + downloaded: 4, + } + + const version = electronApp.getVersion(); + logger.info('[addon:autoUpdater] current version: ', version); + + // 设置下载服务器地址 + let server = cfg.options.url; + let lastChar = server.substring(server.length - 1); + server = lastChar === '/' ? server : server + "/"; + cfg.options.url = server; + + // 是否后台自动下载 + autoUpdater.autoDownload = cfg.force ? true : false; + + try { + autoUpdater.setFeedURL(cfg.options); + } catch (error) { + logger.error('[addon:autoUpdater] setFeedURL error : ', error); + } + + autoUpdater.on('checking-for-update', () => { + //sendStatusToWindow('正在检查更新...'); + }) + autoUpdater.on('update-available', (info) => { + info.status = status.available; + info.desc = '有可用更新'; + this.sendStatusToWindow(info); + }) + autoUpdater.on('update-not-available', (info) => { + info.status = status.noAvailable; + info.desc = '没有可用更新'; + this.sendStatusToWindow(info); + }) + autoUpdater.on('error', (err) => { + const info = { + status: status.error, + desc: err + } + this.sendStatusToWindow(info); + }) + autoUpdater.on('download-progress', (progressObj) => { + let percentNumber = parseInt(progressObj.percent); + let totalSize = this.bytesChange(progressObj.total); + let transferredSize = this.bytesChange(progressObj.transferred); + let text = '已下载 ' + percentNumber + '%'; + text = text + ' (' + transferredSize + "/" + totalSize + ')'; + + let info = { + status: status.downloading, + desc: text, + percentNumber: percentNumber, + totalSize: totalSize, + transferredSize: transferredSize + } + logger.info('[addon:autoUpdater] progress: ', text); + this.sendStatusToWindow(info); + }) + autoUpdater.on('update-downloaded', (info) => { + info.status = status.downloaded; + info.desc = '下载完成'; + this.sendStatusToWindow(info); + + // 托盘插件里面设置了阻止窗口关闭,这里设置允许关闭窗口 + setCloseAndQuit(true); + + // Install updates and exit the application + autoUpdater.quitAndInstall(); + }); + } + + /** + * 检查更新 + */ + checkUpdate () { + autoUpdater.checkForUpdates(); + } + + /** + * 下载更新 + */ + download () { + autoUpdater.downloadUpdate(); + } + + /** + * 向前端发消息 + */ + sendStatusToWindow(content = {}) { + const textJson = JSON.stringify(content); + const channel = 'custom.app.updater'; + const win = getMainWindow(); + win.webContents.send(channel, textJson); + } + + /** + * 单位转换 + */ + bytesChange (limit) { + let size = ""; + if(limit < 0.1 * 1024){ + size = limit.toFixed(2) + "B"; + }else if(limit < 0.1 * 1024 * 1024){ + size = (limit/1024).toFixed(2) + "KB"; + }else if(limit < 0.1 * 1024 * 1024 * 1024){ + size = (limit/(1024 * 1024)).toFixed(2) + "MB"; + }else{ + size = (limit/(1024 * 1024 * 1024)).toFixed(2) + "GB"; + } + + let sizeStr = size + ""; + let index = sizeStr.indexOf("."); + let dou = sizeStr.substring(index + 1 , index + 3); + if(dou == "00"){ + return sizeStr.substring(0, index) + sizeStr.substring(index + 3, index + 5); + } + + return size; + } +} + +AutoUpdater.toString = () => '[class AutoUpdater]'; +module.exports = { + autoUpdater: new AutoUpdater() +}; \ No newline at end of file diff --git a/src/electron/service/os/window.ts b/src/electron/service/os/window.ts new file mode 100644 index 0000000..f648d94 --- /dev/null +++ b/src/electron/service/os/window.ts @@ -0,0 +1,132 @@ +'use strict'; + +const path = require('path'); +const { app: electronApp } = require('electron'); +const { BrowserWindow, Notification } = require('electron'); +const { getMainWindow } = require('ee-core/electron/window'); +const { isProd, getBaseDir } = require('ee-core/ps'); + +/** + * Window + * @class + */ +class WindowService { + + constructor() { + this.myNotification = null; + this.windows = {} + } + + /** + * Create a new window + */ + createWindow(args) { + const { type, content, windowName, windowTitle } = args; + let contentUrl = null; + if (type == 'html') { + contentUrl = path.join('file://', getBaseDir(), content) + } else if (type == 'web') { + contentUrl = content; + } else if (type == 'vue') { + let addr = 'http://localhost:8080' + if (isProd()) { + const { mainServer } = getConfig(); + if (isFileProtocol(mainServer)) { + addr = mainServer.protocol + path.join(getBaseDir(), mainServer.indexPath); + } else { + addr = mainServer.protocol + mainServer.host + ':' + mainServer.port; + } + } + + contentUrl = addr + content; + } else { + // some + } + + console.log('contentUrl: ', contentUrl); + const opt = { + title: windowTitle, + x: 10, + y: 10, + width: 980, + height: 650, + webPreferences: { + contextIsolation: false, + nodeIntegration: true, + }, + } + const win = new BrowserWindow(opt); + const winContentsId = win.webContents.id; + win.loadURL(contentUrl); + win.webContents.openDevTools(); + this.windows[windowName] = win; + + return winContentsId; + } + + /** + * Get window contents id + */ + getWCid(args) { + const { windowName } = args; + let win; + if (windowName == 'main') { + win = getMainWindow(); + } else { + win = this.windows[windowName]; + } + + return win.webContents.id; + } + + /** + * Realize communication between two windows through the transfer of the main process + */ + communicate(args) { + const { receiver, content } = args; + if (receiver == 'main') { + const win = getMainWindow(); + win.webContents.send('controller.os.window2ToWindow1', content); + } else if (receiver == 'window2') { + const win = this.windows[receiver]; + win.webContents.send('controller.os.window1ToWindow2', content); + } + } + + /** + * createNotification + */ + createNotification(options, event) { + const channel = 'controller.os.sendNotification'; + this.myNotification = new Notification(options); + + if (options.clickEvent) { + this.myNotification.on('click', (e) => { + let data = { + type: 'click', + msg: '您点击了通知消息' + } + event.reply(`${channel}`, data) + }); + } + + if (options.closeEvent) { + this.myNotification.on('close', (e) => { + let data = { + type: 'close', + msg: '您关闭了通知消息' + } + event.reply(`${channel}`, data) + }); + } + + this.myNotification.show(); + } + +} + +WindowService.toString = () => '[class WindowService]'; +module.exports = { + WindowService, + windowService: new WindowService() +}; \ No newline at end of file