From 19e12c10f78e959945d63aeb5b4401844f962995 Mon Sep 17 00:00:00 2001 From: * <*> Date: Sat, 12 Oct 2024 06:21:55 +0200 Subject: [PATCH] Full rewrite using appkit instead of swift ui now supports installing multiple vms --- virtualOS.xcodeproj/project.pbxproj | 501 +------ .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/xcschemes/virtualOS.xcscheme | 40 +- virtualOS/.gitignore | 1 + virtualOS/AppDelegate.swift | 26 + .../OperatingSystemVersion+String.swift | 18 + virtualOS/Extension/URL+Paths.swift | 36 +- .../Extension/UserDefaults+Settings.swift | 4 +- virtualOS/Info.plist | 8 - virtualOS/Model/ApplicationDelegate.swift | 18 - virtualOS/Model/Bookmark.swift | 49 +- virtualOS/Model/Bundle.swift | 18 + virtualOS/Model/Constants.swift | 14 + virtualOS/Model/FileModel.swift | 56 + virtualOS/Model/MainViewModel.swift | 63 + .../Model/ParametersViewDataSource.swift | 55 + virtualOS/Model/ParametersViewDelegate.swift | 32 + virtualOS/Model/TableViewDataSource.swift | 33 + virtualOS/Model/TextFieldDelegate.swift | 25 + virtualOS/Model/ViewModel.swift | 355 ----- virtualOS/Model/VirtualMac.swift | 389 ----- virtualOS/Model/VirtualMacConfiguration.swift | 206 --- .../Preview Assets.xcassets/Contents.json | 6 - .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 66 +- .../virtualOS-macOS-1024.png | Bin .../virtualOS-macOS-128.png | Bin .../AppIcon.appiconset/virtualOS-macOS-16.png | Bin .../virtualOS-macOS-256.png | Bin .../AppIcon.appiconset/virtualOS-macOS-32.png | Bin .../virtualOS-macOS-512.png | Bin .../AppIcon.appiconset/virtualOS-macOS-64.png | Bin .../Assets.xcassets/Contents.json | 0 .../Resources/Base.lproj/Main.storyboard | 1252 +++++++++++++++++ .../{ => Resources}/virtualOS.entitlements | 4 +- .../RestoreImage/RestoreImageDownload.swift | 106 ++ .../RestoreImage/RestoreImageInstall.swift | 257 ++++ virtualOS/VM/MacPlatformConfiguration.swift | 110 ++ virtualOS/VM/VMConfiguration.swift | 151 ++ virtualOS/VM/VMParameters.swift | 78 + virtualOS/View/ConfigurationView.swift | 202 --- virtualOS/View/MainView.swift | 58 - virtualOS/View/MenuCommands.swift | 55 - virtualOS/View/SettingsView.swift | 180 --- virtualOS/View/VirtualMachineView.swift | 24 - .../ViewController/MainViewController.swift | 341 +++++ .../ProgressViewController.swift | 89 ++ .../RestoreImageViewController.swift | 107 ++ .../SettingsViewController.swift | 14 + .../ViewController/VMViewController.swift | 122 ++ virtualOS/virtualOSApp.swift | 71 - virtualOSTests/virtualOSTests.swift | 36 - virtualOSUITests/virtualOSUITests.swift | 41 - .../virtualOSUITestsLaunchTests.swift | 32 - 54 files changed, 3123 insertions(+), 2234 deletions(-) delete mode 100644 virtualOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 virtualOS/.gitignore create mode 100644 virtualOS/AppDelegate.swift create mode 100644 virtualOS/Extension/OperatingSystemVersion+String.swift delete mode 100644 virtualOS/Info.plist delete mode 100644 virtualOS/Model/ApplicationDelegate.swift create mode 100644 virtualOS/Model/Bundle.swift create mode 100644 virtualOS/Model/Constants.swift create mode 100644 virtualOS/Model/FileModel.swift create mode 100644 virtualOS/Model/MainViewModel.swift create mode 100644 virtualOS/Model/ParametersViewDataSource.swift create mode 100644 virtualOS/Model/ParametersViewDelegate.swift create mode 100644 virtualOS/Model/TableViewDataSource.swift create mode 100644 virtualOS/Model/TextFieldDelegate.swift delete mode 100644 virtualOS/Model/ViewModel.swift delete mode 100644 virtualOS/Model/VirtualMac.swift delete mode 100644 virtualOS/Model/VirtualMacConfiguration.swift delete mode 100644 virtualOS/Preview Content/Preview Assets.xcassets/Contents.json rename virtualOS/{ => Resources}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename virtualOS/{ => Resources}/Assets.xcassets/AppIcon.appiconset/Contents.json (62%) rename virtualOS/{ => Resources}/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-1024.png (100%) rename virtualOS/{ => Resources}/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-128.png (100%) rename virtualOS/{ => Resources}/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-16.png (100%) rename virtualOS/{ => Resources}/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-256.png (100%) rename virtualOS/{ => Resources}/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-32.png (100%) rename virtualOS/{ => Resources}/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-512.png (100%) rename virtualOS/{ => Resources}/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-64.png (100%) rename virtualOS/{ => Resources}/Assets.xcassets/Contents.json (100%) create mode 100644 virtualOS/Resources/Base.lproj/Main.storyboard rename virtualOS/{ => Resources}/virtualOS.entitlements (100%) create mode 100644 virtualOS/RestoreImage/RestoreImageDownload.swift create mode 100644 virtualOS/RestoreImage/RestoreImageInstall.swift create mode 100644 virtualOS/VM/MacPlatformConfiguration.swift create mode 100644 virtualOS/VM/VMConfiguration.swift create mode 100644 virtualOS/VM/VMParameters.swift delete mode 100644 virtualOS/View/ConfigurationView.swift delete mode 100644 virtualOS/View/MainView.swift delete mode 100644 virtualOS/View/MenuCommands.swift delete mode 100644 virtualOS/View/SettingsView.swift delete mode 100644 virtualOS/View/VirtualMachineView.swift create mode 100644 virtualOS/ViewController/MainViewController.swift create mode 100644 virtualOS/ViewController/ProgressViewController.swift create mode 100644 virtualOS/ViewController/RestoreImageViewController.swift create mode 100644 virtualOS/ViewController/SettingsViewController.swift create mode 100644 virtualOS/ViewController/VMViewController.swift delete mode 100644 virtualOS/virtualOSApp.swift delete mode 100644 virtualOSTests/virtualOSTests.swift delete mode 100644 virtualOSUITests/virtualOSUITests.swift delete mode 100644 virtualOSUITests/virtualOSUITestsLaunchTests.swift diff --git a/virtualOS.xcodeproj/project.pbxproj b/virtualOS.xcodeproj/project.pbxproj index 5b30125..7c06677 100644 --- a/virtualOS.xcodeproj/project.pbxproj +++ b/virtualOS.xcodeproj/project.pbxproj @@ -3,93 +3,23 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 77; objects = { -/* Begin PBXBuildFile section */ - 0005A77A27E2809E0013BE83 /* VirtualMachineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0005A77927E2809E0013BE83 /* VirtualMachineView.swift */; }; - 002E64922871B2DD00CE95A0 /* UserDefaults+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002E64912871B2DD00CE95A0 /* UserDefaults+Settings.swift */; }; - 0044A65527F601E60007988A /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0044A65427F601E60007988A /* ViewModel.swift */; }; - 0044A65A27F76BD30007988A /* URL+Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0044A65927F76BD30007988A /* URL+Paths.swift */; }; - 006504E727F9D59300723BCA /* ConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 006504E627F9D59300723BCA /* ConfigurationView.swift */; }; - 007987AF27E2487200960D74 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 007987AE27E2487200960D74 /* LICENSE */; }; - 007987B127E24A8400960D74 /* VirtualMac.swift in Sources */ = {isa = PBXBuildFile; fileRef = 007987B027E24A8400960D74 /* VirtualMac.swift */; }; - 0090AF6127E25F6F0077D35F /* UInt64+Byte.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0090AF6027E25F6F0077D35F /* UInt64+Byte.swift */; }; - 00989C6427E2340C0048776B /* virtualOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00989C6327E2340C0048776B /* virtualOSApp.swift */; }; - 00989C6827E2340D0048776B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 00989C6727E2340D0048776B /* Assets.xcassets */; }; - 00989C6B27E2340D0048776B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 00989C6A27E2340D0048776B /* Preview Assets.xcassets */; }; - 00989C7627E2340D0048776B /* virtualOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00989C7527E2340D0048776B /* virtualOSTests.swift */; }; - 00989C8027E2340D0048776B /* virtualOSUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00989C7F27E2340D0048776B /* virtualOSUITests.swift */; }; - 00989C8227E2340D0048776B /* virtualOSUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00989C8127E2340D0048776B /* virtualOSUITestsLaunchTests.swift */; }; - 00989C9627E236A10048776B /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00989C9427E236A10048776B /* MainView.swift */; }; - 00989C9A27E238930048776B /* VirtualMacConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00989C9927E238930048776B /* VirtualMacConfiguration.swift */; }; - 00A4FFE8283E3D6F004DD9B3 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A4FFE7283E3D6F004DD9B3 /* SettingsView.swift */; }; - 0114C02629AA2416004159AF /* MenuCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0114C02529AA2416004159AF /* MenuCommands.swift */; }; - 01B042F229CD9F6A003CD5C2 /* Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B042F129CD9F6A003CD5C2 /* Bookmark.swift */; }; - 01FCAD8429AB707C00F12689 /* ApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01FCAD8329AB707C00F12689 /* ApplicationDelegate.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 00989C7227E2340D0048776B /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 00989C5827E2340C0048776B /* Project object */; - proxyType = 1; - remoteGlobalIDString = 00989C5F27E2340C0048776B; - remoteInfo = virtualOS; - }; - 00989C7C27E2340D0048776B /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 00989C5827E2340C0048776B /* Project object */; - proxyType = 1; - remoteGlobalIDString = 00989C5F27E2340C0048776B; - remoteInfo = virtualOS; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXFileReference section */ - 0005A77927E2809E0013BE83 /* VirtualMachineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualMachineView.swift; sourceTree = ""; }; - 002E64912871B2DD00CE95A0 /* UserDefaults+Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Settings.swift"; sourceTree = ""; }; - 0044A65427F601E60007988A /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = ""; }; - 0044A65927F76BD30007988A /* URL+Paths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Paths.swift"; sourceTree = ""; }; - 006504E627F9D59300723BCA /* ConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationView.swift; sourceTree = ""; }; - 007987AE27E2487200960D74 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; - 007987B027E24A8400960D74 /* VirtualMac.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualMac.swift; sourceTree = ""; }; - 0090AF6027E25F6F0077D35F /* UInt64+Byte.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UInt64+Byte.swift"; sourceTree = ""; }; - 00989C6027E2340C0048776B /* virtualOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = virtualOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 00989C6327E2340C0048776B /* virtualOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = virtualOSApp.swift; sourceTree = ""; }; - 00989C6727E2340D0048776B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 00989C6A27E2340D0048776B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 00989C6C27E2340D0048776B /* virtualOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = virtualOS.entitlements; sourceTree = ""; }; - 00989C7127E2340D0048776B /* virtualOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = virtualOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 00989C7527E2340D0048776B /* virtualOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = virtualOSTests.swift; sourceTree = ""; }; - 00989C7B27E2340D0048776B /* virtualOSUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = virtualOSUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 00989C7F27E2340D0048776B /* virtualOSUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = virtualOSUITests.swift; sourceTree = ""; }; - 00989C8127E2340D0048776B /* virtualOSUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = virtualOSUITestsLaunchTests.swift; sourceTree = ""; }; - 00989C9427E236A10048776B /* MainView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; - 00989C9927E238930048776B /* VirtualMacConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualMacConfiguration.swift; sourceTree = ""; }; - 00A4FFE7283E3D6F004DD9B3 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; - 00BA26AC2826DAF200E80B76 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; - 0114C02529AA2416004159AF /* MenuCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuCommands.swift; sourceTree = ""; }; - 01B042F129CD9F6A003CD5C2 /* Bookmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bookmark.swift; sourceTree = ""; }; - 01FCAD8329AB707C00F12689 /* ApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDelegate.swift; sourceTree = ""; }; + 015228552CB27BC100209934 /* virtualOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = virtualOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ -/* Begin PBXFrameworksBuildPhase section */ - 00989C5D27E2340C0048776B /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 00989C6E27E2340D0048776B /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 015228572CB27BC100209934 /* virtualOS */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = virtualOS; + sourceTree = ""; }; - 00989C7827E2340D0048776B /* Frameworks */ = { +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 015228522CB27BC100209934 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( @@ -99,219 +29,83 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 0090AF5F27E25F6F0077D35F /* Extension */ = { + 0152284C2CB27BC100209934 = { isa = PBXGroup; children = ( - 0090AF6027E25F6F0077D35F /* UInt64+Byte.swift */, - 0044A65927F76BD30007988A /* URL+Paths.swift */, - 002E64912871B2DD00CE95A0 /* UserDefaults+Settings.swift */, + 015228572CB27BC100209934 /* virtualOS */, + 015228562CB27BC100209934 /* Products */, ); - path = Extension; sourceTree = ""; }; - 00989C5727E2340C0048776B = { + 015228562CB27BC100209934 /* Products */ = { isa = PBXGroup; children = ( - 007987AE27E2487200960D74 /* LICENSE */, - 00989C6227E2340C0048776B /* virtualOS */, - 00989C7427E2340D0048776B /* virtualOSTests */, - 00989C7E27E2340D0048776B /* virtualOSUITests */, - 00989C6127E2340C0048776B /* Products */, - ); - sourceTree = ""; - }; - 00989C6127E2340C0048776B /* Products */ = { - isa = PBXGroup; - children = ( - 00989C6027E2340C0048776B /* virtualOS.app */, - 00989C7127E2340D0048776B /* virtualOSTests.xctest */, - 00989C7B27E2340D0048776B /* virtualOSUITests.xctest */, + 015228552CB27BC100209934 /* virtualOS.app */, ); name = Products; sourceTree = ""; }; - 00989C6227E2340C0048776B /* virtualOS */ = { - isa = PBXGroup; - children = ( - 00BA26AC2826DAF200E80B76 /* Info.plist */, - 00989C6327E2340C0048776B /* virtualOSApp.swift */, - 00989C9327E236A10048776B /* View */, - 00989C9127E236A10048776B /* Model */, - 0090AF5F27E25F6F0077D35F /* Extension */, - 00989C6727E2340D0048776B /* Assets.xcassets */, - 00989C6C27E2340D0048776B /* virtualOS.entitlements */, - 00989C6927E2340D0048776B /* Preview Content */, - ); - path = virtualOS; - sourceTree = ""; - }; - 00989C6927E2340D0048776B /* Preview Content */ = { - isa = PBXGroup; - children = ( - 00989C6A27E2340D0048776B /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - 00989C7427E2340D0048776B /* virtualOSTests */ = { - isa = PBXGroup; - children = ( - 00989C7527E2340D0048776B /* virtualOSTests.swift */, - ); - path = virtualOSTests; - sourceTree = ""; - }; - 00989C7E27E2340D0048776B /* virtualOSUITests */ = { - isa = PBXGroup; - children = ( - 00989C7F27E2340D0048776B /* virtualOSUITests.swift */, - 00989C8127E2340D0048776B /* virtualOSUITestsLaunchTests.swift */, - ); - path = virtualOSUITests; - sourceTree = ""; - }; - 00989C9127E236A10048776B /* Model */ = { - isa = PBXGroup; - children = ( - 01FCAD8329AB707C00F12689 /* ApplicationDelegate.swift */, - 0044A65427F601E60007988A /* ViewModel.swift */, - 007987B027E24A8400960D74 /* VirtualMac.swift */, - 00989C9927E238930048776B /* VirtualMacConfiguration.swift */, - 01B042F129CD9F6A003CD5C2 /* Bookmark.swift */, - ); - path = Model; - sourceTree = ""; - }; - 00989C9327E236A10048776B /* View */ = { - isa = PBXGroup; - children = ( - 00989C9427E236A10048776B /* MainView.swift */, - 0005A77927E2809E0013BE83 /* VirtualMachineView.swift */, - 006504E627F9D59300723BCA /* ConfigurationView.swift */, - 00A4FFE7283E3D6F004DD9B3 /* SettingsView.swift */, - 0114C02529AA2416004159AF /* MenuCommands.swift */, - ); - path = View; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 00989C5F27E2340C0048776B /* virtualOS */ = { + 015228542CB27BC100209934 /* virtualOS */ = { isa = PBXNativeTarget; - buildConfigurationList = 00989C8527E2340D0048776B /* Build configuration list for PBXNativeTarget "virtualOS" */; + buildConfigurationList = 015228642CB27BC200209934 /* Build configuration list for PBXNativeTarget "virtualOS" */; buildPhases = ( - 00989C5C27E2340C0048776B /* Sources */, - 00989C5D27E2340C0048776B /* Frameworks */, - 00989C5E27E2340C0048776B /* Resources */, + 015228512CB27BC100209934 /* Sources */, + 015228522CB27BC100209934 /* Frameworks */, + 015228532CB27BC100209934 /* Resources */, ); buildRules = ( ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 015228572CB27BC100209934 /* virtualOS */, + ); name = virtualOS; + packageProductDependencies = ( + ); productName = virtualOS; - productReference = 00989C6027E2340C0048776B /* virtualOS.app */; + productReference = 015228552CB27BC100209934 /* virtualOS.app */; productType = "com.apple.product-type.application"; }; - 00989C7027E2340D0048776B /* virtualOSTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 00989C8827E2340D0048776B /* Build configuration list for PBXNativeTarget "virtualOSTests" */; - buildPhases = ( - 00989C6D27E2340D0048776B /* Sources */, - 00989C6E27E2340D0048776B /* Frameworks */, - 00989C6F27E2340D0048776B /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 00989C7327E2340D0048776B /* PBXTargetDependency */, - ); - name = virtualOSTests; - productName = virtualOSTests; - productReference = 00989C7127E2340D0048776B /* virtualOSTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 00989C7A27E2340D0048776B /* virtualOSUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 00989C8B27E2340D0048776B /* Build configuration list for PBXNativeTarget "virtualOSUITests" */; - buildPhases = ( - 00989C7727E2340D0048776B /* Sources */, - 00989C7827E2340D0048776B /* Frameworks */, - 00989C7927E2340D0048776B /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 00989C7D27E2340D0048776B /* PBXTargetDependency */, - ); - name = virtualOSUITests; - productName = virtualOSUITests; - productReference = 00989C7B27E2340D0048776B /* virtualOSUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - 00989C5827E2340C0048776B /* Project object */ = { + 0152284D2CB27BC100209934 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1330; - LastUpgradeCheck = 1400; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; TargetAttributes = { - 00989C5F27E2340C0048776B = { - CreatedOnToolsVersion = 13.3; - }; - 00989C7027E2340D0048776B = { - CreatedOnToolsVersion = 13.3; - TestTargetID = 00989C5F27E2340C0048776B; - }; - 00989C7A27E2340D0048776B = { - CreatedOnToolsVersion = 13.3; - TestTargetID = 00989C5F27E2340C0048776B; + 015228542CB27BC100209934 = { + CreatedOnToolsVersion = 16.0; }; }; }; - buildConfigurationList = 00989C5B27E2340C0048776B /* Build configuration list for PBXProject "virtualOS" */; - compatibilityVersion = "Xcode 13.0"; + buildConfigurationList = 015228502CB27BC100209934 /* Build configuration list for PBXProject "virtualOS" */; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); - mainGroup = 00989C5727E2340C0048776B; - productRefGroup = 00989C6127E2340C0048776B /* Products */; + mainGroup = 0152284C2CB27BC100209934; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 015228562CB27BC100209934 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - 00989C5F27E2340C0048776B /* virtualOS */, - 00989C7027E2340D0048776B /* virtualOSTests */, - 00989C7A27E2340D0048776B /* virtualOSUITests */, + 015228542CB27BC100209934 /* virtualOS */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 00989C5E27E2340C0048776B /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 007987AF27E2487200960D74 /* LICENSE in Resources */, - 00989C6B27E2340D0048776B /* Preview Assets.xcassets in Resources */, - 00989C6827E2340D0048776B /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 00989C6F27E2340D0048776B /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 00989C7927E2340D0048776B /* Resources */ = { + 015228532CB27BC100209934 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -321,67 +115,24 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 00989C5C27E2340C0048776B /* Sources */ = { + 015228512CB27BC100209934 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 002E64922871B2DD00CE95A0 /* UserDefaults+Settings.swift in Sources */, - 00989C9627E236A10048776B /* MainView.swift in Sources */, - 0005A77A27E2809E0013BE83 /* VirtualMachineView.swift in Sources */, - 00989C6427E2340C0048776B /* virtualOSApp.swift in Sources */, - 01FCAD8429AB707C00F12689 /* ApplicationDelegate.swift in Sources */, - 007987B127E24A8400960D74 /* VirtualMac.swift in Sources */, - 0044A65A27F76BD30007988A /* URL+Paths.swift in Sources */, - 00A4FFE8283E3D6F004DD9B3 /* SettingsView.swift in Sources */, - 006504E727F9D59300723BCA /* ConfigurationView.swift in Sources */, - 0114C02629AA2416004159AF /* MenuCommands.swift in Sources */, - 00989C9A27E238930048776B /* VirtualMacConfiguration.swift in Sources */, - 01B042F229CD9F6A003CD5C2 /* Bookmark.swift in Sources */, - 0090AF6127E25F6F0077D35F /* UInt64+Byte.swift in Sources */, - 0044A65527F601E60007988A /* ViewModel.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 00989C6D27E2340D0048776B /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 00989C7627E2340D0048776B /* virtualOSTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 00989C7727E2340D0048776B /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 00989C8227E2340D0048776B /* virtualOSUITestsLaunchTests.swift in Sources */, - 00989C8027E2340D0048776B /* virtualOSUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - 00989C7327E2340D0048776B /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 00989C5F27E2340C0048776B /* virtualOS */; - targetProxy = 00989C7227E2340D0048776B /* PBXContainerItemProxy */; - }; - 00989C7D27E2340D0048776B /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 00989C5F27E2340C0048776B /* virtualOS */; - targetProxy = 00989C7C27E2340D0048776B /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin XCBuildConfiguration section */ - 00989C8327E2340D0048776B /* Debug */ = { + 015228622CB27BC200209934 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; @@ -408,11 +159,11 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; @@ -426,23 +177,25 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.3; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; - 00989C8427E2340D0048776B /* Release */ = { + 015228632CB27BC200209934 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; @@ -469,11 +222,11 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -481,197 +234,99 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.3; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; - 00989C8627E2340D0048776B /* Debug */ = { + 015228652CB27BC200209934 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = virtualOS/virtualOS.entitlements; - CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_ENTITLEMENTS = virtualOS/Resources/virtualOS.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 14; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_ASSET_PATHS = "\"virtualOS/Preview Content\""; + CURRENT_PROJECT_VERSION = 16; DEVELOPMENT_TEAM = 2AD47BTDQ6; ENABLE_HARDENED_RUNTIME = YES; - ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = virtualOS/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSMainStoryboardFile = Main; + INFOPLIST_KEY_NSPrincipalClass = NSApplication; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.3.3; + MARKETING_VERSION = 2.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOS; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Debug; }; - 00989C8727E2340D0048776B /* Release */ = { + 015228662CB27BC200209934 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = virtualOS/virtualOS.entitlements; - CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_ENTITLEMENTS = virtualOS/Resources/virtualOS.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 14; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_ASSET_PATHS = "\"virtualOS/Preview Content\""; + CURRENT_PROJECT_VERSION = 16; DEVELOPMENT_TEAM = 2AD47BTDQ6; ENABLE_HARDENED_RUNTIME = YES; - ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = virtualOS/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSMainStoryboardFile = Main; + INFOPLIST_KEY_NSPrincipalClass = NSApplication; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.3.3; + MARKETING_VERSION = 2.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOS; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Release; }; - 00989C8927E2340D0048776B /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = 2AD47BTDQ6; - GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.3; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOSTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/virtualOS.app/Contents/MacOS/virtualOS"; - }; - name = Debug; - }; - 00989C8A27E2340D0048776B /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = 2AD47BTDQ6; - GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.3; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOSTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/virtualOS.app/Contents/MacOS/virtualOS"; - }; - name = Release; - }; - 00989C8C27E2340D0048776B /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = 2AD47BTDQ6; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOSUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TEST_TARGET_NAME = virtualOS; - }; - name = Debug; - }; - 00989C8D27E2340D0048776B /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = 2AD47BTDQ6; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOSUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TEST_TARGET_NAME = virtualOS; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 00989C5B27E2340C0048776B /* Build configuration list for PBXProject "virtualOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 00989C8327E2340D0048776B /* Debug */, - 00989C8427E2340D0048776B /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 00989C8527E2340D0048776B /* Build configuration list for PBXNativeTarget "virtualOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 00989C8627E2340D0048776B /* Debug */, - 00989C8727E2340D0048776B /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 00989C8827E2340D0048776B /* Build configuration list for PBXNativeTarget "virtualOSTests" */ = { + 015228502CB27BC100209934 /* Build configuration list for PBXProject "virtualOS" */ = { isa = XCConfigurationList; buildConfigurations = ( - 00989C8927E2340D0048776B /* Debug */, - 00989C8A27E2340D0048776B /* Release */, + 015228622CB27BC200209934 /* Debug */, + 015228632CB27BC200209934 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 00989C8B27E2340D0048776B /* Build configuration list for PBXNativeTarget "virtualOSUITests" */ = { + 015228642CB27BC200209934 /* Build configuration list for PBXNativeTarget "virtualOS" */ = { isa = XCConfigurationList; buildConfigurations = ( - 00989C8C27E2340D0048776B /* Debug */, - 00989C8D27E2340D0048776B /* Release */, + 015228652CB27BC200209934 /* Debug */, + 015228662CB27BC200209934 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; - rootObject = 00989C5827E2340C0048776B /* Project object */; + rootObject = 0152284D2CB27BC100209934 /* Project object */; } diff --git a/virtualOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/virtualOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/virtualOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/virtualOS.xcodeproj/xcshareddata/xcschemes/virtualOS.xcscheme b/virtualOS.xcodeproj/xcshareddata/xcschemes/virtualOS.xcscheme index 138a0b2..e7f6fbe 100644 --- a/virtualOS.xcodeproj/xcshareddata/xcschemes/virtualOS.xcscheme +++ b/virtualOS.xcodeproj/xcshareddata/xcschemes/virtualOS.xcscheme @@ -1,10 +1,11 @@ + LastUpgradeVersion = "1600" + version = "1.7"> + buildImplicitDependencies = "YES" + buildArchitectures = "Automatic"> @@ -26,32 +27,11 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> - - - - - - - - - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> @@ -81,7 +61,7 @@ runnableDebuggingMode = "0"> diff --git a/virtualOS/.gitignore b/virtualOS/.gitignore new file mode 100644 index 0000000..b17f2af --- /dev/null +++ b/virtualOS/.gitignore @@ -0,0 +1 @@ +xcuserstate diff --git a/virtualOS/AppDelegate.swift b/virtualOS/AppDelegate.swift new file mode 100644 index 0000000..eaffa4d --- /dev/null +++ b/virtualOS/AppDelegate.swift @@ -0,0 +1,26 @@ +// +// AppDelegate.swift +// virtualOS +// +// Created by Jahn Bertsch. +// + +import Cocoa + +@main +final class AppDelegate: NSObject, NSApplicationDelegate { + func applicationDidFinishLaunching(_ aNotification: Notification) { + // Insert code here to initialize your application + } + + func applicationWillTerminate(_ aNotification: Notification) { + // Insert code here to tear down your application + } + + func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } + + +} + diff --git a/virtualOS/Extension/OperatingSystemVersion+String.swift b/virtualOS/Extension/OperatingSystemVersion+String.swift new file mode 100644 index 0000000..c9e4bb5 --- /dev/null +++ b/virtualOS/Extension/OperatingSystemVersion+String.swift @@ -0,0 +1,18 @@ +// +// OsVersion.swift +// virtualOS +// +// Created by Jahn Bertsch +// + +import Virtualization + +#if arch(arm64) + +extension VZMacOSRestoreImage { + var operatingSystemVersionString: String { + return "macOS \(operatingSystemVersion.majorVersion).\(operatingSystemVersion.minorVersion).\(operatingSystemVersion.patchVersion) (Build \(buildVersion))" + } +} + +#endif diff --git a/virtualOS/Extension/URL+Paths.swift b/virtualOS/Extension/URL+Paths.swift index f9483a9..2b5d06a 100644 --- a/virtualOS/Extension/URL+Paths.swift +++ b/virtualOS/Extension/URL+Paths.swift @@ -11,34 +11,26 @@ extension URL { static let bundleName = "virtualOS.bundle/" static let defaultVmBundlePath = basePath + "/\(bundleName)" - static var vmBundleURL: URL { - return URL(fileURLWithPath: vmBundlePath) + static var baseURL: URL { + return URL(fileURLWithPath: basePath) } - static var diskImageURL: URL { - return URL(fileURLWithPath: vmBundlePath + "/Disk.img") + static var vmBundleURL: URL { + return URL(fileURLWithPath: defaultVmBundlePath) } - static var auxiliaryStorageURL: URL { - return URL(fileURLWithPath: vmBundlePath + "/AuxiliaryStorage") + var auxiliaryStorageURL: URL { + return self.appending(path: "AuxiliaryStorage") } - static var machineIdentifierURL: URL { - return URL(fileURLWithPath: vmBundlePath + "/MachineIdentifier") + var hardwareModelURL: URL { + return self.appending(path: "HardwareModel") } - static var hardwareModelURL: URL { - return URL(fileURLWithPath: vmBundlePath + "/HardwareModel") + var diskImageURL: URL { + return self.appending(path: "Disk.img") } - static var parametersURL: URL { - return URL(fileURLWithPath: vmBundlePath + "/Parameters.txt") + var machineIdentifierURL: URL { + return self.appending(path: "MachineIdentifier") } - - static var vmBundlePath: String { - if let hardDiskDirectoryBookmarkData = UserDefaults.standard.hardDiskDirectoryBookmarkData, - let hardDiskDirectoryURL = Bookmark.startAccess(data: hardDiskDirectoryBookmarkData, forType: .hardDisk) - { - let vmBundlePath = hardDiskDirectoryURL.appendingPathComponent(bundleName).path - return vmBundlePath - } else { - return URL.defaultVmBundlePath - } + var parametersURL: URL { + return self.appending(path: "Parameters.txt") } } diff --git a/virtualOS/Extension/UserDefaults+Settings.swift b/virtualOS/Extension/UserDefaults+Settings.swift index 88743f1..17ace48 100644 --- a/virtualOS/Extension/UserDefaults+Settings.swift +++ b/virtualOS/Extension/UserDefaults+Settings.swift @@ -2,7 +2,7 @@ // UserDefaults+Settings.swift // virtualOS // -// Created by Jahn Bertsch on 03.07.22. +// Created by Jahn Bertsch. // import Foundation @@ -16,7 +16,7 @@ extension UserDefaults { if object(forKey: Self.diskSizeKey) != nil { return integer(forKey: Self.diskSizeKey) } - return 60 // default value + return 30 // default value } set { set(newValue, forKey: Self.diskSizeKey) diff --git a/virtualOS/Info.plist b/virtualOS/Info.plist deleted file mode 100644 index bc11256..0000000 --- a/virtualOS/Info.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - ITSAppUsesNonExemptEncryption - - - diff --git a/virtualOS/Model/ApplicationDelegate.swift b/virtualOS/Model/ApplicationDelegate.swift deleted file mode 100644 index 92b42fc..0000000 --- a/virtualOS/Model/ApplicationDelegate.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// ApplicationDelegate.swift -// virtualOS -// -// Created by Jahn Bertsch on 26.02.23. -// - -import AppKit - -class ApplicationDelegate: NSObject, NSApplicationDelegate { - func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } - - func applicationWillTerminate(_ notification: Notification) { - Bookmark.stopAllAccess() - } -} diff --git a/virtualOS/Model/Bookmark.swift b/virtualOS/Model/Bookmark.swift index c9f6917..34cb17b 100644 --- a/virtualOS/Model/Bookmark.swift +++ b/virtualOS/Model/Bookmark.swift @@ -6,55 +6,38 @@ // import Foundation +import OSLog struct Bookmark { - enum BookmarkType { - case hardDisk - case sharedFolder - } - - fileprivate static var accessedURLs: [BookmarkType: URL] = [:] + fileprivate static let logger = Logger.init(subsystem: "com.github.virtualOS", category: "log") + fileprivate static var currentlyAccessedUrl: URL? static func createBookmarkData(fromUrl url: URL) -> Data? { - if let bookmarkData = try? url.bookmarkData(options: .withSecurityScope, relativeTo: nil) { + do { + let bookmarkData = try url.bookmarkData(options: .withSecurityScope, relativeTo: nil) return bookmarkData + } catch let error { + Self.logger.log(level: .default, "error creating bookmark: \(error.localizedDescription)") } return nil } - static func startAccess(data: Data?, forType key: BookmarkType) -> URL? { + static func startAccess(bookmarkData: Data?) -> URL? { + if let currentlyAccessedUrl = Self.currentlyAccessedUrl { + // stop previous access + currentlyAccessedUrl.stopAccessingSecurityScopedResource() + } + var bookmarkDataIsStale = false - if let bookmarkData = data, + if let bookmarkData, let bookmarkURL = try? URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &bookmarkDataIsStale), !bookmarkDataIsStale { - // stop accessing previous resource - if let previousURL = accessedURLs[key], - previousURL != bookmarkURL - { - previousURL.stopAccessingSecurityScopedResource() - } - - if accessedURLs[key] != bookmarkURL { - // resource not already accessed, start access - _ = bookmarkURL.startAccessingSecurityScopedResource() - accessedURLs[key] = bookmarkURL - } + _ = bookmarkURL.startAccessingSecurityScopedResource() + Self.currentlyAccessedUrl = bookmarkURL return bookmarkURL } return nil } - - static func stopAccess(url: URL, forKey key: BookmarkType) { - url.stopAccessingSecurityScopedResource() - Self.accessedURLs[key] = nil - } - - static func stopAllAccess() { - for (_, accessedURL) in accessedURLs { - accessedURL.stopAccessingSecurityScopedResource() - } - Self.accessedURLs = [:] - } } diff --git a/virtualOS/Model/Bundle.swift b/virtualOS/Model/Bundle.swift new file mode 100644 index 0000000..247264c --- /dev/null +++ b/virtualOS/Model/Bundle.swift @@ -0,0 +1,18 @@ +// +// Bundle.swift +// virtualOS +// +// Created by Jahn Bertsch. +// + +import Foundation + +struct VMBundle: Identifiable, Hashable { + var id: String { + return url.absoluteString + } + var url: URL + var name: String { + return url.lastPathComponent.replacingOccurrences(of: ".bundle", with: "") + } +} diff --git a/virtualOS/Model/Constants.swift b/virtualOS/Model/Constants.swift new file mode 100644 index 0000000..e80227e --- /dev/null +++ b/virtualOS/Model/Constants.swift @@ -0,0 +1,14 @@ +// +// Constants.swift +// virtualOS +// +// Created by Jahn Bertsch +// + +import AppKit + +struct Constants { + static let restoreImageNameLatest = "latest" + static let selectedRestoreImage = "selectedRestoreImage" + static let restoreImageNameSelectedNotification = Notification.Name("restoreImageSelected") +} diff --git a/virtualOS/Model/FileModel.swift b/virtualOS/Model/FileModel.swift new file mode 100644 index 0000000..7f7c81a --- /dev/null +++ b/virtualOS/Model/FileModel.swift @@ -0,0 +1,56 @@ +// +// FileModel.swift +// virtualOS +// +// Created by Jahn Bertsch. +// + +import Foundation + +struct FileModel { + var bundleExists: Bool { + return FileManager.default.fileExists(atPath: URL.vmBundleURL.path()) + } + + var restoreImageExists: Bool { + return FileManager.default.fileExists(atPath: URL.restoreImageURL.path) + } + + var sharedFolderExists: Bool { +// if let hardDiskDirectoryBookmarkData = Bookmark.startAccess(data: virtualMac.parameters.sharedFolder, forType: .hardDisk) { +// var isDirectory = ObjCBool(false) +// if FileManager.default.fileExists(atPath: hardDiskDirectoryBookmarkData.path, isDirectory: &isDirectory), +// isDirectory.boolValue == true +// { +// return true +// } +// } + return false + } + + func getVMBundles() -> [VMBundle] { + var result: [VMBundle] = [] + if let urls = try? FileManager.default.contentsOfDirectory(at: URL.baseURL, includingPropertiesForKeys: nil, options: []) { + for url in urls { + if url.lastPathComponent.hasSuffix("bundle") { + result.append(VMBundle(url: url)) + } + } + } + return result + } + + func getRestoreImages() -> [String] { + var result: [String] = [] + + if let urls = try? FileManager.default.contentsOfDirectory(at: URL.baseURL, includingPropertiesForKeys: nil, options: []) { + for url in urls { + if url.lastPathComponent.hasSuffix("ipsw") { + result.append(url.lastPathComponent) + } + } + } + return result + } + +} diff --git a/virtualOS/Model/MainViewModel.swift b/virtualOS/Model/MainViewModel.swift new file mode 100644 index 0000000..5e0259c --- /dev/null +++ b/virtualOS/Model/MainViewModel.swift @@ -0,0 +1,63 @@ +// +// MainViewModel.swift +// virtualOS +// +// Created by Jahn Bertsch +// + +import AppKit + +#if arch(arm64) + +final class MainViewModel { + let tableViewDataSource = TableViewDataSource() + let parametersViewDataSource = ParametersViewDataSource() + let parametersViewDelegate = ParametersViewDelegate() + let textFieldDelegate = TextFieldDelegate() + var vmBundle: VMBundle? + var selectedRow: Int? = 0 + var vmParameters: VMParameters? + + func storeParametersToDisk() { + if let vmParameters = vmParameters, + let vmBundleUrl = vmBundle?.url + { + vmParameters.writeToDisk(bundleURL: vmBundleUrl) + } + } + + func deleteVM(selection: NSApplication.ModalResponse, vmBundle: VMBundle) { + try? FileManager.default.removeItem(at: vmBundle.url) + if let selectedRow, + selectedRow > tableViewDataSource.rows() - 1 + { + self.selectedRow = tableViewDataSource.rows() - 1 // select last table row + } + } + + func set(sharedFolderUrl: URL?) -> VMParameters? { + var sharedFolderData: Data? = nil + + if let sharedFolderUrl { + sharedFolderData = Bookmark.createBookmarkData(fromUrl: sharedFolderUrl) + if let sharedFolderData { + _ = Bookmark.startAccess(bookmarkData: sharedFolderData) + } + } + + if let selectedRow { + let bundle = tableViewDataSource.vmBundle(forRow: selectedRow) + if let bundleURL = bundle?.url { + var vmParameters = VMParameters.readFrom(url: bundleURL) + vmParameters?.sharedFolder = sharedFolderData + vmParameters?.writeToDisk(bundleURL: bundleURL) + self.vmParameters = vmParameters + return vmParameters + } + } + + return nil + } +} + +#endif diff --git a/virtualOS/Model/ParametersViewDataSource.swift b/virtualOS/Model/ParametersViewDataSource.swift new file mode 100644 index 0000000..9ec7d27 --- /dev/null +++ b/virtualOS/Model/ParametersViewDataSource.swift @@ -0,0 +1,55 @@ +// +// ParametersViewDataSource.swift +// virtualOS +// +// Created by Jahn Bertsch. +// + +import AppKit + +#if arch(arm64) + +final class ParametersViewDataSource: NSObject, NSOutlineViewDataSource { + var vmParameters: VMParameters? + + func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { + if vmParameters != nil { + return 5 + } else { + return 0 + } + } + + func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { + if let vmParameters = vmParameters { + switch index { + case 0: + return ["CPU Count", "\(vmParameters.cpuCount)"] + case 1: + return ["Memory Size (GB)", "\(vmParameters.memorySizeInGB)"] + case 2: + return ["Disk Size (GB)", "\(vmParameters.diskSizeInGB)"] + case 3: + let sharedFolderString = sharedFolderInfo(vmParameters: vmParameters) + return ["Shared Folder", sharedFolderString] + case 4: + return ["Version", "\(vmParameters.version)"] + default: + return ["index \(index)", "value \(index)"] + } + } + + return ["index \(index)", "value \(index)"] + } + + fileprivate func sharedFolderInfo(vmParameters: VMParameters) -> String { + if let sharedFolderData = vmParameters.sharedFolder { + if let sharedFolderURL = Bookmark.startAccess(bookmarkData: sharedFolderData) { + return sharedFolderURL.path() + } + } + return "No shared folder" + } +} + +#endif diff --git a/virtualOS/Model/ParametersViewDelegate.swift b/virtualOS/Model/ParametersViewDelegate.swift new file mode 100644 index 0000000..f8416ff --- /dev/null +++ b/virtualOS/Model/ParametersViewDelegate.swift @@ -0,0 +1,32 @@ +// +// ParametersViewDelegate.swift +// virtualOS +// +// Created by Jahn Bertsch. +// + +import AppKit + +final class ParametersViewDelegate: NSObject, NSOutlineViewDelegate { + func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { + var string: String + + if let identifier = tableColumn?.identifier, + let array = item as? [String] + { + switch identifier.rawValue { + case "AutomaticTableColumnIdentifier.0": + string = array[0] + case "AutomaticTableColumnIdentifier.1": + string = array[1] + default: + string = "default" + + } + } else { + string = "unknown" + } + + return NSTextField(labelWithString: string) + } +} diff --git a/virtualOS/Model/TableViewDataSource.swift b/virtualOS/Model/TableViewDataSource.swift new file mode 100644 index 0000000..bad9fd4 --- /dev/null +++ b/virtualOS/Model/TableViewDataSource.swift @@ -0,0 +1,33 @@ +// +// TableViewDataSource.swift +// virtualOS +// +// Created by Jahn Bertsch.. +// + +import AppKit + +final class TableViewDataSource: NSObject, NSTableViewDataSource { + fileprivate let fileModel = FileModel() + + func rows() -> Int { + return fileModel.getVMBundles().count + } + + func numberOfRows(in tableView: NSTableView) -> Int { + return fileModel.getVMBundles().count + } + + func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { + return vmBundle(forRow: row)?.name + } + + func vmBundle(forRow row: Int) -> VMBundle? { + if row < fileModel.getVMBundles().count { + return fileModel.getVMBundles()[row] + } else { + return nil + } + } +} + diff --git a/virtualOS/Model/TextFieldDelegate.swift b/virtualOS/Model/TextFieldDelegate.swift new file mode 100644 index 0000000..99924c5 --- /dev/null +++ b/virtualOS/Model/TextFieldDelegate.swift @@ -0,0 +1,25 @@ +// +// TextFieldDelegate.swift +// virtualOS +// +// Created by Jahn Bertsch +// + +import AppKit + +final class TextFieldDelegate: NSObject, NSTextFieldDelegate { + var vmBundle: VMBundle? + + func control(_ control: NSControl, textShouldEndEditing fieldEditor: NSText) -> Bool { + if let vmBundle = vmBundle, + vmBundle.name != fieldEditor.string + { + let newFilename = "\(fieldEditor.string).bundle" + let newUrl = vmBundle.url.deletingLastPathComponent().appendingPathComponent(newFilename) + + try? FileManager.default.moveItem(at: vmBundle.url, to: newUrl) + } + + return true + } +} diff --git a/virtualOS/Model/ViewModel.swift b/virtualOS/Model/ViewModel.swift deleted file mode 100644 index 4c4908f..0000000 --- a/virtualOS/Model/ViewModel.swift +++ /dev/null @@ -1,355 +0,0 @@ -// -// ViewModel.swift -// virtualOS -// -// Created by Jahn Bertsch on 31.03.22. -// - -#if arch(arm64) - -import Foundation -import Virtualization -import OSLog - -final class ViewModel: NSObject, ObservableObject { - enum State: String { - case Downloading - case Installing - case Starting - case Running - case Stopping - case Stopped - } - - @Published var statusLabel = "" - @Published var statusButtonLabel = "" - @Published var statusButtonDisabled = false - @Published var showStatusBar = true - @Published var showLicenseInformationModal = false - @Published var showConfirmationAlert = false - @Published var showSettings = false - @Published var isFullScreen = false - @Published var useMainScreenSize = true - @Published var licenseInformationTitleString = "" - @Published var licenseInformationString = "" - @Published var confirmationText = "" - @Published var progress: Progress? - @Published var confirmationHandler: CompletionHander = {_ in} - @Published var virtualMac = VirtualMac() - @Published var virtualMachine: VZVirtualMachine? - @Published var customRestoreImageURL: URL? - @Published var customHardDiskURL: URL? - @Published var diskSize = UserDefaults.standard.diskSize { - didSet { - UserDefaults.standard.diskSize = diskSize - } - } - @Published var state = State.Stopped { - didSet { - virtualOSApp.debugLog(self.state.rawValue) - updateLabels(for: self.state) - } - } - static var bundleExists: Bool { - return FileManager.default.fileExists(atPath: URL.vmBundlePath) - } - static var diskImageExists: Bool { - return FileManager.default.fileExists(atPath: URL.diskImageURL.path) - } - static var restoreImageExists: Bool { - return FileManager.default.fileExists(atPath: URL.restoreImageURL.path) - } - var sharedFolderExists: Bool { - if let hardDiskDirectoryBookmarkData = Bookmark.startAccess(data: virtualMac.parameters.sharedFolder, forType: .hardDisk) { - var isDirectory = ObjCBool(false) - if FileManager.default.fileExists(atPath: hardDiskDirectoryBookmarkData.path, isDirectory: &isDirectory), - isDirectory.boolValue == true - { - return true - } - } - return false - } - var showConfigurationView: Bool { - return (Self.diskImageExists || Self.restoreImageExists) && state == .Stopped - } - var showSettingsInfo: Bool { - return !Self.diskImageExists && state == .Stopped - } - - // MARK: - Public - - override init() { - super.init() - updateLabels(for: state) - readParametersFromDisk() - loadLicenseInformationFromBundle() - handleCommandLineArguments() - } - - func statusButtonPressed() { - switch state { - case .Stopped: - start() - case .Downloading: - virtualMac.stopDownload() - state = .Stopped - case .Installing, .Starting, .Running, .Stopping: - stop() - } - } - - func deleteRestoreImage() { - confirmationText = "Restore Image" - confirmationHandler = { _ in - do { - try FileManager.default.removeItem(atPath: URL.restoreImageURL.path) - } catch { - self.display(errorString: "Error: Could not delete restore image") - } - } - showConfirmationAlert = !showConfirmationAlert - } - - func deleteVirtualMachine() { - confirmationText = "Virtual Machine" - confirmationHandler = { _ in - if Self.bundleExists { - self.stop() - do { - try FileManager.default.removeItem(at: URL.vmBundleURL) - self.updateLabels(for: self.state) - } catch { - self.display(errorString: "Error: Could not delete virtual machine") - } - } - } - showConfirmationAlert = !showConfirmationAlert - } - - func loadLicenseInformationFromBundle() { - if let filepath = Bundle.main.path(forResource: "LICENSE", ofType: "") { - do { - let contents = try String(contentsOfFile: filepath) - licenseInformationString = contents - } catch { - licenseInformationString = "Failed to load license information" - } - } else { - licenseInformationString = "License information not found" - } - - licenseInformationTitleString = "virtualOS" - if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, - let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String - { - licenseInformationTitleString += " \(version) (Build \(build))" - } - } - - func set(sharedFolderUrl: URL) { - if let sharedFolderData = Bookmark.createBookmarkData(fromUrl: sharedFolderUrl) { - _ = Bookmark.startAccess(data: sharedFolderData, forType: .sharedFolder) - virtualMac.parameters.sharedFolder = sharedFolderData - objectWillChange.send() - if let errorString = virtualMac.writeParametersToDisk() { - display(errorString: errorString) - } - } - } - - // MARK: - Private - - fileprivate func readParametersFromDisk() { - if let hardDiskDirectoryBookmarkData = UserDefaults.standard.hardDiskDirectoryBookmarkData { - _ = Bookmark.startAccess(data: hardDiskDirectoryBookmarkData, forType: .hardDisk) - } - - if Self.diskImageExists { - // read previous vm settings - if let errorString = virtualMac.readFromDisk(delegate: self) { - display(errorString: errorString) - } - } else if Self.restoreImageExists { - virtualMac.loadParametersFromRestoreImage(customRestoreImageURL: nil) { (errorString: String?) in - if let errorString = errorString { - self.display(errorString: errorString) - } - } - } - } - - fileprivate func start() { - virtualOSApp.debugLog("Using storage directory \(URL.vmBundlePath)") - if FileManager.default.fileExists(atPath: URL.diskImageURL.path) { - startFromDiskImage() - } else if FileManager.default.fileExists(atPath: URL.restoreImageURL.path) || customRestoreImageURL != nil { - install(virtualMac: virtualMac) - } else { - downloadAndInstall() - } - } - - fileprivate func downloadAndInstall() { - state = .Downloading - statusButtonLabel = "Stop" - - virtualMac.downloadRestoreImage { (progress: Progress) in - virtualOSApp.debugLog("Download progress: \(progress.fractionCompleted * 100)%") - self.progress = progress - self.updateLabels(for: self.state) - } completionHandler: { (errorString: String?) in - if let errorString = errorString { - self.display(errorString: "Download of restore image failed: \(errorString)") - } else { - virtualOSApp.debugLog("Download of restore image completed") - DispatchQueue.main.async { - self.install(virtualMac: self.virtualMac) - } - } - } - } - - fileprivate func install(virtualMac: VirtualMac) { - state = .Installing - virtualMac.install(delegate: self, customRestoreImageURL: customRestoreImageURL) { (progress: Progress) in - virtualOSApp.debugLog("Install progress: \(progress.completedUnitCount)%") - self.progress = progress - self.updateLabels(for: self.state) - } completionHandler: { (errorString: String?, virtualMachine: VZVirtualMachine?) in - DispatchQueue.main.async { - self.progress = nil - } - if let errorString = errorString { - self.display(errorString: errorString) - } else if let virtualMachine = virtualMachine { - self.start(virtualMachine: virtualMachine) - } else { - self.display(errorString: "Error: Install finished but no virtual machine created") - } - } - } - - fileprivate func startFromDiskImage() { - guard let virtualMachine = virtualMac.createVirtualMachine(delegate: self) else { - display(errorString: "Error: Failed to read virtual machine from disk") - return - } - - start(virtualMachine: virtualMachine) - } - - fileprivate func start(virtualMachine: VZVirtualMachine) { - self.state = .Starting - self.virtualMachine = virtualMachine - - if let errorString = virtualMac.writeParametersToDisk() { - display(errorString: errorString) - } - - virtualMachine.start { (result: Result) in - switch result { - case .success: - self.state = .Running - case .failure(let error): - self.display(errorString: "Error while starting: \(error)") - } - } - } - - fileprivate func stop() { - guard let virtualMachine = virtualMachine else { - return // already stopped - } - state = .Stopping - - virtualMachine.stop(completionHandler: { (error: Error?) in - self.state = .Stopped - if let error = error { - self.display(errorString: error.localizedDescription) - } - self.virtualMachine = nil - }) - } - - fileprivate func display(errorString: String) { - virtualOSApp.debugLog(errorString) - - let displayErrorString = { - self.state = .Stopped - self.statusLabel = errorString - } - - if Thread.isMainThread { - displayErrorString() - } else { - DispatchQueue.main.async { - displayErrorString() - } - } - } - - fileprivate func updateLabels(for: State) { - switch state { - case .Stopped: - statusLabel = state.rawValue - statusButtonLabel = "Start" - case .Downloading: - if let progress = progress { - updateDownloadProgress(progress) - } - statusButtonLabel = "Stop" - case .Installing: - if let progress = progress { - statusLabel = "Installing macOS \(virtualMac.versionString): " - if progress.completedUnitCount == 0 { - statusLabel = statusLabel + "Waiting for begin, this may take some time …" - } else { - statusLabel = statusLabel + "\(progress.completedUnitCount)%" - } - } - statusButtonLabel = "Stop" - case .Starting, .Running, .Stopping: - statusLabel = state.rawValue - statusButtonLabel = "Stop" - } - - if state == .Installing { - statusButtonDisabled = true // installing can not be canceled - } else { - statusButtonDisabled = false - } - } - - fileprivate func updateDownloadProgress(_ progress: Progress) { - var statusText = String(format: "Downloading restore image: %2.2f%%", progress.fractionCompleted * 100) - - if let byteCompletedCount = progress.userInfo[ProgressUserInfoKey("NSProgressByteCompletedCountKey")] as? Int, - let byteTotalCount = progress.userInfo[ProgressUserInfoKey("NSProgressByteTotalCountKey")] as? Int - { - let mbCompleted = byteCompletedCount / (1024 * 1024) - let mbTotal = byteTotalCount / (1024 * 1024) - statusText += " (\(mbCompleted) of \(mbTotal) MB)" - } - - statusLabel = statusText - } - - fileprivate func handleCommandLineArguments() { - for arg in CommandLine.arguments where arg == "start" { - start() - } - } -} - -extension ViewModel: VZVirtualMachineDelegate { - func guestDidStop(_ vm: VZVirtualMachine) { - state = .Stopped - } - - func virtualMachine(_ vm: VZVirtualMachine, didStopWithError error: Error) { - display(errorString: error.localizedDescription) - } -} - -#endif diff --git a/virtualOS/Model/VirtualMac.swift b/virtualOS/Model/VirtualMac.swift deleted file mode 100644 index 6ea39b1..0000000 --- a/virtualOS/Model/VirtualMac.swift +++ /dev/null @@ -1,389 +0,0 @@ -// -// VirtualMac.swift -// virtualOS -// -// Created by Jahn Bertsch on 16.03.22. -// - -#if arch(arm64) - -import Virtualization -import Combine - -final class VirtualMac: ObservableObject { - struct Parameters: Codable { - init() {} - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - cpuCount = try container.decode(Int.self, forKey: .cpuCount) - cpuCountMin = try container.decode(Int.self, forKey: .cpuCountMin) - cpuCountMax = try container.decode(Int.self, forKey: .cpuCountMax) - diskSizeInGB = try container.decode(UInt64.self, forKey: .diskSizeInGB) - memorySizeInGB = try container.decode(UInt64.self, forKey: .memorySizeInGB) - memorySizeInGBMin = try container.decode(UInt64.self, forKey: .memorySizeInGBMin) - memorySizeInGBMax = try container.decode(UInt64.self, forKey: .memorySizeInGBMax) - useMainScreenSize = try container.decodeIfPresent(Bool.self, forKey: .useMainScreenSize) ?? true // optional - screenWidth = try container.decode(Int.self, forKey: .screenWidth) - screenHeight = try container.decode(Int.self, forKey: .screenHeight) - pixelsPerInch = try container.decode(Int.self, forKey: .pixelsPerInch) - microphoneEnabled = try container.decode(Bool.self, forKey: .microphoneEnabled) - sharedFolder = try container.decodeIfPresent(Data.self, forKey: .sharedFolder) ?? nil // optional - macAddress = try container.decodeIfPresent(String.self, forKey: .macAddress) ?? VZMACAddress.randomLocallyAdministered().string // optional - } - - var cpuCount = 1 - var cpuCountMin = 1 - var cpuCountMax = 2 - var diskSizeInGB: UInt64 = UInt64(UserDefaults.standard.diskSize) - var memorySizeInGB: UInt64 = 1 - var memorySizeInGBMin: UInt64 = 1 - var memorySizeInGBMax: UInt64 = 2 - var useMainScreenSize = true - var screenWidth = 1500 - var screenHeight = 900 - var pixelsPerInch = 250 - var microphoneEnabled = false - var sharedFolder: Data? - var macAddress = VZMACAddress.randomLocallyAdministered().string - } - - typealias InstallCompletionHander = (String?, VZVirtualMachine?) -> Void - - var parameters = Parameters() - var versionString = "(unknown)" - var virtualMachineConfiguration: VirtualMacConfiguration? - var progressObserverCancellable: Cancellable? - fileprivate var downloadTask: URLSessionDownloadTask? - - func readFromDisk(delegate: VZVirtualMachineDelegate) -> String? { - if let errorString = readParametersFromDisk() { - return errorString - } - - let virtualMacConfiguration = VirtualMacConfiguration() - virtualMacConfiguration.readFromDisk(using: ¶meters) - - do { - try virtualMacConfiguration.validate() - self.virtualMachineConfiguration = virtualMacConfiguration - } catch { - return "Error: Failed to validate virtual machine configuration from disk" - } - - return nil - } - - func downloadRestoreImage(progressHandler: @escaping ProgressHandler, completionHandler: @escaping CompletionHander) { - if let errorString = createBundle() { - completionHandler(errorString) - return - } - - if FileManager.default.fileExists(atPath: URL.restoreImageURL.path) { - completionHandler(nil) // done: already downloaded - } else { - fetchLatestSupportedRestoreImage(progressHandler: progressHandler, completionHandler: { (errorString: String?) in - if let errorString = errorString { - completionHandler(errorString) - } else { - completionHandler(nil) - } - }) - } - } - - func install(delegate: VZVirtualMachineDelegate, customRestoreImageURL: URL?, progressHandler: @escaping ProgressHandler, completionHandler: @escaping InstallCompletionHander) { - loadParametersFromRestoreImage(customRestoreImageURL: customRestoreImageURL) { (errorString: String?) in - if let errorString = errorString { - completionHandler(errorString, nil) - } else { - self.loadAndInstallRestoreImage(delegate: delegate, customRestoreImageURL: customRestoreImageURL, progressHandler: progressHandler, completionHandler: completionHandler) - } - } - } - - func loadParametersFromRestoreImage(customRestoreImageURL: URL?, completionHandler: @escaping CompletionHander) { - if let errorString = createBundle() { - completionHandler(errorString) - return - } - - var restoreImageURL = URL.restoreImageURL - if let customRestoreImageURL = customRestoreImageURL { - restoreImageURL = customRestoreImageURL - } - - VZMacOSRestoreImage.load(from: restoreImageURL) { (result: Result) in - switch result { - case .success(let restoreImage): - self.didLoad(restoreImage: restoreImage, completionHandler: completionHandler) - case .failure(let failure): - completionHandler("Error: Could not read restore image: \(failure)") - } - } - } - - func loadAndInstallRestoreImage(delegate: VZVirtualMachineDelegate, customRestoreImageURL: URL?, progressHandler: @escaping ProgressHandler, completionHandler: @escaping InstallCompletionHander) { - var restoreImageURL = URL.restoreImageURL - if let customRestoreImageURL = customRestoreImageURL { - restoreImageURL = customRestoreImageURL - } - - VZMacOSRestoreImage.load(from: restoreImageURL) { (result: Result) in - switch result { - case .success(let restoreImage): - if let errorString = self.restore(from: restoreImage) { - completionHandler(errorString, nil) - } else if let virtualMachineConfiguration = self.virtualMachineConfiguration { - self.startInstall(ipswURL: restoreImageURL, virtualMacConfiguration: virtualMachineConfiguration, delegate: delegate, progressHandler: progressHandler, completionHandler: completionHandler) - } else { - completionHandler("Error: No virtual machine configuration found", nil) - } - case .failure(let failure): - completionHandler("Error: Loading restore image failed: \(failure)", nil) - return - } - } - } - - func createVirtualMachine(delegate: VZVirtualMachineDelegate) -> VZVirtualMachine? { - guard let virtualMacConfiguration = virtualMachineConfiguration else { - return nil - } - virtualMacConfiguration.configure(with: ¶meters) - - do { - try virtualMacConfiguration.validate() - self.virtualMachineConfiguration = virtualMacConfiguration - } catch (let error) { - virtualOSApp.debugLog("Error: \(error.localizedDescription)") - return nil - } - - if let errorString = writeParametersToDisk() { - virtualOSApp.debugLog(errorString) - return nil - } - - let virtualMachine = VZVirtualMachine(configuration: virtualMacConfiguration, queue: .main) - virtualMachine.delegate = delegate - - virtualOSApp.debugLog("Using \(virtualMacConfiguration.cpuCount) cores, \(virtualMacConfiguration.memorySize.bytesToGigabytes()) GB RAM, screen size \(parameters.screenWidth)x\(parameters.screenHeight) px at \(parameters.pixelsPerInch) ppi, shared folder: \(Bookmark.startAccess(data: parameters.sharedFolder, forType: .sharedFolder)?.absoluteString ?? "none")") - - return virtualMachine - } - - func stop(virtualMachine: VZVirtualMachine, completionHandler: @escaping InstallCompletionHander) { - virtualMachine.stop(completionHandler: { (error: Error?) in - if let error = error { - virtualOSApp.debugLog("Error while stopping: \(error)") - completionHandler(error.localizedDescription, nil) - } else { - virtualOSApp.debugLog("Stopped") - completionHandler(nil, virtualMachine) // nil: no error - } - }) - } - - func stopDownload() { - downloadTask?.cancel() - } - - func writeParametersToDisk() -> String? { - let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted - - do { - let jsonData = try encoder.encode(parameters) - if let json = String(data: jsonData, encoding: .utf8) { - try json.write(to: URL.parametersURL, atomically: true, encoding: String.Encoding.utf8) - } - } catch { - return "Error: Failed to write current CPU and RAM configuration to disk" - } - return nil - } - - // MARK: - Private - - fileprivate func readParametersFromDisk() -> String? { - let decoder = JSONDecoder() - do { - let json = try Data.init(contentsOf: URL.parametersURL) - parameters = try decoder.decode(Parameters.self, from: json) - } catch { - return "Error: Failed to read parameters, please delete virtual machine in 'File' menu" - } - return nil - } - - fileprivate func fetchLatestSupportedRestoreImage(progressHandler: @escaping ProgressHandler, completionHandler: @escaping CompletionHander) { - virtualOSApp.debugLog("Attempting to download latest available restore image") - VZMacOSRestoreImage.fetchLatestSupported { [self](result: Result) in - switch result { - case let .failure(error): - completionHandler(error.localizedDescription) - case let .success(restoreImage): - download(restoreImage: restoreImage, progressHandler: progressHandler, completionHandler: completionHandler) - } - } - } - - fileprivate func download(restoreImage: VZMacOSRestoreImage, progressHandler: @escaping ProgressHandler, completionHandler: @escaping CompletionHander) { - let downloadTask = URLSession.shared.downloadTask(with: restoreImage.url) { localURL, response, error in - if let error = error { - completionHandler(error.localizedDescription) - return - } - if let localURL = localURL { - try? FileManager.default.moveItem(at: localURL, to: URL.restoreImageURL) - } else { - completionHandler("Error: Failed to move downloaded restore image to \(URL.restoreImageURL)") - return - } - - completionHandler(nil) // no error - } - - self.downloadTask = downloadTask - - progressObserverCancellable = downloadTask.progress.publisher(for: \.fractionCompleted) - .throttle(for: 1.0, scheduler: RunLoop.main, latest: true) - .sink() { (progress) in - progressHandler(downloadTask.progress) - } - downloadTask.resume() - } - - fileprivate func didLoad(restoreImage: VZMacOSRestoreImage, completionHandler: @escaping CompletionHander) { - let (_, errorString) = readSupportedConfiguration(from: restoreImage) - if errorString != nil { - completionHandler(errorString) - return - } - - virtualMachineConfiguration = VirtualMacConfiguration() - virtualMachineConfiguration?.setDefault(parameters: ¶meters) - virtualOSApp.debugLog("Parameters from restore image: \(parameters)") - - if let errorString = writeParametersToDisk() { - completionHandler(errorString) - return - } - - completionHandler(nil) // no error - } - - fileprivate func restore(from restoreImage: VZMacOSRestoreImage) -> String? { - let (mostFeaturefulSupportedConfiguration, errorString) = readSupportedConfiguration(from: restoreImage) - guard let mostFeaturefulSupportedConfiguration = mostFeaturefulSupportedConfiguration else { - return errorString - } - - if let errorString = VirtualMac.createDiskImage(sizeInGB: parameters.diskSizeInGB) { - return errorString - } - - let virtualMacConfiguration = VirtualMacConfiguration() - virtualMacConfiguration.create(using: ¶meters, macHardwareModel: mostFeaturefulSupportedConfiguration.hardwareModel) - - do { - try virtualMacConfiguration.validate() - virtualMachineConfiguration = virtualMacConfiguration - } catch { - return "Error: Failed to validate virtual machine configuration during install" - } - - return nil - } - - fileprivate func readSupportedConfiguration(from restoreImage: VZMacOSRestoreImage) -> (VZMacOSConfigurationRequirements?, String?) { - let version = restoreImage.operatingSystemVersion - versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion) (Build \(restoreImage.buildVersion))" - virtualOSApp.debugLog("Restore image operating system version: \(versionString)") - - guard let mostFeaturefulSupportedConfiguration = restoreImage.mostFeaturefulSupportedConfiguration else { - return (nil, "Error: Restore image for macOS version \(versionString) is not supported on this machine") - } - guard mostFeaturefulSupportedConfiguration.hardwareModel.isSupported else { - return (nil, "Error: Hardware model required by restore image for macOS version \(versionString) is not supported on this machine") - } - - parameters.cpuCountMin = mostFeaturefulSupportedConfiguration.minimumSupportedCPUCount - parameters.memorySizeInGBMin = mostFeaturefulSupportedConfiguration.minimumSupportedMemorySize.bytesToGigabytes() - - return (mostFeaturefulSupportedConfiguration, nil) // no error - } - - fileprivate func startInstall(ipswURL: URL, virtualMacConfiguration: VirtualMacConfiguration, delegate: VZVirtualMachineDelegate, progressHandler: @escaping ProgressHandler, completionHandler: @escaping InstallCompletionHander) { - self.virtualMachineConfiguration = virtualMacConfiguration - guard let virtualMachine = createVirtualMachine(delegate: delegate) else { - completionHandler("Error: Could not create virtual machine for install", nil) - return - } - - DispatchQueue.main.async { - let installer = VZMacOSInstaller(virtualMachine: virtualMachine, restoringFromImageAt: ipswURL) - - installer.install { result in - switch result { - case .success: - virtualOSApp.debugLog("Install finished") - self.stop(virtualMachine: virtualMachine, completionHandler: completionHandler) - case .failure(let error): - self.progressObserverCancellable?.cancel() - completionHandler("Error: Install failed: \(error).\nPlease select `Delete Virtual Machine` and `Delete Restore Image` from the file menu or use a different restore image and try again.", nil) - } - } - - self.progressObserverCancellable = installer.progress.publisher(for: \.fractionCompleted) - .throttle(for: 1.0, scheduler: RunLoop.main, latest: true) - .sink() { (progress) in - progressHandler(installer.progress) - } - } - } - - fileprivate func createBundle() -> String? { - if FileManager.default.fileExists(atPath: URL.vmBundlePath) { - return nil // already exists - } - - let bundleFileDescriptor = mkdir(URL.vmBundlePath, S_IRWXU | S_IRWXG | S_IRWXO) - if bundleFileDescriptor == -1 { - if errno == EEXIST { - return "Failed to create VM bundle: the base directory already exists" - } - return "Failed to create VM bundle at \(URL.vmBundlePath) (error number \(errno))" - } - - let result = close(bundleFileDescriptor) - if result != 0 { - virtualOSApp.debugLog("Failed to close VM bundle (\(result))") - } - - return nil // no error - } - - static func createDiskImage(sizeInGB: UInt64) -> String? { - let diskImageFileDescriptor = open(URL.diskImageURL.path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR) - if diskImageFileDescriptor == -1 { - return "Error: Cannot create disk image" - } - - let diskSize = sizeInGB.gigabytesToBytes() - var result = ftruncate(diskImageFileDescriptor, Int64(diskSize)) - if result != 0 { - return "Error: Expanding disk image failed" - } - - result = close(diskImageFileDescriptor) - if result != 0 { - return "Error: Failed to close the disk image" - } - - return nil // no error - } -} - -#endif diff --git a/virtualOS/Model/VirtualMacConfiguration.swift b/virtualOS/Model/VirtualMacConfiguration.swift deleted file mode 100644 index 6bcb99f..0000000 --- a/virtualOS/Model/VirtualMacConfiguration.swift +++ /dev/null @@ -1,206 +0,0 @@ -// -// VirtualMacConfiguration.swift -// virtualOS -// -// Created by Jahn Bertsch on 16.03.22. -// - -#if arch(arm64) - -import Virtualization -import AVFoundation - -final class VirtualMacConfiguration: VZVirtualMachineConfiguration { - fileprivate(set) var machineIdentifier = VZMacMachineIdentifier() - - func create(using parameters: inout VirtualMac.Parameters, macHardwareModel: VZMacHardwareModel) { - if !configurePlatform(parameters: parameters, macHardwareModel: macHardwareModel) { - return // error - } - configure(with: ¶meters) - } - - func readFromDisk(using parameters: inout VirtualMac.Parameters) { - let (errorString, platform) = readPlaformFromDisk() - if let errorString = errorString { - virtualOSApp.debugLog(errorString) - } else if let platform = platform { - self.platform = platform - configure(with: ¶meters) - } else { - virtualOSApp.debugLog("Error: Reading platform from disk failed") - } - } - - func setDefault(parameters: inout VirtualMac.Parameters) { - let cpuCountMax = computeCPUCount() - let bytesMax = VZVirtualMachineConfiguration.maximumAllowedMemorySize - let bytesMaxMinus2GB = bytesMax - UInt64(2).gigabytesToBytes() // substract 2 GB - - cpuCount = cpuCountMax - 1 // substract one core - memorySize = bytesMaxMinus2GB - - parameters.cpuCount = cpuCount - parameters.cpuCountMax = cpuCountMax - parameters.memorySizeInGB = memorySize.bytesToGigabytes() - parameters.memorySizeInGBMax = bytesMax.bytesToGigabytes() - } - - func configure(with parameters: inout VirtualMac.Parameters) { - cpuCount = parameters.cpuCount - memorySize = parameters.memorySizeInGB.gigabytesToBytes() - pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()] - entropyDevices = [VZVirtioEntropyDeviceConfiguration()] - bootLoader = VZMacOSBootLoader() - keyboards = [VZUSBKeyboardConfiguration()] - - configureAudioDevice(parameters: parameters) - configureGraphicsDevice(parameters: parameters) - configureStorageDevice(parameters: parameters) - configureNetworkDevices(parameters: parameters) - if #available(macOS 13.0, *) { - configureSharedFolder(parameters: parameters) - } - } - - // MARK: - Private - - fileprivate func configurePlatform(parameters: VirtualMac.Parameters, macHardwareModel: VZMacHardwareModel) -> Bool { - let platformConfiguration = VZMacPlatformConfiguration() - platformConfiguration.hardwareModel = macHardwareModel - - do { - platformConfiguration.auxiliaryStorage = try VZMacAuxiliaryStorage( - creatingStorageAt: URL.auxiliaryStorageURL, - hardwareModel: macHardwareModel, - options: [.allowOverwrite] - ) - } catch { - virtualOSApp.debugLog("Error: could not create auxiliary storage device") - return false - } - - do { - try platformConfiguration.hardwareModel.dataRepresentation.write(to: URL.hardwareModelURL) - try platformConfiguration.machineIdentifier.dataRepresentation.write(to: URL.machineIdentifierURL) - } catch { - virtualOSApp.debugLog("Error: could store platform information to disk") - return false - } - - platform = platformConfiguration - return true // success - } - - fileprivate func configureNetworkDevices(parameters: VirtualMac.Parameters) { - let networkDevice = VZVirtioNetworkDeviceConfiguration() - let networkAttachment = VZNATNetworkDeviceAttachment() - networkDevice.attachment = networkAttachment - networkDevice.macAddress = VZMACAddress(string: parameters.macAddress) ?? .randomLocallyAdministered() - networkDevices = [networkDevice] - } - - fileprivate func configureAudioDevice(parameters: VirtualMac.Parameters) { - let audioDevice = VZVirtioSoundDeviceConfiguration() - - if parameters.microphoneEnabled { - AVCaptureDevice.requestAccess(for: .audio) { (granted: Bool) in - virtualOSApp.debugLog("Microphone request granted: \(granted)") - } - - let inputStreamConfiguration = VZVirtioSoundDeviceInputStreamConfiguration() - inputStreamConfiguration.source = VZHostAudioInputStreamSource() - audioDevice.streams.append(inputStreamConfiguration) - } - - let outputStreamConfiguration = VZVirtioSoundDeviceOutputStreamConfiguration() - outputStreamConfiguration.sink = VZHostAudioOutputStreamSink() - audioDevice.streams.append(outputStreamConfiguration) - - audioDevices = [audioDevice] - } - - fileprivate func configureGraphicsDevice(parameters: VirtualMac.Parameters) { - let graphicsDevice = VZMacGraphicsDeviceConfiguration() - if parameters.useMainScreenSize, let mainScreen = NSScreen.main { - graphicsDevice.displays = [VZMacGraphicsDisplayConfiguration(for: mainScreen, sizeInPoints: NSSize(width: parameters.screenWidth, height: parameters.screenHeight))] - } else { - graphicsDevice.displays = [VZMacGraphicsDisplayConfiguration( - widthInPixels: parameters.screenWidth, - heightInPixels: parameters.screenHeight, - pixelsPerInch: parameters.pixelsPerInch - )] - } - graphicsDevices = [graphicsDevice] - } - - fileprivate func configureStorageDevice(parameters: VirtualMac.Parameters) { - if let diskImageStorageDeviceAttachment = try? VZDiskImageStorageDeviceAttachment(url: URL.diskImageURL, readOnly: false) { - let blockDeviceConfiguration = VZVirtioBlockDeviceConfiguration(attachment: diskImageStorageDeviceAttachment) - storageDevices = [blockDeviceConfiguration] - } else { - virtualOSApp.debugLog("Error: could not create storage device") - } - } - - @available(macOS 13.0, *) - fileprivate func configureSharedFolder(parameters: VirtualMac.Parameters) { - guard let hardDiskDirectoryBookmarkData = Bookmark.startAccess(data: parameters.sharedFolder, forType: .hardDisk) else { - return - } - - let sharedDirectory = VZSharedDirectory(url: hardDiskDirectoryBookmarkData, readOnly: false) - let singleDirectoryShare = VZSingleDirectoryShare(directory: sharedDirectory) - let sharingConfiguration = VZVirtioFileSystemDeviceConfiguration(tag: VZVirtioFileSystemDeviceConfiguration.macOSGuestAutomountTag) - sharingConfiguration.share = singleDirectoryShare - - directorySharingDevices = [sharingConfiguration] - } - - fileprivate func computeCPUCount() -> Int { - let totalAvailableCPUs = ProcessInfo.processInfo.processorCount - - var virtualCPUCount = totalAvailableCPUs <= 1 ? 1 : totalAvailableCPUs - virtualCPUCount = max(virtualCPUCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount) - virtualCPUCount = min(virtualCPUCount, VZVirtualMachineConfiguration.maximumAllowedCPUCount) - - return virtualCPUCount - } - - fileprivate func readPlaformFromDisk() -> (String?, VZMacPlatformConfiguration?) { - let macPlatform = VZMacPlatformConfiguration() - - let auxiliaryStorage = VZMacAuxiliaryStorage(contentsOf: URL.auxiliaryStorageURL) - macPlatform.auxiliaryStorage = auxiliaryStorage - - if !FileManager.default.fileExists(atPath: URL.vmBundlePath) { - return ("Error: Missing virtual machine bundle at \(URL.vmBundlePath).", nil) - } - - guard let hardwareModelData = try? Data(contentsOf: URL.hardwareModelURL) else { - return ("Error: Failed to retrieve hardware model data", nil) - } - - guard let hardwareModel = VZMacHardwareModel(dataRepresentation: hardwareModelData) else { - return ("Error: Failed to create hardware model", nil) - } - - if !hardwareModel.isSupported { - return ("Error: The hardware model is not supported on the current host", nil) - } - macPlatform.hardwareModel = hardwareModel - - guard let machineIdentifierData = try? Data(contentsOf: URL.machineIdentifierURL) else { - return ("Error: Failed to retrieve machine identifier data.", nil) - } - - guard let machineIdentifier = VZMacMachineIdentifier(dataRepresentation: machineIdentifierData) else { - return ("Error: Failed to create machine identifier.", nil) - } - macPlatform.machineIdentifier = machineIdentifier - - return (nil, macPlatform) - } -} - -#endif diff --git a/virtualOS/Preview Content/Preview Assets.xcassets/Contents.json b/virtualOS/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/virtualOS/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/virtualOS/Assets.xcassets/AccentColor.colorset/Contents.json b/virtualOS/Resources/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from virtualOS/Assets.xcassets/AccentColor.colorset/Contents.json rename to virtualOS/Resources/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/virtualOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 62% rename from virtualOS/Assets.xcassets/AppIcon.appiconset/Contents.json rename to virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 6b2539c..58bc712 100644 --- a/virtualOS/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { "images" : [ { - "size" : "16x16", - "idiom" : "mac", "filename" : "virtualOS-macOS-16.png", - "scale" : "1x" + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" }, { - "size" : "16x16", - "idiom" : "mac", "filename" : "virtualOS-macOS-32.png", - "scale" : "2x" + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" }, { - "size" : "32x32", - "idiom" : "mac", "filename" : "virtualOS-macOS-32.png", - "scale" : "1x" + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" }, { - "size" : "32x32", - "idiom" : "mac", "filename" : "virtualOS-macOS-64.png", - "scale" : "2x" + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" }, { - "size" : "128x128", - "idiom" : "mac", "filename" : "virtualOS-macOS-128.png", - "scale" : "1x" + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" }, { - "size" : "128x128", - "idiom" : "mac", "filename" : "virtualOS-macOS-256.png", - "scale" : "2x" + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" }, { - "size" : "256x256", - "idiom" : "mac", "filename" : "virtualOS-macOS-256.png", - "scale" : "1x" + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" }, { - "size" : "256x256", - "idiom" : "mac", "filename" : "virtualOS-macOS-512.png", - "scale" : "2x" + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" }, { - "size" : "512x512", - "idiom" : "mac", "filename" : "virtualOS-macOS-512.png", - "scale" : "1x" + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" }, { - "size" : "512x512", - "idiom" : "mac", "filename" : "virtualOS-macOS-1024.png", - "scale" : "2x" + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-1024.png b/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-1024.png similarity index 100% rename from virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-1024.png rename to virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-1024.png diff --git a/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-128.png b/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-128.png similarity index 100% rename from virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-128.png rename to virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-128.png diff --git a/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-16.png b/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-16.png similarity index 100% rename from virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-16.png rename to virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-16.png diff --git a/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-256.png b/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-256.png similarity index 100% rename from virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-256.png rename to virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-256.png diff --git a/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-32.png b/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-32.png similarity index 100% rename from virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-32.png rename to virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-32.png diff --git a/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-512.png b/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-512.png similarity index 100% rename from virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-512.png rename to virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-512.png diff --git a/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-64.png b/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-64.png similarity index 100% rename from virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-64.png rename to virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-64.png diff --git a/virtualOS/Assets.xcassets/Contents.json b/virtualOS/Resources/Assets.xcassets/Contents.json similarity index 100% rename from virtualOS/Assets.xcassets/Contents.json rename to virtualOS/Resources/Assets.xcassets/Contents.json diff --git a/virtualOS/Resources/Base.lproj/Main.storyboard b/virtualOS/Resources/Base.lproj/Main.storyboard new file mode 100644 index 0000000..25ce484 --- /dev/null +++ b/virtualOS/Resources/Base.lproj/Main.storyboard @@ -0,0 +1,1252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + line 1 +line 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/virtualOS/virtualOS.entitlements b/virtualOS/Resources/virtualOS.entitlements similarity index 100% rename from virtualOS/virtualOS.entitlements rename to virtualOS/Resources/virtualOS.entitlements index cf5b2e0..a00bf5a 100644 --- a/virtualOS/virtualOS.entitlements +++ b/virtualOS/Resources/virtualOS.entitlements @@ -4,11 +4,11 @@ com.apple.security.app-sandbox - com.apple.security.virtualization + com.apple.security.files.user-selected.read-write com.apple.security.network.client - com.apple.security.files.user-selected.read-write + com.apple.security.virtualization diff --git a/virtualOS/RestoreImage/RestoreImageDownload.swift b/virtualOS/RestoreImage/RestoreImageDownload.swift new file mode 100644 index 0000000..78818fc --- /dev/null +++ b/virtualOS/RestoreImage/RestoreImageDownload.swift @@ -0,0 +1,106 @@ +// +// Download.swift +// virtualOS +// +// Created by Jahn Bertsch +// + +import Virtualization +import Combine +import OSLog + +#if arch(arm64) + +protocol ProgressDelegate: AnyObject { + func progress(_ progress: Double, progressString: String) + func done() +} + +final class RestoreImageDownload { + weak var delegate: ProgressDelegate? + fileprivate var observation: NSKeyValueObservation? + fileprivate var downloadTask: URLSessionDownloadTask? + fileprivate var downloading = true + fileprivate let logger = Logger.init(subsystem: "com.github.virtualOS", category: "log") + + deinit { + observation?.invalidate() + } + + func fetch() { + VZMacOSRestoreImage.fetchLatestSupported { [self](result: Result) in + switch result { + case let .success(restoreImage): + download(restoreImage: restoreImage) + case let .failure(error): + logger.log(level: .default, "failure: \(error.localizedDescription)") + delegate?.done() + } + } + } + + func cancel() { + downloadTask?.cancel() + } + + // MARK: - Private + + fileprivate func download(restoreImage: VZMacOSRestoreImage) { + logger.log(level: .default, "fetched, macOS \(restoreImage.operatingSystemVersionString)") + + let downloadTask = URLSession.shared.downloadTask(with: restoreImage.url) {localUrl, response, error in + self.downloading = false + self.downloadFinished(localURL: localUrl, error: error) + } + observation = downloadTask.progress.observe(\.fractionCompleted) { _, _ in } + downloadTask.resume() + self.downloadTask = downloadTask + + logger.log(level: .default, "downloading") + + func printProgress() { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in + let progressString: String + + if let byteCompletedCount = downloadTask.progress.userInfo[ProgressUserInfoKey("NSProgressByteCompletedCountKey")] as? Int, + let byteTotalCount = downloadTask.progress.userInfo[ProgressUserInfoKey("NSProgressByteTotalCountKey")] as? Int + { + let mbCompleted = byteCompletedCount / (1024 * 1024) + let mbTotal = byteTotalCount / (1024 * 1024) + progressString = "Restore Image\nDownloading \(Int(downloadTask.progress.fractionCompleted * 100))% (\(mbCompleted) of \(mbTotal) MB)" + } else { + progressString = "Restore Image\nDownloading \(Int(downloadTask.progress.fractionCompleted * 100))%" + } + self?.logger.log(level: .default, "\(progressString)") + + self?.delegate?.progress(downloadTask.progress.fractionCompleted, progressString: progressString) + + if let downloading = self?.downloading, downloading { + printProgress() + } + } + } + printProgress() + } + + fileprivate func downloadFinished(localURL: URL?, error: Error?) { + logger.log(level: .default, "download finished") + delegate?.progress(100, progressString: "Error") + + if let error = error { + logger.log(level: .default, "\(error.localizedDescription)") + return + } + + if let localURL = localURL { + try? FileManager.default.moveItem(at: localURL, to: URL.restoreImageURL) + logger.log(level: .default, "moved restore image to \(URL.restoreImageURL)") + delegate?.done() + } else { + logger.log(level: .default, "failed to move downloaded restore image to \(URL.restoreImageURL)") + return + } + } +} + +#endif diff --git a/virtualOS/RestoreImage/RestoreImageInstall.swift b/virtualOS/RestoreImage/RestoreImageInstall.swift new file mode 100644 index 0000000..af77c3a --- /dev/null +++ b/virtualOS/RestoreImage/RestoreImageInstall.swift @@ -0,0 +1,257 @@ +// +// RestoreImageInstall.swift +// virtualOS +// +// Created by Jahn Bertsch +// + +import Foundation +import Virtualization +import OSLog + +#if arch(arm64) + +final class RestoreImageInstall { + weak var delegate: ProgressDelegate? + var restoreImageName: String? + var diskImageSize: Int? + + fileprivate var observation: NSKeyValueObservation? + fileprivate var installing = true + fileprivate var installer: VZMacOSInstaller? + fileprivate let queue = DispatchQueue.global(qos: .userInteractive) + fileprivate let logger = Logger.init(subsystem: "com.github.virtualOS", category: "log") + + deinit { + observation?.invalidate() + } + + func install() { + let restoreImageURL: URL + if let restoreImageName { + restoreImageURL = URL.baseURL.appendingPathComponent(restoreImageName) + } else { + restoreImageURL = URL.restoreImageURL + } + + if !FileManager.default.fileExists(atPath: restoreImageURL.path) { + logger.log(level: .default, "no restore image") + delegate?.progress(0, progressString: "Error: No restore image") + return + } + + loadParametersFromRestoreImage(restoreImageURL: restoreImageURL) + } + + func cancel() { + queue.async { [weak self] in + self?.installer?.virtualMachine.stop(completionHandler: { error in + if let error { + self?.logger.log(level: .default, "vm stopped with error: \(error.localizedDescription)") + } else { + self?.logger.log(level: .default, "vm stopped") + } + }) + } + } + + // MARK: - Private + + fileprivate func loadParametersFromRestoreImage(restoreImageURL: URL?) { + let bundleURl = createBundleURL() + if !createBundle(at: bundleURl) { + return // error + } + + guard let restoreImageURL else { + return + } + + VZMacOSRestoreImage.load(from: restoreImageURL) { (result: Result) in + switch result { + case .success(let restoreImage): + self.restoreImageDidLoad(restoreImage: restoreImage, bundleURL: bundleURl) + case .failure(let failure): + self.logger.log(level: .default, "could not load restore image: \(failure)") + } + } + } + + fileprivate func restoreImageDidLoad(restoreImage: VZMacOSRestoreImage, bundleURL: URL) { + var versionString = "" + guard let macPlatformConfiguration = MacPlatformConfiguration.createDefault(fromRestoreImage: restoreImage, versionString: &versionString, bundleURL: bundleURL) else { + return + } + + var vmParameters = VMParameters() + let vmConfiguration = VMConfiguration() + vmConfiguration.platform = macPlatformConfiguration + + if let diskImageSize = diskImageSize { + vmParameters.diskSizeInGB = UInt64(diskImageSize) + if createDiskImage(diskImageURL: bundleURL.diskImageURL, sizeInGB: UInt64(vmParameters.diskSizeInGB)) { + return + } + } + + vmConfiguration.setDefault(parameters: &vmParameters) + vmConfiguration.setup(parameters: vmParameters, macPlatformConfiguration: macPlatformConfiguration, bundleURL: bundleURL) + + vmParameters.version = restoreImage.operatingSystemVersionString + vmParameters.writeToDisk(bundleURL: bundleURL) + + do { + try vmConfiguration.validate() + logger.log(level: .default, "vm configuration is valid") + } catch let error { + logger.log(level: .default, "failed to validate vm configuration: \(error.localizedDescription)") + return + } + + startInstall(vmConfiguration: vmConfiguration, versionString: versionString) + } + + fileprivate func startInstall(vmConfiguration: VMConfiguration, versionString: String) { + let vm = VZVirtualMachine(configuration: vmConfiguration, queue: queue) + + var restoreImageURL = URL.restoreImageURL + if let restoreImageName { + // use custom restore image + restoreImageURL = URL.baseURL.appendingPathComponent(restoreImageName) + } + + queue.async { [weak self] in + let installer = VZMacOSInstaller(virtualMachine: vm, restoringFromImageAt: restoreImageURL) + self?.installer = installer + + installer.install { result in + self?.installing = false + switch result { + case .success(): + self?.installFinisehd(installer: installer) + case .failure(let error): + self?.logger.log(level: .default, "install error: \(error.localizedDescription)") + DispatchQueue.main.async { [weak self] in + self?.delegate?.done() + } + } + } + + self?.observation = installer.progress.observe(\.fractionCompleted) { _, _ in } + + func printProgress() { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in + var progressString = "Installing \(Int(installer.progress.fractionCompleted * 100))%" + if installer.progress.fractionCompleted == 0 { + progressString += " (Please wait)" + } + progressString += "\n\(versionString)" + // logger.log(level: .default, progressString) + + if let installing = self?.installing, installing { + self?.delegate?.progress(installer.progress.fractionCompleted, progressString: progressString) + printProgress() + } + } + } + printProgress() + } + } + + fileprivate func installFinisehd(installer: VZMacOSInstaller) { + logger.log(level: .default, "Install finished") + + installing = false + + DispatchQueue.main.async { [weak self] in + self?.delegate?.progress(installer.progress.fractionCompleted, progressString: "Install finished") + } + delegate?.done() + + if installer.virtualMachine.canStop { + queue.async { + installer.virtualMachine.stop(completionHandler: { error in + if let error { + self.logger.log(level: .default, "Error stopping VM: \(error)") + } + }) + } + } + } + + fileprivate func createBundleURL() -> URL { + var url = URL.vmBundleURL + + // try to find a filename that does not exist + var exists = true + var i = 1 + while exists { + var filename = url.lastPathComponent + filename = filename.replacingOccurrences(of: ".bundle", with: "") + let filenameComponents = filename.split(separator: "_") + if filenameComponents.count > 0 { + filename = String(filenameComponents[0]) + } + filename += "_\(i).bundle" + + url = URL(fileURLWithPath: URL.baseURL.appendingPathComponent(filename, conformingTo: .bundle).path()) + + if FileManager.default.fileExists(atPath: url.path()) { + i += 1 + } else { + exists = false + } + } + logger.log(level: .default, "using bundle url \(url.lastPathComponent)") + return url + } + + fileprivate func createBundle(at bundleURl: URL) -> Bool { + if FileManager.default.fileExists(atPath: bundleURl.path()) { + return true // already exists, no error + } + + let bundleFileDescriptor = mkdir(bundleURl.path(), S_IRWXU | S_IRWXG | S_IRWXO) + if bundleFileDescriptor == -1 { + if errno == EEXIST { + logger.log(level: .default, "failed to create vm bundle: the base directory already exists") + } + logger.log(level: .default, "failed to create vm bundle at \(bundleURl.path()) (error number \(errno))") + return false // error + } + + let result = close(bundleFileDescriptor) + if result != 0 { + logger.log(level: .default, "failed to close vm bundle (\(result))") + return false // error + } + + return true // no error + } + + fileprivate func createDiskImage(diskImageURL: URL, sizeInGB: UInt64) -> Bool { + let diskImageFileDescriptor = open(diskImageURL.path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR) + if diskImageFileDescriptor == -1 { + logger.log(level: .default, "Error: Cannot create disk image") + return false // failure + } + + let diskSize = sizeInGB.gigabytesToBytes() + var result = ftruncate(diskImageFileDescriptor, Int64(diskSize)) + if result != 0 { + logger.log(level: .default, "Error: Expanding disk image failed") + return false // failure + } + + result = close(diskImageFileDescriptor) + if result != 0 { + logger.log(level: .default, "Error: Failed to close the disk image") + return false // failure + } + + return false // failure + } + +} + +#endif diff --git a/virtualOS/VM/MacPlatformConfiguration.swift b/virtualOS/VM/MacPlatformConfiguration.swift new file mode 100644 index 0000000..c833245 --- /dev/null +++ b/virtualOS/VM/MacPlatformConfiguration.swift @@ -0,0 +1,110 @@ +// +// MacPlatformConfiguration.swift +// virtualOS +// +// Created by Jahn Bertsch +// + +import Virtualization +import OSLog + +#if arch(arm64) + +final class MacPlatformConfiguration: VZMacPlatformConfiguration { + var versionString: String? + fileprivate let logger = Logger.init(subsystem: "com.github.virtualOS", category: "log") + + static func read(fromBundleURL bundleURL: URL) -> VZMacPlatformConfiguration? { + let logger = Logger.init(subsystem: "com.github.virtualOS", category: "log") + + let macPlatformConfiguration = MacPlatformConfiguration() + + let auxiliaryStorage = VZMacAuxiliaryStorage(contentsOf: bundleURL.auxiliaryStorageURL) + macPlatformConfiguration.auxiliaryStorage = auxiliaryStorage + + guard let hardwareModelData = try? Data(contentsOf: bundleURL.hardwareModelURL) else { + logger.log(level: .default, "Error: Failed to retrieve hardware model data") + return nil + } + + guard let hardwareModel = VZMacHardwareModel(dataRepresentation: hardwareModelData) else { + logger.log(level: .default, "Error: Failed to create hardware model") + return nil + } + + if !hardwareModel.isSupported { + logger.log(level: .default, "Error: The hardware model is not supported on the current host") + return nil + } + macPlatformConfiguration.hardwareModel = hardwareModel + + guard let machineIdentifierData = try? Data(contentsOf: bundleURL.machineIdentifierURL) else { + logger.log(level: .default, "Error: Failed to retrieve machine identifier data.") + return nil + } + + guard let machineIdentifier = VZMacMachineIdentifier(dataRepresentation: machineIdentifierData) else { + logger.log(level: .default, "Error: Failed to create machine identifier.") + return nil + } + macPlatformConfiguration.machineIdentifier = machineIdentifier + + return macPlatformConfiguration + } + + static func createDefault(fromRestoreImage restoreImage: VZMacOSRestoreImage, versionString: inout String, bundleURL: URL) -> VZMacPlatformConfiguration? { + let logger = Logger.init(subsystem: "com.github.virtualOS", category: "log") + let macPlatformConfiguration = MacPlatformConfiguration() + + versionString = restoreImage.operatingSystemVersionString + let versionString = versionString + logger.log(level: .default, "restore image operating system version: \(versionString)") + + guard let mostFeaturefulSupportedConfiguration = restoreImage.mostFeaturefulSupportedConfiguration else { + logger.log(level: .default, "restore image for macOS version \(versionString) is not supported on this machine") + return nil + } + guard mostFeaturefulSupportedConfiguration.hardwareModel.isSupported else { + logger.log(level: .default, "hardware model required by restore image for macOS version \(versionString) is not supported on this machine") + return macPlatformConfiguration + } + + let auxiliaryStorage = VZMacAuxiliaryStorage(contentsOf: URL.baseURL.auxiliaryStorageURL) + macPlatformConfiguration.auxiliaryStorage = auxiliaryStorage + + guard let macPlatformConfiguration = macPlatformConfiguration.createPlatformConfiguration(macHardwareModel: mostFeaturefulSupportedConfiguration.hardwareModel, bundleURL: bundleURL) else { + return nil + } + + var vmParameters = VMParameters() + vmParameters.cpuCountMin = mostFeaturefulSupportedConfiguration.minimumSupportedCPUCount + vmParameters.memorySizeInGBMin = mostFeaturefulSupportedConfiguration.minimumSupportedMemorySize.bytesToGigabytes() + + return macPlatformConfiguration + } + + fileprivate func createPlatformConfiguration(macHardwareModel: VZMacHardwareModel, bundleURL: URL) -> VZMacPlatformConfiguration? { + let platformConfiguration = VZMacPlatformConfiguration() + platformConfiguration.hardwareModel = macHardwareModel + + do { + platformConfiguration.auxiliaryStorage = try VZMacAuxiliaryStorage(creatingStorageAt: bundleURL.auxiliaryStorageURL, hardwareModel: macHardwareModel, options: [.allowOverwrite] + ) + } catch { + logger.log(level: .default, "Error: could not create auxiliary storage device") + return nil + } + + do { + try platformConfiguration.hardwareModel.dataRepresentation.write(to: bundleURL.hardwareModelURL) + try platformConfiguration.machineIdentifier.dataRepresentation.write(to: bundleURL.machineIdentifierURL) + } catch { + logger.log(level: .default, "could store platform information to disk") + return nil + } + + return platformConfiguration // success + } +} + +#endif diff --git a/virtualOS/VM/VMConfiguration.swift b/virtualOS/VM/VMConfiguration.swift new file mode 100644 index 0000000..36cd2d1 --- /dev/null +++ b/virtualOS/VM/VMConfiguration.swift @@ -0,0 +1,151 @@ +// +// VM.swift +// virtualOS +// +// Created by Jahn Bertsch. +// + +#if arch(arm64) + +import Virtualization +import AVFoundation // for audio +import OSLog + +final class VMConfiguration: VZVirtualMachineConfiguration { + fileprivate let logger = Logger.init(subsystem: "com.github.virtualOS", category: "log") + + func setup(parameters: VMParameters, macPlatformConfiguration: VZMacPlatformConfiguration, bundleURL: URL) { + cpuCount = parameters.cpuCount + memorySize = parameters.memorySizeInGB.gigabytesToBytes() + pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()] + entropyDevices = [VZVirtioEntropyDeviceConfiguration()] + keyboards = [VZUSBKeyboardConfiguration()] + bootLoader = VZMacOSBootLoader() + + configureAudioDevice(parameters: parameters) + configureGraphicsDevice(parameters: parameters) + configureStorageDevice(parameters: parameters, bundleURL: bundleURL) + configureNetworkDevices(parameters: parameters) + configureSharedFolder(parameters: parameters) + configureClipboardSharing(parameters: parameters) + + platform = macPlatformConfiguration + } + + fileprivate func configureClipboardSharing(parameters: VMParameters) { + let consoleDevice = VZVirtioConsoleDeviceConfiguration() + + let spiceAgentPort = VZVirtioConsolePortConfiguration() + spiceAgentPort.name = VZSpiceAgentPortAttachment.spiceAgentPortName + spiceAgentPort.attachment = VZSpiceAgentPortAttachment() + consoleDevice.ports[0] = spiceAgentPort + + consoleDevices.append(consoleDevice) + } + + func setDefault(parameters: inout VMParameters) { + let cpuCountMax = computeCPUCount() + let bytesMax = VZVirtualMachineConfiguration.maximumAllowedMemorySize + let bytesMaxMinus2GB = bytesMax - UInt64(2).gigabytesToBytes() // substract 2 GB + + cpuCount = cpuCountMax - 1 // substract one core + memorySize = bytesMaxMinus2GB + + parameters.cpuCount = cpuCount + parameters.cpuCountMax = cpuCountMax + parameters.memorySizeInGB = memorySize.bytesToGigabytes() + parameters.memorySizeInGBMax = bytesMax.bytesToGigabytes() + } + + // MARK: - Private + + fileprivate func configureAudioDevice(parameters: VMParameters) { + let audioDevice = VZVirtioSoundDeviceConfiguration() + + if parameters.microphoneEnabled { + AVCaptureDevice.requestAccess(for: .audio) { (granted: Bool) in + self.logger.log(level: .default, "Microphone request granted: \(granted)") + } + + let inputStreamConfiguration = VZVirtioSoundDeviceInputStreamConfiguration() + inputStreamConfiguration.source = VZHostAudioInputStreamSource() + audioDevice.streams.append(inputStreamConfiguration) + } + + let outputStreamConfiguration = VZVirtioSoundDeviceOutputStreamConfiguration() + outputStreamConfiguration.sink = VZHostAudioOutputStreamSink() + audioDevice.streams.append(outputStreamConfiguration) + + audioDevices = [audioDevice] + } + + fileprivate func configureGraphicsDevice(parameters: VMParameters) { + let graphicsDevice = VZMacGraphicsDeviceConfiguration() + if parameters.useMainScreenSize, let mainScreen = NSScreen.main { + graphicsDevice.displays = [VZMacGraphicsDisplayConfiguration(for: mainScreen, sizeInPoints: NSSize(width: parameters.screenWidth, height: parameters.screenHeight))] + } else { + graphicsDevice.displays = [VZMacGraphicsDisplayConfiguration( + widthInPixels: parameters.screenWidth, + heightInPixels: parameters.screenHeight, + pixelsPerInch: parameters.pixelsPerInch + )] + } + graphicsDevices = [graphicsDevice] + } + + fileprivate func configureStorageDevice(parameters: VMParameters, bundleURL: URL) { + let diskImageStorageDeviceAttachment: VZDiskImageStorageDeviceAttachment? + do { + diskImageStorageDeviceAttachment = try VZDiskImageStorageDeviceAttachment(url: bundleURL.diskImageURL, readOnly: false) + } catch let error { + logger.log(level: .default, "could not create storage device: \(error.localizedDescription)") + return + } + + if let diskImageStorageDeviceAttachment { + let blockDeviceConfiguration = VZVirtioBlockDeviceConfiguration(attachment: diskImageStorageDeviceAttachment) + storageDevices = [blockDeviceConfiguration] + } + + if let diskImageStorageDeviceAttachment = try? VZDiskImageStorageDeviceAttachment(url: bundleURL.diskImageURL, readOnly: false) { + let blockDeviceConfiguration = VZVirtioBlockDeviceConfiguration(attachment: diskImageStorageDeviceAttachment) + storageDevices = [blockDeviceConfiguration] + } else { + logger.log(level: .default, "Error: could not create storage device") + } + } + + fileprivate func configureNetworkDevices(parameters: VMParameters) { + let networkDevice = VZVirtioNetworkDeviceConfiguration() + let networkAttachment = VZNATNetworkDeviceAttachment() + networkDevice.attachment = networkAttachment + networkDevice.macAddress = VZMACAddress(string: parameters.macAddress) ?? .randomLocallyAdministered() + networkDevices = [networkDevice] + } + + fileprivate func configureSharedFolder(parameters: VMParameters) { + guard let sharedFolderBookmarkData = Bookmark.startAccess(bookmarkData: parameters.sharedFolder) else { + return + } + + let sharedDirectory = VZSharedDirectory(url: sharedFolderBookmarkData, readOnly: false) + let singleDirectoryShare = VZSingleDirectoryShare(directory: sharedDirectory) + let sharingConfiguration = VZVirtioFileSystemDeviceConfiguration(tag: VZVirtioFileSystemDeviceConfiguration.macOSGuestAutomountTag) + sharingConfiguration.share = singleDirectoryShare + + directorySharingDevices = [sharingConfiguration] + } + + fileprivate func computeCPUCount() -> Int { + let totalAvailableCPUs = ProcessInfo.processInfo.processorCount + + var virtualCPUCount = totalAvailableCPUs <= 1 ? 1 : totalAvailableCPUs + virtualCPUCount = max(virtualCPUCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount) + virtualCPUCount = min(virtualCPUCount, VZVirtualMachineConfiguration.maximumAllowedCPUCount) + + return virtualCPUCount + } + +} + +#endif diff --git a/virtualOS/VM/VMParameters.swift b/virtualOS/VM/VMParameters.swift new file mode 100644 index 0000000..4a44ee2 --- /dev/null +++ b/virtualOS/VM/VMParameters.swift @@ -0,0 +1,78 @@ +// +// VMParameters.swift +// virtualOS +// +// Created by Jahn Bertsch. +// + +#if arch(arm64) + +import Virtualization + +struct VMParameters: Codable { + var cpuCount = 1 + var cpuCountMin = 1 + var cpuCountMax = 2 + var diskSizeInGB: UInt64 = UInt64(UserDefaults.standard.diskSize) + var memorySizeInGB: UInt64 = 1 + var memorySizeInGBMin: UInt64 = 1 + var memorySizeInGBMax: UInt64 = 2 + var useMainScreenSize = true + var screenWidth = 1500 + var screenHeight = 900 + var pixelsPerInch = 250 + var microphoneEnabled = false + var sharedFolder: Data? + var macAddress = VZMACAddress.randomLocallyAdministered().string + var version = "" + + init() {} + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + cpuCount = try container.decode(Int.self, forKey: .cpuCount) + cpuCountMin = try container.decode(Int.self, forKey: .cpuCountMin) + cpuCountMax = try container.decode(Int.self, forKey: .cpuCountMax) + diskSizeInGB = try container.decode(UInt64.self, forKey: .diskSizeInGB) + memorySizeInGB = try container.decode(UInt64.self, forKey: .memorySizeInGB) + memorySizeInGBMin = try container.decode(UInt64.self, forKey: .memorySizeInGBMin) + memorySizeInGBMax = try container.decode(UInt64.self, forKey: .memorySizeInGBMax) + useMainScreenSize = try container.decodeIfPresent(Bool.self, forKey: .useMainScreenSize) ?? true // optional + screenWidth = try container.decode(Int.self, forKey: .screenWidth) + screenHeight = try container.decode(Int.self, forKey: .screenHeight) + pixelsPerInch = try container.decode(Int.self, forKey: .pixelsPerInch) + microphoneEnabled = try container.decode(Bool.self, forKey: .microphoneEnabled) + sharedFolder = try container.decodeIfPresent(Data.self, forKey: .sharedFolder) ?? nil // optional + macAddress = try container.decodeIfPresent(String.self, forKey: .macAddress) ?? VZMACAddress.randomLocallyAdministered().string // optional + version = try container.decodeIfPresent(String.self, forKey: .version) ?? "" // optional + + } + + static func readFrom(url: URL) -> VMParameters? { + let decoder = JSONDecoder() + do { + let json = try Data.init(contentsOf: url.appendingPathComponent("Parameters.txt", conformingTo: .text)) + return try decoder.decode(VMParameters.self, from: json) + } catch { + print("failed to read parameters") + } + return nil + } + + func writeToDisk(bundleURL: URL) { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + do { + let jsonData = try encoder.encode(self) + if let json = String(data: jsonData, encoding: .utf8) { + try json.write(to: bundleURL.parametersURL, atomically: true, encoding: String.Encoding.utf8) + } + } catch { + print("failed to write current CPU and RAM configuration to disk") + } + } +} + +#endif + diff --git a/virtualOS/View/ConfigurationView.swift b/virtualOS/View/ConfigurationView.swift deleted file mode 100644 index 3a0d276..0000000 --- a/virtualOS/View/ConfigurationView.swift +++ /dev/null @@ -1,202 +0,0 @@ -// -// ConfigurationView.swift -// virtualOS -// -// Created by Jahn Bertsch on 03.04.22. -// - -#if arch(arm64) - -import SwiftUI - -struct ConfigurationView: View { - enum ScreenSize: Int, Codable { - case custom = 1 - case mainScreen = 2 - } - public enum SharedFolderType: Int, Codable { - case none = 1 - case custom = 2 - } - - @ObservedObject var viewModel: ViewModel - @State fileprivate var cpuCountSliderValue: Float = 0 { - didSet { - viewModel.virtualMac.parameters.cpuCount = Int(cpuCountSliderValue) - } - } - @State fileprivate var memorySliderValue: Float = 0 { - didSet { - viewModel.virtualMac.parameters.memorySizeInGB = UInt64(memorySliderValue) - } - } - @State fileprivate var screenWidthValue: Float = 0 { - didSet { - viewModel.virtualMac.parameters.screenWidth = Int(screenWidthValue) - } - } - @State fileprivate var screenHeightValue: Float = 0 { - didSet { - viewModel.virtualMac.parameters.screenHeight = Int(screenHeightValue) - } - } - @State fileprivate var screenSize: ScreenSize = .mainScreen - @State var sharedFolderType: SharedFolderType = .none - - fileprivate var sharedFolderInfo: String { - if #available(macOS 13.0, *) { - if sharedFolderType == .custom, - let hardDiskDirectoryBookmarkData = Bookmark.startAccess(data: viewModel.virtualMac.parameters.sharedFolder, forType: .sharedFolder) - { - if viewModel.sharedFolderExists { - return "Using \(hardDiskDirectoryBookmarkData.path)" - } else { - return "Shared folder not found." - } - } else { - return "No shared folder selected." - } - } else { - return "Shared folders require macOS 13 or newer." - } - } - fileprivate let textWidth = CGFloat(150) - - var body: some View { - VStack { - Spacer() - VStack { - let parameters = viewModel.virtualMac.parameters - Text("Virtual Machine Configuration").font(.headline) - - Slider(value: Binding(get: { - cpuCountSliderValue - }, set: { (newValue) in - cpuCountSliderValue = newValue - }), in: Float(parameters.cpuCountMin) ... Float(parameters.cpuCountMax), step: 1) { - Text("CPU Count: \(viewModel.virtualMac.parameters.cpuCount)") - .frame(minWidth: textWidth, alignment: .leading) - } - - Slider(value: Binding(get: { - memorySliderValue - }, set: { (newValue) in - memorySliderValue = newValue - }), in: Float(parameters.memorySizeInGBMin) ... Float(parameters.memorySizeInGBMax), step: 1) { - Text("RAM: \(viewModel.virtualMac.parameters.memorySizeInGB) GB") - .frame(minWidth: textWidth, alignment: .leading) - } - - HStack() { - Text("Screen Size").frame(minWidth: textWidth, alignment: .leading) - Picker("", selection: $screenSize) { - Text("Main Screen").tag(ScreenSize.mainScreen) - Text("Custom").tag(ScreenSize.custom) - }.pickerStyle(.inline) - .onChange(of: screenSize) { newValue in - viewModel.virtualMac.parameters.useMainScreenSize = newValue == .mainScreen - if let mainScreen = NSScreen.main { - screenWidthValue = Float(mainScreen.frame.width) - screenHeightValue = Float(mainScreen.frame.height) - } - } - Spacer() - } - - Slider(value: Binding(get: { - screenWidthValue - }, set: { (newValue) in - screenWidthValue = newValue - }), in: 800 ... Float(NSScreen.main?.frame.width ?? CGFloat(parameters.screenWidth)), step: 100) { - Text("Screen Width: \(viewModel.virtualMac.parameters.screenWidth) px") - .frame(minWidth: textWidth, alignment: .leading) - }.disabled(screenSize == .mainScreen) - - Slider(value: Binding(get: { - screenHeightValue - }, set: { (newValue) in - screenHeightValue = newValue - }), in: 600 ... Float(NSScreen.main?.frame.height ?? CGFloat(parameters.screenHeight)), step: 50) { - Text("Screen Height: \(viewModel.virtualMac.parameters.screenHeight) px") - .frame(minWidth: textWidth, alignment: .leading) - }.disabled(screenSize == .mainScreen) - - HStack() { - Text("Shared Folder").frame(minWidth: textWidth, alignment: .leading) - VStack(alignment: .leading, content: { - Picker("", selection: $sharedFolderType) { - Text("No shared folder").tag(SharedFolderType.none) - Text("Custom").tag(SharedFolderType.custom) - }.pickerStyle(.inline) - Button("Select Shared Folder") { - selectSharedFolder() - } .disabled(sharedFolderType == .none) - .padding(.top, 7) - Text(sharedFolderInfo) - .font(.caption) - .frame(maxWidth: 270, alignment: .leading) - .fixedSize(horizontal: false, vertical: true) - .lineLimit(nil) - .disabled(sharedFolderType == .none) - }) - } - } - .padding() - .overlay { - RoundedRectangle(cornerRadius: 3) - .stroke(.tertiary, lineWidth: 1) - } - - Spacer() - } - .padding() - .frame(maxWidth: 400) - .onAppear { - onAppear() - } - } - - // MARK: - Private - - fileprivate func selectSharedFolder() { - let openPanel = NSOpenPanel() - openPanel.allowsMultipleSelection = false - openPanel.canChooseDirectories = true - openPanel.canChooseFiles = false - openPanel.prompt = "Select" - if openPanel.runModal() == .OK, - let selectedURL = openPanel.url - { - viewModel.set(sharedFolderUrl: selectedURL) - } - } - - fileprivate func onAppear() { - let parameters = viewModel.virtualMac.parameters - cpuCountSliderValue = Float(parameters.cpuCount) - memorySliderValue = Float(parameters.memorySizeInGB) - screenWidthValue = Float(parameters.screenWidth) - screenHeightValue = Float(parameters.screenHeight) - if parameters.useMainScreenSize { - screenSize = .mainScreen - } else { - screenSize = .custom - } - if let hardDiskDirectoryBookmarkData = UserDefaults.standard.hardDiskDirectoryBookmarkData { - _ = Bookmark.startAccess(data: hardDiskDirectoryBookmarkData, forType: .hardDisk) - } - if Bookmark.startAccess(data: parameters.sharedFolder, forType: .sharedFolder) != nil { - sharedFolderType = .custom - } - } -} - -struct ConfigurationViewProvider_Previews: PreviewProvider { - static var previews: some View { - VStack { - ConfigurationView(viewModel: ViewModel()) - } - } -} - -#endif diff --git a/virtualOS/View/MainView.swift b/virtualOS/View/MainView.swift deleted file mode 100644 index 4da6bca..0000000 --- a/virtualOS/View/MainView.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// MainView.swift -// virtualOS -// -// Created by Jahn Bertsch on 16.03.22. -// - -#if arch(arm64) - -import SwiftUI - -struct MainView: View { - @ObservedObject var viewModel: ViewModel - - var body: some View { - VStack { - if viewModel.showStatusBar { - Spacer() - HStack { - Text(viewModel.statusLabel) - Spacer() - Button { - viewModel.statusButtonPressed() - } label: { - Text(viewModel.statusButtonLabel) - }.disabled(viewModel.statusButtonDisabled) - } - .padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)) - } - - if viewModel.showConfigurationView { - ConfigurationView(viewModel: viewModel) - } else if viewModel.showSettingsInfo { - VStack { - Spacer() - Button("Open Settings") { - viewModel.showSettings = !viewModel.showSettings - } - Text("Open settings for basic virtual machine configuration, then press Start to install.") - .lineLimit(nil) - .font(.caption) - Spacer() - } - } else { - VirtualMachineView(virtualMachine: $viewModel.virtualMachine) - } - } - .frame(minWidth: 400, minHeight: 300) - } -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - MainView(viewModel: ViewModel()) - } -} - -#endif diff --git a/virtualOS/View/MenuCommands.swift b/virtualOS/View/MenuCommands.swift deleted file mode 100644 index 076de54..0000000 --- a/virtualOS/View/MenuCommands.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// MenuCommands.swift -// virtualOS -// -// Created by Jahn Bertsch on 25.02.23. -// - -import SwiftUI - -#if arch(arm64) - -struct MenuCommands: Commands { - @ObservedObject var viewModel: ViewModel - - var body: some Commands { - CommandGroup(replacing: .appInfo) { - Button("About virtualOS") { - viewModel.showLicenseInformationModal = !viewModel.showLicenseInformationModal - } - } - CommandGroup(replacing: .appSettings) { - Button("Settings") { - viewModel.showSettings = !viewModel.showSettings - }.keyboardShortcut(",") - } - CommandGroup(replacing: .newItem) { - Button(viewModel.statusButtonLabel) { - viewModel.statusButtonPressed() - }.keyboardShortcut("R") - Divider() - Button("Delete Restore Image") { - viewModel.deleteRestoreImage() - }.disabled(!ViewModel.restoreImageExists) - Button("Delete Virtual Machine", action: { - viewModel.deleteVirtualMachine() - }) - } - CommandGroup(replacing: .toolbar) { - let statusBarVisibilityString = viewModel.showStatusBar ? "Hide" : "Show" - Button(String(format: "%@ Status Bar", statusBarVisibilityString)) { - viewModel.showStatusBar = !viewModel.showStatusBar - }.keyboardShortcut("B") - Divider() - Button(String(format: "%@ Full Screen", viewModel.isFullScreen ? "Exit" : "Enter")) { - if let window = NSApplication.shared.windows.first { - viewModel.isFullScreen = !viewModel.isFullScreen - viewModel.showStatusBar = !viewModel.isFullScreen - window.toggleFullScreen(nil) - } - }.keyboardShortcut("F") - } - } -} - -#endif // #if arch(arm64) diff --git a/virtualOS/View/SettingsView.swift b/virtualOS/View/SettingsView.swift deleted file mode 100644 index 3cb55dd..0000000 --- a/virtualOS/View/SettingsView.swift +++ /dev/null @@ -1,180 +0,0 @@ -// -// SettingsView.swift -// virtualOS -// -// Created by Jahn Bertsch on 25.05.22. -// - -#if arch(arm64) - -import SwiftUI -import UniformTypeIdentifiers -import AppKit - -struct SettingsView: View { - fileprivate enum HardDiskLocation: String, CaseIterable, Identifiable { - case sandbox = "Sandbox" - case custom = "Select location where VM hard disk image will be stored." - var id: Self { self } - } - fileprivate enum RestoreImageType: String, CaseIterable, Identifiable { - case latest = "Downloads latest restore image from Apple." - case custom = "Select custom restore image (.ipsw)\nFor example, download from https://ipsw.me/product/mac" - var id: Self { self } - } - fileprivate struct SizeConstants { - static let totalWidth = CGFloat(470) - static let infoWidth = CGFloat(300) - static let diskWidth = CGFloat(140) - static let locationWidth = CGFloat(450) - static let minTextHeight = CGFloat(28) - } - - @ObservedObject var viewModel: ViewModel - @State fileprivate var diskSize = String(UserDefaults.standard.diskSize) - @State fileprivate var hardDiskLocation = HardDiskLocation.sandbox - @State fileprivate var restoreImageType = RestoreImageType.latest - @State fileprivate var showAlert = false - - fileprivate var hardDiskLocationString: String { - if hardDiskLocation == .sandbox { - return URL.basePath - } else { - if let customHardDiskURL = viewModel.customHardDiskURL { - return customHardDiskURL.path - } else { - return HardDiskLocation.custom.rawValue - } - } - } - fileprivate var restoreImageInfoString: String { - if let restoreImageURL = viewModel.customRestoreImageURL { - return restoreImageURL.path - } else { - return restoreImageType.rawValue - } - } - - var body: some View { - VStack() { - Text("Settings").font(.headline) - Form { - HStack { - TextField("Hard Disk Size:", text: $diskSize) - .frame(maxWidth: SizeConstants.diskWidth) - .onChange(of: diskSize) { newValue in - if let newDiskSize = Int(diskSize) { - viewModel.diskSize = newDiskSize - } else { - diskSize = "" - } - } - Text("(in GB)") - } - - Picker("Hard Disk Location:", selection: $hardDiskLocation) { - Text("Default").tag(HardDiskLocation.sandbox) - Text("Custom").tag(HardDiskLocation.custom) - } - .pickerStyle(.inline) - .onChange(of: hardDiskLocation) { newValue in - if newValue == .sandbox { - UserDefaults.standard.hardDiskDirectoryBookmarkData = nil - } - } - - HStack { - Button("Show in Finder") { - NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: hardDiskLocationString) - }.disabled(hardDiskLocation != .sandbox && viewModel.customHardDiskURL == nil) - - Button("Select Hard Disk Location") { - selectCustomHardDiskLocation() - }.disabled(hardDiskLocation == .sandbox) - } - - Text(.init(hardDiskLocationString)) - .font(.caption) - .frame(maxWidth: SizeConstants.infoWidth, minHeight: SizeConstants.minTextHeight, alignment: .topLeading) - .lineLimit(nil) - .disabled(hardDiskLocation == .sandbox) - - - Picker("Restore Image:", selection: $restoreImageType) { - Text("Latest").tag(RestoreImageType.latest) - Text("Custom").tag(RestoreImageType.custom) - }.pickerStyle(.inline) - - Button("Select Restore Image") { - selectRestoreImage() - }.disabled(restoreImageType == .latest) - - Text(restoreImageInfoString) - .font(.caption) - .frame(maxWidth: SizeConstants.infoWidth, minHeight: SizeConstants.minTextHeight, alignment: .topLeading) - .fixedSize(horizontal: false, vertical: true) - .lineLimit(nil) - .disabled(restoreImageType == .latest) - }.padding(.bottom) - - Button("OK") { - viewModel.showSettings = !viewModel.showSettings - }.keyboardShortcut(.defaultAction) - } - .padding() - .frame(minWidth: SizeConstants.totalWidth, maxWidth: SizeConstants.totalWidth) - .onAppear() { - diskSize = String(viewModel.diskSize) - if let hardDiskDirectoryBookmarkData = UserDefaults.standard.hardDiskDirectoryBookmarkData, - let hardDiskURL = Bookmark.startAccess(data: hardDiskDirectoryBookmarkData, forType: .hardDisk) - { - hardDiskLocation = .custom - viewModel.customHardDiskURL = hardDiskURL - } - } - } - - // MARK: - Private - - fileprivate func selectCustomHardDiskLocation() { - let openPanel = NSOpenPanel() - openPanel.directoryURL = URL(fileURLWithPath: URL.basePath, isDirectory: true) - openPanel.allowsMultipleSelection = false - openPanel.canChooseDirectories = true - openPanel.canChooseFiles = false - if openPanel.runModal() == .OK, - let selectedURL = openPanel.url - { - let hardDiskDirectoryBookmarkData = Bookmark.createBookmarkData(fromUrl: selectedURL) - _ = Bookmark.startAccess(data: hardDiskDirectoryBookmarkData, forType: .hardDisk) - - viewModel.customHardDiskURL = selectedURL - UserDefaults.standard.hardDiskDirectoryBookmarkData = hardDiskDirectoryBookmarkData - } - } - - fileprivate func selectRestoreImage() { - guard let ipswContentType = UTType(filenameExtension: "ipsw") else { - return - } - let openPanel = NSOpenPanel() - openPanel.allowsMultipleSelection = false - openPanel.canChooseFiles = true - openPanel.allowedContentTypes = [ipswContentType] - if openPanel.runModal() == .OK, - let selectedURL = openPanel.url - { - viewModel.customRestoreImageURL = selectedURL - } - } -} - -struct SettingsViewProvider_Previews: PreviewProvider { - static var previews: some View { - VStack { - SettingsView(viewModel: ViewModel()) - } - } -} - -#endif diff --git a/virtualOS/View/VirtualMachineView.swift b/virtualOS/View/VirtualMachineView.swift deleted file mode 100644 index f706fe5..0000000 --- a/virtualOS/View/VirtualMachineView.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// VirtualMachineView.swift -// virtualOS -// -// Created by Jahn Bertsch on 16.03.22. -// - -import SwiftUI -import Virtualization - -struct VirtualMachineView: NSViewRepresentable { - @Binding var virtualMachine: VZVirtualMachine? - - func makeNSView(context: Context) -> VZVirtualMachineView { - let view = VZVirtualMachineView() - view.capturesSystemKeys = true - return view - } - - func updateNSView(_ nsView: VZVirtualMachineView, context: Context) { - nsView.virtualMachine = virtualMachine - nsView.window?.makeFirstResponder(nsView) - } -} diff --git a/virtualOS/ViewController/MainViewController.swift b/virtualOS/ViewController/MainViewController.swift new file mode 100644 index 0000000..eb6f4d5 --- /dev/null +++ b/virtualOS/ViewController/MainViewController.swift @@ -0,0 +1,341 @@ +// +// ViewController.swift +// virtualOS +// +// Created by Jahn Bertsch. +// + +import Cocoa +import Virtualization +import OSLog + +#if arch(arm64) + +final class MainViewController: NSViewController { + @IBOutlet weak var tableView: NSTableView! + @IBOutlet weak var vmNameTextField: NSTextField! + @IBOutlet weak var parameterOutlinew: NSOutlineView! + @IBOutlet weak var startButton: NSButton! + @IBOutlet weak var sharedFolderButton: NSButton! + @IBOutlet weak var deleteButton: NSButton! + @IBOutlet weak var cpuCountLabel: NSTextField! + @IBOutlet weak var cpuCountSlider: NSSlider! + @IBOutlet weak var ramLabel: NSTextField! + @IBOutlet weak var ramSlider: NSSlider! + + fileprivate let mainStoryBoard = NSStoryboard(name: "Main", bundle: nil) + fileprivate let viewModel = MainViewModel() + fileprivate let logger = Logger() + fileprivate var diskImageSize = 1 + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.dataSource = viewModel.tableViewDataSource + tableView.delegate = self + parameterOutlinew.dataSource = viewModel.parametersViewDataSource + parameterOutlinew.delegate = viewModel.parametersViewDelegate + vmNameTextField.delegate = viewModel.textFieldDelegate + + NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: NSApplication.didBecomeActiveNotification, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(textDidEndEditing), name: NSControl.textDidEndEditingNotification, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(restoreImageSelected), name: Constants.restoreImageNameSelectedNotification, object: nil) + + ramSlider.target = self + ramSlider.action = #selector(memorySliderChanged(sender:)) + cpuCountSlider.target = self + cpuCountSlider.action = #selector(cpuCountChanged(sender:)) + } + + @objc func didBecomeActive(notification: Notification) { + self.tableView.reloadData() + self.updateUI() + } + + @objc func textDidEndEditing(notification: Notification) { + self.tableView.reloadData() + self.updateUI() + } + + @objc func restoreImageSelected(notification: Notification) { + if let userInfo = notification.userInfo, + let restoreImageName = userInfo[Constants.selectedRestoreImage] as? String + { + if restoreImageName != Constants.restoreImageNameLatest { + let accessory = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 20)) + accessory.stringValue = "\(UserDefaults.standard.diskSize)" + + let alert: NSAlert = NSAlert() + alert.messageText = "Disk Image Size in GB" + alert.informativeText = "Disk size can not be changed after VM is created." + alert.accessoryView = accessory + alert.alertStyle = .informational + alert.addButton(withTitle: "OK") + alert.addButton(withTitle: "Cancel") + + let modalResponse = alert.runModal() + accessory.becomeFirstResponder() + + if modalResponse == .OK || modalResponse == .alertFirstButtonReturn { + diskImageSize = Int(accessory.intValue) + } else { + return // cancel install + } + if diskImageSize < 30 { + self.diskImageSize = 30 + } + } + + showSheet(mode: .install, restoreImageName: restoreImageName, diskImageSize: self.diskImageSize) + } + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override func viewWillAppear() { + super.viewWillAppear() + vmNameTextField.resignFirstResponder() + tableView.reloadData() + self.updateUI() + } + + @IBAction func startButtonPressed(_ sender: NSButton) { + if let vmViewController = mainStoryBoard.instantiateController(withIdentifier: "VMViewController") as? VMViewController, + let windowController = mainStoryBoard.instantiateController(withIdentifier: "NSWindowController") as? NSWindowController + { + vmViewController.vmBundle = viewModel.vmBundle + vmViewController.vmParameters = viewModel.vmParameters + windowController.showWindow(self) + windowController.contentViewController = vmViewController + } else { + logger.log(level: .default, "show window failed") + } + } + + @IBAction func installButtonPressed(_ sender: NSButton) { + if let restoreImageViewController = mainStoryBoard.instantiateController(withIdentifier: "RestoreImageViewController") as? RestoreImageViewController, + let windowController = mainStoryBoard.instantiateController(withIdentifier: "NSWindowController") as? NSWindowController + { + windowController.showWindow(self) + windowController.contentViewController = restoreImageViewController + } + } + + @IBAction func deleteButtonPressed(_ sender: Any) { + guard let vmBundle = viewModel.vmBundle else { + return + } + + let alert: NSAlert = NSAlert() + alert.messageText = "Delete VM \(vmBundle.name)?" + alert.informativeText = "This can not be undone." + alert.alertStyle = .warning + alert.addButton(withTitle: "OK") + alert.addButton(withTitle: "Cancel") + + let selection = alert.runModal() + if selection == NSApplication.ModalResponse.alertFirstButtonReturn || + selection == NSApplication.ModalResponse.OK + { + viewModel.deleteVM(selection: selection, vmBundle: vmBundle) + viewModel.vmBundle = nil + } + tableView.reloadData() + self.updateUI() + } + + @IBAction func sharedFolderButtonPressed(_ sender: Any) { + let openPanel = NSOpenPanel() + openPanel.allowsMultipleSelection = false + openPanel.canChooseDirectories = true + openPanel.canChooseFiles = false + openPanel.prompt = "Select" + let modalResponse = openPanel.runModal() + var sharedFolderURL: URL? + if modalResponse == .OK, + let selectedURL = openPanel.url + { + sharedFolderURL = selectedURL + } else if modalResponse == .cancel { + sharedFolderURL = nil + } + if let vmParameters = viewModel.set(sharedFolderUrl: sharedFolderURL) { + viewModel.parametersViewDataSource.vmParameters = vmParameters + parameterOutlinew.reloadData() + } + } + + @IBAction func usbButtonPressed(_ sender: NSButtonCell) { + print("usb") + } + + @objc func cpuCountChanged(sender: NSSlider) { + updateLabels() + viewModel.storeParametersToDisk() + } + + @objc func memorySliderChanged(sender: NSSlider) { + updateLabels() + viewModel.storeParametersToDisk() + } + + func updateUI() { + cpuCountSlider.isEnabled = false + ramSlider.isEnabled = false + + if let selectedRow = viewModel.selectedRow, + let vmBundle = viewModel.tableViewDataSource.vmBundle(forRow: selectedRow) + { + vmNameTextField.stringValue = vmBundle.name + tableView.selectRowIndexes(IndexSet(integer: selectedRow), byExtendingSelection: false) + viewModel.vmBundle = vmBundle + if let vmParameters = VMParameters.readFrom(url: vmBundle.url) { + viewModel.vmParameters = vmParameters + viewModel.parametersViewDataSource.vmParameters = viewModel.vmParameters + parameterOutlinew.reloadData() + viewModel.textFieldDelegate.vmBundle = vmBundle + updateCpuCount(vmParameters) + updateRam(vmParameters) + updateLabels() + updateButtons(enabled: true) + } + } else { + vmNameTextField.stringValue = "" + viewModel.parametersViewDataSource.vmParameters = nil + viewModel.selectedRow = 0 + updateLabels(setZero: true) + updateButtons(enabled: false) + } + tableView.reloadData() + } + + fileprivate func updateButtons(enabled: Bool) { + startButton.isEnabled = enabled + sharedFolderButton.isEnabled = enabled + deleteButton.isEnabled = enabled + vmNameTextField.isEnabled = enabled + } + + // MARK: - Private + + fileprivate func updateCpuCount(_ vmParameters: VMParameters) { + cpuCountSlider.minValue = Double(vmParameters.cpuCountMin) + cpuCountSlider.maxValue = Double(vmParameters.cpuCountMax) + cpuCountSlider.numberOfTickMarks = Int(cpuCountSlider.maxValue - cpuCountSlider.minValue) + cpuCountSlider.doubleValue = Double(vmParameters.cpuCount) + cpuCountSlider.isEnabled = true + } + + fileprivate func updateRam(_ vmParameters: VMParameters) { + ramSlider.minValue = max(Double(vmParameters.memorySizeInGBMin), 2.0) + ramSlider.maxValue = Double(vmParameters.memorySizeInGBMax) + ramSlider.numberOfTickMarks = Int(ramSlider.maxValue - ramSlider.minValue) + ramSlider.doubleValue = Double(vmParameters.memorySizeInGB) + ramSlider.isEnabled = true + } + + fileprivate func updateLabels(setZero: Bool = false) { + let cpuCount = Int(round(cpuCountSlider.doubleValue)) + let memorySizeInGB = Int(round(ramSlider.doubleValue)) + viewModel.vmParameters?.cpuCount = cpuCount + viewModel.vmParameters?.memorySizeInGB = UInt64(memorySizeInGB) + + if setZero { + cpuCountLabel.stringValue = "CPU Count" + ramLabel.stringValue = "RAM" + } else { + cpuCountLabel.stringValue = "CPU Count: \(cpuCount)" + ramLabel.stringValue = "RAM: \(memorySizeInGB) GB" + } + + viewModel.parametersViewDataSource.vmParameters = viewModel.vmParameters + parameterOutlinew.reloadData() + } + + // MARK: - Private + + fileprivate func showSheet(mode: ProgressViewController.Mode, restoreImageName: String?, diskImageSize: Int?) { + if let progressWindowController = mainStoryBoard.instantiateController(withIdentifier: "ProgressWindowController") as? NSWindowController, + let progressWindow = progressWindowController.window + { + if let progressViewController = progressWindow.contentViewController as? ProgressViewController { + progressViewController.mode = mode + progressViewController.diskImageSize = diskImageSize + progressViewController.restoreImageName = restoreImageName + presentAsSheet(progressViewController) + } + } else { + logger.log(level: .default, "show modal failed") + } + } + + fileprivate func updateButtonEnabledState() { + var enabled = false + if viewModel.selectedRow != nil { + enabled = true + } + vmNameTextField.isEnabled = enabled + startButton.isEnabled = enabled + sharedFolderButton.isEnabled = enabled + deleteButton.isEnabled = enabled + ramSlider.isEnabled = enabled + cpuCountSlider.isEnabled = enabled + } +} + +extension MainViewController: NSTableViewDelegate { + func tableViewSelectionDidChange(_ notification: Notification) { + var row: Int? = nil + + if let userInfo = notification.userInfo, + let indexSet = userInfo["NSTableViewCurrentRowSelectionUserInfoKey"] as? NSIndexSet { + if indexSet.count > 0 { + row = indexSet.firstIndex + } + } + + if let row = row { + viewModel.selectedRow = row + updateUI() + updateButtonEnabledState() + } + } +} + +#else + +// minimum implementation used for intel cpus + +final class MainViewController: NSViewController { + @IBOutlet weak var vmNameTextField: NSTextField! + @IBOutlet weak var installButton: NSButton! + @IBOutlet weak var startButton: NSButton! + @IBOutlet weak var sharedFolderButton: NSButton! + @IBOutlet weak var deleteButton: NSButton! + @IBOutlet weak var cpuCountLabel: NSTextField! + @IBOutlet weak var cpuCountSlider: NSSlider! + @IBOutlet weak var ramLabel: NSTextField! + @IBOutlet weak var ramSlider: NSSlider! + + override func viewWillAppear() { + super.viewWillAppear() + vmNameTextField.stringValue = "Virtualization requires an Apple Silicon machine" + vmNameTextField.isEditable = false + installButton.isEnabled = false + startButton.isEnabled = false + sharedFolderButton.isEnabled = false + deleteButton.isEnabled = false + cpuCountSlider.isEnabled = false + ramSlider.isEnabled = false + cpuCountLabel.stringValue = "" + ramLabel.stringValue = "" + } +} + +#endif + +// place all code before #else diff --git a/virtualOS/ViewController/ProgressViewController.swift b/virtualOS/ViewController/ProgressViewController.swift new file mode 100644 index 0000000..dbf9854 --- /dev/null +++ b/virtualOS/ViewController/ProgressViewController.swift @@ -0,0 +1,89 @@ +// +// ProgressViewController.swift +// virtualOS +// +// Created by Jahn Bertsch +// + +import AppKit +import OSLog + +#if arch(arm64) + +final class ProgressViewController: NSViewController { + enum Mode { + case download + case install + } + + @IBOutlet weak var progressIndicator: NSProgressIndicator! + @IBOutlet weak var statusTextField: NSTextField! + + var mode: Mode = .download + var restoreImageName: String? + var diskImageSize: Int? = 0 + fileprivate let restoreImageDownload = RestoreImageDownload() + fileprivate var restoreImageInstall = RestoreImageInstall() + fileprivate let logger = Logger.init(subsystem: "com.github.virtualOS", category: "log") + + override func viewWillAppear() { + super.viewWillAppear() + progressIndicator.doubleValue = 0 + statusTextField.stringValue = "Starting" + // logger.log(level: .default, "\(progressViewController.mode): \(mode))") + } + + override func viewDidAppear() { + super.viewDidAppear() + if restoreImageName == Constants.restoreImageNameLatest || + mode == .download + { + restoreImageDownload.delegate = self + restoreImageDownload.fetch() + mode = .download // restoreImageNameLatest is also a download + } else if mode == .install { + restoreImageInstall.restoreImageName = restoreImageName + restoreImageInstall.diskImageSize = diskImageSize + restoreImageInstall.delegate = self + restoreImageInstall.install() + } + } + + override func viewWillDisappear() { + super.viewWillDisappear() + cancel() + } + + @IBAction func cancelButtonPressed(_ sender: NSButton) { + cancel() + if let mainViewController = presentingViewController as? MainViewController { + mainViewController.updateUI() + mainViewController.dismiss(self) + } + } + + fileprivate func cancel() { + if mode == .download { + restoreImageDownload.cancel() + } else if mode == .install { + restoreImageInstall.cancel() + } + } +} + +extension ProgressViewController: ProgressDelegate { + func progress(_ progress: Double, progressString: String) { + progressIndicator.doubleValue = progress * 100 + statusTextField.stringValue = progressString + } + + func done() { + DispatchQueue.main.async { [weak self] in + if let mainViewController = self?.presentingViewController as? MainViewController { + mainViewController.dismiss(self) + } + } + } +} + +#endif diff --git a/virtualOS/ViewController/RestoreImageViewController.swift b/virtualOS/ViewController/RestoreImageViewController.swift new file mode 100644 index 0000000..d492fd9 --- /dev/null +++ b/virtualOS/ViewController/RestoreImageViewController.swift @@ -0,0 +1,107 @@ +// +// RestoreImageViewController.swift +// virtualOS +// +// Created by Jahn Bertsch +// + +import AppKit +import Virtualization +import OSLog + +#if arch(arm64) + +final class RestoreImageViewController: NSViewController { + let fileModel = FileModel() + fileprivate var selectedRestoreImage = "" + fileprivate let logger = Logger.init(subsystem: "com.github.virtualOS", category: "log") + + @IBOutlet weak var tableView: NSTableView! + @IBOutlet weak var selectButton: NSButton! + @IBOutlet weak var infoTextField: NSTextField! + + override func viewDidLoad() { + super.viewDidLoad() + tableView.dataSource = self + tableView.delegate = self + } + + override func viewWillAppear() { + super.viewWillAppear() + tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) + updateInfoLabel() + if tableView.numberOfRows > 0 { + selectButton.isEnabled = true + } else { + selectButton.isEnabled = false + } + } + + @IBAction func selectButtonPressed(_ sender: NSButton) { + if tableView.selectedRow != -1 { + let notification = Notification(name: Constants.restoreImageNameSelectedNotification, userInfo: [Constants.selectedRestoreImage: self.selectedRestoreImage]) + NotificationCenter.default.post(notification) + view.window?.close() + } + } + + @IBAction func downloadLatestButtonPressed(_ sender: Any) { + let notification = Notification(name: Constants.restoreImageNameSelectedNotification, userInfo: [Constants.selectedRestoreImage: Constants.restoreImageNameLatest]) + NotificationCenter.default.post(notification) + + view.window?.close() + } + + fileprivate func updateInfoLabel() { + if tableView.selectedRow < fileModel.getRestoreImages().count && + tableView.selectedRow != -1 + { + let name = fileModel.getRestoreImages()[tableView.selectedRow] + let url = URL.baseURL.appendingPathComponent(name) + VZMacOSRestoreImage.load(from: url) { result in + switch result { + case .success(let restoreImage): + DispatchQueue.main.async { [weak self] in + self?.infoTextField.stringValue = restoreImage.operatingSystemVersionString + } + case .failure(let error): + self.logger.log(level: .default, "\(error)") + } + } + } else { + infoTextField.stringValue = "" + } + } +} + +extension RestoreImageViewController: NSTableViewDataSource { + func numberOfRows(in tableView: NSTableView) -> Int { + return fileModel.getRestoreImages().count + } + + func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { + let restoreImages = fileModel.getRestoreImages() + if row < restoreImages.count { + return restoreImages[row] + } else { + return "Unknown" + } + } +} + +extension RestoreImageViewController: NSTableViewDelegate { + func tableViewSelectionDidChange(_ notification: Notification) { + let restoreImages = fileModel.getRestoreImages() + + let selectedRow = tableView.selectedRow + if selectedRow != -1 && selectedRow < restoreImages.count { + selectedRestoreImage = restoreImages[selectedRow] + selectButton.isEnabled = true + } else { + selectButton.isEnabled = false + } + updateInfoLabel() + } +} + +#endif diff --git a/virtualOS/ViewController/SettingsViewController.swift b/virtualOS/ViewController/SettingsViewController.swift new file mode 100644 index 0000000..af3e2ba --- /dev/null +++ b/virtualOS/ViewController/SettingsViewController.swift @@ -0,0 +1,14 @@ +// +// SettingsViewController.swift +// virtualOS +// +// Created by Jahn Bertsch +// + +import AppKit + +final class SettingsViewController: NSViewController { + @IBAction func showInFinderButtonPressed(_ sender: Any) { + NSWorkspace.shared.activateFileViewerSelecting([URL.baseURL]) + } +} diff --git a/virtualOS/ViewController/VMViewController.swift b/virtualOS/ViewController/VMViewController.swift new file mode 100644 index 0000000..ec07c3c --- /dev/null +++ b/virtualOS/ViewController/VMViewController.swift @@ -0,0 +1,122 @@ +// +// VMViewController.swift +// virtualOS +// +// Created by Jahn Bertsch +// + +import Virtualization +import OSLog + +#if arch(arm64) + +final class VMViewController: NSViewController { + @IBOutlet var containerView: NSView! + @IBOutlet weak var statusLabel: NSTextField! + + var vmBundle: VMBundle? + var vmParameters: VMParameters? + fileprivate var vmConfiguration: VMConfiguration? + fileprivate var vm: VZVirtualMachine? + fileprivate let vmView = VZVirtualMachineView() + fileprivate let logger = Logger.init(subsystem: "com.github.virtualOS", category: "log") + fileprivate let queue = DispatchQueue.global(qos: .userInteractive) + + override func viewDidLoad() { + super.viewDidLoad() + + createVM() + setupConstraints() + + queue.async { [weak self] in + self?.vm?.start { (result: Result) in + switch result { + case .success: + self?.logger.log(level: .default, "running") + case .failure(let error): + self?.logger.log(level: .default, "running failed \(error.localizedDescription)") + } + } + } + } + + // MARK: - Private + + fileprivate func createVM() { + if let bundleURL = vmBundle?.url, + let vmParameters = vmParameters, + let macPlatformConfiguration = MacPlatformConfiguration.read(fromBundleURL: bundleURL) + { + let vmConfiguration = VMConfiguration() + vmConfiguration.setup(parameters: vmParameters, macPlatformConfiguration: macPlatformConfiguration, bundleURL: bundleURL) + self.vmConfiguration = vmConfiguration + } else { + logger.log(level: .default, "could not create vm config") + return + } + + guard let vmConfiguration else { + logger.log(level: .default, "no vm config") + return + } + + do { + try vmConfiguration.validate() + logger.log(level: .default, "vm configuration is valid") + } catch let error { + logger.log(level: .default, "failed to validate vm configuration: \(error.localizedDescription)") + return + } + + let vm = VZVirtualMachine(configuration: vmConfiguration, queue: queue) + vm.delegate = self + + vmView.virtualMachine = vm + vmView.automaticallyReconfiguresDisplay = true + vmView.capturesSystemKeys = true + self.vm = vm + } + + fileprivate func setupConstraints() { + if let containerView { + let top = NSLayoutConstraint(item: containerView, attribute: .top, relatedBy: .equal, toItem: vmView, attribute: .top, multiplier: 1, constant: 0) + let bottom = NSLayoutConstraint(item: containerView, attribute: .bottom, relatedBy: .equal, toItem: vmView, attribute: .bottom, multiplier: 1, constant: 0) + let leading = NSLayoutConstraint(item: containerView, attribute: .leading, relatedBy: .equal, toItem: vmView, attribute: .leading, multiplier: 1, constant: 0) + let trailing = NSLayoutConstraint(item: containerView, attribute: .trailing, relatedBy: .equal, toItem: vmView, attribute: .trailing, multiplier: 1, constant: 0) + + containerView.addSubview(vmView) + containerView.addConstraint(top) + containerView.addConstraint(bottom) + containerView.addConstraint(leading) + containerView.addConstraint(trailing) + + let centerX = NSLayoutConstraint(item: containerView, attribute: .centerX, relatedBy: .equal, toItem: statusLabel, attribute: .centerX, multiplier: 1, constant: 1) + let centerY = NSLayoutConstraint(item: containerView, attribute: .centerY, relatedBy: .equal, toItem: statusLabel, attribute: .centerY, multiplier: 1, constant: 0) + + statusLabel.stringValue = "" + statusLabel.removeFromSuperview() + containerView.addSubview(statusLabel) + containerView.addConstraint(centerX) + containerView.addConstraint(centerY) + } + } +} + +extension VMViewController: VZVirtualMachineDelegate { + func guestDidStop(_ virtualMachine: VZVirtualMachine) { + let message = "Guest did stop" + DispatchQueue.main.async { [weak self] in + self?.statusLabel.stringValue = message + } + logger.log(level: .default, "\(message)") + } + + func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: any Error) { + DispatchQueue.main.async { [weak self] in + self?.statusLabel.stringValue = "Guest did stop with error: \(error.localizedDescription)" + } + logger.log(level: .default, "\(self.statusLabel.stringValue)") + } +} + +#endif diff --git a/virtualOS/virtualOSApp.swift b/virtualOS/virtualOSApp.swift deleted file mode 100644 index 152d047..0000000 --- a/virtualOS/virtualOSApp.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// virtualOSApp.swift -// virtualOS -// -// Created by Jahn Bertsch on 16.03.22. -// - -import Foundation -import SwiftUI -import OSLog - -typealias CompletionHander = (String?) -> Void -typealias ProgressHandler = (Progress) -> Void - -@main -struct virtualOSApp: App { - static let logger = Logger(subsystem: "com.github.yep.virtualOS", category: "main") - - #if arch(arm64) - @StateObject var viewModel = ViewModel() - #endif - - @AppStorage("NSFullScreenMenuItemEverywhere") var fullScreenMenuItemEverywhere = false - @NSApplicationDelegateAdaptor(ApplicationDelegate.self) var applicationDelegate - - init() { - fullScreenMenuItemEverywhere = false - NSWindow.allowsAutomaticWindowTabbing = false - } - - var body: some Scene { - WindowGroup { - #if arch(arm64) - - MainView(viewModel: viewModel) - .alert("Delete \(viewModel.confirmationText)", isPresented: $viewModel.showConfirmationAlert) { - Button("OK") { - viewModel.showConfirmationAlert = !viewModel.showConfirmationAlert - viewModel.confirmationHandler("") - } - Button("Cancel") { - viewModel.showConfirmationAlert = !viewModel.showConfirmationAlert - } - } message: { - Text("Are you sure you want to delete the \(viewModel.confirmationText.lowercased())?") - } - .alert(viewModel.licenseInformationTitleString, isPresented: $viewModel.showLicenseInformationModal, actions: {}, message: { - Text(viewModel.licenseInformationString) - }) - .sheet(isPresented: $viewModel.showSettings, content: { - SettingsView(viewModel: viewModel) - }) - - #else - - Text("Sorry, virtualization requires an Apple Silicon computer.") - .frame(minWidth: 400, minHeight: 300) - - #endif // #if arch(arm64) - } - .commands { - #if arch(arm64) - MenuCommands(viewModel: viewModel) - #endif - } - } - - static func debugLog(_ message: String) { - Self.logger.notice("\(message, privacy: .public)") - } -} diff --git a/virtualOSTests/virtualOSTests.swift b/virtualOSTests/virtualOSTests.swift deleted file mode 100644 index 70e671c..0000000 --- a/virtualOSTests/virtualOSTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// virtualOSTests.swift -// virtualOSTests -// -// Created by Jahn Bertsch on 16.03.22. -// - -import XCTest -@testable import virtualOS - -class virtualOSTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/virtualOSUITests/virtualOSUITests.swift b/virtualOSUITests/virtualOSUITests.swift deleted file mode 100644 index 556d641..0000000 --- a/virtualOSUITests/virtualOSUITests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// virtualOSUITests.swift -// virtualOSUITests -// -// Created by Jahn Bertsch on 16.03.22. -// - -import XCTest - -class virtualOSUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } - } -} diff --git a/virtualOSUITests/virtualOSUITestsLaunchTests.swift b/virtualOSUITests/virtualOSUITestsLaunchTests.swift deleted file mode 100644 index cd594bb..0000000 --- a/virtualOSUITests/virtualOSUITestsLaunchTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// virtualOSUITestsLaunchTests.swift -// virtualOSUITests -// -// Created by Jahn Bertsch on 16.03.22. -// - -import XCTest - -class virtualOSUITestsLaunchTests: XCTestCase { - - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } - - override func setUpWithError() throws { - continueAfterFailure = false - } - - func testLaunch() throws { - let app = XCUIApplication() - app.launch() - - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app - - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } -}