1

My ultimate goal is to let my python script launch a child process, in another terminal, that runs independently from the parent:

  • When the parent process finishes, the child process should continue. It should not be killed.

  • The parent process should be able to either wait for the child to finish, or just continue its own stuff.

I achieved the goal with the script below. Please note that the child process I chose to launch is python3 child.py, but it can be anything else (and doesn't have to be a python process). Also note that, in this case, I let the parent process wait for the child process by adding the .communicate() in the end.

# Script parent.py
# ================
import subprocess, shutil

if __name__ == '__main__':
    print('Launch child process')
    p = subprocess.Popen(
        ['x-terminal-emulator', '-e', 'python3', 'child.py'],
    ).communicate()
    print('Child process finished')
    exit(0)

I then launch the parent:

$ python3 parent.py

It works great. The parent first prints 'Launch child process', then the child process appears in another terminal. The child does its things, then closes. After the child closes, the parent prints 'Child process finished'. Great!

Unfortunately, The x-terminal-emulator is only present on Debian and derivatives. My goal is to make this approach work on most Linux systems.

The most sensible approach I could find so far, would be to try out a few default 'terminal emulators' that are present on the most common Linux distros. For example:

  • 'gnome-terminal'
  • 'konsole'
  • 'xterm'
  • 'urxvt'
  • 'rxvt'
  • 'termit'
  • 'terminator'
  • $TERMINAL (this is a non-standard variable)

I tried with 'gnome-terminal' to get started, but it already goes wrong:

# Script parent.py
# ================
import subprocess, shutil

if __name__ == '__main__':
    print('Launch child process')
    # For the 'gnome-terminal', the '-e' argument is deprecated.
    # One should use the '--' argument instead.
    p = subprocess.Popen(
        ['gnome-terminal', '--', 'python3', 'child.py'],
    ).communicate()
    print('Child process finished')
    exit(0)

The child process launches, but the parent doesn't wait for it to finish - even though I instructed it explicitely to do so by adding the .communicate() at the end of Popen(..).

In other words, the parent prints 'Child process finished' immediately, before the child has actually finished.


Question 1:
Why is this approach working for 'x-terminal-emulator' and not for 'gnome-terminal'? PS: I'm working on Ubuntu.

EDIT: I found the solution to this problem. One has to add the --wait flag to tell the gnome-terminal not to return until its child process exits.

Question 2:
How can I get this approach working on most Linux systems? If I try to launch any other terminal emulator like xterm, konsole, ... should I expect similar problems? I can't test right now, because I only have Ubuntu for the moment.

K.Mulier
  • 8,069
  • 15
  • 79
  • 141

1 Answers1

1

I believe I have a reasonably good approach now. I list the most common terminal emulators in Linux, and loop over them to see which one is present on the system. Then I launch the child process in that terminal.

There are a few subtle differences between the terminals. I've installed some on my Ubuntu to figure out. This is what I have so far:

# parent.py
# =========
import sys, subprocess, shutil, shlex
from typing import *

def __get_terminal() -> Tuple[str, str]:
    '''
    Find a terminal present on this system. Return it as a tuple (terminal_name, terminal_path).
    For example: ('gnome-terminal', '/usr/bin/gnome-terminal').
    '''
    terminal_list = [
        'gnome-terminal', 'x-terminal-emulator', 'xterm', 'konsole', 'xfce4-terminal', 'qterminal',
        'lxterminal', 'alacritty', 'terminator',
    ]
    terminal_name = None
    terminal_path = None
    for terminal in terminal_list:
        if shutil.which(terminal):
            terminal_name = terminal
            terminal_path = shutil.which(terminal)
            break
        continue
    else:
        raise RuntimeError('No terminal found!')
    return terminal_name, terminal_path


def __launch_in_terminal(program:str, argv:List[str]) -> None:
    '''
    Launch the given program in a terminal and pass it the arguments in argv.
    '''
    terminal_name, terminal_path = __get_terminal()
    print(f'terminal_name = {terminal_name}')
    print(f'terminal_path = {terminal_path}')
    print(f'__launch_in_terminal({program}, {argv})')

    # The 'gnome-terminal' requires a '--wait' argument to let it not return until its child process
    # has completed. Also, this terminal needs the '--' argument instead of '-e', which is depre-
    # cated.
    if terminal_name == 'gnome-terminal':
        p = subprocess.Popen(
            [terminal_path, '--wait', '--', program, *argv],
        )
        p.wait()

    # The 'xfce4-terminal' and 'terminator' terminal emulators don't work if you pass the program
    # and arguments as separate list elements. So you need to join them with shlex.
    elif terminal_name in ('xfce4-terminal', 'terminator'):
        p = subprocess.Popen(
            [terminal_path, '-e', shlex.join([program, *argv])],
        )
        p.wait()

    # For all other terminal emulators, the approach is the same.
    else:
        p = subprocess.Popen(
            [terminal_path, '-e', program, *argv],
        )
        p.wait()
    return

if __name__ == '__main__':
    print('Launch child process')
    # Launch the child process in another terminal, and pass it the arguments that were given to
    # this parent process. To make a demonstration, we choose the child process to be a python
    # interpreter running the 'child.py' script.
    __launch_in_terminal(
        program = 'python3',
        argv    = ['child.py', *sys.argv[1:]],
    )
    print('Child process finished')
    exit(0)

Please let me know if I forgot one of the "big" ones, or if I made any mistakes in launching some of these terminals.


If you want to test this script, just copy-paste it into a parent.py file. You'll also need a child.py file in the same folder. That child.py file can be anything. Feel free to take this sample code:

# child.py
# ========
from typing import *
import sys, os, time, argparse

if __name__ == '__main__':
    print('Start child process')
    print('===================')
    input('Press any key to continue...')
    print('')

    # Is this frozen?
    time.sleep(0.3)
    print('Frozen:'.ljust(20), end='')
    print(getattr(sys, "frozen", False))

    # Print the executable running this script. It will be the Python interpreter in general, or
    # the frozen executable if running from compiled code.
    time.sleep(0.3)
    print('sys.executable:'.ljust(20), end='')
    print(sys.executable.replace('\\', '/'))

    # Print the value of __file__
    time.sleep(0.3)
    print('__file__:'.ljust(20), end='')
    print(__file__.replace('\\', '/'))

    # Print the location of this file. It should be equal to __file__ if the Python interpreter is
    # running this script. If this is running as compiled code, the location of this file differs
    # from the value of __file__.
    file_location:Optional[str] = None
    if getattr(sys, 'frozen', False):
        # Frozen, running as compiled code
        file_location = os.path.realpath(sys.executable).replace('\\', '/')
    else:
        # Running from interpreter
        file_location = os.path.realpath(__file__).replace('\\', '/')
    time.sleep(0.3)
    print('File location:'.ljust(20), end='')
    print(file_location)

    # Print the arguments given to this program
    parser = argparse.ArgumentParser(description='Simple program', add_help=False)
    parser.add_argument('-h', '--help', action='store_true')
    parser.add_argument('-f', '--foo',  action='store_true')
    parser.add_argument('-b', '--bar',  action='store')
    args = parser.parse_args()
    print('Arguments:')
    time.sleep(0.3)
    print(f'    --help = {args.help}')
    time.sleep(0.3)
    print(f'    --foo  = {args.foo}')
    time.sleep(0.3)
    print(f'    --bar  = {args.bar}')
    time.sleep(0.3)
    print(f'    sys.argv = {sys.argv}')

    # Exit
    time.sleep(0.3)
    print('')
    input('Press any key to continue...')
    sys.exit(0)

Then run the parent like so:

$ python3 parent.py --foo --bar=baz
K.Mulier
  • 8,069
  • 15
  • 79
  • 141