Update dependencies and styles (#12)

* Update deps and .editorconfig

* run config
This commit is contained in:
Fedor Korotkov 2022-03-24 11:54:49 -04:00 committed by GitHub
parent dc1e502404
commit 49d430b3c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 734 additions and 720 deletions

6
.editorconfig Normal file
View File

@ -0,0 +1,6 @@
root = true
[*]
indent_style = space
indent_size = 2
insert_final_newline = true

8
.run/tart run.run.xml Normal file
View File

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tart run" type="SwiftPackageManagerRunConfiguration" factoryName="Swift Package Run" PROGRAM_PARAMS="run latest" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="tart" TARGET_NAME="tart" CONFIG_NAME="tart" RUN_TARGET_PROJECT_NAME="tart" RUN_TARGET_NAME="tart" WAS_MODIFIED="">
<method v="2">
<option name="SPM.BUILD_TASK_PROVIDER" enabled="true" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="sign debug" run_configuration_type="ShConfigurationType" />
</method>
</configuration>
</component>

View File

@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "e394bf350e38cb100b6bc4172834770ede1b7232",
"version" : "1.0.3"
"revision" : "82905286cc3f0fa8adc4674bf49437cab65a8373",
"version" : "1.1.1"
}
}
],

View File

@ -3,17 +3,17 @@
import PackageDescription
let package = Package(
name: "Tart",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.3"),
],
targets: [
.executableTarget(name: "tart",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]),
]
name: "Tart",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.1"),
],
targets: [
.executableTarget(name: "tart",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]),
]
)

View File

