UTMQemuConfiguration+Arguments.swift 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962
  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. #if os(macOS)
  18. import Virtualization // for getting network interfaces
  19. #endif
  20. /// Build QEMU arguments from config
  21. @MainActor extension UTMQemuConfiguration {
  22. /// Helper function to generate a final argument
  23. /// - Parameter string: Argument fragment
  24. /// - Returns: Final argument fragment
  25. private func f(_ string: String = "") -> QEMUArgumentFragment {
  26. QEMUArgumentFragment(final: string)
  27. }
  28. /// Shared between helper and main process to store Unix sockets
  29. var socketURL: URL {
  30. #if os(iOS) || os(visionOS)
  31. return FileManager.default.temporaryDirectory
  32. #else
  33. let appGroup = Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String
  34. let helper = Bundle.main.infoDictionary?["HelperIdentifier"] as? String
  35. // default to unsigned sandbox path
  36. var parentURL: URL = FileManager.default.homeDirectoryForCurrentUser
  37. parentURL.deleteLastPathComponent()
  38. parentURL.deleteLastPathComponent()
  39. parentURL.appendPathComponent(helper ?? "com.utmapp.QEMUHelper")
  40. parentURL.appendPathComponent("Data")
  41. parentURL.appendPathComponent("tmp")
  42. if let appGroup = appGroup, !appGroup.hasPrefix("invalid.") {
  43. if let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) {
  44. return containerURL
  45. }
  46. }
  47. return parentURL
  48. #endif
  49. }
  50. /// Return the socket file for communicating with SPICE
  51. var spiceSocketURL: URL {
  52. socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("spice")
  53. }
  54. /// Return the socket file for communicating with SWTPM
  55. var swtpmSocketURL: URL {
  56. socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("swtpm")
  57. }
  58. /// Combined generated and user specified arguments.
  59. @QEMUArgumentBuilder var allArguments: [QEMUArgument] {
  60. generatedArguments
  61. userArguments
  62. }
  63. /// Only UTM generated arguments.
  64. @QEMUArgumentBuilder var generatedArguments: [QEMUArgument] {
  65. f("-L")
  66. resourceURL
  67. f()
  68. f("-S") // startup stopped
  69. spiceArguments
  70. networkArguments
  71. displayArguments
  72. serialArguments
  73. cpuArguments
  74. machineArguments
  75. architectureArguments
  76. if !sound.isEmpty {
  77. soundArguments
  78. }
  79. if isUsbUsed {
  80. usbArguments
  81. }
  82. drivesArguments
  83. sharingArguments
  84. miscArguments
  85. }
  86. @QEMUArgumentBuilder private var userArguments: [QEMUArgument] {
  87. let regex = try! NSRegularExpression(pattern: "((?:[^\"\\s]*\"[^\"]*\"[^\"\\s]*)+|[^\"\\s]+)")
  88. for arg in qemu.additionalArguments {
  89. let argString = arg.string
  90. if argString.count > 0 {
  91. let range = NSRange(argString.startIndex..<argString.endIndex, in: argString)
  92. let split = regex.matches(in: argString, options: [], range: range)
  93. for match in split {
  94. let matchRange = Range(match.range(at: 1), in: argString)!
  95. let fragment = argString[matchRange]
  96. f(fragment.replacingOccurrences(of: "\"", with: ""))
  97. }
  98. }
  99. }
  100. }
  101. @QEMUArgumentBuilder private var spiceArguments: [QEMUArgument] {
  102. f("-spice")
  103. "unix=on"
  104. "addr=\(spiceSocketURL.lastPathComponent)"
  105. "disable-ticketing=on"
  106. "image-compression=off"
  107. "playback-compression=off"
  108. "streaming-video=off"
  109. "gl=\(isGLOn ? "on" : "off")"
  110. f()
  111. f("-chardev")
  112. f("spiceport,id=org.qemu.monitor.qmp,name=org.qemu.monitor.qmp.0")
  113. f("-mon")
  114. f("chardev=org.qemu.monitor.qmp,mode=control")
  115. if !isSparc { // disable -vga and other default devices
  116. // prevent QEMU default devices, which leads to duplicate CD drive (fix #2538)
  117. // see https://github.com/qemu/qemu/blob/6005ee07c380cbde44292f5f6c96e7daa70f4f7d/docs/qdev-device-use.txt#L382
  118. f("-nodefaults")
  119. f("-vga")
  120. f("none")
  121. }
  122. }
  123. @QEMUArgumentBuilder private var displayArguments: [QEMUArgument] {
  124. if displays.isEmpty {
  125. f("-nographic")
  126. } else if isSparc { // only one display supported
  127. f("-vga")
  128. displays[0].hardware
  129. if let vgaRamSize = displays[0].vgaRamMib {
  130. "vgamem_mb=\(vgaRamSize)"
  131. }
  132. f()
  133. } else {
  134. for display in displays {
  135. f("-device")
  136. display.hardware
  137. if let vgaRamSize = displays[0].vgaRamMib {
  138. "vgamem_mb=\(vgaRamSize)"
  139. }
  140. f()
  141. }
  142. }
  143. }
  144. private var isGLOn: Bool {
  145. displays.contains { display in
  146. display.hardware.rawValue.contains("-gl-") || display.hardware.rawValue.hasSuffix("-gl")
  147. }
  148. }
  149. private var isSparc: Bool {
  150. system.architecture == .sparc || system.architecture == .sparc64
  151. }
  152. @QEMUArgumentBuilder private var serialArguments: [QEMUArgument] {
  153. for i in serials.indices {
  154. f("-chardev")
  155. switch serials[i].mode {
  156. case .builtin:
  157. f("spiceport,id=term\(i),name=com.utmapp.terminal.\(i)")
  158. case .tcpClient:
  159. "socket"
  160. "id=term\(i)"
  161. "port=\(serials[i].tcpPort ?? 1234)"
  162. "host=\(serials[i].tcpHostAddress ?? "example.com")"
  163. "server=off"
  164. f()
  165. case .tcpServer:
  166. "socket"
  167. "id=term\(i)"
  168. "port=\(serials[i].tcpPort ?? 1234)"
  169. "host=\(serials[i].isRemoteConnectionAllowed == true ? "0.0.0.0" : "127.0.0.1")"
  170. "server=on"
  171. "wait=\(serials[i].isWaitForConnection == true ? "on" : "off")"
  172. f()
  173. #if os(macOS)
  174. case .ptty:
  175. f("pty,id=term\(i)")
  176. #endif
  177. }
  178. switch serials[i].target {
  179. case .autoDevice:
  180. f("-serial")
  181. f("chardev:term\(i)")
  182. case .manualDevice:
  183. f("-device")
  184. f("\(serials[i].hardware?.rawValue ?? "invalid"),chardev=term\(i)")
  185. case .monitor:
  186. f("-mon")
  187. f("chardev=term\(i),mode=readline")
  188. case .gdb:
  189. f("-gdb")
  190. f("chardev:term\(i)")
  191. }
  192. }
  193. }
  194. @QEMUArgumentBuilder private var cpuArguments: [QEMUArgument] {
  195. if system.cpu.rawValue == system.architecture.cpuType.default.rawValue {
  196. // if default and not hypervisor, we don't pass any -cpu argument for x86 and use host for ARM
  197. if isHypervisorUsed {
  198. #if !arch(x86_64)
  199. f("-cpu")
  200. f("host")
  201. #endif
  202. } else if system.architecture == .aarch64 {
  203. // ARM64 QEMU does not support "-cpu default" so we hard code a sensible default
  204. f("-cpu")
  205. f("cortex-a72")
  206. } else if system.architecture == .arm {
  207. // ARM64 QEMU does not support "-cpu default" so we hard code a sensible default
  208. f("-cpu")
  209. f("cortex-a15")
  210. }
  211. } else {
  212. f("-cpu")
  213. system.cpu
  214. for flag in system.cpuFlagsAdd {
  215. "+\(flag.rawValue)"
  216. }
  217. for flag in system.cpuFlagsRemove {
  218. "-\(flag.rawValue)"
  219. }
  220. f()
  221. }
  222. let emulatedCpuCount = self.emulatedCpuCount
  223. f("-smp")
  224. "cpus=\(emulatedCpuCount.1)"
  225. "sockets=1"
  226. "cores=\(emulatedCpuCount.0)"
  227. "threads=\(emulatedCpuCount.1/emulatedCpuCount.0)"
  228. f()
  229. }
  230. private static func sysctlIntRead(_ name: String) -> UInt64 {
  231. var value: UInt64 = 0
  232. var size = MemoryLayout<UInt64>.size
  233. sysctlbyname(name, &value, &size, nil, 0)
  234. return value
  235. }
  236. private var emulatedCpuCount: (Int, Int) {
  237. let singleCpu = (1, 1)
  238. let hostPhysicalCpu = Int(Self.sysctlIntRead("hw.physicalcpu"))
  239. let hostLogicalCpu = Int(Self.sysctlIntRead("hw.logicalcpu"))
  240. let userCpu = system.cpuCount
  241. if userCpu > 0 || hostPhysicalCpu == 0 {
  242. return (userCpu, userCpu) // user override
  243. }
  244. // SPARC5 defaults to single CPU
  245. if isSparc {
  246. return singleCpu
  247. }
  248. #if arch(arm64)
  249. let hostPcorePhysicalCpu = Int(Self.sysctlIntRead("hw.perflevel0.physicalcpu"))
  250. let hostPcoreLogicalCpu = Int(Self.sysctlIntRead("hw.perflevel0.logicalcpu"))
  251. // in ARM we can only emulate other weak architectures
  252. let weakArchitectures: [QEMUArchitecture] = [.alpha, .arm, .aarch64, .avr, .mips, .mips64, .mipsel, .mips64el, .ppc, .ppc64, .riscv32, .riscv64, .xtensa, .xtensaeb]
  253. if weakArchitectures.contains(system.architecture) {
  254. if hostPcorePhysicalCpu > 0 {
  255. return (hostPcorePhysicalCpu, hostPcoreLogicalCpu)
  256. } else {
  257. return (hostPhysicalCpu, hostLogicalCpu)
  258. }
  259. } else {
  260. return singleCpu
  261. }
  262. #elseif arch(x86_64)
  263. // in x86 we can emulate weak on strong
  264. return (hostPhysicalCpu, hostLogicalCpu)
  265. #else
  266. return singleCpu
  267. #endif
  268. }
  269. private var isHypervisorUsed: Bool {
  270. system.architecture.hasHypervisorSupport && qemu.hasHypervisor
  271. }
  272. private var isTSOUsed: Bool {
  273. system.architecture.hasTSOSupport && qemu.hasTSO
  274. }
  275. private var isUsbUsed: Bool {
  276. system.architecture.hasUsbSupport && system.target.hasUsbSupport && input.usbBusSupport != .disabled
  277. }
  278. private var isSecureBootUsed: Bool {
  279. system.architecture.hasSecureBootSupport && system.target.hasSecureBootSupport && qemu.hasTPMDevice
  280. }
  281. @QEMUArgumentBuilder private var machineArguments: [QEMUArgument] {
  282. f("-machine")
  283. system.target
  284. f(machineProperties)
  285. if isHypervisorUsed {
  286. f("-accel")
  287. "hvf"
  288. if isTSOUsed {
  289. "tso=on"
  290. }
  291. f()
  292. } else {
  293. f("-accel")
  294. "tcg"
  295. if system.isForceMulticore {
  296. "thread=multi"
  297. }
  298. let tbSize = system.jitCacheSize > 0 ? system.jitCacheSize : system.memorySize / 4
  299. "tb-size=\(tbSize)"
  300. #if WITH_JIT
  301. // use mirror mapping when we don't have JIT entitlements
  302. if !jb_has_jit_entitlement() {
  303. "split-wx=on"
  304. }
  305. #endif
  306. f()
  307. }
  308. }
  309. private var machineProperties: String {
  310. let target = system.target.rawValue
  311. let architecture = system.architecture.rawValue
  312. var properties = qemu.machinePropertyOverride ?? ""
  313. if target.hasPrefix("pc") || target.hasPrefix("q35") || target == "isapc" {
  314. properties = properties.appendingDefaultPropertyName("vmport", value: "off")
  315. // disable PS/2 emulation if we are not legacy input and it's not explicitly enabled
  316. if isUsbUsed && !qemu.hasPS2Controller {
  317. properties = properties.appendingDefaultPropertyName("i8042", value: "off")
  318. }
  319. #if os(macOS)
  320. if sound.contains(where: { $0.hardware.rawValue == "pcspk" }) {
  321. properties = properties.appendingDefaultPropertyName("pcspk-audiodev", value: "audio1")
  322. }
  323. #endif
  324. // disable HPET because it causes issues for some OS and also hinders performance
  325. properties = properties.appendingDefaultPropertyName("hpet", value: "off")
  326. }
  327. if target == "virt" || target.hasPrefix("virt-") && !architecture.hasPrefix("riscv") {
  328. if #available(macOS 12.4, iOS 15.5, *, *) {
  329. // default highmem value is fine here
  330. } else {
  331. // a kernel panic is triggered on M1 Max if highmem=on and running < macOS 12.4
  332. properties = properties.appendingDefaultPropertyName("highmem", value: "off")
  333. }
  334. // required to boot Windows ARM on TCG
  335. if system.architecture == .aarch64 && !isHypervisorUsed {
  336. properties = properties.appendingDefaultPropertyName("virtualization", value: "on")
  337. }
  338. // required for > 8 CPUs
  339. if system.architecture == .aarch64 && emulatedCpuCount.0 > 8 {
  340. properties = properties.appendingDefaultPropertyName("gic-version", value: "3")
  341. }
  342. }
  343. if target == "mac99" {
  344. properties = properties.appendingDefaultPropertyName("via", value: "pmu")
  345. }
  346. return properties
  347. }
  348. @QEMUArgumentBuilder private var architectureArguments: [QEMUArgument] {
  349. if system.architecture == .x86_64 || system.architecture == .i386 {
  350. f("-global")
  351. f("PIIX4_PM.disable_s3=1") // applies for pc-i440fx-* types
  352. f("-global")
  353. f("ICH9-LPC.disable_s3=1") // applies for pc-q35-* types
  354. }
  355. if qemu.hasUefiBoot {
  356. let secure = isSecureBootUsed ? "-secure" : ""
  357. let code = system.target.rawValue == "microvm" ? "microvm" : "code"
  358. let bios = resourceURL.appendingPathComponent("edk2-\(system.architecture.rawValue)\(secure)-\(code).fd")
  359. let vars = qemu.efiVarsURL ?? URL(fileURLWithPath: "/\(QEMUPackageFileName.efiVariables.rawValue)")
  360. if !hasCustomBios && FileManager.default.fileExists(atPath: bios.path) {
  361. f("-drive")
  362. "if=pflash"
  363. "format=raw"
  364. "unit=0"
  365. "file.filename="
  366. bios
  367. "file.locking=off"
  368. "readonly=on"
  369. f()
  370. f("-drive")
  371. "if=pflash"
  372. "unit=1"
  373. "file="
  374. vars
  375. f()
  376. }
  377. }
  378. f("-m")
  379. system.memorySize
  380. f()
  381. }
  382. private var hasCustomBios: Bool {
  383. for drive in drives {
  384. if drive.imageType == .disk || drive.imageType == .cd {
  385. if drive.interface == .pflash {
  386. return true
  387. }
  388. } else if drive.imageType == .bios || drive.imageType == .linuxKernel {
  389. return true
  390. }
  391. }
  392. return false
  393. }
  394. private var resourceURL: URL {
  395. Bundle.main.url(forResource: "qemu", withExtension: nil)!
  396. }
  397. private var soundBackend: UTMQEMUSoundBackend {
  398. let value = UserDefaults.standard.integer(forKey: "QEMUSoundBackend")
  399. if let backend = UTMQEMUSoundBackend(rawValue: value), backend != .qemuSoundBackendMax {
  400. return backend
  401. } else {
  402. return .qemuSoundBackendDefault
  403. }
  404. }
  405. private var useCoreAudioBackend: Bool {
  406. #if os(iOS) || os(visionOS)
  407. return false
  408. #else
  409. // force CoreAudio backend for mac99 which only supports 44100 Hz
  410. // pcspk doesn't work with SPICE audio
  411. if sound.contains(where: { $0.hardware.rawValue == "screamer" || $0.hardware.rawValue == "pcspk" }) {
  412. return true
  413. }
  414. if soundBackend == .qemuSoundBackendCoreAudio {
  415. return true
  416. }
  417. return false
  418. #endif
  419. }
  420. @QEMUArgumentBuilder private var soundArguments: [QEMUArgument] {
  421. if useCoreAudioBackend {
  422. f("-audiodev")
  423. "coreaudio"
  424. f("id=audio1")
  425. }
  426. f("-audiodev")
  427. "spice"
  428. f("id=audio0")
  429. // screamer has no extra device, pcspk is handled in machineProperties
  430. for _sound in sound.filter({ $0.hardware.rawValue != "screamer" && $0.hardware.rawValue != "pcspk" }) {
  431. f("-device")
  432. _sound.hardware
  433. if _sound.hardware.rawValue.contains("hda") {
  434. f()
  435. f("-device")
  436. if soundBackend == .qemuSoundBackendCoreAudio {
  437. "hda-output"
  438. "audiodev=audio1"
  439. } else {
  440. "hda-duplex"
  441. "audiodev=audio0"
  442. }
  443. f()
  444. } else {
  445. if soundBackend == .qemuSoundBackendCoreAudio {
  446. f("audiodev=audio1")
  447. } else {
  448. f("audiodev=audio0")
  449. }
  450. }
  451. }
  452. }
  453. @QEMUArgumentBuilder private var drivesArguments: [QEMUArgument] {
  454. var busInterfaceMap: [String: Int] = [:]
  455. for drive in drives {
  456. let hasImage = !drive.isExternal && drive.imageURL != nil
  457. if drive.imageType == .disk || drive.imageType == .cd {
  458. driveArgument(for: drive, busInterfaceMap: &busInterfaceMap)
  459. } else if hasImage {
  460. switch drive.imageType {
  461. case .bios:
  462. f("-bios")
  463. drive.imageURL!
  464. case .linuxKernel:
  465. f("-kernel")
  466. drive.imageURL!
  467. case .linuxInitrd:
  468. f("-initrd")
  469. drive.imageURL!
  470. case .linuxDtb:
  471. f("-dtb")
  472. drive.imageURL!
  473. default:
  474. f()
  475. }
  476. f()
  477. }
  478. }
  479. }
  480. /// These machines are hard coded to have one IDE unit per bus in QEMU
  481. private var isIdeInterfaceSingleUnit: Bool {
  482. system.target.rawValue.contains("q35") ||
  483. system.target.rawValue == "microvm" ||
  484. system.target.rawValue == "cubieboard" ||
  485. system.target.rawValue == "highbank" ||
  486. system.target.rawValue == "midway" ||
  487. system.target.rawValue == "xlnx_zcu102"
  488. }
  489. @QEMUArgumentBuilder private func driveArgument(for drive: UTMQemuConfigurationDrive, busInterfaceMap: inout [String: Int]) -> [QEMUArgument] {
  490. let isRemovable = drive.imageType == .cd || drive.isExternal
  491. let isCd = drive.imageType == .cd && drive.interface != .floppy
  492. var bootindex = busInterfaceMap["boot", default: 0]
  493. var busindex = busInterfaceMap[drive.interface.rawValue, default: 0]
  494. var realInterface = QEMUDriveInterface.none
  495. if drive.interface == .ide {
  496. f("-device")
  497. if isCd {
  498. "ide-cd"
  499. } else {
  500. "ide-hd"
  501. }
  502. if drive.interfaceVersion >= 1 && !isIdeInterfaceSingleUnit {
  503. "bus=ide.\(busindex / 2)"
  504. "unit=\(busindex % 2)"
  505. } else {
  506. "bus=ide.\(busindex)"
  507. }
  508. busindex += 1
  509. "drive=drive\(drive.id)"
  510. "bootindex=\(bootindex)"
  511. bootindex += 1
  512. f()
  513. } else if drive.interface == .scsi {
  514. var bus = "scsi"
  515. if system.architecture != .sparc && system.architecture != .sparc64 {
  516. bus = "scsi0"
  517. if busindex == 0 {
  518. f("-device")
  519. f("lsi53c895a,id=scsi0")
  520. }
  521. }
  522. f("-device")
  523. if isCd {
  524. "scsi-cd"
  525. } else {
  526. "scsi-hd"
  527. }
  528. "bus=\(bus).0"
  529. "channel=0"
  530. "scsi-id=\(busindex)"
  531. busindex += 1
  532. "drive=drive\(drive.id)"
  533. "bootindex=\(bootindex)"
  534. bootindex += 1
  535. f()
  536. } else if drive.interface == .virtio {
  537. f("-device")
  538. if system.architecture == .s390x {
  539. "virtio-blk-ccw"
  540. } else {
  541. "virtio-blk-pci"
  542. }
  543. "drive=drive\(drive.id)"
  544. "bootindex=\(bootindex)"
  545. bootindex += 1
  546. f()
  547. } else if drive.interface == .nvme {
  548. f("-device")
  549. "nvme"
  550. "drive=drive\(drive.id)"
  551. "serial=\(drive.id)"
  552. "bootindex=\(bootindex)"
  553. bootindex += 1
  554. f()
  555. } else if drive.interface == .usb {
  556. f("-device")
  557. // use usb 3 bus for virt system, unless using legacy input setting (this mirrors the code in argsForUsb)
  558. let isUsb3 = isUsbUsed && system.target.rawValue.hasPrefix("virt")
  559. "usb-storage"
  560. "drive=drive\(drive.id)"
  561. "removable=\(isRemovable)"
  562. "bootindex=\(bootindex)"
  563. bootindex += 1
  564. if isUsb3 {
  565. "bus=usb-bus.0"
  566. }
  567. f()
  568. } else if drive.interface == .floppy {
  569. if system.target.rawValue.hasPrefix("q35") {
  570. f("-device")
  571. "isa-fdc"
  572. "id=fdc\(busindex)"
  573. "bootindexA=\(bootindex)"
  574. bootindex += 1
  575. f()
  576. f("-device")
  577. "floppy"
  578. "unit=0"
  579. "bus=fdc\(busindex).0"
  580. busindex += 1
  581. "drive=drive\(drive.id)"
  582. f()
  583. } else {
  584. realInterface = drive.interface
  585. }
  586. } else {
  587. realInterface = drive.interface
  588. }
  589. busInterfaceMap["boot"] = bootindex
  590. busInterfaceMap[drive.interface.rawValue] = busindex
  591. f("-drive")
  592. switch realInterface {
  593. case .ide:
  594. "if=ide"
  595. case .scsi:
  596. "if=scsi"
  597. case .sd:
  598. "if=sd"
  599. case .mtd:
  600. "if=mtd"
  601. case .floppy:
  602. "if=floppy"
  603. case .pflash:
  604. "if=pflash"
  605. default:
  606. "if=none"
  607. }
  608. if isCd {
  609. "media=cdrom"
  610. } else {
  611. "media=disk"
  612. }
  613. "id=drive\(drive.id)"
  614. if let imageURL = drive.imageURL {
  615. "file="
  616. imageURL
  617. } else if !isCd {
  618. "file.filename=/dev/null"
  619. "file.locking=off"
  620. }
  621. if drive.isReadOnly || isCd {
  622. "readonly=on"
  623. } else {
  624. "discard=unmap"
  625. "detect-zeroes=unmap"
  626. }
  627. f()
  628. }
  629. @QEMUArgumentBuilder private var usbArguments: [QEMUArgument] {
  630. if system.target.rawValue.hasPrefix("virt") {
  631. f("-device")
  632. f("nec-usb-xhci,id=usb-bus")
  633. } else {
  634. f("-usb")
  635. }
  636. f("-device")
  637. f("usb-tablet,bus=usb-bus.0")
  638. f("-device")
  639. f("usb-mouse,bus=usb-bus.0")
  640. f("-device")
  641. f("usb-kbd,bus=usb-bus.0")
  642. #if WITH_USB
  643. let maxDevices = input.maximumUsbShare
  644. let buses = (maxDevices + 2) / 3
  645. if input.usbBusSupport == .usb3_0 {
  646. var controller = "qemu-xhci"
  647. if system.target.rawValue.hasPrefix("pc") || system.target.rawValue.hasPrefix("q35") {
  648. controller = "nec-usb-xhci"
  649. }
  650. for i in 0..<buses {
  651. f("-device")
  652. f("\(controller),id=usb-controller-\(i)")
  653. }
  654. } else {
  655. for i in 0..<buses {
  656. f("-device")
  657. f("ich9-usb-ehci1,id=usb-controller-\(i)")
  658. f("-device")
  659. f("ich9-usb-uhci1,masterbus=usb-controller-\(i).0,firstport=0,multifunction=on")
  660. f("-device")
  661. f("ich9-usb-uhci2,masterbus=usb-controller-\(i).0,firstport=2,multifunction=on")
  662. f("-device")
  663. f("ich9-usb-uhci3,masterbus=usb-controller-\(i).0,firstport=4,multifunction=on")
  664. }
  665. }
  666. // set up usb forwarding
  667. for i in 0..<maxDevices {
  668. f("-chardev")
  669. f("spicevmc,name=usbredir,id=usbredirchardev\(i)")
  670. f("-device")
  671. f("usb-redir,chardev=usbredirchardev\(i),id=usbredirdev\(i),bus=usb-controller-\(i/3).0")
  672. }
  673. #endif
  674. }
  675. private func parseNetworkSubnet(from network: UTMQemuConfigurationNetwork) -> (start: String, end: String, mask: String)? {
  676. guard let net = network.vlanGuestAddress else {
  677. return nil
  678. }
  679. let components = net.split(separator: "/")
  680. let address: String
  681. let binaryMask: UInt32
  682. guard components.count >= 1 else {
  683. return nil
  684. }
  685. if components.count == 2 {
  686. var netmaskAddr = in_addr()
  687. if inet_pton(AF_INET, String(components[1]), &netmaskAddr) == 1 {
  688. binaryMask = UInt32(bigEndian: netmaskAddr.s_addr)
  689. } else {
  690. let topbits = Int(components[1])
  691. guard let topbits = topbits, topbits >= 0 && topbits < 32 else {
  692. return nil
  693. }
  694. binaryMask = (0xFFFFFFFF as UInt32) << (32 - topbits)
  695. }
  696. } else {
  697. binaryMask = 0xFFFFFF00
  698. }
  699. address = String(components[0])
  700. var networkAddr = in_addr()
  701. let netmask = in_addr(s_addr: in_addr_t(bigEndian: binaryMask))
  702. guard inet_pton(AF_INET, address, &networkAddr) == 1 else {
  703. return nil
  704. }
  705. let firstAddr = in_addr(s_addr: (in_addr_t(bigEndian: networkAddr.s_addr & netmask.s_addr) + 1).bigEndian)
  706. let lastAddr = in_addr(s_addr: (in_addr_t(bigEndian: networkAddr.s_addr | ~netmask.s_addr) - 1).bigEndian)
  707. let firstAddrStr = String(cString: inet_ntoa(firstAddr))
  708. let lastAddrStr = String(cString: inet_ntoa(lastAddr))
  709. let netmaskStr = String(cString: inet_ntoa(netmask))
  710. return (network.vlanDhcpStartAddress ?? firstAddrStr, network.vlanDhcpEndAddress ?? lastAddrStr, netmaskStr)
  711. }
  712. #if os(macOS)
  713. private var defaultBridgedInterface: String {
  714. VZBridgedNetworkInterface.networkInterfaces.first?.identifier ?? "en0"
  715. }
  716. #endif
  717. @QEMUArgumentBuilder private var networkArguments: [QEMUArgument] {
  718. for i in networks.indices {
  719. if isSparc {
  720. f("-net")
  721. "nic"
  722. "model=lance"
  723. "macaddr=\(networks[i].macAddress)"
  724. "netdev=net\(i)"
  725. f()
  726. } else {
  727. f("-device")
  728. networks[i].hardware
  729. "mac=\(networks[i].macAddress)"
  730. "netdev=net\(i)"
  731. f()
  732. }
  733. f("-netdev")
  734. var useVMnet = false
  735. #if os(macOS)
  736. if networks[i].mode == .shared {
  737. useVMnet = true
  738. "vmnet-shared"
  739. "id=net\(i)"
  740. } else if networks[i].mode == .bridged {
  741. useVMnet = true
  742. "vmnet-bridged"
  743. "id=net\(i)"
  744. "ifname=\(networks[i].bridgeInterface ?? defaultBridgedInterface)"
  745. } else if networks[i].mode == .host {
  746. useVMnet = true
  747. "vmnet-host"
  748. "id=net\(i)"
  749. } else {
  750. "user"
  751. "id=net\(i)"
  752. }
  753. #else
  754. "user"
  755. "id=net\(i)"
  756. #endif
  757. if networks[i].isIsolateFromHost {
  758. if useVMnet {
  759. "isolated=on"
  760. } else {
  761. "restrict=on"
  762. }
  763. }
  764. if useVMnet {
  765. if let subnet = parseNetworkSubnet(from: networks[i]) {
  766. "start-address=\(subnet.start)"
  767. "end-address=\(subnet.end)"
  768. "subnet-mask=\(subnet.mask)"
  769. }
  770. if let nat66prefix = networks[i].vlanGuestAddressIPv6 {
  771. "nat66-prefix=\(nat66prefix)"
  772. }
  773. } else {
  774. if let guestAddress = networks[i].vlanGuestAddress {
  775. "net=\(guestAddress)"
  776. }
  777. if let hostAddress = networks[i].vlanHostAddress {
  778. "host=\(hostAddress)"
  779. }
  780. if let guestAddressIPv6 = networks[i].vlanGuestAddressIPv6 {
  781. "ipv6-net=\(guestAddressIPv6)"
  782. }
  783. if let hostAddressIPv6 = networks[i].vlanHostAddressIPv6 {
  784. "ipv6-host=\(hostAddressIPv6)"
  785. }
  786. if let dhcpStartAddress = networks[i].vlanDhcpStartAddress {
  787. "dhcpstart=\(dhcpStartAddress)"
  788. }
  789. if let dnsServerAddress = networks[i].vlanDnsServerAddress {
  790. "dns=\(dnsServerAddress)"
  791. }
  792. if let dnsServerAddressIPv6 = networks[i].vlanDnsServerAddressIPv6 {
  793. "ipv6-dns=\(dnsServerAddressIPv6)"
  794. }
  795. if let dnsSearchDomain = networks[i].vlanDnsSearchDomain {
  796. "dnssearch=\(dnsSearchDomain)"
  797. }
  798. if let dhcpDomain = networks[i].vlanDhcpDomain {
  799. "domainname=\(dhcpDomain)"
  800. }
  801. for forward in networks[i].portForward {
  802. "hostfwd=\(forward.protocol.rawValue.lowercased()):\(forward.hostAddress ?? ""):\(forward.hostPort)-\(forward.guestAddress ?? ""):\(forward.guestPort)"
  803. }
  804. }
  805. f()
  806. }
  807. if networks.count == 0 {
  808. f("-nic")
  809. f("none")
  810. }
  811. }
  812. private var isSpiceAgentUsed: Bool {
  813. guard system.architecture.hasAgentSupport && system.target.hasAgentSupport else {
  814. return false
  815. }
  816. return sharing.hasClipboardSharing || sharing.directoryShareMode == .webdav || displays.contains(where: { $0.isDynamicResolution })
  817. }
  818. @QEMUArgumentBuilder private var sharingArguments: [QEMUArgument] {
  819. if system.architecture.hasAgentSupport && system.target.hasAgentSupport {
  820. f("-device")
  821. f("virtio-serial")
  822. f("-device")
  823. f("virtserialport,chardev=org.qemu.guest_agent,name=org.qemu.guest_agent.0")
  824. f("-chardev")
  825. f("spiceport,id=org.qemu.guest_agent,name=org.qemu.guest_agent.0")
  826. }
  827. if isSpiceAgentUsed {
  828. f("-device")
  829. f("virtserialport,chardev=vdagent,name=com.redhat.spice.0")
  830. f("-chardev")
  831. f("spicevmc,id=vdagent,debug=0,name=vdagent")
  832. if sharing.directoryShareMode == .webdav {
  833. f("-device")
  834. f("virtserialport,chardev=charchannel1,id=channel1,name=org.spice-space.webdav.0")
  835. f("-chardev")
  836. f("spiceport,name=org.spice-space.webdav.0,id=charchannel1")
  837. }
  838. }
  839. if system.architecture.hasSharingSupport && sharing.directoryShareMode == .virtfs, let url = sharing.directoryShareUrl {
  840. f("-fsdev")
  841. "local"
  842. "id=virtfs0"
  843. "path="
  844. url
  845. "security_model=mapped-xattr"
  846. if sharing.isDirectoryShareReadOnly {
  847. "readonly=on"
  848. }
  849. f()
  850. f("-device")
  851. if system.architecture == .s390x {
  852. "virtio-9p-ccw"
  853. } else {
  854. "virtio-9p-pci"
  855. }
  856. "fsdev=virtfs0"
  857. "mount_tag=share"
  858. }
  859. }
  860. private func cleanupName(_ name: String) -> String {
  861. let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces)
  862. let filteredString = name.components(separatedBy: allowedCharacterSet.inverted)
  863. .joined(separator: "")
  864. return filteredString
  865. }
  866. @QEMUArgumentBuilder private var miscArguments: [QEMUArgument] {
  867. f("-name")
  868. f(cleanupName(information.name))
  869. if qemu.isDisposable {
  870. f("-snapshot")
  871. }
  872. f("-uuid")
  873. f(information.uuid.uuidString)
  874. if qemu.hasRTCLocalTime {
  875. f("-rtc")
  876. f("base=localtime")
  877. }
  878. if qemu.hasRNGDevice {
  879. f("-device")
  880. f("virtio-rng-pci")
  881. }
  882. if qemu.hasBalloonDevice {
  883. f("-device")
  884. f("virtio-balloon-pci")
  885. }
  886. if qemu.hasTPMDevice {
  887. tpmArguments
  888. }
  889. }
  890. @QEMUArgumentBuilder private var tpmArguments: [QEMUArgument] {
  891. f("-chardev")
  892. "socket"
  893. "id=chrtpm0"
  894. "path=\(swtpmSocketURL.lastPathComponent)"
  895. f()
  896. f("-tpmdev")
  897. "emulator"
  898. "id=tpm0"
  899. "chardev=chrtpm0"
  900. f()
  901. f("-device")
  902. if system.target.rawValue.hasPrefix("virt") {
  903. "tpm-crb-device"
  904. } else if system.architecture == .ppc64 {
  905. "tpm-spapr"
  906. } else {
  907. "tpm-tis"
  908. }
  909. "tpmdev=tpm0"
  910. f()
  911. }
  912. }
  913. private extension String {
  914. func appendingDefaultPropertyName(_ name: String, value: String) -> String {
  915. if !self.contains(name + "=") {
  916. return self.appending("\(self.count > 0 ? "," : "")\(name)=\(value)")
  917. } else {
  918. return self
  919. }
  920. }
  921. }