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 { 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()) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user