client-exec CLI

This commit is contained in:
Ian Gulliver
2026-10-21 10:48:59 -07:00
parent 48a05d92de
commit fab6ce5831
16 changed files with 760 additions and 364 deletions
+46 -10
View File
@@ -134,6 +134,11 @@ final class Remote {
}
log.info("client request \(req.id, privacy: .public): \(req.op.rawValue, privacy: .public)")
Task {
do {
try await task.send(.string(String(decoding: try JSONEncoder().encode(ClientReply(id: req.id, accepted: true)), as: UTF8.self)))
} catch {
log.error("acknowledge client request \(req.id, privacy: .public): \(errorText(error), privacy: .public)")
}
var reply: ClientReply
do {
reply = try await perform(req)
@@ -154,6 +159,10 @@ final class Remote {
let home = FileManager.default.homeDirectoryForCurrentUser.path
let (code, output) = try await spawn(["/bin/zsh", "-l", "-c", req.command ?? ""], in: home, timeoutMs: req.timeoutMs ?? 120_000)
return ClientReply(id: req.id, exitCode: code, output: output)
case .exec:
let home = FileManager.default.homeDirectoryForCurrentUser.path
let (code, stdout, stderr) = try await spawnSplit(["/bin/zsh", "-l", "-c", req.command ?? ""], in: home, timeoutMs: req.timeoutMs ?? 120_000)
return ClientReply(id: req.id, exitCode: code, stdout: Base64EncodedData(stdout), stderr: Base64EncodedData(stderr))
case .upload:
try await upload(transfer: req.transfer ?? "", to: req.dest ?? "")
return ClientReply(id: req.id)
@@ -299,24 +308,52 @@ private func start(_ p: Process) throws -> AsyncStream<Void> {
}
private func spawn(_ argv: [String], in dir: String, timeoutMs: Int, input: FileHandle = .nullDevice) async throws -> (Int, String) {
let fm = FileManager.default
let out = fm.temporaryDirectory.appending(path: "byre-\(UUID().uuidString).log")
let out = try sinkFile()
defer {
try? fm.removeItem(at: out)
try? FileManager.default.removeItem(at: out)
}
fm.createFile(atPath: out.path, contents: nil, attributes: [.posixPermissions: 0o600])
let sink = try FileHandle(forWritingTo: out)
defer {
try? sink.close()
}
let code = try await run(argv, in: dir, timeoutMs: timeoutMs, input: input, stdout: sink, stderr: sink)
return (code, tail(out))
}
private func spawnSplit(_ argv: [String], in dir: String, timeoutMs: Int) async throws -> (Int, Data, Data) {
let fm = FileManager.default
let out = try sinkFile()
let err = try sinkFile()
defer {
try? fm.removeItem(at: out)
try? fm.removeItem(at: err)
}
let outSink = try FileHandle(forWritingTo: out)
let errSink = try FileHandle(forWritingTo: err)
defer {
try? outSink.close()
try? errSink.close()
}
let code = try await run(argv, in: dir, timeoutMs: timeoutMs, input: .nullDevice, stdout: outSink, stderr: errSink)
return (code, try Data(contentsOf: out), try Data(contentsOf: err))
}
private func sinkFile() throws -> URL {
let url = FileManager.default.temporaryDirectory.appending(path: "byre-\(UUID().uuidString).log")
guard FileManager.default.createFile(atPath: url.path, contents: nil, attributes: [.posixPermissions: 0o600]) else {
throw ApiError(status: 0, message: "create \(url.path)")
}
return url
}
private func run(_ argv: [String], in dir: String, timeoutMs: Int, input: FileHandle, stdout: FileHandle, stderr: FileHandle) async throws -> Int {
let p = Process()
p.executableURL = URL(filePath: argv[0])
p.arguments = Array(argv.dropFirst())
p.currentDirectoryURL = URL(filePath: dir)
p.standardInput = input
p.standardOutput = sink
p.standardError = sink
p.standardOutput = stdout
p.standardError = stderr
let exit: AsyncStream<Void>
do {
exit = try start(p)
@@ -332,14 +369,13 @@ private func spawn(_ argv: [String], in dir: String, timeoutMs: Int, input: File
timer.cancel()
let timedOut = (try? await timer.value) != nil
let output = tail(out)
if timedOut {
return (-1, output)
return -1
}
if p.terminationReason == .exit {
return (Int(p.terminationStatus), output)
return Int(p.terminationStatus)
}
return (128 + Int(p.terminationStatus), output)
return 128 + Int(p.terminationStatus)
}
private func tail(_ file: URL) -> String {