Prechádzať zdrojové kódy

vm(qemu): implement starting with remote SPICE client

osy 1 rok pred
rodič
commit
894a6911a5

+ 64 - 9
Configuration/UTMQemuConfiguration+Arguments.swift

@@ -61,6 +61,16 @@ import Virtualization // for getting network interfaces
         socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("swtpm")
     }
     
+    /// Used only if in remote sever mode.
+    var monitorPipeURL: URL {
+        socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("qmp")
+    }
+
+    /// Used only if in remote sever mode.
+    var guestAgentPipeURL: URL {
+        socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("qga")
+    }
+
     /// Combined generated and user specified arguments.
     @QEMUArgumentBuilder var allArguments: [QEMUArgument] {
         generatedArguments
@@ -109,16 +119,29 @@ import Virtualization // for getting network interfaces
     
     @QEMUArgumentBuilder private var spiceArguments: [QEMUArgument] {
         f("-spice")
-        "unix=on"
-        "addr=\(spiceSocketURL.lastPathComponent)"
+        if let port = qemu.spiceServerPort {
+            "port=\(port)"
+        } else {
+            "unix=on"
+            "addr=\(spiceSocketURL.lastPathComponent)"
+        }
         "disable-ticketing=on"
         "image-compression=off"
         "playback-compression=off"
         "streaming-video=off"
-        "gl=\(isGLOn ? "on" : "off")"
+        "gl=\(isGLSupported && !isRemoteSpice ? "on" : "off")"
         f()
         f("-chardev")
-        f("spiceport,id=org.qemu.monitor.qmp,name=org.qemu.monitor.qmp.0")
+        if isRemoteSpice {
+            "pipe"
+            "path="
+            monitorPipeURL
+        } else {
+            "spiceport"
+            "name=org.qemu.monitor.qmp.0"
+        }
+        "id=org.qemu.monitor.qmp"
+        f()
         f("-mon")
         f("chardev=org.qemu.monitor.qmp,mode=control")
         if !isSparc { // disable -vga and other default devices
@@ -129,7 +152,22 @@ import Virtualization // for getting network interfaces
             f("none")
         }
     }
-    
+
+    private func filterDisplayIfRemote(_ display: any QEMUDisplayDevice) -> any QEMUDisplayDevice {
+        if isRemoteSpice {
+            let rawValue = display.rawValue
+            if rawValue.hasSuffix("-gl") {
+                return AnyQEMUConstant(rawValue: String(rawValue.dropLast(3)))!
+            } else if rawValue.contains("-gl-") {
+                return AnyQEMUConstant(rawValue: String(rawValue.replacingOccurrences(of: "-gl-", with: "")))!
+            } else {
+                return display
+            }
+        } else {
+            return display
+        }
+    }
+
     @QEMUArgumentBuilder private var displayArguments: [QEMUArgument] {
         if displays.isEmpty {
             f("-nographic")
@@ -143,7 +181,7 @@ import Virtualization // for getting network interfaces
         } else {
             for display in displays {
                 f("-device")
-                display.hardware
+                filterDisplayIfRemote(display.hardware)
                 if let vgaRamSize = displays[0].vgaRamMib {
                     "vgamem_mb=\(vgaRamSize)"
                 }
@@ -152,7 +190,7 @@ import Virtualization // for getting network interfaces
         }
     }
     
-    private var isGLOn: Bool {
+    private var isGLSupported: Bool {
         displays.contains { display in
             display.hardware.rawValue.contains("-gl-") || display.hardware.rawValue.hasSuffix("-gl")
         }
@@ -161,7 +199,11 @@ import Virtualization // for getting network interfaces
     private var isSparc: Bool {
         system.architecture == .sparc || system.architecture == .sparc64
     }
-    
+
+    private var isRemoteSpice: Bool {
+        qemu.spiceServerPort != nil
+    }
+
     @QEMUArgumentBuilder private var serialArguments: [QEMUArgument] {
         for i in serials.indices {
             f("-chardev")
@@ -433,6 +475,10 @@ import Virtualization // for getting network interfaces
         #if os(iOS) || os(visionOS)
         return false
         #else
+        // only support SPICE audio if we are running remotely
+        if isRemoteSpice {
+            return false
+        }
         // force CoreAudio backend for mac99 which only supports 44100 Hz
         // pcspk doesn't work with SPICE audio
         if sound.contains(where: { $0.hardware.rawValue == "screamer" || $0.hardware.rawValue == "pcspk" }) {
@@ -859,7 +905,16 @@ import Virtualization // for getting network interfaces
             f("-device")
             f("virtserialport,chardev=org.qemu.guest_agent,name=org.qemu.guest_agent.0")
             f("-chardev")
-            f("spiceport,id=org.qemu.guest_agent,name=org.qemu.guest_agent.0")
+            if isRemoteSpice {
+                "pipe"
+                "path="
+                guestAgentPipeURL
+            } else {
+                "spiceport"
+                "name=org.qemu.guest_agent.0"
+            }
+            "id=org.qemu.guest_agent"
+            f()
         }
         if isSpiceAgentUsed {
             f("-device")

+ 4 - 0
Platform/macOS/macOS-unsigned.entitlements

@@ -4,6 +4,10 @@
 <dict>
 	<key>com.apple.security.app-sandbox</key>
 	<true/>
+	<key>com.apple.security.application-groups</key>
+	<array>
+		<string>$(TeamIdentifierPrefix)$(PRODUCT_BUNDLE_PREFIX:default=com.utmapp).UTM</string>
+	</array>
 	<key>com.apple.security.cs.disable-library-validation</key>
 	<true/>
 	<key>com.apple.security.device.audio-input</key>

+ 152 - 0
Services/UTMPipeInterface.swift

@@ -0,0 +1,152 @@
+//
+// Copyright © 2024 osy. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import QEMUKit
+
+class UTMPipeInterface: NSObject, QEMUInterface {
+    weak var connectDelegate: QEMUInterfaceConnectDelegate?
+
+    var monitorOutPipeURL: URL!
+    var monitorInPipeURL: URL!
+    var guestAgentOutPipeURL: URL!
+    var guestAgentInPipeURL: URL!
+
+    private var pipeIOQueue = DispatchQueue(label: "UTMPipeInterface")
+    private var qemuMonitorPort: Port!
+    private var qemuGuestAgentPort: Port!
+
+    func start() throws {
+        try initializePipe(at: monitorOutPipeURL)
+        try initializePipe(at: monitorInPipeURL)
+        try initializePipe(at: guestAgentOutPipeURL)
+        try initializePipe(at: guestAgentInPipeURL)
+    }
+
+    func connect() throws {
+        pipeIOQueue.async { [self] in
+            do {
+                try openQemuPipes()
+                connectDelegate?.qemuInterface(self, didCreateMonitorPort: qemuMonitorPort)
+                connectDelegate?.qemuInterface(self, didCreateGuestAgentPort: qemuGuestAgentPort)
+            } catch {
+                connectDelegate?.qemuInterface(self, didErrorWithMessage: error.localizedDescription)
+            }
+        }
+    }
+
+    func disconnect() {
+        cleanupPipes()
+    }
+}
+
+extension UTMPipeInterface {
+    class Port: NSObject, QEMUPort {
+        let readPipe: FileHandle
+
+        let writePipe: FileHandle
+
+        var readDataHandler: readDataHandler_t?
+
+        var errorHandler: errorHandler_t?
+
+        var disconnectHandler: disconnectHandler_t?
+
+        let isOpen: Bool = true
+
+        init(readPipe: FileHandle, writePipe: FileHandle) {
+            self.readPipe = readPipe
+            self.writePipe = writePipe
+            super.init()
+            readPipe.readabilityHandler = { fileHandle in
+                self.readDataHandler?(fileHandle.availableData)
+            }
+        }
+
+        func write(_ data: Data) {
+            writePipe.write(data)
+        }
+    }
+
+    private var fileManager: FileManager {
+        FileManager.default
+    }
+
+    private func initializePipe(at url: URL) throws {
+        if fileManager.fileExists(atPath: url.path) {
+            try fileManager.removeItem(at: url)
+        }
+        guard mkfifo(url.path, S_IRUSR | S_IWUSR) == 0 else {
+            throw ServerError.failedToCreatePipe(errno)
+        }
+    }
+
+    private func openPipe(at url: URL, forReading isRead: Bool) throws -> FileHandle {
+        let fileHandle: FileHandle
+        if isRead {
+            fileHandle = try FileHandle(forReadingFrom: url)
+        } else {
+            fileHandle = try FileHandle(forWritingTo: url)
+        }
+        return fileHandle
+    }
+
+    private func cleanupPipes() {
+        // unblock any un-opened pipes
+        _ = try? openPipe(at: monitorOutPipeURL, forReading: false)
+        _ = try? openPipe(at: monitorInPipeURL, forReading: true)
+        _ = try? openPipe(at: guestAgentOutPipeURL, forReading: false)
+        _ = try? openPipe(at: guestAgentInPipeURL, forReading: true)
+        pipeIOQueue.sync {
+            if let monitorOutPipeURL = monitorOutPipeURL {
+                try? fileManager.removeItem(at: monitorOutPipeURL)
+            }
+            if let monitorInPipeURL = monitorInPipeURL {
+                try? fileManager.removeItem(at: monitorInPipeURL)
+            }
+            if let guestAgentOutPipeURL = guestAgentOutPipeURL {
+                try? fileManager.removeItem(at: guestAgentOutPipeURL)
+            }
+            if let guestAgentInPipeURL = guestAgentInPipeURL {
+                try? fileManager.removeItem(at: guestAgentInPipeURL)
+            }
+            qemuMonitorPort = nil
+            qemuGuestAgentPort = nil
+        }
+    }
+
+    private func openQemuPipes() throws {
+        let qmpReadPipe = try openPipe(at: monitorOutPipeURL, forReading: true)
+        let qmpWritePipe = try openPipe(at: monitorInPipeURL, forReading: false)
+        qemuMonitorPort = Port(readPipe: qmpReadPipe, writePipe: qmpWritePipe)
+        let qgaReadPipe = try openPipe(at: guestAgentOutPipeURL, forReading: true)
+        let qgaWritePipe = try openPipe(at: guestAgentInPipeURL, forReading: false)
+        qemuGuestAgentPort = Port(readPipe: qgaReadPipe, writePipe: qgaWritePipe)
+    }
+}
+
+extension UTMPipeInterface {
+    enum ServerError: LocalizedError {
+        case failedToCreatePipe(Int32)
+
+        var errorDescription: String? {
+            switch self {
+            case .failedToCreatePipe(_):
+                return NSLocalizedString("Failed to create pipe for communications.", comment: "UTMPipeInterface")
+            }
+        }
+    }
+}

+ 41 - 14
Services/UTMQemuVirtualMachine.swift

@@ -121,6 +121,9 @@ final class UTMQemuVirtualMachine: UTMSpiceVirtualMachine {
         }
     }
     
+    /// Pipe interface (alternative to UTMSpiceIO)
+    private var pipeInterface: UTMPipeInterface?
+
     private let qemuVM = QEMUVirtualMachine()
     
     private var system: UTMQemuSystem? {
@@ -271,10 +274,13 @@ extension UTMQemuVirtualMachine {
             await qemuVM.setRedirectLog(url: nil)
         }
         let isRunningAsDisposible = options.contains(.bootDisposibleMode)
+        let isRemoteSession = options.contains(.remoteSession)
+        let spicePort = isRemoteSession ? try UTMSocketUtils.reservePort() : nil
         await MainActor.run {
             config.qemu.isDisposable = isRunningAsDisposible
+            config.qemu.spiceServerPort = spicePort
         }
-        
+
         // start TPM
         if await config.qemu.hasTPMDevice {
             let swtpm = UTMSWTPM()
@@ -284,12 +290,12 @@ extension UTMQemuVirtualMachine {
             try await swtpm.start()
             self.swtpm = swtpm
         }
-        
+
         let allArguments = await config.allArguments
         let arguments = allArguments.map({ $0.string })
         let resources = allArguments.compactMap({ $0.fileUrls }).flatMap({ $0 })
         let remoteBookmarks = await remoteBookmarks
-        
+
         let system = await UTMQemuSystem(arguments: arguments, architecture: config.system.architecture.rawValue)
         system.resources = resources
         system.currentDirectoryUrl = await config.socketURL
@@ -299,12 +305,12 @@ extension UTMQemuVirtualMachine {
         system.hasDebugLog = hasDebugLog
         #endif
         try Task.checkCancellation()
-        
+
         if isShortcut {
             try await accessShortcut()
             try Task.checkCancellation()
         }
-        
+
         var options = UTMSpiceIOOptions()
         if await !config.sound.isEmpty {
             options.insert(.hasAudio)
@@ -321,14 +327,28 @@ extension UTMQemuVirtualMachine {
         }
         #endif
         let spiceSocketUrl = await config.spiceSocketURL
-        let ioService = UTMSpiceIO(socketUrl: spiceSocketUrl, options: options)
-        ioService.logHandler = { [weak system] (line: String) -> Void in
-            guard !line.contains("spice_make_scancode") else {
-                return // do not log key presses for privacy reasons
+        let interface: any QEMUInterface
+        if isRemoteSession {
+            let pipeInterface = UTMPipeInterface()
+            await MainActor.run {
+                pipeInterface.monitorInPipeURL = config.monitorPipeURL.appendingPathExtension("in")
+                pipeInterface.monitorOutPipeURL = config.monitorPipeURL.appendingPathExtension("out")
+                pipeInterface.guestAgentInPipeURL = config.guestAgentPipeURL.appendingPathExtension("in")
+                pipeInterface.guestAgentOutPipeURL = config.guestAgentPipeURL.appendingPathExtension("out")
             }
-            system?.logging?.writeLine(line)
+            try pipeInterface.start()
+            interface = pipeInterface
+        } else {
+            let ioService = UTMSpiceIO(socketUrl: spiceSocketUrl, options: options)
+            ioService.logHandler = { [weak system] (line: String) -> Void in
+                guard !line.contains("spice_make_scancode") else {
+                    return // do not log key presses for privacy reasons
+                }
+                system?.logging?.writeLine(line)
+            }
+            try ioService.start()
+            interface = ioService
         }
-        try ioService.start()
         try Task.checkCancellation()
         
         // create EFI variables for legacy config as well as handle UEFI resets
@@ -337,7 +357,7 @@ extension UTMQemuVirtualMachine {
         
         // start QEMU
         await qemuVM.setDelegate(self)
-        try await qemuVM.start(launcher: system, interface: ioService)
+        try await qemuVM.start(launcher: system, interface: interface)
         let monitor = await monitor!
         try Task.checkCancellation()
         
@@ -350,7 +370,11 @@ extension UTMQemuVirtualMachine {
         
         // set up SPICE sharing and removable drives
         try await self.restoreExternalDrives(withMounting: !isSuspended)
-        try await self.restoreSharedDirectory(for: ioService)
+        if let ioService = interface as? UTMSpiceIO {
+            try await self.restoreSharedDirectory(for: ioService)
+        } else {
+            // TODO: implement shared directory in remote interface
+        }
         try Task.checkCancellation()
         
         // continue VM boot
@@ -362,7 +386,8 @@ extension UTMQemuVirtualMachine {
         }
         
         // save ioService and let it set the delegate
-        self.ioService = ioService
+        self.ioService = interface as? UTMSpiceIO
+        self.pipeInterface = interface as? UTMPipeInterface
         self.isRunningAsDisposible = isRunningAsDisposible
         
         // test out snapshots
@@ -592,6 +617,8 @@ extension UTMQemuVirtualMachine: QEMUVirtualMachineDelegate {
         swtpm = nil
         ioService = nil
         ioServiceDelegate = nil
+        pipeInterface?.disconnect()
+        pipeInterface = nil
         snapshotUnsupportedError = nil
         try? saveScreenshot()
         state = .stopped

+ 96 - 0
Services/UTMSocketUtils.swift

@@ -0,0 +1,96 @@
+//
+// Copyright © 2024 osy. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Darwin
+
+struct UTMSocketUtils {
+    /// Reserve an ephemeral port from the system
+    ///
+    /// First we `bind` to port 0 in order to allocate an ephemeral port.
+    /// Next, we `connect` to that port to establish a connection.
+    /// Finally, we close the port and put it into the `TIME_WAIT` state.
+    ///
+    /// This allows another process to `bind` the port with `SO_REUSEADDR` specified.
+    /// However, for the next ~120 seconds, the system will not re-use this port.
+    /// - Returns: A port number that is valid for ~120 seconds.
+    static func reservePort() throws -> UInt16 {
+        let serverSock = socket(AF_INET, SOCK_STREAM, 0)
+        guard serverSock >= 0 else {
+            throw SocketError.cannotReservePort(errno)
+        }
+        defer {
+            close(serverSock)
+        }
+        var addr = sockaddr_in()
+        addr.sin_family = sa_family_t(AF_INET)
+        addr.sin_addr.s_addr = INADDR_ANY
+        addr.sin_port = 0 // request an ephemeral port
+
+        var len = socklen_t(MemoryLayout<sockaddr_in>.stride)
+        let res = withUnsafeMutablePointer(to: &addr) {
+            $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
+                let res1 = bind(serverSock, $0, len)
+                let res2 = getsockname(serverSock, $0, &len)
+                return (res1, res2)
+            }
+        }
+        guard res.0 == 0 && res.1 == 0 else {
+            throw SocketError.cannotReservePort(errno)
+        }
+
+        guard listen(serverSock, 1) == 0 else {
+            throw SocketError.cannotReservePort(errno)
+        }
+
+        let clientSock = socket(AF_INET, SOCK_STREAM, 0)
+        guard clientSock >= 0 else {
+            throw SocketError.cannotReservePort(errno)
+        }
+        defer {
+            close(clientSock)
+        }
+        let res3 = withUnsafeMutablePointer(to: &addr) {
+            $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
+                connect(clientSock, $0, len)
+            }
+        }
+        guard res3 == 0 else {
+            throw SocketError.cannotReservePort(errno)
+        }
+
+        let acceptSock = accept(serverSock, nil, nil)
+        guard acceptSock >= 0 else {
+            throw SocketError.cannotReservePort(errno)
+        }
+        defer {
+            close(acceptSock)
+        }
+        return addr.sin_port.byteSwapped
+    }
+}
+
+extension UTMSocketUtils {
+    enum SocketError: LocalizedError {
+        case cannotReservePort(Int32)
+
+        var errorDescription: String? {
+            switch self {
+            case .cannotReservePort(_):
+                return NSLocalizedString("Cannot reserve an unused port on this system.", comment: "UTMSocketUtils")
+            }
+        }
+    }
+}

+ 20 - 0
UTM.xcodeproj/project.pbxproj

@@ -897,6 +897,10 @@
 		CEF01DB32B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */; };
 		CEF01DB42B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */; };
 		CEF01DB52B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */; };
+		CEF01DB72B674BF000725A0F /* UTMPipeInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */; };
+		CEF01DB82B674BF000725A0F /* UTMPipeInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */; };
+		CEF01DB92B674BF000725A0F /* UTMPipeInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */; };
+		CEF01DCD2B67985100725A0F /* UTMPipeInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */; };
 		CEF0300826A25A6900667B63 /* VMWizardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0300526A25A6900667B63 /* VMWizardView.swift */; };
 		CEF0304E26A2AFBE00667B63 /* BigButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0304C26A2AFBE00667B63 /* BigButtonStyle.swift */; };
 		CEF0304F26A2AFBF00667B63 /* BigButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0304C26A2AFBE00667B63 /* BigButtonStyle.swift */; };
@@ -1207,6 +1211,10 @@
 		CEF83F8D250094E700557D15 /* gthread-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DC22653C7300FC7E63 /* gthread-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CEF83F8E250094EC00557D15 /* gpg-error.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F122653C7400FC7E63 /* gpg-error.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CEF83F8F250094EE00557D15 /* gcrypt.20.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F322653C7400FC7E63 /* gcrypt.20.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+		CEFE96722B699954000F00C9 /* UTMSocketUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE96712B699954000F00C9 /* UTMSocketUtils.swift */; };
+		CEFE96732B699954000F00C9 /* UTMSocketUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE96712B699954000F00C9 /* UTMSocketUtils.swift */; };
+		CEFE96742B699954000F00C9 /* UTMSocketUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE96712B699954000F00C9 /* UTMSocketUtils.swift */; };
+		CEFE96752B699954000F00C9 /* UTMSocketUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE96712B699954000F00C9 /* UTMSocketUtils.swift */; };
 		CEFE96772B69A7CC000F00C9 /* VMRemoteSessionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE96762B69A7CC000F00C9 /* VMRemoteSessionState.swift */; };
 		CEFE98DF29485237007CB7A8 /* UTM.sdef in Resources */ = {isa = PBXBuildFile; fileRef = CEFE98DE29485237007CB7A8 /* UTM.sdef */; };
 		CEFE98E129485776007CB7A8 /* UTMScriptingVirtualMachineImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE98E029485776007CB7A8 /* UTMScriptingVirtualMachineImpl.swift */; };
@@ -2011,6 +2019,7 @@
 		CEEC811A24E48EC600ACB0B3 /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
 		CEECE13B25E47D9500A2AAB8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
 		CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMSpiceVirtualMachine.swift; sourceTree = "<group>"; };
+		CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMPipeInterface.swift; sourceTree = "<group>"; };
 		CEF0300526A25A6900667B63 /* VMWizardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMWizardView.swift; sourceTree = "<group>"; };
 		CEF0304C26A2AFBE00667B63 /* BigButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BigButtonStyle.swift; sourceTree = "<group>"; };
 		CEF0304D26A2AFBE00667B63 /* Spinner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Spinner.swift; sourceTree = "<group>"; };
@@ -2028,6 +2037,7 @@
 		CEF6F5EC26DDD65700BC434D /* QEMULauncher-unsigned.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "QEMULauncher-unsigned.entitlements"; sourceTree = "<group>"; };
 		CEF7F6D32AEEDCC400E34952 /* UTM Remote.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "UTM Remote.app"; sourceTree = BUILT_PRODUCTS_DIR; };
 		CEF84ADA2887D7D300578F41 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
+		CEFE96712B699954000F00C9 /* UTMSocketUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMSocketUtils.swift; sourceTree = "<group>"; };
 		CEFE96762B69A7CC000F00C9 /* VMRemoteSessionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMRemoteSessionState.swift; sourceTree = "<group>"; };
 		CEFE98DE29485237007CB7A8 /* UTM.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = UTM.sdef; sourceTree = "<group>"; };
 		CEFE98E029485776007CB7A8 /* UTMScriptingVirtualMachineImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingVirtualMachineImpl.swift; sourceTree = "<group>"; };
@@ -2766,6 +2776,7 @@
 				CE6EDCE1241DA0E900A719DC /* UTMLogging.m */,
 				CE020BAA24AEE00000B44AB6 /* UTMLoggingSwift.swift */,
 				CEDF83F8258AE24E0030E4AC /* UTMPasteboard.swift */,
