Merge pull request #932 from manaflow-ai/issue-922-notify-steals-window-focus

Fix cmux notify focus steal regression
This commit is contained in:
Lawrence Chen 2026-03-06 00:51:47 -08:00 committed by GitHub
commit d8ffb3eedb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 1161 additions and 46 deletions

View file

@ -1694,6 +1694,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
PostHogAnalytics.shared.startIfNeeded()
}
let forceDuplicateLaunchObserver = env["CMUX_UI_TEST_ENABLE_DUPLICATE_LAUNCH_OBSERVER"] == "1"
// UI tests frequently time out waiting for the main window if we do heavyweight
// LaunchServices registration / single-instance enforcement synchronously at startup.
// Skip these during XCTest (the app-under-test) so the window can appear quickly.
@ -1704,6 +1706,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
self.enforceSingleInstance()
self.observeDuplicateLaunches()
}
} else if forceDuplicateLaunchObserver {
// Some UI regressions specifically exercise launch-observer behavior while still
// running under XCTest. Allow an explicit opt-in for those cases only.
DispatchQueue.main.async { [weak self] in
self?.observeDuplicateLaunches()
}
}
NSWindow.allowsAutomaticWindowTabbing = false
disableNativeTabbingShortcut()
@ -5752,19 +5760,64 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
try? FileManager.default.removeItem(atPath: path)
let deadline = Date().addingTimeInterval(8.0)
let contextDeadline = Date().addingTimeInterval(8.0)
func waitForContexts(minCount: Int, _ completion: @escaping () -> Void) {
if mainWindowContexts.count >= minCount,
mainWindowContexts.values.allSatisfy({ $0.window != nil }) {
completion()
return
}
guard Date() < deadline else { return }
guard Date() < contextDeadline else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
waitForContexts(minCount: minCount, completion)
}
}
func waitForSurfaceId(
on tabManager: TabManager,
tabId: UUID,
timeout: TimeInterval = 8.0,
_ completion: @escaping (UUID) -> Void
) {
let deadline = Date().addingTimeInterval(timeout)
func resolvedSurfaceId() -> UUID? {
if let surfaceId = tabManager.focusedPanelId(for: tabId) {
return surfaceId
}
guard let workspace = tabManager.tabs.first(where: { $0.id == tabId }) else {
return nil
}
if let terminalPanelId = workspace.focusedTerminalPanel?.id {
return terminalPanelId
}
if let terminalPanelId = workspace.terminalPanelForConfigInheritance()?.id {
return terminalPanelId
}
return workspace.panels.values
.compactMap { ($0 as? TerminalPanel)?.id }
.sorted(by: { $0.uuidString < $1.uuidString })
.first
}
func poll() {
if let surfaceId = resolvedSurfaceId() {
completion(surfaceId)
return
}
guard Date() < deadline else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
poll()
}
}
poll()
}
waitForContexts(minCount: 1) { [weak self] in
guard let self else { return }
guard let window1 = self.mainWindowContexts.values.first else { return }
@ -5778,39 +5831,193 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
let contexts = Array(self.mainWindowContexts.values)
guard let window2 = contexts.first(where: { $0.windowId != window1.windowId }) else { return }
guard let tabId2 = window2.tabManager.selectedTabId ?? window2.tabManager.tabs.first?.id else { return }
guard let store = self.notificationStore else { return }
waitForSurfaceId(on: window1.tabManager, tabId: tabId1) { [weak self] surfaceId1 in
guard let self else { return }
waitForSurfaceId(on: window2.tabManager, tabId: tabId2) { [weak self] surfaceId2 in
guard let self else { return }
guard let store = self.notificationStore else { return }
// Ensure the target window is currently showing the Notifications overlay,
// so opening a notification must switch it back to the terminal UI.
window2.sidebarSelectionState.selection = .notifications
// Ensure the target window is currently showing the Notifications overlay,
// so opening a notification must switch it back to the terminal UI.
window2.sidebarSelectionState.selection = .notifications
// Create notifications for both windows. Ensure W2 isn't suppressed just because it's focused.
let prevOverride = AppFocusState.overrideIsFocused
AppFocusState.overrideIsFocused = false
store.addNotification(tabId: tabId2, surfaceId: nil, title: "W2", subtitle: "multiwindow", body: "")
AppFocusState.overrideIsFocused = prevOverride
// Create notifications for both windows. Ensure W2 isn't suppressed just because it's focused.
let prevOverride = AppFocusState.overrideIsFocused
AppFocusState.overrideIsFocused = false
store.addNotification(tabId: tabId2, surfaceId: nil, title: "W2", subtitle: "multiwindow", body: "")
AppFocusState.overrideIsFocused = prevOverride
// Insert after W2 so it becomes "latest unread" (first in list).
store.addNotification(tabId: tabId1, surfaceId: nil, title: "W1", subtitle: "multiwindow", body: "")
// Insert after W2 so it becomes "latest unread" (first in list).
store.addNotification(tabId: tabId1, surfaceId: nil, title: "W1", subtitle: "multiwindow", body: "")
let notif1 = store.notifications.first(where: { $0.tabId == tabId1 && $0.title == "W1" })
let notif2 = store.notifications.first(where: { $0.tabId == tabId2 && $0.title == "W2" })
let notif1 = store.notifications.first(where: { $0.tabId == tabId1 && $0.title == "W1" })
let notif2 = store.notifications.first(where: { $0.tabId == tabId2 && $0.title == "W2" })
self.writeMultiWindowNotificationTestData([
"window1Id": window1.windowId.uuidString,
"window2Id": window2.windowId.uuidString,
"window2InitialSidebarSelection": "notifications",
"tabId1": tabId1.uuidString,
"tabId2": tabId2.uuidString,
"notifId1": notif1?.id.uuidString ?? "",
"notifId2": notif2?.id.uuidString ?? "",
"expectedLatestWindowId": window1.windowId.uuidString,
"expectedLatestTabId": tabId1.uuidString,
], at: path)
self.writeMultiWindowNotificationTestData([
"window1Id": window1.windowId.uuidString,
"window2Id": window2.windowId.uuidString,
"window2InitialSidebarSelection": "notifications",
"tabId1": tabId1.uuidString,
"tabId2": tabId2.uuidString,
"surfaceId1": surfaceId1.uuidString,
"surfaceId2": surfaceId2.uuidString,
"notifId1": notif1?.id.uuidString ?? "",
"notifId2": notif2?.id.uuidString ?? "",
"expectedLatestWindowId": window1.windowId.uuidString,
"expectedLatestTabId": tabId1.uuidString,
], at: path)
self.prepareMultiWindowNotificationSourceTerminalIfNeeded(
at: path,
windowId: window1.windowId,
tabManager: window1.tabManager,
tabId: tabId1,
surfaceId: surfaceId1
)
self.publishMultiWindowNotificationSocketStateIfNeeded(at: path)
}
}
}
}
}
private func prepareMultiWindowNotificationSourceTerminalIfNeeded(
at path: String,
windowId: UUID,
tabManager: TabManager,
tabId: UUID,
surfaceId: UUID
) {
let env = ProcessInfo.processInfo.environment
guard env["CMUX_UI_TEST_NOTIFY_SOURCE_TERMINAL_READY"] == "1" else { return }
writeMultiWindowNotificationTestData([
"sourceTerminalReady": "pending",
"sourceTerminalFocusFailure": "",
], at: path)
let deadline = Date().addingTimeInterval(8.0)
func publish(ready: Bool, failure: String = "") {
writeMultiWindowNotificationTestData([
"sourceTerminalReady": ready ? "1" : "0",
"sourceTerminalFocusFailure": failure,
], at: path)
}
func poll() {
guard let workspace = tabManager.tabs.first(where: { $0.id == tabId }) else {
publish(ready: false, failure: "workspace_missing")
return
}
guard let terminalPanel = workspace.terminalPanel(for: surfaceId) else {
publish(ready: false, failure: "terminal_missing")
return
}
let isWindowFrontmost = {
guard let window = self.mainWindow(for: windowId) else { return false }
return NSApp.keyWindow === window || NSApp.mainWindow === window
}()
if isWindowFrontmost && terminalPanel.hostedView.isSurfaceViewFirstResponder() {
publish(ready: true)
return
}
guard Date() < deadline else {
publish(
ready: false,
failure: isWindowFrontmost ? "terminal_not_first_responder" : "window_not_frontmost"
)
return
}
_ = self.focusMainWindow(windowId: windowId)
if let tab = tabManager.tabs.first(where: { $0.id == tabId }) {
tabManager.selectTab(tab)
tabManager.focusSurface(tabId: tabId, surfaceId: surfaceId)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
poll()
}
}
poll()
}
private func publishMultiWindowNotificationSocketStateIfNeeded(at path: String) {
let env = ProcessInfo.processInfo.environment
guard env["CMUX_UI_TEST_SOCKET_SANITY"] == "1" else { return }
guard let config = socketListenerConfigurationIfEnabled() else {
writeMultiWindowNotificationTestData([
"socketExpectedPath": env["CMUX_SOCKET_PATH"] ?? "",
"socketMode": "off",
"socketReady": "0",
"socketPingResponse": "",
"socketIsRunning": "0",
"socketAcceptLoopAlive": "0",
"socketPathMatches": "0",
"socketPathExists": "0",
"socketFailureSignals": "socket_disabled",
], at: path)
return
}
writeMultiWindowNotificationTestData([
"socketExpectedPath": config.path,
"socketMode": config.mode.rawValue,
"socketReady": "pending",
"socketPingResponse": "",
], at: path)
restartSocketListenerIfEnabled(source: "uiTest.multiWindowNotifications.setup")
let deadline = Date().addingTimeInterval(20.0)
func publish() {
let health = TerminalController.shared.socketListenerHealth(expectedSocketPath: config.path)
let isTimedOut = Date() >= deadline
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, !isReady else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
publish()
}
}
}
}
publish()
}
private func writeMultiWindowNotificationTestData(_ updates: [String: String], at path: String) {
var payload = loadMultiWindowNotificationTestData(at: path)
for (key, value) in updates {
@ -7836,6 +8043,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
private func observeDuplicateLaunches() {
guard let bundleId = Bundle.main.bundleIdentifier else { return }
let embeddedCLIURL = Bundle.main.bundleURL
.appendingPathComponent("Contents/Resources/bin/cmux", isDirectory: false)
.standardizedFileURL
.resolvingSymlinksInPath()
let currentPid = ProcessInfo.processInfo.processIdentifier
workspaceObserver = NSWorkspace.shared.notificationCenter.addObserver(
@ -7846,6 +8057,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
guard self != nil else { return }
guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return }
guard app.bundleIdentifier == bundleId, app.processIdentifier != currentPid else { return }
if let executableURL = app.executableURL?
.standardizedFileURL
.resolvingSymlinksInPath(),
executableURL == embeddedCLIURL {
return
}
app.terminate()
if !app.isTerminated {

View file

@ -775,6 +775,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
@ -1376,6 +1470,9 @@ class TerminalController {
#if DEBUG
case "send_workspace":
return sendInputToWorkspace(args)
case "set_shortcut":
return setShortcut(args)
@ -9524,6 +9621,7 @@ class TerminalController {
sidebar_overlay_gate [active|inactive] - Return true/false if sidebar outside-drop overlay would capture (test-only)
terminal_drop_overlay_probe [deferred|direct] - Trigger focused terminal drop-overlay show path and report animation counts (test-only)
activate_app - Bring app + main window to front (test-only)
send_workspace <workspace_id> <text> - Send text to a workspace's selected terminal (test-only)
is_terminal_focused <id|idx> - Return true/false if terminal surface is first responder (test-only)
read_terminal_text [id|idx] - Read visible terminal text (base64, test-only)
render_stats [id|idx] - Read terminal render stats (draw counters, test-only)
@ -10758,7 +10856,13 @@ class TerminalController {
var result = "OK"
DispatchQueue.main.sync {
guard let tab = resolveTab(from: tabArg, tabManager: tabManager) else {
let tab: Tab?
if let tabId = UUID(uuidString: tabArg) {
tab = tabForSidebarMutation(id: tabId)
} else {
tab = resolveTab(from: tabArg, tabManager: tabManager)
}
guard let tab else {
result = "ERROR: Tab not found"
return
}
@ -11773,6 +11877,97 @@ class TerminalController {
return success ? "OK" : "ERROR: Failed to send input"
}
private func sendInputToWorkspace(_ args: String) -> String {
guard let tabManager else { return "ERROR: TabManager not available" }
let parts = args.split(separator: " ", maxSplits: 1).map(String.init)
guard parts.count == 2 else { return "ERROR: Usage: send_workspace <workspace_id> <text>" }
let workspaceArg = parts[0].trimmingCharacters(in: .whitespacesAndNewlines)
let text = parts[1]
guard let workspaceId = UUID(uuidString: workspaceArg) else {
return "ERROR: Invalid workspace ID"
}
var success = false
var error: String?
DispatchQueue.main.sync {
guard let targetManager = AppDelegate.shared?.tabManagerFor(tabId: workspaceId)
?? (tabManager.tabs.contains(where: { $0.id == workspaceId }) ? tabManager : nil) else {
error = "ERROR: Workspace not found"
return
}
guard let tab = targetManager.tabs.first(where: { $0.id == workspaceId }) else {
error = "ERROR: Workspace not found"
return
}
guard let terminalPanel = sendableWorkspaceTerminalPanel(in: tab) else {
error = "ERROR: No selected terminal in workspace"
return
}
let unescaped = text
.replacingOccurrences(of: "\\n", with: "\r")
.replacingOccurrences(of: "\\r", with: "\r")
.replacingOccurrences(of: "\\t", with: "\t")
// This DEBUG-only command is used by UI tests to enqueue shell work in an
// existing workspace. Return once the input is queued on main so a long
// payload does not hold the control-socket response open in CI.
DispatchQueue.main.async { [weak self] in
guard let self else { return }
if let surface = terminalPanel.surface.surface {
self.sendSocketText(unescaped, surface: surface)
} else {
terminalPanel.sendText(unescaped)
terminalPanel.surface.requestBackgroundSurfaceStartIfNeeded()
}
}
success = true
}
if let error { return error }
return success ? "OK" : "ERROR: Failed to send input"
}
private func sendableWorkspaceTerminalPanel(in workspace: Workspace) -> TerminalPanel? {
func selectedTerminalPanel(in paneId: PaneID) -> TerminalPanel? {
guard let selectedTab = workspace.bonsplitController.selectedTab(inPane: paneId),
let panelId = workspace.panelIdFromSurfaceId(selectedTab.id),
let terminalPanel = workspace.panels[panelId] as? TerminalPanel else {
return nil
}
return terminalPanel
}
func isSelectedTerminalPanel(_ terminalPanel: TerminalPanel) -> Bool {
guard let surfaceId = workspace.surfaceIdFromPanelId(terminalPanel.id) else {
return false
}
return workspace.bonsplitController.allPaneIds.contains { paneId in
workspace.bonsplitController.selectedTab(inPane: paneId)?.id == surfaceId
}
}
if let focusedPane = workspace.bonsplitController.focusedPaneId,
let terminalPanel = selectedTerminalPanel(in: focusedPane) {
return terminalPanel
}
if let rememberedTerminal = workspace.lastRememberedTerminalPanelForConfigInheritance(),
isSelectedTerminalPanel(rememberedTerminal) {
return rememberedTerminal
}
for paneId in workspace.bonsplitController.allPaneIds {
if let terminalPanel = selectedTerminalPanel(in: paneId) {
return terminalPanel
}
}
return nil
}
private func sendInputToSurface(_ args: String) -> String {
guard let tabManager = tabManager else { return "ERROR: TabManager not available" }
let parts = args.split(separator: " ", maxSplits: 1).map(String.init)

View file

@ -190,6 +190,159 @@ final class MultiWindowNotificationsUITests: XCTestCase {
XCTAssertFalse(after.contains(marker), "Expected typing to be blocked while empty notifications popover is open")
}
func testNotifyCLIDoesNotStealFocusAcrossWindows() throws {
let app = XCUIApplication()
app.launchArguments += ["-socketControlMode", "allowAll"]
app.launchEnvironment["CMUX_UI_TEST_MULTI_WINDOW_NOTIF_SETUP"] = "1"
app.launchEnvironment["CMUX_UI_TEST_MULTI_WINDOW_NOTIF_PATH"] = dataPath
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
app.launchEnvironment["CMUX_SOCKET_MODE"] = "allowAll"
app.launchEnvironment["CMUX_SOCKET_ENABLE"] = "1"
app.launchEnvironment["CMUX_UI_TEST_SOCKET_SANITY"] = "1"
app.launchEnvironment["CMUX_UI_TEST_NOTIFY_SOURCE_TERMINAL_READY"] = "1"
app.launchEnvironment["CMUX_UI_TEST_ENABLE_DUPLICATE_LAUNCH_OBSERVER"] = "1"
app.launchEnvironment["CMUX_TAG"] = launchTag
app.launch()
XCTAssertTrue(
ensureForegroundAfterLaunch(app, timeout: 12.0),
"Expected app to launch for notify focus regression test. state=\(app.state.rawValue)"
)
XCTAssertTrue(
waitForDataMatch(timeout: 20.0) { data in
let tabId2 = data["tabId2"] ?? ""
let surfaceId2 = data["surfaceId2"] ?? ""
let socketReady = data["socketReady"] ?? ""
let sourceTerminalReady = data["sourceTerminalReady"] ?? ""
return !tabId2.isEmpty &&
!surfaceId2.isEmpty &&
!socketReady.isEmpty &&
socketReady != "pending" &&
!sourceTerminalReady.isEmpty &&
sourceTerminalReady != "pending"
},
"Expected multi-window notification setup data, socket readiness, and source terminal focus"
)
guard let setup = loadData() else {
XCTFail("Missing setup data")
return
}
guard let tabId2 = setup["tabId2"], !tabId2.isEmpty else {
XCTFail("Missing setup workspace id")
return
}
if let expectedSocketPath = setup["socketExpectedPath"], !expectedSocketPath.isEmpty {
socketPath = expectedSocketPath
}
if setup["socketReady"] != "1" {
XCTFail(
"Control socket unavailable in this test environment. expected=\(socketPath) " +
socketDiagnostics(from: setup)
)
return
}
guard setup["socketPingResponse"] == "PONG" else {
XCTFail(
"Control socket ping sanity check failed. path=\(socketPath) " +
socketDiagnostics(from: setup)
)
return
}
guard let surfaceId = setup["surfaceId2"], !surfaceId.isEmpty else {
XCTFail("Missing target surface id for workspace \(tabId2)")
return
}
guard setup["sourceTerminalReady"] == "1" else {
XCTFail(
"Expected source terminal to be focused before typing. " +
"failure=\(setup["sourceTerminalFocusFailure"] ?? "<unknown>")"
)
return
}
XCTAssertTrue(waitForWindowCount(atLeast: 2, app: app, timeout: 6.0))
let title = "focus-regression-\(UUID().uuidString.prefix(8))"
let commandResultStem = UUID().uuidString
let commandStatusPath = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-ui-test-notify-\(commandResultStem).status")
.path
let commandStdoutPath = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-ui-test-notify-\(commandResultStem).stdout")
.path
let commandStderrPath = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-ui-test-notify-\(commandResultStem).stderr")
.path
let commandScriptPath = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-ui-test-notify-\(commandResultStem).sh")
.path
defer {
try? FileManager.default.removeItem(atPath: commandStatusPath)
try? FileManager.default.removeItem(atPath: commandStdoutPath)
try? FileManager.default.removeItem(atPath: commandStderrPath)
try? FileManager.default.removeItem(atPath: commandScriptPath)
}
guard let bundledCLIPath = resolveCmuxCLIPaths(strategy: .bundledOnly).first else {
XCTFail("Failed to locate bundled cmux CLI for notify regression test")
return
}
let notifyScript = [
"#!/bin/sh",
"sleep 1",
"rm -f \(shellSingleQuote(commandStatusPath)) \(shellSingleQuote(commandStdoutPath)) \(shellSingleQuote(commandStderrPath))",
"\(shellSingleQuote(bundledCLIPath)) --socket \(shellSingleQuote(socketPath)) notify --workspace \(shellSingleQuote(tabId2)) --surface \(shellSingleQuote(surfaceId)) --title \(shellSingleQuote(title)) --subtitle \(shellSingleQuote("ui-test")) --body \(shellSingleQuote("focus-regression")) >\(shellSingleQuote(commandStdoutPath)) 2>\(shellSingleQuote(commandStderrPath))",
"printf '%s' $? >\(shellSingleQuote(commandStatusPath))"
].joined(separator: "\n")
do {
try notifyScript.write(toFile: commandScriptPath, atomically: true, encoding: .utf8)
} catch {
XCTFail(
"Failed to write delayed bundled `cmux notify` script. " +
"path=\(commandScriptPath) error=\(error)"
)
return
}
app.typeText("sh \(commandScriptPath)")
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
let finder = XCUIApplication(bundleIdentifier: "com.apple.finder")
finder.activate()
XCTAssertTrue(
waitForAppToLeaveForeground(app, timeout: 8.0),
"Expected cmux to move to background before delayed notify command runs. state=\(app.state.rawValue)"
)
XCTAssertTrue(
waitForCommandCompletionWhileBackgrounded(
statusPath: commandStatusPath,
app: app,
timeout: 15.0
),
"Expected delayed bundled `cmux notify` command to finish without foregrounding cmux. state=\(app.state.rawValue)"
)
let notifyExitStatus = readTrimmedFile(atPath: commandStatusPath) ?? "<missing>"
let notifyStdout = readTrimmedFile(atPath: commandStdoutPath) ?? ""
let notifyStderr = readTrimmedFile(atPath: commandStderrPath) ?? ""
RunLoop.current.run(until: Date().addingTimeInterval(0.5))
XCTAssertFalse(
app.state == .runningForeground,
"Expected cmux to remain in background after bundled `cmux notify`. state=\(app.state.rawValue) stderr=\(notifyStderr)"
)
guard notifyExitStatus == "0" else {
XCTFail(
"Expected bundled `cmux notify` launched from the in-app shell to succeed. " +
"status=\(notifyExitStatus) stdout=\(notifyStdout) stderr=\(notifyStderr)"
)
return
}
XCTAssertTrue(notifyStdout.contains("OK"), "Expected notify command to return OK. stdout=\(notifyStdout) stderr=\(notifyStderr)")
}
private func clickNotificationPopoverRowAndWaitForFocusChange(
button: XCUIElement,
app: XCUIApplication,
@ -274,6 +427,20 @@ final class MultiWindowNotificationsUITests: XCTestCase {
return false
}
private func waitForDataMatch(timeout: TimeInterval, predicate: ([String: String]) -> Bool) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if let data = loadData(), predicate(data) {
return true
}
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
if let data = loadData(), predicate(data) {
return true
}
return false
}
private func waitForSocketPong(timeout: TimeInterval) -> String? {
let deadline = Date().addingTimeInterval(timeout)
var lastResponse: String?
@ -287,33 +454,549 @@ final class MultiWindowNotificationsUITests: XCTestCase {
return socketCommand("ping") ?? lastResponse
}
private func resolveSocketPath(timeout: TimeInterval) -> String? {
private func waitForTerminalFocus(surfaceId: String, timeout: TimeInterval) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
for candidate in expectedSocketCandidates() {
guard FileManager.default.fileExists(atPath: candidate) else { continue }
if socketRespondsToPing(at: candidate) {
return candidate
}
if socketCommand("is_terminal_focused \(surfaceId)") == "true" {
return true
}
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
for candidate in expectedSocketCandidates() {
guard FileManager.default.fileExists(atPath: candidate) else { continue }
if socketRespondsToPing(at: candidate) {
return socketCommand("is_terminal_focused \(surfaceId)") == "true"
}
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)
}
if isSocketPermissionFailure(stderr),
waitForSocketPong(timeout: 0.5) == "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
if isSocketPermissionFailure(stderr),
waitForSocketPong(timeout: 0.5) == "PONG" {
return ("PONG", stderr)
}
return (stdout ?? lastStdout, stderr ?? lastStderr)
}
private func waitForCommandCompletionWhileBackgrounded(
statusPath: String,
app: XCUIApplication,
timeout: TimeInterval
) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
var sawCompletion = false
while Date() < deadline {
if app.state == .runningForeground {
return false
}
if FileManager.default.fileExists(atPath: statusPath) {
sawCompletion = true
break
}
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
guard sawCompletion || FileManager.default.fileExists(atPath: statusPath) else {
return false
}
let postCompletionDeadline = Date().addingTimeInterval(0.75)
while Date() < postCompletionDeadline {
if app.state == .runningForeground {
return false
}
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
return app.state != .runningForeground
}
private func waitForAppToLeaveForeground(_ app: XCUIApplication, timeout: TimeInterval) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if app.state != .runningForeground {
return true
}
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
return app.state != .runningForeground
}
private func firstSurfaceId(forWorkspaceId workspaceId: String) -> String? {
guard let response = socketCommand("list_surfaces \(workspaceId)"),
!response.isEmpty,
!response.hasPrefix("ERROR"),
response != "No surfaces" else {
return nil
}
for line in response.split(separator: "\n", omittingEmptySubsequences: true) {
let parts = line.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
guard parts.count == 2 else { continue }
let candidate = String(parts[1]).trimmingCharacters(in: .whitespacesAndNewlines)
if UUID(uuidString: candidate) != nil {
return candidate
}
}
return nil
}
private func expectedSocketCandidates() -> [String] {
private func waitForSurfaceId(forWorkspaceId workspaceId: String, timeout: TimeInterval) -> String? {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if let surfaceId = firstSurfaceId(forWorkspaceId: workspaceId) {
return surfaceId
}
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
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 firstSurfaceId(forWorkspaceId: workspaceId)
}
let result = runCmuxCommand(
socketPath: socketPath,
arguments: [
"list-pane-surfaces",
"--workspace",
workspaceId,
"--pane",
paneId,
"--id-format",
"uuids"
],
responseTimeoutSeconds: 3.0
)
guard result.terminationStatus == 0 else {
if isSocketPermissionFailure(result.stderr) {
return firstSurfaceId(forWorkspaceId: workspaceId)
}
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 {
if isSocketPermissionFailure(result.stderr) {
return nil
}
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,
cliStrategy: .bundledOnly
)
}
private func runCmuxCommand(
socketPath: String,
arguments: [String],
responseTimeoutSeconds: Double = 3.0,
cliStrategy: CmuxCLIStrategy = .any
) -> (terminationStatus: Int32, stdout: String, stderr: String) {
var args = ["--socket", socketPath]
args.append(contentsOf: arguments)
var environment = ProcessInfo.processInfo.environment
environment["CMUXTERM_CLI_RESPONSE_TIMEOUT_SEC"] = String(responseTimeoutSeconds)
let cliPaths = resolveCmuxCLIPaths(strategy: cliStrategy)
if cliPaths.isEmpty, cliStrategy == .bundledOnly {
return (
terminationStatus: -1,
stdout: "",
stderr: "Failed to locate bundled cmux CLI"
)
}
var lastPermissionFailure: (terminationStatus: Int32, stdout: String, stderr: String)?
for cliPath in cliPaths {
let result = executeCmuxCommand(
executablePath: cliPath,
arguments: args,
environment: environment
)
if result.terminationStatus == 0 {
return result
}
if result.stderr.localizedCaseInsensitiveContains("operation not permitted") {
lastPermissionFailure = result
continue
}
return result
}
if cliStrategy == .bundledOnly {
return lastPermissionFailure ?? (
terminationStatus: -1,
stdout: "",
stderr: "Bundled cmux CLI command failed without an executable path"
)
}
let fallbackArgs = ["cmux"] + args
let fallbackResult = executeCmuxCommand(
executablePath: "/usr/bin/env",
arguments: fallbackArgs,
environment: environment
)
if fallbackResult.terminationStatus == 0 || lastPermissionFailure == nil {
return fallbackResult
}
return lastPermissionFailure ?? fallbackResult
}
private enum CmuxCLIStrategy: Equatable {
case any
case bundledOnly
}
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 resolveCmuxCLIPaths(strategy: CmuxCLIStrategy) -> [String] {
let fileManager = FileManager.default
let env = ProcessInfo.processInfo.environment
var candidates: [String] = []
var productDirectories: [String] = []
if strategy == .any {
for key in ["CMUX_UI_TEST_CLI_PATH", "CMUXTERM_CLI"] {
if let value = env[key], !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
candidates.append(value)
}
}
}
if let builtProductsDir = env["BUILT_PRODUCTS_DIR"], !builtProductsDir.isEmpty {
productDirectories.append(builtProductsDir)
}
if let hostPath = env["TEST_HOST"], !hostPath.isEmpty {
let hostURL = URL(fileURLWithPath: hostPath)
let productsDir = hostURL
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.path
productDirectories.append(productsDir)
}
productDirectories.append(contentsOf: inferredBuildProductsDirectories())
for productsDir in uniquePaths(productDirectories) {
appendCLIPathCandidates(fromProductsDirectory: productsDir, strategy: strategy, to: &candidates)
}
candidates.append("/tmp/cmux-\(launchTag)/Build/Products/Debug/cmux DEV.app/Contents/Resources/bin/cmux")
candidates.append("/tmp/cmux-\(launchTag)/Build/Products/Debug/cmux.app/Contents/Resources/bin/cmux")
if strategy == .any {
candidates.append("/tmp/cmux-\(launchTag)/Build/Products/Debug/cmux")
}
var resolvedPaths: [String] = []
for path in uniquePaths(candidates) {
guard fileManager.isExecutableFile(atPath: path) else { continue }
resolvedPaths.append(URL(fileURLWithPath: path).resolvingSymlinksInPath().path)
}
return uniquePaths(resolvedPaths)
}
private func inferredBuildProductsDirectories() -> [String] {
let bundleURLs = [
Bundle.main.bundleURL,
Bundle(for: Self.self).bundleURL,
]
return bundleURLs.compactMap { bundleURL in
let standardizedPath = bundleURL.standardizedFileURL.path
let components = standardizedPath.split(separator: "/")
guard let productsIndex = components.firstIndex(of: "Products"),
productsIndex + 1 < components.count else {
return nil
}
let prefixComponents = components.prefix(productsIndex + 2)
return "/" + prefixComponents.joined(separator: "/")
}
}
private func appendCLIPathCandidates(
fromProductsDirectory productsDir: String,
strategy: CmuxCLIStrategy,
to candidates: inout [String]
) {
candidates.append("\(productsDir)/cmux DEV.app/Contents/Resources/bin/cmux")
candidates.append("\(productsDir)/cmux.app/Contents/Resources/bin/cmux")
if strategy == .any {
candidates.append("\(productsDir)/cmux")
}
guard let entries = try? FileManager.default.contentsOfDirectory(atPath: productsDir) else {
return
}
for entry in entries.sorted() where entry.hasSuffix(".app") {
let cliPath = URL(fileURLWithPath: productsDir)
.appendingPathComponent(entry)
.appendingPathComponent("Contents/Resources/bin/cmux")
.path
candidates.append(cliPath)
}
if strategy == .any {
for entry in entries.sorted() where entry == "cmux" {
let cliPath = URL(fileURLWithPath: productsDir)
.appendingPathComponent(entry)
.path
candidates.append(cliPath)
}
}
}
private func executeCmuxCommand(
executablePath: String,
arguments: [String],
environment: [String: String]
) -> (terminationStatus: Int32, stdout: String, stderr: String) {
let process = Process()
process.executableURL = URL(fileURLWithPath: executablePath)
process.arguments = arguments
process.environment = environment
let stdoutPipe = Pipe()
let stderrPipe = Pipe()
process.standardOutput = stdoutPipe
process.standardError = stderrPipe
do {
try process.run()
process.waitUntilExit()
} catch {
return (
terminationStatus: -1,
stdout: "",
stderr: "Failed to run cmux command: \(error.localizedDescription) (cliPath=\(executablePath))"
)
}
let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
let stdout = String(data: stdoutData, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let rawStderr = String(data: stderrData, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let stderr = rawStderr.isEmpty ? "" : "\(rawStderr) (cliPath=\(executablePath))"
return (process.terminationStatus, stdout, stderr)
}
private func isSocketPermissionFailure(_ stderr: String?) -> Bool {
guard let stderr, !stderr.isEmpty else { return false }
return stderr.localizedCaseInsensitiveContains("failed to connect to socket") &&
stderr.localizedCaseInsensitiveContains("operation not permitted")
}
private func uniquePaths(_ paths: [String]) -> [String] {
var unique: [String] = []
var seen = Set<String>()
for path in paths {
if seen.insert(path).inserted {
unique.append(path)
}
}
return unique
}
private func resolveSocketPath(timeout: TimeInterval, requiredWorkspaceId: String? = nil) -> String? {
let primaryCandidates = expectedSocketCandidates(includeGlobalFallback: false)
let fallbackCandidates: [String]
if let requiredWorkspaceId, !requiredWorkspaceId.isEmpty {
fallbackCandidates = expectedSocketCandidates(includeGlobalFallback: true)
.filter { !primaryCandidates.contains($0) }
} else {
fallbackCandidates = []
}
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
for candidate in primaryCandidates {
guard FileManager.default.fileExists(atPath: candidate) else { continue }
// Primary candidate is the explicitly requested CMUX_SOCKET_PATH. If it responds,
// prefer it even before workspace contents are fully initialized.
if socketRespondsToPing(at: candidate) {
return candidate
}
}
for candidate in fallbackCandidates {
guard FileManager.default.fileExists(atPath: candidate) else { continue }
if socketRespondsToPing(at: candidate),
socketMatchesRequiredWorkspace(candidate, workspaceId: requiredWorkspaceId) {
return candidate
}
}
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
for candidate in primaryCandidates {
guard FileManager.default.fileExists(atPath: candidate) else { continue }
if socketRespondsToPing(at: candidate) {
return candidate
}
}
for candidate in fallbackCandidates {
guard FileManager.default.fileExists(atPath: candidate) else { continue }
if socketRespondsToPing(at: candidate),
socketMatchesRequiredWorkspace(candidate, workspaceId: requiredWorkspaceId) {
return candidate
}
}
return nil
}
private func expectedSocketCandidates(includeGlobalFallback: Bool) -> [String] {
var candidates = [socketPath]
let taggedDebugSocket = "/tmp/cmux-debug-\(launchTag).sock"
if taggedDebugSocket != socketPath {
if !taggedDebugSocket.isEmpty {
candidates.append(taggedDebugSocket)
}
return candidates
if includeGlobalFallback {
candidates.append(contentsOf: discoverTmpSocketCandidates(limit: 12))
candidates.append("/tmp/cmux-debug.sock")
candidates.append("/tmp/cmux.sock")
}
var unique: [String] = []
var seen = Set<String>()
for candidate in candidates {
if seen.insert(candidate).inserted {
unique.append(candidate)
}
}
return unique
}
private func socketMatchesRequiredWorkspace(_ candidatePath: String, workspaceId: String?) -> Bool {
guard let workspaceId, !workspaceId.isEmpty else { return true }
let originalPath = socketPath
socketPath = candidatePath
defer { socketPath = originalPath }
guard let response = socketCommand("list_surfaces \(workspaceId)"),
!response.isEmpty,
!response.hasPrefix("ERROR"),
response != "No surfaces" else {
return false
}
return true
}
private func discoverTmpSocketCandidates(limit: Int) -> [String] {
let tmpPath = "/tmp"
guard let entries = try? FileManager.default.contentsOfDirectory(atPath: tmpPath) else {
return []
}
let matches = entries.filter { $0.hasPrefix("cmux") && $0.hasSuffix(".sock") }
let sorted = matches.compactMap { entry -> (path: String, mtime: Date)? in
let fullPath = (tmpPath as NSString).appendingPathComponent(entry)
guard let attrs = try? FileManager.default.attributesOfItem(atPath: fullPath) else {
return nil
}
let mtime = (attrs[.modificationDate] as? Date) ?? .distantPast
return (fullPath, mtime)
}
.sorted { $0.mtime > $1.mtime }
return Array(sorted.prefix(limit)).map(\.path)
}
private func socketRespondsToPing(at path: String) -> Bool {
@ -323,20 +1006,21 @@ final class MultiWindowNotificationsUITests: XCTestCase {
return socketCommand("ping") == "PONG"
}
private func socketCommand(_ cmd: String) -> String? {
if let response = ControlSocketClient(path: socketPath).sendLine(cmd) {
private func socketCommand(_ cmd: String, responseTimeout: TimeInterval = 2.0) -> String? {
if let response = ControlSocketClient(path: socketPath, responseTimeout: responseTimeout).sendLine(cmd) {
return response
}
return socketCommandViaNetcat(cmd)
return socketCommandViaNetcat(cmd, responseTimeout: responseTimeout)
}
private func socketCommandViaNetcat(_ cmd: String) -> String? {
private func socketCommandViaNetcat(_ cmd: String, responseTimeout: TimeInterval = 2.0) -> String? {
let nc = "/usr/bin/nc"
guard FileManager.default.isExecutableFile(atPath: nc) else { return nil }
let proc = Process()
proc.executableURL = URL(fileURLWithPath: "/bin/sh")
let script = "printf '%s\\n' \(shellSingleQuote(cmd)) | \(nc) -U \(shellSingleQuote(socketPath)) -w 2 2>/dev/null"
let timeoutSeconds = max(1, Int(ceil(responseTimeout)))
let script = "printf '%s\\n' \(shellSingleQuote(cmd)) | \(nc) -U \(shellSingleQuote(socketPath)) -w \(timeoutSeconds) 2>/dev/null"
proc.arguments = ["-lc", script]
let outPipe = Pipe()
@ -364,11 +1048,21 @@ final class MultiWindowNotificationsUITests: XCTestCase {
return "'" + value.replacingOccurrences(of: "'", with: "'\"'\"'") + "'"
}
private func readTrimmedFile(atPath path: String) -> String? {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)),
let value = String(data: data, encoding: .utf8) else {
return nil
}
return value.trimmingCharacters(in: .whitespacesAndNewlines)
}
private final class ControlSocketClient {
private let path: String
private let responseTimeout: TimeInterval
init(path: String) {
init(path: String, responseTimeout: TimeInterval = 2.0) {
self.path = path
self.responseTimeout = responseTimeout
}
func sendLine(_ line: String) -> String? {
@ -431,9 +1125,18 @@ final class MultiWindowNotificationsUITests: XCTestCase {
}
guard wrote else { return nil }
let deadline = Date().addingTimeInterval(responseTimeout)
var buf = [UInt8](repeating: 0, count: 4096)
var accum = ""
while true {
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 n = read(fd, &buf, buf.count)
if n <= 0 { break }
if let chunk = String(bytes: buf[0..<n], encoding: .utf8) {