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:
parent
90bb298dbe
commit
d1513a58b3
217
main.js
217
main.js
@ -11,6 +11,7 @@ const DoodstreamUploader = require('./lib/doodstream-upload');
|
||||
const { checkForUpdate, installUpdate, abortUpdate } = require('./lib/updater');
|
||||
const backupCrypto = require('./lib/backup-crypto');
|
||||
const FolderMonitor = require('./lib/folder-monitor');
|
||||
const RemoteServer = require('./lib/remote-server');
|
||||
|
||||
let mainWindow;
|
||||
let dropTargetWindow = null;
|
||||
@ -18,6 +19,8 @@ let tray = null;
|
||||
const configStore = new ConfigStore(app);
|
||||
let uploadManager = null;
|
||||
let folderMonitor = new FolderMonitor();
|
||||
let remoteServer = null;
|
||||
let captureWindow = null;
|
||||
const HEALTH_CHECK_TIMEOUT = 25000;
|
||||
|
||||
// --- 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}`);
|
||||
}
|
||||
|
||||
// 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
|
||||
try {
|
||||
const dtConfig = configStore.load();
|
||||
@ -517,6 +532,10 @@ app.on('window-all-closed', () => {
|
||||
|
||||
app.on('before-quit', () => {
|
||||
try { folderMonitor.stop(); } catch {}
|
||||
try {
|
||||
if (remoteServer) { remoteServer.stop(); remoteServer = null; }
|
||||
destroyCaptureWindow();
|
||||
} catch {}
|
||||
destroyDropTargetWindow();
|
||||
});
|
||||
|
||||
@ -891,6 +910,204 @@ ipcMain.handle('folder-monitor:select-folder', async () => {
|
||||
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 ---
|
||||
ipcMain.handle('set-always-on-top', async (_event, value) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user