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
15 changes: 15 additions & 0 deletions installer/windows/redrob-code.iss
Original file line number Diff line number Diff line change
Expand Up @@ -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. }
Expand Down
11 changes: 11 additions & 0 deletions packages/redrob/src/cli/cmd/serve.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os from "node:os"
import { Effect } from "effect"
import { effectCmd } from "../effect-cmd"
import { withNetworkOptions, resolveNetworkOptions } from "../network"
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions packages/redrob/src/daemon/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -87,6 +88,9 @@ export async function connectOrStart(options: StartOptions = {}): Promise<Connec
detached: true,
stdio: "ignore",
env,
// The daemon outlives its caller. Inheriting the caller's cwd (an app's install folder) would
// hold that folder open for the daemon's whole life and block the app's Windows update.
cwd: os.homedir(),
})
child.unref()

Expand Down
3 changes: 3 additions & 0 deletions packages/sdk/js/src/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ function runServiceStart(options: DaemonOptions, bin: string): Promise<DaemonRec
return new Promise((resolve, reject) => {
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,
})
Expand Down
Loading