Implment more managedsoftwareupdate support functions

This commit is contained in:
Greg Neagle
2024-08-29 08:25:04 -07:00
parent cee2d78acd
commit fe1cab8d12
6 changed files with 407 additions and 5 deletions
+2 -2
View File
@@ -48,12 +48,12 @@ func main() {
}
task.waitUntilExit()
// sleep 10 secs to make launchd happy
usleep(10 * 1_000_000)
usleep(10_000_000)
exit(0)
} else {
// we aren't in the current GUI session
// sleep 10 secs to make launchd happy
usleep(10 * 1_000_000)
usleep(10_000_000)
exit(0)
}
}
@@ -0,0 +1,62 @@
//
// distributednotifications.swift
// munki
//
// Created by Greg Neagle on 8/28/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.
import Foundation
func sendDistributedNotification(_ name: NSNotification.Name, userInfo: PlistDict? = nil) {
// Sends a NSDistributedNotification
let dnc = DistributedNotificationCenter.default()
dnc.postNotificationName(
name,
object: nil,
userInfo: userInfo,
options: [.deliverImmediately, .postToAllSessions]
)
}
func sendUpdateNotification() {
// Sends an update notification via NSDistributedNotificationCenter
// Managed Software Center.app and MunkiStatus.app register to receive these
// events.
let name = NSNotification.Name(rawValue: "com.googlecode.munki.managedsoftwareupdate.updateschanged")
let userInfo = ["pid": ProcessInfo().processIdentifier]
sendDistributedNotification(name, userInfo: userInfo)
}
func sendDockUpdateNotification() {
// Sends an update notification via NSDistributedNotificationCenter
// Managed Software Center.app's dock tile plugin registers to receive these
// events.
let name = NSNotification.Name(rawValue: "com.googlecode.munki.managedsoftwareupdate.dock.updateschanged")
let userInfo = ["pid": ProcessInfo().processIdentifier]
sendDistributedNotification(name, userInfo: userInfo)
}
func sendStartNotification() {
// Sends a start notification via NSDistributedNotificationCenter
let name = NSNotification.Name(rawValue: "com.googlecode.munki.managedsoftwareupdate.started")
let userInfo = ["pid": ProcessInfo().processIdentifier]
sendDistributedNotification(name, userInfo: userInfo)
}
func sendEndedNotification() {
// Sends an ended notification via NSDistributedNotificationCenter
let name = NSNotification.Name(rawValue: "com.googlecode.munki.managedsoftwareupdate.ended")
let userInfo = ["pid": ProcessInfo().processIdentifier]
sendDistributedNotification(name, userInfo: userInfo)
}
@@ -0,0 +1,308 @@
//
// msuutils.swift
// munki
//
// Created by Greg Neagle on 8/27/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.
import Foundation
func clearLastNotifiedDate() {
// Clear the last date the user was notified of updates.
setPref("LastNotifiedDate", nil)
}
func initMunkiDirs() -> Bool {
// attempts to create any missing directories needed by managedsoftwareupdate
// returns a boolean to indicate success
var dirlist = [managedInstallsDir()]
for subdir in [
"Archives",
"Cache",
"Logs",
"catalogs",
"client_resources",
"icons",
"manifests",
] {
dirlist.append(managedInstallsDir(subpath: subdir))
}
var success = true
for dir in dirlist {
if !pathExists(dir) {
do {
try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: false)
} catch {
displayError("Could not create missing directory \(dir): \(error.localizedDescription)")
success = false
}
}
}
return success
}
func runPreOrPostScript(_ scriptPath: String, displayName: String, runType: String) async -> Int {
// Run an external script. Do not run if the permissions on the external
// script file are weaker than the current executable.
if !pathExists(scriptPath) {
return 0
}
displayMinorStatus("Performing \(displayName) tasks...")
do {
let result = try await runExternalScript(
scriptPath, arguments: [runType]
)
if result.exitcode != 0 {
displayInfo("\(displayName) return code: \(result.exitcode)")
}
if !result.output.isEmpty {
displayInfo("\(displayName) stdout: \(result.output)")
}
if !result.error.isEmpty {
displayInfo("\(displayName) stderr: \(result.error)")
}
return result.exitcode
} catch ExternalScriptError.notFound {
// not required, so pass
} catch {
displayWarning("Unexpected error when attempting to run \(displayName): \(error.localizedDescription)")
}
return 0
}
func doCleanupTasks(runType _: String) {
// If there are executables inside the cleanup directory,
// run them and remove them if successful
// TODO: implement this
}
private func getInstallInfo() -> PlistDict? {
// gets info from InstallInfo.plist
// TODO: there is at least one other similar function elsewhere; de-dup
let installInfoPath = managedInstallsDir(subpath: "InstallInfo.plist")
if pathExists(installInfoPath) {
do {
if let plist = try readPlist(fromFile: installInfoPath) as? PlistDict {
return plist
} else {
displayError("\(installInfoPath) does not have the expected format")
}
} catch {
displayError("Could not read \(installInfoPath): \(error.localizedDescription)")
}
} else {
displayInfo("\(installInfoPath) does not exist")
}
return nil
}
func munkiUpdatesAvailable() -> Int {
// Return count of available updates.
if let plist = getInstallInfo() {
var updatesAvailable = 0
if let removals = plist["removals"] as? [PlistDict] {
updatesAvailable += removals.count
}
if let installs = plist["managed_installs"] as? [PlistDict] {
updatesAvailable += installs.count
}
return updatesAvailable
}
return 0
}
func munkiUpdatesContainItemWithInstallerType(_ installerType: String) -> Bool {
// Return true if there is an item with this installerType in the list of updates
if let plist = getInstallInfo(),
let managedInstalls = plist["managed_installs"] as? [PlistDict]
{
for item in managedInstalls {
if let type = item["installer_type"] as? String,
type == installerType
{
return true
}
}
}
return false
}
func munkiUpdatesContainAppleItems() -> Bool {
// Return True if there are any Apple items in the list of updates
if let plist = getInstallInfo() {
for key in ["managed_installs", "removals"] {
if let items = plist[key] as? [PlistDict] {
for item in items {
if let appleItem = item["apple_item"] as? Bool,
appleItem == true
{
return true
}
}
}
}
}
return false
}
func recordUpdateCheckResult(_ result: Int) {
// Record last check date and result
let now = Date()
setPref("LastCheckDate", now)
setPref("LastCheckResult", result)
}
func notifyUserOfUpdates(force: Bool = false) -> Bool {
// 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
let lastNotifiedDate = datePref("LastNotifiedDate") ?? Date.distantPast
if !(pref("DaysBetweenNotifications") is Int) {
displayWarning("Preference DaysBetweenNotifications is not an integer; using a value of 1")
}
let daysBetweenNotifications = intPref("DaysBetweenNotifications") ?? 1
let now = Date()
// calculate interval in seconds
let interval = if daysBetweenNotifications > 0 {
// we make this adjustment so a 'daily' notification
// doesn't require exactly 24 hours to elapse
// subtract 6 hours
// IOW, if we notify today at 4pm, we don't really want to wait
// until after 4pm tomorrow to notifiy again.
Double((daysBetweenNotifications * 24 * 60 * 60) - (6 * 60 * 60))
} else {
0.0
}
if force || now.timeIntervalSince(lastNotifiedDate) >= interval {
// record current notification date
setPref("LastNotifiedDate", now)
munkiLog("Notifying user of available updates.")
munkiLog("LastNotifiedDate was \(lastNotifiedDate)")
// trigger LaunchAgent to launch munki-notifier.app in the right context
let launchfile = "/var/run/com.googlecode.munki.munki-notifier"
FileManager.default.createFile(atPath: launchfile, contents: nil)
usleep(1_000_000)
// 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(_ url: String) {
// Munki defaults to using http://munki/repo as the base URL.
// This is useful as a bootstrapping default, but is insecure.
// Warn the admin if Munki is using an insecure default.
if url.isEmpty {
// hasn't been defined yet; will be auto-detected later
return
}
var server = url
if server.last == "/" {
server = String(server.dropLast())
}
if [DEFAULT_INSECURE_REPO_URL, DEFAULT_INSECURE_REPO_URL + "/manifests"].contains(server) {
displayWarning("Client is configured to use the default repo (\(DEFAULT_INSECURE_REPO_URL)), which is insecure. Client could be trivially compromised when off your organization's network. Consider using a non-default URL, and preferably an https:// URL.")
}
}
func removeLaunchdLogoutJobs() {
// Removes the jobs that launch MunkiStatus and managedsoftwareupdate at
// the loginwindow. We do this if we decide it's not applicable to run right
// now so we don't get relaunched repeatedly, but don't want to remove the
// trigger file because we do want to run again at the next logout/reboot.
// These jobs will be reloaded the next time we're in the loginwindow context.
munkiStatusQuit()
_ = runCLI("/bin/launchctl", arguments: ["remove", "com.googlecode.munki.MunkiStatus"])
_ = runCLI("/bin/launchctl", arguments: ["remove", "com.googlecode.munki.managedsoftwareupdate-loginwindow"])
}
func doRestart(shutdown: Bool = false) {
// Handle the need for a restart or a possible shutdown.
let message = if shutdown {
"Software installed or removed requires a shut down."
} else {
"Software installed or removed requires a restart."
}
if DisplayOptions.shared.munkistatusoutput {
munkiStatusHideStopButton()
munkiStatusMessage(message)
munkiStatusDetail("")
munkiStatusPercent(-1)
munkiLog(message)
} else {
displayInfo(message)
}
// check current console user
let consoleUser = getConsoleUser()
if consoleUser.isEmpty || consoleUser == "loginwindow" {
// no-one is logged in or we're at the loginwindow
usleep(5_000_000)
if shutdown {
// TODO: doAuthorizedOrNormalRestart(shutdown: shutdown)
} else if false { // TODO: !authrestartdRestart() {
// TODO: doAuthorizedOrNormalRestart(shutdown: shutdown)
}
} else {
if DisplayOptions.shared.munkistatusoutput {
// someone is logged in and we're using Managed Software Center.
// We need to notify the active user that a restart is required.
// We actually should almost never get here; generally Munki knows
// a restart is needed before even starting the updates and forces
// a logout before applying the updates
displayInfo("Notifying currently logged-in user to restart.")
munkiStatusActivate()
munkiStatusRestartAlert()
} else {
print("Please restart immediately.")
}
}
}
func doInstallTasks(doAppleUpdates _: Bool = false, onlyUnattended: Bool = false) async -> Bool {
// Perform our installation/removal tasks.
//
// Args:
// doAppleUpdates: Bool. If true, install Apple updates
// onlyUnattended: Bool. If true, only do unattended_(un)install items.
//
// Returns:
// Bool. True if a restart is required, false otherwise.
if !onlyUnattended {
// first, clear the last notified date so we can get notified of new
// changes after this round of installs
clearLastNotifiedDate()
}
var munkiItemsRestartAction = POSTACTION_NONE
var appleItemsRestartAction = POSTACTION_NONE
if munkiUpdatesAvailable() > 0 {}
return true
}
func startLogoutHelper() {
// Handle the need for a forced logout. Start our logouthelper
}
func doFinishingTasks(runtype _: String = "") {
// A collection of tasks to do as we finish up
}
@@ -11,6 +11,10 @@
C0011CAD2C7A64F30004ED70 /* Predicates.m in Sources */ = {isa = PBXBuildFile; fileRef = C0011CAB2C7A64F30004ED70 /* Predicates.m */; };
C0011CB22C7CDEC40004ED70 /* updatecheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0011CB12C7CDEC40004ED70 /* updatecheck.swift */; };
C0011CB32C7CDEC40004ED70 /* updatecheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0011CB12C7CDEC40004ED70 /* updatecheck.swift */; };
C0011CB52C7EC5B60004ED70 /* msuutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0011CB42C7EC5B60004ED70 /* msuutils.swift */; };
C0011CB62C7EC5B60004ED70 /* msuutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0011CB42C7EC5B60004ED70 /* msuutils.swift */; };
C0011CB82C7F86480004ED70 /* distributednotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0011CB72C7F86480004ED70 /* distributednotifications.swift */; };
C0011CB92C7F86480004ED70 /* distributednotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0011CB72C7F86480004ED70 /* distributednotifications.swift */; };
C013643D2C2DC529008DB215 /* makecatalogslib.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013643B2C2DC529008DB215 /* makecatalogslib.swift */; };
C01364402C2DCA5C008DB215 /* admincommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013643E2C2DCA5C008DB215 /* admincommon.swift */; };
C01364412C2DCA5C008DB215 /* admincommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013643E2C2DCA5C008DB215 /* admincommon.swift */; };
@@ -310,6 +314,8 @@
C0011CAE2C7A98280004ED70 /* Predicates.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Predicates.h; sourceTree = "<group>"; };
C0011CAF2C7A990B0004ED70 /* managedsoftwareupdate-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "managedsoftwareupdate-Bridging-Header.h"; sourceTree = "<group>"; };
C0011CB12C7CDEC40004ED70 /* updatecheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = updatecheck.swift; sourceTree = "<group>"; };
C0011CB42C7EC5B60004ED70 /* msuutils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = msuutils.swift; sourceTree = "<group>"; };
C0011CB72C7F86480004ED70 /* distributednotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = distributednotifications.swift; sourceTree = "<group>"; };
C013643B2C2DC529008DB215 /* makecatalogslib.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = makecatalogslib.swift; sourceTree = "<group>"; };
C013643E2C2DCA5C008DB215 /* admincommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = admincommon.swift; sourceTree = "<group>"; };
C01364422C2DD1BA008DB215 /* plistutils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = plistutils.swift; sourceTree = "<group>"; };
@@ -586,6 +592,8 @@
isa = PBXGroup;
children = (
C07A6FA82C2A82B400090743 /* managedsoftwareupdate.swift */,
C0011CB42C7EC5B60004ED70 /* msuutils.swift */,
C0011CB72C7F86480004ED70 /* distributednotifications.swift */,
);
path = managedsoftwareupdate;
sourceTree = "<group>";
@@ -1074,7 +1082,9 @@
C01792EE2C75187E008CBC22 /* autoconfig.swift in Sources */,
C0D00FB02C458EAA0021DA9C /* version.swift in Sources */,
C043ED1F2C4822C70047C025 /* sqlite3.swift in Sources */,
C0011CB52C7EC5B60004ED70 /* msuutils.swift in Sources */,
C0011CAC2C7A64F30004ED70 /* Predicates.m in Sources */,
C0011CB82C7F86480004ED70 /* distributednotifications.swift in Sources */,
C07074DC2C33AE5F00B86310 /* munkilog.swift in Sources */,
C0D9C2B02C62D4120019A067 /* powermanager.swift in Sources */,
C07A6FB22C2B22D300090743 /* constants.swift in Sources */,
@@ -1101,6 +1111,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C0011CB92C7F86480004ED70 /* distributednotifications.swift in Sources */,
C01364442C2DD1BA008DB215 /* plistutils.swift in Sources */,
C01364462C2E051F008DB215 /* makecatalogslib.swift in Sources */,
C01792EC2C75089F008CBC22 /* licensing.swift in Sources */,
@@ -1148,6 +1159,7 @@
C0D9C2982C6012C80019A067 /* dmg.swift in Sources */,
C030A9C22C41B556007F0B34 /* pkginfolib.swift in Sources */,
C07074E92C34932400B86310 /* pkgutils.swift in Sources */,
C0011CB62C7EC5B60004ED70 /* msuutils.swift in Sources */,
C07074EC2C34A6AD00B86310 /* versionutils.swift in Sources */,
C01792EF2C75187E008CBC22 /* autoconfig.swift in Sources */,
C013644A2C2F8EFE008DB215 /* GitFileRepo.swift in Sources */,
+1 -1
View File
@@ -52,7 +52,7 @@ func munkiStatusLaunch() {
let launchfile = "/var/run/com.googlecode.munki.MunkiStatus"
if FileManager.default.createFile(atPath: launchfile, contents: nil) {
usleep(100_000)
usleep(1_000_000)
try? FileManager.default.removeItem(atPath: launchfile)
} else {
printStderr("Couldn't create launchpath \(launchfile)")
+22 -2
View File
@@ -125,14 +125,20 @@ func reloadPrefs() {
CFPreferencesAppSynchronize(BUNDLE_ID)
}
func setPref(_ prefName: String, _ prefValue: Any) {
func setPref(_ prefName: String, _ prefValue: Any?) {
/* Sets a preference, writing it to
/Library/Preferences/ManagedInstalls.plist.
This should normally be used only for 'bookkeeping' values;
values that control the behavior of munki may be overridden
elsewhere (by MCX, for example) */
if let key = prefName as CFString? {
if let value = prefValue as CFPropertyList? {
if prefValue == nil {
CFPreferencesSetValue(
key, nil, BUNDLE_ID,
kCFPreferencesAnyUser, kCFPreferencesCurrentHost
)
CFPreferencesAppSynchronize(BUNDLE_ID)
} else if let value = prefValue as CFPropertyList? {
CFPreferencesSetValue(
key, value, BUNDLE_ID,
kCFPreferencesAnyUser, kCFPreferencesCurrentHost
@@ -188,6 +194,20 @@ func intPref(_ prefName: String) -> Int? {
return pref(prefName) as? Int
}
func datePref(_ prefName: String) -> Date? {
// returns preference as a Date if possible
if let date = pref(prefName) as? Date {
return date
}
if let str = pref(prefName) as? String {
let dateFormatter = ISO8601DateFormatter()
if let date = dateFormatter.date(from: str) {
return date
}
}
return nil
}
func managedInstallsDir(subpath: String? = nil) -> String {
// convenience function to return the path to the Managed Installs dir
// or a subpath of that directory