-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathUSBMonitorMenuBar.m
More file actions
460 lines (383 loc) · 18.4 KB
/
USBMonitorMenuBar.m
File metadata and controls
460 lines (383 loc) · 18.4 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
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
#import <Cocoa/Cocoa.h>
#import <IOKit/IOKitLib.h>
#import <IOKit/usb/IOUSBLib.h>
#import <IOKit/IOCFPlugIn.h>
@interface USBMonitorAppDelegate : NSObject <NSApplicationDelegate, NSUserNotificationCenterDelegate>
@property (strong, nonatomic) NSStatusItem *statusItem;
@property (strong, nonatomic) NSMenu *menu;
@property (strong, nonatomic) NSMenuItem *toggleMenuItem;
@property (strong, nonatomic) NSMenuItem *logMenuItem;
@property (strong, nonatomic) NSMenuItem *notificationsMenuItem;
@property (strong, nonatomic) NSWindow *logWindow;
@property (strong, nonatomic) NSTextView *logTextView;
@property (assign, nonatomic) BOOL isMonitoring;
@property (assign, nonatomic) BOOL notificationsEnabled;
@property (assign, nonatomic) IONotificationPortRef notificationPort;
@property (assign, nonatomic) io_iterator_t addedIterator;
@property (assign, nonatomic) io_iterator_t removedIterator;
@property (strong, nonatomic) NSMutableArray *logEntries;
- (void)toggleMonitoring;
- (void)toggleNotifications;
- (void)setupUSBNotifications;
- (void)tearDownUSBNotifications;
- (void)deviceAdded:(io_iterator_t)iterator;
- (void)deviceRemoved:(io_iterator_t)iterator;
- (void)showLog;
- (void)addLogEntry:(NSString *)entry isConnected:(BOOL)isConnected;
- (void)showNotification:(NSString *)title message:(NSString *)message;
@end
// C callback functions
void DeviceAdded(void *refCon, io_iterator_t iterator) {
USBMonitorAppDelegate *self = (__bridge USBMonitorAppDelegate *)refCon;
[self deviceAdded:iterator];
}
void DeviceRemoved(void *refCon, io_iterator_t iterator) {
USBMonitorAppDelegate *self = (__bridge USBMonitorAppDelegate *)refCon;
[self deviceRemoved:iterator];
}
@implementation USBMonitorAppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
// Initialize log entries array
self.logEntries = [NSMutableArray array];
// Set up status item in menu bar
self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
[self.statusItem setTitle:@"USB*"];
[self.statusItem setHighlightMode:YES];
// Create menu
self.menu = [[NSMenu alloc] init];
self.toggleMenuItem = [[NSMenuItem alloc] initWithTitle:@"Stop Monitoring"
action:@selector(toggleMonitoring)
keyEquivalent:@""];
self.logMenuItem = [[NSMenuItem alloc] initWithTitle:@"Show Log"
action:@selector(showLog)
keyEquivalent:@""];
self.notificationsMenuItem = [[NSMenuItem alloc] initWithTitle:@"Disable Notifications"
action:@selector(toggleNotifications)
keyEquivalent:@""];
NSMenuItem *quitMenuItem = [[NSMenuItem alloc] initWithTitle:@"Quit"
action:@selector(terminate:)
keyEquivalent:@"q"];
[self.menu addItem:self.toggleMenuItem];
[self.menu addItem:self.logMenuItem];
[self.menu addItem:[NSMenuItem separatorItem]];
[self.menu addItem:self.notificationsMenuItem];
[self.menu addItem:[NSMenuItem separatorItem]];
[self.menu addItem:quitMenuItem];
self.statusItem.menu = self.menu;
// Initialize state - monitoring and notifications enabled by default
self.isMonitoring = YES;
self.notificationsEnabled = YES;
// Set up notification center delegate
[[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self];
// Start monitoring by default
[self setupUSBNotifications];
}
- (void)toggleMonitoring {
if (self.isMonitoring) {
// Stop monitoring
[self tearDownUSBNotifications];
self.isMonitoring = NO;
[self.toggleMenuItem setTitle:@"Start Monitoring"];
[self.statusItem setTitle:@"USB"];
} else {
// Start monitoring
[self setupUSBNotifications];
self.isMonitoring = YES;
[self.toggleMenuItem setTitle:@"Stop Monitoring"];
[self.statusItem setTitle:@"USB*"];
}
}
- (void)setupUSBNotifications {
// Set up notification port
self.notificationPort = IONotificationPortCreate(kIOMainPortDefault);
CFRunLoopSourceRef runLoopSource = IONotificationPortGetRunLoopSource(self.notificationPort);
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopDefaultMode);
// Set up matching dictionary for USB devices
CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOUSBDeviceClassName);
// Register for device added notifications
kern_return_t kr = IOServiceAddMatchingNotification(self.notificationPort,
kIOMatchedNotification,
matchingDict,
DeviceAdded,
(__bridge void *)(self),
&_addedIterator);
if (kr != KERN_SUCCESS) {
NSLog(@"Failed to register device added notification");
return;
}
// Call the callback function to arm the notification
[self deviceAdded:self.addedIterator];
// Create a new matching dictionary for removal (since the previous one was consumed)
matchingDict = IOServiceMatching(kIOUSBDeviceClassName);
// Register for device removed notifications
kr = IOServiceAddMatchingNotification(self.notificationPort,
kIOTerminatedNotification,
matchingDict,
DeviceRemoved,
(__bridge void *)(self),
&_removedIterator);
if (kr != KERN_SUCCESS) {
NSLog(@"Failed to register device removed notification");
return;
}
// Call the callback function to arm the notification
[self deviceRemoved:self.removedIterator];
[self addLogEntry:@"Monitoring started" isConnected:YES];
}
- (void)tearDownUSBNotifications {
if (self.notificationPort) {
IONotificationPortDestroy(self.notificationPort);
self.notificationPort = NULL;
}
if (self.addedIterator) {
IOObjectRelease(self.addedIterator);
self.addedIterator = 0;
}
if (self.removedIterator) {
IOObjectRelease(self.removedIterator);
self.removedIterator = 0;
}
[self addLogEntry:@"Monitoring stopped" isConnected:NO];
}
- (void)deviceAdded:(io_iterator_t)iterator {
io_service_t device;
while ((device = IOIteratorNext(iterator))) {
// Get device properties
CFMutableDictionaryRef propertiesDict = NULL;
kern_return_t kr = IORegistryEntryCreateCFProperties(device, &propertiesDict, kCFAllocatorDefault, 0);
if (kr == KERN_SUCCESS && propertiesDict != NULL) {
// Extract device information
CFStringRef productName = CFDictionaryGetValue(propertiesDict, CFSTR("USB Product Name"));
CFStringRef vendorName = CFDictionaryGetValue(propertiesDict, CFSTR("USB Vendor Name"));
CFNumberRef vendorIDRef = CFDictionaryGetValue(propertiesDict, CFSTR("idVendor"));
CFNumberRef productIDRef = CFDictionaryGetValue(propertiesDict, CFSTR("idProduct"));
// Set default values if not available
if (productName == NULL) {
productName = CFSTR("Unknown Device");
}
if (vendorName == NULL) {
vendorName = CFSTR("Unknown Vendor");
}
// Get values
uint16_t vendorID = 0;
uint16_t productID = 0;
if (vendorIDRef) {
CFNumberGetValue(vendorIDRef, kCFNumberSInt16Type, &vendorID);
}
if (productIDRef) {
CFNumberGetValue(productIDRef, kCFNumberSInt16Type, &productID);
}
// Format device info with the new simplified format
NSString *deviceInfo = [NSString stringWithFormat:
@"%@ (%@) [VID: 0x%04X PID: 0x%04X]",
(__bridge NSString *)productName,
(__bridge NSString *)vendorName,
vendorID,
productID];
NSString *logMessage = [NSString stringWithFormat:@"Connected: %@", deviceInfo];
[self addLogEntry:logMessage isConnected:YES];
// Show notification if enabled with new format
if (self.notificationsEnabled) {
[self showNotification:@"USB Device Connected" message:deviceInfo];
}
CFRelease(propertiesDict);
}
IOObjectRelease(device);
}
}
- (void)deviceRemoved:(io_iterator_t)iterator {
io_service_t device;
while ((device = IOIteratorNext(iterator))) {
// Get device properties
CFMutableDictionaryRef propertiesDict = NULL;
kern_return_t kr = IORegistryEntryCreateCFProperties(device, &propertiesDict, kCFAllocatorDefault, 0);
if (kr == KERN_SUCCESS && propertiesDict != NULL) {
// Extract device information
CFStringRef productName = CFDictionaryGetValue(propertiesDict, CFSTR("USB Product Name"));
CFStringRef vendorName = CFDictionaryGetValue(propertiesDict, CFSTR("USB Vendor Name"));
CFNumberRef vendorIDRef = CFDictionaryGetValue(propertiesDict, CFSTR("idVendor"));
CFNumberRef productIDRef = CFDictionaryGetValue(propertiesDict, CFSTR("idProduct"));
// Set default values if not available
if (productName == NULL) {
productName = CFSTR("Unknown Device");
}
if (vendorName == NULL) {
vendorName = CFSTR("Unknown Vendor");
}
// Get values
uint16_t vendorID = 0;
uint16_t productID = 0;
if (vendorIDRef) {
CFNumberGetValue(vendorIDRef, kCFNumberSInt16Type, &vendorID);
}
if (productIDRef) {
CFNumberGetValue(productIDRef, kCFNumberSInt16Type, &productID);
}
// Format device info with the new simplified format
NSString *deviceInfo = [NSString stringWithFormat:
@"%@ (%@) [VID: 0x%04X PID: 0x%04X]",
(__bridge NSString *)productName,
(__bridge NSString *)vendorName,
vendorID,
productID];
NSString *logMessage = [NSString stringWithFormat:@"Disconnected: %@", deviceInfo];
[self addLogEntry:logMessage isConnected:NO];
// Show notification if enabled with new format
if (self.notificationsEnabled) {
[self showNotification:@"USB Device Disconnected" message:deviceInfo];
}
CFRelease(propertiesDict);
}
IOObjectRelease(device);
}
}
- (void)addLogEntry:(NSString *)entry isConnected:(BOOL)isConnected {
// Get current date and time
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
NSString *dateString = [formatter stringFromDate:[NSDate date]];
// Create full log entry with timestamp
NSString *fullEntry = [NSString stringWithFormat:@"[%@] %@", dateString, entry];
// Add to log entries array
[self.logEntries addObject:@{
@"text": fullEntry,
@"isConnected": @(isConnected)
}];
// Update log window if it's open
if (self.logWindow && [self.logWindow isVisible]) {
[self updateLogWindowContent];
}
// Log to console as well
NSLog(@"%@", fullEntry);
}
- (void)showLog {
// Create log window if it doesn't exist or was closed
if (!self.logWindow || ![self.logWindow isVisible]) {
NSRect frame = NSMakeRect(0, 0, 800, 400);
self.logWindow = [[NSWindow alloc] initWithContentRect:frame
styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable
backing:NSBackingStoreBuffered
defer:NO];
[self.logWindow setTitle:@"USB Monitor Log"];
[self.logWindow center];
// Create scroll view
NSScrollView *scrollView = [[NSScrollView alloc] initWithFrame:[[self.logWindow contentView] bounds]];
[scrollView setBorderType:NSNoBorder];
[scrollView setHasVerticalScroller:YES];
[scrollView setHasHorizontalScroller:NO];
[scrollView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
// Create text view
self.logTextView = [[NSTextView alloc] initWithFrame:[[scrollView contentView] bounds]];
[self.logTextView setMinSize:NSMakeSize(0.0, 0.0)];
[self.logTextView setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
[self.logTextView setVerticallyResizable:YES];
[self.logTextView setHorizontallyResizable:NO];
[self.logTextView setAutoresizingMask:NSViewWidthSizable];
[self.logTextView setEditable:NO];
[self.logTextView setRichText:YES];
// Set background color to black
[self.logTextView setBackgroundColor:[NSColor blackColor]];
// Set up scroll view with text view
[scrollView setDocumentView:self.logTextView];
[[self.logWindow contentView] addSubview:scrollView];
}
// Update log content
[self updateLogWindowContent];
// Show the window and bring it to front
[self.logWindow makeKeyAndOrderFront:nil];
[NSApp activateIgnoringOtherApps:YES];
}
- (void)updateLogWindowContent {
// Clear existing content
[self.logTextView setString:@""];
// Setup attributes for log entries
NSMutableAttributedString *logContent = [[NSMutableAttributedString alloc] init];
NSDictionary *normalAttrs = @{
NSFontAttributeName: [NSFont monospacedSystemFontOfSize:12 weight:NSFontWeightRegular],
NSForegroundColorAttributeName: [NSColor whiteColor]
};
NSDictionary *connectedAttrs = @{
NSFontAttributeName: [NSFont monospacedSystemFontOfSize:12 weight:NSFontWeightRegular],
NSForegroundColorAttributeName: [NSColor colorWithRed:0 green:0.6 blue:0 alpha:1]
};
NSDictionary *disconnectedAttrs = @{
NSFontAttributeName: [NSFont monospacedSystemFontOfSize:12 weight:NSFontWeightRegular],
NSForegroundColorAttributeName: [NSColor colorWithRed:0.8 green:0 blue:0 alpha:1]
};
// Add each log entry with appropriate styling
for (NSDictionary *entry in self.logEntries) {
NSString *text = entry[@"text"];
BOOL isConnected = [entry[@"isConnected"] boolValue];
NSDictionary *attrs = normalAttrs;
if ([text containsString:@"Connected:"]) {
attrs = connectedAttrs;
} else if ([text containsString:@"Disconnected:"]) {
attrs = disconnectedAttrs;
}
NSAttributedString *attrLine = [[NSAttributedString alloc]
initWithString:text
attributes:attrs];
[logContent appendAttributedString:attrLine];
[logContent appendAttributedString:[[NSAttributedString alloc]
initWithString:@"\n"
attributes:normalAttrs]];
}
// Set the content
[self.logTextView.textStorage setAttributedString:logContent];
// Scroll to end
[self.logTextView scrollRangeToVisible:NSMakeRange(self.logTextView.string.length, 0)];
}
- (void)toggleNotifications {
self.notificationsEnabled = !self.notificationsEnabled;
if (self.notificationsEnabled) {
[self.notificationsMenuItem setTitle:@"Disable Notifications"];
[self addLogEntry:@"Notifications enabled" isConnected:YES];
} else {
[self.notificationsMenuItem setTitle:@"Enable Notifications"];
[self addLogEntry:@"Notifications disabled" isConnected:NO];
}
}
- (void)showNotification:(NSString *)title message:(NSString *)message {
@try {
// Create a new NSUserNotification
NSUserNotification *notification = [[NSUserNotification alloc] init];
notification.title = title;
notification.informativeText = message;
notification.soundName = NSUserNotificationDefaultSoundName;
// Deliver the notification
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification];
NSLog(@"Native notification sent successfully");
} @catch (NSException *exception) {
NSLog(@"Failed to show notification: %@", exception);
[self addLogEntry:[NSString stringWithFormat:@"Failed to show notification: %@", exception.reason] isConnected:NO];
}
}
// NSUserNotificationCenter delegate method to ensure notifications are shown even when app is active
- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification {
return YES;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Create application programmatically
[NSApplication sharedApplication];
// Add application menu
id menubar = [[NSMenu alloc] init];
id appMenuItem = [[NSMenuItem alloc] init];
[menubar addItem:appMenuItem];
[NSApp setMainMenu:menubar];
id appMenu = [[NSMenu alloc] init];
id quitMenuItem = [[NSMenuItem alloc] initWithTitle:@"Quit"
action:@selector(terminate:)
keyEquivalent:@"q"];
[appMenu addItem:quitMenuItem];
[appMenuItem setSubmenu:appMenu];
// Create and set delegate
USBMonitorAppDelegate *delegate = [[USBMonitorAppDelegate alloc] init];
[NSApp setDelegate:delegate];
// Activate and run
[NSApp activateIgnoringOtherApps:YES];
[NSApp run];
}
return 0;
}