mirror of
https://github.com/utmapp/UTM.git
synced 2026-10-12 02:24:01 +00:00
utmctl: start UTM if needed
This commit is contained in:
@@ -93,6 +93,7 @@ extension UTMAPI {
|
||||
|
||||
/// Error codes
|
||||
enum APIError: Error, LocalizedError {
|
||||
case serverNotFound
|
||||
case notConnected
|
||||
case invalidCommand
|
||||
case handlerNotFound
|
||||
@@ -105,6 +106,7 @@ extension UTMAPI {
|
||||
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .serverNotFound: return "Server not found."
|
||||
case .notConnected: return "Not connected to API server."
|
||||
case .invalidCommand: return "Invalid command."
|
||||
case .handlerNotFound: return "Command handler not found."
|
||||
|
||||
+14
-2
@@ -25,6 +25,11 @@ actor UTMAPIClient {
|
||||
/// The client socket when connected
|
||||
private var clientSocket: Socket?
|
||||
|
||||
/// True if the client socket is connected
|
||||
var isConnected: Bool {
|
||||
clientSocket != nil
|
||||
}
|
||||
|
||||
init(connectPathUrl: URL) {
|
||||
self.connectPathUrl = connectPathUrl
|
||||
}
|
||||
@@ -34,10 +39,17 @@ actor UTMAPIClient {
|
||||
guard clientSocket == nil else {
|
||||
return
|
||||
}
|
||||
guard FileManager.default.isReadableFile(atPath: connectPathUrl.path) else {
|
||||
throw UTMAPI.APIError.serverNotFound
|
||||
}
|
||||
let address = UnixSocketAddress(path: FilePath(connectPathUrl.path))
|
||||
let socket = try await Socket(UnixStreamProtocol.stream)
|
||||
try socket.fileDescriptor.closeIfThrows {
|
||||
try socket.fileDescriptor.connect(to: address)
|
||||
do {
|
||||
try socket.fileDescriptor.closeIfThrows {
|
||||
try socket.fileDescriptor.connect(to: address)
|
||||
}
|
||||
} catch Errno.connectionRefused {
|
||||
throw UTMAPI.APIError.serverNotFound
|
||||
}
|
||||
logger.debug("[API Client] connected to server")
|
||||
clientSocket = socket
|
||||
|
||||
+34
-1
@@ -15,6 +15,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import ArgumentParser
|
||||
import Logging
|
||||
|
||||
@@ -47,11 +48,32 @@ extension UTMAPICommand {
|
||||
logger.logLevel = environment.debug ? .debug : .info
|
||||
let socketUrl = environment.socketPath?.asFileURL ?? defaultSocketUrl
|
||||
let client = UTMAPIClient(connectPathUrl: socketUrl)
|
||||
try await client.connect()
|
||||
try await connectClient(client)
|
||||
try await run(with: client)
|
||||
try await client.disconnect()
|
||||
}
|
||||
|
||||
/// Attempt to connect to the server and spawn UTM if it is not running
|
||||
/// - Parameter client: API client
|
||||
private func connectClient(_ client: UTMAPIClient) async throws {
|
||||
do {
|
||||
try await client.connect()
|
||||
} catch UTMAPI.APIError.serverNotFound {
|
||||
logger.info("Launching UTM...")
|
||||
let config = NSWorkspace.OpenConfiguration()
|
||||
config.arguments = ["cli-request"]
|
||||
try await NSWorkspace.shared.openApplication(at: utmAppUrl, configuration: config)
|
||||
repeat {
|
||||
do {
|
||||
try await client.connect()
|
||||
} catch UTMAPI.APIError.serverNotFound {
|
||||
// try again after a cooldown
|
||||
try await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
|
||||
}
|
||||
} while await !client.isConnected
|
||||
}
|
||||
}
|
||||
|
||||
/// Socket either in app group or app sandbox
|
||||
private var defaultSocketUrl: URL {
|
||||
let appGroup = Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String
|
||||
@@ -70,6 +92,17 @@ extension UTMAPICommand {
|
||||
return parentURL.appendingPathComponent("api.sock")
|
||||
}
|
||||
|
||||
/// Find the path to UTM.app
|
||||
private var utmAppUrl: URL {
|
||||
if let executableURL = Bundle.main.executableURL {
|
||||
let utmURL = executableURL.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
|
||||
if utmURL.lastPathComponent == "UTM.app" {
|
||||
return utmURL
|
||||
}
|
||||
}
|
||||
return URL(fileURLWithPath: "/Applications/UTM.app")
|
||||
}
|
||||
|
||||
/// Default implementation of run command
|
||||
/// - Parameter client: Connected client
|
||||
func run(with client: UTMAPIClient) async throws {
|
||||
|
||||
Reference in New Issue
Block a user