Track user choices for install/removal; employ a different strategy for determining if an updatecheck is needed; display links to updates page for some item statuses

This commit is contained in:
Greg Neagle
2014-04-10 15:23:41 -07:00
parent b231f863d5
commit b01a330c2a
9 changed files with 165 additions and 85 deletions
@@ -33,6 +33,7 @@ from MSUStatusController import MSUStatusController
import munki
import msuhtml
import msulog
import MunkiItems
class MSUAppDelegate(NSObject):
@@ -68,23 +69,26 @@ class MSUAppDelegate(NSObject):
lastcheck = NSDate.date()
else:
lastcheck = munki.pref('LastCheckDate')
max_cache_age = munki.pref('CheckResultsCacheSeconds')
# if there is no lastcheck timestamp, check for updates.
if not lastcheck:
self.mainWindowController.checkForUpdates()
# otherwise, only check for updates if the last check is over the
# configured manualcheck cache age max.
max_cache_age = munki.pref('CheckResultsCacheSeconds')
if lastcheck.timeIntervalSinceNow() * -1 > int(max_cache_age):
elif lastcheck.timeIntervalSinceNow() * -1 > int(max_cache_age):
# check for updates if the last check is over the
# configured manualcheck cache age max.
self.mainWindowController.checkForUpdates()
# load the initial only if we are not already loading something else.
elif MunkiItems.updateCheckNeeded():
# check for updates if we have optional items selected for install
# or removal that have not yet been processed
self.mainWindowController.checkForUpdates()
# load the initial view only if we are not already loading something else.
# enables launching the app to a specific panel, eg. from URL handler
if not self.mainWindowController.webView.isLoading():
self.mainWindowController.loadInitialView()
self.mainWindowController.loadInitialView()
# below is the URL handler for calls outside the app eg. web clicks
def applicationWillFinishLaunching_(self, notification):
'''Installs URL handler for calls outside the app eg. web clicks'''
man = NSAppleEventManager.sharedAppleEventManager()
man.setEventHandler_andSelector_forEventClass_andEventID_(
self,
@@ -93,6 +97,7 @@ class MSUAppDelegate(NSObject):
struct.unpack(">i", "GURL")[0])
def openURL_withReplyEvent_(self, event, replyEvent):
'''Handle openURL messages'''
keyDirectObject = struct.unpack(">i", "----")[0]
url = event.paramDescriptorForKeyword_(keyDirectObject).stringValue().decode('utf8')
NSLog("Called by external URL: %@", url)
@@ -232,9 +232,9 @@ class MSUMainWindowController(NSWindowController):
# all done checking and/or installing: display results
self.resetAndReload()
if self._update_queue:
# more stuff pending? Let's do it
self._update_queue.clear()
if MunkiItems.updateCheckNeeded():
# more stuff pending? Let's do it...
self.updateNow()
@AppHelper.endSheetMethod
@@ -355,15 +355,19 @@ class MSUMainWindowController(NSWindowController):
def checkForUpdates(self, suppress_apple_update_check=False):
'''start an update check session'''
# attempt to start the update check
if self._update_in_progress:
return
result = munki.startUpdateCheck(suppress_apple_update_check)
if result == 0:
self._update_in_progress = True
self.displayUpdateCount()
self.managedsoftwareupdate_task = "manualcheck"
NSApp.delegate().statusController.startMunkiStatusSession()
self.markRequestedItemsAsProcessing()
else:
self.munkiStatusSessionEnded_(2)
def kickOffUpdateSession(self):
def kickOffInstallSession(self):
'''start an update install/removal session'''
# check for need to logout, restart, firmware warnings
# warn about blocking applications, etc...
@@ -407,6 +411,14 @@ class MSUMainWindowController(NSWindowController):
for item in install_info.get('managed_installs', [])]
items_to_be_removed_names = [item['name']
for item in install_info.get('removals', [])]
for name in items_to_be_installed_names:
# remove names for user selections since we are installing
MunkiItems.user_install_selections.discard(name)
for name in items_to_be_removed_names:
# remove names for user selections since we are removing
MunkiItems.user_removal_selections.discard(name)
for item in MunkiItems.getOptionalInstallItems():
new_status = None
@@ -426,7 +438,7 @@ class MSUMainWindowController(NSWindowController):
NSLog('markRequestedItemsAsProcessing')
for item in MunkiItems.getOptionalInstallItems():
new_status = None
NSLog('Status for %s is %s' % (item['name'], item['status']))
#NSLog('Status for %s is %s' % (item['name'], item['status']))
if item['status'] == 'install-requested':
NSLog('Setting status for %s to "downloading"' % item['name'])
new_status = u'downloading'
@@ -440,17 +452,16 @@ class MSUMainWindowController(NSWindowController):
def updateNow(self):
'''If user has added to/removed from the list of things to be updated,
run a check session. If there are no more changes, proceed to an update
installation session'''
installation session if items to be installed/removed are exclusively
those selected by the user in this session'''
if self.stop_requested:
# reset the flag
self.stop_requested = False
self.resetAndReload()
return
current_self_service = MunkiItems.SelfService()
if current_self_service != self.cached_self_service:
NSLog('selfService choices changed')
# recache SelfService
self.cached_self_service = current_self_service
if MunkiItems.updateCheckNeeded():
# any item status changes that require an update check?
NSLog('updateCheck needed')
msulog.log("user", "check_then_install_without_logout")
# since we are just checking for changed self-service items
# we can suppress the Apple update check
@@ -458,7 +469,6 @@ class MSUMainWindowController(NSWindowController):
self._update_in_progress = True
self.displayUpdateCount()
result = munki.startUpdateCheck(suppress_apple_update_check)
result = 0
if result:
NSLog("Error starting check-then-install session: %s" % result)
self.munkiStatusSessionEnded_(2)
@@ -466,17 +476,19 @@ class MSUMainWindowController(NSWindowController):
self.managedsoftwareupdate_task = "checktheninstall"
NSApp.delegate().statusController.startMunkiStatusSession()
self.markRequestedItemsAsProcessing()
elif not self._alertedUserToOutstandingUpdates and MunkiItems.updatesContainNonOptionalItems():
elif (not self._alertedUserToOutstandingUpdates
and MunkiItems.updatesContainNonUserSelectedItems()):
# current list of updates contains some not explicitly chosen by the user
NSLog('updateCheck not needed, items require user approval')
self._update_in_progress = False
self.displayUpdateCount()
self.loadUpdatesPage_(self)
self._alertedUserToOutstandingUpdates = True
self.alert_controller.alertToExtraUpdates()
else:
NSLog('selfService choices unchanged')
NSLog('updateCheck not needed')
self._alertedUserToOutstandingUpdates = False
self.kickOffUpdateSession()
self.kickOffInstallSession()
def getUpdateCount(self):
'''Get the count of effective updates'''
@@ -672,10 +684,8 @@ class MSUMainWindowController(NSWindowController):
elif self.getUpdateCount() == 0:
# no updates, this button must say "Check Again"
self._update_in_progress = True
self.loadUpdatesPage_(self)
self.displayUpdateCount()
self.checkForUpdates()
self.loadUpdatesPage_(self)
else:
# must say "Update"
self.updateNow()
@@ -789,14 +799,9 @@ class MSUMainWindowController(NSWindowController):
# update count badges
self.displayUpdateCount()
if (previous_status in ['update-will-be-installed', 'will-be-installed', 'will-be-removed']
or item['status'] in ['install-requested', 'removal-requested']):
# item was processed and cached for install or removal. Need to run
# an updatecheck session to possibly remove other items (dependencies
# or updates) from the pending list
if MunkiItems.updateCheckNeeded():
# check for updates after a short delay so UI changes visually complete first
self.performSelector_withObject_afterDelay_(self.checkForUpdates, True, 1.0)
self._update_in_progress = True
def myItemsActionButtonClicked_(self, item_name):
'''this method is called from JavaScript when the user clicks
@@ -810,18 +815,11 @@ class MSUMainWindowController(NSWindowController):
return
item.update_status()
if item.get('will_be_installed') or item.get('will_be_removed'):
# item was processed and cached for install or removal. Need to run
# an updatecheck session to possibly remove other items (dependencies
# or updates) from the pending list
if MunkiItems.updateCheckNeeded():
if not self._update_in_progress:
self._update_in_progress = True
self.displayUpdateCount()
self.checkForUpdates(suppress_apple_update_check=True)
self.performSelector_withObject_afterDelay_(self.checkForUpdates, True, 0.1)
else:
# add to queue to check later
# TO-DO: fix this as this can trigger an install as well
#self._update_queue.add(item['name'])
# will happen later...
pass
self.displayUpdateCount()
@@ -835,10 +833,6 @@ class MSUMainWindowController(NSWindowController):
btn.setInnerText_(item['myitem_action_text'])
status_line.setInnerText_(item['status_text'])
status_line.setClassName_('status %s' % item['status'])
if not self._update_in_progress:
if item['status'] in ['will-be-installed', 'update-will-be-installed',
'will-be-removed']:
self.updateNow()
def updateDOMforOptionalItem(self, item):
'''Update displayed status of an item'''
@@ -33,6 +33,8 @@ from AppKit import *
import FoundationPlist
user_install_selections = set()
user_removal_selections = set()
# place to cache our expensive-to-calculate data
_cache = {}
@@ -63,6 +65,12 @@ def getOptionalInstallItems():
return _cache['optional_install_items']
def updateCheckNeeded():
'''Returns True if any item in optional installs list has 'updatecheck_needed' == True'''
return len([item for item in getOptionalInstallItems()
if item.get('updatecheck_needed')]) != 0
def optionalItemForName_(item_name):
for item in getOptionalInstallItems():
if item['name'] == item_name:
@@ -72,12 +80,13 @@ def optionalItemForName_(item_name):
def getOptionalWillBeInstalledItems():
return [item for item in getOptionalInstallItems()
if item['status'] in ['will-be-installed', 'update-will-be-installed']]
if item['status'] in ['install-requested', 'will-be-installed',
'update-will-be-installed', 'install-error']]
def getOptionalWillBeRemovedItems():
return [item for item in getOptionalInstallItems()
if item['status'] == 'will-be-removed']
if item['status'] in ['removal-requested', 'will-be-removed', 'removal-error']]
def getUpdateList():
@@ -134,7 +143,7 @@ def updatesRequireRestart():
if 'Restart' in item.get('RestartAction', '')]) > 0
def updatesContainNonOptionalItems():
def updatesContainNonUserSelectedItems():
'''Does the list of updates contain items not selected by the user?'''
if not munki.munkiUpdatesContainAppleItems() and getAppleUpdates():
# available Apple updates are not user selected
@@ -143,11 +152,11 @@ def updatesContainNonOptionalItems():
install_items = install_info.get('managed_installs', [])
removal_items = install_info.get('removals', [])
filtered_installs = [item for item in install_items
if item['name'] not in SelfService().installs()]
if item['name'] not in user_install_selections]
if filtered_installs:
return True
filtered_uninstalls = [item for item in removal_items
if item['name'] not in SelfService().uninstalls()]
if item['name'] not in user_removal_selections]
if filtered_uninstalls:
return True
return False
@@ -303,7 +312,8 @@ class MSUHTMLFilter(HTMLParser):
def filtered_html(text):
'''Returns filtered HTML for use in description paragraphs'''
'''Returns filtered HTML for use in description paragraphs
or converts plain text into basic HTML for the same use'''
parser = MSUHTMLFilter()
parser.feed(text)
if parser.tag_count:
@@ -363,15 +373,25 @@ class SelfService(object):
def subscribe(item):
'''Add item to SelfServeManifest's managed_installs.
Also track user selections.'''
SelfService().subscribe(item)
user_install_selections.add(item['name'])
def unsubscribe(item):
'''Add item to SelfServeManifest's managed_uninstalls.
Also track user selections.'''
SelfService().unsubscribe(item)
user_removal_selections.add(item['name'])
def unmanage(item):
'''Remove item from SelfServeManifest.
Also track user selections.'''
SelfService().unmanage(item)
user_install_selections.discard(item['name'])
user_removal_selections.discard(item['name'])
class GenericItem(dict):
@@ -443,15 +463,15 @@ class GenericItem(dict):
def dependency_description(self):
'''Return an html description of items this item depends on'''
_description = u''
description = u''
prologue = NSLocalizedString(
u'This item is required by:', u'DependencyListPrologueText')
if self.get('dependent_items'):
_description = u'<br/><br/><strong>' + prologue
description = u'<strong>' + prologue
for item in self['dependent_items']:
_description += u'<br/>&nbsp;&nbsp;&bull; ' + display_name(item)
_description += u'</strong>'
return _description
description += u'<br/>&nbsp;&nbsp;&bull; ' + display_name(item)
description += u'</strong><br/><br/>'
return description
def guess_developer(self):
'''Figure out something to use for the developer name'''
@@ -749,6 +769,10 @@ class GenericItem(dict):
removal_text = NSLocalizedString(
u'Will be removed', u'WillBeRemovedDisplayText')
return '<span class="warning">%s</span>' % removal_text
if self['status'] == 'removal-requested':
removal_text = NSLocalizedString(
u'Removal requested', u'RemovalRequestedDisplayText')
return '<span class="warning">%s</span>' % removal_text
else:
return NSLocalizedString(u'Version', u'VersionLabel')
@@ -798,10 +822,13 @@ class OptionalItem(GenericItem):
self['hide_cancel_button'] = u''
def _get_status(self):
'''Calculates initial status for an item'''
'''Calculates initial status for an item and also sets a boolean
if a updatecheck is needed'''
managed_update_names = getInstallInfo().get('managed_updates', [])
self_service_installs = SelfService().installs()
self_service_uninstalls = SelfService().uninstalls()
self['updatecheck_needed'] = False
self['user_directed_action'] = False
if self.get('installed'):
if self.get('removal_error'):
status = u'removal-error'
@@ -811,6 +838,7 @@ class OptionalItem(GenericItem):
status = u'installed-not-removable'
elif self['name'] in self_service_uninstalls:
status = u'removal-requested'
self['updatecheck_needed'] = True
else: # not in managed_uninstalls
if not self.get('needs_update'):
if self.get('uninstallable'):
@@ -850,6 +878,7 @@ class OptionalItem(GenericItem):
status = u'will-be-installed'
elif self['name'] in self_service_installs:
status = u'install-requested'
self['updatecheck_needed'] = True
else: # not in managed_installs
status = u'not-installed'
return status
@@ -857,28 +886,31 @@ class OptionalItem(GenericItem):
def description(self):
'''return a full description for the item, inserting dynamic data
if needed'''
_description = self['raw_description']
start_text = ''
if self.get('install_error'):
_description = NSLocalizedString(
u'<span class="warning">An installation attempt failed. '
'Installation will be attempted again.<br/>'
'If this situation continues, contact your systems administrator.'
'</span><br/><br/>',
u'InstallErrorMessage') + _description
elif self.get('removal_error'):
_description = NSLocalizedString(
u'<span class="warning">A removal attempt failed. '
'Removal will be attempted again.<br/>'
'If this situation continues, contact your systems administrator.'
'</span><br/><br/>',
u'RemovalErrorMessage') + _description
warning_text = NSLocalizedString(
u'An installation attempt failed. '
'Installation will be attempted again.\n'
'If this situation continues, contact your systems administrator.',
u'InstallErrorMessage')
start_text += '<span class="warning">%s</span><br/><br/>' % filtered_html(warning_text)
if self.get('removal_error'):
warning_text = NSLocalizedString(
u'A removal attempt failed. '
'Removal will be attempted again.\n'
'If this situation continues, contact your systems administrator.',
u'RemovalErrorMessage')
start_text += '<span class="warning">%s</span><br/><br/>' % filtered_html(warning_text)
if self.get('dependent_items'):
# append dependency info to description:
_description += self.dependency_description()
return _description
start_text += self.dependency_description()
return start_text + self['raw_description']
def update_status(self):
# user clicked an item action button - update the item's state
# also sets a boolean indicating if we should run an updatecheck
self['updatecheck_needed'] = True
original_status = self['status']
managed_update_names = getInstallInfo().get('managed_updates', [])
if self['status'] == 'update-available':
# mark the update for install
@@ -900,6 +932,8 @@ class OptionalItem(GenericItem):
# item is simply installed
self['status'] = u'installed'
unmanage(self)
if original_status == 'removal-requested':
self['updatecheck_needed'] = False
elif self['status'] in ['will-be-installed', 'install-requested',
'downloading', 'install-error']:
# cancel install
@@ -908,6 +942,8 @@ class OptionalItem(GenericItem):
else:
self['status'] = u'not-installed'
unmanage(self)
if original_status == 'install-requested':
self['updatecheck_needed'] = False
elif self['status'] == 'not-installed':
# mark for install
self['status'] = u'install-requested'
@@ -940,7 +976,8 @@ class UpdateItem(GenericItem):
self['dependent_items'] = dependentItems(self['name'])
def description(self):
_description = self['raw_description']
warning = ''
dependent_items = ''
if not self['status'] == 'will-be-removed':
force_install_after_date = self.get('force_install_after_date')
if force_install_after_date:
@@ -951,12 +988,11 @@ class UpdateItem(GenericItem):
forced_date_text = NSLocalizedString(
u'This item must be installed by %s',
u'ForcedDateWarning')
# prepend deadline info to description.
_description = ('<span class="warning">' + forced_date_text % date_str
+ '</span><br><br>' + _description)
warning = ('<span class="warning">'
+ forced_date_text % date_str
+ '</span><br><br>')
if self.get('dependent_items'):
# append dependency info to description:
_description += self.dependency_description()
dependent_items = self.dependency_description()
return _description
return warning + dependent_items + self['raw_description']
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

