Files
Copies/copienator_gui/runner.py
T
2026-08-22 17:17:30 +02:00

117 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import os
import queue
import signal
import subprocess
import threading
from pathlib import Path
from typing import Any
class ProcessRunner:
"""Run one subprocess and expose thread-safe events for the Tk loop."""
def __init__(self) -> None:
self.events: queue.Queue[tuple[str, Any]] = queue.Queue()
self.process: subprocess.Popen[bytes] | None = None
self._interrupted = False
self._log_file = None
@property
def running(self) -> bool:
return self.process is not None and self.process.poll() is None
def start(
self,
command: list[str],
cwd: Path,
environment: dict[str, str],
log_path: Path | None,
) -> None:
if self.running:
raise RuntimeError("Un processus est déjà en cours")
if log_path is not None:
log_path.parent.mkdir(parents=True, exist_ok=True)
self._log_file = log_path.open("wb")
else:
self._log_file = None
self._interrupted = False
kwargs: dict[str, Any] = {}
if os.name == "nt":
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
else:
kwargs["start_new_session"] = True
self.process = subprocess.Popen(
command,
cwd=cwd,
env=environment,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=0,
**kwargs,
)
thread = threading.Thread(target=self._read_process, daemon=True)
thread.start()
def _read_process(self) -> None:
process = self.process
if process is None or process.stdout is None:
return
return_code = -1
try:
while True:
chunk = process.stdout.read(4096)
if not chunk:
break
if self._log_file:
self._log_file.write(chunk)
self._log_file.flush()
self.events.put(("output", chunk.decode("utf-8", errors="replace")))
return_code = process.wait()
except (OSError, ValueError) as exc: # pragma: no cover - defensive reporting
self.events.put(("runner_error", str(exc)))
return_code = process.wait()
finally:
if self._log_file:
self._log_file.close()
self._log_file = None
process.stdout.close()
if process.stdin:
process.stdin.close()
self.events.put(("finished", (return_code, self._interrupted)))
def send_input(self, text: str) -> None:
if not self.running or not self.process or not self.process.stdin:
raise RuntimeError("Aucun processus nattend de saisie")
data = (text + "\n").encode("utf-8")
self.process.stdin.write(data)
self.process.stdin.flush()
self.events.put(("input_echo", text + "\n"))
def interrupt(self) -> None:
if not self.running or not self.process:
return
self._interrupted = True
if os.name == "nt":
self.process.send_signal(signal.CTRL_BREAK_EVENT)
else:
os.killpg(self.process.pid, signal.SIGINT)
def force_stop(self) -> None:
if not self.running or not self.process:
return
self._interrupted = True
if os.name == "nt":
subprocess.run(
["taskkill", "/PID", str(self.process.pid), "/T", "/F"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
else:
os.killpg(self.process.pid, signal.SIGKILL)