2025, Dec 17 12:02

Как запустить несколько приложений в Python и корректно завершить скрипт

Разбираем, как в Python запускать несколько программ через subprocess.Popen и корректно ждать их завершения с wait(), избегая ошибок экранирования путей.

Когда вы используете Python как небольшой лончер для запуска нескольких настольных утилит или CLI‑приложений, обычно хочется, чтобы родительский скрипт автоматически завершался, как только все дочерние процессы закрыты. Частая ловушка — чрезмерная обработка строк команд перед запуском: из‑за этого скрипт может «висеть» дольше, чем нужно, или вести себя нестабильно. Ниже — краткое руководство, как запустить несколько программ и завершить скрипт сразу после закрытия каждой из них.

Постановка задачи

Цель проста: запустить из Python четыре программы и остановить скрипт, когда они все будут закрыты. Первоначальный подход ниже пытается вставлять обратные слэши перед пробелами в путях, формирует список команд и ожидает каждый процесс:

import os
import sys
import subprocess
from subprocess import Popen

def inject_backslashes(pathname):
# вставляет обратные слэши в пути, содержащие пробелы
chars = list(pathname)
j = 0

while j < len(chars):
if chars == " ":
chars.insert(j, "\\")
j += 2
else:
j += 1

return "".join(chars)

# укажите полный путь к исполняемому файлу эмулятора:
emu_bin = inject_backslashes("/usr/bin/snes9x-gtk")

# укажите полный путь к вашему трекеру:
tracker_bin = inject_backslashes("/usr/share/OpenTracker/OpenTracker")

# укажите полный путь к Qusb или SNI:
bridge_bin = inject_backslashes("/bin/QUsb2Snes")

# укажите полный путь к вашему таймеру:
timer_bin = inject_backslashes("/home/user/LibreSplit/libresplit")

# запускает эмулятор, сокет, таймер и трекер
cmds = [bridge_bin, timer_bin, tracker_bin, emu_bin]
handles = [Popen(x) for x in cmds]
for h in handles:
h.wait()

Что происходит на самом деле

Сама логика управления жизненным циклом корректна: каждый вызов Popen(...) запускает программу и сразу возвращается, а wait() блокирует выполнение, пока соответствующий процесс не завершится. После закрытия последнего дочернего процесса завершается и скрипт. Узкое место — способ передачи команд. Ручное «экранирование» пробелов в строках команд здесь не нужно и легко допустить ошибку. Достаточно указывать реальные пути к исполняемым файлам (без лишних кавычек и экранирования) — тогда ожидания работают как задумано: закрыли все приложения, и лончер завершился.

Есть и аспект производительности: посимвольную обработку заменили более простым преобразованием, благодаря чему вспомогательная функция работает быстрее, а поведение по обработке пробелов остается прежним.

Решение

Передача реальных путей и упрощение работы с пробелами позволили скрипту корректно завершаться сразу после закрытия всех программ:

import os
import sys
import subprocess
from subprocess import Popen

def escape_space_chars(path_text):
# добавляет обратный слэш перед пробелами
token = path_text.replace(" ", "\\ ")
return token

# запускает эмулятор, сокет, таймер и трекер
jobs = [
escape_space_chars("/usr/bin/snes9x-gtk"),
escape_space_chars("/usr/share/OpenTracker/OpenTracker"),
escape_space_chars("/usr/bin/QUsb2Snes"),
escape_space_chars("/home/user/LibreSplit/libresplit")
]

children = [Popen(item) for item in jobs]
for child in children:
child.wait()

Почему это важно

Управление процессами с subprocess.Popen и wait детерминированно: запускаете процесс, ждете его завершения, и родительский скрипт выходит, когда дети отработали. Проблемы обычно возникают, когда команды готовят как строки с самодельным экранированием. Это привносит лишнюю сложность и скрывает, какое именно имя программы и аргументы получает операционная система.

Есть и аспект безопасности. Собирать командные строки с импровизированным экранированием — рискованно и склонно к ошибкам. Безопаснее передавать в Popen явный список аргументов, чтобы не привлекать оболочку к разбору, а всю дополнительную логику выполнять прямо в Python, не опираясь на возможности shell. Такой подход помогает избежать класса проблем, связанных с внедрением через оболочку.

Выводы

Запускайте каждую программу через Popen и блокируйтесь с помощью wait() до её завершения — этого достаточно, чтобы родительский скрипт вышел, когда всё выполнено. Отдавайте предпочтение реальным путям к исполняемым файлам, а не вручную «экранированным» строкам. Сведите работу со строками к минимуму и, по возможности, передавайте в Popen заранее разбитый список аргументов — так вы полностью обходите подводные камни с экранированием.