diff --git a/desktop/app.js b/desktop/app.js index 7ccea66..cb92944 100644 --- a/desktop/app.js +++ b/desktop/app.js @@ -1,7 +1,9 @@ import { app, dialog, nativeImage, shell, Menu } from 'electron'; +import fs from 'fs/promises'; +import fsSync from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -import { loadDesktopConfig, saveDesktopConfig, resetDesktopConfig } from './config.js'; +import { loadDesktopConfig, saveDesktopConfig, resetDesktopConfig, getDefaultDesktopConfig } from './config.js'; import { resolveLocale, getMessages } from './i18n.js'; import { getFreePort, isPortAvailable } from './ports.js'; import { getServerStatus, startServerWithConfig, stopServerInstance } from './server.js'; @@ -24,6 +26,7 @@ let messages = getMessages(locale); let tray = null; const repoUrl = 'https://github.com/xxnuo/MTranServer'; let portCheckPromise = null; +let desktopLogPath = null; function getLocalHost(host) { if (!host || host === '0.0.0.0') return '127.0.0.1'; @@ -69,6 +72,17 @@ function updateTray() { }); } +async function logDesktop(message, error) { + if (!desktopLogPath) return; + const timestamp = new Date().toISOString(); + const details = error ? ` ${error.stack || error.message || error}` : ''; + try { + await fs.appendFile(desktopLogPath, `[${timestamp}] ${message}${details}\n`, 'utf8'); + } catch { + return; + } +} + async function ensurePortAvailable() { if (portCheckPromise) return portCheckPromise; portCheckPromise = (async () => { @@ -76,6 +90,7 @@ async function ensurePortAvailable() { const available = await isPortAvailable(desktopConfig.server.port, host); if (available) return true; + await logDesktop(`port in use ${desktopConfig.server.port}`); const result = await dialog.showMessageBox({ type: 'warning', buttons: [messages.portInUseUseRandom, messages.portInUseQuit], @@ -87,10 +102,12 @@ async function ensurePortAvailable() { if (result.response === 0) { const newPort = await getFreePort(); + await logDesktop(`use random port ${newPort}`); desktopConfig.server.port = newPort; desktopConfig = await saveDesktopConfig(desktopConfig); return true; } + await logDesktop('quit after port in use'); await quitApp(); return false; })(); @@ -105,9 +122,12 @@ async function startServer() { const ok = await ensurePortAvailable(); if (!ok) return false; try { + await logDesktop('starting server'); await startServerWithConfig(desktopConfig.server); + await logDesktop('server started'); return true; - } catch { + } catch (error) { + await logDesktop('server start failed', error); dialog.showMessageBox({ type: 'error', message: messages.serverStartFailed, @@ -119,7 +139,9 @@ async function startServer() { async function restartServer() { try { + await logDesktop('restarting server'); await stopServerInstance(); + await ensureWritableDirs(); const ok = await startServer(); if (!ok) return false; updateWindowUrls({ @@ -138,6 +160,7 @@ async function restartServer() { } async function quitApp() { + await logDesktop('quit app'); app.isQuitting = true; await stopServerInstance(); app.quit(); @@ -149,6 +172,40 @@ function updateLocale(nextLocale) { updateTray(); } +async function ensureWritableDirs() { + const defaults = getDefaultDesktopConfig(); + const updates = {}; + const targets = [ + ['modelDir', defaults.server.modelDir], + ['logDir', defaults.server.logDir], + ['configDir', defaults.server.configDir] + ]; + + for (const [key, fallback] of targets) { + const target = desktopConfig.server[key]; + if (!target) { + updates[key] = fallback; + continue; + } + try { + await fs.mkdir(target, { recursive: true }); + await fs.access(target, fsSync.constants.W_OK); + } catch { + updates[key] = fallback; + } + } + + if (Object.keys(updates).length > 0) { + desktopConfig = await saveDesktopConfig({ + ...desktopConfig, + server: { + ...desktopConfig.server, + ...updates + } + }); + } +} + function getLoadingUrl() { const html = ` @@ -185,6 +242,10 @@ export async function startDesktop() { Menu.setApplicationMenu(null); desktopConfig = await loadDesktopConfig(); updateLocale(desktopConfig.locale); + desktopLogPath = path.join(desktopConfig.server.configDir, 'desktop.log'); + await fs.mkdir(desktopConfig.server.configDir, { recursive: true }); + await logDesktop('desktop starting'); + await ensureWritableDirs(); const preloadPath = path.join(__dirname, 'preload.cjs'); const loadingUrl = getLoadingUrl(); diff --git a/package.json b/package.json index 4e7c7cc..b06b743 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "bump": "bun scripts/bump.ts", "electron": "bun run build:lib && electron ./electron-main.js", "electron:dev": "bun run scripts/electron-dev.ts", - "electron:build": "bun run build:lib && electron-builder", + "electron:build": "bun run scripts/electron-build.ts", "electron:build:all": "bun run build:lib && electron-builder -mwl", "prepublishOnly": "bun scripts/build.ts --lib", "push": "npm publish --registry=https://registry.npmjs.org", @@ -58,6 +58,7 @@ "typescript": "^5.9.3" }, "dependencies": { + "ee-first": "^1.1.1", "express": "^5.2.1", "express-validator": "^7.3.1", "fzstd": "^0.1.1", @@ -71,15 +72,23 @@ "build": { "appId": "com.github.xxnuo.mtranserver", "productName": "MTranServer", + "executableName": "mtranserver", "copyright": "Copyright © 2025", "extraMetadata": { "main": "electron-main.js" }, + "asar": true, + "asarUnpack": [ + "dist/mtranserver*", + "dist/*.exe" + ], "artifactName": "mtranserver-desktop-${version}-${os}-${arch}.${ext}", "files": [ "desktop/**/*", "electron-main.js", "dist/**/*", + "!dist/mtranserver*", + "!dist/*.exe", "images/**/*", "package.json", "node_modules/**/*" diff --git a/scripts/electron-build.ts b/scripts/electron-build.ts new file mode 100644 index 0000000..5339615 --- /dev/null +++ b/scripts/electron-build.ts @@ -0,0 +1,45 @@ +import { spawn } from 'child_process' +import fs from 'fs' +import path from 'path' + +function run(command: string, args: string[]) { + return new Promise((resolve, reject) => { + const proc = spawn(command, args, { stdio: 'inherit', env: process.env }) + proc.on('exit', (code) => { + if (code === 0) resolve() + else reject(new Error(`exit ${code}`)) + }) + }) +} + +async function main() { + const bunBin = process.execPath + await run(bunBin, ['run', 'build:lib']) + + const distDir = path.join(process.cwd(), 'dist') + if (fs.existsSync(distDir)) { + for (const entry of fs.readdirSync(distDir)) { + if (entry.startsWith('mtranserver') || entry.endsWith('.exe')) { + try { + fs.rmSync(path.join(distDir, entry), { recursive: true, force: true }) + } catch { + continue + } + } + } + } + + const platform = process.platform + const builderArgs = ['node_modules/.bin/electron-builder'] + if (platform === 'linux') { + builderArgs.push('--linux', 'AppImage') + } else if (platform === 'darwin') { + builderArgs.push('--mac') + } else if (platform === 'win32') { + builderArgs.push('--win') + } + + await run(bunBin, builderArgs) +} + +main().catch(() => process.exit(1))