Example #1
0
	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 = []
Example #2
0
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()