0

I'm trying to create a Python script that can do two things:

  • Run normally if no args are present.
  • If install is passed as argument, install itself to a specific directory (/tmp), and run the installed version in the background, detached from the current process.

I've tried multiple combinations of subprocess.run, subprocess.Popen with the shell, close_fds and other options (even tried nohup), but since I do not have a good understanding of how process spawning works, I do not seem to be using the correct one.

What I'm looking for when I use the install argument is to see "Installing..." and that's it, the new process should be running in the background detached and my shell ready. But what I see is the child process still attached and my terminal busy outputting "Running..." just after the installing message.

How should this be done?

import subprocess
import sys
import time
import os
    
def installAndRun():
    print('Installing...')
    scriptPath = os.path.realpath(__file__)
    scriptName = (__file__.split('/')[-1] if '/' in __file__ else __file__)
    
    # Copy script to new location (installation)
    subprocess.run(['cp', scriptPath, '/tmp'])
    
    # Now run the installed script
    subprocess.run(['python3', f'/tmp/{scriptName}'])
    
    
def run():
    for _ in range(5):
        print('Running...')
        time.sleep(1)

if __name__=="__main__":
    if 'install' in sys.argv:
        installAndRun()
    else:
        run()

Edit: I've just realised that the process does not end when called like that.

dvilela
  • 1,200
  • 12
  • 29
  • 1
    Popen() should create a process independent of the parent. Can you confirm that the script is being copied and executed? Python environmental variables are likely different than bash. It's possible that it's unable to run `cp` and `python3` due to pathing issues. Are you able to print stderr and stdout for your commands? – Ghoti Jul 02 '21 at 19:17
  • I've just checked that the copy is correct and that the installed script is running from the installed location (printing _ _file_ _). – dvilela Jul 02 '21 at 19:32
  • 1
    `subprocess.run` waits for the spawned process to terminate, as the documentation says. I gather you don't want that, so you'll have to use `Popen`. You should start the new process in a new session; that's one of Popen's options. Never use `shell=True` (and anyway, it does nothing useful here)... – rici Jul 02 '21 at 20:11
  • 1
    This might be helpful? https://stackoverflow.com/questions/27624850/launch-a-completely-independent-process – Ghoti Jul 02 '21 at 20:19
  • Yes, I've been trying with Popen also with no luck. – dvilela Jul 02 '21 at 21:27
  • If you want help with Popen, you need to provide something more specific than "I've been trying with Popen also". The only sensible response to that is "You did something wrong." To create a more useful dialogue, you should start by describing exactly what you did which you thought would work. Since this question is basically unanswerable, I don't think there is anything wrong with editing it, although normally I would suggest asking a new question. (You shouldn't change questions which have been answered. But this one hasn't been.) – rici Jul 02 '21 at 23:18
  • Actually, I had just found the answer trying different Popen options and yes, that was exactly it @MauriceMeyer – dvilela Jul 03 '21 at 08:52

2 Answers2

1
  1. Do not use "cp" to copy the script, but shutil.copy() instead.

  2. Instead of "python3", use sys.executable to start the script with the same interpreter the original is started with.

  3. subprocess.Popen() without anything else will work as long as the child process isn't writing anything to stdout and stderr, and isn't requesting any output. In general, the process is not started unless communicate() is not called or PIPEs being read/written to. You have to use os.fork() to detach from the parent (research how daemons are made), then use:


p = subprocess.Popen([sys.executable, new_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
p.stdin.close() # If you do not need it
p.communicate()

or do not use subprocess.PIPE for stdin, stderr and stdout and make sure that the terminal is bound to the child when forking. After os.fork() you do with the parent what you want and with the child what you want. You can bind the child to whatever terminal you want or start a new shell e.g.:

pid = os.fork()
if pid==0: # Code in this if block is the child
    <code to change the terminal and appropriately point sys.stdout, sys.stderr and sys.stdin>
    subprocess.Popen([os.getenv("SHELL"), "-c", sys.executable, new_path]).communicate()
  1. Note that you can point PIPEs to file-like objects using stdin, stderr and stdout arguments if you need.

  2. To detach on Windows you can use os.startfile() or use subprocess.Popen(...).communicate() in a thread. If you then sys.exit() the parent, the child should stay opened. (that is how it worked on Windows XP with Python 2.x, I didn't try with Py3 nor on newer Win versions)

Dalen
  • 4,128
  • 1
  • 17
  • 35
0

It seems like the correct combination was to use Popen + subprocess.PIPE for both stdout and stderr. The code now looks like this:

import subprocess
import sys
import time
import os

def installAndRun(scriptPath, scriptName):
    print('Installing...')

    # Copy script to new location (installation)
    subprocess.run(['cp', scriptPath, '/tmp'])

    # Now run the installed script
    subprocess.Popen(['python3', f'/tmp/{scriptName}'], 
                     stdout=subprocess.PIPE, stderr=subprocess.PIPE)

def run(scriptPath):
    for _ in range(5):
        print(f'Running... {scriptPath}')
        time.sleep(1)


if __name__=="__main__":

    scriptPath = os.path.realpath(__file__)
    scriptName = (__file__.split('/')[-1] if '/' in __file__ else __file__)

    if 'install' in sys.argv:
        installAndRun(scriptPath, scriptName)
    else:
        run(scriptPath)
dvilela
  • 1,200
  • 12
  • 29