UTM/Services/UTMQemuImage.swift

183 lines
5.9 KiB
Swift

//
// Copyright © 2022 osy. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import QEMUKitInternal
@objc class UTMQemuImage: UTMProcess {
private var logOutput: String = ""
private var processExitContinuation: CheckedContinuation<Void, any Error>?
private init() {
super.init(arguments: [])
}
override func processHasExited(_ exitCode: Int, message: String?) {
if let processExitContinuation = processExitContinuation {
self.processExitContinuation = nil
if exitCode != 0 {
if let message = message {
processExitContinuation.resume(throwing: UTMQemuImageError.qemuError(message))
} else {
processExitContinuation.resume(throwing: UTMQemuImageError.unknown)
}
} else {
processExitContinuation.resume()
}
}
}
private func start() async throws {
try await withCheckedThrowingContinuation { continuation in
processExitContinuation = continuation
start("qemu-img") { error in
if let error = error {
self.processExitContinuation = nil
continuation.resume(throwing: error)
}
}
}
}
static func convert(from url: URL, toQcow2 dest: URL, withCompression compressed: Bool = false) async throws {
let qemuImg = UTMQemuImage()
let srcBookmark = try url.bookmarkData()
let dstBookmark = try dest.deletingLastPathComponent().bookmarkData()
qemuImg.pushArgv("convert")
if compressed {
qemuImg.pushArgv("-c")
qemuImg.pushArgv("-o")
qemuImg.pushArgv("compression_type=zstd")
}
qemuImg.pushArgv("-O")
qemuImg.pushArgv("qcow2")
qemuImg.accessData(withBookmark: srcBookmark)
qemuImg.pushArgv(url.path)
qemuImg.accessData(withBookmark: dstBookmark)
qemuImg.pushArgv(dest.path)
let logging = QEMULogging()
qemuImg.standardOutput = logging.standardOutput
qemuImg.standardError = logging.standardError
try await qemuImg.start()
}
/*
The info format looks like:
$ qemu-img info foo.img --output=json
{
"virtual-size": 20971520,
"filename": "foo.img",
"cluster-size": 65536,
"format": "qcow2",
"actual-size": 200704,
"format-specific": {
"type": "qcow2",
"data": {
"compat": "1.1",
"compression-type": "zlib",
"lazy-refcounts": false,
"refcount-bits": 16,
"corrupt": false,
"extended-l2": false
}
},
"dirty-flag": false
}
*/
struct QemuImageInfo : Codable {
let virtualSize : Int64
let filename : String
let clusterSize : Int32
let format : String
let actualSize : Int64
let dirtyFlag : Bool
private enum CodingKeys: String, CodingKey {
case virtualSize = "virtual-size"
case filename
case clusterSize = "cluster-size"
case format
case actualSize = "actual-size"
case dirtyFlag = "dirty-flag"
}
}
static func size(image url: URL) async throws -> Int64 {
let qemuImg = UTMQemuImage()
let srcBookmark = try url.bookmarkData()
qemuImg.pushArgv("info")
qemuImg.pushArgv("--output=json")
qemuImg.accessData(withBookmark: srcBookmark)
qemuImg.pushArgv(url.path)
let logging = QEMULogging()
logging.delegate = qemuImg
qemuImg.standardOutput = logging.standardOutput
qemuImg.standardError = logging.standardError
try await qemuImg.start()
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let data = qemuImg.logOutput.data(using: .utf8) ?? Data()
let image_info: QemuImageInfo = try decoder.decode(QemuImageInfo.self, from: data)
return image_info.virtualSize
}
static func resize(image url: URL, size : UInt64) async throws {
let qemuImg = UTMQemuImage()
let srcBookmark = try url.bookmarkData()
qemuImg.pushArgv("resize")
qemuImg.pushArgv("-f")
qemuImg.pushArgv("qcow2")
qemuImg.accessData(withBookmark: srcBookmark)
qemuImg.pushArgv(url.path)
qemuImg.pushArgv(String(size))
let logging = QEMULogging()
logging.delegate = qemuImg
qemuImg.standardOutput = logging.standardOutput
qemuImg.standardError = logging.standardError
try await qemuImg.start()
}
}
private enum UTMQemuImageError: Error {
case qemuError(String)
case unknown
}
extension UTMQemuImageError: LocalizedError {
var errorDescription: String? {
switch self {
case .qemuError(let message): return message
case .unknown: return NSLocalizedString("An unknown QEMU error has occurred.", comment: "UTMQemuImage")
}
}
}
// MARK: - Logging
extension UTMQemuImage: QEMULoggingDelegate {
func logging(_ logging: QEMULogging, didRecieveOutputLine line: String) {
logOutput += line
}
func logging(_ logging: QEMULogging, didRecieveErrorLine line: String) {
}
}