Skip to content
Open
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
11 changes: 11 additions & 0 deletions apps/desktop/src/backend/DesktopBackendConfiguration.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { EnvironmentId } from "@t3tools/contracts";
import { assert, describe, it } from "@effect/vitest";
import { currentDesktopBootstrapToken } from "@t3tools/shared/desktopBootstrapToken";
import * as Clock from "effect/Clock";
Expand Down Expand Up @@ -328,6 +329,12 @@ describe("DesktopBackendConfiguration", () => {
const observedDistros: Array<string | null> = [];
const config = yield* Effect.gen(function* () {
const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
const environment = yield* DesktopEnvironment.DesktopEnvironment;
yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });
yield* fileSystem.writeFileString(
path.join(environment.stateDir, "environment-id"),
"windows-primary\n",
);
return yield* configuration.resolveWsl({ port: 5000, distro: null });
}).pipe(
Effect.provide(
Expand Down Expand Up @@ -368,6 +375,9 @@ describe("DesktopBackendConfiguration", () => {
);

assert.equal(config.runningDistro, "Ubuntu");
assert.deepEqual(config.bootstrap.desktopHost, {
environmentId: EnvironmentId.make("windows-primary"),
});
assert.deepEqual(config.args.slice(0, 2), ["-d", "Ubuntu"]);
assert.deepEqual(observedDistros, ["Ubuntu", "Ubuntu", "Ubuntu"]);
assert.isTrue(Option.isNone(config.preflightFailure));
Expand Down Expand Up @@ -761,6 +771,7 @@ describe("DesktopBackendConfiguration", () => {
assert.equal(config.bootstrap.host, "0.0.0.0");
assert.equal(config.bootstrap.tailscaleServeEnabled, false);
assert.notProperty(config.bootstrap, "desktopTelemetryFd");
assert.deepEqual(config.bootstrap.desktopHost, {});
assert.notProperty(config.bootstrap, "resourceMonitorPath");
assert.equal(config.httpBaseUrl.href, "http://172.27.0.99:5050/");
assert.equal(config.env.OPENAI_API_KEY, "openai-key");
Expand Down
14 changes: 13 additions & 1 deletion apps/desktop/src/backend/DesktopBackendConfiguration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as NodeOS from "node:os";

import { EnvironmentId } from "@t3tools/contracts";
import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";
import { currentDesktopBootstrapToken } from "@t3tools/shared/desktopBootstrapToken";
import * as Clock from "effect/Clock";
Expand Down Expand Up @@ -548,6 +549,7 @@ const resolveWslStartConfig = Effect.fn("desktop.backendConfiguration.resolveWsl
input: SharedBootstrapInput & {
readonly port: number;
readonly distro: string | null;
readonly hostEnvironmentId?: EnvironmentId | undefined;
},
): Effect.fn.Return<
DesktopBackendManager.DesktopBackendStartConfig,
Expand Down Expand Up @@ -575,6 +577,8 @@ const resolveWslStartConfig = Effect.fn("desktop.backendConfiguration.resolveWsl
// PortSchema rejects 0 even when Tailscale Serve is disabled.
tailscaleServeEnabled: false,
tailscaleServePort: 443,
desktopHost:
input.hostEnvironmentId === undefined ? {} : { environmentId: input.hostEnvironmentId },
...buildObservabilityFragment(input.observabilitySettings),
};

Expand Down Expand Up @@ -833,7 +837,15 @@ export const make = Effect.gen(function* () {
resolveWsl: Effect.fnUntraced(
function* (input) {
const shared = yield* sharedInputs;
return yield* resolveWslStartConfig({ ...shared, ...input }).pipe(
// Ownership is optional; WSL must start even before the primary creates its identity.
const hostEnvironmentId = yield* fileSystem
.readFileString(environment.path.join(environment.stateDir, "environment-id"))
.pipe(
Effect.map((value) => value.trim()),
Effect.map((value) => (value ? EnvironmentId.make(value) : undefined)),
Effect.orElseSucceed(() => undefined),
);
return yield* resolveWslStartConfig({ ...shared, ...input, hostEnvironmentId }).pipe(
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Effect.provideService(DesktopEnvironment.DesktopEnvironment, environment),
Effect.provideService(DesktopWslEnvironment.DesktopWslEnvironment, wslEnvironment),
Effect.provideService(DesktopWslServerTree.DesktopWslServerTree, wslServerTree),
Expand Down
19 changes: 14 additions & 5 deletions apps/desktop/src/wsl/DesktopWslBackend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,19 @@ describe("DesktopWslBackend", () => {
let registeredSpec: DesktopBackendPool.BackendInstanceSpec | undefined;
let registeredWsl: DesktopBackendPool.DesktopBackendInstance | undefined;
let startCount = 0;
const primary = makeStubInstance({
id: DesktopBackendPool.PRIMARY_INSTANCE_ID,
label: "Windows",
snapshot: primarySnapshot,
});
let primaryReadyChecked = false;
const primary = {
...makeStubInstance({
id: DesktopBackendPool.PRIMARY_INSTANCE_ID,
label: "Windows",
snapshot: primarySnapshot,
}),
waitForReady: () =>
Effect.sync(() => {
primaryReadyChecked = true;
return true;
}),
};
const layerPool = Layer.succeed(DesktopBackendPool.DesktopBackendPool, {
get: (id) =>
Effect.succeed(
Expand All @@ -104,6 +112,7 @@ describe("DesktopWslBackend", () => {
primary: Effect.succeed(primary),
register: (spec) =>
Effect.sync(() => {
assert.isTrue(primaryReadyChecked);
registeredSpec = spec;
registeredWsl = makeStubInstance({
id: spec.id,
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/wsl/DesktopWslBackend.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as Cause from "effect/Cause";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Duration from "effect/Duration";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
Expand Down Expand Up @@ -92,6 +93,9 @@ export const layer = Layer.effect(
const startNew = Effect.fn("desktop.wslBackend.startNew")(function* (input: {
readonly distro: string | null;
}) {
const primary = yield* pool.primary;
// Readiness creates the owner identity; keep WSL available if the primary cannot start.
yield* primary.waitForReady(Duration.seconds(60));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const primaryConfig = yield* serverExposure.backendConfig;
const port = yield* scanForWslPort(primaryConfig.port + 1).pipe(
Effect.provideService(NetService.NetService, net),
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ export const resolveServerConfig = Effect.fn("config.resolveServerConfig")(funct
const desktopBrowserFd = bootstrap?.desktopBrowserFd;
const desktopBrowserControlFd = bootstrap?.desktopBrowserControlFd;
const resourceMonitorPath = bootstrap?.resourceMonitorPath;
const desktopHost = bootstrap?.desktopHost;
const autoBootstrapProjectFromCwd = Option.getOrElse(
resolveOptionPrecedence(
Option.fromUndefinedOr(options?.forceAutoBootstrapProjectFromCwd),
Expand Down Expand Up @@ -502,6 +503,7 @@ export const resolveServerConfig = Effect.fn("config.resolveServerConfig")(funct
desktopBrowserFd,
desktopBrowserControlFd,
resourceMonitorPath,
...(desktopHost === undefined ? {} : { desktopHost }),
autoBootstrapProjectFromCwd,
logWebSocketEvents,
tailscaleServeEnabled,
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as Path from "effect/Path";
import type * as Redacted from "effect/Redacted";
import * as Schema from "effect/Schema";

import type { OtlpEndpointSource } from "@t3tools/contracts";
import type { DesktopHost, OtlpEndpointSource } from "@t3tools/contracts";
import { sweepStalePendingAttachments } from "./attachmentStore.ts";
import {
DEFAULT_SIGNAL_EXPORT,
Expand Down Expand Up @@ -102,6 +102,7 @@ export class ServerConfig extends Context.Service<
readonly desktopBrowserFd?: number | undefined;
readonly desktopBrowserControlFd?: number | undefined;
readonly resourceMonitorPath?: string | undefined;
readonly desktopHost?: DesktopHost | undefined;
readonly autoBootstrapProjectFromCwd: boolean;
readonly logWebSocketEvents: boolean;
readonly tailscaleServeEnabled: boolean;
Expand Down
28 changes: 27 additions & 1 deletion apps/server/src/environment/ServerEnvironment.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { ORCHESTRATION_PROTOCOL_VERSION } from "@t3tools/contracts";
import { EnvironmentId, ORCHESTRATION_PROTOCOL_VERSION } from "@t3tools/contracts";
import { expect, it } from "@effect/vitest";
import * as Crypto from "effect/Crypto";
import * as Deferred from "effect/Deferred";
Expand Down Expand Up @@ -122,6 +122,32 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
}
}),
);
it.effect("publishes the desktop host environment only for a hosted backend", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const baseDir = yield* fs.makeTempDirectoryScoped();
const config = yield* makeServerConfig(baseDir);
yield* fs.makeDirectory(config.stateDir, { recursive: true });
for (const desktopHost of [
{ environmentId: EnvironmentId.make("windows-primary") },
{},
undefined,
]) {
const descriptor = yield* Effect.gen(function* () {
const environment = yield* ServerEnvironment.ServerEnvironment;
return yield* environment.getDescriptor;
}).pipe(
Effect.provide(
ServerEnvironment.layer.pipe(
Layer.provide(layerEmptySecretStore),
Layer.provide(ServerConfig.layer({ ...config, mode: "desktop", desktopHost })),
),
),
);
expect(descriptor.desktopHost).toEqual(desktopHost);
}
}),
);
it.effect.each([
{ name: "missing", content: undefined },
{ name: "empty", content: "" },
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ export const make = Effect.gen(function* () {
},
serverVersion: packageJson.version,
orchestrationProtocolVersion: ORCHESTRATION_PROTOCOL_VERSION,
...(serverConfig.desktopHost === undefined ? {} : { desktopHost: serverConfig.desktopHost }),
...(databaseMigration === undefined ? {} : { databaseMigration }),
capabilities: {
repositoryIdentity: true,
Expand Down
101 changes: 98 additions & 3 deletions apps/web/src/components/sidebar/updateEverything.logic.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { EnvironmentId, type ServerSelfUpdateCapability } from "@t3tools/contracts";
import {
EnvironmentId,
type ServerConfig,
type ServerSelfUpdateCapability,
} from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import {
Expand Down Expand Up @@ -45,6 +49,47 @@ function machine(
};
}

function withCapabilities(
base: UpdateEverythingMachine,
capabilities: Partial<ServerConfig["environment"]["capabilities"]>,
): UpdateEverythingMachine {
return {
...base,
serverConfig: {
...base.serverConfig!,
environment: {
...base.serverConfig!.environment,
capabilities: { ...base.serverConfig!.environment.capabilities, ...capabilities },
},
},
};
}

const secondary = (
id: string,
overrides: Partial<UpdateEverythingMachine> = {},
desktopHost: { readonly environmentId?: EnvironmentId } = {
environmentId: EnvironmentId.make("windows-primary"),
},
) => {
const base = withCapabilities(machine(id, "0.0.40", "desktop-managed", overrides), {
desktopAppUpdate: false,
});
return {
...base,
serverConfig: {
...base.serverConfig!,
environment: {
...base.serverConfig!.environment,
desktopHost,
},
},
};
};

const oldDesktop = (id: string) =>
withCapabilities(machine(id, "0.0.40", "desktop-managed"), { desktopAppUpdate: false });

describe("collectServerUpdateTargets", () => {
it("updates remote servers that can move to the target and names every skipped one", () => {
const { targets, skipped } = collectServerUpdateTargets(
Expand All @@ -53,6 +98,10 @@ describe("collectServerUpdateTargets", () => {
machine("service", "0.0.40", "boot-service"),
machine("nightly-service", "0.0.49-nightly.20261001.3", "boot-service"),
machine("other-desktop", "0.0.49-nightly.20261001.3", "desktop-managed"),
secondary("other-wsl"),
secondary("offline-wsl", { connected: false }),
secondary("unattributed-wsl", {}, {}),
oldDesktop("old-desktop"),
machine("offline", "0.0.40", "boot-service", { connected: false }),
machine("never-connected", "0.0.40", "boot-service", { serverConfig: null }),
machine("no-scope", "0.0.40", "boot-service", { canMaintain: false }),
Expand All @@ -77,13 +126,57 @@ describe("collectServerUpdateTargets", () => {
]);
expect(skipped.map((entry) => [entry.label, entry.reason])).toEqual([
["nightly-service", "on the nightly channel"],
["other-wsl", "update its owning desktop app"],
["offline-wsl", "not connected"],
["unattributed-wsl", "update its owning desktop app"],
["old-desktop", "needs a newer desktop app"],
["offline", "not connected"],
["never-connected", "not connected"],
["no-scope", "no permission"],
["busy", "already updating"],
["manual", "needs a manual update"],
]);
});

it.each([
{
name: "updating",
owner: machine("windows-primary", "0.0.40", "desktop-managed"),
covered: true,
},
{ name: "absent", owner: undefined, covered: false },
{
name: "offline",
owner: machine("windows-primary", "0.0.40", "desktop-managed", { connected: false }),
covered: false,
},
{
name: "denied",
owner: machine("windows-primary", "0.0.40", "desktop-managed", { canMaintain: false }),
covered: false,
},
{
name: "current",
owner: machine("windows-primary", "0.0.50", "desktop-managed"),
covered: false,
},
{
name: "not a desktop",
owner: machine("windows-primary", "0.0.40", "boot-service"),
covered: false,
},
])("only omits a secondary when its owner is updating: $name", ({ owner, covered }) => {
const { targets, skipped } = collectServerUpdateTargets(
[secondary("wsl"), ...(owner ? [owner] : [])],
"0.0.50",
);
expect(targets.some((target) => target.environmentId === "wsl")).toBe(false);
expect(skipped.find((entry) => entry.environmentId === "wsl")).toEqual(
covered
? undefined
: { environmentId: "wsl", label: "wsl", reason: "update its owning desktop app" },
);
});
});

describe("collectChannelSwitchTargets", () => {
Expand Down Expand Up @@ -238,8 +331,9 @@ describe("collectRestartingMachines", () => {
withContinuation("opted-out", {}, false),
machine("old", "0.0.40", "boot-service"),
withContinuation("providers-only"),
secondary("wsl"),
];
const servers = ["alvin", "opted-out", "old"].map((id) => ({
const servers = ["alvin", "opted-out", "old", "windows-primary"].map((id) => ({
environmentId: EnvironmentId.make(id),
}));

Expand All @@ -248,10 +342,11 @@ describe("collectRestartingMachines", () => {
{ environmentId: "alvin", continuesThreads: true },
{ environmentId: "opted-out", continuesThreads: false },
{ environmentId: "old", continuesThreads: false },
{ environmentId: "wsl", continuesThreads: false },
]);
expect(
collectRestartingMachines(machines, servers, false).map((entry) => entry.environmentId),
).toEqual(["alvin", "opted-out", "old"]);
).toEqual(["alvin", "opted-out", "old", "wsl"]);
});
});

Expand Down
Loading
Loading