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
146 changes: 124 additions & 22 deletions apps/server/src/environment/ServerEnvironmentLabel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,28 @@ import { afterEach, describe, expect, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Logger from "effect/Logger";
import * as PlatformError from "effect/PlatformError";
import * as References from "effect/References";
import * as Schema from "effect/Schema";
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
import { HostProcessHostname, HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { vi } from "vite-plus/test";

import * as ProcessRunner from "../processRunner.ts";
import { resolveServerEnvironmentLabel } from "./ServerEnvironmentLabel.ts";
import * as ServerEnvironmentLabel from "./ServerEnvironmentLabel.ts";

const isServerEnvironmentLabelFileError = Schema.is(
ServerEnvironmentLabel.ServerEnvironmentLabelFileError,
);
const isServerEnvironmentLabelCommandError = Schema.is(
ServerEnvironmentLabel.ServerEnvironmentLabelCommandError,
);

interface CapturedLog {
readonly message: unknown;
readonly annotations: Readonly<Record<string, unknown>>;
}

const runMock = vi.fn<ProcessRunner.ProcessRunner["Service"]["run"]>();

Expand Down Expand Up @@ -47,7 +63,7 @@ afterEach(() => {
describe("resolveServerEnvironmentLabel", () => {
it.effect("uses hostname fallback regardless of launch mode", () =>
Effect.gen(function* () {
const result = yield* resolveServerEnvironmentLabel({
const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
cwdBaseName: "t3code",
}).pipe(Effect.provide(withHostPlatform(TestLayer, "win32", "macbook-pro")));

Expand All @@ -68,7 +84,7 @@ describe("resolveServerEnvironmentLabel", () => {
}),
);

const result = yield* resolveServerEnvironmentLabel({
const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
cwdBaseName: "t3code",
}).pipe(Effect.provide(withHostPlatform(TestLayer, "darwin", "macbook-pro")));

Expand All @@ -85,7 +101,7 @@ describe("resolveServerEnvironmentLabel", () => {

it.effect("prefers Linux PRETTY_HOSTNAME from machine-info", () =>
Effect.gen(function* () {
const result = yield* resolveServerEnvironmentLabel({
const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
cwdBaseName: "t3code",
}).pipe(Effect.provide(withHostPlatform(LinuxMachineInfoLayer, "linux", "buildbox")));

Expand All @@ -107,7 +123,7 @@ describe("resolveServerEnvironmentLabel", () => {
}),
);

const result = yield* resolveServerEnvironmentLabel({
const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
cwdBaseName: "t3code",
}).pipe(Effect.provide(withHostPlatform(TestLayer, "linux", "runner-01")));

Expand All @@ -124,33 +140,119 @@ describe("resolveServerEnvironmentLabel", () => {

it.effect("falls back to the hostname when friendly labels are unavailable", () =>
Effect.gen(function* () {
const result = yield* resolveServerEnvironmentLabel({
const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
cwdBaseName: "t3code",
}).pipe(Effect.provide(withHostPlatform(TestLayer, "win32", "JULIUS-LAPTOP")));

expect(result).toBe("JULIUS-LAPTOP");
}),
);

it.effect("falls back to the hostname when the friendly-label command is missing", () =>
Effect.gen(function* () {
runMock.mockReturnValueOnce(
Effect.fail(
new ProcessRunner.ProcessSpawnError({
command: "scutil",
argumentCount: 2,
cause: new Error("spawn scutil ENOENT"),
}),
),
);
it.effect("falls back to the hostname when the friendly-label command is missing", () => {
const logs: CapturedLog[] = [];
const logger = Logger.make(({ fiber, message }) => {
logs.push({
message,
annotations: fiber.getRef(References.CurrentLogAnnotations),
});
});
const spawnCause = new Error("spawn scutil ENOENT");
const processError = new ProcessRunner.ProcessSpawnError({
command: "scutil",
argumentCount: 2,
cause: spawnCause,
});
runMock.mockReturnValueOnce(Effect.fail(processError));

const result = yield* resolveServerEnvironmentLabel({
return Effect.gen(function* () {
const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
cwdBaseName: "t3code",
}).pipe(Effect.provide(withHostPlatform(TestLayer, "darwin", "macbook-pro")));
});

expect(result).toBe("macbook-pro");
}),
);
expect(logs[0]?.message).toEqual([
"Failed to run environment-label probe 'macos-computer-name' with scutil.",
]);
const error = logs[0]?.annotations.cause;
expect(isServerEnvironmentLabelCommandError(error)).toBe(true);
if (isServerEnvironmentLabelCommandError(error)) {
expect(error.probe).toBe("macos-computer-name");
expect(error.executable).toBe("scutil");
expect(error.argumentCount).toBe(2);
expect(error).not.toHaveProperty("args");
expect(error.message).not.toContain("--get");
expect(error.message).not.toContain("ComputerName");
expect(error.cause).toBe(processError);
expect(processError.cause).toBe(spawnCause);
}
}).pipe(
Effect.provide(
Layer.mergeAll(
withHostPlatform(TestLayer, "darwin", "macbook-pro"),
Logger.layer([logger], { mergeWithExisting: false }),
Layer.succeed(References.MinimumLogLevel, "Debug"),
),
),
);
});

it.effect("continues to hostnamectl after a machine-info inspect failure", () => {
const logs: CapturedLog[] = [];
const logger = Logger.make(({ fiber, message }) => {
logs.push({
message,
annotations: fiber.getRef(References.CurrentLogAnnotations),
});
});
const fileCause = new Error("permission denied");
const platformError = PlatformError.systemError({
_tag: "PermissionDenied",
module: "FileSystem",
method: "exists",
pathOrDescriptor: "/etc/machine-info",
cause: fileCause,
});
const fileSystemLayer = FileSystem.layerNoop({
exists: () => Effect.fail(platformError),
});
runMock.mockReturnValueOnce(
Effect.succeed({
stdout: "CI Runner\n",
stderr: "",
code: ChildProcessSpawner.ExitCode(0),
timedOut: false,
stdoutTruncated: false,
stderrTruncated: false,
}),
);

return Effect.gen(function* () {
const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
cwdBaseName: "t3code",
});

expect(result).toBe("CI Runner");
expect(logs[0]?.message).toEqual([
"Failed to inspect environment-label file at /etc/machine-info.",
]);
const error = logs[0]?.annotations.cause;
expect(isServerEnvironmentLabelFileError(error)).toBe(true);
if (isServerEnvironmentLabelFileError(error)) {
expect(error.operation).toBe("inspect");
expect(error.path).toBe("/etc/machine-info");
expect(error.cause).toBe(platformError);
expect(platformError.cause).toBe(fileCause);
}
}).pipe(
Effect.provide(
Layer.mergeAll(
withHostPlatform(Layer.merge(ProcessRunnerTest, fileSystemLayer), "linux", "buildbox"),
Logger.layer([logger], { mergeWithExisting: false }),
Layer.succeed(References.MinimumLogLevel, "Debug"),
),
),
);
});

it.effect("falls back to the cwd basename when the hostname is blank", () =>
Effect.gen(function* () {
Expand All @@ -165,7 +267,7 @@ describe("resolveServerEnvironmentLabel", () => {
}),
);

const result = yield* resolveServerEnvironmentLabel({
const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
cwdBaseName: "t3code",
}).pipe(Effect.provide(withHostPlatform(TestLayer, "linux", " ")));

Expand Down
130 changes: 111 additions & 19 deletions apps/server/src/environment/ServerEnvironmentLabel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,47 @@ import { HostProcessHostname, HostProcessPlatform } from "@t3tools/shared/hostPr
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

import * as ProcessRunner from "../processRunner.ts";

interface ResolveServerEnvironmentLabelInput {
readonly cwdBaseName: string;
}

const ServerEnvironmentLabelCommandProbe = Schema.Literals([
"macos-computer-name",
"linux-pretty-hostname",
]);
type ServerEnvironmentLabelCommandProbe = typeof ServerEnvironmentLabelCommandProbe.Type;

export class ServerEnvironmentLabelFileError extends Schema.TaggedErrorClass<ServerEnvironmentLabelFileError>()(
"ServerEnvironmentLabelFileError",
{
operation: Schema.Literals(["inspect", "read"]),
path: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to ${this.operation} environment-label file at ${this.path}.`;
}
}

export class ServerEnvironmentLabelCommandError extends Schema.TaggedErrorClass<ServerEnvironmentLabelCommandError>()(
"ServerEnvironmentLabelCommandError",
{
probe: ServerEnvironmentLabelCommandProbe,
executable: Schema.String,
argumentCount: Schema.Number,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to run environment-label probe '${this.probe}' with ${this.executable}.`;
}
}

function normalizeLabel(value: string | null | undefined): string | null {
const trimmed = value?.trim();
return trimmed && trimmed.length > 0 ? trimmed : null;
Expand All @@ -34,30 +68,80 @@ function parseMachineInfoValue(raw: string, key: string): string | null {

const readLinuxMachineInfo = Effect.fn("readLinuxMachineInfo")(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const exists = yield* fileSystem
.exists("/etc/machine-info")
.pipe(Effect.orElseSucceed(() => false));
if (!exists) {
return null;
}

return yield* fileSystem
.readFileString("/etc/machine-info")
.pipe(Effect.orElseSucceed(() => null));
const machineInfoPath = "/etc/machine-info";
return yield* fileSystem.exists(machineInfoPath).pipe(
Effect.mapError(
(cause) =>
new ServerEnvironmentLabelFileError({
operation: "inspect",
path: machineInfoPath,
cause,
}),
),
Effect.flatMap((exists) =>
exists
? fileSystem.readFileString(machineInfoPath).pipe(
Effect.mapError(
(cause) =>
new ServerEnvironmentLabelFileError({
operation: "read",
path: machineInfoPath,
cause,
}),
),
)
: Effect.succeed(null),
),
Effect.catchTags({
ServerEnvironmentLabelFileError: (error) =>
Effect.logDebug(error.message).pipe(
Effect.annotateLogs({
operation: error.operation,
path: error.path,
cause: error,
}),
Effect.as(null),
),
}),
);
});

const runFriendlyLabelCommand = Effect.fn("runFriendlyLabelCommand")(function* (
command: string,
args: readonly string[],
) {
const runFriendlyLabelCommand = Effect.fn("runFriendlyLabelCommand")(function* (input: {
readonly probe: ServerEnvironmentLabelCommandProbe;
readonly command: string;
readonly args: readonly string[];
}) {
const processRunner = yield* ProcessRunner.ProcessRunner;
const result = yield* processRunner
.run({
command,
args,
command: input.command,
args: input.args,
timeoutBehavior: "timedOutResult",
})
.pipe(Effect.option);
.pipe(
Effect.mapError(
(cause) =>
new ServerEnvironmentLabelCommandError({
probe: input.probe,
executable: input.command,
argumentCount: input.args.length,
cause,
}),
),
Effect.map(Option.some),
Effect.catchTags({
ServerEnvironmentLabelCommandError: (error) =>
Effect.logDebug(error.message).pipe(
Effect.annotateLogs({
probe: error.probe,
executable: error.executable,
argumentCount: error.argumentCount,
cause: error,
}),
Effect.as(Option.none()),
),
}),
);

if (Option.isNone(result) || result.value.code !== 0) {
return null;
Expand All @@ -69,7 +153,11 @@ const runFriendlyLabelCommand = Effect.fn("runFriendlyLabelCommand")(function* (
const resolveFriendlyHostLabel = Effect.fn("resolveFriendlyHostLabel")(function* () {
const platform = yield* HostProcessPlatform;
if (platform === "darwin") {
return yield* runFriendlyLabelCommand("scutil", ["--get", "ComputerName"]);
return yield* runFriendlyLabelCommand({
probe: "macos-computer-name",
command: "scutil",
args: ["--get", "ComputerName"],
});
}

if (platform === "linux") {
Expand All @@ -81,7 +169,11 @@ const resolveFriendlyHostLabel = Effect.fn("resolveFriendlyHostLabel")(function*
}
}

return yield* runFriendlyLabelCommand("hostnamectl", ["--pretty"]);
return yield* runFriendlyLabelCommand({
probe: "linux-pretty-hostname",
command: "hostnamectl",
args: ["--pretty"],
});
}

return null;
Expand Down
Loading