pythonpython-3.xmacoslaunchdlaunch-daemon

LaunchDaemon runs different instance of Python to terminal


With a LaunchDaemon having the following 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.test.testing</string>
        <key>ProgramArguments</key>
            <array>
                <string>python3</string>
                <string>/Users/my-name/python-daemon/python_daemon_test.py</string>
            </array>
        <key>StandardErrorPath</key>
            <string>/var/log/test-Error.log</string>
        <key>StandardOutPath</key>
            <string>/var/log/test.log</string>
        <key>RunAtLoad</key>
            <true/>
    </dict>
    </plist>

where the script to be launched is

#!/usr/bin/env python3

import sys
print(sys.version)
print(sys.path)

I am having behaviour that I don't understand.

When I run the script in the terminal (where which python3 results in /Users/my-name/opt/anaconda3/bin/python3), I receive the following output:

3.7.4 (default, Aug 13 2019, 15:17:50)
[Clang 4.0.1 (tags/RELEASE_401/final)]
['/Users/my-name/python-daemon', '/Users/my-name/opt/anaconda3/lib/python37.zip', 
'/Users/my-name/opt/anaconda3/lib/python3.7', 
'/Users/my-name/opt/anaconda3/lib/python3.7/lib-dynload', 
'/Users/my-name/.local/lib/python3.7/site-packages', 
'/Users/my-name/opt/anaconda3/lib/python3.7/site-packages', 
'/Users/my-name/opt/anaconda3/lib/python3.7/site-packages/aeosa']

which is as desired, since the actual (non-toy) script I wish to run uses the PyObjc package which I have installed within Anaconda.

However, when the script is run by the LaunchDaemon, I get the following in the test.log file:

3.7.3 (default, Mar  6 2020, 22:34:30) 
[Clang 11.0.3 (clang-1103.0.32.29)]
['/Users/my-name/python-daemon', 
'/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.7/lib/python37.zip', 
'/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.7/lib/python3.7', 
'/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.7/lib/python3.7/lib-dynload', 
'/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.7/lib/python3.7/site-packages']

This is a problem, seeing as the actual script I wish to run depends on a package within Anaconda. I had expected that since the first program argument in the script was python3 it would run with the same python3 as in the terminal, but I am wrong.

Why is the script behaving this way? I thought it might be because it runs scripts as sudo, but sudo which python3 also returns the path to Anaconda.

How could I resolve this? I am sure a simple solution would be to just install the package I want somewhere within the Xcode version of Python3. However, I'm not actually sure how to do that. It would also feel a bit unsatisfying, as I'd like to know why LaunchDaemons behave this way.


Solution

  • Launch Daemons (and cron jobs and ...) don't run under your usual Terminal/shell environment, so they don't get whatever customization, add-ons, etc are set up in your Terminal/shell environment. In particular, they don't run the various shell initialization scripts (~/.bash_profile, ~/.bashrc, etc) that normally have the commands to set up your environment.

    In the specific case of anaconda, its installer adds a section to your ~/.bashrc file to add the anaconda binaries directory to PATH, and probably a bunch of other changes. This affects your environment your bash sessions in Terminal, but doesn't take effect for Launch Agents, or even Terminal sessions in other shells (zsh is now the default, and it's causing some trouble with this).

    For a Launch Daemon, I'd recommend against making it run your shell setup scripts -- those are your personal config, and don't have any real business being run by a system process. Instead, I'd create a short shell script that includes the relevant setup (copied from your ~/.bashrc), and then runs your python script. Then change the Launch Daemon so it runs that script, rather than running the python script directly.

    P.S. Since the script doesn't have to do anything after starting the python script, this is a case where the script could use the exec command to exit itself, and run the python script instead in the same process. That means that python would be a direct subprocess of launchd, and launchd can monitor, control, etc it in the way that it likes to. (That's as opposed to having the script run python normally, in which case you'd have a now-pointless shell process hanging out waiting for the python script to finish so that it can finish.)

    So I'd end the script with:

    exec python3 /Users/my-name/python-daemon/python_daemon_test.py