@@ -1282,6 +1282,39 @@ div.title div.select {
color: #CC0000 !important;
}
div.lockup li a.follow,
span a.follow {
visibility: hidden;
}
div.lockup li.install-requested a.follow,
div.lockup li.will-be-installed a.follow,
div.lockup li.update-will-be-installed a.follow,
div.lockup li.removal-requested a.follow,
div.lockup li.will-be-removed a.follow,
span.install-requested a.follow,
span.will-be-installed a.follow,
span.update-will-be-installed a.follow,
span.removal-requested a.follow,
span.will-be-removed a.follow {
visibility: visible;
opacity: 0.8;
display: inline-block;
width: 12px;
height: 12px;
background: url('FollowLink.png') 0 0 no-repeat;
-webkit-background-size: 12px 12px;
position: relative;
left: 2px;
top: 3px;
}
div.lockup li a.follow:hover,
span a.follow:hover {
opacity: 1;
}
div.lockup li.will-be-installed,
div.lockup li.will-be-removed,
div.lockup li.install-requested,
@@ -1327,6 +1360,8 @@ div.lockup li.removing:after {
-webkit-animation-duration: 1s;
}
div.lockup li.downloading:after, div.lockup li.preparing-removal:after,
div.lockup li.installing:after, div.lockup li.removing:after {
position: absolute;
@@ -377,6 +377,7 @@ def justUpdate():
except (OSError, IOError):
return 1
def pythonScriptRunning(scriptname):
"""Returns Process ID for a running python script"""
cmd = ['/bin/ps', '-eo', 'pid=,command=']
@@ -72,6 +72,7 @@
<span class="label">${statusLabel} </span>
<span class="${status}" id="${name}_status_text">
${status_text}
<a class="follow" href="updates.html"></a>
</span>
</li>
<li class="warning">${restart_action_text}</li>
@@ -7,7 +7,10 @@
<ul class="list">
<li class="name" title="${display_name}"><a href="${detail_link}">${display_name}</a></li>
<li class="genre">${category_and_developer}</li>
<li class="${status}" id="${name}_status_text">${status_text}</li>
<li class="${status}" id="${name}_status_text">
${status_text}
<a class="follow" href="updates.html"></a>
</li>
<li>
<div class="msu-button small">
<button class="button-area uppercase"
@@ -12,7 +12,12 @@
</td>
<td>${version_to_install}</td>
<td>${size}</td>
<td class="status"><span class="${status}" id="${name}_status_text">${status_text}</span></td>
<td class="status">
<span class="${status}" id="${name}_status_text">
${status_text}
<a class="follow" href="updates.html">
</span>
</td>
<td>
<div class="msu-button install-updates">
<button class="button-area uppercase"