| author | |
| committer | |
| log | f729e0ebfe7578860585a77ddde8f008e30c2351 |
| tree | 410cde85ddc5a8a866d244e502b734d4bb1b2bec |
| parent | f4153a75a4b04e14bd45d1b578af4d1d63cc84e5 |
| signature | Signed by PGP key B5690EEEBB952194 |
2 files changed, 83 insertions(+), 8 deletions(-)
apps/server/src/device/LocalDeviceHost.test.ts+50| ... | ... | @@ -4,9 +4,11 @@ import { |
| 4 | 4 | HostProcessEnvironment, |
| 5 | 5 | HostProcessPlatform, |
| 6 | 6 | HostProcessIsExecutable, |
| 7 | HostProcessUserId, | |
| 7 | 8 | } from "@t3tools/shared/hostProcess"; |
| 8 | 9 | import * as Effect from "effect/Effect"; |
| 9 | 10 | import * as Layer from "effect/Layer"; |
| 11 | import * as Option from "effect/Option"; | |
| 10 | 12 | import * as Path from "effect/Path"; |
| 11 | 13 | import * as FileSystem from "effect/FileSystem"; |
| 12 | 14 | |
| ... | ... | @@ -134,6 +136,54 @@ it.effect("puts detected Android tools on the helper PATH without losing existin |
| 134 | 136 | }).pipe(Effect.provide(NodePath.layer)), |
| 135 | 137 | ); |
| 136 | 138 | |
| 139 | it.effect( | |
| 140 | "recovers an owned Linux runtime directory without overriding explicit environments", | |
| 141 | () => | |
| 142 | Effect.gen(function* () { | |
| 143 | const fs = yield* FileSystem.FileSystem; | |
| 144 | const directory = yield* fs.makeTempDirectoryScoped(); | |
| 145 | const stat = yield* fs.stat(directory); | |
| 146 | for (const [platform, uid, runtimeDir, type, owner, expected] of [ | |
| 147 | ["linux", 1000, undefined, "Directory", 1000, "/run/user/1000"], | |
| 148 | ["linux", 1000, "/custom/runtime", "Directory", 1000, "/custom/runtime"], | |
| 149 | ["linux", 1000, "", "Directory", 1000, ""], | |
| 150 | ["linux", 1000, undefined, "Directory", 1001, undefined], | |
| 151 | ["linux", 1000, undefined, "File", 1000, undefined], | |
| 152 | ["linux", undefined, undefined, "Directory", 1000, undefined], | |
| 153 | ["darwin", 1000, undefined, "Directory", 1000, undefined], | |
| 154 | ["win32", 1000, undefined, "Directory", 1000, undefined], | |
| 155 | ] as const) { | |
| 156 | const environment = { PATH: "/usr/bin", XDG_RUNTIME_DIR: runtimeDir }; | |
| 157 | const result = yield* LocalDeviceHost.__testing.hubEnvironment(environment).pipe( | |
| 158 | Effect.provideService(HostProcessPlatform, platform), | |
| 159 | Effect.provideService(HostProcessUserId, uid), | |
| 160 | Effect.provideService( | |
| 161 | FileSystem.FileSystem, | |
| 162 | FileSystem.makeNoop({ | |
| 163 | stat: (path) => { | |
| 164 | expect(path).toBe("/run/user/1000"); | |
| 165 | return Effect.succeed({ ...stat, type, uid: Option.some(owner) }); | |
| 166 | }, | |
| 167 | }), | |
| 168 | ), | |
| 169 | ); | |
| 170 | expect(result.XDG_RUNTIME_DIR).toBe(expected); | |
| 171 | expect(result.PATH).toBe("/usr/bin"); | |
| 172 | expect(result.FORCE_COLOR).toBe("0"); | |
| 173 | expect(result.NO_COLOR).toBe("1"); | |
| 174 | expect(environment.XDG_RUNTIME_DIR).toBe(runtimeDir); | |
| 175 | } | |
| 176 | const missing = yield* LocalDeviceHost.__testing | |
| 177 | .hubEnvironment({}) | |
| 178 | .pipe( | |
| 179 | Effect.provideService(HostProcessPlatform, "linux"), | |
| 180 | Effect.provideService(HostProcessUserId, 1000), | |
| 181 | Effect.provideService(FileSystem.FileSystem, FileSystem.makeNoop({})), | |
| 182 | ); | |
| 183 | expect(missing.XDG_RUNTIME_DIR).toBeUndefined(); | |
| 184 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), | |
| 185 | ); | |
| 186 | ||
| 137 | 187 | it.effect( |
| 138 | 188 | "constructs and inspects an unconfigured host without installing or starting helpers", |
| 139 | 189 | () => |
apps/server/src/device/LocalDeviceHost.ts+33-8| ... | ... | @@ -19,7 +19,11 @@ import { |
| 19 | 19 | LOCAL_DEVICE_HOST_ID, |
| 20 | 20 | } from "@t3tools/contracts"; |
| 21 | 21 | import { waitForHttpReady } from "@t3tools/shared/httpReadiness"; |
| 22 | import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; | |
| 22 | import { | |
| 23 | HostProcessEnvironment, | |
| 24 | HostProcessPlatform, | |
| 25 | HostProcessUserId, | |
| 26 | } from "@t3tools/shared/hostProcess"; | |
| 23 | 27 | import { |
| 24 | 28 | resolveNodeExecutable, |
| 25 | 29 | type NodeRuntimeUnavailableError, |
| ... | ... | @@ -32,6 +36,7 @@ import * as Effect from "effect/Effect"; |
| 32 | 36 | import * as Exit from "effect/Exit"; |
| 33 | 37 | import * as FileSystem from "effect/FileSystem"; |
| 34 | 38 | import * as Layer from "effect/Layer"; |
| 39 | import * as Option from "effect/Option"; | |
| 35 | 40 | import * as Path from "effect/Path"; |
| 36 | 41 | import * as Option from "effect/Option"; |
| 37 | 42 | import * as Ref from "effect/Ref"; |
| ... | ... | @@ -199,6 +204,28 @@ const deviceHostEnvironment = ( |
| 199 | 204 | : environment; |
| 200 | 205 | }; |
| 201 | 206 | |
| 207 | const hubEnvironment = Effect.fn("LocalDeviceHost.hubEnvironment")(function* ( | |
| 208 | environment: NodeJS.ProcessEnv, | |
| 209 | ) { | |
| 210 | const env: NodeJS.ProcessEnv = { ...environment, FORCE_COLOR: "0", NO_COLOR: "1" }; | |
| 211 | const platform = yield* HostProcessPlatform; | |
| 212 | const uid = yield* HostProcessUserId; | |
| 213 | if (platform === "linux" && env.XDG_RUNTIME_DIR === undefined && uid !== undefined) { | |
| 214 | // SSH sessions may omit the directory where the emulator publishes its gRPC token. | |
| 215 | const runtimeDir = `/run/user/${uid}`; | |
| 216 | const fs = yield* FileSystem.FileSystem; | |
| 217 | const stat = yield* fs.stat(runtimeDir).pipe(Effect.option); | |
| 218 | if ( | |
| 219 | stat._tag === "Some" && | |
| 220 | stat.value.type === "Directory" && | |
| 221 | Option.contains(stat.value.uid, uid) | |
| 222 | ) { | |
| 223 | env.XDG_RUNTIME_DIR = runtimeDir; | |
| 224 | } | |
| 225 | } | |
| 226 | return env; | |
| 227 | }); | |
| 228 | ||
| 202 | 229 | export const make = Effect.fn("LocalDeviceHost.make")(function* () { |
| 203 | 230 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 204 | 231 | const config = yield* ServerConfig.ServerConfig; |
| ... | ... | @@ -267,12 +294,6 @@ export const make = Effect.fn("LocalDeviceHost.make")(function* () { |
| 267 | 294 | }; |
| 268 | 295 | }); |
| 269 | 296 | |
| 270 | const hubEnvironment = (): NodeJS.ProcessEnv => ({ | |
| 271 | ...hostEnvironment, | |
| 272 | FORCE_COLOR: "0", | |
| 273 | NO_COLOR: "1", | |
| 274 | }); | |
| 275 | ||
| 276 | 297 | const stopHub = (hub: HubProcess | undefined) => |
| 277 | 298 | hub ? Scope.close(hub.scope, Exit.void).pipe(Effect.ignore) : Effect.void; |
| 278 | 299 | |
| ... | ... | @@ -376,7 +397,10 @@ export const make = Effect.fn("LocalDeviceHost.make")(function* () { |
| 376 | 397 | shell: false, |
| 377 | 398 | stdout: "pipe", |
| 378 | 399 | stderr: "pipe", |
| 379 | env: hubEnvironment(), | |
| 400 | env: yield* hubEnvironment(hostEnvironment).pipe( | |
| 401 | Effect.provideService(FileSystem.FileSystem, fs), | |
| 402 | Effect.provideService(HostProcessPlatform, hostPlatform), | |
| 403 | ), | |
| 380 | 404 | }, |
| 381 | 405 | ), |
| 382 | 406 | ) |
| ... | ... | @@ -766,4 +790,5 @@ export const __testing = { |
| 766 | 790 | androidSdk, |
| 767 | 791 | platformReason, |
| 768 | 792 | deviceHostEnvironment, |
| 793 | hubEnvironment, | |
| 769 | 794 | }; |