Add desktop RPC layer replacing Electron IPC and window management.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
1728c08e39
commit
f0b6059e70
33
src/desktop/notify.js
Normal file
33
src/desktop/notify.js
Normal file
@ -0,0 +1,33 @@
|
||||
const listeners = new Map();
|
||||
|
||||
let sendMessage = () => {};
|
||||
|
||||
export function setMessageSender(sender) {
|
||||
sendMessage = sender;
|
||||
}
|
||||
|
||||
export function sendIPC(channel, data) {
|
||||
sendMessage(channel, data);
|
||||
}
|
||||
|
||||
export function onIPCData(channel, callback) {
|
||||
if (!listeners.has(channel)) {
|
||||
listeners.set(channel, new Set());
|
||||
}
|
||||
listeners.get(channel).add(callback);
|
||||
}
|
||||
|
||||
export function removeAllListeners(channel) {
|
||||
listeners.delete(channel);
|
||||
}
|
||||
|
||||
export function dispatchIPC(channel, data) {
|
||||
const channelListeners = listeners.get(channel);
|
||||
if (!channelListeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const callback of channelListeners) {
|
||||
callback(data);
|
||||
}
|
||||
}
|
||||
152
src/desktop/rpc.js
Normal file
152
src/desktop/rpc.js
Normal file
@ -0,0 +1,152 @@
|
||||
import log4js from "log4js";
|
||||
import { BrowserView } from "electrobun/bun";
|
||||
import { startWaiting, stopWaiting } from "../spinner.js";
|
||||
import { setMessageSender, sendIPC } from "./notify.js";
|
||||
|
||||
const logger = log4js.getLogger("RPC");
|
||||
let mainWindow = null;
|
||||
|
||||
export function createAppRpc() {
|
||||
return BrowserView.defineRPC({
|
||||
maxRequestTime: 30000,
|
||||
handlers: {
|
||||
requests: {
|
||||
getData: async () => {
|
||||
startWaiting("Getting data...", logger);
|
||||
try {
|
||||
const socketClient = global.socketClient;
|
||||
|
||||
if (!socketClient) {
|
||||
pushState({
|
||||
authenticated: false,
|
||||
connected: false,
|
||||
loading: false,
|
||||
host: {},
|
||||
printers: [],
|
||||
documentPrinters: [],
|
||||
});
|
||||
await stopWaiting();
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
pushState({
|
||||
authenticated: socketClient.authenticated,
|
||||
connected: socketClient.connected,
|
||||
loading: socketClient.loading,
|
||||
host: socketClient.host || {},
|
||||
printers: socketClient.printerManager.printers || [],
|
||||
documentPrinters:
|
||||
socketClient.documentPrinterManager.documentPrinters || [],
|
||||
});
|
||||
await stopWaiting();
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
logger.error("Error getting printer data:", error);
|
||||
pushState({
|
||||
authenticated: false,
|
||||
connected: false,
|
||||
loading: false,
|
||||
host: {},
|
||||
printers: [],
|
||||
documentPrinters: [],
|
||||
});
|
||||
await stopWaiting();
|
||||
return { ok: false };
|
||||
}
|
||||
},
|
||||
authenticateOTP: async ({ otp }) => {
|
||||
startWaiting("Authenticating with OTP...", logger);
|
||||
try {
|
||||
const socketClient = global.socketClient;
|
||||
if (socketClient) {
|
||||
await socketClient.authenticateWithOtp(otp);
|
||||
} else {
|
||||
logger.error("Socket client not available for OTP authentication");
|
||||
}
|
||||
await stopWaiting();
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
await stopWaiting();
|
||||
logger.error("Error during OTP authentication:", error);
|
||||
return { ok: false };
|
||||
}
|
||||
},
|
||||
minimizeWindow: async () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed?.()) {
|
||||
mainWindow.minimize();
|
||||
}
|
||||
return { ok: true };
|
||||
},
|
||||
maximizeWindow: async () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed?.()) {
|
||||
if (mainWindow.isMaximized?.()) {
|
||||
mainWindow.unmaximize();
|
||||
} else {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
}
|
||||
return { ok: true };
|
||||
},
|
||||
closeWindow: async () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed?.()) {
|
||||
mainWindow.close();
|
||||
}
|
||||
return { ok: true };
|
||||
},
|
||||
},
|
||||
messages: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function pushState({
|
||||
authenticated,
|
||||
connected,
|
||||
loading,
|
||||
host,
|
||||
printers,
|
||||
documentPrinters,
|
||||
}) {
|
||||
sendIPC("setAuthenticated", authenticated);
|
||||
sendIPC("setConnected", connected);
|
||||
sendIPC("setLoading", loading);
|
||||
sendIPC("setHost", host);
|
||||
sendIPC("setPrinters", printers);
|
||||
sendIPC("setDocumentPrinters", documentPrinters);
|
||||
}
|
||||
|
||||
export function registerMainWindow(window) {
|
||||
mainWindow = window;
|
||||
global.mainWindow = window;
|
||||
}
|
||||
|
||||
export async function setupIPC(window = mainWindow) {
|
||||
if (window) {
|
||||
registerMainWindow(window);
|
||||
}
|
||||
|
||||
if (!mainWindow) {
|
||||
logger.warn("No main window available, skipping RPC setup");
|
||||
return;
|
||||
}
|
||||
|
||||
setMessageSender((channel, data) => {
|
||||
try {
|
||||
const send = mainWindow?.webview?.rpc?.send;
|
||||
|
||||
if (send) {
|
||||
send[channel](data);
|
||||
logger.info(`Message sent to main window on channel: ${channel}`, data);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
`No RPC sender available for channel: ${channel}. Is the window ready?`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`Error sending message on channel ${channel}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
logger.info("RPC handlers ready");
|
||||
}
|
||||
59
src/desktop/window.js
Normal file
59
src/desktop/window.js
Normal file
@ -0,0 +1,59 @@
|
||||
import log4js from "log4js";
|
||||
import { BrowserWindow, Updater } from "electrobun/bun";
|
||||
import { loadConfig } from "../config.js";
|
||||
import { startWaiting, stopWaiting } from "../spinner.js";
|
||||
import { createAppRpc, registerMainWindow } from "./rpc.js";
|
||||
|
||||
const config = loadConfig();
|
||||
const logger = log4js.getLogger("Desktop");
|
||||
logger.level = config.logLevel;
|
||||
|
||||
const DEV_SERVER_PORT = 5287;
|
||||
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
|
||||
|
||||
export async function getMainViewUrl() {
|
||||
const channel = await Updater.localInfo.channel();
|
||||
if (channel === "dev" || process.env.NODE_ENV === "development") {
|
||||
try {
|
||||
await fetch(DEV_SERVER_URL, { method: "HEAD" });
|
||||
logger.info(`Using Vite dev server at ${DEV_SERVER_URL}`);
|
||||
return DEV_SERVER_URL;
|
||||
} catch {
|
||||
logger.warn(
|
||||
"Vite dev server not running. Start it with `bun run dev:renderer`.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return "views://mainview/index.html";
|
||||
}
|
||||
|
||||
export async function createDesktopWindow() {
|
||||
const url = await getMainViewUrl();
|
||||
const rpc = createAppRpc();
|
||||
|
||||
startWaiting("Creating desktop window...", logger);
|
||||
|
||||
const mainWindow = new BrowserWindow({
|
||||
title: "Farm Control Server",
|
||||
url,
|
||||
rpc,
|
||||
titleBarStyle: "hiddenInset",
|
||||
trafficLightOffset: { x: 14, y: 12 },
|
||||
frame: {
|
||||
width: 900,
|
||||
height: 600,
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
registerMainWindow(mainWindow);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
mainWindow.webview.on("dom-ready", async () => {
|
||||
await stopWaiting();
|
||||
resolve(mainWindow);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -1,122 +0,0 @@
|
||||
import log4js from "log4js";
|
||||
import { notPrompting } from "../utils.js";
|
||||
import { startWaiting, stopWaiting } from "../spinner.js";
|
||||
|
||||
const logger = log4js.getLogger("IPC");
|
||||
let mainWindow = null;
|
||||
|
||||
export async function setupIPC() {
|
||||
// Only import Electron if we're in an Electron environment
|
||||
let ipcMain;
|
||||
try {
|
||||
const electron = await import("electron");
|
||||
ipcMain = electron.ipcMain;
|
||||
mainWindow = global.mainWindow;
|
||||
} catch (error) {
|
||||
logger.warn("Electron not available, skipping IPC setup");
|
||||
return;
|
||||
}
|
||||
|
||||
// Only proceed if we have ipcMain
|
||||
if (!ipcMain) {
|
||||
logger.warn("ipcMain not available, skipping IPC setup");
|
||||
return;
|
||||
}
|
||||
|
||||
ipcMain.on("getData", async () => {
|
||||
startWaiting("Getting data...", logger);
|
||||
try {
|
||||
// Get the global socket client instance
|
||||
const socketClient = global.socketClient;
|
||||
|
||||
if (!socketClient) {
|
||||
sendIPC("setAuthenticated", false);
|
||||
sendIPC("setConnected", false);
|
||||
sendIPC("setLoading", false);
|
||||
sendIPC("setHost", {});
|
||||
sendIPC("setPrinters", []);
|
||||
sendIPC("setDocumentPrinters", []);
|
||||
await stopWaiting();
|
||||
return;
|
||||
}
|
||||
|
||||
// Send individual data pieces to match renderer expectations
|
||||
sendIPC("setAuthenticated", socketClient.authenticated);
|
||||
sendIPC("setConnected", socketClient.connected);
|
||||
sendIPC("setLoading", socketClient.loading);
|
||||
sendIPC("setHost", socketClient.host || {});
|
||||
sendIPC("setPrinters", socketClient.printerManager.printers || []);
|
||||
sendIPC(
|
||||
"setDocumentPrinters",
|
||||
socketClient.documentPrinterManager.documentPrinters || [],
|
||||
);
|
||||
await stopWaiting();
|
||||
} catch (error) {
|
||||
logger.error("Error getting printer data:", error);
|
||||
sendIPC("setAuthenticated", false);
|
||||
sendIPC("setConnected", false);
|
||||
sendIPC("setLoading", false);
|
||||
sendIPC("setHost", {});
|
||||
sendIPC("setPrinters", []);
|
||||
await stopWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
// Window management IPC handlers
|
||||
ipcMain.on("window-minimize", (event) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.minimize();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on("window-maximize", (event) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
if (mainWindow.isMaximized()) {
|
||||
mainWindow.unmaximize();
|
||||
} else {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on("window-close", (event) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.close();
|
||||
}
|
||||
});
|
||||
|
||||
// OTP authentication handler
|
||||
ipcMain.on("authenticateOTP", async (event, otp) => {
|
||||
startWaiting("Authenticating with OTP...", logger);
|
||||
try {
|
||||
const socketClient = global.socketClient;
|
||||
if (socketClient) {
|
||||
notPrompting();
|
||||
await socketClient.authenticateWithOtp(otp);
|
||||
} else {
|
||||
logger.error("Socket client not available for OTP authentication");
|
||||
}
|
||||
await stopWaiting();
|
||||
} catch (error) {
|
||||
await stopWaiting();
|
||||
logger.error("Error during OTP authentication:", error);
|
||||
}
|
||||
});
|
||||
|
||||
logger.info("IPC handlers setup complete");
|
||||
}
|
||||
|
||||
export function sendIPC(channel, data) {
|
||||
try {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send(channel, data);
|
||||
logger.info(`Message sent to main window on channel: ${channel}`, data);
|
||||
} else {
|
||||
logger.warn(
|
||||
`No main window available, cannot send message on channel: ${channel}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error sending message on channel ${channel}:`, error);
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
import { contextBridge, ipcRenderer } from "electron";
|
||||
|
||||
// Expose protected methods that allow the renderer process to use
|
||||
// the ipcRenderer without exposing the entire object
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
onIPCData: (channel, callback) => {
|
||||
ipcRenderer.on(channel, (event, data) => callback(data));
|
||||
},
|
||||
// Send messages to main process
|
||||
sendIPC: (channel, data) => {
|
||||
console.log("SEND IPC", channel);
|
||||
ipcRenderer.send(channel, data);
|
||||
},
|
||||
// Window management
|
||||
minimize: () => ipcRenderer.send("window-minimize"),
|
||||
maximize: () => ipcRenderer.send("window-maximize"),
|
||||
close: () => ipcRenderer.send("window-close"),
|
||||
removeAllListeners: (channel) => {
|
||||
ipcRenderer.removeAllListeners(channel);
|
||||
},
|
||||
});
|
||||
@ -1,107 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
// Remove CommonJS requires and use ES module imports
|
||||
import path from "path";
|
||||
import { loadConfig } from "../config.js";
|
||||
import log4js from "log4js";
|
||||
import { fileURLToPath } from "url";
|
||||
import { startWaiting, stopWaiting } from "../spinner.js";
|
||||
// Load configuration
|
||||
const config = loadConfig();
|
||||
|
||||
const logger = log4js.getLogger("Electron");
|
||||
logger.level = config.logLevel;
|
||||
|
||||
export async function createElectronWindow() {
|
||||
// Only import Electron if we're in an Electron environment
|
||||
let app, BrowserWindow;
|
||||
try {
|
||||
const electron = (await import("electron")).default;
|
||||
app = electron.app;
|
||||
BrowserWindow = electron.BrowserWindow;
|
||||
logger.trace("Imported electron");
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
"Electron not available, skipping window creation. Error:",
|
||||
error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only proceed if we have app
|
||||
if (!app) {
|
||||
logger.warn("Electron app not available, skipping window creation");
|
||||
return;
|
||||
}
|
||||
|
||||
// __dirname workaround for ES modules
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Determine if we are running from the build directory or src
|
||||
const isRunningFromBuild =
|
||||
__dirname.includes(path.sep + "build") || __dirname.includes("app.asar");
|
||||
|
||||
return new Promise((resolve) => {
|
||||
function createWindow() {
|
||||
logger.debug("Creating browser window...");
|
||||
|
||||
// Resolve paths correctly based on environment
|
||||
const preloadPath =
|
||||
process.env.NODE_ENV === "development"
|
||||
? path.join(__dirname, "preload.js")
|
||||
: isRunningFromBuild
|
||||
? path.join(__dirname, "preload.js")
|
||||
: path.join(__dirname, "..", "build", "electron", "preload.js");
|
||||
|
||||
const win = new BrowserWindow({
|
||||
width: 900,
|
||||
height: 600,
|
||||
resizable: true,
|
||||
frame: false,
|
||||
titleBarStyle: "hiddenInset",
|
||||
trafficLightPosition: { x: 14, y: 12 },
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
preload: preloadPath,
|
||||
},
|
||||
});
|
||||
|
||||
// Make the window globally accessible for IPC
|
||||
global.mainWindow = win;
|
||||
|
||||
logger.info("Preload Script", preloadPath);
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
startWaiting("Loading development url...", logger);
|
||||
win.loadURL("http://localhost:5287"); // Vite dev server
|
||||
} else {
|
||||
// In production, the built files will be in the build/electron directory
|
||||
const htmlPath = isRunningFromBuild
|
||||
? path.join(__dirname, "index.html")
|
||||
: path.join(__dirname, "..", "build", "electron", "index.html");
|
||||
|
||||
startWaiting("Loading production file...", logger);
|
||||
logger.info("Loading production file:", htmlPath);
|
||||
win.loadFile(htmlPath);
|
||||
}
|
||||
|
||||
// Resolve the promise when the window is ready
|
||||
win.webContents.on("did-finish-load", async () => {
|
||||
await stopWaiting();
|
||||
resolve(win);
|
||||
});
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
app.on("activate", function () {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
});
|
||||
|
||||
app.on("window-all-closed", function () {
|
||||
if (process.platform !== "darwin") app.quit();
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user