diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv index b91c639d1c07..85849c037a71 100644 --- a/.github/swift-file-length-budget.tsv +++ b/.github/swift-file-length-budget.tsv @@ -4,7 +4,7 @@ 33285 CLI/cmux.swift 19990 Sources/Workspace.swift 19225 Sources/ContentView.swift -18118 Sources/AppDelegate.swift +18077 Sources/AppDelegate.swift 16075 Sources/GhosttyTerminalView.swift 14610 Sources/TerminalController.swift 13606 Sources/Panels/BrowserPanel.swift diff --git a/Packages/CmuxIPCService/Package.swift b/Packages/CmuxIPCService/Package.swift new file mode 100644 index 000000000000..14e81e7a6f11 --- /dev/null +++ b/Packages/CmuxIPCService/Package.swift @@ -0,0 +1,35 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "CmuxIPCService", + platforms: [ + .macOS(.v14), + ], + products: [ + .library( + name: "CmuxIPCService", + targets: ["CmuxIPCService"] + ), + ], + targets: [ + .target( + name: "CmuxIPCService", + swiftSettings: [ + .swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault"), + ] + ), + .testTarget( + name: "CmuxIPCServiceTests", + dependencies: ["CmuxIPCService"], + swiftSettings: [ + .swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault"), + ] + ), + ] +) diff --git a/Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRouteLaunchError.swift b/Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRouteLaunchError.swift new file mode 100644 index 000000000000..9d9cbb5703be --- /dev/null +++ b/Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRouteLaunchError.swift @@ -0,0 +1,22 @@ +/// Thrown by ``MultiWindowRouting/route(arguments:)`` when the cmux CLI +/// process could not be launched at all (the executable is missing, not +/// executable, or `posix_spawn` failed). +/// +/// This is the only error the route throws; once the CLI launches, every +/// outcome (including non-zero exit) is data in ``MultiWindowRouteResult``. +/// `description` preserves the underlying Foundation error's +/// `String(describing:)` text verbatim, so callers that re-encode the legacy +/// `"-1"` capture (`String(describing: error)` into the UI-test data file) +/// produce byte-identical output to the pre-extraction code. +public struct MultiWindowRouteLaunchError: Error, Sendable, Equatable, CustomStringConvertible { + /// The underlying launch error rendered with `String(describing:)`, + /// preserved verbatim for the legacy capture encoding. + public let description: String + + /// Creates a launch error. + /// - Parameter description: The underlying error's `String(describing:)` + /// text. + public init(description: String) { + self.description = description + } +} diff --git a/Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRouteResult.swift b/Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRouteResult.swift new file mode 100644 index 000000000000..4788e7c2bf52 --- /dev/null +++ b/Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRouteResult.swift @@ -0,0 +1,31 @@ +/// The captured outcome of one multi-window route call through the bundled +/// cmux CLI. +/// +/// Produced by ``MultiWindowRouting/route(arguments:)`` once the CLI process +/// launched and exited (a process that never launched throws +/// ``MultiWindowRouteLaunchError`` instead). The streams are UTF-8 decoded +/// with non-decodable output collapsing to the empty string, matching the +/// legacy AppDelegate capture exactly; consumers (the multi-window UI-test +/// scaffolding) write `String(terminationStatus)` and the streams verbatim +/// into the shared test-data file. +public struct MultiWindowRouteResult: Sendable, Equatable { + /// The CLI process termination status. + public let terminationStatus: Int32 + /// The captured standard output, UTF-8 decoded; empty when absent or not + /// valid UTF-8. + public let stdout: String + /// The captured standard error, UTF-8 decoded; empty when absent or not + /// valid UTF-8. + public let stderr: String + + /// Creates a route result. + /// - Parameters: + /// - terminationStatus: The CLI process termination status. + /// - stdout: The captured standard output. + /// - stderr: The captured standard error. + public init(terminationStatus: Int32, stdout: String, stderr: String) { + self.terminationStatus = terminationStatus + self.stdout = stdout + self.stderr = stderr + } +} diff --git a/Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRouter.swift b/Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRouter.swift new file mode 100644 index 000000000000..6585638c0b25 --- /dev/null +++ b/Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRouter.swift @@ -0,0 +1,146 @@ +public import Foundation +import Darwin +import os + +/// Diagnostics for partial pipe reads; mirrors the app-side ProcessPipeReader +/// warning the lifted code emitted (file-scoped `os.Logger` per house style). +nonisolated private let logger = Logger(subsystem: "com.cmuxterm.app", category: "MultiWindowRouter") + +/// Runs the bundled cmux CLI against the app's control socket to route a +/// request to a specific window, capturing its output. +/// +/// This is the production ``MultiWindowRouting``, extracted from AppDelegate's +/// `runMultiWindowRouteCLI`: it spawns the CLI with an implicit +/// `--socket ` argument pair and an explicit child environment (the +/// child inherits nothing beyond what is injected), then captures termination +/// status and both streams. A launch failure throws +/// ``MultiWindowRouteLaunchError``; a launched CLI always returns a result. +/// +/// Isolation design: the router holds only immutable `Sendable` configuration +/// (CLI URL, socket path, environment), so there is no state to protect and an +/// actor would serialize unrelated route calls for no benefit (the same ruling +/// that made `CmuxProcess.CommandRunner` a stateless struct). The legacy +/// synchronous `waitUntilExit` is replaced by a `terminationHandler` +/// continuation, so `route` never blocks its calling thread; the two stream +/// readers run on detached tasks (the `CommandRunner` pattern) so a stream +/// larger than the pipe buffer cannot deadlock the child against an unread +/// pipe. +public struct MultiWindowRouter: MultiWindowRouting, Sendable { + private let cliURL: URL + private let socketPath: String + // Environment is value-like once copied; stored immutable so the struct + // stays Sendable. + private let environment: [String: String] + + /// Creates a router for one CLI binary, socket, and child environment. + /// - Parameters: + /// - cliURL: The bundled cmux CLI executable. + /// - socketPath: The control socket path passed to every call as + /// `--socket `. + /// - environment: The complete child process environment (replaces, not + /// merges with, the app's environment). + public init(cliURL: URL, socketPath: String, environment: [String: String]) { + self.cliURL = cliURL + self.socketPath = socketPath + self.environment = environment + } + + /// Runs the CLI with `arguments` and captures its outcome. + /// + /// Implements ``MultiWindowRouting/route(arguments:)``; see the protocol + /// for the full contract and the type docs for the isolation rationale. + public func route(arguments: [String]) async throws -> MultiWindowRouteResult { + let process = Process() + process.executableURL = cliURL + process.arguments = ["--socket", socketPath] + arguments + process.environment = environment + + let stdoutPipe = Pipe() + let stderrPipe = Pipe() + process.standardOutput = stdoutPipe + process.standardError = stderrPipe + + // Drain both streams on detached tasks, keyed by raw fd so no + // non-Sendable FileHandle crosses the task boundary. Started before the + // spawn so a child that fills a pipe buffer can never deadlock against + // an unread pipe; the reads block until data or EOF arrives. + let stdoutDescriptor = stdoutPipe.fileHandleForReading.fileDescriptor + let stderrDescriptor = stderrPipe.fileHandleForReading.fileDescriptor + let stdoutReader = Task.detached { + self.readDataToEndOfFileOrEmpty(fromFileDescriptor: stdoutDescriptor) + } + let stderrReader = Task.detached { + self.readDataToEndOfFileOrEmpty(fromFileDescriptor: stderrDescriptor) + } + + let terminationStatus: Int32 + do { + terminationStatus = try await withCheckedThrowingContinuation { continuation in + process.terminationHandler = { finished in + continuation.resume(returning: finished.terminationStatus) + } + do { + try process.run() + // Close the parent's write ends so the readers see EOF once + // the child closes its copies. + try? stdoutPipe.fileHandleForWriting.close() + try? stderrPipe.fileHandleForWriting.close() + } catch { + process.terminationHandler = nil + try? stdoutPipe.fileHandleForWriting.close() + try? stderrPipe.fileHandleForWriting.close() + continuation.resume( + throwing: MultiWindowRouteLaunchError(description: String(describing: error)) + ) + } + } + } catch { + // The write ends are closed, so the readers finish on EOF; await + // them before rethrowing so no detached work outlives the call. + _ = await stdoutReader.value + _ = await stderrReader.value + throw error + } + + let stdoutData = await stdoutReader.value + let stderrData = await stderrReader.value + return MultiWindowRouteResult( + terminationStatus: terminationStatus, + stdout: String(data: stdoutData, encoding: .utf8) ?? "", + stderr: String(data: stderrData, encoding: .utf8) ?? "" + ) + } + + /// Reads `fileDescriptor` to end-of-file, retrying `EINTR`, returning + /// partial data (with a logged warning) on a read error. Carried over from + /// the app-side `ProcessPipeReader.readDataToEndOfFileOrEmpty`, which stays + /// app-target for its other callers. Operates on the raw descriptor so the + /// detached reader tasks capture only `Sendable` values (`self` is a + /// Sendable value type). + private func readDataToEndOfFileOrEmpty(fromFileDescriptor fileDescriptor: Int32) -> Data { + let chunkSize = 64 * 1024 + var data = Data() + var buffer = [UInt8](repeating: 0, count: chunkSize) + while true { + let bytesRead = buffer.withUnsafeMutableBytes { pointer -> Int in + guard let baseAddress = pointer.baseAddress else { return 0 } + return Darwin.read(fileDescriptor, baseAddress, chunkSize) + } + if bytesRead > 0 { + data.append(contentsOf: buffer[0..` pair (subcommand, window targeting flags, output + /// format flags). + /// - Returns: The ``MultiWindowRouteResult`` describing how the launched + /// CLI call finished. + /// - Throws: ``MultiWindowRouteLaunchError`` when the CLI process could + /// not be launched; a launched CLI never throws, its outcome (including + /// non-zero exit) is the returned result. + func route(arguments: [String]) async throws -> MultiWindowRouteResult +} + +extension MultiWindowRouting { + /// Routes one CLI call, encoding a launch failure into the result instead + /// of throwing. + /// + /// This is the legacy capture encoding the pre-extraction AppDelegate + /// helper produced and the multi-window UI-test data file still expects: + /// a CLI that never launched yields termination status `-1` with the + /// launch error's description in `stderr` (byte-identical to the old + /// `String(describing:)` text via ``MultiWindowRouteLaunchError``'s + /// `CustomStringConvertible`). Use it when every call in a batch must run + /// regardless of earlier launch failures. + /// + /// - Parameter arguments: The CLI arguments after the implicit + /// `--socket ` pair. + /// - Returns: The route result, with launch failure folded in as + /// termination status `-1`. + public func routeCapturingLaunchFailure(arguments: [String]) async -> MultiWindowRouteResult { + do { + return try await route(arguments: arguments) + } catch { + return MultiWindowRouteResult( + terminationStatus: -1, + stdout: "", + stderr: String(describing: error) + ) + } + } +} diff --git a/Packages/CmuxIPCService/Tests/CmuxIPCServiceTests/MultiWindowRouterTests.swift b/Packages/CmuxIPCService/Tests/CmuxIPCServiceTests/MultiWindowRouterTests.swift new file mode 100644 index 000000000000..0b550ba06615 --- /dev/null +++ b/Packages/CmuxIPCService/Tests/CmuxIPCServiceTests/MultiWindowRouterTests.swift @@ -0,0 +1,122 @@ +import Foundation +import Testing + +@testable import CmuxIPCService + +@Suite struct MultiWindowRouterTests { + /// Writes an executable `/bin/sh` script into a temp directory and returns + /// its URL, so each test exercises the real spawn/capture path against a + /// controlled CLI stand-in. + private func makeScript(_ body: String) throws -> URL { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent("CmuxIPCServiceTests-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + let url = directory.appendingPathComponent("fake-cmux") + try "#!/bin/sh\n\(body)\n".write(to: url, atomically: true, encoding: .utf8) + try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: url.path) + return url + } + + @Test func prependsSocketFlagAndForwardsArguments() async throws { + let script = try makeScript(#"printf '%s\n' "$@""#) + let router = MultiWindowRouter( + cliURL: script, + socketPath: "/tmp/route.sock", + environment: [:] + ) + let result = try await router.route(arguments: ["list-workspaces", "--window", "ABC"]) + #expect(result.terminationStatus == 0) + #expect(result.stdout == "--socket\n/tmp/route.sock\nlist-workspaces\n--window\nABC\n") + #expect(result.stderr == "") + } + + @Test func capturesStderrAndNonZeroExitStatus() async throws { + let script = try makeScript("echo oops 1>&2; exit 3") + let router = MultiWindowRouter( + cliURL: script, + socketPath: "/tmp/route.sock", + environment: [:] + ) + let result = try await router.route(arguments: []) + #expect(result.terminationStatus == 3) + #expect(result.stdout == "") + #expect(result.stderr == "oops\n") + } + + @Test func childEnvironmentIsExactlyTheInjectedOne() async throws { + // The legacy code sets `process.environment` wholesale: injected keys + // are visible and the parent's environment is NOT inherited. + let script = try makeScript(#"printf '%s|%s' "$CMUX_TEST_MARKER" "${HOME:-unset}""#) + let router = MultiWindowRouter( + cliURL: script, + socketPath: "/tmp/route.sock", + environment: ["CMUX_TEST_MARKER": "marker-value"] + ) + let result = try await router.route(arguments: []) + #expect(result.terminationStatus == 0) + #expect(result.stdout == "marker-value|unset") + } + + @Test func throwsLaunchErrorPreservingUnderlyingDescription() async { + let router = MultiWindowRouter( + cliURL: URL(fileURLWithPath: "/nonexistent/cmux-cli-\(UUID().uuidString)"), + socketPath: "/tmp/route.sock", + environment: [:] + ) + do { + _ = try await router.route(arguments: ["ping"]) + Issue.record("route should throw when the CLI cannot launch") + } catch let error as MultiWindowRouteLaunchError { + #expect(!error.description.isEmpty) + // The legacy capture writes String(describing: error); the launch + // error renders as its preserved underlying description so that + // encoding stays byte-identical. + #expect(String(describing: error) == error.description) + } catch { + Issue.record("unexpected error type: \(error)") + } + } + + @Test func captureEncodingFoldsLaunchFailureIntoLegacyResult() async { + // The UI-test capture path must keep running later calls after an + // earlier launch failure, so the convenience folds the throw into the + // legacy "-1" encoding instead of propagating it. + let router = MultiWindowRouter( + cliURL: URL(fileURLWithPath: "/nonexistent/cmux-cli-\(UUID().uuidString)"), + socketPath: "/tmp/route.sock", + environment: [:] + ) + let result = await router.routeCapturingLaunchFailure(arguments: ["ping"]) + #expect(result.terminationStatus == -1) + #expect(result.stdout == "") + #expect(!result.stderr.isEmpty) + } + + @Test func nonUTF8OutputCollapsesToEmptyString() async throws { + let script = try makeScript(#"printf '\377\376'"#) + let router = MultiWindowRouter( + cliURL: script, + socketPath: "/tmp/route.sock", + environment: [:] + ) + let result = try await router.route(arguments: []) + #expect(result.terminationStatus == 0) + #expect(result.stdout == "") + } + + @Test func outputLargerThanPipeBufferDoesNotDeadlock() async throws { + // 256 KiB of stdout exceeds the 64 KiB pipe buffer; the concurrent + // stream readers must drain it while the child runs, otherwise the + // child blocks on a full pipe and never exits. + let script = try makeScript("/usr/bin/head -c 262144 /dev/zero | /usr/bin/tr '\\0' 'a'") + let router = MultiWindowRouter( + cliURL: script, + socketPath: "/tmp/route.sock", + environment: [:] + ) + let result = try await router.route(arguments: []) + #expect(result.terminationStatus == 0) + #expect(result.stdout.count == 262_144) + #expect(result.stdout.allSatisfy { $0 == "a" }) + } +} diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 7c1a39b56bab..8c3b739f7d15 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -1,6 +1,7 @@ import AppKit import CmuxAuthRuntime import CmuxControlSocket +import CmuxIPCService import CmuxSettings import CmuxSettingsUI import CmuxSocketControl @@ -18,44 +19,6 @@ import ObjectiveC.runtime import Darwin import CmuxFoundation -private struct MultiWindowRouteCLIResult { - let status: String - let stdout: String - let stderr: String -} - -private func runMultiWindowRouteCLI( - cliURL: URL, - socketPath: String, - processEnv: [String: String], - arguments: [String] -) -> MultiWindowRouteCLIResult { - let process = Process() - process.executableURL = cliURL - process.arguments = ["--socket", socketPath] + arguments - process.environment = processEnv - - let stdoutPipe = Pipe() - let stderrPipe = Pipe() - process.standardOutput = stdoutPipe - process.standardError = stderrPipe - - do { - try process.run() - } catch { - return MultiWindowRouteCLIResult(status: "-1", stdout: "", stderr: String(describing: error)) - } - process.waitUntilExit() - - let stdoutData = ProcessPipeReader.readDataToEndOfFileOrEmpty(from: stdoutPipe.fileHandleForReading) - let stderrData = ProcessPipeReader.readDataToEndOfFileOrEmpty(from: stderrPipe.fileHandleForReading) - return MultiWindowRouteCLIResult( - status: String(process.terminationStatus), - stdout: String(data: stdoutData, encoding: .utf8) ?? "", - stderr: String(data: stderrData, encoding: .utf8) ?? "" - ) -} - /// Caches `AXWindows` responses so repeated AX polls can reuse the same /// snapshot while the app window graph is unchanged. Only `.windows` is /// cached; `.children` and `.visibleChildren` fall through to AppKit so the @@ -11655,11 +11618,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return } - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - let create = runMultiWindowRouteCLI( - cliURL: cliURL, - socketPath: socketPath, - processEnv: processEnv, + let router = MultiWindowRouter( + cliURL: cliURL, + socketPath: socketPath, + environment: processEnv + ) + // Inherits MainActor; each await runs the CLI off-main and the final + // write lands back on main, matching the legacy queue hops. + Task(priority: .userInitiated) { [weak self] in + let create = await router.routeCapturingLaunchFailure( arguments: [ "new-workspace", "--window", @@ -11670,10 +11637,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent "false", ] ) - let window2List = runMultiWindowRouteCLI( - cliURL: cliURL, - socketPath: socketPath, - processEnv: processEnv, + let window2List = await router.routeCapturingLaunchFailure( arguments: [ "--json", "--id-format", @@ -11683,10 +11647,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent window2Id.uuidString, ] ) - let window1List = runMultiWindowRouteCLI( - cliURL: cliURL, - socketPath: socketPath, - processEnv: processEnv, + let window1List = await router.routeCapturingLaunchFailure( arguments: [ "--json", "--id-format", @@ -11697,20 +11658,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent ] ) - DispatchQueue.main.async { - self?.writeMultiWindowNotificationTestData([ - "windowRouteStatus": "1", - "windowRouteCreateStatus": create.status, - "windowRouteCreateStdout": create.stdout, - "windowRouteCreateStderr": create.stderr, - "windowRouteWindow2Status": window2List.status, - "windowRouteWindow2Stdout": window2List.stdout, - "windowRouteWindow2Stderr": window2List.stderr, - "windowRouteWindow1Status": window1List.status, - "windowRouteWindow1Stdout": window1List.stdout, - "windowRouteWindow1Stderr": window1List.stderr, - ], at: path) - } + self?.writeMultiWindowNotificationTestData([ + "windowRouteStatus": "1", + "windowRouteCreateStatus": String(create.terminationStatus), + "windowRouteCreateStdout": create.stdout, + "windowRouteCreateStderr": create.stderr, + "windowRouteWindow2Status": String(window2List.terminationStatus), + "windowRouteWindow2Stdout": window2List.stdout, + "windowRouteWindow2Stderr": window2List.stderr, + "windowRouteWindow1Status": String(window1List.terminationStatus), + "windowRouteWindow1Stdout": window1List.stdout, + "windowRouteWindow1Stderr": window1List.stderr, + ], at: path) } } diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index c168c6b8ad13..d9a8c9455fe7 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -200,6 +200,7 @@ C0DE34020000000000000001 /* CmuxHelpCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE34020000000000000003 /* CmuxHelpCommands.swift */; }; C0DE34020000000000000002 /* CmuxHelpResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE34020000000000000004 /* CmuxHelpResource.swift */; }; C0DE46010000000000000001 /* CMUXInstalledExtensionSidebarHostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE46010000000000000002 /* CMUXInstalledExtensionSidebarHostView.swift */; }; + C19C5E0300000000000000A3 /* CmuxIPCService in Frameworks */ = {isa = PBXBuildFile; productRef = C19C5E0200000000000000A2 /* CmuxIPCService */; }; E7E00000000000000000000B /* CmuxLifecycleEventPublishing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7E00000000000000000000C /* CmuxLifecycleEventPublishing.swift */; }; 2F0C07000000000000000002 /* CmuxMainWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F0C07000000000000000001 /* CmuxMainWindow.swift */; }; 799F1BEF876006EEB8CD1035 /* CMUXMobileCore in Frameworks */ = {isa = PBXBuildFile; productRef = EFB18E3B3099DFE2ECA3C263 /* CMUXMobileCore */; }; @@ -1616,6 +1617,7 @@ C8000313C8000313C8000313 /* CmuxFileWatch in Frameworks */, CF00F00000000000000000A3 /* CmuxFoundation in Frameworks */, C617000000000000000000A3 /* CmuxGit in Frameworks */, + C19C5E0300000000000000A3 /* CmuxIPCService in Frameworks */, 799F1BEF876006EEB8CD1035 /* CMUXMobileCore in Frameworks */, 3069F1D10000000000000005 /* CMUXPasteboardFidelity in Frameworks */, C8000303C8000303C8000303 /* CmuxProcess in Frameworks */, @@ -2563,6 +2565,7 @@ C5A1FED100000000000000D3 /* CmuxSwiftRenderUI */, C5A1FED200000000000000E3 /* CmuxSidebarInterpreterClient */, C5A1FED200000000000000E6 /* CmuxSidebarRemoteRender */, + C19C5E0200000000000000A2 /* CmuxIPCService */, C8000302C8000302C8000302 /* CmuxProcess */, C8000312C8000312C8000312 /* CmuxFileWatch */, C617000000000000000000A2 /* CmuxGit */, @@ -2727,6 +2730,7 @@ C5A1FED000000000000000C4 /* XCLocalSwiftPackageReference "CmuxSwiftRender" */, C5A1FED100000000000000D4 /* XCLocalSwiftPackageReference "CmuxSwiftRenderUI" */, C5A1FED200000000000000E4 /* XCLocalSwiftPackageReference "CmuxSidebarInterpreterService" */, + C19C5E0100000000000000A1 /* XCLocalSwiftPackageReference "CmuxIPCService" */, C8000301C8000301C8000301 /* XCLocalSwiftPackageReference "CmuxProcess" */, C8000311C8000311C8000311 /* XCLocalSwiftPackageReference "CmuxFileWatch" */, C617000000000000000000A1 /* XCLocalSwiftPackageReference "CmuxGit" */, @@ -4128,6 +4132,10 @@ isa = XCLocalSwiftPackageReference; relativePath = Packages/CmuxSwiftRender; }; + C19C5E0100000000000000A1 /* XCLocalSwiftPackageReference "CmuxIPCService" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Packages/CmuxIPCService; + }; C8000301C8000301C8000301 /* XCLocalSwiftPackageReference "CmuxProcess" */ = { isa = XCLocalSwiftPackageReference; relativePath = Packages/CmuxProcess; @@ -4324,6 +4332,11 @@ package = C750100000000000000000A1 /* XCLocalSwiftPackageReference "CmuxControlSocket" */; productName = CmuxControlSocket; }; + C19C5E0200000000000000A2 /* CmuxIPCService */ = { + isa = XCSwiftPackageProductDependency; + package = C19C5E0100000000000000A1 /* XCLocalSwiftPackageReference "CmuxIPCService" */; + productName = CmuxIPCService; + }; C750200000000000000000A2 /* CmuxTerminalCore */ = { isa = XCSwiftPackageProductDependency; package = C750200000000000000000A1 /* XCLocalSwiftPackageReference "CmuxTerminalCore" */;