diff --git a/installer/windows/redrob-code.iss b/installer/windows/redrob-code.iss index 96470b0b95..387f7f9bbd 100644 --- a/installer/windows/redrob-code.iss +++ b/installer/windows/redrob-code.iss @@ -141,6 +141,21 @@ begin RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', Rebuilt); end; +{ A running shared daemon is `redrob.exe serve --register` from this directory, and Windows will not + overwrite an exe that is running. Stop it through the installed CLI before [Files] replaces it; + attached apps reconnect and start the new version. Any failure (no install yet, nothing running, + an old binary without `service stop`) is ignored: the install proceeds either way. } +function PrepareToInstall(var NeedsRestart: Boolean): String; +var + Existing: string; + ResultCode: Integer; +begin + Result := ''; + Existing := ExpandConstant('{app}\redrob.exe'); + if FileExists(Existing) then + Exec(Existing, 'service stop', ExpandConstant('{localappdata}'), SW_HIDE, ewWaitUntilTerminated, ResultCode); +end; + procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); begin { usUninstall, not usPostUninstall: the value is removed while the app path is still known good. } diff --git a/packages/redrob/src/cli/cmd/serve.ts b/packages/redrob/src/cli/cmd/serve.ts index f4848b510d..59530a2fdd 100644 --- a/packages/redrob/src/cli/cmd/serve.ts +++ b/packages/redrob/src/cli/cmd/serve.ts @@ -1,3 +1,4 @@ +import os from "node:os" import { Effect } from "effect" import { effectCmd } from "../effect-cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" @@ -41,6 +42,16 @@ export const ServeCommand = effectCmd({ handler: Effect.fn("Cli.serve")(function* (args) { const { Server } = yield* Effect.promise(() => import("../../server/server")) const register = args.register === true + // The shared daemon outlives whoever started it. A caller that spawned it from an app's + // install folder (older SDKs did) would otherwise have that folder held open for the daemon's + // whole life, blocking the app's Windows update. Requests carry their own directory. + if (register) { + try { + process.chdir(os.homedir()) + } catch { + /* keep the inherited cwd */ + } + } const Registry = register ? yield* Effect.promise(() => import("../../daemon/registry")) : undefined // The daemon's secret is per start. It must be in the environment BEFORE the server's config // layer reads it, and it is what goes into the record, so other apps can authenticate. diff --git a/packages/redrob/src/daemon/connect.ts b/packages/redrob/src/daemon/connect.ts index 741005ca09..059a3ff377 100644 --- a/packages/redrob/src/daemon/connect.ts +++ b/packages/redrob/src/daemon/connect.ts @@ -9,6 +9,7 @@ * for the new record to answer. */ import { spawn } from "node:child_process" +import os from "node:os" import path from "node:path" import { InstallationVersion } from "@redrob-code/core/installation/version" import * as Registry from "./registry" @@ -87,6 +88,9 @@ export async function connectOrStart(options: StartOptions = {}): Promise { const child = spawn(bin, ["service", "start", "--json", "--idle-exit", String(idle)], { env, + // `service start` spawns the long-lived daemon, which inherits this cwd. The app's own + // install folder must not be it: the daemon would hold it open and block the app's update. + cwd: os.homedir(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, })