@ -3,114 +3,114 @@ import Network
import Virtualization
struct ARPCommandFailedError: Error, CustomStringConvertible {
var terminationReason: Process.TerminationReason
var terminationStatus: Int32
var description: String {
var reason: String
switch terminationReason {
case .exit:
reason = "exit code \(terminationStatus)"
case .uncaughtSignal:
reason = "uncaught signal"
default:
reason = "unknown reason"
}
return "arp command failed: \(reason)"
var terminationReason: Process.TerminationReason
var terminationStatus: Int32
var description: String {
var reason: String
switch terminationReason {
case .exit:
reason = "exit code \(terminationStatus)"
case .uncaughtSignal:
reason = "uncaught signal"
default:
reason = "unknown reason"
}
return "arp command failed: \(reason)"
}
}
struct ARPCommandYieldedInvalidOutputError: Error, CustomStringConvertible {
var explanation: String
var description: String {
"arp command yielded invalid output: \(explanation)"
}
var explanation: String
var description: String {
"arp command yielded invalid output: \(explanation)"
}
}
struct ARPCacheInternalError: Error, CustomStringConvertible {
var explanation: String
var description: String {
"ARPCache internal error: \(explanation)"
}
var explanation: String
var description: String {
"ARPCache internal error: \(explanation)"
}
}
struct ARPCache {
static func ResolveMACAddress(macAddress: MACAddress, bridgeOnly: Bool = true) throws -> IPv4Address? {
let process = Process.init()
process.executableURL = URL.init(fileURLWithPath: "/usr/sbin/arp")
process.arguments = ["-an"]
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe
process.standardInput = FileHandle.nullDevice
try process.run()
process.waitUntilExit()
if !(process.terminationReason == .exit && process.terminationStatus == 0) {
throw ARPCommandFailedError(
terminationReason: process.terminationReason,
terminationStatus: process.terminationStatus)
}
guard let rawLines = try pipe.fileHandleForReading.readToEnd() else {
throw ARPCommandYieldedInvalidOutputError(explanation: "empty output")
}
let lines = String(decoding: rawLines, as: UTF8.self)
static func ResolveMACAddress(macAddress: MACAddress, bridgeOnly: Bool = true) throws -> IPv4Address? {
let process = Process.init()
process.executableURL = URL.init(fileURLWithPath: "/usr/sbin/arp")
process.arguments = ["-an"]
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe
process.standardInput = FileHandle.nullDevice
try process.run()
process.waitUntilExit()
if !(process.terminationReason == .exit && process.terminationStatus == 0) {
throw ARPCommandFailedError(
terminationReason: process.terminationReason,
terminationStatus: process.terminationStatus)
}
guard let rawLines = try pipe.fileHandleForReading.readToEnd() else {
throw ARPCommandYieldedInvalidOutputError(explanation: "empty output")
}
let lines = String(decoding: rawLines, as: UTF8.self)
.trimmingCharacters(in: .whitespacesAndNewlines)
.components(separatedBy: "\n")
// Based on https://opensource.apple.com/source/network_cmds/network_cmds-606.40.2/arp.tproj/arp.c.auto.html
let regex = try NSRegularExpression(pattern: #"^.* \((?<ip>.*)\) at (?<mac>.*) on (?<interface>.*) .*$"#)
for line in lines {
let nsLineRange = NSRange(line.startIndex..<line.endIndex, in: line)
guard let match = regex.firstMatch(in: line, range: nsLineRange) else {
throw ARPCommandYieldedInvalidOutputError(explanation: "unparseable entry \"\(line)\"")
}
let rawIP = try match.getCaptureGroup(name: "ip", for: line)
guard let ip = IPv4Address(rawIP) else {
throw ARPCommandYieldedInvalidOutputError(explanation: "failed to parse IPv4 address \(rawIP)")
}
// Based on https://opensource.apple.com/source/network_cmds/network_cmds-606.40.2/arp.tproj/arp.c.auto.html
let regex = try NSRegularExpression(pattern: #"^.* \((?<ip>.*)\) at (?<mac>.*) on (?<interface>.*) .*$"#)
let rawMAC = try match.getCaptureGroup(name: "mac", for: line)
guard let mac = MACAddress(fromString: rawMAC) else {
throw ARPCommandYieldedInvalidOutputError(explanation: "failed to parse MAC address \(rawMAC)")
}
for line in lines {
let nsLineRange = NSRange(line.startIndex..<line.endIndex, in: line)
let interface = try match.getCaptureGroup(name: "interface", for: line)
if bridgeOnly && !interface.starts(with: "bridge") {
continue
}
if macAddress == mac {
return ip
}
}
return nil
guard let match = regex.firstMatch(in: line, range: nsLineRange) else {
throw ARPCommandYieldedInvalidOutputError(explanation: "unparseable entry \"\(line)\"")
}
let rawIP = try match.getCaptureGroup(name: "ip", for: line)
guard let ip = IPv4Address(rawIP) else {
throw ARPCommandYieldedInvalidOutputError(explanation: "failed to parse IPv4 address \(rawIP)")
}
let rawMAC = try match.getCaptureGroup(name: "mac", for: line)
guard let mac = MACAddress(fromString: rawMAC) else {
throw ARPCommandYieldedInvalidOutputError(explanation: "failed to parse MAC address \(rawMAC)")
}
let interface = try match.getCaptureGroup(name: "interface", for: line)
if bridgeOnly && !interface.starts(with: "bridge") {
continue
}
if macAddress == mac {
return ip
}
}
return nil
}
}
extension NSTextCheckingResult {
func getCaptureGroup(name: String, for string: String) throws -> String {
let nsRange = self.range(withName: name)
if nsRange.location == NSNotFound {
throw ARPCacheInternalError(explanation: "attempted to retrieve non-existent named capture group \(name)")
}
guard let range = Range.init(nsRange, in: string) else {
throw ARPCacheInternalError(explanation: "failed to convert NSRange to Range")
}
return String(string[range])
func getCaptureGroup(name: String, for string: String) throws -> String {
let nsRange = self.range(withName: name)
if nsRange.location == NSNotFound {
throw ARPCacheInternalError(explanation: "attempted to retrieve non-existent named capture group \(name)")
}
guard let range = Range.init(nsRange, in: string) else {
throw ARPCacheInternalError(explanation: "failed to convert NSRange to Range")
}
return String(string[range])
}
}

View File

@ -1,21 +1,21 @@
import Foundation
struct MACAddress: Equatable, CustomStringConvertible {
var mac: [UInt8] = Array(repeating: 0, count: 6)
init?(fromString: String) {
let components = fromString.components(separatedBy: ":")
if components.count != 6 {
return nil
}
for (index, component) in components.enumerated() {
mac[index] = UInt8(component, radix: 16)!
}
var mac: [UInt8] = Array(repeating: 0, count: 6)
init?(fromString: String) {
let components = fromString.components(separatedBy: ":")
if components.count != 6 {
return nil
}
var description: String {
return String(format: "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5])
for (index, component) in components.enumerated() {
mac[index] = UInt8(component, radix: 16)!
}
}
var description: String {
return String(format: "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5])
}
}

View File

@ -3,38 +3,34 @@ import Foundation
import SystemConfiguration
import Virtualization
struct Clone: ParsableCommand {
static var configuration = CommandConfiguration(abstract: "Clone a VM")
@Argument(help: "source VM name")
var sourceName: String
@Argument(help: "new VM name")
var newName: String
func run() throws {
Task {
do {
let vmStorage = VMStorage()
let sourceVMDir = try vmStorage.read(sourceName)
let newVMDir = try vmStorage.create(newName)
try FileManager.default.copyItem(at: sourceVMDir.configURL, to: newVMDir.configURL)
try FileManager.default.copyItem(at: sourceVMDir.nvramURL, to: newVMDir.nvramURL)
try FileManager.default.copyItem(at: sourceVMDir.diskURL, to: newVMDir.diskURL)
var newVMConfig = try VMConfig(fromURL: newVMDir.configURL)
newVMConfig.macAddress = VZMACAddress.randomLocallyAdministered()
try newVMConfig.save(toURL: newVMDir.configURL)
Foundation.exit(0)
} catch {
print(error)
Foundation.exit(1)
}
}
dispatchMain()
struct Clone: AsyncParsableCommand {
static var configuration = CommandConfiguration(abstract: "Clone a VM")
@Argument(help: "source VM name")
var sourceName: String
@Argument(help: "new VM name")
var newName: String
func run() async throws {
do {
let vmStorage = VMStorage()
let sourceVMDir = try vmStorage.read(sourceName)
let newVMDir = try vmStorage.create(newName)
try FileManager.default.copyItem(at: sourceVMDir.configURL, to: newVMDir.configURL)
try FileManager.default.copyItem(at: sourceVMDir.nvramURL, to: newVMDir.nvramURL)
try FileManager.default.copyItem(at: sourceVMDir.diskURL, to: newVMDir.diskURL)
var newVMConfig = try VMConfig(fromURL: newVMDir.configURL)
newVMConfig.macAddress = VZMACAddress.randomLocallyAdministered()
try newVMConfig.save(toURL: newVMDir.configURL)
Foundation.exit(0)
} catch {
print(error)
Foundation.exit(1)
}
}
}

View File

@ -3,39 +3,35 @@ import Dispatch
import SwiftUI
import Foundation
struct Create: ParsableCommand {
static var configuration = CommandConfiguration(abstract: "Create a VM")
@Argument(help: "VM name")
var name: String
@Option(help: ArgumentHelp("Path to the IPSW file (or \"latest\") to fetch the latest appropriate IPSW", valueName: "path")) var fromIPSW: String?
func validate() throws {
if fromIPSW == nil {
throw ValidationError("Please specify a --from-ipsw option!")
}
struct Create: AsyncParsableCommand {
static var configuration = CommandConfiguration(abstract: "Create a VM")
@Argument(help: "VM name")
var name: String
@Option(help: ArgumentHelp("Path to the IPSW file (or \"latest\") to fetch the latest appropriate IPSW", valueName: "path")) var fromIPSW: String?
func validate() throws {
if fromIPSW == nil {
throw ValidationError("Please specify a --from-ipsw option!")
}
func run() throws {
Task {
do {
let vmDir = try VMStorage().create(name)
if fromIPSW! == "latest" {
_ = try await VM(vmDir: vmDir, ipswURL: nil)
} else {
_ = try await VM(vmDir: vmDir, ipswURL: URL(fileURLWithPath: fromIPSW!))
}
Foundation.exit(0)
} catch {
print(error)
Foundation.exit(1)
}
}
dispatchMain()
}
func run() async throws {
do {
let vmDir = try VMStorage().create(name)
if fromIPSW! == "latest" {
_ = try await VM(vmDir: vmDir, ipswURL: nil)
} else {
_ = try await VM(vmDir: vmDir, ipswURL: URL(fileURLWithPath: fromIPSW!))
}
Foundation.exit(0)
} catch {
print(error)
Foundation.exit(1)
}
}
}

View File

@ -2,25 +2,21 @@ import ArgumentParser
import Dispatch
import SwiftUI
struct Delete: ParsableCommand {
static var configuration = CommandConfiguration(abstract: "Delete a VM")
@Argument(help: "VM name")
var name: String
func run() throws {
Task {
do {
try VMStorage().delete(name)
Foundation.exit(0)
} catch {
print(error)
Foundation.exit(1)
}
}
dispatchMain()
struct Delete: AsyncParsableCommand {
static var configuration = CommandConfiguration(abstract: "Delete a VM")
@Argument(help: "VM name")
var name: String
func run() async throws {
do {
try VMStorage().delete(name)
Foundation.exit(0)
} catch {
print(error)
Foundation.exit(1)
}
}
}

View File

@ -2,35 +2,31 @@ import ArgumentParser
import Foundation
import SystemConfiguration
struct IP: ParsableCommand {
static var configuration = CommandConfiguration(abstract: "Get VM's IP address")
@Argument(help: "VM name")
var name: String
func run() throws {
Task {
do {
let vmDir = try VMStorage().read(name)
let vmConfig = try VMConfig.init(fromURL: vmDir.configURL)
let vmMacAddress = MACAddress(fromString: vmConfig.macAddress.string)!
guard let ip = try ARPCache.ResolveMACAddress(macAddress: vmMacAddress) else {
print("no IP address found, is your VM running?")
Foundation.exit(1)
}
print(ip)
struct IP: AsyncParsableCommand {
static var configuration = CommandConfiguration(abstract: "Get VM's IP address")
Foundation.exit(0)
} catch {
print(error)
Foundation.exit(1)
}
}
dispatchMain()
@Argument(help: "VM name")
var name: String
func run() async throws {
do {
let vmDir = try VMStorage().read(name)
let vmConfig = try VMConfig.init(fromURL: vmDir.configURL)
let vmMacAddress = MACAddress(fromString: vmConfig.macAddress.string)!
guard let ip = try ARPCache.ResolveMACAddress(macAddress: vmMacAddress) else {
print("no IP address found, is your VM running?")
Foundation.exit(1)
}
print(ip)
Foundation.exit(0)
} catch {
print(error)
Foundation.exit(1)
}
}
}

View File

@ -2,24 +2,20 @@ import ArgumentParser
import Dispatch
import SwiftUI
struct List: ParsableCommand {
static var configuration = CommandConfiguration(abstract: "List created VMs")
func run() throws {
Task {
do {
for vmURL in try VMStorage().list() {
print(vmURL)
}
Foundation.exit(0)
} catch {
print(error)
Foundation.exit(1)
}
}
dispatchMain()
struct List: AsyncParsableCommand {
static var configuration = CommandConfiguration(abstract: "List created VMs")
func run() async throws {
do {
for vmURL in try VMStorage().list() {
print(vmURL)
}
Foundation.exit(0)
} catch {
print(error)
Foundation.exit(1)
}
}
}

View File

@ -1,7 +0,0 @@
import ArgumentParser
struct Root: ParsableCommand {
static var configuration = CommandConfiguration(
commandName: "tart",
subcommands: [Create.self, Clone.self, Run.self, List.self, IP.self, Delete.self])
}

View File

@ -5,61 +5,61 @@ import Virtualization
var vm: VM?
struct Run: ParsableCommand {
static var configuration = CommandConfiguration(abstract: "Run a VM")
@Argument(help: "VM name")
var name: String
@Flag var noGraphics: Bool = false
func run() throws {
let vmDir = try VMStorage().read(name)
vm = try VM(vmDir: vmDir)
Task {
do {
try await vm!.run()
Foundation.exit(0)
} catch {
print(error)
Foundation.exit(1)
}
}
if noGraphics {
dispatchMain()
} else {
// UI mumbo-jumbo
let nsApp = NSApplication.shared
nsApp.setActivationPolicy(.regular)
nsApp.activate(ignoringOtherApps: true)
struct MainApp : App {
var body: some Scene {
WindowGroup {
VMView(vm: vm!)
}
}
}
MainApp.main()
}
struct Run: AsyncParsableCommand {
static var configuration = CommandConfiguration(abstract: "Run a VM")
@Argument(help: "VM name")
var name: String
@Flag var noGraphics: Bool = false
func run() async throws {
let vmDir = try VMStorage().read(name)
vm = try VM(vmDir: vmDir)
Task {
do {
try await vm!.run()
Foundation.exit(0)
} catch {
print(error)
Foundation.exit(1)
}
}
if noGraphics {
dispatchMain()
} else {
// UI mumbo-jumbo
let nsApp = await NSApplication.shared
await nsApp.setActivationPolicy(.regular)
await nsApp.activate(ignoringOtherApps: true)
struct MainApp: App {
var body: some Scene {
WindowGroup {
VMView(vm: vm!)
}
}
}
await MainApp.main()
}
}
}
struct VMView: NSViewRepresentable {
typealias NSViewType = VZVirtualMachineView
@ObservedObject var vm: VM
func makeNSView(context: Context) -> NSViewType {
VZVirtualMachineView()
}
func updateNSView(_ nsView: NSViewType, context: Context) {
nsView.virtualMachine = vm.virtualMachine
}
typealias NSViewType = VZVirtualMachineView
@ObservedObject var vm: VM
func makeNSView(context: Context) -> NSViewType {
VZVirtualMachineView()
}
func updateNSView(_ nsView: NSViewType, context: Context) {
nsView.virtualMachine = vm.virtualMachine
}
}

View File

@ -1,46 +1,46 @@
import Foundation
public protocol Logger {
func appendNewLine(_ line: String) -> Void
func updateLastLine(_ line: String) -> Void
func appendNewLine(_ line: String) -> Void
func updateLastLine(_ line: String) -> Void
}
var defaultLogger: Logger = {
if ProcessInfo.processInfo.environment["CI"] != nil {
return SimpleConsoleLogger()
} else {
return InteractiveConsoleLogger()
}
if ProcessInfo.processInfo.environment["CI"] != nil {
return SimpleConsoleLogger()
} else {
return InteractiveConsoleLogger()
}
}()
public class InteractiveConsoleLogger: Logger {
private let eraseCursorDown = "\u{001B}[J" // clear entire line
private let moveUp = "\u{001B}[1A" // move one line up
private let moveBeginningOfLine = "\r" //
private let eraseCursorDown = "\u{001B}[J" // clear entire line
private let moveUp = "\u{001B}[1A" // move one line up
private let moveBeginningOfLine = "\r" //
public init() {
public init() {
}
}
public func appendNewLine(_ line: String) {
print(line, terminator: "\n")
}
public func appendNewLine(_ line: String) {
print(line, terminator: "\n")
}
public func updateLastLine(_ line: String) {
print(moveUp, moveBeginningOfLine, eraseCursorDown, line, separator: "", terminator: "\n")
}
public func updateLastLine(_ line: String) {
print(moveUp, moveBeginningOfLine, eraseCursorDown, line, separator: "", terminator: "\n")
}
}
public class SimpleConsoleLogger: Logger {
public init() {
public init() {
}
}
public func appendNewLine(_ line: String) {
print(line, terminator: "\n")
}
public func appendNewLine(_ line: String) {
print(line, terminator: "\n")
}
public func updateLastLine(_ line: String) {
print(line, terminator: "\n")
}
public func updateLastLine(_ line: String) {
print(line, terminator: "\n")
}
}

View File

@ -1,21 +1,21 @@
import Foundation
public class ProgressObserver: NSObject {
@objc var progressToObserve: Progress
var observation: NSKeyValueObservation?
@objc var progressToObserve: Progress
var observation: NSKeyValueObservation?
public init(_ progress: Progress) {
progressToObserve = progress
}
func log(_ renderer: Logger) {
renderer.appendNewLine(ProgressObserver.lineToRender(progressToObserve))
observation = observe(\.progressToObserve.fractionCompleted) { progress, _ in
renderer.updateLastLine(ProgressObserver.lineToRender(self.progressToObserve))
}
}
public init(_ progress: Progress) {
progressToObserve = progress
}
private static func lineToRender(_ progress: Progress) -> String {
String(Int(100 * progress.fractionCompleted)) + "%"
func log(_ renderer: Logger) {
renderer.appendNewLine(ProgressObserver.lineToRender(progressToObserve))
observation = observe(\.progressToObserve.fractionCompleted) { progress, _ in
renderer.updateLastLine(ProgressObserver.lineToRender(self.progressToObserve))
}
}
private static func lineToRender(_ progress: Progress) -> String {
String(Int(100 * progress.fractionCompleted)) + "%"
}
}

View File

@ -1,17 +1,17 @@
import Foundation
public class URLSessionLogger: NSObject, URLSessionTaskDelegate {
let renderer: Logger
let renderer: Logger
public init(_ renderer: Logger) {
self.renderer = renderer
}
public init(_ renderer: Logger) {
self.renderer = renderer
}
public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
renderer.updateLastLine(URLSessionLogger.lineToRender(task.progress))
}
public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
renderer.updateLastLine(URLSessionLogger.lineToRender(task.progress))
}
private static func lineToRender(_ progress: Progress) -> String {
String(100 * progress.completedUnitCount / progress.totalUnitCount) + "%"
}
private static func lineToRender(_ progress: Progress) -> String {
String(100 * progress.completedUnitCount / progress.totalUnitCount) + "%"
}
}

8
Sources/tart/Root.swift Normal file
View File

@ -0,0 +1,8 @@
import ArgumentParser
@main
struct Root: AsyncParsableCommand {
static var configuration = CommandConfiguration(
commandName: "tart",
subcommands: [Create.self, Clone.self, Run.self, List.self, IP.self, Delete.self])
}

View File

@ -1,222 +1,235 @@
import Foundation
import Virtualization
struct UnsupportedRestoreImageError: Error {}
struct NoMainScreenFoundError: Error {}
struct DownloadFailed: Error {}
struct UnsupportedRestoreImageError: Error {
}
struct NoMainScreenFoundError: Error {
}
struct DownloadFailed: Error {
}
class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
// Virtualization.Framework's virtual machine
@Published var virtualMachine: VZVirtualMachine
// Semaphore used to communicate with the VZVirtualMachineDelegate
var sema = DispatchSemaphore(value: 0)
// VM's config
var vmConfig: VMConfig
init(vmDir: VMDirectory) throws {
let auxStorage = VZMacAuxiliaryStorage(contentsOf: vmDir.nvramURL)
self.vmConfig = try VMConfig.init(fromURL: vmDir.configURL)
let configuration = try VM.craftConfiguration(
diskURL: vmDir.diskURL,
ecid: vmConfig.ecid,
auxStorage: auxStorage,
hardwareModel: vmConfig.hardwareModel,
cpuCount: vmConfig.cpuCount,
memorySize: vmConfig.memorySize,
macAddress: vmConfig.macAddress
)
self.virtualMachine = VZVirtualMachine(configuration: configuration)
super.init()
self.virtualMachine.delegate = self
// Virtualization.Framework's virtual machine
@Published var virtualMachine: VZVirtualMachine
// Semaphore used to communicate with the VZVirtualMachineDelegate
var sema = DispatchSemaphore(value: 0)
// VM's config
var vmConfig: VMConfig
init(vmDir: VMDirectory) throws {
let auxStorage = VZMacAuxiliaryStorage(contentsOf: vmDir.nvramURL)
self.vmConfig = try VMConfig.init(fromURL: vmDir.configURL)
let configuration = try VM.craftConfiguration(
diskURL: vmDir.diskURL,
ecid: vmConfig.ecid,
auxStorage: auxStorage,
hardwareModel: vmConfig.hardwareModel,
cpuCount: vmConfig.cpuCount,
memorySize: vmConfig.memorySize,
macAddress: vmConfig.macAddress
)
self.virtualMachine = VZVirtualMachine(configuration: configuration)
super.init()
self.virtualMachine.delegate = self
}
static func retrieveLatestIPSW() async throws -> URL {
defaultLogger.appendNewLine("Looking up the latest supported IPSW...")
let image = try await withCheckedThrowingContinuation { continuation in
VZMacOSRestoreImage.fetchLatestSupported() { result in
continuation.resume(with: result)
}
}
static func retrieveLatestIPSW() async throws -> URL {
defaultLogger.appendNewLine("Looking up the latest supported IPSW...")
let image = try await withCheckedThrowingContinuation { continuation in
VZMacOSRestoreImage.fetchLatestSupported() { result in continuation.resume(with: result) }
}
let ipswCacheFolder = VMStorage.tartCacheDir.appendingPathComponent("IPSWs", isDirectory: true)
try FileManager.default.createDirectory(at: ipswCacheFolder, withIntermediateDirectories: true)
let expectedIPSWLocation = ipswCacheFolder.appendingPathComponent("\(image.buildVersion).ipsw", isDirectory: false)
if FileManager.default.fileExists(atPath: expectedIPSWLocation.path) {
defaultLogger.appendNewLine("Using cached *.ipsw file...")
return expectedIPSWLocation
}
let ipswCacheFolder = VMStorage.tartCacheDir.appendingPathComponent("IPSWs", isDirectory: true)
try FileManager.default.createDirectory(at: ipswCacheFolder, withIntermediateDirectories: true)
defaultLogger.appendNewLine("Fetching \(expectedIPSWLocation.lastPathComponent)...")
let expectedIPSWLocation = ipswCacheFolder.appendingPathComponent("\(image.buildVersion).ipsw", isDirectory: false)
let data: Data = try await withCheckedThrowingContinuation { continuation in
let downloadedTask = URLSession.shared.dataTask(with: image.url) { data, response, error in
if FileManager.default.fileExists(atPath: expectedIPSWLocation.path) {
defaultLogger.appendNewLine("Using cached *.ipsw file...")
return expectedIPSWLocation
}
defaultLogger.appendNewLine("Fetching \(expectedIPSWLocation.lastPathComponent)...")
let data: Data = try await withCheckedThrowingContinuation { continuation in
let downloadedTask = URLSession.shared.dataTask(with: image.url) { data, response, error in
if error != nil {
continuation.resume(throwing: error!)
return
}
continuation.resume(throwing: error!)
return
}
if (data == nil) {
continuation.resume(throwing: DownloadFailed())
return
continuation.resume(throwing: DownloadFailed())
return
}
continuation.resume(returning: data!)
}
ProgressObserver(downloadedTask.progress).log(defaultLogger)
downloadedTask.resume()
}
try data.write(to: expectedIPSWLocation, options: [.atomic])
return expectedIPSWLocation
}
ProgressObserver(downloadedTask.progress).log(defaultLogger)
downloadedTask.resume()
}
init(vmDir: VMDirectory, ipswURL: URL?, diskSize: UInt64 = 32 * 1024 * 1024 * 1024) async throws {
let ipswURL = ipswURL != nil ? ipswURL! : try await VM.retrieveLatestIPSW();
// Load the restore image and try to get the requirements
// that match both the image and our platform
let image = try await withCheckedThrowingContinuation { continuation in
VZMacOSRestoreImage.load(from: ipswURL) { result in continuation.resume(with: result) }
}
guard let requirements = image.mostFeaturefulSupportedConfiguration else { throw UnsupportedRestoreImageError() }
// Create NVRAM
let auxStorage = try VZMacAuxiliaryStorage(creatingStorageAt: vmDir.nvramURL, hardwareModel: requirements.hardwareModel)
// Create disk
FileManager.default.createFile(atPath: vmDir.diskURL.path, contents: nil, attributes: nil)
let diskFileHandle = try FileHandle.init(forWritingTo: vmDir.diskURL)
try diskFileHandle.truncate(atOffset: diskSize)
try diskFileHandle.close()
// Create config
self.vmConfig = VMConfig(
hardwareModel: requirements.hardwareModel,
cpuCount: requirements.minimumSupportedCPUCount,
memorySize: requirements.minimumSupportedMemorySize
)
try self.vmConfig.save(toURL: vmDir.configURL)
// Initialize the virtual machine and its configuration
let configuration = try VM.craftConfiguration(
diskURL: vmDir.diskURL,
ecid: self.vmConfig.ecid,
auxStorage: auxStorage,
hardwareModel: requirements.hardwareModel,
cpuCount: self.vmConfig.cpuCount,
memorySize: self.vmConfig.memorySize,
macAddress: self.vmConfig.macAddress
)
self.virtualMachine = VZVirtualMachine(configuration: configuration)
super.init()
self.virtualMachine.delegate = self
// Run automated installation
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
DispatchQueue.main.async {
try data.write(to: expectedIPSWLocation, options: [.atomic])
return expectedIPSWLocation
}
init(vmDir: VMDirectory, ipswURL: URL?, diskSize: UInt64 = 32 * 1024 * 1024 * 1024) async throws {
let ipswURL = ipswURL != nil ? ipswURL! : try await VM.retrieveLatestIPSW();
// Load the restore image and try to get the requirements
// that match both the image and our platform
let image = try await withCheckedThrowingContinuation { continuation in
VZMacOSRestoreImage.load(from: ipswURL) { result in
continuation.resume(with: result)
}
}
guard let requirements = image.mostFeaturefulSupportedConfiguration else {
throw UnsupportedRestoreImageError()
}
// Create NVRAM
let auxStorage = try VZMacAuxiliaryStorage(creatingStorageAt: vmDir.nvramURL, hardwareModel: requirements.hardwareModel)
// Create disk
FileManager.default.createFile(atPath: vmDir.diskURL.path, contents: nil, attributes: nil)
let diskFileHandle = try FileHandle.init(forWritingTo: vmDir.diskURL)
try diskFileHandle.truncate(atOffset: diskSize)
try diskFileHandle.close()
// Create config
self.vmConfig = VMConfig(
hardwareModel: requirements.hardwareModel,
cpuCount: requirements.minimumSupportedCPUCount,
memorySize: requirements.minimumSupportedMemorySize
)
try self.vmConfig.save(toURL: vmDir.configURL)
// Initialize the virtual machine and its configuration
let configuration = try VM.craftConfiguration(
diskURL: vmDir.diskURL,
ecid: self.vmConfig.ecid,
auxStorage: auxStorage,
hardwareModel: requirements.hardwareModel,
cpuCount: self.vmConfig.cpuCount,
memorySize: self.vmConfig.memorySize,
macAddress: self.vmConfig.macAddress
)
self.virtualMachine = VZVirtualMachine(configuration: configuration)
super.init()
self.virtualMachine.delegate = self
// Run automated installation
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
DispatchQueue.main.async {
let installer = VZMacOSInstaller(virtualMachine: self.virtualMachine, restoringFromImageAt: ipswURL)
defaultLogger.appendNewLine("Installing OS...")
ProgressObserver(installer.progress).log(defaultLogger)
installer.install { result in continuation.resume(with: result) }
}
}
ProgressObserver(installer.progress).log(defaultLogger)
installer.install { result in
continuation.resume(with: result)
}
}
}
func run() async throws {
try await withCheckedThrowingContinuation { continuation in
DispatchQueue.main.async {
}
func run() async throws {
try await withCheckedThrowingContinuation { continuation in
DispatchQueue.main.async {
self.virtualMachine.start(completionHandler: { result in
continuation.resume(with: result)
})
}
}
sema.wait()
continuation.resume(with: result)
})
}
}
static func craftConfiguration(
diskURL: URL,
ecid: VZMacMachineIdentifier,
auxStorage: VZMacAuxiliaryStorage,
hardwareModel: VZMacHardwareModel,
cpuCount: Int,
memorySize: UInt64,
macAddress: VZMACAddress
) throws -> VZVirtualMachineConfiguration {
let configuration = VZVirtualMachineConfiguration()
// Boot loader
configuration.bootLoader = VZMacOSBootLoader()
// CPU and memory
configuration.cpuCount = cpuCount
configuration.memorySize = memorySize
// Platform
let platform = VZMacPlatformConfiguration()
platform.machineIdentifier = ecid
platform.auxiliaryStorage = auxStorage
platform.hardwareModel = hardwareModel
configuration.platform = platform
// Display
let graphicsDeviceConfiguration = VZMacGraphicsDeviceConfiguration()
guard let mainScreen = NSScreen.main else {
throw NoMainScreenFoundError()
}
graphicsDeviceConfiguration.displays = [
VZMacGraphicsDisplayConfiguration(for: mainScreen, sizeInPoints: mainScreen.frame.size)
]
configuration.graphicsDevices = [graphicsDeviceConfiguration]
// Keyboard and mouse
configuration.keyboards = [VZUSBKeyboardConfiguration()]
configuration.pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()]
// Networking
let vio = VZVirtioNetworkDeviceConfiguration()
vio.attachment = VZNATNetworkDeviceAttachment()
vio.macAddress = macAddress
configuration.networkDevices = [vio]
// Storage
let attachment = try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false)
let storage = VZVirtioBlockDeviceConfiguration(attachment: attachment)
configuration.storageDevices = [storage]
// Entropy
configuration.entropyDevices = [VZVirtioEntropyDeviceConfiguration()]
try configuration.validate()
return configuration
}
func guestDidStop(_ virtualMachine: VZVirtualMachine) {
print("guest has stopped the virtual machine")
sema.signal()
}
func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: Error) {
print("guest has stopped the virtual machine due to error")
sema.signal()
}
func virtualMachine(_ virtualMachine: VZVirtualMachine, networkDevice: VZNetworkDevice, attachmentWasDisconnectedWithError error: Error) {
print("virtual machine's network attachment has been disconnected")
sema.signal()
sema.wait()
}
static func craftConfiguration(
diskURL: URL,
ecid: VZMacMachineIdentifier,
auxStorage: VZMacAuxiliaryStorage,
hardwareModel: VZMacHardwareModel,
cpuCount: Int,
memorySize: UInt64,
macAddress: VZMACAddress
) throws -> VZVirtualMachineConfiguration {
let configuration = VZVirtualMachineConfiguration()
// Boot loader
configuration.bootLoader = VZMacOSBootLoader()
// CPU and memory
configuration.cpuCount = cpuCount
configuration.memorySize = memorySize
// Platform
let platform = VZMacPlatformConfiguration()
platform.machineIdentifier = ecid
platform.auxiliaryStorage = auxStorage
platform.hardwareModel = hardwareModel
configuration.platform = platform
// Display
let graphicsDeviceConfiguration = VZMacGraphicsDeviceConfiguration()
guard let mainScreen = NSScreen.main else {
throw NoMainScreenFoundError()
}
graphicsDeviceConfiguration.displays = [
VZMacGraphicsDisplayConfiguration(for: mainScreen, sizeInPoints: mainScreen.frame.size)
]
configuration.graphicsDevices = [graphicsDeviceConfiguration]
// Keyboard and mouse
configuration.keyboards = [VZUSBKeyboardConfiguration()]
configuration.pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()]
// Networking
let vio = VZVirtioNetworkDeviceConfiguration()
vio.attachment = VZNATNetworkDeviceAttachment()
vio.macAddress = macAddress
configuration.networkDevices = [vio]
// Storage
let attachment = try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false)
let storage = VZVirtioBlockDeviceConfiguration(attachment: attachment)
configuration.storageDevices = [storage]
// Entropy
configuration.entropyDevices = [VZVirtioEntropyDeviceConfiguration()]
try configuration.validate()
return configuration
}
func guestDidStop(_ virtualMachine: VZVirtualMachine) {
print("guest has stopped the virtual machine")
sema.signal()
}
func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: Error) {
print("guest has stopped the virtual machine due to error")
sema.signal()
}
func virtualMachine(_ virtualMachine: VZVirtualMachine, networkDevice: VZNetworkDevice, attachmentWasDisconnectedWithError error: Error) {
print("virtual machine's network attachment has been disconnected")
sema.signal()
}
}

View File

@ -1,96 +1,96 @@
import Virtualization
enum CodingKeys: String, CodingKey {
case version
case ecid
case hardwareModel
case cpuCount
case memorySize
case macAddress
case version
case ecid
case hardwareModel
case cpuCount
case memorySize
case macAddress
}
struct VMConfig: Encodable, Decodable {
var version: Int = 0
var ecid: VZMacMachineIdentifier
var hardwareModel: VZMacHardwareModel
var cpuCount: Int
var memorySize: UInt64
var macAddress: VZMACAddress
init(
ecid: VZMacMachineIdentifier = VZMacMachineIdentifier(),
hardwareModel: VZMacHardwareModel,
cpuCount: Int,
memorySize: UInt64,
macAddress: VZMACAddress = VZMACAddress.randomLocallyAdministered()
) {
self.ecid = ecid
self.hardwareModel = hardwareModel
self.cpuCount = cpuCount
self.memorySize = memorySize
self.macAddress = macAddress
var version: Int = 0
var ecid: VZMacMachineIdentifier
var hardwareModel: VZMacHardwareModel
var cpuCount: Int
var memorySize: UInt64
var macAddress: VZMACAddress
init(
ecid: VZMacMachineIdentifier = VZMacMachineIdentifier(),
hardwareModel: VZMacHardwareModel,
cpuCount: Int,
memorySize: UInt64,
macAddress: VZMACAddress = VZMACAddress.randomLocallyAdministered()
) {
self.ecid = ecid
self.hardwareModel = hardwareModel
self.cpuCount = cpuCount
self.memorySize = memorySize
self.macAddress = macAddress
}
init(fromURL: URL) throws {
let jsonConfigData = try FileHandle.init(forReadingFrom: fromURL).readToEnd()!
self = try JSONDecoder().decode(VMConfig.self, from: jsonConfigData)
}
func save(toURL: URL) throws {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
try encoder.encode(self).write(to: toURL)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.version = try container.decode(Int.self, forKey: .version)
let encodedECID = try container.decode(String.self, forKey: .ecid)
guard let data = Data.init(base64Encoded: encodedECID) else {
throw DecodingError.dataCorruptedError(forKey: .ecid,
in: container,
debugDescription: "failed to initialize Data using the provided value")
}
init(fromURL: URL) throws {
let jsonConfigData = try FileHandle.init(forReadingFrom: fromURL).readToEnd()!
self = try JSONDecoder().decode(VMConfig.self, from: jsonConfigData)
guard let ecid = VZMacMachineIdentifier.init(dataRepresentation: data) else {
throw DecodingError.dataCorruptedError(forKey: .ecid,
in: container,
debugDescription: "failed to initialize VZMacMachineIdentifier using the provided value")
}
func save(toURL: URL) throws {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
try encoder.encode(self).write(to: toURL)
self.ecid = ecid
let encodedHardwareModel = try container.decode(String.self, forKey: .hardwareModel)
guard let data = Data.init(base64Encoded: encodedHardwareModel) else {
throw DecodingError.dataCorruptedError(forKey: .hardwareModel, in: container, debugDescription: "")
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.version = try container.decode(Int.self, forKey: .version)
let encodedECID = try container.decode(String.self, forKey: .ecid)
guard let data = Data.init(base64Encoded: encodedECID) else {
throw DecodingError.dataCorruptedError(forKey: .ecid,
in: container,
debugDescription: "failed to initialize Data using the provided value")
}
guard let ecid = VZMacMachineIdentifier.init(dataRepresentation: data) else {
throw DecodingError.dataCorruptedError(forKey: .ecid,
in: container,
debugDescription: "failed to initialize VZMacMachineIdentifier using the provided value")
}
self.ecid = ecid
let encodedHardwareModel = try container.decode(String.self, forKey: .hardwareModel)
guard let data = Data.init(base64Encoded: encodedHardwareModel) else {
throw DecodingError.dataCorruptedError(forKey: .hardwareModel, in: container, debugDescription: "")
}
guard let hardwareModel = VZMacHardwareModel.init(dataRepresentation: data) else {
throw DecodingError.dataCorruptedError(forKey: .hardwareModel, in: container, debugDescription: "")
}
self.hardwareModel = hardwareModel
self.cpuCount = try container.decode(Int.self, forKey: .cpuCount)
self.memorySize = try container.decode(UInt64.self, forKey: .memorySize)
let encodedMacAddress = try container.decode(String.self, forKey: .macAddress)
guard let macAddress = VZMACAddress.init(string: encodedMacAddress) else {
throw DecodingError.dataCorruptedError(
guard let hardwareModel = VZMacHardwareModel.init(dataRepresentation: data) else {
throw DecodingError.dataCorruptedError(forKey: .hardwareModel, in: container, debugDescription: "")
}
self.hardwareModel = hardwareModel
self.cpuCount = try container.decode(Int.self, forKey: .cpuCount)
self.memorySize = try container.decode(UInt64.self, forKey: .memorySize)
let encodedMacAddress = try container.decode(String.self, forKey: .macAddress)
guard let macAddress = VZMACAddress.init(string: encodedMacAddress) else {
throw DecodingError.dataCorruptedError(
forKey: .hardwareModel,
in: container,
debugDescription: "failed to initialize VZMacAddress using the provided value")
}
self.macAddress = macAddress
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.version, forKey: .version)
try container.encode(self.ecid.dataRepresentation.base64EncodedString(), forKey: .ecid)
try container.encode(self.hardwareModel.dataRepresentation.base64EncodedString(), forKey: .hardwareModel)
try container.encode(self.cpuCount, forKey: .cpuCount)
try container.encode(self.memorySize, forKey: .memorySize)
try container.encode(self.macAddress.string, forKey: .macAddress)
}
self.macAddress = macAddress
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.version, forKey: .version)
try container.encode(self.ecid.dataRepresentation.base64EncodedString(), forKey: .ecid)
try container.encode(self.hardwareModel.dataRepresentation.base64EncodedString(), forKey: .hardwareModel)
try container.encode(self.cpuCount, forKey: .cpuCount)
try container.encode(self.memorySize, forKey: .memorySize)
try container.encode(self.macAddress.string, forKey: .macAddress)
}
}

View File

@ -1,32 +1,41 @@
import Foundation
struct UninitializedVMDirectoryError: Error {}
struct AlreadyInitializedVMDirectoryError: Error {}
struct UninitializedVMDirectoryError: Error {
}
struct AlreadyInitializedVMDirectoryError: Error {
}
struct VMDirectory {
var baseURL: URL
var configURL: URL { self.baseURL.appendingPathComponent("config.json") }
var diskURL: URL { self.baseURL.appendingPathComponent("disk.bin") }
var nvramURL: URL { self.baseURL.appendingPathComponent("nvram.bin") }
var initialized: Bool {
FileManager.default.fileExists(atPath: configURL.path) &&
FileManager.default.fileExists(atPath: diskURL.path) &&
FileManager.default.fileExists(atPath: nvramURL.path)
var baseURL: URL
var configURL: URL {
self.baseURL.appendingPathComponent("config.json")
}
var diskURL: URL {
self.baseURL.appendingPathComponent("disk.bin")
}
var nvramURL: URL {
self.baseURL.appendingPathComponent("nvram.bin")
}
var initialized: Bool {
FileManager.default.fileExists(atPath: configURL.path) &&
FileManager.default.fileExists(atPath: diskURL.path) &&
FileManager.default.fileExists(atPath: nvramURL.path)
}
func initialize() throws {
if initialized {
throw AlreadyInitializedVMDirectoryError()
}
func initialize() throws {
if initialized {
throw AlreadyInitializedVMDirectoryError()
}
try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true, attributes: nil)
}
func validate() throws {
if !initialized {
throw UninitializedVMDirectoryError()
}
try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true, attributes: nil)
}
func validate() throws {
if !initialized {
throw UninitializedVMDirectoryError()
}
}
}

View File

@ -1,58 +1,58 @@
import Foundation
struct VMStorage {
public static let tartHomeDir: URL = FileManager.default
.homeDirectoryForCurrentUser
.appendingPathComponent(".tart", isDirectory: true)
public static let tartVMsDir: URL = tartHomeDir.appendingPathComponent("vms", isDirectory: true)
public static let tartCacheDir: URL = tartHomeDir.appendingPathComponent("cache", isDirectory: true)
func create(_ name: String) throws -> VMDirectory {
let vmDir = VMDirectory(baseURL: vmURL(name))
try vmDir.initialize()
return vmDir
}
func read(_ name: String) throws -> VMDirectory {
let vmDir = VMDirectory(baseURL: vmURL(name))
try vmDir.validate()
return vmDir
}
func delete(_ name: String) throws {
try FileManager.default.removeItem(at: vmURL(name))
}
func list() throws -> [URL] {
do {
return try FileManager.default.contentsOfDirectory(
public static let tartHomeDir: URL = FileManager.default
.homeDirectoryForCurrentUser
.appendingPathComponent(".tart", isDirectory: true)
public static let tartVMsDir: URL = tartHomeDir.appendingPathComponent("vms", isDirectory: true)
public static let tartCacheDir: URL = tartHomeDir.appendingPathComponent("cache", isDirectory: true)
func create(_ name: String) throws -> VMDirectory {
let vmDir = VMDirectory(baseURL: vmURL(name))
try vmDir.initialize()
return vmDir
}
func read(_ name: String) throws -> VMDirectory {
let vmDir = VMDirectory(baseURL: vmURL(name))
try vmDir.validate()
return vmDir
}
func delete(_ name: String) throws {
try FileManager.default.removeItem(at: vmURL(name))
}
func list() throws -> [URL] {
do {
return try FileManager.default.contentsOfDirectory(
at: VMStorage.tartVMsDir,
includingPropertiesForKeys: [.isDirectoryKey],
options: .skipsSubdirectoryDescendants)
} catch {
if error.isFileNotFound() {
return []
}
throw error
}
}
private func vmURL(_ name: String) -> URL {
return URL.init(
fileURLWithPath: name,
isDirectory: true,
relativeTo: VMStorage.tartVMsDir)
} catch {
if error.isFileNotFound() {
return []
}
throw error
}
}
private func vmURL(_ name: String) -> URL {
return URL.init(
fileURLWithPath: name,
isDirectory: true,
relativeTo: VMStorage.tartVMsDir)
}
}
extension Error {
func isFileNotFound() -> Bool {
return (self as NSError).code == NSFileReadNoSuchFileError
}
func isFileNotFound() -> Bool {
return (self as NSError).code == NSFileReadNoSuchFileError
}
}

View File

@ -1,3 +0,0 @@
import SwiftUI
Root.main()