Compare commits

..
7 Commits
Author SHA1 Message Date
Nikolay Edigaryev 166e3e570f Ditch AsyncHTTPClient in favor of URLSession (#260) 2022-10-05 00:39:36 +04:00
Nikolay Edigaryev 7a2c20ba30 tart create: support fetching URLs specified in the --from-ipsw option (#256)
* tart create: support fetching URLs specified in the --from-ipsw option

* Use x-amz-meta-digest-sha256 header to cache IPSWs
2022-09-27 23:36:18 +04:00
Nikolay Edigaryev e90d53eceb tart delete: allow removing multiple VMs at once (#257) 2022-09-27 10:30:44 -04:00
Nikolay Edigaryev afbc2e0764 tart ip: keep waiting for the /var/db/dhcpd_leases file to appear (#254) 2022-09-22 10:24:14 -04:00
Fedor Korotkov 0229138bd5 Fixed wait delay for IP command (#250)
I initially wanted a one-second wait but made a human mistake.

Fixes #249
2022-09-19 21:44:14 +04:00
Nikolay EdigaryevandPete Goldsmith 4d08e6365e Set line-buffered output for stdout and introduce --graphics for tart run (#248)
* Set line-buffered output for stdout

* tart run: introduce --graphics

* Update Sources/tart/Commands/Run.swift

Co-authored-by: Pete Goldsmith <peter.n.goldsmith@gmail.com>

* Update Sources/tart/Commands/Run.swift

Co-authored-by: Pete Goldsmith <peter.n.goldsmith@gmail.com>

Co-authored-by: Pete Goldsmith <peter.n.goldsmith@gmail.com>
2022-09-19 21:43:37 +04:00
Nikolay Edigaryev 8cd68ea8ef Revert com.apple.vm.networking entitlement (#247) 2022-09-14 11:01:16 -04:00
14 changed files with 189 additions and 188 deletions
+8 -62
View File
@@ -1,14 +1,5 @@
{ {
"pins" : [ "pins" : [
{
"identity" : "async-http-client",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swift-server/async-http-client",
"state" : {
"revision" : "df87a860fdc41a595d5ca67f74cde9adbccc099a",
"version" : "1.11.4"
}
},
{ {
"identity" : "dynamic", "identity" : "dynamic",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@@ -37,12 +28,12 @@
} }
}, },
{ {
"identity" : "swift-atomics", "identity" : "swift-async-algorithms",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git", "location" : "https://github.com/apple/swift-async-algorithms",
"state" : { "state" : {
"revision" : "919eb1d83e02121cdb434c7bfc1f0c66ef17febe", "branch" : "main",
"version" : "1.0.2" "revision" : "f05e450f0b909c0e80670a47516c4b9700b9e5da"
} }
}, },
{ {
@@ -55,57 +46,12 @@
} }
}, },
{ {
"identity" : "swift-log", "identity" : "swift-collections",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git", "location" : "https://github.com/apple/swift-collections.git",
"state" : { "state" : {
"revision" : "5d66f7ba25daf4f94100e7022febf3c75e37a6c7", "revision" : "f504716c27d2e5d4144fa4794b12129301d17729",
"version" : "1.4.2" "version" : "1.0.3"
}
},
{
"identity" : "swift-nio",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "124119f0bb12384cef35aa041d7c3a686108722d",
"version" : "2.40.0"
}
},
{
"identity" : "swift-nio-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-extras.git",
"state" : {
"revision" : "8eea84ec6144167354387ef9244b0939f5852dc8",
"version" : "1.11.0"
}
},
{
"identity" : "swift-nio-http2",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-http2.git",
"state" : {
"revision" : "108ac15087ea9b79abb6f6742699cf31de0e8772",
"version" : "1.22.0"
}
},
{
"identity" : "swift-nio-ssl",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl.git",
"state" : {
"revision" : "1750873bce84b4129b5303655cce2c3d35b9ed3a",
"version" : "2.19.0"
}
},
{
"identity" : "swift-nio-transport-services",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-transport-services.git",
"state" : {
"revision" : "1a4692acb88156e3da1b0c6732a8a38b2a744166",
"version" : "1.12.0"
} }
}, },
{ {
+2 -2
View File
@@ -13,15 +13,15 @@ let package = Package(
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.2"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.2"),
.package(url: "https://github.com/mhdhejazi/Dynamic", branch: "master"), .package(url: "https://github.com/mhdhejazi/Dynamic", branch: "master"),
.package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.9.2"), .package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.9.2"),
.package(url: "https://github.com/swift-server/async-http-client", from: "1.11.4"),
.package(url: "https://github.com/apple/swift-algorithms", from: "1.0.0"), .package(url: "https://github.com/apple/swift-algorithms", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-async-algorithms", branch: "main"),
.package(url: "https://github.com/malcommac/SwiftDate", from: "6.3.1") .package(url: "https://github.com/malcommac/SwiftDate", from: "6.3.1")
], ],
targets: [ targets: [
.executableTarget(name: "tart", dependencies: [ .executableTarget(name: "tart", dependencies: [
.product(name: "Algorithms", package: "swift-algorithms"), .product(name: "Algorithms", package: "swift-algorithms"),
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
.product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "AsyncHTTPClient", package: "async-http-client"),
.product(name: "Dynamic", package: "Dynamic"), .product(name: "Dynamic", package: "Dynamic"),
.product(name: "Parsing", package: "swift-parsing"), .product(name: "Parsing", package: "swift-parsing"),
.product(name: "SwiftDate", package: "SwiftDate"), .product(name: "SwiftDate", package: "SwiftDate"),
-2
View File
@@ -4,7 +4,5 @@
<dict> <dict>
<key>com.apple.security.virtualization</key> <key>com.apple.security.virtualization</key>
<true/> <true/>
<key>com.apple.vm.networking</key>
<true/>
</dict> </dict>
</plist> </plist>
+9 -3
View File
@@ -9,7 +9,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 \"latest\") to fetch the latest appropriate IPSW", 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"))
var fromIPSW: String? var fromIPSW: String?
@Flag(help: "create a Linux VM") @Flag(help: "create a Linux VM")
@@ -34,11 +34,17 @@ struct Create: AsyncParsableCommand {
try await withTaskCancellationHandler(operation: { try await withTaskCancellationHandler(operation: {
if let fromIPSW = fromIPSW { if let fromIPSW = fromIPSW {
let ipswURL: URL
if fromIPSW == "latest" { if fromIPSW == "latest" {
_ = try await VM(vmDir: tmpVMDir, ipswURL: nil, diskSizeGB: diskSize) ipswURL = try await VM.latestIPSWURL()
} else if fromIPSW.starts(with: "http://") || fromIPSW.starts(with: "https://") {
ipswURL = URL(string: fromIPSW)!
} else { } else {
_ = try await VM(vmDir: tmpVMDir, ipswURL: URL(fileURLWithPath: fromIPSW), diskSizeGB: diskSize) ipswURL = URL(fileURLWithPath: fromIPSW)
} }
_ = try await VM(vmDir: tmpVMDir, ipswURL: ipswURL, diskSizeGB: diskSize)
} }
if linux { if linux {
+4 -2
View File
@@ -6,11 +6,13 @@ struct Delete: AsyncParsableCommand {
static var configuration = CommandConfiguration(abstract: "Delete a VM") static var configuration = CommandConfiguration(abstract: "Delete a VM")
@Argument(help: "VM name") @Argument(help: "VM name")
var name: String var name: [String]
func run() async throws { func run() async throws {
do { do {
try VMStorageHelper.delete(name) for it in name {
try VMStorageHelper.delete(it)
}
Foundation.exit(0) Foundation.exit(0)
} catch { } catch {
+3 -2
View File
@@ -44,11 +44,12 @@ struct IP: AsyncParsableCommand {
let waitUntil = Calendar.current.date(byAdding: .second, value: Int(secondsToWait), to: Date.now)! let waitUntil = Calendar.current.date(byAdding: .second, value: Int(secondsToWait), to: Date.now)!
repeat { repeat {
if let ip = try Leases().resolveMACAddress(macAddress: vmMACAddress) { if let leases = try Leases(), let ip = try leases.resolveMACAddress(macAddress: vmMACAddress) {
return ip return ip
} }
try await Task.sleep(nanoseconds: 1_000_000) // wait a second
try await Task.sleep(nanoseconds: 1_000_000_000)
} while Date.now < waitUntil } while Date.now < waitUntil
return nil return nil
+9 -1
View File
@@ -19,6 +19,9 @@ struct Run: AsyncParsableCommand {
discussion: "Useful for integrating Tart VMs into other tools.\nUse `tart ip` in order to get an IP for SSHing or VNCing into the VM.")) discussion: "Useful for integrating Tart VMs into other tools.\nUse `tart ip` in order to get an IP for SSHing or VNCing into the VM."))
var noGraphics: Bool = false var noGraphics: Bool = false
@Flag(help: "Force open a UI window, even when VNC is enabled.")
var graphics: Bool = false
@Flag(help: "Boot into recovery mode") @Flag(help: "Boot into recovery mode")
var recovery: Bool = false var recovery: Bool = false
@@ -69,6 +72,10 @@ struct Run: AsyncParsableCommand {
if withSoftnet && netBridged != nil { if withSoftnet && netBridged != nil {
throw ValidationError("--with-softnet and --net-bridged are mutually exclusive") throw ValidationError("--with-softnet and --net-bridged are mutually exclusive")
} }
if graphics && noGraphics {
throw ValidationError("--graphics and --no-graphics are mutually exclusive")
}
} }
@MainActor @MainActor
@@ -129,7 +136,8 @@ struct Run: AsyncParsableCommand {
} }
sigintSrc.activate() sigintSrc.activate()
if noGraphics || vnc || vncExperimental { let useVNCWithoutGraphics = (vnc || vncExperimental) && !graphics
if noGraphics || useVNCWithoutGraphics {
dispatchMain() dispatchMain()
} else { } else {
runUI() runUI()
+2 -2
View File
@@ -9,8 +9,8 @@ class IPSWCache: PrunableStorage {
try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true)
} }
func locationFor(image: VZMacOSRestoreImage) -> URL { func locationFor(fileName: String) -> URL {
baseURL.appendingPathComponent("\(image.buildVersion).ipsw", isDirectory: false) baseURL.appendingPathComponent(fileName, isDirectory: false)
} }
func prunables() throws -> [Prunable] { func prunables() throws -> [Prunable] {
+11 -4
View File
@@ -19,14 +19,21 @@ enum LeasesError: Error {
class Leases { class Leases {
private let leases: [MACAddress : Lease] private let leases: [MACAddress : Lease]
convenience init() throws { convenience init?() throws {
try self.init(URL(fileURLWithPath: "/var/db/dhcpd_leases")) try self.init(URL(fileURLWithPath: "/var/db/dhcpd_leases"))
} }
convenience init(_ fromURL: URL) throws { convenience init?(_ fromURL: URL) throws {
let fileContents = try String(contentsOf: fromURL, encoding: .utf8) do {
let urlContents = try String(contentsOf: fromURL, encoding: .utf8)
try self.init(urlContents)
} catch {
if error.isFileNotFound() {
return nil
}
try self.init(fileContents) throw error
}
} }
init(_ fromString: String) throws { init(_ fromString: String) throws {
+87 -77
View File
@@ -1,28 +1,44 @@
import Foundation import Foundation
import NIOCore
import NIOHTTP1
import AsyncHTTPClient
import Algorithms import Algorithms
import NIOPosix import AsyncAlgorithms
let chunkSizeBytes = 1 * 1024 * 1024
enum RegistryError: Error { enum RegistryError: Error {
case UnexpectedHTTPStatusCode(when: String, code: UInt, details: String = "") case UnexpectedHTTPStatusCode(when: String, code: Int, details: String = "")
case MissingLocationHeader case MissingLocationHeader
case AuthFailed(why: String, details: String = "") case AuthFailed(why: String, details: String = "")
case MalformedHeader(why: String) case MalformedHeader(why: String)
} }
extension HTTPClientResponse.Body { enum HTTPMethod: String {
func readTextResponse() async throws -> String? { case GET = "GET"
let data = try await readResponse() case POST = "POST"
return String(decoding: data, as: UTF8.self) case PUT = "PUT"
} case PATCH = "PATCH"
}
func readResponse() async throws -> Data { enum HTTPCode: Int {
case Ok = 200
case Created = 201
case Accepted = 202
case Unauthorized = 401
}
extension Data {
func asText() async throws -> String? {
String(decoding: self, as: UTF8.self)
}
}
extension URLSession.AsyncBytes {
func asData() async throws -> Data {
var result = Data() var result = Data()
for try await part in self {
result.append(Data(buffer: part)) for try await chunk in chunks(ofCount: chunkSizeBytes) {
result += chunk
} }
return result return result
} }
} }
@@ -78,14 +94,6 @@ struct TokenResponse: Decodable, Authentication {
} }
class Registry { class Registry {
private let httpClient = HTTPClient(
eventLoopGroupProvider: .shared(MultiThreadedEventLoopGroup(numberOfThreads: 1))
)
deinit {
try! httpClient.syncShutdown()
}
let baseURL: URL let baseURL: URL
let namespace: String let namespace: String
let credentialsProviders: [CredentialsProvider] let credentialsProviders: [CredentialsProvider]
@@ -114,43 +122,43 @@ class Registry {
} }
func ping() async throws { func ping() async throws {
let response = try await endpointRequest(.GET, "/v2/") let (_, response) = try await endpointRequest(.GET, "/v2/")
if response.status != .ok { if response.statusCode != HTTPCode.Ok.rawValue {
throw RegistryError.UnexpectedHTTPStatusCode(when: "doing ping", code: response.status.code) throw RegistryError.UnexpectedHTTPStatusCode(when: "doing ping", code: response.statusCode)
} }
} }
func pushManifest(reference: String, manifest: OCIManifest) async throws -> String { func pushManifest(reference: String, manifest: OCIManifest) async throws -> String {
let manifestJSON = try manifest.toJSON() let manifestJSON = try manifest.toJSON()
let response = try await endpointRequest(.PUT, "\(namespace)/manifests/\(reference)", let (bytes, response) = try await endpointRequest(.PUT, "\(namespace)/manifests/\(reference)",
headers: ["Content-Type": manifest.mediaType], headers: ["Content-Type": manifest.mediaType],
body: manifestJSON) body: manifestJSON)
if response.status != .created { if response.statusCode != HTTPCode.Created.rawValue {
throw RegistryError.UnexpectedHTTPStatusCode(when: "pushing manifest", code: response.status.code, throw RegistryError.UnexpectedHTTPStatusCode(when: "pushing manifest", code: response.statusCode,
details: try await response.body.readTextResponse() ?? "") details: try await bytes.asData().asText() ?? "")
} }
return Digest.hash(manifestJSON) return Digest.hash(manifestJSON)
} }
public func pullManifest(reference: String) async throws -> (OCIManifest, Data) { public func pullManifest(reference: String) async throws -> (OCIManifest, Data) {
let response = try await endpointRequest(.GET, "\(namespace)/manifests/\(reference)", let (bytes, response) = try await endpointRequest(.GET, "\(namespace)/manifests/\(reference)",
headers: ["Accept": ociManifestMediaType]) headers: ["Accept": ociManifestMediaType])
if response.status != .ok { if response.statusCode != HTTPCode.Ok.rawValue {
let body = try await response.body.readTextResponse() let body = try await bytes.asData().asText()
throw RegistryError.UnexpectedHTTPStatusCode(when: "pulling manifest", code: response.status.code, throw RegistryError.UnexpectedHTTPStatusCode(when: "pulling manifest", code: response.statusCode,
details: body ?? "") details: body ?? "")
} }
let manifestData = try await response.body.readResponse() let manifestData = try await bytes.asData()
let manifest = try OCIManifest(fromJSON: manifestData) let manifest = try OCIManifest(fromJSON: manifestData)
return (manifest, manifestData) return (manifest, manifestData)
} }
private func uploadLocationFromResponse(_ response: HTTPClientResponse) throws -> URLComponents { private func uploadLocationFromResponse(_ response: HTTPURLResponse) throws -> URLComponents {
guard let uploadLocationRaw = response.headers.first(name: "Location") else { guard let uploadLocationRaw = response.value(forHTTPHeaderField: "Location") else {
throw RegistryError.MissingLocationHeader throw RegistryError.MissingLocationHeader
} }
@@ -163,11 +171,11 @@ class Registry {
public func pushBlob(fromData: Data, chunkSizeMb: Int = 0) async throws -> String { public func pushBlob(fromData: Data, chunkSizeMb: Int = 0) async throws -> String {
// Initiate a blob upload // Initiate a blob upload
let postResponse = try await endpointRequest(.POST, "\(namespace)/blobs/uploads/", let (bytes, postResponse) = try await endpointRequest(.POST, "\(namespace)/blobs/uploads/",
headers: ["Content-Length": "0"]) headers: ["Content-Length": "0"])
if postResponse.status != .accepted { if postResponse.statusCode != HTTPCode.Accepted.rawValue {
let body = try await postResponse.body.readTextResponse() let body = try await bytes.asData().asText()
throw RegistryError.UnexpectedHTTPStatusCode(when: "pushing blob (POST)", code: postResponse.status.code, throw RegistryError.UnexpectedHTTPStatusCode(when: "pushing blob (POST)", code: postResponse.statusCode,
details: body ?? "") details: body ?? "")
} }
@@ -178,7 +186,7 @@ class Registry {
if chunkSizeMb == 0 { if chunkSizeMb == 0 {
// monolithic upload // monolithic upload
let response = try await rawRequest( let (bytes, response) = try await rawRequest(
.PUT, .PUT,
uploadLocation, uploadLocation,
headers: [ headers: [
@@ -187,10 +195,10 @@ class Registry {
parameters: ["digest": digest], parameters: ["digest": digest],
body: fromData body: fromData
) )
if response.status != .created { if response.statusCode != HTTPCode.Created.rawValue {
let body = try await response.body.readTextResponse() let body = try await bytes.asData().asText()
throw RegistryError.UnexpectedHTTPStatusCode(when: "pushing blob (PUT) to \(uploadLocation)", throw RegistryError.UnexpectedHTTPStatusCode(when: "pushing blob (PUT) to \(uploadLocation)",
code: response.status.code, details: body ?? "") code: response.statusCode, details: body ?? "")
} }
return digest return digest
} }
@@ -200,7 +208,7 @@ class Registry {
let chunks = fromData.chunks(ofCount: chunkSizeMb == 0 ? fromData.count : chunkSizeMb * 1_000_000) let chunks = fromData.chunks(ofCount: chunkSizeMb == 0 ? fromData.count : chunkSizeMb * 1_000_000)
for (index, chunk) in chunks.enumerated() { for (index, chunk) in chunks.enumerated() {
let lastChunk = index == (chunks.count - 1) let lastChunk = index == (chunks.count - 1)
let response = try await rawRequest( let (bytes, response) = try await rawRequest(
lastChunk ? .PUT : .PATCH, lastChunk ? .PUT : .PATCH,
uploadLocation, uploadLocation,
headers: [ headers: [
@@ -210,11 +218,11 @@ class Registry {
parameters: lastChunk ? ["digest": digest] : [:], parameters: lastChunk ? ["digest": digest] : [:],
body: chunk body: chunk
) )
let expectedStatus: HTTPResponseStatus = lastChunk ? .created : .accepted let expectedStatus = lastChunk ? HTTPCode.Created.rawValue : HTTPCode.Accepted.rawValue
if response.status != expectedStatus { if response.statusCode != expectedStatus {
let body = try await response.body.readTextResponse() let body = try await bytes.asData().asText()
throw RegistryError.UnexpectedHTTPStatusCode(when: "streaming blob to \(uploadLocation)", throw RegistryError.UnexpectedHTTPStatusCode(when: "streaming blob to \(uploadLocation)",
code: response.status.code, details: body ?? "") code: response.statusCode, details: body ?? "")
} }
uploadedBytes += chunk.count uploadedBytes += chunk.count
// Update location for the next chunk // Update location for the next chunk
@@ -224,18 +232,18 @@ class Registry {
return digest return digest
} }
public func pullBlob(_ digest: String, handler: (ByteBuffer) throws -> Void) async throws { public func pullBlob(_ digest: String, handler: (Data) throws -> Void) async throws {
let response = try await endpointRequest(.GET, "\(namespace)/blobs/\(digest)") let (bytes, response) = try await endpointRequest(.GET, "\(namespace)/blobs/\(digest)")
if response.status != .ok { if response.statusCode != HTTPCode.Ok.rawValue {
let body = try await response.body.readTextResponse() let body = try await bytes.asData().asText()
throw RegistryError.UnexpectedHTTPStatusCode(when: "pulling blob", code: response.status.code, throw RegistryError.UnexpectedHTTPStatusCode(when: "pulling blob", code: response.statusCode,
details: body ?? "") details: body ?? "")
} }
for try await part in response.body { for try await part in bytes.chunks(ofCount: chunkSizeBytes) {
try Task.checkCancellation() try Task.checkCancellation()
try handler(part) try handler(Data(part))
} }
} }
@@ -245,7 +253,7 @@ class Registry {
headers: Dictionary<String, String> = Dictionary(), headers: Dictionary<String, String> = Dictionary(),
parameters: Dictionary<String, String> = Dictionary(), parameters: Dictionary<String, String> = Dictionary(),
body: Data? = nil body: Data? = nil
) async throws -> HTTPClientResponse { ) async throws -> (URLSession.AsyncBytes, HTTPURLResponse) {
let url = URL(string: endpoint, relativeTo: baseURL)! let url = URL(string: endpoint, relativeTo: baseURL)!
let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)! let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)!
@@ -259,7 +267,7 @@ class Registry {
parameters: Dictionary<String, String> = Dictionary(), parameters: Dictionary<String, String> = Dictionary(),
body: Data? = nil, body: Data? = nil,
doAuth: Bool = true doAuth: Bool = true
) async throws -> HTTPClientResponse { ) async throws -> (URLSession.AsyncBytes, HTTPURLResponse) {
var urlComponents = urlComponents var urlComponents = urlComponents
if urlComponents.queryItems == nil && !parameters.isEmpty { if urlComponents.queryItems == nil && !parameters.isEmpty {
@@ -269,14 +277,14 @@ class Registry {
URLQueryItem(name: key, value: value) URLQueryItem(name: key, value: value)
}) })
var request = HTTPClientRequest(url: urlComponents.string!) var request = URLRequest(url: urlComponents.url!)
request.method = method request.httpMethod = method.rawValue
for (key, value) in headers { for (key, value) in headers {
request.headers.add(name: key, value: value) request.addValue(value, forHTTPHeaderField: key)
} }
if body != nil { if let body = body {
request.headers.add(name: "Content-Length", value: "\(body!.count)") request.addValue("\(body.count)", forHTTPHeaderField: "Content-Length")
request.body = HTTPClientRequest.Body.bytes(body!) request.httpBody = body
} }
// Invalidate token if it has expired // Invalidate token if it has expired
@@ -284,19 +292,19 @@ class Registry {
currentAuthToken = nil currentAuthToken = nil
} }
var response = try await authAwareRequest(request: request) var (bytes, response) = try await authAwareRequest(request: request)
if doAuth && response.status == .unauthorized { if doAuth && response.statusCode == HTTPCode.Unauthorized.rawValue {
try await auth(response: response) try await auth(response: response)
response = try await authAwareRequest(request: request) (bytes, response) = try await authAwareRequest(request: request)
} }
return response return (bytes, response)
} }
private func auth(response: HTTPClientResponse) async throws { private func auth(response: HTTPURLResponse) async throws {
// Process WWW-Authenticate header // Process WWW-Authenticate header
guard let wwwAuthenticateRaw = response.headers.first(name: "WWW-Authenticate") else { guard let wwwAuthenticateRaw = response.value(forHTTPHeaderField: "WWW-Authenticate") else {
throw RegistryError.AuthFailed(why: "got HTTP 401, but WWW-Authenticate header is missing") throw RegistryError.AuthFailed(why: "got HTTP 401, but WWW-Authenticate header is missing")
} }
@@ -345,14 +353,14 @@ class Registry {
headers["Authorization"] = "Basic \(encodedCredentials!)" headers["Authorization"] = "Basic \(encodedCredentials!)"
} }
let response = try await rawRequest(.GET, authenticateURL, headers: headers, doAuth: false) let (bytes, response) = try await rawRequest(.GET, authenticateURL, headers: headers, doAuth: false)
if response.status != .ok { if response.statusCode != HTTPCode.Ok.rawValue {
let body = try await response.body.readTextResponse() ?? "" let body = try await bytes.asData() .asText() ?? ""
throw RegistryError.AuthFailed(why: "received unexpected HTTP status code \(response.status.code) " throw RegistryError.AuthFailed(why: "received unexpected HTTP status code \(response.statusCode) "
+ "while retrieving an authentication token", details: body) + "while retrieving an authentication token", details: body)
} }
let bodyData = try await response.body.readResponse() let bodyData = try await bytes.asData()
currentAuthToken = try TokenResponse.parse(fromData: bodyData) currentAuthToken = try TokenResponse.parse(fromData: bodyData)
} }
@@ -365,14 +373,16 @@ class Registry {
return nil return nil
} }
private func authAwareRequest(request: HTTPClientRequest) async throws -> HTTPClientResponse { private func authAwareRequest(request: URLRequest) async throws -> (URLSession.AsyncBytes, HTTPURLResponse) {
var request = request var request = request
if let token = currentAuthToken { if let token = currentAuthToken {
let (name, value) = token.header() let (name, value) = token.header()
request.headers.add(name: name, value: value) request.addValue(value, forHTTPHeaderField: name)
} }
return try await httpClient.execute(request, deadline: .distantFuture) let (bytes, response) = try await URLSession.shared.bytes(for: request)
return (bytes, response as! HTTPURLResponse)
} }
} }
+3
View File
@@ -33,6 +33,9 @@ struct Root: AsyncParsableCommand {
} }
sigintSrc.activate() sigintSrc.activate()
// Set line-buffered output for stdout
setlinebuf(stdout)
// Parse and run command // Parse and run command
do { do {
var command = try parseAsRoot() var command = try parseAsRoot()
+42 -21
View File
@@ -60,26 +60,29 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
virtualMachine.delegate = self virtualMachine.delegate = self
} }
static func retrieveLatestIPSW() async throws -> URL { static func retrieveIPSW(remoteURL: URL) async throws -> URL {
defaultLogger.appendNewLine("Looking up the latest supported IPSW...") // Check if we already have this IPSW in cache
let image = try await withCheckedThrowingContinuation { continuation in var request = URLRequest(url: remoteURL)
VZMacOSRestoreImage.fetchLatestSupported() { result in request.httpMethod = "HEAD"
continuation.resume(with: result) let (_, response) = try await URLSession.shared.data(for: request)
let httpURLResponse = response as! HTTPURLResponse
if let hash = httpURLResponse.value(forHTTPHeaderField: "x-amz-meta-digest-sha256") {
let ipswLocation = try IPSWCache().locationFor(fileName: "sha256:\(hash).ipsw")
if FileManager.default.fileExists(atPath: ipswLocation.path) {
defaultLogger.appendNewLine("Using cached *.ipsw file...")
try ipswLocation.updateAccessDate()
return ipswLocation
} }
} }
let expectedIPSWLocation = try IPSWCache().locationFor(image: image) // Download the IPSW
defaultLogger.appendNewLine("Fetching \(remoteURL.lastPathComponent)...")
if FileManager.default.fileExists(atPath: expectedIPSWLocation.path) {
defaultLogger.appendNewLine("Using cached *.ipsw file...")
try expectedIPSWLocation.updateAccessDate()
return expectedIPSWLocation
}
defaultLogger.appendNewLine("Fetching \(expectedIPSWLocation.lastPathComponent)...")
let data: Data = try await withCheckedThrowingContinuation { continuation in let data: Data = try await withCheckedThrowingContinuation { continuation in
let downloadedTask = URLSession.shared.dataTask(with: image.url) { data, response, error in let downloadedTask = URLSession.shared.dataTask(with: remoteURL) { data, response, error in
if error != nil { if error != nil {
continuation.resume(throwing: error!) continuation.resume(throwing: error!)
return return
@@ -94,10 +97,24 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
downloadedTask.resume() downloadedTask.resume()
} }
try data.write(to: expectedIPSWLocation, options: [.atomic]) let ipswLocation = try IPSWCache().locationFor(fileName: Digest.hash(data) + ".ipsw")
return expectedIPSWLocation try data.write(to: ipswLocation, options: [.atomic])
return ipswLocation
} }
static func latestIPSWURL() 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)
}
}
return image.url
}
var inFinalState: Bool { var inFinalState: Bool {
get { get {
virtualMachine.state == VZVirtualMachine.State.stopped || virtualMachine.state == VZVirtualMachine.State.stopped ||
@@ -109,12 +126,16 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
init( init(
vmDir: VMDirectory, vmDir: VMDirectory,
ipswURL: URL?, ipswURL: URL,
diskSizeGB: UInt16, diskSizeGB: UInt16,
network: Network = NetworkShared(), network: Network = NetworkShared(),
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [] additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = []
) async throws { ) async throws {
let ipswURL = ipswURL != nil ? ipswURL! : try await VM.retrieveLatestIPSW(); var ipswURL = ipswURL
if !ipswURL.isFileURL {
ipswURL = try await VM.retrieveIPSW(remoteURL: ipswURL)
}
// Load the restore image and try to get the requirements // Load the restore image and try to get the requirements
// that match both the image and our platform // that match both the image and our platform
@@ -158,7 +179,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
// Run automated installation // Run automated installation
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
DispatchQueue.main.async { DispatchQueue.main.async { [ipswURL] in
let installer = VZMacOSInstaller(virtualMachine: self.virtualMachine, restoringFromImageAt: ipswURL) let installer = VZMacOSInstaller(virtualMachine: self.virtualMachine, restoringFromImageAt: ipswURL)
defaultLogger.appendNewLine("Installing OS...") defaultLogger.appendNewLine("Installing OS...")
+5 -6
View File
@@ -35,8 +35,8 @@ extension VMDirectory {
throw OCIError.FailedToCreateVmFile throw OCIError.FailedToCreateVmFile
} }
let configFile = try FileHandle(forWritingTo: configURL) let configFile = try FileHandle(forWritingTo: configURL)
try await registry.pullBlob(configLayers.first!.digest) { buffer in try await registry.pullBlob(configLayers.first!.digest) { data in
configFile.write(Data(buffer: buffer)) configFile.write(data)
} }
try configFile.close() try configFile.close()
@@ -70,8 +70,7 @@ extension VMDirectory {
ProgressObserver(progress).log(defaultLogger) ProgressObserver(progress).log(defaultLogger)
for diskLayer in diskLayers { for diskLayer in diskLayers {
try await registry.pullBlob(diskLayer.digest) { buffer in try await registry.pullBlob(diskLayer.digest) { data in
let data = Data(buffer: buffer)
try filter.write(data) try filter.write(data)
progress.completedUnitCount += Int64(data.count) progress.completedUnitCount += Int64(data.count)
} }
@@ -92,8 +91,8 @@ extension VMDirectory {
throw OCIError.FailedToCreateVmFile throw OCIError.FailedToCreateVmFile
} }
let nvram = try FileHandle(forWritingTo: nvramURL) let nvram = try FileHandle(forWritingTo: nvramURL)
try await registry.pullBlob(nvramLayers.first!.digest) { buffer in try await registry.pullBlob(nvramLayers.first!.digest) { data in
nvram.write(Data(buffer: buffer)) nvram.write(data)
} }
try nvram.close() try nvram.close()
} }
+4 -4
View File
@@ -34,8 +34,8 @@ final class RegistryTests: XCTestCase {
// Pull it // Pull it
var pulledBlob = Data() var pulledBlob = Data()
try await registry.pullBlob(pushedBlobDigest) { buffer in try await registry.pullBlob(pushedBlobDigest) { data in
pulledBlob.append(Data(buffer: buffer)) pulledBlob.append(data)
} }
// Ensure that both blobs are identical // Ensure that both blobs are identical
@@ -52,8 +52,8 @@ final class RegistryTests: XCTestCase {
// Pull it // Pull it
var pulledLargeBlob = Data() var pulledLargeBlob = Data()
try await registry.pullBlob(largeBlobDigest) { buffer in try await registry.pullBlob(largeBlobDigest) { data in
pulledLargeBlob.append(Data(buffer: buffer)) pulledLargeBlob.append(data)
} }
// Ensure that both blobs are identical // Ensure that both blobs are identical