+				CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */,
 				CE9D197A226542FE00355E14 /* UTMProcess.h */,
 				CE9D197B226542FE00355E14 /* UTMProcess.m */,
 				8453DCB3278CE5410037A0DA /* UTMQemuImage.swift */,
@@ -2776,6 +2787,7 @@
 				841E997828AA119B003C6CB6 /* UTMRegistryEntry.swift */,
 				848F71E7277A2A4E006A0240 /* UTMSerialPort.swift */,
 				848F71EB277A2F47006A0240 /* UTMSerialPortDelegate.swift */,
+				CEFE96712B699954000F00C9 /* UTMSocketUtils.swift */,
 				E2D64BC7241DB24B0034E0C6 /* UTMSpiceIO.h */,
 				E2D64BC8241DB24B0034E0C6 /* UTMSpiceIO.m */,
 				E2D64BE0241EAEBE0034E0C6 /* UTMSpiceIODelegate.h */,
@@ -3510,6 +3522,7 @@
 				CE2D957524AD4F990059923A /* VMConfigInputView.swift in Sources */,
 				CEF0305B26A2AFDF00667B63 /* VMWizardOSOtherView.swift in Sources */,
 				84C60FB72681A41B00B58C00 /* VMToolbarView.swift in Sources */,
+				CEF01DB72B674BF000725A0F /* UTMPipeInterface.swift in Sources */,
 				CE2D92CB24AD46670059923A /* VMDisplayMetalViewController+Gamepad.m in Sources */,
 				CEF0307426A2B40B00667B63 /* VMWizardHardwareView.swift in Sources */,
 				841E997528AA1191003C6CB6 /* UTMRegistry.swift in Sources */,
