942 lines
25 KiB
Mathematica
942 lines
25 KiB
Mathematica
|
//
|
||
|
// AVMonitor.m
|
||
|
// OverSight
|
||
|
//
|
||
|
// Created by Patrick Wardle on 9/01/16.
|
||
|
// Copyright (c) 2015 Objective-See. All rights reserved.
|
||
|
//
|
||
|
|
||
|
//TODO: NSLOg -> logmSg
|
||
|
|
||
|
#import "Consts.h"
|
||
|
#import "Logging.h"
|
||
|
#import "Utilities.h"
|
||
|
#import "AVMonitor.h"
|
||
|
#import "AppDelegate.h"
|
||
|
|
||
|
#import "../Shared/XPCProtocol.h"
|
||
|
|
||
|
//TODO: make instance methods?!
|
||
|
|
||
|
//grab first apple camera
|
||
|
AVCaptureDevice* findAppleCamera()
|
||
|
{
|
||
|
//apple camera
|
||
|
// ->likely FaceTime camera
|
||
|
AVCaptureDevice* appleCamera = nil;
|
||
|
|
||
|
//list of cameras
|
||
|
NSArray *cameras = nil;
|
||
|
|
||
|
//get cameras
|
||
|
cameras = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
|
||
|
for(AVCaptureDevice* camera in cameras)
|
||
|
{
|
||
|
//check if apple
|
||
|
if(YES == [camera.manufacturer isEqualToString:@"Apple Inc."])
|
||
|
{
|
||
|
//save
|
||
|
appleCamera = camera;
|
||
|
|
||
|
//exit loop
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return appleCamera;
|
||
|
}
|
||
|
|
||
|
//grab built-in mic
|
||
|
AVCaptureDevice* findAppleMic()
|
||
|
{
|
||
|
//built-in mic
|
||
|
AVCaptureDevice* appleMic = nil;
|
||
|
|
||
|
//list of mics
|
||
|
NSArray *mics = nil;
|
||
|
|
||
|
//get mics
|
||
|
mics = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
|
||
|
for(AVCaptureDevice* mic in mics)
|
||
|
{
|
||
|
//check if apple
|
||
|
// ->also check input source
|
||
|
if( (YES == [mic.manufacturer isEqualToString:@"Apple Inc."]) &&
|
||
|
(YES == [[[mic activeInputSource] inputSourceID] isEqualToString:@"imic"]) )
|
||
|
{
|
||
|
//save
|
||
|
appleMic = mic;
|
||
|
|
||
|
//exit loop
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
return appleMic;
|
||
|
}
|
||
|
|
||
|
|
||
|
@implementation AVMonitor
|
||
|
|
||
|
@synthesize mic;
|
||
|
@synthesize camera;
|
||
|
@synthesize audioActive;
|
||
|
@synthesize videoActive;
|
||
|
@synthesize videoMonitorThread;
|
||
|
|
||
|
//init
|
||
|
-(id)init
|
||
|
{
|
||
|
//init super
|
||
|
self = [super init];
|
||
|
if(nil != self)
|
||
|
{
|
||
|
|
||
|
}
|
||
|
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
//initialiaze AV notifcations/callbacks
|
||
|
-(BOOL)monitor
|
||
|
{
|
||
|
//return var
|
||
|
BOOL bRet = NO;
|
||
|
|
||
|
//xpc connection
|
||
|
__block NSXPCConnection* xpcConnection = nil;
|
||
|
|
||
|
//device's connection id
|
||
|
unsigned int connectionID = 0;
|
||
|
|
||
|
//selector for getting device id
|
||
|
SEL methodSelector = nil;
|
||
|
|
||
|
//array for devices + status
|
||
|
NSMutableArray* devices = nil;
|
||
|
|
||
|
//wait semaphore
|
||
|
dispatch_semaphore_t waitSema = nil;
|
||
|
|
||
|
//alloc XPC connection
|
||
|
xpcConnection = [[NSXPCConnection alloc] initWithServiceName:@"com.objective-see.OverSightXPC"];
|
||
|
|
||
|
//alloc device array
|
||
|
devices = [NSMutableArray array];
|
||
|
|
||
|
//set remote object interface
|
||
|
xpcConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(XPCProtocol)];
|
||
|
|
||
|
//resume
|
||
|
[xpcConnection resume];
|
||
|
|
||
|
//dbg msg
|
||
|
logMsg(LOG_DEBUG, @"telling XPC service to begin base-lining mach messages");
|
||
|
|
||
|
//init wait semaphore
|
||
|
waitSema = dispatch_semaphore_create(0);
|
||
|
|
||
|
//XPC service to begin baselining mach messages
|
||
|
// ->wait, since want this to compelete before doing other things!
|
||
|
[[xpcConnection remoteObjectProxy] initialize:^
|
||
|
{
|
||
|
|
||
|
//signal sema
|
||
|
dispatch_semaphore_signal(waitSema);
|
||
|
|
||
|
}];
|
||
|
|
||
|
//wait until XPC is done
|
||
|
// ->XPC reply block will signal semaphore
|
||
|
dispatch_semaphore_wait(waitSema, DISPATCH_TIME_FOREVER);
|
||
|
|
||
|
//init selector
|
||
|
methodSelector = NSSelectorFromString(@"connectionID");
|
||
|
|
||
|
//find (first) apple camera
|
||
|
self.camera = findAppleCamera();
|
||
|
|
||
|
//find built in mic
|
||
|
self.mic = findAppleMic();
|
||
|
|
||
|
//dbg msg
|
||
|
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"found mic: %@", self.mic]);
|
||
|
|
||
|
//got camera
|
||
|
// ->grab connection ID and invoke helper functions
|
||
|
if( (nil != self.camera) &&
|
||
|
(YES == [self.camera respondsToSelector:methodSelector]) )
|
||
|
{
|
||
|
//ignore leak warning
|
||
|
// ->we know what we're doing via this 'performSelector'
|
||
|
#pragma clang diagnostic push
|
||
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||
|
|
||
|
//grab connection ID
|
||
|
connectionID = (unsigned int)[self.camera performSelector:methodSelector withObject:nil];
|
||
|
|
||
|
//restore
|
||
|
#pragma clang diagnostic pop
|
||
|
|
||
|
//set status
|
||
|
// ->will set 'videoActive' iVar
|
||
|
[self setVideoDevStatus:connectionID];
|
||
|
|
||
|
//if video is already active
|
||
|
// ->start monitoring thread
|
||
|
if(YES == self.videoActive)
|
||
|
{
|
||
|
//tell XPC video is active
|
||
|
[[xpcConnection remoteObjectProxy] updateVideoStatus:self.videoActive reply:^{
|
||
|
|
||
|
//signal sema
|
||
|
dispatch_semaphore_signal(waitSema);
|
||
|
|
||
|
}];
|
||
|
|
||
|
//wait until XPC is done
|
||
|
// ->XPC reply block will signal semaphore
|
||
|
dispatch_semaphore_wait(waitSema, DISPATCH_TIME_FOREVER);
|
||
|
|
||
|
//alloc
|
||
|
videoMonitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(monitor4Procs) object:nil];
|
||
|
|
||
|
//start
|
||
|
[self.videoMonitorThread start];
|
||
|
}
|
||
|
|
||
|
//save camera/status into device array
|
||
|
[devices addObject:@{EVENT_DEVICE:self.camera, EVENT_DEVICE_STATUS:@(self.videoActive)}];
|
||
|
|
||
|
//register for video events
|
||
|
if(YES != [self watchVideo:connectionID])
|
||
|
{
|
||
|
//err msg
|
||
|
}
|
||
|
|
||
|
//dbg msg
|
||
|
logMsg(LOG_DEBUG, @"registerd for video events");
|
||
|
}
|
||
|
//err msg
|
||
|
else
|
||
|
{
|
||
|
//err msg
|
||
|
logMsg(LOG_ERR, @"failed to find (apple) camera :(");
|
||
|
}
|
||
|
|
||
|
//watch mic
|
||
|
// ->grab connection ID and invoke helper function
|
||
|
if( (nil != self.mic) &&
|
||
|
(YES == [self.mic respondsToSelector:methodSelector]) )
|
||
|
{
|
||
|
//ignore leak warning
|
||
|
// ->we know what we're doing via this 'performSelector'
|
||
|
#pragma clang diagnostic push
|
||
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||
|
|
||
|
//grab connection ID
|
||
|
connectionID = (unsigned int)[self.mic performSelector:NSSelectorFromString(@"connectionID") withObject:nil];
|
||
|
|
||
|
//restore
|
||
|
#pragma clang diagnostic pop
|
||
|
|
||
|
//save camera/status into device array
|
||
|
[devices addObject:@{EVENT_DEVICE:self.mic, EVENT_DEVICE_STATUS:@(self.audioActive)}];
|
||
|
|
||
|
//register for audio events
|
||
|
if(YES != [self watchAudio:connectionID])
|
||
|
{
|
||
|
//err msg
|
||
|
}
|
||
|
|
||
|
//dbg msg
|
||
|
logMsg(LOG_DEBUG, @"registerd for audio events");
|
||
|
}
|
||
|
|
||
|
//err msg
|
||
|
else
|
||
|
{
|
||
|
//err msg
|
||
|
logMsg(LOG_ERR, @"failed to find (apple) mic :(");
|
||
|
}
|
||
|
|
||
|
//send msg to status menu
|
||
|
// ->update menu to show devices & their status
|
||
|
[((AppDelegate*)[[NSApplication sharedApplication] delegate]).statusBarMenuController updateStatusItemMenu:devices];
|
||
|
|
||
|
//no errors
|
||
|
bRet = YES;
|
||
|
|
||
|
//TODO: not needed?
|
||
|
//bail
|
||
|
bail:
|
||
|
|
||
|
//cleanup XPC
|
||
|
if(nil != xpcConnection)
|
||
|
{
|
||
|
//close connection
|
||
|
[xpcConnection invalidate];
|
||
|
|
||
|
//nil out
|
||
|
xpcConnection = nil;
|
||
|
}
|
||
|
|
||
|
|
||
|
return bRet;
|
||
|
}
|
||
|
|
||
|
//determine if video is active
|
||
|
// ->sets 'videoActive' iVar
|
||
|
-(void)setVideoDevStatus:(CMIODeviceID)deviceID
|
||
|
{
|
||
|
//status var
|
||
|
OSStatus status = -1;
|
||
|
|
||
|
//running flag
|
||
|
UInt32 isRunning = -1;
|
||
|
|
||
|
//size of query flag
|
||
|
UInt32 propertySize = 0;
|
||
|
|
||
|
//property address struct
|
||
|
CMIOObjectPropertyAddress propertyStruct = {0};
|
||
|
|
||
|
//init size
|
||
|
propertySize = sizeof(isRunning);
|
||
|
|
||
|
//init property struct's selector
|
||
|
propertyStruct.mSelector = kAudioDevicePropertyDeviceIsRunningSomewhere;
|
||
|
|
||
|
//init property struct's scope
|
||
|
propertyStruct.mScope = kCMIOObjectPropertyScopeGlobal;
|
||
|
|
||
|
//init property struct's element
|
||
|
propertyStruct.mElement = 0;
|
||
|
|
||
|
//query to get 'kAudioDevicePropertyDeviceIsRunningSomewhere' status
|
||
|
status = CMIOObjectGetPropertyData(deviceID, &propertyStruct, 0, NULL, sizeof(kAudioDevicePropertyDeviceIsRunningSomewhere), &propertySize, &isRunning);
|
||
|
if(noErr != status)
|
||
|
{
|
||
|
//err msg
|
||
|
logMsg(LOG_ERR, [NSString stringWithFormat:@"getting status of video device failed with %d", status]);
|
||
|
|
||
|
//set error
|
||
|
isRunning = -1;
|
||
|
|
||
|
//bail
|
||
|
goto bail;
|
||
|
}
|
||
|
|
||
|
//set iVar
|
||
|
self.videoActive = isRunning;
|
||
|
|
||
|
//bail
|
||
|
bail:
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
|
||
|
//helper function
|
||
|
// ->determines if video went active/inactive then invokes notification generator method
|
||
|
-(void)handleVideoNotification:(CMIOObjectID)deviceID addresses:(const CMIOObjectPropertyAddress[]) addresses
|
||
|
{
|
||
|
//event dictionary
|
||
|
NSMutableDictionary* event = nil;
|
||
|
|
||
|
//xpc connection
|
||
|
__block NSXPCConnection* xpcConnection = nil;
|
||
|
|
||
|
//wait semaphore
|
||
|
dispatch_semaphore_t waitSema = nil;
|
||
|
|
||
|
//init dictionary
|
||
|
event = [NSMutableDictionary dictionary];
|
||
|
|
||
|
//sync?
|
||
|
//TODO: is this a good idea?
|
||
|
@synchronized (self)
|
||
|
{
|
||
|
|
||
|
//set status
|
||
|
[self setVideoDevStatus:deviceID];
|
||
|
|
||
|
//send msg to status menu
|
||
|
// ->update menu to show (all) devices & their status
|
||
|
[((AppDelegate*)[[NSApplication sharedApplication] delegate]).statusBarMenuController updateStatusItemMenu:@[@{EVENT_DEVICE:self.camera, EVENT_DEVICE_STATUS:@(self.videoActive)},@{EVENT_DEVICE:self.mic, EVENT_DEVICE_STATUS:@(self.audioActive)}]];
|
||
|
|
||
|
//add device
|
||
|
event[EVENT_DEVICE] = self.camera;
|
||
|
|
||
|
//set device status
|
||
|
event[EVENT_DEVICE_STATUS] = [NSNumber numberWithInt:self.videoActive];
|
||
|
|
||
|
//dbg msg
|
||
|
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"got video change notification; is running? %x", self.videoActive]);
|
||
|
|
||
|
//alloc XPC connection
|
||
|
xpcConnection = [[NSXPCConnection alloc] initWithServiceName:@"com.objective-see.OverSightXPC"];
|
||
|
|
||
|
//set remote object interface
|
||
|
xpcConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(XPCProtocol)];
|
||
|
|
||
|
//resume
|
||
|
[xpcConnection resume];
|
||
|
|
||
|
//init wait semaphore
|
||
|
waitSema = dispatch_semaphore_create(0);
|
||
|
|
||
|
//tell XPC about video status
|
||
|
// ->for example, when video is active, will stop baselining
|
||
|
[[xpcConnection remoteObjectProxy] updateVideoStatus:self.videoActive reply:^{
|
||
|
|
||
|
//signal sema
|
||
|
dispatch_semaphore_signal(waitSema);
|
||
|
|
||
|
}];
|
||
|
|
||
|
//wait until XPC is done
|
||
|
// ->XPC reply block will signal semaphore
|
||
|
dispatch_semaphore_wait(waitSema, DISPATCH_TIME_FOREVER);
|
||
|
|
||
|
//if video just started
|
||
|
// ->ask for video procs from XPC
|
||
|
if(YES == self.videoActive)
|
||
|
{
|
||
|
//dbg msg
|
||
|
logMsg(LOG_DEBUG, @"querying XPC to get video process(s)");
|
||
|
|
||
|
//set allowed classes
|
||
|
[xpcConnection.remoteObjectInterface setClasses: [NSSet setWithObjects: [NSMutableArray class], [NSNumber class], nil]
|
||
|
forSelector: @selector(getVideoProcs:) argumentIndex: 0 ofReply: YES];
|
||
|
|
||
|
//invoke XPC service
|
||
|
[[xpcConnection remoteObjectProxy] getVideoProcs:^(NSMutableArray* videoProcesses)
|
||
|
{
|
||
|
//close connection
|
||
|
[xpcConnection invalidate];
|
||
|
|
||
|
//nil out
|
||
|
xpcConnection = nil;
|
||
|
|
||
|
//dbg msg
|
||
|
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"video procs from XPC: %@", videoProcesses]);
|
||
|
|
||
|
//generate notification for each process
|
||
|
for(NSNumber* processID in videoProcesses)
|
||
|
{
|
||
|
//set pid
|
||
|
event[EVENT_PROCESS_ID] = processID;
|
||
|
|
||
|
//generate notification
|
||
|
[self generateNotification:event];
|
||
|
}
|
||
|
|
||
|
//signal sema
|
||
|
dispatch_semaphore_signal(waitSema);
|
||
|
|
||
|
}];
|
||
|
|
||
|
//wait until XPC is done
|
||
|
// ->XPC reply block will signal semaphore
|
||
|
dispatch_semaphore_wait(waitSema, DISPATCH_TIME_FOREVER);
|
||
|
}
|
||
|
|
||
|
//video deactivated
|
||
|
// ->close XPC connection and alert user
|
||
|
else
|
||
|
{
|
||
|
//close connection
|
||
|
[xpcConnection invalidate];
|
||
|
|
||
|
//nil out
|
||
|
xpcConnection = nil;
|
||
|
|
||
|
//generate notification
|
||
|
[self generateNotification:event];
|
||
|
}
|
||
|
|
||
|
//poll for new video procs
|
||
|
// ->this thread will exit itself as its checks the 'videoActive' iVar
|
||
|
if(YES == self.videoActive)
|
||
|
{
|
||
|
//start monitor thread if needed
|
||
|
if(YES != videoMonitorThread.isExecuting)
|
||
|
{
|
||
|
//alloc
|
||
|
videoMonitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(monitor4Procs) object:nil];
|
||
|
|
||
|
//start
|
||
|
[self.videoMonitorThread start];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}//sync
|
||
|
|
||
|
//bail
|
||
|
bail:
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
//register for video notifcations
|
||
|
// ->block will invoke method on event
|
||
|
-(BOOL)watchVideo:(CMIOObjectID)deviceID
|
||
|
{
|
||
|
//ret var
|
||
|
BOOL bRegistered = NO;
|
||
|
|
||
|
//status var
|
||
|
OSStatus status = -1;
|
||
|
|
||
|
//property struct
|
||
|
CMIOObjectPropertyAddress propertyStruct = {0};
|
||
|
|
||
|
//init property struct's selector
|
||
|
propertyStruct.mSelector = kAudioDevicePropertyDeviceIsRunningSomewhere;
|
||
|
|
||
|
//init property struct's scope
|
||
|
propertyStruct.mScope = kAudioObjectPropertyScopeGlobal;
|
||
|
|
||
|
//init property struct's element
|
||
|
propertyStruct.mElement = kAudioObjectPropertyElementMaster;
|
||
|
|
||
|
//block
|
||
|
// ->invoked when video changes & just calls helper function
|
||
|
CMIOObjectPropertyListenerBlock listenerBlock = ^(UInt32 inNumberAddresses, const CMIOObjectPropertyAddress addresses[])
|
||
|
{
|
||
|
//invoke helper function
|
||
|
[self handleVideoNotification:deviceID addresses:addresses];
|
||
|
|
||
|
};
|
||
|
|
||
|
//register (add) property block listener
|
||
|
status = CMIOObjectAddPropertyListenerBlock(deviceID, &propertyStruct, dispatch_get_main_queue(), listenerBlock);
|
||
|
if(noErr != status)
|
||
|
{
|
||
|
//err msg
|
||
|
//TODO: add
|
||
|
|
||
|
//bail
|
||
|
goto bail;
|
||
|
}
|
||
|
|
||
|
//happy
|
||
|
bRegistered = YES;
|
||
|
|
||
|
//bail
|
||
|
bail:
|
||
|
|
||
|
return bRegistered;
|
||
|
}
|
||
|
|
||
|
//determine if audio is active
|
||
|
// ->sets 'audioActive' iVar
|
||
|
-(void)setAudioDevStatus:(AudioObjectID)deviceID
|
||
|
{
|
||
|
//status var
|
||
|
OSStatus status = -1;
|
||
|
|
||
|
//running flag
|
||
|
UInt32 isRunning = -1;
|
||
|
|
||
|
//size of query flag
|
||
|
UInt32 propertySize = 0;
|
||
|
|
||
|
//init size
|
||
|
propertySize = sizeof(isRunning);
|
||
|
|
||
|
//query to get 'kAudioDevicePropertyDeviceIsRunningSomewhere' status
|
||
|
status = AudioDeviceGetProperty(deviceID, 0, false, kAudioDevicePropertyDeviceIsRunningSomewhere, &propertySize, &isRunning);
|
||
|
if(noErr != status)
|
||
|
{
|
||
|
//err msg
|
||
|
logMsg(LOG_ERR, [NSString stringWithFormat:@"getting status of audio device failed with %d", status]);
|
||
|
|
||
|
//set error
|
||
|
isRunning = -1;
|
||
|
|
||
|
//bail
|
||
|
goto bail;
|
||
|
}
|
||
|
|
||
|
//set iVar
|
||
|
self.audioActive = isRunning;
|
||
|
|
||
|
//bail
|
||
|
bail:
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
//helper function
|
||
|
// ->determines if audio went active/inactive then invokes notification generator method
|
||
|
-(void)handleAudioNotification:(AudioObjectID)deviceID
|
||
|
{
|
||
|
//event dictionary
|
||
|
NSMutableDictionary* event = nil;
|
||
|
|
||
|
//init dictionary
|
||
|
event = [NSMutableDictionary dictionary];
|
||
|
|
||
|
//sync
|
||
|
@synchronized (self)
|
||
|
{
|
||
|
|
||
|
//set status
|
||
|
[self setAudioDevStatus:deviceID];
|
||
|
|
||
|
//send msg to status menu
|
||
|
// ->update menu to show (all) devices & their status
|
||
|
[((AppDelegate*)[[NSApplication sharedApplication] delegate]).statusBarMenuController updateStatusItemMenu:@[@{EVENT_DEVICE:self.mic, EVENT_DEVICE_STATUS:@(self.audioActive)},@{EVENT_DEVICE:self.camera, EVENT_DEVICE_STATUS:@(self.videoActive)}]];
|
||
|
|
||
|
//add device
|
||
|
event[EVENT_DEVICE] = self.mic;
|
||
|
|
||
|
//set device status
|
||
|
event[EVENT_DEVICE_STATUS] = [NSNumber numberWithInt:self.audioActive];
|
||
|
|
||
|
//dbg msg
|
||
|
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"got audio change notification; is running? %x", self.audioActive]);
|
||
|
|
||
|
//generate notification
|
||
|
[self generateNotification:event];
|
||
|
|
||
|
}
|
||
|
|
||
|
//bail
|
||
|
bail:
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
//register for audio notifcations
|
||
|
// ->block will invoke method on event
|
||
|
-(BOOL)watchAudio:(AudioObjectID)deviceID
|
||
|
{
|
||
|
//ret var
|
||
|
BOOL bRegistered = NO;
|
||
|
|
||
|
//property struct
|
||
|
AudioObjectPropertyAddress propertyStruct = {0};
|
||
|
|
||
|
//init property struct's selector
|
||
|
propertyStruct.mSelector = kAudioDevicePropertyDeviceIsRunningSomewhere;
|
||
|
|
||
|
//init property struct's scope
|
||
|
propertyStruct.mScope = kAudioObjectPropertyScopeGlobal;
|
||
|
|
||
|
//init property struct's element
|
||
|
propertyStruct.mElement = kAudioObjectPropertyElementMaster;
|
||
|
|
||
|
//block
|
||
|
// ->invoked when audio changes & just calls helper function
|
||
|
AudioObjectPropertyListenerBlock listenerBlock = ^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress *inAddresses)
|
||
|
{
|
||
|
[self handleAudioNotification:deviceID];
|
||
|
};
|
||
|
|
||
|
|
||
|
OSStatus ret = AudioObjectAddPropertyListenerBlock(deviceID, &propertyStruct, dispatch_get_main_queue(), listenerBlock);
|
||
|
if (ret)
|
||
|
{
|
||
|
abort(); // FIXME
|
||
|
}
|
||
|
|
||
|
|
||
|
//happy
|
||
|
bRegistered = YES;
|
||
|
|
||
|
//bail
|
||
|
bail:
|
||
|
|
||
|
return bRegistered;
|
||
|
|
||
|
}
|
||
|
|
||
|
//build and display notification
|
||
|
-(void)generateNotification:(NSDictionary*)event
|
||
|
{
|
||
|
//notification
|
||
|
NSUserNotification* notification = nil;
|
||
|
|
||
|
//title
|
||
|
NSMutableString* title = nil;
|
||
|
|
||
|
//details
|
||
|
NSMutableString* details = nil;
|
||
|
|
||
|
//process name
|
||
|
NSString* processName = nil;
|
||
|
|
||
|
//alloc notificaiton
|
||
|
notification = [[NSUserNotification alloc] init];
|
||
|
|
||
|
//alloc title
|
||
|
title = [NSMutableString string];
|
||
|
|
||
|
//alloc details
|
||
|
details = [NSMutableString string];
|
||
|
|
||
|
//set title
|
||
|
// ->audio device
|
||
|
if(YES == [event[EVENT_DEVICE] isKindOfClass:NSClassFromString(@"AVCaptureHALDevice")])
|
||
|
{
|
||
|
//add
|
||
|
[title appendString:@"Audio Device"];
|
||
|
}
|
||
|
//add source
|
||
|
// ->video device
|
||
|
else
|
||
|
{
|
||
|
//add
|
||
|
[title appendString:@"Video Device"];
|
||
|
}
|
||
|
|
||
|
//add action
|
||
|
// ->device went inactive
|
||
|
if(YES == [DEVICE_INACTIVE isEqual:event[EVENT_DEVICE_STATUS]])
|
||
|
{
|
||
|
//add
|
||
|
[title appendString:@" became inactive"];
|
||
|
}
|
||
|
//add action
|
||
|
// ->device went active
|
||
|
else
|
||
|
{
|
||
|
//add
|
||
|
[title appendString:@" became active"];
|
||
|
}
|
||
|
|
||
|
//customize buttons
|
||
|
// ->for mic or inactive events, just say 'ok'
|
||
|
if( (YES == [event[EVENT_DEVICE] isKindOfClass:NSClassFromString(@"AVCaptureHALDevice")]) ||
|
||
|
(YES == [DEVICE_INACTIVE isEqual:event[EVENT_DEVICE_STATUS]]) )
|
||
|
{
|
||
|
//set other button title
|
||
|
notification.otherButtonTitle = @"ok";
|
||
|
|
||
|
//remove action button
|
||
|
notification.hasActionButton = NO;
|
||
|
}
|
||
|
|
||
|
//customize buttons
|
||
|
// ->for activatated video; allow/block
|
||
|
else
|
||
|
{
|
||
|
//set other button title
|
||
|
notification.otherButtonTitle = @"allow";
|
||
|
|
||
|
//set action title
|
||
|
notification.actionButtonTitle = @"block";
|
||
|
|
||
|
//get process name
|
||
|
// TODO: see 'determineName' in BB (to get name from bundle, etc)
|
||
|
processName = [getProcessPath([event[EVENT_PROCESS_ID] intValue]) lastPathComponent];
|
||
|
|
||
|
//set pid in user info
|
||
|
// ->allows code to try kill proc (later) if user clicks 'block'
|
||
|
notification.userInfo = @{EVENT_PROCESS_ID:event[EVENT_PROCESS_ID]};
|
||
|
|
||
|
//set details
|
||
|
// ->name of process using it / icon too?
|
||
|
[notification setInformativeText:[NSString stringWithFormat:@"%@ (%@)", processName, event[EVENT_PROCESS_ID]]];
|
||
|
|
||
|
}
|
||
|
|
||
|
//icon issues
|
||
|
//http://stackoverflow.com/questions/11856766/osx-notification-center-icon
|
||
|
|
||
|
//contentImage for process icon?
|
||
|
|
||
|
//custom icon?
|
||
|
//http://stackoverflow.com/questions/24923979/nsusernotification-customisable-app-icon
|
||
|
|
||
|
//icon set automatically?
|
||
|
//[notification set]
|
||
|
|
||
|
//set title
|
||
|
[notification setTitle:title];
|
||
|
|
||
|
//set subtitle
|
||
|
[notification setSubtitle:((AVCaptureDevice*)event[EVENT_DEVICE]).localizedName];
|
||
|
|
||
|
//set details
|
||
|
// ->name of process using it / icon too?
|
||
|
//[notification setInformativeText:@"some process (1337)"];
|
||
|
|
||
|
//set notification
|
||
|
[[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self];
|
||
|
|
||
|
//deliver notification
|
||
|
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification];
|
||
|
|
||
|
//bail
|
||
|
bail:
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
//always present notifications
|
||
|
-(BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification
|
||
|
{
|
||
|
return YES;
|
||
|
}
|
||
|
|
||
|
//automatically invoked when user interacts w/ the notification popup
|
||
|
// ->only action we care about, is killing the process if they click 'block'
|
||
|
-(void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification
|
||
|
{
|
||
|
//xpc connection
|
||
|
__block NSXPCConnection* xpcConnection = nil;
|
||
|
|
||
|
//process id
|
||
|
NSNumber* processID = nil;
|
||
|
|
||
|
//dbg msg
|
||
|
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"use responded to notification: %@", notification]);
|
||
|
|
||
|
//for video
|
||
|
// ->kill process if user clicked 'block'
|
||
|
if( (YES == [notification.actionButtonTitle isEqualToString:@"block"]) &&
|
||
|
(notification.activationType == NSUserNotificationActivationTypeActionButtonClicked))
|
||
|
{
|
||
|
//extract process id
|
||
|
processID = notification.userInfo[EVENT_PROCESS_ID];
|
||
|
if(nil == processID)
|
||
|
{
|
||
|
//err msg
|
||
|
logMsg(LOG_ERR, [NSString stringWithFormat:@"failed to extract process id from notification, %@", notification.userInfo]);
|
||
|
|
||
|
//bail
|
||
|
goto bail;
|
||
|
}
|
||
|
|
||
|
//alloc XPC connection
|
||
|
xpcConnection = [[NSXPCConnection alloc] initWithServiceName:@"com.objective-see.OverSightXPC"];
|
||
|
|
||
|
//set remote object interface
|
||
|
xpcConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(XPCProtocol)];
|
||
|
|
||
|
//resume
|
||
|
[xpcConnection resume];
|
||
|
|
||
|
//dbg msg
|
||
|
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"invoking XPC method to kill: %@", processID]);
|
||
|
|
||
|
//invoke XPC method 'killProcess' to terminate
|
||
|
[[xpcConnection remoteObjectProxy] killProcess:processID reply:^(BOOL wasKilled)
|
||
|
{
|
||
|
//check for err
|
||
|
if(YES != wasKilled)
|
||
|
{
|
||
|
//err msg
|
||
|
logMsg(LOG_ERR, [NSString stringWithFormat:@"failed to kill/block: %@", processID]);
|
||
|
|
||
|
}
|
||
|
|
||
|
//close connection
|
||
|
[xpcConnection invalidate];
|
||
|
|
||
|
//nil out
|
||
|
xpcConnection = nil;
|
||
|
}];
|
||
|
|
||
|
}//user clicked 'block'
|
||
|
|
||
|
//bail
|
||
|
bail:
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
|
||
|
//monitor for new procs (video only at the moment)
|
||
|
// ->runs until video is no longer in use (set elsewhere)
|
||
|
-(void)monitor4Procs
|
||
|
{
|
||
|
//xpc connection
|
||
|
NSXPCConnection* xpcConnection = nil;
|
||
|
|
||
|
//wait semaphore
|
||
|
dispatch_semaphore_t waitSema = nil;
|
||
|
|
||
|
//dbg msg
|
||
|
logMsg(LOG_DEBUG, @"video is active, so polling for new procs");
|
||
|
|
||
|
//alloc XPC connection
|
||
|
xpcConnection = [[NSXPCConnection alloc] initWithServiceName:@"com.objective-see.OverSightXPC"];
|
||
|
|
||
|
//set remote object interface
|
||
|
xpcConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(XPCProtocol)];
|
||
|
|
||
|
//set classes
|
||
|
// ->arrays/numbers ok to vend
|
||
|
[xpcConnection.remoteObjectInterface setClasses: [NSSet setWithObjects: [NSMutableArray class], [NSNumber class], nil]
|
||
|
forSelector: @selector(getVideoProcs:) argumentIndex: 0 ofReply: YES];
|
||
|
//resume
|
||
|
[xpcConnection resume];
|
||
|
|
||
|
//poll while video is active
|
||
|
while(YES == self.videoActive)
|
||
|
{
|
||
|
//init wait semaphore
|
||
|
waitSema = dispatch_semaphore_create(0);
|
||
|
|
||
|
//dbg msg
|
||
|
logMsg(LOG_DEBUG, @"asking XPC for (new) video procs");
|
||
|
|
||
|
//invoke XPC service to get (new) video procs
|
||
|
// ->will generate user notifications for any new processes
|
||
|
[[xpcConnection remoteObjectProxy] getVideoProcs:^(NSMutableArray* videoProcesses)
|
||
|
{
|
||
|
//dbg msg
|
||
|
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"new video procs: %@", videoProcesses]);
|
||
|
|
||
|
//generate a notification for each process
|
||
|
// ->double check video is still active though...
|
||
|
for(NSNumber* processID in videoProcesses)
|
||
|
{
|
||
|
//check video
|
||
|
if(YES != self.videoActive)
|
||
|
{
|
||
|
//exit loop
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
//generate notification
|
||
|
[self generateNotification:@{EVENT_DEVICE:self.camera, EVENT_DEVICE_STATUS:DEVICE_ACTIVE, EVENT_PROCESS_ID:processID}];
|
||
|
}
|
||
|
|
||
|
//signal sema
|
||
|
dispatch_semaphore_signal(waitSema);
|
||
|
|
||
|
}];
|
||
|
|
||
|
//wait until XPC is done
|
||
|
// ->XPC reply block will signal semaphore
|
||
|
dispatch_semaphore_wait(waitSema, DISPATCH_TIME_FOREVER);
|
||
|
|
||
|
//nap
|
||
|
[NSThread sleepForTimeInterval:5.0f];
|
||
|
|
||
|
}//run until video (camera) is off
|
||
|
|
||
|
//bail
|
||
|
bail:
|
||
|
|
||
|
//close connection
|
||
|
[xpcConnection invalidate];
|
||
|
|
||
|
//nil out
|
||
|
xpcConnection = nil;
|
||
|
|
||
|
//dbg msg
|
||
|
logMsg(LOG_DEBUG, @"exiting monitor thread");
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
@end
|