mirror of
https://github.com/munki/munki.git
synced 2026-05-21 05:38:31 -05:00
Initial dmgutils implementation
This commit is contained in:
@@ -19,6 +19,9 @@
|
||||
C013644E2C30F5D8008DB215 /* RepoFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013644C2C30F5D8008DB215 /* RepoFactory.swift */; };
|
||||
C013644F2C30F5D8008DB215 /* RepoFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013644C2C30F5D8008DB215 /* RepoFactory.swift */; };
|
||||
C01364522C311DFA008DB215 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = C01364512C311DFA008DB215 /* ArgumentParser */; };
|
||||
C01364542C321FE7008DB215 /* dmgutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01364532C321FE7008DB215 /* dmgutils.swift */; };
|
||||
C01364552C321FE7008DB215 /* dmgutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01364532C321FE7008DB215 /* dmgutils.swift */; };
|
||||
C01364562C321FE7008DB215 /* dmgutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01364532C321FE7008DB215 /* dmgutils.swift */; };
|
||||
C07A6FA92C2A82B400090743 /* managedsoftwareupdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FA82C2A82B400090743 /* managedsoftwareupdate.swift */; };
|
||||
C07A6FB02C2B22A400090743 /* prefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FAF2C2B22A400090743 /* prefs.swift */; };
|
||||
C07A6FB22C2B22D300090743 /* constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FB12C2B22D300090743 /* constants.swift */; };
|
||||
@@ -75,6 +78,7 @@
|
||||
C01364422C2DD1BA008DB215 /* plistutils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = plistutils.swift; sourceTree = "<group>"; };
|
||||
C01364482C2F8EFE008DB215 /* GitFileRepo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitFileRepo.swift; sourceTree = "<group>"; };
|
||||
C013644C2C30F5D8008DB215 /* RepoFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepoFactory.swift; sourceTree = "<group>"; };
|
||||
C01364532C321FE7008DB215 /* dmgutils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dmgutils.swift; sourceTree = "<group>"; };
|
||||
C07A6FA52C2A82B400090743 /* managedsoftwareupdate */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = managedsoftwareupdate; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C07A6FA82C2A82B400090743 /* managedsoftwareupdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = managedsoftwareupdate.swift; sourceTree = "<group>"; };
|
||||
C07A6FAF2C2B22A400090743 /* prefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = prefs.swift; sourceTree = "<group>"; };
|
||||
@@ -197,6 +201,7 @@
|
||||
C07A6FD12C2B654300090743 /* utils.swift */,
|
||||
C07A6FD92C2CF19600090743 /* cliutils.swift */,
|
||||
C01364422C2DD1BA008DB215 /* plistutils.swift */,
|
||||
C01364532C321FE7008DB215 /* dmgutils.swift */,
|
||||
);
|
||||
path = shared;
|
||||
sourceTree = "<group>";
|
||||
@@ -315,6 +320,7 @@
|
||||
C07A6FB02C2B22A400090743 /* prefs.swift in Sources */,
|
||||
C07A6FA92C2A82B400090743 /* managedsoftwareupdate.swift in Sources */,
|
||||
C07A6FB22C2B22D300090743 /* constants.swift in Sources */,
|
||||
C01364542C321FE7008DB215 /* dmgutils.swift in Sources */,
|
||||
C07A6FD22C2B654300090743 /* utils.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -325,6 +331,7 @@
|
||||
files = (
|
||||
C01364442C2DD1BA008DB215 /* plistutils.swift in Sources */,
|
||||
C01364462C2E051F008DB215 /* makecatalogslib.swift in Sources */,
|
||||
C01364552C321FE7008DB215 /* dmgutils.swift in Sources */,
|
||||
C07A6FDB2C2CF19600090743 /* cliutils.swift in Sources */,
|
||||
C013644E2C30F5D8008DB215 /* RepoFactory.swift in Sources */,
|
||||
C07A6FBE2C2B5AF000090743 /* prefs.swift in Sources */,
|
||||
@@ -343,6 +350,7 @@
|
||||
files = (
|
||||
C07A6FDC2C2CF19600090743 /* cliutils.swift in Sources */,
|
||||
C01364452C2DD1BA008DB215 /* plistutils.swift in Sources */,
|
||||
C01364562C321FE7008DB215 /* dmgutils.swift in Sources */,
|
||||
C07A6FCB2C2B5C3A00090743 /* prefs.swift in Sources */,
|
||||
C013644F2C30F5D8008DB215 /* RepoFactory.swift in Sources */,
|
||||
C07A6FC72C2B5C0700090743 /* makecatalogs.swift in Sources */,
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
//
|
||||
// dmgutils.swift
|
||||
// munki
|
||||
//
|
||||
// Created by Greg Neagle on 6/30/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
func hdiutilData(arguments: [String], stdIn: String = "") -> PlistDict {
|
||||
// runs an hdiutil <command> on a dmg and attempts to return a plist data structure
|
||||
var hdiUtilArgs = arguments
|
||||
if !hdiUtilArgs.contains("-plist") {
|
||||
hdiUtilArgs.append("-plist")
|
||||
}
|
||||
let results = runCLI("/usr/bin/hdiutil", arguments: hdiUtilArgs, stdIn: stdIn)
|
||||
if results.exitcode != 0 {
|
||||
//TODO: implement Munki's display* methods
|
||||
printStderr("hdiutil error \(results.error) with arguments \(arguments)")
|
||||
}
|
||||
let (plistStr, _) = parseFirstPlist(fromString: results.output)
|
||||
if !plistStr.isEmpty {
|
||||
do {
|
||||
if let plist = try readPlistFromString(plistStr) as? PlistDict {
|
||||
return plist
|
||||
}
|
||||
} catch {
|
||||
return PlistDict()
|
||||
}
|
||||
}
|
||||
return PlistDict()
|
||||
}
|
||||
|
||||
func dmgImageInfo(_ dmgPath: String) -> PlistDict {
|
||||
// runs hdiutil imageinfo on a dmg and returns a plist data structure
|
||||
return hdiutilData(arguments: ["imageinfo", dmgPath])
|
||||
}
|
||||
|
||||
|
||||
func dmgIsWritable(_ dmgPath: String) -> Bool {
|
||||
// Attempts to determine if the given disk image is writable
|
||||
let imageInfo = dmgImageInfo(dmgPath)
|
||||
if let format = imageInfo["Format"] as? String {
|
||||
if ["UDSB", "UDSP", "UDRW", "RdWr"].contains(format) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func dmgHasSLA(_ dmgPath: String) -> Bool {
|
||||
// Returns true if dmg has a Software License Agreement.
|
||||
// These dmgs normally cannot be attached without user intervention
|
||||
let imageInfo = dmgImageInfo(dmgPath)
|
||||
if let properties = imageInfo["Properties"] as? PlistDict {
|
||||
if let hasSLA = properties["Software License Agreement"] as? Bool {
|
||||
return hasSLA
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func hdiutilInfo() -> PlistDict {
|
||||
// runs hdiutil info on a dmg and returns a plist data structure
|
||||
return hdiutilData(arguments: ["info"])
|
||||
}
|
||||
|
||||
|
||||
func pathIsVolumeMountPoint(_ path: String) -> Bool {
|
||||
// Returns a boolean to indicate if path is a mountpoint for a disk image
|
||||
let info = hdiutilInfo()
|
||||
if let images = info["images"] as? [PlistDict] {
|
||||
// "images" is an array of dicts
|
||||
for image in images {
|
||||
if let systemEntities = image["system-entities"] as? [PlistDict] {
|
||||
// "system-entities" is an array of dicts
|
||||
for entity in systemEntities {
|
||||
if let mountpoint = entity["mount-point"] as? String {
|
||||
// there's a mount-point for this!
|
||||
if path == mountpoint {
|
||||
// our path is this mountpoint
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func diskImageForMountPoint(_ path: String) -> String? {
|
||||
// Attempts to find the path to a dmg for a given mount point
|
||||
let info = hdiutilInfo()
|
||||
if let images = info["images"] as? [PlistDict] {
|
||||
// "images" is an array of dicts
|
||||
for image in images {
|
||||
if let imagePath = image["image-path"] as? String {
|
||||
if let systemEntities = image["system-entities"] as? [PlistDict] {
|
||||
// "system-entities" is an array of dicts
|
||||
for entity in systemEntities {
|
||||
if let mountpoint = entity["mount-point"] as? String {
|
||||
// there's a mount-point for this!
|
||||
if path == mountpoint {
|
||||
// our path is this mountpoint
|
||||
return imagePath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mountPointsForDiskImage(_ dmgPath: String) -> [String] {
|
||||
// returns a list of mountpoints for the given disk image
|
||||
var mountpoints = [String]()
|
||||
let info = hdiutilInfo()
|
||||
if let images = info["images"] as? [PlistDict] {
|
||||
// "images" is an array of dicts
|
||||
for image in images {
|
||||
if let imagePath = image["image-path"] as? String {
|
||||
// "image-path" is path to dmg file
|
||||
if imagePath == dmgPath {
|
||||
// this is our disk image
|
||||
if let systemEntities = image["system-entities"] as? [PlistDict] {
|
||||
// "system-entities" is an array of dicts
|
||||
for entity in systemEntities {
|
||||
if let mountpoint = entity["mount-point"] as? String {
|
||||
// there's a mount-point for this!
|
||||
mountpoints.append(mountpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return mountpoints
|
||||
}
|
||||
|
||||
|
||||
func diskImageIsMounted(_ dmgPath: String) -> Bool {
|
||||
// Returns true if the given disk image is currently mounted
|
||||
return !mountPointsForDiskImage(dmgPath).isEmpty
|
||||
}
|
||||
|
||||
|
||||
func mountdmg(
|
||||
_ dmgPath: String,
|
||||
useShadow: Bool = false,
|
||||
useExistingMounts: Bool = false,
|
||||
randomMountpoint: Bool = true,
|
||||
skipVerification: Bool = false) -> [String]
|
||||
{
|
||||
// Attempts to mount the dmg at dmgpath
|
||||
// and returns a list of mountpoints
|
||||
// If use_shadow is true, mount image with shadow file
|
||||
// If random_mountpoint, mount at random dir under /tmp
|
||||
var mountpoints = [String]()
|
||||
let dmgName = (dmgPath as NSString).lastPathComponent
|
||||
|
||||
if useExistingMounts {
|
||||
if diskImageIsMounted(dmgPath) {
|
||||
return mountPointsForDiskImage(dmgPath)
|
||||
}
|
||||
}
|
||||
|
||||
// attempt to mount the dmg
|
||||
var stdIn = ""
|
||||
if dmgHasSLA(dmgPath) {
|
||||
stdIn = "Y\n"
|
||||
// TODO: implement Munki display* methods
|
||||
print("NOTE: \(dmgName) has embedded Software License Agreement")
|
||||
}
|
||||
var arguments = ["attach", dmgPath, "-nobrowse"]
|
||||
if randomMountpoint {
|
||||
arguments += ["-mountRandom", "/tmp"]
|
||||
}
|
||||
if useShadow {
|
||||
arguments.append("-shadow")
|
||||
}
|
||||
if skipVerification {
|
||||
arguments.append("-noverify")
|
||||
}
|
||||
let plistData = hdiutilData(arguments: arguments, stdIn: stdIn)
|
||||
if let systemEntities = plistData["system-entities"] as? [PlistDict] {
|
||||
for entity in systemEntities {
|
||||
if let mountPoint = entity["mount-point"] as? String {
|
||||
mountpoints.append(mountPoint)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: implement Munki display* methods
|
||||
printStderr("Could not get mountpoint info from results of hdiutil attach \(dmgName)")
|
||||
}
|
||||
return mountpoints
|
||||
}
|
||||
|
||||
func unmountdmg(_ mountpoint: String) {
|
||||
// Unmounts the dmg at mountpoint
|
||||
var arguments = ["detach", mountpoint]
|
||||
let results = runCLI("/usr/bin/hdiutil", arguments: arguments)
|
||||
if results.exitcode != 0 {
|
||||
// regular unmount failed; try to force unmount
|
||||
// TODO: implement Munki display* methods
|
||||
printStderr("Polite unmount failed: \(results.error)")
|
||||
printStderr("Attempting to force unmount \(mountpoint)")
|
||||
arguments.append("-force")
|
||||
let results = runCLI("/usr/bin/hdiutil", arguments: arguments)
|
||||
if results.exitcode != 0 {
|
||||
// TODO: implement Munki display* methods
|
||||
print("WARNING: Failed to unmount \(mountpoint): \(results.error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,3 +12,35 @@ func getVersion() -> String {
|
||||
// or figure out a way to update this at build time
|
||||
return "0.0.1"
|
||||
}
|
||||
|
||||
func parseFirstPlist(fromString str: String) -> (String, String) {
|
||||
// Parses a string, looking for the first thing that looks like a plist.
|
||||
// Returns two strings. The first will be a string representaion of a plist (or empty)
|
||||
// The second is any characters remaining after the found plist
|
||||
let header = "<?xml version"
|
||||
let footer = "</plist>"
|
||||
let headerRange = (str as NSString).range(of: header)
|
||||
if headerRange.location == NSNotFound {
|
||||
// header not found
|
||||
return ("", str)
|
||||
}
|
||||
let footerSearchIndex = headerRange.location + headerRange.length
|
||||
let footerSearchRange = NSRange(
|
||||
location: footerSearchIndex,
|
||||
length: str.count - footerSearchIndex
|
||||
)
|
||||
let footerRange = (str as NSString).range(of: footer, range: footerSearchRange)
|
||||
if footerRange.location == NSNotFound {
|
||||
// footer not found
|
||||
return ("", str)
|
||||
}
|
||||
let plistRange = NSRange(
|
||||
location: headerRange.location,
|
||||
length: footerRange.location + footerRange.length - headerRange.location
|
||||
)
|
||||
let plistStr = (str as NSString).substring(with: plistRange)
|
||||
let remainderIndex = plistRange.location + plistRange.length
|
||||
let remainder = (str as NSString).substring(from: remainderIndex)
|
||||
return(plistStr, remainder)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user