@@ -3570,6 +3583,7 @@
 				8471770627CC974F00D3A50B /* DefaultTextField.swift in Sources */,
 				84E6F6FD289319AE00080EEF /* VMToolbarDisplayMenuView.swift in Sources */,
 				CE9B15472B12A87E003A32DD /* GenerateKey.c in Sources */,
+				CEFE96722B699954000F00C9 /* UTMSocketUtils.swift in Sources */,
 				CE8813D324CD230300532628 /* ActivityView.swift in Sources */,
 				CEDF83F9258AE24E0030E4AC /* UTMPasteboard.swift in Sources */,
 				848D99B4286300160055C215 /* QEMUArgument.swift in Sources */,
@@ -3592,6 +3606,7 @@
 			files = (
 				CEE06B272B2FC89400A811AE /* UTMServerView.swift in Sources */,
 				CEB63A7724F4654400CAF323 /* Main.swift in Sources */,
+				CEFE96752B699954000F00C9 /* UTMSocketUtils.swift in Sources */,
 				84E3A91B2946D2590024A740 /* UTMMenuBarExtraScene.swift in Sources */,
 				CEB63A7B24F469E300CAF323 /* UTMJailbreak.m in Sources */,
 				83A004BB26A8CC95001AC09E /* UTMDownloadTask.swift in Sources */,
@@ -3741,6 +3756,7 @@
 				848F71EE277A2F47006A0240 /* UTMSerialPortDelegate.swift in Sources */,
 				848A98BA286A17A8006F0550 /* UTMAppleConfigurationNetwork.swift in Sources */,
 				84018699288B71BF0050AC51 /* BusyIndicator.swift in Sources */,
+				CEF01DB92B674BF000725A0F /* UTMPipeInterface.swift in Sources */,
 				CEB54C852931E32F000D2AA9 /* UTMPatches.swift in Sources */,
 				84C2E8672AA429E800B17308 /* VMWizardContent.swift in Sources */,
 				848A98C0286A20E3006F0550 /* UTMAppleConfigurationBoot.swift in Sources */,
@@ -3880,6 +3896,7 @@
 				CEF01DB32B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */,
 				843232B828C4816100CFBC97 /* UTMDownloadSupportToolsTask.swift in Sources */,
 				CEF0306526A2AFDF00667B63 /* VMWizardOSLinuxView.swift in Sources */,
+				CEFE96732B699954000F00C9 /* UTMSocketUtils.swift in Sources */,
 				CEA45EC3263519B5002FA97D /* VMCommands.swift in Sources */,
 				84CE3DB22904C7A100FF068B /* UTMSettingsView.swift in Sources */,
 				8443EFF32845641600B2E6E2 /* UTMQemuConfigurationDrive.swift in Sources */,
@@ -3896,6 +3913,7 @@
 				CE19392726DCB094005CEC17 /* RAMSlider.swift in Sources */,
 				84E6F6FE289319AE00080EEF /* VMToolbarDisplayMenuView.swift in Sources */,
 				841E58CC28937EE200137A20 /* UTMExternalSceneDelegate.swift in Sources */,
+				CEF01DB82B674BF000725A0F /* UTMPipeInterface.swift in Sources */,
 				CEF0307226A2B04400667B63 /* VMWizardView.swift in Sources */,
 				83034C0826AB630F006B4BAF /* UTMPendingVMView.swift in Sources */,
 				CEA45ED8263519B5002FA97D /* VMKeyboardButton.m in Sources */,
@@ -3962,6 +3980,7 @@
 				CEE06B292B30013500A811AE /* UTMRemoteConnectView.swift in Sources */,
 				CEF7F59B2AEEDCC400E34952 /* UTMQemuConfigurationSharing.swift in Sources */,
 				CEF7F59C2AEEDCC400E34952 /* ContentView.swift in Sources */,
+				CEFE96742B699954000F00C9 /* UTMSocketUtils.swift in Sources */,
 				CEF7F59D2AEEDCC400E34952 /* VMData.swift in Sources */,
 				CEF7F59E2AEEDCC400E34952 /* UTMLegacyQemuConfiguration+System.m in Sources */,
 				CEF7F59F2AEEDCC400E34952 /* UTMQemuConfigurationNetwork.swift in Sources */,
@@ -4044,6 +4063,7 @@
 				CEF7F5EA2AEEDCC400E34952 /* VMConfigConstantPicker.swift in Sources */,
 				CEF7F5EB2AEEDCC400E34952 /* UTMQemuImage.swift in Sources */,
 				CEF7F5EC2AEEDCC400E34952 /* VMToolbarModifier.swift in Sources */,
+				CEF01DCD2B67985100725A0F /* UTMPipeInterface.swift in Sources */,
 				CEF7F5ED2AEEDCC400E34952 /* VMCursor.m in Sources */,
 				CEF7F5EE2AEEDCC400E34952 /* VMConfigDriveDetailsView.swift in Sources */,
 				CEF7F5EF2AEEDCC400E34952 /* UTMQemuSystem.m in Sources */,