#!/usr/bin/python # encoding: utf-8 """ adobeutils.py Utilities to enable munki to install/uninstall Adobe CS3/CS4/CS5 products using the CS3/CS4/CS5 Deployment Toolkits. """ # Copyright 2009-2016 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. #import sys import os import re import subprocess import time import tempfile import sqlite3 from xml.dom import minidom from glob import glob import FoundationPlist import munkicommon import munkistatus import utils # we use lots of camelCase-style names. Deal with it. # pylint: disable=C0103 class AdobeInstallProgressMonitor(object): """A class to monitor installs/removals of Adobe products. Finds the currently active installation log and scrapes data out of it. Installations that install a product and updates may actually create multiple logs.""" def __init__(self, kind='CS5', operation='install'): '''Provide some hints as to what type of installer is running and whether we are installing or removing''' self.kind = kind self.operation = operation self.payload_count = {} def get_current_log(self): '''Returns the current Adobe install log''' logpath = '/Library/Logs/Adobe/Installers' # find the most recently-modified log file proc = subprocess.Popen(['/bin/ls', '-t1', logpath], bufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (output, dummy_err) = proc.communicate() if output: firstitem = str(output).splitlines()[0] if firstitem.endswith(".log"): # return path of most recently modified log file return os.path.join(logpath, firstitem) return None def info(self): '''Returns the number of completed Adobe payloads, and the AdobeCode of the most recently completed payload.''' last_adobecode = "" logfile = self.get_current_log() if logfile: if self.kind in ['CS6', 'CS5']: regex = r'END TIMER :: \[Payload Operation :\{' elif self.kind in ['CS3', 'CS4']: if self.operation == 'install': regex = r'Closed PCD cache session payload with ID' else: regex = r'Closed CAPS session for removal of payload' else: if self.operation == 'install': regex = r'Completing installation for payload at ' else: regex = r'Physical payload uninstall result ' cmd = ['/usr/bin/grep', '-E', regex, logfile] proc = subprocess.Popen(cmd, bufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (output, dummy_err) = proc.communicate() if output: lines = str(output).splitlines() completed_payloads = len(lines) if (not logfile in self.payload_count or completed_payloads > self.payload_count[logfile]): # record number of completed payloads self.payload_count[logfile] = completed_payloads # now try to get the AdobeCode of the most recently # completed payload. # this isn't 100% accurate, but it's mostly for show # anyway... regex = re.compile(r'[^{]*(\{[A-Fa-f0-9-]+\})') lines.reverse() for line in lines: m = regex.match(line) try: last_adobecode = m.group(1) break except (IndexError, AttributeError): pass total_completed_payloads = 0 for key in self.payload_count.keys(): total_completed_payloads += self.payload_count[key] return (total_completed_payloads, last_adobecode) # dmg helper # we need this instead of the one in munkicommon because the Adobe stuff # needs the dmgs mounted under /Volumes. We can merge this later (or not). def mountAdobeDmg(dmgpath): """ Attempts to mount the dmg at dmgpath and returns a list of mountpoints """ mountpoints = [] dmgname = os.path.basename(dmgpath) proc = subprocess.Popen(['/usr/bin/hdiutil', 'attach', dmgpath, '-nobrowse', '-noverify', '-plist'], bufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (pliststr, err) = proc.communicate() if err: munkicommon.display_error('Error %s mounting %s.' % (err, dmgname)) if pliststr: plist = FoundationPlist.readPlistFromString(pliststr) for entity in plist['system-entities']: if 'mount-point' in entity: mountpoints.append(entity['mount-point']) return mountpoints def getCS5uninstallXML(optionXMLfile): '''Gets the uninstall deployment data from a CS5 installer''' xml = '' dom = minidom.parse(optionXMLfile) DeploymentInfo = dom.getElementsByTagName('DeploymentInfo') if DeploymentInfo: for info_item in DeploymentInfo: DeploymentUninstall = info_item.getElementsByTagName( 'DeploymentUninstall') if DeploymentUninstall: deploymentData = DeploymentUninstall[0].getElementsByTagName( 'Deployment') if deploymentData: Deployment = deploymentData[0] xml += Deployment.toxml('UTF-8') return xml def getCS5mediaSignature(dirpath): '''Returns the CS5 mediaSignature for an AAMEE CS5 install. dirpath is typically the root of a mounted dmg''' payloads_dir = "" # look for a payloads folder for (path, dummy_dirs, dummy_files) in os.walk(dirpath): if path.endswith('/payloads'): payloads_dir = path # return empty-handed if we didn't find a payloads folder if not payloads_dir: return '' # now look for setup.xml setupxml = os.path.join(payloads_dir, 'Setup.xml') if os.path.exists(setupxml) and os.path.isfile(setupxml): # parse the XML dom = minidom.parse(setupxml) setupElements = dom.getElementsByTagName('Setup') if setupElements: mediaSignatureElements = \ setupElements[0].getElementsByTagName('mediaSignature') if mediaSignatureElements: element = mediaSignatureElements[0] elementvalue = '' for node in element.childNodes: elementvalue += node.nodeValue return elementvalue return "" def getPayloadInfo(dirpath): '''Parses Adobe payloads, pulling out info useful to munki. .proxy.xml files are used if available, or for CC-era updates which do not contain one, the Media_db.db file, which contains identical XML, is instead used. CS3/CS4: contain only .proxy.xml CS5/CS5.5/CS6: contain both CC: contain only Media_db.db''' payloadinfo = {} # look for .proxy.xml file dir if os.path.isdir(dirpath): proxy_paths = glob(os.path.join(dirpath, '*.proxy.xml')) if proxy_paths: xmlpath = proxy_paths[0] dom = minidom.parse(xmlpath) # if there's no .proxy.xml we should hope there's a Media_db.db else: db_path = os.path.join(dirpath, 'Media_db.db') if os.path.exists(db_path): conn = sqlite3.connect(db_path) cur = conn.cursor() cur.execute("SELECT value FROM PayloadData WHERE " "PayloadData.key = 'PayloadInfo'") result = cur.fetchone() cur.close() if result: info_xml = result[0].encode('UTF-8') dom = minidom.parseString(info_xml) else: # no xml, no db, no payload info! return payloadinfo payload_info = dom.getElementsByTagName('PayloadInfo') if payload_info: installer_properties = payload_info[0].getElementsByTagName( 'InstallerProperties') if installer_properties: properties = installer_properties[0].getElementsByTagName( 'Property') for prop in properties: if 'name' in prop.attributes.keys(): propname = prop.attributes['name'].value.encode('UTF-8') propvalue = '' for node in prop.childNodes: propvalue += node.nodeValue if propname == 'AdobeCode': payloadinfo['AdobeCode'] = propvalue if propname == 'ProductName': payloadinfo['display_name'] = propvalue if propname == 'ProductVersion': payloadinfo['version'] = propvalue installmetadata = payload_info[0].getElementsByTagName( 'InstallDestinationMetadata') if installmetadata: totalsizes = installmetadata[0].getElementsByTagName( 'TotalSize') if totalsizes: installsize = '' for node in totalsizes[0].childNodes: installsize += node.nodeValue payloadinfo['installed_size'] = int(installsize)/1024 return payloadinfo def getAdobeSetupInfo(installroot): '''Given the root of mounted Adobe DMG, look for info about the installer or updater''' info = {} payloads = [] # look for all the payloads folders for (path, dummy_dirs, dummy_files) in os.walk(installroot): if path.endswith('/payloads'): driverfolder = '' mediaSignature = '' setupxml = os.path.join(path, 'setup.xml') if os.path.exists(setupxml): dom = minidom.parse(setupxml) drivers = dom.getElementsByTagName('Driver') if drivers: driver = drivers[0] if 'folder' in driver.attributes.keys(): driverfolder = driver.attributes[ 'folder'].value.encode('UTF-8') if driverfolder == '': # look for mediaSignature (CS5 AAMEE install) setupElements = dom.getElementsByTagName('Setup') if setupElements: mediaSignatureElements = setupElements[ 0].getElementsByTagName('mediaSignature') if mediaSignatureElements: element = mediaSignatureElements[0] for node in element.childNodes: mediaSignature += node.nodeValue for item in munkicommon.listdir(path): payloadpath = os.path.join(path, item) payloadinfo = getPayloadInfo(payloadpath) if payloadinfo: payloads.append(payloadinfo) if ((driverfolder and item == driverfolder) or (mediaSignature and payloadinfo['AdobeCode'] == mediaSignature)): info['display_name'] = payloadinfo['display_name'] info['version'] = payloadinfo['version'] info['AdobeSetupType'] = 'ProductInstall' if not payloads: # look for an extensions folder; almost certainly this is an Updater for (path, dummy_dirs, dummy_files) in os.walk(installroot): if path.endswith("/extensions"): for item in munkicommon.listdir(path): #skip LanguagePacks if item.find("LanguagePack") == -1: itempath = os.path.join(path, item) payloadinfo = getPayloadInfo(itempath) if payloadinfo: payloads.append(payloadinfo) # we found an extensions dir, # so no need to keep walking the install root break if payloads: if len(payloads) == 1: info['display_name'] = payloads[0]['display_name'] info['version'] = payloads[0]['version'] else: if not 'display_name' in info: info['display_name'] = "ADMIN: choose from payloads" if not 'version' in info: info['version'] = "ADMIN please set me" info['payloads'] = payloads installed_size = 0 for payload in payloads: installed_size = installed_size + payload.get('installed_size', 0) info['installed_size'] = installed_size return info def getAdobePackageInfo(installroot): '''Gets the package name from the AdobeUberInstaller.xml file; other info from the payloads folder''' info = getAdobeSetupInfo(installroot) info['description'] = "" installerxml = os.path.join(installroot, "AdobeUberInstaller.xml") if os.path.exists(installerxml): description = '' dom = minidom.parse(installerxml) installinfo = dom.getElementsByTagName("InstallInfo") if installinfo: packagedescriptions = \ installinfo[0].getElementsByTagName("PackageDescription") if packagedescriptions: prop = packagedescriptions[0] for node in prop.childNodes: description += node.nodeValue if description: description_parts = description.split(' : ', 1) info['display_name'] = description_parts[0] if len(description_parts) > 1: info['description'] = description_parts[1] else: info['description'] = "" return info else: installerxml = os.path.join(installroot, "optionXML.xml") if os.path.exists(installerxml): dom = minidom.parse(installerxml) installinfo = dom.getElementsByTagName("InstallInfo") if installinfo: pkgname_elems = installinfo[0].getElementsByTagName( "PackageName") if pkgname_elems: prop = pkgname_elems[0] pkgname = "" for node in prop.childNodes: pkgname += node.nodeValue info['display_name'] = pkgname if not info.get('display_name'): info['display_name'] = os.path.basename(installroot) return info def getXMLtextElement(dom_node, name): '''Returns the text value of the first item found with the given tagname''' value = None subelements = dom_node.getElementsByTagName(name) if subelements: value = '' for node in subelements[0].childNodes: value += node.nodeValue return value def parseOptionXML(option_xml_file): '''Parses an optionXML.xml file and pulls ot items of interest, returning them in a dictionary''' info = {} dom = minidom.parse(option_xml_file) installinfo = dom.getElementsByTagName('InstallInfo') if installinfo: if 'id' in installinfo[0].attributes.keys(): info['packager_id'] = installinfo[0].attributes['id'].value if 'version' in installinfo[0].attributes.keys(): info['packager_version'] = installinfo[ 0].attributes['version'].value info['package_name'] = getXMLtextElement(installinfo[0], 'PackageName') info['package_id'] = getXMLtextElement(installinfo[0], 'PackageID') info['products'] = [] medias_elements = installinfo[0].getElementsByTagName('Medias') if medias_elements: media_elements = medias_elements[0].getElementsByTagName('Media') if media_elements: for media in media_elements: product = {} product['prodName'] = getXMLtextElement(media, 'prodName') product['prodVersion'] = getXMLtextElement( media, 'prodVersion') setup_elements = media.getElementsByTagName('Setup') if setup_elements: mediaSignatureElements = setup_elements[ 0].getElementsByTagName('mediaSignature') if mediaSignatureElements: product['mediaSignature'] = '' element = mediaSignatureElements[0] for node in element.childNodes: product['mediaSignature'] += node.nodeValue info['products'].append(product) return info def countPayloads(dirpath): '''Attempts to count the payloads in the Adobe installation item''' count = 0 for (path, dummy_dirs, dummy_files) in os.walk(dirpath): if path.endswith("/payloads"): for subitem in munkicommon.listdir(path): subitempath = os.path.join(path, subitem) if os.path.isdir(subitempath): count = count + 1 return count def getPercent(current, maximum): '''Returns a value useful with MunkiStatus to use when displaying precent-done stauts''' if maximum == 0: percentdone = -1 elif current < 0: percentdone = -1 elif current > maximum: percentdone = -1 elif current == maximum: percentdone = 100 else: percentdone = int(float(current)/float(maximum)*100) return percentdone def findSetupApp(dirpath): '''Search dirpath and enclosed directories for Setup.app. Returns the path to the actual executable.''' for (path, dummy_dirs, dummy_files) in os.walk(dirpath): if path.endswith("Setup.app"): setup_path = os.path.join(path, "Contents", "MacOS", "Setup") if os.path.exists(setup_path): return setup_path return '' def findInstallApp(dirpath): '''Searches dirpath and enclosed directories for Install.app. Returns the path to the actual executable.''' for (path, dummy_dirs, dummy_files) in os.walk(dirpath): if path.endswith("Install.app"): setup_path = os.path.join(path, "Contents", "MacOS", "Install") if os.path.exists(setup_path): return setup_path return '' def findAdobePatchInstallerApp(dirpath): '''Searches dirpath and enclosed directories for AdobePatchInstaller.app. Returns the path to the actual executable.''' for (path, dummy_dirs, dummy_files) in os.walk(dirpath): if path.endswith("AdobePatchInstaller.app"): setup_path = os.path.join( path, "Contents", "MacOS", "AdobePatchInstaller") if os.path.exists(setup_path): return setup_path return '' def findAdobeDeploymentManager(dirpath): '''Searches dirpath and enclosed directories for AdobeDeploymentManager. Returns path to the executable.''' for (path, dummy_dirs, dummy_files) in os.walk(dirpath): if path.endswith("pkg/Contents/Resources"): dm_path = os.path.join(path, "AdobeDeploymentManager") if os.path.exists(dm_path): return dm_path return '' secondsToLive = {} def killStupidProcesses(): '''A nasty bit of hackery to get Adobe CS5 AAMEE packages to install when at the loginwindow.''' stupid_processes = ["Adobe AIR Installer", "Adobe AIR Application Installer", "InstallAdobeHelp", "open -a /Library/Application Support/Adobe/" "SwitchBoard/SwitchBoard.app", "/bin/bash /Library/Application Support/Adobe/" "SwitchBoard/SwitchBoard.app/Contents/MacOS/" "switchboard.sh"] for procname in stupid_processes: pid = utils.getPIDforProcessName(procname) if pid: if not pid in secondsToLive: secondsToLive[pid] = 30 else: secondsToLive[pid] = secondsToLive[pid] - 1 if secondsToLive[pid] == 0: # it's been running too long; kill it munkicommon.log("Killing PID %s: %s" % (pid, procname)) try: os.kill(int(pid), 9) except OSError: pass # remove this PID from our list del secondsToLive[pid] # only kill one process per invocation return def runAdobeInstallTool( cmd, number_of_payloads=0, killAdobeAIR=False, payloads=None, kind="CS5", operation="install"): '''An abstraction of the tasks for running Adobe Setup, AdobeUberInstaller, AdobeUberUninstaller, AdobeDeploymentManager, etc''' # initialize an AdobeInstallProgressMonitor object. progress_monitor = AdobeInstallProgressMonitor( kind=kind, operation=operation) if munkicommon.munkistatusoutput and not number_of_payloads: # indeterminate progress bar munkistatus.percent(-1) proc = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) old_payload_completed_count = 0 payloadname = "" while proc.poll() == None: time.sleep(1) (payload_completed_count, adobe_code) = progress_monitor.info() if payload_completed_count > old_payload_completed_count: old_payload_completed_count = payload_completed_count if adobe_code and payloads: matched_payloads = [payload for payload in payloads if payload.get('AdobeCode') == adobe_code] if matched_payloads: payloadname = matched_payloads[0].get('display_name') else: payloadname = adobe_code payloadinfo = " - " + payloadname else: payloadinfo = "" if number_of_payloads: munkicommon.display_status_minor( 'Completed payload %s of %s%s' % (payload_completed_count, number_of_payloads, payloadinfo)) else: munkicommon.display_status_minor( 'Completed payload %s%s', payload_completed_count, payloadinfo) if munkicommon.munkistatusoutput: munkistatus.percent( getPercent(payload_completed_count, number_of_payloads)) # Adobe AIR Installer workaround/hack # CSx installs at the loginwindow hang when Adobe AIR is installed. # So we check for this and kill the process. Ugly. # Hopefully we can disable this in the future. if killAdobeAIR: if (not munkicommon.getconsoleuser() or munkicommon.getconsoleuser() == u"loginwindow"): # we're at the loginwindow. killStupidProcesses() # run of tool completed retcode = proc.poll() #check output for errors output = proc.stdout.readlines() for line in output: line = line.rstrip("\n") if line.startswith("Error"): munkicommon.display_error(line) if line.startswith("Exit Code:"): if retcode == 0: try: retcode = int(line[11:]) except (ValueError, TypeError): retcode = -1 if retcode != 0 and retcode != 8: munkicommon.display_error( 'Adobe Setup error: %s: %s', retcode, adobeSetupError(retcode)) else: if munkicommon.munkistatusoutput: munkistatus.percent(100) munkicommon.display_status_minor('Done.') return retcode def runAdobeSetup(dmgpath, uninstalling=False, payloads=None): '''Runs the Adobe setup tool in silent mode from an Adobe update DMG or an Adobe CS3 install DMG''' munkicommon.display_status_minor( 'Mounting disk image %s' % os.path.basename(dmgpath)) mountpoints = mountAdobeDmg(dmgpath) if mountpoints: setup_path = findSetupApp(mountpoints[0]) if setup_path: # look for install.xml or uninstall.xml at root deploymentfile = None installxml = os.path.join(mountpoints[0], "install.xml") uninstallxml = os.path.join(mountpoints[0], "uninstall.xml") if uninstalling: operation = 'uninstall' if os.path.exists(uninstallxml): deploymentfile = uninstallxml else: # we've been asked to uninstall, # but found no uninstall.xml # so we need to bail munkicommon.unmountdmg(mountpoints[0]) munkicommon.display_error( '%s doesn\'t appear to contain uninstall info.', os.path.basename(dmgpath)) return -1 else: operation = 'install' if os.path.exists(installxml): deploymentfile = installxml # try to find and count the number of payloads # so we can give a rough progress indicator number_of_payloads = countPayloads(mountpoints[0]) munkicommon.display_status_minor('Running Adobe Setup') adobe_setup = [setup_path, '--mode=silent', '--skipProcessCheck=1'] if deploymentfile: adobe_setup.append('--deploymentFile=%s' % deploymentfile) retcode = runAdobeInstallTool( adobe_setup, number_of_payloads, payloads=payloads, kind='CS3', operation=operation) else: munkicommon.display_error( '%s doesn\'t appear to contain Adobe Setup.' % os.path.basename(dmgpath)) retcode = -1 munkicommon.unmountdmg(mountpoints[0]) return retcode else: munkicommon.display_error('No mountable filesystems on %s' % dmgpath) return -1 def writefile(stringdata, path): '''Writes string data to path. Returns the path on success, empty string on failure.''' try: fileobject = open(path, mode='w', buffering=1) print >> fileobject, stringdata.encode('UTF-8') fileobject.close() return path except (OSError, IOError): munkicommon.display_error("Couldn't write %s" % stringdata) return "" def doAdobeCS5Uninstall(adobeInstallInfo, payloads=None): '''Runs the locally-installed Adobe CS5 tools to remove CS5 products. We need the uninstallxml and the CS5 Setup.app.''' uninstallxml = adobeInstallInfo.get('uninstallxml') if not uninstallxml: munkicommon.display_error("No uninstall.xml in adobe_install_info") return -1 payloadcount = adobeInstallInfo.get('payload_count', 0) path = os.path.join(munkicommon.tmpdir(), "uninstall.xml") deploymentFile = writefile(uninstallxml, path) if not deploymentFile: return -1 setupapp = "/Library/Application Support/Adobe/OOBE/PDApp/DWA/Setup.app" setup = os.path.join(setupapp, "Contents/MacOS/Setup") if not os.path.exists(setup): munkicommon.display_error("%s is not installed." % setupapp) return -1 uninstall_cmd = [setup, '--mode=silent', '--action=uninstall', '--skipProcessCheck=1', '--deploymentFile=%s' % deploymentFile] munkicommon.display_status_minor('Running Adobe Uninstall') return runAdobeInstallTool(uninstall_cmd, payloadcount, payloads=payloads, kind='CS5', operation='uninstall') def runAdobeCCPpkgScript(dmgpath, payloads=None, operation='install'): '''Installs or removes an Adobe product packaged via Creative Cloud Packager''' munkicommon.display_status_minor( 'Mounting disk image %s' % os.path.basename(dmgpath)) mountpoints = mountAdobeDmg(dmgpath) if not mountpoints: munkicommon.display_error("No mountable filesystems on %s" % dmgpath) return -1 deploymentmanager = findAdobeDeploymentManager(mountpoints[0]) if not deploymentmanager: munkicommon.display_error( '%s doesn\'t appear to contain AdobeDeploymentManager', os.path.basename(dmgpath)) munkicommon.unmountdmg(mountpoints[0]) return -1 # big hack to convince the Adobe tools to install off a mounted # disk image. # # For some reason, some versions of the Adobe install tools refuse to # install when the payloads are on a "removable" disk, # which includes mounted disk images. # # we create a temporary directory on the local disk and then symlink # some resources from the mounted disk image to the temporary # directory. When we pass this temporary directory to the Adobe # installation tools, they are now happy. basepath = os.path.dirname(deploymentmanager) preinstall_script = os.path.join(basepath, "preinstall") if not os.path.exists(preinstall_script): if operation == 'install': munkicommon.display_error( "No Adobe install script found on %s" % dmgpath) else: munkicommon.display_error( "No Adobe uninstall script found on %s" % dmgpath) munkicommon.unmountdmg(mountpoints[0]) return -1 number_of_payloads = countPayloads(basepath) tmpdir = tempfile.mkdtemp(prefix='munki-', dir='/tmp') # make our symlinks for dir_name in ['ASU' 'ASU2', 'ProvisioningTool', 'uninstallinfo']: if os.path.isdir(os.path.join(basepath, dir_name)): os.symlink(os.path.join(basepath, dir_name), os.path.join(tmpdir, dir_name)) for dir_name in ['Patches', 'Setup']: realdir = os.path.join(basepath, dir_name) if os.path.isdir(realdir): tmpsubdir = os.path.join(tmpdir, dir_name) os.mkdir(tmpsubdir) for item in munkicommon.listdir(realdir): os.symlink(os.path.join(realdir, item), os.path.join(tmpsubdir, item)) os_version_tuple = munkicommon.getOsVersion(as_tuple=True) if (os_version_tuple < (10, 11) and (not munkicommon.getconsoleuser() or munkicommon.getconsoleuser() == u"loginwindow")): # we're at the loginwindow, so we need to run the deployment # manager in the loginwindow context using launchctl bsexec # launchctl bsexec doesn't work for this in El Cap, so do it # only if we're running Yosemite or earlier loginwindowPID = utils.getPIDforProcessName("loginwindow") cmd = ['/bin/launchctl', 'bsexec', loginwindowPID] else: cmd = [] # preinstall script is in pkg/Contents/Resources, so calculate # path to pkg pkg_dir = os.path.dirname(os.path.dirname(basepath)) cmd.extend([preinstall_script, pkg_dir, '/', '/']) if operation == 'install': munkicommon.display_status_minor('Starting Adobe installer...') retcode = runAdobeInstallTool( cmd, number_of_payloads, killAdobeAIR=True, payloads=payloads, kind='CS6', operation=operation) # now clean up and return dummy_result = subprocess.call(["/bin/rm", "-rf", tmpdir]) munkicommon.unmountdmg(mountpoints[0]) return retcode def runAdobeCS5AAMEEInstall(dmgpath, payloads=None): '''Installs a CS5 product using an AAMEE-generated package on a disk image.''' munkicommon.display_status_minor( 'Mounting disk image %s' % os.path.basename(dmgpath)) mountpoints = mountAdobeDmg(dmgpath) if not mountpoints: munkicommon.display_error("No mountable filesystems on %s" % dmgpath) return -1 deploymentmanager = findAdobeDeploymentManager(mountpoints[0]) if deploymentmanager: # big hack to convince the Adobe tools to install off a mounted # disk image. # # For some reason, some versions of the Adobe install tools refuse to # install when the payloads are on a "removable" disk, # which includes mounted disk images. # # we create a temporary directory on the local disk and then symlink # some resources from the mounted disk image to the temporary # directory. When we pass this temporary directory to the Adobe # installation tools, they are now happy. basepath = os.path.dirname(deploymentmanager) number_of_payloads = countPayloads(basepath) tmpdir = tempfile.mkdtemp(prefix='munki-', dir='/tmp') # make our symlinks os.symlink(os.path.join(basepath, "ASU"), os.path.join(tmpdir, "ASU")) os.symlink(os.path.join(basepath, "ProvisioningTool"), os.path.join(tmpdir, "ProvisioningTool")) for dir_name in ['Patches', 'Setup']: realdir = os.path.join(basepath, dir_name) if os.path.isdir(realdir): tmpsubdir = os.path.join(tmpdir, dir_name) os.mkdir(tmpsubdir) for item in munkicommon.listdir(realdir): os.symlink( os.path.join(realdir, item), os.path.join(tmpsubdir, item)) optionXMLfile = os.path.join(basepath, "optionXML.xml") os_version_tuple = munkicommon.getOsVersion(as_tuple=True) if (os_version_tuple < (10, 11) and (not munkicommon.getconsoleuser() or munkicommon.getconsoleuser() == u"loginwindow")): # we're at the loginwindow, so we need to run the deployment # manager in the loginwindow context using launchctl bsexec # launchctl bsexec doesn't work for this in El Cap, so do it # only if we're running Yosemite or earlier loginwindowPID = utils.getPIDforProcessName("loginwindow") cmd = ['/bin/launchctl', 'bsexec', loginwindowPID] else: cmd = [] cmd.extend([deploymentmanager, '--optXMLPath=%s' % optionXMLfile, '--setupBasePath=%s' % basepath, '--installDirPath=/', '--mode=install']) munkicommon.display_status_minor('Starting Adobe installer...') retcode = runAdobeInstallTool( cmd, number_of_payloads, killAdobeAIR=True, payloads=payloads, kind='CS5', operation='install') # now clean up our symlink hackfest dummy_result = subprocess.call(["/bin/rm", "-rf", tmpdir]) else: munkicommon.display_error( '%s doesn\'t appear to contain AdobeDeploymentManager', os.path.basename(dmgpath)) retcode = -1 munkicommon.unmountdmg(mountpoints[0]) return retcode def runAdobeCS5PatchInstaller(dmgpath, copylocal=False, payloads=None): '''Runs the AdobePatchInstaller for CS5. Optionally can copy the DMG contents to the local disk to work around issues with the patcher.''' munkicommon.display_status_minor( 'Mounting disk image %s' % os.path.basename(dmgpath)) mountpoints = mountAdobeDmg(dmgpath) if mountpoints: if copylocal: # copy the update to the local disk before installing updatedir = tempfile.mkdtemp(prefix='munki-', dir='/tmp') retcode = subprocess.call( ["/bin/cp", "-r", mountpoints[0], updatedir]) # unmount diskimage munkicommon.unmountdmg(mountpoints[0]) if retcode: munkicommon.display_error( 'Error copying items from %s' % dmgpath) return -1 # remove the dmg file to free up space, since we don't need it # any longer dummy_result = subprocess.call(["/bin/rm", dmgpath]) else: updatedir = mountpoints[0] patchinstaller = findAdobePatchInstallerApp(updatedir) if patchinstaller: # try to find and count the number of payloads # so we can give a rough progress indicator number_of_payloads = countPayloads(updatedir) munkicommon.display_status_minor('Running Adobe Patch Installer') install_cmd = [patchinstaller, '--mode=silent', '--skipProcessCheck=1'] retcode = runAdobeInstallTool(install_cmd, number_of_payloads, payloads=payloads, kind='CS5', operation='install') else: munkicommon.display_error( "%s doesn't appear to contain AdobePatchInstaller.app.", os.path.basename(dmgpath)) retcode = -1 if copylocal: # clean up our mess dummy_result = subprocess.call(["/bin/rm", "-rf", updatedir]) else: munkicommon.unmountdmg(mountpoints[0]) return retcode else: munkicommon.display_error('No mountable filesystems on %s' % dmgpath) return -1 def runAdobeUberTool(dmgpath, pkgname='', uninstalling=False, payloads=None): '''Runs either AdobeUberInstaller or AdobeUberUninstaller from a disk image and provides progress feedback. pkgname is the name of a directory at the top level of the dmg containing the AdobeUber tools and their XML files.''' munkicommon.display_status_minor( 'Mounting disk image %s' % os.path.basename(dmgpath)) mountpoints = mountAdobeDmg(dmgpath) if mountpoints: installroot = mountpoints[0] if uninstalling: ubertool = os.path.join(installroot, pkgname, "AdobeUberUninstaller") else: ubertool = os.path.join(installroot, pkgname, "AdobeUberInstaller") if os.path.exists(ubertool): info = getAdobePackageInfo(installroot) packagename = info['display_name'] action = "Installing" operation = "install" if uninstalling: action = "Uninstalling" operation = "uninstall" munkicommon.display_status_major('%s %s' % (action, packagename)) if munkicommon.munkistatusoutput: munkistatus.detail('Starting %s' % os.path.basename(ubertool)) # try to find and count the number of payloads # so we can give a rough progress indicator number_of_payloads = countPayloads(installroot) retcode = runAdobeInstallTool( [ubertool], number_of_payloads, killAdobeAIR=True, payloads=payloads, kind='CS4', operation=operation) else: munkicommon.display_error("No %s found" % ubertool) retcode = -1 munkicommon.unmountdmg(installroot) return retcode else: munkicommon.display_error("No mountable filesystems on %s" % dmgpath) return -1 def findAcrobatPatchApp(dirpath): '''Attempts to find an AcrobatPro patching application in dirpath. If found, returns the path to the bundled patching script.''' for (path, dummy_dirs, dummy_files) in os.walk(dirpath): if path.endswith(".app"): # look for Adobe's patching script patch_script_path = os.path.join( path, 'Contents', 'Resources', 'ApplyOperation.py') if os.path.exists(patch_script_path): return path return '' def updateAcrobatPro(dmgpath): """Uses the scripts and Resources inside the Acrobat Patch application bundle to silently update Acrobat Pro and related apps Why oh why does this use a different mechanism than the other Adobe apps?""" if munkicommon.munkistatusoutput: munkistatus.percent(-1) #first mount the dmg munkicommon.display_status_minor( 'Mounting disk image %s' % os.path.basename(dmgpath)) mountpoints = mountAdobeDmg(dmgpath) if mountpoints: installroot = mountpoints[0] pathToAcrobatPatchApp = findAcrobatPatchApp(installroot) else: munkicommon.display_error("No mountable filesystems on %s" % dmgpath) return -1 if not pathToAcrobatPatchApp: munkicommon.display_error( 'No Acrobat Patch app at %s', pathToAcrobatPatchApp) munkicommon.unmountdmg(installroot) return -1 # some values needed by the patching script resourcesDir = os.path.join( pathToAcrobatPatchApp, 'Contents', 'Resources') ApplyOperation = os.path.join(resourcesDir, 'ApplyOperation.py') callingScriptPath = os.path.join(resourcesDir, 'InstallUpdates.sh') appList = [] appListFile = os.path.join(resourcesDir, 'app_list.txt') if os.path.exists(appListFile): fileobj = open(appListFile, mode='r', buffering=-1) if fileobj: for line in fileobj.readlines(): appList.append(line) fileobj.close() if not appList: munkicommon.display_error('Did not find a list of apps to update.') munkicommon.unmountdmg(installroot) return -1 payloadNum = -1 for line in appList: payloadNum = payloadNum + 1 if munkicommon.munkistatusoutput: munkistatus.percent(getPercent(payloadNum + 1, len(appList) + 1)) (appname, status) = line.split("\t") munkicommon.display_status_minor('Searching for %s' % appname) # first look in the obvious place pathname = os.path.join("/Applications/Adobe Acrobat 9 Pro", appname) if os.path.exists(pathname): item = {} item['path'] = pathname candidates = [item] else: # use system_profiler to search for the app candidates = [item for item in munkicommon.getAppData() if item['path'].endswith('/' + appname)] # hope there's only one! if len(candidates) == 0: if status == "optional": continue else: munkicommon.display_error("Cannot patch %s because it " "was not found on the startup " "disk." % appname) munkicommon.unmountdmg(installroot) return -1 if len(candidates) > 1: munkicommon.display_error("Cannot patch %s because we found " "more than one copy on the " "startup disk." % appname) munkicommon.unmountdmg(installroot) return -1 munkicommon.display_status_minor('Updating %s' % appname) apppath = os.path.dirname(candidates[0]["path"]) cmd = [ApplyOperation, apppath, appname, resourcesDir, callingScriptPath, str(payloadNum)] proc = subprocess.Popen(cmd, shell=False, bufsize=-1, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) while proc.poll() == None: time.sleep(1) # run of patch tool completed retcode = proc.poll() if retcode != 0: munkicommon.display_error( 'Error patching %s: %s', appname, retcode) break else: munkicommon.display_status_minor('Patching %s complete.', appname) munkicommon.display_status_minor('Done.') if munkicommon.munkistatusoutput: munkistatus.percent(100) munkicommon.unmountdmg(installroot) return retcode def getBundleInfo(path): """ Returns Info.plist data if available for bundle at path """ infopath = os.path.join(path, "Contents", "Info.plist") if not os.path.exists(infopath): infopath = os.path.join(path, "Resources", "Info.plist") if os.path.exists(infopath): try: plist = FoundationPlist.readPlist(infopath) return plist except FoundationPlist.NSPropertyListSerializationException: pass return None def getAdobeInstallInfo(installdir): '''Encapsulates info used by the Adobe Setup/Install app.''' adobeInstallInfo = {} if installdir: adobeInstallInfo['media_signature'] = getCS5mediaSignature(installdir) adobeInstallInfo['payload_count'] = countPayloads(installdir) optionXMLfile = os.path.join(installdir, "optionXML.xml") if os.path.exists(optionXMLfile): adobeInstallInfo['uninstallxml'] = \ getCS5uninstallXML(optionXMLfile) return adobeInstallInfo def getAdobeCatalogInfo(mountpoint, pkgname=""): '''Used by makepkginfo to build pkginfo data for Adobe installers/updaters''' # look for AdobeDeploymentManager (AAMEE installer) deploymentmanager = findAdobeDeploymentManager(mountpoint) if deploymentmanager: dirpath = os.path.dirname(deploymentmanager) option_xml_file = os.path.join(dirpath, 'optionXML.xml') option_xml_info = {} if os.path.exists(option_xml_file): option_xml_info = parseOptionXML(option_xml_file) cataloginfo = getAdobePackageInfo(dirpath) if cataloginfo: # add some more data if option_xml_info.get('packager_id') == u'CloudPackager': # CCP package cataloginfo['display_name'] = option_xml_info.get( 'package_name', 'unknown') cataloginfo['name'] = cataloginfo['display_name'].replace( ' ', '') cataloginfo['uninstallable'] = True cataloginfo['uninstall_method'] = "AdobeCCPUninstaller" cataloginfo['installer_type'] = "AdobeCCPInstaller" cataloginfo['minimum_os_version'] = "10.6.8" mediasignatures = [ item['mediaSignature'] for item in option_xml_info.get('products', []) if 'mediaSignature' in item] else: # AAMEE package cataloginfo['name'] = cataloginfo['display_name'].replace( ' ', '') cataloginfo['uninstallable'] = True cataloginfo['uninstall_method'] = "AdobeCS5AAMEEPackage" cataloginfo['installer_type'] = "AdobeCS5AAMEEPackage" cataloginfo['minimum_os_version'] = "10.5.0" cataloginfo['adobe_install_info'] = getAdobeInstallInfo( installdir=dirpath) mediasignature = cataloginfo['adobe_install_info'].get( "media_signature") mediasignatures = [mediasignature] if mediasignatures: # make a default installs array uninstalldir = "/Library/Application Support/Adobe/Uninstall" installs = [] for mediasignature in mediasignatures: signaturefile = mediasignature + ".db" filepath = os.path.join(uninstalldir, signaturefile) installitem = {} installitem['path'] = filepath installitem['type'] = 'file' installs.append(installitem) cataloginfo['installs'] = installs return cataloginfo # Look for Install.app (Bare metal CS5 install) # we don't handle this type, but we'll report it # back so makepkginfo can provide an error message installapp = findInstallApp(mountpoint) if installapp: cataloginfo = {} cataloginfo['installer_type'] = "AdobeCS5Installer" return cataloginfo # Look for AdobePatchInstaller.app (CS5 updater) installapp = findAdobePatchInstallerApp(mountpoint) if os.path.exists(installapp): # this is a CS5 updater disk image cataloginfo = getAdobePackageInfo(mountpoint) if cataloginfo: # add some more data cataloginfo['name'] = cataloginfo['display_name'].replace(' ', '') cataloginfo['uninstallable'] = False cataloginfo['installer_type'] = "AdobeCS5PatchInstaller" if pkgname: cataloginfo['package_path'] = pkgname # make some (hopfully functional) installs items from the payloads installs = [] uninstalldir = "/Library/Application Support/Adobe/Uninstall" # first look for a payload with a display_name matching the # overall display_name for payload in cataloginfo.get('payloads', []): if (payload.get('display_name', '') == cataloginfo['display_name']): if 'AdobeCode' in payload: dbfile = payload['AdobeCode'] + ".db" filepath = os.path.join(uninstalldir, dbfile) installitem = {} installitem['path'] = filepath installitem['type'] = 'file' installs.append(installitem) break if installs == []: # didn't find a payload with matching name # just add all of the non-LangPack payloads # to the installs list. for payload in cataloginfo.get('payloads', []): if 'AdobeCode' in payload: if ("LangPack" in payload.get("display_name") or "Language Files" in payload.get( "display_name")): # skip Language Packs continue dbfile = payload['AdobeCode'] + ".db" filepath = os.path.join(uninstalldir, dbfile) installitem = {} installitem['path'] = filepath installitem['type'] = 'file' installs.append(installitem) cataloginfo['installs'] = installs return cataloginfo # Look for AdobeUberInstaller items (CS4 install) pkgroot = os.path.join(mountpoint, pkgname) adobeinstallxml = os.path.join(pkgroot, "AdobeUberInstaller.xml") if os.path.exists(adobeinstallxml): # this is a CS4 Enterprise Deployment package cataloginfo = getAdobePackageInfo(pkgroot) if cataloginfo: # add some more data cataloginfo['name'] = cataloginfo['display_name'].replace(' ', '') cataloginfo['uninstallable'] = True cataloginfo['uninstall_method'] = "AdobeUberUninstaller" cataloginfo['installer_type'] = "AdobeUberInstaller" if pkgname: cataloginfo['package_path'] = pkgname return cataloginfo # maybe this is an Adobe update DMG or CS3 installer # look for Adobe Setup.app setuppath = findSetupApp(mountpoint) if setuppath: cataloginfo = getAdobeSetupInfo(mountpoint) if cataloginfo: # add some more data cataloginfo['name'] = cataloginfo['display_name'].replace(' ', '') cataloginfo['installer_type'] = "AdobeSetup" if cataloginfo.get('AdobeSetupType') == "ProductInstall": cataloginfo['uninstallable'] = True cataloginfo['uninstall_method'] = "AdobeSetup" else: cataloginfo['description'] = "Adobe updater" cataloginfo['uninstallable'] = False cataloginfo['update_for'] = ["PleaseEditMe-1.0.0.0.0"] return cataloginfo # maybe this is an Adobe Acrobat 9 Pro patcher? acrobatpatcherapp = findAcrobatPatchApp(mountpoint) if acrobatpatcherapp: cataloginfo = {} cataloginfo['installer_type'] = "AdobeAcrobatUpdater" cataloginfo['uninstallable'] = False plist = getBundleInfo(acrobatpatcherapp) cataloginfo['version'] = munkicommon.getVersionString(plist) cataloginfo['name'] = "AcrobatPro9Update" cataloginfo['display_name'] = "Adobe Acrobat Pro Update" cataloginfo['update_for'] = ["AcrobatPro9"] cataloginfo['RestartAction'] = 'RequireLogout' cataloginfo['requires'] = [] cataloginfo['installs'] = [ {'CFBundleIdentifier': 'com.adobe.Acrobat.Pro', 'CFBundleName': 'Acrobat', 'CFBundleShortVersionString': cataloginfo['version'], 'path': '/Applications/Adobe Acrobat 9 Pro/Adobe Acrobat Pro.app', 'type': 'application'} ] return cataloginfo # didn't find any Adobe installers/updaters we understand return None def adobeSetupError(errorcode): '''Returns text description for numeric error code Reference: http://www.adobe.com/devnet/creativesuite/pdfs/DeployGuide.pdf''' errormessage = { 0: "Application installed successfully", 1: "Unable to parse command line", 2: "Unknown user interface mode specified", 3: "Unable to initialize ExtendScript", 4: "User interface workflow failed", 5: "Unable to initialize user interface workflow", 6: "Silent workflow completed with errors", 7: "Unable to complete the silent workflow", 8: "Exit and restart", 9: "Unsupported operating system version", 10: "Unsupported file system", 11: "Another instance of Adobe Setup is running", 12: "CAPS integrity error", 13: "Media optimization failed", 14: "Failed due to insufficient privileges", 15: "Media DB Sync Failed", 16: "Failed to laod the Deployment file", 17: "EULA Acceptance Failed", 18: "C3PO Bootstrap Failed", 19: "Conflicting processes running", 20: "Install source path not specified or does not exist", 21: "Version of payloads is not supported by this version of RIB", 22: "Install Directory check failed", 23: "System Requirements Check failed", 24: "Exit User Canceled Workflow", 25: "A binary path Name exceeded Operating System's MAX PATH limit", 26: "Media Swap Required in Silent Mode", 27: "Keyed files detected in target", 28: "Base product is not installed", 29: "Base product has been moved", 30: "Insufficient disk space to install the payload + Done with errors", 31: "Insufficient disk space to install the payload + Failed", 32: "The patch is already applied", 9999: "Catastrophic error", -1: "AdobeUberInstaller failed before launching Setup"} return errormessage.get(errorcode, "Unknown error") def doAdobeRemoval(item): '''Wrapper for all the Adobe removal methods''' uninstallmethod = item['uninstall_method'] payloads = item.get("payloads") itempath = "" if "uninstaller_item" in item: managedinstallbase = munkicommon.pref('ManagedInstallDir') itempath = os.path.join(managedinstallbase, 'Cache', item["uninstaller_item"]) if not os.path.exists(itempath): munkicommon.display_error("%s package for %s was " "missing from the cache." % (uninstallmethod, item['name'])) return -1 if uninstallmethod == "AdobeSetup": # CS3 uninstall retcode = runAdobeSetup(itempath, uninstalling=True, payloads=payloads) elif uninstallmethod == "AdobeUberUninstaller": # CS4 uninstall pkgname = item.get("adobe_package_name") or item.get("package_path", "") retcode = runAdobeUberTool( itempath, pkgname, uninstalling=True, payloads=payloads) elif uninstallmethod == "AdobeCS5AAMEEPackage": # CS5 uninstall. Sheesh. Three releases, three methods. adobeInstallInfo = item.get('adobe_install_info') retcode = doAdobeCS5Uninstall(adobeInstallInfo, payloads=payloads) elif uninstallmethod == "AdobeCCPUninstaller": # Adobe Creative Cloud Packager packages retcode = runAdobeCCPpkgScript( itempath, payloads=payloads, operation="uninstall") if retcode: munkicommon.display_error("Uninstall of %s failed.", item['name']) return retcode def doAdobeInstall(item): '''Wrapper to handle all the Adobe installer methods. First get the path to the installer dmg. We know it exists because installer.py already checked.''' managedinstallbase = munkicommon.pref('ManagedInstallDir') itempath = os.path.join( managedinstallbase, 'Cache', item['installer_item']) installer_type = item.get("installer_type", "") payloads = item.get("payloads") if installer_type == "AdobeSetup": # Adobe CS3/CS4 updater or Adobe CS3 installer retcode = runAdobeSetup(itempath, payloads=payloads) elif installer_type == "AdobeUberInstaller": # Adobe CS4 installer pkgname = item.get("adobe_package_name") or item.get("package_path", "") retcode = runAdobeUberTool(itempath, pkgname, payloads=payloads) elif installer_type == "AdobeAcrobatUpdater": # Acrobat Pro 9 updater retcode = updateAcrobatPro(itempath) elif installer_type == "AdobeCS5AAMEEPackage": # Adobe CS5 AAMEE package retcode = runAdobeCS5AAMEEInstall(itempath, payloads=payloads) elif installer_type == "AdobeCS5PatchInstaller": # Adobe CS5 updater retcode = runAdobeCS5PatchInstaller( itempath, copylocal=item.get("copy_local"), payloads=payloads) elif installer_type == "AdobeCCPInstaller": # Adobe Creative Cloud Packager packages retcode = runAdobeCCPpkgScript(itempath, payloads=payloads) return retcode def main(): '''Placeholder''' pass if __name__ == '__main__': main()