mirror of
https://github.com/cirruslabs/tart.git
synced 2026-10-12 00:45:34 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c25364b2d4 | ||
|
|
e12f95878e | ||
|
|
7efef28250 | ||
|
|
1e90752ded | ||
|
|
2020324ef5 | ||
|
|
b581db5be4 |
@@ -36,5 +36,7 @@ brews:
|
||||
homepage: https://github.com/cirruslabs/tart
|
||||
description: Run macOS VMs on Apple Silicon
|
||||
skip_upload: auto
|
||||
dependencies:
|
||||
- "cirruslabs/cli/softnet"
|
||||
custom_block: |
|
||||
depends_on :macos => :monterey
|
||||
|
||||
@@ -27,20 +27,31 @@ struct Run: AsyncParsableCommand {
|
||||
discussion: "Useful since VNC supports copy/paste, drag and drop, etc.\nNote that Remote Login option should be enabled inside the VM."))
|
||||
var vnc: Bool = false
|
||||
|
||||
@MainActor
|
||||
func run() async throws {
|
||||
if recovery && vnc {
|
||||
print("You can't run in recovery and use VNC!")
|
||||
Foundation.exit(1)
|
||||
}
|
||||
|
||||
let vmDir = try VMStorageLocal().open(name)
|
||||
vm = try VM(vmDir: vmDir)
|
||||
@Flag var withSoftnet: Bool = false
|
||||
|
||||
Task {
|
||||
@MainActor
|
||||
func run() async throws {
|
||||
let vmDir = try VMStorageLocal().open(name)
|
||||
vm = try VM(vmDir: vmDir, withSoftnet: withSoftnet)
|
||||
|
||||
var vncWrapper: VNCWrapper?
|
||||
|
||||
if vnc {
|
||||
vncWrapper = VNCWrapper(virtualMachine: vm!.virtualMachine)
|
||||
}
|
||||
|
||||
let task = Task {
|
||||
do {
|
||||
if let vncWrapper = vncWrapper {
|
||||
await vncWrapper.open()
|
||||
}
|
||||
|
||||
try await vm!.run(recovery)
|
||||
|
||||
if let vncWrapper = vncWrapper {
|
||||
try vncWrapper.stop()
|
||||
}
|
||||
|
||||
Foundation.exit(0)
|
||||
} catch {
|
||||
if error.localizedDescription.contains("Failed to lock auxiliary storage.") {
|
||||
@@ -52,23 +63,16 @@ struct Run: AsyncParsableCommand {
|
||||
Foundation.exit(1)
|
||||
}
|
||||
}
|
||||
if vnc {
|
||||
do {
|
||||
print("Waiting for the VM to boot...")
|
||||
let resolvedIP = try await IP.resolveIP(vm!.config, secondsToWait: 60)
|
||||
guard let ip = resolvedIP else {
|
||||
throw IPNotFound()
|
||||
}
|
||||
let url = URL(string: "vnc://\(ip)")!
|
||||
print("Opening \(url)")
|
||||
NSWorkspace.shared.open(url)
|
||||
} catch {
|
||||
print("Failed to get an IP for screen sharing: \(error)")
|
||||
}
|
||||
while !(vm?.inFinalState ?? false) {
|
||||
try await Task.sleep(nanoseconds: 1_000_000)
|
||||
}
|
||||
} else if !noGraphics {
|
||||
|
||||
let sigintSrc = DispatchSource.makeSignalSource(signal: SIGINT)
|
||||
sigintSrc.setEventHandler {
|
||||
task.cancel()
|
||||
}
|
||||
sigintSrc.activate()
|
||||
|
||||
if noGraphics || vnc {
|
||||
dispatchMain()
|
||||
} else {
|
||||
runUI()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ struct OCIManifest: Codable, Equatable {
|
||||
var mediaType: String = ociManifestMediaType
|
||||
var config: OCIManifestConfig
|
||||
var layers: [OCIManifestLayer] = Array()
|
||||
var annotations: Dictionary<String, String> = Dictionary()
|
||||
var annotations: Dictionary<String, String>?
|
||||
|
||||
init(config: OCIManifestConfig, layers: [OCIManifestLayer], uncompressedDiskSize: UInt64? = nil) {
|
||||
self.config = config
|
||||
@@ -29,7 +29,7 @@ struct OCIManifest: Codable, Equatable {
|
||||
}
|
||||
|
||||
func uncompressedDiskSize() -> UInt64? {
|
||||
guard let value = annotations[uncompressedDiskSizeAnnotation] else {
|
||||
guard let value = annotations?[uncompressedDiskSizeAnnotation] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -200,6 +200,8 @@ class Registry {
|
||||
}
|
||||
|
||||
for try await part in response.body {
|
||||
try Task.checkCancellation()
|
||||
|
||||
try handler(part)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import Foundation
|
||||
|
||||
struct PassphraseGenerator: Sequence {
|
||||
func makeIterator() -> PassphraseIterator {
|
||||
PassphraseIterator()
|
||||
}
|
||||
}
|
||||
|
||||
struct PassphraseIterator: IteratorProtocol {
|
||||
mutating func next() -> String? {
|
||||
passphrases[Int(arc4random_uniform(UInt32(passphrases.count)))]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,6 @@ struct Root: AsyncParsableCommand {
|
||||
let sigintSrc = DispatchSource.makeSignalSource(signal: SIGINT)
|
||||
sigintSrc.setEventHandler {
|
||||
task.cancel()
|
||||
Darwin.exit(1)
|
||||
}
|
||||
sigintSrc.activate()
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import Foundation
|
||||
|
||||
enum SoftnetError: Error {
|
||||
case InitializationFailed(why: String)
|
||||
}
|
||||
|
||||
class Softnet {
|
||||
private let process = Process()
|
||||
|
||||
let vmFD: Int32
|
||||
|
||||
init(vmMACAddress: String) throws {
|
||||
let binaryName = "softnet"
|
||||
|
||||
guard let executableURL = Self.resolveBinaryPath(binaryName) else {
|
||||
throw SoftnetError.InitializationFailed(why: "\(binaryName) not found in PATH")
|
||||
}
|
||||
|
||||
let fds = UnsafeMutablePointer<Int32>.allocate(capacity: MemoryLayout<Int>.stride * 2)
|
||||
|
||||
let ret = socketpair(AF_UNIX, SOCK_DGRAM, 0, fds)
|
||||
if ret != 0 {
|
||||
throw SoftnetError.InitializationFailed(why: "socketpair() failed with exit code \(ret)")
|
||||
}
|
||||
|
||||
vmFD = fds[0]
|
||||
let softnetFD = fds[1]
|
||||
|
||||
try setSocketBuffers(vmFD, 1 * 1024 * 1024);
|
||||
try setSocketBuffers(softnetFD, 1 * 1024 * 1024);
|
||||
|
||||
process.executableURL = executableURL
|
||||
process.arguments = ["--vm-fd", String(STDIN_FILENO), "--vm-mac-address", vmMACAddress]
|
||||
process.standardInput = FileHandle(fileDescriptor: softnetFD, closeOnDealloc: false)
|
||||
}
|
||||
|
||||
func run() throws {
|
||||
try process.run()
|
||||
}
|
||||
|
||||
func stop() throws {
|
||||
process.interrupt()
|
||||
process.waitUntilExit()
|
||||
}
|
||||
|
||||
private static func resolveBinaryPath(_ name: String) -> URL? {
|
||||
guard let path = ProcessInfo.processInfo.environment["PATH"] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
for pathComponent in path.split(separator: ":") {
|
||||
let url = URL(fileURLWithPath: String(pathComponent))
|
||||
.appendingPathComponent(name, isDirectory: false)
|
||||
|
||||
if FileManager.default.fileExists(atPath: url.path) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func setSocketBuffers(_ fd: Int32, _ sizeBytes: Int) throws {
|
||||
var option_value = sizeBytes
|
||||
let option_len = socklen_t(MemoryLayout<Int>.size)
|
||||
|
||||
var ret = setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &option_value, option_len)
|
||||
if ret != 0 {
|
||||
throw SoftnetError.InitializationFailed(why: "setsockopt(SO_RCVBUF) returned \(ret)")
|
||||
}
|
||||
|
||||
ret = setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &option_value, option_len)
|
||||
if ret != 0 {
|
||||
throw SoftnetError.InitializationFailed(why: "setsockopt(SO_SNDBUF) returned \(ret)")
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
-8
@@ -23,17 +23,24 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
// VM's config
|
||||
var config: VMConfig
|
||||
|
||||
init(vmDir: VMDirectory) throws {
|
||||
var softnet: Softnet? = nil
|
||||
|
||||
init(vmDir: VMDirectory, withSoftnet: Bool = false) throws {
|
||||
let auxStorage = VZMacAuxiliaryStorage(contentsOf: vmDir.nvramURL)
|
||||
|
||||
name = vmDir.name
|
||||
config = try VMConfig.init(fromURL: vmDir.configURL)
|
||||
|
||||
let configuration = try VM.craftConfiguration(diskURL: vmDir.diskURL, auxStorage: auxStorage, vmConfig: config)
|
||||
// Initialize the virtual machine and its configuration
|
||||
if withSoftnet {
|
||||
softnet = try Softnet(vmMACAddress: config.macAddress.string)
|
||||
}
|
||||
|
||||
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, auxStorage: auxStorage, vmConfig: config,
|
||||
softnet: softnet)
|
||||
virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||
|
||||
super.init()
|
||||
|
||||
virtualMachine.delegate = self
|
||||
}
|
||||
|
||||
@@ -87,7 +94,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
init(vmDir: VMDirectory, ipswURL: URL?, diskSizeGB: UInt16) async throws {
|
||||
init(vmDir: VMDirectory, ipswURL: URL?, diskSizeGB: UInt16, withSoftnet: Bool = false) async throws {
|
||||
let ipswURL = ipswURL != nil ? ipswURL! : try await VM.retrieveLatestIPSW();
|
||||
|
||||
// Load the restore image and try to get the requirements
|
||||
@@ -120,11 +127,15 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
try config.save(toURL: vmDir.configURL)
|
||||
|
||||
// Initialize the virtual machine and its configuration
|
||||
let configuration = try VM.craftConfiguration(diskURL: vmDir.diskURL, auxStorage: auxStorage, vmConfig: config)
|
||||
if withSoftnet {
|
||||
softnet = try Softnet(vmMACAddress: config.macAddress.string)
|
||||
}
|
||||
|
||||
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, auxStorage: auxStorage, vmConfig: config,
|
||||
softnet: softnet)
|
||||
virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||
|
||||
super.init()
|
||||
|
||||
virtualMachine.delegate = self
|
||||
|
||||
// Run automated installation
|
||||
@@ -143,6 +154,10 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
}
|
||||
|
||||
func run(_ recovery: Bool) async throws {
|
||||
if let softnet = softnet {
|
||||
try softnet.run()
|
||||
}
|
||||
|
||||
try await virtualMachine.start(recovery)
|
||||
|
||||
await withTaskCancellationHandler(operation: {
|
||||
@@ -158,9 +173,18 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let softnet = softnet {
|
||||
try softnet.stop();
|
||||
}
|
||||
}
|
||||
|
||||
static func craftConfiguration(diskURL: URL, auxStorage: VZMacAuxiliaryStorage, vmConfig: VMConfig) throws -> VZVirtualMachineConfiguration {
|
||||
static func craftConfiguration(
|
||||
diskURL: URL,
|
||||
auxStorage: VZMacAuxiliaryStorage,
|
||||
vmConfig: VMConfig,
|
||||
softnet: Softnet? = nil
|
||||
) throws -> VZVirtualMachineConfiguration {
|
||||
let configuration = VZVirtualMachineConfiguration()
|
||||
|
||||
// Boot loader
|
||||
@@ -212,7 +236,13 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
|
||||
// Networking
|
||||
let vio = VZVirtioNetworkDeviceConfiguration()
|
||||
vio.attachment = VZNATNetworkDeviceAttachment()
|
||||
|
||||
if let softnet = softnet {
|
||||
let fh = FileHandle.init(fileDescriptor: softnet.vmFD)
|
||||
vio.attachment = VZFileHandleNetworkDeviceAttachment(fileHandle: fh)
|
||||
} else {
|
||||
vio.attachment = VZNATNetworkDeviceAttachment()
|
||||
}
|
||||
vio.macAddress = vmConfig.macAddress
|
||||
configuration.networkDevices = [vio]
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import Foundation
|
||||
import Dynamic
|
||||
import Virtualization
|
||||
|
||||
class VNCWrapper {
|
||||
private let password: String
|
||||
private let vnc: Dynamic
|
||||
|
||||
init(virtualMachine: VZVirtualMachine) {
|
||||
password = Array(PassphraseGenerator().prefix(4)).joined(separator: "-")
|
||||
let securityConfiguration = Dynamic._VZVNCAuthenticationSecurityConfiguration(password: password)
|
||||
vnc = Dynamic._VZVNCServer(port: 0, queue: DispatchQueue.global(),
|
||||
securityConfiguration: securityConfiguration)
|
||||
vnc.virtualMachine = virtualMachine
|
||||
vnc.start()
|
||||
}
|
||||
|
||||
func open() async {
|
||||
do {
|
||||
let port = try await Self.waitForPort(vnc: vnc)
|
||||
|
||||
let url = URL(string: "vnc://:\(password)@127.0.0.1:\(port)")!
|
||||
print("Opening \(url)...")
|
||||
|
||||
if ProcessInfo.processInfo.environment["CI"] == nil {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
} catch {
|
||||
print("Failed to retrieve a VNC server port: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func stop() throws {
|
||||
vnc.stop()
|
||||
}
|
||||
|
||||
deinit {
|
||||
try? stop()
|
||||
}
|
||||
|
||||
private static func waitForPort(vnc: Dynamic) async throws -> UInt16 {
|
||||
while true {
|
||||
// Port is 0 shortly after start(),
|
||||
// but will be initialized later
|
||||
if let port = vnc.port.asUInt16, port != 0 {
|
||||
return port
|
||||
}
|
||||
|
||||
// Wait 50 ms.
|
||||
try await Task.sleep(nanoseconds: 50_000_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user