├── .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 |
--------------------------------------------------------------------------------