2
0

VMConfigQEMUView.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. //
  2. // Copyright © 2020 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 SwiftUI
  17. struct VMConfigQEMUView: View {
  18. private struct Argument: Identifiable {
  19. let id: Int
  20. let string: String
  21. }
  22. @Binding var config: UTMQemuConfigurationQEMU
  23. @Binding var system: UTMQemuConfigurationSystem
  24. let fetchFixedArguments: () -> [QEMUArgument]
  25. @State private var showExportLog: Bool = false
  26. @State private var showExportArgs: Bool = false
  27. @EnvironmentObject private var data: UTMData
  28. private var logExists: Bool {
  29. guard let debugLogURL = config.debugLogURL else {
  30. return false
  31. }
  32. return FileManager.default.fileExists(atPath: debugLogURL.path)
  33. }
  34. private var supportsUefi: Bool {
  35. UTMQemuConfigurationQEMU.uefiImagePrefix(forArchitecture: system.architecture) != nil
  36. }
  37. private var supportsPs2: Bool {
  38. if system.target.rawValue.starts(with: "pc") || system.target.rawValue.starts(with: "q35") {
  39. return true
  40. } else {
  41. return false
  42. }
  43. }
  44. var body: some View {
  45. VStack {
  46. Form {
  47. Section(header: Text("Logging")) {
  48. Toggle(isOn: $config.hasDebugLog, label: {
  49. Text("Debug Logging")
  50. })
  51. Button("Export Debug Log") {
  52. showExportLog.toggle()
  53. }.modifier(VMShareItemModifier(isPresented: $showExportLog, shareItem: exportDebugLog()))
  54. .disabled(!logExists)
  55. }
  56. DetailedSection("Tweaks", description: "These are advanced settings affecting QEMU which should be kept default unless you are running into issues.") {
  57. Toggle("UEFI Boot", isOn: $config.hasUefiBoot)
  58. .disabled(!supportsUefi)
  59. .help("Should be off for older operating systems such as Windows 7 or lower.")
  60. Toggle("RNG Device", isOn: $config.hasRNGDevice)
  61. .help("Should be on always unless the guest cannot boot because of this.")
  62. Toggle("Balloon Device", isOn: $config.hasBalloonDevice)
  63. .help("Should be on always unless the guest cannot boot because of this.")
  64. Toggle("TPM 2.0 Device", isOn: $config.hasTPMDevice)
  65. .help("TPM can be used to protect secrets in the guest operating system. Note that the host will always be able to read these secrets and therefore no expectation of physical security is provided.")
  66. .onChange(of: config.hasTPMDevice) { newValue in
  67. if newValue {
  68. config.isUefiVariableResetRequested = true
  69. config.hasPreloadedSecureBootKeys = true
  70. } else {
  71. config.hasPreloadedSecureBootKeys = false
  72. }
  73. }
  74. Toggle("Use Hypervisor", isOn: $config.hasHypervisor)
  75. .help("Only available if host architecture matches the target. Otherwise, TCG emulation is used.")
  76. .disabled(!system.architecture.hasHypervisorSupport)
  77. if config.hasHypervisor {
  78. Toggle("Use TSO", isOn: $config.hasTSO)
  79. .help("Only available when Hypervisor is used on supported hardware. TSO speeds up Intel emulation in the guest at the cost of decreased performance in general.")
  80. .disabled(!system.architecture.hasTSOSupport)
  81. }
  82. Toggle("Use local time for base clock", isOn: $config.hasRTCLocalTime)
  83. .help("If checked, use local time for RTC which is required for Windows. Otherwise, use UTC clock.")
  84. Toggle("Force PS/2 controller", isOn: $config.hasPS2Controller)
  85. .disabled(!supportsPs2)
  86. .help("Instantiate PS/2 controller even when USB input is supported. Required for older Windows.")
  87. }
  88. DetailedSection("Maintenance", description: "Options here only apply on next boot and are not saved.") {
  89. Toggle("Reset UEFI Variables", isOn: $config.isUefiVariableResetRequested)
  90. .help("You can use this if your boot options are corrupted or if you wish to re-enroll in the default keys for secure boot.")
  91. .disabled(!config.hasUefiBoot)
  92. Toggle("Preload Secure Boot Keys", isOn: $config.hasPreloadedSecureBootKeys)
  93. .help("Enable Secure Boot with Microsoft UEFI keys. This is required to Secure Boot Windows.")
  94. .disabled(!config.isUefiVariableResetRequested || !config.hasTPMDevice)
  95. .onChange(of: config.isUefiVariableResetRequested) { newValue in
  96. if !newValue {
  97. config.hasPreloadedSecureBootKeys = false
  98. }
  99. }
  100. }
  101. DetailedSection("QEMU Machine Properties", description: "This is appended to the -machine argument.") {
  102. DefaultTextField("", text: $config.machinePropertyOverride.bound, prompt: "Default")
  103. }
  104. #if os(macOS)
  105. // macOS 12+ uses the new VMConfigQEMUArgumentsView
  106. if #unavailable(macOS 12) {
  107. additionalArguments
  108. }
  109. #else
  110. additionalArguments
  111. #endif
  112. }.navigationBarItems(trailing: EditButton())
  113. .disableAutocorrection(true)
  114. }
  115. }
  116. @ViewBuilder
  117. var additionalArguments: some View {
  118. Section(header: Text("QEMU Arguments")) {
  119. let fixedArgs = fetchFixedArguments()
  120. Button("Export QEMU Command…") {
  121. showExportArgs.toggle()
  122. }.modifier(VMShareItemModifier(isPresented: $showExportArgs, shareItem: exportArgs(fixedArgs)))
  123. #if os(macOS)
  124. // SwiftUI bug: on macOS 11, the ForEach crashes during save
  125. if !data.busy {
  126. VStack {
  127. ForEach(fixedArgs) { arg in
  128. TextField("", text: .constant(arg.string))
  129. }.disabled(true)
  130. CustomArguments(config: $config)
  131. NewArgumentTextField(config: $config)
  132. }
  133. }
  134. #else
  135. List {
  136. ForEach(fixedArgs) { arg in
  137. Text(arg.string)
  138. }.foregroundColor(.secondary)
  139. CustomArguments(config: $config)
  140. NewArgumentTextField(config: $config)
  141. }
  142. #endif
  143. }
  144. }
  145. private func exportDebugLog() -> VMShareItemModifier.ShareItem? {
  146. guard let srcLogPath = config.debugLogURL else {
  147. return nil
  148. }
  149. return .debugLog(srcLogPath)
  150. }
  151. private func exportArgs(_ args: [QEMUArgument]) -> VMShareItemModifier.ShareItem {
  152. var argString = "qemu-system-\(system.architecture.rawValue)"
  153. for arg in args {
  154. if arg.string.contains(" ") {
  155. argString += " \"\(arg.string)\""
  156. } else {
  157. argString += " \(arg.string)"
  158. }
  159. }
  160. for arg in config.additionalArguments {
  161. argString += " \(arg.string)"
  162. }
  163. return .qemuCommand(argString)
  164. }
  165. }
  166. struct CustomArguments: View {
  167. @Binding var config: UTMQemuConfigurationQEMU
  168. var body: some View {
  169. ForEach($config.additionalArguments) { $arg in
  170. let i = config.additionalArguments.firstIndex(of: arg) ?? 0
  171. HStack {
  172. DefaultTextField("", text: $arg.string, prompt: "(Delete)", onEditingChanged: { editing in
  173. if !editing && arg.string == "" {
  174. DispatchQueue.main.async { // SwiftUI doesn't like removing in a ForEach binding
  175. config.additionalArguments.remove(at: i)
  176. }
  177. }
  178. })
  179. #if os(macOS)
  180. Spacer()
  181. if i != 0 {
  182. Button(action: {
  183. config.additionalArguments.move(fromOffsets: IndexSet(integer: i), toOffset: i-1)
  184. }, label: {
  185. Label("Move Up", systemImage: "arrow.up").labelStyle(.iconOnly)
  186. })
  187. }
  188. #endif
  189. }
  190. }.onDelete { offsets in
  191. config.additionalArguments.remove(atOffsets: offsets)
  192. }
  193. .onMove { offsets, index in
  194. config.additionalArguments.move(fromOffsets: offsets, toOffset: index)
  195. }
  196. }
  197. }
  198. struct NewArgumentTextField: View {
  199. @Binding var config: UTMQemuConfigurationQEMU
  200. @State private var newArg: String = ""
  201. var body: some View {
  202. Group {
  203. DefaultTextField("", text: $newArg, prompt: "New…", onEditingChanged: addArg)
  204. }.onDisappear {
  205. if newArg != "" {
  206. addArg(editing: false)
  207. }
  208. }
  209. }
  210. private func addArg(editing: Bool) {
  211. guard !editing else {
  212. return
  213. }
  214. if newArg != "" {
  215. config.additionalArguments.append(QEMUArgument(newArg))
  216. }
  217. newArg = ""
  218. }
  219. }
  220. struct VMConfigQEMUView_Previews: PreviewProvider {
  221. @State static private var config = UTMQemuConfigurationQEMU()
  222. @State static private var system = UTMQemuConfigurationSystem()
  223. static var previews: some View {
  224. VMConfigQEMUView(config: $config, system: $system, fetchFixedArguments: { [] })
  225. .frame(minHeight: 500)
  226. }
  227. }