feat(remote): wire up remote server, capture window, and IPC handlers in main process

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-03-12 06:56:47 +01:00
parent 90bb298dbe
commit d1513a58b3

217
main.js
View File

@ -11,6 +11,7 @@ const DoodstreamUploader = require('./lib/doodstream-upload');
const { checkForUpdate, installUpdate, abortUpdate } = require('./lib/updater'); const { checkForUpdate, installUpdate, abortUpdate } = require('./lib/updater');
const backupCrypto = require('./lib/backup-crypto'); const backupCrypto = require('./lib/backup-crypto');
const FolderMonitor = require('./lib/folder-monitor'); const FolderMonitor = require('./lib/folder-monitor');
const RemoteServer = require('./lib/remote-server');
let mainWindow; let mainWindow;
let dropTargetWindow = null; let dropTargetWindow = null;
@ -18,6 +19,8 @@ let tray = null;
const configStore = new ConfigStore(app); const configStore = new ConfigStore(app);
let uploadManager = null; let uploadManager = null;
let folderMonitor = new FolderMonitor(); let folderMonitor = new FolderMonitor();
let remoteServer = null;
let captureWindow = null;
const HEALTH_CHECK_TIMEOUT = 25000; const HEALTH_CHECK_TIMEOUT = 25000;
// --- Debug logging (writes to upload-debug.log next to the app) --- // --- Debug logging (writes to upload-debug.log next to the app) ---
@ -488,6 +491,18 @@ app.whenReady().then(() => {
debugLog(`folder-monitor auto-start failed: ${err.message}`); debugLog(`folder-monitor auto-start failed: ${err.message}`);
} }
// Auto-start remote server if enabled
try {
const remoteConfig = configStore.load().globalSettings && configStore.load().globalSettings.remote;
if (remoteConfig && remoteConfig.enabled) {
startRemoteServer().catch(err => {
debugLog(`remote-server auto-start failed: ${err.message}`);
});
}
} catch (err) {
debugLog(`remote-server auto-start failed: ${err.message}`);
}
// Auto-show drop target if enabled // Auto-show drop target if enabled
try { try {
const dtConfig = configStore.load(); const dtConfig = configStore.load();
@ -517,6 +532,10 @@ app.on('window-all-closed', () => {
app.on('before-quit', () => { app.on('before-quit', () => {
try { folderMonitor.stop(); } catch {} try { folderMonitor.stop(); } catch {}
try {
if (remoteServer) { remoteServer.stop(); remoteServer = null; }
destroyCaptureWindow();
} catch {}
destroyDropTargetWindow(); destroyDropTargetWindow();
}); });
@ -891,6 +910,204 @@ ipcMain.handle('folder-monitor:select-folder', async () => {
return result.filePaths[0]; return result.filePaths[0];
}); });
// --- Remote Control ---
function generateToken() {
const crypto = require('crypto');
return crypto.randomBytes(32).toString('hex');
}
function createCaptureWindow() {
if (captureWindow && !captureWindow.isDestroyed()) return;
captureWindow = new BrowserWindow({
show: false,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.join(__dirname, 'lib', 'remote-capture-preload.js')
}
});
captureWindow.loadFile(path.join(__dirname, 'lib', 'remote-capture.html'));
// Crash recovery: if hidden window closes unexpectedly while clients connected, recreate it
captureWindow.on('closed', () => {
captureWindow = null;
if (remoteServer && remoteServer.getClientCount() > 0) {
debugLog('remote: capture window crashed, recreating...');
createCaptureWindow();
}
});
}
function destroyCaptureWindow() {
if (captureWindow && !captureWindow.isDestroyed()) {
captureWindow.close();
captureWindow = null;
}
}
async function startRemoteServer() {
if (remoteServer) {
remoteServer.stop();
remoteServer = null;
}
const config = configStore.load();
const remote = config.globalSettings && config.globalSettings.remote;
if (!remote || !remote.enabled) return;
let token = remote.token;
if (!token) {
token = generateToken();
const gs = { ...config.globalSettings, remote: { ...remote, token } };
await configStore.save({ globalSettings: gs });
}
remoteServer = new RemoteServer();
await remoteServer.start({
port: remote.port || 9100,
token,
allowInput: remote.allowInput !== false,
mainWindow,
onSignalingToCapture: (data) => {
if (captureWindow && !captureWindow.isDestroyed()) {
captureWindow.webContents.send('remote:signaling-to-capture', data);
}
},
onCreateCaptureWindow: () => createCaptureWindow(),
onDestroyCaptureWindow: () => destroyCaptureWindow()
});
debugLog(`remote-server started on port ${remoteServer.getPort()}`);
}
// IPC: Signaling from capture window back to dashboard client
ipcMain.on('remote:signaling-from-capture', (_event, data) => {
if (remoteServer && data.clientId) {
remoteServer.sendToClient(data.clientId, data);
}
});
// IPC: Input events from capture window
ipcMain.on('remote:input-event', (_event, data) => {
if (!mainWindow || mainWindow.isDestroyed()) return;
const config = configStore.load();
const remote = config.globalSettings && config.globalSettings.remote;
if (!remote || !remote.allowInput) return;
if (data.role !== 'admin') return;
const bounds = mainWindow.getContentBounds();
const x = Math.round((data.x || 0) * bounds.width);
const y = Math.round((data.y || 0) * bounds.height);
switch (data.type) {
case 'mousemove':
mainWindow.webContents.sendInputEvent({ type: 'mouseMove', x, y });
break;
case 'mousedown':
mainWindow.webContents.sendInputEvent({
type: 'mouseDown', x, y,
button: data.button === 'right' ? 'right' : 'left',
clickCount: 1
});
break;
case 'mouseup':
mainWindow.webContents.sendInputEvent({
type: 'mouseUp', x, y,
button: data.button === 'right' ? 'right' : 'left',
clickCount: 1
});
break;
case 'scroll':
mainWindow.webContents.sendInputEvent({
type: 'mouseWheel', x, y,
deltaX: data.deltaX || 0,
deltaY: data.deltaY || 0
});
break;
case 'keydown':
mainWindow.webContents.sendInputEvent({
type: 'keyDown',
keyCode: data.key,
modifiers: buildModifiers(data)
});
if (data.key.length === 1) {
mainWindow.webContents.sendInputEvent({
type: 'char',
keyCode: data.key,
modifiers: buildModifiers(data)
});
}
break;
case 'keyup':
mainWindow.webContents.sendInputEvent({
type: 'keyUp',
keyCode: data.key,
modifiers: buildModifiers(data)
});
break;
}
});
function buildModifiers(data) {
const mods = [];
if (data.shift) mods.push('shift');
if (data.ctrl) mods.push('control');
if (data.alt) mods.push('alt');
return mods;
}
// IPC: Get capture source ID (desktopCapturer must run in main process in Electron 33+)
ipcMain.handle('remote:get-capture-source-id', async () => {
if (!mainWindow || mainWindow.isDestroyed()) return null;
const { desktopCapturer } = require('electron');
const sources = await desktopCapturer.getSources({ types: ['window'] });
const title = mainWindow.getTitle();
const source = sources.find(s => s.name === title);
return source ? source.id : null;
});
// IPC: Client count updates from capture window
ipcMain.on('remote:client-count', (_event, count) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('remote:client-count', count);
}
});
// IPC: Remote settings
ipcMain.handle('remote:get-settings', () => {
const config = configStore.load();
return config.globalSettings && config.globalSettings.remote || {};
});
ipcMain.handle('remote:save-settings', async (_event, remoteSettings) => {
const config = configStore.load();
const gs = { ...config.globalSettings, remote: remoteSettings };
await configStore.save({ globalSettings: gs });
if (remoteSettings.enabled) {
await startRemoteServer();
} else if (remoteServer) {
remoteServer.stop();
remoteServer = null;
destroyCaptureWindow();
debugLog('remote-server stopped');
}
return true;
});
ipcMain.handle('remote:generate-token', () => {
return generateToken();
});
ipcMain.handle('remote:status', () => {
return {
running: !!remoteServer,
port: remoteServer ? remoteServer.getPort() : null,
clientCount: remoteServer ? remoteServer.getClientCount() : 0
};
});
// --- Always on top --- // --- Always on top ---
ipcMain.handle('set-always-on-top', async (_event, value) => { ipcMain.handle('set-always-on-top', async (_event, value) => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {