Files
munki/code/client/app_usage_monitor

177 lines
6.3 KiB
Python
Executable File

#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2017 Greg Neagle.
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
app_usage_monitor
Created by Greg Neagle 14 Feb 2017
A tool to monitor application usage and record it to a database.
Borrowing lots of code and ideas from the crankd project, part of pyamcadmin:
https://github.com/MacSysadmin/pymacadmin
and the application_usage scripts created by Google MacOps:
https://github.com/google/macops/tree/master/crankd
"""
# standard Python libs
import logging
import os
import sys
try:
# Apple frameworks
from AppKit import NSWorkspace
from Foundation import NSDate
from Foundation import NSDictionary
from Foundation import NSDistributedNotificationCenter
from Foundation import NSObject
from Foundation import NSRunLoop
except ImportError:
logging.critical("PyObjC wrappers for Apple frameworks are missing.")
sys.exit(-1)
# our libs
from munkilib import app_usage
from munkilib import prefs
class NotificationHandler(NSObject):
"""A subclass of NSObject to handle workspace notifications"""
# Disable PyLint complaining about 'invalid' camelCase names
# pylint: disable=C0103
def init(self):
"""NSObject-compatible initializer"""
self = super(NotificationHandler, self).init()
if self is None:
return None
self.usage = app_usage.ApplicationUsageRecorder()
self.ws_nc = NSWorkspace.sharedWorkspace().notificationCenter()
self.ws_nc.addObserver_selector_name_object_(
self, 'didLaunchApplicationNotification:',
'NSWorkspaceDidLaunchApplicationNotification', None)
self.ws_nc.addObserver_selector_name_object_(
self, 'didActivateApplicationNotification:',
'NSWorkspaceDidActivateApplicationNotification', None)
self.ws_nc.addObserver_selector_name_object_(
self, 'didTerminateApplicationNotification:',
'NSWorkspaceDidTerminateApplicationNotification', None)
self.dnc = NSDistributedNotificationCenter.defaultCenter()
self.dnc.addObserver_selector_name_object_(
self, 'requestedItemForInstall:',
'com.googlecode.munki.managedsoftwareupdate.installrequest', None)
return self # NOTE: Unlike Python, NSObject's init() must return self!
def __del__(self):
"""Unregister for all the notifications we registered for"""
self.ws_nc.removeObserver_(self)
self.dnc.removeObserver_(self)
def get_app_dict(self, app_object):
"""Returns a dict with info about an application.
Args:
app_object: NSRunningApplication object
Returns:
app_dict: {bundle_id: str,
path: str,
version: str}"""
# pylint: disable=no-self-use
if app_object:
try:
url = app_object.bundleURL()
app_path = url.path()
except AttributeError:
app_path = None
try:
bundle_id = app_object.bundleIdentifier()
except AttributeError:
# use the base filename
if app_path:
bundle_id = os.path.basename(app_path)
if app_path:
app_info_plist = NSDictionary.dictionaryWithContentsOfFile_(
'%s/Contents/Info.plist' % app_path)
if app_info_plist:
app_version = app_info_plist.get(
'CFBundleShortVersionString',
app_info_plist.get('CFBundleVersion', '0'))
else:
app_version = '0'
return {'bundle_id': bundle_id,
'path': app_path,
'version': app_version}
def didLaunchApplicationNotification_(self, notification):
"""Handle NSWorkspaceDidLaunchApplicationNotification"""
app_object = notification.userInfo().get('NSWorkspaceApplicationKey')
app_dict = self.get_app_dict(app_object)
self.usage.log_application_usage('launch', app_dict)
def didActivateApplicationNotification_(self, notification):
"""Handle NSWorkspaceDidActivateApplicationNotification"""
app_object = notification.userInfo().get('NSWorkspaceApplicationKey')
app_dict = self.get_app_dict(app_object)
self.usage.log_application_usage('activate', app_dict)
def didTerminateApplicationNotification_(self, notification):
"""Handle NSWorkspaceDidTerminateApplicationNotification"""
app_object = notification.userInfo().get('NSWorkspaceApplicationKey')
app_dict = self.get_app_dict(app_object)
self.usage.log_application_usage('quit', app_dict)
def requestedItemForInstall_(self, notification):
"""Handle com.googlecode.munki.managedsoftwareupdate.installrequest"""
logging.info('got install request notification')
user_info = notification.userInfo()
self.usage.log_install_request(user_info)
def main():
"""Initialize our handler object and let NSWorkspace's notification center
know we are interested in notifications"""
# configure logging
logpath = os.path.join(
os.path.dirname(prefs.pref('LogFile')), 'app_usage_monitor.log')
logging.basicConfig(
filename=logpath,
format='%(asctime)s %(levelname)s:%(message)s',
level=logging.INFO)
logging.info('app_usage_monitor started')
# PyLint can't tell that NotificationHandler' NSObject superclass
# has an alloc() method
# pylint: disable=no-member
notification_handler = NotificationHandler.alloc().init()
# pylint: enable=no-member
while True:
# listen for notifications forever
NSRunLoop.currentRunLoop().runUntilDate_(
NSDate.dateWithTimeIntervalSinceNow_(0.1))
if __name__ == '__main__':
main()