mirror of
https://github.com/munki/munki.git
synced 2026-02-14 11:10:01 -06:00
Implement still more of managedsoftwareupdate
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user