#!/usr/bin/python # encoding: utf-8 # # Copyright 2011-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. """ logouthelper Created by Greg Neagle on 2011-06-21. A helper tool for forced logouts to allow munki to force install items by a certain deadline. """ # standard libs import os import time # our libs from munkilib import info from munkilib import munkilog from munkilib import osutils from munkilib import prefs from munkilib import processes from munkilib import FoundationPlist # Apple frameworks via PyObjC # PyLint cannot properly find names inside Cocoa libraries, so issues bogus # No name 'Foo' in module 'Bar' warnings. Disable them. # pylint: disable=E0611 from Foundation import NSDate from Foundation import NSDictionary from Foundation import NSDistributedNotificationCenter from Foundation import NSNotificationDeliverImmediately from Foundation import NSNotificationPostToAllSessions # pylint: enable=E0611 NOTIFICATION_MINS = [240, 180, 120, 90, 60, 45, 30, 15, 10, 5] MANDATORY_NOTIFICATIONS = [60, 30, 10, 5] PROCESS_ID = 'com.googlecode.munki.logouthelper' def log(msg): '''Logs messages from this tool with an identifier''' munkilog.log('%s: %s' % (PROCESS_ID, msg)) def earliest_force_install_date(): '''Check installable packages for force_install_after_dates Returns None or earliest force_install_after_date converted to local time ''' earliest_date = None managed_install_dir = prefs.pref('ManagedInstallDir') installinfo_types = { 'InstallInfo.plist' : 'managed_installs', 'AppleUpdates.plist': 'AppleUpdates' } installinfopath = os.path.join(managed_install_dir, 'InstallInfo.plist') try: installinfo = FoundationPlist.readPlist(installinfopath) except FoundationPlist.NSPropertyListSerializationException: return None for plist_name in installinfo_types: key_to_check = installinfo_types[plist_name] plist_path = os.path.join(managed_install_dir, plist_name) try: installinfo = FoundationPlist.readPlist(plist_path) except FoundationPlist.NSPropertyListSerializationException: continue for install in installinfo.get(key_to_check, []): force_install_date = install.get('force_install_after_date') if force_install_date: force_install_date = info.subtract_tzoffset_from_date( force_install_date) if not earliest_date or force_install_date < earliest_date: earliest_date = force_install_date return earliest_date def alert_user_of_forced_logout(info_dict=None): '''Uses Managed Software Center.app to notify the user of an upcoming forced logout. Args: info: dict of data to send with the notification. ''' consoleuser = osutils.getconsoleuser() if not processes.find_processes( exe="/Applications/Managed Software Center.app", user=consoleuser): # Managed Software Center.app isn't running. # Use our LaunchAgent to start # Managed Software Center.app in the user context. launchfile = '/var/run/com.googlecode.munki.ManagedSoftwareCenter' fileref = open(launchfile, 'w') fileref.close() # now wait a bit for it to launch before proceeding # because if we don't, sending the logoutwarn notification # may fall on deaf ears. time.sleep(5) if os.path.exists(launchfile): os.unlink(launchfile) # if set, convert Python dictionary to NSDictionary. if info_dict is not None: info_dict = NSDictionary.dictionaryWithDictionary_(info_dict) # cause MSC.app to display the Forced Logout warning dnc = NSDistributedNotificationCenter.defaultCenter() dnc.postNotificationName_object_userInfo_options_( 'com.googlecode.munki.ManagedSoftwareUpdate.logoutwarn', None, info_dict, NSNotificationDeliverImmediately + NSNotificationPostToAllSessions) # make sure flag is in place to cause munki to install at logout fileref = open('/private/tmp/com.googlecode.munki.installatlogout', 'w') fileref.close() def main(): '''Check for logged-in users and upcoming forced installs; notify the user if needed; sleep a minute and do it again.''' if prefs.pref('LogToSyslog'): munkilog.configure_syslog() log('launched') sent_notifications = [] logout_time_override = None # datetime of now plus largest MANDATORY_NOTIFICATIONS value (with padding). minimum_notifications_logout_time = NSDate.date().addTimeInterval_( 60 * max(MANDATORY_NOTIFICATIONS) + 30) while True: if not osutils.currentGUIusers(): # no-one is logged in, so bail log('no-one logged in') time.sleep(10) # makes launchd happy log('exited') exit(0) # we check each time because items might have been added or removed # from the list; or their install date may have been changed. next_logout_time = earliest_force_install_date() if not next_logout_time: # no forced logout needed, so bail log('no forced installs found') time.sleep(10) # makes launchd happy log('exited') exit(0) if logout_time_override is None: logout_time = next_logout_time else: # allow the new next_logout_time from InstallInfo to be used # if it has changed to a later time since when we decided to # override it. if next_logout_time > logout_time_override: logout_time = next_logout_time log('reset logout_time to: %s' % logout_time) logout_time_override = None sent_notifications = [] # always give at least MANDATORY_NOTIFICATIONS warnings if logout_time < minimum_notifications_logout_time: for mandatory_notification in MANDATORY_NOTIFICATIONS: if mandatory_notification not in sent_notifications: # logout time is in the past, and a mandatory notification # has not been sent, so reset the logout_time to the future. log('%d minute notification not sent.' % mandatory_notification) logout_time = NSDate.date( ).addTimeInterval_(60 * mandatory_notification + 30) log('reset logout_time to: %s' % logout_time) logout_time_override = logout_time break minutes_until_logout = int(logout_time.timeIntervalSinceNow() / 60) info_dict = {'logout_time': logout_time} if minutes_until_logout in NOTIFICATION_MINS: sent_notifications.append(minutes_until_logout) log('Warning user of %s minutes until forced logout' % minutes_until_logout) alert_user_of_forced_logout(info_dict) elif minutes_until_logout < 1: log('Forced logout in 60 seconds') alert_user_of_forced_logout(info_dict) time.sleep(60) if minutes_until_logout < 1: break if osutils.currentGUIusers() and earliest_force_install_date(): log('Beginning forced logout') processes.force_logout_now() log('exited') exit(0) if __name__ == '__main__': main()