Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion apps/server/src/device/SshDeviceHost.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { expect, it } from "@effect/vitest";
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as ChildProcess from "effect/process/ChildProcess";
import * as HostProcess from "@t3tools/shared/HostProcess";
import * as Net from "@t3tools/shared/Net";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
Expand All @@ -26,6 +28,8 @@ it.effect("preserves installed status after probes and cleans failed agent activ
let forwards = 0;
let failForward = true;
let rejectConfig = true;
const nativeSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const platform = yield* HostProcess.Platform;
const releases: Array<string | undefined> = [];
const spawner = ChildProcessSpawner.make((command) =>
Effect.gen(function* () {
Expand Down Expand Up @@ -68,6 +72,17 @@ it.effect("preserves installed status after probes and cleans failed agent activ
),
);
const mode = /const mode = "([^"]+)"/.exec(script)?.[1] ?? "";
if (!mode && script.includes("readFileSync")) {
return yield* nativeSpawner.spawn(
ChildProcess.make(
platform === "win32" ? "powershell.exe" : "sh",
platform === "win32"
? ["-NoProfile", "-NonInteractive", "-Command", command.args.slice(-2).join(" ")]
: ["-c", command.args.slice(-2).join(" ")],
{ stdin, env: { ...process.env, HOME: home } },
),
);
}
modes.push(mode);
owners.push(/const owner = "([^"]+)"/.exec(script)?.[1] ?? "");
output = JSON.stringify({
Expand Down Expand Up @@ -128,7 +143,23 @@ it.effect("preserves installed status after probes and cleans failed agent activ
),
),
);
yield* host.ensureReady(() => Effect.void);
const initial = yield* host.ensureReady(() => Effect.void);
const owner = owners[0]!;
yield* fs.makeDirectory(home + "/.t3/device/hosts/" + owner, { recursive: true });
const value = "quotes ' \" $HOME \nno trailing newline";
const input = value + "\r\nCR\rNUL\0backslash \\";
const code =
"process.stdout.write(JSON.stringify({args:process.argv.slice(1),input:require('node:fs').readFileSync(0,'utf8'),cwd:process.cwd()}))";
const exact = yield* initial.run(process.execPath.replaceAll("\\", "/"), ["-e", code, value], {
stdin: input,
});
expect(exact.code).toBe(0);
const received = JSON.parse(exact.stdout);
expect(received.args).toEqual([value]);
expect(received.input).toBe(input);
expect(received.cwd.replaceAll("\\", "/")).toBe(
(home + "/.t3/device/hosts/" + owner).replaceAll("\\", "/"),
);
yield* SshDeviceHost.probe({
kind: "ssh",
id: "test",
Expand Down
21 changes: 8 additions & 13 deletions apps/server/src/device/SshDeviceHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
import * as ServerConfig from "../config.ts";
import { releaseAgentDeviceSession } from "./AgentDeviceTarget.ts";
import * as DeviceHost from "./DeviceHost.ts";
import * as SshDeviceScript from "./sshDeviceScript.ts";
import { makeDeviceHubOperations } from "./DeviceHubOperations.ts";
import { quoteRemoteArg, remoteDeviceEnvironment, remoteDeviceScript } from "./sshDeviceScript.ts";

const Probe = Schema.Struct({
nodePath: Schema.String,
Expand Down Expand Up @@ -53,22 +53,18 @@ const targetFor = (config: SshDeviceHostConfig) => ({
});
const identityArgs = (config: SshDeviceHostConfig) =>
config.identityFile ? ["-i", config.identityFile] : [];
const commandArgs = (script: string) => [
"sh",
"-c",
quoteRemoteArg(remoteDeviceEnvironment + script),
];
const bootstrap = (
config: SshDeviceHostConfig,
owner: string,
mode: "probe" | "start" | "agent-start" | "stop-agent" | "stop",
) =>
runSshCommand(targetFor(config), {
preHostArgs: identityArgs(config),
remoteCommandArgs: commandArgs(
...SshDeviceScript.remoteDeviceCommand(
'command -v node >/dev/null 2>&1 || { echo "Node is missing from the non-interactive SSH PATH" >&2; exit 1; }; exec node',
SshDeviceScript.remoteDeviceScript(owner, mode),
{ nodeBootstrap: true },
),
stdin: remoteDeviceScript(owner, mode),
timeoutMs: mode === "start" || mode === "agent-start" ? 1_300_000 : 45_000,
}).pipe(
Effect.mapError(
Expand Down Expand Up @@ -174,12 +170,11 @@ export const make = Effect.fn("SshDeviceHost.make")(function* (
provide(
runSshCommand(targetFor(config), {
preHostArgs: identityArgs(config),
// Match the hub cwd so relative executables and PATH entries resolve identically.
remoteCommandArgs: commandArgs(
`cd "$HOME/.t3/device/hosts/${owner}" && exec ${[command, ...args].map(quoteRemoteArg).join(" ")}`,
...SshDeviceScript.remoteDeviceCommand(
`cd "$HOME/.t3/device/hosts/${owner}" && exec ${[command, ...args].map(SshDeviceScript.quoteRemoteArg).join(" ")}`,
options?.stdin,
),
keepErrorOutputTail: true,
...(options?.stdin === undefined ? {} : { stdin: options.stdin }),
...(options?.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
}),
).pipe(
Expand Down Expand Up @@ -286,7 +281,7 @@ export const make = Effect.fn("SshDeviceHost.make")(function* (
// close, instead of being orphaned.
"sh",
"-c",
quoteRemoteArg("exec cat >/dev/null"),
SshDeviceScript.quoteRemoteArg("exec cat >/dev/null"),
],
{ stdin: "pipe", stdout: "ignore", stderr: "pipe" },
),
Expand Down
Loading
Loading