Harden notify focus regression socket checks

This commit is contained in:
Lawrence Chen 2026-03-05 18:10:00 -08:00
parent 9c9670ea71
commit 5fa3570632
3 changed files with 281 additions and 43 deletions

View file

@ -5739,6 +5739,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
"socketExpectedPath": env["CMUX_SOCKET_PATH"] ?? "",
"socketMode": "off",
"socketReady": "0",
"socketPingResponse": "",
"socketIsRunning": "0",
"socketAcceptLoopAlive": "0",
"socketPathMatches": "0",
@ -5752,27 +5753,50 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
"socketExpectedPath": config.path,
"socketMode": config.mode.rawValue,
"socketReady": "pending",
"socketPingResponse": "",
], at: path)
restartSocketListenerIfEnabled(source: "uiTest.multiWindowNotifications.setup")
let deadline = Date().addingTimeInterval(12.0)
let deadline = Date().addingTimeInterval(20.0)
func publish() {
let health = TerminalController.shared.socketListenerHealth(expectedSocketPath: config.path)
let isTimedOut = Date() >= deadline
writeMultiWindowNotificationTestData([
"socketExpectedPath": config.path,
"socketMode": config.mode.rawValue,
"socketReady": health.isHealthy ? "1" : (isTimedOut ? "0" : "pending"),
"socketIsRunning": health.isRunning ? "1" : "0",
"socketAcceptLoopAlive": health.acceptLoopAlive ? "1" : "0",
"socketPathMatches": health.socketPathMatches ? "1" : "0",
"socketPathExists": health.socketPathExists ? "1" : "0",
"socketFailureSignals": health.failureSignals.joined(separator: ","),
], at: path)
guard !health.isHealthy, !isTimedOut else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
publish()
let socketPath = config.path
let socketMode = config.mode.rawValue
let dataPath = path
DispatchQueue.global(qos: .utility).async { [weak self] in
let pingResponse = health.isHealthy
? TerminalController.probeSocketCommand("ping", at: socketPath, timeout: 1.0)
: nil
let isReady = health.isHealthy && pingResponse == "PONG"
let failureSignals = {
var signals = health.failureSignals
if health.isHealthy && pingResponse != "PONG" {
signals.append("ping_timeout")
}
return signals.joined(separator: ",")
}()
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.writeMultiWindowNotificationTestData([
"socketExpectedPath": socketPath,
"socketMode": socketMode,
"socketReady": isReady ? "1" : (isTimedOut ? "0" : "pending"),
"socketPingResponse": pingResponse ?? "",
"socketIsRunning": health.isRunning ? "1" : "0",
"socketAcceptLoopAlive": health.acceptLoopAlive ? "1" : "0",
"socketPathMatches": health.socketPathMatches ? "1" : "0",
"socketPathExists": health.socketPathExists ? "1" : "0",
"socketFailureSignals": failureSignals,
], at: dataPath)
guard !isTimedOut else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
publish()
}
}
}
}

View file

