Open History to see the last commit
1import * as HostProcess from "@t3tools/shared/HostProcess";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Option from "effect/Option";
5import * as Schema from "effect/Schema";
6
7import * as ProcessRunner from "../processRunner.ts";
8
9interface ResolveServerEnvironmentLabelInput {
10 readonly cwdBaseName: string;
11}
12
13const ServerEnvironmentLabelCommandProbe = Schema.Literals([
14 "macos-computer-name",
15 "linux-pretty-hostname",
16]);
17type ServerEnvironmentLabelCommandProbe = typeof ServerEnvironmentLabelCommandProbe.Type;
18
19export class ServerEnvironmentLabelFileError extends Schema.TaggedError<ServerEnvironmentLabelFileError>()(
20 "ServerEnvironmentLabelFileError",
21 {
22 operation: Schema.Literals(["inspect", "read"]),
23 path: Schema.String,
24 cause: Schema.Defect(),
25 },
26) {
27 override get message(): string {
28 return `Failed to ${this.operation} environment-label file at ${this.path}.`;
29 }
30}
31
32export class ServerEnvironmentLabelCommandError extends Schema.TaggedError<ServerEnvironmentLabelCommandError>()(
33 "ServerEnvironmentLabelCommandError",
34 {
35 probe: ServerEnvironmentLabelCommandProbe,
36 executable: Schema.String,
37 argumentCount: Schema.Number,
38 cause: Schema.Defect(),
39 },
40) {
41 override get message(): string {
42 return `Failed to run environment-label probe '${this.probe}' with ${this.executable}.`;
43 }
44}
45
46function normalizeLabel(value: string | null | undefined): string | null {
47 const trimmed = value?.trim();
48 return trimmed && trimmed.length > 0 ? trimmed : null;
49}
50
51function parseMachineInfoValue(raw: string, key: string): string | null {
52 for (const line of raw.split(/\r?\n/g)) {
53 const trimmed = line.trim();
54 if (trimmed.length === 0 || trimmed.startsWith("#") || !trimmed.startsWith(`${key}=`)) {
55 continue;
56 }
57 const value = trimmed.slice(key.length + 1).trim();
58 if (
59 (value.startsWith('"') && value.endsWith('"')) ||
60 (value.startsWith("'") && value.endsWith("'"))
61 ) {
62 return normalizeLabel(value.slice(1, -1));
63 }
64 return normalizeLabel(value);
65 }
66 return null;
67}
68
69const readLinuxMachineInfo = Effect.fn("readLinuxMachineInfo")(function* () {
70 const fileSystem = yield* FileSystem.FileSystem;
71 const machineInfoPath = "/etc/machine-info";
72 return yield* fileSystem.exists(machineInfoPath).pipe(
73 Effect.mapError(
74 (cause) =>
75 new ServerEnvironmentLabelFileError({
76 operation: "inspect",
77 path: machineInfoPath,
78 cause,
79 }),
80 ),
81 Effect.flatMap((exists) =>
82 exists
83 ? fileSystem.readFileString(machineInfoPath).pipe(
84 Effect.mapError(
85 (cause) =>
86 new ServerEnvironmentLabelFileError({
87 operation: "read",
88 path: machineInfoPath,
89 cause,
90 }),
91 ),
92 )
93 : Effect.succeed(null),
94 ),
95 Effect.catchTags({
96 ServerEnvironmentLabelFileError: (error) =>
97 Effect.logDebug(error.message).pipe(
98 Effect.annotateLogs({
99 operation: error.operation,
100 path: error.path,
101 cause: error,
102 }),
103 Effect.as(null),
104 ),
105 }),
106 );
107});
108
109const runFriendlyLabelCommand = Effect.fn("runFriendlyLabelCommand")(function* (input: {
110 readonly probe: ServerEnvironmentLabelCommandProbe;
111 readonly command: string;
112 readonly args: readonly string[];
113}) {
114 const processRunner = yield* ProcessRunner.ProcessRunner;
115 const result = yield* processRunner
116 .run({
117 command: input.command,
118 args: input.args,
119 // Server startup waits on this; hostnamectl can block on D-Bus when hostnamed is down.
120 timeout: "5 seconds",
121 timeoutBehavior: "timedOutResult",
122 })
123 .pipe(
124 Effect.mapError(
125 (cause) =>
126 new ServerEnvironmentLabelCommandError({
127 probe: input.probe,
128 executable: input.command,
129 argumentCount: input.args.length,
130 cause,
131 }),
132 ),
133 Effect.asSome,
134 Effect.catchTags({
135 ServerEnvironmentLabelCommandError: (error) =>
136 Effect.logDebug(error.message).pipe(
137 Effect.annotateLogs({
138 probe: error.probe,
139 executable: error.executable,
140 argumentCount: error.argumentCount,
141 cause: error,
142 }),
143 Effect.as(Option.none()),
144 ),
145 }),
146 );
147
148 if (Option.isNone(result) || result.value.code !== 0) {
149 return null;
150 }
151
152 return normalizeLabel(result.value.stdout);
153});
154
155const resolveFriendlyHostLabel = Effect.fn("resolveFriendlyHostLabel")(function* () {
156 const platform = yield* HostProcess.Platform;
157 if (platform === "darwin") {
158 return yield* runFriendlyLabelCommand({
159 probe: "macos-computer-name",
160 command: "scutil",
161 args: ["--get", "ComputerName"],
162 });
163 }
164
165 if (platform === "linux") {
166 const machineInfo = normalizeLabel(yield* readLinuxMachineInfo());
167 if (machineInfo) {
168 const prettyHostname = parseMachineInfoValue(machineInfo, "PRETTY_HOSTNAME");
169 if (prettyHostname) {
170 return prettyHostname;
171 }
172 }
173
174 return yield* runFriendlyLabelCommand({
175 probe: "linux-pretty-hostname",
176 command: "hostnamectl",
177 args: ["--pretty"],
178 });
179 }
180
181 return null;
182});
183
184export const resolveServerEnvironmentLabel = Effect.fn("resolveServerEnvironmentLabel")(function* (
185 input: ResolveServerEnvironmentLabelInput,
186) {
187 const friendlyHostLabel = yield* resolveFriendlyHostLabel();
188 if (friendlyHostLabel) {
189 return friendlyHostLabel;
190 }
191
192 const hostname = normalizeLabel(yield* HostProcess.Hostname);
193 if (hostname) {
194 return hostname;
195 }
196
197 return normalizeLabel(input.cwdBaseName) ?? "T3 environment";
198});