Skip to content

Commit 45362ae

Browse files
committed
add logic for acceptableUpdatePreparingUsage
1 parent 921a087 commit 45362ae

File tree

4 files changed

+109
-1
lines changed

4 files changed

+109
-1
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ Requires macOS 12.0 and higher. Further releases and feature requests may make t
7676
- [Issue 548](https://github.com/macadmins/nudge/issues/548)
7777
- When the device is running macOS 12.3 or higher, Nudge uses the delta logic for macOS Upgrades
7878
- [Issue 417](https://github.com/macadmins/nudge/issues/417)
79+
- Nudge can now bypass activations and re-activations when a macOS update is `Downloading`, `Preparing` or `Staged` for installation.
80+
- To enable this, please configure the `acceptableUpdatePreparingUsage` key under `optionalFeatures` to true
81+
- Be aware that the current logic used for this **cannot differentiate** when an update has completed preparing and is in the `Staged` phase, waiting for a user to reboot. This is due to an Apple process staying in memory. This will result in a reduction in Nudge re-activations
82+
- Issue [555](https://github.com/macadmins/nudge/issues/555)
7983

8084
## [1.1.16] - 2024-03-13
8185
This will be the **final Nudge release** for macOS 11 and potentially other versions of macOS.

Nudge/Preferences/DefaultPreferencesNudge.swift

+6
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ struct OptionalFeatureVariables {
8282
optionalFeaturesJSON?.acceptableCameraUsage ??
8383
false
8484
}
85+
86+
static var acceptableUpdatePreparingUsage: Bool {
87+
optionalFeaturesProfile?["acceptableUpdatePreparingUsage"] as? Bool ??
88+
optionalFeaturesJSON?.acceptableUpdatePreparingUsage ??
89+
false
90+
}
8591

8692
static var acceptableScreenSharingUsage: Bool {
8793
optionalFeaturesProfile?["acceptableScreenSharingUsage"] as? Bool ??

Nudge/Preferences/PreferencesStructure.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ extension NudgePreferences {
6666
// MARK: - OptionalFeatures
6767
struct OptionalFeatures: Codable {
6868
var acceptableApplicationBundleIDs, acceptableAssertionApplicationNames: [String]?
69-
var acceptableAssertionUsage, acceptableCameraUsage, acceptableScreenSharingUsage, aggressiveUserExperience, aggressiveUserFullScreenExperience, asynchronousSoftwareUpdate, attemptToBlockApplicationLaunches, attemptToCheckForSupportedDevice, attemptToFetchMajorUpgrade: Bool?
69+
var acceptableAssertionUsage, acceptableCameraUsage, acceptableUpdatePreparingUsage, acceptableScreenSharingUsage, aggressiveUserExperience, aggressiveUserFullScreenExperience, asynchronousSoftwareUpdate, attemptToBlockApplicationLaunches, attemptToCheckForSupportedDevice, attemptToFetchMajorUpgrade: Bool?
7070
var blockedApplicationBundleIDs: [String]?
7171
var customSOFAFeedURL: String?
7272
var disableNudgeForStandardInstalls, disableSoftwareUpdateWorkflow, enforceMinorUpdates, honorFocusModes, honorCycleTimersOnExit: Bool?
@@ -97,6 +97,7 @@ extension OptionalFeatures {
9797
acceptableAssertionApplicationNames: [String]? = nil,
9898
acceptableAssertionUsage: Bool? = nil,
9999
acceptableCameraUsage: Bool? = nil,
100+
acceptableUpdatePreparingUsage: Bool? = nil,
100101
acceptableScreenSharingUsage: Bool? = nil,
101102
aggressiveUserExperience: Bool? = nil,
102103
aggressiveUserFullScreenExperience: Bool? = nil,
@@ -120,6 +121,7 @@ extension OptionalFeatures {
120121
acceptableAssertionApplicationNames: acceptableAssertionApplicationNames ?? self.acceptableAssertionApplicationNames,
121122
acceptableAssertionUsage: acceptableAssertionUsage ?? self.acceptableAssertionUsage,
122123
acceptableCameraUsage: acceptableCameraUsage ?? self.acceptableCameraUsage,
124+
acceptableUpdatePreparingUsage: acceptableUpdatePreparingUsage ?? self.acceptableUpdatePreparingUsage,
123125
acceptableScreenSharingUsage: acceptableScreenSharingUsage ?? self.acceptableScreenSharingUsage,
124126
aggressiveUserExperience: aggressiveUserExperience ?? self.aggressiveUserExperience,
125127
aggressiveUserFullScreenExperience: aggressiveUserFullScreenExperience ?? self.aggressiveUserFullScreenExperience,

Nudge/Utilities/UILogic.swift

+96
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,17 @@
66
//
77

88
import AppKit
9+
import Darwin
910
import Foundation
1011
import IOKit.pwr_mgt // Asertions
1112
import SwiftUI
1213

14+
struct ProcessInfoStruct {
15+
var pid: Int32
16+
var command: String
17+
var arguments: [String]
18+
}
19+
1320
func initialLaunchLogic() {
1421
guard !CommandLineUtilities().unitTestingEnabled() else {
1522
LogManager.debug("App being ran in test mode", logger: uiLog)
@@ -177,6 +184,88 @@ private func logDeferralStates() {
177184
LoggerUtilities().logUserDeferrals()
178185
}
179186

187+
func getAllProcesses() -> [ProcessInfoStruct] {
188+
var processes = [ProcessInfoStruct]()
189+
190+
// Get the number of processes
191+
var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_ALL]
192+
var size = 0
193+
sysctl(&mib, u_int(mib.count), nil, &size, nil, 0)
194+
195+
let processCount = size / MemoryLayout<kinfo_proc>.size
196+
var processList = [kinfo_proc](repeating: kinfo_proc(), count: processCount)
197+
198+
// Get the list of processes
199+
sysctl(&mib, u_int(mib.count), &processList, &size, nil, 0)
200+
201+
// Extract process info
202+
for process in processList {
203+
let command = withUnsafePointer(to: process.kp_proc.p_comm) {
204+
$0.withMemoryRebound(to: CChar.self, capacity: Int(MAXCOMLEN)) {
205+
String(cString: $0)
206+
}
207+
}
208+
let pid = process.kp_proc.p_pid
209+
let arguments = getArgumentsForPID(pid: pid)
210+
processes.append(ProcessInfoStruct(pid: pid, command: command, arguments: arguments))
211+
}
212+
213+
return processes
214+
}
215+
216+
func getArgumentsForPID(pid: Int32) -> [String] {
217+
var args = [String]()
218+
219+
var mib: [Int32] = [CTL_KERN, KERN_PROCARGS2, pid]
220+
var size = 0
221+
sysctl(&mib, u_int(mib.count), nil, &size, nil, 0)
222+
223+
var buffer = [CChar](repeating: 0, count: size)
224+
sysctl(&mib, u_int(mib.count), &buffer, &size, nil, 0)
225+
226+
// Convert buffer to a string with proper bounds checking
227+
let bufferString = String(bytesNoCopy: &buffer, length: size, encoding: .ascii, freeWhenDone: false)
228+
229+
// Split the string into arguments
230+
if let bufferString = bufferString {
231+
args = bufferString.split(separator: "\0").map { String($0) }
232+
}
233+
234+
// Drop the first element which is the full path to the executable
235+
if !args.isEmpty {
236+
args.removeFirst()
237+
}
238+
239+
return args
240+
}
241+
242+
func isAnyProcessRunning(commandsWithArgs: [(commandPattern: String, arguments: [String]?)]) -> Bool {
243+
let processes = getAllProcesses()
244+
for (commandPattern, arguments) in commandsWithArgs {
245+
let matchingProcesses = processes.filter { process in
246+
fnmatch(commandPattern, process.command, 0) == 0 &&
247+
(arguments == nil || arguments!.allSatisfy { arg in
248+
process.arguments.contains(where: { $0.contains(arg) })
249+
})
250+
}
251+
if !matchingProcesses.isEmpty {
252+
return true
253+
}
254+
}
255+
return false
256+
}
257+
258+
func isDownloadingOrPreparingSoftwareUpdate() -> Bool {
259+
let commandsWithArgs: [(commandPattern: String, arguments: [String]?)] = [
260+
("softwareupdated", ["/System/Library/PrivateFrameworks/MobileSoftwareUpdate.framework/Support/softwareupdated"]), // When downloading a minor update, this process is running.
261+
("installcoordinationd", ["/System/Library/PrivateFrameworks/InstallCoordination.framework/Support/installcoordinationd"]), // When preparing a minor update, this process is running. Unfortunately, after preparing the update, this process appears to stay running.
262+
("softwareupdate", ["/usr/bin/softwareupdate", "--fetch-full-installer"]), // When downloading a major upgrade via SoftwareUpdate prefpane, it triggers a --fetch-full-installer run. Nudge also performs this method.
263+
("osinstallersetupd" ,["/Applications/*Install macOS *.app/Contents/Frameworks/OSInstallerSetup.framework/Resources/osinstallersetupd"]), // When installing a major upgrade, this process is running.
264+
// /System/Library/PrivateFrameworks/PackageKit.framework/Resources/installd||system_installd - system_installd may be interesting, but I think installd is being used for any package
265+
]
266+
return isAnyProcessRunning(commandsWithArgs: commandsWithArgs)
267+
}
268+
180269
func needToActivateNudge() -> Bool {
181270
if NSApplication.shared.isActive && nudgeLogState.afterFirstLaunch {
182271
LogManager.notice("Nudge is currently the frontmostApplication", logger: uiLog)
@@ -265,6 +354,7 @@ private func shouldBailOutEarly() -> Bool {
265354
/// 6. Acceptable Assertions are on
266355
/// 7. Acceptable Apps are in front
267356
/// 8. Refresh Timer hasn't been met
357+
/// 9. macOS Updates are downloading or preparing for installation
268358
let frontmostApplication = NSWorkspace.shared.frontmostApplication
269359
let pastRequiredInstallationDate = DateManager().pastRequiredInstallationDate()
270360

@@ -313,6 +403,12 @@ private func shouldBailOutEarly() -> Bool {
313403
if isRefreshTimerPassedThreshold() {
314404
return true
315405
}
406+
407+
// Check if downloading or preparing updates
408+
if OptionalFeatureVariables.acceptableUpdatePreparingUsage && isDownloadingOrPreparingSoftwareUpdate() {
409+
LogManager.info("Ignoring Nudge activation - macOS is currently downloading or preparing an update", logger: uiLog)
410+
return true
411+
}
316412

317413
return false
318414
}

0 commit comments

Comments
 (0)