Implement still more of managedsoftwareupdate

This commit is contained in:
Greg Neagle
2024-09-07 07:56:54 -07:00
parent 3931840e35
commit de81c6a752
2 changed files with 324 additions and 67 deletions

View File

@@ -43,67 +43,12 @@ struct ManagedSoftwareUpdate: AsyncParsableCommand {
// runtype is used for pre and postflight scripts
var runtype = "custom"
mutating func run() async throws {
if version {
print(getVersion())
return
}
// check to see if we're root
if NSUserName() != "root" {
printStderr("You must run this as root!")
throw ExitCode(EXIT_STATUS_ROOT_REQUIRED)
}
try handleConfigOptions()
try exitIfAnotherManagedSoftwareUpdateIsRunning()
try processLaunchdOptions()
try ensureMunkiDirsExist()
configureDisplayOptions()
doCleanupTasks(runType: runtype)
initializeReport()
// TODO: support logging to syslog and unified logging
munkiLog("### Starting managedsoftwareupdate run: \(runtype) ###")
if DisplayOptions.shared.verbose > 0 {
print("Managed Software Update Tool")
print("Version \(getVersion())")
print("Copyright 2010-2024 The Munki Project")
print("https://github.com/munki/munki\n")
}
displayMajorStatus("Starting...")
sendStartNotification()
try await runPreflight()
let appleupdatesonly = (boolPref("AppleSoftwareUpdatesOnly") ?? false) || commonOptions.appleSUSPkgsOnly
let skipMunkiCheck = commonOptions.installOnly || appleupdatesonly
if !skipMunkiCheck {
warnIfServerIsDefault()
}
// reset our errors and warnings files, rotate main log if needed
munkiLogResetErrors()
munkiLogResetWarnings()
munkiLogRotateMainLog()
// archive the previous session's report
Report.shared.archiveReport()
if appleupdatesonly, DisplayOptions.shared.verbose > 0 {
print("NOTE: managedsoftwareupdate is configured to process Apple Software Updates only.")
}
let updateCheckResult = try await doMunkiUpdateCheck(skipCheck: skipMunkiCheck)
let appleUpdatesAvailable = doAppleUpdateCheckIfAppropriate(
appleUpdatesOnly: appleupdatesonly)
// display any available update info
if updateCheckResult == .updatesAvailable {
displayUpdateInfo()
}
if let stagedOSInstallerInfo = getStagedOSInstallerInfo() {
displayStagedOSInstallerInfo(info: stagedOSInstallerInfo)
} else if appleUpdatesAvailable > 0 {
// TODO: displayAppleUpdateInfo()
}
}
var munkiUpdateCount = 0
var appleUpdateCount = 0
var restartAction = POSTACTION_NONE
var forcedSoon = false
var mustLogout = false
var shouldNotifyUser = false
private func handleConfigOptions() throws {
if configOptions.showConfig {
@@ -348,4 +293,311 @@ struct ManagedSoftwareUpdate: AsyncParsableCommand {
}
return 0
}
private mutating func reconfigureOptionsForInstall() {
if runtype == "installatstartup" {
// turn off options.installonly; we need options.auto behavior from here
// on out because if FileVault is active we may actually be logged in
// at this point!
commonOptions.installOnly = false
otherOptions.auto = true
}
if runtype == "checkandinstallatstartup",
munkiUpdateCount == 0, appleUpdateCount > 0,
false // installableAppleUpdateCount() == 0
{
// TODO: implement installableAppleUpdateCount()
// we're in bootstrap mode and
// there are only Apple updates, but we can't install
// some of them
// so clear bootstrapping mode so we don't loop endlessly
do {
try clearBootstrapMode()
} catch {
displayError(error.localizedDescription)
}
}
if otherOptions.launchosinstaller {
// user chose to update from Managed Software Center and there is
// a cached macOS installer. We'll do that _only_.
munkiUpdateCount = 0
appleUpdateCount = 0
if getStagedOSInstallerInfo() != nil {
// TODO: implement osinstaller.launch()
} else {
// staged OS installer is missing
displayError("Requested to launch staged OS installer, but no info on a staged installer was found.")
}
}
}
private mutating func handleInstallTasks() async {
// Complex logic here to handle lots of install scenarios
// and options
if munkiUpdateCount == 0, appleUpdateCount == 0 {
// no updates available
if commonOptions.installOnly, !otherOptions.quiet {
print("Nothing to install or remove.")
}
if runtype == "checkandinstallatstartup" {
// we have nothing to do, clear the bootstrapping mode
// so we'll stop running at startup/logout
do {
try clearBootstrapMode()
} catch {
displayError(error.localizedDescription)
}
}
return
}
if commonOptions.installOnly || otherOptions.logoutinstall {
// admin has triggered install or MSC has triggered install,
// so just install everything
restartAction = await doInstallTasks(
doAppleUpdates: appleUpdateCount > 0)
// reset our count of available updates (it might not actually
// be zero, but we want to clear the badge on the Dock icon;
// it can be updated to the "real" count on the next Munki run)
munkiUpdateCount = 0
appleUpdateCount = 0
// send a notification event so MSU can update its display
// if needed
sendUpdateNotification()
return
} else if otherOptions.auto {
// admin has specified --auto, or launch daemon background run
await handleAutoInstallTasks()
return
} else if !otherOptions.quiet {
// this is a checkonly run
print("\nRun managedsoftwareupdate --installonly to install the downloaded updates.")
return
}
}
private mutating func handleAutoInstallTasks() async {
if currentGUIUsers().isEmpty {
// we're at the loginwindow
if boolPref("SuppressAutoInstall") ?? false {
munkiLog("Skipping auto install because SuppressAutoInstall is true.")
return
}
if boolPref("SuppressLoginwindowInstall") ?? false {
// admin says we can't install pkgs at loginwindow
// unless they don't require a logout or restart
// (and are marked with unattended_install = True)
//
// check for packages that need to be force installed
// soon and convert them to unattended_installs if they
// don't require a logout
_ = forceInstallPackageCheck() // this might mark some more items as unattended
// now install anything that can be done unattended
munkiLog("Installing only items marked unattended because SuppressLoginwindowInstall is true.")
_ = await doInstallTasks(
doAppleUpdates: appleUpdateCount > 0,
onlyUnattended: true
)
return
}
if getIdleSeconds() < 10 {
// user may be attempting to login
munkiLog("Skipping auto install at loginwindow because system is not idle (keyboard or mouse activity).")
return
}
// at loginwindow, system is idle, so we can install
// but first, enable status output over login window
DisplayOptions.shared.munkistatusoutput = true
munkiLog("No GUI users, installing at login window.")
munkiStatusLaunch()
restartAction = await doInstallTasks(
doAppleUpdates: appleUpdateCount > 0
)
// reset our count of available updates
munkiUpdateCount = 0
appleUpdateCount = 0
return
// end at loginwindow
} else {
// there are GUI users
if boolPref("SuppressAutoInstall") ?? false {
munkiLog("Skipping unattended installs because SuppressAutoInstall is true.")
return
}
// check for packages that need to be force installed
// soon and convert them to unattended_installs if they
// don't require a logout
_ = forceInstallPackageCheck()
// install anything that can be done unattended
_ = await doInstallTasks(
doAppleUpdates: appleUpdateCount > 0,
onlyUnattended: true
)
// send a notification event so MSC can update its display
// if needed
sendUpdateNotification()
let forceAction = forceInstallPackageCheck()
// if any installs are still requiring force actions, just
// initiate a logout to get started. blocking apps might
// have stopped even non-logout/reboot installs from
// occurring.
forcedSoon = forceAction != .none
let mustLogoutActions: [ForceInstallStatus] = [.now, .logout, .restart]
if mustLogoutActions.contains(forceAction) {
mustLogout = true
}
// it's possible that we no longer have any available updates
// so we need to check InstallInfo.plist and
// AppleUpdates.plist again
munkiUpdateCount = munkiUpdatesAvailable()
if appleUpdateCount > 0 {
// there were Apple updates available, but we might have
// installed some unattended
// TODO: appleUpdateCount = appleupdates.appleSoftwareUpdatesAvailable(
// suppresscheck=True, client_id=options.id))
}
if munkiUpdateCount > 0 || appleUpdateCount > 0 {
// set a flag to notify the user of available updates
// after we conclude this run.
shouldNotifyUser = true
}
}
}
private func clearBootstrapModeIfAppropriate() {
// TODO: rethink all this
if runtype == "checkandinstallatstatup",
restartAction == POSTACTION_NONE,
pathExists(CHECKANDINSTALLATSTARTUPFLAG),
currentGUIUsers().isEmpty
{
if getIdleSeconds() < 10 {
// system is not idle, but check again in case someone has
// simply briefly touched the mouse to see progress.
usleep(10_500_000)
}
if getIdleSeconds() < 10 {
// we're still not idle.
// if the trigger file is present when we exit, we'll
// be relaunched by launchd, so we need to remove it
// to prevent automatic relaunch.
munkiLog("System not idle -- clearing bootstrap mode to prevent relaunch")
do {
try clearBootstrapMode()
} catch {
displayError(error.localizedDescription)
}
}
}
}
// MARK: main run function
mutating func run() async throws {
// TODO: implment signal handler for SIGTERM
if version {
print(getVersion())
return
}
// check to see if we're root
if NSUserName() != "root" {
printStderr("You must run this as root!")
throw ExitCode(EXIT_STATUS_ROOT_REQUIRED)
}
try handleConfigOptions()
try exitIfAnotherManagedSoftwareUpdateIsRunning()
try processLaunchdOptions()
try ensureMunkiDirsExist()
configureDisplayOptions()
doCleanupTasks(runType: runtype)
initializeReport()
// TODO: support logging to syslog and unified logging
munkiLog("### Starting managedsoftwareupdate run: \(runtype) ###")
if DisplayOptions.shared.verbose > 0 {
print("Managed Software Update Tool")
print("Version \(getVersion())")
print("Copyright 2010-2024 The Munki Project")
print("https://github.com/munki/munki\n")
}
displayMajorStatus("Starting...")
sendStartNotification()
try await runPreflight() // can exit early
let appleupdatesonly = (boolPref("AppleSoftwareUpdatesOnly") ?? false) || commonOptions.appleSUSPkgsOnly
let skipMunkiCheck = commonOptions.installOnly || appleupdatesonly
if !skipMunkiCheck {
warnIfServerIsDefault()
}
// reset our errors and warnings files, rotate main log if needed
munkiLogResetErrors()
munkiLogResetWarnings()
munkiLogRotateMainLog()
// archive the previous session's report
Report.shared.archiveReport()
if appleupdatesonly, DisplayOptions.shared.verbose > 0 {
print("NOTE: managedsoftwareupdate is configured to process Apple Software Updates only.")
}
let updateCheckResult = try await doMunkiUpdateCheck(skipCheck: skipMunkiCheck)
appleUpdateCount = doAppleUpdateCheckIfAppropriate(
appleUpdatesOnly: appleupdatesonly)
// display any available update info
if updateCheckResult == .updatesAvailable {
displayUpdateInfo()
}
if let stagedOSInstallerInfo = getStagedOSInstallerInfo() {
displayStagedOSInstallerInfo(info: stagedOSInstallerInfo)
} else if appleUpdateCount > 0 {
// TODO: displayAppleUpdateInfo()
}
// send a notification event so MSC can update its display if needed
sendUpdateNotification()
// this will get us a count of available Munki updates even if
// we did not check this time (one of the installonly modes)
munkiUpdateCount = munkiUpdatesAvailable()
reconfigureOptionsForInstall()
await handleInstallTasks()
displayMajorStatus("Finishing...")
await doFinishingTasks(runtype: runtype)
sendDockUpdateNotification()
sendEndedNotification()
munkiLog("### Ending managedsoftwareupdate run ###")
if !otherOptions.quiet {
print("Done.")
}
if mustLogout {
// not handling this currently
}
if restartAction == POSTACTION_SHUTDOWN {
doRestart(shutdown: true)
} else if restartAction == POSTACTION_RESTART {
doRestart()
} else {
// tell MunkiStatus/MSC we're done sending status info
munkiStatusQuit()
if shouldNotifyUser {
// it may have been more than a minute since we ran our original
// updatecheck so tickle the updatecheck time so MSC.app knows to
// display results immediately
recordUpdateCheckResult(.updatesAvailable)
notifyUserOfUpdates(force: forcedSoon)
if forcedSoon {
usleep(2_000_000)
startLogoutHelper()
}
}
}
clearBootstrapModeIfAppropriate()
}
}

View File

@@ -145,15 +145,22 @@ func recordUpdateCheckResult(_ result: UpdateCheckResult) {
setPref("LastCheckResult", result.rawValue)
}
func notifyUserOfUpdates(force: Bool = false) -> Bool {
func notifyUserOfUpdates(force: Bool = false) {
// Notify the logged-in user of available updates.
//
// Args:
// force: bool, default false, forcefully notify user regardless
// of LastNotifiedDate.
// Returns:
// Bool. true if the user was notified, false otherwise.
var userWasNotified = false
if getConsoleUser() == "loginwindow" {
// someone is logged in, but we're sitting at the loginwindow
// due to to fast user switching so do nothing
munkiLog("Skipping user notification because we are at the loginwindow.")
return
} else if boolPref("SuppressUserNotification") ?? false {
munkiLog("Skipping user notification because SuppressUserNotification is true.")
return
}
let lastNotifiedDate = datePref("LastNotifiedDate") ?? Date.distantPast
if !(pref("DaysBetweenNotifications") is Int) {
displayWarning("Preference DaysBetweenNotifications is not an integer; using a value of 1")
@@ -183,9 +190,7 @@ func notifyUserOfUpdates(force: Bool = false) -> Bool {
// clear the trigger file. We have to do it because we're root,
// and the munki-notifier process is running as the user
try? FileManager.default.removeItem(atPath: launchfile)
userWasNotified = true
}
return userWasNotified
}
func warnIfServerIsDefault() {