def __init__(self, sniffer): # keep track of the sniffer that launched this app recorder self.sniffer = sniffer # keep track of app listeners so they don't get garbage collected self.watched = {} # keep track of screen geometry self.apps_and_windows = {} # subclass definitions self.wr = WebRecorder(self) self.browser_pids = []
class AppRecorder: def __init__(self, sniffer): # keep track of the sniffer that launched this app recorder self.sniffer = sniffer # keep track of app listeners so they don't get garbage collected self.watched = {} # keep track of screen geometry self.apps_and_windows = {} # subclass definitions self.wr = WebRecorder(self) self.browser_pids = [] ### Application event callbacks ### def appLaunchCallback_(self, notification): recording = preferences.getValueForPreference('recording') # get event info t = cfg.NOW() app = notification.userInfo()["NSWorkspaceApplicationKey"] name = unicode(app.localizedName()) pid = int(app.processIdentifier()) # create app listener for this app's window events if app.activationPolicy() == 0: mess = acc.create_application_ref(pid = pid) mess.set_callback(self.windowCallback) mess.watch("AXMoved", "AXWindowResized", "AXFocusedWindowChanged", "AXWindowCreated","AXWindowMiniaturized", "AXWindowDeminiaturized") self.watched[pid] = mess if recording: # log that the application launched text = '{"time": '+ str(t) + ' , "type": "Open", "app": "' + name + '"}' utils_cocoa.write_to_file(text, cfg.APPLOG) # log that application is active if app.isActive(): text = '{"time": '+ str(t) + ' , "type": "Active", "app": "' + name + '"}' utils_cocoa.write_to_file(text, cfg.APPLOG) # take a screenshot eventScreenshots = preferences.getValueForPreference('eventScreenshots') if eventScreenshots: self.sniffer.activity_tracker.take_screenshot() # check if the screen geometry changed and update active window self.updateWindowList() def appTerminateCallback_(self, notification): recording = preferences.getValueForPreference('recording') # get event info t = cfg.NOW() app = notification.userInfo()["NSWorkspaceApplicationKey"] #TODO find out why we are getting no name from the app terminate name = unicode(app.localizedName()) pid = int(app.processIdentifier()) # let app listener be garbage collected by removing our saved reference to it if pid in self.watched.keys(): del self.watched[pid] # watcher = self.watched[p] if recording: # log the the application has closed text = '{"time": '+ str(t) + ' , "type": "Close", "app": "' + name + '"}' utils_cocoa.write_to_file(text, cfg.APPLOG) #TODO throw app deactivate event when active application terminates? # would have to tell what app this is, and if it was the most # previously active app if unicode(app.localizedName()) == "\"Google Chrome\"": self.wr.closeChrome() # check if the screen geometry changed and update active window self.updateWindowList() def appActivateCallback_(self, notification): recording = preferences.getValueForPreference('recording') # get event info t = cfg.NOW() app = notification.userInfo()["NSWorkspaceApplicationKey"] name = unicode(app.localizedName()) pid = int(app.processIdentifier()) # log that the application has become active if pid in self.watched.keys() and recording: text = '{"time": '+ str(t) + ' , "type": "Active", "app": "' + name + '"}' utils_cocoa.write_to_file(text, cfg.APPLOG) # take screenshot eventScreenshots = preferences.getValueForPreference('eventScreenshots') if eventScreenshots: self.sniffer.activity_tracker.take_screenshot() # check if the screen geometry changed and update active window self.updateWindowList() def appDeactivateCallback_(self, notification): recording = preferences.getValueForPreference('recording') # get event info t = cfg.NOW() app = notification.userInfo()["NSWorkspaceApplicationKey"] # only save the info if we have app information # we don't get app information when this is thrown after an app closes if app.processIdentifier() == -1: return name = unicode(app.localizedName()) pid = int(app.processIdentifier()) # log that the application has become inactive if pid in self.watched.keys() and recording: text = '{"time": '+ str(t) + ' , "type": "Inactive", "app": "' + name + '"}' utils_cocoa.write_to_file(text, cfg.APPLOG) # check if the screen geometry changed and update active window self.updateWindowList() ### Window event callbacks ### #TODO make function more robust so it does not fail if some of the accessibility data is not available def windowCallback(self, **kwargs): recording = preferences.getValueForPreference('recording') if recording: # get event info t = cfg.NOW() notification_title = str(kwargs['notification'])[2:-1] # remove 'AX' from front of event names before we save that info # TODO this app title may not match what we get from app.localizedName() # find way to reconcile app_title = unicode(kwargs['element']['AXTitle']) # take screenshot eventScreenshots = preferences.getValueForPreference('eventScreenshots') if eventScreenshots: self.sniffer.activity_tracker.take_screenshot() # when miniaturized, we may not be able to get window title and position data if notification_title == "WindowMiniaturized": # write to window log file about event text = '{"time": '+ str(t) + ' , "type": "' + notification_title + '", "app": "' + app_title + '"}' utils_cocoa.write_to_file(text, cfg.WINDOWLOG) # all other events should let us get title and postiion data else: # get the relevant window data title = unicode(kwargs['element']['AXFocusedWindow']['AXTitle']) position = str(kwargs['element']['AXFocusedWindow']['AXPosition']) size = str(kwargs['element']['AXFocusedWindow']['AXSize']) # write to window log file about event text = '{"time": ' + str(t) + ' , "type": "' + notification_title + '", "app": "' + app_title + '", "window": "' + title + '", "position": ' + position + ' , "size": ' + size +' }' utils_cocoa.write_to_file(text, cfg.WINDOWLOG) # get most recent screen geometry and update active window self.updateWindowList() def updateWindowList(self): # get an early timestamp recording = preferences.getValueForPreference('recording') t = cfg.NOW() if recording: # clear our past geometry and active window self.apps_and_windows = {} active_window = None # get list of applications that show up in the dock workspace = NSWorkspace.sharedWorkspace() activeApps = workspace.runningApplications() regularApps = [] for app in activeApps: if app.activationPolicy() == 0: regularApps.append(app) # save app info to dictionary for app in regularApps: name = unicode(app.localizedName()) active = app.isActive() pid = app.processIdentifier() d = {'name': name, 'active': active, 'windows':{}} # get window and tab info for browsers if name == 'Google Chrome': d['windows'] = self.wr.getChromeURLs(active) if pid not in self.browser_pids: self.browser_pids.append(pid) elif name == 'Safari': d['windows'] = self.wr.getSafariURLs(active) if pid not in self.browser_pids: self.browser_pids.append(pid) self.apps_and_windows[int(pid)] = d # store app data by pid # get title of the active window if possible if active: try: mess = self.watched[pid] active_window = unicode(mess['AXFocusedWindow']['AXTitle']) except: pass # add list of current windows options = kCGWindowListOptionAll + kCGWindowListExcludeDesktopElements windows = CGWindowListCopyWindowInfo(options, kCGNullWindowID) for window in windows: try: # get window data owning_app_pid = window['kCGWindowOwnerPID'] # don't record window data if for a browser if owning_app_pid in self.browser_pids: continue window_layer = window['kCGWindowLayer'] name = unicode(window['kCGWindowName']) # window_id = window['kCGWindowNumber'] window_id = str(window['kCGWindowNumber']) if window_id[-1] == "L": window_id = window_id[0:-1] window_id = int(window_id) bounds = window['kCGWindowBounds'] win_bounds = {'width':bounds['Width'], 'height':bounds['Height'], 'x':bounds['X'], 'y':bounds['Y']} active = False if unicode(window['kCGWindowName']) == active_window: active = True on_screen = False if 'kCGWindowIsOnscreen' in window.keys(): on_screen = window['kCGWindowIsOnscreen'] # unless it has a name and is on the top layer, we don't count it if owning_app_pid in self.apps_and_windows and window_layer == 0 and name: # add window data to the app_window dictionary window_dict = {'name': name, 'bounds': win_bounds, 'active': active, 'onscreen': on_screen} self.apps_and_windows[owning_app_pid]['windows'][window_id] = window_dict except: pass # write self.apps_and_windows to a geometry file text = '{"time": ' + str(t) + ', "geometry": ' + str(self.apps_and_windows) + "}" utils_cocoa.write_to_file(text, cfg.GEOLOG) ### Computer sleep/wake callbacks ### def sleepCallback_(self, notification): recording = preferences.getValueForPreference('recording') if recording: t = cfg.NOW() text = '{"time": '+ str(t) + ' , "type": "Sleep"}' utils_cocoa.write_to_file(text, cfg.RECORDERLOG) def wakeCallback_(self, notification): recording = preferences.getValueForPreference('recording') if recording: t = cfg.NOW() text = '{"time": '+ str(t) + ' , "type": "Wake"}' utils_cocoa.write_to_file(text, cfg.RECORDERLOG) # take a screenshot eventScreenshots = preferences.getValueForPreference('eventScreenshots') if eventScreenshots: self.sniffer.activity_tracker.take_screenshot() # get updated list of applications and windows self.updateWindowList() ### Manager the event listeners ### def start_app_observers(self): recording = preferences.getValueForPreference('recording') # prompt user to grant accessibility access to Traces, if not already granted acc.is_enabled() # get an early timestamp t = cfg.NOW() # create listeners for application events workspace = NSWorkspace.sharedWorkspace() nc = workspace.notificationCenter() s = objc.selector(self.appLaunchCallback_,signature='v@:@') nc.addObserver_selector_name_object_(self, s, 'NSWorkspaceDidLaunchApplicationNotification', None) s = objc.selector(self.appTerminateCallback_,signature='v@:@') nc.addObserver_selector_name_object_(self, s, 'NSWorkspaceDidTerminateApplicationNotification', None) s = objc.selector(self.appActivateCallback_,signature='v@:@') nc.addObserver_selector_name_object_(self, s, 'NSWorkspaceDidActivateApplicationNotification', None) s = objc.selector(self.appDeactivateCallback_,signature='v@:@') nc.addObserver_selector_name_object_(self, s, 'NSWorkspaceDidDeactivateApplicationNotification', None) # create listeners for system events s = objc.selector(self.wakeCallback_,signature='v@:@') nc.addObserver_selector_name_object_(self, s, 'NSWorkspaceDidWakeNotification', None) s = objc.selector(self.sleepCallback_,signature='v@:@') nc.addObserver_selector_name_object_(self, s, 'NSWorkspaceWillSleepNotification', None) # other events that may be useful to track in the future # https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSWorkspace_Class/ # NSWorkspaceDidHideApplicationNotification # NSWorkspaceDidUnhideApplicationNotification # NSWorkspaceActiveSpaceDidChangeNotification # NSWorkspaceWillPowerOffNotification # NSWorkspaceDidPerformFileOperationNotification # get list of active applications activeApps = workspace.runningApplications() regularApps = [] for app in activeApps: if app.activationPolicy() == 0: # those that show up in the Dock regularApps.append(app) # listen for window events of these applications for app in regularApps: try: p = int(app.processIdentifier()) name = unicode(app.localizedName()) mess = acc.create_application_ref(pid=p) mess.set_callback(self.windowCallback) mess.watch("AXMoved", "AXWindowResized", "AXFocusedWindowChanged", "AXWindowCreated","AXWindowMiniaturized", "AXWindowDeminiaturized") # AXMainWindowChanged self.watched[p] = mess # we need to maintain the listener or it will be deleted on cleanup if recording: # log that the app is open text = '{"time": '+ str(t) + ' , "type": "Open", "app": "' + name + '"}' utils_cocoa.write_to_file(text, cfg.APPLOG) if app.isActive(): text = '{"time": '+ str(t) + ' , "type": "Active", "app": "' + name + '"}' utils_cocoa.write_to_file(text, cfg.APPLOG) except: raise print "Could not create event listener for application: " + str(name) # get inital list of windows and add window listeners self.updateWindowList() # start event loop to track events from other applications CFRunLoopRun() def stop_app_observers(self): recording = preferences.getValueForPreference('recording') t = cfg.NOW() # get workspace and list of all applications workspace = NSWorkspace.sharedWorkspace() activeApps = workspace.runningApplications() # let app observers be garabage collected self.watched = {} if recording: # prune list of applications to apps that appear in the dock regularApps = [] for app in activeApps: if app.activationPolicy() == 0: regularApps.append(app) # listen for window events of these applications for app in regularApps: name = unicode(app.localizedName()) # log that the app recording will stop text = '{"time": '+ str(t) + ' , "type": "Close", "app": "' + name + '"}' utils_cocoa.write_to_file(text, cfg.APPLOG) if app.isActive(): text = '{"time": '+ str(t) + ' , "type": "Inactive", "app": "' + name + '"}' utils_cocoa.write_to_file(text, cfg.APPLOG) # write a blank line to the geometry table to close out all windows text = '{"time": ' + str(t) + ', "geometry": {} }' utils_cocoa.write_to_file(text, cfg.GEOLOG) def pause_app_observers(self): t = cfg.NOW() # get workspace and list of all applications workspace = NSWorkspace.sharedWorkspace() activeApps = workspace.runningApplications() # prune list of applications to apps that appear in the dock regularApps = [] for app in activeApps: if app.activationPolicy() == 0: regularApps.append(app) # listen for window events of these applications for app in regularApps: name = unicode(app.localizedName()) # log that the app recording will stop text = '{"time": '+ str(t) + ' , "type": "Close", "app": "' + name + '"}' utils_cocoa.write_to_file(text, cfg.APPLOG) if app.isActive(): text = '{"time": '+ str(t) + ' , "type": "Inactive", "app": "' + name + '"}' utils_cocoa.write_to_file(text, cfg.APPLOG) # write a blank line to the geometry table to close out all windows text = '{"time": ' + str(t) + ', "geometry": {} }' utils_cocoa.write_to_file(text, cfg.GEOLOG) def unpause_app_observers(self): t = cfg.NOW() # get workspace and list of all applications workspace = NSWorkspace.sharedWorkspace() activeApps = workspace.runningApplications() # prune list of applications to apps that appear in the dock regularApps = [] for app in activeApps: if app.activationPolicy() == 0: regularApps.append(app) # listen for window events of these applications for app in regularApps: name = unicode(app.localizedName()) # log that the app recording will stop text = '{"time": '+ str(t) + ' , "type": "Open", "app": "' + name + '"}' utils_cocoa.write_to_file(text, cfg.APPLOG) if app.isActive(): text = '{"time": '+ str(t) + ' , "type": "Active", "app": "' + name + '"}' utils_cocoa.write_to_file(text, cfg.APPLOG) # check if the screen geometry changed and update active window self.updateWindowList()