feat(core): the command line with its local commands after Java, measured against Java

The terminal splits a line as Java's window does and runs ls, cd, pwd,
mkdir, touch, cp, mv, rm, cat, test, help, ipconfig and route on the
node's files, which are the project's own. Its texts are generated from
the bundle Java loads. TerminalCases feeds Java's Terminal without a
network (16 cases, de and en); eight cases name Java bugs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 20:32:35 +00:00
co-authored by Claude Opus 5.5
parent 058675a148
commit 016f9461df
17 changed files with 14404 additions and 7 deletions
+29
View File
@@ -764,3 +764,32 @@ Gemessen (01.10.2026):
* Wer von zwei fast gleichzeitig gebauten Sockets die nächste
TCP-Startnummer zieht, entscheidet der Thread-Takt (Programm und
Weiterleitung des Servers, wenige Millisekunden auseinander).
## Befehlszeile, lokale Befehle (F3.4a)
`TerminalCases` füttert Javas `Terminal` mit Zeilen, wie sie getippt
werden, und zerlegt sie wie das Fenster (`GUIApplicationTerminalWindow`:
erstes Wort der Befehl, dann die Parameter, mindestens drei leere dazu;
`exit`, `cls`, `reset` und leere Zeilen behandelt das Fenster selbst). Ohne
laufendes Netz, auf Rechner 10.0.0.2 von `router-web` mit festen Dateien.
Geschrieben werden je Zeile, was das Programm dem Fenster sagt, der
Arbeitsordner und alle Dateien, dazu die Terminal-Texte des Bündels, wie
Java sie lädt (daraus `terminalTexts.ts`).
Gemessen (01.10.2026):
* Java ruft jede Methode des Terminals mit `String[]`-Parameter auf, deren
Name der Befehl ist, mit Groß- und Kleinschreibung (`LS` ist unbekannt).
* Ordner listet `ls` in der Reihenfolge, in der die Einträge entstanden;
Pfade findet Java ohne Rücksicht auf Groß- und Kleinschreibung und zeigt
die gespeicherte Schreibweise.
* Die Größe einer Datei: Java dekodiert jeden Inhalt als Base64 und zählt
die Bytes, nur bei einem Fehler die Zeichen; „Hallo Welt“ hat 6.
* Fehler: `ls` auf einen fehlenden Ordner sagt „leer“, `dir /` schreibt
„Verzeichnis :“; `cd` auf eine Datei macht sie zum Arbeitsordner; `touch`
und `mkdir` in einem fehlenden Ordner melden „erstellt“ bzw. „bereits
vorhanden“; `cp` auf einen Ordnernamen wirft; `..` über der Wurzel findet
nichts oder wirft; `rm` (und das Löschen bei `mv`) hängt einen absoluten
Pfad an den Arbeitsordner, `rm /` löscht den Arbeitsordner und meldet
Erfolg; `mv` auf die Datei selbst (auch in anderer Schreibweise) löscht
sie.
+11 -1
View File
@@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then
fi
classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
mkdir -p "$here/.build"
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java"
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java"
# FILIUS writes its settings below $HOME; keep them out of the real one.
mkdir -p "$here/.build/home"
# Removed first: a crashed run must not leave the previous result standing.
@@ -276,6 +276,16 @@ for run in send:15000 fetch:10000 byName:35000 wrongPass:8000 noServer:15000 unk
test -s "$output"
done
# The command line fed with lines as typed, without a running network, in
# both languages (TerminalCases; packages/core/test/terminal.test.ts).
for language in de en; do
output="$root/packages/core/test/terminal.$language.java.json"
rm -f "$output"
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" TerminalCases "$language" \
"$root/packages/core/test/scenarios/router-web.scenario.json" "$root/packages/core/test/terminal.cases.txt" "$output" 2>&1 | grep -v 'logback\|^$' || true
test -s "$output"
done
# FILIUS' own examples as Java FILIUS runs them, opened and started as FILIUS
# does (ExampleRun): the acceptance of F2 (packages/fls/test/examplesJava.test.ts).
for run in internetwork_komplett:routing:12000 einfaches_rechnernetz_komplett:dhcp:20000 dns_server:dns:12000 \
+170
View File
@@ -0,0 +1,170 @@
// Feeds FILIUS' command line (Terminal, with the input handling of its window
// GUIApplicationTerminalWindow) with lines as typed, without a running
// network, and writes for each line what the program told the window (each
// text and each Boolean "busy" mark), what it returned, the working directory
// after it, and every file and directory of the host: the reference for the
// core's command line (packages/core/src/runtime/apps/terminal.ts). It also
// writes the terminal's texts of the message bundle, as Java loads them.
// Each case starts on host 10.0.0.2 of a scenario (ScenarioNetwork) with the
// files below and a new Terminal. The cases file is ASCII, with Java-style
// unicode escapes (backslash, u, four hex digits) for anything else:
//
// case <name>
// <line as typed>
//
// Usage: java -cp filius.jar:lib/*:classes TerminalCases <de|en> <scenario.json> <cases txt> <output json>
import filius.rahmenprogramm.Base64;
import filius.rahmenprogramm.Information;
import filius.software.lokal.Terminal;
import filius.software.system.Datei;
import filius.software.system.Dateisystem;
import filius.software.system.InternetKnotenBetriebssystem;
import java.io.FileOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.StringTokenizer;
import java.util.TreeMap;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;
public class TerminalCases {
static final String HOST = "00000000-0000-0000-0000-000000000301";
/** A tiny PNG (1 x 1 pixel), as FILIUS keeps images: Base64. */
static final String IMAGE = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
/** The image as FILIUS writes a file's bytes (Base64.encodeBytes, with its line breaks). */
static String imageAsFiliusWritesIt() {
try {
return Base64.encodeBytes(Base64.decode(IMAGE));
} catch (Exception error) {
throw new IllegalStateException(error);
}
}
/** The host's files at the start of each case, in this order. */
static void files(Dateisystem files) {
files.getRoot().removeAllChildren();
files.speicherDatei("/", new Datei("notizen.txt", "txt", "Hallo Welt"));
files.erstelleVerzeichnis("/", "Dokumente");
files.speicherDatei("/Dokumente", new Datei("brief.txt", "txt", "Sehr geehrte Damen und Herren,\nviele Grüße"));
files.erstelleVerzeichnis("/Dokumente", "Alt");
files.speicherDatei("/", new Datei("bild.png", "png", imageAsFiliusWritesIt()));
files.speicherDatei("/", new Datei("kurz.txt", "txt", "SGFsbG8="));
files.erstelleVerzeichnis("/", "leer");
}
public static void main(String[] args) throws Exception {
// Before any class reads the message bundle (I18n.messages is read once).
Information.getInformation().setLocale(args[0].equals("en") ? new Locale("en", "GB") : Locale.GERMANY);
ScenarioNetwork network = ScenarioNetwork.read(args[1]);
InternetKnotenBetriebssystem system = network.system(HOST);
List<Object> cases = new ArrayList<>();
List<Object> steps = null;
Terminal terminal = null;
List<Object> told = new ArrayList<>();
for (String line : Files.readAllLines(Path.of(args[2]), StandardCharsets.US_ASCII)) {
if (line.startsWith("#")) continue;
if (line.startsWith("case ")) {
Map<String, Object> current = new LinkedHashMap<>();
steps = new ArrayList<>();
current.put("name", line.substring("case ".length()));
current.put("steps", steps);
cases.add(current);
files(system.getDateisystem());
system.getInstallierteAnwendungen().remove("filius.software.lokal.Terminal");
system.installApp("filius.software.lokal.Terminal", false);
terminal = (Terminal) system.holeSoftware("filius.software.lokal.Terminal");
terminal.setSystemSoftware(system);
terminal.hinzuBeobachter((observable, data) -> {
if (data instanceof Boolean) told.add(Arrays.asList("busy", data));
else if (data != null) told.add(Arrays.asList("text", data.toString()));
});
current.put("start", tree(system.getDateisystem()));
continue;
}
String typed = IpCheckCases.unescape(line);
Map<String, Object> step = new LinkedHashMap<>();
step.put("typed", typed);
told.clear();
Object returned = null;
String window = null;
// GUIApplicationTerminalWindow: the first word is the command, the others its parameters, then at least three empty ones.
if (typed.trim().isEmpty()) {
window = "blank";
} else {
StringTokenizer tokens = new StringTokenizer(typed, " ");
String command = tokens.nextToken();
String[] parameters = new String[3 + tokens.countTokens()];
Arrays.fill(parameters, "");
for (int index = 0; tokens.hasMoreTokens(); index++) parameters[index] = tokens.nextToken();
if (command.equals("exit")) {
window = "exit";
} else if (command.equals("reset") || command.equals("cls")) {
window = "clear";
} else {
// Terminal.terminalEingabeAuswerten, but in this thread instead of the program's.
try {
Method method = Terminal.class.getDeclaredMethod(command, String[].class);
terminal.setInterrupt(false);
returned = method.invoke(terminal, (Object) parameters);
} catch (NoSuchMethodException error) {
terminal.benachrichtigeBeobachter(terminal.messages.getString("terminal_msg2") + "\n" + terminal.messages.getString("terminal_msg3"));
} catch (InvocationTargetException error) {
returned = "!" + error.getCause().getClass().getSimpleName();
}
}
}
step.put("window", window);
step.put("told", new ArrayList<>(told));
step.put("returned", returned == null ? null : returned.toString());
step.put("directory", Dateisystem.absoluterPfad(terminal.getAktuellerOrdner()));
step.put("files", tree(system.getDateisystem()));
steps.add(step);
}
Map<String, Object> out = new LinkedHashMap<>();
out.put("cases", cases);
ResourceBundle bundle = terminal.messages;
Map<String, Object> texts = new TreeMap<>();
for (String key : Collections.list(bundle.getKeys())) {
if (key.startsWith("sw_terminal") || key.startsWith("terminal_msg")) texts.put(key, bundle.getString(key));
}
out.put("texts", texts);
try (FileOutputStream stream = new FileOutputStream(args[3])) {
stream.write((FlsOracle.Json.write(out, "") + "\n").getBytes(StandardCharsets.UTF_8));
}
System.out.println("terminal cases (" + args[0] + "): " + cases.size());
System.exit(0);
}
/** Every node below the root in preorder: path, and for a file its type, content and size (Datei.holeGroesse). */
static List<Object> tree(Dateisystem files) {
List<Object> nodes = new ArrayList<>();
Enumeration<TreeNode> all = files.getRoot().preorderEnumeration();
while (all.hasMoreElements()) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) all.nextElement();
if (node == files.getRoot()) continue;
if (node.getUserObject() instanceof Datei) {
Datei file = (Datei) node.getUserObject();
nodes.add(Arrays.asList(Dateisystem.absoluterPfad(node), file.getDateiTyp(), file.getDateiInhalt(), file.holeGroesse()));
} else {
nodes.add(Arrays.asList(Dateisystem.absoluterPfad(node)));
}
}
return nodes;
}
}