From bfb66cb080ceb5d20d2024b1834e50d64f81b55f Mon Sep 17 00:00:00 2001 From: zonble Date: Sat, 22 Jan 2022 00:01:21 +0800 Subject: [PATCH] Converts the installer to Swift. --- McBopomofo.xcodeproj/project.pbxproj | 30 +- Source/Installer/AppDelegate.h | 49 --- Source/Installer/AppDelegate.m | 289 ------------------ Source/Installer/AppDelegate.swift | 258 ++++++++++++++++ Source/Installer/ArchiveUtil.h | 39 --- Source/Installer/ArchiveUtil.m | 132 -------- Source/Installer/ArchiveUtil.swift | 121 ++++++++ Source/Installer/Base.lproj/MainMenu.xib | 23 +- ...nstallerUtil.swift => BundleTranslocate.h} | 16 +- Source/Installer/BundleTranslocate.m | 30 ++ .../McBopomofoInstaller-Bridging-Header.h | 1 + Source/Installer/zh-Hant.lproj/MainMenu.xib | 25 +- 12 files changed, 457 insertions(+), 556 deletions(-) delete mode 100644 Source/Installer/AppDelegate.h delete mode 100644 Source/Installer/AppDelegate.m create mode 100644 Source/Installer/AppDelegate.swift delete mode 100644 Source/Installer/ArchiveUtil.h delete mode 100644 Source/Installer/ArchiveUtil.m create mode 100644 Source/Installer/ArchiveUtil.swift rename Source/Installer/{InstallerUtil.swift => BundleTranslocate.h} (79%) create mode 100644 Source/Installer/BundleTranslocate.m diff --git a/McBopomofo.xcodeproj/project.pbxproj b/McBopomofo.xcodeproj/project.pbxproj index 36892a2a..ae5f7bc1 100644 --- a/McBopomofo.xcodeproj/project.pbxproj +++ b/McBopomofo.xcodeproj/project.pbxproj @@ -17,7 +17,6 @@ 6A0D4F5815FC0EF900ABF4B3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6A0D4F4A15FC0EE100ABF4B3 /* Localizable.strings */; }; 6A187E2616004C5900466B2E /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6A187E2816004C5900466B2E /* MainMenu.xib */; }; 6A225A1F23679F2600F685C6 /* NotarizedArchives in Resources */ = {isa = PBXBuildFile; fileRef = 6A225A1E23679F2600F685C6 /* NotarizedArchives */; }; - 6A225A232367A1D700F685C6 /* ArchiveUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A225A222367A1D700F685C6 /* ArchiveUtil.m */; }; 6A2E40F6253A69DA00D1AE1D /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A2E40F5253A69DA00D1AE1D /* Images.xcassets */; }; 6A2E40F9253A6AA000D1AE1D /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A2E40F5253A69DA00D1AE1D /* Images.xcassets */; }; 6A38BC1515FC117A00A8A51F /* data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A38BBF615FC117A00A8A51F /* data.txt */; }; @@ -26,9 +25,7 @@ 6A6ED16C2797650A0012872E /* template-data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A6ED1652797650A0012872E /* template-data.txt */; }; 6A6ED16D2797650A0012872E /* template-exclude-phrases-plain-bpmf.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A6ED1672797650A0012872E /* template-exclude-phrases-plain-bpmf.txt */; }; 6A6ED16E2797650A0012872E /* template-exclude-phrases.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A6ED1692797650A0012872E /* template-exclude-phrases.txt */; }; - 6A93050E2798780000D370DA /* InstallerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A93050D2798780000D370DA /* InstallerUtil.swift */; }; 6ACA41CD15FC1D7500935EF6 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A0D4EA615FC0D2D00ABF4B3 /* Cocoa.framework */; }; - 6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6ACA41E915FC1D9000935EF6 /* AppDelegate.m */; }; 6ACA41FA15FC1D9000935EF6 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41EA15FC1D9000935EF6 /* InfoPlist.strings */; }; 6ACA41FB15FC1D9000935EF6 /* License.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41EC15FC1D9000935EF6 /* License.rtf */; }; 6ACA41FC15FC1D9000935EF6 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41EE15FC1D9000935EF6 /* Localizable.strings */; }; @@ -62,6 +59,9 @@ D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; }; D485D3B92796A8A000657FF3 /* PreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3B82796A8A000657FF3 /* PreferencesTests.swift */; }; D485D3C02796CE3200657FF3 /* VersionUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */; }; + D4F0BBDF279AF1AF0071253C /* ArchiveUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F0BBDE279AF1AF0071253C /* ArchiveUtil.swift */; }; + D4F0BBE1279AF8B30071253C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F0BBE0279AF8B30071253C /* AppDelegate.swift */; }; + D4F0BBE4279B08900071253C /* BundleTranslocate.m in Sources */ = {isa = PBXBuildFile; fileRef = D4F0BBE3279B08900071253C /* BundleTranslocate.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -154,8 +154,6 @@ 6A15B32721A51F2300B92CD3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Source/Base.lproj/preferences.xib; sourceTree = ""; }; 6A187E2916004C7300466B2E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = "zh-Hant"; path = "zh-Hant.lproj/MainMenu.xib"; sourceTree = ""; }; 6A225A1E23679F2600F685C6 /* NotarizedArchives */ = {isa = PBXFileReference; lastKnownFileType = folder; path = NotarizedArchives; sourceTree = ""; }; - 6A225A212367A1D700F685C6 /* ArchiveUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ArchiveUtil.h; sourceTree = ""; }; - 6A225A222367A1D700F685C6 /* ArchiveUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ArchiveUtil.m; sourceTree = ""; }; 6A2E40F5253A69DA00D1AE1D /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 6A38BBF615FC117A00A8A51F /* data.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = data.txt; sourceTree = ""; }; 6A38BC2715FC158A00A8A51F /* InputMethodKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputMethodKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.8.sdk/System/Library/Frameworks/InputMethodKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -168,10 +166,7 @@ 6A6ED171279765170012872E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text; name = "zh-Hant"; path = "zh-Hant.lproj/template-exclude-phrases.txt"; sourceTree = ""; }; 6A6ED1722797651A0012872E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text; name = "zh-Hant"; path = "zh-Hant.lproj/template-phrases-replacement.txt"; sourceTree = ""; }; 6A93050C279877FF00D370DA /* McBopomofoInstaller-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "McBopomofoInstaller-Bridging-Header.h"; sourceTree = ""; }; - 6A93050D2798780000D370DA /* InstallerUtil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InstallerUtil.swift; path = InstallerUtil.swift; sourceTree = ""; }; 6ACA41CB15FC1D7500935EF6 /* McBopomofoInstaller.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = McBopomofoInstaller.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 6ACA41E815FC1D9000935EF6 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Source/Installer/AppDelegate.h; sourceTree = SOURCE_ROOT; }; - 6ACA41E915FC1D9000935EF6 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = Source/Installer/AppDelegate.m; sourceTree = SOURCE_ROOT; }; 6ACA41EB15FC1D9000935EF6 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 6ACA41ED15FC1D9000935EF6 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/License.rtf; sourceTree = ""; }; 6ACA41EF15FC1D9000935EF6 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -218,6 +213,10 @@ D485D3B62796A8A000657FF3 /* McBopomofoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = McBopomofoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D485D3B82796A8A000657FF3 /* PreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesTests.swift; sourceTree = ""; }; D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionUpdateTests.swift; sourceTree = ""; }; + D4F0BBDE279AF1AF0071253C /* ArchiveUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveUtil.swift; sourceTree = ""; }; + D4F0BBE0279AF8B30071253C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + D4F0BBE2279B08900071253C /* BundleTranslocate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BundleTranslocate.h; sourceTree = ""; }; + D4F0BBE3279B08900071253C /* BundleTranslocate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BundleTranslocate.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -450,12 +449,9 @@ 6ACA41E715FC1D9000935EF6 /* Installer */ = { isa = PBXGroup; children = ( - 6A93050D2798780000D370DA /* InstallerUtil.swift */, - 6A225A212367A1D700F685C6 /* ArchiveUtil.h */, - 6A225A222367A1D700F685C6 /* ArchiveUtil.m */, + D4F0BBE0279AF8B30071253C /* AppDelegate.swift */, + D4F0BBDE279AF1AF0071253C /* ArchiveUtil.swift */, 6A225A1E23679F2600F685C6 /* NotarizedArchives */, - 6ACA41E815FC1D9000935EF6 /* AppDelegate.h */, - 6ACA41E915FC1D9000935EF6 /* AppDelegate.m */, 6ACA41EA15FC1D9000935EF6 /* InfoPlist.strings */, 6ACA41EC15FC1D9000935EF6 /* License.rtf */, 6ACA41EE15FC1D9000935EF6 /* Localizable.strings */, @@ -464,6 +460,8 @@ 6ACA41F315FC1D9000935EF6 /* Installer-Prefix.pch */, 6ACA41F415FC1D9000935EF6 /* main.m */, 6A93050C279877FF00D370DA /* McBopomofoInstaller-Bridging-Header.h */, + D4F0BBE2279B08900071253C /* BundleTranslocate.h */, + D4F0BBE3279B08900071253C /* BundleTranslocate.m */, ); path = Installer; sourceTree = ""; @@ -716,10 +714,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */, - 6A225A232367A1D700F685C6 /* ArchiveUtil.m in Sources */, - 6A93050E2798780000D370DA /* InstallerUtil.swift in Sources */, + D4F0BBE1279AF8B30071253C /* AppDelegate.swift in Sources */, 6ACA41FF15FC1D9000935EF6 /* main.m in Sources */, + D4F0BBE4279B08900071253C /* BundleTranslocate.m in Sources */, + D4F0BBDF279AF1AF0071253C /* ArchiveUtil.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Source/Installer/AppDelegate.h b/Source/Installer/AppDelegate.h deleted file mode 100644 index 714f920e..00000000 --- a/Source/Installer/AppDelegate.h +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2012 and onwards The McBopomofo Authors. -// -// 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. - -#import -#import "ArchiveUtil.h" - -@interface AppDelegate : NSWindowController -{ -@protected - ArchiveUtil *_archiveUtil; - NSString *_installingVersion; - BOOL _upgrading; - NSButton *__weak _installButton; - NSButton *__weak _cancelButton; - NSTextView *__unsafe_unretained _textView; - NSWindow *__weak _progressSheet; - NSProgressIndicator *__weak _progressIndicator; - NSDate *_translocationRemovalStartTime; - NSInteger _currentVersionNumber; -} -- (IBAction)agreeAndInstallAction:(id)sender; -- (IBAction)cancelAction:(id)sender; - -@property (weak) IBOutlet NSButton *installButton; -@property (weak) IBOutlet NSButton *cancelButton; -@property (unsafe_unretained) IBOutlet NSTextView *textView; -@property (weak) IBOutlet NSWindow *progressSheet; -@property (weak) IBOutlet NSProgressIndicator *progressIndicator; -@end diff --git a/Source/Installer/AppDelegate.m b/Source/Installer/AppDelegate.m deleted file mode 100644 index faf034d0..00000000 --- a/Source/Installer/AppDelegate.m +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright (c) 2012 and onwards The McBopomofo Authors. -// -// 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. - -#import "AppDelegate.h" -#import -//#import "McBopomofoInstaller-Swift.h" -@import InputSourceHelper; - -static NSString *const kTargetBin = @"McBopomofo"; -static NSString *const kTargetType = @"app"; -static NSString *const kTargetBundle = @"McBopomofo.app"; -static NSString *const kDestinationPartial = @"~/Library/Input Methods/"; -static NSString *const kTargetPartialPath = @"~/Library/Input Methods/McBopomofo.app"; -static NSString *const kTargetFullBinPartialPath = @"~/Library/Input Methods/McBopomofo.app/Contents/MacOS/McBopomofo"; - -static const NSTimeInterval kTranslocationRemovalTickInterval = 0.5; -static const NSTimeInterval kTranslocationRemovalDeadline = 60.0; - -/// A simple replacement for the deprecated NSRunAlertPanel. -void RunAlertPanel(NSString *title, NSString *message, NSString *buttonTitle) { - NSAlert *alert = [[NSAlert alloc] init]; - [alert setAlertStyle:NSAlertStyleInformational]; - [alert setMessageText:title]; - [alert setInformativeText:message]; - [alert addButtonWithTitle:buttonTitle]; - [alert runModal]; -} - -@implementation AppDelegate -@synthesize installButton = _installButton; -@synthesize cancelButton = _cancelButton; -@synthesize textView = _textView; -@synthesize progressSheet = _progressSheet; -@synthesize progressIndicator = _progressIndicator; - -- (void)applicationDidFinishLaunching:(NSNotification *)aNotification -{ - _installingVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:(id)kCFBundleVersionKey]; - NSString *versionString = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; - - _archiveUtil = [[ArchiveUtil alloc] initWithAppName:kTargetBin targetAppBundleName:kTargetBundle]; - [_archiveUtil validateIfNotarizedArchiveExists]; - - [self.cancelButton setNextKeyView:self.installButton]; - [self.installButton setNextKeyView:self.cancelButton]; - [[self window] setDefaultButtonCell:[self.installButton cell]]; - - NSAttributedString *attrStr = [[NSAttributedString alloc] initWithRTF:[NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"License" ofType:@"rtf"]] documentAttributes:NULL]; - - NSMutableAttributedString *mutableAttrStr = [attrStr mutableCopy]; - [mutableAttrStr addAttribute:NSForegroundColorAttributeName value:[NSColor controlTextColor] range:NSMakeRange(0, [mutableAttrStr length])]; - [[self.textView textStorage] setAttributedString:mutableAttrStr]; - [self.textView setSelectedRange:NSMakeRange(0, 0)]; - - [[self window] setTitle:[NSString stringWithFormat:NSLocalizedString(@"%@ (for version %@, r%@)", nil), [[self window] title], versionString, _installingVersion]]; - - if ([[NSFileManager defaultManager] fileExistsAtPath:[kTargetPartialPath stringByExpandingTildeInPath]]) { - NSBundle *currentBundle = [NSBundle bundleWithPath:[kTargetPartialPath stringByExpandingTildeInPath]]; - - NSString *shortVersion = [[currentBundle infoDictionary] objectForKey:@"CFBundleShortVersionString"]; - NSString *currentVersion = [[currentBundle infoDictionary] objectForKey:(id)kCFBundleVersionKey]; - - _currentVersionNumber = [currentVersion integerValue]; - if (shortVersion && currentVersion && [currentVersion compare:_installingVersion options:NSNumericSearch] == NSOrderedAscending) { - _upgrading = YES; - } - } - - if (_upgrading) { - [_installButton setTitle:NSLocalizedString(@"Agree and Upgrade", nil)]; - } - - [[self window] center]; - [[self window] orderFront:self]; - [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; -} - -- (IBAction)agreeAndInstallAction:(id)sender -{ - [_cancelButton setEnabled:NO]; - [_installButton setEnabled:NO]; - [self removeThenInstallInputMethod]; -} - -- (void)removeThenInstallInputMethod -{ - if ([[NSFileManager defaultManager] fileExistsAtPath:[kTargetPartialPath stringByExpandingTildeInPath]]) { - - BOOL shouldWaitForTranslocationRemoval = - [self appBundleTranslocatedToARandomizedPath:kTargetPartialPath] && - [self.window respondsToSelector:@selector(beginSheet:completionHandler:)]; - - // http://www.cocoadev.com/index.pl?MoveToTrash - NSString *sourceDir = [kDestinationPartial stringByExpandingTildeInPath]; - NSString *trashDir = [NSHomeDirectory() stringByAppendingPathComponent:@".Trash"]; - NSInteger tag; - - [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation source:sourceDir destination:trashDir files:[NSArray arrayWithObject:kTargetBundle] tag:&tag]; - (void)tag; - - NSTask *killTask = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/killall" arguments:[NSArray arrayWithObjects: @"-9", kTargetBin, nil]]; - [killTask waitUntilExit]; - - if (shouldWaitForTranslocationRemoval) { - [self.progressIndicator startAnimation:self]; - [self.window beginSheet:self.progressSheet completionHandler:^(NSModalResponse returnCode) { - // Schedule the install action in runloop so that the sheet gets a change to dismiss itself. - dispatch_async(dispatch_get_main_queue(), ^{ - if (returnCode == NSModalResponseContinue) { - [self installInputMethodWithPreviousExists:YES previousVersionNotFullyDeactivatedWarning:NO]; - } else { - [self installInputMethodWithPreviousExists:YES previousVersionNotFullyDeactivatedWarning:YES]; - } - }); - }]; - - _translocationRemovalStartTime = [NSDate date]; - [NSTimer scheduledTimerWithTimeInterval:kTranslocationRemovalTickInterval target:self selector:@selector(timerTick:) userInfo:nil repeats:YES]; - return; - } - } - - [self installInputMethodWithPreviousExists:NO previousVersionNotFullyDeactivatedWarning:NO]; -} - -- (void)timerTick:(NSTimer *)timer -{ - NSTimeInterval elapsed = [[NSDate date] timeIntervalSinceDate:_translocationRemovalStartTime]; - [self.progressIndicator setDoubleValue:MIN(elapsed / kTranslocationRemovalDeadline, 1.0)]; - - if (elapsed >= kTranslocationRemovalDeadline) { - [timer invalidate]; - [self.window endSheet:self.progressSheet returnCode:NSModalResponseCancel]; - } else if (![self appBundleTranslocatedToARandomizedPath:kTargetPartialPath]) { - [self.progressIndicator setDoubleValue:1.0]; - [timer invalidate]; - [self.window endSheet:self.progressSheet returnCode:NSModalResponseContinue]; - } -} - - -- (void)installInputMethodWithPreviousExists:(BOOL)previousVersionExists previousVersionNotFullyDeactivatedWarning:(BOOL)warning -{ - // If the unzipped archive does not exist, this must be a dev-mode installer. - NSString *targetBundle = [_archiveUtil unzipNotarizedArchive]; - if (!targetBundle) { - targetBundle = [[NSBundle mainBundle] pathForResource:kTargetBin ofType:kTargetType]; - } - - NSTask *cpTask = [NSTask launchedTaskWithLaunchPath:@"/bin/cp" arguments:[NSArray arrayWithObjects:@"-R", targetBundle, [kDestinationPartial stringByExpandingTildeInPath], nil]]; - [cpTask waitUntilExit]; - if ([cpTask terminationStatus] != 0) { - RunAlertPanel(NSLocalizedString(@"Install Failed", nil), NSLocalizedString(@"Cannot copy the file to the destination.", nil), NSLocalizedString(@"Cancel", nil)); - [NSApp terminate:self]; - } - - NSBundle *imeBundle = [NSBundle bundleWithPath:[kTargetPartialPath stringByExpandingTildeInPath]]; - NSCAssert(imeBundle != nil, @"Target bundle must exists"); - NSURL *imeBundleURL = imeBundle.bundleURL; - NSString *imeIdentifier = imeBundle.bundleIdentifier; - - TISInputSourceRef inputSource = [InputSourceHelper inputSourceForInputSourceID:imeIdentifier]; - - // if this IME name is not found in the list of available IMEs - if (!inputSource) { - NSLog(@"Registering input source %@ at %@.", imeIdentifier, imeBundleURL.absoluteString); - // then register - BOOL status = [InputSourceHelper registerInputSource:imeBundleURL]; - - if (!status) { - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"Cannot register input source %@ at %@.", nil), imeIdentifier, imeBundleURL.absoluteString]; - RunAlertPanel(NSLocalizedString(@"Fatal Error", nil), message, NSLocalizedString(@"Abort", nil)); - [self endAppWithDelay]; - return; - } - - inputSource = [InputSourceHelper inputSourceForInputSourceID:imeIdentifier]; - // if it still doesn't register successfully, bail. - if (!inputSource) { - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"Cannot find input source %@ after registration.", nil), imeIdentifier]; - RunAlertPanel(NSLocalizedString(@"Fatal Error", nil), message, NSLocalizedString(@"Abort", nil)); - [self endAppWithDelay]; - return; - } - } - - BOOL isMacOS12OrAbove = NO; - if (@available(macOS 12.0, *)) { - NSLog(@"macOS 12 or later detected."); - isMacOS12OrAbove = YES; - } else { - NSLog(@"Installer runs with the pre-macOS 12 flow."); - } - - // If the IME is not enabled, enable it. Also, unconditionally enable it on macOS 12.0+, - // as the kTISPropertyInputSourceIsEnabled can still be true even if the IME is *not* - // enabled in the user's current set of IMEs (which means the IME does not show up in - // the user's input menu). - BOOL mainInputSourceEnabled = [InputSourceHelper inputSourceEnabled:inputSource]; - if (!mainInputSourceEnabled || isMacOS12OrAbove) { - - mainInputSourceEnabled = [InputSourceHelper enableInputSource:inputSource]; - if (mainInputSourceEnabled) { - NSLog(@"Input method enabled: %@", imeIdentifier); - } else { - NSLog(@"Failed to enable input method: %@", imeIdentifier); - } - } - - if (warning) { - RunAlertPanel(NSLocalizedString(@"Attention", nil), NSLocalizedString(@"McBopomofo is upgraded, but please log out or reboot for the new version to be fully functional.", nil), NSLocalizedString(@"OK", nil)); - } else { - // Only prompt a warning if pre-macOS 12. The flag is not indicative of anything meaningful due to the need of user intervention in Prefernces.app on macOS 12. - if (!mainInputSourceEnabled && !isMacOS12OrAbove) { - RunAlertPanel(NSLocalizedString(@"Warning", nil), NSLocalizedString(@"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.", nil), NSLocalizedString(@"Continue", nil)); - } else { - RunAlertPanel(NSLocalizedString(@"Installation Successful", nil), NSLocalizedString(@"McBopomofo is ready to use.", nil), NSLocalizedString(@"OK", nil)); - } - } - - [self endAppWithDelay]; -} - -- (void)endAppWithDelay -{ - [[NSApplication sharedApplication] performSelector:@selector(terminate:) withObject:self afterDelay:0.1]; -} - -- (IBAction)cancelAction:(id)sender -{ - [NSApp terminate:self]; -} - -- (void)windowWillClose:(NSNotification *)notification -{ - [NSApp terminate:self]; -} - -// Determines if an app is translocated by Gatekeeper to a randomized path -// See https://weblog.rogueamoeba.com/2016/06/29/sierra-and-gatekeeper-path-randomization/ -- (BOOL)appBundleTranslocatedToARandomizedPath:(NSString *)bundle -{ - const char *bundleAbsPath = [[bundle stringByExpandingTildeInPath] UTF8String]; - int entryCount = getfsstat(NULL, 0, 0); - int entrySize = sizeof(struct statfs); - struct statfs *bufs = (struct statfs *)calloc(entryCount, entrySize); - entryCount = getfsstat(bufs, entryCount * entrySize, MNT_NOWAIT); - for (int i = 0; i < entryCount; i++) { - if (!strcmp(bundleAbsPath, bufs[i].f_mntfromname)) { - free(bufs); - - // getfsstat() may return us a cached result, and so we need to get the stat of the mounted fs. - // If statfs() returns an error, the mounted fs is already gone. - struct statfs stat; - int checkResult = statfs(bundleAbsPath, &stat); - if (checkResult != 0) { - // Meaning the app's bundle is not mounted, that is it's not translocated. - // It also means that the app is not loaded. - return NO; - } - - return YES; - } - } - free(bufs); - return NO; - -} -@end diff --git a/Source/Installer/AppDelegate.swift b/Source/Installer/AppDelegate.swift new file mode 100644 index 00000000..e3b51baa --- /dev/null +++ b/Source/Installer/AppDelegate.swift @@ -0,0 +1,258 @@ +// Copyright (c) 2022 and onwards The McBopomofo Authors. +// +// 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. + +import Cocoa +import InputSourceHelper + +private let kTargetBin = "McBopomofo" +private let kTargetType = "app" +private let kTargetBundle = "McBopomofo.app" +private let kDestinationPartial = "~/Library/Input Methods/" +private let kTargetPartialPath = "~/Library/Input Methods/McBopomofo.app" +private let kTargetFullBinPartialPath = "~/Library/Input Methods/McBopomofo.app/Contents/MacOS/McBopomofo" + +private let kTranslocationRemovalTickInterval: TimeInterval = 0.5 +private let kTranslocationRemovalDeadline: TimeInterval = 60.0 + +@objc (AppDelegate) +class AppDelegate: NSWindowController, NSApplicationDelegate { + @IBOutlet weak private var installButton: NSButton! + @IBOutlet weak private var cancelButton: NSButton! + @IBOutlet private var textView: NSTextView! + @IBOutlet weak private var progressSheet: NSWindow! + @IBOutlet weak private var progressIndicator: NSProgressIndicator! + + private var archiveUtil: ArchiveUtil? + private var installingVersion = "" + private var upgrading = false + private var translocationRemovalStartTime: Date? + private var currentVersionNumber: Int = 0 + + func runAlertPanel(title: String, message: String, buttonTitle: String) { + let alert = NSAlert() + alert.alertStyle = .informational + alert.messageText = title + alert.informativeText = message + alert.addButton(withTitle: buttonTitle) + alert.runModal() + } + + func applicationDidFinishLaunching(_ notification: Notification) { + guard let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String, + let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { + return + } + self.installingVersion = installingVersion + self.archiveUtil = ArchiveUtil(appName: kTargetBin, targetAppBundleName: kTargetBundle) + _ = archiveUtil?.validateIfNotarizedArchiveExists() + + cancelButton.nextKeyView = installButton + installButton.nextKeyView = cancelButton + if let cell = installButton.cell as? NSButtonCell { + window?.defaultButtonCell = cell + } + + var attrStr = NSAttributedString() + if let rtfPath = Bundle.main.url(forResource: "License", withExtension: "rtf"), + let rtfData = try? Data(contentsOf: rtfPath), + let rtf = NSAttributedString(rtf: rtfData, documentAttributes: nil) { + attrStr = rtf + } + + let mutableAttrStr = NSMutableAttributedString(attributedString: attrStr) + mutableAttrStr.addAttribute(.foregroundColor, value: NSColor.controlTextColor, range: NSMakeRange(0, mutableAttrStr.length)) + textView.textStorage?.setAttributedString(mutableAttrStr) + textView.setSelectedRange(NSMakeRange(0, 0)) + window?.title = String(format: NSLocalizedString("%@ (for version %@, r%@)", comment: ""), window?.title ?? "", versionString, installingVersion) + + if FileManager.default.fileExists(atPath: (kTargetPartialPath as NSString).expandingTildeInPath) { + let currentBundle = Bundle(path: (kTargetPartialPath as NSString).expandingTildeInPath) + let shortVersion = currentBundle?.infoDictionary?["CFBundleShortVersionString"] as? String + let currentVersion = currentBundle?.infoDictionary?[kCFBundleVersionKey as String] as? String + currentVersionNumber = (currentVersion as NSString?)?.integerValue ?? 0 + if shortVersion != nil, let currentVersion = currentVersion, currentVersion.compare(installingVersion, options: .numeric) == .orderedAscending { + upgrading = true + } + } + + if upgrading { + installButton.title = NSLocalizedString("Agree and Upgrade", comment: "") + } + + window?.center() + window?.orderFront(self) + NSApp.activate(ignoringOtherApps: true) + } + + @IBAction func agreeAndInstallAction(_ sender: AnyObject) { + cancelButton.isEnabled = false + installButton.isEnabled = false + removeThenInstallInputMethod() + } + + @objc func timerTick(_ timer: Timer) { + let elapsed = Date().timeIntervalSince(translocationRemovalStartTime ?? Date()) + if elapsed >= kTranslocationRemovalDeadline { + timer.invalidate() + window?.endSheet(progressSheet, returnCode: .cancel) + } else if appBundleTranslocatedToARandomizedPath(kTargetPartialPath) == false { + progressIndicator.doubleValue = 1.0 + timer.invalidate() + window?.endSheet(progressSheet, returnCode: .continue) + } + } + + func removeThenInstallInputMethod() { + if FileManager.default.fileExists(atPath: (kTargetPartialPath as NSString).expandingTildeInPath) == false { + self.installInputMethod(previousExists: false, previousVersionNotFullyDeactivatedWarning: false) + return + } + + let shouldWaitForTranslocationRemoval = appBundleTranslocatedToARandomizedPath(kTargetPartialPath) && (window?.responds(to: #selector(NSWindow.beginSheet(_:completionHandler:))) ?? false) + + // http://www.cocoadev.com/index.pl?MoveToTrash + let sourceDir = (kDestinationPartial as NSString).expandingTildeInPath + let trashDir = (NSHomeDirectory() as NSString).appendingPathComponent(".Trash") + var tag = 0 + + NSWorkspace.shared.performFileOperation(.recycleOperation, source: sourceDir, destination: trashDir, files: [kTargetBundle], tag: &tag) + + let killTask = Process() + killTask.launchPath = "/usr/bin/killall" + killTask.arguments = ["-9", kTargetBin] + killTask.launch() + killTask.waitUntilExit() + + if shouldWaitForTranslocationRemoval { + progressIndicator.startAnimation(self) + window?.beginSheet(progressSheet) { returnCode in + DispatchQueue.main.async { + if returnCode == .continue { + self.installInputMethod(previousExists: true, previousVersionNotFullyDeactivatedWarning: false) + } else { + self.installInputMethod(previousExists: true, previousVersionNotFullyDeactivatedWarning: true) + } + } + } + + translocationRemovalStartTime = Date() + Timer.scheduledTimer(timeInterval: kTranslocationRemovalTickInterval, target: self, selector: #selector(timerTick(_:)), userInfo: nil, repeats: true) + } + } + + func installInputMethod(previousExists: Bool, previousVersionNotFullyDeactivatedWarning warning: Bool) { + guard let targetBundle = archiveUtil?.unzipNotarizedArchive() ?? Bundle.main.path(forResource: kTargetBin, ofType: kTargetType) else { + return + } + let cpTask = Process() + cpTask.launchPath = "/bin/cp" + cpTask.arguments = ["-R", targetBundle, (kDestinationPartial as NSString).expandingTildeInPath] + cpTask.launch() + cpTask.waitUntilExit() + + if cpTask.terminationStatus != 0 { + runAlertPanel(title: NSLocalizedString("Install Failed", comment: ""), + message: NSLocalizedString("Cannot copy the file to the destination.", comment: ""), + buttonTitle: NSLocalizedString("Cancel", comment: "")) + endAppWithDelay() + } + + guard let imeBundle = Bundle(path: (kTargetPartialPath as NSString).expandingTildeInPath), + let imeIdentifier = imeBundle.bundleIdentifier + else { + endAppWithDelay() + return + } + + let imeBundleURL = imeBundle.bundleURL + var inputSource = InputSourceHelper.inputSource(for: imeIdentifier) + + if inputSource == nil { + NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString)."); + let status = InputSourceHelper.registerTnputSource(at: imeBundleURL) + if !status { + let message = String(format: NSLocalizedString("Cannot find input source %@ after registration.", comment: ""), imeIdentifier) + runAlertPanel(title: NSLocalizedString("Fatal Error", comment: ""), message: message, buttonTitle: NSLocalizedString("Abort", comment: "")) + endAppWithDelay() + return + } + + inputSource = InputSourceHelper.inputSource(for: imeIdentifier) + if inputSource == nil { + let message = String(format: NSLocalizedString("Cannot find input source %@ after registration.", comment: ""), imeIdentifier) + runAlertPanel(title: NSLocalizedString("Fatal Error", comment: ""), message: message, buttonTitle: NSLocalizedString("Abort", comment: "")) + } + } + + var isMacOS12OrAbove = false + if #available(macOS 12.0, *) { + NSLog("macOS 12 or later detected."); + isMacOS12OrAbove = true + } else { + NSLog("Installer runs with the pre-macOS 12 flow."); + } + + // If the IME is not enabled, enable it. Also, unconditionally enable it on macOS 12.0+, + // as the kTISPropertyInputSourceIsEnabled can still be true even if the IME is *not* + // enabled in the user's current set of IMEs (which means the IME does not show up in + // the user's input menu). + + var mainInputSourceEnabled = InputSourceHelper.inputSourceEnabled(for: inputSource!) + if !mainInputSourceEnabled || isMacOS12OrAbove { + mainInputSourceEnabled = InputSourceHelper.enable(inputSource: inputSource!) + if (mainInputSourceEnabled) { + NSLog("Input method enabled: \(imeIdentifier)"); + } else { + NSLog("Failed to enable input method: \(imeIdentifier)"); + } + } + + if warning { + runAlertPanel(title: NSLocalizedString("Attention", comment: ""), message: NSLocalizedString("McBopomofo is upgraded, but please log out or reboot for the new version to be fully functional.", comment: ""), buttonTitle: NSLocalizedString("OK", comment: "")) + } else { + if !mainInputSourceEnabled && !isMacOS12OrAbove { + runAlertPanel(title: NSLocalizedString("Warning", comment: ""), message: NSLocalizedString("Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.", comment: ""), buttonTitle: NSLocalizedString("Continue", comment: "")) + } else { + runAlertPanel(title: NSLocalizedString("Installation Successful", comment: ""), message: NSLocalizedString("McBopomofo is ready to use.", comment: ""), buttonTitle: NSLocalizedString("OK", comment: "")) + } + } + + endAppWithDelay() + } + + func endAppWithDelay() { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { + NSApp.terminate(self) + } + } + + @IBAction func cancelAction(_ sender: AnyObject) { + NSApp.terminate(self) + } + + func windowWillClose(_ Notification: Notification) { + NSApp.terminate(self) + } + + +} diff --git a/Source/Installer/ArchiveUtil.h b/Source/Installer/ArchiveUtil.h deleted file mode 100644 index 9bf1415e..00000000 --- a/Source/Installer/ArchiveUtil.h +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2019 and onwards The McBopomofo Authors. -// -// 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. - -#import - -@interface ArchiveUtil : NSObject { - NSString *_appName; - NSString *_targetAppBundleName; -} -- (instancetype _Nonnull)initWithAppName:(NSString *_Nonnull)name - targetAppBundleName:(NSString *_Nonnull)invalidAppBundleName; - -// Returns YES if (1) a zip file under -// Resources/NotarizedArchives/$_appName-$bundleVersion.zip exists, and (2) if -// Resources/$_invalidAppBundleName does not exist. -- (BOOL)validateIfNotarizedArchiveExists; - -- (NSString *_Nullable)unzipNotarizedArchive; -@end diff --git a/Source/Installer/ArchiveUtil.m b/Source/Installer/ArchiveUtil.m deleted file mode 100644 index cf2c9f30..00000000 --- a/Source/Installer/ArchiveUtil.m +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) 2012 and onwards The McBopomofo Authors. -// -// 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. - -#import "ArchiveUtil.h" - -@implementation ArchiveUtil -- (instancetype)initWithAppName:(NSString *)name - targetAppBundleName:(NSString *)targetAppBundleName { - self = [super init]; - if (self) { - _appName = name; - _targetAppBundleName = targetAppBundleName; - } - return self; -} - -- (void)delloc { - _appName = nil; - _targetAppBundleName = nil; -} - -- (BOOL)validateIfNotarizedArchiveExists { - NSString *resourePath = [[NSBundle mainBundle] resourcePath]; - NSString *devModeAppBundlePath = - [resourePath stringByAppendingPathComponent:_targetAppBundleName]; - - NSArray *notarizedArchivesContent = - [[NSFileManager defaultManager] subpathsAtPath:[self notarizedArchivesPath]]; - NSInteger count = [notarizedArchivesContent count]; - BOOL notarizedArchiveExists = - [[NSFileManager defaultManager] fileExistsAtPath:[self notarizedArchive]]; - BOOL devModeAppBundleExists = - [[NSFileManager defaultManager] fileExistsAtPath:devModeAppBundlePath]; - - if (count > 0) { - // Not a valid distribution package. - if (count != 1 || !notarizedArchiveExists || devModeAppBundleExists) { - NSAlert *alert = [[NSAlert alloc] init]; - [alert setAlertStyle:NSAlertStyleInformational]; - [alert setMessageText:@"Internal Error"]; - [alert - setInformativeText: - [NSString stringWithFormat:@"devMode installer, expected archive name: %@, " - @"archive exists: %d, devMode app bundle exists: %d", - [self notarizedArchive], notarizedArchiveExists, - devModeAppBundleExists]]; - [alert addButtonWithTitle:@"Terminate"]; - [alert runModal]; - - [[NSApplication sharedApplication] terminate:nil]; - } else { - return YES; - } - } - - if (!devModeAppBundleExists) { - NSAlert *alert = [[NSAlert alloc] init]; - [alert setAlertStyle:NSAlertStyleInformational]; - [alert setMessageText:@"Internal Error"]; - [alert - setInformativeText:[NSString stringWithFormat:@"Dev target bundle does not exist: %@", - devModeAppBundlePath]]; - [alert addButtonWithTitle:@"Terminate"]; - [alert runModal]; - [[NSApplication sharedApplication] terminate:nil]; - } - - // Notarized archive does not exist, but it's ok. - return NO; -} - -- (NSString *)unzipNotarizedArchive { - if (![self validateIfNotarizedArchiveExists]) { - return nil; - } - - NSString *tempFilePath = - [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]]; - NSArray *arguments = @[ [self notarizedArchive], @"-d", tempFilePath ]; - - NSTask *unzipTask = [[NSTask alloc] init]; - [unzipTask setLaunchPath:@"/usr/bin/unzip"]; - [unzipTask setCurrentDirectoryPath:[[NSBundle mainBundle] resourcePath]]; - [unzipTask setArguments:arguments]; - [unzipTask launch]; - [unzipTask waitUntilExit]; - - NSAssert(unzipTask.terminationStatus == 0, @"Must successfully unzipped"); - - NSString *result = [tempFilePath stringByAppendingPathComponent:_targetAppBundleName]; - NSAssert([[NSFileManager defaultManager] fileExistsAtPath:result], - @"App bundle must be unzipped at %@", result); - return result; -} - -- (NSString *)notarizedArchivesPath { - NSString *resourePath = [[NSBundle mainBundle] resourcePath]; - NSString *notarizedArchivesPath = - [resourePath stringByAppendingPathComponent:@"NotarizedArchives"]; - return notarizedArchivesPath; -} - -- (NSString *)notarizedArchive { - NSString *bundleVersion = - [[[NSBundle mainBundle] infoDictionary] objectForKey:(id)kCFBundleVersionKey]; - NSString *notarizedArchiveBasename = - [NSString stringWithFormat:@"%@-r%@.zip", _appName, bundleVersion]; - NSString *notarizedArchive = - [[self notarizedArchivesPath] stringByAppendingPathComponent:notarizedArchiveBasename]; - return notarizedArchive; -} -@end diff --git a/Source/Installer/ArchiveUtil.swift b/Source/Installer/ArchiveUtil.swift new file mode 100644 index 00000000..182317c2 --- /dev/null +++ b/Source/Installer/ArchiveUtil.swift @@ -0,0 +1,121 @@ +// Copyright (c) 2022 and onwards The McBopomofo Authors. +// +// 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. + +import Cocoa + +struct ArchiveUtil { + var appName: String + var targetAppBundleName: String + + init(appName: String, targetAppBundleName: String) { + self.appName = appName + self.targetAppBundleName = targetAppBundleName + } + + // Returns YES if (1) a zip file under + // Resources/NotarizedArchives/$_appName-$bundleVersion.zip exists, and (2) if + // Resources/$_invalidAppBundleName does not exist. + func validateIfNotarizedArchiveExists() -> Bool { + guard let resourePath = Bundle.main.resourcePath, + let notarizedArchivesPath = notarizedArchivesPath, + let notarizedArchive = notarizedArchive, + let notarizedArchivesContent: [String] = try? FileManager.default.subpathsOfDirectory(atPath: notarizedArchivesPath) + else { + return false + } + + let devModeAppBundlePath = (resourePath as NSString).appendingPathComponent(targetAppBundleName) + let count = notarizedArchivesContent.count + let notarizedArchiveExists = FileManager.default.fileExists(atPath: notarizedArchive) + let devModeAppBundleExists = FileManager.default.fileExists(atPath: devModeAppBundlePath) + + if count > 0 { + if count != 1 || !notarizedArchiveExists || devModeAppBundleExists { + let alert = NSAlert() + alert.alertStyle = .informational + alert.messageText = "Internal Error" + alert.informativeText = "devMode installer, expected archive name: \(notarizedArchive), " + + "archive exists: \(notarizedArchiveExists), devMode app bundle exists: \(devModeAppBundleExists)" + alert.addButton(withTitle: "Terminate") + alert.runModal() + NSApp.terminate(nil) + } else { + return true + } + } + + if !devModeAppBundleExists { + let alert = NSAlert() + alert.alertStyle = .informational + alert.messageText = "Internal Error" + alert.informativeText = "Dev target bundle does not exist: \(devModeAppBundlePath)" + alert.addButton(withTitle: "Terminate") + alert.runModal() + NSApp.terminate(nil) + } + + return false + } + + func unzipNotarizedArchive() -> String? { + if !self.validateIfNotarizedArchiveExists() { + return nil + } + guard let notarizedArchive = notarizedArchive, + let resourcePath = Bundle.main.resourcePath else { + return nil + } + let tempFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent(UUID().uuidString) + let arguments: [String] = [notarizedArchive, "-d", tempFilePath] + let unzipTask = Process() + unzipTask.launchPath = "/usr/bin/unzip" + unzipTask.currentDirectoryPath = resourcePath + unzipTask.arguments = arguments + unzipTask.launch() + unzipTask.waitUntilExit() + + assert(unzipTask.terminationStatus == 0, "Must successfully unzipped") + guard let result = (tempFilePath as NSString).appendingPathExtension(targetAppBundleName) else { + return nil + } + assert(FileManager.default.fileExists(atPath: result), "App bundle must be unzipped at \(resourcePath).") + return result + } + + private var notarizedArchivesPath: String? { + let resourePath = Bundle.main.resourcePath + let notarizedArchivesPath = resourePath?.appending("NotarizedArchives") + return notarizedArchivesPath + } + + private var notarizedArchive: String? { + guard let notarizedArchivesPath = notarizedArchivesPath, + let bundleVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String else { + return nil + } + let notarizedArchiveBasename = "\(appName)-r\(bundleVersion).zip" + let notarizedArchive = notarizedArchivesPath.appending(notarizedArchiveBasename) + return notarizedArchive + } + +} diff --git a/Source/Installer/Base.lproj/MainMenu.xib b/Source/Installer/Base.lproj/MainMenu.xib index 000f018d..6664bcf1 100644 --- a/Source/Installer/Base.lproj/MainMenu.xib +++ b/Source/Installer/Base.lproj/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -81,17 +81,18 @@ + - + - + @@ -118,7 +119,7 @@ - - - + @@ -152,7 +153,7 @@ Gw - + @@ -181,16 +182,16 @@ Gw - + - + - + diff --git a/Source/Installer/InstallerUtil.swift b/Source/Installer/BundleTranslocate.h similarity index 79% rename from Source/Installer/InstallerUtil.swift rename to Source/Installer/BundleTranslocate.h index 861d52a1..72cbf53b 100644 --- a/Source/Installer/InstallerUtil.swift +++ b/Source/Installer/BundleTranslocate.h @@ -21,12 +21,12 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. -import Cocoa -import Carbon +@import Cocoa; -public class InstallerUtil: NSObject { - @available(*, unavailable) - public override init() { - super.init() - } -} +NS_ASSUME_NONNULL_BEGIN + +// Determines if an app is translocated by Gatekeeper to a randomized path +// See https://weblog.rogueamoeba.com/2016/06/29/sierra-and-gatekeeper-path-randomization/ +BOOL appBundleTranslocatedToARandomizedPath(NSString *bundle); + +NS_ASSUME_NONNULL_END diff --git a/Source/Installer/BundleTranslocate.m b/Source/Installer/BundleTranslocate.m new file mode 100644 index 00000000..a04798bc --- /dev/null +++ b/Source/Installer/BundleTranslocate.m @@ -0,0 +1,30 @@ +#import "BundleTranslocate.h" +#import + +BOOL appBundleTranslocatedToARandomizedPath(NSString *bundle) +{ + const char *bundleAbsPath = [[bundle stringByExpandingTildeInPath] UTF8String]; + int entryCount = getfsstat(NULL, 0, 0); + int entrySize = sizeof(struct statfs); + struct statfs *bufs = (struct statfs *)calloc(entryCount, entrySize); + entryCount = getfsstat(bufs, entryCount * entrySize, MNT_NOWAIT); + for (int i = 0; i < entryCount; i++) { + if (!strcmp(bundleAbsPath, bufs[i].f_mntfromname)) { + free(bufs); + + // getfsstat() may return us a cached result, and so we need to get the stat of the mounted fs. + // If statfs() returns an error, the mounted fs is already gone. + struct statfs stat; + int checkResult = statfs(bundleAbsPath, &stat); + if (checkResult != 0) { + // Meaning the app's bundle is not mounted, that is it's not translocated. + // It also means that the app is not loaded. + return NO; + } + + return YES; + } + } + free(bufs); + return NO; +} diff --git a/Source/Installer/McBopomofoInstaller-Bridging-Header.h b/Source/Installer/McBopomofoInstaller-Bridging-Header.h index 1b2cb5d6..d2208adb 100644 --- a/Source/Installer/McBopomofoInstaller-Bridging-Header.h +++ b/Source/Installer/McBopomofoInstaller-Bridging-Header.h @@ -2,3 +2,4 @@ // Use this file to import your target's public headers that you would like to expose to Swift. // +#import "BundleTranslocate.h" diff --git a/Source/Installer/zh-Hant.lproj/MainMenu.xib b/Source/Installer/zh-Hant.lproj/MainMenu.xib index c9bcd182..8577bad5 100644 --- a/Source/Installer/zh-Hant.lproj/MainMenu.xib +++ b/Source/Installer/zh-Hant.lproj/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -81,17 +81,18 @@ + - + - + @@ -118,7 +119,7 @@ - - - + @@ -152,7 +153,7 @@ Gw - + @@ -181,20 +182,20 @@ Gw - + - + - + - +