Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions electron/ipc-web.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"use strict";

/**
* IPC_HANDLERS rows for web:* (#1512 I2): the Solenta Web switch, device
* tokens and Tailscale Serve. Desktop only: a paired browser must not be
* able to mint more tokens, revoke others, or turn the server off under
* itself.
*/

function requireWebAccess(ctx) {
if (ctx.transport !== "desktop" || !ctx.webAccess) {
throw new Error("Solenta Web settings require the desktop app.");
}
return ctx.webAccess;
}

module.exports = {
"web:status": async (ctx) => requireWebAccess(ctx).status(),
"web:setEnabled": async (ctx, input) => requireWebAccess(ctx).setEnabled(input || {}),
"web:addDevice": async (ctx, input) => requireWebAccess(ctx).addDevice(input || {}),
"web:revokeDevice": async (ctx, input) => requireWebAccess(ctx).revokeDevice(input || {}),
"web:setTailscale": async (ctx, input) => requireWebAccess(ctx).setTailscale(input || {}),
};
3 changes: 3 additions & 0 deletions electron/ipc.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const gitHandlers = require("./ipc-git.js");
const filesHandlers = require("./ipc-files.js");
const devserverHandlers = require("./ipc-devserver.js");
const simulatorHandlers = require("./ipc-simulator.js");
const webHandlers = require("./ipc-web.js");

/**
* Default window fan-out (desktop transport). main.js replaces this with a
Expand Down Expand Up @@ -74,6 +75,7 @@ function makeCtx(deps) {
: () => ({ running: false, port: null }),
openRemoteConnection: deps.openRemoteConnection,
forgetRemoteConnection: deps.forgetRemoteConnection,
webAccess: deps.webAccess || null,
transport: "desktop",
};
}
Expand Down Expand Up @@ -107,6 +109,7 @@ const IPC_HANDLERS = {
...filesHandlers,
...devserverHandlers,
...simulatorHandlers,
...webHandlers,
};

/**
Expand Down
59 changes: 37 additions & 22 deletions electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ const {
startWebServer,
loadOrCreateToken,
HOST_FLAG_HELP,
DEFAULT_PORT: WEB_DEFAULT_PORT,
} = require("./webServer.js");
const { createWebDevices } = require("./webDevices.js");
const { createWebAccess } = require("./webAccess.js");
const { migrateLegacyUserData } = require("./legacy-migration.js");
const { configureDefaultSecrets } = require("./secrets.js");
const { installCrashGuard } = require("./crash-guard.js");
Expand Down Expand Up @@ -161,8 +164,9 @@ let postMergeScheduler = null;
/** @type {ReturnType<typeof startMemoryConsolidateScheduler> | null} */
let memoryConsolidateScheduler = null;

/** @type {Awaited<ReturnType<typeof startWebServer>> | null} */
let webServer = null;
/** Solenta Web: Settings switch, devices, Tailscale (#1512 I2). */
/** @type {ReturnType<typeof createWebAccess> | null} */
let webAccess = null;

/** @type {InstanceType<typeof Store> | null} */
let store = null;
Expand Down Expand Up @@ -354,8 +358,8 @@ function broadcast(channel, payload) {
win.webContents.send(channel, payload);
}
}
if (webServer) {
webServer.broadcast(channel, payload);
if (webAccess) {
webAccess.broadcast(channel, payload);
}
}

