After IORegisterForSystemPower failing to call IODeregisterForSystemPower - objective-c

I have an application, written in Objective-C for MacOS 10.10+ which registers for sleep/wake notifications (code sample below, but the code isn't the question). What I am wondering is, if I call IORegisterForSystemPower at App initialisation, but during debugging I kill the app before it has a chance to call IODeregisterForSystemPower, what are the implications? Does the app get de-registered automatically when it dies in any case? Is there a system dictionary I need to clear out (a plist somewhere, etc.)? Thanks in advance for any help.
io_object_t root_notifier = MACH_PORT_NULL;
IONotificationPortRef notify = NULL;
DebugLog(#"App: Logging IORegisterForSystemPower sleep/wake notifications %#", [NSDate date]);
/* Log sleep/wake messages */
powerCallbackPort = IORegisterForSystemPower ((__bridge void *)self, &notify, sleepWakeCallback, &root_notifier);
if ( powerCallbackPort == IO_OBJECT_NULL ) {
DebugLog(#"IORegisterForSystemPower failed");
return;
}
self.rootNotifierPtr = &(root_notifier); // MARK: deregister with this pointer
if ( notify && powerCallbackPort )
{
CFRunLoopAddSource(CFRunLoopGetCurrent(),IONotificationPortGetRunLoopSource(notify), kCFRunLoopDefaultMode);
}

To be honest, I don't know the exact answer. But it may help you.
First of, if you call IORegisterForSystemPower, you need to make two calls in this order: - Call IODeregisterForSystemPower with the 'notifier' argument returned here. - Then call IONotificationPortDestroy passing the 'thePortRef' argument returned here (Please visit apple's document for more detail).
In case of port binding, if I use CFSocketSetAddress, before releasing this socket no other can use this port for binding. But in case of app terminate/closed without releasing this socket this port is available. That means after terminated the app system automatically releasing this.
Does the app get de-registered automatically when it dies in any case?
I think it will automatically de-registered by system.
I also used similar code as you in one of my project. But recently replaced with below codes:
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver: self selector: #selector(receiveWakeNotification:) name: NSWorkspaceDidWakeNotification object: nil];
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver: self selector: #selector(receiveSleepNotification:) name: NSWorkspaceWillSleepNotification object: nil];

Related

NSConnection - how to properly do "unvending" of an object?

For Mac OSX, I'm trying to use an NSConnection to proxy access of an object from one application instance to another on the same host. The relevant code is below. I can provide more if needed. Assume when I say "server", I'm referring to the application that actually "vends" an object with an NSConnection. And "client" is the other instance of the same application that gets a proxy to it.
Everything works, except for two issues.
When an application acting as a server attempts to tear down the object it is vending, any client connected through a proxy still remains. That is, even after I call my stopLocalServer function below, any client app that previously connected and got a proxy object is still able to send messages and invoke code on the server app. I would have expected the client to throw an exception when passing a message after the server calls NSConnection:invalidate. How do I forcibly disconnect any client without requiring the server process to exit?
In the startClientConnection code below, if the server never vended the object in the first place with the expected registered name, then the call on the client to NSConnection:connectionWithRegisteredName:host will immediately return nil. This is good. But if the server had started vending an object via the startLocalServer code below, then later stops vending it with stopLocalServer, subsequent client attempts to connect will hang (block forever) until the server application process exits. The call to NSConnection:connectionWithRegisteredName returns a non-nil object, but the call to [_clientConnection rootProxy] hangs forever until the server application actually exits.
I suspect that I'm not properly tearing down the original NSConnection object or I'm missing something basic here.
Here is some relevant code for the platform that my user interface code sits on top of:
-(void)startLocalServer:(NSString*)str
{
[self stopLocalServer]; // clean up any previous instance that might be running
_serverConnection = [NSConnection new];
[_serverConnection setRootObject:self];
[_serverConnection registerName:str];
}
-(void)stopLocalServer
{
[_serverConnection registerName:nil];
[_serverConnection setRootObject:nil];
[_serverConnection invalidate];
_serverConnection = nil;
}
-(void)startClientConnection:(NSString*)str
{
[self stopClientConnection]; // tear down any previous connection
_clientConnection = [NSConnection connectionWithRegisteredName:str host:nil];
if ((_clientConnection == nil) || (_clientConnection.valid == NO))
{
LogEvent(#"ERROR - _clientConnection is nil or invalid!");
}
else
{
_proxy = [_clientConnection rootProxy];
}
}
-(void)stopClientConnection
{
_proxy = nil;
[_clientConnection invalidate];
_clientConnection = nil;
}
Answering my own question. I'll still hold out for a better answer, or if anyone can do a better job explaining the reasoning about why this is needed.
I believe the stopLocalServer function needs to call [[_serverConnection receivePort] invalidate] such that the port created with the connection is closed. Just adding that line to the original stopLocalServer function solves my problem. That prevents further connection attempts and messages from succeeding.
More appropriately, the application call can just own the port that the NSConnection uses. So this becomes a better solution for starting and stopping a distributed object listener:
-(void)startLocalServer:(NSString*)str
{
[self stopLocalServer]; // clean up any previous instance that might be running
_port = [NSPort port]; // _port is of type NSPort*
_serverConnection = [NSConnection connectionWithReceivePort:_port sendPort:nil];
[_serverConnection setRootObject:self];
[_serverConnection registerName:str];
}
-(void)stopLocalServer
{
[_serverConnection registerName:nil];
[_serverConnection setRootObject:nil];
[_serverConnection invalidate];
_serverConnection = nil;
[_port invalidate];
_port = nil;
}
That seems to solve both #1 and #2 above.

How to observe notifications from a different application?

I'd like to get notified when a certain application triggers an event. I'm not an Objective-C developer nor do I know the OS X APIs—so I hope this question is not too basic.
My goal is to write the meta info of the currently playing song to a log file. For iTunes, I got this working with the following Objective-C line:
[[NSDistributedNotificationCenter defaultCenter]
addObserver: myObserver selector: #selector(observeNotification:)
name: #"com.apple.iTunes.playerInfo" object:nil];
However, I also need this for AirServer (which is an software AirPlay receiver). Unfortunately, the following doesn't work—the observer never gets called:
[[NSDistributedNotificationCenter defaultCenter]
addObserver: myObserver selector: #selector(observeNotification:)
name: #"com.pratikkumar.airserver-mac" object:nil];
So apparently, AirServer does not send these type of notifications. However, there's a notification in the Notification Center when a new song starts playing.
My next step would be to periodically check for new notifications in the OS X Notification Center (as described here: https://stackoverflow.com/a/25930769/1387396). This is not too clean though—so my questions is: is there another option in this particular case?
Catalina silently changed the addObserver behavior - you can no longer use a nil value for the name to observe all notifications - you have to pass in a name. This makes discovery of event names more difficult.
Firstly, you have to understand that while the NSDistributedNotificationCenter has the word Notification in it; it's not related. From the About Local Notifications and Remote Notifications, it does state:
Note: Remote notifications and local notifications are not related to broadcast notifications (NSNotificationCenter) or key-value observing notifications.
So, at that point, I'm going to answer from the perspective of NSDistributedNotificationCenter, and not about Remote/Local notifications - you already have a potential solution in the answer you linked for observing the DB file that contains a record of the notifications in that way.
This example code will not work on Catalina (10.15) because of API behavior changes
The first thing you need to do is listen for the correct notification. Creating a simple application that listens for all events; e.g. using:
NSDistributedNotificationCenter *center = [NSDistributedNotificationCenter defaultCenter];
[center addObserver:self
selector:#selector(notificationEvent:)
name:nil
object:nil];
-(void)notificationEvent:(NSNotification *)notif {
NSLog(#"%#", notif);
}
It indicates that the notifications are:
__CFNotification 0x6100000464b0 {name = com.airserverapp.AudioDidStart; object = com.pratikkumar.airserver-mac; userInfo = {
ip = "2002:d55e:dbb2:1:10e0:1bfb:4e81:b481";
remote = YES;
}}
__CFNotification 0x618000042790 {name = com.airserverapp.AudioDidStop; object = com.pratikkumar.airserver-mac; userInfo = {
ip = "2002:d55e:dbb2:1:10e0:1bfb:4e81:b481";
remote = YES;
}}
This indicates that the name parameter in the addObserver call should be com.airserverapp.AudioDidStart and com.airserverapp.AudioDidStop.
You can use code like that to determine all notifications, which will allow you to add the relevant observers when you want the particular observer. This is probably the easiest way to observe notifications of this type.

Why does my MCSession peer disconnect randomly?

Im using MCNearbyServiceBrowser and MCNearbyServiceAdvertiser to join two peers to a MCSession. I am able to send data between them using MCSession's sendData method. All seems to be working as expected until I randomly (and not due to any event I control) receive a MCSessionStateNotConnected via the session's MCSessionDelegate didChangeState handler. Additionally, the MCSession's connectedPeers array no longer has my peers.
Two questions: Why? and How do i keep the MCSession from disconnecting?
This is a bug, which I just reported to Apple. The docs claim the didReceiveCertificate callback is optional, but it's not. Add this method to your MCSessionDelegate:
- (void) session:(MCSession *)session didReceiveCertificate:(NSArray *)certificate fromPeer:(MCPeerID *)peerID certificateHandler:(void (^)(BOOL accept))certificateHandler
{
certificateHandler(YES);
}
The random disconnects should cease.
UPDATE After using a support ticket to Apple, they confirmed that calling sendData too often and with too much data can cause disconnects.
I have had disconnects when hitting break points and when backgrounding. Since the break points won't happen on the app store, you need to handle the backgrounding case by beginning a background task when your app is about to enter the background. Then end this task when your app comes back to the foreground. On iOS 7 this gives you about 3 background minutes which is better than nothing.
An additional strategy would be to schedule a local notification for maybe 15 seconds before your background time expires by using [[UIApplication sharedApplication] backgroundTimeRemaining], that way you can bring the user back into the app before it suspends and the multi peer framework has to be shutdown. Perhaps the local notification would warn them that their session will expire in 10 seconds or something...
If the background task expires and the app is still in the background, you have to tear down everything related to multi-peer connectivity, otherwise you will get crashes.
- (void) createExpireNotification
{
[self killExpireNotification];
if (self.connectedPeerCount != 0) // if peers connected, setup kill switch
{
NSTimeInterval gracePeriod = 20.0f;
// create notification that will get the user back into the app when the background process time is about to expire
NSTimeInterval msgTime = UIApplication.sharedApplication.backgroundTimeRemaining - gracePeriod;
UILocalNotification* n = [[UILocalNotification alloc] init];
self.expireNotification = n;
self.expireNotification.fireDate = [NSDate dateWithTimeIntervalSinceNow:msgTime];
self.expireNotification.alertBody = TR(#"Text_MultiPeerIsAboutToExpire");
self.expireNotification.soundName = UILocalNotificationDefaultSoundName;
self.expireNotification.applicationIconBadgeNumber = 1;
[UIApplication.sharedApplication scheduleLocalNotification:self.expireNotification];
}
}
- (void) killExpireNotification
{
if (self.expireNotification != nil)
{
[UIApplication.sharedApplication cancelLocalNotification:self.expireNotification];
self.expireNotification = nil;
}
}
- (void) applicationWillEnterBackground
{
self.taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^
{
[self shutdownMultiPeerStuff];
[[UIApplication sharedApplication] endBackgroundTask:self.taskId];
self.taskId = UIBackgroundTaskInvalid;
}];
[self createExpireNotification];
}
- (void) applicationWillEnterForeground
{
[self killExpireNotification];
if (self.taskId != UIBackgroundTaskInvalid)
{
[[UIApplication sharedApplication] endBackgroundTask:self.taskId];
self.taskId = UIBackgroundTaskInvalid;
}
}
- (void) applicationWillTerminate
{
[self killExpireNotification];
[self stop]; // shutdown multi-peer
}
You'll also want this handler in your MCSession delegate due to Apple bug:
- (void) session:(MCSession*)session didReceiveCertificate:(NSArray*)certificate fromPeer:(MCPeerID*)peerID certificateHandler:(void (^)(BOOL accept))certificateHandler
{
if (certificateHandler != nil) { certificateHandler(YES); }
}
There are many causes of this, and the two answers thus far are both correct in my experience. Another which you'll find in other similar questions is this: Only one peer can accept another's invitation.
So, to clarify, if you set up an app where all devices are both advertisers and browsers, any devices can freely invite any others found to join a session. However, between any two given devices, only one device can actually accept the invitation and connect to the other device. If both devices accept each others' invitations they will disconnect within a minute or less.
Note that this limitation does not prevent the desired behavior because - unlike what my intuition stated before I built my multipeer implementation - when one device accepts an invitation and connects to another device they both become connected and receive connection delegate methods and can send each other messages.
Therefore, if you are connecting devices which both browse and advertise, send invitations freely but only accept one of a pair.
The problem of only accepting one of two invitations can be solved a myriad of ways. To begin, understand that you can pass any arbitrary object or dictionary (archived as data) as the context argument in an invitation. Therefore, both devices have access to any arbitrary information about the other (and of course itself). So, you could use at least these strategies:
simply compare: the display name of the peerID. But there's no guarantee these won't be equal.
store the date your multipeer controller was initialized and use that for comparison
give each peer a UUID and send this for comparison (my technique, in which each device - indeed each user of the app on a device - has a persistent UUID it employs).
etc - any object which supports both NSCoding and compare: will do fine.
I've been having similar problems. It seems though that if I have run my app on one iOS device, and connected to another, then quit and relaunch (say when I rerun from Xcode), then I am in a situation where I get a Connected message and then a Not Connected message a little later. This was throwing me off. But looking more carefully, I can see that the Not Connected message is actually meant for a different peerId than the one that has connected.
I think the problem here is that most samples I've seen just care about the displayName of the peerID, and neglect the fact that you can get multiple peerIDs for the same device/displayName.
I am now checking the displayName first and then verifying that the peerID is the same, by doing a compare of the pointers.
- (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state {
MyPlayer *player = _players[peerID.displayName];
if ((state == MCSessionStateNotConnected) &&
(peerID != player.peerID)) {
NSLog(#"remnant connection drop");
return; // note that I don't care if player is nil, since I don't want to
// add a dictionary object for a Not Connecting peer.
}
if (player == nil) {
player = [MyPlayer init];
player.peerID = peerID;
_players[peerID.displayName] = player;
}
player.state = state;
...
I was disconnecting immediately after I accepted the connection request. Observing the state, I saw it change from MCSessionStateConnected to MCSessionStateNotConnected.
I am creating my sessions with:
[[MCSession alloc] initWithPeer:peerID]
NOT the instantiation method dealing with security certificates:
- (instancetype)initWithPeer:(MCPeerID *)myPeerID securityIdentity:(NSArray *)identity encryptionPreference:(MCEncryptionPreference)encryptionPreference
Based on Andrew's tip above, I added the delegate method
- (void) session:(MCSession *)session didReceiveCertificate:(NSArray *)certificate fromPeer:(MCPeerID *)peerID certificateHandler:(void (^)(BOOL accept))certificateHandler {
certificateHandler(YES);
}
and the disconnects stopped.

Cocoa app behaves diffirently with breakpoint on & off

Important update: I found out that most part of my question was based on a false premise (see my answer below). Notifications actually got to the receiver, they just got there too fast. (Although, it still doesn't explain why the behavior with breakpoint and without it was different.)
I'm developing the app that calculates the hashes of files given to it. The calculation takes place in SHHashComputer. It's an abstract class (well, intended to be abstract, as there are no abstract classes in Objective C) that takes the file path and creates an NSInvocationOperation. It, in turn, calls the method (void)computeAndSendHash, which uses the file path saved in the object to compute hash and sends it as notification. The actual computing takes place in (NSString*)computeHash method that child classes need to override.
Here's SHHashComputer.m:
- (NSString*)computeHash {
return [NSString stringWithFormat:#"unimplemented hash for file %#", self.path];
}
- (void)computeAndSendHash {
NSString *result = [self computeHash];
NSString *notificationName = [NSString stringWithFormat:#"%#%#",
gotResultNotification,
self.hashType];
[[NSNotificationCenter defaultCenter] postNotificationName:notificationName
object:result];
self.operation = nil;
}
And here's SHMD5Computer.m (the child class of SHHashComputer):
- (NSString*)computeHash {
return #"MD5 test"; // it actually doesn't matter what it returns
}
I won't bother you with the receivers of notification. Let's just say that as long as I comment out the computeHash method in SHMD5Computer.m everything works just fine: the notification with text "unimplemented ..." is received & displayed in GUI. But if I don't — then it gets really interesting.
If I don't set up any breakpoints, the notification just never comes. However, if I set up a breakpoint at the declaration of computeHash in SHMD5Computer.h and then step over until the line 'self.operation = nil', and continue execution at that point, the notification gets to destination. If I don't stop there, the debugger suddenly switches to the state as if it isn't debugging anything, and the app freezes.
I don't think that 'WTF' is a good form for a question here, so let me put it this way: am I missing something? Are there errors in my code? What can cause this type of behavior in xcode? How can I fix this?
(If you'll want to get all my code to reproduce it, I'll gladly give it to you.)
More experiments:
If I continute execution exactly after stopping at breakpoint, the application encounters EXC_BAD_ACCESS error in the code that receives the notification, at the last line:
id newResult = [newResultNotification object];
if (newResult == nil)
[NSException raise:#"No object"
format:#"Expected object with notification!"];
else if (![newResult isKindOfClass:[NSString class]])
[NSException raise:#"Not NSString"
format:#"Expected NSString object!"];
else
self.result = (NSString*) newResult;
[self.textField setStringValue:self.result];
When I tried to reproduce the previous experiment, something even stranger happenned. In my debug setup, I have two hash computer objects: one SHMD5HashComputer (which we're talking about), and one SHHashComputer (which, of course, produces the "unimpemented" hash). In all previous experiments, as long as app didn't crash, the notification form SHHashComputer always successfully arrived. But in this case, both notifications didn't arrive, and the app didn't crash. (All the steps are exactly the same as in previous one).
As Josh Caswell pointer out in the comments, I wasn't using the notifications correctly. I should've sent the object itself as notification object, as described in documentation. I fixed that, and I'm getting exactly the same results. (Which means that I fixed it correctly, because sometimes the notifications work correctly, and also that it wasn't the problem).
More updates:
The notification that I'm sending should arrive at SHHashResultViewController. That's how I create it and register for notification:
- (id)initWithHashType:(NSString *)hashType {
self = [self initWithNibName:#"SHHashResultView" bundle:[NSBundle mainBundle]];
if (self) {
[self setHashType:hashType];
}
return self;
}
- (void)setHashType:(NSString *)hashType {
[self.label setStringValue:[NSString stringWithFormat:#"%#:", hashType]];
_hashType = hashType;
NSString *notificationName = [NSString stringWithFormat:#"%#%#",
gotResultNotification,
_hashType];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(gotResult:)
name:notificationName
object:nil];
}
Actually, the question was based on a false premise. I thought that notification never came through because I never saw the information displayed in the GUI; however, my error was in the code of controllers (not published there) which made possible the situation in which the GUI first got results of hash calculation and only after that got information about a new input — which resulted in deleting all the text and activating progress animation.

Graceful termination of NSApplication with Core Data and Grand Central Dispatch (GCD)

I have an Cocoa Application (Mac OS X SDK 10.7) that is performing some processes via Grand Central Dispatch (GCD). These processes are manipulating some Core Data NSManagedObjects (non-document-based) in a manner that I believe is thread safe (creating a new managedObjectContext for use in this thread).
The problem I have is when the user tries to quit the application while the dispatch queue is still running.
The NSApplication delegate is being called before actually quitting.
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
I get an error "Could not merge changes." Which is somewhat expected since there are still operations being performed through the different managedObjectContext. I am then presented with the NSAlert from the template that is generated with a core data application.
In the Threading Programming Guide there is a section called "Be Aware of Thread Behaviors at Quit Time" which alludes to using replyToApplicationShouldTerminate: method. I'm having a little trouble implementing this.
What I would like is for my application to complete processing the queued items and then terminate without presenting an error message to the user. It would also be helpful to update the view or use a sheet to let the user know that the app is performing some action and will terminate when the action is complete.
Where and how would I implement this behavior?
Solution:
So I had a few different issues here.
I had blocks that were accessing core data in a dispatch_queue preventing my application from terminating gracefully.
When I tried to add a new item to the dispatch_queue a new instance of the dispatch_queue was started on a new thread.
What I did to solve this was use NSNotificationCenter in my AppDelegate (where (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender was being called. In the template code that Core Data generates add the following:
// Customize this code block to include application-specific recovery steps.
if (error) {
// Do something here to add queue item in AppController
[[NSNotificationCenter defaultCenter] postNotificationName:#"TerminateApplicationFromQueue" object:self];
return NSTerminateLater;
}
Then in AppController add an observer for the notification (I added this to awakeFromNib):
- (void)awakeFromNib {
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:#selector(terminateApplicationFromQueue:) name:#"TerminateApplicationFromQueue" object:nil];
// Set initial state of struct that dispatch_queue checks to see if it should terminate the application.
appTerminating.isAppTerminating = NO;
appTerminating.isTerminatingNow = NO;
}
I have also created a struct that can be checked against to see if the user wants to terminate the application. (I set the initial state of the struct in awakeFromNib above). Place the struct after your #synthesize statements:
struct {
bool isAppTerminating;
bool isTerminatingNow;
} appTerminating;
Now for the long-running dispatch_queue that is preventing the app from gracefully terminating. When I initially create this dispatch_queue, a for loop is used to add the items that need updating. After this for loop is executed, I have tacked on another queue item that will check the struct to see if the app should terminate:
// Additional queue item block to check if app should terminate and then update struct to terminate if required.
dispatch_group_async(refreshGroup, trackingQueue, ^{
NSLog(#"check if app should terminate");
if (appTerminating.isAppTerminating) {
NSLog(#"app is terminating");
appTerminating.isTerminatingNow = YES;
}
});
dispatch_release(refreshGroup);
And the method to be called when the notification is received:
- (void)terminateApplicationFromQueue:(NSNotification *)notification {
// Struct to check against at end of dispatch_queue to see if it should shutdown.
if (!appTerminating.isAppTerminating) {
appTerminating.isAppTerminating = YES;
dispatch_queue_t terminateQueue = dispatch_queue_create("com.example.appname.terminate", DISPATCH_QUEUE_SERIAL); // or NULL
dispatch_group_t terminateGroup = dispatch_group_create();
dispatch_group_async(terminateGroup, terminateQueue, ^{
NSLog(#"termination queued until after operation is complete");
while (!appTerminating.isTerminatingNow) {
// add a little delay before checking termination status again
[NSThread sleepForTimeInterval:0.5];
}
NSLog(#"terminate now");
[NSApp replyToApplicationShouldTerminate:YES];
});
dispatch_release(terminateGroup);
}
}
I haven't dealt with this myself, but just from my reading of the docs, it looks like what you should do is:
Return NSTerminateLater from applicationShouldTerminate:. This lets the system know that your app isn't ready to terminate just yet, but will do so shortly.
Enqueue a "final" block on your dispatch queue. (You need to make sure that other blocks are not enqueued after this. This block will then be run after all the other work has been performed. Note the queue must be serial -- not one of the concurrent queues) for this to work correctly.) The "final" block should do [NSApp replyToApplicationShouldTerminate:YES];, which will complete the normal termination process.
There isn't any direct way to find out whether a GCD queue is still working. The only other thing that you can do (that I know of) to handle this is to put all of the blocks into a dispatch group, and then wait on the group in applicationShouldTerminate: (using dispatch_group_wait().