├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── VFHost.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── jds.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── VFHost ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon-1024.png │ │ ├── icon-128.png │ │ ├── icon-16.png │ │ ├── icon-256.png │ │ ├── icon-32.png │ │ ├── icon-512.png │ │ └── icon-64.png │ └── Contents.json ├── ContentView.swift ├── DownloadURLs.plist ├── Info.plist ├── ManagedMode.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── VFHost.entitlements ├── VFHostApp.swift └── VirtualMachine.swift └── header.png /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata/ 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ![Image with icon and text "VFHost.app"](https://github.com/JackSteele/VFHost/raw/main/header.png) 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.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.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /VFHost/Assets.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackSteele/VFHost/dc1ee8b61d98e9a0aafaaec6b0e9c5dde4b300ac/VFHost/Assets.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /VFHost/Assets.xcassets/AppIcon.appiconset/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackSteele/VFHost/dc1ee8b61d98e9a0aafaaec6b0e9c5dde4b300ac/VFHost/Assets.xcassets/AppIcon.appiconset/icon-128.png -------------------------------------------------------------------------------- /VFHost/Assets.xcassets/AppIcon.appiconset/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackSteele/VFHost/dc1ee8b61d98e9a0aafaaec6b0e9c5dde4b300ac/VFHost/Assets.xcassets/AppIcon.appiconset/icon-16.png -------------------------------------------------------------------------------- /VFHost/Assets.xcassets/AppIcon.appiconset/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackSteele/VFHost/dc1ee8b61d98e9a0aafaaec6b0e9c5dde4b300ac/VFHost/Assets.xcassets/AppIcon.appiconset/icon-256.png -------------------------------------------------------------------------------- /VFHost/Assets.xcassets/AppIcon.appiconset/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackSteele/VFHost/dc1ee8b61d98e9a0aafaaec6b0e9c5dde4b300ac/VFHost/Assets.xcassets/AppIcon.appiconset/icon-32.png -------------------------------------------------------------------------------- /VFHost/Assets.xcassets/AppIcon.appiconset/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackSteele/VFHost/dc1ee8b61d98e9a0aafaaec6b0e9c5dde4b300ac/VFHost/Assets.xcassets/AppIcon.appiconset/icon-512.png -------------------------------------------------------------------------------- /VFHost/Assets.xcassets/AppIcon.appiconset/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackSteele/VFHost/dc1ee8b61d98e9a0aafaaec6b0e9c5dde4b300ac/VFHost/Assets.xcassets/AppIcon.appiconset/icon-64.png -------------------------------------------------------------------------------- /VFHost/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackSteele/VFHost/dc1ee8b61d98e9a0aafaaec6b0e9c5dde4b300ac/header.png --------------------------------------------------------------------------------