def listApfsVolumes(cls): LogDebug("Listing APFS volumes") if cls.hostVersionTuple()[1] < 13: return {} p = subprocess.Popen(["/usr/sbin/diskutil", "apfs", "list", "-plist"], bufsize=1, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() if p.returncode != 0: LogError("diskutil apfs list failed with return code %d" % p.returncode) return None outData = NSData.dataWithBytes_length_(out, len(out)) plist, format, error = \ NSPropertyListSerialization.propertyListWithData_options_format_error_(outData, NSPropertyListImmutable, None, None) if "Containers" not in plist: LogError("diskutil apfs list does not have any Containers") return None volumes = {} for container in plist["Containers"]: for volume in container.get("Volumes", []): volumes[volume["DeviceIdentifier"]] = volume return volumes
def taskFinalize(self): LogNotice(u"Finalize task running") self.delegate.buildSetProgressMessage_( u"Scanning disk image for restore") # The script is wrapped with progresswatcher.py which parses script # output and sends it back as notifications to IEDSocketListener. args = [ NSBundle.mainBundle().pathForResource_ofType_( u"progresswatcher", u"py"), u"--socket", self.listenerPath, u"imagescan", self.outputPath(), ] LogInfo(u"Launching finalize with arguments:") for arg in args: LogInfo(u" '%@'", arg) try: p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) out = p.communicate()[0].decode(u"utf-8") LogDebug(u"Finalize exited with status %d and output '%@'", p.returncode, out) if p.returncode != 0: errMsg = u"Finalize task failed with status %d" % p.returncode LogError(u"%@: %@", errMsg, out) self.fail_details_(errMsg, out) except BaseException as e: LogError(u"Failed to launch finalize task: %@", unicode(e)) self.fail_details_(u"Failed to launch finalize task", unicode(e))
def getFlatPkgInfo_(cls, pkgPath): tempdir = tempfile.mkdtemp() try: # Extract to tempdir, excluding all except Distribution and # PackageInfo. subprocess.check_output([ "/usr/bin/xar", "-x", "--exclude", "^[^DP]", "--exclude", "Payload", "-C", tempdir, "-f", pkgPath ]) distPath = os.path.join(tempdir, "Distribution") pkgInfoPath = os.path.join(tempdir, "PackageInfo") if os.path.exists(distPath): return cls.getSizeFromDistribution_(distPath) elif os.path.exists(pkgInfoPath): return cls.getSizeFromPackageInfo_(pkgInfoPath) else: LogError("No Distribution or PackageInfo found in '%@'", pkgPath) return None except subprocess.CalledProcessError as e: LogError("xar failed with return code %d", e.returncode) return None finally: try: shutil.rmtree(tempdir) except Exception as e: LogWarning("Unable to remove tempdir: %@", str(e))
def listenInBackground_(self, ignored): try: sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, (IEDSL_MAX_MSG_SIZE + 16) * IEDSL_MAX_MSG_COUNT) sock.bind(self.socketPath) except socket.error as e: LogError("Error creating datagram socket at %@: %@", self.socketPath, str(e)) return LogDebug("Listening to socket in background thread") while True: msg = sock.recv(IEDSL_MAX_MSG_SIZE, socket.MSG_WAITALL) if not msg: continue msgData = NSData.dataWithBytes_length_(msg, len(msg)) plist, format, error = NSPropertyListSerialization.propertyListWithData_options_format_error_( msgData, NSPropertyListImmutable, None, None) if not plist: LogError("Error decoding plist: %@", error) continue if self.delegate.respondsToSelector_("socketReceivedMessage:"): self.delegate.performSelectorOnMainThread_withObject_waitUntilDone_( "socketReceivedMessage:", plist, False)
def connectionDidFinishLoading_(self, connection): LogInfo(u"%@ finished downloading to %@", self.package.name(), self.cacheTmpPath_(self.package.sha1())) self.fileHandle.closeFile() self.delegate.downloadStopped_(self.package) if self.checksum.hexdigest() == self.package.sha1(): try: os.rename(self.cacheTmpPath_(self.package.sha1()), self.cachePath_(self.package.sha1())) except OSError as e: error = u"Failed when moving download to %s: %s" % ( self.cachePath_(self.package.sha1()), unicode(e)) LogError(error) self.delegate.downloadFailed_withError_(self.package, error) return linkPath = self.updatePath_(self.package.sha1()) try: os.symlink(self.package.sha1(), linkPath) except OSError as e: error = u"Failed when creating link from %s to %s: %s" % ( self.package.sha1(), linkPath, unicode(e)) LogError(error) self.delegate.downloadFailed_withError_(self.package, error) return LogNotice(u"%@ added to cache with sha1 %@", self.package.name(), self.package.sha1()) self.delegate.downloadSucceeded_(self.package) self.downloadNextUpdate() else: error = u"Expected sha1 checksum %s but got %s" % ( sha1.lower(), m.hexdigest().lower()) LogError(error) self.delegate.downloadFailed_withError_(self.package, error)
def getInstalledPkgSize_(cls, pkgPath): # For apps just return the size on disk. name, ext = os.path.splitext(pkgPath) if ext == u".app": return cls.getPackageSize_(pkgPath) # For packages try to get the size requirements with installer. pkgFileName = os.path.os.path.basename(pkgPath) tempdir = tempfile.mkdtemp() try: symlinkPath = os.path.join(tempdir, pkgFileName) os.symlink(pkgPath, symlinkPath) p = subprocess.Popen([u"/usr/sbin/installer", u"-pkginfo", u"-verbose", u"-plist", u"-pkg", symlinkPath], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() finally: try: shutil.rmtree(tempdir) except BaseException as e: LogWarning(u"Unable to remove tempdir: %@", unicode(e)) # Try to handle some common scenarios when installer fails. if p.returncode == -11: LogWarning(u"Estimating package size since installer -pkginfo " u"'%@' crashed", pkgPath) return cls.getPackageSize_(pkgPath) * 2 elif p.returncode != 0: mountPoints = IEDMountInfo.getMountPoints() fsInfo = mountPoints[cls.findMountPoint_(pkgPath)] if not fsInfo[u"islocal"]: LogWarning(u"Estimating package size since installer -pkginfo " u"failed and '%@' is on a remote (%@) filesystem", pkgPath, fsInfo[u"fstypename"]) return cls.getPackageSize_(pkgPath) * 2 else: LogError(u"installer -pkginfo -pkg '%@' failed with exit code %d", pkgPath, p.returncode) return None outData = NSData.dataWithBytes_length_(out, len(out)) plist, format, error = NSPropertyListSerialization.propertyListWithData_options_format_error_(outData, NSPropertyListImmutable, None, None) if not plist: LogError(u"Error decoding plist: %@", error) return None LogDebug(u"%@ requires %@", pkgPath, cls.formatBytes_(int(plist[u"Size"]) * 1024)) return int(plist[u"Size"]) * 1024
def getSizeFromPkgInfoPlist_(cls, infoPlistPath): try: infoDict = NSDictionary.dictionaryWithContentsOfFile_(infoPlistPath) return infoDict[u"IFPkgFlagInstalledSize"] * 1024 except Exception as e: LogError(u"Failed parsing '%@': %@", infoPlistPath, unicode(e)) return None
def logFailure_(self, message): LogError("Version check failed: %@", message) if not self.checkSilently: alert = NSAlert.alloc().init() alert.setMessageText_("Version check failed") alert.setInformativeText_(message) alert.runModal()
def getInstalledPkgSize_(cls, pkgPath): # For apps just return the size on disk. ext = os.path.splitext(pkgPath)[1].lower() if ext == ".app": return cls.getPackageSize_(pkgPath) elif ext in (".pkg", ".mpkg"): # For packages first try to get the size requirements with # installer. size = cls.getInstalledPkgSizeFromInstaller_(pkgPath) if size is None: # If this fails, manually extract the size requirements from # the package. return cls.calculateInstalledPkgSize_(pkgPath) else: return size elif os.path.basename(pkgPath) == "InstallInfo.plist": iedPath = os.path.join(os.path.dirname(pkgPath), "InstallESD.dmg") try: iedSize = os.stat(iedPath).st_size estimatedSize = int(iedSize * 2.72127696157) LogDebug( "Estimating size requirements of InstallInfo.plist to %@", cls.formatByteSize_(estimatedSize)) return estimatedSize except OSError: pass LogError("Don't know how to calculate installed size for '%@'", pkgPath) return None
def getInstalledPkgSizeFromInstaller_(cls, pkgPath): pkgFileName = os.path.os.path.basename(pkgPath) tempdir = tempfile.mkdtemp() try: symlinkPath = os.path.join(tempdir, pkgFileName) os.symlink(pkgPath, symlinkPath) p = subprocess.Popen([ "/usr/sbin/installer", "-pkginfo", "-verbose", "-plist", "-pkg", symlinkPath ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() finally: try: shutil.rmtree(tempdir) except BaseException as e: LogWarning("Unable to remove tempdir: %@", str(e)) if p.returncode != 0: LogDebug( "/usr/sbin/installer failed to determine size requirements") return None outData = NSData.dataWithBytes_length_(out, len(out)) plist, format, error = NSPropertyListSerialization.propertyListWithData_options_format_error_( outData, NSPropertyListImmutable, None, None) if not plist: LogError("Error decoding plist: %@", error) return None LogDebug("Installer says %@ requires %@", pkgPath, cls.formatByteSize_(int(plist["Size"]) * 1024)) return int(plist["Size"]) * 1024
def connection_didFailWithError_(self, connection, error): LogError(u"%@ failed: %@", self.package.name(), error) self.delegate.downloadStopped_(self.package) self.fileHandle.closeFile() self.delegate.downloadFailed_withError_(self.package, error.localizedDescription()) self.delegate.downloadAllDone()
def saveUsersProfiles_(self, plist): """Save UpdateProfiles.plist to application support.""" LogInfo("Saving update profiles with PublicationDate %@", plist["PublicationDate"]) if not plist.writeToFile_atomically_(self.userUpdateProfilesPath, False): LogError("Failed to write %@", self.userUpdateProfilesPath)
def getSizeFromDistribution_(cls, distPath): kbytes = 0 try: tree = ElementTree.parse(distPath) for pkgref in tree.iterfind("pkg-ref[@installKBytes]"): kbytes += int(pkgref.get("installKBytes")) except Exception as e: LogError("Failed parsing '%@': %@", distPath, str(e)) return None return kbytes * 1024
def getPackageSize_(cls, path): p = subprocess.Popen(["/usr/bin/du", "-sk", path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() if p.returncode != 0: LogError("du failed with exit code %d", p.returncode) return 0 else: return int(out.split()[0]) * 1024
def getSizeFromPackageInfo_(cls, pkgInfoPath): kbytes = 0 try: tree = ElementTree.parse(pkgInfoPath) for payload in tree.iterfind("payload[@installKBytes]"): kbytes += int(payload.get("installKBytes")) except Exception as e: LogError("Failed parsing '%@': %@", pkgInfoPath, str(e)) return None return kbytes * 1024
def getBundlePkgInfo_(cls, pkgPath): distPath = os.path.join(pkgPath, u"Contents", u"distribution.dist") infoPlistPath = os.path.join(pkgPath, u"Contents", u"Info.plist") if os.path.exists(distPath): return cls.getSizeFromDistribution_(distPath) elif os.path.exists(infoPlistPath): return cls.getSizeFromPkgInfoPlist_(infoPlistPath) else: LogError(u"No distribution.dist or Info.plist found in '%@'", pkgPath) return None
def init(self): self = super(IEDUpdateController, self).init() if self is None: LogError("Failed to initialize IEDUpdateController") return None self.cache = IEDUpdateCache.alloc().initWithDelegate_(self) if not self.cache: LogError("Failed to initialize IEDUpdateController") return None self.updates = list() self.downloadTotalSize = 0 self.downloads = list() self.delegate = None self.version = None self.build = None self.profileWarning = None self.boxTableSizeDelta = 0 return self
def setAdditionalPackages_(self, packagePaths): self.additionalPackageError = None for packagePath in packagePaths: path = IEDUtil.resolvePath_( os.path.abspath(os.path.expanduser(packagePath))) if not os.path.exists(path): self.additionalPackageError = u"Package '%s' not found" % packagePath LogError(u"'%@'", self.additionalPackageError) return False name, ext = os.path.splitext(path) if ext.lower() not in IEDUtil.PACKAGE_EXTENSIONS: self.additionalPackageError = u"'%s' is not valid software package" % packagePath LogError(u"'%@'", self.additionalPackageError) return False if path not in self.additionalPackages: LogInfo(u"Adding '%@' to additional packages", path) self.additionalPackages.append(IEDUtil.resolvePath_(path)) else: LogInfo(u"Skipping duplicate package '%@'", path) return True
def setAdditionalPackages_(self, packagePaths): for packagePath in packagePaths: path = IEDUtil.resolvePath_(os.path.expanduser(packagePath)) if not path: LogError(u"Package '%@' not found", packagePath) return False if path not in self.additionalPackages: LogInfo(u"Adding '%@' to additional packages", path) self.additionalPackages.append(IEDUtil.resolvePath_(path)) else: LogInfo(u"Skipping duplicate package '%@'", path) return True
def loadProfilesFromPlist_(self, plist): """Load UpdateProfiles from a plist dictionary.""" LogInfo("Loading update profiles with PublicationDate %@", plist["PublicationDate"]) try: # FIXME: Add profile verification. self.profiles = dict() for name, updates in plist["Profiles"].iteritems(): profile = list() for update in updates: profile.append(plist["Updates"][update]) self.profiles[name] = profile self.publicationDate = plist["PublicationDate"] self.updatePaths = dict() for name, update in plist["Updates"].iteritems(): filename, ext = os.path.splitext( os.path.basename(update["url"])) self.updatePaths[update["sha1"]] = "%s(%s)%s" % ( filename, update["sha1"][:7], ext) self.deprecatedInstallerBuilds = dict() try: for replacement, builds in plist[ "DeprecatedInstallers"].iteritems(): for build in builds: self.deprecatedInstallerBuilds[build] = replacement except KeyError: LogWarning("No deprecated installers in profile") self.deprecatedOS = False try: for osVerStr in plist["DeprecatedOSVersions"]: deprecatedVerMajor = IEDUtil.splitVersion(osVerStr)[1] if IEDUtil.hostMajorVersion() <= deprecatedVerMajor: self.deprecatedOS = True LogWarning("%@ is no longer being updated by Apple", osVerStr) break except KeyError: LogWarning("No deprecated OS versions in profile") if self.delegate: self.delegate.profilesUpdated() except BaseException as e: LogError("Failed to load profile: %@", unicode(e))
def connection_didReceiveData_(self, connection, data): try: self.fileHandle.writeData_(data) except BaseException as e: LogError(u"Write error: %@", unicode(e)) connection.cancel() error = u"Writing to %s failed: %s" % (self.cacheTmpPath_(self.package.sha1()), unicode(e)) self.fileHandle.closeFile() self.delegate.downloadFailed_withError_(self.package, error) self.delegate.downloadAllDone() return self.checksum.update(data) self.bytesReceived += data.length() self.delegate.downloadGotData_bytesRead_(self.package, self.bytesReceived)
def init(self): self = super(IEDUpdateCache, self).init() if self is None: return None fm = NSFileManager.defaultManager() url, error = fm.URLForDirectory_inDomain_appropriateForURL_create_error_( NSApplicationSupportDirectory, NSUserDomainMask, None, True, None) if not url: if error: LogError("Unable to locate Application Support directory: %@", error.localizedDescription()) else: LogError("Unable to locate Application Support directory") return None self.updateDir = os.path.join(url.path(), "AutoDMG", "Updates") if not os.path.exists(self.updateDir): try: os.makedirs(self.updateDir) except OSError as e: LogError("Failed to create %@: %@", self.updateDir, str(e)) return self
def init(self): self = super(IEDUpdateCache, self).init() if self is None: return None fm = NSFileManager.defaultManager() url, error = fm.URLForDirectory_inDomain_appropriateForURL_create_error_( NSApplicationSupportDirectory, NSUserDomainMask, None, True, None) self.updateDir = os.path.join(url.path(), u"AutoDMG", u"Updates") if not os.path.exists(self.updateDir): try: os.makedirs(self.updateDir) except OSError as e: LogError(u"Failed to create %@: %@", self.updateDir, unicode(e)) return self
def checkAppleBugWarning_(self, path): LogDebug("checkAppleBugWarning:%@", path) if (IEDUtil.hostMajorVersion() != 14): return True LogDebug("We're running on Mojave") if not path.endswith(".app"): return True LogDebug("The source is a .app") if IEDUtil.volumePathForPath_(path) != "/": return True LogError("The source is an installer app on a Mojave system volume") text = "A bug in the OS installer (radar 43296160) causes installs to fail " \ "if the Mojave installer is on the system volume. Copy it to an external " \ "volume or wrap it in a dmg before dropping it on AutoDMG." self.delegate.sourceFailed_text_("Install will fail due to Apple bug", text) return False
def socketReceivedMessage_(self, msg): # The message is a dictionary with "action" as the only required key. action = msg["action"] if action == "update_progress": percent = msg["percent"] currentProgress = self.progress + self.currentPhase[ "weight"] * percent / 100.0 self.delegate.buildSetProgress_(currentProgress) elif action == "update_message": if self.lastUpdateMessage != msg["message"]: # Only log update messages when they change. LogInfo("%@", msg["message"]) self.lastUpdateMessage = msg["message"] self.delegate.buildSetProgressMessage_(msg["message"]) elif action == "select_phase": LogNotice("Script phase: %@", msg["phase"]) self.nextPhase() elif action == "log_message": LogMessage(msg["log_level"], msg["message"]) elif action == "notify_failure": self.fail_details_("Build failed", msg["message"]) elif action == "notify_success": LogNotice("Build success: %@", msg["message"]) elif action == "task_done": status = msg["termination_status"] if status == 0: self.nextTask() else: details = NSString.stringWithFormat_( "Task exited with status %@", msg["termination_status"]) LogError("%@", details) # Status codes 100-199 are from installesdtodmg.sh, and have # been preceeded by a "notify_failure" message. if (status < 100) or (status > 199): self.fail_details_("Build failed", details) else: self.fail_details_("Unknown progress notification", "Message: %@", msg)
def getInstalledPkgSize_(cls, pkgPath): # For apps just return the size on disk. ext = os.path.splitext(pkgPath)[1].lower() if ext == ".app": return cls.getPackageSize_(pkgPath) elif ext in (".pkg", ".mpkg"): # For packages first try to get the size requirements with # installer. size = cls.getInstalledPkgSizeFromInstaller_(pkgPath) if size is None: # If this fails, manually extract the size requirements from # the package. return cls.calculateInstalledPkgSize_(pkgPath) else: return size else: LogError("Don't know how to calculate installed size for '%@'", pkgPath) return None
def init(self): self = super(IEDCLIController, self).init() if self is None: return None self.cache = IEDUpdateCache.alloc().initWithDelegate_(self) if not self.cache: LogError("Failed to initialize IEDUpdateController") return None self.workflow = IEDWorkflow.alloc().initWithDelegate_(self) self.profileController = IEDProfileController.alloc().init() self.profileController.awakeFromNib() self.profileController.setDelegate_(self) self.cache.pruneAndCreateSymlinks(self.profileController.updatePaths) self.busy = False self.progressMax = 1.0 self.lastMessage = "" self.hasFailed = False return self
def cli_main(argv): IEDLog.IEDLogToController = False IEDLog.IEDLogToSyslog = True IEDLog.IEDLogToStdOut = True IEDLog.IEDLogToFile = False from IEDCLIController import IEDCLIController clicontroller = IEDCLIController.alloc().init() try: # Initialize user defaults before application starts. defaults = NSUserDefaults.standardUserDefaults() defaultsPath = NSBundle.mainBundle().pathForResource_ofType_( u"Defaults", u"plist") defaultsDict = NSDictionary.dictionaryWithContentsOfFile_(defaultsPath) defaults.registerDefaults_(defaultsDict) p = argparse.ArgumentParser() p.add_argument(u"-v", u"--verbose", action=u"store_true", help=u"Verbose output") p.add_argument(u"-L", u"--log-level", type=int, choices=range(0, 8), default=6, metavar=u"LEVEL", help=u"Log level (0-7), default 6") p.add_argument(u"-l", u"--logfile", help=u"Log to file") p.add_argument(u"-r", u"--root", action=u"store_true", help=u"Allow running as root") sp = p.add_subparsers(title=u"subcommands", dest=u"subcommand") # Populate subparser for each verb. for verb in clicontroller.listVerbs(): verb_method = getattr(clicontroller, u"cmd%s_" % verb.capitalize()) addargs_method = getattr(clicontroller, u"addargs%s_" % verb.capitalize()) parser = sp.add_parser(verb, help=verb_method.__doc__) addargs_method(parser) parser.set_defaults(func=verb_method) args = p.parse_args(argv) if args.verbose: IEDLog.IEDLogStdOutLogLevel = IEDLog.IEDLogLevelInfo else: IEDLog.IEDLogStdOutLogLevel = IEDLog.IEDLogLevelNotice IEDLog.IEDLogFileLogLevel = args.log_level if args.logfile == u"-": # Redirect log to stdout instead. IEDLog.IEDLogFileHandle = sys.stdout IEDLog.IEDLogToFile = True IEDLog.IEDLogToStdOut = False else: try: if args.logfile: logFile = args.logfile else: logFile = os.path.join( get_log_dir(), u"AutoDMG-%s.log" % get_date_string()) IEDLog.IEDLogFileHandle = open(logFile, u"a", buffering=1) except OSError as e: print >> sys.stderr, (u"Couldn't open %s for writing" % logFile).encode(u"utf-8") return os.EX_CANTCREAT IEDLog.IEDLogToFile = True # Check if we're running with root. if os.getuid() == 0: if args.root: fm = NSFileManager.defaultManager() url, error = fm.URLForDirectory_inDomain_appropriateForURL_create_error_( NSApplicationSupportDirectory, NSUserDomainMask, None, False, None) LogWarning(u"Running as root, using %@", os.path.join(url.path(), u"AutoDMG")) else: LogError( u"Running as root isn't recommended (use -r to override)") return os.EX_USAGE # Log version info on startup. version, build = IEDUtil.getAppVersion() LogInfo(u"AutoDMG v%@ build %@", version, build) name, version, build = IEDUtil.readSystemVersion_(u"/") LogInfo(u"%@ %@ %@", name, version, build) LogInfo(u"%@ %@ (%@)", platform.python_implementation(), platform.python_version(), platform.python_compiler()) LogInfo(u"PyObjC %@", objc.__version__) return args.func(args) finally: clicontroller.cleanup()
def failWithMessage_(self, message): LogError("%@", message) self.hasFailed = True self.busy = False
def detachFailed_details_(self, dmgPath, details): LogError("Failed to detach '%@': %@", dmgPath, details)