chore: rename project to clawdbot
This commit is contained in:
99
apps/macos/Sources/Clawdbot/ShellExecutor.swift
Normal file
99
apps/macos/Sources/Clawdbot/ShellExecutor.swift
Normal file
@@ -0,0 +1,99 @@
|
||||
import ClawdbotIPC
|
||||
import Foundation
|
||||
|
||||
enum ShellExecutor {
|
||||
struct ShellResult {
|
||||
var stdout: String
|
||||
var stderr: String
|
||||
var exitCode: Int?
|
||||
var timedOut: Bool
|
||||
var success: Bool
|
||||
var errorMessage: String?
|
||||
}
|
||||
|
||||
static func runDetailed(
|
||||
command: [String],
|
||||
cwd: String?,
|
||||
env: [String: String]?,
|
||||
timeout: Double?) async -> ShellResult
|
||||
{
|
||||
guard !command.isEmpty else {
|
||||
return ShellResult(
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
exitCode: nil,
|
||||
timedOut: false,
|
||||
success: false,
|
||||
errorMessage: "empty command")
|
||||
}
|
||||
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
|
||||
process.arguments = command
|
||||
if let cwd { process.currentDirectoryURL = URL(fileURLWithPath: cwd) }
|
||||
if let env { process.environment = env }
|
||||
|
||||
let stdoutPipe = Pipe()
|
||||
let stderrPipe = Pipe()
|
||||
process.standardOutput = stdoutPipe
|
||||
process.standardError = stderrPipe
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
} catch {
|
||||
return ShellResult(
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
exitCode: nil,
|
||||
timedOut: false,
|
||||
success: false,
|
||||
errorMessage: "failed to start: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
let waitTask = Task { () -> ShellResult in
|
||||
process.waitUntilExit()
|
||||
let out = stdoutPipe.fileHandleForReading.readToEndSafely()
|
||||
let err = stderrPipe.fileHandleForReading.readToEndSafely()
|
||||
let status = Int(process.terminationStatus)
|
||||
return ShellResult(
|
||||
stdout: String(bytes: out, encoding: .utf8) ?? "",
|
||||
stderr: String(bytes: err, encoding: .utf8) ?? "",
|
||||
exitCode: status,
|
||||
timedOut: false,
|
||||
success: status == 0,
|
||||
errorMessage: status == 0 ? nil : "exit \(status)")
|
||||
}
|
||||
|
||||
if let timeout, timeout > 0 {
|
||||
let nanos = UInt64(timeout * 1_000_000_000)
|
||||
let result = await withTaskGroup(of: ShellResult.self) { group in
|
||||
group.addTask { await waitTask.value }
|
||||
group.addTask {
|
||||
try? await Task.sleep(nanoseconds: nanos)
|
||||
if process.isRunning { process.terminate() }
|
||||
_ = await waitTask.value // drain pipes after termination
|
||||
return ShellResult(
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
exitCode: nil,
|
||||
timedOut: true,
|
||||
success: false,
|
||||
errorMessage: "timeout")
|
||||
}
|
||||
let first = await group.next()!
|
||||
group.cancelAll()
|
||||
return first
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return await waitTask.value
|
||||
}
|
||||
|
||||
static func run(command: [String], cwd: String?, env: [String: String]?, timeout: Double?) async -> Response {
|
||||
let result = await self.runDetailed(command: command, cwd: cwd, env: env, timeout: timeout)
|
||||
let combined = result.stdout.isEmpty ? result.stderr : result.stdout
|
||||
let payload = combined.isEmpty ? nil : Data(combined.utf8)
|
||||
return Response(ok: result.success, message: result.errorMessage, payload: payload)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user