1import type { EnvironmentMachineKind } from "@t3tools/contracts";
2import * as HostProcess from "@t3tools/shared/HostProcess";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5
6import * as ProcessRunner from "../processRunner.ts";
7
8/**
9 * Best-effort hardware detection for the environment icon. Every probe is
10 * allowed to fail: a null result means "no signal", and the client draws a
11 * generic server until the user picks something in Settings → Connections.
12 */
13
14const DMI_ROOT = "/sys/class/dmi/id";
15const KERNEL_RELEASE_PATH = "/proc/sys/kernel/osrelease";
16
17// SMBIOS 3.x System Enclosure types (table 17). Codes that describe a shape
18// rather than a machine (docking stations, blades enclosures, IoT gateways)
19// fall through to null on purpose.
20const DMI_CHASSIS_KINDS: Readonly<Record<string, EnvironmentMachineKind>> = {
21 "3": "desktop", // Desktop
22 "4": "desktop", // Low Profile Desktop
23 "5": "desktop", // Pizza Box
24 "6": "desktop", // Mini Tower
25 "7": "desktop", // Tower
26 "8": "laptop", // Portable
27 "9": "laptop", // Laptop
28 "10": "laptop", // Notebook
29 "13": "desktop", // All in One
30 "14": "laptop", // Sub Notebook
31 "15": "desktop", // Space-saving
32 "16": "desktop", // Lunch Box
33 "17": "server", // Main Server Chassis
34 "18": "server", // Expansion Chassis
35 "19": "server", // SubChassis
36 "20": "server", // Bus Expansion Chassis
37 "21": "server", // Peripheral Chassis
38 "22": "server", // RAID Chassis
39 "23": "server", // Rack Mount Chassis
40 "24": "server", // Sealed-case PC
41 "28": "server", // Blade
42 "31": "laptop", // Convertible
43 "32": "laptop", // Detachable
44 "35": "desktop", // Mini PC
45};
46
47// Hypervisors and cloud providers write themselves into the DMI vendor or
48// product strings; any hit means the box is a VM, and a VM reads as "cloud"
49// regardless of the chassis type the hypervisor fakes. Hyper-V is matched on
50// its "Virtual Machine" product, not the "Microsoft Corporation" vendor that
51// physical Surface devices share.
52const VIRTUALIZATION_MARKERS = [
53 "qemu",
54 "kvm",
55 "bochs",
56 "vmware",
57 "virtualbox",
58 "innotek",
59 "xen",
60 "parallels",
61 "amazon ec2",
62 "google compute engine",
63 "digitalocean",
64 "hetzner",
65 "linode",
66 "vultr",
67 "scaleway",
68 "openstack",
69 "cloud",
70 "virtual machine",
71];
72
73function normalize(value: string | null | undefined): string | null {
74 const trimmed = value?.trim();
75 return trimmed && trimmed.length > 0 ? trimmed : null;
76}
77
78/** Marketing names and Intel-era model identifiers share these prefixes. */
79export function machineKindFromAppleProductName(name: string): EnvironmentMachineKind | null {
80 const normalized = name.trim().toLowerCase().replaceAll(/\s+/g, "");
81 if (normalized.startsWith("macmini")) return "mac-mini";
82 if (normalized.startsWith("macstudio")) return "mac-studio";
83 if (normalized.startsWith("macbook")) return "laptop";
84 if (normalized.startsWith("imac") || normalized.startsWith("macpro")) return "desktop";
85 return null;
86}
87
88export function machineKindFromDmi(input: {
89 readonly chassisType: string | null;
90 readonly sysVendor: string | null;
91 readonly productName: string | null;
92}): EnvironmentMachineKind | null {
93 const productName = input.productName ?? "";
94 const vendorAndProduct = `${input.sysVendor ?? ""} ${productName}`.toLowerCase();
95 if (VIRTUALIZATION_MARKERS.some((marker) => vendorAndProduct.includes(marker))) {
96 return "cloud";
97 }
98 // Apple hardware booting Linux (Asahi) still reports the Apple product name.
99 const appleKind = machineKindFromAppleProductName(productName);
100 if (appleKind !== null) {
101 return appleKind;
102 }
103 return input.chassisType === null ? null : (DMI_CHASSIS_KINDS[input.chassisType] ?? null);
104}
105
106const readOptionalFile = Effect.fn("readOptionalFile")(function* (path: string) {
107 const fileSystem = yield* FileSystem.FileSystem;
108 return yield* fileSystem.readFileString(path).pipe(
109 Effect.map(normalize),
110 Effect.orElseSucceed(() => null),
111 );
112});
113
114const runProbe = Effect.fn("runMachineProbe")(function* (input: {
115 readonly command: string;
116 readonly args: ReadonlyArray<string>;
117}) {
118 const processRunner = yield* ProcessRunner.ProcessRunner;
119 return yield* processRunner
120 .run({
121 command: input.command,
122 args: input.args,
123 timeout: "5 seconds",
124 timeoutBehavior: "timedOutResult",
125 })
126 .pipe(
127 Effect.map((result) => (result.code === 0 ? normalize(result.stdout) : null)),
128 Effect.orElseSucceed(() => null),
129 );
130});
131
132// IOKit's `product` node carries the marketing name ("Mac mini (2024)") on
133// Apple silicon; Intel Macs lack it, so `hw.model` ("Macmini8,1") is the
134// fallback. Both are single-digit-millisecond calls.
135const detectDarwinMachineKind = Effect.fn("detectDarwinMachineKind")(function* () {
136 const ioreg = yield* runProbe({ command: "ioreg", args: ["-rd1", "-n", "product"] });
137 const productName = ioreg?.match(/"product-name"\s*=\s*<"([^"]+)">/)?.[1] ?? null;
138 const fromProductName =
139 productName === null ? null : machineKindFromAppleProductName(productName);
140 if (fromProductName !== null) {
141 return fromProductName;
142 }
143 const model = yield* runProbe({ command: "sysctl", args: ["-n", "hw.model"] });
144 return model === null ? null : machineKindFromAppleProductName(model);
145});
146
147const detectLinuxMachineKind = Effect.fn("detectLinuxMachineKind")(function* () {
148 const [kernelRelease, chassisType, sysVendor, productName] = yield* Effect.all([
149 readOptionalFile(KERNEL_RELEASE_PATH),
150 readOptionalFile(`${DMI_ROOT}/chassis_type`),
151 readOptionalFile(`${DMI_ROOT}/sys_vendor`),
152 readOptionalFile(`${DMI_ROOT}/product_name`),
153 ]);
154 // WSL exposes Microsoft in its kernel release on both WSL 1 and WSL 2.
155 // Check it before DMI because WSL 2 presents as a Hyper-V VM.
156 if (kernelRelease?.toLowerCase().includes("microsoft")) {
157 return "linux";
158 }
159 return machineKindFromDmi({ chassisType, sysVendor, productName });
160});
161
162export const detectServerEnvironmentMachineKind = Effect.fn("detectServerEnvironmentMachineKind")(
163 function* () {
164 const platform = yield* HostProcess.Platform;
165 switch (platform) {
166 case "darwin":
167 return yield* detectDarwinMachineKind();
168 case "linux":
169 return yield* detectLinuxMachineKind();
170 default:
171 return null;
172 }
173 },
174);