| 1 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as FileSystem from "effect/FileSystem"; |
| 4 | import * as Option from "effect/Option"; |
| 5 | import * as Schema from "effect/Schema"; |
| 6 | |
| 7 | import * as ProcessRunner from "../processRunner.ts"; |
| 8 | |
| 9 | interface ResolveServerEnvironmentLabelInput { |
| 10 | readonly cwdBaseName: string; |
| 11 | } |
| 12 | |
| 13 | const ServerEnvironmentLabelCommandProbe = Schema.Literals([ |
| 14 | "macos-computer-name", |
| 15 | "linux-pretty-hostname", |
| 16 | ]); |
| 17 | type ServerEnvironmentLabelCommandProbe = typeof ServerEnvironmentLabelCommandProbe.Type; |
| 18 | |
| 19 | export 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 | |
| 32 | export 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 | |
| 46 | function normalizeLabel(value: string | null | undefined): string | null { |
| 47 | const trimmed = value?.trim(); |
| 48 | return trimmed && trimmed.length > 0 ? trimmed : null; |
| 49 | } |
| 50 | |
| 51 | function 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 | |
| 69 | const 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 | |
| 109 | const 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 | timeoutBehavior: "timedOutResult", |
| 120 | }) |
| 121 | .pipe( |
| 122 | Effect.mapError( |
| 123 | (cause) => |
| 124 | new ServerEnvironmentLabelCommandError({ |
| 125 | probe: input.probe, |
| 126 | executable: input.command, |
| 127 | argumentCount: input.args.length, |
| 128 | cause, |
| 129 | }), |
| 130 | ), |
| 131 | Effect.asSome, |
| 132 | Effect.catchTags({ |
| 133 | ServerEnvironmentLabelCommandError: (error) => |
| 134 | Effect.logDebug(error.message).pipe( |
| 135 | Effect.annotateLogs({ |
| 136 | probe: error.probe, |
| 137 | executable: error.executable, |
| 138 | argumentCount: error.argumentCount, |
| 139 | cause: error, |
| 140 | }), |
| 141 | Effect.as(Option.none()), |
| 142 | ), |
| 143 | }), |
| 144 | ); |
| 145 | |
| 146 | if (Option.isNone(result) || result.value.code !== 0) { |
| 147 | return null; |
| 148 | } |
| 149 | |
| 150 | return normalizeLabel(result.value.stdout); |
| 151 | }); |
| 152 | |
| 153 | const resolveFriendlyHostLabel = Effect.fn("resolveFriendlyHostLabel")(function* () { |
| 154 | const platform = yield* HostProcess.Platform; |
| 155 | if (platform === "darwin") { |
| 156 | return yield* runFriendlyLabelCommand({ |
| 157 | probe: "macos-computer-name", |
| 158 | command: "scutil", |
| 159 | args: ["--get", "ComputerName"], |
| 160 | }); |
| 161 | } |
| 162 | |
| 163 | if (platform === "linux") { |
| 164 | const machineInfo = normalizeLabel(yield* readLinuxMachineInfo()); |
| 165 | if (machineInfo) { |
| 166 | const prettyHostname = parseMachineInfoValue(machineInfo, "PRETTY_HOSTNAME"); |
| 167 | if (prettyHostname) { |
| 168 | return prettyHostname; |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | return yield* runFriendlyLabelCommand({ |
| 173 | probe: "linux-pretty-hostname", |
| 174 | command: "hostnamectl", |
| 175 | args: ["--pretty"], |
| 176 | }); |
| 177 | } |
| 178 | |
| 179 | return null; |
| 180 | }); |
| 181 | |
| 182 | export const resolveServerEnvironmentLabel = Effect.fn("resolveServerEnvironmentLabel")(function* ( |
| 183 | input: ResolveServerEnvironmentLabelInput, |
| 184 | ) { |
| 185 | const friendlyHostLabel = yield* resolveFriendlyHostLabel(); |
| 186 | if (friendlyHostLabel) { |
| 187 | return friendlyHostLabel; |
| 188 | } |
| 189 | |
| 190 | const hostname = normalizeLabel(yield* HostProcess.Hostname); |
| 191 | if (hostname) { |
| 192 | return hostname; |
| 193 | } |
| 194 | |
| 195 | return normalizeLabel(input.cwdBaseName) ?? "T3 environment"; |
| 196 | }); |