Permalink
Browse files

Precaching improvements so that managedsoftwareupdate won't remove pa…

…rtially precached items, and that precache_agent is stopped when managedsoftwareupdate does an updatecheck
  • Loading branch information...
gregneagle committed Jan 9, 2019
1 parent c97d961 commit c2ced7cfe665aa12b9b9311efbb99e91bea1b790
@@ -202,8 +202,7 @@ def get_url(url, destinationpath,

tempdownloadpath = destinationpath + '.download'
if os.path.exists(tempdownloadpath) and not resume:
if resume and not os.path.exists(destinationpath):
os.remove(tempdownloadpath)
os.remove(tempdownloadpath)

cache_data = None
if onlyifnewer and os.path.exists(destinationpath):
@@ -70,18 +70,80 @@ class LaunchdJobException(Exception):
pass


def job_info(job_label):
'''Get info about a launchd job. Returns a dictionary.'''
info = {'state': 'unknown',
'PID': None,
'LastExitStatus': None}
launchctl_cmd = ['/bin/launchctl', 'list']
proc = subprocess.Popen(launchctl_cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
output = proc.communicate()[0]
if proc.returncode or not output:
return info
else:
lines = str(output).splitlines()
# search launchctl list output for our job label
job_lines = [item for item in lines
if item.endswith('\t' + job_label)]
if len(job_lines) != 1:
# unexpected number of lines matched our label
return info
j_info = job_lines[0].split('\t')
if len(j_info) != 3:
# unexpected number of fields in the line
return info
if j_info[0] == '-':
info['PID'] = None
info['state'] = 'stopped'
else:
info['PID'] = int(j_info[0])
info['state'] = 'running'
if j_info[1] == '-':
info['LastExitStatus'] = None
else:
info['LastExitStatus'] = int(j_info[1])
return info


def stop_job(job_label):
'''Stop the launchd job'''
launchctl_cmd = ['/bin/launchctl', 'stop', job_label]
proc = subprocess.Popen(launchctl_cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
err = proc.communicate()[1]
if proc.returncode:
raise LaunchdJobException(err)


def remove_job(job_label):
'''Remove a job from launchd by label'''
launchctl_cmd = ['/bin/launchctl', 'remove', job_label]
proc = subprocess.Popen(launchctl_cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
err = proc.communicate()[1]
if proc.returncode:
raise LaunchdJobException(err)


class Job(object):
'''launchd job object'''

def __init__(self, cmd, environment_vars=None, cleanup_at_exit=True):
def __init__(self, cmd, environment_vars=None,
job_label=None, cleanup_at_exit=True):
'''Initialize our launchd job'''
self.cleanup_at_exit = cleanup_at_exit
tmpdir = osutils.tmpdir()
labelprefix = 'com.googlecode.munki.'
# create a unique id for this job
jobid = str(uuid.uuid1())

self.label = labelprefix + jobid
# label this job
self.label = job_label or 'com.googlecode.munki.' + str(uuid.uuid1())

self.cleanup_at_exit = cleanup_at_exit
self.stdout_path = os.path.join(tmpdir, self.label + '.stdout')
self.stderr_path = os.path.join(tmpdir, self.label + '.stderr')
self.plist_path = os.path.join(tmpdir, self.label + '.plist')
@@ -153,61 +215,20 @@ def start(self):

def stop(self):
'''Stop the launchd job'''
launchctl_cmd = ['/bin/launchctl', 'stop', self.label]
proc = subprocess.Popen(launchctl_cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
err = proc.communicate()[1]
if proc.returncode:
raise LaunchdJobException(err)
stop_job(self.label)

def info(self):
'''Get info about the launchd job. Returns a dictionary.'''
info = {'state': 'unknown',
'PID': None,
'LastExitStatus': None}
launchctl_cmd = ['/bin/launchctl', 'list']
proc = subprocess.Popen(launchctl_cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
output = proc.communicate()[0]
if proc.returncode or not output:
return info
else:
lines = str(output).splitlines()
# search launchctl list output for our job label
job_lines = [item for item in lines
if item.endswith('\t' + self.label)]
if len(job_lines) != 1:
# unexpected number of lines matched our label
return info
job_info = job_lines[0].split('\t')
if len(job_info) != 3:
# unexpected number of fields in the line
return info
if job_info[0] == '-':
info['PID'] = None
info['state'] = 'stopped'
else:
info['PID'] = int(job_info[0])
info['state'] = 'running'
if job_info[1] == '-':
info['LastExitStatus'] = None
else:
info['LastExitStatus'] = int(job_info[1])
return info
return job_info(self.label)

def returncode(self):
'''Returns the process exit code, if the job has exited; otherwise,
returns None'''
info = self.info()
if info['state'] == 'stopped':
return info['LastExitStatus']
else:
return None
return None


if __name__ == '__main__':
print 'This is a library of support tools for the Munki Suite.'
print 'This is a library of support tools for the Munki Suite.'
@@ -87,6 +87,9 @@ def check(client_id='', localmanifestpath=None):
if processes.stop_requested():
return 0

# stop precaching_agent if it's running
download.stop_precaching_agent()

# prevent idle sleep only if we are on AC power
caffeinator = None
if powermgr.onACPower():
@@ -422,14 +425,12 @@ def check(client_id='', localmanifestpath=None):
# we have a partial and a full download
# for the same item. (This shouldn't happen.)
# remove the partial download.
display.display_detail(
'Removing partial download %s from cache', item)
os.unlink(os.path.join(cachedir, item))
elif problem_items == []:
# problem items is our list of items
# that need to be installed but are missing
# the installer_item; these might be partial
# downloads. So if we have no problem items, it's
# OK to get rid of any partial downloads hanging
# around.
elif fullitem not in cache_list:
display.display_detail(
'Removing partial download %s from cache', item)
os.unlink(os.path.join(cachedir, item))
elif item not in cache_list:
display.display_detail('Removing %s from cache', item)
@@ -379,7 +379,7 @@ def _items_to_precache(install_info):

def cache():
'''Download any applicable precache items into our Cache folder'''
display.display_info("#### Beginning precaching session ####")
display.display_info("### Beginning precaching session ###")
install_info = _installinfo()
for item in _items_to_precache(install_info):
try:
@@ -388,7 +388,7 @@ def cache():
display.display_warning(
'Failed to precache the installer for %s because %s',
item['name'], unicode(err))
display.display_info("#### Ending precaching session ####")
display.display_info("### Ending precaching session ###")


def uncache(space_needed_in_kb):
@@ -452,6 +452,8 @@ def uncache(space_needed_in_kb):
"Could not remove precached item %s: %s" % (item_path, err))


PRECACHING_AGENT_LABEL = "com.googlecode.munki.precache_agent"

def run_precaching_agent():
'''Kick off a run of our precaching agent, which allows the precaching to
run in the background after a normal Munki run'''
@@ -469,10 +471,13 @@ def run_precaching_agent():
# try absolute path in Munki's normal install dir
precache_agent_path = '/usr/local/munki/precache_agent'
if os.path.exists(precache_agent_path):
display.display_info("Starting precaching agent")
display.display_debug1(
'Launching precache_agent from %s', precache_agent_path)
try:
job = launchd.Job([precache_agent_path], cleanup_at_exit=False)
job = launchd.Job([precache_agent_path],
job_label=PRECACHING_AGENT_LABEL,
cleanup_at_exit=False)
job.start()
except launchd.LaunchdJobException as err:
display.display_error(
@@ -481,5 +486,18 @@ def run_precaching_agent():
display.display_error("Could not find precache_agent")


def stop_precaching_agent():
'''Stop the precaching_agent if it's running'''
agent_info = launchd.job_info(PRECACHING_AGENT_LABEL)
if agent_info.get('state') != 'unknown':
# it's either running or stopped. Removing it will stop it.
if agent_info.get('state') == 'running':
display.display_info("Stopping precaching agent")
try:
launchd.remove_job(PRECACHING_AGENT_LABEL)
except launchd.LaunchdJobException, err:
display.display_error('Error stopping precaching agent: %s', err)


if __name__ == '__main__':
print 'This is a library of support tools for the Munki Suite.'
@@ -21,13 +21,27 @@ Created by Greg Neagle on 2018-07-18.
A privileged agent that downloads optional installs items marked for precaching.
"""
import signal
import sys
import time

from munkilib import display
from munkilib.updatecheck import download


def signal_handler(signum, _):
"""Handle any signals we've been told to.
Right now just handle SIGTERM so clean up can happen, like
garbage collection, which will trigger object destructors and
kill any launchd processes we've started."""
if signum == signal.SIGTERM:
display.display_info("### Precaching session stopped ###")
sys.exit()


if __name__ == '__main__':
# install handler for SIGTERM
signal.signal(signal.SIGTERM, signal_handler)
# turn off Munki status output; this should be silent
display.munkistatusoutput = False
download.cache()

0 comments on commit c2ced7c

Please sign in to comment.