mirror of https://github.com/cirruslabs/tart.git
Introduce "tart exec" command as an alternative to SSH (#1074)
* Introduce "tart exec" command as an alternative to SSH * Simplify control socket machinery by using NIO async/await primitives * No reason to print the "vm" object directly, just refer to it as "VM" * Log to Apple’s Unified Logging System
This commit is contained in:
parent
40ab5c3af4
commit
dfbdb5559c
119
Package.resolved
119
Package.resolved
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"originHash" : "22b3726bc4e4c6e9c04ac97cb08a82967feb39960a93d2909768a16e11576748",
|
||||
"originHash" : "fe99b8634d39cad3971bde2180657a2bf711968e2e9cf5e3823bc51ea1530663",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "antlr4",
|
||||
|
|
@ -10,6 +10,24 @@
|
|||
"version" : "4.13.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "cirruslabs_tart-guest-agent_apple_swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://buf.build/gen/swift/git/1.28.2-00000000000000-dfeb75ad2b39.1/cirruslabs_tart-guest-agent_apple_swift.git",
|
||||
"state" : {
|
||||
"revision" : "3e13bec2dd36788e80a2e5a2022d44d4a1f373cf",
|
||||
"version" : "1.28.2-00000000000000-dfeb75ad2b39.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "cirruslabs_tart-guest-agent_grpc_swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://buf.build/gen/swift/git/1.24.2-00000000000000-dfeb75ad2b39.1/cirruslabs_tart-guest-agent_grpc_swift.git",
|
||||
"state" : {
|
||||
"branch" : "1.24.2-00000000000000-dfeb75ad2b39.1",
|
||||
"revision" : "5b6ff43b580fe435f0a174e137e2b197759a7170"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dynamic",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
|
@ -19,6 +37,15 @@
|
|||
"revision" : "772883073d044bc754d401cabb6574624eb3778f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "grpc-swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/grpc/grpc-swift.git",
|
||||
"state" : {
|
||||
"revision" : "8c5e99d0255c373e0330730d191a3423c57373fb",
|
||||
"version" : "1.24.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "semaphore",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
|
@ -64,6 +91,33 @@
|
|||
"version" : "1.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-collections",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-collections.git",
|
||||
"state" : {
|
||||
"revision" : "671108c96644956dddcd89dd59c203dcdb36cec7",
|
||||
"version" : "1.1.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-http-structured-headers",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-http-structured-headers.git",
|
||||
"state" : {
|
||||
"revision" : "db6eea3692638a65e2124990155cd220c2915903",
|
||||
"version" : "1.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-http-types",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-http-types.git",
|
||||
"state" : {
|
||||
"revision" : "a0a57e949a8903563aba4615869310c0ebf14c03",
|
||||
"version" : "1.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-log",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
|
@ -73,6 +127,51 @@
|
|||
"version" : "1.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-nio",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-nio.git",
|
||||
"state" : {
|
||||
"revision" : "34d486b01cd891297ac615e40d5999536a1e138d",
|
||||
"version" : "2.83.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-nio-extras",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-nio-extras.git",
|
||||
"state" : {
|
||||
"revision" : "f1f6f772198bee35d99dd145f1513d8581a54f2c",
|
||||
"version" : "1.26.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-nio-http2",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-nio-http2.git",
|
||||
"state" : {
|
||||
"revision" : "4281466512f63d1bd530e33f4aa6993ee7864be0",
|
||||
"version" : "1.36.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-nio-ssl",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-nio-ssl.git",
|
||||
"state" : {
|
||||
"revision" : "4b38f35946d00d8f6176fe58f96d83aba64b36c7",
|
||||
"version" : "2.31.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-nio-transport-services",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-nio-transport-services.git",
|
||||
"state" : {
|
||||
"revision" : "cd1e89816d345d2523b11c55654570acd5cd4c56",
|
||||
"version" : "1.24.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-numerics",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
|
@ -82,6 +181,15 @@
|
|||
"version" : "1.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-protobuf",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-protobuf.git",
|
||||
"state" : {
|
||||
"revision" : "ebc7251dd5b37f627c93698e4374084d98409633",
|
||||
"version" : "1.28.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-retry",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
|
@ -100,6 +208,15 @@
|
|||
"version" : "1.8.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-system",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-system.git",
|
||||
"state" : {
|
||||
"revision" : "a34201439c74b53f0fd71ef11741af7e7caf01e1",
|
||||
"version" : "1.4.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-xattr",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ let package = Package(
|
|||
.package(url: "https://github.com/groue/Semaphore", from: "0.0.8"),
|
||||
.package(url: "https://github.com/fumoboy007/swift-retry", from: "0.2.3"),
|
||||
.package(url: "https://github.com/jozefizso/swift-xattr", from: "3.0.0"),
|
||||
.package(url: "https://github.com/grpc/grpc-swift.git", .upToNextMajor(from: "1.24.2")),
|
||||
.package(url: "https://buf.build/gen/swift/git/1.24.2-00000000000000-dfeb75ad2b39.1/cirruslabs_tart-guest-agent_grpc_swift.git", revision: "1.24.2-00000000000000-dfeb75ad2b39.1"),
|
||||
],
|
||||
targets: [
|
||||
.executableTarget(name: "tart", dependencies: [
|
||||
|
|
@ -40,6 +42,8 @@ let package = Package(
|
|||
.product(name: "Semaphore", package: "Semaphore"),
|
||||
.product(name: "DMRetry", package: "swift-retry"),
|
||||
.product(name: "XAttr", package: "swift-xattr"),
|
||||
.product(name: "GRPC", package: "grpc-swift"),
|
||||
.product(name: "Cirruslabs_TartGuestAgent_Grpc_Swift", package: "cirruslabs_tart-guest-agent_grpc_swift"),
|
||||
], exclude: [
|
||||
"OCI/Reference/Makefile",
|
||||
"OCI/Reference/Reference.g4",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,168 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
import NIOPosix
|
||||
import GRPC
|
||||
import Cirruslabs_TartGuestAgent_Grpc_Swift
|
||||
|
||||
struct ExecCustomExitCodeError: Error {
|
||||
let exitCode: Int32
|
||||
}
|
||||
|
||||
struct Exec: AsyncParsableCommand {
|
||||
static var configuration = CommandConfiguration(abstract: "Execute a command in a running VM")
|
||||
|
||||
@Argument(help: "VM name", completion: .custom(completeLocalMachines))
|
||||
var name: String
|
||||
|
||||
@Flag(name: [.customShort("i")], help: "Attach host's standard input to a remote command")
|
||||
var interactive: Bool = false
|
||||
|
||||
@Flag(name: [.customShort("t")], help: "Allocate a remote pseudo-terminal (PTY)")
|
||||
var tty: Bool = false
|
||||
|
||||
@Argument(parsing: .captureForPassthrough, help: "Command to execute")
|
||||
var command: [String]
|
||||
|
||||
func run() async throws {
|
||||
// We only have withThrowingDiscardingTaskGroup available starting from macOS 14
|
||||
if #unavailable(macOS 14) {
|
||||
throw RuntimeError.Generic("\"tart exec\" is only available on macOS 14 (Sonoma) or newer")
|
||||
}
|
||||
|
||||
// Open VM's directory
|
||||
let vmDir = try VMStorageLocal().open(name)
|
||||
|
||||
// Ensure that the VM is running
|
||||
if try !vmDir.running() {
|
||||
throw RuntimeError.VMNotRunning(name)
|
||||
}
|
||||
|
||||
// Create a gRPC channel connected to the VM's control socket
|
||||
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
||||
defer {
|
||||
try! group.syncShutdownGracefully()
|
||||
}
|
||||
|
||||
let channel = try GRPCChannelPool.with(
|
||||
target: .unixDomainSocket(vmDir.controlSocketURL.path()),
|
||||
transportSecurity: .plaintext,
|
||||
eventLoopGroup: group,
|
||||
)
|
||||
defer {
|
||||
try! channel.close().wait()
|
||||
}
|
||||
|
||||
// Switch controlling terminal into raw mode when remote pseudo-terminal is requested
|
||||
var state: State? = nil
|
||||
|
||||
if tty && Term.IsTerminal() {
|
||||
state = try Term.MakeRaw()
|
||||
}
|
||||
defer {
|
||||
// Restore terminal to its initial state
|
||||
if let state {
|
||||
try! Term.Restore(state)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute a command in a running VM
|
||||
let agentAsyncClient = AgentAsyncClient(channel: channel)
|
||||
let execCall = agentAsyncClient.makeExecCall()
|
||||
|
||||
try await execCall.requestStream.send(.with {
|
||||
$0.type = .command(.with {
|
||||
$0.name = command[0]
|
||||
$0.args = Array(command.dropFirst(1))
|
||||
$0.interactive = interactive
|
||||
$0.tty = tty
|
||||
$0.terminalSize = .with {
|
||||
let (width, height) = try! Term.GetSize()
|
||||
|
||||
$0.cols = UInt32(width)
|
||||
$0.rows = UInt32(height)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Process command events and optionally send our standard input and/or terminal dimensions
|
||||
try await withThrowingTaskGroup { group in
|
||||
// Stream host's standard input if interactive mode is enabled
|
||||
if interactive {
|
||||
let stdinStream = AsyncStream<Data> { continuation in
|
||||
let handle = FileHandle.standardInput
|
||||
|
||||
handle.readabilityHandler = { handle in
|
||||
let data = handle.availableData
|
||||
|
||||
continuation.yield(data)
|
||||
|
||||
if data.isEmpty {
|
||||
continuation.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.addTask {
|
||||
for await stdinData in stdinStream {
|
||||
try await execCall.requestStream.send(.with {
|
||||
$0.type = .standardInput(.with {
|
||||
$0.data = stdinData
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stream host's terminal dimensions if pseudo-terminal is requested
|
||||
signal(SIGWINCH, SIG_IGN)
|
||||
let sigwinchSrc = DispatchSource.makeSignalSource(signal: SIGWINCH)
|
||||
sigwinchSrc.activate()
|
||||
|
||||
if tty {
|
||||
let terminalDimensionsStream = AsyncStream { continuation in
|
||||
sigwinchSrc.setEventHandler {
|
||||
continuation.yield(try! Term.GetSize())
|
||||
}
|
||||
}
|
||||
|
||||
group.addTask {
|
||||
for await (width, height) in terminalDimensionsStream {
|
||||
try await execCall.requestStream.send(.with {
|
||||
$0.type = .terminalResize(.with {
|
||||
$0.cols = UInt32(width)
|
||||
$0.rows = UInt32(height)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process command events
|
||||
group.addTask {
|
||||
for try await response in execCall.responseStream {
|
||||
switch response.type {
|
||||
case .standardOutput(let ioChunk):
|
||||
try FileHandle.standardOutput.write(contentsOf: ioChunk.data)
|
||||
case .standardError(let ioChunk):
|
||||
try FileHandle.standardError.write(contentsOf: ioChunk.data)
|
||||
case .exit(let exit):
|
||||
throw ExecCustomExitCodeError(exitCode: exit.code)
|
||||
default:
|
||||
// Unknown event, do nothing
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while !group.isEmpty {
|
||||
do {
|
||||
try await group.next()
|
||||
} catch {
|
||||
group.cancelAll()
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -489,6 +489,12 @@ struct Run: AsyncParsableCommand {
|
|||
}
|
||||
}
|
||||
|
||||
if #available(macOS 14, *) {
|
||||
Task {
|
||||
try await ControlSocket(vmDir.controlSocketURL).run()
|
||||
}
|
||||
}
|
||||
|
||||
try await vm!.run()
|
||||
|
||||
if let vncImpl = vncImpl {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
import Foundation
|
||||
import Network
|
||||
import os.log
|
||||
import NIO
|
||||
import NIOPosix
|
||||
|
||||
@available(macOS 14, *)
|
||||
class ControlSocket {
|
||||
let controlSocketURL: URL
|
||||
let vmPort: UInt32
|
||||
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
||||
let logger: os.Logger = os.Logger(subsystem: "org.cirruslabs.tart.control-socket", category: "network")
|
||||
|
||||
init(_ controlSocketURL: URL, vmPort: UInt32 = 8080) {
|
||||
self.controlSocketURL = controlSocketURL
|
||||
self.vmPort = vmPort
|
||||
}
|
||||
|
||||
func run() async throws {
|
||||
// Remove control socket file from previous "tart run" invocations,
|
||||
// if any, otherwise we may get the "address already in use" error
|
||||
try? FileManager.default.removeItem(atPath: controlSocketURL.path())
|
||||
|
||||
let serverChannel = try await ServerBootstrap(group: eventLoopGroup)
|
||||
.bind(unixDomainSocketPath: controlSocketURL.path()) { childChannel in
|
||||
childChannel.eventLoop.makeCompletedFuture {
|
||||
return try NIOAsyncChannel<ByteBuffer, ByteBuffer>(
|
||||
wrappingChannelSynchronously: childChannel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
try await withThrowingDiscardingTaskGroup { group in
|
||||
try await serverChannel.executeThenClose { serverInbound in
|
||||
for try await clientChannel in serverInbound {
|
||||
group.addTask {
|
||||
try await self.handleClient(clientChannel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleClient(_ clientChannel: NIOAsyncChannel<ByteBuffer, ByteBuffer>) async throws {
|
||||
self.logger.info("received new control socket connection from a client")
|
||||
|
||||
try await clientChannel.executeThenClose { clientInbound, clientOutbound in
|
||||
self.logger.info("dialing to VM on port \(self.vmPort)...")
|
||||
|
||||
do {
|
||||
guard let vmConnection = try await vm?.connect(toPort: self.vmPort) else {
|
||||
throw RuntimeError.VMSocketFailed(self.vmPort, "VM is not running")
|
||||
}
|
||||
|
||||
self.logger.info("running control socket proxy")
|
||||
|
||||
let vmChannel = try await ClientBootstrap(group: eventLoopGroup).withConnectedSocket(vmConnection.fileDescriptor) { childChannel in
|
||||
childChannel.eventLoop.makeCompletedFuture {
|
||||
try NIOAsyncChannel<ByteBuffer, ByteBuffer>(
|
||||
wrappingChannelSynchronously: childChannel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
try await vmChannel.executeThenClose { (vmInbound, vmOutbound) in
|
||||
try await withThrowingDiscardingTaskGroup { group in
|
||||
// Proxy data from a client (e.g. "tart exec") to a VM
|
||||
group.addTask {
|
||||
for try await message in clientInbound {
|
||||
try await vmOutbound.write(message)
|
||||
}
|
||||
}
|
||||
|
||||
// Proxy data from a VM to a client (e.g. "tart exec")
|
||||
group.addTask {
|
||||
for try await message in vmInbound {
|
||||
try await clientOutbound.write(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.logger.info("control socket client disconnected")
|
||||
} catch (let error) {
|
||||
self.logger.error("control socket connection failed: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ struct Root: AsyncParsableCommand {
|
|||
Login.self,
|
||||
Logout.self,
|
||||
IP.self,
|
||||
Exec.self,
|
||||
Pull.self,
|
||||
Push.self,
|
||||
Import.self,
|
||||
|
|
@ -101,6 +102,11 @@ struct Root: AsyncParsableCommand {
|
|||
try command.run()
|
||||
}
|
||||
} catch {
|
||||
// Not an error, just a custom exit code from "tart exec"
|
||||
if let execCustomExitCodeError = error as? ExecCustomExitCodeError {
|
||||
Foundation.exit(execCustomExitCodeError.exitCode)
|
||||
}
|
||||
|
||||
// Capture the error into Sentry
|
||||
SentrySDK.capture(error: error)
|
||||
SentrySDK.flush(timeout: 2.seconds.timeInterval)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
import Foundation
|
||||
import System
|
||||
|
||||
struct State {
|
||||
fileprivate let termios: termios
|
||||
}
|
||||
|
||||
class Term {
|
||||
static func IsTerminal() -> Bool {
|
||||
var termios = termios()
|
||||
|
||||
return tcgetattr(FileHandle.standardInput.fileDescriptor, &termios) != -1
|
||||
}
|
||||
|
||||
static func MakeRaw() throws -> State {
|
||||
var termiosOrig = termios()
|
||||
|
||||
var ret = tcgetattr(FileHandle.standardInput.fileDescriptor, &termiosOrig)
|
||||
if ret == -1 {
|
||||
let details = Errno(rawValue: CInt(errno))
|
||||
|
||||
throw RuntimeError.TerminalOperationFailed("failed to retrieve terminal parameters: \(details)")
|
||||
}
|
||||
|
||||
var termiosRaw = termiosOrig
|
||||
cfmakeraw(&termiosRaw)
|
||||
|
||||
ret = tcsetattr(FileHandle.standardInput.fileDescriptor, TCSANOW, &termiosRaw)
|
||||
if ret == -1 {
|
||||
let details = Errno(rawValue: CInt(errno))
|
||||
|
||||
throw RuntimeError.TerminalOperationFailed("failed to set terminal parameters: \(details)")
|
||||
}
|
||||
|
||||
return State(termios: termiosOrig)
|
||||
}
|
||||
|
||||
static func Restore(_ state: State) throws {
|
||||
var termios = state.termios
|
||||
|
||||
let ret = tcsetattr(FileHandle.standardInput.fileDescriptor, TCSANOW, &termios)
|
||||
if ret == -1 {
|
||||
let details = Errno(rawValue: CInt(errno))
|
||||
|
||||
throw RuntimeError.TerminalOperationFailed("failed to set terminal parameters: \(details)")
|
||||
}
|
||||
}
|
||||
|
||||
static func GetSize() throws -> (width: UInt16, height: UInt16) {
|
||||
var winsize = winsize()
|
||||
|
||||
guard ioctl(STDOUT_FILENO, TIOCGWINSZ, &winsize) != -1 else {
|
||||
let details = Errno(rawValue: CInt(errno))
|
||||
|
||||
throw RuntimeError.TerminalOperationFailed("failed to get terminal size: \(details)")
|
||||
}
|
||||
|
||||
return (width: winsize.ws_col, height: winsize.ws_row)
|
||||
}
|
||||
}
|
||||
|
|
@ -248,6 +248,19 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func connect(toPort: UInt32) async throws -> VZVirtioSocketConnection {
|
||||
guard let socketDevice = virtualMachine.socketDevices.first else {
|
||||
throw RuntimeError.VMSocketFailed(toPort, ", VM has no socket devices configured")
|
||||
}
|
||||
|
||||
guard let virtioSocketDevice = socketDevice as? VZVirtioSocketDevice else {
|
||||
throw RuntimeError.VMSocketFailed(toPort, ", expected VM's first socket device to have a type of VZVirtioSocketDevice, got \(type(of: socketDevice)) instead")
|
||||
}
|
||||
|
||||
return try await virtioSocketDevice.connect(toPort: toPort)
|
||||
}
|
||||
|
||||
func run() async throws {
|
||||
do {
|
||||
try await sema.waitUnlessCancelled()
|
||||
|
|
@ -409,6 +422,9 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||
configuration.consoleDevices.append(consoleDevice)
|
||||
}
|
||||
|
||||
// Socket device
|
||||
configuration.socketDevices = [VZVirtioSocketDeviceConfiguration()]
|
||||
|
||||
try configuration.validate()
|
||||
|
||||
return configuration
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ struct VMDirectory: Prunable {
|
|||
var manifestURL: URL {
|
||||
baseURL.appendingPathComponent("manifest.json")
|
||||
}
|
||||
var controlSocketURL: URL {
|
||||
baseURL.appendingPathComponent("control.sock")
|
||||
}
|
||||
|
||||
var explicitlyPulledMark: URL {
|
||||
baseURL.appendingPathComponent(".explicitly-pulled")
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ extension Error {
|
|||
}
|
||||
|
||||
enum RuntimeError : Error {
|
||||
case Generic(_ message: String)
|
||||
case VMConfigurationError(_ message: String)
|
||||
case VMDoesNotExist(name: String)
|
||||
case VMMissingFiles(_ message: String)
|
||||
|
|
@ -75,6 +76,8 @@ enum RuntimeError : Error {
|
|||
case SuspendFailed(_ message: String)
|
||||
case PullFailed(_ message: String)
|
||||
case VirtualMachineLimitExceeded(_ hint: String)
|
||||
case VMSocketFailed(_ port: UInt32, _ explanation: String)
|
||||
case TerminalOperationFailed(_ message: String)
|
||||
}
|
||||
|
||||
protocol HasExitCode {
|
||||
|
|
@ -84,6 +87,8 @@ protocol HasExitCode {
|
|||
extension RuntimeError : CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .Generic(let message):
|
||||
return message
|
||||
case .VMConfigurationError(let message):
|
||||
return message
|
||||
case .VMDoesNotExist(let name):
|
||||
|
|
@ -136,6 +141,10 @@ extension RuntimeError : CustomStringConvertible {
|
|||
return message
|
||||
case .VirtualMachineLimitExceeded(let hint):
|
||||
return "The number of VMs exceeds the system limit\(hint)"
|
||||
case .VMSocketFailed(let port, let explanation):
|
||||
return "Failed to establish a VM socket connection to port \(port): \(explanation)"
|
||||
case .TerminalOperationFailed(let message):
|
||||
return message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue