/
QemuImg.swift
152 lines (117 loc) · 5.31 KB
/
QemuImg.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//
// QemuAdapter.swift
// UTM Snapshot-Manager
//
// Created by Jan Zombik on 04.03.23.
//
import Foundation
class QemuImg {
private static let qemuImgPath = FileManager.default.fileExists(atPath:"/usr/local/bin/qemu-img") ? "/usr/local/bin/qemu-img" : "/opt/homebrew/bin/qemu-img"
private static let snapshotLinePattern = /^\d+.*?\n/.anchorsMatchLineEndings()
private static let idPattern = /^\d+/
private static let tagPattern = /^\d+\s+(?<tag>.*?)\s/
private static let dateTimePattern = /\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}/
private enum QemuImgCommand: String {
case info = "info"
case snapshot = "snapshot"
}
private enum QemuImgSnapshotSubcommand: String {
case none
case create = "-c"
case delete = "-d"
case restore = "-a"
}
static func snapshotsForImageUrl(_ url: URL) -> [VMSnapshot] {
guard let imageInfo = QemuImg.infoForImageUrl(url),
imageInfo.contains("Snapshot list:") else {
return []
}
var snapshots: [VMSnapshot] = []
for match in imageInfo.matches(of: QemuImg.snapshotLinePattern) {
guard let snapshot = QemuImg.snapshotFromString(match.output.description) else {
continue
}
// "suspend" is a reserved name for UTM suspension snapshots
if (snapshot.tag != "suspend") {
snapshots.append(snapshot)
}
}
return snapshots
}
static func createSnapshotForImageUrl(_ url: URL, snapshotTag: String = "") {
var snapshotTag = snapshotTag
if (snapshotTag.isEmpty) {
var hasher = Hasher()
hasher.combine(url)
hasher.combine(Date.now)
let hash = hasher.finalize()
let hexString = String(format: "%02X", hash)
snapshotTag = String(hexString.prefix(8))
}
self.runQemuImgCommand(.snapshot, snapshotSubCommand: .create, snapshotTag: snapshotTag, imageUrl: url)
}
static func deleteSnapshotForImageUrl(_ url: URL, snapshotTag: String) {
self.runQemuImgCommand(.snapshot, snapshotSubCommand: .delete, snapshotTag: snapshotTag, imageUrl: url)
}
static func restoreSnapshotForImageUrl(_ url: URL, snapshotTag: String) {
self.runQemuImgCommand(.snapshot, snapshotSubCommand: .restore, snapshotTag: snapshotTag, imageUrl: url)
}
private static func infoForImageUrl(_ url: URL) -> String? {
return self.runQemuImgCommand(.info, imageUrl: url)
}
private static func snapshotFromString(_ snapshotString: String) -> VMSnapshot? {
guard let id = self.idFromString(snapshotString),
let tag = self.tagFromString(snapshotString),
let creationDate = self.creationDateFromString(snapshotString)
else {
return nil
}
return VMSnapshot(id: id, tag: tag, creationDate: creationDate)
}
private static func idFromString(_ snapshotString: String) -> UInt? {
guard let idString = try? QemuImg.idPattern.firstMatch(in: snapshotString)?.description else {
return nil
}
return UInt(idString)
}
private static func tagFromString(_ snapshotString: String) -> String? {
guard let tagString = try? QemuImg.tagPattern.firstMatch(in: snapshotString)?.tag.description else {
return nil
}
return tagString
}
private static func creationDateFromString(_ snapshotString: String) -> Date? {
guard let dateTimeString = try? QemuImg.dateTimePattern.firstMatch(in: snapshotString)?.description else {
return nil
}
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy'-'MM'-'dd' 'HH'-'mm'-'ss"
return dateFormatter.date(from: dateTimeString)
}
@discardableResult private static func runQemuImgCommand(_ command: QemuImgCommand, snapshotSubCommand: QemuImgSnapshotSubcommand = .none, snapshotTag: String = "", imageUrl: URL) -> String? {
guard command != .snapshot || (snapshotSubCommand != .none && !snapshotTag.isEmpty) else {
return nil
}
var commandComponents: [String] = [Self.qemuImgPath, command.rawValue]
if command == .snapshot {
commandComponents.append(snapshotSubCommand.rawValue)
commandComponents.append(snapshotTag)
}
commandComponents.append("\"\(imageUrl.path(percentEncoded: false))\"")
let qemuImgCommand = commandComponents.joined(separator: " ")
return try? self.runCommandInShell(qemuImgCommand)
}
@discardableResult private static func runCommandInShell(_ command: String) throws -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", command]
task.executableURL = URL(fileURLWithPath: "/bin/zsh")
task.standardInput = nil
try task.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
return output
}
}