Antra

Themen

Process und fork fuer Einsteiger

Dieses Kapitel ist absichtlich fuer Leute geschrieben, die mit Child-Processes, Pipes oder IPC bisher kaum etwas gemacht haben.

Wenn dir Woerter wie stdin, stdout oder fork erstmal gross vorkommen, dann nimm fuer den Anfang nur diese zwei Bilder mit:

  • Antra::Process startet ein anderes Programm.
  • System.fork(...) startet einen zweiten Antra-Kindprozess aus dem laufenden Interpreter heraus.

Das ist fast schon die halbe Wahrheit.

Die Kurzversion#

Wenn du nur schnell wissen willst, was du nehmen sollst:

  • Nimm Antra::Process, wenn du ein externes Programm wie perl, curl oder ein Shell-Tool starten willst.
  • Nimm System.fork(...), wenn Eltern- und Kindprozess beide Antra sprechen und Werte hin- und herschicken sollen.

Das Grundgefuehl#

Antra::Process#

Process.spawn(...) bedeutet:

"Starte bitte ein anderes Programm und gib mir einen Handle darauf zurueck."

Dieser Handle ist ein opaker Process-Wert. Ueber ihn kannst du dann:

  • die PID lesen
  • Text an stdin schreiben
  • stdout und stderr lesen
  • Signale wie TERM oder KILL schicken
  • auf das Ende des Prozesses warten

System.fork(...)#

System.fork(...) bedeutet:

"Erzeuge eine Kopie des aktuellen Prozesses und fuehre im Kind eine Antra-Funktion aus."

Der Elternprozess bekommt ebenfalls einen Process-aehnlichen Handle zurueck. Der Unterschied ist nicht die Form des Rueckgabewerts, sondern die Herkunft des Kindes:

  • bei Process.spawn(...) kommt das Kind von einem externen Kommando
  • bei System.fork(...) kommt das Kind aus deinem laufenden Antra-Prozess selbst

Bildlich gedacht#

Du kannst es dir so merken:

  • Process.spawn(...) ist wie "oeffne ein Walkie-Talkie zu einem fremden Programm".
  • System.fork(...) ist wie "klone mich selbst und gib meinem Klon ebenfalls ein Walkie-Talkie".

Erster Schritt mit Antra::Process#

Nimm zuerst das einfachste Modell:

  • du startest ein Programm
  • du schreibst Text hinein
  • du liest Text wieder heraus
import Antra::Process as Process
import Antra::Result as Result

my process = Process.spawn("perl", ["-e", 'print uc(do { local $/; <STDIN> })']).!

Result.unwrap!(Process.write(process, "hello"))
Result.unwrap!(Process.close_input(process))
println(Process.read_all(process).!)
println(Process.wait(process).!)

Was passiert hier?

  • Process.spawn! startet perl
  • das kleine Perl-Snippet liest alles von STDIN und macht es gross
  • Result.unwrap!(Process.write(...)) schickt hello hinein
  • Result.unwrap!(Process.close_input(...)) signalisiert: es kommt nichts mehr
  • Process.read_all! liest die komplette Antwort
  • Process.wait! liefert den Exit-Code

Die Ausgabe ist:

HELLO
0

Warum close_input(...) oft wichtig ist#

Viele Programme warten, bis auf stdin wirklich Schluss ist.

Wenn du nur schreibst, aber nie Result.unwrap!(Process.close_input(process)) aufrufst, denkt das Kind oft:

"Vielleicht kommt gleich noch mehr Input."

Dann blockiert dein Programm scheinbar grundlos.

Merksatz:

  • Wenn das Kind den kompletten Input am Stueck lesen soll, schliesse danach fast immer stdin.

Die naechste Komfortstufe: with_spawn#

Oft willst du gar nicht dauernd selbst daran denken, den Prozess spaeter sauber zu schliessen.

Dafuer gibt es with_spawn(...) und with_spawn!(...).

import Antra::Process as Process

my result = Process.with_spawn!("perl", ["-e", 'print qq[ok]']) |process| -> {
    Process.read_all(process).!
}

println(result)

Das ist praktisch, wenn du sagen willst:

  • starte kurz einen Prozess
  • arbeite mit ihm
  • raeum ihn danach automatisch auf

Wenn ein Prozess haengen bleibt#

Nicht jeder Kindprozess beendet sich von selbst.

Dann gibt es drei Stufen:

  • Process.signal(process, :int) fuer ein explizites Signal
  • Process.terminate(process) fuer das uebliche freundliche Stoppen per TERM
  • Process.kill(process) fuer den harten Abbruch per KILL

Die gaengigen Signal-Keywords sind in Process.ant direkt sichtbar:

  • :term
  • :kill
  • :int
  • :hup
  • :quit
  • :usr1
  • :usr2
  • :stop
  • :cont

Ein kleines Beispiel:

import Antra::Process as Process
import Antra::Result as Result

my process = Process.spawn("perl", ["-e", 'select undef, undef, undef, 30;']).!

println(Result.unwrap!(Process.terminate(process)))
println(Process.close(process).!)

Wichtig ist der Unterschied:

  • Process.close(...) schickt selbst kein Signal
  • Process.close(...) schliesst nur die Streams und wartet dann auf das Ende
  • wenn der Prozess aktiv beendet werden soll, brauchst du vorher signal, terminate oder kill

Wann Process.stdin/stdout/stderr sinnvoll sind#

Die High-Level-Funktionen reichen oft schon:

  • write
  • read_all
  • read_stderr_all

Manchmal willst du aber direkt mit den einzelnen Streams arbeiten. Dann holst du dir explizit die FileHandles:

import Antra::IO::File as File
import Antra::Process as Process
import Antra::Result as Result

Process.with_spawn!("perl", ["-e", 'print uc(do { local $/; <STDIN> })']) |process| -> {
    my stdin = Process.stdin(process).!
    my stdout = Process.stdout(process).!

    Result.unwrap!(File.write(stdin, "hello"))
    Result.unwrap!(File.close(stdin))
    println(File.read_all(stdout).!)
}

Das brauchst du vor allem dann, wenn du absichtlich mit den Streams als Handles arbeiten willst.

Das erste Gefuehl fuer System.fork(...)#

fork ist nicht dafuer da, irgendein externes Programm zu starten.

Stattdessen sagst du:

  • hier ist eine Funktion fuer das Kind
  • gib dem Kind einen Eingangs- und Ausgangskanal
  • lass Eltern- und Kindprozess miteinander reden

Das kleinste Beispiel sieht so aus:

import Antra::AND as AND
import Antra::Process as Process
import Antra::Result as Result
import Antra::System as System

my child = System.fork(|stdin, stdout| -> {
    my msg = System.receive(stdin).!
    Result.unwrap!(System.send(stdout, %Reply{ok: true, payload: msg}))
}).!

Result.unwrap!(System.send(child, %User{name: "Ada", tags: [:admin, :active]}))
Result.unwrap!(Process.close_input(child))
println(AND.encode(System.receive(child).!).!)
println(Process.wait(child).!)

Wenn du lieber ein vollstaendiges Beispiel aus dem Repo starten willst, sieh dir ../examples/fork_worker.ant an.

Wenn du statt Text oder normaler Antra-Werte rohe Binärdaten transportieren willst, lies danach direkt auch 29-binaere-prozesskanale-und-bytestrings.md.

Wichtig ist hier die Rollenverteilung:

  • im Kind ist stdin das, was vom Elternprozess kommt
  • im Kind ist stdout das, was zum Elternprozess zurueckgeht
  • im Elternprozess ist child der Handle auf dieses Gegenueber

Warum System.send/receive so bequem sind#

System.send(...) und System.receive(...) transportieren nicht nur nackten Text.

Sie benutzen intern AND. Das heisst:

  • du kannst Strings schicken
  • du kannst Zahlen schicken
  • du kannst Arrays und Maps schicken
  • du kannst Tagged Values wie %User{...} oder %Ok[...] schicken

Du musst also fuer normale Antra-Daten nicht selbst AND.encode(...) und AND.decode(...) schreiben.

Das einfachste Denkmodell fuer fork#

Denk nicht zuerst an Betriebssystem-Theorie.

Denk erstmal nur so:

  • Der Elternprozess schickt einen Wert.
  • Das Kind liest diesen Wert.
  • Das Kind baut eine Antwort.
  • Der Elternprozess liest diese Antwort.

Wenn du das verstanden hast, ist der Rest nur noch sauberes Schliessen und Warten.

Was Process.close_input(...) bei fork bedeutet#

Auch beim geforkten Kind gilt:

  • solange die Eingabeseite offen ist, kann das Kind auf weitere Nachrichten warten
  • wenn du sicher bist, dass nichts mehr kommt, kannst du die Eingabe schliessen

Das ist besonders dann wichtig, wenn der Kindcode mehr als nur eine einzelne Nachricht lesen will oder bewusst auf EOF wartet.

Roher Text statt AND#

Manchmal spricht das Gegenueber kein Antra-Datenformat, sondern nur einfache Textzeilen.

Dafuer gibt es:

  • System.send_text(...)
  • System.receive_text(...)

Beispiel:

import Antra::Process as Process
import Antra::Result as Result
import Antra::System as System

my child = System.fork(|stdin, stdout| -> {
    my msg = System.receive_text(stdin).!
    Result.unwrap!(System.send_text(stdout, "raw: " <> msg))
}).!

Result.unwrap!(System.send_text(child, "hello raw"))
Result.unwrap!(Process.close_input(child))
println(System.receive_text(child).!)
println(Process.wait(child).!)

Das ist der richtige Weg, wenn du absichtlich ein schlichtes Zeilenprotokoll willst.

Wann Process und wann fork?#

Hier ist die wichtigste Entscheidungshilfe:

  • Nimm Process, wenn du ein externes Tool starten willst.
  • Nimm fork, wenn dein Kindprozess direkt als Antra-Funktion beschrieben werden soll.
  • Nimm System.send/receive, wenn beide Seiten normale Antra-Daten austauschen.
  • Nimm System.send_text/receive_text, wenn beide Seiten nur rohe Textzeilen austauschen.

Typische Fehler am Anfang#

1. Process und fork verwechseln#

Falsche Erwartung:

  • "Mit fork starte ich einfach mal schnell perl oder curl."

Richtig ist:

  • fork startet keinen fremden Befehl
  • Process.spawn(...) startet fremde Befehle

2. Input nicht schliessen#

Wenn der Kindprozess auf das Ende des Inputs wartet, aber der Elternprozess close_input! vergisst, bleibt oft alles haengen.

3. Text und Daten durcheinanderwerfen#

Wenn du System.send(...) benutzt, dann wird ueber AND serialisiert.

Wenn du absichtlich nur rohe Zeilen willst, nimm System.send_text(...).

4. Zu frueh an Handles denken#

Viele Anfaenger greifen sofort zu stdin, stdout und stderr als FileHandle.

Oft ist das gar nicht noetig. Starte lieber zuerst mit:

  • Process.write(...)
  • Process.read_all(...)
  • System.send(...)
  • System.receive(...)

Die direkte Handle-Ebene kannst du spaeter immer noch dazu nehmen.

Ein kleines Entscheidungsrezept#

Wenn du vor einer neuen Aufgabe sitzt, geh diese Fragen in der Reihenfolge durch:

  1. Starte ich ein externes Programm oder nur eine zweite Antra-Logik?
  2. Reicht einfacher Text oder will ich echte Antra-Werte austauschen?
  3. Muss ich die Streams selbst als Handles anfassen oder reicht die bequemere API?

Fast immer kommst du dann automatisch bei der passenden Funktion an.

Zum Mitnehmen#

Wenn du nur drei Dinge behalten willst, dann diese:

  • Process.spawn(...) startet externe Programme.
  • System.fork(...) startet einen Antra-Kindprozess.
  • System.send/receive sind fuer Daten, System.send_text/receive_text fuer rohe Textzeilen.

Quelle dieser Seite