Expand Down Expand Up @@ -747,7 +751,25 @@ app.whenReady().then(async () => {
syncAttentionBadge(store);
setInterval(() => syncAttentionBadge(store), 2000);

const registered = registerIpc({
// ctx is read when the server starts, after registerIpc below returns.
let registered = null;
const webStaticDir = path.join(__dirname, "../dist");
webAccess = createWebAccess({
devices: createWebDevices(userData),
defaultPort: WEB_DEFAULT_PORT,
startServer: (opts) =>
startWebServer({
...opts,
staticDir: fs.existsSync(webStaticDir) ? webStaticDir : null,
ctx: registered.ctx,
artifactStore,
log: (msg) => console.warn(msg),
}),
log: (msg) => console.warn(msg),
});

registered = registerIpc({
webAccess,
ipcMain,
dialog,
store,
Expand Down Expand Up @@ -786,33 +808,30 @@ app.whenReady().then(async () => {

if (serveOpts.enabled) {
const token = loadOrCreateToken(userData);
// --serve-web keeps printing one token; it is the Legacy device row.
webAccess.adoptLegacy(token);
// Contract: print the token to stdout when serve mode starts.
process.stdout.write(`solenta-web: token ${token}\n`);
if (serveOpts.host !== "127.0.0.1") {
process.stdout.write(`solenta-web: ${HOST_FLAG_HELP}\n`);
}
const staticDir = path.join(__dirname, "../dist");
// The port is fixed, so EADDRINUSE is routine (a previous instance still
// holds it). Never let that reject out of whenReady: the rest of boot
// (IPC ready push, schedulers) would be skipped even though the window
// is already up.
try {
webServer = await startWebServer({
host: serveOpts.host,
port: serveOpts.port,
staticDir: fs.existsSync(staticDir) ? staticDir : null,
token,
ctx: registered.ctx,
artifactStore,
log: (msg) => console.warn(msg),
});
await webAccess.start({ host: serveOpts.host, port: serveOpts.port });
} catch (err) {
// No listener means nothing to keep the process alive headless either.
serveOpts.enabled = false;
process.stdout.write(
`solenta-web: cannot listen on ${serveOpts.host}:${serveOpts.port} (${err && err.message ? err.message : err}); continuing without web serve\n`,
);
}
} else {
// The Settings switch, if the user left it on. Loopback unless they
// also allowed network access. Never throws.
await webAccess.restore();
}

// Round 47: lazy PR-state freshness. Async/serialized/latched so a slow gh
Expand Down Expand Up @@ -982,13 +1001,9 @@ shutdown = installShutdown({
/** Servers, schedulers, and child processes: last, after runs and the device. */
function teardownServices() {
closeRemoteConnections();
if (webServer) {
try {
void webServer.close();
} catch {
// ignore
}
webServer = null;
if (webAccess) {
void webAccess.stop().catch(() => {});
webAccess = null;
}
if (prStateRefresher) {
try {
Expand Down
5 changes: 5 additions & 0 deletions electron/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ const IPC_CHANNELS = Object.freeze([
{ ns: "pairing", method: "revoke" },
{ ns: "pairing", method: "approve" },
{ ns: "pairing", method: "reject" },
{ ns: "web", method: "status" },
{ ns: "web", method: "setEnabled" },
{ ns: "web", method: "addDevice" },
{ ns: "web", method: "revokeDevice" },
{ ns: "web", method: "setTailscale" },
{ ns: "skills", method: "list" },
{ ns: "skills", method: "add" },
{ ns: "skills", method: "remove" },
Expand Down
8 changes: 5 additions & 3 deletions electron/test/web.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ describe("one map, two transports + packaging", () => {
assert.match(verify, /webBridge\.js/);
});

it("main.js starts only on --serve-web via shared ctx", () => {
it("main.js starts on --serve-web or the saved switch, via shared ctx", () => {
const main = fs.readFileSync(path.join(__dirname, "../main.js"), "utf8");
assert.match(main, /parseServeWebArgs/);
assert.match(main, /startWebServer/);
Expand All @@ -504,15 +504,17 @@ describe("one map, two transports + packaging", () => {
assert.match(main, /HOST_FLAG_HELP/);
assert.doesNotMatch(main, /parseServeArgs\(/);
assert.match(main, /process\.stdout\.write\(`solenta-web: token/);
assert.match(main, /webAccess\.adoptLegacy\(token\)/);
assert.match(main, /\} else \{[^}]*await webAccess\.restore\(\);/);
});

it("main.js survives a web-server bind failure and still opens a window", async () => {
const main = fs.readFileSync(path.join(__dirname, "../main.js"), "utf8");
// #618: createWindow is first-paint (via bootFirstPaint), so a bind
// failure can no longer skip the window. The try/catch still matters:
// without it whenReady rejects and IPC-ready / schedulers never run.
assert.match(main, /try \{\s*webServer = await startWebServer\(/);
const start = main.indexOf("webServer = await startWebServer(");
assert.match(main, /try \{\s*await webAccess\.start\(/);
const start = main.indexOf("await webAccess.start(");
const guard = main.slice(start, start + 900);
assert.match(guard, /\} catch \(err\) \{/);
assert.match(guard, /serveOpts\.enabled = false/);
Expand Down
Loading
Loading