gui
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
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:
|
||||
if self.running:
|
||||
raise RuntimeError("Un processus est déjà en cours")
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._log_file = log_path.open("wb")
|
||||
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 n’attend 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)
|
||||
Reference in New Issue
Block a user