1

I have a simple question. I have tried to search for a solution but there are no answers which would explain what I need.

The question is: How do I start a nohup command from Python? Basically the idea is, that I have a Python script which prepares my environment and I need it to launch multiple scripts with nohup commands. How do I start a nohup command like nohup python3 my_script.py & from within a running Python script to have that nohup command running even after I log out?

Thank you

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
neisor
  • 384
  • 4
  • 15
  • did you check this post? https://stackoverflow.com/questions/37118991/subprocess-gets-killed-even-with-nohup – mjoppich Oct 21 '21 at 15:13
  • In general, `nohup` is useless even in bash -- it does nothing you can't do just with some redirections and potentially the shell `disown` builtin (which has, and needs, no Python equivalent). There's no reason to use it in Python; none whatsoever. – Charles Duffy Oct 21 '21 at 15:21
  • `subprocess.Popen(['python3', 'my_script.py'], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)` -- replace the `subprocess.DEVNULL`s for stdout and stderr with pointers to a file if you want (`nohup`'s own default is `nohup.out`, but you can call it anything you want). – Charles Duffy Oct 21 '21 at 15:23
  • (that said, if your goal is to start a service, the right way to do that is to go through your operating system's init system -- systemd has a concept of transient user services, if that's what you want; whereas if you want to emulate a _system_ service, the advantages of setting it up through systemd are even stronger). – Charles Duffy Oct 21 '21 at 15:25
  • Thank you everyone for your replies. @CharlesDuffy what do you suggest then for executing a Python script, from Python script, to run in the background endlessly? – neisor Oct 21 '21 at 15:26
  • @neisor, I gave you a line of code in my very second comment that is _exactly_ what I suggest, if your requirements are genuine and unavoidable. I'd need to know more context (what your script does, when and by what it's started, when if ever it's supposed to be _re_started, etc etc) to determine if "genuine and unavoidable" is true. – Charles Duffy Oct 21 '21 at 15:28
  • Remember, `SIGHUP` is sent to processes _using a terminal_ when that terminal closes, but when you use `/dev/null 2>&1` (which that `stdin=subprocess.DEVNULL`, `stdout=subprocess.DEVNULL` and `stderr=subprocess.DEVNULL` do), you're then opting out of having the new process you start have any file descriptors connecting it to the terminal. And then when you add `start_new_session=True`, you're taking it out of the process group for the parent, so it no longer gets included when something tries to signal the parent's group. – Charles Duffy Oct 21 '21 at 15:31

1 Answers1

3

You do not need nohup -- not even in shell, and even less so in Python. It does the following things:

  • Configures the HUP signal to be ignored (rarely relevant: if a process has no handles on a TTY it isn't going to be notified when that TTY exits regardless; the shell only propagates signals to children in interactive mode, not when running scripts).
  • If stdout is a terminal, redirects it to nohup.out
  • If stderr is a terminal, redirects it to wherever stdout was already redirected.
  • Redirects stdin to /dev/null

That's it. There's no reason to use nohup to do any of those things; they're all trivial to do without it:

  • </dev/null redirects stdin from /dev/null in shell; stdin=subprocess.DEVNULL does so in Python.
  • >nohup.out redirects stdout to nohup.out in shell; stdout=open('nohup.out', 'w') does so in Python.
  • 2>&1 makes stderr go to the same place as stdout in shell; stderr=subprocess.STDOUT does so in Python.

Because your process isn't attached to the terminal by virtue of the above redirections, it won't implicitly get a HUP when that terminal closes. If you're worried about a signal being sent to the parent's entire process group, however, you can avoid that by splitting off the child into a separate one:

  • The subprocess.Popen argument start_new_session=True splits the child process into a separate group from the parent in Python, so a parent sent to the process group of the parent as a whole will not be received by the child.
  • Adding a preexec_fn with signal.signal(signal.SIGHUP, signal.SIG_IGN) is even more explicit that the child should by default ignore a SIGHUP even if one is received.

Putting this all together might look like (if you really do want logs to go to a file named nohup.out -- I would suggest picking a better name):

import subprocess, signal
subprocess.Popen(['python3', 'my_script.py'],
                 stdin=subprocess.DEVNULL,
                 stdout=open('nohup.out', 'w'),
                 stderr=subprocess.STDOUT,
                 start_new_session=True,
                 preexec_fn=(lambda: signal.signal(signal.SIGHUP, signal.SIG_IGN)))
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Thank you for your extensive explanation. I'm still a bit confused on how to proceed though. Would you, please, add a short example Python code on how to create the equivalent of `nohup` in Python? Thank you very much – neisor Oct 21 '21 at 15:43
  • @neisor Refer the documentation for `subprocess.Popen `. The arguments to provide are listed out in this answer – OneCricketeer Oct 21 '21 at 15:47
  • Thank you very much! I appreciate your extensive explanation and help. – neisor Oct 21 '21 at 16:28
  • @DanNagle, ...why add an explanation of a bug in the code, vs just proposing an edit that fixes that bug? (There's no reasonable interpretation that that was by intent, because the prose explanation above the code _describes_ `signal.SIGHUP` as the correct thing). – Charles Duffy Nov 30 '22 at 14:22
  • @CharlesDuffy My edit was to move the added remark out of the code fragment. – Dan Nagle Nov 30 '22 at 14:30
  • ...funny; that remark's addition is attributed to "Community", so afaict there's no knowing where it originated. – Charles Duffy Nov 30 '22 at 15:25