UTMQemuImage.swift 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  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: UTMQemu {
  19. private var logOutput: String = ""
  20. private var processExitContinuation: CheckedContinuation<Void, any Error>?
  21. private init() {
  22. super.init(arguments: [])
  23. }
  24. override func qemuHasExited(_ 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.logging = logging
  67. try await qemuImg.start()
  68. }
  69. /*
  70. The info format looks like:
  71. $ qemu-img info foo.img --output=json
  72. {
  73. "virtual-size": 20971520,
  74. "filename": "foo.img",
  75. "cluster-size": 65536,
  76. "format": "qcow2",
  77. "actual-size": 200704,
  78. "format-specific": {
  79. "type": "qcow2",
  80. "data": {
  81. "compat": "1.1",
  82. "compression-type": "zlib",
  83. "lazy-refcounts": false,
  84. "refcount-bits": 16,
  85. "corrupt": false,
  86. "extended-l2": false
  87. }
  88. },
  89. "dirty-flag": false
  90. }
  91. */
  92. struct QemuImageInfo : Codable {
  93. let virtualSize : Int64
  94. let filename : String
  95. let clusterSize : Int32
  96. let format : String
  97. let actualSize : Int64
  98. let dirtyFlag : Bool
  99. private enum CodingKeys: String, CodingKey {
  100. case virtualSize = "virtual-size"
  101. case filename
  102. case clusterSize = "cluster-size"
  103. case format
  104. case actualSize = "actual-size"
  105. case dirtyFlag = "dirty-flag"
  106. }
  107. }
  108. static func size(image url: URL) async throws -> Int64 {
  109. let qemuImg = UTMQemuImage()
  110. let srcBookmark = try url.bookmarkData()
  111. qemuImg.pushArgv("info")
  112. qemuImg.pushArgv("--output=json")
  113. qemuImg.accessData(withBookmark: srcBookmark)
  114. qemuImg.pushArgv(url.path)
  115. let logging = QEMULogging()
  116. logging.delegate = qemuImg
  117. qemuImg.logging = logging
  118. try await qemuImg.start()
  119. let decoder = JSONDecoder()
  120. decoder.keyDecodingStrategy = .convertFromSnakeCase
  121. let data = qemuImg.logOutput.data(using: .utf8) ?? Data()
  122. let image_info: QemuImageInfo = try decoder.decode(QemuImageInfo.self, from: data)
  123. return image_info.virtualSize
  124. }
  125. static func resize(image url: URL, size : UInt64) async throws {
  126. let qemuImg = UTMQemuImage()
  127. let srcBookmark = try url.bookmarkData()
  128. qemuImg.pushArgv("resize")
  129. qemuImg.pushArgv("-f")
  130. qemuImg.pushArgv("qcow2")
  131. qemuImg.accessData(withBookmark: srcBookmark)
  132. qemuImg.pushArgv(url.path)
  133. qemuImg.pushArgv(String(size))
  134. let logging = QEMULogging()
  135. logging.delegate = qemuImg
  136. qemuImg.logging = logging
  137. try await qemuImg.start()
  138. }
  139. }
  140. private enum UTMQemuImageError: Error {
  141. case qemuError(String)
  142. case unknown
  143. }
  144. extension UTMQemuImageError: LocalizedError {
  145. var errorDescription: String? {
  146. switch self {
  147. case .qemuError(let message): return message
  148. case .unknown: return NSLocalizedString("An unknown QEMU error has occurred.", comment: "UTMQemuImage")
  149. }
  150. }
  151. }
  152. // MARK: - Logging
  153. extension UTMQemuImage: QEMULoggingDelegate {
  154. func logging(_ logging: QEMULogging, didRecieveOutputLine line: String) {
  155. logOutput += line
  156. }
  157. func logging(_ logging: QEMULogging, didRecieveErrorLine line: String) {
  158. }
  159. }