mirror of
https://github.com/munki/munki.git
synced 2026-04-24 13:59:56 -05:00
Merging forced_install_after_date branch to trunk.
This adds Force Install Notifications support to the MSU GUI, and logouthelper support to managedsoftwareupdate/launchd. Documentation on using the pkginfo force_install_after_date key to come.... This merge also includes localization fixes and on-the-fly updating of the MSU GUI when managedsoftwareupdate runs in the background while the GUI is open, changing InstallInfo. With this merge, the Munki version is increased to 0.8.0 and MSU GUI version to 3.2. git-svn-id: http://munki.googlecode.com/svn/trunk@1270 a4e17f2e-e282-11dd-95e1-755cbddbdd66
This commit is contained in:
Executable
+165
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/python
|
||||
# encoding: utf-8
|
||||
# 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
|
||||
#
|
||||
# http://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.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
from munkilib import munkicommon
|
||||
from munkilib import FoundationPlist
|
||||
from munkilib.updatecheck import discardTimeZoneFromDate
|
||||
from Foundation import NSDate
|
||||
from Foundation import NSDictionary
|
||||
from Foundation import NSDistributedNotificationCenter
|
||||
from Foundation import NSNotificationDeliverImmediately
|
||||
from Foundation import NSNotificationPostToAllSessions
|
||||
|
||||
|
||||
NOTIFICATION_MINS = [240, 180, 120, 90, 60, 45, 30, 15, 10, 5]
|
||||
MINIMUM_NOTIFICATION_MINS = 60
|
||||
|
||||
|
||||
def earliestForceInstallDate():
|
||||
'''Check installable packages for force_install_after_dates
|
||||
Returns None or earliest force_install_after_date converted to local time
|
||||
'''
|
||||
earliest_date = None
|
||||
|
||||
ManagedInstallDir = munkicommon.pref('ManagedInstallDir')
|
||||
installinfopath = os.path.join(ManagedInstallDir, 'InstallInfo.plist')
|
||||
|
||||
try:
|
||||
installinfo = FoundationPlist.readPlist(installinfopath)
|
||||
except FoundationPlist.NSPropertyListSerializationException:
|
||||
return None
|
||||
|
||||
for install in installinfo.get('managed_installs', []):
|
||||
this_force_install_date = install.get('force_install_after_date')
|
||||
|
||||
if this_force_install_date:
|
||||
this_force_install_date = discardTimeZoneFromDate(
|
||||
this_force_install_date)
|
||||
if not earliest_date or this_force_install_date < earliest_date:
|
||||
earliest_date = this_force_install_date
|
||||
|
||||
return earliest_date
|
||||
|
||||
|
||||
def alertUserOfForcedLogout(info=None):
|
||||
'''Uses Managed Software Update.app to notify the user of an
|
||||
upcoming forced logout.
|
||||
|
||||
Args:
|
||||
info: dict of data to send with the notification.
|
||||
'''
|
||||
consoleuser = munkicommon.getconsoleuser()
|
||||
if not munkicommon.findProcesses(
|
||||
exe="/Applications/Utilities/Managed Software Update.app",
|
||||
user=consoleuser):
|
||||
# Managed Software Update.app isn't running.
|
||||
# Use our LaunchAgent to start
|
||||
# Managed Software Update.app in the user context.
|
||||
launchfile = '/var/run/com.googlecode.munki.ManagedSoftwareUpdate'
|
||||
f = open(launchfile, 'w')
|
||||
f.close()
|
||||
time.sleep(0.1)
|
||||
if os.path.exists(launchfile):
|
||||
os.unlink(launchfile)
|
||||
# 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(4)
|
||||
|
||||
# if set, convert Python dictionary to NSDictionary.
|
||||
if info is not None:
|
||||
info = NSDictionary.dictionaryWithDictionary_(info)
|
||||
# cause MSU.app to display the Forced Logout warning
|
||||
dnc = NSDistributedNotificationCenter.defaultCenter()
|
||||
dnc.postNotificationName_object_userInfo_options_(
|
||||
'com.googlecode.munki.ManagedSoftwareUpdate.logoutwarn',
|
||||
None, info,
|
||||
NSNotificationDeliverImmediately + NSNotificationPostToAllSessions)
|
||||
|
||||
# make sure flag is in place to cause munki to install at logout
|
||||
f = open('/private/tmp/com.googlecode.munki.installatlogout', 'w')
|
||||
f.close()
|
||||
|
||||
|
||||
def main():
|
||||
'''Check for logged-in users and upcoming forced installs;
|
||||
notify the user if needed; sleep a minute and do it again.'''
|
||||
ID = 'com.googlecode.munki.logouthelper'
|
||||
munkicommon.log('%s invoked' % ID)
|
||||
sent_notifications = []
|
||||
logout_time_override = False
|
||||
minimum_notifications_logout_time = NSDate.date().addTimeInterval_(
|
||||
60 * MINIMUM_NOTIFICATION_MINS + 30)
|
||||
while True:
|
||||
if not munkicommon.currentGUIusers():
|
||||
# no-one is logged in, so bail
|
||||
munkicommon.log('%s: no-one logged in' % ID)
|
||||
time.sleep(10) # makes launchd happy
|
||||
munkicommon.log('%s exited' % ID)
|
||||
exit(0)
|
||||
|
||||
if not logout_time_override:
|
||||
logout_time = earliestForceInstallDate()
|
||||
if not logout_time:
|
||||
# no forced logout needed, so bail
|
||||
munkicommon.log('%s: no forced installs found' % ID)
|
||||
time.sleep(10) # makes launchd happy
|
||||
munkicommon.log('%s exited' % ID)
|
||||
exit(0)
|
||||
elif logout_time < minimum_notifications_logout_time:
|
||||
if MINIMUM_NOTIFICATION_MINS not in sent_notifications:
|
||||
# logout time is in the past, and the minimum notification
|
||||
# has not been sent, so reset the logout_time to the future.
|
||||
munkicommon.log('%d minute notification not sent.' % (
|
||||
MINIMUM_NOTIFICATION_MINS))
|
||||
logout_time = minimum_notifications_logout_time
|
||||
munkicommon.log('Reset logout_time to: %s' % logout_time)
|
||||
logout_time_override = True
|
||||
|
||||
minutes_until_logout = int(logout_time.timeIntervalSinceNow() / 60)
|
||||
info = {'logout_time': logout_time}
|
||||
if minutes_until_logout in NOTIFICATION_MINS:
|
||||
sent_notifications.append(minutes_until_logout)
|
||||
munkicommon.log(
|
||||
'%s: Warning user of %s minutes until forced logout' %
|
||||
(ID, minutes_until_logout))
|
||||
alertUserOfForcedLogout(info)
|
||||
elif minutes_until_logout < 1:
|
||||
munkicommon.log('%s: Forced logout in 60 seconds' % ID)
|
||||
alertUserOfForcedLogout(info)
|
||||
|
||||
time.sleep(60)
|
||||
if minutes_until_logout < 1:
|
||||
break
|
||||
|
||||
if munkicommon.currentGUIusers() and earliestForceInstallDate():
|
||||
munkicommon.log('%s: Beginning forced logout' % ID)
|
||||
munkicommon.forceLogoutNow()
|
||||
munkicommon.log('%s exited' % ID)
|
||||
exit(0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -31,6 +31,9 @@ import traceback
|
||||
# Do not place any imports with ObjC bindings above this!
|
||||
try:
|
||||
from Foundation import NSDate
|
||||
from Foundation import NSDistributedNotificationCenter
|
||||
from Foundation import NSNotificationDeliverImmediately
|
||||
from Foundation import NSNotificationPostToAllSessions
|
||||
except:
|
||||
# Python is missing ObjC bindings. Run external report script.
|
||||
from munkilib import utils
|
||||
@@ -195,9 +198,14 @@ def doInstallTasks(only_unattended=False):
|
||||
return need_to_restart
|
||||
|
||||
|
||||
def doLogout():
|
||||
"""Handle the need for a logout."""
|
||||
munkicommon.forceLogoutNow()
|
||||
def startLogoutHelper():
|
||||
"""Handle the need for a forced logout. Start our logouthelper"""
|
||||
cmd = ['/bin/launchctl', 'start', 'com.googlecode.munki.logouthelper']
|
||||
result = subprocess.call(cmd)
|
||||
if result:
|
||||
# some problem with the launchd job
|
||||
munkicommon.display_error(
|
||||
'Could not start com.googlecode.munki.logouthelper')
|
||||
|
||||
|
||||
def doRestart():
|
||||
@@ -212,8 +220,14 @@ def doRestart():
|
||||
else:
|
||||
munkicommon.display_info(restartMessage)
|
||||
|
||||
if not munkicommon.currentGUIusers():
|
||||
# no-one is logged in and we're at the loginwindow
|
||||
# TODO: temporary fix for forced logout problem where we've killed
|
||||
# loginwindow sessions, but munkicommon.currentGUIusers() still returns
|
||||
# users. Need to find a better solution, though.
|
||||
#if not munkicommon.currentGUIusers():
|
||||
# # no-one is logged in and we're at the loginwindow
|
||||
consoleuser = munkicommon.getconsoleuser()
|
||||
if not consoleuser or consoleuser == u'loginwindow':
|
||||
# no-one is logged in or we're at the loginwindow
|
||||
time.sleep(5)
|
||||
unused_retcode = subprocess.call(['/sbin/shutdown', '-r', 'now'])
|
||||
else:
|
||||
@@ -253,6 +267,16 @@ def recordUpdateCheckResult(result):
|
||||
munkicommon.set_pref('LastCheckResult', result)
|
||||
|
||||
|
||||
def sendUpdateNotification():
|
||||
'''Sends an update notification via NSDistributedNotificationCenter
|
||||
MSU.app registers to receive these events.'''
|
||||
dnc = NSDistributedNotificationCenter.defaultCenter()
|
||||
dnc.postNotificationName_object_userInfo_options_(
|
||||
'com.googlecode.munki.ManagedSoftwareUpdate.update',
|
||||
None, None,
|
||||
NSNotificationDeliverImmediately + NSNotificationPostToAllSessions)
|
||||
|
||||
|
||||
def notifyUserOfUpdates(force=False):
|
||||
"""Notify the logged-in user of available updates.
|
||||
|
||||
@@ -282,25 +306,13 @@ def notifyUserOfUpdates(force=False):
|
||||
if force or now.timeIntervalSinceDate_(nextNotifyDate) >= 0:
|
||||
# record current notification date
|
||||
munkicommon.set_pref('LastNotifiedDate', now)
|
||||
|
||||
# Kill Managed Software Update.app if it's already
|
||||
# open so it will update its display
|
||||
# using subprocess.Popen instead of subprocess.call
|
||||
# so stderr doesn't get output to the terminal
|
||||
# when there is no Managed Software Update process running
|
||||
cmd = ['/usr/bin/killall', 'Managed Software Update']
|
||||
proc = subprocess.Popen(cmd, shell=False, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
(unused_output, unused_err) = proc.communicate()
|
||||
|
||||
# notify user of available updates using LaunchAgent to start
|
||||
# Managed Software Update.app in the user context.
|
||||
launchfile = '/var/run/com.googlecode.munki.ManagedSoftwareUpdate'
|
||||
cmd = ['/usr/bin/touch', launchfile]
|
||||
unused_retcode = subprocess.call(cmd)
|
||||
|
||||
time.sleep(0.1)
|
||||
time.sleep(1)
|
||||
if os.path.exists(launchfile):
|
||||
os.unlink(launchfile)
|
||||
user_was_notified = True
|
||||
@@ -585,7 +597,7 @@ def main():
|
||||
|
||||
if updatecheckresult is not None:
|
||||
recordUpdateCheckResult(updatecheckresult)
|
||||
|
||||
|
||||
updatesavailable = munkiUpdatesAvailable()
|
||||
appleupdatesavailable = False
|
||||
if (not updatesavailable and not options.installonly and
|
||||
@@ -619,6 +631,9 @@ def main():
|
||||
munkicommon.display_error(traceback.format_exc())
|
||||
munkicommon.savereport()
|
||||
exit(-1)
|
||||
|
||||
# send a notification event so MSU can update its display if needed
|
||||
sendUpdateNotification()
|
||||
|
||||
mustrestart = False
|
||||
mustlogout = False
|
||||
@@ -667,6 +682,8 @@ def main():
|
||||
pass
|
||||
elif force_action:
|
||||
notifyUserOfUpdates(force=True)
|
||||
time.sleep(2)
|
||||
startLogoutHelper()
|
||||
elif not munkicommon.pref('SuppressUserNotification'):
|
||||
notifyUserOfUpdates()
|
||||
else:
|
||||
@@ -719,8 +736,8 @@ def main():
|
||||
munkicommon.cleanUpTmpDir()
|
||||
if mustrestart:
|
||||
doRestart()
|
||||
elif mustlogout:
|
||||
doLogout()
|
||||
#elif mustlogout:
|
||||
# doForcedLogout() # indirectly done via logouthelper
|
||||
elif munkicommon.munkistatusoutput:
|
||||
munkistatus.quit()
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ def set_file_nonblock(f, non_blocking=True):
|
||||
|
||||
|
||||
class Popen(subprocess.Popen):
|
||||
'''Subclass of subprocess.Popen to add support for
|
||||
'''Subclass of subprocess.Popen to add support for
|
||||
timeouts for some operations.'''
|
||||
def timed_readline(self, f, timeout):
|
||||
"""Perform readline-like operation with timeout.
|
||||
@@ -1144,7 +1144,7 @@ def getOnePackageInfo(pkgpath):
|
||||
#if bomlist:
|
||||
# pkginfo['apps'] = [os.path.basename(item) for item in bomlist
|
||||
# if item.endswith('.app')]
|
||||
|
||||
|
||||
else:
|
||||
# look for old-style .info files!
|
||||
infopath = os.path.join(pkgpath, 'Contents', 'Resources',
|
||||
@@ -1371,7 +1371,7 @@ def nameAndVersion(aString):
|
||||
version = aString[index:]
|
||||
return (aString[0:index].rstrip(' .-_v'), version)
|
||||
else:
|
||||
# no version number found,
|
||||
# no version number found,
|
||||
# just return original string and empty string
|
||||
return (aString, '')
|
||||
|
||||
@@ -1856,24 +1856,23 @@ def listdir(path):
|
||||
return os.listdir(path)
|
||||
|
||||
|
||||
def findProcesses(user=None, exe=None, args=None):
|
||||
def findProcesses(user=None, exe=None):
|
||||
"""Find processes in process list.
|
||||
|
||||
Args:
|
||||
user: str, optional, username owning process
|
||||
exe: str, optional, executable name of process
|
||||
args: str, optional, string arguments to match to process
|
||||
Returns:
|
||||
dictionary of pids = {
|
||||
pid: {
|
||||
'user': str, username owning process,
|
||||
'args': str, string executable and arguments of process,
|
||||
'exe': str, string executable of process,
|
||||
}
|
||||
}
|
||||
|
||||
list of pids, or {} if none
|
||||
"""
|
||||
argv = ['/bin/ps', '-x', '-w', '-w', '-a', '-o', 'pid=,user=,args=']
|
||||
argv = ['/bin/ps', '-x', '-w', '-w', '-a', '-o', 'pid=,user=,comm=']
|
||||
|
||||
p = subprocess.Popen(
|
||||
argv,
|
||||
@@ -1889,20 +1888,17 @@ def findProcesses(user=None, exe=None, args=None):
|
||||
lines = stdout.splitlines()
|
||||
|
||||
for proc in lines:
|
||||
(p_pid, p_user, p_args) = proc.split(None, 2)
|
||||
(p_pid, p_user, p_comm) = proc.split(None, 2)
|
||||
|
||||
if exe is not None:
|
||||
if not p_args.startswith(exe):
|
||||
continue
|
||||
if args is not None:
|
||||
if not p_args.find(args) > -1:
|
||||
if not p_comm.startswith(exe):
|
||||
continue
|
||||
if user is not None:
|
||||
if p_user != user:
|
||||
continue
|
||||
pids[int(p_pid)] = {
|
||||
'user': p_user,
|
||||
'args': p_args,
|
||||
'exe': p_comm,
|
||||
}
|
||||
|
||||
except (ValueError, TypeError, IndexError):
|
||||
@@ -1911,6 +1907,7 @@ def findProcesses(user=None, exe=None, args=None):
|
||||
return pids
|
||||
|
||||
|
||||
|
||||
def forceLogoutNow():
|
||||
"""Force the logout of interactive GUI users and spawn MSU."""
|
||||
try:
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.7.3</string>
|
||||
<string>0.8.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Reference in New Issue
Block a user