600 lines
15 KiB
Objective-C
600 lines
15 KiB
Objective-C
//
|
|
// file: ConfigureWindowController.m
|
|
// project: OverSight (config)
|
|
// description: install/uninstall window logic
|
|
//
|
|
// created by Patrick Wardle
|
|
// copyright (c) 2018 Objective-See. All rights reserved.
|
|
//
|
|
|
|
@import OSLog;
|
|
|
|
#import "consts.h"
|
|
#import "Configure.h"
|
|
#import "utilities.h"
|
|
#import "AppDelegate.h"
|
|
#import "ConfigureWindowController.h"
|
|
|
|
/* GLOBALS */
|
|
|
|
//log handle
|
|
extern os_log_t logHandle;
|
|
|
|
@implementation ConfigureWindowController
|
|
|
|
@synthesize statusMsg;
|
|
@synthesize configureObj;
|
|
@synthesize moreInfoButton;
|
|
@synthesize appActivationObserver;
|
|
|
|
//automatically called when nib is loaded
|
|
// just center window, alloc some objs, etc
|
|
-(void)awakeFromNib
|
|
{
|
|
//center
|
|
[self.window center];
|
|
|
|
//when supported
|
|
// indicate title bar is transparent (too)
|
|
if(YES == [self.window respondsToSelector:@selector(titlebarAppearsTransparent)])
|
|
{
|
|
//set transparency
|
|
self.window.titlebarAppearsTransparent = YES;
|
|
}
|
|
|
|
//make first responder
|
|
// calling this without a timeout sometimes fails :/
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (100 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
|
|
|
|
//and make it first responder
|
|
[self.window makeFirstResponder:self.installButton];
|
|
|
|
});
|
|
|
|
//init configure object
|
|
if(nil == self.configureObj)
|
|
{
|
|
//alloc/init Config obj
|
|
configureObj = [[Configure alloc] init];
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//configure window/buttons
|
|
// also brings window to front
|
|
-(void)configure
|
|
{
|
|
//flag
|
|
BOOL isInstalled = NO;
|
|
|
|
//init flag
|
|
isInstalled = [self.configureObj isInstalled];
|
|
|
|
//set window title
|
|
[self window].title = [NSString stringWithFormat:@"version %@", getAppVersion()];
|
|
|
|
//init status msg
|
|
[self.statusMsg setStringValue:@"...protects your webcam & microphone!"];
|
|
|
|
//uninstall via app?
|
|
// just enable uinstall button
|
|
if(YES == [NSProcessInfo.processInfo.arguments containsObject:CMD_UNINSTALL_VIA_UI])
|
|
{
|
|
//enable uninstall
|
|
self.uninstallButton.enabled = YES;
|
|
|
|
//disable install
|
|
self.installButton.enabled = NO;
|
|
|
|
//make uninstall button first responder
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (100 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
|
|
|
|
//set first responder
|
|
[self.window makeFirstResponder:self.uninstallButton];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
//app already installed?
|
|
// enable 'uninstall' button
|
|
// change 'install' button to say 'upgrade'
|
|
else if(YES == isInstalled)
|
|
{
|
|
//enable 'uninstall'
|
|
self.uninstallButton.enabled = YES;
|
|
|
|
//set to 'upgrade'
|
|
self.installButton.title = ACTION_UPGRADE;
|
|
}
|
|
|
|
//otherwise disable
|
|
else
|
|
{
|
|
//disable
|
|
self.uninstallButton.enabled = NO;
|
|
}
|
|
|
|
//set delegate
|
|
[self.window setDelegate:self];
|
|
|
|
return;
|
|
}
|
|
|
|
//display (show) window
|
|
// center, make front, set bg to white, etc
|
|
-(void)display
|
|
{
|
|
//center window
|
|
[[self window] center];
|
|
|
|
//show (now configured) windows
|
|
[self showWindow:self];
|
|
|
|
//make it key window
|
|
[self.window makeKeyAndOrderFront:self];
|
|
|
|
//make window front
|
|
[NSApp activateIgnoringOtherApps:YES];
|
|
|
|
//not in dark mode?
|
|
// make window white
|
|
if(YES != isDarkMode())
|
|
{
|
|
//make white
|
|
self.window.backgroundColor = NSColor.whiteColor;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//button handler for configure window
|
|
// install/uninstall/close logic
|
|
-(IBAction)configureButtonHandler:(id)sender
|
|
{
|
|
//action
|
|
NSInteger action = 0;
|
|
|
|
//app path
|
|
NSURL* appPath = nil;
|
|
|
|
//app config
|
|
NSWorkspaceOpenConfiguration* configuration = nil;
|
|
|
|
//grab tag
|
|
action = ((NSButton*)sender).tag;
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "handling action click: %{public}@ (tag: %ld)", ((NSButton*)sender).title, (long)action);
|
|
|
|
//process button
|
|
switch(action)
|
|
{
|
|
//install/uninstall
|
|
case ACTION_INSTALL_FLAG:
|
|
case ACTION_UNINSTALL_FLAG:
|
|
{
|
|
//disable 'x' button
|
|
// don't want user killing app during install/upgrade
|
|
[[self.window standardWindowButton:NSWindowCloseButton] setEnabled:NO];
|
|
|
|
//clear status msg
|
|
self.statusMsg.stringValue = @"";
|
|
|
|
//force redraw of status msg
|
|
// sometime doesn't refresh (e.g. slow VM)
|
|
self.statusMsg.needsDisplay = YES;
|
|
|
|
//invoke logic to install/uninstall
|
|
// do in background so UI doesn't block
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
|
|
^{
|
|
//install/uninstall
|
|
[self lifeCycleEvent:action];
|
|
});
|
|
|
|
break;
|
|
}
|
|
|
|
//show 'support' view
|
|
case ACTION_SHOW_SUPPORT:
|
|
{
|
|
//dbg msg
|
|
os_log_debug(logHandle, "showing 'support' view");
|
|
|
|
//show view
|
|
[self showView:self.supportView firstResponder:self.supportButton];
|
|
|
|
//unset window title
|
|
self.window.title = @"";
|
|
|
|
break;
|
|
}
|
|
|
|
//support, yes!
|
|
case ACTION_SUPPORT:
|
|
|
|
//open URL
|
|
// invokes user's default browser
|
|
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:PATREON_URL]];
|
|
|
|
//fall thru as we want to launch app and terminate
|
|
|
|
//close
|
|
// on non-error, launch login item
|
|
case ACTION_CLOSE_FLAG:
|
|
{
|
|
//coming from support view?
|
|
// launch helper/login item
|
|
if(YES == self.supportView.window.isVisible)
|
|
{
|
|
//init app path
|
|
appPath = [NSURL fileURLWithPath:[@"/Applications" stringByAppendingPathComponent:APP_NAME]];
|
|
|
|
//alloc configuration
|
|
configuration = [[NSWorkspaceOpenConfiguration alloc] init];
|
|
|
|
//set args
|
|
configuration.arguments = @[INITIAL_LAUNCH];
|
|
|
|
//unset recent
|
|
configuration.addsToRecentItems = NO;
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "now launching: %{public}@", appPath.path);
|
|
|
|
//launch it
|
|
[NSWorkspace.sharedWorkspace openApplicationAtURL:appPath configuration:configuration completionHandler:^(NSRunningApplication * _Nullable application, NSError * _Nullable error)
|
|
{
|
|
#pragma unused(application)
|
|
|
|
//error?
|
|
if(nil != error)
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to launch %{public}@ (error: %{public}@)", appPath, error);
|
|
}
|
|
|
|
//close window
|
|
// triggers cleanup logic
|
|
dispatch_sync(dispatch_get_main_queue(),
|
|
^{
|
|
[self.window close];
|
|
});
|
|
|
|
}];
|
|
}
|
|
|
|
else
|
|
{
|
|
//close window
|
|
// triggers cleanup logic
|
|
[self.window close];
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
//default
|
|
default:
|
|
|
|
break;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//show view
|
|
// adds to main window, resizes, etc
|
|
-(void)showView:(NSView*)view firstResponder:(NSButton*)firstResponder
|
|
{
|
|
//not in dark mode?
|
|
// make window white
|
|
if(YES != isDarkMode())
|
|
{
|
|
//set white
|
|
view.layer.backgroundColor = [NSColor whiteColor].CGColor;
|
|
}
|
|
|
|
//set content view size
|
|
self.window.contentSize = view.frame.size;
|
|
|
|
//update config view
|
|
self.window.contentView = view;
|
|
|
|
//make 'next' button first responder
|
|
// calling this without a timeout, sometimes fails :/
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (100 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
|
|
|
|
//set first responder
|
|
[self.window makeFirstResponder:firstResponder];
|
|
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
//button handler for '?' button (on an error)
|
|
// load objective-see's documentation for error(s) in default browser
|
|
-(IBAction)info:(id)sender
|
|
{
|
|
#pragma unused(sender)
|
|
|
|
//open URL
|
|
// invokes user's default browser
|
|
[NSWorkspace.sharedWorkspace openURL:[NSURL URLWithString:ERRORS_URL]];
|
|
|
|
return;
|
|
}
|
|
|
|
//perform install | uninstall via Control obj
|
|
// invoked on background thread so that UI doesn't block
|
|
-(void)lifeCycleEvent:(NSInteger)event
|
|
{
|
|
//status var
|
|
BOOL status = NO;
|
|
|
|
//begin event
|
|
// updates ui on main thread
|
|
dispatch_sync(dispatch_get_main_queue(),
|
|
^{
|
|
//begin
|
|
[self beginEvent:event];
|
|
});
|
|
|
|
//in background
|
|
// perform action (install | uninstall)
|
|
status = [self.configureObj configure:event];
|
|
|
|
//complete event
|
|
// updates ui on main thread
|
|
dispatch_async(dispatch_get_main_queue(),
|
|
^{
|
|
//complete
|
|
[self completeEvent:status event:event];
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
//begin event
|
|
// basically just update UI
|
|
-(void)beginEvent:(NSInteger)event
|
|
{
|
|
//status msg frame
|
|
CGRect statusMsgFrame;
|
|
|
|
//grab exiting frame
|
|
statusMsgFrame = self.statusMsg.frame;
|
|
|
|
//avoid activity indicator
|
|
// shift frame shift delta
|
|
statusMsgFrame.origin.x += FRAME_SHIFT;
|
|
|
|
//update frame to align
|
|
self.statusMsg.frame = statusMsgFrame;
|
|
|
|
//align text left
|
|
self.statusMsg.alignment = NSTextAlignmentLeft;
|
|
|
|
//observe app activation
|
|
// allows workaround where process indicator stops
|
|
self.appActivationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSWorkspaceDidActivateApplicationNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification)
|
|
{
|
|
#pragma unused(notification)
|
|
|
|
//show spinner
|
|
self.activityIndicator.hidden = NO;
|
|
|
|
//start spinner
|
|
[self.activityIndicator startAnimation:nil];
|
|
|
|
}];
|
|
|
|
//install msg
|
|
if(ACTION_INSTALL_FLAG == event)
|
|
{
|
|
//update status msg
|
|
[self.statusMsg setStringValue:@"Installing..."];
|
|
}
|
|
//uninstall msg
|
|
else
|
|
{
|
|
//update status msg
|
|
[self.statusMsg setStringValue:@"Uninstalling..."];
|
|
}
|
|
|
|
//disable action button
|
|
self.uninstallButton.enabled = NO;
|
|
|
|
//disable cancel button
|
|
self.installButton.enabled = NO;
|
|
|
|
//show spinner
|
|
self.activityIndicator.hidden = NO;
|
|
|
|
//start spinner
|
|
[self.activityIndicator startAnimation:nil];
|
|
|
|
return;
|
|
}
|
|
|
|
//complete event
|
|
// update UI after background event has finished
|
|
-(void)completeEvent:(BOOL)success event:(NSInteger)event
|
|
{
|
|
//status msg frame
|
|
CGRect statusMsgFrame;
|
|
|
|
//action
|
|
NSString* action = nil;
|
|
|
|
//result msg
|
|
NSMutableString* resultMsg = nil;
|
|
|
|
//msg font
|
|
NSColor* resultMsgColor = nil;
|
|
|
|
//remove app activation observer
|
|
if(nil != self.appActivationObserver)
|
|
{
|
|
//remove
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self.appActivationObserver];
|
|
|
|
//unset
|
|
self.appActivationObserver = nil;
|
|
}
|
|
|
|
//set action msg for install
|
|
if(ACTION_INSTALL_FLAG == event)
|
|
{
|
|
//set msg
|
|
action = @"install";
|
|
}
|
|
//set action msg for uninstall
|
|
else
|
|
{
|
|
//set msg
|
|
action = @"uninstall";
|
|
}
|
|
|
|
//success
|
|
if(YES == success)
|
|
{
|
|
//set result msg
|
|
resultMsg = [NSMutableString stringWithFormat:@"☑️ %@: %@ed!\n", PRODUCT_NAME, action];
|
|
}
|
|
//failure
|
|
else
|
|
{
|
|
//set result msg
|
|
resultMsg = [NSMutableString stringWithFormat:@"⚠️ Error: %@ failed", action];
|
|
|
|
//show 'get more info' button
|
|
self.moreInfoButton.hidden = NO;
|
|
}
|
|
|
|
//stop/hide spinner
|
|
[self.activityIndicator stopAnimation:nil];
|
|
|
|
//hide spinner
|
|
self.activityIndicator.hidden = YES;
|
|
|
|
//grab exiting frame
|
|
statusMsgFrame = self.statusMsg.frame;
|
|
|
|
//shift back since activity indicator is gone
|
|
statusMsgFrame.origin.x -= FRAME_SHIFT;
|
|
|
|
//update frame to align
|
|
self.statusMsg.frame = statusMsgFrame;
|
|
|
|
//set font to bold
|
|
self.statusMsg.font = [NSFont fontWithName:@"Menlo-Bold" size:13];
|
|
|
|
//set msg color
|
|
self.statusMsg.textColor = resultMsgColor;
|
|
|
|
//set status msg
|
|
self.statusMsg.stringValue = resultMsg;
|
|
|
|
//install success?
|
|
// set button title & tag for 'next'
|
|
if( (YES == success) &&
|
|
(ACTION_INSTALL_FLAG == event) )
|
|
{
|
|
//next
|
|
self.installButton.title = ACTION_NEXT;
|
|
|
|
//set tag
|
|
self.installButton.tag = ACTION_SHOW_SUPPORT;
|
|
}
|
|
//otherwise
|
|
// set button and tag for close/exit
|
|
else
|
|
{
|
|
//close
|
|
self.installButton.title = ACTION_CLOSE;
|
|
|
|
//update it's tag
|
|
// will allow button handler method process
|
|
self.installButton.tag = ACTION_CLOSE_FLAG;
|
|
|
|
//(re)enable 'x' button
|
|
[[self.window standardWindowButton:NSWindowCloseButton] setEnabled:YES];
|
|
}
|
|
|
|
//enable
|
|
self.installButton.enabled = YES;
|
|
|
|
//...and highlighted
|
|
[self.window makeFirstResponder:self.installButton];
|
|
|
|
//(re)make window window key
|
|
[self.window makeKeyAndOrderFront:self];
|
|
|
|
//(re)make window front
|
|
[NSApp activateIgnoringOtherApps:YES];
|
|
|
|
return;
|
|
}
|
|
|
|
//perform any cleanup/termination
|
|
// for now, just call into Config obj to remove helper
|
|
-(BOOL)cleanup
|
|
{
|
|
//flag
|
|
BOOL cleanedUp = NO;
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "cleaning up...");
|
|
|
|
//remove helper
|
|
if(YES != [self.configureObj removeHelper])
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to remove config helper");
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//happy
|
|
cleanedUp = YES;
|
|
|
|
bail:
|
|
|
|
return cleanedUp;
|
|
}
|
|
|
|
//automatically invoked when window is closing
|
|
// perform cleanup logic, then manually terminate app
|
|
-(void)windowWillClose:(NSNotification *)notification
|
|
{
|
|
#pragma unused(notification)
|
|
|
|
//cleanup in background
|
|
// then exit application
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
|
|
^{
|
|
//cleanup
|
|
[self cleanup];
|
|
|
|
//exit on main thread
|
|
dispatch_async(dispatch_get_main_queue(),
|
|
^{
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "%{public}@ exiting...", [NSProcessInfo.processInfo.arguments.firstObject lastPathComponent]);
|
|
|
|
//exit
|
|
[NSApp terminate:self];
|
|
});
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
@end
|