forked from hholtmann/smcFanControl
-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathOCLPUpdateGuardian.m
More file actions
395 lines (322 loc) · 13.6 KB
/
Copy pathOCLPUpdateGuardian.m
File metadata and controls
395 lines (322 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
/*
* OCLPUpdateGuardian.m
* smcFanControl Community Edition
*
* Blocks macOS updates that aren't yet supported by OpenCore Legacy Patcher.
* One toggle: ON or OFF. Queries OCLP GitHub releases to determine compatibility.
*
* Copyright (c) 2026 wolffcatskyy. Licensed under GPL v2.
*/
#import "OCLPUpdateGuardian.h"
// MARK: - Constants
static NSString *const kDortaniaPath = @"/Library/Application Support/Dortania";
static NSString *const kSoftwareUpdateDomain = @"/Library/Preferences/com.apple.SoftwareUpdate";
static NSString *const kOCLPReleasesURL = @"https://api.github.com/repos/dortania/OpenCore-Legacy-Patcher/releases/latest";
static NSString *const kLogFilePath = @"/var/log/oclp-update-guardian.log";
static NSString *const kPrefsKey = @"UpdateGuardianEnabled";
static NSString *const kStagedUpdatesPath = @"/Library/Updates";
// MARK: - Private Interface
@interface OCLPUpdateGuardian ()
@property (nonatomic, strong) NSFileManager *fileManager;
@property (nonatomic, copy) NSString *cachedPendingUpdateVersion;
@property (nonatomic, assign) BOOL cachedPendingUpdateIsOCLPCompatible;
@property (nonatomic, copy) NSString *cachedOCLPVersion;
@end
@implementation OCLPUpdateGuardian
// MARK: - Singleton
+ (instancetype)sharedInstance {
static OCLPUpdateGuardian *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[OCLPUpdateGuardian alloc] init];
});
return instance;
}
- (instancetype)init {
self = [super init];
if (self) {
_fileManager = [NSFileManager defaultManager];
}
return self;
}
// MARK: - OCLP Detection
- (BOOL)isOCLPMac {
BOOL isDir = NO;
return [self.fileManager fileExistsAtPath:kDortaniaPath isDirectory:&isDir] && isDir;
}
// MARK: - Toggle (enabled)
- (BOOL)enabled {
return [[NSUserDefaults standardUserDefaults] boolForKey:kPrefsKey];
}
- (void)setEnabled:(BOOL)enabled {
[[NSUserDefaults standardUserDefaults] setBool:enabled forKey:kPrefsKey];
[[NSUserDefaults standardUserDefaults] synchronize];
[self log:@"Update Guardian %@", enabled ? @"enabled" : @"disabled"];
}
// MARK: - Current macOS Version
- (NSString *)currentMacOSVersion {
NSOperatingSystemVersion ver = [NSProcessInfo processInfo].operatingSystemVersion;
if (ver.patchVersion == 0) {
return [NSString stringWithFormat:@"%ld.%ld",
(long)ver.majorVersion, (long)ver.minorVersion];
}
return [NSString stringWithFormat:@"%ld.%ld.%ld",
(long)ver.majorVersion, (long)ver.minorVersion, (long)ver.patchVersion];
}
// MARK: - Pending Update Detection
- (NSString *)pendingUpdateVersion {
return self.cachedPendingUpdateVersion;
}
- (BOOL)pendingUpdateIsOCLPCompatible {
return self.cachedPendingUpdateIsOCLPCompatible;
}
- (NSString *)oclpVersion {
return self.cachedOCLPVersion;
}
- (NSString *)detectPendingUpdate {
// Method 1: Read SoftwareUpdate preferences for recommended updates
NSString *plistPath = [NSString stringWithFormat:@"%@.plist", kSoftwareUpdateDomain];
NSDictionary *prefs = [NSDictionary dictionaryWithContentsOfFile:plistPath];
NSArray *recommended = prefs[@"RecommendedUpdates"];
if ([recommended isKindOfClass:[NSArray class]] && recommended.count > 0) {
NSDictionary *update = recommended.firstObject;
NSString *version = update[@"Display Version"];
if (version.length > 0) {
return version;
}
}
// Method 2: Parse softwareupdate --list output
NSString *output = [self runTask:@"/usr/sbin/softwareupdate" arguments:@[@"--list"]];
if (output) {
NSRegularExpression *regex = [NSRegularExpression
regularExpressionWithPattern:@"macOS\\s+\\S+\\s+([\\d.]+)"
options:0 error:nil];
NSTextCheckingResult *match = [regex firstMatchInString:output
options:0 range:NSMakeRange(0, output.length)];
if (match && match.numberOfRanges >= 2) {
return [output substringWithRange:[match rangeAtIndex:1]];
}
}
return nil;
}
// MARK: - OCLP Compatibility Check
- (BOOL)fetchOCLPSupportsVersion:(NSString *)macOSVersion oclpVersion:(NSString **)outVersion {
if (!macOSVersion || macOSVersion.length == 0) {
return NO;
}
NSURL *url = [NSURL URLWithString:kOCLPReleasesURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setValue:@"application/vnd.github.v3+json" forHTTPHeaderField:@"Accept"];
[request setValue:@"smcFanControl-UpdateGuardian/2.0" forHTTPHeaderField:@"User-Agent"];
request.timeoutInterval = 30;
__block NSData *responseData = nil;
__block NSError *responseError = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionDataTask *task = [[NSURLSession sharedSession]
dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
responseData = data;
responseError = error;
dispatch_semaphore_signal(semaphore);
}];
[task resume];
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC));
if (responseError || !responseData) {
[self log:@"OCLP API request failed: %@", responseError.localizedDescription ?: @"timeout"];
return NO;
}
NSError *jsonError = nil;
NSDictionary *release = [NSJSONSerialization JSONObjectWithData:responseData
options:0
error:&jsonError];
if (jsonError || ![release isKindOfClass:[NSDictionary class]]) {
[self log:@"OCLP API returned invalid JSON"];
return NO;
}
NSString *tagName = release[@"tag_name"] ?: release[@"name"] ?: @"Unknown";
if (outVersion) {
*outVersion = tagName;
}
NSString *body = release[@"body"] ?: @"";
NSString *name = release[@"name"] ?: @"";
NSString *combined = [NSString stringWithFormat:@"%@ %@", name, body];
// Check if the release mentions the macOS version directly (e.g. "15.5")
if ([combined containsString:macOSVersion]) {
return YES;
}
// Check for "macOS 15.5" pattern
NSString *macOSPattern = [NSString stringWithFormat:@"macOS %@", macOSVersion];
if ([combined containsString:macOSPattern]) {
return YES;
}
// Check for support/compatible mentions (case-insensitive)
NSString *lowerCombined = combined.lowercaseString;
NSString *supportPattern = [NSString stringWithFormat:@"%@ support", macOSVersion];
if ([lowerCombined containsString:supportPattern.lowercaseString]) {
return YES;
}
// Check for the major version: if we're on 15.x and update is 15.y,
// and OCLP mentions "15." anywhere, that's a strong signal
NSArray<NSString *> *pendingParts = [macOSVersion componentsSeparatedByString:@"."];
NSArray<NSString *> *currentParts = [self.currentMacOSVersion componentsSeparatedByString:@"."];
if (pendingParts.count > 0 && currentParts.count > 0 &&
[pendingParts[0] isEqualToString:currentParts[0]]) {
// Same major version — minor update. Check if OCLP supports this major at all
NSString *majorPrefix = [NSString stringWithFormat:@"macOS %@.", pendingParts[0]];
if ([combined containsString:majorPrefix]) {
return YES;
}
}
return NO;
}
// MARK: - Core Logic: Check and Enforce
- (void)checkAndEnforce {
[self log:@"Running check (enabled: %@)", self.enabled ? @"YES" : @"NO"];
// Detect pending update
NSString *pending = [self detectPendingUpdate];
self.cachedPendingUpdateVersion = pending;
if (!pending) {
[self log:@"No pending macOS update detected"];
self.cachedPendingUpdateIsOCLPCompatible = NO;
self.cachedOCLPVersion = nil;
// If enabled, still enforce suppression in case macOS re-enabled auto-updates
if (self.enabled) {
[self suppressUpdates];
}
return;
}
[self log:@"Pending update: macOS %@", pending];
// Check OCLP compatibility
NSString *oclpVer = nil;
BOOL compatible = [self fetchOCLPSupportsVersion:pending oclpVersion:&oclpVer];
self.cachedPendingUpdateIsOCLPCompatible = compatible;
self.cachedOCLPVersion = oclpVer;
[self log:@"OCLP %@ — macOS %@ compatibility: %@",
oclpVer ?: @"(unknown)", pending, compatible ? @"CONFIRMED" : @"NOT CONFIRMED"];
if (!self.enabled) {
// Not enabled — remove any overrides we may have set before
[self restoreDefaults];
[self log:@"Guardian disabled, restoring default update behavior"];
return;
}
if (compatible) {
// OCLP supports this update — let it through
[self restoreDefaults];
[self log:@"Update is OCLP-compatible, allowing through"];
} else {
// OCLP does NOT support this update — block it
[self suppressUpdates];
[self log:@"Update is NOT OCLP-compatible, suppressing notifications and downloads"];
}
}
// MARK: - Suppress / Restore
- (void)suppressUpdates {
// Disable automatic check and download
[self runTask:@"/usr/bin/defaults" arguments:@[
@"write", kSoftwareUpdateDomain,
@"AutomaticDownload", @"-bool", @"false"
]];
[self runTask:@"/usr/bin/defaults" arguments:@[
@"write", kSoftwareUpdateDomain,
@"AutomaticCheckEnabled", @"-bool", @"false"
]];
// Push the notification date far into the future so the badge disappears
[self runTask:@"/usr/bin/defaults" arguments:@[
@"write", @"com.apple.SoftwareUpdate",
@"MajorOSUserNotificationDate", @"-date",
@"2030-01-01 00:00:00 +0000"
]];
// Clear the System Preferences notification badge
[self runTask:@"/usr/bin/defaults" arguments:@[
@"write", @"com.apple.systempreferences",
@"AttentionPrefBundleIDs", @""
]];
[self log:@"Update notifications suppressed, automatic downloads disabled"];
}
- (void)restoreDefaults {
// Re-enable automatic check and download
[self runTask:@"/usr/bin/defaults" arguments:@[
@"write", kSoftwareUpdateDomain,
@"AutomaticDownload", @"-bool", @"true"
]];
[self runTask:@"/usr/bin/defaults" arguments:@[
@"write", kSoftwareUpdateDomain,
@"AutomaticCheckEnabled", @"-bool", @"true"
]];
// Remove our notification date override
[self runTask:@"/usr/bin/defaults" arguments:@[
@"delete", @"com.apple.SoftwareUpdate",
@"MajorOSUserNotificationDate"
]];
[self log:@"Default update behavior restored"];
}
// MARK: - Abort Staged Update
- (void)abortStagedUpdate {
[self log:@"Aborting staged update"];
// Kill the update daemons
[self runTask:@"/usr/bin/killall" arguments:@[@"softwareupdated"]];
[self runTask:@"/usr/bin/killall" arguments:@[@"mobileassetd"]];
// Remove staged update files
NSInteger removedCount = 0;
if ([self.fileManager fileExistsAtPath:kStagedUpdatesPath]) {
NSError *error = nil;
NSArray<NSString *> *contents = [self.fileManager contentsOfDirectoryAtPath:kStagedUpdatesPath
error:&error];
for (NSString *item in contents) {
NSString *fullPath = [kStagedUpdatesPath stringByAppendingPathComponent:item];
NSError *removeError = nil;
if ([self.fileManager removeItemAtPath:fullPath error:&removeError]) {
[self log:@"Removed staged file: %@", item];
removedCount++;
} else {
[self log:@"Failed to remove %@: %@", item, removeError.localizedDescription];
}
}
}
// Also reset the ignored updates list
[self runTask:@"/usr/sbin/softwareupdate" arguments:@[@"--reset-ignored"]];
// Suppress notifications again in case they re-appeared
if (self.enabled) {
[self suppressUpdates];
}
[self log:@"Abort complete — killed daemons, removed %ld staged files", (long)removedCount];
}
// MARK: - Shell Task Execution
- (NSString *)runTask:(NSString *)launchPath arguments:(NSArray<NSString *> *)arguments {
@try {
NSTask *task = [[NSTask alloc] init];
task.launchPath = launchPath;
task.arguments = arguments;
NSPipe *stdoutPipe = [NSPipe pipe];
NSPipe *stderrPipe = [NSPipe pipe];
task.standardOutput = stdoutPipe;
task.standardError = stderrPipe;
[task launch];
[task waitUntilExit];
NSData *outputData = [stdoutPipe.fileHandleForReading readDataToEndOfFile];
return [[NSString alloc] initWithData:outputData encoding:NSUTF8StringEncoding];
} @catch (NSException *exception) {
[self log:@"Task exception (%@): %@", launchPath, exception.reason];
return nil;
}
}
// MARK: - Logging
- (void)log:(NSString *)format, ... {
va_list args;
va_start(args, format);
NSString *message = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"yyyy-MM-dd HH:mm:ss";
NSString *timestamp = [fmt stringFromDate:[NSDate date]];
NSString *logLine = [NSString stringWithFormat:@"[%@] %@\n", timestamp, message];
NSFileHandle *handle = [NSFileHandle fileHandleForWritingAtPath:kLogFilePath];
if (handle) {
[handle seekToEndOfFile];
[handle writeData:[logLine dataUsingEncoding:NSUTF8StringEncoding]];
[handle closeFile];
} else {
[logLine writeToFile:kLogFilePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
}
}
@end