mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-16 23:52:40 -06:00
915c25d713
Fresh Dev installs now preflight disk space and stream honest stages, while Codex activation probes the refreshed request-scoped registry. Closes #120779 Closes #120780
159 lines
5.3 KiB
Swift
159 lines
5.3 KiB
Swift
import Darwin
|
|
import Foundation
|
|
import Testing
|
|
@testable import OpenClaw
|
|
|
|
private actor ShellLineRecorder {
|
|
private var lines: [String] = []
|
|
|
|
func append(_ line: String) {
|
|
self.lines.append(line)
|
|
}
|
|
|
|
func snapshot() -> [String] {
|
|
self.lines
|
|
}
|
|
}
|
|
|
|
struct ShellExecutorTimeoutTests {
|
|
@Test func `streaming captures both streams while delivering stdout lines`() async {
|
|
let recorder = ShellLineRecorder()
|
|
let result = await ShellExecutor.runStreamingDetailed(
|
|
command: [
|
|
"/bin/sh",
|
|
"-c",
|
|
"printf 'one\\ntwo\\n'; printf 'problem\\n' >&2",
|
|
],
|
|
cwd: nil,
|
|
env: nil,
|
|
timeout: 5)
|
|
{ line in
|
|
await recorder.append(line)
|
|
}
|
|
|
|
#expect(result.success)
|
|
#expect(await recorder.snapshot() == ["one", "two"])
|
|
#expect(result.stdout == "one\ntwo\n")
|
|
#expect(result.stderr == "problem\n")
|
|
}
|
|
|
|
@Test func `timeout kills and reaps a TERM-ignoring command`() async throws {
|
|
try await self.assertTimeoutKillsAndReaps(streaming: false)
|
|
}
|
|
|
|
@Test func `streaming timeout kills and reaps a TERM-ignoring command`() async throws {
|
|
try await self.assertTimeoutKillsAndReaps(streaming: true)
|
|
}
|
|
|
|
@Test func `timeout terminates TERM-ignoring descendants`() async throws {
|
|
try await self.assertTimeoutTerminatesDescendants(streaming: false)
|
|
}
|
|
|
|
@Test func `streaming timeout terminates TERM-ignoring descendants`() async throws {
|
|
try await self.assertTimeoutTerminatesDescendants(streaming: true)
|
|
}
|
|
|
|
private func assertTimeoutKillsAndReaps(streaming: Bool) async throws {
|
|
let pidFile = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("openclaw-shell-timeout-\(UUID().uuidString).pid")
|
|
defer { try? FileManager.default.removeItem(at: pidFile) }
|
|
|
|
let command = [
|
|
"/bin/sh",
|
|
"-c",
|
|
"echo $$ > \"$PID_FILE\"; trap '' TERM; exec /bin/sleep 30",
|
|
]
|
|
let environment = ["PID_FILE": pidFile.path]
|
|
let result = if streaming {
|
|
await ShellExecutor.runStreamingDetailed(
|
|
command: command,
|
|
cwd: nil,
|
|
env: environment,
|
|
timeout: 0.1,
|
|
onStandardOutputLine: { _ in })
|
|
} else {
|
|
await ShellExecutor.runDetailed(
|
|
command: command,
|
|
cwd: nil,
|
|
env: environment,
|
|
timeout: 0.1)
|
|
}
|
|
|
|
#expect(result.timedOut)
|
|
let pid = try self.readPID(from: pidFile)
|
|
defer {
|
|
if kill(pid, 0) == 0 {
|
|
kill(pid, SIGKILL)
|
|
}
|
|
}
|
|
#expect(await self.waitUntilGone(pid))
|
|
}
|
|
|
|
private func assertTimeoutTerminatesDescendants(streaming: Bool) async throws {
|
|
let directory = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("openclaw-shell-timeout-\(UUID().uuidString)", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: directory) }
|
|
let parentPIDFile = directory.appendingPathComponent("parent.pid")
|
|
let childPIDFile = directory.appendingPathComponent("child.pid")
|
|
|
|
let command = [
|
|
"/bin/sh",
|
|
"-c",
|
|
"""
|
|
/bin/sh -c 'trap "" TERM; echo $$ > "$CHILD_PID_FILE"; while :; do sleep 10; done' &
|
|
echo $$ > "$PARENT_PID_FILE"
|
|
while [ ! -s "$CHILD_PID_FILE" ]; do sleep 0.01; done
|
|
while :; do sleep 10; done
|
|
""",
|
|
]
|
|
let environment = [
|
|
"PARENT_PID_FILE": parentPIDFile.path,
|
|
"CHILD_PID_FILE": childPIDFile.path,
|
|
]
|
|
let result = if streaming {
|
|
await ShellExecutor.runStreamingDetailed(
|
|
command: command,
|
|
cwd: nil,
|
|
env: environment,
|
|
timeout: 0.2,
|
|
onStandardOutputLine: { _ in })
|
|
} else {
|
|
await ShellExecutor.runDetailed(
|
|
command: command,
|
|
cwd: nil,
|
|
env: environment,
|
|
timeout: 0.2)
|
|
}
|
|
|
|
#expect(result.timedOut)
|
|
let parentPID = try self.readPID(from: parentPIDFile)
|
|
let childPID = try self.readPID(from: childPIDFile)
|
|
defer {
|
|
for pid in [parentPID, childPID] where kill(pid, 0) == 0 {
|
|
kill(pid, SIGKILL)
|
|
}
|
|
}
|
|
#expect(await self.waitUntilGone(parentPID))
|
|
#expect(await self.waitUntilGone(childPID))
|
|
}
|
|
|
|
private func readPID(from file: URL) throws -> pid_t {
|
|
let value = try String(contentsOf: file, encoding: .utf8)
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
return try #require(pid_t(value))
|
|
}
|
|
|
|
private func waitUntilGone(_ pid: pid_t) async -> Bool {
|
|
let deadline = ContinuousClock.now + .seconds(1)
|
|
while ContinuousClock.now < deadline {
|
|
errno = 0
|
|
if kill(pid, 0) == -1, errno == ESRCH {
|
|
return true
|
|
}
|
|
try? await Task.sleep(for: .milliseconds(10))
|
|
}
|
|
return false
|
|
}
|
|
}
|