mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
8e398d9591
* fix(macos): reap app-owned child process groups macOS-owned SSH, Codex, and node-host descendants no longer survive terminal shutdown. Codex retains EOF-first graceful exit before bounded process-group termination and reaping. * fix(macos): correct managed cleanup wake binding * fix(macos): preserve Codex shutdown escalation Keep the app-owned EOF grace window while allowing abortive requests to interrupt it before process-group TERM and KILL.
133 lines
4.5 KiB
Swift
133 lines
4.5 KiB
Swift
import Testing
|
|
@testable import OpenClaw
|
|
|
|
#if canImport(Darwin)
|
|
import Darwin
|
|
import Foundation
|
|
|
|
struct RemotePortTunnelTests {
|
|
@Test func `tunnel owns its SSH process instead of multiplexing`() {
|
|
let options = RemotePortTunnel._testSSHOptions(localPort: 28789, remotePort: 18789)
|
|
|
|
#expect(options.contains("ControlMaster=no"))
|
|
#expect(options.contains("ControlPath=none"))
|
|
#expect(options.contains("ControlPersist=no"))
|
|
#expect(options.contains("ForkAfterAuthentication=no"))
|
|
#expect(options.contains("28789:127.0.0.1:18789"))
|
|
#expect(options.contains("StrictHostKeyChecking=yes"))
|
|
#expect(options.contains("UpdateHostKeys=yes"))
|
|
}
|
|
|
|
@Test func `tunnel requires explicit opt in to use SSH config host key policy`() {
|
|
let options = RemotePortTunnel._testSSHOptions(
|
|
localPort: 28789,
|
|
remotePort: 18789,
|
|
hostKeyPolicy: .openssh)
|
|
|
|
#expect(!options.contains { $0.hasPrefix("StrictHostKeyChecking=") })
|
|
#expect(!options.contains { $0.hasPrefix("UpdateHostKeys=") })
|
|
}
|
|
|
|
@Test func `drain stderr does not crash when handle closed`() {
|
|
let pipe = Pipe()
|
|
let handle = pipe.fileHandleForReading
|
|
try? handle.close()
|
|
|
|
let drained = RemotePortTunnel._testDrainStderr(handle)
|
|
#expect(drained.isEmpty)
|
|
}
|
|
|
|
@Test func `port is free detects I pv4 listener`() {
|
|
var fd = socket(AF_INET, SOCK_STREAM, 0)
|
|
#expect(fd >= 0)
|
|
guard fd >= 0 else { return }
|
|
defer {
|
|
if fd >= 0 { _ = Darwin.close(fd) }
|
|
}
|
|
|
|
var one: Int32 = 1
|
|
_ = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, socklen_t(MemoryLayout.size(ofValue: one)))
|
|
|
|
var addr = sockaddr_in()
|
|
addr.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
|
|
addr.sin_family = sa_family_t(AF_INET)
|
|
addr.sin_port = 0
|
|
addr.sin_addr = in_addr(s_addr: inet_addr("127.0.0.1"))
|
|
|
|
let bound = withUnsafePointer(to: &addr) { ptr in
|
|
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
|
|
Darwin.bind(fd, sa, socklen_t(MemoryLayout<sockaddr_in>.size))
|
|
}
|
|
}
|
|
#expect(bound == 0)
|
|
guard bound == 0 else { return }
|
|
#expect(Darwin.listen(fd, 1) == 0)
|
|
|
|
var name = sockaddr_in()
|
|
var nameLen = socklen_t(MemoryLayout<sockaddr_in>.size)
|
|
let got = withUnsafeMutablePointer(to: &name) { ptr in
|
|
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
|
|
getsockname(fd, sa, &nameLen)
|
|
}
|
|
}
|
|
#expect(got == 0)
|
|
guard got == 0 else { return }
|
|
|
|
let port = UInt16(bigEndian: name.sin_port)
|
|
#expect(RemotePortTunnel._testPortIsFree(port) == false)
|
|
|
|
_ = Darwin.close(fd)
|
|
fd = -1
|
|
|
|
// In parallel test runs, another test may briefly grab the same ephemeral port.
|
|
// Poll for a short window to avoid flakiness.
|
|
let deadline = Date().addingTimeInterval(0.5)
|
|
var free = false
|
|
while Date() < deadline {
|
|
if RemotePortTunnel._testPortIsFree(port) {
|
|
free = true
|
|
break
|
|
}
|
|
usleep(10000) // 10ms
|
|
}
|
|
#expect(free == true)
|
|
}
|
|
|
|
@Test @MainActor func `remote port override prefers explicit remote port`() async {
|
|
let configPath = TestIsolation.tempConfigPath()
|
|
await TestIsolation.withIsolatedState(env: ["OPENCLAW_CONFIG_PATH": configPath]) {
|
|
OpenClawConfigFile.saveDict([
|
|
"gateway": [
|
|
"remote": [
|
|
"url": "ws://127.0.0.1:19089",
|
|
"remotePort": 18789,
|
|
],
|
|
],
|
|
])
|
|
|
|
#expect(RemotePortTunnel._testResolveRemotePortOverride(
|
|
defaultRemotePort: 19089,
|
|
sshHost: "gateway.example") == 18789)
|
|
}
|
|
}
|
|
|
|
@Test @MainActor func `remote port override can read loopback url port`() async {
|
|
let configPath = TestIsolation.tempConfigPath()
|
|
await TestIsolation.withIsolatedState(env: ["OPENCLAW_CONFIG_PATH": configPath]) {
|
|
OpenClawConfigFile.saveDict([
|
|
"gateway": [
|
|
"remote": [
|
|
"url": "ws://127.0.0.1:18789",
|
|
],
|
|
],
|
|
])
|
|
|
|
#expect(RemotePortTunnel._testResolveRemotePortOverride(
|
|
defaultRemotePort: 19089,
|
|
sshHost: "gateway.example") == 18789)
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|