2025, Nov 20 06:01

Фоновый запуск Python-логгера через subprocess.Popen и остановка по SIGINT

Показано, как запустить логгер на Python в фоне с subprocess.Popen и корректно остановить его SIGINT (KeyboardInterrupt). Разбираем ошибки с run и нюансы Windows

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

Минимальный пример блокирующего варианта

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

import subprocess
import time

log_stub = "capture_001.xlsx"

# Этот вызов блокирует до завершения дочернего процесса
res = subprocess.run(["python", "logger_child.py", log_stub])

# До кода ниже управление не дойдет, пока работает дочерний процесс
# поэтому здесь нельзя запускать таймеры или отправлять команды ПЛК
 time.sleep(2)

Родительский процесс остаётся заблокированным, пока дочерний не завершится сам, — это не позволяет вовремя отправить сигнал и выстроить нужную последовательность команд ПЛК.

В чём на самом деле причина

Проблема управления возникает из‑за использования синхронного API для порождения дочернего процесса. Когда вы вызываете блокирующую функцию, родитель не может планировать таймеры, общаться с вашим ПЛК или решать, когда завершить логгер. Для этого сценария родитель должен продолжить работу сразу после запуска и иметь живой дескриптор дочернего процесса, чтобы позже послать сигнал.

Фоновый запуск и аккуратная остановка через subprocess

Здесь уместен subprocess с Popen. Он запускает дочерний процесс и сразу возвращает управление родителю, предоставляя дескриптор процесса. Когда будете готовы остановить запись, отправьте дочернему SIGINT. Это эквивалент Ctrl+C и запускает в дочернем скрипте обработчик KeyboardInterrupt — ровно тот механизм, который нужен, чтобы отработала финализация перед закрытием файла.

import subprocess
import signal
import time

output_name = "capture_001.xlsx"
child_file = "logger_child.py"

# Запустите дочерний логгер в фоне и сохраните дескриптор процесса
child_job = subprocess.Popen(["python", child_file, output_name])

# Родитель свободен выполнять задачи с ПЛК, таймерами и т. п.
# ... запускаем процесс, запускаем таймер 1, отправляем команду ПЛК, ждём, отправляем вторую команду ...
# Это лишь условные паузы для иллюстрации последовательности
 time.sleep(5)

# Когда захотите остановить логгер, попросите его корректно завершиться
child_job.send_signal(signal.SIGINT)

# При необходимости дождитесь завершения, чтобы синхронизировать следующий запуск
child_job.wait()

Если дочерний процесс пишет в stdout, настройте Popen на перехват или перенаправление вывода, чтобы не засорять консоль. Если он пишет в файл, потоки можно не трогать.

Особенности Windows, о которых важно помнить

На Windows у сигналов есть ограничения. В частности, отправка CTRL_C_EVENT группе процессов не ведёт себя как реальный прерывающий сигнал с клавиатуры. Это работает ненадёжно; в этой схеме предполагается использовать SIGINT через send_signal. Для контекста и обсуждения обходных путей см.: Sending ^C to Python subprocess objects on Windows: https://stackoverflow.com/questions/7085604/sending-c-to-python-subprocess-objects-on-windows.

Важно и то, чтобы в дочернем процессе был обработчик KeyboardInterrupt. Если он есть, SIGINT запустит путь очистки, а не жёсткую остановку.

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

При автоматизации повторяющихся сборов данных нужны чёткие границы каждого прогона. Асинхронный запуск логгера позволяет родителю точно оркестровать команды ПЛК и таймеры. Завершение логгера через SIGINT сохраняет путь финализации в дочернем процессе, чтобы построение графиков, сводок или сброс буферов проходили стабильно. Это и есть разница между воспроизводимыми прогонами и хрупкими захватами данных.

Практическое резюме

Используйте subprocess.Popen, чтобы запустить дочерний логгер с аргументом-именем файла и сохранить полученный дескриптор. Пока дочерний процесс работает, в родителе управляйте таймерами и командами ПЛК. Когда цикл завершён, отправьте SIGINT, чтобы в дочернем сработал обработчик KeyboardInterrupt и отработали финальные действия. Если следующий прогон должен стартовать только после того, как дочерний допишет файл, дождитесь завершения процесса. На Windows учитывайте ограничения сигналов и обращайтесь к приведённому обсуждению, если столкнётесь с особенностями CTRL_C_EVENT.