| 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,9 +4,11 @@ import { |
| 4 | HostProcessEnvironment, | 4 | HostProcessEnvironment, |
| 5 | HostProcessPlatform, | 5 | HostProcessPlatform, |
| 6 | HostProcessIsExecutable, | 6 | HostProcessIsExecutable, |
| 7 | HostProcessUserId, | ||
| 7 | } from "@t3tools/shared/hostProcess"; | 8 | } from "@t3tools/shared/hostProcess"; |
| 8 | import * as Effect from "effect/Effect"; | 9 | import * as Effect from "effect/Effect"; |
| 9 | import * as Layer from "effect/Layer"; | 10 | import * as Layer from "effect/Layer"; |
| 11 | import * as Option from "effect/Option"; | ||
| 10 | import * as Path from "effect/Path"; | 12 | import * as Path from "effect/Path"; |
| 11 | import * as FileSystem from "effect/FileSystem"; | 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,6 +136,54 @@ it.effect("puts detected Android tools on the helper PATH without losing existin |
| 134 | }).pipe(Effect.provide(NodePath.layer)), | 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 | it.effect( | 187 | it.effect( |
| 138 | "constructs and inspects an unconfigured host without installing or starting helpers", | 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,7 +19,11 @@ import { |
| 19 | LOCAL_DEVICE_HOST_ID, | 19 | LOCAL_DEVICE_HOST_ID, |
| 20 | } from "@t3tools/contracts"; | 20 | } from "@t3tools/contracts"; |
| 21 | import { waitForHttpReady } from "@t3tools/shared/httpReadiness"; | 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 | import { | 27 | import { |
| 24 | resolveNodeExecutable, | 28 | resolveNodeExecutable, |
| 25 | type NodeRuntimeUnavailableError, | 29 | type NodeRuntimeUnavailableError, |
| ... | @@ -32,6 +36,7 @@ import * as Effect from "effect/Effect"; | ... | @@ -32,6 +36,7 @@ import * as Effect from "effect/Effect"; |
| 32 | import * as Exit from "effect/Exit"; | 36 | import * as Exit from "effect/Exit"; |
| 33 | import * as FileSystem from "effect/FileSystem"; | 37 | import * as FileSystem from "effect/FileSystem"; |
| 34 | import * as Layer from "effect/Layer"; | 38 | import * as Layer from "effect/Layer"; |
| 39 | import * as Option from "effect/Option"; | ||
| 35 | import * as Path from "effect/Path"; | 40 | import * as Path from "effect/Path"; |
| 36 | import * as Option from "effect/Option"; | 41 | import * as Option from "effect/Option"; |
| 37 | import * as Ref from "effect/Ref"; | 42 | import * as Ref from "effect/Ref"; |
| ... | @@ -199,6 +204,28 @@ const deviceHostEnvironment = ( | ... | @@ -199,6 +204,28 @@ const deviceHostEnvironment = ( |
| 199 | : environment; | 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 | export const make = Effect.fn("LocalDeviceHost.make")(function* () { | 229 | export const make = Effect.fn("LocalDeviceHost.make")(function* () { |
| 203 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | 230 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 204 | const config = yield* ServerConfig.ServerConfig; | 231 | const config = yield* ServerConfig.ServerConfig; |
| ... | @@ -267,12 +294,6 @@ export const make = Effect.fn("LocalDeviceHost.make")(function* () { | ... | @@ -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 | const stopHub = (hub: HubProcess | undefined) => | 297 | const stopHub = (hub: HubProcess | undefined) => |
| 277 | hub ? Scope.close(hub.scope, Exit.void).pipe(Effect.ignore) : Effect.void; | 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,7 +397,10 @@ export const make = Effect.fn("LocalDeviceHost.make")(function* () { |
| 376 | shell: false, | 397 | shell: false, |
| 377 | stdout: "pipe", | 398 | stdout: "pipe", |
| 378 | stderr: "pipe", | 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,4 +790,5 @@ export const __testing = { |
| 766 | androidSdk, | 790 | androidSdk, |
| 767 | platformReason, | 791 | platformReason, |
| 768 | deviceHostEnvironment, | 792 | deviceHostEnvironment, |
| 793 | hubEnvironment, | ||
| 769 | }; | 794 | }; |