├── .gitignore
├── header.png
├── VFHost
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── icon-1024.png
│ │ ├── icon-128.png
│ │ ├── icon-16.png
│ │ ├── icon-256.png
│ │ ├── icon-32.png
│ │ ├── icon-512.png
│ │ ├── icon-64.png
│ │ └── Contents.json
│ └── AccentColor.colorset
│ │ └── Contents.json
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── VFHost.entitlements
├── DownloadURLs.plist
├── Info.plist
├── VFHostApp.swift
├── VirtualMachine.swift
├── ManagedMode.swift
└── ContentView.swift
├── VFHost.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcuserdata
│ └── jds.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── project.pbxproj
├── .github
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── LICENSE
├── README.md
└── CODE_OF_CONDUCT.md
/.gitignore:
--------------------------------------------------------------------------------
1 | xcuserdata/
2 |
--------------------------------------------------------------------------------
/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackSteele/VFHost/HEAD/header.png
--------------------------------------------------------------------------------
/VFHost/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/VFHost/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/VFHost/Assets.xcassets/AppIcon.appiconset/icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackSteele/VFHost/HEAD/VFHost/Assets.xcassets/AppIcon.appiconset/icon-1024.png
--------------------------------------------------------------------------------
/VFHost/Assets.xcassets/AppIcon.appiconset/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackSteele/VFHost/HEAD/VFHost/Assets.xcassets/AppIcon.appiconset/icon-128.png
--------------------------------------------------------------------------------
/VFHost/Assets.xcassets/AppIcon.appiconset/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackSteele/VFHost/HEAD/VFHost/Assets.xcassets/AppIcon.appiconset/icon-16.png
--------------------------------------------------------------------------------
/VFHost/Assets.xcassets/AppIcon.appiconset/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackSteele/VFHost/HEAD/VFHost/Assets.xcassets/AppIcon.appiconset/icon-256.png
--------------------------------------------------------------------------------
/VFHost/Assets.xcassets/AppIcon.appiconset/icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackSteele/VFHost/HEAD/VFHost/Assets.xcassets/AppIcon.appiconset/icon-32.png
--------------------------------------------------------------------------------
/VFHost/Assets.xcassets/AppIcon.appiconset/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackSteele/VFHost/HEAD/VFHost/Assets.xcassets/AppIcon.appiconset/icon-512.png
--------------------------------------------------------------------------------
/VFHost/Assets.xcassets/AppIcon.appiconset/icon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackSteele/VFHost/HEAD/VFHost/Assets.xcassets/AppIcon.appiconset/icon-64.png
--------------------------------------------------------------------------------
/VFHost.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/VFHost/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 |
--------------------------------------------------------------------------------
/VFHost.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/VFHost.xcodeproj/xcuserdata/jds.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | VFHost.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/VFHost/VFHost.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.automation.apple-events
8 |
9 | com.apple.security.files.user-selected.read-write
10 |
11 | com.apple.security.virtualization
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/VFHost/DownloadURLs.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Focal arm64
6 |
7 | kernel
8 | http://cloud-images.ubuntu.com/focal/current/unpacked/focal-server-cloudimg-arm64-vmlinuz-generic
9 | ramdisk
10 | http://cloud-images.ubuntu.com/focal/current/unpacked/focal-server-cloudimg-arm64-initrd-generic
11 | disk
12 | http://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-arm64.tar.gz
13 |
14 | Focal x86_64
15 |
16 | kernel
17 | http://cloud-images.ubuntu.com/focal/current/unpacked/focal-server-cloudimg-amd64-vmlinuz-generic
18 | ramdisk
19 | http://cloud-images.ubuntu.com/focal/current/unpacked/focal-server-cloudimg-amd64-initrd-generic
20 | disk
21 | http://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.tar.gz
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/VFHost/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | LSApplicationCategoryType
22 | public.app-category.developer-tools
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSAppTransportSecurity
26 |
27 | NSAllowsArbitraryLoads
28 |
29 |
30 | NSAppleEventsUsageDescription
31 | You must allow Apple events to open Terminal attached to your VM.
32 |
33 |
34 |
--------------------------------------------------------------------------------
/VFHost/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon-16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "icon-32.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "icon-32.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "icon-64.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "icon-128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "icon-256.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "icon-256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "icon-512.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "icon-512.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "icon-1024.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2021, Jack Steele
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | ### VFHost is a simple GUI for hosting Linux VMs on macOS Big Sur's Virtualization.framework.
4 |
5 | ## Downloads
6 | Downloads are available [for version 0.2.x](https://github.com/JackSteele/VFHost/releases). These builds are notarized by Apple.
7 |
8 | ## You should know
9 | This information will be incorporated in the app, but for now...
10 | - Managed Mode
11 | - Your installations are located at ~/Library/Application Support/VFHost
12 | - You can't currently change your CPU/memory allocation through Managed Mode. Turning off Managed Mode and pointing VFHost to the files in the directory above is a workaround for now, though this will be implemented soon.
13 | - During the install process, the root disk is resized to ~8GB. You can manually resize it if you wish. Disk resize through VFHost is a high priority feature, and will be implemented soon.
14 | - To boot your VM outside of Managed Mode, you'll need to set the kernel parameters `console=hvc0 root=/dev/vda`
15 |
16 | ## Building
17 | Open `VFHost.xcodeproj`, add your certificate, and you're off to the races.
18 |
19 | ## Known issues & workarounds
20 | - VFHost uses `screen` internally to attach to your VM. On rare occasion, `screen` sessions are left behind and error messages appear, even after the app is restarted. First, make sure you're not using any `screen` sessions yourself - we're about to kill them all. Open Terminal and run `% pkill SCREEN`.
21 | - If you find issues, please report them!
22 |
23 | ## Similar projects
24 | **[evansm7/vftool](https://github.com/evansm7/vftool)** - this is a more mature (CLI-only) wrapper for Virtualization.framework
25 |
26 | ## License
27 | VFHost is under the BSD license - you can find it [here](https://github.com/JackSteele/VFHost/blob/main/LICENSE)
28 |
--------------------------------------------------------------------------------
/VFHost/VFHostApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VFHostApp.swift
3 | // VFHost
4 | //
5 | // Created by Jack Steele on 2/4/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct VFHostApp: App {
12 | // @Environment var willTerminate: Bool = false
13 |
14 | @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
15 | // @State var quitAttempted: Bool
16 |
17 | var body: some Scene {
18 | WindowGroup {
19 | ContentView()
20 | }
21 | .commands {
22 | CommandGroup(replacing: CommandGroupPlacement.newItem, addition: { })
23 | }
24 |
25 | }
26 | }
27 |
28 | class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
29 | @Published var shouldTerminate = false
30 | @Published var canTerminate = true
31 |
32 | func applicationWillFinishLaunching(_ notification: Notification) {
33 | NSWindow.allowsAutomaticWindowTabbing = false
34 | }
35 |
36 | func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
37 | if canTerminate {
38 | return .terminateNow
39 | }
40 |
41 | if NSApplication.shared.windows.count == 0 {
42 | return .terminateNow
43 | } else {
44 | let alert = NSAlert()
45 | alert.messageText = "Really quit?"
46 | alert.informativeText = "You're about to close VFHost with a VM running. Is this what you want?"
47 | alert.addButton(withTitle: "No, don't quit.")
48 | alert.addButton(withTitle: "Yes, quit.")
49 | alert.alertStyle = .critical
50 | let res = alert.runModal()
51 | if res == .alertFirstButtonReturn {
52 | return .terminateCancel
53 | } else if res == .alertSecondButtonReturn {
54 | return .terminateNow
55 | }
56 |
57 | return .terminateNow
58 | }
59 | }
60 |
61 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
62 | return true
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at jack@jacksteele.net. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/VFHost/VirtualMachine.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VirtualMachine.swift
3 | // VFHost
4 | //
5 | // Created by Jack Steele on 2/4/21.
6 | //
7 |
8 | import Foundation
9 | import Cocoa
10 | import Darwin
11 | import Virtualization
12 | import os.log
13 |
14 | class VirtualMachine: ObservableObject {
15 | var cfg: VZVirtualMachineConfiguration?
16 | var vm: VZVirtualMachine?
17 |
18 | var ptyFD: Int32 = 0
19 | var ptyPath = ""
20 | var screenPID: Int32 = 0
21 | var screenSession: Process?
22 |
23 | @Published var running = false
24 |
25 | func configure(_ vp: VMParameters) throws {
26 | let config = VZVirtualMachineConfiguration()
27 |
28 | let bootLoader = VZLinuxBootLoader(kernelURL: URL(fileURLWithPath: vp.kernelPath))
29 | if vp.ramdiskPath != "" {
30 | bootLoader.initialRamdiskURL = URL(fileURLWithPath: vp.ramdiskPath)
31 | }
32 | bootLoader.commandLine = vp.kernelParams
33 |
34 | config.bootLoader = bootLoader
35 |
36 | do {
37 | let storage = try VZDiskImageStorageDeviceAttachment(url: URL(fileURLWithPath: vp.diskPath), readOnly: false)
38 | let blockDevice = VZVirtioBlockDeviceConfiguration(attachment: storage)
39 | config.storageDevices = [blockDevice]
40 | } catch {
41 | os_log("Couldn't attach disk image")
42 | throw VZError(.internalError)
43 | }
44 |
45 | let ptyFD = configurePTY()
46 | if ptyFD == -1 {
47 | // should throw something more descriptive
48 | throw VZError(.internalError)
49 | }
50 |
51 | let mfile = FileHandle.init(fileDescriptor: ptyFD)
52 | let console = VZVirtioConsoleDeviceSerialPortConfiguration()
53 | console.attachment = VZFileHandleSerialPortAttachment(fileHandleForReading: mfile, fileHandleForWriting: mfile)
54 | config.serialPorts = [console]
55 |
56 | let balloonConfig = VZVirtioTraditionalMemoryBalloonDeviceConfiguration()
57 | config.memoryBalloonDevices = [balloonConfig]
58 |
59 | let entropyConfig = VZVirtioEntropyDeviceConfiguration()
60 | config.entropyDevices = [entropyConfig]
61 |
62 | let networkConfig = VZVirtioNetworkDeviceConfiguration()
63 | networkConfig.attachment = VZNATNetworkDeviceAttachment()
64 | config.networkDevices = [networkConfig]
65 |
66 | if !vp.autoCore {
67 | config.cpuCount = Int(vp.coreAlloc)
68 | }
69 |
70 | if !vp.autoMem {
71 | config.memorySize = UInt64(vp.memoryAlloc * 1024*1024*1024)
72 | } else {
73 | let minMem = VZVirtualMachineConfiguration.minimumAllowedMemorySize/(1024*1024*1024)
74 | let memRange = (VZVirtualMachineConfiguration.maximumAllowedMemorySize - VZVirtualMachineConfiguration.minimumAllowedMemorySize)/(1024*1024*1024)
75 | var mem = (memRange / 4) + minMem
76 | mem = mem * 1024*1024*1024
77 | config.memorySize = mem
78 | }
79 |
80 | try config.validate()
81 |
82 | os_log(.error, "VM configuration validation succeeded")
83 | cfg = config
84 | }
85 |
86 | func configurePTY() -> Int32 {
87 | var ptyFD: Int32 = 0
88 | var sfd: Int32 = 1
89 |
90 | if openpty(&ptyFD, &sfd, nil, nil, nil) == -1 {
91 | os_log(.error, "Error opening PTY")
92 | return -1
93 | }
94 |
95 | self.ptyPath = String(cString: ptsname(ptyFD))
96 | self.ptyFD = ptyFD
97 |
98 | return ptyFD
99 | }
100 |
101 | func start() throws {
102 | vm = VZVirtualMachine(configuration: cfg!)
103 | vm?.start { result in
104 | switch result {
105 | case .success:
106 | os_log("VM started")
107 | case .failure:
108 | os_log(.error, "Error starting VM")
109 | }
110 | }
111 | }
112 |
113 | // Calling this breaks everything, I might be an idiot
114 | func gracefulStop() {
115 | guard let vm = vm else { return }
116 | if vm.canRequestStop {
117 | do {
118 | try vm.requestStop()
119 | } catch {
120 | os_log(.error, "Couldn't stop VM gracefully")
121 | }
122 | }
123 | }
124 |
125 | func stop() {
126 | if vm != nil {
127 | // lol
128 | vm = nil
129 | // got 'em
130 | os.close(ptyFD)
131 | os_log("VM stopped")
132 | }
133 | }
134 |
135 | func isRunning() -> Bool {
136 | if vm?.state == .running {
137 | return true
138 | } else {
139 | return false
140 | }
141 | }
142 |
143 | func startScreen() {
144 | let task = Process()
145 | task.launchPath = "/usr/bin/screen"
146 | task.arguments = ["-S", "VFHost", "-dm", ptyPath]
147 | // print(task.arguments)
148 | task.launch()
149 | self.screenPID = task.processIdentifier + 1
150 | task.waitUntilExit()
151 | }
152 |
153 | func wipeScreens() {
154 | let task = Process()
155 | task.launchPath = "/usr/bin/screen"
156 | task.arguments = ["-wipe"]
157 | task.launch()
158 | task.waitUntilExit()
159 | }
160 |
161 | func attachScreen() {
162 | let script = "tell application \"Terminal\" to activate do script \"screen -x VFHost\""
163 | let applescript = NSAppleScript(source: script)
164 | var error: NSDictionary?
165 | applescript?.executeAndReturnError(&error)
166 | if let error = error {
167 | NSLog(error["NSAppleScriptErrorMessage"] as! String)
168 | }
169 | // let task = Process()
170 | // task.launchPath = "/usr/bin/env"
171 | // task.arguments = ["screen", "-x", "VFHost"]
172 | // task.launch()
173 | // self.screenSession = task
174 | }
175 |
176 | func execute(_ cmd: String) {
177 | let task = Process()
178 | task.launchPath = "/usr/bin/screen"
179 | task.arguments = ["-S", "VFHost", "-p0", "-X", "stuff", "\(cmd)\n"]
180 | task.launch()
181 | task.waitUntilExit()
182 | }
183 |
184 | func status() -> VZVirtualMachine.State? {
185 | return vm?.state
186 | }
187 |
188 | func connect() {
189 | let script = "tell application \"Terminal\" to do script \"screen \(ptyPath)\""
190 | let applescript = NSAppleScript(source: script)
191 | var error: NSDictionary?
192 | applescript?.executeAndReturnError(&error)
193 | if let error = error {
194 | NSLog(error["NSAppleScriptErrorMessage"] as! String)
195 | }
196 | }
197 | }
198 |
199 | struct VMParameters {
200 | var kernelParams = "console=hvc0"
201 | var kernelPath = ""
202 | var ramdiskPath = ""
203 | var diskPath = ""
204 | // in GB - very lazy
205 | var memoryAlloc: Double
206 | var autoCore = true
207 | var autoMem = true
208 | var coreAlloc: Double
209 | }
210 |
--------------------------------------------------------------------------------
/VFHost/ManagedMode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AutoInstall.swift
3 | // VFHost
4 | //
5 | // Created by Jack Steele on 2/5/21.
6 | //
7 |
8 | import Foundation
9 | import os.log
10 |
11 | class ManagedMode: NSObject, ObservableObject {
12 | @Published var installing: Bool = false
13 | @Published var installProgress: Progress?
14 | @Published var fractionCompleted: Double?
15 | @Published var installed: [Distro?] = []
16 |
17 | private var progressObs: [NSKeyValueObservation?] = []
18 | let fm = FileManager.default
19 |
20 | var vm = VirtualMachine()
21 |
22 | func getArch() -> Arch {
23 | let archInfo = NSString(utf8String: NXGetLocalArchInfo().pointee.description)
24 | return archInfo!.contains("ARM64") ? .arm64 : .x86_64
25 | }
26 |
27 | func startVM(_ dist: Distro) {
28 | let arch = String(describing: getArch())
29 | let distDir = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("VFHost").appendingPathComponent("Focal")
30 | let kernelPath = distDir.appendingPathComponent("kernel-\(arch)").path
31 | let ramdiskPath = distDir.appendingPathComponent("ramdisk-\(arch)").path
32 | let diskPath = distDir.appendingPathComponent("disk-\(arch)").path
33 | let kernelParams = "console=hvc0 root=/dev/vda"
34 | let memoryAlloc = 2.0
35 | let autoCore = false
36 | let autoMem = false
37 | let coreAlloc = 2.0
38 | let vp = VMParameters(kernelParams: kernelParams, kernelPath: kernelPath, ramdiskPath: ramdiskPath, diskPath: diskPath, memoryAlloc: memoryAlloc, autoCore: autoCore, autoMem: autoMem, coreAlloc: coreAlloc)
39 |
40 | do {
41 | try vm.configure(vp)
42 | try vm.start()
43 | } catch {
44 | os_log(.error, "Something went wrong starting the VM")
45 | return
46 | }
47 | // msg from kernel:
48 | // Check rootdelay= (did the system wait long enough?)
49 | // doesn't need to wait at all on first launch
50 |
51 | }
52 |
53 | func extractKernel(_ dist: Distro, arch: Arch) -> Bool {
54 | if (arch == .x86_64) { return true } // x86_64 kernel doesn't seem to be gzipped
55 | var task = Process()
56 | let distDir = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("VFHost").appendingPathComponent("Focal")
57 | let kernelPath = distDir.appendingPathComponent("kernel-\(arch)").path
58 | installProgress?.becomeCurrent(withPendingUnitCount: 2)
59 | task.launchPath = "/bin/mv"
60 | task.arguments = [kernelPath, kernelPath + ".gz"]
61 | task.launch()
62 | task.waitUntilExit()
63 | if task.terminationStatus != 0 { return false }
64 | installProgress?.resignCurrent()
65 | installProgress?.becomeCurrent(withPendingUnitCount: 2)
66 | task = Process()
67 | task.launchPath = "/usr/bin/gunzip"
68 | task.arguments = [kernelPath + ".gz"]
69 | task.launch()
70 | task.waitUntilExit()
71 | installProgress?.resignCurrent()
72 | if task.terminationStatus != 0 { return false }
73 | return true
74 | }
75 |
76 | func extractDisk(_ dist: Distro, arch: Arch) -> Bool {
77 | var task = Process()
78 | let distDir = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("VFHost").appendingPathComponent(String(describing: dist))
79 | let diskPath = distDir.appendingPathComponent("disk-\(arch)").path
80 | installProgress?.becomeCurrent(withPendingUnitCount: 2)
81 | // Cmd + C
82 | // Cmd + V
83 | task = Process()
84 | task.launchPath = "/usr/bin/tar"
85 | task.arguments = ["xvf", diskPath, "-C", distDir.path]
86 | task.launch()
87 | task.waitUntilExit()
88 | if task.terminationStatus != 0 { return false }
89 |
90 | task = Process()
91 | task.launchPath = "/bin/rm"
92 | task.arguments = [diskPath]
93 | task.launch()
94 | task.waitUntilExit()
95 | if task.terminationStatus != 0 { return false }
96 |
97 | var archString = ""
98 | if (arch == .x86_64) {
99 | archString = "amd64"
100 | } else if (arch == .arm64) {
101 | archString = "arm64"
102 | }
103 |
104 | task = Process()
105 | task.launchPath = "/usr/bin/env"
106 | let emptyPath = distDir.appendingPathComponent("disk-\(String(describing: arch))").path
107 | task.arguments = ["dd", "if=/dev/zero", "of=\(emptyPath)", "bs=1g", "count=8", "conv=notrunc"] // , ">>", diskPath]
108 | task.launch()
109 | task.waitUntilExit()
110 | if task.terminationStatus != 0 { return false }
111 |
112 | let extracted = distDir.appendingPathComponent("\(String(describing: dist).lowercased())-server-cloudimg-\(archString).img").path
113 |
114 | task = Process()
115 | task.launchPath = "/usr/bin/env"
116 | task.arguments = ["dd", "if=\(extracted)", "of=\(emptyPath)", "bs=4m", "conv=notrunc"] // , ">>", diskPath]
117 | task.launch()
118 | task.waitUntilExit()
119 | if task.terminationStatus != 0 { return false }
120 |
121 | installProgress?.resignCurrent()
122 | return true
123 | }
124 |
125 | func stopVM() {
126 | vm.stop()
127 | }
128 |
129 | func detectInstalled() {
130 | let fm = FileManager.default
131 | let ourDir = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("VFHost")
132 |
133 | var installed = [Distro]()
134 |
135 | for dist in Distro.allCases {
136 | let distDir = ourDir.appendingPathComponent(String(describing: dist)).path
137 | var isDir: ObjCBool = false
138 | if fm.fileExists(atPath: distDir, isDirectory: &isDir) {
139 | if isDir.boolValue {
140 | installed.append(dist)
141 | }
142 | }
143 | }
144 |
145 | self.installed = installed
146 | }
147 |
148 | func firstLaunch(_ dist: Distro, arch: Arch) {
149 | installProgress = Progress(totalUnitCount: 20)
150 | switch dist {
151 | case .Focal:
152 | if extractKernel(dist, arch: arch) {
153 | if extractDisk(dist, arch: arch) {
154 | focalFirstLaunch(arch)
155 | }
156 | }
157 | }
158 | }
159 |
160 | func focalFirstLaunch(_ a: Arch) {
161 | installProgress?.becomeCurrent(withPendingUnitCount: 1)
162 | let arch = String(describing: a)
163 | let distDir = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("VFHost").appendingPathComponent("Focal")
164 | let kernelPath = distDir.appendingPathComponent("kernel-\(arch)").path
165 | let ramdiskPath = distDir.appendingPathComponent("ramdisk-\(arch)").path
166 | let diskPath = distDir.appendingPathComponent("disk-\(arch)").path
167 | let kernelParams = "console=hvc0"
168 | let memoryAlloc = 2.0
169 | let autoCore = false
170 | let autoMem = false
171 | let coreAlloc = 2.0
172 | vm = VirtualMachine()
173 | installProgress?.resignCurrent()
174 | installProgress?.becomeCurrent(withPendingUnitCount: 1)
175 |
176 | let vp = VMParameters(kernelParams: kernelParams, kernelPath: kernelPath, ramdiskPath: ramdiskPath, diskPath: diskPath, memoryAlloc: memoryAlloc, autoCore: autoCore, autoMem: autoMem, coreAlloc: coreAlloc)
177 |
178 | do {
179 | try vm.configure(vp)
180 | try vm.start()
181 | } catch {
182 | os_log(.error, "Something went wrong starting the VM")
183 | return
184 | }
185 |
186 | installProgress?.resignCurrent()
187 |
188 | self.vm.startScreen()
189 |
190 | DispatchQueue.global().async {
191 | for _ in 0...5 {
192 | self.installProgress?.becomeCurrent(withPendingUnitCount: 1)
193 | sleep(10)
194 | self.installProgress?.resignCurrent()
195 | }
196 | self.installProgress?.becomeCurrent(withPendingUnitCount: 1)
197 | sleep(5)
198 | self.vm.execute("")
199 | self.vm.execute("")
200 | self.vm.execute("")
201 | self.vm.execute("mkdir /mnt")
202 | self.vm.execute("mount /dev/vda /mnt")
203 | self.vm.execute("chroot /mnt")
204 | self.vm.execute("touch /etc/cloud/cloud-init.disabled")
205 | self.vm.execute("echo 'root:toor' | chpasswd")
206 | self.vm.execute("ssh-keygen -A")
207 | let path = "/etc/netplan/01-dhcp.yaml"
208 | self.vm.execute("echo \"network:\" >> \(path)")
209 | self.vm.execute("echo \" renderer: networkd\" >> \(path)")
210 | self.vm.execute("echo \" version: 2\" >> \(path)")
211 | self.vm.execute("echo \" ethernets:\" >> \(path)")
212 | self.vm.execute("echo \" enp0s1:\" >> \(path)")
213 | self.vm.execute("echo \" dhcp4: true\" >> \(path)")
214 | self.vm.execute("exit")
215 | self.vm.execute("umount /dev/vda")
216 | sleep(5)
217 | self.installProgress?.resignCurrent()
218 | DispatchQueue.main.async {
219 | self.stopVM()
220 |
221 | let vp = VMParameters(kernelParams: "\(kernelParams) root=/dev/vda", kernelPath: kernelPath, ramdiskPath: ramdiskPath, diskPath: diskPath, memoryAlloc: memoryAlloc, autoCore: autoCore, autoMem: autoMem, coreAlloc: coreAlloc)
222 |
223 | do {
224 | try self.vm.configure(vp)
225 | try self.vm.start()
226 | } catch {
227 | os_log(.error, "Something went wrong starting the VM")
228 | return
229 | }
230 |
231 | self.vm.startScreen()
232 | DispatchQueue.global().async {
233 | for _ in 0...5 {
234 | self.installProgress?.becomeCurrent(withPendingUnitCount: 1)
235 | sleep(10)
236 | self.installProgress?.resignCurrent()
237 | }
238 | self.installProgress?.becomeCurrent(withPendingUnitCount: 1)
239 | sleep(5)
240 | self.vm.execute("root")
241 | sleep(1)
242 | self.vm.execute("toor")
243 | sleep(1)
244 | self.vm.execute("resize2fs /dev/vda")
245 | sleep(10)
246 | DispatchQueue.main.async {
247 | self.stopVM()
248 | self.installing = false
249 | self.detectInstalled()
250 | }
251 | }
252 | }
253 | }
254 | }
255 |
256 | func getDistro(_ dist: Distro, arch: Arch) {
257 | guard let path = Bundle.main.path(forResource: "DownloadURLs", ofType: "plist") else { return }
258 | let data = try! Data(contentsOf: URL(fileURLWithPath: path))
259 | let urls = try! PropertyListSerialization.propertyList(from: data, options: .mutableContainers, format: nil) as! [String: [String: String]]
260 | let sessionConfig = URLSessionConfiguration.default
261 | let session = URLSession(configuration: sessionConfig)
262 | let distURLs = urls[String(describing: dist) + " " + String(describing: arch)]!
263 | let fm = FileManager.default
264 | let ourDir = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("VFHost")
265 | let distDir = ourDir.appendingPathComponent(String(describing: dist))
266 |
267 | installProgress = Progress(totalUnitCount: Int64(distURLs.count))
268 |
269 | do {
270 | try fm.createDirectory(at: distDir, withIntermediateDirectories: true, attributes: nil)
271 | } catch {
272 | return
273 | }
274 |
275 | installing = true
276 |
277 | for (type, urlString) in distURLs {
278 | var req = URLRequest(url: URL(string: urlString)!)
279 | req.httpMethod = "GET"
280 | let task = session.dataTask(with: req) { (data, res, error) in
281 | guard let res = res as? HTTPURLResponse else { return }
282 | if res.statusCode != 200 { return }
283 | if let data = data {
284 | do {
285 | try data.write(to: distDir.appendingPathComponent(type + "-" + String(describing: arch)))
286 | } catch {
287 | os_log(.error, "I just couldn't pull it off this time. Sorry guys.")
288 | return
289 |
290 | }
291 | if self.installProgress!.isFinished {
292 | DispatchQueue.main.async {
293 | self.firstLaunch(dist, arch: arch)
294 | }
295 | }
296 | }
297 | }
298 | self.installProgress?.addChild(task.progress, withPendingUnitCount: 1)
299 | task.resume()
300 | }
301 | }
302 |
303 | func rmDistro(_ dist: Distro) {
304 | let fm = FileManager.default
305 | let ourDir = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("VFHost")
306 | let distDir = ourDir.appendingPathComponent(String(describing: dist))
307 | do {
308 | try fm.removeItem(at: distDir)
309 | detectInstalled()
310 | } catch {
311 | os_log(.error, "had trouble removing distro directory")
312 | }
313 | }
314 | }
315 |
316 | // We have one option right now
317 | enum Distro: CaseIterable {
318 | case Focal
319 | }
320 |
321 | enum Arch {
322 | case arm64
323 | case x86_64
324 | }
325 |
--------------------------------------------------------------------------------
/VFHost.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 214D0F5225CC9FC00065636A /* VFHostApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214D0F5125CC9FC00065636A /* VFHostApp.swift */; };
11 | 214D0F5425CC9FC00065636A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214D0F5325CC9FC00065636A /* ContentView.swift */; };
12 | 214D0F5625CC9FC10065636A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 214D0F5525CC9FC10065636A /* Assets.xcassets */; };
13 | 214D0F5925CC9FC10065636A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 214D0F5825CC9FC10065636A /* Preview Assets.xcassets */; };
14 | 214D0F6325CCA1850065636A /* VirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214D0F6225CCA1850065636A /* VirtualMachine.swift */; };
15 | 21F49D3325CE67BB00612858 /* ManagedMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21F49D3225CE67BB00612858 /* ManagedMode.swift */; };
16 | 21F49D3625CE727A00612858 /* DownloadURLs.plist in Resources */ = {isa = PBXBuildFile; fileRef = 21F49D3525CE727A00612858 /* DownloadURLs.plist */; };
17 | /* End PBXBuildFile section */
18 |
19 | /* Begin PBXFileReference section */
20 | 214D0F4E25CC9FC00065636A /* VFHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VFHost.app; sourceTree = BUILT_PRODUCTS_DIR; };
21 | 214D0F5125CC9FC00065636A /* VFHostApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VFHostApp.swift; sourceTree = ""; };
22 | 214D0F5325CC9FC00065636A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
23 | 214D0F5525CC9FC10065636A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
24 | 214D0F5825CC9FC10065636A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
25 | 214D0F5A25CC9FC10065636A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
26 | 214D0F5B25CC9FC10065636A /* VFHost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VFHost.entitlements; sourceTree = ""; };
27 | 214D0F6225CCA1850065636A /* VirtualMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualMachine.swift; sourceTree = ""; };
28 | 21F49D3225CE67BB00612858 /* ManagedMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedMode.swift; sourceTree = ""; };
29 | 21F49D3525CE727A00612858 /* DownloadURLs.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DownloadURLs.plist; sourceTree = ""; };
30 | /* End PBXFileReference section */
31 |
32 | /* Begin PBXFrameworksBuildPhase section */
33 | 214D0F4B25CC9FC00065636A /* Frameworks */ = {
34 | isa = PBXFrameworksBuildPhase;
35 | buildActionMask = 2147483647;
36 | files = (
37 | );
38 | runOnlyForDeploymentPostprocessing = 0;
39 | };
40 | /* End PBXFrameworksBuildPhase section */
41 |
42 | /* Begin PBXGroup section */
43 | 214D0F4525CC9FC00065636A = {
44 | isa = PBXGroup;
45 | children = (
46 | 214D0F5025CC9FC00065636A /* VFHost */,
47 | 214D0F4F25CC9FC00065636A /* Products */,
48 | );
49 | sourceTree = "";
50 | };
51 | 214D0F4F25CC9FC00065636A /* Products */ = {
52 | isa = PBXGroup;
53 | children = (
54 | 214D0F4E25CC9FC00065636A /* VFHost.app */,
55 | );
56 | name = Products;
57 | sourceTree = "";
58 | };
59 | 214D0F5025CC9FC00065636A /* VFHost */ = {
60 | isa = PBXGroup;
61 | children = (
62 | 214D0F5125CC9FC00065636A /* VFHostApp.swift */,
63 | 214D0F5325CC9FC00065636A /* ContentView.swift */,
64 | 214D0F6225CCA1850065636A /* VirtualMachine.swift */,
65 | 21F49D3225CE67BB00612858 /* ManagedMode.swift */,
66 | 21F49D3525CE727A00612858 /* DownloadURLs.plist */,
67 | 214D0F5525CC9FC10065636A /* Assets.xcassets */,
68 | 214D0F5A25CC9FC10065636A /* Info.plist */,
69 | 214D0F5B25CC9FC10065636A /* VFHost.entitlements */,
70 | 214D0F5725CC9FC10065636A /* Preview Content */,
71 | );
72 | path = VFHost;
73 | sourceTree = "";
74 | };
75 | 214D0F5725CC9FC10065636A /* Preview Content */ = {
76 | isa = PBXGroup;
77 | children = (
78 | 214D0F5825CC9FC10065636A /* Preview Assets.xcassets */,
79 | );
80 | path = "Preview Content";
81 | sourceTree = "";
82 | };
83 | /* End PBXGroup section */
84 |
85 | /* Begin PBXNativeTarget section */
86 | 214D0F4D25CC9FC00065636A /* VFHost */ = {
87 | isa = PBXNativeTarget;
88 | buildConfigurationList = 214D0F5E25CC9FC10065636A /* Build configuration list for PBXNativeTarget "VFHost" */;
89 | buildPhases = (
90 | 214D0F4A25CC9FC00065636A /* Sources */,
91 | 214D0F4B25CC9FC00065636A /* Frameworks */,
92 | 214D0F4C25CC9FC00065636A /* Resources */,
93 | );
94 | buildRules = (
95 | );
96 | dependencies = (
97 | );
98 | name = VFHost;
99 | productName = VFHost;
100 | productReference = 214D0F4E25CC9FC00065636A /* VFHost.app */;
101 | productType = "com.apple.product-type.application";
102 | };
103 | /* End PBXNativeTarget section */
104 |
105 | /* Begin PBXProject section */
106 | 214D0F4625CC9FC00065636A /* Project object */ = {
107 | isa = PBXProject;
108 | attributes = {
109 | LastSwiftUpdateCheck = 1240;
110 | LastUpgradeCheck = 1240;
111 | TargetAttributes = {
112 | 214D0F4D25CC9FC00065636A = {
113 | CreatedOnToolsVersion = 12.4;
114 | };
115 | };
116 | };
117 | buildConfigurationList = 214D0F4925CC9FC00065636A /* Build configuration list for PBXProject "VFHost" */;
118 | compatibilityVersion = "Xcode 9.3";
119 | developmentRegion = en;
120 | hasScannedForEncodings = 0;
121 | knownRegions = (
122 | en,
123 | Base,
124 | );
125 | mainGroup = 214D0F4525CC9FC00065636A;
126 | productRefGroup = 214D0F4F25CC9FC00065636A /* Products */;
127 | projectDirPath = "";
128 | projectRoot = "";
129 | targets = (
130 | 214D0F4D25CC9FC00065636A /* VFHost */,
131 | );
132 | };
133 | /* End PBXProject section */
134 |
135 | /* Begin PBXResourcesBuildPhase section */
136 | 214D0F4C25CC9FC00065636A /* Resources */ = {
137 | isa = PBXResourcesBuildPhase;
138 | buildActionMask = 2147483647;
139 | files = (
140 | 21F49D3625CE727A00612858 /* DownloadURLs.plist in Resources */,
141 | 214D0F5925CC9FC10065636A /* Preview Assets.xcassets in Resources */,
142 | 214D0F5625CC9FC10065636A /* Assets.xcassets in Resources */,
143 | );
144 | runOnlyForDeploymentPostprocessing = 0;
145 | };
146 | /* End PBXResourcesBuildPhase section */
147 |
148 | /* Begin PBXSourcesBuildPhase section */
149 | 214D0F4A25CC9FC00065636A /* Sources */ = {
150 | isa = PBXSourcesBuildPhase;
151 | buildActionMask = 2147483647;
152 | files = (
153 | 21F49D3325CE67BB00612858 /* ManagedMode.swift in Sources */,
154 | 214D0F5425CC9FC00065636A /* ContentView.swift in Sources */,
155 | 214D0F6325CCA1850065636A /* VirtualMachine.swift in Sources */,
156 | 214D0F5225CC9FC00065636A /* VFHostApp.swift in Sources */,
157 | );
158 | runOnlyForDeploymentPostprocessing = 0;
159 | };
160 | /* End PBXSourcesBuildPhase section */
161 |
162 | /* Begin XCBuildConfiguration section */
163 | 214D0F5C25CC9FC10065636A /* Debug */ = {
164 | isa = XCBuildConfiguration;
165 | buildSettings = {
166 | ALWAYS_SEARCH_USER_PATHS = NO;
167 | CLANG_ANALYZER_NONNULL = YES;
168 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
169 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
170 | CLANG_CXX_LIBRARY = "libc++";
171 | CLANG_ENABLE_MODULES = YES;
172 | CLANG_ENABLE_OBJC_ARC = YES;
173 | CLANG_ENABLE_OBJC_WEAK = YES;
174 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
175 | CLANG_WARN_BOOL_CONVERSION = YES;
176 | CLANG_WARN_COMMA = YES;
177 | CLANG_WARN_CONSTANT_CONVERSION = YES;
178 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
179 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
180 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
181 | CLANG_WARN_EMPTY_BODY = YES;
182 | CLANG_WARN_ENUM_CONVERSION = YES;
183 | CLANG_WARN_INFINITE_RECURSION = YES;
184 | CLANG_WARN_INT_CONVERSION = YES;
185 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
186 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
187 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
188 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
189 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
190 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
191 | CLANG_WARN_STRICT_PROTOTYPES = YES;
192 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
193 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
194 | CLANG_WARN_UNREACHABLE_CODE = YES;
195 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
196 | COPY_PHASE_STRIP = NO;
197 | DEBUG_INFORMATION_FORMAT = dwarf;
198 | ENABLE_STRICT_OBJC_MSGSEND = YES;
199 | ENABLE_TESTABILITY = YES;
200 | GCC_C_LANGUAGE_STANDARD = gnu11;
201 | GCC_DYNAMIC_NO_PIC = NO;
202 | GCC_NO_COMMON_BLOCKS = YES;
203 | GCC_OPTIMIZATION_LEVEL = 0;
204 | GCC_PREPROCESSOR_DEFINITIONS = (
205 | "DEBUG=1",
206 | "$(inherited)",
207 | );
208 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
209 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
210 | GCC_WARN_UNDECLARED_SELECTOR = YES;
211 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
212 | GCC_WARN_UNUSED_FUNCTION = YES;
213 | GCC_WARN_UNUSED_VARIABLE = YES;
214 | MACOSX_DEPLOYMENT_TARGET = 11.1;
215 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
216 | MTL_FAST_MATH = YES;
217 | ONLY_ACTIVE_ARCH = YES;
218 | SDKROOT = macosx;
219 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
220 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
221 | };
222 | name = Debug;
223 | };
224 | 214D0F5D25CC9FC10065636A /* Release */ = {
225 | isa = XCBuildConfiguration;
226 | buildSettings = {
227 | ALWAYS_SEARCH_USER_PATHS = NO;
228 | CLANG_ANALYZER_NONNULL = YES;
229 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
230 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
231 | CLANG_CXX_LIBRARY = "libc++";
232 | CLANG_ENABLE_MODULES = YES;
233 | CLANG_ENABLE_OBJC_ARC = YES;
234 | CLANG_ENABLE_OBJC_WEAK = YES;
235 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
236 | CLANG_WARN_BOOL_CONVERSION = YES;
237 | CLANG_WARN_COMMA = YES;
238 | CLANG_WARN_CONSTANT_CONVERSION = YES;
239 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
240 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
241 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
242 | CLANG_WARN_EMPTY_BODY = YES;
243 | CLANG_WARN_ENUM_CONVERSION = YES;
244 | CLANG_WARN_INFINITE_RECURSION = YES;
245 | CLANG_WARN_INT_CONVERSION = YES;
246 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
247 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
248 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
249 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
250 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
251 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
252 | CLANG_WARN_STRICT_PROTOTYPES = YES;
253 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
254 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
255 | CLANG_WARN_UNREACHABLE_CODE = YES;
256 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
257 | COPY_PHASE_STRIP = NO;
258 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
259 | ENABLE_NS_ASSERTIONS = NO;
260 | ENABLE_STRICT_OBJC_MSGSEND = YES;
261 | GCC_C_LANGUAGE_STANDARD = gnu11;
262 | GCC_NO_COMMON_BLOCKS = YES;
263 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
264 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
265 | GCC_WARN_UNDECLARED_SELECTOR = YES;
266 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
267 | GCC_WARN_UNUSED_FUNCTION = YES;
268 | GCC_WARN_UNUSED_VARIABLE = YES;
269 | MACOSX_DEPLOYMENT_TARGET = 11.1;
270 | MTL_ENABLE_DEBUG_INFO = NO;
271 | MTL_FAST_MATH = YES;
272 | SDKROOT = macosx;
273 | SWIFT_COMPILATION_MODE = wholemodule;
274 | SWIFT_OPTIMIZATION_LEVEL = "-O";
275 | };
276 | name = Release;
277 | };
278 | 214D0F5F25CC9FC10065636A /* Debug */ = {
279 | isa = XCBuildConfiguration;
280 | buildSettings = {
281 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
282 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
283 | CODE_SIGN_ENTITLEMENTS = VFHost/VFHost.entitlements;
284 | CODE_SIGN_IDENTITY = "Apple Development";
285 | CODE_SIGN_STYLE = Automatic;
286 | COMBINE_HIDPI_IMAGES = YES;
287 | CURRENT_PROJECT_VERSION = 15;
288 | DEVELOPMENT_ASSET_PATHS = "\"VFHost/Preview Content\"";
289 | DEVELOPMENT_TEAM = BE6Q2675KP;
290 | ENABLE_HARDENED_RUNTIME = YES;
291 | ENABLE_PREVIEWS = YES;
292 | INFOPLIST_FILE = VFHost/Info.plist;
293 | LD_RUNPATH_SEARCH_PATHS = (
294 | "$(inherited)",
295 | "@executable_path/../Frameworks",
296 | );
297 | MACOSX_DEPLOYMENT_TARGET = 11.0;
298 | MARKETING_VERSION = 0.3;
299 | PRODUCT_BUNDLE_IDENTIFIER = lol.jds.VFHost;
300 | PRODUCT_NAME = "$(TARGET_NAME)";
301 | SWIFT_VERSION = 5.0;
302 | };
303 | name = Debug;
304 | };
305 | 214D0F6025CC9FC10065636A /* Release */ = {
306 | isa = XCBuildConfiguration;
307 | buildSettings = {
308 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
309 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
310 | CODE_SIGN_ENTITLEMENTS = VFHost/VFHost.entitlements;
311 | CODE_SIGN_IDENTITY = "Apple Development";
312 | CODE_SIGN_STYLE = Automatic;
313 | COMBINE_HIDPI_IMAGES = YES;
314 | CURRENT_PROJECT_VERSION = 15;
315 | DEVELOPMENT_ASSET_PATHS = "\"VFHost/Preview Content\"";
316 | DEVELOPMENT_TEAM = BE6Q2675KP;
317 | ENABLE_HARDENED_RUNTIME = YES;
318 | ENABLE_PREVIEWS = YES;
319 | INFOPLIST_FILE = VFHost/Info.plist;
320 | LD_RUNPATH_SEARCH_PATHS = (
321 | "$(inherited)",
322 | "@executable_path/../Frameworks",
323 | );
324 | MACOSX_DEPLOYMENT_TARGET = 11.0;
325 | MARKETING_VERSION = 0.3;
326 | PRODUCT_BUNDLE_IDENTIFIER = lol.jds.VFHost;
327 | PRODUCT_NAME = "$(TARGET_NAME)";
328 | SWIFT_VERSION = 5.0;
329 | };
330 | name = Release;
331 | };
332 | /* End XCBuildConfiguration section */
333 |
334 | /* Begin XCConfigurationList section */
335 | 214D0F4925CC9FC00065636A /* Build configuration list for PBXProject "VFHost" */ = {
336 | isa = XCConfigurationList;
337 | buildConfigurations = (
338 | 214D0F5C25CC9FC10065636A /* Debug */,
339 | 214D0F5D25CC9FC10065636A /* Release */,
340 | );
341 | defaultConfigurationIsVisible = 0;
342 | defaultConfigurationName = Release;
343 | };
344 | 214D0F5E25CC9FC10065636A /* Build configuration list for PBXNativeTarget "VFHost" */ = {
345 | isa = XCConfigurationList;
346 | buildConfigurations = (
347 | 214D0F5F25CC9FC10065636A /* Debug */,
348 | 214D0F6025CC9FC10065636A /* Release */,
349 | );
350 | defaultConfigurationIsVisible = 0;
351 | defaultConfigurationName = Release;
352 | };
353 | /* End XCConfigurationList section */
354 | };
355 | rootObject = 214D0F4625CC9FC00065636A /* Project object */;
356 | }
357 |
--------------------------------------------------------------------------------
/VFHost/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // VFHost
4 | //
5 | // Created by Jack Steele on 2/4/21.
6 | //
7 |
8 | import SwiftUI
9 | import Virtualization
10 |
11 | struct ContentView: View {
12 | @ObservedObject var VM = VirtualMachine()
13 | @ObservedObject var MM = ManagedMode()
14 |
15 | let paramLimits = ParameterLimits()
16 | @EnvironmentObject var appDelegate: AppDelegate
17 | @State var windowDelegate = WindowDelegate()
18 |
19 | @State private var window: NSWindow?
20 | @State var installProgress = 0.0
21 | @State var errorShown = false
22 | @State var killConfirmationShown = false
23 | @State var killCancelled = false
24 | @State var uninstallConfirmationShown = false
25 | @State var errorMessage = ""
26 | @State var started = false
27 | @State var managed = true
28 | @State var height = 300
29 | @State var installed: [Distro?] = []
30 |
31 | @StateObject var params = UIParameters()
32 |
33 | let timer = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()
34 |
35 | func loadData() {
36 | if let def = UserDefaults.standard.string(forKey: "kernelPath") {
37 | params.kernelPath = def
38 | }
39 | if let def = UserDefaults.standard.string(forKey: "ramdiskPath") {
40 | params.ramdiskPath = def
41 | }
42 | if let def = UserDefaults.standard.string(forKey: "diskPath") {
43 | params.diskPath = def
44 | }
45 | if let def = UserDefaults.standard.string(forKey: "kernelParams") {
46 | params.kernelParams = def
47 | }
48 | let def = UserDefaults.standard.object(forKey: "managed") as? Bool
49 | if let def = def {
50 | self.managed = def
51 | } else {
52 | self.managed = true
53 | }
54 | }
55 |
56 | var body: some View {
57 | VStack {
58 | HStack {
59 | Spacer()
60 | if killConfirmationShown {
61 | Text("VM stopping").font(.largeTitle)
62 | } else {
63 | Text(started ? "VM running" : "VM stopped").font(.largeTitle)
64 | }
65 | Spacer()
66 |
67 | Toggle(isOn: $started) {
68 | Text("")
69 | }
70 | .onChange(of: started, perform: { running in
71 | if killCancelled {
72 | killCancelled = false
73 | return
74 | }
75 | if running {
76 | if managed {
77 | appDelegate.canTerminate = false
78 | windowDelegate.canTerminate = false
79 | if let distro = MM.installed.first {
80 | MM.startVM(distro!)
81 | MM.vm.startScreen()
82 | MM.vm.attachScreen()
83 | }
84 | } else {
85 | appDelegate.canTerminate = false
86 | windowDelegate.canTerminate = false
87 | if validateParams() {
88 | saveDefaults()
89 | startVM()
90 | }
91 | }
92 | } else {
93 | self.killConfirmationShown.toggle()
94 | }
95 | })
96 | .toggleStyle(SwitchToggleStyle())
97 | .disabled(managed ? (MM.installed.count == 0) : false)
98 | .alert(isPresented: $killConfirmationShown, content: {
99 | Alert(title: Text("Really stop this VM?"), primaryButton: .default(Text("Keep running"), action: {
100 | self.killCancelled = true
101 | self.started = true
102 | }), secondaryButton: .destructive(Text("Stop VM"), action: {
103 | kill()
104 | }))
105 | })
106 | }
107 | Divider()
108 | if managed {
109 | Spacer()
110 | VStack {
111 | if MM.installed.count > 0 {
112 | Spacer()
113 | Text("Linux is installed.")
114 | .font(.title)
115 | .padding()
116 | Text("root : toor")
117 | .font(.body)
118 | .padding(.horizontal)
119 | Spacer()
120 | Text("Something wrong?")
121 | .font(.footnote)
122 | Button("Uninstall Ubuntu Focal") {
123 | self.uninstallConfirmationShown.toggle()
124 | }
125 | .alert(isPresented: $uninstallConfirmationShown, content: {
126 | Alert(title: Text("Uninstall?"),
127 | message: Text("Are you sure you'd like to uninstall this VM?"),
128 | primaryButton: .cancel(Text("Don't do it!"), action: {
129 | }), secondaryButton: .destructive(Text("Uninstall"), action: {
130 | for distro in MM.installed {
131 | MM.rmDistro(distro!)
132 | }
133 | }))
134 | })
135 | .disabled(started)
136 | .font(.footnote)
137 | } else {
138 | Text(MM.installing ? "Linux is installing." : "Linux is not installed.")
139 | .font(.title)
140 | .padding()
141 | Text("Installation requires about 10GB of space.")
142 | .font(.title2)
143 | .padding()
144 | Text("\(String(describing: MM.getArch())) Mac detected.")
145 | .font(.title2)
146 | .padding()
147 | Button("Install Ubuntu Focal") {
148 | MM.getDistro(.Focal, arch: MM.getArch())
149 | }
150 | .disabled(MM.installing)
151 | .padding()
152 |
153 | if MM.installing {
154 | ProgressView(value: installProgress)
155 | .padding()
156 | .onReceive(timer, perform: { _ in
157 | // had trouble observing MM.installProgress for some reason
158 | // went with this dirty timer instead
159 | // kinda sucks
160 | if let dp = MM.installProgress {
161 | installProgress = dp.fractionCompleted
162 | }
163 | })
164 | }
165 | }
166 | }.onAppear(perform: {
167 | MM.detectInstalled()
168 | })
169 | Spacer()
170 | } else {
171 | Form {
172 | Text("Kernel path")
173 | HStack {
174 | TextField("~/distribution/vmlinuz", text: $params.kernelPath)
175 | Button("Select file") {
176 | params.kernelPath = openFile(kind: "kernel")
177 | }
178 | }
179 |
180 | Text("Ramdisk path")
181 | HStack {
182 | TextField("~/distribution/initrd", text: $params.ramdiskPath)
183 | Button("Select file") {
184 | params.ramdiskPath = openFile(kind: "ramdisk")
185 | }
186 | }
187 |
188 | Text("Disk image path")
189 | HStack {
190 | TextField("~/distribution/disk.img", text: $params.diskPath)
191 | Button("Select file") {
192 | params.diskPath = openFile(kind: "disk image")
193 | }
194 | }
195 |
196 | Text("Kernel parameters")
197 | HStack {
198 | TextField("console=hvc0", text: $params.kernelParams)
199 | }
200 | }
201 | .alert(isPresented: $errorShown, content: {
202 | Alert(title: Text(errorMessage))
203 | })
204 | .disabled(started)
205 | .padding()
206 |
207 | Form {
208 | HStack {
209 | Text("CPU cores allocated")
210 | Toggle(isOn: $params.autoCore, label: {
211 | Text("Auto")
212 | })
213 | Spacer()
214 | Text(params.autoCore ? "" : "\(Int(params.coreAlloc)) core(s)")
215 | }
216 |
217 | Slider(value: $params.coreAlloc,
218 | in: paramLimits.minCores...paramLimits.maxCores,
219 | step: 1
220 | )
221 | .padding(.horizontal, 10)
222 | .disabled(params.autoCore)
223 |
224 | HStack {
225 | Text("Memory allocated")
226 | Toggle(isOn: $params.autoMem, label: {
227 | Text("Auto")
228 | })
229 | Spacer()
230 | Text(params.autoMem ? "" : String(format: "%.2f GB", params.memoryAlloc))
231 | }
232 |
233 | Slider(value: $params.memoryAlloc,
234 | in: paramLimits.minMem...paramLimits.maxMem,
235 | step: 0.5)
236 | .padding(.horizontal, 10)
237 | .disabled(params.autoMem)
238 | }
239 | .disabled(started)
240 | .padding()
241 | }
242 |
243 | /// Bottom bit
244 | Divider()
245 |
246 | HStack {
247 | Button("Reconnect") {
248 | if managed {
249 | MM.vm.attachScreen()
250 | } else {
251 | VM.connect()
252 | }
253 | }
254 | .disabled(!started)
255 | Spacer()
256 | Toggle(isOn: $managed, label: {
257 | Text("Managed mode")
258 | })
259 | .onChange(of: managed, perform: { _ in
260 | UserDefaults.standard.set(self.managed, forKey: "managed")
261 | })
262 | .disabled(MM.installing)
263 | .disabled(started)
264 | }
265 | .padding()
266 | }
267 | .padding()
268 | .frame(minWidth: 300, idealWidth: 500, maxWidth: .infinity, minHeight: 550, idealHeight: 550, maxHeight: .infinity, alignment: .center)
269 | .onAppear {
270 | loadData()
271 | }
272 | .background(WindowAccessor(window: self.$window, windowDelegate: self.$windowDelegate))
273 | // .alert(isPresented: Binding(get: { appDelegate.shouldTerminate ? self.started : false }, set: { appDelegate.shouldTerminate = $0 }), content: {
274 | // Alert(title: Text("Quit requested."),
275 | // message: Text("Do you really want to quit while the VM is running?"),
276 | // primaryButton: .default(Text("Don't quit!"), action: {
277 | // self.appDelegate.noQuit()
278 | // }),
279 | // secondaryButton: .destructive(Text("Quit"), action: {
280 | // self.appDelegate.quit()
281 | // }))
282 | // })
283 | }
284 |
285 | func kill() {
286 | if managed {
287 | appDelegate.canTerminate = true
288 | windowDelegate.canTerminate = true
289 | MM.stopVM()
290 | } else {
291 | appDelegate.canTerminate = true
292 | windowDelegate.canTerminate = true
293 | VM.stop()
294 | }
295 | }
296 |
297 | func killUnmanaged() {
298 |
299 | }
300 |
301 | func saveDefaults() {
302 | UserDefaults.standard.set(params.ramdiskPath, forKey: "ramdiskPath")
303 | UserDefaults.standard.set(params.kernelPath, forKey: "kernelPath")
304 | UserDefaults.standard.set(params.diskPath, forKey: "diskPath")
305 | UserDefaults.standard.set(params.kernelParams, forKey: "kernelParams")
306 | }
307 |
308 | func validateParams() -> Bool {
309 | guard params.kernelPath != "" else {
310 | started = false
311 | errorMessage = "Missing kernel path."
312 | errorShown = true
313 | return false
314 | }
315 | guard params.ramdiskPath != "" else {
316 | started = false
317 | errorMessage = "Missing ramdisk path."
318 | errorShown = true
319 | return false
320 | }
321 | guard params.diskPath != "" else {
322 | started = false
323 | errorMessage = "Missing disk path."
324 | errorShown = true
325 | return false
326 | }
327 | return true
328 | }
329 |
330 | func startVM() {
331 | do {
332 | let vp = VMParameters(kernelParams: params.kernelParams, kernelPath: params.kernelPath, ramdiskPath: params.ramdiskPath, diskPath: params.diskPath, memoryAlloc: params.memoryAlloc, autoCore: params.autoCore, autoMem: params.autoMem, coreAlloc: params.coreAlloc)
333 | try VM.configure(vp)
334 | try VM.start()
335 | VM.connect()
336 | } catch {
337 | started.toggle()
338 | }
339 | }
340 |
341 | func termPerms() -> Bool {
342 | let script = "tell application \"Terminal\" to activate"
343 | let applescript = NSAppleScript(source: script)
344 | var error: NSDictionary?
345 | applescript?.executeAndReturnError(&error)
346 | if let _ = error {
347 | return false
348 | }
349 | return true
350 | }
351 |
352 | func openFile(kind: String) -> String {
353 | let dialog = NSOpenPanel()
354 | dialog.title = "Select your \(kind)"
355 | dialog.allowsMultipleSelection = false
356 | dialog.canChooseDirectories = false
357 | dialog.showsResizeIndicator = true
358 |
359 | if dialog.runModal() == NSApplication.ModalResponse.OK {
360 | if let url = dialog.url {
361 | return url.path
362 | }
363 | }
364 | return ""
365 | }
366 | }
367 |
368 | struct WindowAccessor: NSViewRepresentable {
369 | @Binding var window: NSWindow?
370 | @Binding var windowDelegate: WindowDelegate
371 |
372 | func makeNSView(context: Context) -> some NSView {
373 | let view = NSView()
374 | DispatchQueue.main.async {
375 | view.window?.delegate = self.windowDelegate
376 | self.window = view.window
377 | }
378 | return view
379 | }
380 |
381 | func updateNSView(_ nsView: NSViewType, context: Context) {
382 |
383 | }
384 | }
385 |
386 | class WindowDelegate: NSObject, NSWindowDelegate {
387 | var canTerminate = true
388 |
389 | func windowShouldClose(_ sender: NSWindow) -> Bool {
390 | NSApplication.shared.hide(sender)
391 | return false
392 |
393 | // if canTerminate {
394 | // return true
395 | // } else {
396 | // let alert = NSAlert()
397 | // alert.messageText = "Really quit?"
398 | // alert.informativeText = "You're about to close VFHost with a VM running. Is this what you want?"
399 | // alert.addButton(withTitle: "No, don't quit.")
400 | // alert.addButton(withTitle: "Yes, quit.")
401 | // alert.alertStyle = .critical
402 | // let res = alert.runModal()
403 | // if res == .alertFirstButtonReturn {
404 | // return false
405 | // } else if res == .alertSecondButtonReturn {
406 | // // I feel bad for this
407 | // // sincerely
408 | // exit(0)
409 | //// return true
410 | // }
411 | // return true
412 | // }
413 | }
414 | }
415 |
416 | struct ParameterLimits {
417 | // (10
418 | let minMem = Double(VZVirtualMachineConfiguration.minimumAllowedMemorySize/(1073741824)) + 0.5
419 | let maxMem = Double(VZVirtualMachineConfiguration.maximumAllowedMemorySize/(1073741824))
420 | let memRange = Double((VZVirtualMachineConfiguration.maximumAllowedMemorySize - VZVirtualMachineConfiguration.minimumAllowedMemorySize)/(1073741824))
421 | let minCores = Double(VZVirtualMachineConfiguration.minimumAllowedCPUCount)
422 | let maxCores = Double(VZVirtualMachineConfiguration.maximumAllowedCPUCount)
423 | let coreRange = Double(VZVirtualMachineConfiguration.maximumAllowedCPUCount - VZVirtualMachineConfiguration.minimumAllowedCPUCount)
424 | }
425 |
426 | class UIParameters: NSObject, ObservableObject {
427 | @Published var kernelParams = "console=hvc0"
428 | @Published var kernelPath = ""
429 | @Published var ramdiskPath = ""
430 | @Published var diskPath = ""
431 | // in GB - very lazy
432 | @Published var memoryAlloc: Double = Double(VZVirtualMachineConfiguration.minimumAllowedMemorySize/(1024*1024*1024)) + 0.5
433 | @Published var autoCore = true
434 | @Published var autoMem = true
435 | @Published var coreAlloc: Double = Double(VZVirtualMachineConfiguration.minimumAllowedCPUCount)
436 | }
437 |
438 | struct ContentView_Previews: PreviewProvider {
439 | static var previews: some View {
440 | ContentView()
441 | }
442 | }
443 |
--------------------------------------------------------------------------------