diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0491c7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +**/xcuserdata/* diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..abe2656 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Victor Gama + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Podman.xcodeproj/project.pbxproj b/Podman.xcodeproj/project.pbxproj new file mode 100644 index 0000000..ecbc8b4 --- /dev/null +++ b/Podman.xcodeproj/project.pbxproj @@ -0,0 +1,482 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXBuildFile section */ + DA1ED08826E2B6AF009A8CD8 /* PMContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = DA1ED08726E2B6AF009A8CD8 /* PMContainer.m */; }; + DA1ED08B26E2BD06009A8CD8 /* PMOperationResult.m in Sources */ = {isa = PBXBuildFile; fileRef = DA1ED08A26E2BD06009A8CD8 /* PMOperationResult.m */; }; + DA1ED08F26E2C816009A8CD8 /* PMDispatch.m in Sources */ = {isa = PBXBuildFile; fileRef = DA1ED08E26E2C816009A8CD8 /* PMDispatch.m */; }; + DA60FC2826E2DF5A00756E30 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = DA60FC2726E2DF5A00756E30 /* Sparkle */; }; + DA60FC2B26E2E79600756E30 /* PMMoveToApplications.m in Sources */ = {isa = PBXBuildFile; fileRef = DA60FC2A26E2E79600756E30 /* PMMoveToApplications.m */; }; + DA60FC3026E2F1AA00756E30 /* PMPreferencesController.m in Sources */ = {isa = PBXBuildFile; fileRef = DA60FC2F26E2F1AA00756E30 /* PMPreferencesController.m */; }; + DA60FC3326E2F31300756E30 /* PMPreferences.m in Sources */ = {isa = PBXBuildFile; fileRef = DA60FC3226E2F31300756E30 /* PMPreferences.m */; }; + DA60FC6A26E318AC00756E30 /* PMLoginItem.m in Sources */ = {isa = PBXBuildFile; fileRef = DA60FC6926E318AC00756E30 /* PMLoginItem.m */; }; + DA618A9726E1233A000AB06C /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DA618A9626E1233A000AB06C /* AppDelegate.m */; }; + DA618A9A26E1233A000AB06C /* PopoverController.m in Sources */ = {isa = PBXBuildFile; fileRef = DA618A9926E1233A000AB06C /* PopoverController.m */; }; + DA618A9C26E1233C000AB06C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DA618A9B26E1233C000AB06C /* Assets.xcassets */; }; + DA618A9F26E1233C000AB06C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DA618A9D26E1233C000AB06C /* Main.storyboard */; }; + DA618AA226E1233C000AB06C /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DA618AA126E1233C000AB06C /* main.m */; }; + DA618AAB26E12E1A000AB06C /* PMStatusBarController.m in Sources */ = {isa = PBXBuildFile; fileRef = DA618AAA26E12E1A000AB06C /* PMStatusBarController.m */; }; + DA618AAE26E13CAD000AB06C /* PMApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = DA618AAD26E13CAD000AB06C /* PMApplication.m */; }; + DA618AB126E14BF8000AB06C /* PMManager.m in Sources */ = {isa = PBXBuildFile; fileRef = DA618AB026E14BF8000AB06C /* PMManager.m */; }; + DA618AB426E1684C000AB06C /* PMInstallWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = DA618AB326E1684C000AB06C /* PMInstallWindowController.m */; }; + DA618AB826E1AB30000AB06C /* PMInstallProgressViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DA618AB626E1AB30000AB06C /* PMInstallProgressViewController.m */; }; + DA618ABC26E1D227000AB06C /* PMContainerCellView.m in Sources */ = {isa = PBXBuildFile; fileRef = DA618ABB26E1D227000AB06C /* PMContainerCellView.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + DA60FC6626E3146200756E30 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = Contents/Library/LoginItems; + dstSubfolderSpec = 1; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + DA1ED08626E2B6AF009A8CD8 /* PMContainer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PMContainer.h; sourceTree = ""; }; + DA1ED08726E2B6AF009A8CD8 /* PMContainer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PMContainer.m; sourceTree = ""; }; + DA1ED08926E2BD06009A8CD8 /* PMOperationResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PMOperationResult.h; sourceTree = ""; }; + DA1ED08A26E2BD06009A8CD8 /* PMOperationResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PMOperationResult.m; sourceTree = ""; }; + DA1ED08C26E2BFC1009A8CD8 /* PMCommon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PMCommon.h; sourceTree = ""; }; + DA1ED08D26E2C816009A8CD8 /* PMDispatch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PMDispatch.h; sourceTree = ""; }; + DA1ED08E26E2C816009A8CD8 /* PMDispatch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PMDispatch.m; sourceTree = ""; }; + DA60FC2926E2E79600756E30 /* PMMoveToApplications.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PMMoveToApplications.h; sourceTree = ""; }; + DA60FC2A26E2E79600756E30 /* PMMoveToApplications.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PMMoveToApplications.m; sourceTree = ""; }; + DA60FC2E26E2F1AA00756E30 /* PMPreferencesController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PMPreferencesController.h; sourceTree = ""; }; + DA60FC2F26E2F1AA00756E30 /* PMPreferencesController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PMPreferencesController.m; sourceTree = ""; }; + DA60FC3126E2F31300756E30 /* PMPreferences.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PMPreferences.h; sourceTree = ""; }; + DA60FC3226E2F31300756E30 /* PMPreferences.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PMPreferences.m; sourceTree = ""; }; + DA60FC6826E318AC00756E30 /* PMLoginItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PMLoginItem.h; sourceTree = ""; }; + DA60FC6926E318AC00756E30 /* PMLoginItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PMLoginItem.m; sourceTree = ""; }; + DA618A9226E1233A000AB06C /* Podman.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Podman.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DA618A9526E1233A000AB06C /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + DA618A9626E1233A000AB06C /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + DA618A9826E1233A000AB06C /* PopoverController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PopoverController.h; sourceTree = ""; }; + DA618A9926E1233A000AB06C /* PopoverController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PopoverController.m; sourceTree = ""; }; + DA618A9B26E1233C000AB06C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + DA618A9E26E1233C000AB06C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + DA618AA026E1233C000AB06C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DA618AA126E1233C000AB06C /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + DA618AA326E1233C000AB06C /* Podman.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Podman.entitlements; sourceTree = ""; }; + DA618AA926E12E1A000AB06C /* PMStatusBarController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PMStatusBarController.h; sourceTree = ""; }; + DA618AAA26E12E1A000AB06C /* PMStatusBarController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PMStatusBarController.m; sourceTree = ""; }; + DA618AAC26E13CAD000AB06C /* PMApplication.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PMApplication.h; sourceTree = ""; }; + DA618AAD26E13CAD000AB06C /* PMApplication.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PMApplication.m; sourceTree = ""; }; + DA618AAF26E14BF8000AB06C /* PMManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PMManager.h; sourceTree = ""; }; + DA618AB026E14BF8000AB06C /* PMManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PMManager.m; sourceTree = ""; }; + DA618AB226E1684C000AB06C /* PMInstallWindowController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PMInstallWindowController.h; sourceTree = ""; }; + DA618AB326E1684C000AB06C /* PMInstallWindowController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PMInstallWindowController.m; sourceTree = ""; }; + DA618AB526E1AB30000AB06C /* PMInstallProgressViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PMInstallProgressViewController.h; sourceTree = ""; }; + DA618AB626E1AB30000AB06C /* PMInstallProgressViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PMInstallProgressViewController.m; sourceTree = ""; }; + DA618ABA26E1D227000AB06C /* PMContainerCellView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PMContainerCellView.h; sourceTree = ""; }; + DA618ABB26E1D227000AB06C /* PMContainerCellView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PMContainerCellView.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + DA618A8F26E1233A000AB06C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DA60FC2826E2DF5A00756E30 /* Sparkle in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + DA1ED08526E2B698009A8CD8 /* Interop */ = { + isa = PBXGroup; + children = ( + DA618AAF26E14BF8000AB06C /* PMManager.h */, + DA618AB026E14BF8000AB06C /* PMManager.m */, + DA1ED08626E2B6AF009A8CD8 /* PMContainer.h */, + DA1ED08726E2B6AF009A8CD8 /* PMContainer.m */, + DA1ED08926E2BD06009A8CD8 /* PMOperationResult.h */, + DA1ED08A26E2BD06009A8CD8 /* PMOperationResult.m */, + DA1ED08C26E2BFC1009A8CD8 /* PMCommon.h */, + DA60FC6826E318AC00756E30 /* PMLoginItem.h */, + DA60FC6926E318AC00756E30 /* PMLoginItem.m */, + ); + path = Interop; + sourceTree = ""; + }; + DA60FC2C26E2EE3500756E30 /* Controllers */ = { + isa = PBXGroup; + children = ( + DA618A9826E1233A000AB06C /* PopoverController.h */, + DA618A9926E1233A000AB06C /* PopoverController.m */, + DA618AA926E12E1A000AB06C /* PMStatusBarController.h */, + DA618AAA26E12E1A000AB06C /* PMStatusBarController.m */, + DA618AB226E1684C000AB06C /* PMInstallWindowController.h */, + DA618AB326E1684C000AB06C /* PMInstallWindowController.m */, + DA618AB526E1AB30000AB06C /* PMInstallProgressViewController.h */, + DA618AB626E1AB30000AB06C /* PMInstallProgressViewController.m */, + DA60FC2E26E2F1AA00756E30 /* PMPreferencesController.h */, + DA60FC2F26E2F1AA00756E30 /* PMPreferencesController.m */, + ); + path = Controllers; + sourceTree = ""; + }; + DA60FC2D26E2EE6200756E30 /* Controls */ = { + isa = PBXGroup; + children = ( + DA618ABA26E1D227000AB06C /* PMContainerCellView.h */, + DA618ABB26E1D227000AB06C /* PMContainerCellView.m */, + ); + path = Controls; + sourceTree = ""; + }; + DA618A8926E1233A000AB06C = { + isa = PBXGroup; + children = ( + DA618A9426E1233A000AB06C /* Podman */, + DA618A9326E1233A000AB06C /* Products */, + ); + sourceTree = ""; + }; + DA618A9326E1233A000AB06C /* Products */ = { + isa = PBXGroup; + children = ( + DA618A9226E1233A000AB06C /* Podman.app */, + ); + name = Products; + sourceTree = ""; + }; + DA618A9426E1233A000AB06C /* Podman */ = { + isa = PBXGroup; + children = ( + DA60FC2D26E2EE6200756E30 /* Controls */, + DA60FC2C26E2EE3500756E30 /* Controllers */, + DA1ED08526E2B698009A8CD8 /* Interop */, + DA60FC2926E2E79600756E30 /* PMMoveToApplications.h */, + DA60FC2A26E2E79600756E30 /* PMMoveToApplications.m */, + DA618A9526E1233A000AB06C /* AppDelegate.h */, + DA618A9626E1233A000AB06C /* AppDelegate.m */, + DA618AAC26E13CAD000AB06C /* PMApplication.h */, + DA618AAD26E13CAD000AB06C /* PMApplication.m */, + DA1ED08D26E2C816009A8CD8 /* PMDispatch.h */, + DA1ED08E26E2C816009A8CD8 /* PMDispatch.m */, + DA60FC3126E2F31300756E30 /* PMPreferences.h */, + DA60FC3226E2F31300756E30 /* PMPreferences.m */, + DA618A9B26E1233C000AB06C /* Assets.xcassets */, + DA618A9D26E1233C000AB06C /* Main.storyboard */, + DA618AA026E1233C000AB06C /* Info.plist */, + DA618AA126E1233C000AB06C /* main.m */, + DA618AA326E1233C000AB06C /* Podman.entitlements */, + ); + path = Podman; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + DA618A9126E1233A000AB06C /* Podman */ = { + isa = PBXNativeTarget; + buildConfigurationList = DA618AA626E1233C000AB06C /* Build configuration list for PBXNativeTarget "Podman" */; + buildPhases = ( + DA618A8E26E1233A000AB06C /* Sources */, + DA618A8F26E1233A000AB06C /* Frameworks */, + DA618A9026E1233A000AB06C /* Resources */, + DA60FC6626E3146200756E30 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Podman; + packageProductDependencies = ( + DA60FC2726E2DF5A00756E30 /* Sparkle */, + ); + productName = "Podman macOS"; + productReference = DA618A9226E1233A000AB06C /* Podman.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + DA618A8A26E1233A000AB06C /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1250; + TargetAttributes = { + DA618A9126E1233A000AB06C = { + CreatedOnToolsVersion = 12.5.1; + }; + }; + }; + buildConfigurationList = DA618A8D26E1233A000AB06C /* Build configuration list for PBXProject "Podman" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = DA618A8926E1233A000AB06C; + packageReferences = ( + DA60FC2626E2DF5A00756E30 /* XCRemoteSwiftPackageReference "Sparkle" */, + ); + productRefGroup = DA618A9326E1233A000AB06C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + DA618A9126E1233A000AB06C /* Podman */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + DA618A9026E1233A000AB06C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DA618A9C26E1233C000AB06C /* Assets.xcassets in Resources */, + DA618A9F26E1233C000AB06C /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + DA618A8E26E1233A000AB06C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DA60FC3326E2F31300756E30 /* PMPreferences.m in Sources */, + DA618AAB26E12E1A000AB06C /* PMStatusBarController.m in Sources */, + DA618A9A26E1233A000AB06C /* PopoverController.m in Sources */, + DA618AA226E1233C000AB06C /* main.m in Sources */, + DA618A9726E1233A000AB06C /* AppDelegate.m in Sources */, + DA1ED08F26E2C816009A8CD8 /* PMDispatch.m in Sources */, + DA618AB826E1AB30000AB06C /* PMInstallProgressViewController.m in Sources */, + DA618ABC26E1D227000AB06C /* PMContainerCellView.m in Sources */, + DA1ED08B26E2BD06009A8CD8 /* PMOperationResult.m in Sources */, + DA618AB126E14BF8000AB06C /* PMManager.m in Sources */, + DA60FC2B26E2E79600756E30 /* PMMoveToApplications.m in Sources */, + DA618AB426E1684C000AB06C /* PMInstallWindowController.m in Sources */, + DA1ED08826E2B6AF009A8CD8 /* PMContainer.m in Sources */, + DA60FC6A26E318AC00756E30 /* PMLoginItem.m in Sources */, + DA618AAE26E13CAD000AB06C /* PMApplication.m in Sources */, + DA60FC3026E2F1AA00756E30 /* PMPreferencesController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + DA618A9D26E1233C000AB06C /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + DA618A9E26E1233C000AB06C /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + DA618AA426E1233C000AB06C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 11.3; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = Debug; + }; + DA618AA526E1233C000AB06C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 11.3; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + }; + name = Release; + }; + DA618AA726E1233C000AB06C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Podman/Podman.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 7GEB3UCDV7; + ENABLE_HARDENED_RUNTIME = YES; + INFOPLIST_FILE = "$(SRCROOT)/Podman/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + PRODUCT_BUNDLE_IDENTIFIER = "io.vito.Podman-macOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + DA618AA826E1233C000AB06C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Podman/Podman.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 7GEB3UCDV7; + ENABLE_HARDENED_RUNTIME = YES; + INFOPLIST_FILE = "$(SRCROOT)/Podman/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + PRODUCT_BUNDLE_IDENTIFIER = "io.vito.Podman-macOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + DA618A8D26E1233A000AB06C /* Build configuration list for PBXProject "Podman" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DA618AA426E1233C000AB06C /* Debug */, + DA618AA526E1233C000AB06C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DA618AA626E1233C000AB06C /* Build configuration list for PBXNativeTarget "Podman" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DA618AA726E1233C000AB06C /* Debug */, + DA618AA826E1233C000AB06C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + DA60FC2626E2DF5A00756E30 /* XCRemoteSwiftPackageReference "Sparkle" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sparkle-project/Sparkle"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.26.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + DA60FC2726E2DF5A00756E30 /* Sparkle */ = { + isa = XCSwiftPackageProductDependency; + package = DA60FC2626E2DF5A00756E30 /* XCRemoteSwiftPackageReference "Sparkle" */; + productName = Sparkle; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = DA618A8A26E1233A000AB06C /* Project object */; +} diff --git a/Podman.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Podman.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Podman.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Podman.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Podman.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Podman.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Podman.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Podman.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..c7cb63e --- /dev/null +++ b/Podman.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "Sparkle", + "repositoryURL": "https://github.com/sparkle-project/Sparkle", + "state": { + "branch": null, + "revision": "c0933a516b420806e9216e71bd13ba76c54f0de6", + "version": "1.26.0" + } + } + ] + }, + "version": 1 +} diff --git a/Podman/AppDelegate.h b/Podman/AppDelegate.h new file mode 100644 index 0000000..d21ebaa --- /dev/null +++ b/Podman/AppDelegate.h @@ -0,0 +1,18 @@ +// +// AppDelegate.h +// Podman macOS +// +// Created by Victor Gama on 02/09/2021. +// + +#import + +@interface AppDelegate : NSObject + + +/// Starts the agent by placing an icon in the system's Menu Bar. +/// Multiple calls to this method has no effect. +- (void)startAgent; + +@end + diff --git a/Podman/AppDelegate.m b/Podman/AppDelegate.m new file mode 100644 index 0000000..9f2800e --- /dev/null +++ b/Podman/AppDelegate.m @@ -0,0 +1,164 @@ +// +// AppDelegate.m +// Podman macOS +// +// Created by Victor Gama on 02/09/2021. +// + +#import + +#import "AppDelegate.h" +#import "PMStatusBarController.h" +#import "PopoverController.h" +#import "PMManager.h" +#import "PMMoveToApplications.h" +#import "PMPreferences.h" +#import "PMDispatch.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate { + PMStatusBarController *controller; + NSPopover *popover; + BOOL agentRunning; +} + +- (void)ensureSingleInstance { + pid_t selfPid = [[NSRunningApplication currentApplication] processIdentifier]; + NSArray *appArray = [NSRunningApplication runningApplicationsWithBundleIdentifier:[[NSBundle mainBundle] bundleIdentifier]]; + for (NSRunningApplication *app in appArray) { + if ([app processIdentifier] != selfPid) { + [NSApp terminate:nil]; + break; + } + } +} + + +- (void)applicationWillFinishLaunching:(NSNotification *)notification { + [self ensureSingleInstance]; +#ifndef DEBUG + [PMMoveToApplications moveToApplicationsFolderIfNecessary]; +#endif +} + + +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + SUUpdater *updater = [SUUpdater sharedUpdater]; + updater.automaticallyChecksForUpdates = PMPreferences.checkForUpdates; + updater.feedURL = [NSURL URLWithString:@"https://heyvito.github.io/podman-macos/sparkle.xml"]; + + PMOperationResult *detectPodmanResult = [PMManager.manager detectPodman]; + switch ([detectPodmanResult detectStateValue]) { + case PMDetectStateNotInPath: { + NSAlert *alert = [[NSAlert alloc] init]; + alert.alertStyle = NSAlertStyleCritical; + alert.messageText = @"Podman for macOS did not find a Podman executable"; + alert.informativeText = @"Make sure Podman is installed and available in your PATH."; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; + [NSApp terminate:nil]; + return; + } + case PMDetectStateError: { + NSAlert *alert = [[NSAlert alloc] init]; + alert.alertStyle = NSAlertStyleCritical; + alert.messageText = @"Error detecting Podman executable"; + alert.informativeText = detectPodmanResult.output; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; + [NSApp terminate:nil]; + return; + } + case PMDetectStateOK: + NSLog(@"Podman detection succeeded."); + } + + PMOperationResult *detectVMResult = [PMManager.manager detectVM]; + switch ([detectVMResult vmPresenceValue]) { + case PMVMPresenceError: { + NSAlert *alert = [[NSAlert alloc] init]; + alert.alertStyle = NSAlertStyleCritical; + alert.messageText = @"Error detecting Podman Machine"; + alert.informativeText = detectVMResult.output; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; + [NSApp terminate:nil]; + return; + } + + case PMVMPresenceAbsent: { + NSStoryboard *story = [NSStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]]; + NSWindowController *windowController = [story instantiateControllerWithIdentifier:@"InstallWindow"]; + [windowController showWindow:self]; + return; + } + + case PMVMPresencePresent: + NSLog(@"Podman Machine detection succeeded."); + } + + [self startAgent]; +} + + +- (void)startAgent { + if (self->agentRunning) { + return; + } + self->agentRunning = YES; + + NSStoryboard *story = [NSStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]]; + NSViewController *viewController = [story instantiateControllerWithIdentifier:@"PopoverViewController"]; + popover = [[NSPopover alloc] init]; + popover.contentViewController = viewController; + popover.behavior = NSPopoverBehaviorTransient; + controller = [[PMStatusBarController alloc] initWithPopover:popover]; + if (PMPreferences.startPodmanVM) { + NSLog(@"PM is set to autostart VM"); + if (PMManager.manager.serviceStatus == PMServiceStatusStopped) { + [PMDispatch background:^{ + PMOperationResult *result = [PMManager.manager startVM]; + if (!result.succeeded) { + [PMDispatch sync:^{ + NSAlert *alert = [[NSAlert alloc] init]; + alert.alertStyle = NSAlertStyleWarning; + alert.messageText = @"Podman for macOS could not automatically start Podman's VM"; + alert.informativeText = result.output; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; + }]; + } else { + NSLog(@"VM started successfully."); + } + }]; + } else { + NSLog(@"VM is already running."); + } + } + + if (!PMPreferences.askedToStartAtLogin) { + PMPreferences.askedToStartAtLogin = YES; + NSAlert *alert = [[NSAlert alloc] init]; + alert.alertStyle = NSAlertStyleInformational; + alert.messageText = @"Would you like to Podman for macOS to start at login?"; + alert.informativeText = @"This can be changed both in Podman for macOS's preferences and the System Preferences."; + NSButton *yesButton = [alert addButtonWithTitle:@"Yes"]; + yesButton.keyEquivalent = @"\r"; + NSButton *noButton = [alert addButtonWithTitle:@"No"]; + noButton.keyEquivalent = @"\033"; + if([alert runModal] == NSAlertFirstButtonReturn) { + PMPreferences.startAtLogin = YES; + } + } +} + + +- (void)applicationWillResignActive:(NSNotification *)notification { + [popover close]; +} + + +@end diff --git a/Podman/Assets.xcassets/AccentColor.colorset/Contents.json b/Podman/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Podman/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Podman/Assets.xcassets/AppIcon.appiconset/Contents.json b/Podman/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..b8bd462 --- /dev/null +++ b/Podman/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "icon_16x16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "icon_16x16@2x@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "icon_32x32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "icon_32x32@2x@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "icon_128x128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "icon_128x128@2x@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "icon_256x256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "icon_256x256@2x@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "icon_512x512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "icon_512x512@2x@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Podman/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/Podman/Assets.xcassets/AppIcon.appiconset/icon_128x128.png new file mode 100644 index 0000000..874cf1c Binary files /dev/null and b/Podman/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/Podman/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png b/Podman/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png new file mode 100644 index 0000000..e367b9d Binary files /dev/null and b/Podman/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png differ diff --git a/Podman/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/Podman/Assets.xcassets/AppIcon.appiconset/icon_16x16.png new file mode 100644 index 0000000..fc718b0 Binary files /dev/null and b/Podman/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/Podman/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x@2x.png b/Podman/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x@2x.png new file mode 100644 index 0000000..c4960d1 Binary files /dev/null and b/Podman/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x@2x.png differ diff --git a/Podman/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/Podman/Assets.xcassets/AppIcon.appiconset/icon_256x256.png new file mode 100644 index 0000000..59c3fc5 Binary files /dev/null and b/Podman/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/Podman/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x@2x.png b/Podman/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x@2x.png new file mode 100644 index 0000000..2728759 Binary files /dev/null and b/Podman/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x@2x.png differ diff --git a/Podman/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/Podman/Assets.xcassets/AppIcon.appiconset/icon_32x32.png new file mode 100644 index 0000000..c4960d1 Binary files /dev/null and b/Podman/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/Podman/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x@2x.png b/Podman/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x@2x.png new file mode 100644 index 0000000..7b22828 Binary files /dev/null and b/Podman/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x@2x.png differ diff --git a/Podman/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/Podman/Assets.xcassets/AppIcon.appiconset/icon_512x512.png new file mode 100644 index 0000000..2728759 Binary files /dev/null and b/Podman/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ diff --git a/Podman/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png b/Podman/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png new file mode 100644 index 0000000..c7ea9f2 Binary files /dev/null and b/Podman/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png differ diff --git a/Podman/Assets.xcassets/Contents.json b/Podman/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Podman/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Podman/Assets.xcassets/icon.imageset/Contents.json b/Podman/Assets.xcassets/icon.imageset/Contents.json new file mode 100644 index 0000000..e6b7fe1 --- /dev/null +++ b/Podman/Assets.xcassets/icon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Podman/Assets.xcassets/icon.imageset/icon.pdf b/Podman/Assets.xcassets/icon.imageset/icon.pdf new file mode 100644 index 0000000..83a105d Binary files /dev/null and b/Podman/Assets.xcassets/icon.imageset/icon.pdf differ diff --git a/Podman/Assets.xcassets/podman-logo-full.imageset/Contents.json b/Podman/Assets.xcassets/podman-logo-full.imageset/Contents.json new file mode 100644 index 0000000..f624839 --- /dev/null +++ b/Podman/Assets.xcassets/podman-logo-full.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "podman-logo-full.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Podman/Assets.xcassets/podman-logo-full.imageset/podman-logo-full.pdf b/Podman/Assets.xcassets/podman-logo-full.imageset/podman-logo-full.pdf new file mode 100644 index 0000000..13af723 Binary files /dev/null and b/Podman/Assets.xcassets/podman-logo-full.imageset/podman-logo-full.pdf differ diff --git a/Podman/Assets.xcassets/podman-logo-text.imageset/Contents.json b/Podman/Assets.xcassets/podman-logo-text.imageset/Contents.json new file mode 100644 index 0000000..bb54a61 --- /dev/null +++ b/Podman/Assets.xcassets/podman-logo-text.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "podman-logo-text.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/Podman/Assets.xcassets/podman-logo-text.imageset/podman-logo-text.pdf b/Podman/Assets.xcassets/podman-logo-text.imageset/podman-logo-text.pdf new file mode 100644 index 0000000..18d0202 Binary files /dev/null and b/Podman/Assets.xcassets/podman-logo-text.imageset/podman-logo-text.pdf differ diff --git a/Podman/Assets.xcassets/podman-logo.imageset/Contents.json b/Podman/Assets.xcassets/podman-logo.imageset/Contents.json new file mode 100644 index 0000000..6b096cb --- /dev/null +++ b/Podman/Assets.xcassets/podman-logo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "podman-logo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/Podman/Assets.xcassets/podman-logo.imageset/podman-logo.pdf b/Podman/Assets.xcassets/podman-logo.imageset/podman-logo.pdf new file mode 100644 index 0000000..125aa7d Binary files /dev/null and b/Podman/Assets.xcassets/podman-logo.imageset/podman-logo.pdf differ diff --git a/Podman/Base.lproj/Main.storyboard b/Podman/Base.lproj/Main.storyboard new file mode 100644 index 0000000..24907ed --- /dev/null +++ b/Podman/Base.lproj/Main.storyboard @@ -0,0 +1,1216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +CA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Podman on macOS requires a virtual machine in place to run containers. For those purposes, Podman provides the command "podman machine", which executes a virtual machine through QEMU to which the podman CLI communicates to. Continuing will create a new "podman-machine-default" virtual machine (later accessible through the podman machine list) using the "podman machine init" command. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Podman for macOS is distributed under the MIT license: + +The MIT License (MIT) + +Copyright (c) 2021 Victor Gama + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +Podman is distributed under the Apache License. +Podman is maintained by the containers organization. https://github.com/containers + + +Apache License +Version 2.0, January 2004 +https://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files. + +"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by Licensor and +subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions: + +(a) You must give any other recipients of the Work or +Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices +stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works +that You distribute, all copyright, patent, trademark, and +attribution notices from the Source form of the Work, +excluding those notices that do not pertain to any part of +the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its +distribution, then any Derivative Works that You distribute must +include a readable copy of the attribution notices contained +within such NOTICE file, excluding those notices that do not +pertain to any part of the Derivative Works, in at least one +of the following places: within a NOTICE text file distributed +as part of the Derivative Works; within the Source form or +documentation, if provided along with the Derivative Works; or, +within a display generated by the Derivative Works, if and +wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and +do not modify the License. You may add Your own attribution +notices within Derivative Works that You distribute, alongside +or as an addendum to the NOTICE text from the Work, provided +that such additional attribution notices cannot be construed +as modifying the License. + +You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work +by You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses), even if such Contributor +has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05T +S2V5ZWRBcmNoaXZlctEICVRyb290gAGvEBcLDBkaIRQmKywzNjs+P0RHSEtVXV5iZVUkbnVsbNYNDg8Q +ERITFBUWFxhWTlNTaXplXk5TUmVzaXppbmdNb2RlViRjbGFzc1xOU0ltYWdlRmxhZ3NWTlNSZXBzV05T +Q29sb3KAAhAAgBYSIMMAAIADgBFWezEsIDF90hsPHCBaTlMub2JqZWN0c6MdHh+ABIAKgA2AENIbDyIl +oiMkgAWABoAJ0w8nKCkqFF8QFE5TVElGRlJlcHJlc2VudGF0aW9uXxAZTlNJbnRlcm5hbExheW91dERp +cmVjdGlvboAIgAdPEQ0qTU0AKgAAAAwAAAAAABABAAADAAAAAQABAAABAQADAAAAAQABAAABAgADAAAA +BAAAANIBAwADAAAAAQABAAABBgADAAAAAQACAAABCgADAAAAAQABAAABEQAEAAAAAQAAAAgBEgADAAAA +AQABAAABFQADAAAAAQAEAAABFgADAAAAAQABAAABFwAEAAAAAQAAAAQBHAADAAAAAQABAAABKAADAAAA +AQACAAABUgADAAAAAQABAAABUwADAAAABAAAANqHcwAHAAAMSAAAAOIAAAAAAAgACAAIAAgAAQABAAEA +AQAADEhMaW5vAhAAAG1udHJSR0IgWFlaIAfOAAIACQAGADEAAGFjc3BNU0ZUAAAAAElFQyBzUkdCAAAA +AAAAAAAAAAAAAAD21gABAAAAANMtSFAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAEWNwcnQAAAFQAAAAM2Rlc2MAAAGEAAAAbHd0cHQAAAHwAAAAFGJrcHQAAAIEAAAA +FHJYWVoAAAIYAAAAFGdYWVoAAAIsAAAAFGJYWVoAAAJAAAAAFGRtbmQAAAJUAAAAcGRtZGQAAALEAAAA +iHZ1ZWQAAANMAAAAhnZpZXcAAAPUAAAAJGx1bWkAAAP4AAAAFG1lYXMAAAQMAAAAJHRlY2gAAAQwAAAA +DHJUUkMAAAQ8AAAIDGdUUkMAAAQ8AAAIDGJUUkMAAAQ8AAAIDHRleHQAAAAAQ29weXJpZ2h0IChjKSAx +OTk4IEhld2xldHQtUGFja2FyZCBDb21wYW55AABkZXNjAAAAAAAAABJzUkdCIElFQzYxOTY2LTIuMQAA +AAAAAAAAAAAAEnNSR0IgSUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAABYWVogAAAAAAAA81EAAQAAAAEWzFhZWiAAAAAAAAAAAAAAAAAAAAAAWFla +IAAAAAAAAG+iAAA49QAAA5BYWVogAAAAAAAAYpkAALeFAAAY2lhZWiAAAAAAAAAkoAAAD4QAALbPZGVz +YwAAAAAAAAAWSUVDIGh0dHA6Ly93d3cuaWVjLmNoAAAAAAAAAAAAAAAWSUVDIGh0dHA6Ly93d3cuaWVj +LmNoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRlc2MAAAAAAAAA +LklFQyA2MTk2Ni0yLjEgRGVmYXVsdCBSR0IgY29sb3VyIHNwYWNlIC0gc1JHQgAAAAAAAAAAAAAALklF +QyA2MTk2Ni0yLjEgRGVmYXVsdCBSR0IgY29sb3VyIHNwYWNlIC0gc1JHQgAAAAAAAAAAAAAAAAAAAAAA +AAAAAABkZXNjAAAAAAAAACxSZWZlcmVuY2UgVmlld2luZyBDb25kaXRpb24gaW4gSUVDNjE5NjYtMi4x +AAAAAAAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAdmlldwAAAAAAE6T+ABRfLgAQzxQAA+3MAAQTCwADXJ4AAAABWFla +IAAAAAAATAlWAFAAAABXH+dtZWFzAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAACjwAAAAJzaWcgAAAA +AENSVCBjdXJ2AAAAAAAABAAAAAAFAAoADwAUABkAHgAjACgALQAyADcAOwBAAEUASgBPAFQAWQBeAGMA +aABtAHIAdwB8AIEAhgCLAJAAlQCaAJ8ApACpAK4AsgC3ALwAwQDGAMsA0ADVANsA4ADlAOsA8AD2APsB +AQEHAQ0BEwEZAR8BJQErATIBOAE+AUUBTAFSAVkBYAFnAW4BdQF8AYMBiwGSAZoBoQGpAbEBuQHBAckB +0QHZAeEB6QHyAfoCAwIMAhQCHQImAi8COAJBAksCVAJdAmcCcQJ6AoQCjgKYAqICrAK2AsECywLVAuAC +6wL1AwADCwMWAyEDLQM4A0MDTwNaA2YDcgN+A4oDlgOiA64DugPHA9MD4APsA/kEBgQTBCAELQQ7BEgE +VQRjBHEEfgSMBJoEqAS2BMQE0wThBPAE/gUNBRwFKwU6BUkFWAVnBXcFhgWWBaYFtQXFBdUF5QX2BgYG +FgYnBjcGSAZZBmoGewaMBp0GrwbABtEG4wb1BwcHGQcrBz0HTwdhB3QHhgeZB6wHvwfSB+UH+AgLCB8I +MghGCFoIbgiCCJYIqgi+CNII5wj7CRAJJQk6CU8JZAl5CY8JpAm6Cc8J5Qn7ChEKJwo9ClQKagqBCpgK +rgrFCtwK8wsLCyILOQtRC2kLgAuYC7ALyAvhC/kMEgwqDEMMXAx1DI4MpwzADNkM8w0NDSYNQA1aDXQN +jg2pDcMN3g34DhMOLg5JDmQOfw6bDrYO0g7uDwkPJQ9BD14Peg+WD7MPzw/sEAkQJhBDEGEQfhCbELkQ +1xD1ERMRMRFPEW0RjBGqEckR6BIHEiYSRRJkEoQSoxLDEuMTAxMjE0MTYxODE6QTxRPlFAYUJxRJFGoU +ixStFM4U8BUSFTQVVhV4FZsVvRXgFgMWJhZJFmwWjxayFtYW+hcdF0EXZReJF64X0hf3GBsYQBhlGIoY +rxjVGPoZIBlFGWsZkRm3Gd0aBBoqGlEadxqeGsUa7BsUGzsbYxuKG7Ib2hwCHCocUhx7HKMczBz1HR4d +Rx1wHZkdwx3sHhYeQB5qHpQevh7pHxMfPh9pH5Qfvx/qIBUgQSBsIJggxCDwIRwhSCF1IaEhziH7Iici +VSKCIq8i3SMKIzgjZiOUI8Ij8CQfJE0kfCSrJNolCSU4JWgllyXHJfcmJyZXJocmtyboJxgnSSd6J6sn +3CgNKD8ocSiiKNQpBik4KWspnSnQKgIqNSpoKpsqzysCKzYraSudK9EsBSw5LG4soizXLQwtQS12Last +4S4WLkwugi63Lu4vJC9aL5Evxy/+MDUwbDCkMNsxEjFKMYIxujHyMioyYzKbMtQzDTNGM38zuDPxNCs0 +ZTSeNNg1EzVNNYc1wjX9Njc2cjauNuk3JDdgN5w31zgUOFA4jDjIOQU5Qjl/Obw5+To2OnQ6sjrvOy07 +azuqO+g8JzxlPKQ84z0iPWE9oT3gPiA+YD6gPuA/IT9hP6I/4kAjQGRApkDnQSlBakGsQe5CMEJyQrVC +90M6Q31DwEQDREdEikTORRJFVUWaRd5GIkZnRqtG8Ec1R3tHwEgFSEtIkUjXSR1JY0mpSfBKN0p9SsRL +DEtTS5pL4kwqTHJMuk0CTUpNk03cTiVObk63TwBPSU+TT91QJ1BxULtRBlFQUZtR5lIxUnxSx1MTU19T +qlP2VEJUj1TbVShVdVXCVg9WXFapVvdXRFeSV+BYL1h9WMtZGllpWbhaB1pWWqZa9VtFW5Vb5Vw1XIZc +1l0nXXhdyV4aXmxevV8PX2Ffs2AFYFdgqmD8YU9homH1YklinGLwY0Njl2PrZEBklGTpZT1lkmXnZj1m +kmboZz1nk2fpaD9olmjsaUNpmmnxakhqn2r3a09rp2v/bFdsr20IbWBtuW4SbmtuxG8eb3hv0XArcIZw +4HE6cZVx8HJLcqZzAXNdc7h0FHRwdMx1KHWFdeF2Pnabdvh3VnezeBF4bnjMeSp5iXnnekZ6pXsEe2N7 +wnwhfIF84X1BfaF+AX5ifsJ/I3+Ef+WAR4CogQqBa4HNgjCCkoL0g1eDuoQdhICE44VHhauGDoZyhteH +O4efiASIaYjOiTOJmYn+imSKyoswi5aL/IxjjMqNMY2Yjf+OZo7OjzaPnpAGkG6Q1pE/kaiSEZJ6kuOT +TZO2lCCUipT0lV+VyZY0lp+XCpd1l+CYTJi4mSSZkJn8mmia1ZtCm6+cHJyJnPedZJ3SnkCerp8dn4uf ++qBpoNihR6G2oiailqMGo3aj5qRWpMelOKWpphqmi6b9p26n4KhSqMSpN6mpqhyqj6sCq3Wr6axcrNCt +RK24ri2uoa8Wr4uwALB1sOqxYLHWskuywrM4s660JbSctRO1irYBtnm28Ldot+C4WbjRuUq5wro7urW7 +LrunvCG8m70VvY++Cr6Evv+/er/1wHDA7MFnwePCX8Lbw1jD1MRRxM7FS8XIxkbGw8dBx7/IPci8yTrJ +uco4yrfLNsu2zDXMtc01zbXONs62zzfPuNA50LrRPNG+0j/SwdNE08bUSdTL1U7V0dZV1tjXXNfg2GTY +6Nls2fHadtr724DcBdyK3RDdlt4c3qLfKd+v4DbgveFE4cziU+Lb42Pj6+Rz5PzlhOYN5pbnH+ep6DLo +vOlG6dDqW+rl63Dr++yG7RHtnO4o7rTvQO/M8Fjw5fFy8f/yjPMZ86f0NPTC9VD13vZt9vv3ivgZ+Kj5 +OPnH+lf65/t3/Af8mP0p/br+S/7c/23//9ItLi8wWiRjbGFzc25hbWVYJGNsYXNzZXNfEBBOU0JpdG1h +cEltYWdlUmVwoy8xMlpOU0ltYWdlUmVwWE5TT2JqZWN00i0uNDVXTlNBcnJheaI0MtIbDzcloiM5gAWA +C4AJ0w8nKCk9FIAIgAxPEQ1eTU0AKgAAABgAAAAAAAAAAAAAAAAAAAAAABIBAAADAAAAAQACAAABAQAD +AAAAAQACAAABAgADAAAABAAAAQYBAwADAAAAAQABAAABBgADAAAAAQACAAABCgADAAAAAQABAAABEQAE +AAAAAQAAAAgBEgADAAAAAQABAAABFQADAAAAAQAEAAABFgADAAAAAQACAAABFwAEAAAAAQAAABABGgAF +AAAAAQAAAPYBGwAFAAAAAQAAAP4BHAADAAAAAQABAAABKAADAAAAAQACAAABUgADAAAAAQABAAABUwAD +AAAABAAAAQ6HcwAHAAAMSAAAARYAAAAAAAAAkAAAAAEAAACQAAAAAQAIAAgACAAIAAEAAQABAAEAAAxI +TGlubwIQAABtbnRyUkdCIFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAA +AAAAAAAA9tYAAQAAAADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAABFjcHJ0AAABUAAAADNkZXNjAAABhAAAAGx3dHB0AAAB8AAAABRia3B0AAACBAAAABRyWFla +AAACGAAAABRnWFlaAAACLAAAABRiWFlaAAACQAAAABRkbW5kAAACVAAAAHBkbWRkAAACxAAAAIh2dWVk +AAADTAAAAIZ2aWV3AAAD1AAAACRsdW1pAAAD+AAAABRtZWFzAAAEDAAAACR0ZWNoAAAEMAAAAAxyVFJD +AAAEPAAACAxnVFJDAAAEPAAACAxiVFJDAAAEPAAACAx0ZXh0AAAAAENvcHlyaWdodCAoYykgMTk5OCBI +ZXdsZXR0LVBhY2thcmQgQ29tcGFueQAAZGVzYwAAAAAAAAASc1JHQiBJRUM2MTk2Ni0yLjEAAAAAAAAA +AAAAABJzUkdCIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAA +AABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z2Rlc2MAAAAA +AAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkZXNjAAAAAAAAAC5JRUMg +NjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAC5JRUMgNjE5 +NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +ZGVzYwAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAA +AAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAHZpZXcAAAAAABOk/gAUXy4AEM8UAAPtzAAEEwsAA1yeAAAAAVhZWiAAAAAA +AEwJVgBQAAAAVx/nbWVhcwAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAo8AAAACc2lnIAAAAABDUlQg +Y3VydgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQBy +AHcAfACBAIYAiwCQAJUAmgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwEN +ARMBGQEfASUBKwEyATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHh +AekB8gH6AgMCDAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMA +AwsDFgMhAy0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRx +BH4EjASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3 +BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gICwgfCDIIRgha +CG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqYCq4KxQrc +CvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUANWg10DY4NqQ3D +Dd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBhEH4QmxC5ENcQ9RET +ETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT5RQGFCcUSRRqFIsUrRTO +FPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReuF9IX9xgbGEAYZRiKGK8Y1Rj6 +GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9ocAhwqHFIcexyjHMwc9R0eHUcdcB2Z +HcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCYIMQg8CEcIUghdSGhIc4h+yInIlUigiKv +It0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZclxyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/ +KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5M +LoIuty7uLyQvWi+RL8cv/jA1MGwwpDDbMRIxSjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTY +NRM1TTWHNcI1/TY3NnI2rjbpNyQ3YDecN9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvo +PCc8ZTykPOM9Ij1hPaE94D4gPmA+oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9 +Q8BEA0RHRIpEzkUSRVVFmkXeRiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0ua +S+JMKkxyTLpNAk1KTZNN3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRC +VI9U21UoVXVVwlYPVlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114 +XcleGl5sXr1fD19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9 +Z5Nn6Wg/aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGV +cfByS3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyB +fOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuHn4gE +iGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLjk02TtpQg +lIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6fHZ+Ln/qgaaDY +oUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1q+msXKzQrUStuK4t +rqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm40blKucK6O7q1uy67p7wh +vJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvFyMZGxsPHQce/yD3IvMk6ybnKOMq3 +yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG1EnUy9VO1dHWVdbY11zX4Nhk2OjZbNnx +2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ +6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozzGfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX ++uf7d/wH/Jj9Kf26/kv+3P9t///SGw9AJaIjQoAFgA6ACdMPJygpRhSACIAPTxENck1NACoAAAAsAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIBAAADAAAAAQADAAABAQADAAAAAQADAAAB +AgADAAAABAAAARoBAwADAAAAAQABAAABBgADAAAAAQACAAABCgADAAAAAQABAAABEQAEAAAAAQAAAAgB +EgADAAAAAQABAAABFQADAAAAAQAEAAABFgADAAAAAQADAAABFwAEAAAAAQAAACQBGgAFAAAAAQAAAQoB +GwAFAAAAAQAAARIBHAADAAAAAQABAAABKAADAAAAAQACAAABUgADAAAAAQABAAABUwADAAAABAAAASKH +cwAHAAAMSAAAASoAAAAAAAAA2AAAAAEAAADYAAAAAQAIAAgACAAIAAEAAQABAAEAAAxITGlubwIQAABt +bnRyUkdCIFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYA +AQAAAADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFj +cHJ0AAABUAAAADNkZXNjAAABhAAAAGx3dHB0AAAB8AAAABRia3B0AAACBAAAABRyWFlaAAACGAAAABRn +WFlaAAACLAAAABRiWFlaAAACQAAAABRkbW5kAAACVAAAAHBkbWRkAAACxAAAAIh2dWVkAAADTAAAAIZ2 +aWV3AAAD1AAAACRsdW1pAAAD+AAAABRtZWFzAAAEDAAAACR0ZWNoAAAEMAAAAAxyVFJDAAAEPAAACAxn +VFJDAAAEPAAACAxiVFJDAAAEPAAACAx0ZXh0AAAAAENvcHlyaWdodCAoYykgMTk5OCBIZXdsZXR0LVBh +Y2thcmQgQ29tcGFueQAAZGVzYwAAAAAAAAASc1JHQiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAABJzUkdC +IElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUA +AAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z2Rlc2MAAAAAAAAAFklFQyBo +dHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkZXNjAAAAAAAAAC5JRUMgNjE5NjYtMi4x +IERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERl +ZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAA +AAAsUmVmZXJlbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAALFJl +ZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAHZpZXcAAAAAABOk/gAUXy4AEM8UAAPtzAAEEwsAA1yeAAAAAVhZWiAAAAAAAEwJVgBQAAAA +Vx/nbWVhcwAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAo8AAAACc2lnIAAAAABDUlQgY3VydgAAAAAA +AAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYA +iwCQAJUAmgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUB +KwEyATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMC +DAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMhAy0D +OANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4EjASaBKgE +tgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3BkgGWQZqBnsG +jAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gICwgfCDIIRghaCG4IggiWCKoI +vgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqYCq4KxQrcCvMLCwsiCzkL +UQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUANWg10DY4NqQ3DDd4N+A4TDi4O +SQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBhEH4QmxC5ENcQ9RETETERTxFtEYwR +qhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYV +eBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReuF9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZ +txndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9ocAhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAe +ah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCYIMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2Yj +lCPCI/AkHyRNJHwkqyTaJQklOCVoJZclxyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYp +OClrKZ0p0CoCKjUqaCqbKs8rAis2K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQv +Wi+RL8cv/jA1MGwwpDDbMRIxSjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1 +/TY3NnI2rjbpNyQ3YDecN9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9 +Ij1hPaE94D4gPmA+oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpE +zkUSRVVFmkXeRiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpN +Ak1KTZNN3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVV +wlYPVlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1f +D19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/aJZo +7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfByS3KmcwFz +XXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyBfOF9QX2hfgF+ +Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuHn4gEiGmIzokziZmJ +/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLjk02TtpQglIqU9JVflcmW +NJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6fHZ+Ln/qgaaDYoUehtqImopaj +BqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1q+msXKzQrUStuK4trqGvFq+LsACw +dbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm40blKucK6O7q1uy67p7whvJu9Fb2Pvgq+ +hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvFyMZGxsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXN +Nc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG1EnUy9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXc +it0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vs +hu0R7ZzuKO6070DvzPBY8OXxcvH/8ozzGfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9 +Kf26/kv+3P9t///SLS5JSl5OU011dGFibGVBcnJheaNJNDLVTE1OTw9QUVJTVFdOU1doaXRlXE5TQ29t +cG9uZW50c1xOU0NvbG9yU3BhY2VfEBJOU0N1c3RvbUNvbG9yU3BhY2VEMCAwAEMwIDAQA4ASgBXUVldY +D1laW1xUTlNJRFVOU0lDQ1dOU01vZGVsEAmAExAAgBRPERGcAAARnGFwcGwCAAAAbW50ckdSQVlYWVog +B9wACAAXAA8ALgAPYWNzcEFQUEwAAAAAbm9uZQAAAAAAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1hcHBs +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFZGVzYwAAAMAAAAB5 +ZHNjbQAAATwAAAgaY3BydAAACVgAAAAjd3RwdAAACXwAAAAUa1RSQwAACZAAAAgMZGVzYwAAAAAAAAAf +R2VuZXJpYyBHcmF5IEdhbW1hIDIuMiBQcm9maWxlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG1sdWMAAAAA +AAAAHwAAAAxza1NLAAAALgAAAYRkYURLAAAAOgAAAbJjYUVTAAAAOAAAAex2aVZOAAAAQAAAAiRwdEJS +AAAASgAAAmR1a1VBAAAALAAAAq5mckZVAAAAPgAAAtpodUhVAAAANAAAAxh6aFRXAAAAGgAAA0xrb0tS +AAAAIgAAA2ZuYk5PAAAAOgAAA4hjc0NaAAAAKAAAA8JoZUlMAAAAJAAAA+pyb1JPAAAAKgAABA5kZURF +AAAATgAABDhpdElUAAAATgAABIZzdlNFAAAAOAAABNR6aENOAAAAGgAABQxqYUpQAAAAJgAABSZlbEdS +AAAAKgAABUxwdFBPAAAAUgAABXZubE5MAAAAQAAABchlc0VTAAAATAAABgh0aFRIAAAAMgAABlR0clRS +AAAAJAAABoZmaUZJAAAARgAABqpockhSAAAAPgAABvBwbFBMAAAASgAABy5hckVHAAAALAAAB3hydVJV +AAAAOgAAB6RlblVTAAAAPAAAB94AVgFhAGUAbwBiAGUAYwBuAOEAIABzAGkAdgDhACAAZwBhAG0AYQAg +ADIALAAyAEcAZQBuAGUAcgBpAHMAawAgAGcAcgDlACAAMgAsADIAIABnAGEAbQBtAGEALQBwAHIAbwBm +AGkAbABHAGEAbQBtAGEAIABkAGUAIABnAHIAaQBzAG8AcwAgAGcAZQBuAOgAcgBpAGMAYQAgADIALgAy +AEMepQB1ACAAaADsAG4AaAAgAE0A4AB1ACAAeADhAG0AIABDAGgAdQBuAGcAIABHAGEAbQBtAGEAIAAy +AC4AMgBQAGUAcgBmAGkAbAAgAEcAZQBuAOkAcgBpAGMAbwAgAGQAYQAgAEcAYQBtAGEAIABkAGUAIABD +AGkAbgB6AGEAcwAgADIALAAyBBcEMAQzBDAEOwRMBD0EMAAgAEcAcgBhAHkALQQzBDAEPAQwACAAMgAu +ADIAUAByAG8AZgBpAGwAIABnAOkAbgDpAHIAaQBxAHUAZQAgAGcAcgBpAHMAIABnAGEAbQBtAGEAIAAy +ACwAMgDBAGwAdABhAGwA4QBuAG8AcwAgAHMAegD8AHIAawBlACAAZwBhAG0AbQBhACAAMgAuADKQGnUo +cHCWjlFJXqYAMgAuADKCcl9pY8+P8Md8vBgAINaMwMkAIKwQucgAIAAyAC4AMgAg1QS4XNMMx3wARwBl +AG4AZQByAGkAcwBrACAAZwByAOUAIABnAGEAbQBtAGEAIAAyACwAMgAtAHAAcgBvAGYAaQBsAE8AYgBl +AGMAbgDhACABYQBlAGQA4QAgAGcAYQBtAGEAIAAyAC4AMgXSBdAF3gXUACAF0AXkBdUF6AAgBdsF3AXc +BdkAIAAyAC4AMgBHAGEAbQBhACAAZwByAGkAIABnAGUAbgBlAHIAaQBjAQMAIAAyACwAMgBBAGwAbABn +AGUAbQBlAGkAbgBlAHMAIABHAHIAYQB1AHMAdAB1AGYAZQBuAC0AUAByAG8AZgBpAGwAIABHAGEAbQBt +AGEAIAAyACwAMgBQAHIAbwBmAGkAbABvACAAZwByAGkAZwBpAG8AIABnAGUAbgBlAHIAaQBjAG8AIABk +AGUAbABsAGEAIABnAGEAbQBtAGEAIAAyACwAMgBHAGUAbgBlAHIAaQBzAGsAIABnAHIA5QAgADIALAAy +ACAAZwBhAG0AbQBhAHAAcgBvAGYAaQBsZm6QGnBwXqZ8+2VwADIALgAyY8+P8GWHTvZOAIIsMLAw7DCk +MKww8zDeACAAMgAuADIAIDDXMO0w1TChMKQw6wOTA7UDvQO5A7oDzAAgA5MDugPBA7kAIAOTA6wDvAO8 +A7EAIAAyAC4AMgBQAGUAcgBmAGkAbAAgAGcAZQBuAOkAcgBpAGMAbwAgAGQAZQAgAGMAaQBuAHoAZQBu +AHQAbwBzACAAZABhACAARwBhAG0AbQBhACAAMgAsADIAQQBsAGcAZQBtAGUAZQBuACAAZwByAGkAagBz +ACAAZwBhAG0AbQBhACAAMgAsADIALQBwAHIAbwBmAGkAZQBsAFAAZQByAGYAaQBsACAAZwBlAG4A6QBy +AGkAYwBvACAAZABlACAAZwBhAG0AbQBhACAAZABlACAAZwByAGkAcwBlAHMAIAAyACwAMg4jDjEOBw4q +DjUOQQ4BDiEOIQ4yDkAOAQ4jDiIOTA4XDjEOSA4nDkQOGwAgADIALgAyAEcAZQBuAGUAbAAgAEcAcgBp +ACAARwBhAG0AYQAgADIALAAyAFkAbABlAGkAbgBlAG4AIABoAGEAcgBtAGEAYQBuACAAZwBhAG0AbQBh +ACAAMgAsADIAIAAtAHAAcgBvAGYAaQBpAGwAaQBHAGUAbgBlAHIAaQENAGsAaQAgAEcAcgBhAHkAIABH +AGEAbQBtAGEAIAAyAC4AMgAgAHAAcgBvAGYAaQBsAFUAbgBpAHcAZQByAHMAYQBsAG4AeQAgAHAAcgBv +AGYAaQBsACAAcwB6AGEAcgBvAVsAYwBpACAAZwBhAG0AbQBhACAAMgAsADIGOgYnBkUGJwAgADIALgAy +ACAGRAZIBkYAIAYxBkUGJwYvBkoAIAY5BicGRQQeBDEESQQwBE8AIARBBDUEQAQwBE8AIAQzBDAEPAQ8 +BDAAIAAyACwAMgAtBD8EQAQ+BEQEOAQ7BEwARwBlAG4AZQByAGkAYwAgAEcAcgBhAHkAIABHAGEAbQBt +AGEAIAAyAC4AMgAgAFAAcgBvAGYAaQBsAGUAAHRleHQAAAAAQ29weXJpZ2h0IEFwcGxlIEluYy4sIDIw +MTIAAFhZWiAAAAAAAADzUQABAAAAARbMY3VydgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AMgA3 +ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYAiwCQAJUAmgCfAKQAqQCuALIAtwC8AMEAxgDL +ANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUBKwEyATgBPgFFAUwBUgFZAWABZwFuAXUBfAGD +AYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMCDAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKE +Ao4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMhAy0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPT +A+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4EjASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3 +BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0 +B4YHmQesB78H0gflB/gICwgfCDIIRghaCG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnP +CeUJ+woRCicKPQpUCmoKgQqYCq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyO +DKcMwAzZDPMNDQ0mDUANWg10DY4NqQ3DDd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+z +D88P7BAJECYQQxBhEH4QmxC5ENcQ9RETETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxND +E2MTgxOkE8UT5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdB +F2UXiReuF9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2Mbihuy +G9ocAhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCY +IMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZclxyX3 +JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2K2krnSvR +LAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi+RL8cv/jA1MGwwpDDbMRIxSjGCMbox8jIq +MmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1/TY3NnI2rjbpNyQ3YDecN9c4FDhQOIw4yDkF +OUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9Ij1hPaE94D4gPmA+oD7gPyE/YT+iP+JAI0Bk +QKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUSRVVFmkXeRiJGZ0arRvBHNUd7R8BIBUhL +SJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpNAk1KTZNN3E4lTm5Ot08AT0lPk0/dUCdQcVC7 +UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVVwlYPVlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4 +WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1fD19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GND +Y5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1g +bbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfByS3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gR +eG54zHkqeYl553pGeqV7BHtje8J8IXyBfOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INX +g7qEHYSAhOOFR4Wrhg6GcobXhzuHn4gEiGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82 +j56QBpBukNaRP5GokhGSepLjk02TtpQglIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuv +nByciZz3nWSd0p5Anq6fHZ+Ln/qgaaDYoUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjE +qTepqaocqo+rAqt1q+msXKzQrUStuK4trqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5 +tvC3aLfguFm40blKucK6O7q1uy67p7whvJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTO +xUvFyMZGxsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG +1EnUy9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj +4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozzGfOn +9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9Kf26/kv+3P9t///SLS5fYFxOU0NvbG9yU3Bh +Y2WiYTJcTlNDb2xvclNwYWNl0i0uY2RXTlNDb2xvcqJjMtItLmZnV05TSW1hZ2WiZjIACAARABoAJAAp +ADIANwBJAEwAUQBTAG0AcwCAAIcAlgCdAKoAsQC5ALsAvQC/AMQAxgDIAM8A1ADfAOMA5QDnAOkA6wDw +APMA9QD3APkBAAEXATMBNQE3DmUOag51Dn4OkQ6VDqAOqQ6uDrYOuQ6+DsEOww7FDscOzg7QDtIcNBw5 +HDwcPhxAHEIcSRxLHE0pwynIKdcp2ynmKe4p+yoIKh0qIiomKigqKiosKjUqOipAKkgqSipMKk4qUDvw +O/U8AjwFPBI8FzwfPCI8JzwvAAAAAAAAAgEAAAAAAAAAaAAAAAAAAAAAAAAAAAAAPDI + + + + + + + diff --git a/Podman/Controllers/PMInstallProgressViewController.h b/Podman/Controllers/PMInstallProgressViewController.h new file mode 100644 index 0000000..3d1c780 --- /dev/null +++ b/Podman/Controllers/PMInstallProgressViewController.h @@ -0,0 +1,16 @@ +// +// PMInstallProgressViewController.h +// Podman macOS +// +// Created by Victor Gama on 02/09/2021. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PMInstallProgressViewController : NSViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/Podman/Controllers/PMInstallProgressViewController.m b/Podman/Controllers/PMInstallProgressViewController.m new file mode 100644 index 0000000..081ed91 --- /dev/null +++ b/Podman/Controllers/PMInstallProgressViewController.m @@ -0,0 +1,72 @@ +// +// PMInstallProgressViewController.m +// Podman macOS +// +// Created by Victor Gama on 02/09/2021. +// + +#import "PMInstallProgressViewController.h" +#import "PMManager.h" +#import "AppDelegate.h" + +@interface PMInstallProgressViewController () +@property (weak) IBOutlet NSTextField *stateLabel; +@property (weak) IBOutlet NSProgressIndicator *progressBar; +@property (weak) IBOutlet NSButton *cancelButton; + +@end + +@implementation PMInstallProgressViewController { + NSTask *installTask; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do view setup here. +} + +- (void)viewDidAppear { + self.progressBar.maxValue = 100; + self.progressBar.doubleValue = 0; + installTask = [[PMManager manager] installVirtualMachineWithProgress:^(PMManagerInstallState * _Nonnull state) { + dispatch_sync(dispatch_get_main_queue(), ^{ + if (state.status == PMManagerInstallStatusDownloadingVM) { + self.stateLabel.stringValue = @"Downloading VM Image..."; + self.progressBar.maxValue = state.total; + self.progressBar.doubleValue = state.completed; + } else { + self.stateLabel.stringValue = @"Extracting image..."; + if (!self.progressBar.indeterminate) { + self.progressBar.indeterminate = YES; + self.progressBar.usesThreadedAnimation = YES; + [self.progressBar startAnimation:self]; + } + } + }); + } andCompletion:^(NSError * _Nullable error) { + dispatch_sync(dispatch_get_main_queue(), ^{ + if (error != nil) { + NSAlert *alert = [[NSAlert alloc] init]; + alert.alertStyle = NSAlertStyleCritical; + alert.messageText = @"An error occurred during the installation process"; + alert.informativeText = @"Extra details may be available in the system's log."; + [alert addButtonWithTitle:@"OK"]; + [alert beginSheetModalForWindow:self.view.window + completionHandler:^(NSModalResponse returnCode) { + [NSApplication.sharedApplication terminate:nil]; + }]; + } else { + [[PMManager manager] startVM]; + AppDelegate *delegate = [NSApp delegate]; + [delegate startAgent]; + [self.view.window close]; + } + }); + }]; +} + +- (IBAction)cancelButtonDidClick:(id)sender { + +} + +@end diff --git a/Podman/Controllers/PMInstallWindowController.h b/Podman/Controllers/PMInstallWindowController.h new file mode 100644 index 0000000..485bee7 --- /dev/null +++ b/Podman/Controllers/PMInstallWindowController.h @@ -0,0 +1,16 @@ +// +// PMInstallWindowController.h +// Podman macOS +// +// Created by Victor Gama on 02/09/2021. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PMInstallWindowController : NSViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/Podman/Controllers/PMInstallWindowController.m b/Podman/Controllers/PMInstallWindowController.m new file mode 100644 index 0000000..95d28cf --- /dev/null +++ b/Podman/Controllers/PMInstallWindowController.m @@ -0,0 +1,62 @@ +// +// PMInstallWindowController.m +// Podman macOS +// +// Created by Victor Gama on 02/09/2021. +// + +#import "PMInstallWindowController.h" +#import "PMManager.h" + +@interface PMInstallWindowController () +@property (weak) IBOutlet NSButton *disclosureButton; +@property (weak) IBOutlet NSTextField *moreInfoLabel; +@property (strong) IBOutlet NSLayoutConstraint *moreInfoHideConstraint; + +@end + +@implementation PMInstallWindowController { + CGSize initialSize; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + self.moreInfoHideConstraint.active = YES; +} + +- (IBAction)moreInfoDidClick:(id)sender { + self.disclosureButton.state = self.disclosureButton.state == 1 ? 0 : 1; + [self disclosureDidClick:sender]; +} + +- (IBAction)disclosureDidClick:(id)sender { + + + [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) { + context.duration = 0.2; + context.allowsImplicitAnimation = YES; + + self.moreInfoHideConstraint.animator.active = self.disclosureButton.state == 0; + [self.view layoutSubtreeIfNeeded]; + + NSWindow *window = self.view.window; + NSRect rect = [window contentRectForFrameRect:window.frame]; + rect.size = self.view.fittingSize; + NSRect frame = [window frameRectForContentRect:rect]; + frame.origin.y = window.frame.origin.y + (window.frame.size.height - frame.size.height); + [self.view.window.animator setFrame:frame display:YES animate:YES]; + }]; +} + +- (IBAction)exitDidClick:(id)sender { + [NSApplication.sharedApplication terminate:sender]; +} + +- (IBAction)installDidClick:(id)sender { + NSWindow *window = self.view.window; + NSViewController *newController = [[NSStoryboard storyboardWithName:@"Main" bundle:nil] instantiateControllerWithIdentifier:@"PMInstallProgressViewController"]; + window.contentViewController = newController; +} + + +@end diff --git a/Podman/Controllers/PMPreferencesController.h b/Podman/Controllers/PMPreferencesController.h new file mode 100644 index 0000000..1f6cfea --- /dev/null +++ b/Podman/Controllers/PMPreferencesController.h @@ -0,0 +1,16 @@ +// +// PMPreferencesController.h +// Podman macOS +// +// Created by Victor Gama on 03/09/2021. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PMPreferencesController : NSViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/Podman/Controllers/PMPreferencesController.m b/Podman/Controllers/PMPreferencesController.m new file mode 100644 index 0000000..23720dd --- /dev/null +++ b/Podman/Controllers/PMPreferencesController.m @@ -0,0 +1,39 @@ +// +// PMPreferencesController.m +// Podman macOS +// +// Created by Victor Gama on 03/09/2021. +// + +#import "PMPreferencesController.h" +#import "PMPreferences.h" + +@interface PMPreferencesController () +@property (weak) IBOutlet NSButton *startAtLoginCheckbox; +@property (weak) IBOutlet NSButton *startVMCheckbox; +@property (weak) IBOutlet NSButton *checkForUpdatesCheckbox; + +@end + +@implementation PMPreferencesController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.startAtLoginCheckbox.state = PMPreferences.startAtLogin ? NSControlStateValueOn : NSControlStateValueOff; + self.startVMCheckbox.state = PMPreferences.startPodmanVM ? NSControlStateValueOn : NSControlStateValueOff; + self.checkForUpdatesCheckbox.state = PMPreferences.checkForUpdates ? NSControlStateValueOn : NSControlStateValueOff; +} + +- (IBAction)startAtLoginDidChange:(id)sender { + PMPreferences.startAtLogin = self.startAtLoginCheckbox.state == NSControlStateValueOn; +} + +- (IBAction)startVMDidChange:(id)sender { + PMPreferences.startPodmanVM = self.startVMCheckbox.state == NSControlStateValueOn; +} + +- (IBAction)checkForUpdatesDidChange:(id)sender { + PMPreferences.checkForUpdates = self.checkForUpdatesCheckbox.state == NSControlStateValueOn; +} + +@end diff --git a/Podman/Controllers/PMStatusBarController.h b/Podman/Controllers/PMStatusBarController.h new file mode 100644 index 0000000..cd71d25 --- /dev/null +++ b/Podman/Controllers/PMStatusBarController.h @@ -0,0 +1,19 @@ +// +// PMStatusBarController.h +// Podman macOS +// +// Created by Victor Gama on 02/09/2021. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PMStatusBarController : NSObject + +- (instancetype)initWithPopover:(NSPopover *)popover; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Podman/Controllers/PMStatusBarController.m b/Podman/Controllers/PMStatusBarController.m new file mode 100644 index 0000000..2a9cd0b --- /dev/null +++ b/Podman/Controllers/PMStatusBarController.m @@ -0,0 +1,50 @@ +// +// PMStatusBarController.m +// Podman macOS +// +// Created by Victor Gama on 02/09/2021. +// + +#import "PMStatusBarController.h" + +@implementation PMStatusBarController { + NSStatusBar *statusBar; + NSStatusItem *statusItem; + NSPopover *popover; +} + +- (instancetype)initWithPopover:(NSPopover *)popover { + if ((self = [super init]) == nil) { + return nil; + } + + self->popover = popover; + self->statusBar = [[NSStatusBar alloc] init]; + self->statusItem = [statusBar statusItemWithLength:28]; + self->statusItem.button.image = [NSImage imageNamed:@"icon"]; + self->statusItem.button.image.size = CGSizeMake(17.27, 16); + self->statusItem.button.image.template = YES; + self->statusItem.button.action = @selector(togglePopover:); + self->statusItem.button.target = self; + + return self; +} + +- (void)togglePopover:(id)sender { + if (self->popover.isShown) { + [self hidePopover:sender]; + } else { + [self showPopover:sender]; + } +} + +- (void)showPopover:(id)sender { + [NSApp activateIgnoringOtherApps:YES]; + [self->popover showRelativeToRect:self->statusItem.button.bounds ofView:self->statusItem.button preferredEdge:NSRectEdgeMaxY]; +} + +- (void)hidePopover:(id)sender { + [self->popover performClose:sender]; +} + +@end diff --git a/Podman/Controllers/PopoverController.h b/Podman/Controllers/PopoverController.h new file mode 100644 index 0000000..ae7b17a --- /dev/null +++ b/Podman/Controllers/PopoverController.h @@ -0,0 +1,13 @@ +// +// ViewController.h +// Podman macOS +// +// Created by Victor Gama on 02/09/2021. +// + +#import + +@interface PopoverController : NSViewController + +@end + diff --git a/Podman/Controllers/PopoverController.m b/Podman/Controllers/PopoverController.m new file mode 100644 index 0000000..2d15cfe --- /dev/null +++ b/Podman/Controllers/PopoverController.m @@ -0,0 +1,341 @@ +// +// ViewController.m +// Podman macOS +// +// Created by Victor Gama on 02/09/2021. +// + +#import "PopoverController.h" +#import "PMManager.h" +#import "PMContainerCellView.h" +#import + +@interface PopoverController () + +@property (weak) IBOutlet NSButton *optionsButton; +@property (weak) IBOutlet NSButton *vmStateChangeButton; +@property (weak) IBOutlet NSTextField *statusLabel; +@property (weak) IBOutlet NSImageView *statusIndicatorImage; +@property (strong) IBOutlet NSMenu *optionsMenu; +@property (weak) IBOutlet NSTableView *containersTableView; +@property (strong) IBOutlet NSMenu *contextMenu; + +@property (weak) IBOutlet NSMenuItem *startContainerMenuItem; +@property (weak) IBOutlet NSMenuItem *restartContainerMenuItem; +@property (weak) IBOutlet NSMenuItem *killContainerMenuItem; +@property (weak) IBOutlet NSMenuItem *deleteContainerMenuItem; + +@end + +typedef NS_ENUM(NSUInteger, ServiceState) { + ServiceStateAny, + ServiceStateStopped, + ServiceStateStopping, + ServiceStateStarting, + ServiceStateRunning, +}; + +@implementation PopoverController { + NSTimer *updateTimer; + PMManager *manager; + ServiceState serviceState; + ServiceState expectedNextState; + NSArray *containers; +} + +- (void)setPodmanState:(ServiceState)newState { + if (self->expectedNextState != ServiceStateAny) { + if (newState != self->expectedNextState) { + return; + } + self->expectedNextState = ServiceStateAny; + } + + switch (newState) { + case ServiceStateAny: + NSLog(@"BUG: Attempt to setPodmanState to ServiceStateAny"); + abort(); + return; + case ServiceStateStopping: + self.statusLabel.stringValue = @"Podman is Stopping"; + self.statusIndicatorImage.image = [NSImage imageNamed:NSImageNameStatusPartiallyAvailable]; + self.vmStateChangeButton.title = @"Stop Podman"; + self.vmStateChangeButton.enabled = NO; + break; + case ServiceStateRunning: + self.statusLabel.stringValue = @"Podman is Running"; + self.statusIndicatorImage.image = [NSImage imageNamed:NSImageNameStatusAvailable]; + self.vmStateChangeButton.title = @"Stop Podman"; + self.vmStateChangeButton.enabled = YES; + self.vmStateChangeButton.tag = ServiceStateStopped; + break; + case ServiceStateStopped: + self.vmStateChangeButton.enabled = YES; + self.vmStateChangeButton.tag = ServiceStateRunning; + self.vmStateChangeButton.title = @"Start Podman"; + self.statusLabel.stringValue = @"Podman is Stopped"; + self.statusIndicatorImage.image = [NSImage imageNamed:NSImageNameStatusUnavailable]; + break; + case ServiceStateStarting: + self.vmStateChangeButton.enabled = NO; + self.vmStateChangeButton.title = @"Stop Podman"; + self.statusLabel.stringValue = @"Podman is Starting"; + self.statusIndicatorImage.image = [NSImage imageNamed:NSImageNameStatusPartiallyAvailable]; + break; + } + self->serviceState = newState; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + self.containersTableView.delegate = self; + self.containersTableView.dataSource = self; + self->expectedNextState = ServiceStateAny; + self->manager = [PMManager manager]; + [self executePeriodicTasks:nil]; + [self.contextMenu setDelegate:self]; +} + +- (void)scheduleNextPeriodicTasks { + updateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 + target:self + selector:@selector(executePeriodicTasks:) + userInfo:nil + repeats:NO]; +} + +- (void)executePeriodicTasks:(NSTimer *)timer { + if (timer != nil) { + [timer invalidate]; + updateTimer = nil; + } + + switch (self->manager.serviceStatus) { + case PMServiceStatusRunning: { + [self setPodmanState:ServiceStateRunning]; + [self->manager listContainersWithCallback:^(PMOperationResult *> * result) { + if (result.succeeded) { + self->containers = result.result; + NSInteger row = [self.containersTableView selectedRow]; + [self.containersTableView reloadData]; + [self.containersTableView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO]; + } + [self scheduleNextPeriodicTasks]; + }]; + return; + } + + case PMServiceStatusStarting: { + [self setPodmanState:ServiceStateStarting]; + break; + } + + case PMServiceStatusStopped: { + [self setPodmanState:ServiceStateStopped]; + break; + } + } + + [self scheduleNextPeriodicTasks]; +} + +- (IBAction)optionsButtonDidClick:(id)sender { + [self.optionsMenu popUpMenuPositioningItem:[self.optionsMenu itemAtIndex:0] atLocation:NSEvent.mouseLocation inView:nil]; +} + +- (IBAction)vmStateChangeButtonDidClick:(id)sender { + if (self->updateTimer != nil) { + [self->updateTimer invalidate]; + self->updateTimer = nil; + } + + if (self.vmStateChangeButton.tag == ServiceStateRunning) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + [self->manager startVM]; + }); + [self setPodmanState:ServiceStateStarting]; + self->expectedNextState = ServiceStateRunning; + } else { + NSAlert *alert = [[NSAlert alloc] init]; + alert.alertStyle = NSAlertStyleWarning; + alert.messageText = @"Are you sure you want to stop Podman?"; + alert.informativeText = @"Ensure you stopped your containers before continuing. Otherwise, data loss may occur."; + NSButton *yesButton = [alert addButtonWithTitle:@"Yes"]; + yesButton.keyEquivalent = @"\r"; + NSButton *noButton = [alert addButtonWithTitle:@"No"]; + noButton.keyEquivalent = @"\033"; + if([alert runModal] == NSAlertFirstButtonReturn) { + [self setPodmanState:ServiceStateStopping]; + self->expectedNextState = ServiceStateStopped; + [self->manager stopVM]; + } + } + [self scheduleNextPeriodicTasks]; +} + +- (IBAction)aboutPodmanMenuDidClick:(id)sender { + NSWindowController *controller = [[NSStoryboard storyboardWithName:@"Main" bundle:nil] instantiateControllerWithIdentifier:@"PMAboutWindow"]; + [controller showWindow:sender]; +} + +- (IBAction)quitPodmanMenuDidClick:(id)sender { + if (self->serviceState == ServiceStateRunning) { + NSAlert *alert = [[NSAlert alloc] init]; + alert.alertStyle = NSAlertStyleInformational; + alert.messageText = @"Do you wish to keep Podman VM running?"; + alert.informativeText = @"Podman's VM seems to be running. Choosing Yes will keep the Podman Virtual Machine running, and will close this application. Otherwise, the VM will be stopped."; + NSButton *yesButton = [alert addButtonWithTitle:@"Yes"]; + yesButton.keyEquivalent = @"\r"; + NSButton *noButton = [alert addButtonWithTitle:@"No"]; + noButton.keyEquivalent = @"\033"; + if([alert runModal] == NSAlertSecondButtonReturn) { + [self->manager stopVM]; + } + } + + [NSApp terminate:self]; +} + + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { + return self->containers.count; +} + +- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { + PMContainer *container = self->containers[row]; + PMContainerCellView *view = [tableView makeViewWithIdentifier:@"ContainerCellView" owner:nil]; + view.containerID = container.containerID; + view.containerStatus = container.containerStatus; + view.containerName = container.containerName; + view.containerImage = container.containerImage; + view.showsSeparator = row < self->containers.count - 1; + return view; +} + +- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row { + return 52; +} + +- (void)menuWillOpen:(NSMenu *)menu { + if (menu != self.contextMenu) { + return; + } + + if (self.containersTableView.selectedRow == -1) { + self.startContainerMenuItem.enabled = NO; + self.restartContainerMenuItem.enabled = NO; + self.killContainerMenuItem.enabled = NO; + self.deleteContainerMenuItem.enabled = NO; + return; + } + + PMContainer *container = [self->containers objectAtIndex:self.containersTableView.selectedRow]; + + self.startContainerMenuItem.enabled = YES; + self.deleteContainerMenuItem.enabled = YES; + if (container.isRunning) { + self.startContainerMenuItem.title = @"Stop"; + self.restartContainerMenuItem.enabled = YES; + self.killContainerMenuItem.enabled = YES; + } else { + self.startContainerMenuItem.title = @"Start"; + self.restartContainerMenuItem.enabled = NO; + self.killContainerMenuItem.enabled = NO; + } +} + + +- (IBAction)startContainerMenuDidClick:(id)sender { + if (self.containersTableView.selectedRow == -1) { + return; + } + + PMContainer *container = [self->containers objectAtIndex:self.containersTableView.selectedRow]; + __block NSString *operation; + PMOperationCallback callback = ^(PMOperationResult *result) { + if (!result.succeeded) { + NSAlert *alert = [[NSAlert alloc] init]; + alert.alertStyle = NSAlertStyleWarning; + alert.messageText = [NSString stringWithFormat:@"Could not %@ container %@", operation, container.containerName]; + alert.informativeText = result.output; + NSButton *yesButton = [alert addButtonWithTitle:@"OK"]; + yesButton.keyEquivalent = @"\r"; + [alert runModal]; + } + }; + + if (container.isRunning) { + container.containerStatus = @"Stopping..."; + operation = @"stop"; + [PMManager.manager stopContainer:container withCallback:callback]; + } else { + container.containerStatus = @"Starting..."; + operation = @"start"; + [PMManager.manager startContainer:container withCallback:callback]; + } +} + +- (IBAction)restartContainerMenuDidClick:(id)sender { + if (self.containersTableView.selectedRow == -1) { + return; + } + + PMContainer *container = [self->containers objectAtIndex:self.containersTableView.selectedRow]; + [PMManager.manager restartContainer:container withCallback:^(PMOperationResult * _Nonnull result) { + if (!result.succeeded) { + NSAlert *alert = [[NSAlert alloc] init]; + alert.alertStyle = NSAlertStyleWarning; + alert.messageText = [NSString stringWithFormat:@"Could not restart %@", container.containerName]; + alert.informativeText = result.output; + NSButton *yesButton = [alert addButtonWithTitle:@"OK"]; + yesButton.keyEquivalent = @"\r"; + [alert runModal]; + } + }]; +} + +- (IBAction)killContainerMenuDidClick:(id)sender { + if (self.containersTableView.selectedRow == -1) { + return; + } + + PMContainer *container = [self->containers objectAtIndex:self.containersTableView.selectedRow]; + [PMManager.manager killContainer:container withCallback:^(PMOperationResult * _Nonnull result) { + if (!result.succeeded) { + NSAlert *alert = [[NSAlert alloc] init]; + alert.alertStyle = NSAlertStyleWarning; + alert.messageText = [NSString stringWithFormat:@"Could not kill %@", container.containerName]; + alert.informativeText = result.output; + NSButton *yesButton = [alert addButtonWithTitle:@"OK"]; + yesButton.keyEquivalent = @"\r"; + [alert runModal]; + } + }]; +} + +- (IBAction)deleteContainerMenuDidClick:(id)sender { + if (self.containersTableView.selectedRow == -1) { + return; + } + + PMContainer *container = [self->containers objectAtIndex:self.containersTableView.selectedRow]; + [PMManager.manager deleteContainer:container withCallback:^(PMOperationResult * _Nonnull result) { + if (!result.succeeded) { + NSAlert *alert = [[NSAlert alloc] init]; + alert.alertStyle = NSAlertStyleWarning; + alert.messageText = [NSString stringWithFormat:@"Could not delete %@", container.containerName]; + alert.informativeText = result.output; + NSButton *yesButton = [alert addButtonWithTitle:@"OK"]; + yesButton.keyEquivalent = @"\r"; + [alert runModal]; + } + }]; +} + +- (IBAction)preferencesMenuItemDidClick:(id)sender { + NSStoryboard *story = [NSStoryboard storyboardWithName:@"Main" bundle:nil]; + NSWindowController *controller = [story instantiateControllerWithIdentifier:@"PMPreferencesWindow"]; + [controller showWindow:sender]; +} + +@end diff --git a/Podman/Controls/PMContainerCellView.h b/Podman/Controls/PMContainerCellView.h new file mode 100644 index 0000000..36f9541 --- /dev/null +++ b/Podman/Controls/PMContainerCellView.h @@ -0,0 +1,22 @@ +// +// PMContainerCellView.h +// Podman macOS +// +// Created by Victor Gama on 03/09/2021. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PMContainerCellView : NSTableCellView + +@property (nonatomic) NSString *containerName; +@property (nonatomic) NSString *containerImage; +@property (nonatomic) NSString *containerStatus; +@property (nonatomic, strong) NSString *containerID; +@property (nonatomic) BOOL showsSeparator; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Podman/Controls/PMContainerCellView.m b/Podman/Controls/PMContainerCellView.m new file mode 100644 index 0000000..b5bdba5 --- /dev/null +++ b/Podman/Controls/PMContainerCellView.m @@ -0,0 +1,53 @@ +// +// PMContainerCellView.m +// Podman macOS +// +// Created by Victor Gama on 03/09/2021. +// + +#import "PMContainerCellView.h" + +@interface PMContainerCellView () + +@property (weak) IBOutlet NSTextField *containerNameLabel; +@property (weak) IBOutlet NSTextField *containerImageLabel; +@property (weak) IBOutlet NSTextField *containerStatusLabel; +@property (weak) IBOutlet NSBox *bottomSeparator; + +@end + +@implementation PMContainerCellView + +- (void)setContainerName:(NSString *)containerName { + self.containerNameLabel.stringValue = containerName; +} + +- (NSString *)containerName { + return self.containerNameLabel.stringValue; +} + +- (void)setContainerImage:(NSString *)containerImage { + self.containerImageLabel.stringValue = containerImage; +} + +- (NSString *)containerImage { + return self.containerImageLabel.stringValue; +} + +- (void)setContainerStatus:(NSString *)containerStatus { + self.containerStatusLabel.stringValue = containerStatus; +} + +- (NSString *)containerStatus { + return self.containerStatusLabel.stringValue; +} + +- (void)setShowsSeparator:(BOOL)showsSeparator { + self.bottomSeparator.hidden = !showsSeparator; +} + +- (BOOL)showsSeparator { + return !self.bottomSeparator.hidden; +} + +@end diff --git a/Podman/Info.plist b/Podman/Info.plist new file mode 100644 index 0000000..4453391 --- /dev/null +++ b/Podman/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSApplicationCategoryType + public.app-category.developer-tools + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + LSUIElement + + NSPrincipalClass + PMApplication + + diff --git a/Podman/Interop/PMCommon.h b/Podman/Interop/PMCommon.h new file mode 100644 index 0000000..c1464b4 --- /dev/null +++ b/Podman/Interop/PMCommon.h @@ -0,0 +1,34 @@ +// +// PMCommon.h +// Podman macOS +// +// Created by Victor Gama on 03/09/2021. +// + +#ifndef PMCommon_h +#define PMCommon_h + +typedef enum : NSUInteger { + PMManagerInstallStatusDownloadingVM, + PMManagerInstallStatusExtracting, +} PMManagerInstallStatus; + +typedef enum : NSUInteger { + PMServiceStatusRunning, + PMServiceStatusStarting, + PMServiceStatusStopped, +} PMServiceStatus; + +typedef enum : NSUInteger { + PMDetectStateNotInPath, + PMDetectStateError, + PMDetectStateOK, +} PMDetectState; + +typedef enum : NSUInteger { + PMVMPresencePresent, + PMVMPresenceAbsent, + PMVMPresenceError, +} PMVMPresence; + +#endif /* PMCommon_h */ diff --git a/Podman/Interop/PMContainer.h b/Podman/Interop/PMContainer.h new file mode 100644 index 0000000..0ef9cad --- /dev/null +++ b/Podman/Interop/PMContainer.h @@ -0,0 +1,34 @@ +// +// PMContainer.h +// Podman macOS +// +// Created by Victor Gama on 03/09/2021. +// + +#import +#import "PMOperationResult.h" + +NS_ASSUME_NONNULL_BEGIN + +/// PMContainer represents a single container in the Podman +@interface PMContainer : NSObject + + +/// Represents the container ID +@property (nonatomic) NSString *containerID; + +/// Represents the container name +@property (nonatomic) NSString *containerName; + +/// Represents the image this container is running +@property (nonatomic) NSString *containerImage; + +/// Represents the container status +@property (nonatomic) NSString *containerStatus; + +/// Returns whether the container is running inferring the value of `containerStatus` +@property (readonly, nonatomic) BOOL isRunning; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Podman/Interop/PMContainer.m b/Podman/Interop/PMContainer.m new file mode 100644 index 0000000..119606e --- /dev/null +++ b/Podman/Interop/PMContainer.m @@ -0,0 +1,17 @@ +// +// PMContainer.m +// Podman macOS +// +// Created by Victor Gama on 03/09/2021. +// + +#import "PMContainer.h" +#import "PMManager.h" + +@implementation PMContainer + +- (BOOL)isRunning { + return [self.containerStatus containsString:@"Up"]; +} + +@end diff --git a/Podman/Interop/PMLoginItem.h b/Podman/Interop/PMLoginItem.h new file mode 100644 index 0000000..61efce2 --- /dev/null +++ b/Podman/Interop/PMLoginItem.h @@ -0,0 +1,31 @@ +// +// PMLoginItem.h +// Podman macOS +// +// Created by Victor Gama on 03/09/2021. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PMLoginItem : NSObject + + +/// Determines whether a Login Item exists with the provided URL +/// @param url URL to check for ++ (BOOL)loginItemExistsWithURL:(NSURL *)url; + + +/// Adds a given URL to the Login Items list in case it still does not exist. +/// @param url URL to the Application to be added to Login Items ++ (void)addLoginItemWithURL:(NSURL *)url; + + +/// Removes a given URL from the Login Items list in case it exists +/// @param url URL to the Application to be removed from Login Items ++ (void)removeLoginItemWithURL:(NSURL *)url; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Podman/Interop/PMLoginItem.m b/Podman/Interop/PMLoginItem.m new file mode 100644 index 0000000..1cb4eab --- /dev/null +++ b/Podman/Interop/PMLoginItem.m @@ -0,0 +1,74 @@ +// +// PMLoginItem.m +// Podman macOS +// +// Created by Victor Gama on 03/09/2021. +// + +#import "PMLoginItem.h" +#import + +@implementation PMLoginItem + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" ++ (BOOL)loginItemExistsWithURL:(NSURL *)url { + LSSharedFileListRef itemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL); + CFArrayRef items = LSSharedFileListCopySnapshot(itemsRef, nil); + + BOOL exists = NO; + for (id item in (__bridge NSArray *)items) { + CFURLRef loginItem = LSSharedFileListItemCopyResolvedURL((__bridge LSSharedFileListItemRef)item, 0, nil); + if (loginItem == nil) { + continue; + } + + if ([(__bridge NSURL *)loginItem isEqual:url]) { + exists = YES; + CFRelease(loginItem); + break; + } + CFRelease(loginItem); + } + + CFRelease(itemsRef); + CFRelease(items); + + return exists; +} + ++ (void)addLoginItemWithURL:(NSURL *)url { + if ([self loginItemExistsWithURL:url]) { + return; + } + LSSharedFileListRef itemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL); + LSSharedFileListItemRef newItem = LSSharedFileListInsertItemURL(itemsRef, kLSSharedFileListItemLast, nil, nil, (__bridge CFURLRef)url, nil, nil); + CFRelease(newItem); + CFRelease(itemsRef); +} + ++ (void)removeLoginItemWithURL:(NSURL *)url { + if (![self loginItemExistsWithURL:url]) { + return; + } + + LSSharedFileListRef itemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL); + CFArrayRef items = LSSharedFileListCopySnapshot(itemsRef, nil); + + for (id item in (__bridge NSArray *)items) { + CFURLRef loginItem = LSSharedFileListItemCopyResolvedURL((__bridge LSSharedFileListItemRef)item, 0, nil); + if ([(__bridge NSURL *)loginItem isEqual:url]) { + LSSharedFileListItemRemove(itemsRef, (__bridge LSSharedFileListItemRef)item); + CFRelease(loginItem); + break; + } + CFRelease(loginItem); + } + + CFRelease(items); + CFRelease(itemsRef); +} + +#pragma clang diagnostic pop + +@end diff --git a/Podman/Interop/PMManager.h b/Podman/Interop/PMManager.h new file mode 100644 index 0000000..a52cbbc --- /dev/null +++ b/Podman/Interop/PMManager.h @@ -0,0 +1,115 @@ +// +// PMManager.h +// Podman macOS +// +// Created by Victor Gama on 02/09/2021. +// + +#import +#import "PMCommon.h" +#import "PMContainer.h" +#import "PMOperationResult.h" + +NS_ASSUME_NONNULL_BEGIN + +/// PMManagerInstallState provides a snapshot of the installation progress in a determined moment. +/// This class is passed as a parameter of PMInstallCompletionHandler. +@interface PMManagerInstallState : NSObject + +/// Indicates the installation status on this snapshot +@property (readonly, nonatomic) PMManagerInstallStatus status; + +/// Represents a value that indicates the total progress to complete a task. +@property (readonly, nonatomic) NSUInteger total; + +/// Represents a value indicating the amount of progress completed in relation to `total`. +@property (readonly, nonatomic) NSUInteger completed; + + +/// Initialises a new PMManagerInstallState +/// @param status The status to contain in this state +/// @param total The total progress of the operation +/// @param completed The completed progress of the operation +- (instancetype)initWithStatus:(PMManagerInstallStatus)status total:(NSUInteger)total andCompleted:(NSUInteger)completed; + +@end + +typedef void (^_Nonnull PMInstallProgressHandler)(PMManagerInstallState * _Nonnull state); +typedef void (^_Nonnull PMInstallCompletionHandler)(NSError * _Nullable error); +typedef void (^_Nonnull PMOperationCallback)(PMOperationResult *result); + + +/// PMManager provides utilities to execute operations against the local Podman installation +@interface PMManager : NSObject + +/// Provides a singleton instance of this class. +@property (nonatomic, class, readonly) PMManager *manager; + + +/// Represents the current service status. +@property (nonatomic, readonly) PMServiceStatus serviceStatus; + + +/// Detects whether Podman is available in the current system. Use the method `detectStateValue` to obtain +/// the underlying value representing the result of this operation. +- (PMOperationResult *)detectPodman; + + +/// Detects whether a Podman machine is already provisioned on this system. Use `detectVMValue` to obtain +/// the underlying value representing the result of this operation. +- (PMOperationResult *)detectVM; + + +/// Provisions a default Podman machine on this system. +/// @param progress The handler to be called with information about the progress of the installation. +/// @param completion The handler to be called when the installation completes (with or without errors) +- (nullable NSTask *)installVirtualMachineWithProgress:(PMInstallProgressHandler)progress + andCompletion:(PMInstallCompletionHandler)completion; + + +/// Starts the default Podman machine provisioned on this system. +- (PMOperationResult *)startVM; + +/// Stops the default Podman machine provisioned on this system. +- (PMOperationResult *)stopVM; + + +/// Lists all containers on this system. +/// @param callback Callback to be called with the result of the operation. The callback is already +/// executed in the main queue. +- (void)listContainersWithCallback:(void (^_Nonnull) (PMOperationResult *> * _Nullable list))callback; + + +/// Starts a given PMContainer +/// @param container Container to be started +/// @param callback Callback to be called when the operation completes. The callback is invoked in the +/// main queue. +- (void)startContainer:(PMContainer *)container withCallback:(PMOperationCallback)callback; + +/// Stops a given PMContainer +/// @param container Container to be stopped +/// @param callback Callback to be called when the operation completes. The callback is invoked in the +/// main queue. +- (void)stopContainer:(PMContainer *)container withCallback:(PMOperationCallback)callback; + +/// Restarts a given PMContainer +/// @param container Container to be restarted +/// @param callback Callback to be called when the operation completes. The callback is invoked in the +/// main queue. +- (void)restartContainer:(PMContainer *)container withCallback:(PMOperationCallback)callback; + +/// Kills a given PMContainer +/// @param container Container to be killed +/// @param callback Callback to be called when the operation completes. The callback is invoked in the +/// main queue. +- (void)killContainer:(PMContainer *)container withCallback:(PMOperationCallback)callback; + +/// Deletes a given PMContainer +/// @param container Container to be deleted +/// @param callback Callback to be called when the operation completes. The callback is invoked in the +/// main queue. +- (void)deleteContainer:(PMContainer *)container withCallback:(PMOperationCallback)callback; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Podman/Interop/PMManager.m b/Podman/Interop/PMManager.m new file mode 100644 index 0000000..0035f93 --- /dev/null +++ b/Podman/Interop/PMManager.m @@ -0,0 +1,391 @@ +// +// PMManager.m +// Podman macOS +// +// Created by Victor Gama on 02/09/2021. +// + +#import +#include +#include +#include +#include +#include + +#import "PMManager.h" +#include "PMDispatch.h" + +@import Darwin.C; + +@implementation PMManagerInstallState + +- (instancetype)initWithStatus:(PMManagerInstallStatus)status total:(NSUInteger)total andCompleted:(NSUInteger)completed { + if ((self = [super init]) == nil) { + return nil; + } + + self->_status = status; + self->_total = total; + self->_completed = completed; + + return self; +} + +@end + +@implementation PMManager { + NSString *podmanPath; + NSRegularExpression *downloadExpr; + NSRegularExpression *extractExpr; + NSFileHandle *masterHandle, *slaveHandle; +} + ++ (PMManager *)manager { + static dispatch_once_t onceToken; + static PMManager *manager; + dispatch_once(&onceToken, ^{ + manager = [[self alloc] init]; + }); + + return manager; +} + +- (instancetype)init { + if ((self = [super init]) == nil) { + return nil; + } + + NSError *exprErr = nil; + downloadExpr = [NSRegularExpression + regularExpressionWithPattern:@"(?:\\^\\[\\[\\d*[A-Z])*([^:\\.]+): ?([^\\[]+)\\[([^\\]]+)\\] ?([a-zA-Z0-9\\. \\/]+)" + options:0 + error:&exprErr]; + if (exprErr != nil) { + NSLog(@"CRITICAL: installVirtualMachineWithProgress downloadExpr initialisation failed: %@", exprErr); + abort(); + } + + extractExpr = [NSRegularExpression + regularExpressionWithPattern:@"(?:\\\\^\\[\\[\\d*[A-Z])*([Ee]xtracting[\\n]*)" + options:0 + error:&exprErr + ]; + if (exprErr != nil) { + NSLog(@"CRITICAL: installVirtualMachineWithProgress downloadExpr initialisation failed: %@", exprErr); + abort(); + } + + return self; +} + +- (PMServiceStatus)serviceStatus { + if (![self vmRunning]) { + return PMServiceStatusStopped; + } + + if (![self serviceAccessible]) { + return PMServiceStatusStarting; + } + + return PMServiceStatusRunning; +} + +- (NSArray *)paths { + NSString *path = [NSProcessInfo.processInfo.environment objectForKey:@"PATH"]; + return [path componentsSeparatedByString:@":"]; +} + +- (NSString *)findInPath:(NSString *)appName { + for (NSString *base in [self paths]) { + NSString *target = [base stringByAppendingPathComponent:appName]; + if ([[NSFileManager defaultManager] isExecutableFileAtPath:target]) { + return target; + } + } + + // Test for homebrew path as a last resort... + NSString *base = @"/usr/local/bin"; + NSString *target = [base stringByAppendingPathComponent:appName]; + if ([[NSFileManager defaultManager] isExecutableFileAtPath:target]) { + return target; + } + + return nil; +} + +- (NSTask *)execCommand:(NSString *)command withPipe:(NSPipe *)pipe andArgs:(NSString *)procArgs, ... { + NSMutableArray *argsArr = [[NSMutableArray alloc] init]; + [argsArr addObject:procArgs]; + va_list args; + NSString *arg = nil; + va_start(args, procArgs); + while ((arg = va_arg(args, NSString *)) != nil) { + [argsArr addObject:[arg copy]]; + } + va_end(args); + + NSTask *task = [[NSTask alloc] init]; + task.launchPath = command; + task.arguments = argsArr; + if (pipe != nil) { + task.standardOutput = pipe; + } + + return task; +} + +- (PMOperationResult *)detectPodman { + NSString *podmanPath = [self findInPath:@"podman"]; + if (podmanPath == nil) { + NSLog(@"detectPodman: podman not in PATH"); + return [PMOperationResult resultWithSuccess:YES object:@(PMDetectStateNotInPath) andOutput:nil]; + } + NSPipe *pipe = [NSPipe pipe]; + NSTask *task = [self execCommand:podmanPath withPipe:pipe andArgs:@"--version", nil]; + [task launch]; + [task waitUntilExit]; + if (task.terminationStatus != 0) { + return [PMOperationResult resultWithSuccess:NO object:@(PMDetectStateError) andOutput:pipe.fileHandleForReading]; + } + self->podmanPath = podmanPath; + NSLog(@"detectPodman: Podman executable found at %@", podmanPath); + return [PMOperationResult resultWithSuccess:YES object:@(PMDetectStateOK) andOutput:pipe.fileHandleForReading]; +} + +- (PMOperationResult *)detectVM { + NSPipe *pipe = [NSPipe pipe]; + NSTask *task = [self execCommand:self->podmanPath withPipe:pipe andArgs:@"machine", @"list", @"--noheading", nil]; + [task launch]; + [task waitUntilExit]; + + if (task.terminationStatus != 0) { + return [PMOperationResult resultWithSuccess:NO object:@(PMVMPresenceError) andOutput:pipe.fileHandleForReading]; + } + + NSString *output = [[NSString alloc] initWithData:[pipe.fileHandleForReading readDataToEndOfFile] encoding:NSUTF8StringEncoding]; + if ([output stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length == 0) { + return [PMOperationResult resultWithSuccess:YES object:@(PMVMPresenceAbsent) andOutput:nil]; + } + + return [PMOperationResult resultWithSuccess:YES object:@(PMVMPresencePresent) andOutput:nil]; +} + +- (void)handleInstallerOutput:(NSString *)input forProgressHandler:(PMInstallProgressHandler)handler { + NSArray *res = [downloadExpr matchesInString:input options:0 range:NSMakeRange(0, input.length)]; + if (res.count > 0) { + NSTextCheckingResult *match = res[0]; +// NSString *status = [input substringWithRange:[match rangeAtIndex:1]]; // Downloading VM image +// NSString *imageName = [input substringWithRange:[match rangeAtIndex:2]]; // fedora-coreos-34.20210821.1.1-qemu.x86_64.qcow2.xz + NSString *progress = [input substringWithRange:[match rangeAtIndex:3]]; // =================>---------------------------------------- +// NSString *progressText = [input substringWithRange:[match rangeAtIndex:4]]; // 188.2MiB / 601.1MiB + + progress = [progress stringByReplacingOccurrencesOfString:@">" withString:@"="]; + NSUInteger total = 0, completed = 0; + + for (NSUInteger i = 0; i < progress.length; i++) { + total++; + if ([progress characterAtIndex:i] == '=') { + completed++; + } + } + + handler([[PMManagerInstallState alloc] initWithStatus:PMManagerInstallStatusDownloadingVM total:total andCompleted:completed]); + return; + } + + res = [extractExpr matchesInString:input options:0 range:NSMakeRange(0, input.length)]; + if (res.count > 0) { + handler([[PMManagerInstallState alloc] initWithStatus:PMManagerInstallStatusExtracting total:0 andCompleted:0]); + return; + } + + NSLog(@"%@", res); +} + +- (NSTask *)installVirtualMachineWithProgress:(void (^)(PMManagerInstallState * _Nonnull))progress andCompletion:(void (^)(NSError * _Nullable))completion { + // Fake a terminal so we can get download information while it runs. + // This is a really ugly hack, but I'm really not sure what to do instead. + + int fdMaster, fdSlave; + struct termios fakeTermios; + struct winsize sz; + sz.ws_col = 1024; + sz.ws_row = 1024; + sz.ws_xpixel = 60000; + sz.ws_ypixel = 60000; + memset(&fakeTermios, 0, sizeof(struct termios)); + + int rc = openpty(&fdMaster, &fdSlave, NULL, &fakeTermios, &sz); + if (rc != 0) { + NSLog(@"installVirtualMachineWithProgress: openpty failed with status %d", rc); + completion([NSError errorWithDomain:NSPOSIXErrorDomain code:rc userInfo:nil]); + return nil; + } + + fcntl(fdMaster, F_SETFD, FD_CLOEXEC); + fcntl(fdSlave, F_SETFD, FD_CLOEXEC); + + masterHandle = [[NSFileHandle alloc] initWithFileDescriptor:fdMaster closeOnDealloc:YES]; + slaveHandle = [[NSFileHandle alloc] initWithFileDescriptor:fdSlave closeOnDealloc:YES]; + + NSTask *task = [self execCommand:self->podmanPath withPipe:nil andArgs:@"machine", @"init", nil]; + task.standardInput = slaveHandle; + task.standardOutput = slaveHandle; + + __weak typeof(self) weakSelf = self; + masterHandle.readabilityHandler = ^(NSFileHandle *handle) { + NSString *str = [[NSString alloc] initWithData:handle.availableData encoding:NSUTF8StringEncoding]; + NSLog(@"installVirtualMachineWithProgress: [handle offset %lld]: %@", handle.offsetInFile, str); + [weakSelf handleInstallerOutput:str forProgressHandler:progress]; + }; + + NSLog(@"installVirtualMachineWithProgress: Installation launch"); + [task launch]; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + [task waitUntilExit]; + self->masterHandle = nil; + self->slaveHandle = nil; + if (task.terminationStatus != 0) { + completion([NSError errorWithDomain:@"PMMachineInitExitStatus" code:task.terminationStatus userInfo:nil]); + } else { + completion(nil); + } + }); + return task; +} + +- (bool)vmRunning { + NSPipe *pipe = [NSPipe pipe]; + NSTask *task = [self execCommand:self->podmanPath withPipe:pipe andArgs:@"machine", @"list", @"--noheading", @"--format", @"\"{{.LastUp}}\"", nil]; + [task launch]; + [task waitUntilExit]; + NSString *output = [[NSString alloc] initWithData:[pipe.fileHandleForReading readDataToEndOfFile] encoding:NSUTF8StringEncoding]; + return [output containsString:@"Currently running"]; +} + +- (bool)serviceAccessible { + NSPipe *pipe = [NSPipe pipe]; + NSTask *task = [self execCommand:self->podmanPath withPipe:pipe andArgs:@"ps", nil]; + [task launch]; + [task waitUntilExit]; + return task.terminationStatus == 0; +} + +- (PMOperationResult *)startVM { + NSPipe *pipe = [NSPipe pipe]; + NSTask *task = [self execCommand:self->podmanPath withPipe:pipe andArgs:@"machine", @"start", nil]; + [task launch]; + [task waitUntilExit]; + return [PMOperationResult resultWithTask:task andPipe:pipe]; +} + +- (PMOperationResult *)stopVM { + NSPipe *pipe = [NSPipe pipe]; + NSTask *task = [self execCommand:self->podmanPath withPipe:pipe andArgs:@"machine", @"stop", nil]; + [task launch]; + [task waitUntilExit]; + return [PMOperationResult resultWithTask:task andPipe:pipe]; +} + +- (void)listContainersWithCallback:(void (^)(PMOperationResult *> *))callback { + NSPipe *pipe = [NSPipe pipe]; + NSTask *task = [self execCommand:self->podmanPath + withPipe:pipe + andArgs:@"ps", @"-a", @"--format", @"{{.ID}}${{.Image}}${{.Names}}${{.State}}", nil]; + [PMDispatch background:^{ + [task launch]; + [task waitUntilExit]; + if (task.terminationStatus != 0) { + callback([PMOperationResult resultWithTask:task andPipe:pipe]); + return; + } + + NSString *output = [[NSString alloc] initWithData:[pipe.fileHandleForReading readDataToEndOfFile] encoding:NSUTF8StringEncoding]; + NSArray *lines = [output componentsSeparatedByString:@"\n"]; + NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:lines.count]; + for (NSString *line in lines) { + NSArray *components = [[line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] componentsSeparatedByString:@"$"]; + if (components.count != 4) { + continue; + } + PMContainer *container = [[PMContainer alloc] init]; + container.containerID = components[0]; + container.containerImage = components[1]; + container.containerName = components[2]; + NSString *status = [components[3] stringByReplacingOccurrencesOfString:@" ago" withString:@""]; + container.containerStatus = status; + [result addObject:container]; + } + [result sortUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { + PMContainer *ca = obj1, *cb = obj2; + return [ca.containerName compare:cb.containerName]; + }]; + + [PMDispatch sync:^{ + callback([PMOperationResult resultWithSuccess:YES object:result andOutput:nil]); + }]; + }]; +} + +- (void)startContainer:(PMContainer *)container withCallback:(PMOperationCallback)callback { + NSPipe *pipe = [NSPipe pipe]; + NSTask *task = [self execCommand:self->podmanPath withPipe:pipe andArgs:@"start", container.containerID, nil]; + [PMDispatch background:^{ + [task launch]; + [task waitUntilExit]; + [PMDispatch sync:^{ + callback([PMOperationResult resultWithTask:task andPipe:pipe]); + }]; + }]; +} + +- (void)stopContainer:(PMContainer *)container withCallback:(PMOperationCallback)callback { + NSPipe *pipe = [NSPipe pipe]; + NSTask *task = [self execCommand:self->podmanPath withPipe:pipe andArgs:@"stop", container.containerID, nil]; + [PMDispatch background:^{ + [task launch]; + [task waitUntilExit]; + [PMDispatch sync:^{ + callback([PMOperationResult resultWithTask:task andPipe:pipe]); + }]; + }]; +} + +- (void)restartContainer:(PMContainer *)container withCallback:(PMOperationCallback)callback { + NSPipe *pipe = [NSPipe pipe]; + NSTask *task = [self execCommand:self->podmanPath withPipe:pipe andArgs:@"restart", container.containerID, nil]; + [PMDispatch background:^{ + [task launch]; + [task waitUntilExit]; + [PMDispatch sync:^{ + callback([PMOperationResult resultWithTask:task andPipe:pipe]); + }]; + }]; +} + +- (void)killContainer:(PMContainer *)container withCallback:(PMOperationCallback)callback { + NSPipe *pipe = [NSPipe pipe]; + NSTask *task = [self execCommand:self->podmanPath withPipe:pipe andArgs:@"kill", container.containerID, nil]; + [PMDispatch background:^{ + [task launch]; + [task waitUntilExit]; + [PMDispatch sync:^{ + callback([PMOperationResult resultWithTask:task andPipe:pipe]); + }]; + }]; +} + +- (void)deleteContainer:(PMContainer *)container withCallback:(PMOperationCallback)callback { + NSPipe *pipe = [NSPipe pipe]; + NSTask *task = [self execCommand:self->podmanPath withPipe:pipe andArgs:@"rm", @"-f", container.containerID, nil]; + [PMDispatch background:^{ + [task launch]; + [task waitUntilExit]; + [PMDispatch sync:^{ + callback([PMOperationResult resultWithTask:task andPipe:pipe]); + }]; + }]; +} + +@end diff --git a/Podman/Interop/PMOperationResult.h b/Podman/Interop/PMOperationResult.h new file mode 100644 index 0000000..98f58f3 --- /dev/null +++ b/Podman/Interop/PMOperationResult.h @@ -0,0 +1,45 @@ +// +// PMOperationResult.h +// Podman macOS +// +// Created by Victor Gama on 03/09/2021. +// + +#import +#import "PMCommon.h" + +NS_ASSUME_NONNULL_BEGIN + +/// PMOperationResult represents the result of a background-running task, usually started by a PMManger +/// method. +@interface PMOperationResult <__covariant T> : NSObject + +/// Indicates whether the operation succeeded. This flag indicates whether the underlying process exited with +/// a zeroed status code. +@property (nonatomic, readonly) BOOL succeeded; + +/// Contains the text outputted to the standard output of the underlying process. +@property (nonatomic, readonly) NSString *output; + +/// Contains an arbitrary value set by the method that created this result. Consult the method's documentation +/// to obtain further information. +@property (nonatomic, readonly, strong, nullable) T result; + ++ (instancetype)resultWithSuccess:(BOOL)success andOutput:(nullable NSFileHandle *)output; ++ (instancetype)resultWithSuccess:(BOOL)success object:(nullable T)obj andOutput:(nullable NSFileHandle *)output; ++ (instancetype)resultWithTask:(NSTask *)task andPipe:(NSPipe *)pipe; + +- (instancetype)initWithSuccess:(BOOL)success andOutput:(nullable NSFileHandle *)output; +- (instancetype)initWithSuccess:(BOOL)success object:(nullable T)obj andOutput:(nullable NSFileHandle *)output; + +/// Returns the underlying value as a PMDetectState value +- (PMDetectState)detectStateValue; + +/// Returns the underlying value as a PMServiceStatus value +- (PMServiceStatus)serviceStateValue; + +/// Returns the underlying value as a PMVMPresence value +- (PMVMPresence)vmPresenceValue; +@end + +NS_ASSUME_NONNULL_END diff --git a/Podman/Interop/PMOperationResult.m b/Podman/Interop/PMOperationResult.m new file mode 100644 index 0000000..2989562 --- /dev/null +++ b/Podman/Interop/PMOperationResult.m @@ -0,0 +1,63 @@ +// +// PMOperationResult.m +// Podman macOS +// +// Created by Victor Gama on 03/09/2021. +// + +#import "PMOperationResult.h" + +@implementation PMOperationResult + ++ (instancetype)resultWithSuccess:(BOOL)success andOutput:(NSFileHandle *)output { + return [self resultWithSuccess:success object:nil andOutput:output]; +} + ++ (instancetype)resultWithSuccess:(BOOL)success object:(id)obj andOutput:(NSFileHandle *)output { + return [[self alloc] initWithSuccess:success object:obj andOutput:output]; +} + ++ (instancetype)resultWithTask:(NSTask *)task andPipe:(NSPipe *)pipe { + return [self resultWithSuccess:task.terminationStatus == 0 andOutput:pipe.fileHandleForReading]; +} + +- (instancetype)initWithSuccess:(BOOL)success object:(id)obj andOutput:(NSFileHandle *)output { + if ((self = [self initWithSuccess:success andOutput:output]) == nil) { + return self; + } + self->_result = obj; + return self; +} + +- (instancetype)initWithSuccess:(BOOL)success andOutput:(NSFileHandle *)output { + if ((self = [super init]) == nil) { + return nil; + } + + if (output != nil) { + self->_output = [[NSString alloc] initWithData:[output readDataToEndOfFile] encoding:NSUTF8StringEncoding]; + } else { + self->_output = @""; + } + + self->_succeeded = success; + + return self; +} + +- (PMDetectState)detectStateValue { + NSNumber *value = self.result; + return value.unsignedIntegerValue; +} + +- (PMServiceStatus)serviceStateValue { + NSNumber *value = self.result; + return value.unsignedIntegerValue; +} + +- (PMVMPresence)vmPresenceValue { + NSNumber *value = self.result; + return value.unsignedIntegerValue; +} + +@end diff --git a/Podman/PMApplication.h b/Podman/PMApplication.h new file mode 100644 index 0000000..31549fa --- /dev/null +++ b/Podman/PMApplication.h @@ -0,0 +1,17 @@ +// +// PMApplication.h +// Podman macOS +// +// Created by Victor Gama on 02/09/2021. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// PMApplication represents the entrypoint for this application. +@interface PMApplication : NSApplication + +@end + +NS_ASSUME_NONNULL_END diff --git a/Podman/PMApplication.m b/Podman/PMApplication.m new file mode 100644 index 0000000..af59ffb --- /dev/null +++ b/Podman/PMApplication.m @@ -0,0 +1,26 @@ +// +// PMApplication.m +// Podman macOS +// +// Created by Victor Gama on 02/09/2021. +// + +#import "PMApplication.h" +#import "AppDelegate.h" + +@implementation PMApplication { + AppDelegate *appDelegate; +} + +- (instancetype)init { + if ((self = [super init]) == nil) { + return self; + } + + self->appDelegate = [[AppDelegate alloc] init]; + self.delegate = self->appDelegate; + + return self; +} + +@end diff --git a/Podman/PMDispatch.h b/Podman/PMDispatch.h new file mode 100644 index 0000000..07ee397 --- /dev/null +++ b/Podman/PMDispatch.h @@ -0,0 +1,32 @@ +// +// PMDispatch.h +// Podman macOS +// +// Created by Victor Gama on 03/09/2021. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + + +/// PMDispatch provides utility functions for dispatching blocks through the Grand Central Dispatch +@interface PMDispatch : NSObject + +/// Asynchronously executes a given block in a background queue. Blocks being executed here must not +/// attempt to update the UI. In case that's needed, execute any UI updates in another block provided to +/// `sync:` +/// @param block Block to be executed asynchronously. ++ (void)background:(void (^ _Nonnull) (void))block; + +/// Executes a block asynchronously in the main queue. +/// @param block Block to be executed asynchronously. ++ (void)async:(void (^ _Nonnull) (void))block; + +/// Executes a block synchronously in the main queue. +/// @param block Block to be executed synchronously. ++ (void)sync:(void (^ _Nonnull) (void))block; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Podman/PMDispatch.m b/Podman/PMDispatch.m new file mode 100644 index 0000000..74e11e6 --- /dev/null +++ b/Podman/PMDispatch.m @@ -0,0 +1,24 @@ +// +// PMDispatch.m +// Podman macOS +// +// Created by Victor Gama on 03/09/2021. +// + +#import "PMDispatch.h" + +@implementation PMDispatch + ++ (void)background:(void (^)(void))block { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), block); +} + ++ (void)async:(void (^)(void))block { + dispatch_async(dispatch_get_main_queue(), block); +} + ++ (void)sync:(void (^)(void))block { + dispatch_sync(dispatch_get_main_queue(), block); +} + +@end diff --git a/Podman/PMMoveToApplications.h b/Podman/PMMoveToApplications.h new file mode 100644 index 0000000..c0cc369 --- /dev/null +++ b/Podman/PMMoveToApplications.h @@ -0,0 +1,23 @@ +// +// PFMoveApplication.h, version 1.25 +// LetsMove +// +// Created by Andy Kim at Potion Factory LLC on 9/17/09 +// Updated by Victor Gama on 9/03/21 +// +// The contents of this file are dedicated to the public domain. + + +#import + +@interface PMMoveToApplications : NSObject + +/** + Moves the running application to ~/Applications or /Applications if the former does not exist. + After the move, it relaunches app from the new location. + DOES NOT work for sandboxed applications. + + Call from \c NSApplication's delegate method \c -applicationWillFinishLaunching: method. */ ++ (void)moveToApplicationsFolderIfNecessary; + +@end diff --git a/Podman/PMMoveToApplications.m b/Podman/PMMoveToApplications.m new file mode 100644 index 0000000..dfdb020 --- /dev/null +++ b/Podman/PMMoveToApplications.m @@ -0,0 +1,513 @@ +// +// PFMoveApplication.m, version 1.25 +// LetsMove +// +// Created by Andy Kim at Potion Factory LLC on 9/17/09 +// Updated by Victor Gama on 9/03/21 +// +// The contents of this file are dedicated to the public domain. + +#import "PMMoveToApplications.h" +#import +#import +#import +#import + +// Strings +// These are macros to be able to use custom i18n tools +#define _I10NS(nsstr) NSLocalizedStringFromTableInBundle(nsstr, @"MoveApplication", [PMMoveToApplications bundle], nil) +#define kStrMoveApplicationCouldNotMove _I10NS(@"Could not move to Applications folder") +#define kStrMoveApplicationQuestionTitle _I10NS(@"Move to Applications folder?") +#define kStrMoveApplicationQuestionTitleHome _I10NS(@"Move to Applications folder in your Home folder?") +#define kStrMoveApplicationQuestionMessage _I10NS(@"I can move myself to the Applications folder if you'd like.") +#define kStrMoveApplicationButtonMove _I10NS(@"Move to Applications Folder") +#define kStrMoveApplicationButtonDoNotMove _I10NS(@"Do Not Move") +#define kStrMoveApplicationQuestionInfoWillRequirePasswd _I10NS(@"Note that this will require an administrator password.") +#define kStrMoveApplicationQuestionInfoInDownloadsFolder _I10NS(@"This will keep your Downloads folder uncluttered.") + +// By default, we use a small control/font for the suppression button. +// If you prefer to use the system default (to match your other alerts), +// set this to 0. +#define PFUseSmallAlertSuppressCheckbox 1 + +@interface PMMoveToApplications () + ++ (NSBundle *)bundle; + +@end + +@implementation PMMoveToApplications + ++ (NSBundle *)bundle { + return [NSBundle mainBundle]; +} + +static NSString *AlertSuppressKey = @"moveToApplicationsFolderAlertSuppress"; +static BOOL MoveInProgress = NO; + +// Main worker function ++ (void)moveToApplicationsFolderIfNecessary { + // Make sure to do our work on the main thread. + // Apparently Electron apps need this for things to work properly. + if (![NSThread isMainThread]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self moveToApplicationsFolderIfNecessary]; + }); + return; + } + + // Skip if user suppressed the alert before + if ([[NSUserDefaults standardUserDefaults] boolForKey:AlertSuppressKey]) return; + + // Path of the bundle + NSString *bundlePath = [[NSBundle mainBundle] bundlePath]; + + // Check if the bundle is embedded in another application + BOOL isNestedApplication = [self isApplicationAtPathNested:bundlePath]; + + // Skip if the application is already in some Applications folder, + // unless it's inside another app's bundle. + if ([self isInApplicationsFolder:bundlePath] && !isNestedApplication) return; + + // OK, looks like we'll need to do a move - set the status variable appropriately + MoveInProgress = YES; + + // File Manager + NSFileManager *fm = [NSFileManager defaultManager]; + + // Are we on a disk image? + NSString *diskImageDevice = [self containingDiskImageDevice:bundlePath]; + + // Since we are good to go, get the preferred installation directory. + BOOL installToUserApplications = NO; + NSString *applicationsDirectory = [self preferredInstallLocation:&installToUserApplications]; + NSString *bundleName = [bundlePath lastPathComponent]; + NSString *destinationPath = [applicationsDirectory stringByAppendingPathComponent:bundleName]; + + // Check if we need admin password to write to the Applications directory + BOOL needAuthorization = ([fm isWritableFileAtPath:applicationsDirectory] == NO); + + // Check if the destination bundle is already there but not writable + needAuthorization |= ([fm fileExistsAtPath:destinationPath] && ![fm isWritableFileAtPath:destinationPath]); + + // Setup the alert + NSAlert *alert = [[NSAlert alloc] init]; + { + NSString *informativeText = nil; + + [alert setMessageText:(installToUserApplications ? kStrMoveApplicationQuestionTitleHome : kStrMoveApplicationQuestionTitle)]; + + informativeText = kStrMoveApplicationQuestionMessage; + + if (needAuthorization) { + informativeText = [informativeText stringByAppendingString:@" "]; + informativeText = [informativeText stringByAppendingString:kStrMoveApplicationQuestionInfoWillRequirePasswd]; + } + else if ([self isInDownloadsFolder:bundlePath]) { + // Don't mention this stuff if we need authentication. The informative text is long enough as it is in that case. + informativeText = [informativeText stringByAppendingString:@" "]; + informativeText = [informativeText stringByAppendingString:kStrMoveApplicationQuestionInfoInDownloadsFolder]; + } + + [alert setInformativeText:informativeText]; + + // Add accept button + [alert addButtonWithTitle:kStrMoveApplicationButtonMove]; + + // Add deny button + NSButton *cancelButton = [alert addButtonWithTitle:kStrMoveApplicationButtonDoNotMove]; + [cancelButton setKeyEquivalent:[NSString stringWithFormat:@"%C", 0x1b]]; // Escape key + + // Setup suppression button + [alert setShowsSuppressionButton:YES]; + + if (PFUseSmallAlertSuppressCheckbox) { + NSCell *cell = [[alert suppressionButton] cell]; + [cell setControlSize:NSControlSizeSmall]; + [cell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + } + } + + // Activate app -- work-around for focus issues related to "scary file from internet" OS dialog. + if (![NSApp isActive]) { + [NSApp activateIgnoringOtherApps:YES]; + } + + if ([alert runModal] == NSAlertFirstButtonReturn) { + NSLog(@"INFO -- Moving myself to the Applications folder"); + + // Move + if (needAuthorization) { + BOOL authorizationCanceled; + if (![self authorizedInstallAt:bundlePath withDestinationPath:destinationPath andCancelFlag:&authorizationCanceled]) { + if (authorizationCanceled) { + NSLog(@"INFO -- Not moving because user canceled authorization"); + MoveInProgress = NO; + return; + } + else { + NSLog(@"ERROR -- Could not copy myself to /Applications with authorization"); + goto fail; + } + } + } + else { + // If a copy already exists in the Applications folder, put it in the Trash + if ([fm fileExistsAtPath:destinationPath]) { + // But first, make sure that it's not running + if ([self isApplicationAtPathRunning:destinationPath]) { + // Give the running app focus and terminate myself + NSLog(@"INFO -- Switching to an already running version"); + [[NSTask launchedTaskWithLaunchPath:@"/usr/bin/open" arguments:[NSArray arrayWithObject:destinationPath]] waitUntilExit]; + MoveInProgress = NO; + exit(0); + } + else { + if (![self trash:[applicationsDirectory stringByAppendingPathComponent:bundleName]]) + goto fail; + } + } + + if (![self copyBundleAt:bundlePath to:destinationPath]) { + NSLog(@"ERROR -- Could not copy myself to %@", destinationPath); + goto fail; + } + } + + // Trash the original app. It's okay if this fails. + // NOTE: This final delete does not work if the source bundle is in a network mounted volume. + // Calling rm or file manager's delete method doesn't work either. It's unlikely to happen + // but it'd be great if someone could fix this. + if (!isNestedApplication && diskImageDevice == nil && ![self deleteOrTrash:bundlePath]) { + NSLog(@"WARNING -- Could not delete application after moving it to Applications folder"); + } + + // Relaunch. + [self relaunchWithPath:destinationPath]; + + // Launched from within a disk image? -- unmount (if no files are open after 5 seconds, + // otherwise leave it mounted). + if (diskImageDevice && !isNestedApplication) { + NSString *script = [NSString stringWithFormat:@"(/bin/sleep 5 && /usr/bin/hdiutil detach %@) &", [self shellQuotedString:diskImageDevice]]; + [NSTask launchedTaskWithLaunchPath:@"/bin/sh" arguments:[NSArray arrayWithObjects:@"-c", script, nil]]; + } + + MoveInProgress = NO; + exit(0); + } + // Save the alert suppress preference if checked + else if ([[alert suppressionButton] state] == NSControlStateValueOn) { + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:AlertSuppressKey]; + } + + MoveInProgress = NO; + return; + +fail: + { + // Show failure message + alert = [[NSAlert alloc] init]; + [alert setMessageText:kStrMoveApplicationCouldNotMove]; + [alert runModal]; + MoveInProgress = NO; + } +} + ++ (BOOL) PFMoveIsInProgres { + return MoveInProgress; +} + + + +#pragma mark - +#pragma mark Helper Functions + ++ (NSString *)preferredInstallLocation:(BOOL *)isUserDirectory { + // Return the preferred install location. + // Assume that if the user has a ~/Applications folder, they'd prefer their + // applications to go there. + + NSFileManager *fm = [NSFileManager defaultManager]; + + NSArray *userApplicationsDirs = NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSUserDomainMask, YES); + + if ([userApplicationsDirs count] > 0) { + NSString *userApplicationsDir = [userApplicationsDirs objectAtIndex:0]; + BOOL isDirectory; + + if ([fm fileExistsAtPath:userApplicationsDir isDirectory:&isDirectory] && isDirectory) { + // User Applications directory exists. Get the directory contents. + NSArray *contents = [fm contentsOfDirectoryAtPath:userApplicationsDir error:NULL]; + + // Check if there is at least one ".app" inside the directory. + for (NSString *contentsPath in contents) { + if ([[contentsPath pathExtension] isEqualToString:@"app"]) { + if (isUserDirectory) *isUserDirectory = YES; + return [userApplicationsDir stringByResolvingSymlinksInPath]; + } + } + } + } + + // No user Applications directory in use. Return the machine local Applications directory + if (isUserDirectory) *isUserDirectory = NO; + + return [[NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSLocalDomainMask, YES) lastObject] stringByResolvingSymlinksInPath]; +} + ++ (BOOL)isInApplicationsFolder:(NSString *)path { + // Check all the normal Application directories + NSArray *applicationDirs = NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSAllDomainsMask, YES); + for (NSString *appDir in applicationDirs) { + if ([path hasPrefix:appDir]) return YES; + } + + // Also, handle the case that the user has some other Application directory (perhaps on a separate data partition). + if ([[path pathComponents] containsObject:@"Applications"]) return YES; + + return NO; +} + ++ (BOOL)isInDownloadsFolder:(NSString *)path { + NSArray *downloadDirs = NSSearchPathForDirectoriesInDomains(NSDownloadsDirectory, NSAllDomainsMask, YES); + for (NSString *downloadsDirPath in downloadDirs) { + if ([path hasPrefix:downloadsDirPath]) return YES; + } + + return NO; +} + ++ (BOOL)isApplicationAtPathRunning:(NSString *)bundlePath { + bundlePath = [bundlePath stringByStandardizingPath]; + + for (NSRunningApplication *runningApplication in [[NSWorkspace sharedWorkspace] runningApplications]) { + NSString *runningAppBundlePath = [[[runningApplication bundleURL] path] stringByStandardizingPath]; + if ([runningAppBundlePath isEqualToString:bundlePath]) { + return YES; + } + } + + return NO; +} + ++ (BOOL)isApplicationAtPathNested:(NSString *)path { + NSString *containingPath = [path stringByDeletingLastPathComponent]; + + NSArray *components = [containingPath pathComponents]; + for (NSString *component in components) { + if ([[component pathExtension] isEqualToString:@"app"]) { + return YES; + } + } + + return NO; +} + ++ (NSString *)containingDiskImageDevice:(NSString *)path { + NSString *containingPath = [path stringByDeletingLastPathComponent]; + + struct statfs fs; + if (statfs([containingPath fileSystemRepresentation], &fs) || (fs.f_flags & MNT_ROOTFS)) + return nil; + + NSString *device = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:fs.f_mntfromname length:strlen(fs.f_mntfromname)]; + + NSTask *hdiutil = [[NSTask alloc] init]; + [hdiutil setLaunchPath:@"/usr/bin/hdiutil"]; + [hdiutil setArguments:[NSArray arrayWithObjects:@"info", @"-plist", nil]]; + [hdiutil setStandardOutput:[NSPipe pipe]]; + [hdiutil launch]; + [hdiutil waitUntilExit]; + + NSData *data = [[[hdiutil standardOutput] fileHandleForReading] readDataToEndOfFile]; + NSDictionary *info = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:NULL error:NULL];; + + if (![info isKindOfClass:[NSDictionary class]]) return nil; + + NSArray *images = (NSArray *)[info objectForKey:@"images"]; + if (![images isKindOfClass:[NSArray class]]) return nil; + + for (NSDictionary *image in images) { + if (![image isKindOfClass:[NSDictionary class]]) return nil; + + id systemEntities = [image objectForKey:@"system-entities"]; + if (![systemEntities isKindOfClass:[NSArray class]]) return nil; + + for (NSDictionary *systemEntity in systemEntities) { + if (![systemEntity isKindOfClass:[NSDictionary class]]) return nil; + + NSString *devEntry = [systemEntity objectForKey:@"dev-entry"]; + if (![devEntry isKindOfClass:[NSString class]]) return nil; + + if ([devEntry isEqualToString:device]) + return device; + } + } + + return nil; +} + ++ (BOOL) trash:(NSString *)path { + BOOL result = [[NSFileManager defaultManager] trashItemAtURL:[NSURL fileURLWithPath:path] resultingItemURL:NULL error:NULL]; + + // As a last resort try trashing with AppleScript. + // This allows us to trash the app in macOS Sierra even when the app is running inside + // an app translocation image. + if (!result) { + NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource: + [NSString stringWithFormat:@"\ + set theFile to POSIX file \"%@\" \n\ + tell application \"Finder\" \n\ + move theFile to trash \n\ + end tell", path]]; + NSDictionary *errorDict = nil; + NSAppleEventDescriptor *scriptResult = [appleScript executeAndReturnError:&errorDict]; + if (scriptResult == nil) { + NSLog(@"Trash AppleScript error: %@", errorDict); + } + result = (scriptResult != nil); + } + + if (!result) { + NSLog(@"ERROR -- Could not trash '%@'", path); + } + + return result; +} + ++ (BOOL)deleteOrTrash:(NSString *)path { + NSError *error; + + if ([[NSFileManager defaultManager] removeItemAtPath:path error:&error]) { + return YES; + } + else { + // Don't log warning if on Sierra and running inside App Translocation path + if ([path rangeOfString:@"/AppTranslocation/"].location == NSNotFound) + NSLog(@"WARNING -- Could not delete '%@': %@", path, [error localizedDescription]); + + return [self trash:path]; + } +} + ++ (BOOL)authorizedInstallAt:(NSString *)srcPath withDestinationPath:(NSString *)dstPath andCancelFlag:(BOOL *)canceled { + if (canceled) *canceled = NO; + + // Make sure that the destination path is an app bundle. We're essentially running 'sudo rm -rf' + // so we really don't want to fuck this up. + if (![[dstPath pathExtension] isEqualToString:@"app"]) return NO; + + // Do some more checks + if ([[dstPath stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] length] == 0) return NO; + if ([[srcPath stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] length] == 0) return NO; + + int pid, status; + AuthorizationRef myAuthorizationRef; + + // Get the authorization + OSStatus err = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &myAuthorizationRef); + if (err != errAuthorizationSuccess) return NO; + + AuthorizationItem myItems = {kAuthorizationRightExecute, 0, NULL, 0}; + AuthorizationRights myRights = {1, &myItems}; + AuthorizationFlags myFlags = (AuthorizationFlags)(kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights | kAuthorizationFlagPreAuthorize); + + err = AuthorizationCopyRights(myAuthorizationRef, &myRights, NULL, myFlags, NULL); + if (err != errAuthorizationSuccess) { + if (err == errAuthorizationCanceled && canceled) + *canceled = YES; + goto fail; + } + + static OSStatus (*security_AuthorizationExecuteWithPrivileges)(AuthorizationRef authorization, const char *pathToTool, + AuthorizationFlags options, char * const *arguments, + FILE **communicationsPipe) = NULL; + if (!security_AuthorizationExecuteWithPrivileges) { + // On 10.7, AuthorizationExecuteWithPrivileges is deprecated. We want to still use it since there's no + // good alternative (without requiring code signing). We'll look up the function through dyld and fail + // if it is no longer accessible. If Apple removes the function entirely this will fail gracefully. If + // they keep the function and throw some sort of exception, this won't fail gracefully, but that's a + // risk we'll have to take for now. + security_AuthorizationExecuteWithPrivileges = (OSStatus (*)(AuthorizationRef, const char*, + AuthorizationFlags, char* const*, + FILE **)) dlsym(RTLD_DEFAULT, "AuthorizationExecuteWithPrivileges"); + } + if (!security_AuthorizationExecuteWithPrivileges) goto fail; + + // Delete the destination + { + char *args[] = {"-rf", (char *)[dstPath fileSystemRepresentation], NULL}; + err = security_AuthorizationExecuteWithPrivileges(myAuthorizationRef, "/bin/rm", kAuthorizationFlagDefaults, args, NULL); + if (err != errAuthorizationSuccess) goto fail; + + // Wait until it's done + pid = wait(&status); + if (pid == -1 || !WIFEXITED(status)) goto fail; // We don't care about exit status as the destination most likely does not exist + } + + // Copy + { + char *args[] = {"-pR", (char *)[srcPath fileSystemRepresentation], (char *)[dstPath fileSystemRepresentation], NULL}; + err = security_AuthorizationExecuteWithPrivileges(myAuthorizationRef, "/bin/cp", kAuthorizationFlagDefaults, args, NULL); + if (err != errAuthorizationSuccess) goto fail; + + // Wait until it's done + pid = wait(&status); + if (pid == -1 || !WIFEXITED(status) || WEXITSTATUS(status)) goto fail; + } + + AuthorizationFree(myAuthorizationRef, kAuthorizationFlagDefaults); + return YES; + +fail: + AuthorizationFree(myAuthorizationRef, kAuthorizationFlagDefaults); + return NO; +} + ++ (BOOL)copyBundleAt:(NSString *)srcPath to:(NSString *)dstPath { + NSFileManager *fm = [NSFileManager defaultManager]; + NSError *error = nil; + + if ([fm copyItemAtPath:srcPath toPath:dstPath error:&error]) { + return YES; + } + else { + NSLog(@"ERROR -- Could not copy '%@' to '%@' (%@)", srcPath, dstPath, error); + return NO; + } +} + ++ (NSString *)shellQuotedString:(NSString *)string { + return [NSString stringWithFormat:@"'%@'", [string stringByReplacingOccurrencesOfString:@"'" withString:@"'\\''"]]; +} + ++ (void)relaunchWithPath:(NSString *)destinationPath { + // The shell script waits until the original app process terminates. + // This is done so that the relaunched app opens as the front-most app. + int pid = [[NSProcessInfo processInfo] processIdentifier]; + + // Command run just before running open /final/path + NSString *preOpenCmd = @""; + + NSString *quotedDestinationPath = [self shellQuotedString:destinationPath]; + + // OS X >=10.5: + // Before we launch the new app, clear xattr:com.apple.quarantine to avoid + // duplicate "scary file from the internet" dialog. + if (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_5) { + // Add the -r flag on 10.6 + preOpenCmd = [NSString stringWithFormat:@"/usr/bin/xattr -d -r com.apple.quarantine %@", quotedDestinationPath]; + } + else { + preOpenCmd = [NSString stringWithFormat:@"/usr/bin/xattr -d com.apple.quarantine %@", quotedDestinationPath]; + } + + NSString *script = [NSString stringWithFormat:@"(while /bin/kill -0 %d >&/dev/null; do /bin/sleep 0.1; done; %@; /usr/bin/open %@) &", pid, preOpenCmd, quotedDestinationPath]; + + [NSTask launchedTaskWithLaunchPath:@"/bin/sh" arguments:[NSArray arrayWithObjects:@"-c", script, nil]]; +} + + +@end + + diff --git a/Podman/PMPreferences.h b/Podman/PMPreferences.h new file mode 100644 index 0000000..3202cfd --- /dev/null +++ b/Podman/PMPreferences.h @@ -0,0 +1,30 @@ +// +// PMPreferences.h +// Podman macOS +// +// Created by Victor Gama on 03/09/2021. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + + +/// PMPreferences provides helper properties for handling the application's preferences +@interface PMPreferences : NSObject + +/// Determines whether the application should start automatically during login +@property (nonatomic, class) BOOL startAtLogin; + +/// Determines whether the application should start Podman's VM at startup +@property (nonatomic, class) BOOL startPodmanVM; + +/// Determines whether the application should check for updates automatically +@property (nonatomic, class) BOOL checkForUpdates; + +/// Determines whether the application already offered to start at login +@property (nonatomic, class) BOOL askedToStartAtLogin; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Podman/PMPreferences.m b/Podman/PMPreferences.m new file mode 100644 index 0000000..d041ac5 --- /dev/null +++ b/Podman/PMPreferences.m @@ -0,0 +1,52 @@ +// +// PMPreferences.m +// Podman macOS +// +// Created by Victor Gama on 03/09/2021. +// + +#import "PMPreferences.h" +#import "PMLoginItem.h" + +@implementation PMPreferences + ++ (BOOL)startAtLogin { + return [PMLoginItem loginItemExistsWithURL:[[NSBundle mainBundle] bundleURL]]; +} + ++ (void)setStartAtLogin:(BOOL)startAtLogin { + if (startAtLogin) { + [PMLoginItem addLoginItemWithURL:[[NSBundle mainBundle] bundleURL]]; + } else { + [PMLoginItem removeLoginItemWithURL:[[NSBundle mainBundle] bundleURL]]; + } +} + ++ (BOOL)startPodmanVM { + if ([[NSUserDefaults standardUserDefaults] objectForKey:@"autoStartVM"] == nil) { + return YES; + } + return [[NSUserDefaults standardUserDefaults] boolForKey:@"autoStartVM"]; +} + ++ (void)setStartPodmanVM:(BOOL)startPodmanVM { + [[NSUserDefaults standardUserDefaults] setBool:startPodmanVM forKey:@"autoStartVM"]; +} + ++ (BOOL)checkForUpdates { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"autoUpdate"]; +} + ++ (void)setCheckForUpdates:(BOOL)checkForUpdates { + [[NSUserDefaults standardUserDefaults] setBool:checkForUpdates forKey:@"autoUpdate"]; +} + ++ (BOOL)askedToStartAtLogin { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"askedToAutoStart"]; +} + ++ (void)setAskedToStartAtLogin:(BOOL)askedToStartAtLogin { + [[NSUserDefaults standardUserDefaults] setBool:askedToStartAtLogin forKey:@"askedToAutoStart"]; +} + +@end diff --git a/Podman/Podman.entitlements b/Podman/Podman.entitlements new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Podman/Podman.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/Podman/main.m b/Podman/main.m new file mode 100644 index 0000000..86f64a7 --- /dev/null +++ b/Podman/main.m @@ -0,0 +1,15 @@ +// +// main.m +// Podman macOS +// +// Created by Victor Gama on 02/09/2021. +// + +#import + +int main(int argc, const char * argv[]) { + @autoreleasepool { + // Setup code that might create autoreleased objects goes here. + } + return NSApplicationMain(argc, argv); +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..5dc2192 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# Podman for macOS + + + +![](https://img.shields.io/badge/-Electron--free-blue) ![](https://img.shields.io/badge/license-MIT-blue) + + +"Podman for macOS" is a macOS frontend for [Podman](https://github.com/containers/podman). It can be +used to start and stop both the Podman Machine and its running containers. In case a Podman Machine +is not yet setup, the application can provision and start it automatically. Additionally, users may +set it to automatically start and bring the machine up during login. + +> ⚠️ **Heads up!** Support to Apple M1 is under development. + +

+ +## Installing + +1. Install [Podman](https://github.com/containers/podman) through [Homebrew](https://brew.sh): + ``` + brew install podman + ``` +2. Download a [Precompiled Binary](https://github.com/heyvito/podman-macos), or clone this repo and +build it. +3. Move the application to your Application's folder +4. Launch it. + +## Contributing +Contributions are welcome! Feel free to send a Pull-Request, or file an issue in case you run into +any problem. + +## TODO + +- [ ] Provide Notarized binaries +- [ ] Add support to Apple M1 + +## Acknowledgements + +- Thanks [@ofeefo](https://github.com/ofeefo) for tips regarding UI ♥️ +- Thanks [@RLMD](https://github.com/RLMD) for creating the Menu Bar icon ♥️ + +## License + +``` +The MIT License (MIT) + +Copyright (c) 2021 Victor Gama + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +```