Funktioniert die Bereinigung von Remote-Prozessen nach einem SSH-Abbruch?
Die Bereinigung von Remote-Prozessen nach einem SSH-Abbruch braucht Prozessgruppen, dauerhaften Status, Tests mit Kindprozessen und einen ehrlichen Plan für Verbindungsabbrüche.

Ein abgebrochener Agentenlauf ist nicht dasselbe wie ein gestoppter Remote-Befehl. Der lokale Prozess kann sauber beendet werden, während der SSH-Transport noch aktiv ist. Der Transport kann verschwinden, während die Remote-Shell weiterläuft. Und die Shell kann sterben, während ihre Kinder in einer anderen Prozessgruppe weiterarbeiten. Wenn du all diese Ereignisse unter einem Status namens «abgebrochen» zusammenfasst, bleibt irgendwann eine Datenbankmigration, eine Paketinstallation, ein Test-Worker oder ein Deployment-Helfer laufen, obwohl der Agent gemeldet hat, dass er beendet wurde.
Die Lösung ist kein besonders cleverer Signal-Handler. Du brauchst einen Abbruchvertrag, der den Remote-Lauf benennt, eine beendbare Grenze um seine Nachkommen zieht, genügend Ausgabe für die Diagnose eines unterbrochenen Laufs bewahrt und eine Antwort für den Fall bereithält, dass der Controller verschwindet. Entwickle und teste diesen Vertrag, bevor du einen Agenten Befehle mit Nebenwirkungen ausführen lässt.
SSH-Abbruch hat vier getrennte Stationen
Eine Abbruchanfrage muss vier Grenzen passieren: Der Agent entscheidet sich für den Stopp, der lokale Supervisor beendet oder signalisiert seinen SSH-Client, das SSH-Protokoll überträgt ein Kanalereignis oder Signal, und der Remote-Host reagiert darauf. Jede Station kann unabhängig ausfallen.
RFC 4254 trennt diese Konzepte. Der Standard definiert eine Nachricht zum Schließen eines Kanals und separat eine signal-Kanalanfrage für Namen wie TERM, INT und HUP. Das Schließen eines Kanals ist ein Transportereignis. Es bedeutet nicht «Sende SIGTERM an jeden Remote-Nachkommen». Der RFC sagt außerdem, dass vor dem Schließen gesendete Daten nach Möglichkeit zugestellt werden sollen. «Nach Möglichkeit» bedeutet viel, wenn ein Laptop in den Ruhezustand geht, eine Netzwerkroute ausfällt oder ein lokaler Prozess zwangsweise beendet wird.
Diese Unterscheidung macht einen häufigen Fehlentwurf sichtbar:
- Ein Agent startet
ssh host long-command. - Der Benutzer drückt auf Abbrechen.
- Der Agenten-Runner beendet seinen lokalen Kindprozess.
- Die Benutzeroberfläche markiert den Job als abgebrochen.
long-commandoder eines seiner Kinder läuft auf dem Host weiter.
Die fünfte Zeile ist kein Sonderfall. Sie ist das Standardergebnis, wenn der Server keinen Grund hat, den Befehl zu beenden, oder wenn sich der Befehl vor dem Abbruch der Verbindung von der Sitzung gelöst hat.
Ein brauchbarer Abbruchvertrag sagt genau, was die lokale Seite versucht und wofür die Remote-Seite verantwortlich ist:
- Der Starter erstellt einen identifizierbaren Remote-Lauf mit einer zufälligen Lauf-ID.
- Der Remote-Wrapper startet die Arbeit in einer eigenen Prozessgruppe oder Sitzung.
- Bei einem normalen Abbruch wird
TERMan diese Prozessgruppe gesendet und das Ergebnis aufgezeichnet. - Der Wrapper wechselt erst nach einer festgelegten Schonfrist zu
KILL. - Eine Remote-Frist oder Lease beendet die Arbeit, wenn der Controller nicht zurückkehrt.
- Ausgabe und endgültiger Status überleben den SSH-Stream.
Nenne eine Aufgabe erst dann abgebrochen, wenn eines von zwei Ergebnissen vorliegt: ein bestätigter endgültiger Remote-Datensatz oder ausdrücklich der Zustand «Status unbekannt». Nach einem Verbindungsverlust Sicherheit vorzutäuschen, verlangsamt die Incident-Bearbeitung, weil alle von einer falschen Annahme ausgehen.
Eine Remote-PID reicht nicht für die Bereinigung von Kindern
Die PID der Remote-Shell zu beenden ist nur dann sicher, wenn die Shell nie forkt, keine Pipeline startet, keinen Hintergrundprozess ausführt und kein Werkzeug aufruft, das Hilfsprozesse erzeugt. Das trifft auf wenige Befehle zu.
Betrachte diesen gewöhnlichen Remote-Befehl:
build-assets | tee build.log 6
wait
Die Shell hat eine PID. Die Pipeline besteht aus mehreren Prozessen. tee kann nach dem Ende der Shell weiterhin in die Protokolldatei schreiben. Ein Compiler kann Worker-Prozesse starten. Ein Paketmanager kann Arbeit an einen Dienst übergeben. Wenn du kill -TERM "$shell_pid" sendest, beendest du ein Mitglied einer größeren Gruppe und weißt nur wenig über den Rest.
Prozessgruppen liefern für einen kurzlebigen Remote-Lauf die passende Abbruchgrenze. Unter Linux gehört jeder Prozess zu einer Prozessgruppe, und jede Prozessgruppe gehört zu einer Sitzung. Vom Terminal erzeugte Signale gehen an die Vordergrund-Prozessgruppe. Deshalb wirkt das Verhalten eines Terminals manchmal magischer, als es ist. Die Linux-Dokumentation zu setpgid(2) macht außerdem deutlich, dass ein Kind zunächst die Prozessgruppe seines Elternprozesses erbt, sofern nichts daran geändert wird.
Erstelle für einen von einem Agenten gesteuerten Befehl eine neue Sitzung. Der Sitzungsleiter hat normalerweise eine PID, die seiner PGID und SID entspricht. Eine negative PID in kill adressiert dann die Prozessgruppe:
kill -TERM -- -"$pgid"
Das vorangestellte Minuszeichen entscheidet darüber, ob ein Prozess oder seine Prozessgruppe beendet wird. Auch -- ist wichtig. Es verhindert, dass ein fehlerhafter Wert als Option interpretiert wird.
Gehe nicht blind davon aus, dass die Workload-PID zugleich ihre Prozessgruppen-ID ist. Prüfe das beim Start. Ein Shell-Wrapper, ein Service-Manager oder ein Programm, das setpgid aufruft, kann den Baum verändern. Dies ist der kleinste nützliche Prüf-Befehl für deine Testsammlung:
ps -o pid=,ppid=,pgid=,sid=,stat=,etime=,command= -p "$pid"
Eine typische Ausgabe sieht so aus:
24182 24177 24182 24182 Ss 00:03 bash ./worker.sh /tmp/agent-runs/6c4...
Hier stimmen PID, PGID und SID überein. Das ist ein Hinweis darauf, dass kill -TERM -- -24182 die gewünschte Grenze trifft. Wenn die PGID nicht mit dem Laufdatensatz übereinstimmt, brich den Start ab, statt zu raten.
Eine Prozessgruppe hat weiterhin Grenzen. Ein Kind kann setsid aufrufen, eine Container-Laufzeit kann einen Prozess verschieben, und eine Workload kann einen Service-Manager bitten, etwas außerhalb der Gruppe auszuführen. Das kann durchaus legitim sein. Deine Abbruchgarantie endet aber an dieser Übergabe. Behandle losgelöste Arbeit als eigenen Jobtyp mit eigener Identität, eigener Stoppaktion und eigenem Audit-Datensatz.
Ein Abbruch-Wrapper braucht einen echten Bereinigungspfad
Ein Remote-Shell-Wrapper sollte die Workload-PID verwalten, erwartete Beendigungssignale abfangen, die Workload-Gruppe ansprechen, kurz warten und einen endgültigen Datensatz schreiben. Er sollte weder pkill command-name verwenden noch eine lose Prozessliste auswerten oder jeden Prozess eines Kontos beenden. Diese Abkürzungen funktionieren bis zu dem Tag, an dem zwei Agentenläufe denselben Benutzer verwenden, sich ein Hostname ändert oder ein Befehlsname zur Arbeit eines anderen Benutzers passt.
Dieses Linux-orientierte Testprogramm ist absichtlich schlicht. Es erstellt ein geschütztes Laufverzeichnis, startet eine Workload in einer neuen Sitzung, schreibt die Ausgabe in Dateien und beendet die Workload-Gruppe, wenn der Wrapper TERM, INT oder HUP empfängt.
#!/usr/bin/env bash
set -Eeuo pipefail
run_id=${1:?run ID required}
shift
run_dir="${HOME}/.agent-runs/${run_id}"
umask 077
mkdir -p "$run_dir"
child_pid=""
child_pgid=""
finished=0
write_status() {
local state=$1
local code=${2:-}
local tmp="$run_dir/status.tmp"
printf '{"run_id":"%s","state":"%s","exit_code":"%s"}\n' \
"$run_id" "$state" "$code" >"$tmp"
mv "$tmp" "$run_dir/status.json"
}
stop_group() {
if [[ -z ${child_pgid:-} ]]; then
return
fi
kill -TERM -- "-$child_pgid" 2>/dev/null || true
for _ in 1 2 3 4 5; do
if ! kill -0 -- "-$child_pgid" 2>/dev/null; then
return
fi
sleep 1
done
kill -KILL -- "-$child_pgid" 2>/dev/null || true
}
cancel() {
local signal=$1
trap - TERM INT HUP
write_status "cancelling:$signal"
stop_group
wait "$child_pid" 2>/dev/null || true
write_status "cancelled:$signal"
finished=1
exit 143
}
trap 'cancel TERM' TERM
trap 'cancel INT' INT
trap 'cancel HUP' HUP
write_status "starting"
setsid "$@" >"$run_dir/stdout.log" 2>"$run_dir/stderr.log" &
child_pid=$!
child_pgid=$(ps -o pgid= -p "$child_pid" | tr -d ' ')
if [[ "$child_pgid" != "$child_pid" ]]; then
printf 'unexpected PGID for %s: %s\n' "$child_pid" "$child_pgid" \
>"$run_dir/stderr.log"
kill -TERM "$child_pid" 2>/dev/null || true
write_status "launch_failed"
exit 70
fi
printf '%s\n' "$child_pid" >"$run_dir/pid"
printf '%s\n' "$child_pgid" >"$run_dir/pgid"
write_status "running"
set +e
wait "$child_pid"
code=$?
set -e
if [[ $finished -eq 0 ]]; then
write_status "finished" "$code"
fi
exit "$code"
Dieser Wrapper verhindert einen konkreten Fehler: Ein Abbruchsignal erreicht den Wrapper, aber er beendet nur sich selbst und lässt die Workload zurück. Er verspricht nicht, absichtlich losgelöste Nachkommen zu stoppen. Dieses Versprechen sollte er auch nicht geben.
Das Linux-Handbuch zu setsid(2) sagt, dass setsid() eine neue Sitzung erstellt und den Aufrufer zum Leiter einer neuen Prozessgruppe macht, zunächst ohne steuerndes Terminal. Der Befehl setsid aus util-linux führt ein Programm in dieser neuen Sitzung aus und forkt bei Bedarf. Deshalb ist dies eine praktische Grenze für einen Befehlslauf und kein magischer Bereinigungsschalter.
Halte den Wrapper klein. Er sollte starten, aufzeichnen, stoppen und melden. Verstecke keine Geschäftslogik darin. Die Workload braucht weiterhin eigene Regeln für Transaktionen, temporäre Dateien und Idempotenz.
Eine saubere Transporttrennung garantiert keine Bereinigung
SSH-Nutzer schließen aus einem Terminaltest oft zu viel. Sie führen einen Befehl mit PTY aus, schließen das Terminal, sehen, dass ein Prozess nach SIGHUP endet, und entscheiden, dass die Bereinigung bei einer Trennung funktioniert. Dann verwendet ein Agent einen nicht interaktiven SSH-Kanal ohne PTY und das Verhalten ändert sich.
Ein PTY erzeugt Terminalsemantik. Ein Hangup des Terminals kann zur Zustellung von SIGHUP führen, aber nur unter den Bedingungen, die für steuernde Terminals und Vordergrund-Prozessgruppen gelten. Das Linux-Handbuch beschreibt SIGHUP als Auflegen eines steuernden Terminals oder als Tod eines steuernden Prozesses. Daraus folgt nicht, dass jede SSH-Trennung jeden über SSH gestarteten Prozess signalisiert.
Nicht interaktives SSH ist für Agenten meist die bessere Voreinstellung, weil es eine sauberere Ausgabe und weniger Überraschungen durch den Shell-Start liefert. Gleichzeitig verschwindet jede zufällige Abhängigkeit vom Terminalverhalten. Verwende ein PTY nur, wenn das Remote-Programm eines braucht, etwa ein alter Installer, der sich sonst nicht ausführen lässt. Dokumentiere dann, dass das PTY zum Verhalten des Befehls gehört, und teste es separat.
Es gibt drei Fälle bei einer Trennung, die du benennen solltest:
Der Client sendet einen bewussten Abbruch
Der lokale Supervisor hat noch eine aktive Verbindung und kann ein Protokollsignal senden. Alternativ kann er eine separate authentifizierte Steueraktion öffnen, die die aufgezeichnete PGID signalisiert. Das ist der beste Fall. Der Remote-Wrapper empfängt TERM, bereinigt die Gruppe und schreibt cancelled:TERM.
Verlasse dich nicht darauf, dass ein SSH-Client ein lokales SIGINT genau so umsetzt, ohne die konkrete Client-Bibliothek und den ausgelieferten Aufruf getestet zu haben. Ein Terminal-Client, eine eingebettete SSH-Bibliothek und ein MCP-Tool können eine lokale Abbruchaktion unterschiedlich abbilden. Manche schließen einen Socket, manche beenden den lokalen Prozess und manche können eine SSH-signal-Anfrage senden. Das sind unterschiedliche Implementierungen einer Schnittstelle, die Benutzer «Abbrechen» nennen.
Der lokale Client stürzt ab oder verliert das Netzwerk
Der Remote-Befehl kann weiterlaufen. Der Server kann eine vorübergehende Routingstörung nicht von einem Benutzer unterscheiden, der die Arbeit fortsetzen will, sofern dein Protokoll ihm das nicht mitteilt. Eine Remote-Lease ist die ehrliche Antwort.
Schreibe beim Start eine deadline_epoch in das Remote-Laufverzeichnis. Ein lokaler Supervisor erneuert sie, solange der Lauf autorisiert bleibt. Ein Remote-Watchdog prüft sie und verwendet nach ihrem Ablauf denselben Bereinigungspfad für die Prozessgruppe. Wähle eine Lease-Dauer, die zur Operation passt. Fünf Minuten können für einen Shell-Befehl passen, für einen Build mit langen, aber normalen Ruhephasen jedoch riskant sein.
Der Remote-Host fällt aus oder startet neu
Du kannst sowohl den Prozess als auch den endgültigen Status verlieren. Melde nicht «abgebrochen» oder «abgeschlossen», nur weil die SSH-Verbindung beendet wurde. Markiere den Lauf als unbekannt, bis eine spätere Abstimmung das Journal des Hosts, den Deployment-Status, einen Sperrdatensatz oder ein anwendungsspezifisches Ergebnis liest.
Die Schwierigkeit liegt nicht darin, ein Statuswort auszugeben. Sie liegt darin, ein Statuswort zurückzuhalten, das dein System nicht belegen kann.
Unvollständige Ausgabe beweist Beobachtung, nicht Abschluss
Ein Stream beantwortet die Frage «Welche Bytes hat der Client bisher empfangen?». Er beantwortet nicht «Welchen Zustand hat der Remote-Befehl hinterlassen?». Dieser Fehler tritt auf, wenn ein Remote-Programm done ausgibt, bevor es seine letzte Datei flush't, oder wenn das Netzwerk ausfällt, nachdem der Befehl abgeschlossen wurde, aber bevor der Client den SSH-Exit-Status erhalten hat.
Führe zwei Datensätze:
stdout.logundstderr.logenthalten diagnostische Ausgabe, sobald die Workload sie schreibt.status.jsonist ein kleiner endgültiger Datensatz, den der Wrapper atomar schreibt, nachdem er das Ende oder den Abbruch beobachtet hat.
Das mv im Wrapper ist wichtig. Schreibe eine temporäre Statusdatei im selben Verzeichnis und benenne sie anschließend um. Leser sehen entweder die vollständige vorherige Datei oder die vollständige neue Datei. Sie sollten niemals die Hälfte eines JSON-Dokuments lesen und daraus ein Ergebnis erfinden.
Auch die Ausgabe braucht Regeln. Ein Befehl kann stark puffern, wenn stdout in eine Datei statt in ein Terminal schreibt. Wenn der Fortschritt wichtig ist, soll die Workload explizite zeilenorientierte Statusmeldungen an stderr oder in eine anwendungsspezifische Fortschrittsdatei schreiben. Löse Pufferung nicht dadurch, dass du jedem Befehl ein PTY zuweist. Das verändert das Verhalten und kann stdout mit stderr zusammenführen, wodurch Auditierung und Fehleranalyse schwieriger werden.
Unterscheide diese Fälle in der Agentenoberfläche oder im Journal:
| Remote-Datensatz | Stream-Zustand | Bedeutung |
|---|---|---|
finished, Exit-Code vorhanden | vollständig | Der Wrapper hat den normalen Abschluss beobachtet. |
cancelled:TERM | kann abrupt enden | Der Wrapper hat den Abbruch eingeleitet und seine Gruppe beendet. |
nur cancelling:TERM | getrennt | Die Bereinigung hat begonnen, aber der endgültige Datensatz wurde nicht beobachtet. Abstimmen. |
nur running, Lease gültig | getrennt | Die Arbeit kann noch laufen. Nicht blind wiederholen. |
| kein brauchbarer Datensatz | getrennt | Der Status ist unbekannt. Nebenwirkungen prüfen, bevor neu gestartet wird. |
Lege keine Zugriffstoken, ungekürzten Konfigurations-Dumps oder Zugangsdaten in diese Protokolle. Die Injektion von SSH-Zugangsdaten kann den privaten Schlüssel vom Agenten fernhalten. Ein Remote-Befehl kann aber weiterhin Geheimnisse ausgeben, die er aus seiner eigenen Umgebung oder Konfiguration gelesen hat. Die Aufbewahrung der Ausgabe gehört zum Befehlsentwurf und ist kein nachträglicher Zusatz.
Teste Prozessbäume statt einer schläfrigen Shell
trap 'exit' TERM; sleep 600 ist ein schwacher Abbruchtest. Er zeigt, dass eine Vordergrund-Shell ein Signal empfangen kann. Er testet jedoch keine Kinder, Prozessgruppen, verzögerte Bereinigung, persistierte Ausgabe oder eine Remote-Shell, die im falschen Moment verschwindet.
Verwende eine Workload, die einen sichtbaren Prozessbaum erzeugt und jedes Signal aufzeichnet. Speichere dies als worker.sh auf einem nicht produktiven Linux-Host:
#!/usr/bin/env bash
set -Eeuo pipefail
run_dir=${1:?run directory required}
note() {
printf '%s pid=%s pgid=%s %s\n' \
"$(date +%s)" "$$" "$(ps -o pgid= -p $$ | tr -d ' ')" "$1" \
>>"$run_dir/worker.log"
}
trap 'note TERM; exit 143' TERM
trap 'note INT; exit 130' INT
trap 'note HUP; exit 129' HUP
(
trap 'note grandchild_TERM; exit 143' TERM
trap 'note grandchild_HUP; exit 129' HUP
while :; do
note grandchild_tick
sleep 1
done
) &
grandchild=$!
note "started grandchild=$grandchild"
while :; do
note parent_tick
sleep 1
done
Führe es über den Wrapper mit einer zufälligen Lauf-ID aus. Prüfe in einer zweiten SSH-Sitzung den Prozessbaum und das Laufverzeichnis:
run_id=cancel-test-$(date +%s)
ssh host.example './remote-wrapper.sh '"$run_id"' ./worker.sh "'$HOME/.agent-runs/'"$run_id"'"'
Die genaue Maskierung wird in einem echten Starter anders aussehen. Das ist in Ordnung. Unverändert bleiben muss jedoch die Testevidenz: Du brauchst die Remote-Lauf-ID, die PID des Wrappers, die PGID der Workload, die Speicherorte der Protokolle und den endgültigen Status.
Teste anschließend diese Fehlerpfade einzeln:
- Sende
TERMan die PID des Wrappers. Bestätige, dass Elternprozess und Enkelprozess ihre Beendigung protokolliert haben undpskeinen Prozess in der aufgezeichneten PGID findet. - Sende
TERMdirekt an die Workload-PID. Prüfe, dass das Verhalten der Kinder nicht als sicher vorausgesetzt wird. Dieser Test erklärt, warum der Wrapper eine Gruppe anspricht. - Beende den lokalen SSH-Client, ohne ein Remote-Signal zu senden. Bestätige, dass die Workload aktiv bleibt, bis ihre Remote-Lease abläuft. Wenn sie sofort stoppt, halte den Grund fest, etwa das Verhalten eines PTY bei einer Trennung, statt dieses Ergebnis als allgemeingültig anzusehen.
- Trenne die Verbindung, nachdem der Wrapper
cancelling:TERM, aber bevor er seinen endgültigen Datensatz schreibt. Bestätige, dass die Abstimmung eine unvollständige Beobachtung von einem neuen laufenden Job unterscheiden kann. - Starte zwei Läufe unter demselben Konto, brich einen davon ab und beweise, dass der andere weiterläuft. Dadurch werden gefährliche breite
pkill-Befehle und eine kontoweite Bereinigungslogik sichtbar.
Verwende während des Tests ps und pgrep -a -g "$pgid", und wiederhole die Prüfung nach der Schonfrist. Prüfe die Remote-Statusdatei und die Protokolle erst, nachdem du die Prozesstabelle geprüft hast. Eine Statusdatei, die «abgebrochen» meldet, während Worker noch laufen, ist ein Fehler im Supervisor und keine harmlose Abweichung der Berichterstattung.
Parent-Death-Signale helfen nur in einem kontrollierten Worker
Linux stellt PR_SET_PDEATHSIG bereit. Damit kann ein Prozess den Kernel bitten, ihm ein Signal zu senden, wenn der Thread, der ihn erstellt hat, beendet wird. Das kann nützlich sein, wenn du einen kleinen nativen Helfer kontrollierst, der ein direktes Kind erzeugt und dieses beenden soll, sobald der Helfer stirbt. Die Einstellung bleibt unter normalen Bedingungen über execve hinweg erhalten. Das Handbuch dokumentiert jedoch wichtige Ausnahmen, darunter Änderungen an den Zugangsdaten.
Damit ist die Bereinigung durch einen Remote-Agenten nicht gelöst.
Erstens ist der SSH-Serverprozess nicht unbedingt der Elternprozess, der dich interessiert. Zweitens gilt das Signal für den Prozess, der es gesetzt hat, und nicht automatisch für alle Nachkommen. Drittens verliert sich diese Beziehung, wenn ein Remote-Befehl forkt, sich doppelt forkt oder Arbeit an einen anderen Dienst übergibt. Viertens ist das Verfahren Linux-spezifisch, was bei einer gemischten Unix-Flotte wichtig ist.
Verwende es nur als zusätzliche Absicherung, wenn der Prozessbaum dir gehört. Ein kleiner Linux-Worker kann beispielsweise PR_SET_PDEATHSIG setzen, bevor er ein einzelnes kontrolliertes Kind per exec startet, während der äußere Wrapper weiterhin Prozessgruppe und Lease verwaltet. Damit erhältst du zwei Ausfallerkennungen mit unterschiedlicher Reichweite. Die Gruppenabgrenzung oder den endgültigen Remote-Datensatz ersetzt das nicht.
Die gleiche Warnung gilt für nohup, disown und setsid innerhalb der Workload. Sie sind nützlich, wenn jemand bewusst möchte, dass Arbeit ein Terminal überlebt. Sie widersprechen aber dem Versprechen, dass ein Abbruch des Agentenlaufs die Arbeit beendet. Entscheide dich beim Start ausdrücklich.
Ein zweiter Steuerkanal ist oft sauberer, als den ersten zu beenden
Wenn ein Agent einen laufenden SSH-Aufruf abbricht, wird sein eigener lokaler Ausführungskontext möglicherweise bereits beendet. Sich darauf zu verlassen, dass dieser sterbende Prozess noch ein abschließendes Protokollsignal sendet, führt zu Wettläufen. Ein separater Supervisor sollte Abbruch und Abstimmung übernehmen.
Ein praktikabler Entwurf sieht so aus:
- Der Supervisor erzeugt eine kryptografisch zufällige Lauf-ID und ruft den Remote-Wrapper auf.
- Der Wrapper zeichnet PID, PGID, Startzeit und Status unter einem Verzeichnis auf, das nach der Lauf-ID benannt ist.
- Der Supervisor speichert Lauf-ID und Remote-Host, bevor er mit dem Einlesen der Ausgabe beginnt.
- Beim Abbruch öffnet der Supervisor eine neue Steueraktion, liest den Laufdatensatz, prüft erwarteten Besitzer und das Alter und signalisiert dann die aufgezeichnete PGID.
- Der Supervisor fragt den endgültigen Status ab, bis ein Endzustand erscheint oder eine Frist für die Meldung erreicht ist.
Die Steueraktion muss den Laufdatensatz prüfen, bevor sie etwas signalisiert. Prüfe mindestens, ob das Datensatzverzeichnis dem erwarteten Benutzer gehört, die PID noch existiert, die aufgezeichnete PGID mit ps übereinstimmt und die Startzeit zum gestarteten Prozess passt. Linux kann PIDs wiederverwenden. Eine veraltete PID-Datei zusammen mit einem bedingungslosen kill kann Wochen später unabhängige Arbeit beenden.
Verstecke den Bereinigungsbefehl nicht hinter einer allgemeinen Agentenanweisung wie «Beende den Prozess meiner vorherigen Aufgabe». Der Agent sollte eine undurchsichtige Laufkennung erhalten. Der Supervisor übersetzt diese Kennung in eine eng begrenzte Remote-Aktion. Das macht auch die Auditierung verständlicher: Ein Prüfer sieht, dass Lauf 6c4... den Abbruch der PGID 24182 auf einem Host angefordert hat, nicht, dass ein Agent einen beliebigen Kill-Befehl zusammengestellt hat.
Für autonome Coding-Agenten kann Sallyport SSH-Aktionen ausführen, ohne den SSH-Schlüssel dem Agenten zugänglich zu machen. Damit bleibt die Verwahrung der Zugangsdaten vom Abbruchvertrag getrennt. Der Vertrag braucht dennoch Laufkennung, Gruppenprüfung, Remote-Lease und endgültigen Datensatz.
Die richtige Wiederholungsentscheidung hängt von der Nebenwirkung ab
Ein Befehl, der nur eine Datei liest, kann nach einer unbekannten Trennung meist wiederholt werden. Ein Befehl, der einen Benutzer anlegt, eine Migration anwendet, ein Zertifikat rotiert oder ein Deployment startet, kann das nicht. Die SSH-Schicht kann dir nicht sagen, ob die Operation sicher wiederholbar ist.
Gib Remote-Befehlen mit Nebenwirkungen ein Idempotenz-Token, das aus der Lauf-ID abgeleitet wird. Das Remote-Programm sollte das Token zusammen mit dem Ergebnis der Operation speichern und bei erneutem Auftreten desselben Tokens das vorhandene Ergebnis zurückgeben. Wenn das nicht möglich ist, ergänze eine Vorabfrage, die feststellt, ob die gewünschte Änderung bereits erfolgt ist.
Verwende Bereinigung nicht als Ersatz für Idempotenz. Selbst ein perfektes TERM kann eintreffen, nachdem eine Remote-API eine Anfrage angenommen hat, aber bevor der Befehl seine Antwort ausgegeben hat. Auch KILL kann erst nach dem Commit einer Datenbanktransaktion eintreffen. Prozessbereinigung beantwortet die Frage, ob dein Worker noch läuft. Sie macht externe Auswirkungen nicht rückgängig.
Halte in der Steuerung drei Ergebnisse fest: abgeschlossen, abgebrochen mit bestätigter Bereinigung und unbekannt. Unbekannt ist unangenehm, liefert dem nächsten Operator aber die entscheidende Information: Vor einer weiteren Aktion muss der Remote-Zustand geprüft werden. Das ist weitaus besser als ein grüner Wiederholen-Button, der auf Wunschdenken beruht.
Eine Abbruchfunktion ist bereit, wenn du sie an jeder Grenze unterbrechen kannst und den verbleibenden Prozessbaum, die gespeicherte Ausgabe, den endgültigen Datensatz und die Wiederholungsentscheidung erklären kannst. Wenn dir das auf einem nicht produktiven Host nicht gelingt, vertraue der Funktion nicht auf einem Produktionshost um zwei Uhr morgens.
FAQ
Wie stoppe ich einen Remote-SSH-Befehl, wenn ein KI-Agent ihn abbricht?
Behandle den Abbruch als separate Steueraktion und nicht als Ende des lokalen SSH-Clients. Sende ein Signal an eine aufgezeichnete Remote-Prozessgruppe, warte eine begrenzte Zeit auf die Bereinigung und prüfe anschließend den endgültigen Zustand des Laufs anhand eines dauerhaft gespeicherten Datensatzes. Wenn der Steuerpfad bereits verschwunden ist, muss eine Remote-Lease oder ein Watchdog entscheiden, wann die Arbeit beendet wird.
Beendet das Schließen einer SSH-Verbindung den Remote-Befehl?
Nein. Das Schließen eines SSH-Kanals beendet den Kanal. Es garantiert nicht, dass der Server SIGTERM an den Befehl oder seine Nachkommen sendet. Ein PTY kann das Verhalten durch Hangup-Signale des Terminals verändern. Trotzdem ist das kein Bereinigungsvertrag, auf den du dich verlassen solltest.
Wie beende ich Kindprozesse, die von einem Remote-Shell-Skript gestartet wurden?
Verwende für jeden Remote-Lauf eine eigene Prozessgruppe oder Sitzung und sende ein Signal an die negative PGID, etwa kill -TERM -- -12345. Prüfe die PGID, bevor du dich darauf verlässt. Wenn du nur die PID der Shell beendest, bleiben Hintergrundprozesse, Pipelines und Unterprozesse zurück.
Sollte ich `setsid` für Remote-Befehle eines Agenten verwenden?
Ein normaler Aufruf von setsid erstellt für den gestarteten Befehl eine neue Sitzung und Prozessgruppe. Damit erhält der Supervisor eine einzelne Gruppe, die er beenden kann. Verwende es nicht für Arbeiten, die einen Abbruch überleben sollen. Prüfe SID und PGID mit ps, weil Wrapper und Service-Manager den Prozessbaum verändern können.
Wie bereinige ich nach einer SSH-Trennung oder einem Netzwerkausfall?
Ein Remote-Prozess sollte eine maximale Laufzeit oder eine erneuerbare Lease haben, wenn es unsicher oder teuer wäre, ihn weiterlaufen zu lassen. Die Lease muss auf dem Remote-Host liegen, denn einem getrennten Client kann man nicht anvertrauen, dass er sein Verschwinden meldet. Ein Abbruchsignal beendet die Arbeit schnell, während die Lease den Fall abdeckt, in dem dieses Signal nie ankommt.
Kann ich der unvollständigen Ausgabe nach dem Abbruch einer SSH-Verbindung vertrauen?
Nutze die Stream-Ausgabe zur Beobachtung, schreibe sie aber zusätzlich in eine Remote-Datei oder ein Journal. Speichere einen separaten Abschlussdatensatz erst, wenn der Befehl beendet und die Bereinigung abgeschlossen ist. Ein abgebrochener Stream zeigt, dass die Übertragung fehlgeschlagen ist, nicht, ob der Befehl abgeschlossen wurde.
Reicht PR_SET_PDEATHSIG für die Bereinigung von Remote-Prozessen?
Sie ist Linux-spezifisch und hilft einem Prozess nur zu erkennen, dass sein direkter Elternprozess beendet wurde. PR_SET_PDEATHSIG deckt Enkelprozesse nicht automatisch ab, und Änderungen an den Berechtigungen können die Einstellung löschen. Verwende es als zusätzliche lokale Absicherung in einem speziell entwickelten Worker, nicht als einzigen Mechanismus zum Stoppen beliebiger Befehle.
Sollte ein Agent für Remote-SSH-Befehle ein PTY zuweisen?
Normalerweise nein. Ein Terminal verändert Signalzustellung, Pufferung und das Verhalten von Programmen, während ein nicht interaktiver Befehl eine sauberere maschinenlesbare Ausgabe liefert. Weise nur dann ein PTY zu, wenn das Remote-Programm wirklich Terminalsemantik benötigt. Teste das Verhalten bei einer Trennung anschließend separat.
Welche Angaben sollte ein Audit-Datensatz für einen SSH-Abbruch enthalten?
Zeichne eine zufällige Lauf-ID, die Identität des Remote-Hosts, Start-PID, PGID, SID, Startzeit, den angeforderten Befehl und das Endergebnis auf. Schütze den Remote-Datensatz vor anderen Benutzern und führe die Aktualisierung des Endstatus atomar aus. Eine PID allein ist ein schwacher Nachweis, weil PIDs wiederverwendet werden können.
Kann ein Agent destruktive Arbeiten sicher über SSH ausführen?
Starte keinen unbegrenzten SSH-Befehl und nenne den Job abgebrochen, sobald der lokale Prozess endet. Verlange eine Laufkennung, eine Prüfung der Prozessgruppe, eine Bereinigungsfrist und einen endgültigen Remote-Status. Sallyport kann den SSH-Schlüssel vom Agenten fernhalten. Ob ein Abbruch tatsächlich zuverlässig ist, entscheidet aber weiterhin der Aufbau des Remote-Befehls.