mirror of
https://github.com/munki/munki.git
synced 2026-05-18 20:28:30 -05:00
Implment more managedsoftwareupdate support functions
This commit is contained in:
@@ -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 */,
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user