UTMQemuImage.swift 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. //
  2. // Copyright © 2022 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import Foundation
  17. import QEMUKitInternal
  18. @objc class UTMQemuImage: UTMProcess {
  19. private var logOutput: String = ""
  20. private var processExitContinuation: CheckedContinuation<Void, any Error>?
  21. private init() {
  22. super.init(arguments: [])
  23. }
  24. override func processHasExited(_ exitCode: Int, message: String?) {
  25. if let processExitContinuation = processExitContinuation {
  26. self.processExitContinuation = nil
  27. if exitCode != 0 {
  28. if let message = message {
  29. processExitContinuation.resume(throwing: UTMQemuImageError.qemuError(message))
  30. } else {
  31. processExitContinuation.resume(throwing: UTMQemuImageError.unknown)
  32. }
  33. } else {
  34. processExitContinuation.resume()
  35. }
  36. }
  37. }
  38. private func start() async throws {
  39. try await withCheckedThrowingContinuation { continuation in
  40. processExitContinuation = continuation
  41. start("qemu-img") { error in
  42. if let error = error {
  43. self.processExitContinuation = nil
  44. continuation.resume(throwing: error)
  45. }
  46. }
  47. }
  48. }
  49. static func convert(from url: URL, toQcow2 dest: URL, withCompression compressed: Bool = false) async throws {
  50. let qemuImg = UTMQemuImage()
  51. let srcBookmark = try url.bookmarkData()
  52. let dstBookmark = try dest.deletingLastPathComponent().bookmarkData()
  53. qemuImg.pushArgv("convert")
  54. if compressed {
  55. qemuImg.pushArgv("-c")
  56. qemuImg.pushArgv("-o")
  57. qemuImg.pushArgv("compression_type=zstd")
  58. }
  59. qemuImg.pushArgv("-O")
  60. qemuImg.pushArgv("qcow2")
  61. qemuImg.accessData(withBookmark: srcBookmark)
  62. qemuImg.pushArgv(url.path)
  63. qemuImg.accessData(withBookmark: dstBookmark)
  64. qemuImg.pushArgv(dest.path)
  65. let logging = QEMULogging()
  66. qemuImg.standardOutput = logging.standardOutput
  67. qemuImg.standardError = logging.standardError
  68. try await qemuImg.start()
  69. }
  70. /*
  71. The info format looks like:
  72. $ qemu-img info foo.img --output=json
  73. {
  74. "virtual-size": 20971520,
  75. "filename": "foo.img",
  76. "cluster-size": 65536,
  77. "format": "qcow2",
  78. "actual-size": 200704,
  79. "format-specific": {
  80. "type": "qcow2",
  81. "data": {
  82. "compat": "1.1",
  83. "compression-type": "zlib",
  84. "lazy-refcounts": false,
  85. "refcount-bits": 16,
  86. "corrupt": false,
  87. "extended-l2": false
  88. }
  89. },
  90. "dirty-flag": false
  91. }
  92. */
  93. struct QemuImageInfo : Codable {
  94. let virtualSize : Int64
  95. let filename : String
  96. let clusterSize : Int32
  97. let format : String
  98. let actualSize : Int64
  99. let dirtyFlag : Bool
  100. private enum CodingKeys: String, CodingKey {
  101. case virtualSize = "virtual-size"
  102. case filename
  103. case clusterSize = "cluster-size"
  104. case format
  105. case actualSize = "actual-size"
  106. case dirtyFlag = "dirty-flag"
  107. }
  108. }
  109. static func size(image url: URL) async throws -> Int64 {
  110. let qemuImg = UTMQemuImage()
  111. let srcBookmark = try url.bookmarkData()
  112. qemuImg.pushArgv("info")
  113. qemuImg.pushArgv("--output=json")
  114. qemuImg.accessData(withBookmark: srcBookmark)
  115. qemuImg.pushArgv(url.path)
  116. let logging = QEMULogging()
  117. logging.delegate = qemuImg
  118. qemuImg.standardOutput = logging.standardOutput
  119. qemuImg.standardError = logging.standardError
  120. try await qemuImg.start()
  121. let decoder = JSONDecoder()
  122. decoder.keyDecodingStrategy = .convertFromSnakeCase
  123. let data = qemuImg.logOutput.data(using: .utf8) ?? Data()
  124. let image_info: QemuImageInfo = try decoder.decode(QemuImageInfo.self, from: data)
  125. return image_info.virtualSize
  126. }
  127. static func resize(image url: URL, size : UInt64) async throws {
  128. let qemuImg = UTMQemuImage()
  129. let srcBookmark = try url.bookmarkData()
  130. qemuImg.pushArgv("resize")
  131. qemuImg.pushArgv("-f")
  132. qemuImg.pushArgv("qcow2")
  133. qemuImg.accessData(withBookmark: srcBookmark)
  134. qemuImg.pushArgv(url.path)
  135. qemuImg.pushArgv(String(size))
  136. let logging = QEMULogging()
  137. logging.delegate = qemuImg
  138. qemuImg.standardOutput = logging.standardOutput
  139. qemuImg.standardError = logging.standardError
  140. try await qemuImg.start()
  141. }
  142. }
  143. private enum UTMQemuImageError: Error {
  144. case qemuError(String)
  145. case unknown
  146. }
  147. extension UTMQemuImageError: LocalizedError {
  148. var errorDescription: String? {
  149. switch self {
  150. case .qemuError(let message): return message
  151. case .unknown: return NSLocalizedString("An unknown QEMU error has occurred.", comment: "UTMQemuImage")
  152. }
  153. }
  154. }
  155. // MARK: - Logging
  156. extension UTMQemuImage: QEMULoggingDelegate {
  157. func logging(_ logging: QEMULogging, didRecieveOutputLine line: String) {
  158. logOutput += line
  159. }
  160. func logging(_ logging: QEMULogging, didRecieveErrorLine line: String) {
  161. }
  162. }