From 1a79b54dd09131e3a1ce1c10622bcdaf7bee7821 Mon Sep 17 00:00:00 2001 From: icexb Date: Thu, 27 Jul 2023 13:40:23 +0800 Subject: [PATCH] Switch to go core --- electron/main/index.ts | 142 +++++++++++++++++-------- electron/main/port.js | 180 ++++++++++++++++++++++++++++++++ package.json | 7 +- src/components/SubtitleEmit.vue | 76 +++++++++----- src/components/SubtitleTask.vue | 83 +++++---------- src/utils/core.ts | 38 ++++--- src/views/Home.vue | 2 +- src/views/Subtitle.vue | 51 ++------- src/views/Translate.vue | 41 ++++++-- vite.config.ts | 22 +--- 10 files changed, 429 insertions(+), 213 deletions(-) create mode 100644 electron/main/port.js diff --git a/electron/main/index.ts b/electron/main/index.ts index 16f15d0..530408f 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -4,7 +4,6 @@ import {join} from 'node:path' import path from 'path'; import cp from 'child_process'; import * as fs from 'fs'; -import * as net from "net"; const axios = require("axios") const WebSocket = require('ws') @@ -89,7 +88,10 @@ let AppLatestVersion; let CoreProcess: cp.ChildProcess = null; let CorePort: number = null; let CoreConnected: Boolean = false; -let CoreAliveSocket: WebSocket = null; +let CoreWebSocket: WebSocket = null; +let CoreLogs: string[] = []; +let CoreTaskLogs: object = {}; +let CoreTasks: object = {}; async function createWindow() { win = new BrowserWindow({ @@ -107,9 +109,8 @@ async function createWindow() { }) win.setMenuBarVisibility(false) - - if (process.env.VITE_DEV_SERVER_URL) { // electron-vite-vue#298 - await win.loadURL(url) + if (!app.isPackaged) { // electron-vite-vue#298 + await win.loadURL(`http://127.0.0.1:50023`) // Open devTool if the app is not packaged win.webContents.openDevTools() } else { @@ -131,26 +132,50 @@ async function createWindow() { } function initSocket() { - CoreAliveSocket = new WebSocket(`ws://127.0.0.1:${CorePort}/alive`) + CoreWebSocket = new WebSocket(`ws://127.0.0.1:${CorePort}/`) - CoreAliveSocket.onopen = () => { + CoreWebSocket.onopen = () => { console.log("WebSocket Connected") CoreConnected = true; - CoreAliveSocket.send(JSON.stringify({type: "alive"})); + CoreWebSocket.send(JSON.stringify({type: "alive"})); } - CoreAliveSocket.onclose = () => { + CoreWebSocket.onclose = () => { CoreConnected = false; if (fs.existsSync(CoreBin) && CoreProcess && running) { setTimeout(initSocket, 1500); } + CoreTasks = [] + CoreTaskLogs = {} } - CoreAliveSocket.onmessage = () => { + CoreWebSocket.onmessage = (m) => { if (!CoreConnected) CoreConnected = true; - setTimeout(() => { - CoreAliveSocket.send(JSON.stringify({type: "alive"})); - }, 500) + let msg = JSON.parse(m.data) + + switch (msg.type) { + case "log": + let msgData = JSON.parse(msg.data) + let taskId = msgData.id + let taskLog = msgData.message + if (CoreTaskLogs.hasOwnProperty(taskId)) { + CoreTaskLogs[taskId] = [...CoreTaskLogs[taskId], taskLog] + } else { + CoreTaskLogs[taskId] = [taskLog] + } + + win.webContents.send(`task-log-${taskId}`, taskLog) + break; + case "tasks": + CoreTasks = JSON.parse(msg.data) + win.webContents.send("task-status-change", CoreTasks) + break; + case "alive": + setTimeout(() => { + CoreWebSocket.send(JSON.stringify({type: "alive"})); + }, 500); + break; + } } - CoreAliveSocket.onerror = (res) => { + CoreWebSocket.onerror = (res) => { if (!fs.existsSync(CoreBin)) { CoreConnected = false; //TODO @@ -161,7 +186,6 @@ function initSocket() { } } -let CoreLogs: string[] = []; function appendLog(data: Buffer) { const iconv = require('iconv-lite'); @@ -178,39 +202,36 @@ function appendLog(data: Buffer) { } -function getPort() { - return new Promise((resolve) => { - while (true) { - let port = 1024 + Math.floor(Math.random() * (65535 - 1024)) - let server = net.createServer().listen(port); - if (server.listening) { - server.close() - resolve(port); - break - } - } - }); +function startReleaseCore() { + if (fs.existsSync(CoreBin)) { + CoreProcess = cp.spawn(`"${CoreBin}" -p ${CorePort}`, {shell: true}) + CoreProcess.stderr.on("data", appendLog) + CoreProcess.stdout.on("data", appendLog) + console.log("Core Started") + } else + console.log("Core Not Found") } +import getPort from "./port" function initCore() { initCoreVersion() - getPort().then( - (port: number) => { - CorePort = port - if (fs.existsSync(CoreBin)) { - CoreProcess = cp.spawn(`"${CoreBin}" -p ${CorePort}`, {shell: true}) - CoreProcess.stderr.on("data", appendLog) - CoreProcess.stdout.on("data", appendLog) - console.log("Core Started!") - } else - console.log("Core Not Found!") + let devPort = 50000; + getPort({port: devPort}).then((port) => { + if (port === devPort) { + console.log("Using Release Core") + getPort().then(port => { + CorePort = port + }).then(startReleaseCore) + } else { + console.log("Using Dev Core") + CorePort = 50000 } - ).finally(() => { + }).finally(() => { setTimeout(initSocket, 1500) }) -} +} app.whenReady().then(createWindow).then(initCore).then(() => { const setting = fs.existsSync(path.join(PROGRAM_DIR, 'setting.json')) @@ -223,14 +244,13 @@ app.on('window-all-closed', () => { running = false win = null try { - CoreAliveSocket.close() + CoreWebSocket.close() if (CoreProcess) CoreProcess.kill(); } finally { app.quit(); } }) - app.on('second-instance', () => { if (win) { // Focus on the main window if the user tried to open another @@ -310,6 +330,17 @@ ipcMain.on('select-file-save-subtitle', function (event) { event.sender.send('selected-subtitle-path', result) }) }); + +ipcMain.on('select-file-exist-story', function (event) { + dialog.showOpenDialog({ + title: '选择数据文件', + properties: ['openFile'], + filters: [{name: '世界计划数据文件', extensions: ['json', 'asset','pjs.txt']}] + }).then(result => { + event.sender.send('selected-story', result) + }) +}); + ipcMain.on('get-system-font', function (event) { let fontList = require('font-list') fontList.getFonts() @@ -418,7 +449,7 @@ ipcMain.on("get-core-version", (event) => { axios.get("https://api.github.com/repos/Icexbb/SekaiSubtitle-core/releases").then(resp => { CoreLatestVersion = resp.data[0].tag_name; event.sender.send("get-core-version-result", [CoreVersion, CoreLatestVersion]) - }) + }).catch() else event.sender.send("get-core-version-result", [CoreVersion, CoreLatestVersion]) }) @@ -453,13 +484,13 @@ ipcMain.on("get-app-version", (event) => { axios.get("https://api.github.com/repos/Icexbb/SekaiSubtitle-electron/releases").then(resp => { AppLatestVersion = resp.data[0].tag_name; event.sender.send("get-app-version-result", [AppVersion, AppLatestVersion]) - }) + }).catch() else event.sender.send("get-app-version-result", [AppVersion, AppLatestVersion]) }) ipcMain.on("restart-core", (event) => { try { - CoreAliveSocket.close() + CoreWebSocket.close() CoreProcess.kill() CoreProcess = null; } catch (e) { @@ -470,10 +501,31 @@ ipcMain.on("restart-core", (event) => { }) ipcMain.on("stop-core", (event, args) => { try { - CoreAliveSocket.close() + CoreWebSocket.close() CoreProcess.kill() CoreProcess = null; } catch (e) { console.log(e) } }) + +ipcMain.on("get-task-log", (event, args) => { + event.returnValue = CoreTaskLogs[args] +}) +ipcMain.on("get-task-status", (event, args) => { + event.returnValue = CoreTasks +}) +ipcMain.on("task-operate", (event, args) => { + if (CoreWebSocket.readyState == CoreWebSocket.OPEN) { + CoreWebSocket.send(JSON.stringify({type: args[1], data: args[0]})) + } +}) + +ipcMain.on("task-new", (event, args) => { + if (CoreWebSocket.readyState == CoreWebSocket.OPEN) { + CoreWebSocket.send(JSON.stringify({ + type: "new", + data: JSON.stringify({config: JSON.parse(args[0]), runAfterCreate: args[1]}) + })) + } +}) \ No newline at end of file diff --git a/electron/main/port.js b/electron/main/port.js new file mode 100644 index 0000000..cbea1b0 --- /dev/null +++ b/electron/main/port.js @@ -0,0 +1,180 @@ +import net from 'node:net'; +import os from 'node:os'; + +class Locked extends Error { + constructor(port) { + super(`${port} is locked`); + } +} + +const lockedPorts = { + old: new Set(), + young: new Set(), +}; + +// On this interval, the old locked ports are discarded, +// the young locked ports are moved to old locked ports, +// and a new young set for locked ports are created. +const releaseOldLockedPortsIntervalMs = 1000 * 15; + +const minPort = 1024; +const maxPort = 65_535; + +// Lazily create timeout on first use +let timeout; + +const getLocalHosts = () => { + const interfaces = os.networkInterfaces(); + + // Add undefined value for createServer function to use default host, + // and default IPv4 host in case createServer defaults to IPv6. + const results = new Set([undefined, '0.0.0.0']); + + for (const _interface of Object.values(interfaces)) { + for (const config of _interface) { + results.add(config.address); + } + } + + return results; +}; + +const checkAvailablePort = options => + new Promise((resolve, reject) => { + const server = net.createServer(); + server.unref(); + server.on('error', reject); + + server.listen(options, () => { + const {port} = server.address(); + server.close(() => { + resolve(port); + }); + }); + }); + +const getAvailablePort = async (options, hosts) => { + if (options.host || options.port === 0) { + return checkAvailablePort(options); + } + + for (const host of hosts) { + try { + await checkAvailablePort({port: options.port, host}); // eslint-disable-line no-await-in-loop + } catch (error) { + if (!['EADDRNOTAVAIL', 'EINVAL'].includes(error.code)) { + throw error; + } + } + } + + return options.port; +}; + +const portCheckSequence = function * (ports) { + if (ports) { + yield * ports; + } + + yield 0; // Fall back to 0 if anything else failed +}; + +export default async function getPorts(options) { + let ports; + let exclude = new Set(); + + if (options) { + if (options.port) { + ports = typeof options.port === 'number' ? [options.port] : options.port; + } + + if (options.exclude) { + const excludeIterable = options.exclude; + + if (typeof excludeIterable[Symbol.iterator] !== 'function') { + throw new TypeError('The `exclude` option must be an iterable.'); + } + + for (const element of excludeIterable) { + if (typeof element !== 'number') { + throw new TypeError('Each item in the `exclude` option must be a number corresponding to the port you want excluded.'); + } + + if (!Number.isSafeInteger(element)) { + throw new TypeError(`Number ${element} in the exclude option is not a safe integer and can't be used`); + } + } + + exclude = new Set(excludeIterable); + } + } + + if (timeout === undefined) { + timeout = setTimeout(() => { + timeout = undefined; + + lockedPorts.old = lockedPorts.young; + lockedPorts.young = new Set(); + }, releaseOldLockedPortsIntervalMs); + + // Does not exist in some environments (Electron, Jest jsdom env, browser, etc). + if (timeout.unref) { + timeout.unref(); + } + } + + const hosts = getLocalHosts(); + + for (const port of portCheckSequence(ports)) { + try { + if (exclude.has(port)) { + continue; + } + + let availablePort = await getAvailablePort({...options, port}, hosts); // eslint-disable-line no-await-in-loop + while (lockedPorts.old.has(availablePort) || lockedPorts.young.has(availablePort)) { + if (port !== 0) { + throw new Locked(port); + } + + availablePort = await getAvailablePort({...options, port}, hosts); // eslint-disable-line no-await-in-loop + } + + lockedPorts.young.add(availablePort); + + return availablePort; + } catch (error) { + if (!['EADDRINUSE', 'EACCES'].includes(error.code) && !(error instanceof Locked)) { + throw error; + } + } + } + + throw new Error('No available ports found'); +} + +export function portNumbers(from, to) { + if (!Number.isInteger(from) || !Number.isInteger(to)) { + throw new TypeError('`from` and `to` must be integer numbers'); + } + + if (from < minPort || from > maxPort) { + throw new RangeError(`'from' must be between ${minPort} and ${maxPort}`); + } + + if (to < minPort || to > maxPort) { + throw new RangeError(`'to' must be between ${minPort} and ${maxPort}`); + } + + if (from > to) { + throw new RangeError('`to` must be greater than or equal to `from`'); + } + + const generator = function * (from, to) { + for (let port = from; port <= to; port++) { + yield port; + } + }; + + return generator(from, to); +} diff --git a/package.json b/package.json index 1fd0a23..4b5d32e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sekaisubtitle-electron", - "version": "0.9.9-rc.5", + "version": "0.9.9-rc.6", "main": "dist-electron/main/index.js", "description": "", "author": "XBB ", @@ -13,11 +13,6 @@ "vue3", "vue" ], - "debug": { - "env": { - "VITE_DEV_SERVER_URL": "http://127.0.0.1:3344/" - } - }, "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build && electron-builder --config electron-builder.without-core.json && electron-builder --config electron-builder.with-core.json", diff --git a/src/components/SubtitleEmit.vue b/src/components/SubtitleEmit.vue index 4bd2b02..826db6c 100644 --- a/src/components/SubtitleEmit.vue +++ b/src/components/SubtitleEmit.vue @@ -62,7 +62,9 @@ - diff --git a/src/utils/core.ts b/src/utils/core.ts index 84a711e..2be6570 100644 --- a/src/utils/core.ts +++ b/src/utils/core.ts @@ -1,24 +1,34 @@ import axios from "axios"; - -const fs = require('fs') +import fs from 'fs'; +import os from "os"; import {ipcRenderer} from "electron" - -const releaseUrl: string = "https://api.github.com/repos/Icexbb/SekaiSubtitle-core/releases" +const releaseUrl: string = "https://api.github.com/repos/Icexbb/SekaiSubtitle-Core-Go/releases" const CoreBin: string = ipcRenderer.sendSync("get-core-path") export async function downloadLatestCore(progress) { const coreUrlResp = await axios.get(releaseUrl) - let coreUrl = coreUrlResp.data[0]['assets'][0]['browser_download_url'] - ipcRenderer.send("stop-core") - return axios.get(coreUrl, { - responseType: "arraybuffer", - onDownloadProgress: (event) => { - progress(event) + let assetList :object[] = coreUrlResp.data[0]['assets'][0] + assetList.forEach(value => { + let coreName:string = value['name'] + let found = false; + if (os.platform()=="win32"&& coreName.toLowerCase().endsWith(".exe")) found = true; + + if (found){ + let coreUrl :string= value['browser_download_url'] + ipcRenderer.send("stop-core") + return axios.get(coreUrl, { + responseType: "arraybuffer", + onDownloadProgress: (event) => { + progress(event) + } + }).then((resp) => { + const data = Buffer.from(resp.data, 'binary'); + fs.writeFileSync(CoreBin, data); + ipcRenderer.send("restart-core") + }) } - }).then((resp) => { - const data = Buffer.from(resp.data, 'binary'); - fs.writeFileSync(CoreBin, data); - ipcRenderer.send("restart-core") + }) + } diff --git a/src/views/Home.vue b/src/views/Home.vue index c47be31..fca617b 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -192,7 +192,7 @@ export default defineComponent({ shell.openPath(path.dirname(ipcRenderer.sendSync("get-core-path"))) }, showCorePage() { - shell.openExternal("https://github.com/Icexbb/SekaiSubtitle-Core/releases/latest") + shell.openExternal("https://github.com/Icexbb/SekaiSubtitle-Core-GO/releases/latest") }, getLatestCore() { this.showModal = false; diff --git a/src/views/Subtitle.vue b/src/views/Subtitle.vue index c848d01..36e5454 100644 --- a/src/views/Subtitle.vue +++ b/src/views/Subtitle.vue @@ -84,65 +84,28 @@ export default defineComponent({ return { modalActiveControl: () => { this.modalActive = false - }, - newTask: (config, runAfterCreate) => { - let data = JSON.stringify({type: 'new', data: config, runAfterCreate: runAfterCreate}) - if (this.webSocket) this.webSocket.send(data); - }, - GeneralTasksControl: (operate, taskId: string) => { - this.tasksControl(operate, [taskId]) } }; }, methods: { - initSocket() { - let url = this.url - this.webSocket = new WebSocket(url) - this.webSocket.onopen = () => { - this.webSocket.send(JSON.stringify({type: "alive"})); - }; - this.webSocket.onclose = this.webSocketOnClose - this.webSocket.onmessage = this.webSocketOnMessage - this.webSocket.onerror = (err) => { - console.log('websocket连接失败', err); - }; - }, - webSocketOnMessage(res) { - const data = JSON.parse(res.data) - if (data['type'] === 'tasks') this.taskList = data['data'] - setTimeout(() => { - if (this.webSocket) this.webSocket.send(JSON.stringify({type: "alive"})); - }, 100) - }, - webSocketOnClose() { - if (this.webSocket) - this.webSocket.close() - if (this.living) { - setTimeout(this.initSocket, 100) - console.log("WebSocket Closed Unexpectedly") - this.taskList = {} - } - }, tasksControl(operate, taskList: string[] | null = null) { if (taskList == null) taskList = Object.keys(this.taskList) taskList.forEach( (key) => { - if (this.webSocket) this.webSocket.send(JSON.stringify({type: operate, data: key})); + ipcRenderer.send("task-operate",[key,operate]) } ) } }, - created() { - if (!this.webSocket) this.initSocket(); + mounted() { + this.taskList=ipcRenderer.sendSync("get-task-status") + ipcRenderer.on("task-status-change",(event, args)=>{ + this.taskList=args + }) }, unmounted() { - this.living = false - try { - if (this.webSocket) this.webSocket.close(); - } finally { - this.webSocket = null - } + ipcRenderer.removeAllListeners("task-status-change") } }) diff --git a/src/views/Translate.vue b/src/views/Translate.vue index 344d0e2..7c1a7b8 100644 --- a/src/views/Translate.vue +++ b/src/views/Translate.vue @@ -1,19 +1,48 @@ diff --git a/vite.config.ts b/vite.config.ts index b8b30b4..dbdefb5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -14,6 +14,7 @@ export default defineConfig(({command}) => { const isBuild = command === 'build' const sourcemap = isServe || !!process.env.VSCODE_DEBUG + return { plugins: [ vue({}), @@ -61,23 +62,10 @@ export default defineConfig(({command}) => { // Use Node.js API in the Renderer-process renderer(), ], - server: process.env.VSCODE_DEBUG && (() => { - const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL) - return { - host: url.hostname, - port: +url.port, - proxy: { - "^/best": { - target: "https://storage.sekai.best/sekai-assets", - changeOrigin: true, - rewrite: (path) => { - path.replace(/^\/best/, "") - }, - - } - }, - } - })(), + server: { + host: "127.0.0.1", + port: 50023 + }, clearScreen: false, } })