mirror of
https://github.com/objective-see/TaskExplorer.git
synced 2026-03-22 07:02:39 +00:00
v2.2.0 friends updates removed error reporting removed old code (pre-macOS 11) ...more changes / improvements soon!
2480 lines
65 KiB
Objective-C
Executable File
2480 lines
65 KiB
Objective-C
Executable File
//
|
|
// AppDelegate.m
|
|
// TaskExplorer
|
|
//
|
|
|
|
|
|
#import <syslog.h>
|
|
|
|
#import "Task.h"
|
|
#import "Consts.h"
|
|
#import "Binary.h"
|
|
#import "Update.h"
|
|
#import "Utilities.h"
|
|
#import "Connection.h"
|
|
#import "AppDelegate.h"
|
|
#import "serviceInterface.h"
|
|
#import "TaskTableController.h"
|
|
#import "RequestRootWindowController.h"
|
|
|
|
//TODO: autolayout vertically
|
|
|
|
@implementation AppDelegate
|
|
|
|
@synthesize topPane;
|
|
@synthesize filterObj;
|
|
@synthesize vtThreads;
|
|
@synthesize bottomPane;
|
|
@synthesize saveButton;
|
|
@synthesize currentTask;
|
|
@synthesize searchButton;
|
|
@synthesize viewSelector;
|
|
@synthesize taskViewFormat;
|
|
@synthesize commandHandling;
|
|
@synthesize completePosting;
|
|
@synthesize filteringMessage;
|
|
@synthesize filteringOverlay;
|
|
@synthesize customItemsFilter;
|
|
@synthesize customTasksFilter;
|
|
@synthesize progressIndicator;
|
|
@synthesize filteringIndicator;
|
|
@synthesize taskTableController;
|
|
@synthesize bottomViewController;
|
|
@synthesize aboutWindowController;
|
|
@synthesize searchWindowController;
|
|
@synthesize updateWindowController;
|
|
@synthesize resultsWindowController;
|
|
@synthesize flagItemsWindowController;
|
|
@synthesize requestRootWindowController;
|
|
|
|
//kick off main window/logic
|
|
-(void)taskExplore
|
|
{
|
|
//for autolayout
|
|
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"NSConstraintBasedLayoutVisualizeMutuallyExclusiveConstraints"];
|
|
|
|
//init virus total object
|
|
virusTotal = [[VirusTotal alloc] init];
|
|
|
|
//init filter obj
|
|
filterObj = [[Filter alloc] init];
|
|
|
|
//alloc/init custom search field for tasks
|
|
customTasksFilter = [[CustomTextField alloc] init];
|
|
|
|
//set owner
|
|
self.customTasksFilter.owner = self;
|
|
|
|
//alloc/init custom search field for items
|
|
customItemsFilter = [[CustomTextField alloc] init];
|
|
|
|
//set owner
|
|
self.customItemsFilter.owner = self;
|
|
|
|
//set field editor for tasks
|
|
[self.customTasksFilter setFieldEditor:YES];
|
|
|
|
//set field editor for items
|
|
[self.customItemsFilter setFieldEditor:YES];
|
|
|
|
//center
|
|
[self.window center];
|
|
|
|
//first responder: filter tasks
|
|
[self.window makeFirstResponder:self.filterTasksBox];
|
|
|
|
//make main window front
|
|
[self.window makeKeyAndOrderFront:self];
|
|
|
|
//make app front
|
|
[NSApp activateIgnoringOtherApps:YES];
|
|
|
|
//check
|
|
[self check4Update:nil];
|
|
|
|
//register for hotkey presses
|
|
[self registerKeypressHandler];
|
|
|
|
//check if authenticated
|
|
// display authentication request if needed
|
|
if(YES != [self isAuthenticated])
|
|
{
|
|
//wait to allow app to become front
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, .33 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
|
|
|
|
//display auth popup
|
|
// will invoke 'go' method on successful auth
|
|
[self askForRoot];
|
|
|
|
});
|
|
}
|
|
|
|
//go!
|
|
// setup tracking areas and begin thread that explores tasks
|
|
else
|
|
{
|
|
//go!
|
|
[self go];
|
|
}
|
|
|
|
//set default top pane view to flat
|
|
self.taskViewFormat = FLAT_VIEW;
|
|
|
|
//set initial view for top pane
|
|
// ->default is flat (non-tree) view
|
|
[self changeViewController];
|
|
|
|
//alloc/init bottom pane controller
|
|
self.bottomViewController = [[TaskTableController alloc] initWithNibName:@"FlatView" bundle:nil];
|
|
|
|
//set flag
|
|
self.bottomViewController.isBottomPane = YES;
|
|
|
|
//disable autosize translations
|
|
self.bottomViewController.view.translatesAutoresizingMaskIntoConstraints = NO;
|
|
|
|
//add subview
|
|
[self.bottomPane addSubview:self.bottomViewController.view];
|
|
|
|
//set frame
|
|
[[self.bottomViewController view] setFrame:[self.bottomPane bounds]];
|
|
|
|
//constrain to parent
|
|
[self constrainView:self.bottomPane subView:self.bottomViewController.view];
|
|
|
|
//add 'items' not found msg
|
|
[self.bottomViewController.view addSubview:self.noItemsLabel];
|
|
|
|
//set delegate
|
|
// ->ensures our 'windowWillClose' method, which has logic to fully exit app
|
|
self.window.delegate = self;
|
|
|
|
return;
|
|
}
|
|
|
|
//automatically invoked by OS
|
|
// ->main entry point
|
|
-(void)applicationDidFinishLaunching:(NSNotification *)notification
|
|
{
|
|
|
|
//toggle away
|
|
[[[NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.loginwindow"] firstObject] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
|
|
|
|
//toggle back
|
|
// work-around for menu not showing since we set Application is agent(UIElement): YES
|
|
[[[NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.objective-see.TaskExplorer"] firstObject] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
|
|
|
|
|
|
//first time run?
|
|
// show thanks to friends window!
|
|
// note: on close, invokes method to show main window
|
|
if(YES != [[NSUserDefaults standardUserDefaults] boolForKey:NOT_FIRST_TIME])
|
|
{
|
|
//set key
|
|
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:NOT_FIRST_TIME];
|
|
|
|
//front
|
|
[self.friends makeKeyAndOrderFront:self];
|
|
|
|
//front
|
|
[NSApp activateIgnoringOtherApps: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.friends makeFirstResponder:[self.friends.contentView viewWithTag:1]];
|
|
|
|
});
|
|
|
|
//close after 3 seconds
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
|
|
|
|
//hide
|
|
[self hideFriends:nil];
|
|
|
|
});
|
|
}
|
|
//start task exploring
|
|
else
|
|
{
|
|
//show
|
|
[self taskExplore];
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//register handler for hot keys
|
|
-(void)registerKeypressHandler
|
|
{
|
|
//handler
|
|
NSEvent * (^keypressHandler)(NSEvent *) = nil;
|
|
|
|
//handler block
|
|
keypressHandler = ^NSEvent * (NSEvent * theEvent){
|
|
|
|
return [self handleKeypress:theEvent];
|
|
|
|
};
|
|
|
|
//register for key-down events
|
|
[NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:keypressHandler];
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
//invoked for any (but only) key-down events
|
|
-(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
|
|
// refresh (cmd+r)
|
|
// save (cmd+s)
|
|
// search (cmd+f)
|
|
// close window (cmd+w)
|
|
// info for selected task (cmd+i)
|
|
switch ([event keyCode])
|
|
{
|
|
//'r' (refresh)
|
|
case KEYCODE_R:
|
|
|
|
//refresh
|
|
[self refreshTasks:nil];
|
|
|
|
//set flag
|
|
wasHandled = YES;
|
|
|
|
break;
|
|
|
|
//'f' (find, search)
|
|
case KEYCODE_F:
|
|
|
|
//find
|
|
[self search:nil];
|
|
|
|
//set flag
|
|
wasHandled = YES;
|
|
|
|
break;
|
|
|
|
|
|
//'s' (save)
|
|
case KEYCODE_S:
|
|
|
|
//save
|
|
[self saveResults:nil];
|
|
|
|
//set flag
|
|
wasHandled = YES;
|
|
|
|
break;
|
|
|
|
//'w' (close window)
|
|
case KEYCODE_W:
|
|
|
|
//close
|
|
// ->if not main window
|
|
if(self.window != [[NSApplication sharedApplication] keyWindow])
|
|
{
|
|
//close window
|
|
[[[NSApplication sharedApplication] keyWindow] close];
|
|
|
|
//set flag
|
|
wasHandled = YES;
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
//bail
|
|
bail:
|
|
|
|
//nil out event if it was handled
|
|
if(YES == wasHandled)
|
|
{
|
|
event = nil;
|
|
}
|
|
|
|
// Return the event, a new event, or, to stop
|
|
// the event from being dispatched, nil
|
|
return event;
|
|
|
|
}
|
|
|
|
//hide friends view
|
|
// also shows/kicks off main window
|
|
- (IBAction)hideFriends:(id)sender
|
|
{
|
|
//once
|
|
static dispatch_once_t onceToken;
|
|
|
|
//close and launch main window
|
|
dispatch_once (&onceToken, ^{
|
|
|
|
//close
|
|
[self.friends close];
|
|
|
|
//start task exploring
|
|
[self taskExplore];
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
//complete a few inits
|
|
// ->then invoke helper method to start enum'ing task (in bg thread)
|
|
-(void)go
|
|
{
|
|
//init mouse-over areas
|
|
[self initTrackingAreas];
|
|
|
|
//go!
|
|
[self exploreTasks];
|
|
|
|
//bail
|
|
bail:
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
//check if app is auth'd
|
|
// ->specifically, if XPC service is setuid
|
|
-(BOOL)isAuthenticated
|
|
{
|
|
//flag
|
|
BOOL isAuthenticated = NO;
|
|
|
|
//path to XPC service
|
|
NSString* xpcService = nil;
|
|
|
|
//file attributes
|
|
NSDictionary* fileAttributes = nil;
|
|
|
|
//get path to XPC service
|
|
xpcService = getPath2XPC();
|
|
|
|
//sanity check
|
|
if(nil == xpcService)
|
|
{
|
|
//err msg
|
|
syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: failed to get path to XPC service");
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//get XPC services' attributes
|
|
fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:xpcService error:nil];
|
|
|
|
//sanity check
|
|
if(nil == fileAttributes)
|
|
{
|
|
//err msg
|
|
syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: failed to file attributes for XPC service");
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//check if (fully) auth'd
|
|
// ->owned by r00t & SETUID
|
|
if( (0 == [fileAttributes[NSFileOwnerAccountID] unsignedLongValue]) &&
|
|
(0 != (S_ISUID & [fileAttributes[NSFilePosixPermissions] unsignedLongValue])) )
|
|
{
|
|
//set flag
|
|
isAuthenticated = YES;
|
|
}
|
|
|
|
//bail
|
|
bail:
|
|
|
|
return isAuthenticated;
|
|
}
|
|
|
|
//display window that asks for r00t
|
|
// ->if user agrees, execs cmd to set XPC to be setuid!
|
|
-(void)askForRoot
|
|
{
|
|
//alloc/init request window
|
|
if(nil == self.requestRootWindowController)
|
|
{
|
|
//alloc/init
|
|
requestRootWindowController = [[RequestRootWindowController alloc] initWithWindowNibName:@"RequestRootWindow"];
|
|
}
|
|
|
|
//center window
|
|
[[self.requestRootWindowController window] center];
|
|
|
|
//show it
|
|
[self.requestRootWindowController showWindow:self];
|
|
|
|
//invoke function in background that will make window modal
|
|
// ->waits until window is non-nil
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
|
|
//make modal
|
|
makeModal(self.requestRootWindowController);
|
|
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
//begin task enumeration
|
|
-(void)exploreTasks
|
|
{
|
|
//alloc task enumerator
|
|
if(nil == taskEnumerator)
|
|
{
|
|
//alloc
|
|
taskEnumerator = [[TaskEnumerator alloc] init];
|
|
}
|
|
|
|
//kick off thread to enum task
|
|
// ->will update table as results come in
|
|
[NSThread detachNewThreadSelector:@selector(enumerateTasks:) toTarget:taskEnumerator withObject:nil];
|
|
|
|
return;
|
|
}
|
|
|
|
//change top pane
|
|
// ->switch between either flat (default) or tree-based (hierachical) view
|
|
-(void)changeViewController
|
|
{
|
|
//remove old view
|
|
if([self.taskTableController view] != nil)
|
|
{
|
|
//remove
|
|
[[self.taskTableController view] removeFromSuperview];
|
|
|
|
//'free'
|
|
self.taskTableController = nil;
|
|
}
|
|
|
|
switch(self.taskViewFormat)
|
|
{
|
|
case FLAT_VIEW:
|
|
{
|
|
//alloc/init
|
|
taskTableController = [[TaskTableController alloc] initWithNibName:@"FlatView" bundle:nil];
|
|
|
|
break;
|
|
}
|
|
case TREE_VIEW:
|
|
{
|
|
//alloc/init
|
|
taskTableController = [[TaskTableController alloc] initWithNibName:@"TreeView" bundle:nil];
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
//set bottom view?
|
|
self.bottomPaneBtn.selectedSegment = DYLIBS_VIEW;
|
|
|
|
//add subview
|
|
[self.topPane addSubview:self.taskTableController.view];
|
|
|
|
//disable autosize translations
|
|
self.taskTableController.view.translatesAutoresizingMaskIntoConstraints = NO;
|
|
|
|
//set frame
|
|
[[self.taskTableController view] setFrame:[self.topPane bounds]];
|
|
|
|
//constrain to parent
|
|
[self constrainView:self.topPane subView:self.taskTableController.view];
|
|
|
|
return;
|
|
}
|
|
|
|
//smartly reload a specific row in table
|
|
// ->arg determines pane (top/bottom) and for bottom pane, the active view the item belongs to
|
|
-(void)reloadRow:(id)item
|
|
{
|
|
//table view
|
|
__block NSTableView* tableView = nil;
|
|
|
|
//row
|
|
__block NSUInteger row = 0;
|
|
|
|
//run everything on main thread
|
|
// ensures table view isn't changed out from under us....
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
//top table (pane)
|
|
if(YES == [item isKindOfClass:[Task class]])
|
|
{
|
|
//top table view
|
|
tableView = [((id)self.taskTableController) itemView];
|
|
|
|
//reload item
|
|
// ->flat view
|
|
if(YES != [tableView isKindOfClass:[NSOutlineView class]])
|
|
{
|
|
//no filtering
|
|
// grab row from all tasks
|
|
if(YES != self.taskTableController.isFiltered)
|
|
{
|
|
//sync to get row
|
|
@synchronized (taskEnumerator.tasks)
|
|
{
|
|
//get row
|
|
row = [taskEnumerator.tasks indexOfKey:((Task*)item).pid];
|
|
}
|
|
}
|
|
//filtering
|
|
// grab row from filtered tasks
|
|
else
|
|
{
|
|
//sync to get row
|
|
@synchronized (self.taskTableController.filteredItems)
|
|
{
|
|
//get row
|
|
row = [self.taskTableController.filteredItems indexOfObject:item];
|
|
}
|
|
}
|
|
}
|
|
//reload item
|
|
// tree view, so no need to worry about filtering
|
|
else
|
|
{
|
|
//get row
|
|
row = [(NSOutlineView*)tableView rowForItem:item];
|
|
}
|
|
|
|
//sanity check
|
|
if(NSNotFound == row)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//begin updates
|
|
[tableView beginUpdates];
|
|
|
|
//reload row
|
|
[tableView reloadDataForRowIndexes:[NSIndexSet indexSetWithIndex:(row)] columnIndexes:[NSIndexSet indexSetWithIndex:0]];
|
|
|
|
//end updates
|
|
[tableView endUpdates];
|
|
}
|
|
//bottom pane
|
|
else
|
|
{
|
|
//bottom table view
|
|
tableView = [((id)self.bottomViewController) itemView];
|
|
|
|
//sync bottom view controller
|
|
@synchronized(self.bottomViewController)
|
|
{
|
|
|
|
//no filtering
|
|
// ->grab row from all items
|
|
if(YES != self.bottomViewController.isFiltered)
|
|
{
|
|
//get row
|
|
@synchronized(self.bottomViewController.tableItems)
|
|
{
|
|
//get
|
|
row = [self.bottomViewController.tableItems indexOfObject:item];
|
|
}
|
|
}
|
|
//filtering
|
|
// ->grab row from filtered item
|
|
else
|
|
{
|
|
//get row
|
|
@synchronized(self.bottomViewController.filteredItems)
|
|
{
|
|
//get row
|
|
row = [self.bottomViewController.filteredItems indexOfObject:item];
|
|
}
|
|
}
|
|
|
|
}//sync
|
|
|
|
//make sure item was found
|
|
if(NSNotFound == row)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//begin updates
|
|
[tableView beginUpdates];
|
|
|
|
//reload row
|
|
[tableView reloadDataForRowIndexes:[NSIndexSet indexSetWithIndex:(row)] columnIndexes:[NSIndexSet indexSetWithIndex:0]];
|
|
|
|
//end updates
|
|
[tableView endUpdates];
|
|
|
|
}//bottom pane
|
|
|
|
//bail
|
|
bail:
|
|
;
|
|
|
|
}); //dispatch on main thread
|
|
|
|
return;
|
|
}
|
|
|
|
//smartly, reload bottom pane on main thread
|
|
// ->checks if task & item type (e.g. files) are both selected
|
|
-(void)reloadBottomPane:(Task*)task itemView:(NSUInteger)itemView
|
|
{
|
|
//tag
|
|
__block NSUInteger segmentTag = 0;
|
|
|
|
//not found msg
|
|
NSString* noItemsMsg = nil;
|
|
|
|
//main thread
|
|
// get segment tag
|
|
if(YES == [NSThread isMainThread])
|
|
{
|
|
//get segment tag
|
|
segmentTag = [[self.bottomPaneBtn selectedCell] tagForSegment:[self.bottomPaneBtn selectedSegment]];
|
|
}
|
|
//background thread
|
|
// get segment tag on main thread
|
|
else
|
|
{
|
|
//on main thread
|
|
// get segment tag
|
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
|
|
//get segment tag
|
|
segmentTag = [[self.bottomPaneBtn selectedCell] tagForSegment:[self.bottomPaneBtn selectedSegment]];
|
|
|
|
});
|
|
}
|
|
|
|
//ignore reloads for unselected tasks
|
|
// ->note: when no task is passed in, always reload though
|
|
if( (nil != task) &&
|
|
(self.currentTask != task))
|
|
{
|
|
//ignore
|
|
goto bail;
|
|
}
|
|
|
|
//ignore reloads for unselected views
|
|
if( (segmentTag != itemView) &&
|
|
(CURRENT_VIEW != itemView))
|
|
{
|
|
//ignore
|
|
goto bail;
|
|
}
|
|
|
|
//set input
|
|
switch(segmentTag)
|
|
{
|
|
//dylibs
|
|
case DYLIBS_VIEW:
|
|
|
|
//set table items
|
|
@synchronized(self.bottomViewController)
|
|
{
|
|
//set
|
|
self.bottomViewController.tableItems = self.currentTask.dylibs;
|
|
}
|
|
|
|
//if there is none
|
|
noItemsMsg = @"no dylibs found";
|
|
|
|
break;
|
|
|
|
//files
|
|
case FILES_VIEW:
|
|
|
|
//set table items
|
|
@synchronized(self.bottomViewController)
|
|
{
|
|
//set
|
|
self.bottomViewController.tableItems = self.currentTask.files;
|
|
}
|
|
|
|
//if there is none
|
|
noItemsMsg = @"no files found";
|
|
|
|
break;
|
|
|
|
//networking
|
|
case NETWORKING_VIEW:
|
|
|
|
//set table items
|
|
@synchronized(self.bottomViewController)
|
|
{
|
|
//set
|
|
self.bottomViewController.tableItems = self.currentTask.connections;
|
|
}
|
|
|
|
//if there is none
|
|
noItemsMsg = @"no network connections found";
|
|
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
//execute on current thread
|
|
if(YES == [NSThread isMainThread])
|
|
{
|
|
//set not found label
|
|
self.noItemsLabel.stringValue = noItemsMsg;
|
|
|
|
//finalize
|
|
[self finalizeBottomReload];
|
|
}
|
|
else
|
|
{
|
|
//reload
|
|
// ->in main UI thread
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
//set not found label
|
|
self.noItemsLabel.stringValue = noItemsMsg;
|
|
|
|
//finalize
|
|
[self finalizeBottomReload];
|
|
|
|
});
|
|
}
|
|
|
|
//bail
|
|
bail:
|
|
|
|
return;
|
|
}
|
|
|
|
//finish bottom reload
|
|
-(void)finalizeBottomReload
|
|
{
|
|
//stop progress indicator
|
|
[self.bottomPaneSpinner stopAnimation:nil];
|
|
|
|
//hide
|
|
self.bottomPaneSpinner.hidden = YES;
|
|
|
|
//invoke refresh
|
|
[(id)self.bottomViewController reloadTable];
|
|
|
|
//no items?
|
|
// but there is something in top....
|
|
if( (0 == self.bottomViewController.tableItems.count) &&
|
|
(nil != self.currentTask) )
|
|
{
|
|
//how
|
|
self.noItemsLabel.hidden = NO;
|
|
}
|
|
//got items?
|
|
else
|
|
{
|
|
//hide
|
|
self.noItemsLabel.hidden = YES;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//VT callback to reload a binary
|
|
// ->task binary: reload all instances that match in top pane
|
|
// dylib: reload row if dylib is loaded in current/selected task
|
|
-(void)reloadBinary:(Binary*)binary
|
|
{
|
|
//all tasks
|
|
OrderedDictionary* tasks = nil;
|
|
|
|
//task
|
|
Task* task = nil;
|
|
|
|
//task binary
|
|
// ->reload all instances that match in top pane
|
|
if(YES == binary.isTaskBinary)
|
|
{
|
|
//when not filtered
|
|
// ->use all tasks
|
|
if(YES != self.taskTableController.isFiltered)
|
|
{
|
|
//sync
|
|
@synchronized(taskEnumerator.tasks)
|
|
{
|
|
|
|
//get tasks
|
|
tasks = taskEnumerator.tasks;
|
|
|
|
//reload each row w/ new VT info
|
|
for(NSNumber* taskPid in tasks)
|
|
{
|
|
//extract task
|
|
task = tasks[taskPid];
|
|
|
|
//check for match
|
|
if(task.binary == binary)
|
|
{
|
|
//reload
|
|
[self reloadRow:task];
|
|
}
|
|
}
|
|
|
|
}//sync
|
|
|
|
}
|
|
//when filtered
|
|
// ->use filtered items
|
|
else
|
|
{
|
|
//sync
|
|
@synchronized(self.taskTableController.filteredItems)
|
|
{
|
|
|
|
//filtered items
|
|
for(Task* task in self.taskTableController.filteredItems)
|
|
{
|
|
//check for match
|
|
if(task.binary == binary)
|
|
{
|
|
//reload
|
|
[self reloadRow:task];
|
|
}
|
|
}
|
|
|
|
}//sync
|
|
}
|
|
|
|
}//top pane
|
|
|
|
//bottom pane
|
|
// ->can just invoke 'reloadRow' method (which has logic to handle filtering, ignoring 'not found' items, etc.)
|
|
else
|
|
{
|
|
//reload
|
|
[self reloadRow:binary];
|
|
}
|
|
|
|
//main thread
|
|
//update 'flagged' item
|
|
// ...method might be invoked by VT callback, etc
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
//flagged items?
|
|
// set icon to red
|
|
if(0 != taskEnumerator.flaggedItems.count)
|
|
{
|
|
//set main image
|
|
self.flaggedButton.image = [NSImage imageNamed:@"flaggedRed"];
|
|
|
|
//set alternate image
|
|
self.flaggedButton.alternateImage = [NSImage imageNamed:@"flaggedRedBG"];
|
|
}
|
|
//no flagged items
|
|
// set icon back to default
|
|
else
|
|
{
|
|
//set main image
|
|
self.flaggedButton.image = [NSImage imageNamed:@"flagged"];
|
|
|
|
//set alternate image
|
|
self.flaggedButton.alternateImage = [NSImage imageNamed:@"flaggedBG"];
|
|
}
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
}
|
|
|
|
//display alert about OS not being supported
|
|
-(void)showUnsupportedAlert
|
|
{
|
|
//alert box
|
|
NSAlert* fullScanAlert = nil;
|
|
|
|
//alloc/init alert
|
|
fullScanAlert = [NSAlert alertWithMessageText:[NSString stringWithFormat:@"OS X %@ is not supported", [[NSProcessInfo processInfo] operatingSystemVersionString]] defaultButton:@"Ok" alternateButton:nil otherButton:nil informativeTextWithFormat:@"sorry for the inconvenience!"];
|
|
|
|
//and show it
|
|
[fullScanAlert runModal];
|
|
|
|
return;
|
|
}
|
|
|
|
//init tracking areas for buttons
|
|
// ->provide mouse over effects
|
|
-(void)initTrackingAreas
|
|
{
|
|
//tracking area for buttons
|
|
NSTrackingArea* trackingArea = nil;
|
|
|
|
//init tracking area
|
|
// ->for 'refresh' button
|
|
trackingArea = [[NSTrackingArea alloc] initWithRect:[self.refreshButton bounds] options:(NSTrackingInVisibleRect|NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways) owner:self userInfo:@{@"tag":[NSNumber numberWithUnsignedInteger:self.refreshButton.tag]}];
|
|
|
|
//add tracking area to pref button
|
|
[self.refreshButton addTrackingArea:trackingArea];
|
|
|
|
//init tracking area
|
|
// ->for search button
|
|
trackingArea = [[NSTrackingArea alloc] initWithRect:[self.searchButton bounds] options:(NSTrackingInVisibleRect|NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways) owner:self userInfo:@{@"tag":[NSNumber numberWithUnsignedInteger:self.searchButton.tag]}];
|
|
|
|
//add tracking area to search button
|
|
[self.searchButton addTrackingArea:trackingArea];
|
|
|
|
//init tracking area
|
|
// ->for save button
|
|
trackingArea = [[NSTrackingArea alloc] initWithRect:[self.saveButton bounds] options:(NSTrackingInVisibleRect|NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways) owner:self userInfo:@{@"tag":[NSNumber numberWithUnsignedInteger:self.saveButton.tag]}];
|
|
|
|
//add tracking area to search button
|
|
[self.saveButton addTrackingArea:trackingArea];
|
|
|
|
//init tracking area
|
|
// ->for logo button
|
|
trackingArea = [[NSTrackingArea alloc] initWithRect:[self.logoButton bounds] options:(NSTrackingInVisibleRect|NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways) owner:self userInfo:@{@"tag":[NSNumber numberWithUnsignedInteger:self.logoButton.tag]}];
|
|
|
|
//add tracking area to logo button
|
|
[self.logoButton addTrackingArea:trackingArea];
|
|
|
|
//init tracking area
|
|
// ->for flagged items button
|
|
trackingArea = [[NSTrackingArea alloc] initWithRect:[self.flaggedButton bounds] options:(NSTrackingInVisibleRect|NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways) owner:self userInfo:@{@"tag":[NSNumber numberWithUnsignedInteger:self.flaggedButton.tag]}];
|
|
|
|
//add tracking area to flaggd items button
|
|
[self.flaggedButton addTrackingArea:trackingArea];
|
|
|
|
return;
|
|
}
|
|
|
|
//automatically invoked when user clicks logo
|
|
// ->load objective-see's html page
|
|
-(IBAction)logoButtonHandler:(id)sender
|
|
{
|
|
//open URL
|
|
// ->invokes user's default browser
|
|
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://objective-see.com"]];
|
|
|
|
return;
|
|
}
|
|
|
|
//sort tasks
|
|
// ->just by name (for flat view)
|
|
-(void)sortTasksForView:(OrderedDictionary*)tasks;
|
|
{
|
|
//sort tasks
|
|
// ->flat view, sort by name
|
|
if(FLAT_VIEW == self.taskViewFormat)
|
|
{
|
|
//sort
|
|
[tasks sort:SORT_BY_NAME];
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//reload task table
|
|
// invoke custom refresh method on main thread
|
|
-(void)reloadTaskTable
|
|
{
|
|
//sort tasks
|
|
[self sortTasksForView:taskEnumerator.tasks];
|
|
|
|
//when exec'ing on background thread
|
|
// ->exec on main thread
|
|
if(YES != [NSThread isMainThread])
|
|
{
|
|
//refresh on main thread
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
//refresh
|
|
[(id)self.taskTableController refresh];
|
|
|
|
});
|
|
}
|
|
|
|
//on main thread
|
|
// ->just refresh
|
|
else
|
|
{
|
|
//refresh
|
|
[(id)self.taskTableController refresh];
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//automatically invoked when window is closing
|
|
// ->terminate app
|
|
-(void)windowWillClose:(NSNotification *)notification
|
|
{
|
|
//exit
|
|
[NSApp terminate:self];
|
|
|
|
return;
|
|
}
|
|
|
|
//automatically invoked when mouse entered
|
|
-(void)mouseEntered:(NSEvent*)theEvent
|
|
{
|
|
//mouse entered
|
|
// ->highlight (visual) state
|
|
[self buttonAppearance:theEvent shouldReset:NO];
|
|
|
|
return;
|
|
}
|
|
|
|
//automatically invoked when mouse exits
|
|
-(void)mouseExited:(NSEvent*)theEvent
|
|
{
|
|
//mouse exited
|
|
// ->so reset button to original (visual) state
|
|
[self buttonAppearance:theEvent shouldReset:YES];
|
|
|
|
return;
|
|
}
|
|
|
|
//set or unset button's highlight
|
|
-(void)buttonAppearance:(NSEvent*)theEvent shouldReset:(BOOL)shouldReset
|
|
{
|
|
//tag
|
|
NSUInteger tag = 0;
|
|
|
|
//image name
|
|
NSString* imageName = nil;
|
|
|
|
//button
|
|
NSButton* button = nil;
|
|
|
|
//extract tag
|
|
tag = [((NSDictionary*)theEvent.userData)[@"tag"] unsignedIntegerValue];
|
|
|
|
//restore button back to default (visual) state
|
|
if(YES == shouldReset)
|
|
{
|
|
//set original refresh image
|
|
if(REFRESH_BUTTON_TAG == tag)
|
|
{
|
|
//set
|
|
imageName = @"refreshIcon";
|
|
}
|
|
|
|
//set original search image
|
|
else if(SEARCH_BUTTON_TAG == tag)
|
|
{
|
|
//set
|
|
imageName = @"search";
|
|
}
|
|
|
|
//set original save image
|
|
else if(SAVE_BUTTON_TAG == tag)
|
|
{
|
|
//set
|
|
imageName = @"saveIcon";
|
|
}
|
|
|
|
//set original logo image
|
|
else if(LOGO_BUTTON_TAG == tag)
|
|
{
|
|
//set
|
|
imageName = @"logoApple";
|
|
}
|
|
|
|
//set original flagged items image
|
|
// ->but also handle case where there are still flagged items
|
|
else if(FLAGGED_BUTTON_TAG == tag)
|
|
{
|
|
//when no flagged items
|
|
if(0 == taskEnumerator.flaggedItems.count)
|
|
{
|
|
//set
|
|
imageName = @"flagged";
|
|
}
|
|
//flagged items
|
|
else
|
|
{
|
|
//set
|
|
imageName = @"flaggedRed";
|
|
}
|
|
|
|
}
|
|
}
|
|
//highlight button
|
|
else
|
|
{
|
|
//set original refresh image
|
|
if(REFRESH_BUTTON_TAG == tag)
|
|
{
|
|
//set
|
|
imageName = @"refreshIconOver";
|
|
}
|
|
//set mouse over search image
|
|
else if(SEARCH_BUTTON_TAG == tag)
|
|
{
|
|
//set
|
|
imageName = @"searchOver";
|
|
}
|
|
//set mouse over 'save' image
|
|
else if(SAVE_BUTTON_TAG == tag)
|
|
{
|
|
//set
|
|
imageName = @"saveIconOver";
|
|
}
|
|
//set mouse over logo image
|
|
else if(LOGO_BUTTON_TAG == tag)
|
|
{
|
|
//set
|
|
imageName = @"logoAppleOver";
|
|
}
|
|
|
|
//set mouse over flagged items image
|
|
// ->also handles case where flagged items are present
|
|
else if(FLAGGED_BUTTON_TAG == tag)
|
|
{
|
|
//when no flagged items
|
|
if(0 == taskEnumerator.flaggedItems.count)
|
|
{
|
|
//set
|
|
imageName = @"flaggedOver";
|
|
}
|
|
//flagged items
|
|
else
|
|
{
|
|
//set
|
|
imageName = @"flaggedRedOver";
|
|
}
|
|
}
|
|
}
|
|
|
|
//set image
|
|
|
|
//grab button
|
|
button = [[[self window] contentView] viewWithTag:tag];
|
|
if(YES != [button isKindOfClass:[NSButton class]])
|
|
{
|
|
//wtf
|
|
goto bail;
|
|
}
|
|
|
|
//when enabled
|
|
// ->set image
|
|
if(YES == [button isEnabled])
|
|
{
|
|
//set
|
|
[button setImage:[NSImage imageNamed:imageName]];
|
|
}
|
|
|
|
//bail
|
|
bail:
|
|
|
|
return;
|
|
}
|
|
|
|
//invoked when user clicks 'save' icon
|
|
// ->show popup that allows user to save results
|
|
-(IBAction)saveResults:(id)sender
|
|
{
|
|
//save panel
|
|
NSSavePanel *panel = nil;
|
|
|
|
//save results popup
|
|
__block NSAlert* saveResultPopup = nil;
|
|
|
|
//output
|
|
// ->json of all tasks/dylibs, etc
|
|
__block NSMutableString* output = nil;
|
|
|
|
//error
|
|
__block NSError* error = nil;
|
|
|
|
//create panel
|
|
panel = [NSSavePanel savePanel];
|
|
|
|
//suggest file name
|
|
[panel setNameFieldStringValue:@"tasks.json"];
|
|
|
|
//show panel
|
|
// ->completion handler will invoked when user clicks 'ok'
|
|
[panel beginWithCompletionHandler:^(NSInteger result)
|
|
{
|
|
//only need to handle 'ok'
|
|
if(NSFileHandlingPanelOKButton == result)
|
|
{
|
|
//alloc output JSON
|
|
output = [NSMutableString string];
|
|
|
|
//start JSON
|
|
[output appendString:@"{\"tasks:\":["];
|
|
|
|
//sync
|
|
@synchronized(taskEnumerator.tasks)
|
|
{
|
|
|
|
//get tasks
|
|
for(NSNumber* taskPid in taskEnumerator.tasks)
|
|
{
|
|
//append task JSON
|
|
[output appendFormat:@"{%@},", [taskEnumerator.tasks[taskPid] toJSON]];
|
|
}
|
|
|
|
}//sync
|
|
|
|
//remove last ','
|
|
if(YES == [output hasSuffix:@","])
|
|
{
|
|
//remove
|
|
[output deleteCharactersInRange:NSMakeRange([output length]-1, 1)];
|
|
}
|
|
|
|
//terminate list/output
|
|
[output appendString:@"]}"];
|
|
|
|
//save JSON to disk
|
|
// ->on error will show err msg in popup
|
|
if(YES != [output writeToURL:[panel URL] atomically:NO encoding:NSUTF8StringEncoding error:&error])
|
|
{
|
|
//err msg
|
|
syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: saving output to %s failed with %s", [[panel URL] fileSystemRepresentation], [[error description] UTF8String]);
|
|
|
|
//init popup w/ error msg
|
|
saveResultPopup = [NSAlert alertWithMessageText:@"ERROR: failed to save output" defaultButton:@"Ok" alternateButton:nil otherButton:nil informativeTextWithFormat:@"details: %@", error];
|
|
}
|
|
//happy
|
|
// ->set result msg
|
|
else
|
|
{
|
|
//init popup w/ msg
|
|
saveResultPopup = [NSAlert alertWithMessageText:@"Succesfully saved output" defaultButton:@"Ok" alternateButton:nil otherButton:nil informativeTextWithFormat:@"file: %s", [[panel URL] fileSystemRepresentation]];
|
|
}
|
|
|
|
//show popup
|
|
[saveResultPopup runModal];
|
|
}
|
|
|
|
}];
|
|
|
|
//bail
|
|
bail:
|
|
|
|
return;
|
|
}
|
|
|
|
//automatically invoked when user clicks 'search' button/ cmd+f hotkey
|
|
// ->perform global search
|
|
-(IBAction)search:(id)sender
|
|
{
|
|
//alloc/init flagged items window
|
|
if(nil == self.searchWindowController)
|
|
{
|
|
//alloc/init
|
|
searchWindowController = [[SearchWindowController alloc] initWithWindowNibName:@"SearchWindow"];
|
|
}
|
|
|
|
//init/prep it
|
|
[self.searchWindowController prepare];
|
|
|
|
//show it
|
|
[self.searchWindowController showWindow:self];
|
|
|
|
//reset search string
|
|
[self.searchWindowController.searchBox setStringValue:@""];
|
|
|
|
|
|
return;
|
|
}
|
|
|
|
#pragma mark Menu Handler(s) #pragma mark -
|
|
|
|
//automatically invoked when user clicks 'About/Info'
|
|
// ->show about window
|
|
-(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];
|
|
|
|
return;
|
|
}
|
|
|
|
/*
|
|
|
|
//automatically invoked when user clicks gear icon
|
|
// ->show preferences
|
|
-(IBAction)showPreferences:(id)sender
|
|
{
|
|
//alloc/init settings window
|
|
if(nil == self.prefsWindowController)
|
|
{
|
|
//alloc/init
|
|
prefsWindowController = [[PrefsWindowController alloc] initWithWindowNibName:@"PrefsWindow"];
|
|
}
|
|
|
|
//show it
|
|
[self.prefsWindowController showWindow:self];
|
|
|
|
//invoke function in background that will make window modal
|
|
// ->waits until window is non-nil
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
|
|
//make modal
|
|
makeModal(self.prefsWindowController);
|
|
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
*/
|
|
|
|
/*
|
|
//automatically invoked when menu is clicked
|
|
// ->tell menu to disable 'Preferences' when scan is running
|
|
-(BOOL)validateMenuItem:(NSMenuItem *)item
|
|
{
|
|
//enable
|
|
BOOL bEnabled = YES;
|
|
|
|
//check if item is 'Preferences'
|
|
if(PREF_MENU_ITEM_TAG == item.tag)
|
|
{
|
|
//unset enabled flag if scan is running
|
|
if(YES != [[self.scanButtonLabel stringValue] isEqualToString:START_SCAN])
|
|
{
|
|
//disable
|
|
bEnabled = NO;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
return bEnabled;
|
|
}
|
|
*/
|
|
|
|
|
|
//automatically invoked when user clicks on Flat/Tree view
|
|
// ->invoke helper function to change view
|
|
-(IBAction)switchView:(id)sender
|
|
{
|
|
//ignore same selection
|
|
if(self.taskViewFormat == [[sender selectedCell] tag])
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//save selected view
|
|
self.taskViewFormat = [[sender selectedCell] tag];
|
|
|
|
//flat view
|
|
// ->enable 'filter tasks' search field
|
|
if(FLAT_VIEW == self.taskViewFormat)
|
|
{
|
|
//enable
|
|
self.filterTasksBox.enabled = YES;
|
|
}
|
|
//tree view
|
|
// ->disable 'filter tasks' search field
|
|
else
|
|
{
|
|
//disable
|
|
self.filterTasksBox.enabled = NO;
|
|
}
|
|
|
|
//switch (top) view/pane
|
|
[self changeViewController];
|
|
|
|
//always unset filter flag
|
|
self.taskTableController.isFiltered = NO;
|
|
|
|
//always reset filter text
|
|
[self.filterTasksBox setStringValue:@""];
|
|
|
|
//sync
|
|
@synchronized(self.taskTableController.filteredItems)
|
|
{
|
|
|
|
//remove all filtered tasks
|
|
[self.taskTableController.filteredItems removeAllObjects];
|
|
|
|
}//sync
|
|
|
|
//reset current task
|
|
self.currentTask = nil;
|
|
|
|
//reload top pane
|
|
[self reloadTaskTable];
|
|
|
|
//select top row
|
|
[self.taskTableController.itemView selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
|
|
|
|
//reload bottom pane
|
|
// ->ensures correct info is shown for selected (top) task
|
|
[((AppDelegate*)[[NSApplication sharedApplication] delegate]) selectBottomPaneContent:nil];
|
|
|
|
//bail
|
|
bail:
|
|
|
|
return;
|
|
}
|
|
|
|
//(automatically) invoked on segmented button click or (manually) on task (top pane) switch
|
|
// ->change input (for pane) & then refresh it
|
|
-(IBAction)selectBottomPaneContent:(id)sender
|
|
{
|
|
//tag
|
|
NSUInteger segmentTag = 0;
|
|
|
|
//filter box placeholder text
|
|
NSString* filterPlaceholder = nil;
|
|
|
|
//always reset filter flag
|
|
self.bottomViewController.isFiltered = NO;
|
|
|
|
//always reset filter box's text
|
|
self.filterItemsBox.stringValue = @"";
|
|
|
|
//remove all filtered items
|
|
[self.bottomViewController.filteredItems removeAllObjects];
|
|
|
|
//for currently selected tasks
|
|
// ->check if its still alive
|
|
if(nil != self.currentTask)
|
|
{
|
|
//helper method that takes care of all check/handling dead tasks
|
|
[self.taskTableController handleRowSelection];
|
|
}
|
|
|
|
//when no current task
|
|
// set to first task in sorted tasks, unless filtering
|
|
if(nil == self.currentTask)
|
|
{
|
|
if( (YES != self.taskTableController.isFiltered) ||
|
|
( (YES == self.taskTableController.isFiltered) && (0 != self.taskTableController.filteredItems.count) ) )
|
|
{
|
|
//set to first
|
|
self.currentTask = taskEnumerator.tasks[[taskEnumerator.tasks keyAtIndex:0]];
|
|
}
|
|
}
|
|
|
|
//get segment tag
|
|
segmentTag = [[self.bottomPaneBtn selectedCell] tagForSegment:[self.bottomPaneBtn selectedSegment]];
|
|
|
|
//always hide 'no items' label
|
|
self.noItemsLabel.hidden = YES;
|
|
|
|
//unset existing items
|
|
@synchronized(self.bottomViewController)
|
|
{
|
|
//unset
|
|
self.bottomViewController.tableItems = nil;
|
|
}
|
|
|
|
//when in a background thread
|
|
// ->perform UI stuff on main thread
|
|
if(YES != [NSThread isMainThread])
|
|
{
|
|
//refresh on main thread
|
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
|
|
//reload to clear
|
|
[(id)self.bottomViewController reloadTable];
|
|
|
|
//show spinner
|
|
self.bottomPaneSpinner.hidden = NO;
|
|
|
|
//start progress indicator
|
|
[self.bottomPaneSpinner startAnimation:nil];
|
|
|
|
});
|
|
}
|
|
//in main thread already
|
|
// ->just perform UI actions directly
|
|
else
|
|
{
|
|
//reload to clear
|
|
[(id)self.bottomViewController reloadTable];
|
|
|
|
//start spinner
|
|
self.bottomPaneSpinner.hidden = NO;
|
|
|
|
//start progress indicator
|
|
[self.bottomPaneSpinner startAnimation:nil];
|
|
}
|
|
|
|
//set input
|
|
switch(segmentTag)
|
|
{
|
|
//dylibs
|
|
case DYLIBS_VIEW:
|
|
|
|
//init placeholder text for dylibs
|
|
filterPlaceholder = @"Filter Dylibs";
|
|
|
|
//remove all task's dylibs
|
|
[self.currentTask.dylibs removeAllObjects];
|
|
|
|
//skip kernel
|
|
if(0 == self.currentTask.pid.intValue)
|
|
{
|
|
//skip
|
|
break;
|
|
}
|
|
|
|
//(re)enumerate dylibs via XPC
|
|
// ->triggers table reload when done
|
|
[self.currentTask enumerateDylibs:taskEnumerator.dylibs shouldWait:NO];
|
|
|
|
break;
|
|
|
|
//files
|
|
case FILES_VIEW:
|
|
|
|
//init placeholder text for files
|
|
filterPlaceholder = @"Filter Files";
|
|
|
|
//remove all task's dylibs
|
|
[self.currentTask.files removeAllObjects];
|
|
|
|
//skip kernel
|
|
if(0 == self.currentTask.pid.intValue)
|
|
{
|
|
//skip
|
|
break;
|
|
}
|
|
|
|
//(re)enumerate files via XPC
|
|
// ->triggers table reload when done
|
|
[self.currentTask enumerateFiles:NO];
|
|
|
|
break;
|
|
|
|
//networking
|
|
case NETWORKING_VIEW:
|
|
|
|
//init placeholder text for network
|
|
filterPlaceholder = @"Filter Connections";
|
|
|
|
//skip kernel
|
|
if(0 == self.currentTask.pid.intValue)
|
|
{
|
|
//skip
|
|
break;
|
|
}
|
|
|
|
//(re)enumerate network connections via XPC
|
|
// ->triggers table reload when done
|
|
[self.currentTask enumerateNetworking:NO];
|
|
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
//when in a background thread
|
|
// ->perform UI stuff on main thread
|
|
if(YES != [NSThread isMainThread])
|
|
{
|
|
//set filter box's placeholder text
|
|
// ->UI change, so do on main thread
|
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
|
|
//set placeholder
|
|
[[self.filterItemsBox cell] setPlaceholderString:filterPlaceholder];
|
|
|
|
//kernel
|
|
// ->didn't enum so reload
|
|
if(0 == self.currentTask.pid.intValue)
|
|
{
|
|
//reload
|
|
[self reloadBottomPane:self.currentTask itemView:segmentTag];
|
|
}
|
|
|
|
});
|
|
}
|
|
//in main thread already
|
|
// ->just perform UI actions directly
|
|
else
|
|
{
|
|
//set placeholder
|
|
[[self.filterItemsBox cell] setPlaceholderString:filterPlaceholder];
|
|
|
|
//kernel
|
|
// ->didn't enum so reload
|
|
if(0 == self.currentTask.pid.intValue)
|
|
{
|
|
//reload
|
|
[self reloadBottomPane:self.currentTask itemView:segmentTag];
|
|
}
|
|
}
|
|
|
|
//bail
|
|
bail:
|
|
|
|
return;
|
|
}
|
|
|
|
//automatically invoked when user enters text in filter search boxes
|
|
// ->filter tasks and/or items
|
|
-(void)controlTextDidChange:(NSNotification *)aNotification
|
|
{
|
|
//search text
|
|
NSTextView* search = nil;
|
|
|
|
//tag for bottom pane selector
|
|
NSUInteger segmentTag = 0;
|
|
|
|
//extract search (text) view
|
|
search = aNotification.userInfo[@"NSFieldEditor"];
|
|
|
|
//sanity check
|
|
if(nil == search)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//prevent calling "complete" too often
|
|
if( (YES != self.completePosting) &&
|
|
(YES != self.commandHandling) )
|
|
{
|
|
//set flag
|
|
self.completePosting = YES;
|
|
|
|
//invoke complete
|
|
[aNotification.userInfo[@"NSFieldEditor"] complete:nil];
|
|
|
|
//unset flag
|
|
self.completePosting = NO;
|
|
}
|
|
|
|
//top pane
|
|
if(YES == [aNotification.object isEqualTo:self.filterTasksBox])
|
|
{
|
|
//when text is reset
|
|
// ->just reset flag
|
|
if(0 == search.string.length)
|
|
{
|
|
//set flag
|
|
self.taskTableController.isFiltered = NO;
|
|
}
|
|
//filter tasks
|
|
else
|
|
{
|
|
//'#' indicates a keyword search
|
|
// ->this is handled by customized auto-complete logic, so ignore
|
|
if(YES == [search.string hasPrefix:@"#"])
|
|
{
|
|
//ignore
|
|
goto bail;
|
|
}
|
|
|
|
//sync
|
|
@synchronized(self.taskTableController.filteredItems)
|
|
{
|
|
|
|
//remove prev
|
|
[self.taskTableController.filteredItems removeAllObjects];
|
|
|
|
//normal filter
|
|
[self.filterObj filterTasks:search.string items:taskEnumerator.tasks results:self.taskTableController.filteredItems pane:PANE_TOP];
|
|
|
|
}//sync
|
|
|
|
//set flag
|
|
self.taskTableController.isFiltered = YES;
|
|
}
|
|
|
|
//finalize filtering/search
|
|
// ->updates UI, etc
|
|
[self finalizeFiltration:PANE_TOP];
|
|
}
|
|
//bottom pane
|
|
else if(YES == [aNotification.object isEqualTo:self.filterItemsBox])
|
|
{
|
|
//when text is reset
|
|
// ->just reset flag
|
|
if(0 == search.string.length)
|
|
{
|
|
//set flag
|
|
self.bottomViewController.isFiltered = NO;
|
|
}
|
|
|
|
//filter items
|
|
else
|
|
{
|
|
//'#' indicates a keyword search
|
|
// ->this is handled by customized auto-complete logic, so ignore
|
|
if(YES == [search.string hasPrefix:@"#"])
|
|
{
|
|
//ignore
|
|
goto bail;
|
|
}
|
|
|
|
//get segment tag
|
|
segmentTag = [[self.bottomPaneBtn selectedCell] tagForSegment:[self.bottomPaneBtn selectedSegment]];
|
|
|
|
//remove all filtered tasks
|
|
[self.bottomViewController.filteredItems removeAllObjects];
|
|
|
|
//get item
|
|
// ->will bail if item isn't in (current) view, etc
|
|
switch(segmentTag)
|
|
{
|
|
//dylibs
|
|
case DYLIBS_VIEW:
|
|
{
|
|
//normal filter
|
|
[self.filterObj filterFiles:search.string items:self.currentTask.dylibs results:self.bottomViewController.filteredItems pane:PANE_BOTTOM];
|
|
|
|
break;
|
|
}
|
|
|
|
//files
|
|
case FILES_VIEW:
|
|
{
|
|
//filter
|
|
[self.filterObj filterFiles:search.string items:self.currentTask.files results:self.bottomViewController.filteredItems pane:PANE_BOTTOM];
|
|
|
|
break;
|
|
}
|
|
|
|
//network connections
|
|
case NETWORKING_VIEW:
|
|
{
|
|
//filter
|
|
[self.filterObj filterConnections:search.string items:self.currentTask.connections results:self.bottomViewController.filteredItems];
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
|
|
}//switch
|
|
|
|
//set flag
|
|
self.bottomViewController.isFiltered = YES;
|
|
}
|
|
|
|
//finalize filtering/searching
|
|
// ->updates UI, etc
|
|
[self finalizeFiltration:PANE_BOTTOM];
|
|
}
|
|
|
|
//bail
|
|
bail:
|
|
|
|
return;
|
|
}
|
|
|
|
//config/show the filter overlay
|
|
// ->also disable all inputs/buttons
|
|
-(void)prepUIForFiltering:(NSUInteger)pane
|
|
{
|
|
//pre-req
|
|
[self.filteringOverlay setWantsLayer:YES];
|
|
|
|
//rounded corners
|
|
self.filteringOverlay.layer.cornerRadius = 20.0;
|
|
|
|
//maks
|
|
self.filteringOverlay.layer.masksToBounds = YES;
|
|
|
|
//dark mode
|
|
// set overlay to light
|
|
if(YES == isDarkMode())
|
|
{
|
|
//light gray
|
|
self.filteringOverlay.layer.backgroundColor = NSColor.lightGrayColor.CGColor;
|
|
}
|
|
//light mode
|
|
// set overlay to gray
|
|
else
|
|
{
|
|
//gray
|
|
self.filteringOverlay.layer.backgroundColor = NSColor.grayColor.CGColor;
|
|
}
|
|
|
|
//make it semi-transparent
|
|
self.filteringOverlay.alphaValue = 0.95;
|
|
|
|
//show overlay
|
|
self.filteringOverlay.hidden = NO;
|
|
|
|
//set text
|
|
// top pane
|
|
if(PANE_TOP == pane)
|
|
{
|
|
//set text
|
|
self.filteringMessage.stringValue = [NSString stringWithFormat:@"filtering tasks (%@)", self.filterTasksBox.stringValue];
|
|
}
|
|
//set text
|
|
// bottom pane
|
|
else
|
|
{
|
|
//set text
|
|
self.filteringMessage.stringValue = [NSString stringWithFormat:@"filtering dylibs (%@)", self.filterItemsBox.stringValue];
|
|
}
|
|
|
|
//start spinner
|
|
[self.filteringIndicator startAnimation:nil];
|
|
|
|
//disable view switcher
|
|
self.viewSelector.enabled = NO;
|
|
|
|
//disable filter box
|
|
self.filterTasksBox.enabled = NO;
|
|
|
|
//disable bottom pane switcher
|
|
self.bottomPaneBtn.enabled = NO;
|
|
|
|
//disable bottom filter item box
|
|
self.filterItemsBox.enabled = NO;
|
|
|
|
//disable refresh
|
|
self.refreshButton.enabled = NO;
|
|
|
|
//disable search
|
|
self.searchButton.enabled = NO;
|
|
|
|
//disable save
|
|
self.saveButton.enabled = NO;
|
|
|
|
//disable flagged
|
|
self.flaggedButton.enabled = NO;
|
|
|
|
//disable task table
|
|
self.taskTableController.itemView.enabled = NO;
|
|
|
|
//disable item table
|
|
self.bottomViewController.itemView.enabled = NO;
|
|
|
|
return;
|
|
}
|
|
|
|
//roll back changes to UI due to filtering
|
|
// ->hide overlay and enable all inputs/buttons
|
|
-(void)unprepUIForFiltering
|
|
{
|
|
//always (attempt to) hide filter overlay
|
|
self.filteringOverlay.hidden = YES;
|
|
|
|
//stop spinner
|
|
[self.filteringIndicator stopAnimation:nil];
|
|
|
|
//enable view switcher
|
|
self.viewSelector.enabled = YES;
|
|
|
|
//enable filter box
|
|
self.filterTasksBox.enabled = YES;
|
|
|
|
//enable bottom pane switcher
|
|
self.bottomPaneBtn.enabled = YES;
|
|
|
|
//enable bottom filter item box
|
|
self.filterItemsBox.enabled = YES;
|
|
|
|
//enable refresh
|
|
self.refreshButton.enabled = YES;
|
|
|
|
//enable search
|
|
self.searchButton.enabled = YES;
|
|
|
|
//enable save
|
|
self.saveButton.enabled = YES;
|
|
|
|
//enable flagged
|
|
self.flaggedButton.enabled = YES;
|
|
|
|
//enable task table
|
|
self.taskTableController.itemView.enabled = YES;
|
|
|
|
//enable item table
|
|
self.bottomViewController.itemView.enabled = YES;
|
|
|
|
return;
|
|
}
|
|
|
|
//code to complete filtering/search
|
|
// ->reload table/scroll to top etc
|
|
-(void)finalizeFiltration:(NSUInteger)pane
|
|
{
|
|
//hide overlay, etc
|
|
[self unprepUIForFiltering];
|
|
|
|
//top pane (task)
|
|
if(PANE_TOP == pane)
|
|
{
|
|
//always reload task (top) pane
|
|
// ->will trigger bottom load too
|
|
[self.taskTableController.itemView reloadData];
|
|
|
|
//scroll to top
|
|
[self.taskTableController scrollToTop];
|
|
|
|
//when nothing matches
|
|
// ->reset current task and bottom pane
|
|
if( (YES == self.taskTableController.isFiltered) &&
|
|
(0 == self.taskTableController.filteredItems.count) )
|
|
{
|
|
//unset bottom pane's items
|
|
@synchronized(self.bottomViewController)
|
|
{
|
|
//unset
|
|
self.bottomViewController.tableItems = nil;
|
|
}
|
|
|
|
//reset current task
|
|
self.currentTask = nil;
|
|
|
|
//stop progress indicator
|
|
[self.bottomPaneSpinner stopAnimation:nil];
|
|
|
|
//reload bottom pane
|
|
[self.bottomViewController.itemView reloadData];
|
|
}
|
|
}
|
|
//bottom pane (dylibs, files, etc)
|
|
else if(PANE_BOTTOM == pane)
|
|
{
|
|
//always reload item (bottom) pane
|
|
[self.bottomViewController.itemView reloadData];
|
|
|
|
//scroll to top
|
|
[self.bottomViewController scrollToTop];
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//action for 'refresh' button/cmd+r hotkey
|
|
// ->query OS to refresh/reload all tasks
|
|
-(IBAction)refreshTasks:(id)sender
|
|
{
|
|
//unselect current task
|
|
self.currentTask = nil;
|
|
|
|
//unset filter flag
|
|
self.taskTableController.isFiltered = NO;
|
|
|
|
//sync
|
|
@synchronized(self.taskTableController.filteredItems)
|
|
{
|
|
//remove all filtered items
|
|
[self.taskTableController.filteredItems removeAllObjects];
|
|
}
|
|
|
|
//reset filter box
|
|
self.filterTasksBox.stringValue = @"";
|
|
|
|
//reset segment (bottom pane) back to dylibs
|
|
self.bottomPaneBtn.selectedSegment = DYLIBS_VIEW;
|
|
|
|
//scroll to top
|
|
[self.taskTableController scrollToTop];
|
|
|
|
//select top row
|
|
[self.taskTableController.itemView selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
|
|
|
|
//wait till (existing) task enumerator thread is done
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
|
|
//wait
|
|
while(YES == [taskEnumerator.enumerator isExecuting])
|
|
{
|
|
//nap
|
|
[NSThread sleepForTimeInterval:0.5f];
|
|
}
|
|
|
|
//(re)explore tasks
|
|
[self exploreTasks];
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
//constrain subview to parent view
|
|
-(void)constrainView:(NSView*)containerView subView:(NSView*)subView
|
|
{
|
|
//remove top constraint
|
|
if(nil != self.topConstraint)
|
|
{
|
|
//remove
|
|
[containerView removeConstraint:self.topConstraint];
|
|
}
|
|
|
|
//remove bottom constraint
|
|
if(nil != self.bottomConstraint)
|
|
{
|
|
//remove
|
|
[containerView removeConstraint:self.bottomConstraint];
|
|
}
|
|
|
|
//remove leading constraint
|
|
if(nil != self.leadingConstraint)
|
|
{
|
|
//remove
|
|
[containerView removeConstraint:self.leadingConstraint];
|
|
}
|
|
|
|
//remove trailing constraint
|
|
if(nil != self.trailingConstraint)
|
|
{
|
|
//remove
|
|
[containerView removeConstraint:self.trailingConstraint];
|
|
}
|
|
|
|
//create top constraint
|
|
self.topConstraint = [NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:containerView attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0];
|
|
|
|
//add top constraint
|
|
[containerView addConstraint:self.topConstraint];
|
|
|
|
//create bottom constraint
|
|
self.bottomConstraint = [NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:containerView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0];
|
|
|
|
//add bottom constraint
|
|
[containerView addConstraint:self.bottomConstraint];
|
|
|
|
//create leading constraint
|
|
self.leadingConstraint = [NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:containerView attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0.0];
|
|
|
|
//add leading constraint
|
|
[containerView addConstraint:self.leadingConstraint];
|
|
|
|
//create trailing constraint
|
|
self.trailingConstraint = [NSLayoutConstraint constraintWithItem:subView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:containerView attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:0.0];
|
|
|
|
//add trailing constraint
|
|
[containerView addConstraint:self.trailingConstraint];
|
|
|
|
return;
|
|
}
|
|
|
|
//button handle for 'flagged items' button
|
|
// ->display (in separate popup) all flagged items
|
|
-(IBAction)showFlaggedItems:(id)sender
|
|
{
|
|
//alert box
|
|
NSAlert* alert = nil;
|
|
|
|
//handle case where there aren't any flagged items
|
|
// ->just show alert
|
|
if(0 == taskEnumerator.flaggedItems.count)
|
|
{
|
|
//alloc/init alert
|
|
alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:@"No items flagged by VirusTotal"] defaultButton:@"Ok" alternateButton:nil otherButton:nil informativeTextWithFormat:@"hooray! 😇"];
|
|
|
|
//and show it
|
|
[alert runModal];
|
|
}
|
|
|
|
//show flagged items
|
|
else
|
|
{
|
|
//alloc/init flagged items window
|
|
if(nil == self.flagItemsWindowController)
|
|
{
|
|
//alloc/init
|
|
flagItemsWindowController = [[FlaggedItems alloc] initWithWindowNibName:@"FlaggedItems"];
|
|
}
|
|
|
|
//init/prep it
|
|
[self.flagItemsWindowController prepare];
|
|
|
|
//show it
|
|
[self.flagItemsWindowController showWindow:self];
|
|
|
|
/*
|
|
//invoke function in background that will make window modal
|
|
// ->waits until window is non-nil
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
|
|
//make modal
|
|
makeModal(self.flagItemsWindowController);
|
|
|
|
});
|
|
*/
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//delegate method, automatically called
|
|
// ->generate list of matches to return for drop-down
|
|
-(NSArray *)control:(NSControl *)control textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index
|
|
{
|
|
//matches
|
|
NSMutableArray *matches = nil;
|
|
|
|
//range options
|
|
NSUInteger rangeOptions = {0};
|
|
|
|
//segment tag
|
|
NSUInteger segmentTag = 0;
|
|
|
|
//init array for matches
|
|
matches = [[NSMutableArray alloc] init];
|
|
|
|
//init range options
|
|
rangeOptions = NSAnchoredSearch | NSCaseInsensitiveSearch;
|
|
|
|
//grab segment tag
|
|
segmentTag = [[self.bottomPaneBtn selectedCell] tagForSegment:[self.bottomPaneBtn selectedSegment]];
|
|
|
|
//for now, only filter binaries
|
|
// ->top pane: any (well, just tasks)
|
|
// bottom pane: only dylibs
|
|
if( (textView != self.customTasksFilter) &&
|
|
(DYLIBS_VIEW != segmentTag) )
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//check all filters
|
|
for(NSString* filter in self.filterObj.binaryFilters)
|
|
{
|
|
//check if found
|
|
// ->add to match when found
|
|
if([filter rangeOfString:textView.string options:rangeOptions range:NSMakeRange(0, filter.length)].location != NSNotFound)
|
|
{
|
|
//add
|
|
[matches addObject:filter];
|
|
}
|
|
}
|
|
|
|
//sort matches
|
|
[matches sortUsingComparator:^(NSString *a, NSString *b)
|
|
{
|
|
//sort
|
|
return [a localizedStandardCompare:b];
|
|
}];
|
|
|
|
//bail
|
|
bail:
|
|
|
|
return matches;
|
|
}
|
|
|
|
//delegate method, automatically invoked
|
|
// ->handle invocations for text view
|
|
- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
|
|
{
|
|
//flag
|
|
BOOL didPerformRequestedSelectorOnTextView = NO;
|
|
|
|
//invocation
|
|
NSInvocation *textViewInvocationForSelector = nil;
|
|
|
|
//check if text view can handle selector
|
|
if(YES != [textView respondsToSelector:commandSelector])
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//set iVar flag
|
|
self.commandHandling = YES;
|
|
|
|
//handle 'enter'
|
|
// ->but only for #'s
|
|
if( (commandSelector == @selector(insertNewline:)) &&
|
|
(YES == [filterObj isKeyword:textView.string]) )
|
|
{
|
|
//filter
|
|
[self filterAutoComplete:textView];
|
|
|
|
//unset iVar
|
|
self.commandHandling = NO;
|
|
|
|
//set flag
|
|
didPerformRequestedSelectorOnTextView = YES;
|
|
}
|
|
//just invoke system handler for rest...
|
|
else
|
|
{
|
|
//init invocation
|
|
textViewInvocationForSelector = [NSInvocation invocationWithMethodSignature:[textView methodSignatureForSelector:commandSelector]];
|
|
|
|
//set target
|
|
[textViewInvocationForSelector setTarget:textView];
|
|
|
|
//set selector
|
|
[textViewInvocationForSelector setSelector:commandSelector];
|
|
|
|
//invoke selector
|
|
[textViewInvocationForSelector invoke];
|
|
|
|
//unset iVar
|
|
self.commandHandling = NO;
|
|
|
|
//indicate that selector was performed
|
|
didPerformRequestedSelectorOnTextView = YES;
|
|
}
|
|
|
|
bail:
|
|
|
|
return didPerformRequestedSelectorOnTextView;
|
|
}
|
|
|
|
//callback for custom search fields
|
|
// handles the auto-complete filterings
|
|
-(void)filterAutoComplete:(NSTextView*)textView
|
|
{
|
|
//filter string
|
|
NSString* filterString = nil;
|
|
|
|
//tag
|
|
NSUInteger segmentTag = 0;
|
|
|
|
//alert box
|
|
NSAlert* noKeywordsAlert = nil;
|
|
|
|
//extract filter
|
|
filterString = textView.textStorage.string;
|
|
|
|
//handle top pane (tasks)
|
|
if(textView == self.customTasksFilter)
|
|
{
|
|
//set flag
|
|
self.taskTableController.isFiltered = YES;
|
|
|
|
//sync
|
|
@synchronized(self.taskTableController.filteredItems)
|
|
{
|
|
//remove all filtered tasks
|
|
[self.taskTableController.filteredItems removeAllObjects];
|
|
|
|
//update ui
|
|
[self finalizeFiltration:PANE_TOP];
|
|
|
|
//filter
|
|
[self.filterObj filterTasks:filterString items:taskEnumerator.tasks results:self.taskTableController.filteredItems pane:PANE_TOP];
|
|
}
|
|
|
|
//refresh/update UI when not a keyword search
|
|
// ->keyword search are done in background, and will call this directly when pau
|
|
if(YES != [filterString hasPrefix:@"#"])
|
|
{
|
|
//finalize filtering
|
|
[self finalizeFiltration:PANE_TOP];
|
|
}
|
|
}
|
|
//handle bottom pane
|
|
// ->just dylibs
|
|
else if(textView == self.customItemsFilter)
|
|
{
|
|
//get segment tag
|
|
segmentTag = [[self.bottomPaneBtn selectedCell] tagForSegment:[self.bottomPaneBtn selectedSegment]];
|
|
|
|
//check if keyword was entered for files/networks
|
|
if(segmentTag != DYLIBS_VIEW)
|
|
{
|
|
//alloc/init alert
|
|
noKeywordsAlert = [NSAlert alertWithMessageText:@"#keywords not (yet) supported for files/connections" defaultButton:@"Ok" alternateButton:nil otherButton:nil informativeTextWithFormat:@""];
|
|
|
|
//and show it
|
|
[noKeywordsAlert runModal];
|
|
|
|
//reset search
|
|
self.filterItemsBox.stringValue = @"";
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//set flag
|
|
self.bottomViewController.isFiltered = YES;
|
|
|
|
@synchronized(self.bottomViewController.filteredItems)
|
|
{
|
|
//remove all filtered tasks
|
|
[self.bottomViewController.filteredItems removeAllObjects];
|
|
|
|
//update ui
|
|
[self finalizeFiltration:PANE_BOTTOM];
|
|
|
|
//filter
|
|
[self.filterObj filterFiles:filterString items:self.currentTask.dylibs results:self.bottomViewController.filteredItems pane:PANE_BOTTOM];
|
|
}
|
|
|
|
//refresh/update UI when not a keyword search
|
|
// ->keyword search are done in background, and will call this directly when pau
|
|
if(YES != [filterString hasPrefix:@"#"])
|
|
{
|
|
//finalize filtering
|
|
[self finalizeFiltration:PANE_BOTTOM];
|
|
}
|
|
}
|
|
|
|
//bail
|
|
bail:
|
|
|
|
return;
|
|
}
|
|
|
|
//automatically invoked
|
|
// ->set all NSSearchFields to be instances of our custom NSTextView
|
|
-(id)windowWillReturnFieldEditor:(NSWindow *)sender toObject:(id)client
|
|
{
|
|
//field editor
|
|
id fieldEditor = nil;
|
|
|
|
//ignore non-NSSearchField classes
|
|
if(YES != [client isKindOfClass:[NSSearchField class]])
|
|
{
|
|
//ingnore
|
|
goto bail;
|
|
}
|
|
|
|
//set task's filter search field
|
|
if(client == self.filterTasksBox)
|
|
{
|
|
//assign for return
|
|
fieldEditor = self.customTasksFilter;
|
|
}
|
|
//set item's filter search field
|
|
else if(client == self.filterItemsBox)
|
|
{
|
|
//assign for return
|
|
fieldEditor = self.customItemsFilter;
|
|
}
|
|
|
|
//bail
|
|
bail:
|
|
|
|
return fieldEditor;
|
|
}
|
|
|
|
|
|
//call into Update obj
|
|
// check to see if there an update?
|
|
- (IBAction)check4Update:(id)sender
|
|
{
|
|
//update obj
|
|
Update* update = nil;
|
|
|
|
//init update obj
|
|
update = [[Update alloc] init];
|
|
|
|
//check for update
|
|
// ->'updateResponse newVersion:' method will be called when check is done
|
|
[update checkForUpdate:^(NSUInteger result, NSString* newVersion) {
|
|
|
|
//process response
|
|
// when invoked from menu show full results
|
|
[self updateResponse:result newVersion:newVersion alwaysShow:(nil != sender)];
|
|
|
|
}];
|
|
|
|
return;
|
|
}
|
|
|
|
//process update response
|
|
// error, no update, update/new version
|
|
-(void)updateResponse:(NSInteger)result newVersion:(NSString*)newVersion alwaysShow:(BOOL)alwaysShow
|
|
{
|
|
//details
|
|
NSString* details = nil;
|
|
|
|
//action
|
|
NSString* action = nil;
|
|
|
|
//handle response
|
|
// new version, show popup
|
|
switch(result)
|
|
{
|
|
//error
|
|
case UPDATE_ERROR:
|
|
|
|
//set details
|
|
details = @"error, failed to check for an update.";
|
|
|
|
//set action
|
|
action = @"Close";
|
|
|
|
break;
|
|
|
|
//no updates
|
|
case UPDATE_NOTHING_NEW:
|
|
|
|
//set details
|
|
details = [NSString stringWithFormat:@"you're all up to date! (v. %@)", getAppVersion()];
|
|
|
|
//set action
|
|
action = @"Close";
|
|
|
|
break;
|
|
|
|
//new version
|
|
case UPDATE_NEW_VERSION:
|
|
|
|
//set details
|
|
details = [NSString stringWithFormat:@"a new version (%@) is available!", newVersion];
|
|
|
|
//set action
|
|
action = @"Update";
|
|
|
|
break;
|
|
}
|
|
|
|
//new version?
|
|
//...or always show results?
|
|
if( (YES == alwaysShow) ||
|
|
(UPDATE_NEW_VERSION == result) )
|
|
{
|
|
//alloc update window
|
|
updateWindowController = [[UpdateWindowController alloc] initWithWindowNibName:@"UpdateWindow"];
|
|
|
|
//configure
|
|
[self.updateWindowController configure:details buttonTitle:action];
|
|
|
|
//center window
|
|
[[self.updateWindowController window] center];
|
|
|
|
//show it
|
|
[self.updateWindowController 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.updateWindowController);
|
|
|
|
});
|
|
}
|
|
|
|
return;
|
|
}
|
|
@end
|