├── .gitignore ├── virtualOS ├── Resources │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── virtual os app icon-128.png │ │ │ ├── virtual os app icon-16.png │ │ │ ├── virtual os app icon-256.png │ │ │ ├── virtual os app icon-32.png │ │ │ ├── virtual os app icon-512.png │ │ │ ├── virtual os app icon-64.png │ │ │ ├── virtual os app icon-1024.png │ │ │ └── Contents.json │ │ └── AccentColor.colorset │ │ │ └── Contents.json │ ├── Info.plist │ └── virtualOS.entitlements ├── Model │ ├── Errors.swift │ ├── Enums.swift │ ├── Logger.swift │ ├── Bundle.swift │ ├── Constants.swift │ ├── AppDelegate.swift │ ├── TextFieldDelegate.swift │ ├── TableViewDataSource.swift │ ├── Results.swift │ ├── ParametersViewDelegate.swift │ ├── ParametersViewDataSource.swift │ ├── Bookmark.swift │ ├── FileModel.swift │ └── MainViewModel.swift ├── Extension │ ├── UInt64+Byte.swift │ ├── RestoreImage+SystemVersion.swift │ ├── UserDefaults+Settings.swift │ └── URL+Paths.swift ├── ViewController │ ├── AlertController.swift │ ├── WindowController.swift │ ├── SettingsViewController.swift │ ├── ProgressViewController.swift │ ├── VMViewController.swift │ ├── RestoreImageViewController.swift │ └── MainViewController.swift ├── VM │ ├── VMParameters.swift │ ├── MacPlatformConfiguration.swift │ └── VMConfiguration.swift └── RestoreImage │ ├── RestoreImageDownload.swift │ └── RestoreImageInstall.swift ├── virtualOS.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcshareddata │ └── xcschemes │ │ └── virtualOS.xcscheme └── project.pbxproj ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | virtualOS.xcodeproj/xcuserdata 2 | *.xcuserdatad 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yep/virtualOS/HEAD/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-128.png -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yep/virtualOS/HEAD/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-16.png -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yep/virtualOS/HEAD/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-256.png -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yep/virtualOS/HEAD/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-32.png -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yep/virtualOS/HEAD/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-512.png -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yep/virtualOS/HEAD/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-64.png -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yep/virtualOS/HEAD/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-1024.png -------------------------------------------------------------------------------- /virtualOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /virtualOS/Model/Errors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Errors.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | struct RestoreError: Error { 10 | var localizedDescription = "Restore Error" 11 | } 12 | -------------------------------------------------------------------------------- /virtualOS/Model/Enums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Enums.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | enum NetworkType: String, CaseIterable, Codable { 10 | case NAT 11 | case Bridge 12 | } 13 | -------------------------------------------------------------------------------- /virtualOS/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ITSAppUsesNonExemptEncryption 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /virtualOS/Model/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import OSLog 10 | 11 | extension Logger { 12 | static let shared = Logger.init(subsystem: "com.github.virtualOS", category: "log") 13 | } 14 | -------------------------------------------------------------------------------- /virtualOS/Resources/virtualOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.vm.networking 6 | 7 | com.apple.security.device.audio-input 8 | 9 | com.apple.security.virtualization 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /virtualOS/Extension/UInt64+Byte.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UInt+Byte.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Foundation 10 | 11 | extension UInt64 { 12 | func bytesToGigabytes() -> UInt64 { 13 | return self / (1024 * 1024 * 1024) 14 | } 15 | 16 | func gigabytesToBytes() -> UInt64 { 17 | return self * 1024 * 1024 * 1024 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /virtualOS/Model/Bundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Foundation 10 | 11 | struct VMBundle: Identifiable, Hashable { 12 | var id: String { 13 | return url.path 14 | } 15 | var url: URL 16 | var name: String { 17 | return url.lastPathComponent.replacingOccurrences(of: ".bundle", with: "") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /virtualOS/Extension/RestoreImage+SystemVersion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OsVersion.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Virtualization 10 | 11 | #if arch(arm64) 12 | 13 | extension VZMacOSRestoreImage { 14 | var operatingSystemVersionString: String { 15 | return "macOS \(operatingSystemVersion.majorVersion).\(operatingSystemVersion.minorVersion).\(operatingSystemVersion.patchVersion) (Build \(buildVersion))" 16 | } 17 | } 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /virtualOS/Model/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | 11 | struct Constants { 12 | static let restoreImageNameLatest = "latest" 13 | static let selectedRestoreImage = "selectedRestoreImage" 14 | static let defaultDiskImageSize = 30 15 | static let restoreImageNameSelectedNotification = Notification.Name("restoreImageSelected") 16 | static let didChangeAppSettingsNotification = Notification.Name("didChangeAppSettings") 17 | 18 | enum NetworkType: String, CaseIterable, Codable { 19 | case nat 20 | case bridged 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /virtualOS/Model/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Cocoa 10 | 11 | @main 12 | final class AppDelegate: NSObject, NSApplicationDelegate { 13 | func applicationDidFinishLaunching(_ aNotification: Notification) { 14 | // Insert code here to initialize your application 15 | } 16 | 17 | func applicationWillTerminate(_ aNotification: Notification) { 18 | // Insert code here to tear down your application 19 | } 20 | 21 | func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 22 | return true 23 | } 24 | 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /virtualOS/Model/TextFieldDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextFieldDelegate.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | 11 | final class TextFieldDelegate: NSObject, NSTextFieldDelegate { 12 | var vmBundle: VMBundle? 13 | 14 | func control(_ control: NSControl, textShouldEndEditing fieldEditor: NSText) -> Bool { 15 | if let vmBundle = vmBundle, 16 | vmBundle.name != fieldEditor.string 17 | { 18 | let newFilename = "\(fieldEditor.string).bundle" 19 | let newUrl = vmBundle.url.deletingLastPathComponent().appendingPathComponent(newFilename) 20 | 21 | try? FileManager.default.moveItem(at: vmBundle.url, to: newUrl) 22 | } 23 | 24 | return true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /virtualOS/ViewController/AlertController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertController.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | 11 | extension NSAlert { 12 | static func okCancelAlert(messageText: String, informativeText: String, showCancelButton: Bool = true, accessoryView: NSView? = nil, alertStyle: NSAlert.Style = .informational) -> NSAlert { 13 | let alert: NSAlert = NSAlert() 14 | 15 | alert.messageText = messageText 16 | alert.informativeText = informativeText 17 | alert.accessoryView = accessoryView 18 | alert.alertStyle = alertStyle 19 | alert.addButton(withTitle: "OK") 20 | if showCancelButton { 21 | alert.addButton(withTitle: "Cancel") 22 | } 23 | 24 | return alert 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # virtualOS 2 | 3 | Run a virtual macOS machine on your Apple Silicon computer. 4 | 5 | On first start, the latest macOS restore image can be downloaded from Apple servers. After installation has finished, you can start using the virtual machine by performing the initial operating system (OS) setup. 6 | 7 | You can configure the following virtual machine parameters: 8 | - CPU count 9 | - RAM 10 | - Screen size 11 | - Shared folder 12 | 13 | To use USB disks, you can set the location where VM files are stored. 14 | 15 | Unlike other apps on the AppStore, no In-App purchases are required for managing multiple virtual machines, setting CPU count or the amount of RAM. 16 | 17 | ## Download 18 | 19 | You can download this app from the [macOS AppStore](https://apps.apple.com/us/app/virtualos/id1614659226) 20 | 21 | This application is free and open source software, source code is available at: https://github.com/yep/virtualOS 22 | 23 | Mac and macOS are trademarks of Apple Inc., registered in the U.S. and other countries and regions. 24 | -------------------------------------------------------------------------------- /virtualOS/Model/TableViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewDataSource.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | 11 | final class TableViewDataSource: NSObject, NSTableViewDataSource { 12 | fileprivate let fileModel = FileModel() 13 | 14 | func rows() -> Int { 15 | return fileModel.getVMBundles().count 16 | } 17 | 18 | func numberOfRows(in tableView: NSTableView) -> Int { 19 | return fileModel.getVMBundles().count 20 | } 21 | 22 | func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { 23 | return vmBundle(forRow: row)?.name 24 | } 25 | 26 | func vmBundle(forRow row: Int) -> VMBundle? { 27 | let bundles = fileModel.getVMBundles() 28 | if 0 <= row && row < bundles.count { 29 | return bundles[row] 30 | } else { 31 | return nil 32 | } 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /virtualOS/Model/Results.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Results.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import OSLog 10 | import Virtualization 11 | 12 | #if arch(arm64) 13 | 14 | typealias RestoreResult = Result 15 | extension RestoreResult { 16 | init() { 17 | self = .success(true) 18 | } 19 | 20 | init(errorMessage: String) { 21 | Logger.shared.log(level: .default, "\(errorMessage)") 22 | self = .failure(.init(localizedDescription: errorMessage)) 23 | } 24 | } 25 | 26 | typealias MacPlatformConfigurationResult = Result 27 | extension MacPlatformConfigurationResult { 28 | init(macPlatformConfiguration: VZMacPlatformConfiguration) { 29 | self = .success(macPlatformConfiguration) 30 | } 31 | 32 | init(errorMessage: String) { 33 | Logger.shared.log(level: .default, "\(errorMessage)") 34 | self = .failure(.init(localizedDescription: errorMessage)) 35 | } 36 | } 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /virtualOS/Model/ParametersViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParametersViewDelegate.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | 11 | final class ParametersViewDelegate: NSObject, NSOutlineViewDelegate { 12 | func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { 13 | var string = "unknown" 14 | 15 | if let identifier = tableColumn?.identifier, 16 | let array = item as? [String] 17 | { 18 | if array.count == 2 { 19 | switch identifier.rawValue { 20 | case "AutomaticTableColumnIdentifier.0": 21 | string = array[0] 22 | case "AutomaticTableColumnIdentifier.1": 23 | string = array[1] 24 | default: 25 | string = "default" 26 | 27 | } 28 | } else if array.count == 1 { 29 | string = array[0] 30 | } 31 | } 32 | 33 | return NSTextField(labelWithString: string) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022-2025 Jahn Bertsch 2 | Copyright 2021 Khaos Tian 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | See the License for the specific language governing permissions and limitations under the License. 10 | 11 | – 12 | 13 | Copyright © 2021 Apple Inc. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "virtual os app icon-16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "virtual os app icon-32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "virtual os app icon-32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "virtual os app icon-64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "virtual os app icon-128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "virtual os app icon-256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "virtual os app icon-256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "virtual os app icon-512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "virtual os app icon-512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "virtual os app icon-1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /virtualOS/ViewController/WindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowController.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Cocoa 10 | import OSLog 11 | 12 | #if arch(arm64) 13 | 14 | class WindowController: NSWindowController { 15 | @IBOutlet weak var toolbar: NSToolbar! 16 | @IBOutlet weak var installButton: NSToolbarItem! 17 | @IBOutlet weak var startButton: NSToolbarItem! 18 | @IBOutlet weak var sharedFolderButton: NSToolbarItem! 19 | @IBOutlet weak var deleteButton: NSToolbarItem! 20 | 21 | weak var mainViewController: MainViewController? 22 | 23 | override func windowDidLoad() { 24 | super.windowDidLoad() 25 | toolbar.allowsUserCustomization = false 26 | } 27 | 28 | @IBAction func installButtonPressed(_ sender: NSButton) { 29 | mainViewController?.installButtonPressed(sender) 30 | } 31 | 32 | @IBAction func startButtonPressed(_ sender: NSButton) { 33 | mainViewController?.startButtonPressed(sender) 34 | } 35 | 36 | @IBAction func sharedFolderButtonPressed(_ sender: NSButton) { 37 | mainViewController?.sharedFolderButtonPressed(sender) 38 | } 39 | 40 | @IBAction func deleteButtonPressed(_ sender: NSButton) { 41 | mainViewController?.deleteButtonPressed(sender) 42 | } 43 | 44 | func updateButtons(hidden: Bool) { 45 | if hidden { 46 | while toolbar.items.count > 1 { 47 | toolbar.removeItem(at: 1) 48 | } 49 | } else if toolbar.items.count == 1 { 50 | toolbar.insertItem(withItemIdentifier: startButton.itemIdentifier, at: 1) 51 | toolbar.insertItem(withItemIdentifier: sharedFolderButton.itemIdentifier, at: 2) 52 | toolbar.insertItem(withItemIdentifier: deleteButton.itemIdentifier, at: 3) 53 | } 54 | } 55 | } 56 | 57 | #endif 58 | -------------------------------------------------------------------------------- /virtualOS/Model/ParametersViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParametersViewDataSource.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | 11 | #if arch(arm64) 12 | 13 | final class ParametersViewDataSource: NSObject, NSOutlineViewDataSource { 14 | weak var mainViewModel: MainViewModel? 15 | 16 | func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { 17 | if let vmParameters = mainViewModel?.vmParameters { 18 | if vmParameters.installFinished == true { 19 | return 3 20 | } else { 21 | return 1 22 | } 23 | } else { 24 | return 0 25 | } 26 | } 27 | 28 | func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { 29 | if let vmParameters = mainViewModel?.vmParameters { 30 | if vmParameters.installFinished == false { 31 | return ["Install incomplete", "Delete this VM and reinstall."] 32 | } else { 33 | if index == 0 { 34 | return ["Disk Size (GB)", "\(vmParameters.diskSizeInGB)"] 35 | } else if index == 1 { 36 | let sharedFolderString = sharedFolderInfo(vmParameters: vmParameters) 37 | return ["Shared Folder", sharedFolderString] 38 | } else if index == 2 { 39 | return ["Version", "\(vmParameters.version)"] 40 | } 41 | } 42 | 43 | } 44 | 45 | return ["index \(index)", "value \(index)"] 46 | } 47 | 48 | fileprivate func sharedFolderInfo(vmParameters: VMParameters) -> String { 49 | if let sharedFolderURL = vmParameters.sharedFolderURL, 50 | let sharedFolderData = vmParameters.sharedFolderData, 51 | let bookmarkURL = Bookmark.startAccess(bookmarkData: sharedFolderData, for: sharedFolderURL.path), 52 | let bookmarkPath = bookmarkURL.path.removingPercentEncoding 53 | { 54 | return bookmarkPath 55 | } 56 | return "No shared folder" 57 | } 58 | } 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /virtualOS/Model/Bookmark.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+Bookmark.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file.. 7 | // 8 | 9 | import Foundation 10 | import OSLog 11 | 12 | struct Bookmark { 13 | fileprivate static var accessedURLs: [String: URL] = [:] 14 | 15 | static func createBookmarkData(fromUrl url: URL) -> Data? { 16 | if let bookmarkData = try? url.bookmarkData(options: .withSecurityScope, relativeTo: nil) { 17 | return bookmarkData 18 | } 19 | return nil 20 | } 21 | 22 | static func startAccess(bookmarkData: Data?, for path: String) -> URL? { 23 | var bookmarkDataIsStale = false 24 | if let bookmarkData = bookmarkData, 25 | let bookmarkURL = try? URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &bookmarkDataIsStale), 26 | !bookmarkDataIsStale 27 | { 28 | // stop accessing previous resource 29 | if let previousURL = accessedURLs[path], 30 | previousURL != bookmarkURL 31 | { 32 | stopAccess(url: previousURL) 33 | } 34 | 35 | if accessedURLs[path] != bookmarkURL { 36 | // resource not already accessed, start access 37 | if bookmarkURL.startAccessingSecurityScopedResource() { 38 | // Logger.shared.log(level: .info, "start accessing security scoped resource: \(path)") 39 | } else { 40 | // Logger.shared.log(level: .info, "stop accessing security scoped resource failed") 41 | } 42 | accessedURLs[path] = bookmarkURL 43 | } 44 | return bookmarkURL 45 | } 46 | 47 | return nil 48 | } 49 | 50 | static func stopAccess(url: URL) { 51 | url.stopAccessingSecurityScopedResource() 52 | // Logger.shared.log(level: .info, "stop accessing security scoped resource (1): \(url.path)") 53 | Self.accessedURLs[url.path] = nil 54 | } 55 | 56 | static func stopAllAccess() { 57 | for (urlString, accessedURL) in accessedURLs { 58 | accessedURL.stopAccessingSecurityScopedResource() 59 | Logger.shared.log(level: .info, "stop accessing security scoped resource (2): \(urlString)") 60 | } 61 | Self.accessedURLs = [:] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /virtualOS/Model/FileModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileModel.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Foundation 10 | import OSLog 11 | 12 | struct FileModel { 13 | func getVMBundles() -> [VMBundle] { 14 | var result: [VMBundle] = [] 15 | let vmFilesDirectoryPath = URL.vmFilesDirectoryURL 16 | 17 | if let urls = try? FileManager.default.contentsOfDirectory(at: vmFilesDirectoryPath, includingPropertiesForKeys: nil, options: []) { 18 | for url in urls { 19 | if url.lastPathComponent.hasSuffix("bundle") { 20 | result.append(VMBundle(url: url)) 21 | } 22 | } 23 | } 24 | result.sort { lhs, rhs in 25 | lhs.name < rhs.name 26 | } 27 | return result 28 | } 29 | 30 | /// Returns the names of the restore images in the images directory 31 | /// - Performs directory scan each time 32 | func getRestoreImages() -> [String] { 33 | var result: [String] = [] 34 | 35 | if let urls = try? FileManager.default.contentsOfDirectory(at: URL.restoreImagesDirectoryURL, includingPropertiesForKeys: nil, options: []) { 36 | for url in urls { 37 | if url.lastPathComponent.hasSuffix("ipsw") { 38 | result.append(url.lastPathComponent) 39 | } 40 | } 41 | } 42 | 43 | return result 44 | } 45 | 46 | static func createVMFilesDirectory() -> URL { 47 | var url = URL.baseURL 48 | if let vmFilesDirectory = UserDefaults.standard.vmFilesDirectory { 49 | url = URL(fileURLWithPath: vmFilesDirectory) 50 | } 51 | 52 | if !FileManager.default.fileExists(atPath: url.path) { 53 | try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) 54 | } 55 | 56 | return url 57 | } 58 | 59 | static func cleanUpTemporaryFiles() { 60 | do { 61 | let files = try FileManager.default.contentsOfDirectory(at: URL.tmpURL, includingPropertiesForKeys: nil) 62 | for file in files { 63 | try FileManager.default.removeItem(at: file) 64 | } 65 | } catch let error { 66 | Logger.shared.log(level: .default, "error: removing temporary file failed: \(error)") 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /virtualOS/Model/MainViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewModel.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | import AVFoundation // for microphone support 11 | import OSLog 12 | 13 | #if arch(arm64) 14 | 15 | final class MainViewModel { 16 | let tableViewDataSource = TableViewDataSource() 17 | let parametersViewDataSource = ParametersViewDataSource() 18 | let parametersViewDelegate = ParametersViewDelegate() 19 | let textFieldDelegate = TextFieldDelegate() 20 | var vmBundle: VMBundle? 21 | var selectedRow: Int? = 0 22 | var vmParameters: VMParameters? 23 | 24 | init() { 25 | parametersViewDataSource.mainViewModel = self 26 | } 27 | 28 | func storeParametersToDisk() { 29 | if let vmParameters = vmParameters, 30 | let vmBundleUrl = vmBundle?.url 31 | { 32 | vmParameters.writeToDisk(bundleURL: vmBundleUrl) 33 | } 34 | } 35 | 36 | func deleteVM(selection: NSApplication.ModalResponse, vmBundle: VMBundle) { 37 | try? FileManager.default.removeItem(at: vmBundle.url) 38 | if let selectedRow, 39 | selectedRow > tableViewDataSource.rows() - 1 40 | { 41 | self.selectedRow = tableViewDataSource.rows() - 1 // select last table row 42 | } 43 | } 44 | 45 | func set(sharedFolderUrl: URL?) { 46 | var sharedFolderData: Data? = nil 47 | 48 | if let sharedFolderUrl { 49 | sharedFolderData = Bookmark.createBookmarkData(fromUrl: sharedFolderUrl) 50 | if let sharedFolderData { 51 | _ = Bookmark.startAccess(bookmarkData: sharedFolderData, for: sharedFolderUrl.path) 52 | 53 | if let selectedRow { 54 | let bundle = tableViewDataSource.vmBundle(forRow: selectedRow) 55 | if let bundleURL = bundle?.url { 56 | var vmParameters = VMParameters.readFrom(url: bundleURL) 57 | vmParameters?.sharedFolderURL = sharedFolderUrl 58 | vmParameters?.sharedFolderData = sharedFolderData 59 | vmParameters?.writeToDisk(bundleURL: bundleURL) 60 | self.vmParameters = vmParameters 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | func checkMicrophonePermission(completion: @escaping () -> Void) { 68 | AVCaptureDevice.requestAccess(for: .audio) { granted in 69 | // Logger.shared.log(level: .default, "audio support in the vm enabled: \(granted)") 70 | if !granted { 71 | self.vmParameters?.microphoneEnabled = false 72 | self.storeParametersToDisk() 73 | completion() 74 | } 75 | } 76 | } 77 | 78 | } 79 | 80 | #endif 81 | -------------------------------------------------------------------------------- /virtualOS/Extension/UserDefaults+Settings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults+Settings.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Foundation 10 | 11 | extension UserDefaults { 12 | fileprivate static let diskSizeKey = "diskSize" 13 | fileprivate static let vmFilesDirectoryKey = "vmFilesDirectoryKey" 14 | fileprivate static let vmFilesDirectoryBookmarkDataKey = "vmFilesDirectoryBookmarkData" 15 | fileprivate static let restoreImagesDirectoryKey = "restoreImagesDirectoryKey" 16 | fileprivate static let restoreImagesDirectoryBookmarkDataKey = "restoreImagesDirectoryBookmarkData" 17 | fileprivate static let userRatingCounterKey = "userRatingCounterKey" 18 | 19 | var diskSize: Int { 20 | get { 21 | if object(forKey: Self.diskSizeKey) != nil { 22 | return integer(forKey: Self.diskSizeKey) 23 | } 24 | return Constants.defaultDiskImageSize 25 | } 26 | set { 27 | set(newValue, forKey: Self.diskSizeKey) 28 | synchronize() 29 | } 30 | } 31 | 32 | var vmFilesDirectory: String? { 33 | get { 34 | if let result = string(forKey: Self.vmFilesDirectoryKey) { 35 | return result.removingPercentEncoding 36 | } else { 37 | return nil 38 | } 39 | } 40 | set { 41 | set(newValue, forKey: Self.vmFilesDirectoryKey) 42 | synchronize() 43 | } 44 | } 45 | 46 | var vmFilesDirectoryBookmarkData: Data? { 47 | get { 48 | return data(forKey: Self.vmFilesDirectoryBookmarkDataKey) 49 | } 50 | set { 51 | set(newValue, forKey: Self.vmFilesDirectoryBookmarkDataKey) 52 | synchronize() 53 | } 54 | } 55 | 56 | var restoreImagesDirectory: String? { 57 | get { 58 | if let result = string(forKey: Self.restoreImagesDirectoryKey) { 59 | return result.removingPercentEncoding 60 | } else { 61 | return nil 62 | } 63 | } 64 | set { 65 | set(newValue, forKey: Self.restoreImagesDirectoryKey) 66 | synchronize() 67 | } 68 | } 69 | 70 | var restoreImagesDirectoryBookmarkData: Data? { 71 | get { 72 | return data(forKey: Self.restoreImagesDirectoryBookmarkDataKey) 73 | } 74 | set { 75 | set(newValue, forKey: Self.restoreImagesDirectoryBookmarkDataKey) 76 | synchronize() 77 | } 78 | } 79 | 80 | var userRatingCounter: Int { 81 | get { 82 | return integer(forKey: Self.userRatingCounterKey) 83 | } 84 | set { 85 | set(newValue, forKey: Self.userRatingCounterKey) 86 | synchronize() 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /virtualOS.xcodeproj/xcshareddata/xcschemes/virtualOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /virtualOS/ViewController/SettingsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewController.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | import OSLog 11 | 12 | fileprivate enum SettingTag: Int { 13 | case vmFilesDirectory = 1 14 | case restoreImagesDirectory = 2 15 | } 16 | 17 | final class SettingsViewController: NSViewController { 18 | @IBOutlet weak var vmFilesURLLabel: NSTextField! 19 | @IBOutlet weak var restoreImageFilesURLLabel: NSTextField! 20 | 21 | override func viewWillAppear() { 22 | super.viewWillAppear() 23 | updateSettingsLabels() 24 | } 25 | 26 | @IBAction func selectFolderButtonPressed(_ sender: NSButton) { 27 | let openPanel = NSOpenPanel() 28 | openPanel.allowsMultipleSelection = false 29 | openPanel.canChooseDirectories = true 30 | openPanel.canChooseFiles = false 31 | openPanel.prompt = "Select Folder" 32 | let modalResponse = openPanel.runModal() 33 | 34 | guard modalResponse == .OK, 35 | let selectedURL = openPanel.url else 36 | { 37 | return 38 | } 39 | 40 | let selectedPath = selectedURL.path(percentEncoded: false) 41 | 42 | guard let bookmarkData = Bookmark.createBookmarkData(fromUrl: selectedURL), 43 | Bookmark.startAccess(bookmarkData: bookmarkData, for: selectedPath) != nil else 44 | { 45 | Logger.shared.log("Could not create or start accessing bookmark \(selectedURL.path)") 46 | return 47 | } 48 | 49 | switch SettingTag(rawValue: sender.tag) { 50 | case .vmFilesDirectory: 51 | UserDefaults.standard.vmFilesDirectory = selectedPath 52 | UserDefaults.standard.vmFilesDirectoryBookmarkData = bookmarkData 53 | case .restoreImagesDirectory: 54 | UserDefaults.standard.restoreImagesDirectory = selectedPath 55 | UserDefaults.standard.restoreImagesDirectoryBookmarkData = bookmarkData 56 | default: 57 | Logger.shared.log("Invalid setting tag") 58 | } 59 | 60 | postNotification() 61 | } 62 | 63 | @IBAction func resetButtonPressed(_ sender: Any) { 64 | UserDefaults.standard.vmFilesDirectory = nil 65 | UserDefaults.standard.vmFilesDirectoryBookmarkData = nil 66 | 67 | UserDefaults.standard.restoreImagesDirectory = nil 68 | UserDefaults.standard.restoreImagesDirectoryBookmarkData = nil 69 | 70 | postNotification() 71 | } 72 | 73 | @IBAction func showInFinderButtonPressed(_ sender: NSButton) { 74 | var url = URL.baseURL 75 | 76 | switch SettingTag(rawValue: sender.tag) { 77 | case .vmFilesDirectory: 78 | url = URL.vmFilesDirectoryURL 79 | case .restoreImagesDirectory: 80 | url = URL.restoreImagesDirectoryURL 81 | default: 82 | Logger.shared.log("Invalid setting tag") 83 | } 84 | 85 | NSWorkspace.shared.activateFileViewerSelecting([url]) 86 | } 87 | 88 | fileprivate func updateSettingsLabels() { 89 | let vmFilesDirectory = FileModel.createVMFilesDirectory() 90 | vmFilesURLLabel.stringValue = vmFilesDirectory.path 91 | restoreImageFilesURLLabel.stringValue = URL.restoreImagesDirectoryURL.path 92 | } 93 | 94 | fileprivate func postNotification() { 95 | updateSettingsLabels() 96 | NotificationCenter.default.post(name: Constants.didChangeAppSettingsNotification, object: nil) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /virtualOS/ViewController/ProgressViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProgressViewController.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | import OSLog 11 | 12 | #if arch(arm64) 13 | 14 | final class ProgressViewController: NSViewController { 15 | enum Mode { 16 | case download 17 | case install 18 | } 19 | 20 | @IBOutlet weak var progressIndicator: NSProgressIndicator! 21 | @IBOutlet weak var statusTextField: NSTextField! 22 | @IBOutlet weak var cancelButton: NSButton! 23 | 24 | var mode: Mode = .download 25 | var restoreImageName: String? 26 | var diskImageSize: Int? = 0 27 | fileprivate let restoreImageDownload = RestoreImageDownload() 28 | fileprivate var restoreImageInstall = RestoreImageInstall() 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | 33 | progressIndicator.doubleValue = 0 34 | statusTextField.stringValue = "Starting" 35 | statusTextField.font = .monospacedDigitSystemFont(ofSize: 13, weight: .regular) 36 | } 37 | 38 | override func viewDidAppear() { 39 | super.viewDidAppear() 40 | if restoreImageName == Constants.restoreImageNameLatest || 41 | mode == .download 42 | { 43 | restoreImageDownload.delegate = self 44 | restoreImageDownload.fetch() 45 | mode = .download // restoreImageNameLatest is also a download 46 | } else if mode == .install { 47 | restoreImageInstall.restoreImageName = restoreImageName 48 | restoreImageInstall.diskImageSize = diskImageSize 49 | restoreImageInstall.delegate = self 50 | restoreImageInstall.install() 51 | } 52 | } 53 | 54 | override func viewWillDisappear() { 55 | super.viewWillDisappear() 56 | cancel() 57 | } 58 | 59 | @IBAction func cancelButtonPressed(_ sender: NSButton) { 60 | cancel() 61 | if let mainViewController = presentingViewController as? MainViewController { 62 | mainViewController.updateUI() 63 | mainViewController.dismiss(self) 64 | } 65 | } 66 | 67 | fileprivate func cancel() { 68 | if mode == .download { 69 | restoreImageDownload.cancel() 70 | } else if mode == .install { 71 | restoreImageInstall.cancel() 72 | } 73 | } 74 | } 75 | 76 | extension ProgressViewController: ProgressDelegate { 77 | func progress(_ progress: Double, progressString: String) { 78 | DispatchQueue.main.async { [weak self] in 79 | self?.progressIndicator.doubleValue = progress * 100 80 | self?.statusTextField.stringValue = progressString 81 | } 82 | } 83 | 84 | func done(error: Error? = nil) { 85 | DispatchQueue.main.async { [weak self] in 86 | guard let mainViewController = self?.presentingViewController as? MainViewController else { 87 | return 88 | } 89 | 90 | mainViewController.dismiss(self) 91 | mainViewController.updateUI() 92 | 93 | if let error = error { 94 | Logger.shared.log(level: .error, "\(error)") 95 | mainViewController.showErrorAlert(error: error) 96 | self?.statusTextField.stringValue = "Install Failed." 97 | self?.cancelButton.title = "Close" 98 | } else { 99 | self?.cancelButton.title = "Done" 100 | } 101 | } 102 | } 103 | } 104 | 105 | #endif 106 | -------------------------------------------------------------------------------- /virtualOS/Extension/URL+Paths.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+Paths.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Foundation 10 | 11 | extension URL { 12 | static let basePath = NSHomeDirectory() + "/Documents" 13 | 14 | static var baseURL: URL { 15 | return URL(fileURLWithPath: basePath) 16 | } 17 | static var restoreImageURL: URL { 18 | return fileURL(for: UserDefaults.standard.restoreImagesDirectory) 19 | } 20 | static var restoreImagesDirectoryURL: URL { 21 | return fileURL(for: UserDefaults.standard.restoreImagesDirectory) 22 | } 23 | static var vmFilesDirectoryURL: URL { 24 | return fileURL(for: UserDefaults.standard.vmFilesDirectory) 25 | } 26 | static var tmpURL: URL { 27 | return URL(fileURLWithPath: NSHomeDirectory() + "/tmp") 28 | } 29 | 30 | var auxiliaryStorageURL: URL { 31 | return self.appending(path: "AuxiliaryStorage") 32 | } 33 | var hardwareModelURL: URL { 34 | return self.appending(path: "HardwareModel") 35 | } 36 | var diskImageURL: URL { 37 | return self.appending(path: "Disk.img") 38 | } 39 | var machineIdentifierURL: URL { 40 | return self.appending(path: "MachineIdentifier") 41 | } 42 | var parametersURL: URL { 43 | return self.appending(path: "Parameters.txt") 44 | } 45 | 46 | /// Start accessing security scoped URL for the VM files directory. 47 | /// - Returns: VM files directory URL or default value 48 | static func startAccessingVMFilesDirectory() -> URL { 49 | if let bookmarkPath = UserDefaults.standard.vmFilesDirectory?.removingPercentEncoding, 50 | let bookmarkData = UserDefaults.standard.vmFilesDirectoryBookmarkData 51 | { 52 | if Bookmark.startAccess(bookmarkData: bookmarkData, for: bookmarkPath) == nil { 53 | // previous vm file directory no longer exists, reset to default 54 | UserDefaults.standard.vmFilesDirectory = URL.basePath 55 | return URL.baseURL 56 | } 57 | return URL.restoreImageURL 58 | } 59 | return URL.baseURL // default 60 | } 61 | 62 | /// Start accessing security scoped URL for the restore images directory. 63 | /// - Returns: Restore image directory URL or default value 64 | static func startAccessingRestoreImagesDirectory() -> URL { 65 | if let bookmarkPath = UserDefaults.standard.restoreImagesDirectory?.removingPercentEncoding, 66 | let bookmarkData = UserDefaults.standard.restoreImagesDirectoryBookmarkData 67 | { 68 | if Bookmark.startAccess(bookmarkData: bookmarkData, for: bookmarkPath) == nil { 69 | // previous restore image directory no longer exists, reset 70 | UserDefaults.standard.restoreImagesDirectory = URL.basePath 71 | return URL.baseURL 72 | } 73 | return URL.restoreImageURL 74 | } 75 | return URL.baseURL // default 76 | } 77 | 78 | fileprivate static func fileURL(for path: String?) -> URL { 79 | if let path { 80 | return URL(fileURLWithPath: path) 81 | } 82 | return baseURL // default value 83 | } 84 | 85 | static func createFilename(baseURL: URL, name: String, suffix: String) -> URL { 86 | // try to find a filename that does not exist 87 | let restoreImagesDirectoryURL = URL.startAccessingRestoreImagesDirectory() 88 | var url = restoreImagesDirectoryURL.appendingPathComponent("\(name).\(suffix)") 89 | var i = 1 90 | var exists = true 91 | 92 | while exists { 93 | if FileManager.default.fileExists(atPath: url.path) { 94 | url = restoreImagesDirectoryURL.appendingPathComponent("\(name)_\(i).\(suffix)") 95 | i += 1 96 | } else { 97 | exists = false 98 | } 99 | } 100 | return url 101 | } 102 | 103 | } 104 | 105 | -------------------------------------------------------------------------------- /virtualOS/VM/VMParameters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VMParameters.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | #if arch(arm64) 10 | 11 | import Virtualization 12 | import OSLog 13 | 14 | struct VMParameters: Codable { 15 | var installFinished: Bool? = false 16 | var cpuCount = 1 17 | var cpuCountMin = 1 18 | var cpuCountMax = 2 19 | var diskSizeInGB: UInt64 = UInt64(UserDefaults.standard.diskSize) 20 | var memorySizeInGB: UInt64 = 1 21 | var memorySizeInGBMin: UInt64 = 1 22 | var memorySizeInGBMax: UInt64 = 2 23 | var useMainScreenSize = true 24 | var screenWidth = 1500 25 | var screenHeight = 900 26 | var pixelsPerInch = 250 27 | var microphoneEnabled = true 28 | var sharedFolderURL: URL? 29 | var sharedFolderData: Data? 30 | var networkType: NetworkType? 31 | var networkBridge: String? 32 | var macAddress = VZMACAddress.randomLocallyAdministered().string 33 | var version = "" 34 | 35 | init() {} 36 | 37 | init(from decoder: Decoder) throws { 38 | let container = try decoder.container(keyedBy: CodingKeys.self) 39 | installFinished = try container.decodeIfPresent(Bool.self, forKey: .installFinished) ?? true // optional 40 | cpuCount = try container.decode(Int.self, forKey: .cpuCount) 41 | cpuCountMin = try container.decode(Int.self, forKey: .cpuCountMin) 42 | cpuCountMax = try container.decode(Int.self, forKey: .cpuCountMax) 43 | diskSizeInGB = try container.decode(UInt64.self, forKey: .diskSizeInGB) 44 | memorySizeInGB = try container.decode(UInt64.self, forKey: .memorySizeInGB) 45 | memorySizeInGBMin = try container.decode(UInt64.self, forKey: .memorySizeInGBMin) 46 | memorySizeInGBMax = try container.decode(UInt64.self, forKey: .memorySizeInGBMax) 47 | useMainScreenSize = try container.decodeIfPresent(Bool.self, forKey: .useMainScreenSize) ?? true // optional 48 | screenWidth = try container.decode(Int.self, forKey: .screenWidth) 49 | screenHeight = try container.decode(Int.self, forKey: .screenHeight) 50 | pixelsPerInch = try container.decode(Int.self, forKey: .pixelsPerInch) 51 | microphoneEnabled = try container.decode(Bool.self, forKey: .microphoneEnabled) 52 | sharedFolderURL = try container.decodeIfPresent(URL.self, forKey: .sharedFolderURL) ?? nil // optional 53 | sharedFolderData = try container.decodeIfPresent(Data.self, forKey: .sharedFolderData) ?? nil // optional 54 | networkType = try container.decodeIfPresent(NetworkType.self, forKey: .networkType) ?? NetworkType.NAT 55 | networkBridge = try container.decodeIfPresent(String.self, forKey: .networkBridge) ?? "" // optional 56 | macAddress = try container.decodeIfPresent(String.self, forKey: .macAddress) ?? VZMACAddress.randomLocallyAdministered().string // optional 57 | version = try container.decodeIfPresent(String.self, forKey: .version) ?? "" // optional 58 | 59 | } 60 | 61 | static func readFrom(url: URL) -> VMParameters? { 62 | let decoder = JSONDecoder() 63 | do { 64 | let json = try Data.init(contentsOf: url.appendingPathComponent("Parameters.txt", conformingTo: .text)) 65 | return try decoder.decode(VMParameters.self, from: json) 66 | } catch (let error) { 67 | Logger.shared.log(level: .default, "failed to read parameters from \(url): \(error)") 68 | } 69 | return nil 70 | } 71 | 72 | func writeToDisk(bundleURL: URL) { 73 | let encoder = JSONEncoder() 74 | encoder.outputFormatting = .prettyPrinted 75 | 76 | do { 77 | let jsonData = try encoder.encode(self) 78 | if let json = String(data: jsonData, encoding: .utf8) { 79 | try json.write(to: bundleURL.parametersURL, atomically: true, encoding: String.Encoding.utf8) 80 | } 81 | } catch { 82 | Logger.shared.log(level: .default, "failed to write current CPU and RAM configuration to disk") 83 | } 84 | } 85 | } 86 | 87 | #endif 88 | 89 | -------------------------------------------------------------------------------- /virtualOS/ViewController/VMViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VMViewController.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Virtualization 10 | import StoreKit 11 | import OSLog 12 | 13 | #if arch(arm64) 14 | 15 | final class VMViewController: NSViewController { 16 | @IBOutlet var containerView: NSView! 17 | @IBOutlet var vmView: VZVirtualMachineView! 18 | @IBOutlet var statusLabel: NSTextField! 19 | 20 | var vmBundle: VMBundle? 21 | var vmParameters: VMParameters? 22 | fileprivate var vmConfiguration: VMConfiguration? 23 | fileprivate var vm: VZVirtualMachine? 24 | fileprivate let queue = DispatchQueue.global(qos: .userInteractive) 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | statusLabel.stringValue = "" 29 | 30 | createVM() 31 | 32 | queue.async { [weak self] in 33 | self?.vm?.start { (result: Result) in 34 | switch result { 35 | case .success: 36 | Logger.shared.log(level: .default, "vm started") 37 | case .failure(let error): 38 | self?.show(errorString: "Starting VM failed: \(error.localizedDescription)") 39 | } 40 | } 41 | } 42 | } 43 | 44 | override func viewWillAppear() { 45 | super.viewWillAppear() 46 | view.window?.delegate = self 47 | } 48 | 49 | override func viewDidAppear() { 50 | super.viewDidAppear() 51 | 52 | UserDefaults.standard.userRatingCounter += 1 53 | if UserDefaults.standard.userRatingCounter.isMultiple(of: 5) { 54 | AppStore.requestReview(in: self) 55 | } 56 | } 57 | 58 | // MARK: - Private 59 | 60 | fileprivate func createVM() { 61 | guard let bundleURL = vmBundle?.url, 62 | let vmParameters = vmParameters else 63 | { 64 | show(errorString: "Bundle URL or VM parameters invalid") 65 | return 66 | } 67 | 68 | let macPlatformConfigurationResult = MacPlatformConfiguration.read(fromBundleURL: bundleURL) 69 | if case .failure(let restoreError) = macPlatformConfigurationResult { 70 | show(errorString: restoreError.localizedDescription) 71 | return 72 | } else if case .success(let macPlatformConfiguration) = macPlatformConfigurationResult, 73 | let macPlatformConfiguration = macPlatformConfiguration 74 | { 75 | let vmConfiguration = VMConfiguration() 76 | vmConfiguration.setup(parameters: vmParameters, macPlatformConfiguration: macPlatformConfiguration, bundleURL: bundleURL) 77 | self.vmConfiguration = vmConfiguration 78 | 79 | do { 80 | try vmConfiguration.validate() 81 | Logger.shared.log(level: .default, "vm configuration is valid, using \(vmParameters.cpuCount) cpus and \(vmParameters.memorySizeInGB) gb ram") 82 | } catch let error { 83 | show(errorString: "Failed to validate VM configuration: \(error.localizedDescription)") 84 | return 85 | } 86 | 87 | let vm = VZVirtualMachine(configuration: vmConfiguration, queue: queue) 88 | vm.delegate = self 89 | 90 | vmView.virtualMachine = vm 91 | vmView.automaticallyReconfiguresDisplay = true 92 | vmView.capturesSystemKeys = true 93 | self.vm = vm 94 | } else { 95 | show(errorString: "Could not create platform configuration.") 96 | return 97 | } 98 | } 99 | 100 | fileprivate func show(errorString: String) { 101 | Logger.shared.log(level: .default, "\(errorString)") 102 | DispatchQueue.main.async { [weak self] in 103 | self?.statusLabel.stringValue = errorString 104 | } 105 | } 106 | } 107 | 108 | extension VMViewController: VZVirtualMachineDelegate { 109 | func guestDidStop(_ virtualMachine: VZVirtualMachine) { 110 | show(errorString: "Guest did stop") 111 | } 112 | 113 | func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: any Error) { 114 | show(errorString: "Guest did stop with error: \(error.localizedDescription)") 115 | } 116 | } 117 | 118 | extension VMViewController: NSWindowDelegate { 119 | func windowShouldClose(_ sender: NSWindow) -> Bool { 120 | let alert: NSAlert = NSAlert.okCancelAlert(messageText: "Stop VM", informativeText: "Are you sure you want to stop the VM?", alertStyle: .warning) 121 | let selection = alert.runModal() 122 | if selection == NSApplication.ModalResponse.alertFirstButtonReturn || 123 | selection == NSApplication.ModalResponse.OK 124 | { 125 | return true 126 | } else { 127 | return false 128 | } 129 | } 130 | } 131 | 132 | #endif 133 | -------------------------------------------------------------------------------- /virtualOS/VM/MacPlatformConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MacPlatformConfiguration.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Virtualization 10 | import OSLog 11 | 12 | #if arch(arm64) 13 | 14 | final class MacPlatformConfiguration: VZMacPlatformConfiguration { 15 | var versionString: String? 16 | 17 | static func read(fromBundleURL bundleURL: URL) -> MacPlatformConfigurationResult { 18 | let macPlatformConfiguration = MacPlatformConfiguration() 19 | 20 | let auxiliaryStorage = VZMacAuxiliaryStorage(contentsOf: bundleURL.auxiliaryStorageURL) 21 | macPlatformConfiguration.auxiliaryStorage = auxiliaryStorage 22 | 23 | guard let hardwareModelData = try? Data(contentsOf: bundleURL.hardwareModelURL) else { 24 | return MacPlatformConfigurationResult(errorMessage: "Error: Failed to retrieve hardware model data") 25 | } 26 | 27 | guard let hardwareModel = VZMacHardwareModel(dataRepresentation: hardwareModelData) else { 28 | return MacPlatformConfigurationResult(errorMessage: "Error: Failed to create hardware model") 29 | } 30 | 31 | if !hardwareModel.isSupported { 32 | return MacPlatformConfigurationResult(errorMessage: "Error: The hardware model is not supported on the current host") 33 | } 34 | macPlatformConfiguration.hardwareModel = hardwareModel 35 | 36 | guard let machineIdentifierData = try? Data(contentsOf: bundleURL.machineIdentifierURL) else { 37 | return MacPlatformConfigurationResult(errorMessage: "Error: Failed to retrieve machine identifier data.") 38 | } 39 | 40 | guard let machineIdentifier = VZMacMachineIdentifier(dataRepresentation: machineIdentifierData) else { 41 | return MacPlatformConfigurationResult(errorMessage: "Error: Failed to create machine identifier.") 42 | } 43 | macPlatformConfiguration.machineIdentifier = machineIdentifier 44 | 45 | return MacPlatformConfigurationResult(macPlatformConfiguration: macPlatformConfiguration) 46 | } 47 | 48 | static func createDefault(fromRestoreImage restoreImage: VZMacOSRestoreImage, versionString: inout String, bundleURL: URL) -> MacPlatformConfigurationResult { 49 | versionString = restoreImage.operatingSystemVersionString 50 | let versionString = versionString 51 | Logger.shared.log(level: .default, "restore image version: \(versionString)") 52 | 53 | guard let mostFeaturefulSupportedConfiguration = restoreImage.mostFeaturefulSupportedConfiguration else { 54 | return MacPlatformConfigurationResult(errorMessage: "Restore image for macOS version \(versionString) is not supported on this machine.") 55 | } 56 | guard mostFeaturefulSupportedConfiguration.hardwareModel.isSupported else { 57 | return MacPlatformConfigurationResult(errorMessage: "Hardware model required by restore image for macOS version \(versionString) is not supported on this machine.") 58 | } 59 | 60 | let auxiliaryStorage = VZMacAuxiliaryStorage(contentsOf: URL.baseURL.auxiliaryStorageURL) 61 | let macPlatformConfiguration = MacPlatformConfiguration() 62 | macPlatformConfiguration.auxiliaryStorage = auxiliaryStorage 63 | 64 | let macPlatformConfigurationResult = macPlatformConfiguration.createPlatformConfiguration(macHardwareModel: mostFeaturefulSupportedConfiguration.hardwareModel, bundleURL: bundleURL) 65 | if case .failure(_) = macPlatformConfigurationResult { 66 | return macPlatformConfigurationResult 67 | } 68 | 69 | var vmParameters = VMParameters() 70 | vmParameters.cpuCountMin = mostFeaturefulSupportedConfiguration.minimumSupportedCPUCount 71 | vmParameters.memorySizeInGBMin = mostFeaturefulSupportedConfiguration.minimumSupportedMemorySize.bytesToGigabytes() 72 | 73 | return macPlatformConfigurationResult 74 | } 75 | 76 | fileprivate func createPlatformConfiguration(macHardwareModel: VZMacHardwareModel, bundleURL: URL) -> MacPlatformConfigurationResult { 77 | let platformConfiguration = VZMacPlatformConfiguration() 78 | platformConfiguration.hardwareModel = macHardwareModel 79 | 80 | do { 81 | platformConfiguration.auxiliaryStorage = try VZMacAuxiliaryStorage(creatingStorageAt: bundleURL.auxiliaryStorageURL, hardwareModel: macHardwareModel, options: [.allowOverwrite] 82 | ) 83 | } catch let error { 84 | return MacPlatformConfigurationResult(errorMessage: "Could not create auxiliary storage device: \(error).") 85 | } 86 | 87 | do { 88 | try platformConfiguration.hardwareModel.dataRepresentation.write(to: bundleURL.hardwareModelURL) 89 | try platformConfiguration.machineIdentifier.dataRepresentation.write(to: bundleURL.machineIdentifierURL) 90 | } catch { 91 | return MacPlatformConfigurationResult(errorMessage: "Could store platform information to disk.") 92 | } 93 | 94 | return MacPlatformConfigurationResult(macPlatformConfiguration: platformConfiguration) // success 95 | } 96 | } 97 | 98 | #endif 99 | -------------------------------------------------------------------------------- /virtualOS/RestoreImage/RestoreImageDownload.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Download.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Virtualization 10 | import Combine 11 | import OSLog 12 | 13 | #if arch(arm64) 14 | 15 | protocol ProgressDelegate: AnyObject { 16 | func progress(_ progress: Double, progressString: String) 17 | func done(error: Error?) 18 | } 19 | 20 | final class RestoreImageDownload { 21 | weak var delegate: ProgressDelegate? 22 | fileprivate var observation: NSKeyValueObservation? 23 | fileprivate var downloadTask: URLSessionDownloadTask? 24 | fileprivate var downloading = true 25 | 26 | deinit { 27 | observation?.invalidate() 28 | } 29 | 30 | func fetch() { 31 | VZMacOSRestoreImage.fetchLatestSupported { [self](result: Result) in 32 | switch result { 33 | case let .success(restoreImage): 34 | download(restoreImage: restoreImage) 35 | case let .failure(error): 36 | delegate?.done(error: error) 37 | } 38 | } 39 | } 40 | 41 | func cancel() { 42 | downloadTask?.cancel() 43 | } 44 | 45 | // MARK: - Private 46 | 47 | fileprivate func progressDone(error: Error?) { 48 | FileModel.cleanUpTemporaryFiles() 49 | delegate?.progress(100, progressString: "Restore image download finished.") 50 | delegate?.done(error: error) 51 | } 52 | 53 | fileprivate func download(restoreImage: VZMacOSRestoreImage) { 54 | Logger.shared.log(level: .default, "downloading restore image for \(restoreImage.operatingSystemVersionString)") 55 | 56 | let restoreImagesDirectoryURL = URL.startAccessingRestoreImagesDirectory() 57 | let restoreImagesURL = URL.createFilename(baseURL: restoreImagesDirectoryURL, name: "RestoreImage", suffix: "ipsw") 58 | 59 | let downloadTask = URLSession.shared.downloadTask(with: restoreImage.url) { tempURL, response, error in 60 | self.downloading = false 61 | self.downloadFinished(tempURL: tempURL, restoreImageURL: restoreImagesURL, error: error) 62 | } 63 | observation = downloadTask.progress.observe(\.fractionCompleted) { _, _ in } 64 | downloadTask.resume() 65 | self.downloadTask = downloadTask 66 | 67 | func updateDownloadProgress() { 68 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in 69 | guard self?.downloading == true else { 70 | return 71 | } 72 | 73 | let progressString: String 74 | 75 | if let byteCompletedCount = downloadTask.progress.userInfo[ProgressUserInfoKey("NSProgressByteCompletedCountKey")] as? Int, 76 | let byteTotalCount = downloadTask.progress.userInfo[ProgressUserInfoKey("NSProgressByteTotalCountKey")] as? Int 77 | { 78 | let mbCompleted = byteCompletedCount / (1024 * 1024) 79 | let mbTotal = byteTotalCount / (1024 * 1024) 80 | progressString = "\(Int(downloadTask.progress.fractionCompleted * 100))% (\(mbCompleted) of \(mbTotal) MB)" 81 | } else { 82 | progressString = "\(Int(downloadTask.progress.fractionCompleted * 100))%" 83 | } 84 | Logger.shared.log(level: .debug, "download progress: \(progressString)") 85 | 86 | self?.delegate?.progress(downloadTask.progress.fractionCompleted, progressString: "\(restoreImage.operatingSystemVersionString)\nDownloading \(progressString)") 87 | 88 | // continue updating 89 | updateDownloadProgress() 90 | } 91 | } 92 | 93 | updateDownloadProgress() 94 | } 95 | 96 | fileprivate func downloadFinished(tempURL: URL?, restoreImageURL: URL?, error: Error?) { 97 | if let error { 98 | Logger.shared.error("\(error.localizedDescription)") 99 | progressDone(error: error) 100 | return 101 | } 102 | Logger.shared.log(level: .default, "download finished") 103 | 104 | if let tempURL, let restoreImageURL { 105 | Logger.shared.log(level: .debug, "moving restore image: \(tempURL) to \(restoreImageURL)") 106 | delegate?.progress(99, progressString: "Preparing file. Please wait...") 107 | 108 | do { 109 | try FileManager.default.moveItem(at: tempURL, to: restoreImageURL) 110 | Logger.shared.log(level: .default, "moved restore image to \(restoreImageURL)") 111 | progressDone(error: nil) 112 | } catch { 113 | progressDone(error: RestoreError(localizedDescription: "Failed to prepare downloaded restore image: \(error.localizedDescription)")) 114 | } 115 | } else { 116 | progressDone(error: RestoreError(localizedDescription: "Failed to prepare downloaded restore image: invalid destination")) 117 | } 118 | } 119 | } 120 | 121 | #endif 122 | -------------------------------------------------------------------------------- /virtualOS/ViewController/RestoreImageViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RestoreImageViewController.swift 3 | // virtualOS 4 | // 5 | 6 | import AppKit 7 | import Virtualization 8 | import OSLog 9 | 10 | #if arch(arm64) 11 | 12 | final class RestoreImageViewController: NSViewController { 13 | let fileModel = FileModel() 14 | fileprivate var selectedRestoreImage = "" 15 | 16 | @IBOutlet weak var tableView: NSTableView! 17 | @IBOutlet weak var installButton: NSButton! 18 | @IBOutlet weak var showInFinderButton: NSButton! 19 | @IBOutlet weak var infoTextField: NSTextField! 20 | 21 | var restoreImages: [String] { 22 | return fileModel.getRestoreImages() 23 | } 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | _ = URL.startAccessingRestoreImagesDirectory() 29 | tableView.dataSource = self 30 | tableView.delegate = self 31 | 32 | NotificationCenter.default.addObserver(self, selector: #selector(reloadTable), name: Constants.didChangeAppSettingsNotification, object: nil) 33 | } 34 | 35 | override func viewWillAppear() { 36 | super.viewWillAppear() 37 | 38 | updateUI() 39 | 40 | if tableView.numberOfRows > 0 { 41 | // select the first item 42 | tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) 43 | setButtons(enabled: true) 44 | } else { 45 | setButtons(enabled: false) 46 | } 47 | } 48 | 49 | @IBAction func installButtonPressed(_ sender: NSButton) { 50 | if tableView.selectedRow != -1 { 51 | let notification = Notification(name: Constants.restoreImageNameSelectedNotification, userInfo: [Constants.selectedRestoreImage: self.selectedRestoreImage]) 52 | NotificationCenter.default.post(notification) 53 | view.window?.close() 54 | } 55 | } 56 | 57 | @IBAction func showInFinderButtonPressed(_ sender: NSButton) { 58 | if tableView.selectedRow != -1 { 59 | let url = URL.baseURL.appendingPathComponent(self.selectedRestoreImage) 60 | NSWorkspace.shared.activateFileViewerSelecting([url]) 61 | } 62 | } 63 | 64 | @IBAction func downloadLatestButtonPressed(_ sender: NSButton) { 65 | let notification = Notification(name: Constants.restoreImageNameSelectedNotification, userInfo: [Constants.selectedRestoreImage: Constants.restoreImageNameLatest]) 66 | NotificationCenter.default.post(notification) 67 | view.window?.close() 68 | } 69 | 70 | @objc private func reloadTable() { 71 | tableView.reloadData() 72 | } 73 | 74 | fileprivate func updateUI() { 75 | let restoreImageCount = restoreImages.count 76 | if restoreImageCount == 0 { 77 | infoTextField.stringValue = "No restore image available, download latest image." 78 | } else if tableView.selectedRow < restoreImageCount && tableView.selectedRow != -1 { 79 | infoTextField.stringValue = "Loading image..." 80 | 81 | let name = restoreImages[tableView.selectedRow] 82 | let url = URL.restoreImagesDirectoryURL.appendingPathComponent(name) 83 | VZMacOSRestoreImage.load(from: url) { result in 84 | DispatchQueue.main.async { [weak self] in 85 | var info = "" 86 | switch result { 87 | case .success(let restoreImage): 88 | info = restoreImage.operatingSystemVersionString 89 | self?.setButtons(enabled: true) 90 | case .failure(let error): 91 | info = "Invalid image" 92 | self?.setButtons(enabled: false) 93 | Logger.shared.log(level: .default, "\(error)") 94 | } 95 | self?.infoTextField.stringValue = info 96 | } 97 | } 98 | } else { 99 | infoTextField.stringValue = "" 100 | setButtons(enabled: false) 101 | } 102 | } 103 | 104 | fileprivate func setButtons(enabled: Bool) { 105 | installButton.isEnabled = enabled 106 | showInFinderButton.isEnabled = enabled 107 | } 108 | } 109 | 110 | extension RestoreImageViewController: NSTableViewDataSource { 111 | func numberOfRows(in tableView: NSTableView) -> Int { 112 | return restoreImages.count 113 | } 114 | 115 | func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { 116 | if row < restoreImages.count { 117 | return restoreImages[row] 118 | } else { 119 | return "Unknown" 120 | } 121 | } 122 | } 123 | 124 | extension RestoreImageViewController: NSTableViewDelegate { 125 | func tableViewSelectionDidChange(_ notification: Notification) { 126 | let selectedRow = tableView.selectedRow 127 | if selectedRow != -1 && selectedRow < restoreImages.count { 128 | selectedRestoreImage = restoreImages[selectedRow] 129 | } 130 | 131 | updateUI() 132 | } 133 | } 134 | 135 | #endif 136 | -------------------------------------------------------------------------------- /virtualOS/VM/VMConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VM.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | #if arch(arm64) 10 | 11 | import Virtualization 12 | import AVFoundation // for microphone support 13 | import OSLog 14 | 15 | final class VMConfiguration: VZVirtualMachineConfiguration { 16 | func setup(parameters: VMParameters, macPlatformConfiguration: VZMacPlatformConfiguration, bundleURL: URL) { 17 | cpuCount = parameters.cpuCount 18 | memorySize = parameters.memorySizeInGB.gigabytesToBytes() 19 | pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()] 20 | entropyDevices = [VZVirtioEntropyDeviceConfiguration()] 21 | keyboards = [VZUSBKeyboardConfiguration()] 22 | bootLoader = VZMacOSBootLoader() 23 | 24 | configureAudioDevice(parameters: parameters) 25 | configureGraphicsDevice(parameters: parameters) 26 | configureStorageDevice(parameters: parameters, bundleURL: bundleURL) 27 | configureNetworkDevices(parameters: parameters) 28 | configureSharedFolder(parameters: parameters) 29 | configureClipboardSharing() 30 | configureUSB() 31 | 32 | platform = macPlatformConfiguration 33 | } 34 | 35 | func setDefault(parameters: inout VMParameters) { 36 | let cpuCountMax = computeCPUCount() 37 | let bytesMax = VZVirtualMachineConfiguration.maximumAllowedMemorySize 38 | cpuCount = cpuCountMax - 1 // substract one core 39 | memorySize = bytesMax - UInt64(3).gigabytesToBytes() // substract 3 GB 40 | 41 | parameters.cpuCount = cpuCount 42 | parameters.cpuCountMax = cpuCountMax 43 | parameters.memorySizeInGB = memorySize.bytesToGigabytes() 44 | parameters.memorySizeInGBMax = bytesMax.bytesToGigabytes() 45 | } 46 | 47 | // MARK: - Private 48 | 49 | fileprivate func configureAudioDevice(parameters: VMParameters) { 50 | let audioDevice = VZVirtioSoundDeviceConfiguration() 51 | 52 | if parameters.microphoneEnabled { 53 | AVCaptureDevice.requestAccess(for: .audio) { (granted: Bool) in 54 | // Logger.shared.log(level: .default, "microphone access granted: \(granted)") 55 | } 56 | 57 | let inputStreamConfiguration = VZVirtioSoundDeviceInputStreamConfiguration() 58 | inputStreamConfiguration.source = VZHostAudioInputStreamSource() 59 | audioDevice.streams.append(inputStreamConfiguration) 60 | } 61 | 62 | let outputStreamConfiguration = VZVirtioSoundDeviceOutputStreamConfiguration() 63 | outputStreamConfiguration.sink = VZHostAudioOutputStreamSink() 64 | audioDevice.streams.append(outputStreamConfiguration) 65 | 66 | audioDevices = [audioDevice] 67 | } 68 | 69 | fileprivate func configureGraphicsDevice(parameters: VMParameters) { 70 | let graphicsDevice = VZMacGraphicsDeviceConfiguration() 71 | if parameters.useMainScreenSize, let mainScreen = NSScreen.main { 72 | graphicsDevice.displays = [VZMacGraphicsDisplayConfiguration(for: mainScreen, sizeInPoints: NSSize(width: parameters.screenWidth, height: parameters.screenHeight))] 73 | } else { 74 | graphicsDevice.displays = [VZMacGraphicsDisplayConfiguration( 75 | widthInPixels: parameters.screenWidth, 76 | heightInPixels: parameters.screenHeight, 77 | pixelsPerInch: parameters.pixelsPerInch 78 | )] 79 | } 80 | graphicsDevices = [graphicsDevice] 81 | } 82 | 83 | fileprivate func configureStorageDevice(parameters: VMParameters, bundleURL: URL) { 84 | let diskImageStorageDeviceAttachment: VZDiskImageStorageDeviceAttachment? 85 | do { 86 | diskImageStorageDeviceAttachment = try VZDiskImageStorageDeviceAttachment(url: bundleURL.diskImageURL, readOnly: false) 87 | } catch let error { 88 | Logger.shared.log(level: .default, "could not create storage device: \(error.localizedDescription)") 89 | return 90 | } 91 | 92 | if let diskImageStorageDeviceAttachment { 93 | let blockDeviceConfiguration = VZVirtioBlockDeviceConfiguration(attachment: diskImageStorageDeviceAttachment) 94 | storageDevices = [blockDeviceConfiguration] 95 | } 96 | 97 | if let diskImageStorageDeviceAttachment = try? VZDiskImageStorageDeviceAttachment(url: bundleURL.diskImageURL, readOnly: false) { 98 | let blockDeviceConfiguration = VZVirtioBlockDeviceConfiguration(attachment: diskImageStorageDeviceAttachment) 99 | storageDevices = [blockDeviceConfiguration] 100 | } else { 101 | Logger.shared.log(level: .default, "could not create storage device") 102 | } 103 | } 104 | 105 | fileprivate func configureNetworkDevices(parameters: VMParameters) { 106 | let networkDeviceConfiguration = VZVirtioNetworkDeviceConfiguration() 107 | if parameters.networkType == .Bridge { 108 | for interface in VZBridgedNetworkInterface.networkInterfaces { 109 | if interface.description == parameters.networkBridge { 110 | networkDeviceConfiguration.attachment = VZBridgedNetworkDeviceAttachment(interface: interface) 111 | } 112 | } 113 | 114 | if networkDeviceConfiguration.attachment == nil, 115 | let defaultInterface = VZBridgedNetworkInterface.networkInterfaces.first 116 | { 117 | networkDeviceConfiguration.attachment = VZBridgedNetworkDeviceAttachment(interface: defaultInterface) 118 | } 119 | } else { 120 | networkDeviceConfiguration.attachment = VZNATNetworkDeviceAttachment() 121 | } 122 | 123 | networkDeviceConfiguration.macAddress = VZMACAddress(string: parameters.macAddress) ?? .randomLocallyAdministered() 124 | networkDevices = [networkDeviceConfiguration] 125 | } 126 | 127 | fileprivate func configureSharedFolder(parameters: VMParameters) { 128 | guard let sharedFolderURL = parameters.sharedFolderURL, 129 | let sharedFolderBookmarkData = Bookmark.startAccess(bookmarkData: parameters.sharedFolderData, for: sharedFolderURL.path) else 130 | { 131 | return 132 | } 133 | 134 | let sharedDirectory = VZSharedDirectory(url: sharedFolderBookmarkData, readOnly: false) 135 | let singleDirectoryShare = VZSingleDirectoryShare(directory: sharedDirectory) 136 | let sharingConfiguration = VZVirtioFileSystemDeviceConfiguration(tag: VZVirtioFileSystemDeviceConfiguration.macOSGuestAutomountTag) 137 | sharingConfiguration.share = singleDirectoryShare 138 | 139 | directorySharingDevices = [sharingConfiguration] 140 | } 141 | 142 | fileprivate func configureClipboardSharing() { 143 | let consoleDevice = VZVirtioConsoleDeviceConfiguration() 144 | 145 | let spiceAgentPortConfiguration = VZVirtioConsolePortConfiguration() 146 | spiceAgentPortConfiguration.name = VZSpiceAgentPortAttachment.spiceAgentPortName 147 | spiceAgentPortConfiguration.attachment = VZSpiceAgentPortAttachment() 148 | consoleDevice.ports[0] = spiceAgentPortConfiguration 149 | 150 | consoleDevices.append(consoleDevice) 151 | } 152 | 153 | fileprivate func configureUSB() { 154 | let usbControllerConfiguration = VZXHCIControllerConfiguration() 155 | usbControllers = [usbControllerConfiguration] 156 | } 157 | 158 | fileprivate func computeCPUCount() -> Int { 159 | let totalAvailableCPUs = ProcessInfo.processInfo.processorCount 160 | 161 | var virtualCPUCount = totalAvailableCPUs <= 1 ? 1 : totalAvailableCPUs 162 | virtualCPUCount = max(virtualCPUCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount) 163 | virtualCPUCount = min(virtualCPUCount, VZVirtualMachineConfiguration.maximumAllowedCPUCount) 164 | 165 | return virtualCPUCount 166 | } 167 | } 168 | 169 | #endif 170 | -------------------------------------------------------------------------------- /virtualOS/RestoreImage/RestoreImageInstall.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RestoreImageInstall.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Foundation 10 | import Virtualization 11 | import OSLog 12 | 13 | #if arch(arm64) 14 | 15 | final class RestoreImageInstall { 16 | fileprivate struct InstallState { 17 | fileprivate var installing = true 18 | fileprivate var vmParameters: VMParameters? 19 | fileprivate var bundleURL: URL? 20 | } 21 | 22 | weak var delegate: ProgressDelegate? 23 | var restoreImageName: String? 24 | var diskImageSize: Int? 25 | 26 | fileprivate var observation: NSKeyValueObservation? 27 | fileprivate var installer: VZMacOSInstaller? 28 | fileprivate let userInteractivQueue = DispatchQueue.global(qos: .userInteractive) 29 | fileprivate var installState = InstallState() 30 | 31 | deinit { 32 | observation?.invalidate() 33 | } 34 | 35 | func install() { 36 | guard let restoreImageName else { 37 | self.delegate?.done(error: RestoreError(localizedDescription: "Restore image name unavailable.")) 38 | return // error 39 | } 40 | let restoreImagesDirectoryURL = URL.startAccessingRestoreImagesDirectory() 41 | let restoreImageURL = restoreImagesDirectoryURL.appending(path: restoreImageName) 42 | guard FileManager.default.fileExists(atPath: URL.restoreImageURL.path) else { 43 | delegate?.done(error: RestoreError(localizedDescription: "Restore image does not exist at \(restoreImageURL.path)")) 44 | return 45 | } 46 | 47 | let vmFilesDirectoryURL = URL.startAccessingVMFilesDirectory() 48 | let bundleURL = URL.createFilename(baseURL: vmFilesDirectoryURL, name: "virtualOS", suffix: "bundle") 49 | if let error = createBundle(at: bundleURL) { 50 | self.delegate?.done(error: error) 51 | return 52 | } 53 | installState.bundleURL = bundleURL 54 | Logger.shared.log(level: .default, "using bundle url \(bundleURL.path)") 55 | 56 | VZMacOSRestoreImage.load(from: restoreImageURL) { (result: Result) in 57 | switch result { 58 | case .success(let restoreImage): 59 | self.restoreImageLoaded(restoreImage: restoreImage, bundleURL: bundleURL) 60 | case .failure(let error): 61 | self.delegate?.done(error: error) 62 | } 63 | } 64 | } 65 | 66 | func cancel() { 67 | stopVM() 68 | } 69 | 70 | // MARK: - Private 71 | 72 | fileprivate func restoreImageLoaded(restoreImage: VZMacOSRestoreImage, bundleURL: URL) { 73 | var versionString = "" 74 | let macPlatformConfigurationResult = MacPlatformConfiguration.createDefault(fromRestoreImage: restoreImage, versionString: &versionString, bundleURL: bundleURL) 75 | if case .failure(let error) = macPlatformConfigurationResult { 76 | delegate?.done(error: error) 77 | return 78 | } 79 | 80 | var vmParameters = VMParameters() 81 | let vmConfiguration = VMConfiguration() 82 | if case .success(let macPlatformConfiguration) = macPlatformConfigurationResult, 83 | let macPlatformConfiguration 84 | { 85 | vmConfiguration.platform = macPlatformConfiguration 86 | 87 | if let diskImageSize = diskImageSize { 88 | vmParameters.diskSizeInGB = UInt64(diskImageSize) 89 | let restoreResult = createDiskImage(diskImageURL: bundleURL.diskImageURL, sizeInGB: UInt64(vmParameters.diskSizeInGB)) 90 | if case .failure(let restoreError) = restoreResult { 91 | delegate?.done(error: restoreError) 92 | return 93 | } 94 | } 95 | 96 | vmConfiguration.setDefault(parameters: &vmParameters) 97 | vmConfiguration.setup(parameters: vmParameters, macPlatformConfiguration: macPlatformConfiguration, bundleURL: bundleURL) 98 | } else { 99 | delegate?.done(error: RestoreError(localizedDescription: "Could not create mac platform configuration.")) 100 | return 101 | } 102 | 103 | vmParameters.version = restoreImage.operatingSystemVersionString 104 | vmParameters.writeToDisk(bundleURL: bundleURL) 105 | installState.vmParameters = vmParameters // keep reference to update installFinished parameter later 106 | 107 | do { 108 | try vmConfiguration.validate() 109 | Logger.shared.log(level: .default, "vm configuration is valid, using \(vmParameters.cpuCount) cpus and \(vmParameters.memorySizeInGB) gb ram") 110 | } catch let error { 111 | Logger.shared.log(level: .default, "failed to validate vm configuration: \(error.localizedDescription)") 112 | return 113 | } 114 | 115 | let vm = VZVirtualMachine(configuration: vmConfiguration, queue: userInteractivQueue) 116 | 117 | userInteractivQueue.async { [weak self] in 118 | guard let self else { 119 | Logger.shared.log(level: .default, "Error: Could not install VM, weak self is nil") 120 | return 121 | } 122 | startMacOSInstaller(vm: vm, restoreImageURL: restoreImage.url, versionString: versionString) 123 | } 124 | } 125 | 126 | fileprivate func startMacOSInstaller(vm: VZVirtualMachine, restoreImageURL: URL, versionString: String) { 127 | let installer = VZMacOSInstaller(virtualMachine: vm, restoringFromImageAt: restoreImageURL) 128 | self.installer = installer 129 | 130 | installer.install { result in 131 | self.installState.installing = false 132 | switch result { 133 | case .success(): 134 | self.installFinished(installer: installer) 135 | case .failure(let error): 136 | self.delegate?.done(error: error) 137 | } 138 | } 139 | 140 | self.observation = installer.progress.observe(\.fractionCompleted) { _, _ in } 141 | 142 | func updateInstallProgress() { 143 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in 144 | var progressString = "Installing \(Int(installer.progress.fractionCompleted * 100))%" 145 | if installer.progress.fractionCompleted == 0 { 146 | progressString += " (Please wait)" 147 | } 148 | progressString += "\n\(versionString)" 149 | 150 | if let installing = self?.installState.installing, installing { 151 | self?.delegate?.progress(installer.progress.fractionCompleted, progressString: progressString) 152 | updateInstallProgress() 153 | } 154 | } 155 | } 156 | 157 | updateInstallProgress() 158 | } 159 | 160 | fileprivate func installFinished(installer: VZMacOSInstaller) { 161 | Logger.shared.log(level: .default, "Install finished") 162 | installState.installing = false 163 | installState.vmParameters?.installFinished = true 164 | if let bundleURL = installState.bundleURL { 165 | installState.vmParameters?.writeToDisk(bundleURL: bundleURL) 166 | } 167 | delegate?.progress(installer.progress.fractionCompleted, progressString: "Install finished successfully.") 168 | delegate?.done(error: nil) 169 | stopVM() 170 | } 171 | 172 | fileprivate func stopVM() { 173 | if let installer = installer { 174 | userInteractivQueue.async { 175 | if installer.virtualMachine.canStop { 176 | installer.virtualMachine.stop(completionHandler: { error in 177 | if let error { 178 | Logger.shared.log(level: .default, "Error stopping VM: \(error.localizedDescription)") 179 | } else { 180 | Logger.shared.log(level: .default, "VM stopped") 181 | } 182 | }) 183 | } 184 | } 185 | } 186 | } 187 | 188 | fileprivate func createBundle(at bundleURL: URL) -> RestoreError? { 189 | if FileManager.default.fileExists(atPath: bundleURL.path) { 190 | return nil // already exists, no error 191 | } 192 | 193 | do { 194 | try FileManager.default.createDirectory(at: bundleURL, withIntermediateDirectories: true, attributes: nil) 195 | } catch let error { 196 | return RestoreError(localizedDescription: "Failed to create VM bundle directory: \(error.localizedDescription)") 197 | } 198 | 199 | // Logger.shared.log(level: .default, "bundle created at \(bundleURL.path)") 200 | return nil // no error 201 | } 202 | 203 | fileprivate func createDiskImage(diskImageURL: URL, sizeInGB: UInt64) -> RestoreResult { 204 | let diskImageFileDescriptor = open(diskImageURL.path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR) 205 | if diskImageFileDescriptor == -1 { 206 | return RestoreResult(errorMessage: "Error: can not create disk image") 207 | } 208 | 209 | let diskSize = sizeInGB.gigabytesToBytes() 210 | var result = ftruncate(diskImageFileDescriptor, Int64(diskSize)) 211 | if result != 0 { 212 | return RestoreResult(errorMessage: "Error: expanding disk image failed") 213 | } 214 | 215 | result = close(diskImageFileDescriptor) 216 | if result != 0 { 217 | return RestoreResult(errorMessage: "Error: failed to close the disk image") 218 | } 219 | 220 | return RestoreResult() // success 221 | } 222 | } 223 | 224 | #endif 225 | -------------------------------------------------------------------------------- /virtualOS.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXFileReference section */ 10 | 015228552CB27BC100209934 /* virtualOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = virtualOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 11 | 01D610692D95489600FFF92D /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 12 | 01D6106A2D95489D00FFF92D /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 13 | 01D6106D2D95580000FFF92D /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; 14 | /* End PBXFileReference section */ 15 | 16 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 17 | 014188A22CD756B200DCD9A0 /* Exceptions for "virtualOS" folder in "virtualOS" target */ = { 18 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 19 | membershipExceptions = ( 20 | Resources/Info.plist, 21 | ); 22 | target = 015228542CB27BC100209934 /* virtualOS */; 23 | }; 24 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ 25 | 26 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 27 | 015228572CB27BC100209934 /* virtualOS */ = { 28 | isa = PBXFileSystemSynchronizedRootGroup; 29 | exceptions = ( 30 | 014188A22CD756B200DCD9A0 /* Exceptions for "virtualOS" folder in "virtualOS" target */, 31 | ); 32 | path = virtualOS; 33 | sourceTree = ""; 34 | }; 35 | /* End PBXFileSystemSynchronizedRootGroup section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | 015228522CB27BC100209934 /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | 0152284C2CB27BC100209934 = { 49 | isa = PBXGroup; 50 | children = ( 51 | 01D6106D2D95580000FFF92D /* .gitignore */, 52 | 01D610692D95489600FFF92D /* README.md */, 53 | 01D6106A2D95489D00FFF92D /* LICENSE */, 54 | 015228572CB27BC100209934 /* virtualOS */, 55 | 015228562CB27BC100209934 /* Products */, 56 | ); 57 | sourceTree = ""; 58 | }; 59 | 015228562CB27BC100209934 /* Products */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | 015228552CB27BC100209934 /* virtualOS.app */, 63 | ); 64 | name = Products; 65 | sourceTree = ""; 66 | }; 67 | /* End PBXGroup section */ 68 | 69 | /* Begin PBXNativeTarget section */ 70 | 015228542CB27BC100209934 /* virtualOS */ = { 71 | isa = PBXNativeTarget; 72 | buildConfigurationList = 015228642CB27BC200209934 /* Build configuration list for PBXNativeTarget "virtualOS" */; 73 | buildPhases = ( 74 | 015228512CB27BC100209934 /* Sources */, 75 | 015228522CB27BC100209934 /* Frameworks */, 76 | 015228532CB27BC100209934 /* Resources */, 77 | ); 78 | buildRules = ( 79 | ); 80 | dependencies = ( 81 | ); 82 | fileSystemSynchronizedGroups = ( 83 | 015228572CB27BC100209934 /* virtualOS */, 84 | ); 85 | name = virtualOS; 86 | packageProductDependencies = ( 87 | ); 88 | productName = virtualOS; 89 | productReference = 015228552CB27BC100209934 /* virtualOS.app */; 90 | productType = "com.apple.product-type.application"; 91 | }; 92 | /* End PBXNativeTarget section */ 93 | 94 | /* Begin PBXProject section */ 95 | 0152284D2CB27BC100209934 /* Project object */ = { 96 | isa = PBXProject; 97 | attributes = { 98 | BuildIndependentTargetsInParallel = 1; 99 | LastSwiftUpdateCheck = 1600; 100 | LastUpgradeCheck = 2600; 101 | TargetAttributes = { 102 | 015228542CB27BC100209934 = { 103 | CreatedOnToolsVersion = 16.0; 104 | }; 105 | }; 106 | }; 107 | buildConfigurationList = 015228502CB27BC100209934 /* Build configuration list for PBXProject "virtualOS" */; 108 | developmentRegion = en; 109 | hasScannedForEncodings = 0; 110 | knownRegions = ( 111 | en, 112 | Base, 113 | ); 114 | mainGroup = 0152284C2CB27BC100209934; 115 | minimizedProjectReferenceProxies = 1; 116 | preferredProjectObjectVersion = 77; 117 | productRefGroup = 015228562CB27BC100209934 /* Products */; 118 | projectDirPath = ""; 119 | projectRoot = ""; 120 | targets = ( 121 | 015228542CB27BC100209934 /* virtualOS */, 122 | ); 123 | }; 124 | /* End PBXProject section */ 125 | 126 | /* Begin PBXResourcesBuildPhase section */ 127 | 015228532CB27BC100209934 /* Resources */ = { 128 | isa = PBXResourcesBuildPhase; 129 | buildActionMask = 2147483647; 130 | files = ( 131 | ); 132 | runOnlyForDeploymentPostprocessing = 0; 133 | }; 134 | /* End PBXResourcesBuildPhase section */ 135 | 136 | /* Begin PBXSourcesBuildPhase section */ 137 | 015228512CB27BC100209934 /* Sources */ = { 138 | isa = PBXSourcesBuildPhase; 139 | buildActionMask = 2147483647; 140 | files = ( 141 | ); 142 | runOnlyForDeploymentPostprocessing = 0; 143 | }; 144 | /* End PBXSourcesBuildPhase section */ 145 | 146 | /* Begin XCBuildConfiguration section */ 147 | 015228622CB27BC200209934 /* Debug */ = { 148 | isa = XCBuildConfiguration; 149 | buildSettings = { 150 | ALWAYS_SEARCH_USER_PATHS = NO; 151 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 152 | CLANG_ANALYZER_NONNULL = YES; 153 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 154 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 155 | CLANG_ENABLE_MODULES = YES; 156 | CLANG_ENABLE_OBJC_ARC = YES; 157 | CLANG_ENABLE_OBJC_WEAK = YES; 158 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 159 | CLANG_WARN_BOOL_CONVERSION = YES; 160 | CLANG_WARN_COMMA = YES; 161 | CLANG_WARN_CONSTANT_CONVERSION = YES; 162 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 163 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 164 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 165 | CLANG_WARN_EMPTY_BODY = YES; 166 | CLANG_WARN_ENUM_CONVERSION = YES; 167 | CLANG_WARN_INFINITE_RECURSION = YES; 168 | CLANG_WARN_INT_CONVERSION = YES; 169 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 170 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 171 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 172 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 173 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 174 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 175 | CLANG_WARN_STRICT_PROTOTYPES = YES; 176 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 177 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 178 | CLANG_WARN_UNREACHABLE_CODE = YES; 179 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 180 | COPY_PHASE_STRIP = NO; 181 | DEAD_CODE_STRIPPING = YES; 182 | DEBUG_INFORMATION_FORMAT = dwarf; 183 | ENABLE_STRICT_OBJC_MSGSEND = YES; 184 | ENABLE_TESTABILITY = YES; 185 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 186 | GCC_C_LANGUAGE_STANDARD = gnu17; 187 | GCC_DYNAMIC_NO_PIC = NO; 188 | GCC_NO_COMMON_BLOCKS = YES; 189 | GCC_OPTIMIZATION_LEVEL = 0; 190 | GCC_PREPROCESSOR_DEFINITIONS = ( 191 | "DEBUG=1", 192 | "$(inherited)", 193 | ); 194 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 195 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 196 | GCC_WARN_UNDECLARED_SELECTOR = YES; 197 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 198 | GCC_WARN_UNUSED_FUNCTION = YES; 199 | GCC_WARN_UNUSED_VARIABLE = YES; 200 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 201 | MACOSX_DEPLOYMENT_TARGET = 15.0; 202 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 203 | MTL_FAST_MATH = YES; 204 | ONLY_ACTIVE_ARCH = YES; 205 | SDKROOT = macosx; 206 | STRING_CATALOG_GENERATE_SYMBOLS = YES; 207 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 208 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 209 | }; 210 | name = Debug; 211 | }; 212 | 015228632CB27BC200209934 /* Release */ = { 213 | isa = XCBuildConfiguration; 214 | buildSettings = { 215 | ALWAYS_SEARCH_USER_PATHS = NO; 216 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 217 | CLANG_ANALYZER_NONNULL = YES; 218 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 219 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 220 | CLANG_ENABLE_MODULES = YES; 221 | CLANG_ENABLE_OBJC_ARC = YES; 222 | CLANG_ENABLE_OBJC_WEAK = YES; 223 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 224 | CLANG_WARN_BOOL_CONVERSION = YES; 225 | CLANG_WARN_COMMA = YES; 226 | CLANG_WARN_CONSTANT_CONVERSION = YES; 227 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 228 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 229 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 230 | CLANG_WARN_EMPTY_BODY = YES; 231 | CLANG_WARN_ENUM_CONVERSION = YES; 232 | CLANG_WARN_INFINITE_RECURSION = YES; 233 | CLANG_WARN_INT_CONVERSION = YES; 234 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 235 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 236 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 237 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 238 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 239 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 240 | CLANG_WARN_STRICT_PROTOTYPES = YES; 241 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 242 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 243 | CLANG_WARN_UNREACHABLE_CODE = YES; 244 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 245 | COPY_PHASE_STRIP = NO; 246 | DEAD_CODE_STRIPPING = YES; 247 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 248 | ENABLE_NS_ASSERTIONS = NO; 249 | ENABLE_STRICT_OBJC_MSGSEND = YES; 250 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 251 | GCC_C_LANGUAGE_STANDARD = gnu17; 252 | GCC_NO_COMMON_BLOCKS = YES; 253 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 254 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 255 | GCC_WARN_UNDECLARED_SELECTOR = YES; 256 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 257 | GCC_WARN_UNUSED_FUNCTION = YES; 258 | GCC_WARN_UNUSED_VARIABLE = YES; 259 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 260 | MACOSX_DEPLOYMENT_TARGET = 15.0; 261 | MTL_ENABLE_DEBUG_INFO = NO; 262 | MTL_FAST_MATH = YES; 263 | SDKROOT = macosx; 264 | STRING_CATALOG_GENERATE_SYMBOLS = YES; 265 | SWIFT_COMPILATION_MODE = wholemodule; 266 | }; 267 | name = Release; 268 | }; 269 | 015228652CB27BC200209934 /* Debug */ = { 270 | isa = XCBuildConfiguration; 271 | buildSettings = { 272 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 273 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 274 | CODE_SIGN_ENTITLEMENTS = virtualOS/Resources/virtualOS.entitlements; 275 | CODE_SIGN_IDENTITY = "Apple Development"; 276 | CODE_SIGN_STYLE = Automatic; 277 | COMBINE_HIDPI_IMAGES = YES; 278 | CURRENT_PROJECT_VERSION = 34; 279 | DEAD_CODE_STRIPPING = YES; 280 | DEVELOPMENT_TEAM = 2AD47BTDQ6; 281 | ENABLE_APP_SANDBOX = YES; 282 | ENABLE_HARDENED_RUNTIME = YES; 283 | ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; 284 | ENABLE_USER_SELECTED_FILES = readwrite; 285 | GENERATE_INFOPLIST_FILE = YES; 286 | INFOPLIST_FILE = virtualOS/Resources/Info.plist; 287 | INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; 288 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 289 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 290 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 291 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "Please allow microphone access to use audio input in the virtual machine."; 292 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 293 | LD_RUNPATH_SEARCH_PATHS = ( 294 | "$(inherited)", 295 | "@executable_path/../Frameworks", 296 | ); 297 | MARKETING_VERSION = 2.3.1; 298 | PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOS; 299 | PRODUCT_NAME = "$(TARGET_NAME)"; 300 | PROVISIONING_PROFILE_SPECIFIER = ""; 301 | SWIFT_EMIT_LOC_STRINGS = YES; 302 | SWIFT_VERSION = 5.0; 303 | }; 304 | name = Debug; 305 | }; 306 | 015228662CB27BC200209934 /* Release */ = { 307 | isa = XCBuildConfiguration; 308 | buildSettings = { 309 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 310 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 311 | CODE_SIGN_ENTITLEMENTS = virtualOS/Resources/virtualOS.entitlements; 312 | CODE_SIGN_IDENTITY = "Apple Development"; 313 | CODE_SIGN_STYLE = Automatic; 314 | COMBINE_HIDPI_IMAGES = YES; 315 | CURRENT_PROJECT_VERSION = 34; 316 | DEAD_CODE_STRIPPING = YES; 317 | DEVELOPMENT_TEAM = 2AD47BTDQ6; 318 | ENABLE_APP_SANDBOX = YES; 319 | ENABLE_HARDENED_RUNTIME = YES; 320 | ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; 321 | ENABLE_USER_SELECTED_FILES = readwrite; 322 | GENERATE_INFOPLIST_FILE = YES; 323 | INFOPLIST_FILE = virtualOS/Resources/Info.plist; 324 | INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; 325 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 326 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 327 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 328 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "Please allow microphone access to use audio input in the virtual machine."; 329 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 330 | LD_RUNPATH_SEARCH_PATHS = ( 331 | "$(inherited)", 332 | "@executable_path/../Frameworks", 333 | ); 334 | MARKETING_VERSION = 2.3.1; 335 | PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOS; 336 | PRODUCT_NAME = "$(TARGET_NAME)"; 337 | PROVISIONING_PROFILE_SPECIFIER = ""; 338 | SWIFT_EMIT_LOC_STRINGS = YES; 339 | SWIFT_VERSION = 5.0; 340 | }; 341 | name = Release; 342 | }; 343 | /* End XCBuildConfiguration section */ 344 | 345 | /* Begin XCConfigurationList section */ 346 | 015228502CB27BC100209934 /* Build configuration list for PBXProject "virtualOS" */ = { 347 | isa = XCConfigurationList; 348 | buildConfigurations = ( 349 | 015228622CB27BC200209934 /* Debug */, 350 | 015228632CB27BC200209934 /* Release */, 351 | ); 352 | defaultConfigurationIsVisible = 0; 353 | defaultConfigurationName = Release; 354 | }; 355 | 015228642CB27BC200209934 /* Build configuration list for PBXNativeTarget "virtualOS" */ = { 356 | isa = XCConfigurationList; 357 | buildConfigurations = ( 358 | 015228652CB27BC200209934 /* Debug */, 359 | 015228662CB27BC200209934 /* Release */, 360 | ); 361 | defaultConfigurationIsVisible = 0; 362 | defaultConfigurationName = Release; 363 | }; 364 | /* End XCConfigurationList section */ 365 | }; 366 | rootObject = 0152284D2CB27BC100209934 /* Project object */; 367 | } 368 | -------------------------------------------------------------------------------- /virtualOS/ViewController/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Cocoa 10 | import Virtualization 11 | import OSLog 12 | 13 | #if arch(arm64) 14 | 15 | final class MainViewController: NSViewController { 16 | @IBOutlet weak var tableView: NSTableView! 17 | @IBOutlet weak var vmNameTextField: NSTextField! 18 | @IBOutlet weak var parameterOutlineView: NSOutlineView! 19 | @IBOutlet weak var startButton: NSButton! 20 | @IBOutlet weak var sharedFolderButton: NSButton! 21 | @IBOutlet weak var deleteButton: NSButton! 22 | @IBOutlet weak var cpuCountLabel: NSTextField! 23 | @IBOutlet weak var cpuCountSlider: NSSlider! 24 | @IBOutlet weak var ramLabel: NSTextField! 25 | @IBOutlet weak var ramSlider: NSSlider! 26 | @IBOutlet weak var microphoneSwitch: NSSwitch! 27 | @IBOutlet weak var networkPopUpButton: NSPopUpButton! 28 | @IBOutlet weak var networkBridgePopUpButton: NSPopUpButton! 29 | 30 | fileprivate let mainStoryBoard = NSStoryboard(name: "Main", bundle: nil) 31 | fileprivate let viewModel = MainViewModel() 32 | fileprivate var diskImageSize = 1 33 | fileprivate var windowController: WindowController? { 34 | return view.window?.windowController as? WindowController 35 | } 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | view.window?.delegate = self 41 | tableView.delegate = self 42 | tableView.dataSource = viewModel.tableViewDataSource 43 | parameterOutlineView.dataSource = viewModel.parametersViewDataSource 44 | parameterOutlineView.delegate = viewModel.parametersViewDelegate 45 | vmNameTextField.delegate = viewModel.textFieldDelegate 46 | 47 | NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: NSApplication.didBecomeActiveNotification, object: nil) 48 | NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: NSControl.textDidEndEditingNotification, object: nil) 49 | NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: Constants.didChangeAppSettingsNotification, object: nil) 50 | NotificationCenter.default.addObserver(self, selector: #selector(restoreImageSelected), name: Constants.restoreImageNameSelectedNotification, object: nil) 51 | NotificationCenter.default.addObserver(self, selector: #selector(networkBridgeInterfaceWillPopUp), name: NSPopUpButton.willPopUpNotification, object: nil) 52 | 53 | ramSlider.target = self 54 | ramSlider.action = #selector(memorySliderChanged(sender:)) 55 | cpuCountSlider.target = self 56 | cpuCountSlider.action = #selector(cpuCountChanged(sender:)) 57 | } 58 | 59 | deinit { 60 | NotificationCenter.default.removeObserver(self) 61 | } 62 | 63 | override func viewWillAppear() { 64 | super.viewWillAppear() 65 | windowController?.mainViewController = self 66 | vmNameTextField.resignFirstResponder() 67 | _ = URL.startAccessingVMFilesDirectory() 68 | FileModel.cleanUpTemporaryFiles() 69 | updateUI() 70 | } 71 | 72 | override func viewDidAppear() { 73 | super.viewDidAppear() 74 | if viewModel.vmParameters?.microphoneEnabled == true { 75 | viewModel.checkMicrophonePermission {} 76 | } 77 | 78 | for arg in CommandLine.arguments { 79 | if (arg == "start") { 80 | Logger.shared.log(level: .default, "auto-starting vm") 81 | startVM() 82 | } 83 | } 84 | } 85 | 86 | @IBAction func startButtonPressed(_ sender: NSButton) { 87 | startVM() 88 | } 89 | 90 | @IBAction func installButtonPressed(_ sender: NSButton) { 91 | if let restoreImageViewController = mainStoryBoard.instantiateController(withIdentifier: "RestoreImageViewController") as? RestoreImageViewController 92 | { 93 | show(viewController: restoreImageViewController, title: "Restore Image") 94 | } else { 95 | Logger.shared.log(level: .default, "show restore image window failed") 96 | } 97 | } 98 | 99 | @IBAction func deleteButtonPressed(_ sender: Any) { 100 | guard let vmBundle = viewModel.vmBundle else { 101 | return 102 | } 103 | 104 | let alert: NSAlert = NSAlert.okCancelAlert(messageText: "Delete VM '\(vmBundle.name)'?", informativeText: "This can not be undone.", alertStyle: .warning) 105 | let selection = alert.runModal() 106 | if selection == NSApplication.ModalResponse.alertFirstButtonReturn || 107 | selection == NSApplication.ModalResponse.OK 108 | { 109 | viewModel.deleteVM(selection: selection, vmBundle: vmBundle) 110 | viewModel.vmBundle = nil 111 | viewModel.vmParameters = nil 112 | } 113 | 114 | self.updateUI() 115 | } 116 | 117 | @IBAction func sharedFolderButtonPressed(_ sender: Any) { 118 | let openPanel = NSOpenPanel() 119 | openPanel.allowsMultipleSelection = false 120 | openPanel.canChooseDirectories = true 121 | openPanel.canChooseFiles = false 122 | openPanel.prompt = "Select" 123 | openPanel.message = "Select a folder to share with the VM" 124 | let modalResponse = openPanel.runModal() 125 | var sharedFolderURL: URL? 126 | if modalResponse == .OK, 127 | let selectedURL = openPanel.url 128 | { 129 | sharedFolderURL = selectedURL 130 | } else if modalResponse == .cancel { 131 | sharedFolderURL = nil 132 | } 133 | 134 | viewModel.set(sharedFolderUrl: sharedFolderURL) 135 | updateOutlineView() 136 | } 137 | 138 | @objc func restoreImageSelected(notification: Notification) { 139 | if let userInfo = notification.userInfo, 140 | let restoreImageName = userInfo[Constants.selectedRestoreImage] as? String 141 | { 142 | if restoreImageName != Constants.restoreImageNameLatest { 143 | let accessoryView = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 20)) 144 | accessoryView.stringValue = "\(UserDefaults.standard.diskSize)" 145 | 146 | let alert = NSAlert.okCancelAlert(messageText: "Disk Image Size in GB", informativeText: "Disk size can not be changed after VM is created. Minimum disk size is \(Constants.defaultDiskImageSize) GB.", accessoryView: accessoryView) 147 | let modalResponse = alert.runModal() 148 | accessoryView.becomeFirstResponder() 149 | 150 | if modalResponse == .OK || modalResponse == .alertFirstButtonReturn { 151 | diskImageSize = Int(accessoryView.intValue) 152 | } else { 153 | return // cancel install 154 | } 155 | if diskImageSize < Constants.defaultDiskImageSize { 156 | self.diskImageSize = Constants.defaultDiskImageSize 157 | } 158 | } 159 | 160 | showSheet(mode: .install, restoreImageName: restoreImageName, diskImageSize: self.diskImageSize) 161 | } 162 | } 163 | 164 | @objc func cpuCountChanged(sender: NSSlider) { 165 | updateUIAndStoreParametersToDisk() 166 | } 167 | 168 | @objc func memorySliderChanged(sender: NSSlider) { 169 | updateUIAndStoreParametersToDisk() 170 | } 171 | 172 | @IBAction func microphoneSwitchToggled(_ sender: NSSwitch) { 173 | if sender.state == .on { 174 | viewModel.checkMicrophonePermission { 175 | DispatchQueue.main.async { [weak self] in 176 | let alert = NSAlert.okCancelAlert(messageText: "Microphone Permission", informativeText: "Please allow microphone access in System Settings → Privacy → Camera and Microphone to use audio input in the virtual machine.", showCancelButton: false) 177 | let _ = alert.runModal() 178 | self?.microphoneSwitch.state = .off 179 | } 180 | } 181 | } 182 | viewModel.vmParameters?.microphoneEnabled = sender.state == .on 183 | viewModel.storeParametersToDisk() 184 | } 185 | 186 | @IBAction func networkPopUpChanged(_ sender: NSPopUpButton) { 187 | var networkType = NetworkType.NAT 188 | if sender.selectedItem?.title == NetworkType.Bridge.rawValue { 189 | networkType = .Bridge 190 | } 191 | 192 | updateBridges() 193 | viewModel.vmParameters?.networkType = networkType 194 | viewModel.storeParametersToDisk() 195 | updateUI() 196 | } 197 | 198 | @IBAction func networkBridgeInterfacePopUpChanged(_ sender: NSPopUpButton) { 199 | viewModel.vmParameters?.networkBridge = sender.selectedItem?.title 200 | viewModel.storeParametersToDisk() 201 | } 202 | 203 | @objc func networkBridgeInterfaceWillPopUp() { 204 | updateBridges() 205 | } 206 | @objc func updateUI() { 207 | self.tableView.reloadData() 208 | 209 | if let selectedRow = viewModel.selectedRow, 210 | selectedRow < 0, 211 | viewModel.tableViewDataSource.numberOfRows(in: tableView) > 0 212 | { 213 | viewModel.selectedRow = 0 // one or more vms available, select first 214 | } 215 | 216 | if let selectedRow = viewModel.selectedRow, 217 | let vmBundle = viewModel.tableViewDataSource.vmBundle(forRow: selectedRow) 218 | { 219 | vmNameTextField.stringValue = vmBundle.name 220 | tableView.selectRowIndexes(IndexSet(integer: selectedRow), byExtendingSelection: false) 221 | viewModel.vmBundle = vmBundle 222 | if let vmParameters = VMParameters.readFrom(url: vmBundle.url) { 223 | viewModel.vmParameters = vmParameters 224 | if vmParameters.installFinished == true { 225 | viewModel.textFieldDelegate.vmBundle = vmBundle 226 | updateLabels(setZero: false) 227 | updateCpuCount(vmParameters) 228 | updateRam(vmParameters) 229 | updateNetwork(vmParameters) 230 | updateEnabledState(enabled: true, vmParameters: vmParameters) 231 | windowController?.updateButtons(hidden: false) 232 | } else { 233 | invalidBundleOrInstallIncomplete() 234 | } 235 | } else { 236 | viewModel.vmParameters = nil 237 | invalidBundleOrInstallIncomplete() 238 | } 239 | } else { 240 | vmNameTextField.stringValue = "No virtual machines available. Press install to add one." 241 | viewModel.vmParameters = nil 242 | updateLabels(setZero: true) 243 | updateEnabledState(enabled: false) 244 | windowController?.updateButtons(hidden: true) 245 | } 246 | 247 | updateBridges() 248 | updateOutlineView() 249 | } 250 | 251 | fileprivate func invalidBundleOrInstallIncomplete() { 252 | updateLabels(setZero: true) 253 | updateEnabledState(enabled: false) 254 | } 255 | 256 | fileprivate func updateBridges() { 257 | networkBridgePopUpButton.removeAllItems() 258 | var i = 0 259 | let selectedBridge = viewModel.vmParameters?.networkBridge 260 | for interface in VZBridgedNetworkInterface.networkInterfaces { 261 | networkBridgePopUpButton.insertItem(withTitle: interface.description, at: i) 262 | i += 1 263 | if selectedBridge == interface.description { 264 | networkBridgePopUpButton.selectItem(at: i) 265 | viewModel.vmParameters?.networkBridge = interface.description 266 | } 267 | } 268 | 269 | if networkBridgePopUpButton.selectedItem == nil { 270 | networkBridgePopUpButton.selectItem(at: 0) 271 | } 272 | } 273 | 274 | func showErrorAlert(error: Error) { 275 | var messageText = "Error" 276 | var informativeText = "An unknown error occurred." 277 | 278 | if let vzError = error as? VZError, 279 | let reason = vzError.userInfo[NSLocalizedFailureErrorKey] as? String, 280 | let failureReason = vzError.userInfo[NSLocalizedFailureReasonErrorKey] as? String, 281 | let underlyingError = vzError.userInfo[NSUnderlyingErrorKey] as? NSError 282 | { 283 | messageText = failureReason 284 | informativeText = reason + " " + underlyingError.localizedDescription + "\n\n(Error Code: \(vzError.errorCode), Underlying Error Domain: \(underlyingError.domain), Underlying Error Code: \(underlyingError.code))" 285 | if vzError.errorCode == 10007 && underlyingError.code == 3004 { 286 | informativeText += "\n\nYou have to be connected to the internet to start the install." 287 | } 288 | } else if let restoreError = error as? RestoreError { 289 | informativeText = error.localizedDescription + " " + restoreError.localizedDescription 290 | } else { 291 | informativeText = error.localizedDescription 292 | } 293 | 294 | Logger.shared.log(level: .default, "\(messageText) \(informativeText)") 295 | let alert = NSAlert.okCancelAlert(messageText: messageText, informativeText: informativeText, showCancelButton: false) 296 | let _ = alert.runModal() 297 | } 298 | 299 | // MARK: - Private 300 | 301 | fileprivate func updateEnabledState(enabled: Bool, vmParameters: VMParameters? = nil) { 302 | ramSlider.isEnabled = enabled 303 | cpuCountSlider.isEnabled = enabled 304 | vmNameTextField.isEnabled = enabled 305 | microphoneSwitch.isEnabled = enabled 306 | networkPopUpButton.isEnabled = enabled 307 | if vmParameters?.microphoneEnabled ?? false { 308 | microphoneSwitch.state = .on 309 | } else { 310 | microphoneSwitch.state = .off 311 | } 312 | } 313 | 314 | fileprivate func updateCpuCount(_ vmParameters: VMParameters) { 315 | cpuCountSlider.minValue = Double(vmParameters.cpuCountMin) 316 | cpuCountSlider.maxValue = Double(vmParameters.cpuCountMax) 317 | cpuCountSlider.numberOfTickMarks = Int(cpuCountSlider.maxValue - cpuCountSlider.minValue) 318 | cpuCountSlider.doubleValue = Double(vmParameters.cpuCount) 319 | cpuCountSlider.isEnabled = true 320 | } 321 | 322 | fileprivate func updateRam(_ vmParameters: VMParameters) { 323 | ramSlider.minValue = max(Double(vmParameters.memorySizeInGBMin), 2.0) 324 | ramSlider.maxValue = Double(vmParameters.memorySizeInGBMax) 325 | ramSlider.numberOfTickMarks = Int(ramSlider.maxValue - ramSlider.minValue) 326 | ramSlider.doubleValue = Double(vmParameters.memorySizeInGB) 327 | ramSlider.isEnabled = true 328 | } 329 | 330 | fileprivate func updateNetwork(_ vmParameters: VMParameters) { 331 | switch vmParameters.networkType { 332 | case .NAT: 333 | networkPopUpButton.selectItem(at: 0) 334 | case .Bridge: 335 | networkPopUpButton.selectItem(at: 1) 336 | default: 337 | networkPopUpButton.selectItem(at: 0) 338 | } 339 | 340 | if vmParameters.networkType == .NAT { 341 | networkBridgePopUpButton.isHidden = true 342 | } else { 343 | networkBridgePopUpButton.isHidden = false 344 | } 345 | } 346 | 347 | fileprivate func updateLabels(setZero: Bool) { 348 | let cpuCount = Int(round(cpuCountSlider.doubleValue)) 349 | let memorySizeInGB = Int(round(ramSlider.doubleValue)) 350 | viewModel.vmParameters?.cpuCount = cpuCount 351 | viewModel.vmParameters?.memorySizeInGB = UInt64(memorySizeInGB) 352 | 353 | if setZero { 354 | cpuCountLabel.stringValue = "CPU Count" 355 | ramLabel.stringValue = "RAM" 356 | } else { 357 | cpuCountLabel.stringValue = "CPU Count: \(cpuCount)" 358 | ramLabel.stringValue = "RAM: \(memorySizeInGB) GB" 359 | } 360 | } 361 | 362 | fileprivate func updateOutlineView() { 363 | parameterOutlineView.reloadData() 364 | } 365 | 366 | fileprivate func updateUIAndStoreParametersToDisk() { 367 | viewModel.storeParametersToDisk() 368 | updateLabels(setZero: false) 369 | } 370 | 371 | fileprivate func showSheet(mode: ProgressViewController.Mode, restoreImageName: String?, diskImageSize: Int?) { 372 | if let progressWindowController = mainStoryBoard.instantiateController(withIdentifier: "ProgressWindowController") as? NSWindowController, 373 | let progressWindow = progressWindowController.window 374 | { 375 | if let progressViewController = progressWindow.contentViewController as? ProgressViewController { 376 | progressViewController.mode = mode 377 | progressViewController.diskImageSize = diskImageSize 378 | progressViewController.restoreImageName = restoreImageName 379 | presentAsSheet(progressViewController) 380 | } 381 | } else { 382 | Logger.shared.log(level: .default, "show modal failed") 383 | } 384 | } 385 | 386 | fileprivate func show(viewController: NSViewController, title: String) { 387 | let newWindow = NSWindow(contentViewController: viewController) 388 | newWindow.title = title 389 | newWindow.makeKeyAndOrderFront(nil) 390 | if let parentFrame = view.window?.frame { 391 | newWindow.setFrame(parentFrame.offsetBy(dx: 90, dy: -10), display: true) 392 | } 393 | } 394 | 395 | fileprivate func startVM() { 396 | if let vmViewController = mainStoryBoard.instantiateController(withIdentifier: "VMViewController") as? VMViewController 397 | { 398 | vmViewController.vmBundle = viewModel.vmBundle 399 | vmViewController.vmParameters = viewModel.vmParameters 400 | show(viewController: vmViewController, title: viewModel.vmBundle?.name ?? "virtualOS VM") 401 | } else { 402 | Logger.shared.log(level: .default, "show vm window failed") 403 | } 404 | } 405 | 406 | } 407 | 408 | extension MainViewController: NSTableViewDelegate { 409 | func tableViewSelectionDidChange(_ notification: Notification) { 410 | var row: Int? = nil 411 | 412 | if let userInfo = notification.userInfo, 413 | let indexSet = userInfo["NSTableViewCurrentRowSelectionUserInfoKey"] as? NSIndexSet { 414 | if indexSet.count > 0 { 415 | row = indexSet.firstIndex 416 | } 417 | } 418 | 419 | if let row = row { 420 | viewModel.selectedRow = row 421 | updateUI() 422 | } 423 | } 424 | } 425 | 426 | extension MainViewController: NSWindowDelegate { 427 | func windowShouldClose(_ sender: NSWindow) -> Bool { 428 | let alert: NSAlert = NSAlert.okCancelAlert(messageText: "Quit", informativeText: "Quitting the app will stop all virtual machines.", alertStyle: .warning) 429 | let selection = alert.runModal() 430 | if selection == NSApplication.ModalResponse.alertFirstButtonReturn || 431 | selection == NSApplication.ModalResponse.OK 432 | { 433 | NSApplication.shared.terminate(self) 434 | return true 435 | } else { 436 | return false 437 | } 438 | } 439 | } 440 | 441 | #else 442 | 443 | // minimum implementation used for intel cpus 444 | 445 | final class MainViewController: NSViewController { 446 | @IBOutlet weak var vmNameTextField: NSTextField! 447 | @IBOutlet weak var installButton: NSButton! 448 | @IBOutlet weak var startButton: NSButton! 449 | @IBOutlet weak var sharedFolderButton: NSButton! 450 | @IBOutlet weak var deleteButton: NSButton! 451 | @IBOutlet weak var cpuCountLabel: NSTextField! 452 | @IBOutlet weak var cpuCountSlider: NSSlider! 453 | @IBOutlet weak var ramLabel: NSTextField! 454 | @IBOutlet weak var ramSlider: NSSlider! 455 | 456 | override func viewWillAppear() { 457 | super.viewWillAppear() 458 | vmNameTextField.stringValue = "Virtualization requires an Apple Silicon machine" 459 | vmNameTextField.isEditable = false 460 | installButton.isEnabled = false 461 | startButton.isEnabled = false 462 | sharedFolderButton.isEnabled = false 463 | deleteButton.isEnabled = false 464 | cpuCountSlider.isEnabled = false 465 | ramSlider.isEnabled = false 466 | cpuCountLabel.stringValue = "" 467 | ramLabel.stringValue = "" 468 | } 469 | } 470 | 471 | #endif 472 | 473 | // place all code before #else 474 | --------------------------------------------------------------------------------