Note: further down in the edits there\'s simple code that generates the problem without the full complexity of my original program.
I\'m trying to code an alarm-cloc
Yes, it's possible (I've done it).
I tried a few different ways, and I was not able to get my daemon/NSTimer
to fail in the way you're describing. However, I haven't seen all the files/code that defines your app, so there's at least one more thing I'm concerned about.
If you look in the Apple docs for NSRunLoop run:
If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.
Manually removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit. OS X can install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting.
In the code you show for you daemon's main
program, you don't (directly) create any timers. Of course, I don't know what you do in [[AMMQRDaemonManager alloc] init]
, so maybe I'm wrong. You then use:
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop run];
to start the run loop. The problem is, if there are no timers at this point, I'm not sure your daemon is going to stay alive. If you look at the second paragraph above, it also indicates that it might stay alive, so maybe that's why I don't see my daemon die when I attempt to use your code.
Your comment says that you see the daemon process alive, when the alarm should go off. However, I'm wondering if maybe your daemon process did die, and then was restarted. Maybe you could also show us the .plist file you use for your Launch Daemon (that goes in /System/Library/LaunchDaemons
).
One quick experiment, might be to not start your daemon automatically. Just uninstall the plist file from the LaunchDaemons
folder, and make sure you kill the process. Then, start it manually from the command line, ssh'd into the phone:
$ /Applications/MyApp.app/MyDaemon
then, watch the command line. You'll see if it dies or not, and since it's not actually being run by launchd
, it won't get restarted if it does die.
If it turns out that you do have problems with it dying, then I would try adding a timer that always starts when you daemon does. If you look at my other example, or Chris Alvares' daemon tutorial, it shows this. In the daemon main()
, you set one NSTimer
to fire a run:
method. In that run:
method, you could use a while
loop and a sleep()
call. Or just schedule the timer to repeat at some slow interval.
I'm also not sure how your entire app works. Is it only a tool for scheduling (NSTimer
) alarms? If so, it's possible that at any time, there might be no alarms set. Maybe another solution, instead of using the UIApplication
to notify_post()
to communicate a new timer to the daemon, you could configure the daemon to simply watch a data file. The UIApplication
would write out the data file, whenever there is a new timer. Then, iOS could wake your daemon to schedule the NSTimer
.
Anyway, this may be a separate issue from your original problem, but it also might be a more efficient way to build an alarm clock daemon, since it doesn't really need to run if there's no alarms active.
Post more if these ideas don't help you fix it (the body of [AMMQRDaemonManager init]
might help).
Two more suggestions:
make sure your app (daemon and UI) are installed in /Applications
. This is the normal location for jailbreak apps, but I just wanted to make sure you weren't installing it in the sandbox area.
try replacing your NSTimer
implementation (for the alarms, you can leave the main()
daemon keepalive timer as is) with GCD blocks:
// you have used notify_post() to tell the daemon to schedule a new alarm:
double delayInSeconds = 1000.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// put timer expiration code here
});
I also noticed that in your original alarm:
callback, you use CFUserNotificationReceiveResponse()
with an infinite timeout. That means that if the user doesn't dismiss the popup, the timer callback won't complete, and I believe that means that no subsequently scheduled timer callbacks can fire. Probably, you should put all the CFUserNotification
code into its own method (e.g. showPopup
), and then have your timer callback like so:
- (void)soundAlarm:(NSTimer *)theTimer {
dispatch_async(dispatch_get_main_queue(), ^(void) {
[self showPopup];
});
}
Then, there's the main program (in the code you put on Dropbox). I would recommend changing your main timer (that you call directly from main()
) to be a repeating timer, with a relatively small interval, instead of using a fire date with distantFuture
. If you want, you can do nothing in it. It's just a heartbeat.
main.m:
NSTimer *singleTimer = [[NSTimer alloc] initWithFireDate:[NSDate date]
interval:5*60 // 5 minutes
target:obj
selector:@selector(heartbeat:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:singleTimer
forMode:NSRunLoopCommonModes];
MyClass.m:
- (void)heartbeat:(NSTimer *)theTimer {
NSLog(@"daemon-timer-test: heartbeat timer fired");
}
My last comment is that I don't use syslogd
. I'm wondering if any of your tests are failing, not because timers aren't running, but because NSLog
statements aren't showing up in your log file. I've done all tests where I actually run the daemon executable at the command line, ssh'd into the phone, and I just watch the console for NSLog
output. Take logging out of the list of possible failure points ...
I've worked out a method that works for me. As per my long exchange with Nate (and I definitely wouldn't have been able to work out what was going on without his help), this seems to happen automatically on some systems, but not on others. The problem on my phone seemed to be that powerd
was putting the phone into some sort of deep sleep that paused the NSTimer
s and didn't allow them to fire properly.
Rather than disabling deep sleep (which I suspect has negative power implications) I scheduled a power event:
NSDate *wakeTime = [[NSDate date] dateByAddingTimeInterval:(delayInSeconds - 10)];
int reply = IOPMSchedulePowerEvent((CFDateRef)wakeTime, CFSTR("com.amm.daemontimertest"), CFSTR(kIOPMAutoWake));
This successfully wakes the phone 10 seconds before the alarm is supposed to go off. (The interval isn't precise. I wanted it to be short enough that the phone wouldn't go back to sleep, but long enough that if the phone takes a moment to wake up the timer can still go at the right time. I'll probably shorten it to just 3 or 4 seconds.
The remaining problem is that the NSTimer
for the alarm itself won't update automatically, and so it'll be late by whatever period the phone was asleep for. To fix this you can cancel and reschedule the NSTimer whenever the phone wakes up. I did this by registering for a notification that the power management system posts whenever the power state changes:
int status, notifyToken;
status = notify_register_dispatch("com.apple.powermanagement.systempowerstate",
¬ifyToken,
dispatch_get_main_queue(), ^(int t) {
// do stuff to cancel currently running timer and schedule a new one here
});
The inefficiency here is that the notification is posted both on sleeps and wakes, but I haven't been able to find an alternative yet.
I hope this is helpful to anyone else who was struggling with this issue.