Files
openclaw/apps/macos/Sources/OpenClawDiscovery/BoundedCommand.swift
Vincent Koc cba59e4674 fix(macos): prevent remote discovery hangs during Tailscale checks (#115852)
* fix(macos): bound discovery subprocess execution

* refactor(macos): migrate tailscale discovery runner

* refactor(macos): make wide-area discovery asynchronous

* test(macos): prove discovery subprocess cleanup

* chore(macos): keep release notes in the PR
2026-07-29 21:38:57 +08:00

69 lines
2.4 KiB
Swift

import Foundation
import Subprocess
enum BoundedCommand {
private static let defaultOutputLimit = 8 * 1024 * 1024
static func run(
path: String,
arguments: [String],
environment: [String: String]? = nil,
timeout: TimeInterval,
outputLimit: Int = defaultOutputLimit) async -> String?
{
guard timeout > 0, outputLimit > 0 else { return nil }
let executable: Executable = path.contains("/")
? .path(.init(path))
: .name(path)
let subprocessEnvironment = environment.map(self.environment(from:)) ?? .inherit
do {
return try await withThrowingTaskGroup(
of: String?.self,
returning: String?.self)
{ group in
group.addTask {
let result = try await Subprocess.run(
executable,
arguments: Arguments(arguments),
environment: subprocessEnvironment,
output: .string(limit: outputLimit))
guard result.terminationStatus.isSuccess,
let output = result.standardOutput?
.trimmingCharacters(in: .whitespacesAndNewlines),
!output.isEmpty
else {
return nil
}
return output
}
group.addTask {
try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
return nil
}
// Keep timeout structured so swift-subprocess kills and reaps a canceled
// child before callers can start the next discovery probe.
defer { group.cancelAll() }
guard let result = try await group.next() else {
return nil
}
return result
}
} catch {
return nil
}
}
private static func environment(from values: [String: String]) -> Environment {
var converted: [Environment.Key: String] = [:]
converted.reserveCapacity(values.count)
for (key, value) in values {
guard let environmentKey = Environment.Key(rawValue: key) else { continue }
converted[environmentKey] = value
}
return .custom(converted)
}
}