mirror of
https://github.com/munki/munki.git
synced 2026-05-19 04:38:34 -05:00
1109 lines
46 KiB
Swift
1109 lines
46 KiB
Swift
//
|
|
// analyze.swift
|
|
// munki
|
|
//
|
|
// Created by Greg Neagle on 8/19/24.
|
|
//
|
|
// 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.
|
|
|
|
// TODO: this file has very long, very complex functions that are hard to follow
|
|
// and hard to understand.
|
|
// (Notably processInstall, processOptionalInstall, and processRemoval)
|
|
// Need to refactor these
|
|
|
|
import Foundation
|
|
|
|
func itemInInstallInfo(_ thisItem: PlistDict, theList: [PlistDict], version: String = "") -> Bool {
|
|
// Determines if an item is in a list of processed items.
|
|
//
|
|
// Returns true if the item has already been processed (it's in the list)
|
|
// and, optionally, the version is the same or greater.
|
|
for listItem in theList {
|
|
if let listItemName = listItem["name"] as? String,
|
|
let thisItemName = thisItem["name"] as? String
|
|
{
|
|
if listItemName == thisItemName {
|
|
if version.isEmpty {
|
|
return true
|
|
}
|
|
// if the version already installed or processed to be
|
|
// installed is the same or greater, then we're good.
|
|
if let installed = listItem["installed"] as? Bool,
|
|
installed == true,
|
|
let installedVersion = listItem["installed_version"] as? String,
|
|
compareVersions(installedVersion, version).rawValue > 0
|
|
{
|
|
return true
|
|
}
|
|
if let versionToInstall = listItem["version_to_install"] as? String,
|
|
compareVersions(versionToInstall, version).rawValue > 0
|
|
{
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isAppleItem(_ pkginfo: PlistDict) -> Bool {
|
|
// Returns True if the item to be installed or removed appears to be from
|
|
// Apple. If we are installing or removing any Apple items in a check/install
|
|
// cycle, we skip checking/installing Apple updates from an Apple Software
|
|
// Update server so we don't stomp on each other
|
|
|
|
// is this a startosinstall item?
|
|
if let installerType = pkginfo["installer_type"] as? String,
|
|
installerType == "startosinstall"
|
|
{
|
|
return true
|
|
}
|
|
// check receipts
|
|
if let receipts = pkginfo["receipts"] as? [PlistDict] {
|
|
for receipt in receipts {
|
|
if let pkgid = receipt["packageid"] as? String,
|
|
pkgid.hasPrefix("com.apple.")
|
|
{
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
// check installs items
|
|
if let installs = pkginfo["installs"] as? [PlistDict] {
|
|
for install in installs {
|
|
if let bundleID = install["CFBundleIdentifier"] as? String,
|
|
bundleID.hasPrefix("com.apple.")
|
|
{
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
// if we get here, no receipts or installs items have Apple
|
|
// identifiers
|
|
return false
|
|
}
|
|
|
|
func alreadyProcessed(_ itemName: String, installInfo: PlistDict, sections: [String]) -> Bool {
|
|
// Returns True if itemname has already been added to installinfo in one
|
|
// of the given sections
|
|
let description = [
|
|
"processed_installs": "install",
|
|
"processed_uninstalls": "uninstall",
|
|
"managed_updates": "update",
|
|
"optional_installs": "optional install",
|
|
]
|
|
for section in sections {
|
|
if let listOfNames = installInfo[section] as? [String],
|
|
listOfNames.contains(itemName)
|
|
{
|
|
displayDebug1("\(itemName) has already been processed for \(description[section] ?? "")")
|
|
return true
|
|
} else if let listOfPkgInfos = installInfo[section] as? [PlistDict] {
|
|
for pkginfo in listOfPkgInfos {
|
|
if let pkginfoName = pkginfo["name"] as? String {
|
|
if itemName == pkginfoName {
|
|
displayDebug1("\(itemName) has already been processed for \(description[section] ?? "")")
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func processInstall(
|
|
_ manifestItem: String,
|
|
catalogList: [String],
|
|
installInfo: inout PlistDict,
|
|
isManagedUpdate: Bool = false,
|
|
isOptionalInstall: Bool = false
|
|
) async -> Bool {
|
|
// Processes a manifest item for install. Determines if it needs to be
|
|
// installed, and if so, if any items it is dependent on need to
|
|
// be installed first. Installation detail is added to
|
|
// installinfo['managed_installs']
|
|
// Calls itself recursively as it processes dependencies.
|
|
// Returns a boolean; when processing dependencies, a false return
|
|
// will stop the installation of a dependent item
|
|
|
|
func appendToProcessedManagedInstalls(_ item: PlistDict) {
|
|
// helper function
|
|
var managedInstalls = installInfo["managed_installs"] as? [PlistDict] ?? []
|
|
managedInstalls.append(item)
|
|
installInfo["managed_installs"] = managedInstalls
|
|
}
|
|
|
|
let manifestItemName = (manifestItem as NSString).lastPathComponent
|
|
displayDebug1("* Processing manifest item \(manifestItemName) for install")
|
|
let (manifestItemNameWithoutVersion, includedVersion) = nameAndVersion(manifestItemName, onlySplitOnHyphens: true)
|
|
|
|
// have we processed this already?
|
|
if let processedInstalls = installInfo["processed_installs"] as? [String],
|
|
processedInstalls.contains(manifestItemName)
|
|
{
|
|
displayDebug1("\(manifestItemName) has already been processed for install.")
|
|
return true
|
|
}
|
|
if let processedUninstalls = installInfo["processed_uninstalls"] as? [String],
|
|
processedUninstalls.contains(manifestItemNameWithoutVersion)
|
|
{
|
|
displayDebug1("Will not process \(manifestItemName) for install because it has already been processed for uninstall!")
|
|
return false
|
|
}
|
|
|
|
guard let pkginfo = await getItemDetail(manifestItemName, catalogList: catalogList) else {
|
|
displayWarning("Could not process item \(manifestItemName) for install. No pkginfo found in catalogs: \(catalogList)")
|
|
return false
|
|
}
|
|
|
|
if let managedInstalls = installInfo["managed_installs"] as? [PlistDict],
|
|
let itemName = pkginfo["name"] as? String,
|
|
let itemVersion = pkginfo["version"] as? String,
|
|
itemInInstallInfo(pkginfo, theList: managedInstalls, version: itemVersion)
|
|
{
|
|
// item has been processed for install; now check to see if there
|
|
// was a problem when it was processed
|
|
let problemItemNames: [String]
|
|
problemItemNames = managedInstalls.filter {
|
|
$0["installed"] is Bool &&
|
|
($0["installed"] as? Bool ?? true) == false &&
|
|
$0["installer_item"] == nil
|
|
}.map {
|
|
$0["name"] as? String ?? ""
|
|
}.filter {
|
|
!$0.isEmpty
|
|
}
|
|
|
|
if problemItemNames.contains(itemName) {
|
|
// item was processed, but not successfully downloaded
|
|
displayDebug1("\(manifestItemName) was processed earlier, but download failed.")
|
|
return false
|
|
}
|
|
|
|
// item was processed successfully
|
|
displayDebug1("\(manifestItem) is or will be installed.")
|
|
return true
|
|
}
|
|
|
|
// check dependencies
|
|
var dependenciesMet = true
|
|
|
|
// there are two kinds of dependencies/relationships.
|
|
//
|
|
// 'requires' are prerequisites:
|
|
// package A requires package B be installed first.
|
|
// if package A is removed, package B is unaffected.
|
|
// requires can be a one to many relationship.
|
|
//
|
|
// The second type of relationship is 'update_for'.
|
|
// This signifies that that current package should be considered an update
|
|
// for the packages listed in the 'update_for' array. When processing a
|
|
// package, we look through the catalogs for other packages that declare
|
|
// they are updates for the current package and install them if needed.
|
|
// This can be a one-to-many relationship - one package can be an update
|
|
// for several other packages; for example, 'PhotoshopCS4update-11.0.1'
|
|
// could be an update for PhotoshopCS4 and for AdobeCS4DesignSuite.
|
|
//
|
|
// When removing an item, any updates for that item are removed as well.
|
|
|
|
let name = pkginfo["name"] as? String ?? "<unknown>"
|
|
let version = pkginfo["version"] as? String ?? "<unknown>"
|
|
|
|
// get list of dependencies. 'requires' should be a list of strings, but
|
|
// sometimes admins define it as just a single string. Account for both
|
|
// possibilities
|
|
var dependencies = [String]()
|
|
if let requires = pkginfo["requires"] as? [String] {
|
|
dependencies = requires
|
|
} else if let requires = pkginfo["requires"] as? String {
|
|
dependencies = [requires]
|
|
}
|
|
for item in dependencies {
|
|
displayDetail("\(name)-\(version) requires \(item). Getting info on \(item)...")
|
|
let success = await processInstall(
|
|
item,
|
|
catalogList: catalogList,
|
|
installInfo: &installInfo,
|
|
isManagedUpdate: isManagedUpdate,
|
|
isOptionalInstall: isOptionalInstall
|
|
)
|
|
if !success {
|
|
dependenciesMet = false
|
|
}
|
|
}
|
|
|
|
var processedItem = PlistDict()
|
|
processedItem["name"] = name
|
|
let displayName = pkginfo["display_name"] as? String ?? name
|
|
processedItem["display_name"] = displayName
|
|
processedItem["description"] = pkginfo["description"] as? String ?? ""
|
|
processedItem["localized_strings"] = pkginfo["localized_strings"]
|
|
processedItem["developer"] = pkginfo["developer"]
|
|
processedItem["icon_name"] = pkginfo["icon_name"]
|
|
|
|
let installedState = await installedState(pkginfo)
|
|
if installedState == .thisVersionNotInstalled {
|
|
if !dependenciesMet {
|
|
// we should not attempt to install
|
|
displayWarning("Didn't attempt ro install \(manifestItemName) because could not resolve all dependencies.")
|
|
// add information to managed_installs so we have some feedback
|
|
// to display in MSC.app
|
|
processedItem["installed"] = false
|
|
processedItem["note"] = "Can't install \(displayName) because could not verify all other items it requires are or will be installed."
|
|
appendToProcessedManagedInstalls(processedItem)
|
|
return false
|
|
}
|
|
|
|
displayDetail("Need to install \(manifestItemName)")
|
|
processedItem["installed"] = false
|
|
processedItem["version_to_install"] = version
|
|
let installerItemSize = pkginfo["installer_item_size"] as? Int ?? 0
|
|
processedItem["installer_item_size"] = installerItemSize
|
|
processedItem["installed_size"] = pkginfo["installer_item_size"] as? Int ?? installerItemSize
|
|
var downloadSeconds = 0.0
|
|
var filename = ""
|
|
if let installerType = pkginfo["installer_type"] as? String,
|
|
installerType == "nopkg"
|
|
{
|
|
// "install" that has no actual download
|
|
filename = "packageless_install"
|
|
} else {
|
|
do {
|
|
// record starttime
|
|
let startTime = Date()
|
|
if try downloadInstallerItem(pkginfo, installInfo: installInfo) {
|
|
// Record the download speed for the InstallResults output.
|
|
downloadSeconds = Date().timeIntervalSince(startTime)
|
|
} else {
|
|
// item was in cache and unchanged
|
|
}
|
|
} catch let FetchError.fileSystem(description) {
|
|
displayWarning("Can't install \(manifestItemName) because \(description).")
|
|
processedItem["installed"] = false
|
|
processedItem["note"] = description
|
|
if let installerItemLocation = pkginfo["installer_item_location"] as? String {
|
|
processedItem["partial_installer_item"] = baseName(installerItemLocation)
|
|
}
|
|
processedItem["version_to_install"] = version
|
|
appendToProcessedManagedInstalls(processedItem)
|
|
return false
|
|
} catch {
|
|
// download unsuccessful
|
|
if let installerItemLocation = pkginfo["installer_item_location"] as? String {
|
|
processedItem["partial_installer_item"] = baseName(installerItemLocation)
|
|
}
|
|
if let err = error as? FetchError {
|
|
switch err {
|
|
case .verification:
|
|
displayWarning("Can't install \(manifestItemName) because the integrity check failed.")
|
|
processedItem["note"] = "Integrity check failed"
|
|
processedItem["partial_installer_item"] = nil
|
|
case let .fileSystem(description):
|
|
displayWarning("Can't install \(manifestItemName) because \(description).")
|
|
processedItem["note"] = description
|
|
case let .connection(errorCode, description),
|
|
let .download(errorCode, description),
|
|
let .http(errorCode, description):
|
|
displayWarning("Download of \(manifestItemName) failed: error \(errorCode): \(description)")
|
|
processedItem["note"] = "Download failed: \(description)"
|
|
}
|
|
} else {
|
|
displayWarning("Can't install \(manifestItemName) because \(error.localizedDescription)")
|
|
processedItem["note"] = error.localizedDescription
|
|
}
|
|
processedItem["version_to_install"] = version
|
|
appendToProcessedManagedInstalls(processedItem)
|
|
return false
|
|
}
|
|
// download succeeded or cached item is current
|
|
if let installerItemLocation = pkginfo["installer_item_location"] as? String {
|
|
filename = baseName(installerItemLocation)
|
|
}
|
|
if installerItemSize >= 1024, downloadSeconds > 0 {
|
|
let downloadSpeed = Int(Double(installerItemSize) / downloadSeconds)
|
|
processedItem["download_kbytes_per_sec"] = downloadSpeed
|
|
displayDetail("\(filename) downloaded at \(downloadSpeed) KB/sec")
|
|
}
|
|
}
|
|
processedItem["installer_item"] = filename
|
|
// we will ignore the unattended_install key if the item needs a
|
|
// restart or logout...
|
|
if (pkginfo["unattended_install"] as? Bool ?? false) || (pkginfo["forced_install"] as? Bool ?? false) {
|
|
let restartAction = pkginfo["RestartAction"] as? String ?? "None"
|
|
if restartAction != "None" {
|
|
displayWarning("Ignoring unattended_install key for \(name) because RestartAction is \(restartAction).")
|
|
} else {
|
|
processedItem["unattended_install"] = true
|
|
}
|
|
}
|
|
|
|
// optional keys to copy if they exist
|
|
var optionalKeys = [
|
|
"force_install_after_date",
|
|
"additional_startosinstall_options",
|
|
"allow_untrusted",
|
|
"installer_choices_xml",
|
|
"installer_environment",
|
|
"adobe_install_info",
|
|
"RestartAction",
|
|
"installer_type",
|
|
"adobe_package_name",
|
|
"package_path",
|
|
"blocking_applications",
|
|
"installs",
|
|
"requires",
|
|
"update_for",
|
|
"payloads",
|
|
"preinstall_script",
|
|
"postinstall_script",
|
|
"items_to_copy", // used w/ copy_from_dmg
|
|
"apple_item",
|
|
"category",
|
|
"developer",
|
|
"icon_name",
|
|
"PayloadIdentifier",
|
|
"icon_hash",
|
|
"OnDemand",
|
|
"precache",
|
|
"display_name_staged", // used w/ stage_os_installer
|
|
"description_staged",
|
|
"installed_size_staged",
|
|
]
|
|
|
|
if isOptionalInstall {
|
|
let someVersionInstalled = await someVersionInstalled(pkginfo)
|
|
if !someVersionInstalled {
|
|
// For optional installs where no version is installed yet
|
|
// we do not enforce force_install_after_date
|
|
optionalKeys = optionalKeys.filter {
|
|
$0 != "force_install_after_date"
|
|
}
|
|
}
|
|
}
|
|
|
|
for key in optionalKeys {
|
|
processedItem[key] = pkginfo[key]
|
|
}
|
|
|
|
if pkginfo["apple_item"] == nil {
|
|
// admin did not explicitly mark this item; let's determine if
|
|
// it's from Apple
|
|
if isAppleItem(pkginfo) {
|
|
munkiLog("Marking \(name) as an apple_item - this will block Apple SUS updates")
|
|
processedItem["apple_item"] = true
|
|
}
|
|
}
|
|
|
|
appendToProcessedManagedInstalls(processedItem)
|
|
|
|
// now look for update_for items
|
|
var updateList = [String]()
|
|
if !includedVersion.isEmpty {
|
|
// a specific version was specified in the manifest
|
|
// so look only for updates for this specific version
|
|
updateList = lookForUpdatesForName(
|
|
manifestItemNameWithoutVersion,
|
|
version: includedVersion,
|
|
catalogList: catalogList
|
|
)
|
|
} else {
|
|
// didn't specify a specific version, so
|
|
// now look for all updates for this item
|
|
updateList = lookForUpdatesFor(
|
|
manifestItemNameWithoutVersion,
|
|
catalogList: catalogList
|
|
)
|
|
// now append any updates specifically
|
|
// for the version to be installed
|
|
updateList += lookForUpdatesForName(
|
|
manifestItemNameWithoutVersion,
|
|
version: version,
|
|
catalogList: catalogList
|
|
)
|
|
}
|
|
// if we have any update items, process them
|
|
for updateItem in updateList {
|
|
_ = await processInstall(
|
|
updateItem,
|
|
catalogList: catalogList,
|
|
installInfo: &installInfo
|
|
)
|
|
}
|
|
} else {
|
|
// same or higher version installed
|
|
processedItem["installed"] = true
|
|
|
|
if !dependenciesMet {
|
|
displayWarning("Could not resolve all dependencies for \(manifestItemName), but no install or update needed.")
|
|
}
|
|
|
|
if let installerType = pkginfo["installer_type"] as? String,
|
|
installerType == "stage_os_installer"
|
|
{
|
|
// installer appears to be staged; make sure the info is recorded
|
|
// so we know we can launch the installer later
|
|
// TODO: maybe filter the actual info recorded
|
|
displayInfo("Recording staged macOS installer...")
|
|
// TODO: recordStagedOSInstaller(pkginfo)
|
|
}
|
|
// record installed size and version
|
|
let installedSize = pkginfo["installed_size"] as? Int ?? pkginfo["installer_item_size"] as? Int ?? 0
|
|
processedItem["installed_size"] = installedSize
|
|
if installedState == .thisVersionInstalled {
|
|
// just use the version from the pkginfo
|
|
processedItem["installed_version"] = version
|
|
} else {
|
|
// might be newer; attempt to figure out the version
|
|
var installedVersion = getInstalledVersion(pkginfo)
|
|
if installedVersion == "UNKNOWN" {
|
|
installedVersion = "(newer than \(version))"
|
|
}
|
|
processedItem["installed_version"] = installedVersion
|
|
}
|
|
appendToProcessedManagedInstalls(processedItem)
|
|
displayDetail("\(manifestItemNameWithoutVersion) version \(version) (or newer) is already installed.")
|
|
|
|
// now look for update_for items
|
|
var updateList = [String]()
|
|
let installedVersion = processedItem["installed_version"] as? String ?? ""
|
|
if includedVersion.isEmpty {
|
|
// no specific version is specified;
|
|
// the item is already installed;
|
|
// now look for updates for this item
|
|
updateList = lookForUpdatesFor(name, catalogList: catalogList)
|
|
// and also any for this specific version
|
|
if !installedVersion.hasPrefix("(newer than") {
|
|
updateList += lookForUpdatesForName(
|
|
name,
|
|
version: installedVersion,
|
|
catalogList: catalogList
|
|
)
|
|
}
|
|
} else if compareVersions(includedVersion, installedVersion) == .same {
|
|
// manifest specifies a specific version.
|
|
// if that's what's installed, look for any updates
|
|
// specific to this version
|
|
updateList = lookForUpdatesForName(
|
|
name,
|
|
version: includedVersion,
|
|
catalogList: catalogList
|
|
)
|
|
}
|
|
// if we have any update items, process them
|
|
for updateItem in updateList {
|
|
_ = await processInstall(
|
|
updateItem,
|
|
catalogList: catalogList,
|
|
installInfo: &installInfo,
|
|
isManagedUpdate: isManagedUpdate,
|
|
isOptionalInstall: isOptionalInstall
|
|
)
|
|
}
|
|
}
|
|
// done successfully processing this install; add it to our list
|
|
// of processed installs so we don't process it again in the future
|
|
// (unless it is a managed_update)
|
|
if !isManagedUpdate {
|
|
displayDebug2("Adding \(manifestItemName) to the list of processed installs")
|
|
var processedInstalls = installInfo["processed_installs"] as? [String] ?? []
|
|
processedInstalls.append(manifestItemName)
|
|
installInfo["processed_installs"] = processedInstalls
|
|
}
|
|
return true
|
|
}
|
|
|
|
func processManagedUpdate(
|
|
_ manifestItem: String,
|
|
catalogList: [String],
|
|
installInfo: inout PlistDict
|
|
) async {
|
|
// Process a managed_updates item to see if it is installed, and if so,
|
|
// if it needs an update.
|
|
let manifestItemName = (manifestItem as NSString).lastPathComponent
|
|
displayDebug1("* Processing manifest item \(manifestItemName) for update")
|
|
|
|
if alreadyProcessed(
|
|
manifestItemName,
|
|
installInfo: installInfo,
|
|
sections: ["managed_updates", "processed_installs", "processed_uninstalls"]
|
|
) { return }
|
|
|
|
guard let pkginfo = await getItemDetail(manifestItemName, catalogList: catalogList) else {
|
|
displayWarning("Could not process item \(manifestItemName) for update. No pkginfo found in catalogs: \(catalogList) ")
|
|
return
|
|
}
|
|
|
|
// we only offer to update if some version of the item is already
|
|
// installed, so let's check
|
|
if await someVersionInstalled(pkginfo) {
|
|
// add to the list of processed managed_updates
|
|
var managedUpdates = installInfo["managed_updates"] as? [String] ?? []
|
|
managedUpdates.append(manifestItemName)
|
|
installInfo["managed_updates"] = managedUpdates
|
|
_ = await processInstall(
|
|
manifestItemName,
|
|
catalogList: catalogList,
|
|
installInfo: &installInfo,
|
|
isManagedUpdate: true
|
|
)
|
|
} else {
|
|
displayDebug1("\(manifestItemName) does not appear to be installed, so no managed updates.")
|
|
}
|
|
}
|
|
|
|
func processOptionalInstall(
|
|
_ manifestItem: String,
|
|
catalogList: [String],
|
|
installInfo: inout PlistDict
|
|
) async {
|
|
// Process an optional install item to see if it should be added to
|
|
// the list of optional installs
|
|
let manifestItemName = (manifestItem as NSString).lastPathComponent
|
|
displayDebug1("* Processing manifest item \(manifestItemName) for optional install")
|
|
|
|
if alreadyProcessed(
|
|
manifestItemName,
|
|
installInfo: installInfo,
|
|
sections: ["optional_installs", "processed_installs", "processed_uninstalls"]
|
|
) {
|
|
return
|
|
}
|
|
|
|
var processedItem = PlistDict()
|
|
var pkginfo = PlistDict()
|
|
if let testPkginfo = await getItemDetail(
|
|
manifestItemName,
|
|
catalogList: catalogList,
|
|
suppressWarnings: true
|
|
) {
|
|
pkginfo = testPkginfo
|
|
}
|
|
|
|
if pkginfo.isEmpty,
|
|
let show = boolPref("ShowOptionalInstallsForHigherOSVersions"),
|
|
show == true
|
|
{
|
|
// could not find an item valid for the current OS and hardware
|
|
// try again to see if there is an item for a higher OS
|
|
if let testPkginfo = await getItemDetail(
|
|
manifestItemName,
|
|
catalogList: catalogList,
|
|
skipMinimumOSCheck: true,
|
|
suppressWarnings: true
|
|
) {
|
|
pkginfo = testPkginfo
|
|
}
|
|
if !pkginfo.isEmpty {
|
|
// found an item that requires a higher OS version
|
|
let pkginfoName = pkginfo["name"] as? String ?? "<unknown>"
|
|
let pkginfoVersion = pkginfo["version"] as? String ?? "<unknown>"
|
|
displayDebug1("Found \(pkginfoName), version \(pkginfoVersion) that requires a higher os version")
|
|
// insert a note about the OS version requirement
|
|
if let minimumOSVersion = pkginfo["minimum_os_version"] {
|
|
processedItem["note"] = "Requires macOS version \(minimumOSVersion)."
|
|
}
|
|
processedItem["update_available"] = true
|
|
}
|
|
}
|
|
if pkginfo.isEmpty {
|
|
// could not find anything that matches and is applicable
|
|
displayWarning("Could not process item \(manifestItemName) for optional install. No pkginfo found in catalogs: \(catalogList)")
|
|
return
|
|
}
|
|
|
|
let isCurrentlyInstalled = await someVersionInstalled(pkginfo)
|
|
var needsUpdate = false
|
|
if isCurrentlyInstalled {
|
|
// TODO: if shouldBeRemovedIfUnused(pkginfo) {
|
|
// processRemoval(manifestItemName, catalogList: catalogList, installInfo: &installInfo)
|
|
// removeFromSelfServeInstalls(manifestItemName)
|
|
// return
|
|
// }
|
|
if pkginfo["installcheck_script"] == nil {
|
|
// installcheck_scripts can be expensive and only tell us if
|
|
// an item is installed or not. So if iteminfo['installed'] is
|
|
// True, and we're using an installcheck_script,
|
|
// installedState() is going to return 1
|
|
// (which does not equal 0), so we can avoid running it again.
|
|
// We should really revisit all of this in the future to avoid
|
|
// repeated checks of the same data.
|
|
// (installcheck_script isn't called if OnDemand is True, but if
|
|
// OnDemand is true, is_currently_installed would be False, and
|
|
// therefore we would not be here!)
|
|
//
|
|
// TL;DR: only check installed_state if no installcheck_script
|
|
let installationState = await installedState(pkginfo)
|
|
if let installerType = pkginfo["installer_type"] as? String,
|
|
installerType == "stage_os_installer"
|
|
{
|
|
// .thisVersionNotInstalled means installer is staged, but not _installed_
|
|
needsUpdate = installationState != .newerVersionInstalled
|
|
} else {
|
|
needsUpdate = installationState == .thisVersionNotInstalled
|
|
}
|
|
}
|
|
|
|
if !needsUpdate,
|
|
let show = boolPref("ShowOptionalInstallsForHigherOSVersions"),
|
|
show == true
|
|
{
|
|
// the version we have installed is the newest for the current OS.
|
|
// check again to see if there is a newer version for a higher OS
|
|
displayDebug1("Checking for versions of \(manifestItemName) that require a higher OS version")
|
|
if let anotherPkgInfo = await getItemDetail(
|
|
manifestItemName,
|
|
catalogList: catalogList,
|
|
skipMinimumOSCheck: true,
|
|
suppressWarnings: true
|
|
) {
|
|
if !NSDictionary(dictionary: anotherPkgInfo).isEqual(to: NSDictionary(dictionary: pkginfo)) {
|
|
// we found a different item. Replace the one we found
|
|
// previously with this one.
|
|
pkginfo = anotherPkgInfo
|
|
let pkginfoName = pkginfo["name"] as? String ?? "<unknown>"
|
|
let pkginfoVersion = pkginfo["version"] as? String ?? "<unknown>"
|
|
displayDebug1("Found \(pkginfoName), version \(pkginfoVersion) that requires a higher os version")
|
|
// insert a note about the OS version requirement
|
|
if let minimumOSVersion = pkginfo["minimum_os_version"] {
|
|
processedItem["note"] = "Requires macOS version \(minimumOSVersion)."
|
|
}
|
|
processedItem["update_available"] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// if we get to this point we can add this item
|
|
// to the list of optional installs
|
|
processedItem["name"] = pkginfo["name"] as? String ?? manifestItemName
|
|
processedItem["display_name"] = pkginfo["display_name"] ?? ""
|
|
processedItem["description"] = pkginfo["description"] ?? ""
|
|
processedItem["version_to_install"] = pkginfo["version"] ?? "UNKNOWN"
|
|
processedItem["needs_update"] = needsUpdate
|
|
for key in [
|
|
"category",
|
|
"developer",
|
|
"featured",
|
|
"icon_name",
|
|
"icon_hash",
|
|
"requires",
|
|
"RestartAction",
|
|
] {
|
|
processedItem[key] = pkginfo[key]
|
|
}
|
|
processedItem["installed"] = isCurrentlyInstalled
|
|
processedItem["licensed_seat_info_available"] = pkginfo["licensed_seat_info_available"] as? Bool ?? false
|
|
processedItem["uninstallable"] = (pkginfo["uninstallable"] as? Bool ?? false) && !(pkginfo["uninstall_method"] as? String ?? "").isEmpty
|
|
// If the item is a precache item, record the precache flag
|
|
// and also the installer item location (as long as item doesn't have a note
|
|
// explaining why it's not available and as long as available seats is not 0)
|
|
if let installerItemLocation = pkginfo["installer_item_location"] as? String,
|
|
!installerItemLocation.isEmpty,
|
|
pkginfo["precache"] as? Bool ?? false,
|
|
pkginfo["note"] == nil,
|
|
pkginfo["licensed_seats_available"] as? Bool ?? true
|
|
{
|
|
processedItem["precache"] = true
|
|
for key in [
|
|
"installer_item_location",
|
|
"installer_item_hash",
|
|
"PackageCompleteURL",
|
|
"PackageURL",
|
|
] {
|
|
processedItem[key] = pkginfo[key]
|
|
}
|
|
}
|
|
let installerSize = pkginfo["installer_item_size"] as? Int ?? 0
|
|
processedItem["installer_item_size"] = installerSize
|
|
processedItem["installed_size"] = pkginfo["installed_size"] as? Int ?? installerSize
|
|
if let pkgInfoNote = pkginfo["note"] as? String,
|
|
processedItem["note"] == nil
|
|
{
|
|
processedItem["note"] = pkgInfoNote
|
|
} else if needsUpdate || !isCurrentlyInstalled {
|
|
if !enoughDiskSpaceFor(
|
|
pkginfo,
|
|
installList: installInfo["managed_installs"] as? [PlistDict] ?? [],
|
|
warn: true
|
|
) {
|
|
processedItem["note"] = "Insufficient disk space to download and install."
|
|
if needsUpdate {
|
|
processedItem["needs_update"] = false
|
|
processedItem["update_available"] = true
|
|
}
|
|
}
|
|
}
|
|
let optionalKeys = [
|
|
"preinstall_alert",
|
|
"preuninstall_alert",
|
|
"preupgrade_alert",
|
|
"OnDemand",
|
|
"minimum_os_version",
|
|
"update_available",
|
|
"localized_strings",
|
|
]
|
|
for key in optionalKeys {
|
|
processedItem[key] = pkginfo[key]
|
|
}
|
|
let itemName = processedItem["name"] as? String ?? "<unknown>"
|
|
displayDebug1("Adding \(itemName) to the optional install list")
|
|
var optionalInstalls = installInfo["optional_installs"] as? [PlistDict] ?? []
|
|
optionalInstalls.append(processedItem)
|
|
installInfo["optional_installs"] = optionalInstalls
|
|
}
|
|
|
|
func processRemoval(
|
|
_ manifestItem: String,
|
|
catalogList: [String],
|
|
installInfo: inout PlistDict
|
|
) async -> Bool {
|
|
// Processes a manifest item; attempts to determine if it
|
|
// needs to be removed, and if it can be removed.
|
|
|
|
// Unlike installs, removals aren't really version-specific -
|
|
// If we can figure out how to remove the currently installed
|
|
// version, we do, unless the admin specifies a specific version
|
|
// number in the manifest. In that case, we only attempt a
|
|
// removal if the version installed matches the specific version
|
|
// in the manifest.
|
|
|
|
// Any items dependent on the given item need to be removed first.
|
|
// Items to be removed are added to installinfo['removals'].
|
|
|
|
// Calls itself recursively as it processes dependencies.
|
|
// Returns a boolean; when processing dependencies, a false return
|
|
// will stop the removal of a dependent item.
|
|
|
|
func getReceiptsToRemove(_ item: PlistDict) async -> [String] {
|
|
// Returns a list of (installed/present) receipts to remove for item
|
|
if let name = item["name"] as? String {
|
|
let pkgdata = await analyzeInstalledPkgs()
|
|
if let receiptsForName = pkgdata["receipts_for_name"] as? [String: [String]] {
|
|
return receiptsForName[name] ?? [String]()
|
|
}
|
|
}
|
|
return [String]()
|
|
}
|
|
|
|
func appendToProcessedRemovals(_ item: PlistDict) {
|
|
// helper function
|
|
var removals = installInfo["removals"] as? [PlistDict] ?? []
|
|
removals.append(item)
|
|
installInfo["removals"] = removals
|
|
}
|
|
|
|
let manifestItemNameWithVersion = (manifestItem as NSString).lastPathComponent
|
|
displayDebug1("* Processing manifest item \(manifestItemNameWithVersion) for removal")
|
|
|
|
let (manifestItemName, manifestItemVersion) = nameAndVersion(manifestItemNameWithVersion, onlySplitOnHyphens: true)
|
|
|
|
// have we processed this item already?
|
|
let processedInstalls = installInfo["processed_installs"] as? [String] ?? []
|
|
let processedInstallsNames = processedInstalls.map {
|
|
nameAndVersion($0, onlySplitOnHyphens: true).0
|
|
}
|
|
if processedInstallsNames.contains(manifestItemName) {
|
|
displayWarning("Will not attempt to remove \(manifestItemName) because some version of it is in the list of managed installs, or it is required by another managed install.")
|
|
return false
|
|
}
|
|
var processedUninstallsNames = installInfo["processed_uninstalls"] as? [String] ?? []
|
|
if processedUninstallsNames.contains(manifestItemName) {
|
|
displayDebug1("\(manifestItemName) has already been processed for removal.")
|
|
return true
|
|
}
|
|
processedUninstallsNames.append(manifestItemName)
|
|
installInfo["processed_uninstalls"] = processedUninstallsNames
|
|
|
|
var pkginfos = [PlistDict]()
|
|
if !manifestItemVersion.isEmpty {
|
|
// a specific version was specified
|
|
if let pkginfo = await getItemDetail(manifestItemName, catalogList: catalogList, version: manifestItemVersion) {
|
|
pkginfos.append(pkginfo)
|
|
}
|
|
} else {
|
|
// get all items matching the name provided
|
|
pkginfos = getAllItemsWithName(manifestItemName, catalogList: catalogList)
|
|
}
|
|
|
|
if pkginfos.isEmpty {
|
|
displayWarning("Could not process item \(manifestItemName) for removal. No pkginfo found in catalogs: \(catalogList)")
|
|
return false
|
|
}
|
|
|
|
var installEvidence = false
|
|
var foundItem = PlistDict()
|
|
for pkginfo in pkginfos {
|
|
let name = pkginfo["name"] as? String ?? "<unknown>"
|
|
let version = pkginfo["version"] as? String ?? "<unknown>"
|
|
displayDebug2("Considering item \(name)-\(version) for removal info")
|
|
if await evidenceThisIsInstalled(pkginfo) {
|
|
installEvidence = true
|
|
foundItem = pkginfo
|
|
break
|
|
}
|
|
displayDebug2("\(name)-\(version) is not installed")
|
|
}
|
|
|
|
if !installEvidence {
|
|
displayDetail("\(manifestItemNameWithVersion) doesn\'t appear to be installed.")
|
|
var processedItem = PlistDict()
|
|
processedItem["name"] = manifestItemName
|
|
processedItem["installed"] = false
|
|
appendToProcessedRemovals(processedItem)
|
|
return true
|
|
}
|
|
|
|
// if we get here, installEvidence is true, and foundItem
|
|
// holds the item we found install evidence for, so we
|
|
// should use that item to do the removal
|
|
var uninstallItem = PlistDict()
|
|
var packagesToRemove = [String]()
|
|
let foundItemName = foundItem["name"] as? String ?? "<unknown>"
|
|
let foundItemVersion = foundItem["version"] as? String ?? "<unknown>"
|
|
let uninstallable = foundItem["uninstallable"] as? Bool ?? false
|
|
let uninstallMethod = foundItem["uninstall_method"] as? String ?? ""
|
|
if !uninstallable {
|
|
displayWarning("Item \(foundItemName)-\(foundItemVersion) is not marked as uninstallable.")
|
|
} else if uninstallMethod.isEmpty {
|
|
displayWarning("No uninstall_method in \(foundItemName)-\(foundItemVersion).")
|
|
} else if uninstallMethod.hasPrefix("Adobe") {
|
|
displayWarning("Adobe-specific uninstall methods are no longer supported.")
|
|
} else if ["remove_app", "remove_profile"].contains(uninstallMethod) {
|
|
displayWarning("Uninstall method \(uninstallMethod) is no longer supported.")
|
|
} else if uninstallMethod == "removepackages" {
|
|
packagesToRemove = await getReceiptsToRemove(foundItem)
|
|
if !packagesToRemove.isEmpty {
|
|
uninstallItem = foundItem
|
|
} else {
|
|
displayWarning("uninstall_method for \(manifestItemNameWithVersion) is removepackages, but no packages found to remove")
|
|
}
|
|
} else if ["remove_copied_items", "uninstall_script", "uninstall_package"].contains(uninstallMethod) {
|
|
uninstallItem = foundItem
|
|
} else {
|
|
// might be a path to a locally-installed script/executable
|
|
if pathIsExecutableFile(uninstallMethod) {
|
|
uninstallItem = foundItem
|
|
} else {
|
|
displayWarning("Uninstall method \(uninstallMethod) is not a valid method.")
|
|
}
|
|
}
|
|
|
|
if uninstallItem.isEmpty {
|
|
// could not find usable uninstall_method
|
|
return false
|
|
}
|
|
|
|
// if we got this far, we have enough info to attempt an uninstall.
|
|
// the pkginfo is in uninstall_item
|
|
// Now check for dependent items
|
|
//
|
|
// First, look through catalogs for items that are required by this item;
|
|
// if any are installed, we need to remove them as well
|
|
//
|
|
// still not sure how to handle references to specific versions --
|
|
// if another package says it requires SomePackage--1.0.0.0.0
|
|
// and we're supposed to remove SomePackage--1.0.1.0.0... what do we do?
|
|
var dependentItemsRemoved = true
|
|
let uninstallItemName = uninstallItem["name"] as? String ?? "<unknown>"
|
|
let uninstallItemVersion = uninstallItem["version"] as? String ?? "<unknown>"
|
|
let uninstallItemNameWVersion = "\(uninstallItemName)-\(uninstallItemVersion)"
|
|
let altUninstallItemNameWVersion = "\(uninstallItemName)--\(uninstallItemVersion)"
|
|
var processedNames = [String]()
|
|
for catalogName in catalogList {
|
|
guard let catalogDB = Catalogs.shared.get(catalogName) else {
|
|
// in case the list refers to a non-existent catalog
|
|
continue
|
|
}
|
|
let catalogItems = catalogDB["items"] as? [PlistDict] ?? []
|
|
for catalogItem in catalogItems {
|
|
let name = catalogItem["name"] as? String ?? "<unknown>"
|
|
if processedNames.contains(name) {
|
|
// already added
|
|
continue
|
|
}
|
|
guard let requires = catalogItem["requires"] as? [String] else {
|
|
// no requires, so skip to next
|
|
processedNames.append(name)
|
|
continue
|
|
}
|
|
if requires.contains(uninstallItemName) ||
|
|
requires.contains(uninstallItemNameWVersion) ||
|
|
requires.contains(altUninstallItemNameWVersion)
|
|
{
|
|
displayDebug1("\(name) requires \(manifestItemName), checking to see if it's installed...")
|
|
if await evidenceThisIsInstalled(catalogItem) {
|
|
displayDetail("\(name) requires \(manifestItemName). \(name) must be removed as well.")
|
|
let success = await processRemoval(
|
|
name, catalogList: catalogList, installInfo: &installInfo
|
|
)
|
|
if !success {
|
|
dependentItemsRemoved = false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
processedNames.append(name)
|
|
}
|
|
}
|
|
if !dependentItemsRemoved {
|
|
displayWarning("Will not attempt to remove %s because could not remove all items dependent on it.")
|
|
return false
|
|
}
|
|
|
|
// Finally! We can record the removal information!
|
|
var processedItem = PlistDict()
|
|
let name = uninstallItem["name"] as? String ?? "<unknown>"
|
|
let version = uninstallItem["version"] as? String ?? "<unknown>"
|
|
processedItem["name"] = uninstallItem["name"]
|
|
processedItem["display_name"] = uninstallItem["display_name"]
|
|
processedItem["description"] = "Will be removed."
|
|
|
|
// we will ignore the unattended_uninstall key if the item needs a restart
|
|
// or logout...
|
|
if (uninstallItem["unattended_install"] as? Bool ?? false) || (uninstallItem["forced_install"] as? Bool ?? false) {
|
|
let restartAction = uninstallItem["RestartAction"] as? String ?? "None"
|
|
if restartAction != "None" {
|
|
displayWarning("Ignoring unattended_install key for \(name) because RestartAction is \(restartAction).")
|
|
} else {
|
|
processedItem["unattended_install"] = true
|
|
}
|
|
}
|
|
|
|
// some keys we'll copy if they exist
|
|
let optionalKeys = [
|
|
"RestartAction",
|
|
"blocking_applications",
|
|
"installs",
|
|
"requires",
|
|
"update_for",
|
|
"payloads",
|
|
"preuninstall_script",
|
|
"postuninstall_script",
|
|
"apple_item",
|
|
"category",
|
|
"developer",
|
|
"icon_name",
|
|
"PayloadIdentifier",
|
|
]
|
|
for key in optionalKeys {
|
|
processedItem[key] = uninstallItem[key]
|
|
}
|
|
|
|
if processedItem["apple_item"] == nil {
|
|
// admin did not explicitly mark this item; let's determine if
|
|
// it's from Apple
|
|
if isAppleItem(uninstallItem) {
|
|
munkiLog("Marking \(name) as an apple_item - this will block Apple SUS updates")
|
|
processedItem["apple_item"] = true
|
|
}
|
|
}
|
|
|
|
if !packagesToRemove.isEmpty {
|
|
// remove references for each package
|
|
var packagesToReallyRemove = [String]()
|
|
let pkgdata = await analyzeInstalledPkgs()
|
|
let pkgReferences = pkgdata["pkg_references"] as? [String: [String]] ?? [:]
|
|
var pkgReferencesMessages = [String]()
|
|
for pkg in packagesToRemove {
|
|
displayDebug1("Considering \(pkg) for removal...")
|
|
// find pkg in pkgdata["pkg_references"] and remove the reference
|
|
// so we only remove packages if we're the only reference to it
|
|
// pkgdata["pkg_references"] is [String:[String]]
|
|
guard let references = pkgReferences[pkg] else {
|
|
// This shouldn't happen
|
|
displayWarning("pkg id \(pkg) missing from pkgdata")
|
|
continue
|
|
}
|
|
let msg = "Package \(pkg) references are: \(references)"
|
|
displayDebug1(msg)
|
|
pkgReferencesMessages.append(msg)
|
|
if references.contains(name) {
|
|
let filteredReferences = references.filter {
|
|
$0 != name
|
|
}
|
|
if filteredReferences.isEmpty {
|
|
// no other references than this item
|
|
displayDebug1("Adding \(pkg) to removal list.")
|
|
packagesToReallyRemove.append(pkg)
|
|
} else {
|
|
displayDebug1("Will not attempt to remove \(pkg)")
|
|
}
|
|
}
|
|
}
|
|
if !packagesToReallyRemove.isEmpty {
|
|
processedItem["packages"] = packagesToReallyRemove
|
|
} else {
|
|
// no packages that belong to this item only
|
|
displayWarning("could not find unique packages to remove for \(name)")
|
|
for msg in pkgReferencesMessages {
|
|
displayWarning(msg)
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
processedItem["uninstall_method"] = uninstallMethod
|
|
if uninstallMethod == "remove_copied_items" {
|
|
if let itemsToCopy = uninstallItem["items_to_copy"] as? [PlistDict] {
|
|
processedItem["items_to_remove"] = itemsToCopy
|
|
} else {
|
|
displayWarning("Can't uninstall \(name) because there is no info on installed items.")
|
|
return false
|
|
}
|
|
} else if uninstallMethod == "uninstall_script" {
|
|
if let uninstallScript = uninstallItem["uninstall_script"] as? String {
|
|
processedItem["uninstall_script"] = uninstallScript
|
|
} else {
|
|
displayWarning("Can't uninstall \(name) because uninstall_script is undefined or invalid.")
|
|
return false
|
|
}
|
|
} else if uninstallMethod == "uninstall_package" {
|
|
if let location = uninstallItem["uninstaller_item_location"] as? String {
|
|
do {
|
|
_ = try downloadInstallerItem(
|
|
uninstallItem, installInfo: installInfo, uninstalling: true
|
|
)
|
|
processedItem["uninstaller_item"] = baseName(location)
|
|
} catch FetchError.verification {
|
|
displayWarning("Can't uninstall \(name) because the integrity check for the uninstall package failed.")
|
|
return false
|
|
} catch {
|
|
displayWarning("Failed to download the uninstaller for \(name) because \(error.localizedDescription)")
|
|
return false
|
|
}
|
|
} else {
|
|
displayWarning("Can't uninstall \(name) because there is no URL for the uninstall package.")
|
|
return false
|
|
}
|
|
}
|
|
// before we add this removal to the list,
|
|
// check for installed updates and add them to the
|
|
// removal list as well
|
|
var updateList = lookForUpdatesFor(uninstallItemName, catalogList: catalogList)
|
|
updateList += lookForUpdatesFor(uninstallItemNameWVersion, catalogList: catalogList)
|
|
updateList += lookForUpdatesFor(altUninstallItemNameWVersion, catalogList: catalogList)
|
|
updateList = Array(Set(updateList))
|
|
for updateItem in updateList {
|
|
// call us recursively
|
|
_ = await processRemoval(
|
|
updateItem, catalogList: catalogList, installInfo: &installInfo
|
|
)
|
|
}
|
|
|
|
// finish recording info for this removal
|
|
processedItem["installed"] = true
|
|
processedItem["installed_version"] = version
|
|
appendToProcessedRemovals(processedItem)
|
|
displayDetail("Removal of \(manifestItemNameWithVersion) added to managedsoftwareupdate tasks.")
|
|
return true
|
|
}
|