mirror of
https://github.com/cirruslabs/tart.git
synced 2026-10-09 15:35:31 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8539b8faae | ||
|
|
71159373e5 | ||
|
|
8248f19943 | ||
|
|
1cbc1e2cda | ||
|
|
0187834c34 | ||
|
|
dfbdb5559c | ||
|
|
40ab5c3af4 |
+120
-3
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "22b3726bc4e4c6e9c04ac97cb08a82967feb39960a93d2909768a16e11576748",
|
"originHash" : "90af6efa7ed1bbb0cd6985eb109c1e265a223a697b7fbaae2453206a892180e7",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "antlr4",
|
"identity" : "antlr4",
|
||||||
@@ -10,6 +10,24 @@
|
|||||||
"version" : "4.13.2"
|
"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",
|
"identity" : "dynamic",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -19,6 +37,15 @@
|
|||||||
"revision" : "772883073d044bc754d401cabb6574624eb3778f"
|
"revision" : "772883073d044bc754d401cabb6574624eb3778f"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "grpc-swift",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/grpc/grpc-swift.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "8c5e99d0255c373e0330730d191a3423c57373fb",
|
||||||
|
"version" : "1.24.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "semaphore",
|
"identity" : "semaphore",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -33,8 +60,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/getsentry/sentry-cocoa",
|
"location" : "https://github.com/getsentry/sentry-cocoa",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "5575af93efb776414f243e93d6af9f6258dc539a",
|
"revision" : "65b3d2a7608685e8d4a37c68fa2c64f28d0b537e",
|
||||||
"version" : "8.36.0"
|
"version" : "8.51.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -64,6 +91,33 @@
|
|||||||
"version" : "1.2.0"
|
"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",
|
"identity" : "swift-log",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -73,6 +127,51 @@
|
|||||||
"version" : "1.6.1"
|
"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",
|
"identity" : "swift-numerics",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -82,6 +181,15 @@
|
|||||||
"version" : "1.0.2"
|
"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",
|
"identity" : "swift-retry",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -100,6 +208,15 @@
|
|||||||
"version" : "1.8.0"
|
"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",
|
"identity" : "swift-xattr",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
+5
-1
@@ -17,13 +17,15 @@ let package = Package(
|
|||||||
.package(url: "https://github.com/antlr/antlr4", exact: "4.13.2"),
|
.package(url: "https://github.com/antlr/antlr4", exact: "4.13.2"),
|
||||||
.package(url: "https://github.com/apple/swift-atomics.git", .upToNextMajor(from: "1.2.0")),
|
.package(url: "https://github.com/apple/swift-atomics.git", .upToNextMajor(from: "1.2.0")),
|
||||||
.package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.53.6"),
|
.package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.53.6"),
|
||||||
.package(url: "https://github.com/getsentry/sentry-cocoa", from: "8.36.0"),
|
.package(url: "https://github.com/getsentry/sentry-cocoa", from: "8.51.1"),
|
||||||
.package(url: "https://github.com/cfilipov/TextTable", branch: "master"),
|
.package(url: "https://github.com/cfilipov/TextTable", branch: "master"),
|
||||||
.package(url: "https://github.com/sersoft-gmbh/swift-sysctl.git", from: "1.8.0"),
|
.package(url: "https://github.com/sersoft-gmbh/swift-sysctl.git", from: "1.8.0"),
|
||||||
.package(url: "https://github.com/orchetect/SwiftRadix", from: "1.3.1"),
|
.package(url: "https://github.com/orchetect/SwiftRadix", from: "1.3.1"),
|
||||||
.package(url: "https://github.com/groue/Semaphore", from: "0.0.8"),
|
.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/fumoboy007/swift-retry", from: "0.2.3"),
|
||||||
.package(url: "https://github.com/jozefizso/swift-xattr", from: "3.0.0"),
|
.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: [
|
targets: [
|
||||||
.executableTarget(name: "tart", dependencies: [
|
.executableTarget(name: "tart", dependencies: [
|
||||||
@@ -40,6 +42,8 @@ let package = Package(
|
|||||||
.product(name: "Semaphore", package: "Semaphore"),
|
.product(name: "Semaphore", package: "Semaphore"),
|
||||||
.product(name: "DMRetry", package: "swift-retry"),
|
.product(name: "DMRetry", package: "swift-retry"),
|
||||||
.product(name: "XAttr", package: "swift-xattr"),
|
.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: [
|
], exclude: [
|
||||||
"OCI/Reference/Makefile",
|
"OCI/Reference/Makefile",
|
||||||
"OCI/Reference/Reference.g4",
|
"OCI/Reference/Reference.g4",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ struct Create: AsyncParsableCommand {
|
|||||||
@Argument(help: "VM name")
|
@Argument(help: "VM name")
|
||||||
var name: String
|
var name: String
|
||||||
|
|
||||||
@Option(help: ArgumentHelp("create a macOS VM using path to the IPSW file or URL (or \"latest\", to fetch the latest supported IPSW automatically)", valueName: "path"))
|
@Option(help: ArgumentHelp("create a macOS VM using path to the IPSW file or URL (or \"latest\", to fetch the latest supported IPSW automatically)", valueName: "path"), completion: .file())
|
||||||
var fromIPSW: String?
|
var fromIPSW: String?
|
||||||
|
|
||||||
@Flag(help: "create a Linux VM")
|
@Flag(help: "create a Linux VM")
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
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", discussion: """
|
||||||
|
Requires Tart Guest Agent running in a guest VM.
|
||||||
|
|
||||||
|
Note that all non-vanilla Cirrus Labs VM images already have the Tart Guest Agent installed.
|
||||||
|
""")
|
||||||
|
|
||||||
|
@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
|
||||||
|
do {
|
||||||
|
try await execute(channel)
|
||||||
|
} catch let error as GRPCConnectionPoolError {
|
||||||
|
throw RuntimeError.Generic("Failed to connect to the VM using its control socket: \(error.localizedDescription), is the Tart Guest Agent running?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func execute(_ channel: GRPCChannel) async throws {
|
||||||
|
let agentAsyncClient = AgentAsyncClient(channel: channel)
|
||||||
|
let callOptions = CallOptions(timeLimit: .timeout(.seconds(1)))
|
||||||
|
let execCall = agentAsyncClient.makeExecCall(callOptions: callOptions)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ struct Export: AsyncParsableCommand {
|
|||||||
@Argument(help: "Source VM name.", completion: .custom(completeMachines))
|
@Argument(help: "Source VM name.", completion: .custom(completeMachines))
|
||||||
var name: String
|
var name: String
|
||||||
|
|
||||||
@Argument(help: "Path to the destination file.")
|
@Argument(help: "Path to the destination file.", completion: .file())
|
||||||
var path: String?
|
var path: String?
|
||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import Foundation
|
|||||||
struct Import: AsyncParsableCommand {
|
struct Import: AsyncParsableCommand {
|
||||||
static var configuration = CommandConfiguration(abstract: "Import VM from a compressed .tvm file")
|
static var configuration = CommandConfiguration(abstract: "Import VM from a compressed .tvm file")
|
||||||
|
|
||||||
@Argument(help: "Path to a file created with \"tart export\".")
|
@Argument(help: "Path to a file created with \"tart export\".", completion: .file())
|
||||||
var path: String
|
var path: String
|
||||||
|
|
||||||
@Argument(help: "Destination VM name.")
|
@Argument(help: "Destination VM name.", completion: .custom(completeLocalMachines))
|
||||||
var name: String
|
var name: String
|
||||||
|
|
||||||
func validate() throws {
|
func validate() throws {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ struct List: AsyncParsableCommand {
|
|||||||
@Option(help: ArgumentHelp("Only display VMs from the specified source (e.g. --source local, --source oci)."))
|
@Option(help: ArgumentHelp("Only display VMs from the specified source (e.g. --source local, --source oci)."))
|
||||||
var source: String?
|
var source: String?
|
||||||
|
|
||||||
@Option(help: "Output format: text or json")
|
@Option(help: "Output format: text or json", completion: .list(["text", "json"]))
|
||||||
var format: Format = .text
|
var format: Format = .text
|
||||||
|
|
||||||
@Flag(name: [.short, .long], help: ArgumentHelp("Only display VM names."))
|
@Flag(name: [.short, .long], help: ArgumentHelp("Only display VM names."))
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import SwiftDate
|
|||||||
struct Prune: AsyncParsableCommand {
|
struct Prune: AsyncParsableCommand {
|
||||||
static var configuration = CommandConfiguration(abstract: "Prune OCI and IPSW caches or local VMs")
|
static var configuration = CommandConfiguration(abstract: "Prune OCI and IPSW caches or local VMs")
|
||||||
|
|
||||||
@Option(help: ArgumentHelp("Entries to remove: \"caches\" targets OCI and IPSW caches and \"vms\" targets local VMs."))
|
@Option(help: ArgumentHelp("Entries to remove: \"caches\" targets OCI and IPSW caches and \"vms\" targets local VMs."), completion: .list(["caches", "vms"]))
|
||||||
var entries: String = "caches"
|
var entries: String = "caches"
|
||||||
|
|
||||||
@Option(help: ArgumentHelp("Remove entries that were last accessed more than n days ago",
|
@Option(help: ArgumentHelp("Remove entries that were last accessed more than n days ago",
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ struct Run: AsyncParsableCommand {
|
|||||||
@Option(help: ArgumentHelp(
|
@Option(help: ArgumentHelp(
|
||||||
"Attach an externally created serial console",
|
"Attach an externally created serial console",
|
||||||
discussion: "Alternative to `--serial` flag for programmatic integrations."
|
discussion: "Alternative to `--serial` flag for programmatic integrations."
|
||||||
))
|
), completion: .file())
|
||||||
var serialPath: String?
|
var serialPath: String?
|
||||||
|
|
||||||
@Flag(help: ArgumentHelp("Force open a UI window, even when VNC is enabled.", visibility: .private))
|
@Flag(help: ArgumentHelp("Force open a UI window, even when VNC is enabled.", visibility: .private))
|
||||||
@@ -116,8 +116,12 @@ struct Run: AsyncParsableCommand {
|
|||||||
#endif
|
#endif
|
||||||
var vncExperimental: Bool = false
|
var vncExperimental: Bool = false
|
||||||
|
|
||||||
|
// Note that the valueName here should really be "path[:options]" instead of just "path",
|
||||||
|
// see ArgumentParser issue[1] for more details.
|
||||||
|
//
|
||||||
|
// [1]: https://github.com/apple/swift-argument-parser/issues/761
|
||||||
@Option(help: ArgumentHelp("""
|
@Option(help: ArgumentHelp("""
|
||||||
Additional disk attachments with an optional read-only and synchronization options (e.g. --disk="disk.bin", --disk="ubuntu.iso:ro", --disk="/dev/disk0", --disk "ghcr.io/cirruslabs/xcode:16.0:ro" or --disk="nbd://localhost:10809/myDisk:sync=none")
|
Additional disk attachments with an optional read-only and synchronization options in the form of path[:options] (e.g. --disk="disk.bin", --disk="ubuntu.iso:ro", --disk="/dev/disk0", --disk "ghcr.io/cirruslabs/xcode:16.0:ro" or --disk="nbd://localhost:10809/myDisk:sync=none")
|
||||||
""", discussion: """
|
""", discussion: """
|
||||||
The disk attachment can be a:
|
The disk attachment can be a:
|
||||||
|
|
||||||
@@ -139,7 +143,7 @@ struct Run: AsyncParsableCommand {
|
|||||||
To work around this pass TART_HOME explicitly:
|
To work around this pass TART_HOME explicitly:
|
||||||
|
|
||||||
sudo TART_HOME="$HOME/.tart" tart run sequoia --disk=/dev/disk0
|
sudo TART_HOME="$HOME/.tart" tart run sequoia --disk=/dev/disk0
|
||||||
""", valueName: "path[:options]"))
|
""", valueName: "path"), completion: .file())
|
||||||
var disk: [String] = []
|
var disk: [String] = []
|
||||||
|
|
||||||
#if arch(arm64)
|
#if arch(arm64)
|
||||||
@@ -158,7 +162,11 @@ struct Run: AsyncParsableCommand {
|
|||||||
#endif
|
#endif
|
||||||
var rosettaTag: String?
|
var rosettaTag: String?
|
||||||
|
|
||||||
@Option(help: ArgumentHelp("Additional directory shares with an optional read-only and mount tag options (e.g. --dir=\"~/src/build\" or --dir=\"~/src/sources:ro\")", discussion: """
|
// Note that the valueName here should really be "[name:]path[:options]" instead of just "path",
|
||||||
|
// see ArgumentParser issue[1] for more details.
|
||||||
|
//
|
||||||
|
// [1]: https://github.com/apple/swift-argument-parser/issues/761
|
||||||
|
@Option(help: ArgumentHelp("Additional directory shares with an optional read-only and mount tag options in the form of [name:]path[:options] (e.g. --dir=\"~/src/build\" or --dir=\"~/src/sources:ro\")", discussion: """
|
||||||
Requires host to be macOS 13.0 (Ventura) or newer. macOS guests must be running macOS 13.0 (Ventura) or newer too.
|
Requires host to be macOS 13.0 (Ventura) or newer. macOS guests must be running macOS 13.0 (Ventura) or newer too.
|
||||||
|
|
||||||
Options are comma-separated and are as follows:
|
Options are comma-separated and are as follows:
|
||||||
@@ -170,7 +178,7 @@ struct Run: AsyncParsableCommand {
|
|||||||
Mount tag can be overridden by appending tag property to the directory share (e.g. --dir=\"~/src/build:tag=build\" or --dir=\"~/src/build:ro,tag=build\"). Then it can be mounted via "mount_virtiofs build ~/build" inside guest macOS and "mount -t virtiofs build ~/build" inside guest Linux.
|
Mount tag can be overridden by appending tag property to the directory share (e.g. --dir=\"~/src/build:tag=build\" or --dir=\"~/src/build:ro,tag=build\"). Then it can be mounted via "mount_virtiofs build ~/build" inside guest macOS and "mount -t virtiofs build ~/build" inside guest Linux.
|
||||||
|
|
||||||
In case of passing multiple directories per mount tag it is required to prefix them with names e.g. --dir=\"build:~/src/build\" --dir=\"sources:~/src/sources:ro\". These names will be used as directory names under the mounting point inside guests. For the example above it will be "/Volumes/My Shared Files/build" and "/Volumes/My Shared Files/sources" respectively.
|
In case of passing multiple directories per mount tag it is required to prefix them with names e.g. --dir=\"build:~/src/build\" --dir=\"sources:~/src/sources:ro\". These names will be used as directory names under the mounting point inside guests. For the example above it will be "/Volumes/My Shared Files/build" and "/Volumes/My Shared Files/sources" respectively.
|
||||||
""", valueName: "[name:]path[:options]"))
|
""", valueName: "path"), completion: .directory)
|
||||||
var dir: [String] = []
|
var dir: [String] = []
|
||||||
|
|
||||||
@Flag(help: ArgumentHelp("Enable nested virtualization if possible"))
|
@Flag(help: ArgumentHelp("Enable nested virtualization if possible"))
|
||||||
@@ -312,9 +320,6 @@ struct Run: AsyncParsableCommand {
|
|||||||
if !(config.platform is PlatformSuspendable) {
|
if !(config.platform is PlatformSuspendable) {
|
||||||
throw ValidationError("You can only suspend macOS VMs")
|
throw ValidationError("You can only suspend macOS VMs")
|
||||||
}
|
}
|
||||||
if dir.count > 0 {
|
|
||||||
throw ValidationError("Suspending VMs with shared directories is not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
if noTrackpad {
|
if noTrackpad {
|
||||||
throw ValidationError("--no-trackpad cannot be used with --suspendable")
|
throw ValidationError("--no-trackpad cannot be used with --suspendable")
|
||||||
@@ -481,6 +486,12 @@ struct Run: AsyncParsableCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if #available(macOS 14, *) {
|
||||||
|
Task {
|
||||||
|
try await ControlSocket(vmDir.controlSocketURL).run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try await vm!.run()
|
try await vm!.run()
|
||||||
|
|
||||||
if let vncImpl = vncImpl {
|
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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
-34
@@ -18,6 +18,7 @@ struct Root: AsyncParsableCommand {
|
|||||||
Login.self,
|
Login.self,
|
||||||
Logout.self,
|
Logout.self,
|
||||||
IP.self,
|
IP.self,
|
||||||
|
Exec.self,
|
||||||
Pull.self,
|
Pull.self,
|
||||||
Push.self,
|
Push.self,
|
||||||
Import.self,
|
Import.self,
|
||||||
@@ -30,39 +31,6 @@ struct Root: AsyncParsableCommand {
|
|||||||
])
|
])
|
||||||
|
|
||||||
public static func main() async throws {
|
public static func main() async throws {
|
||||||
// Initialize Sentry
|
|
||||||
if let dsn = ProcessInfo.processInfo.environment["SENTRY_DSN"] {
|
|
||||||
SentrySDK.start { options in
|
|
||||||
options.dsn = dsn
|
|
||||||
options.releaseName = CI.release
|
|
||||||
options.tracesSampleRate = Float(
|
|
||||||
ProcessInfo.processInfo.environment["SENTRY_TRACES_SAMPLE_RATE"] ?? "1.0"
|
|
||||||
) as NSNumber?
|
|
||||||
|
|
||||||
// By default only 5XX are captured
|
|
||||||
// Let's capture everything but 401 (unauthorized)
|
|
||||||
options.enableCaptureFailedRequests = true
|
|
||||||
options.failedRequestStatusCodes = [
|
|
||||||
HttpStatusCodeRange(min: 400, max: 400),
|
|
||||||
HttpStatusCodeRange(min: 402, max: 599)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer { SentrySDK.flush(timeout: 2.seconds.timeInterval) }
|
|
||||||
|
|
||||||
SentrySDK.configureScope { scope in
|
|
||||||
scope.setExtra(value: ProcessInfo.processInfo.arguments, key: "Command-line arguments")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enrich future events with Cirrus CI-specific tags
|
|
||||||
if let tags = ProcessInfo.processInfo.environment["CIRRUS_SENTRY_TAGS"] {
|
|
||||||
SentrySDK.configureScope { scope in
|
|
||||||
for (key, value) in tags.split(separator: ",").compactMap({ parseCirrusSentryTag($0) }) {
|
|
||||||
scope.setTag(value: value, key: key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add commands that are only available on specific macOS versions
|
// Add commands that are only available on specific macOS versions
|
||||||
if #available(macOS 14, *) {
|
if #available(macOS 14, *) {
|
||||||
configuration.subcommands.append(Suspend.self)
|
configuration.subcommands.append(Suspend.self)
|
||||||
@@ -82,10 +50,43 @@ struct Root: AsyncParsableCommand {
|
|||||||
// Set line-buffered output for stdout
|
// Set line-buffered output for stdout
|
||||||
setlinebuf(stdout)
|
setlinebuf(stdout)
|
||||||
|
|
||||||
// Parse and run command
|
|
||||||
do {
|
do {
|
||||||
|
// Parse command
|
||||||
var command = try parseAsRoot()
|
var command = try parseAsRoot()
|
||||||
|
|
||||||
|
// Initialize Sentry
|
||||||
|
if let dsn = ProcessInfo.processInfo.environment["SENTRY_DSN"] {
|
||||||
|
SentrySDK.start { options in
|
||||||
|
options.dsn = dsn
|
||||||
|
options.releaseName = CI.release
|
||||||
|
options.tracesSampleRate = Float(
|
||||||
|
ProcessInfo.processInfo.environment["SENTRY_TRACES_SAMPLE_RATE"] ?? "1.0"
|
||||||
|
) as NSNumber?
|
||||||
|
|
||||||
|
// By default only 5XX are captured
|
||||||
|
// Let's capture everything but 401 (unauthorized)
|
||||||
|
options.enableCaptureFailedRequests = true
|
||||||
|
options.failedRequestStatusCodes = [
|
||||||
|
HttpStatusCodeRange(min: 400, max: 400),
|
||||||
|
HttpStatusCodeRange(min: 402, max: 599)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer { SentrySDK.flush(timeout: 2.seconds.timeInterval) }
|
||||||
|
|
||||||
|
SentrySDK.configureScope { scope in
|
||||||
|
scope.setExtra(value: ProcessInfo.processInfo.arguments, key: "Command-line arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich future events with Cirrus CI-specific tags
|
||||||
|
if let tags = ProcessInfo.processInfo.environment["CIRRUS_SENTRY_TAGS"] {
|
||||||
|
SentrySDK.configureScope { scope in
|
||||||
|
for (key, value) in tags.split(separator: ",").compactMap({ parseCirrusSentryTag($0) }) {
|
||||||
|
scope.setTag(value: value, key: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run garbage-collection before each command (shouldn't take too long)
|
// Run garbage-collection before each command (shouldn't take too long)
|
||||||
if type(of: command) != type(of: Pull()) && type(of: command) != type(of: Clone()){
|
if type(of: command) != type(of: Pull()) && type(of: command) != type(of: Clone()){
|
||||||
do {
|
do {
|
||||||
@@ -95,12 +96,18 @@ struct Root: AsyncParsableCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run command
|
||||||
if var asyncCommand = command as? AsyncParsableCommand {
|
if var asyncCommand = command as? AsyncParsableCommand {
|
||||||
try await asyncCommand.run()
|
try await asyncCommand.run()
|
||||||
} else {
|
} else {
|
||||||
try command.run()
|
try command.run()
|
||||||
}
|
}
|
||||||
} catch {
|
} 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
|
// Capture the error into Sentry
|
||||||
SentrySDK.capture(error: error)
|
SentrySDK.capture(error: error)
|
||||||
SentrySDK.flush(timeout: 2.seconds.timeInterval)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-7
@@ -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 {
|
func run() async throws {
|
||||||
do {
|
do {
|
||||||
try await sema.waitUnlessCancelled()
|
try await sema.waitUnlessCancelled()
|
||||||
@@ -399,15 +412,16 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
//
|
//
|
||||||
// A dummy console device useful for implementing
|
// A dummy console device useful for implementing
|
||||||
// host feature checks in the guest agent software.
|
// host feature checks in the guest agent software.
|
||||||
if !suspendable {
|
let consolePort = VZVirtioConsolePortConfiguration()
|
||||||
let consolePort = VZVirtioConsolePortConfiguration()
|
consolePort.name = "tart-version-\(CI.version)"
|
||||||
consolePort.name = "tart-version-\(CI.version)"
|
|
||||||
|
|
||||||
let consoleDevice = VZVirtioConsoleDeviceConfiguration()
|
let consoleDevice = VZVirtioConsoleDeviceConfiguration()
|
||||||
consoleDevice.ports[0] = consolePort
|
consoleDevice.ports[0] = consolePort
|
||||||
|
|
||||||
configuration.consoleDevices.append(consoleDevice)
|
configuration.consoleDevices.append(consoleDevice)
|
||||||
}
|
|
||||||
|
// Socket device
|
||||||
|
configuration.socketDevices = [VZVirtioSocketDeviceConfiguration()]
|
||||||
|
|
||||||
try configuration.validate()
|
try configuration.validate()
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ struct VMDirectory: Prunable {
|
|||||||
var manifestURL: URL {
|
var manifestURL: URL {
|
||||||
baseURL.appendingPathComponent("manifest.json")
|
baseURL.appendingPathComponent("manifest.json")
|
||||||
}
|
}
|
||||||
|
var controlSocketURL: URL {
|
||||||
|
baseURL.appendingPathComponent("control.sock")
|
||||||
|
}
|
||||||
|
|
||||||
var explicitlyPulledMark: URL {
|
var explicitlyPulledMark: URL {
|
||||||
baseURL.appendingPathComponent(".explicitly-pulled")
|
baseURL.appendingPathComponent(".explicitly-pulled")
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ extension Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum RuntimeError : Error {
|
enum RuntimeError : Error {
|
||||||
|
case Generic(_ message: String)
|
||||||
case VMConfigurationError(_ message: String)
|
case VMConfigurationError(_ message: String)
|
||||||
case VMDoesNotExist(name: String)
|
case VMDoesNotExist(name: String)
|
||||||
case VMMissingFiles(_ message: String)
|
case VMMissingFiles(_ message: String)
|
||||||
@@ -75,6 +76,8 @@ enum RuntimeError : Error {
|
|||||||
case SuspendFailed(_ message: String)
|
case SuspendFailed(_ message: String)
|
||||||
case PullFailed(_ message: String)
|
case PullFailed(_ message: String)
|
||||||
case VirtualMachineLimitExceeded(_ hint: String)
|
case VirtualMachineLimitExceeded(_ hint: String)
|
||||||
|
case VMSocketFailed(_ port: UInt32, _ explanation: String)
|
||||||
|
case TerminalOperationFailed(_ message: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol HasExitCode {
|
protocol HasExitCode {
|
||||||
@@ -84,6 +87,8 @@ protocol HasExitCode {
|
|||||||
extension RuntimeError : CustomStringConvertible {
|
extension RuntimeError : CustomStringConvertible {
|
||||||
public var description: String {
|
public var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .Generic(let message):
|
||||||
|
return message
|
||||||
case .VMConfigurationError(let message):
|
case .VMConfigurationError(let message):
|
||||||
return message
|
return message
|
||||||
case .VMDoesNotExist(let name):
|
case .VMDoesNotExist(let name):
|
||||||
@@ -136,6 +141,10 @@ extension RuntimeError : CustomStringConvertible {
|
|||||||
return message
|
return message
|
||||||
case .VirtualMachineLimitExceeded(let hint):
|
case .VirtualMachineLimitExceeded(let hint):
|
||||||
return "The number of VMs exceeds the system limit\(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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user