Du verstehst, warum eine TUI / GUI / Server-App auf einem einzigen Thread läuft, was ein „Event-Loop" ist, und welche Auswirkung eine einzige blockierende Funktion hat. Du erkennst blockierende Aufrufe in deinem Code, triagest sie nach Intention und wählst den richtigen Pattern für jeden Fall.
Konkret nach Tier 3: Du hast eine eigene Subprocess-Triage-Matrix und einen Plan, wie du existierenden Code daraufhin sweepst.
In iMenu öffnete der Benutzer das Sessions-Overlay. Im Hintergrund wurde alle 2 Sekunden tmux list-sessions aufgerufen. Eines Tages hing ein tmux-Server. Der subprocess.run("tmux list-sessions") blockierte. Das Overlay fror ein. Die ganze App war tot — Tastendrücke verpufften.
Drei subtile Aspekte dieses Bugs:
subprocess.run(["tmux", "list-sessions"]) — ohne timeout=.Ein Audit der SessionBridge fand 18 weitere Stellen mit derselben Eigenschaft.
Eine TUI hat einen einzigen Hauptthread, der in einer Schleife Folgendes tut:
while running:
event = wait_for_next_event() # Tastendruck, Timer, IO
handle(event) # Widgets aktualisieren, etc.
redraw_if_needed()
wait_for_next_event ist die einzige Stelle, an der der Thread „pausieren" darf — überall sonst muss er schnell wieder zurückkehren. Wenn handle(event) 30 Sekunden braucht, ist die App für 30 Sekunden tot.
Das gleiche Muster gilt für:
Wenn du das Pattern in einem dieser Kontexte gelernt hast, gilt es in allen.
subprocess.run(["tmux", "list-sessions"])
# blockiert, bis das Subkommando endet — ohne Begrenzung
time.sleep(5)
# blockiert für 5 Sekunden
requests.get("https://api.example.com/...")
# blockiert, bis die HTTP-Antwort da ist (kann unendlich sein)
with open("/proc/whatever") as f:
data = f.read()
# blockiert, bis das Kernel die Datei liefert
Alle vier sind im Event-Loop-Kontext gefährlich. Die Frage ist nicht „darf ich blockieren?" — sondern „wie begrenze ich die Blockade?"
Wenn dein Code ein Subprocess aufruft, frag erst: Wofür? Drei Hauptfälle, drei Patterns.
Beispiel: tmux list-sessions — du brauchst die Ausgabe, um eine Liste anzuzeigen.
Lösung: subprocess.run mit timeout=.
import subprocess
def list_tmux_sessions() -> list[str]:
try:
result = subprocess.run(
["tmux", "list-sessions", "-F", "#{session_name}"],
capture_output=True,
text=True,
timeout=3, # ← die entscheidende Zeile
)
except subprocess.TimeoutExpired:
return []
if result.returncode != 0:
return []
return result.stdout.splitlines()
Drei Werte gleichzeitig erzwungen:
timeout=TimeoutExpired-BehandlungWas ist ein guter timeout-Wert? Pro Aufruf abwägen: „Wenn das nicht in X Sekunden kommt, ist es kaputt." Für tmux-Befehle: 3 Sekunden. Für git status: 5–10. Für API-Aufrufe: je nach Dienst.
Beispiel: tmux switch-client — der Benutzer wechselt die Session, die TUI wird im selben Moment beendet, niemand wartet auf das Ergebnis.
Lösung: subprocess.Popen ohne .wait().
def switch_to_session(name: str) -> None:
subprocess.Popen(
["tmux", "switch-client", "-t", name],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
# Funktion kehrt sofort zurück — der Kindprozess läuft im Hintergrund
Achtung: Popen ist nicht „macht alles asynchron". Es startet einen Kindprozess. Scheitert dieser, erfährst du es nicht. Wähle den Pattern nur, wenn du das Ergebnis wirklich nicht brauchst.
Beispiel: Der Benutzer drückt e → vim soll im selben Terminal aufgehen → die TUI muss sich erst zurückziehen, sonst kollidieren beide um das Terminal.
Lösung in Textual: with app.suspend():.
def edit_in_external_editor(self, path: str) -> None:
with self.app.suspend():
subprocess.run(["vim", path])
# Die TUI ist während dieses Blocks pausiert
# vim hat das volle Terminal
# Nach dem Block: TUI nimmt das Terminal zurück, redraw automatisch
app.suspend() macht drei Dinge gleichzeitig:
with-Blocks: Terminal wird neu initialisiert, App rendert neu| Frage | Pattern |
|---|---|
| Ich brauche die Ausgabe und kann begrenzt warten | subprocess.run(..., timeout=N) |
| Ich brauche das Ergebnis nicht | subprocess.Popen(...) |
| Der Kindprozess soll mit dem Terminal interagieren | with app.suspend(): |
Vor jedem subprocess.run ohne timeout=: hast du absichtlich Fall A mit unbegrenzter Wartezeit gewählt? Wenn nein — füg timeout= hinzu.
# subprocess.run ohne timeout=
grep -rn "subprocess\.run(" --include="*.py" . | grep -v "timeout="
# synchrone HTTP-Aufrufe in UI-Code
grep -rn "requests\.\(get\|post\|put\|delete\)" --include="*.py" .
# time.sleep
grep -rn "time\.sleep(" --include="*.py" .
Pro Treffer: nach der Matrix triagieren. Pro Fall A ohne timeout=: entweder hinzufügen — oder bewusst dokumentieren, warum unbegrenzt OK ist.
Bisher ging es um Code, der zu lange wartet. Es gibt aber ein zweites Anti-Pattern: Code, der zu eifrig reagiert.
Textual hat reactive Felder und watch_<name>-Hooks:
class MyOverlay(ModalScreen):
count: reactive[int] = reactive(0)
def watch_count(self, old: int, new: int) -> None:
self.refresh_display()
Wenn count 100 Mal pro Sekunde sich ändert, läuft refresh_display 100 Mal pro Sekunde. Auch das ist eine Blockade — nicht durch Warten, sondern durch zu viel Arbeit.
*Frage an jeden `watch_`-Hook:** Ist die Arbeit hier billig genug, dass sie jedem Wert-Wechsel folgen darf? Oder muss ich entprellen?
Wähl ein Stück deines Codes mit Subprocess-Aufrufen oder einer Event-Loop. Erstelle einen kleinen Bericht:
DATEI: SessionBridge.py
SUBPROCESS-AUFRUFE: 18
- 4 × Fall A (mit timeout=3 angepasst)
- 11 × Fall B (Popen, fire-and-forget)
- 3 × Fall C (suspend für tmux attach)
RESTRISIKO: keiner ohne explizite Wahl
Wenn dir auch nur ein Aufruf bleibt, der „nur 99% der Zeit schnell ist" — das ist die Zeitbombe.
run+timeout, Popen, suspend. Pro Aufruf bewusst.subprocess.run ohne timeout=" und hänge ihn als Pre-Commit-Hook ein.fix(SessionBridge): add timeout=3 to 4 blocking tmux callssubprocess.run, subprocess.PopenApp.suspend()