# 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. """ launchd Created by Greg Neagle on 2011-07-22. get_socket_fd and refactoring by Greg Neagle on 2017-04-14. Code for getting a socket file descriptor from launchd. Returns a file descriptor for a socket defined in a launchd plist. -and- A wrapper for using launchd to run a process as root outside of Munki's process space. Needed to properly run /usr/sbin/softwareupdate, for example. """ import os import subprocess import time import uuid from .. import osutils from .. import FoundationPlist def get_socket_fd(socket_name): '''Get socket file descriptors from launchd.''' os_version = osutils.getOsVersion(as_tuple=True) if os_version >= (10, 10): # use new launchd api from . import launch2 try: sockets = launch2.launch_activate_socket(socket_name) except launch2.LaunchDError: # no sockets found return None return sockets[0] else: # use old launchd api from . import launch1 try: socket_dict = launch1.get_launchd_socket_fds() except launch1.LaunchDCheckInError: # no sockets found return None if socket_name not in socket_dict: # no sockets found with the expected name return None return socket_dict[socket_name][0] class LaunchdJobException(Exception): '''Exception for launchctl errors and other errors from this module.''' pass class Job(object): '''launchd job object''' def __init__(self, cmd, environment_vars=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 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') self.stdout = None self.stderr = None self.plist = {} self.plist['Label'] = self.label self.plist['ProgramArguments'] = cmd self.plist['StandardOutPath'] = self.stdout_path self.plist['StandardErrorPath'] = self.stderr_path if environment_vars: self.plist['EnvironmentVariables'] = environment_vars # write out launchd plist FoundationPlist.writePlist(self.plist, self.plist_path) # set owner, group and mode to those required # by launchd os.chown(self.plist_path, 0, 0) os.chmod(self.plist_path, int('644', 8)) launchctl_cmd = ['/bin/launchctl', 'load', self.plist_path] 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 __del__(self): '''Attempt to clean up''' if self.cleanup_at_exit: if self.plist: launchctl_cmd = ['/bin/launchctl', 'unload', self.plist_path] dummy_result = subprocess.call(launchctl_cmd) try: self.stdout.close() self.stderr.close() except AttributeError: pass try: os.unlink(self.plist_path) os.unlink(self.stdout_path) os.unlink(self.stderr_path) except (OSError, IOError): pass def start(self): '''Start the launchd job''' launchctl_cmd = ['/bin/launchctl', 'start', 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) else: if (not os.path.exists(self.stdout_path) or not os.path.exists(self.stderr_path)): # wait a second for the stdout/stderr files # to be created by launchd time.sleep(1) try: # open the stdout and stderr output files and # store their file descriptors for use self.stdout = open(self.stdout_path, 'r') self.stderr = open(self.stderr_path, 'r') except (OSError, IOError), err: raise LaunchdJobException(err) 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) 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 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 if __name__ == '__main__': print 'This is a library of support tools for the Munki Suite.'