From e76d1496ec4d500e4bd844aca5593c72245f6aa6 Mon Sep 17 00:00:00 2001 From: Robbie Date: Tue, 20 Oct 2026 21:04:14 -0600 Subject: [PATCH 1/2] fix(wsl): label WSL environments with their Windows host on every client WSL inherits the Windows computer name as its hostname, so remote clients showed the Windows and WSL servers under the same name. The server now reports "HOST WSL (distro)" when running in WSL, and the desktop app uses the same format instead of "Windows" / "WSL (distro)", so the host and remote clients agree. Co-Authored-By: Claude Opus 5.5 --- .../backend/DesktopBackendConfiguration.ts | 7 +- apps/desktop/src/ipc/methods/window.test.ts | 12 ++- apps/desktop/src/ipc/methods/window.ts | 6 +- apps/desktop/src/wsl/DesktopWslBackend.ts | 8 +- .../ServerEnvironmentLabel.test.ts | 76 +++++++++++++++++++ .../src/environment/ServerEnvironmentLabel.ts | 19 ++++- apps/web/src/connection/platform.ts | 3 +- packages/shared/package.json | 4 + packages/shared/src/environmentLabel.ts | 7 ++ 9 files changed, 129 insertions(+), 13 deletions(-) create mode 100644 apps/server/src/environment/ServerEnvironmentLabel.test.ts create mode 100644 packages/shared/src/environmentLabel.ts diff --git a/apps/desktop/src/backend/DesktopBackendConfiguration.ts b/apps/desktop/src/backend/DesktopBackendConfiguration.ts index ca69f5d025899..8c8b20247ffaa 100644 --- a/apps/desktop/src/backend/DesktopBackendConfiguration.ts +++ b/apps/desktop/src/backend/DesktopBackendConfiguration.ts @@ -2,6 +2,8 @@ import * as NodeOS from "node:os"; import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings"; import { currentDesktopBootstrapToken } from "@t3tools/shared/desktopBootstrapToken"; +import { wslEnvironmentLabel } from "@t3tools/shared/environmentLabel"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; @@ -824,10 +826,11 @@ export const make = Effect.gen(function* () { }).pipe(Effect.withSpan("desktop.backendConfiguration.resolvePrimary")), resolvePrimaryLabel: Effect.gen(function* () { const { useWsl, distro } = yield* describePrimary; + const hostname = yield* HostProcess.Hostname; if (!useWsl) { - return environment.platform === "win32" ? "Windows" : "Local environment"; + return environment.platform === "win32" ? hostname : "Local environment"; } - return distro ? `WSL (${distro})` : "WSL"; + return wslEnvironmentLabel(hostname, distro); }).pipe(Effect.withSpan("desktop.backendConfiguration.resolvePrimaryLabel")), // The span options depend on each request's port and distro. resolveWsl: Effect.fnUntraced( diff --git a/apps/desktop/src/ipc/methods/window.test.ts b/apps/desktop/src/ipc/methods/window.test.ts index b5fe707f4caee..76044b9761f00 100644 --- a/apps/desktop/src/ipc/methods/window.test.ts +++ b/apps/desktop/src/ipc/methods/window.test.ts @@ -51,7 +51,7 @@ const readyWslConfig: DesktopBackendManager.DesktopBackendStartConfig = { const defaultWslInstance: DesktopBackendManager.DesktopBackendInstance = { id: DesktopBackendManager.BackendInstanceId("wsl:default"), - label: Effect.succeed("WSL (default distro)"), + label: Effect.succeed("DESKTOP-PC WSL"), start: Effect.void, stop: () => Effect.void, currentConfig: Effect.succeedSome(readyWslConfig), @@ -76,7 +76,11 @@ const backendConfigurationLayer = Layer.succeed( ); const bootstrapsLayer = (instances: ReadonlyArray) => - Layer.merge(DesktopBackendPool.layerTest([...instances]), backendConfigurationLayer); + Layer.mergeAll( + DesktopBackendPool.layerTest([...instances]), + backendConfigurationLayer, + Layer.succeed(HostProcess.Hostname, "DESKTOP-PC"), + ); describe("getLocalEnvironmentBootstraps", () => { it.effect("hands out the current window's token to a backend launched with the secret", () => @@ -86,7 +90,7 @@ describe("getLocalEnvironmentBootstraps", () => { assert.deepEqual(result, [ { id: "wsl:default", - label: "WSL (Ubuntu)", + label: "DESKTOP-PC WSL (Ubuntu)", runningDistro: "Ubuntu", httpBaseUrl: "http://127.0.0.1:3774/", wsBaseUrl: "ws://127.0.0.1:3774/", @@ -137,7 +141,7 @@ describe("getLocalEnvironmentBootstraps", () => { assert.deepEqual(result, [ { id: "wsl:default", - label: "WSL (default distro)", + label: "DESKTOP-PC WSL", runningDistro: null, httpBaseUrl: null, wsBaseUrl: null, diff --git a/apps/desktop/src/ipc/methods/window.ts b/apps/desktop/src/ipc/methods/window.ts index f0d661b58bc9a..4770f72cf093f 100644 --- a/apps/desktop/src/ipc/methods/window.ts +++ b/apps/desktop/src/ipc/methods/window.ts @@ -15,6 +15,7 @@ import { } from "@t3tools/contracts"; import { PROJECT_FAVICON_EXTENSIONS } from "@t3tools/shared/projectFavicon"; import { resolveEditorCommand } from "@t3tools/shared/editor"; +import { wslEnvironmentLabel } from "@t3tools/shared/environmentLabel"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { isRelayTracingEnabled } from "@t3tools/shared/otelEnvironment"; import * as FileSystem from "effect/FileSystem"; @@ -134,7 +135,10 @@ export const getLocalEnvironmentBootstraps = DesktopIpc.makeSyncIpcMethod({ const runningDistro = config.value.runningDistro ?? null; bootstraps.push({ id: instance.id, - label: runningDistro === null ? yield* instance.label : `WSL (${runningDistro})`, + label: + runningDistro === null + ? yield* instance.label + : wslEnvironmentLabel(yield* HostProcess.Hostname, runningDistro), runningDistro, httpBaseUrl: httpBaseUrl.href, wsBaseUrl: toWebSocketBaseUrl(httpBaseUrl), diff --git a/apps/desktop/src/wsl/DesktopWslBackend.ts b/apps/desktop/src/wsl/DesktopWslBackend.ts index e77d896be2469..5cacdf232ccdb 100644 --- a/apps/desktop/src/wsl/DesktopWslBackend.ts +++ b/apps/desktop/src/wsl/DesktopWslBackend.ts @@ -6,6 +6,8 @@ import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; import * as Semaphore from "effect/Semaphore"; +import { wslEnvironmentLabel } from "@t3tools/shared/environmentLabel"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as NetService from "@t3tools/shared/Net"; import * as DesktopObservability from "../app/DesktopObservability.ts"; @@ -38,9 +40,6 @@ const resolveTargetInstanceId = (distro: string | null): DesktopBackendPool.Back const isWslInstanceId = (id: DesktopBackendPool.BackendInstanceId): boolean => id.startsWith(WSL_INSTANCE_ID_PREFIX); -const buildLabel = (distro: string | null): string => - distro === null ? "WSL (default distro)" : `WSL (${distro})`; - // WSL auto-forwards loopback ports, so only IPv4 loopback availability matters. const scanForWslPort = Effect.fn("desktop.wslBackend.scanForWslPort")(function* ( startPort: number, @@ -65,6 +64,7 @@ export const layer = Layer.effect( const wslEnvironment = yield* DesktopWslEnvironment.DesktopWslEnvironment; const appSettings = yield* DesktopAppSettings.DesktopAppSettings; const net = yield* NetService.NetService; + const hostname = yield* HostProcess.Hostname; // Serialize bootstrap and IPC reconciles so concurrent reads cannot register competing WSL instances. const reconcileMutex = yield* Semaphore.make(1); @@ -118,7 +118,7 @@ export const layer = Layer.effect( const instance = yield* pool .register({ id: targetId, - label: Effect.succeed(buildLabel(input.distro)), + label: Effect.succeed(wslEnvironmentLabel(hostname, input.distro)), configResolve: configuration.resolveWsl({ port: allocatedPort, distro: input.distro }), onPreflightFailed: (failure) => Ref.set(preflightErrorRef, Option.some(failure.reason)).pipe(Effect.as(false)), diff --git a/apps/server/src/environment/ServerEnvironmentLabel.test.ts b/apps/server/src/environment/ServerEnvironmentLabel.test.ts new file mode 100644 index 0000000000000..a937a9d57763c --- /dev/null +++ b/apps/server/src/environment/ServerEnvironmentLabel.test.ts @@ -0,0 +1,76 @@ +import { expect, it } from "@effect/vitest"; +import * as HostProcess from "@t3tools/shared/HostProcess"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; + +import * as ProcessRunner from "../processRunner.ts"; +import { resolveServerEnvironmentLabel } from "./ServerEnvironmentLabel.ts"; + +const resolveLinuxLabel = (input: { + readonly kernelRelease: string; + readonly env: NodeJS.ProcessEnv; +}) => + resolveServerEnvironmentLabel({ cwdBaseName: "project" }).pipe( + Effect.provideService( + ProcessRunner.ProcessRunner, + ProcessRunner.ProcessRunner.of({ + run: () => + Effect.succeed({ + stdout: "", + stderr: "", + code: ChildProcessSpawner.ExitCode(1), + timedOut: false, + stdoutTruncated: false, + stderrTruncated: false, + stdoutInvalidUtf8: false, + stderrInvalidUtf8: false, + }), + }), + ), + Effect.provide( + Layer.mergeAll( + FileSystem.layerNoop({ + exists: () => Effect.succeed(false), + readFileString: (path) => + path === "/proc/sys/kernel/osrelease" + ? Effect.succeed(input.kernelRelease) + : Effect.die(`unexpected read: ${path}`), + }), + Layer.succeed(HostProcess.Platform, "linux"), + Layer.succeed(HostProcess.Hostname, "DESKTOP-PC"), + Layer.succeed(HostProcess.Environment, input.env), + ), + ), + ); + +it.effect("labels a WSL server with its host and distro", () => + Effect.gen(function* () { + const label = yield* resolveLinuxLabel({ + kernelRelease: "6.6.87.2-microsoft-standard-WSL2\n", + env: { WSL_DISTRO_NAME: "Ubuntu" }, + }); + expect(label).toBe("DESKTOP-PC WSL (Ubuntu)"); + }), +); + +it.effect("labels a WSL server without a distro name by its host", () => + Effect.gen(function* () { + const label = yield* resolveLinuxLabel({ + kernelRelease: "6.6.87.2-microsoft-standard-WSL2\n", + env: {}, + }); + expect(label).toBe("DESKTOP-PC WSL"); + }), +); + +it.effect("keeps the hostname for non-WSL Linux", () => + Effect.gen(function* () { + const label = yield* resolveLinuxLabel({ + kernelRelease: "6.8.0-45-generic\n", + env: { WSL_DISTRO_NAME: "Ubuntu" }, + }); + expect(label).toBe("DESKTOP-PC"); + }), +); diff --git a/apps/server/src/environment/ServerEnvironmentLabel.ts b/apps/server/src/environment/ServerEnvironmentLabel.ts index 73ff4a7639103..1a56801a57fac 100644 --- a/apps/server/src/environment/ServerEnvironmentLabel.ts +++ b/apps/server/src/environment/ServerEnvironmentLabel.ts @@ -1,3 +1,4 @@ +import { wslEnvironmentLabel } from "@t3tools/shared/environmentLabel"; import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -179,15 +180,31 @@ const resolveFriendlyHostLabel = Effect.fn("resolveFriendlyHostLabel")(function* return null; }); +const isWsl = Effect.fn("isWsl")(function* () { + if ((yield* HostProcess.Platform) !== "linux") { + return false; + } + const fileSystem = yield* FileSystem.FileSystem; + const kernelRelease = yield* fileSystem + .readFileString("/proc/sys/kernel/osrelease") + .pipe(Effect.orElseSucceed(() => "")); + return kernelRelease.toLowerCase().includes("microsoft"); +}); + export const resolveServerEnvironmentLabel = Effect.fn("resolveServerEnvironmentLabel")(function* ( input: ResolveServerEnvironmentLabelInput, ) { + const hostname = normalizeLabel(yield* HostProcess.Hostname); + if (hostname && (yield* isWsl())) { + const distro = normalizeLabel((yield* HostProcess.Environment).WSL_DISTRO_NAME); + return wslEnvironmentLabel(hostname, distro); + } + const friendlyHostLabel = yield* resolveFriendlyHostLabel(); if (friendlyHostLabel) { return friendlyHostLabel; } - const hostname = normalizeLabel(yield* HostProcess.Hostname); if (hostname) { return hostname; } diff --git a/apps/web/src/connection/platform.ts b/apps/web/src/connection/platform.ts index 0fe0129f11a1c..ea05db869ea5d 100644 --- a/apps/web/src/connection/platform.ts +++ b/apps/web/src/connection/platform.ts @@ -466,7 +466,8 @@ const loadSecondaryConnectionRegistration = Effect.fn( }).pipe(Effect.mapError(mapRemoteEnvironmentError)); // Desktop operations need the pool backend id to reach the owning instance; RPC uses the environment id. const connectionId = desktopLocalConnectionId(entry.id); - const label = entry.label || descriptor.label; + // The server's label is what every other client shows for this environment. + const label = descriptor.label || entry.label; return { registration: new BearerConnectionRegistration({ target: new BearerConnectionTarget({ diff --git a/packages/shared/package.json b/packages/shared/package.json index baa12fd43c5a3..0608116e9e272 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -67,6 +67,10 @@ "types": "./src/observability.ts", "import": "./src/observability.ts" }, + "./environmentLabel": { + "types": "./src/environmentLabel.ts", + "import": "./src/environmentLabel.ts" + }, "./otelEnvironment": { "types": "./src/otelEnvironment.ts", "import": "./src/otelEnvironment.ts" diff --git a/packages/shared/src/environmentLabel.ts b/packages/shared/src/environmentLabel.ts new file mode 100644 index 0000000000000..f3c2bc162403f --- /dev/null +++ b/packages/shared/src/environmentLabel.ts @@ -0,0 +1,7 @@ +// WSL mirrors the Windows computer name into its hostname, so a WSL server +// and its Windows host need the distro in the label to tell them apart. The +// server and the desktop app both label WSL environments with this, so the +// host and remote clients show the same name. +export function wslEnvironmentLabel(hostname: string, distro: string | null): string { + return distro ? `${hostname} WSL (${distro})` : `${hostname} WSL`; +} From c106818467b8ea42f99574419fab0b0731888b80 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 10:05:00 +0000 Subject: [PATCH 2/2] test(wsl): verify the Windows primary label uses its host --- .../src/backend/DesktopBackendConfiguration.test.ts | 9 +++++++-- apps/web/src/connection/platform.ts | 1 - packages/shared/src/environmentLabel.ts | 5 +---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts b/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts index 7464d8fbe9e2f..3a07271aa785c 100644 --- a/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts +++ b/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts @@ -1,6 +1,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; import { currentDesktopBootstrapToken } from "@t3tools/shared/desktopBootstrapToken"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Clock from "effect/Clock"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -985,8 +986,12 @@ describe("DesktopBackendConfiguration", () => { DesktopBackendConfiguration.DesktopBackendConfiguration, ); // oxlint-disable-next-line t3code/no-manual-effect-runtime-in-tests -- Same reason: this is the synchronous resolution the IPC handler performs. - const label = Effect.runSync(configuration.resolvePrimaryLabel); - assert.equal(typeof label, "string"); + const label = Effect.runSync( + configuration.resolvePrimaryLabel.pipe( + Effect.provideService(HostProcess.Hostname, "DESKTOP-PC"), + ), + ); + assert.equal(label, "DESKTOP-PC"); } finally { await runtime.dispose(); } diff --git a/apps/web/src/connection/platform.ts b/apps/web/src/connection/platform.ts index c0381b81b043a..2983345312289 100644 --- a/apps/web/src/connection/platform.ts +++ b/apps/web/src/connection/platform.ts @@ -466,7 +466,6 @@ const loadSecondaryConnectionRegistration = Effect.fn( }).pipe(Effect.mapError(mapRemoteEnvironmentError)); // Desktop operations need the pool backend id to reach the owning instance; RPC uses the environment id. const connectionId = desktopLocalConnectionId(entry.id); - // The server's label is what every other client shows for this environment. const label = descriptor.label || entry.label; return { registration: new BearerConnectionRegistration({ diff --git a/packages/shared/src/environmentLabel.ts b/packages/shared/src/environmentLabel.ts index f3c2bc162403f..1c5a6759acde4 100644 --- a/packages/shared/src/environmentLabel.ts +++ b/packages/shared/src/environmentLabel.ts @@ -1,7 +1,4 @@ -// WSL mirrors the Windows computer name into its hostname, so a WSL server -// and its Windows host need the distro in the label to tell them apart. The -// server and the desktop app both label WSL environments with this, so the -// host and remote clients show the same name. +// WSL inherits the Windows hostname; distinguish the environments on every client. export function wslEnvironmentLabel(hostname: string, distro: string | null): string { return distro ? `${hostname} WSL (${distro})` : `${hostname} WSL`; }