VMConfigQEMUView.swift 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  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. [.arm, .aarch64, .i386, .x86_64].contains(system.architecture)
  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. private var isMontereyOrHigher: Bool {
  45. if #available(macOS 12, *) {
  46. return true
  47. } else {
  48. return false
  49. }
  50. }
  51. var body: some View {
  52. VStack {
  53. Form {
  54. Section(header: Text("Logging")) {
  55. Toggle(isOn: $config.hasDebugLog, label: {
  56. Text("Debug Logging")
  57. })
  58. Button("Export Debug Log") {
  59. showExportLog.toggle()
  60. }.modifier(VMShareItemModifier(isPresented: $showExportLog, shareItem: exportDebugLog()))
  61. .disabled(!logExists)
  62. }
  63. DetailedSection("Tweaks", description: "These are advanced settings affecting QEMU which should be kept default unless you are running into issues.") {
  64. Toggle("UEFI Boot", isOn: $config.hasUefiBoot)
  65. .disabled(!supportsUefi)
  66. .help("Should be off for older operating systems such as Windows 7 or lower.")
  67. Toggle("RNG Device", isOn: $config.hasRNGDevice)
  68. .help("Should be on always unless the guest cannot boot because of this.")
  69. Toggle("Balloon Device", isOn: $config.hasBalloonDevice)
  70. .help("Should be on always unless the guest cannot boot because of this.")
  71. Toggle("TPM 2.0 Device", isOn: $config.hasTPMDevice)
  72. .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.")
  73. Toggle("Use Hypervisor", isOn: $config.hasHypervisor)
  74. .help("Only available if host architecture matches the target. Otherwise, TCG emulation is used.")
  75. .disabled(!system.architecture.hasHypervisorSupport)
  76. #if os(iOS)
  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. #endif
  83. Toggle("Use local time for base clock", isOn: $config.hasRTCLocalTime)
  84. .help("If checked, use local time for RTC which is required for Windows. Otherwise, use UTC clock.")
  85. Toggle("Force PS/2 controller", isOn: $config.hasPS2Controller)
  86. .disabled(!supportsPs2)
  87. .help("Instantiate PS/2 controller even when USB input is supported. Required for older Windows.")
  88. }
  89. DetailedSection("QEMU Machine Properties", description: "This is appended to the -machine argument.") {
  90. DefaultTextField("", text: $config.machinePropertyOverride.bound, prompt: "Default")
  91. }
  92. Section(header: Text("QEMU Arguments")) {
  93. let fixedArgs = fetchFixedArguments()
  94. Button("Export QEMU Command") {
  95. showExportArgs.toggle()
  96. }.modifier(VMShareItemModifier(isPresented: $showExportArgs, shareItem: exportArgs(fixedArgs)))
  97. #if os(macOS)
  98. // SwiftUI bug: on macOS 11, the ForEach crashes during save
  99. if isMontereyOrHigher || !data.busy {
  100. VStack {
  101. ForEach(fixedArgs) { arg in
  102. TextField("", text: .constant(arg.string))
  103. }.disabled(true)
  104. CustomArguments(config: $config)
  105. NewArgumentTextField(config: $config)
  106. }
  107. }
  108. #else
  109. List {
  110. ForEach(fixedArgs) { arg in
  111. Text(arg.string)
  112. }.foregroundColor(.secondary)
  113. CustomArguments(config: $config)
  114. NewArgumentTextField(config: $config)
  115. }
  116. #endif
  117. }
  118. }.navigationBarItems(trailing: EditButton())
  119. .disableAutocorrection(true)
  120. }
  121. }
  122. private func exportDebugLog() -> VMShareItemModifier.ShareItem? {
  123. guard let srcLogPath = config.debugLogURL else {
  124. return nil
  125. }
  126. return .debugLog(srcLogPath)
  127. }
  128. private func exportArgs(_ args: [QEMUArgument]) -> VMShareItemModifier.ShareItem {
  129. var argString = "qemu-system-\(system.architecture.rawValue)"
  130. for arg in args {
  131. if arg.string.contains(" ") {
  132. argString += " \"\(arg.string)\""
  133. } else {
  134. argString += " \(arg.string)"
  135. }
  136. }
  137. for arg in config.additionalArguments {
  138. argString += " \(arg.string)"
  139. }
  140. return .qemuCommand(argString)
  141. }
  142. }
  143. struct CustomArguments: View {
  144. @Binding var config: UTMQemuConfigurationQEMU
  145. var body: some View {
  146. ForEach($config.additionalArguments) { $arg in
  147. let i = config.additionalArguments.firstIndex(of: arg) ?? 0
  148. HStack {
  149. DefaultTextField("", text: $arg.string, prompt: "(Delete)", onEditingChanged: { editing in
  150. if !editing && arg.string == "" {
  151. DispatchQueue.main.async { // SwiftUI doesn't like removing in a ForEach binding
  152. config.additionalArguments.remove(at: i)
  153. }
  154. }
  155. })
  156. #if os(macOS)
  157. Spacer()
  158. if i != 0 {
  159. Button(action: {
  160. config.additionalArguments.move(fromOffsets: IndexSet(integer: i), toOffset: i-1)
  161. }, label: {
  162. Label("Move Up", systemImage: "arrow.up").labelStyle(.iconOnly)
  163. })
  164. }
  165. #endif
  166. }
  167. }.onDelete { offsets in
  168. config.additionalArguments.remove(atOffsets: offsets)
  169. }
  170. .onMove { offsets, index in
  171. config.additionalArguments.move(fromOffsets: offsets, toOffset: index)
  172. }
  173. }
  174. }
  175. struct NewArgumentTextField: View {
  176. @Binding var config: UTMQemuConfigurationQEMU
  177. @State private var newArg: String = ""
  178. var body: some View {
  179. Group {
  180. DefaultTextField("", text: $newArg, prompt: "New…", onEditingChanged: addArg)
  181. }.onDisappear {
  182. if newArg != "" {
  183. addArg(editing: false)
  184. }
  185. }
  186. }
  187. private func addArg(editing: Bool) {
  188. guard !editing else {
  189. return
  190. }
  191. if newArg != "" {
  192. config.additionalArguments.append(QEMUArgument(newArg))
  193. }
  194. newArg = ""
  195. }
  196. }
  197. struct VMConfigQEMUView_Previews: PreviewProvider {
  198. @State static private var config = UTMQemuConfigurationQEMU()
  199. @State static private var system = UTMQemuConfigurationSystem()
  200. static var previews: some View {
  201. VMConfigQEMUView(config: $config, system: $system, fetchFixedArguments: { [] })
  202. .frame(minHeight: 500)
  203. }
  204. }