3

How can I schedule some code to be executed at a certain time later?

I tried:

import time
import datetime

time.sleep(420)
print(datetime.datetime.now())

But this doesn't work if the Mac goes to sleep.

To clarify, I need a script (well, some python function, which I could put into a separate script) to run at a precise time in the future.

time.sleep doesn't meet my needs because if the computer sleeps during time.sleep's timeout then the real-time delay is much longer than the one passed to time.sleep. E.g., I start a 7 minute delay at 12:00 with time.sleep. Then I close my macbook lid. Open it at 12:07, but the timeout hasn't finished yet. In fact, I have to wait until about 12:13 for the timeout to finish, even though originally I wanted the rest of my script to continue at 12:07.

So, I don't need it to run while the computer sleeps, but rather, any sleeping the computer does should not affect the time that it does run.

theonlygusti
  • 11,032
  • 11
  • 64
  • 119
  • check `sched` module – Nizam Mohamed Dec 31 '16 at 19:16
  • @NizamMohamed from what I can tell, `sched` uses `time.sleep` as its delay function, so would suffer the same problem my current solution is subject to. – theonlygusti Dec 31 '16 at 19:20
  • cant use `cron` ? – tihom Dec 31 '16 at 19:29
  • @tihom I am allowed to use it. I don't know how though, least of all how to use it from a python script. – theonlygusti Dec 31 '16 at 19:29
  • nvm cron also does not run if mac sleeps – tihom Dec 31 '16 at 19:32
  • didn't my solution work for you? – Nizam Mohamed Dec 31 '16 at 19:35
  • Maybe [this](http://stackoverflow.com/questions/14215960/prevent-os-x-from-going-to-sleep-with-python) would help. Running a script while the computer is sleeping is tricky but you can force it not to sleep. – tihom Dec 31 '16 at 19:41
  • @tihom it's not that I want it to run while the computer is sleeping, I want it to run _at a certain time while the computer is awake_ `time.sleep` is inaccurate if the computer happens to sleep at any moment during its "waiting" – theonlygusti Dec 31 '16 at 19:42
  • Just clarify, do you want your code to interact at all with anything outside of your code itself? There are some great answers here, but most are coming from an assumption that you want to do stuff _outside_ your own program. Is that the case, or do you just want your code to do something, then wait until a certain amount of time has passed and continue? Making that clear would help a lot here. – Chris Larson Jan 01 '17 at 09:31
  • @ChrisLarson my program needs to wait until a certain time, then it starts a thread that plays an alarm noise, and on the main thread it makes a system call to a program that shows an alert. I am writing an alarm clock. – theonlygusti Jan 01 '17 at 09:33
  • Ah! That makes things a lot more clear. I downvoted the cron answer because it answers a _different_ question (really well) based on the phrase "...I need a script (well, some python function, which I could put into a separate script). `cron/launchd` is a useful and powerful tool, but is completely independent of python and as I understood your goals, didn't actually help. If you want everything to be done within your code, including timing, it's off-topic. Well-written, but off-topic. I downvoted the atrun answer for the same reason. – Chris Larson Jan 01 '17 at 09:51
  • @ChrisLarson neither of those answers deserve downvoting. – theonlygusti Jan 01 '17 at 09:53
  • @NizamMohamed's answer is a very good one in the scope defined in the current phrasing of your Question. In fact, I wasn't aware of `sched`, and am now playing with it myself. Thank you, Nizam, for the headsup on that! I think my own answer is on-target, as wel, but then I would, wouldn't I? :) – Chris Larson Jan 01 '17 at 09:54
  • @theonlygusti Your comment led me to read the purpose of `Downvoting`. I see I've completely misunderstood its intent, and you're right. If I could reverse the cron downvote, I would. the `atrun` answer, though, I stand by. Apple disable `at` deliberately because it is a security vulnerability. The SO downvoting help page states: "Use your downvotes whenever you encounter an egregiously sloppy, no-effort-expended post, or an answer that is clearly and perhaps dangerously incorrect." That answer was, and is, both. Link-only, then man page quote, and calling you to enable a security risk. – Chris Larson Jan 01 '17 at 10:07
  • I was under the misapprehension that Downvoting was also intended for Answers that weren't relevant or that were overly complicated, so, yeah, I apologize to bouth you and @l'L'l for that one. And thanks for pointing out I might have misused that one. – Chris Larson Jan 01 '17 at 10:10
  • @theonlygusti I'm still getting my head around SO, so I appreciate being called out if I do something wrong. Thanks. – Chris Larson Jan 01 '17 at 10:17

4 Answers4

2

Your best option is to use cron or Apple's launchd. Since you want whatever it is to be executed at set intervals without a delay after waking up from sleep this is what I recommend.

Cron Method

To setup a new cron job you would open up Terminal and edit it with the time information and script you are wanting to execute (eg. every 7 minutes):

$ crontab -e

*/7 * * * * /usr/bin/python /path/to/myscript.py

Here's a quick breakdown of the meaning:

* * * * *  command to execute
│ │ │ │ │
│ │ │ │ └─── day of week (0 - 6) (0 to 6 are Sunday to Saturday, or use names; 7 is Sunday, the same as 0)
│ │ │ └──────── month (1 - 12)
│ │ └───────────── day of month (1 - 31)
│ └────────────────── hour (0 - 23)
└─────────────────────── min (0 - 59)

To list jobs you have set in your crontab:

$ crontab -l

Timed Jobs Using launchd

Apple's recommendation is not to use crontab, rather launchd. Basically this entails creating a preference list with the information about your task and what time to run it, etc.

$ cd $HOME/Library/LaunchAgents
$ nano com.username.mytask.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.username.mytask</string>
    <key>ProgramArguments</key>
    <array>
        <string>/path/to/myscript.sh</string>
    </array>
    <key>StartInterval</key>
    <integer>7</integer>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

In nano press Control + O followed by Control + X to save.

$ chmod +x /path/to/myscript.sh
$ launchctl load com.username.mytask.plist
$ launchctl start com.username.mytask.plist

The following would make your script executable, and then load and start the launch agent.

To stop and unload:

$ launchctl stop com.username.mytask.plist
$ launchctl unload com.username.mytask.plist

More information:

Scheduling Timed JobsCreating a launchd Property List File

Effects of Sleeping and Powering Off

If the system is turned off or asleep, cron jobs do not execute; they will not run until the next designated time occurs.

If you schedule a launchd job by setting the StartCalendarInterval key and the computer is asleep when the job should have run, your job will run when the computer wakes up. However, if the machine is off when the job should have run, the job does not execute until the next designated time occurs.

All other launchd jobs are skipped when the computer is turned off or asleep; they will not run until the next designated time occurs.

Consequently, if the computer is always off at the job’s scheduled time, both cron jobs and launchd jobs never run. For example, if you always turn your computer off at night, a job scheduled to run at 1 A.M. will never be run.

l'L'l
  • 44,951
  • 10
  • 95
  • 146
  • I don't want it to execute every 7 minutes, just one-time. – theonlygusti Jan 01 '17 at 01:58
  • @theonlygusti: Both cron and launchd can be set to run something at certain time, please read the last part of my answer. – l'L'l Jan 01 '17 at 01:59
  • How do I use these from python though? – theonlygusti Jan 01 '17 at 02:00
  • You would call python from either of them... I show how in my examples. In the case of `launchd` substitute `/path/to/myscript.sh` with `/path/to/yourscript.py`. The only way you are going to be able to "resume" something because your system has gone to sleep is to use some on-wake tool, as that's seemingly what you are describing; there really isn't anything (that I'm aware of) which is built-in. – l'L'l Jan 01 '17 at 02:07
  • This is _way_ overthinking the problem. See my answer for a much simpler approach that I believe gives you exactly what you want. With the added benefit of being cross-platform. – Chris Larson Jan 01 '17 at 08:33
  • I don't want to call python from them, I want to call them from python. – theonlygusti Jan 01 '17 at 08:58
  • 1
    I upvoted to cancel the downvote, as this is a very detailed and useful answer. – theonlygusti Jan 01 '17 at 09:01
  • @l'L'l If you make an edit to your Answer, I'd love to reverse my downvote, with apologies. I'd do it now, but SO won't let me unless it is edited. – Chris Larson Jan 01 '17 at 10:14
  • @ChrisLarson: There's no reason for me to edit the answer; I could care less about rep, although seeing as how your code has mistakes (eg. `print(wait_string, end = "\r"),` is assigned to nothing, and changing `wait_time=5` never gets to the "done waiting at...") I would recommend against using such tactics as you've displayed here, however, I will accept your apology. – l'L'l Jan 01 '17 at 10:27
  • @l'L'l I was not using any tactics. I completely and honestly believed that downvotes were intended in part for Answers that weren't directly addressing the Question, which, as I read it, was asking specifically for a python-only answer. theonlygusti pointed out that my vote might be inappropriate, and being new as an active user here, I immediately went to the SO Help Center and saw that he was correct and that I had misunderstood their intent. Thus my apology to you both. – Chris Larson Jan 01 '17 at 11:14
  • I'd like to reverse my vote because I now know it _was_ incorrect, but can't do so unless you edit your answer. It doesn't have to be an edit of any substance. Just so that it has been edited. Perhaps change `In nano press` to `In nano, press`. I'm sincere here, and see that my downvote for your Answer wasn't correct, and that `comments` are where I should have responded rather than a downvote. See my comments to theonlygusti at the bottom of his answer thread. – Chris Larson Jan 01 '17 at 11:17
  • Regarding my code, thank you for pointing out the loop error. I had overwritten the necessary `seconds=` when I pasted the variable `wait_time` into `timedelta` and didn't catch it. Regarding `print(wait_string, end = "\r"),` however, that line is valid, though now redundant. The trailing comma syntax in python 2.7 suppresses newlines, which is now handled in >=python 3.0 by the `end=` syntax, but it is still accepted by the interpreter and has no impact on execution. I leave it so replacing `, end = ` with ` + ` is all that is necessary to make this code run in python 2 variants. – Chris Larson Jan 01 '17 at 11:31
  • @l'L'l You can test it for yourself in the updated code from my Answer by replacing the line ` print(wait_string, end = "\r"),` with ` print(wait_string + "\r"),` and running the code in a python less than 3.0. Remove the comma in a 2 series python and you'll see the line no longer overwrites itself, but prints line after line after line of `Zzzzzzzzzzz` instead of just rewriting the same line in place. Put the trailing comma back and it will revert to the original behavior. – Chris Larson Jan 01 '17 at 11:39
  • @l'L'l And for what it's worth, I didn't downvote your answer based on it's quality. I thought it was extremely well presented, and have made a copy of it for my own reference. The best, most concise and most readable presentation of `launchd` and `cron` I've seen. Thank you for that. As I said, my downvote was entirely based on the fact that the Question specifies "To clarify, I need a script (well, some python function, which I could put into a separate script) to run at a precise time in the future," your answer seemed off-topic, and I _thought_ that was what downvotes were for. – Chris Larson Jan 01 '17 at 11:46
1

sched module is a generally useful event scheduler. It can schedule events in the future by relative (enter method) or absolute time (enterabs method) with priority.

As time.sleep uses select syscall on Linux which introduces delays we have to define a sleep function based on time.time which doesn't suffer from the unnecessary delay.

from __future__ import print_function
import sched
from datetime import datetime
from time import sleep, time

def my_sleep(n):
    s = time()
    while (time() - s) < n:
        # introduce small delay for optimization
        sleep(0.1)

schedule = sched.scheduler(time, my_sleep)
schedule.enter(2, 0, lambda: print(datetime.now()), ())
schedule.enter(3, 0, lambda: print(datetime.now()), ())
schedule.run()
Nizam Mohamed
  • 8,751
  • 24
  • 32
  • 1
    I upvoted to cancel the downvote. This is a good answer, and the first to demonstrate how we can just wait until the wanted time has come to pass. – theonlygusti Jan 01 '17 at 09:01
1

EDIT
From the man page for "atrun"
Execute the following command as root to enable atrun:

launchctl load -w /System/Library/LaunchDaemons/com.apple.atrun.plist  

then something like:

at now + 5 minutes
echo 'testing the at command' > myfile.txt
<EOD>  

See the man page for "at" for other options

Marichyasana
  • 2,966
  • 1
  • 19
  • 20
  • This answer calls for the user to enable something that Apple has disabled because of a security vulnerability. That could be dangerous. The fact that it is disabled could mean that Apple has yet to take steps to fix the vulnerability, and has chosen instead to disable it. – Chris Larson Jan 01 '17 at 15:14
  • @ChrisLarson There is no mention of a vulnerability in `man atrun`? – HappyFace Sep 06 '21 at 08:35
  • @HappyFace, See, for example, https://github.com/carlospolop/hacktricks/blob/master/macos/macos-security-and-privilege-escalation/README.md, search for `atrun`. – Chris Larson Feb 06 '22 at 04:54
0

Update: User @I'L'I pointed out that there was an error in my code. In copy pasting the wait_time variable, I overwrote a necessary seconds= in timedelta which prevented the loop from ever exiting. This is now fixed.

Your code waits until the program has run for a set number of seconds. You don't actually want that, according to your Question, correct? You actually want to wait until a certain number of seconds have passed from your start time, and continue at the first opportunity after they have passed.

Do this:

from datetime import datetime
from datetime import timedelta

# Establish the start time as a static variable. Basically lock the start time in.
start_time = datetime.now()

# How long should we wait?
wait_time = 420

# What should I say while I wait?
wait_string = "Zzzzzzzzzzz"

# Loop until at least wait_time seconds have passed
while datetime.now() <= start_time + timedelta(seconds=wait_time):
    print(wait_string, end = "\r"),

print("Done waiting at:", datetime.now())
Chris Larson
  • 1,684
  • 1
  • 11
  • 19