diff --git a/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts b/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts index e71d838188706..eca88feb4a393 100644 --- a/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts +++ b/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts @@ -960,8 +960,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/desktop/src/backend/DesktopBackendConfiguration.ts b/apps/desktop/src/backend/DesktopBackendConfiguration.ts index abadf58f3f02f..fa2222c3626a2 100644 --- a/apps/desktop/src/backend/DesktopBackendConfiguration.ts +++ b/apps/desktop/src/backend/DesktopBackendConfiguration.ts @@ -2,6 +2,7 @@ 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"; @@ -831,10 +832,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 2ddc672891755..213409da27322 100644 --- a/apps/desktop/src/ipc/methods/window.test.ts +++ b/apps/desktop/src/ipc/methods/window.test.ts @@ -62,7 +62,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), @@ -87,7 +87,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", () => @@ -97,7 +101,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/", @@ -148,7 +152,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 2833abca43859..c471762706fd9 100644 --- a/apps/desktop/src/ipc/methods/window.ts +++ b/apps/desktop/src/ipc/methods/window.ts @@ -16,6 +16,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"; @@ -151,7 +152,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 dcd50342f1c91..b62956ac0da9d 100644 --- a/apps/web/src/connection/platform.ts +++ b/apps/web/src/connection/platform.ts @@ -468,7 +468,7 @@ 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; + 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 67426ca8d8a02..f4b5bef8c7204 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -79,6 +79,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..1c5a6759acde4 --- /dev/null +++ b/packages/shared/src/environmentLabel.ts @@ -0,0 +1,4 @@ +// 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`; +}