673 lines
17 KiB
Objective-C
673 lines
17 KiB
Objective-C
//
|
|
// AppDelegate.m
|
|
// Test Application
|
|
//
|
|
// Created by Patrick Wardle on 9/10/16.
|
|
// Copyright (c) 2016 Objective-See. All rights reserved.
|
|
//
|
|
|
|
#import "Consts.h"
|
|
#import "Logging.h"
|
|
#import "Utilities.h"
|
|
#import "AppDelegate.h"
|
|
|
|
@interface AppDelegate ()
|
|
|
|
@property (weak) IBOutlet NSWindow *window;
|
|
@end
|
|
|
|
@implementation AppDelegate
|
|
|
|
@synthesize viewLogLabel;
|
|
@synthesize infoWindowController;
|
|
@synthesize aboutWindowController;
|
|
@synthesize rulesWindowController;
|
|
|
|
//center window
|
|
// ->also make front, init title bar, etc
|
|
-(void)awakeFromNib
|
|
{
|
|
//center
|
|
[self.window center];
|
|
|
|
//set button states
|
|
[self setButtonStates];
|
|
|
|
//make it key window
|
|
[self.window makeKeyAndOrderFront:self];
|
|
|
|
//make window front
|
|
[NSApp activateIgnoringOtherApps:YES];
|
|
|
|
//set title
|
|
self.window.title = [NSString stringWithFormat:@"OverSight Preferences (v. %@)", getAppVersion()];
|
|
|
|
//make log link clickable
|
|
makeTextViewHyperlink(self.viewLogLabel, [NSURL fileURLWithPath:logFilePath()]);
|
|
|
|
return;
|
|
}
|
|
|
|
//app interface
|
|
// ->init user interface
|
|
-(void)applicationDidFinishLaunching:(NSNotification *)aNotification
|
|
{
|
|
//preferences
|
|
NSDictionary* preferences = nil;
|
|
|
|
//dbg msg
|
|
#ifdef DEBUG
|
|
logMsg(LOG_DEBUG, @"OverSight Preferences App Launched");
|
|
#endif
|
|
|
|
//register for hotkey presses
|
|
// ->for now, just cmd+q to quit app
|
|
[self registerKeypressHandler];
|
|
|
|
//create default prefs if there aren't any
|
|
// ->should only happen if new user runs the app
|
|
if(YES != [[NSFileManager defaultManager] fileExistsAtPath:[APP_PREFERENCES stringByExpandingTildeInPath]])
|
|
{
|
|
//dbg msg
|
|
#ifdef DEBUG
|
|
logMsg(LOG_DEBUG, @"preference file not found; manually creating");
|
|
#endif
|
|
|
|
//write em out
|
|
// ->note; set 'start at login' to false, since no prefs here, mean installer wasn't run (user can later toggle)
|
|
[@{PREF_LOG_ACTIVITY:@YES, PREF_START_AT_LOGIN:@NO, PREF_RUN_HEADLESS:@NO, PREF_DISABLE_INACTIVE:@NO, PREF_CHECK_4_UPDATES:@YES} writeToFile:[APP_PREFERENCES stringByExpandingTildeInPath] atomically:NO];
|
|
}
|
|
|
|
//load preferences
|
|
preferences = [NSMutableDictionary dictionaryWithContentsOfFile:[APP_PREFERENCES stringByExpandingTildeInPath]];
|
|
|
|
//when logging is enabled
|
|
// ->open/create log file
|
|
if(YES == [preferences[PREF_LOG_ACTIVITY] boolValue])
|
|
{
|
|
//init
|
|
if(YES != initLogging())
|
|
{
|
|
//err msg
|
|
logMsg(LOG_ERR, @"failed to init logging");
|
|
}
|
|
}
|
|
|
|
//start login item in background
|
|
// ->checks if already running though
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
|
|
^{
|
|
//start
|
|
// -> 'NO' means don't start if already running
|
|
[self startLoginItem:NO args:nil];
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
//automatically close when user closes window
|
|
-(BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
//set button states from preferences
|
|
-(void)setButtonStates
|
|
{
|
|
//preferences
|
|
NSDictionary* preferences = nil;
|
|
|
|
//load preferences
|
|
preferences = [NSDictionary dictionaryWithContentsOfFile:[APP_PREFERENCES stringByExpandingTildeInPath]];
|
|
|
|
//set 'log activity' button state
|
|
self.logActivity.state = [preferences[PREF_LOG_ACTIVITY] boolValue];
|
|
|
|
//set 'start at login' button state
|
|
self.startAtLogin.state = [preferences[PREF_START_AT_LOGIN] boolValue];
|
|
|
|
//set 'run headless' button state
|
|
self.runHeadless.state = [preferences[PREF_RUN_HEADLESS] boolValue];
|
|
|
|
//set 'disable inactive' button state
|
|
self.disableInactive.state = [preferences[PREF_DISABLE_INACTIVE] boolValue];
|
|
|
|
//set 'automatically check for updates' button state
|
|
self.check4Updates.state = [preferences[PREF_CHECK_4_UPDATES] boolValue];
|
|
|
|
return;
|
|
}
|
|
|
|
//register handler for hot keys
|
|
// ->for now, it just handles cmd+q to quit
|
|
-(void)registerKeypressHandler
|
|
{
|
|
//event handler
|
|
NSEvent* (^keypressHandler)(NSEvent *) = nil;
|
|
|
|
//init handler block
|
|
// ->just call helper function
|
|
keypressHandler = ^NSEvent * (NSEvent * theEvent){
|
|
|
|
//invoke helper
|
|
return [self handleKeypress:theEvent];
|
|
};
|
|
|
|
//register for key-down events
|
|
[NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:keypressHandler];
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
//helper function for keypresses
|
|
// ->for now, only handle cmd+q, to quit
|
|
-(NSEvent*)handleKeypress:(NSEvent*)event
|
|
{
|
|
//flag indicating event was handled
|
|
BOOL wasHandled = NO;
|
|
|
|
//only care about 'cmd' + something
|
|
if(NSCommandKeyMask != (event.modifierFlags & NSCommandKeyMask))
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//handle key-code
|
|
// command+q: quite
|
|
switch ([event keyCode])
|
|
{
|
|
//'q' (quit)
|
|
case KEYCODE_Q:
|
|
|
|
//bye!
|
|
[[NSApplication sharedApplication] terminate:nil];
|
|
|
|
//set flag
|
|
wasHandled = YES;
|
|
|
|
break;
|
|
|
|
//default
|
|
// ->do nothing
|
|
default:
|
|
|
|
break;
|
|
}
|
|
|
|
//bail
|
|
bail:
|
|
|
|
//nil out event if it was handled
|
|
if(YES == wasHandled)
|
|
{
|
|
//nil
|
|
event = nil;
|
|
}
|
|
|
|
return event;
|
|
|
|
}
|
|
|
|
//toggle/set preferences
|
|
-(IBAction)togglePreference:(NSButton *)sender
|
|
{
|
|
//preferences
|
|
NSMutableDictionary* preferences = nil;
|
|
|
|
//path to login item
|
|
NSURL* loginItem = nil;
|
|
|
|
//load preferences
|
|
preferences = [NSMutableDictionary dictionaryWithContentsOfFile:[APP_PREFERENCES stringByExpandingTildeInPath]];
|
|
|
|
//set 'log activity' button
|
|
// ->also start/stop logging based on button state
|
|
if(sender == self.logActivity)
|
|
{
|
|
//set
|
|
preferences[PREF_LOG_ACTIVITY] = [NSNumber numberWithBool:[sender state]];
|
|
|
|
//when logging is enabled
|
|
// ->open/create log file
|
|
if(YES == [preferences[PREF_LOG_ACTIVITY] boolValue])
|
|
{
|
|
//init
|
|
if(YES != initLogging())
|
|
{
|
|
//err msg
|
|
logMsg(LOG_ERR, @"failed to init logging");
|
|
}
|
|
//happy
|
|
// ->log msg
|
|
else
|
|
{
|
|
//log msg
|
|
logMsg(LOG_DEBUG|LOG_TO_FILE, @"logging initialized (main app)");
|
|
}
|
|
}
|
|
//when logging is disabled
|
|
// ->close out the log file
|
|
else
|
|
{
|
|
//log msg
|
|
logMsg(LOG_DEBUG|LOG_TO_FILE, @"logging deinitialized (main app)");
|
|
|
|
//close
|
|
deinitLogging();
|
|
}
|
|
}
|
|
|
|
//set 'automatically check for updates'
|
|
else if(sender == self.check4Updates)
|
|
{
|
|
//set
|
|
preferences[PREF_CHECK_4_UPDATES] = [NSNumber numberWithBool:[sender state]];
|
|
}
|
|
|
|
//set 'start at login'
|
|
// ->then also toggle for current user
|
|
else if(sender == self.startAtLogin)
|
|
{
|
|
//set
|
|
preferences[PREF_START_AT_LOGIN] = [NSNumber numberWithBool:[sender state]];
|
|
|
|
//init path to login item
|
|
loginItem = [NSURL fileURLWithPath:[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"/Contents/Library/LoginItems/OverSight Helper.app"]];
|
|
|
|
//toggle
|
|
if(YES != toggleLoginItem(loginItem, (int)[sender state]))
|
|
{
|
|
//err msg
|
|
logMsg(LOG_ERR, [NSString stringWithFormat:@"failed to toggle login item: %@", loginItem]);
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
}
|
|
|
|
//set 'run in headless mode'
|
|
// ->then restart login item to realize this
|
|
else if(sender == self.runHeadless)
|
|
{
|
|
//set
|
|
preferences[PREF_RUN_HEADLESS] = [NSNumber numberWithBool:[sender state]];
|
|
|
|
//save em now so new instance of login item can read them
|
|
[preferences writeToFile:[APP_PREFERENCES stringByExpandingTildeInPath] atomically:YES];
|
|
|
|
//restart login item in background
|
|
// ->will read prefs, and run in headless mode
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
|
|
^{
|
|
//start
|
|
[self startLoginItem:YES args:nil];
|
|
});
|
|
}
|
|
|
|
//set 'disable inactive alerts'
|
|
else if(sender == self.disableInactive)
|
|
{
|
|
//set
|
|
preferences[PREF_DISABLE_INACTIVE] = [NSNumber numberWithBool:[sender state]];
|
|
}
|
|
|
|
//save em
|
|
[preferences writeToFile:[APP_PREFERENCES stringByExpandingTildeInPath] atomically:YES];
|
|
|
|
//bail
|
|
bail:
|
|
|
|
return;
|
|
}
|
|
|
|
//'about' button/menu handler
|
|
-(IBAction)about:(id)sender
|
|
{
|
|
//alloc/init settings window
|
|
if(nil == self.aboutWindowController)
|
|
{
|
|
//alloc/init
|
|
aboutWindowController = [[AboutWindowController alloc] initWithWindowNibName:@"AboutWindow"];
|
|
}
|
|
|
|
//center window
|
|
[[self.aboutWindowController window] center];
|
|
|
|
//show it
|
|
[self.aboutWindowController showWindow:self];
|
|
|
|
//invoke function in background that will make window modal
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
|
|
//make modal
|
|
makeModal(self.aboutWindowController);
|
|
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
//'check for update' (now) button handler
|
|
-(IBAction)check4Update:(id)sender
|
|
{
|
|
//disable button
|
|
self.check4UpdatesNow.enabled = NO;
|
|
|
|
//reset
|
|
self.versionLabel.stringValue = @"";
|
|
|
|
//re-draw
|
|
[self.versionLabel displayIfNeeded];
|
|
|
|
//show spinner
|
|
[self.spinner startAnimation:self];
|
|
|
|
//check for update
|
|
[self isThereAnUpdate];
|
|
|
|
return;
|
|
}
|
|
|
|
//check for an update
|
|
-(void)isThereAnUpdate
|
|
{
|
|
//version string
|
|
NSMutableString* versionString = nil;
|
|
|
|
//alloc string
|
|
versionString = [NSMutableString string];
|
|
|
|
//dbg msg
|
|
#ifdef DEBUG
|
|
logMsg(LOG_DEBUG, @"checking for new version");
|
|
#endif
|
|
|
|
//check if available version is newer
|
|
// ->show update popup/window
|
|
if(YES == isNewVersion(versionString))
|
|
{
|
|
//dbg msg
|
|
#ifdef DEBUG
|
|
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"a new version (%@) is available", versionString]);
|
|
#endif
|
|
|
|
//hide version message
|
|
self.versionLabel.hidden = YES;
|
|
|
|
//alloc/init about window
|
|
infoWindowController = [[InfoWindowController alloc] initWithWindowNibName:@"InfoWindow"];
|
|
|
|
//configure
|
|
[self.infoWindowController configure:[NSString stringWithFormat:@"a new version (%@) is available!", versionString] buttonTitle:@"update"];
|
|
|
|
//center window
|
|
[[self.infoWindowController window] center];
|
|
|
|
//show it
|
|
[self.infoWindowController showWindow:self];
|
|
|
|
//invoke function in background that will make window modal
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
|
|
//make modal
|
|
makeModal(self.infoWindowController);
|
|
|
|
});
|
|
|
|
//stop/hide spinner
|
|
[self.spinner stopAnimation:self];
|
|
|
|
//re-enable button
|
|
self.check4UpdatesNow.enabled = YES;
|
|
}
|
|
|
|
//no new version
|
|
// ->stop animations, etc
|
|
else
|
|
{
|
|
//dbg msg
|
|
#ifdef DEBUG
|
|
logMsg(LOG_DEBUG, @"no updates available");
|
|
#endif
|
|
|
|
//stop/hide spinner
|
|
[self.spinner stopAnimation:self];
|
|
|
|
//re-enable button
|
|
self.check4UpdatesNow.enabled = YES;
|
|
|
|
//show now new version message
|
|
self.versionLabel.hidden = NO;
|
|
|
|
//set message
|
|
self.versionLabel.stringValue = @"no new versions";
|
|
|
|
//re-draw
|
|
[self.versionLabel displayIfNeeded];
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//(re)start the login item
|
|
-(void)startLoginItem:(BOOL)shouldRestart args:(NSArray*)args
|
|
{
|
|
//path to login item
|
|
NSString* loginItem = nil;
|
|
|
|
//login item's pid
|
|
pid_t loginItemPID = -1;
|
|
|
|
//error
|
|
NSError* error = nil;
|
|
|
|
//config (args, etc)
|
|
// ->can't be nil, so init to blank here
|
|
NSDictionary* configuration = @{};
|
|
|
|
//get pid of login item for user
|
|
loginItemPID = getProcessID(@"OverSight Helper", getuid());
|
|
|
|
//no need to start if already running
|
|
// ->well, and if 'shouldRestart' is not set
|
|
if( (-1 != loginItemPID) &&
|
|
(YES != shouldRestart) )
|
|
{
|
|
//dbg msg
|
|
#ifdef DEBUG
|
|
logMsg(LOG_DEBUG, @"login item already running and 'shouldRestart' not set, so no need to start it!");
|
|
#endif
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//running?
|
|
// ->kill
|
|
else if(-1 != loginItemPID)
|
|
{
|
|
//kill it
|
|
kill(loginItemPID, SIGTERM);
|
|
|
|
//sleep
|
|
[NSThread sleepForTimeInterval:1.0f];
|
|
|
|
//really kill
|
|
kill(loginItemPID, SIGKILL);
|
|
}
|
|
|
|
//dbg msg
|
|
#ifdef DEBUG
|
|
logMsg(LOG_DEBUG, @"starting login item");
|
|
#endif
|
|
|
|
//add overlay
|
|
[self addOverlay:shouldRestart];
|
|
|
|
//init path to login item
|
|
loginItem = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"/Contents/Library/LoginItems/OverSight Helper.app"];
|
|
|
|
//any args?
|
|
// ->init config with them args
|
|
if(nil != args)
|
|
{
|
|
//add args
|
|
configuration = @{NSWorkspaceLaunchConfigurationArguments:args};
|
|
}
|
|
|
|
//dbg msg
|
|
#ifdef DEBUG
|
|
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"starting login item with: %@/%@", configuration, args]);
|
|
#endif
|
|
|
|
//launch it
|
|
[[NSWorkspace sharedWorkspace] launchApplicationAtURL:[NSURL fileURLWithPath:loginItem] options:NSWorkspaceLaunchWithoutActivation configuration:configuration error:&error];
|
|
|
|
//remove overlay
|
|
[self removeOverlay];
|
|
|
|
//check if login launch was ok
|
|
// ->do down here, since always want to remove overlay
|
|
if(nil != error)
|
|
{
|
|
//err msg
|
|
logMsg(LOG_ERR, [NSString stringWithFormat:@"failed to start login item, %@/%@", loginItem, error]);
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//bail
|
|
bail:
|
|
|
|
return;
|
|
}
|
|
|
|
//add overlay to main window
|
|
-(void)addOverlay:(BOOL)restarting
|
|
{
|
|
//show overlay view on main thread
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
//frame
|
|
NSRect frame = {0};
|
|
|
|
//pre-req
|
|
[self.overlay setWantsLayer:YES];
|
|
|
|
//get main window's frame
|
|
frame = self.window.contentView.frame;
|
|
|
|
//set origin to 0/0
|
|
frame.origin = CGPointZero;
|
|
|
|
//tweak since window is rounded
|
|
// ->and adding this view doesn't get rounded?
|
|
frame.origin.y += 1;
|
|
frame.origin.x += 1;
|
|
frame.size.width -= 2;
|
|
|
|
//update overlay to take up entire window
|
|
self.overlay.frame = frame;
|
|
|
|
//set overlay's view color to white
|
|
self.overlay.layer.backgroundColor = [NSColor whiteColor].CGColor;
|
|
|
|
//make it semi-transparent
|
|
self.overlay.alphaValue = 0.85;
|
|
|
|
//set start message
|
|
if(YES != restarting)
|
|
{
|
|
//set
|
|
self.statusMessage.stringValue = @"starting monitor...";
|
|
}
|
|
//set restart message
|
|
else
|
|
{
|
|
//set
|
|
self.statusMessage.stringValue = @"(re)starting monitor...";
|
|
}
|
|
|
|
//show message
|
|
self.statusMessage.hidden = NO;
|
|
|
|
//show spinner
|
|
self.progressIndicator.hidden = NO;
|
|
|
|
//animate it
|
|
[self.progressIndicator startAnimation:nil];
|
|
|
|
//add to main window
|
|
[self.window.contentView addSubview:self.overlay];
|
|
|
|
//show
|
|
self.overlay.hidden = NO;
|
|
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
//remove overlay from main window
|
|
-(void)removeOverlay
|
|
{
|
|
//sleep to give message more viewing time
|
|
[NSThread sleepForTimeInterval:2];
|
|
|
|
//remove overlay view on main thread
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
//hide spinner
|
|
self.progressIndicator.hidden = YES;
|
|
|
|
//hide view
|
|
self.overlay.hidden = YES;
|
|
|
|
//hide message
|
|
self.statusMessage.hidden = YES;
|
|
|
|
//remove
|
|
[self.overlay removeFromSuperview];
|
|
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
-(IBAction)showLog:(id)sender
|
|
{
|
|
return;
|
|
}
|
|
|
|
|
|
//button handle when user clicks 'Manage Rules'
|
|
// ->just shwo the rules window
|
|
-(IBAction)manageRules:(id)sender
|
|
{
|
|
//alloc
|
|
rulesWindowController = [[RulesWindowController alloc] initWithWindowNibName:@"Rules"];
|
|
|
|
//center window
|
|
[[self.rulesWindowController window] center];
|
|
|
|
//show it
|
|
[self.rulesWindowController showWindow:self];
|
|
|
|
return;
|
|
}
|
|
|
|
-(NSAttributedString *)stringFromHTML:(NSString *)html withFont:(NSFont *)font
|
|
{
|
|
if (!font) font = [NSFont systemFontOfSize:0.0]; // Default font
|
|
html = [NSString stringWithFormat:@"<span style=\"font-family:'%@'; font-size:%dpx;\">%@</span>", [font fontName], (int)[font pointSize], html];
|
|
NSData *data = [html dataUsingEncoding:NSUTF8StringEncoding];
|
|
NSAttributedString* string = [[NSAttributedString alloc] initWithHTML:data documentAttributes:nil];
|
|
return string;
|
|
}
|
|
|
|
|
|
@end
|