diff --git a/lib/remote-server.js b/lib/remote-server.js index 6af9a1e..86c4008 100644 --- a/lib/remote-server.js +++ b/lib/remote-server.js @@ -21,8 +21,7 @@ class RemoteServer { return new Promise((resolve, reject) => { this._config = opts; - const wssOpts = { port: opts.port, maxPayload: 256 * 1024 }; - if (opts.host) wssOpts.host = opts.host; + const wssOpts = { port: opts.port, host: opts.host || '127.0.0.1', maxPayload: 256 * 1024 }; this._wss = new WebSocketServer(wssOpts, () => { resolve(); }); diff --git a/main.js b/main.js index 0ad87bf..618c62b 100644 --- a/main.js +++ b/main.js @@ -1,10 +1,13 @@ process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE || '8'; const { monitorEventLoopDelay, PerformanceObserver } = require('perf_hooks'); const { app, BrowserWindow, ipcMain, dialog, clipboard, nativeTheme, Tray, Menu, nativeImage } = require('electron'); +const path = require('path'); +app.setPath('userData', path.join(app.getPath('appData'), 'multi-hoster-uploader')); +app.setName('Multi Hoster Uploader'); +app.setAppUserModelId('com.multihoster.uploader'); const { configureStartupRenderer, createStartupWindow, resolveStartupLanguage } = require('./lib/startup-renderer'); configureStartupRenderer(app); nativeTheme.themeSource = 'dark'; -const path = require('path'); const fs = require('fs'); const ConfigStore = require('./lib/config-store'); const UploadManager = require('./lib/upload-manager'); @@ -2187,7 +2190,7 @@ ipcMain.handle('start-upload', async (_event, payload) => { updateTrayTooltip(`Upload: ${data.activeJobs} aktiv - ${speedMb} MB/s`); _maybeLogEventLoopDelay(data.activeJobs); } else { - updateTrayTooltip('Multi-Hoster-Upload'); + updateTrayTooltip('Multi Hoster Uploader'); } } catch (e) { debugLog(`stats listener error: ${e && e.message}`); } }); @@ -3373,6 +3376,7 @@ async function startRemoteServer() { try { await remoteServer.start({ port: remote.port || 9100, + host: '127.0.0.1', token, allowInput: remote.allowInput !== false, mainWindow, diff --git a/package.json b/package.json index 0cb1dd5..a7e77af 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ }, "build": { "appId": "com.multihoster.uploader", - "productName": "Multi-Hoster-Upload", + "productName": "Multi Hoster Uploader", "directories": { "buildResources": "assets", "output": "release" @@ -47,14 +47,19 @@ "portable" ], "icon": "assets/app_icon.ico", + "executableName": "Multi Hoster Uploader", "signAndEditExecutable": false }, "nsis": { + "artifactName": "Multi-Hoster-Upload Setup ${version}.${ext}", "oneClick": false, "perMachine": false, "allowToChangeInstallationDirectory": true, "createDesktopShortcut": true }, + "portable": { + "artifactName": "Multi-Hoster-Upload ${version}.${ext}" + }, "afterPack": "scripts/afterPack.cjs" } } diff --git a/scripts/afterPack.cjs b/scripts/afterPack.cjs index 374a3b7..7384e73 100644 --- a/scripts/afterPack.cjs +++ b/scripts/afterPack.cjs @@ -1,44 +1,32 @@ const path = require("path"); +const fs = require("fs"); module.exports = async function afterPack(context) { - let rcedit; - try { - rcedit = require("rcedit"); - } catch { - console.warn(" rcedit: skipped - rcedit not installed"); - return; - } + const rcedit = require("rcedit"); const productFilename = context.packager?.appInfo?.productFilename; const version = context.packager?.appInfo?.version; if (!productFilename || !version) { - console.warn(" rcedit: skipped - application metadata not available"); - return; + throw new Error("Application metadata is unavailable"); } const exePath = path.join(context.appOutDir, `${productFilename}.exe`); const iconPath = path.resolve(__dirname, "..", "assets", "app_icon.ico"); - try { - const fs = require("fs"); - if (!fs.existsSync(iconPath)) { - console.warn(" rcedit: skipped - app_icon.ico not found"); - return; - } - console.log(` rcedit: branding executable -> ${exePath}`); - await rcedit(exePath, { - icon: iconPath, - "file-version": version, - "product-version": version, - "version-string": { - CompanyName: "Sucukdeluxe", - FileDescription: "Multi Hoster Uploader", - InternalName: productFilename, - OriginalFilename: `${productFilename}.exe`, - ProductName: "Multi Hoster Uploader" - } - }); - } catch (error) { - console.warn(` rcedit: failed - ${String(error)}`); + if (!fs.existsSync(iconPath)) { + throw new Error("Application icon is unavailable"); } + console.log(` rcedit: branding executable -> ${exePath}`); + await rcedit(exePath, { + icon: iconPath, + "file-version": version, + "product-version": version, + "version-string": { + CompanyName: "Sucukdeluxe", + FileDescription: "Multi Hoster Uploader", + InternalName: productFilename, + OriginalFilename: `${productFilename}.exe`, + ProductName: "Multi Hoster Uploader" + } + }); }; diff --git a/tests/package-build-files.test.js b/tests/package-build-files.test.js index e223295..f42fdc0 100644 --- a/tests/package-build-files.test.js +++ b/tests/package-build-files.test.js @@ -1,6 +1,7 @@ const test = require('node:test'); const assert = require('node:assert/strict'); const Module = require('node:module'); +const fs = require('node:fs'); const path = require('node:path'); const packageJson = require('../package.json'); @@ -10,6 +11,19 @@ test('packages every Electron preload referenced by the main process', () => { assert.equal(packageJson.build.win.signAndEditExecutable, false); }); +test('packaged identity is exact while public artifact routing remains stable', () => { + assert.equal(packageJson.build.productName, 'Multi Hoster Uploader'); + assert.equal(packageJson.build.win.executableName, 'Multi Hoster Uploader'); + assert.equal(packageJson.build.nsis.artifactName, 'Multi-Hoster-Upload Setup ${version}.${ext}'); + assert.equal(packageJson.build.portable.artifactName, 'Multi-Hoster-Upload ${version}.${ext}'); + + const source = fs.readFileSync(path.join(__dirname, '..', 'main.js'), 'utf8'); + assert.match(source, /app\.setPath\('userData', path\.join\(app\.getPath\('appData'\), 'multi-hoster-uploader'\)\)/); + assert.match(source, /app\.setName\('Multi Hoster Uploader'\)/); + assert.match(source, /app\.setAppUserModelId\('com\.multihoster\.uploader'\)/); + assert.doesNotMatch(source, /updateTrayTooltip\('Multi-Hoster-Upload'\)/); +}); + test('floating drop target resolves native paths through Electron webUtils', () => { let exposedApi = null; const nativeFile = { name: 'fixture.mkv' }; @@ -57,25 +71,45 @@ test('afterPack brands the executable metadata shown by Windows', async () => { const afterPack = require(afterPackPath); await afterPack({ appOutDir: 'C:\\release', - packager: { appInfo: { productFilename: 'Multi-Hoster-Upload', version: '9.8.7' } } + packager: { appInfo: { productFilename: 'Multi Hoster Uploader', version: '9.8.7' } } }); } finally { Module._load = originalLoad; delete require.cache[afterPackPath]; } - assert.equal(editCall.exePath, path.join('C:\\release', 'Multi-Hoster-Upload.exe')); + assert.equal(editCall.exePath, path.join('C:\\release', 'Multi Hoster Uploader.exe')); assert.equal(editCall.options['file-version'], '9.8.7'); assert.equal(editCall.options['product-version'], '9.8.7'); assert.deepEqual(editCall.options['version-string'], { CompanyName: 'Sucukdeluxe', FileDescription: 'Multi Hoster Uploader', - InternalName: 'Multi-Hoster-Upload', - OriginalFilename: 'Multi-Hoster-Upload.exe', + InternalName: 'Multi Hoster Uploader', + OriginalFilename: 'Multi Hoster Uploader.exe', ProductName: 'Multi Hoster Uploader' }); }); +test('afterPack fails the build when executable branding fails', async () => { + const originalLoad = Module._load; + const afterPackPath = require.resolve('../scripts/afterPack.cjs'); + delete require.cache[afterPackPath]; + Module._load = function (request, parent, isMain) { + if (request === 'rcedit') return async () => { throw new Error('branding failed'); }; + return originalLoad.call(this, request, parent, isMain); + }; + try { + const afterPack = require(afterPackPath); + await assert.rejects(afterPack({ + appOutDir: 'C:\\release', + packager: { appInfo: { productFilename: 'Multi Hoster Uploader', version: '9.8.7' } } + }), /branding failed/); + } finally { + Module._load = originalLoad; + delete require.cache[afterPackPath]; + } +}); + test('close readiness is signaled only after the renderer explicitly finishes initialization', () => { const listeners = new Map(); const sent = []; diff --git a/tests/remote-server.test.js b/tests/remote-server.test.js index a84db1f..7056325 100644 --- a/tests/remote-server.test.js +++ b/tests/remote-server.test.js @@ -1,5 +1,7 @@ const { describe, it, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); // Test the module can be required and has the expected API describe('RemoteServer', () => { @@ -39,4 +41,30 @@ describe('RemoteServer', () => { assert.strictEqual(server.getClientCount(), 0); server.stop(); }); + + it('binds to loopback when no host is supplied', async () => { + const RemoteServer = require('../lib/remote-server'); + const server = new RemoteServer(); + try { + await server.start({ + port: 0, + token: 'test-token-123', + onSignalingToCapture: () => {}, + onCreateCaptureWindow: () => {}, + onDestroyCaptureWindow: () => {} + }); + assert.strictEqual(server._wss.address().address, '127.0.0.1'); + } finally { + server.stop(); + } + }); + + it('keeps the application remote-control listener on loopback', () => { + const source = fs.readFileSync(path.join(__dirname, '..', 'main.js'), 'utf8'); + const start = source.indexOf('async function startRemoteServer()'); + const end = source.indexOf("ipcMain.on('remote:signaling-from-capture'", start); + assert.notStrictEqual(start, -1); + assert.notStrictEqual(end, -1); + assert.match(source.slice(start, end), /host:\s*'127\.0\.0\.1'/); + }); });