@ -662,6 +662,100 @@ class TerminalController {
)
}
nonisolated static func probeSocketCommand(
_ command: String,
at socketPath: String,
timeout: TimeInterval
) -> String? {
let fd = socket(AF_UNIX, SOCK_STREAM, 0)
guard fd >= 0 else { return nil }
defer { close(fd) }
#if os(macOS)
var noSigPipe: Int32 = 1
_ = withUnsafePointer(to: &noSigPipe) { ptr in
setsockopt(
fd,
SOL_SOCKET,
SO_NOSIGPIPE,
ptr,
socklen_t(MemoryLayout<Int32>.size)
)
}
#endif
var addr = sockaddr_un()
memset(&addr, 0, MemoryLayout<sockaddr_un>.size)
addr.sun_family = sa_family_t(AF_UNIX)
let maxLen = MemoryLayout.size(ofValue: addr.sun_path)
let pathBytes = Array(socketPath.utf8CString)
guard pathBytes.count <= maxLen else { return nil }
withUnsafeMutablePointer(to: &addr.sun_path) { ptr in
let raw = UnsafeMutableRawPointer(ptr).assumingMemoryBound(to: CChar.self)
memset(raw, 0, maxLen)
for index in 0..<pathBytes.count {
raw[index] = pathBytes[index]
}
}
let pathOffset = MemoryLayout<sockaddr_un>.offset(of: \.sun_path) ?? 0
let addrLen = socklen_t(pathOffset + pathBytes.count)
#if os(macOS)
addr.sun_len = UInt8(min(Int(addrLen), 255))
#endif
let connectResult = withUnsafePointer(to: &addr) { ptr in
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPtr in
connect(fd, sockaddrPtr, addrLen)
}
}
guard connectResult == 0 else { return nil }
let payload = command + "\n"
let wroteAll = payload.withCString { cString in
var remaining = strlen(cString)
var pointer = UnsafeRawPointer(cString)
while remaining > 0 {
let written = write(fd, pointer, remaining)
if written <= 0 { return false }
remaining -= written
pointer = pointer.advanced(by: written)
}
return true
}
guard wroteAll else { return nil }
let deadline = Date().addingTimeInterval(timeout)
var buffer = [UInt8](repeating: 0, count: 4096)
var response = ""
while Date() < deadline {
var pollDescriptor = pollfd(fd: fd, events: Int16(POLLIN), revents: 0)
let ready = poll(&pollDescriptor, 1, 100)
if ready < 0 {
return nil
}
if ready == 0 {
continue
}
let count = read(fd, &buffer, buffer.count)
if count <= 0 {
break
}
if let chunk = String(bytes: buffer[0..<count], encoding: .utf8) {
response.append(chunk)
if let newlineIndex = response.firstIndex(of: "\n") {
return String(response[..<newlineIndex])
}
}
}
let trimmed = response.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
nonisolated func stop() {
let (socketToClose, socketPathToUnlink) = withListenerState {
isRunning = false

View file

@ -229,34 +229,37 @@ final class MultiWindowNotificationsUITests: XCTestCase {
if setup["socketReady"] != "1" {
XCTFail(
"Control socket unavailable in this test environment. expected=\(socketPath) " +
"mode=\(setup["socketMode"] ?? "") running=\(setup["socketIsRunning"] ?? "") " +
"acceptLoopAlive=\(setup["socketAcceptLoopAlive"] ?? "") pathMatches=\(setup["socketPathMatches"] ?? "") " +
"pathExists=\(setup["socketPathExists"] ?? "") signals=\(setup["socketFailureSignals"] ?? "")"
socketDiagnostics(from: setup)
)
return
}
XCTAssertTrue(waitForWindowCount(atLeast: 2, app: app, timeout: 6.0))
let pingResponse = waitForSocketPong(timeout: 20.0)
if pingResponse != "PONG",
let pingResult = waitForCmuxPing(timeout: 20.0)
if pingResult.stdout != "PONG",
let resolvedPath = resolveSocketPath(timeout: 5.0, requiredWorkspaceId: tabId2) {
socketPath = resolvedPath
}
let confirmedPingResponse = pingResponse == "PONG" ? pingResponse : waitForSocketPong(timeout: 5.0)
guard confirmedPingResponse == "PONG" else {
let confirmedPingResult = pingResult.stdout == "PONG" ? pingResult : waitForCmuxPing(timeout: 5.0)
guard confirmedPingResult.stdout == "PONG" else {
let failureSetup = loadData() ?? setup
XCTFail(
"Control socket did not respond in time. path=\(socketPath) response=\(confirmedPingResponse ?? "<nil>") " +
"mode=\(setup["socketMode"] ?? "") running=\(setup["socketIsRunning"] ?? "") " +
"acceptLoopAlive=\(setup["socketAcceptLoopAlive"] ?? "") pathMatches=\(setup["socketPathMatches"] ?? "") " +
"pathExists=\(setup["socketPathExists"] ?? "") signals=\(setup["socketFailureSignals"] ?? "")"
"Control socket did not respond in time. path=\(socketPath) response=\(confirmedPingResult.stdout ?? "<nil>") " +
"stderr=\(confirmedPingResult.stderr ?? "<nil>") " +
socketDiagnostics(from: failureSetup)
)
return
}
guard let surfaceId = waitForSurfaceId(forWorkspaceId: tabId2, timeout: 12.0) else {
XCTFail("Expected at least one surface in workspace \(tabId2). socket=\(socketPath)")
guard let surfaceId = waitForSurfaceIdViaCLI(forWorkspaceId: tabId2, timeout: 12.0)
?? waitForSurfaceId(forWorkspaceId: tabId2, timeout: 3.0) else {
let failureSetup = loadData() ?? setup
XCTFail(
"Expected at least one surface in workspace \(tabId2). socket=\(socketPath) " +
socketDiagnostics(from: failureSetup)
)
return
}
@ -395,6 +398,40 @@ final class MultiWindowNotificationsUITests: XCTestCase {
return socketCommand("ping") ?? lastResponse
}
private func waitForCmuxPing(timeout: TimeInterval) -> (stdout: String?, stderr: String?) {
let deadline = Date().addingTimeInterval(timeout)
var lastStdout: String?
var lastStderr: String?
while Date() < deadline {
let result = runCmuxCommand(
socketPath: socketPath,
arguments: ["ping"],
responseTimeoutSeconds: 2.0
)
let stdout = result.stdout.isEmpty ? nil : result.stdout
let stderr = result.stderr.isEmpty ? nil : result.stderr
if let stdout {
lastStdout = stdout
}
if let stderr {
lastStderr = stderr
}
if result.terminationStatus == 0, stdout == "PONG" {
return ("PONG", stderr)
}
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
let result = runCmuxCommand(
socketPath: socketPath,
arguments: ["ping"],
responseTimeoutSeconds: 2.0
)
let stdout = result.stdout.isEmpty ? nil : result.stdout
let stderr = result.stderr.isEmpty ? nil : result.stderr
return (stdout ?? lastStdout, stderr ?? lastStderr)
}
private func waitForAppToLeaveForeground(_ app: XCUIApplication, timeout: TimeInterval) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
@ -436,29 +473,101 @@ final class MultiWindowNotificationsUITests: XCTestCase {
return firstSurfaceId(forWorkspaceId: workspaceId)
}
private func waitForSurfaceIdViaCLI(forWorkspaceId workspaceId: String, timeout: TimeInterval) -> String? {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if let surfaceId = firstSurfaceIdViaCLI(forWorkspaceId: workspaceId) {
return surfaceId
}
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
return firstSurfaceIdViaCLI(forWorkspaceId: workspaceId)
}
private func firstSurfaceIdViaCLI(forWorkspaceId workspaceId: String) -> String? {
guard let paneId = firstPaneIdViaCLI(forWorkspaceId: workspaceId) else {
return nil
}
let result = runCmuxCommand(
socketPath: socketPath,
arguments: [
"list-pane-surfaces",
"--workspace",
workspaceId,
"--pane",
paneId,
"--id-format",
"uuids"
],
responseTimeoutSeconds: 3.0
)
guard result.terminationStatus == 0 else { return nil }
return firstHandle(in: result.stdout)
}
private func firstPaneIdViaCLI(forWorkspaceId workspaceId: String) -> String? {
let result = runCmuxCommand(
socketPath: socketPath,
arguments: [
"list-panes",
"--workspace",
workspaceId,
"--id-format",
"uuids"
],
responseTimeoutSeconds: 3.0
)
guard result.terminationStatus == 0 else { return nil }
return firstHandle(in: result.stdout)
}
private func firstHandle(in output: String) -> String? {
for rawLine in output.split(separator: "\n", omittingEmptySubsequences: true) {
var line = rawLine.trimmingCharacters(in: .whitespacesAndNewlines)
guard !line.isEmpty, !line.hasPrefix("No ") else { continue }
if line.hasPrefix("* ") || line.hasPrefix(" ") {
line = String(line.dropFirst(2))
}
guard let token = line.split(whereSeparator: \.isWhitespace).first else { continue }
return String(token)
}
return nil
}
private func runCmuxNotify(
socketPath: String,
workspaceId: String,
surfaceId: String,
title: String
) -> (terminationStatus: Int32, stdout: String, stderr: String) {
runCmuxCommand(
socketPath: socketPath,
arguments: [
"notify",
"--workspace",
workspaceId,
"--surface",
surfaceId,
"--title",
title,
"--subtitle",
"ui-test",
"--body",
"focus-regression"
],
responseTimeoutSeconds: 4.0
)
}
private func runCmuxCommand(
socketPath: String,
arguments: [String],
responseTimeoutSeconds: Double = 3.0
) -> (terminationStatus: Int32, stdout: String, stderr: String) {
let process = Process()
let cliPath = resolveCmuxCLIPath()
var args = [
"--socket",
socketPath,
"notify",
"--workspace",
workspaceId,
"--surface",
surfaceId,
"--title",
title,
"--subtitle",
"ui-test",
"--body",
"focus-regression"
]
var args = ["--socket", socketPath]
args.append(contentsOf: arguments)
if let cliPath {
process.executableURL = URL(fileURLWithPath: cliPath)
} else {
@ -466,6 +575,9 @@ final class MultiWindowNotificationsUITests: XCTestCase {
args.insert("cmux", at: 0)
}
process.arguments = args
var environment = ProcessInfo.processInfo.environment
environment["CMUXTERM_CLI_RESPONSE_TIMEOUT_SEC"] = String(responseTimeoutSeconds)
process.environment = environment
let stdoutPipe = Pipe()
let stderrPipe = Pipe()
@ -479,7 +591,7 @@ final class MultiWindowNotificationsUITests: XCTestCase {
return (
terminationStatus: -1,
stdout: "",
stderr: "Failed to run cmux notify: \(error.localizedDescription) (cliPath=\(cliPath ?? "env:cmux"))"
stderr: "Failed to run cmux command: \(error.localizedDescription) (cliPath=\(cliPath ?? "env:cmux"))"
)
}
@ -492,6 +604,14 @@ final class MultiWindowNotificationsUITests: XCTestCase {
return (process.terminationStatus, stdout, stderr)
}
private func socketDiagnostics(from data: [String: String]) -> String {
let pingResponse = data["socketPingResponse"].flatMap { $0.isEmpty ? nil : $0 } ?? "<nil>"
return "mode=\(data["socketMode"] ?? "") running=\(data["socketIsRunning"] ?? "") " +
"acceptLoopAlive=\(data["socketAcceptLoopAlive"] ?? "") pathMatches=\(data["socketPathMatches"] ?? "") " +
"pathExists=\(data["socketPathExists"] ?? "") ping=\(pingResponse) " +
"signals=\(data["socketFailureSignals"] ?? "")"
}
private func resolveCmuxCLIPath() -> String? {
let fileManager = FileManager.default
let env = ProcessInfo.processInfo.environment