feat: F4.6g4 the Gnutella window, measured against Java's

GnutellaWindowRun uses Java's Gnutella window on terminal-net beside the
line of GnutellaRun's story "search". The core cancels a search as Java
does (every connection of the program's own requests closed, passed-on
queries kept), lets the number of peers change and lists /peer2peer's
files; the room takes the window's actions and sends its view; the editor
shows Java's four tabs with Java's symbols. Not adopted from Java: a number
of peers of one digit only, a results table that keeps the last search's
rows, a files table that lags behind the folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 21:39:29 +00:00
co-authored by Claude Opus 5.5
parent 2b85e7432c
commit 5224e1a28b
24 changed files with 2211 additions and 17 deletions
+33
View File
@@ -1565,3 +1565,36 @@ Gemessen (03.10.2026):
je Verbindung einen eigenen `TCPSocket`. Javas Echo-Server verträgt keine
sofort wieder geschlossene Verbindung (die folgenden laufen in die
Zeitüberschreitung) — die Sonde hält sie eine halbe Sekunde.
### Gnutella (F4.6g4)
`GnutellaWindowRun` öffnet Javas `GUIApplicationPeerToPeerAnwendungWindow`
auf dem ersten Rechner von `terminal-net`, neben der Kette aus
`GnutellaRun`s Geschichte „search“ (.30 und 10.0.0.2 sind .31 beigetreten,
.31 teilt `notizen.txt`, 10.0.0.2 „Lied der Woche.txt“), und benutzt es wie
eine Person: Adresse (auch keine) und „Netz beitreten“, Suchbegriff mit
„Suchen“ und „Stopp!“, ein Ergebnis gewählt und „Herunterladen“, „Liste
leeren“, die Zahl der Teilnehmer mit „Speichern“ (auch keine Zahl), „Netz
verlassen“. Javas Knöpfe hören auf `mousePressed`, die Felder prüfen bei
`keyReleased` — die Sonde schickt beides. Je Schritt: was das Fenster zeigt
(Adressfeld und ob es rot ist, die drei Tabellen, das Feld der Teilnehmer)
und was das Programm hält (Zahl, Teilnehmer, Ergebnisse, Dateien von
`/peer2peer`) (`packages/core/test/gnutellaWindow.java.json`).
Gemessen (03.10.2026):
* Das Adressfeld beginnt mit 192.168.0.1; keine Adresse wird rot, „Netz
beitreten“ meldet dann „Dies ist keine gültige IP-Adresse“. Die eigene
Adresse tut nichts. Das Feld der Teilnehmer zeigt die Zahl des Programms.
* „Stopp!“ kurz nach „Suchen“: kein Treffer kommt an; eine neue Suche
findet wieder. Ohne gewählte Zeile tut „Herunterladen“ nichts; ein zweites
Herunterladen überschreibt die Datei.
* „Netz verlassen“ leert die Nachbarn, nicht die Ergebnisse.
* Javas Ungenauigkeiten:
* Die Prüfung der Teilnehmerzahl (`musterNurZahl` = `\d`) nimmt nur eine
Ziffer: 12 wird mit „Es sind nur Zahlen erlaubt!“ abgelehnt, 0 genommen.
* Nach einer Suche ohne Treffer zeigt die Ergebnistabelle weiter die
Ergebnisse der vorigen (das Programm hat keine mehr), bis etwas anderes
das Fenster auffrischt.
* Die Dateitabelle zeigt eine heruntergeladene Datei erst, wenn das
Programm das nächste Mal Bescheid gibt; eine daneben angelegte gar nicht.
+12 -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" "$here/src/TerminalCases.java" "$here/src/TerminalRun.java" "$here/src/FileCases.java" "$here/src/GnutellaRun.java" "$here/src/NewDeviceRun.java" "$here/src/SheetRun.java" "$here/src/TerminalWindowRun.java" "$here/src/InstallRun.java" "$here/src/BrowserWindowRun.java" "$here/src/MailWindowRun.java" "$here/src/SaveAfterRunRun.java" "$here/src/TextEditorWindowRun.java" "$here/src/FileExplorerWindowRun.java" "$here/src/EchoWindowRun.java" "$here/src/DnsWindowRun.java" "$here/src/FirewallWindowRun.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" "$here/src/TerminalRun.java" "$here/src/FileCases.java" "$here/src/GnutellaRun.java" "$here/src/NewDeviceRun.java" "$here/src/SheetRun.java" "$here/src/TerminalWindowRun.java" "$here/src/InstallRun.java" "$here/src/BrowserWindowRun.java" "$here/src/MailWindowRun.java" "$here/src/SaveAfterRunRun.java" "$here/src/TextEditorWindowRun.java" "$here/src/FileExplorerWindowRun.java" "$here/src/EchoWindowRun.java" "$here/src/DnsWindowRun.java" "$here/src/FirewallWindowRun.java" "$here/src/GnutellaWindowRun.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.
@@ -453,6 +453,17 @@ HOME="$here/.build/home" xvfb-run -a java -Duser.language=de -Duser.country=DE -
"$root/packages/core/test/scenarios/firewall-pc.scenario.json" "$output" 2>&1 | grep -v 'logback\|^$\|^\s*at ' || true
test -s "$output"
# FILIUS' Gnutella window used as a person does — joining (also with what is
# no address), searching, "Stopp!", downloading, emptying the list, the number
# of peers (also what is no number), leaving — beside the line of
# GnutellaRun's story "search" (GnutellaWindowRun): the Gnutella window
# (packages/core/test/gnutellaWindow.test.ts).
output="$root/packages/core/test/gnutellaWindow.java.json"
rm -f "$output"
HOME="$here/.build/home" xvfb-run -a java -Duser.language=de -Duser.country=DE -cp "$classpath:$here/.build" GnutellaWindowRun \
"$root/packages/core/test/scenarios/terminal-net.scenario.json" "$output" 2>&1 | grep -v 'logback\|^$\|^\s*at \|scrollpane' || true
test -s "$output"
# 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)
# and the classics of F3 (web, email, gnutella; packages/fls/test/classicsJava.test.ts).
@@ -0,0 +1,299 @@
// Uses FILIUS' own Gnutella window (GUIApplicationPeerToPeerAnwendungWindow
// on a GUIDesktopPanel) on terminal-net's first computer as a person does:
// the address field (also with what is no address) and "Netz beitreten",
// the search field with "Suchen", "Stopp!", a result chosen and
// "Herunterladen", "Liste leeren", the field of the number of peers with
// "Speichern" (also with what is no number), "Netz verlassen". The other
// computers form the line of GnutellaRun's story "search" through their
// programs. Java's message dialogs are answered and written down. After each
// step it writes what the window shows — the address field (and whether it
// is red), the three tables, the field of the number of peers — and what the
// program keeps: the number, its peers, its results, the files of
// /peer2peer. The reference for the Gnutella window
// (packages/core/test/gnutellaWindow.test.ts).
//
// Usage: java -cp filius.jar:lib/*:classes GnutellaWindowRun <scenario.json> <output json>
import filius.gui.anwendungssicht.GUIApplicationPeerToPeerAnwendungWindow;
import filius.gui.anwendungssicht.GUIDesktopPanel;
import filius.rahmenprogramm.EingabenUeberpruefung;
import filius.software.dateiaustausch.PeerToPeerAnwendung;
import filius.software.system.Betriebssystem;
import filius.software.system.Datei;
import filius.software.system.Dateisystem;
import java.awt.Component;
import java.awt.Container;
import java.awt.Window;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.FileOutputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.table.TableModel;
public class GnutellaWindowRun {
static final String CLIENT = "00000000-0000-0000-0000-000000000401";
static final String WEB = "00000000-0000-0000-0000-000000000403";
static final String ECHO = "00000000-0000-0000-0000-000000000404";
static final String FAR = "00000000-0000-0000-0000-000000000406";
static final String[] PEERS = { CLIENT, WEB, ECHO, FAR };
static final String CLASS = "filius.software.dateiaustausch.PeerToPeerAnwendung";
static final List<Object> steps = new ArrayList<>();
static final List<String> dialogs = new ArrayList<>();
static GUIApplicationPeerToPeerAnwendungWindow window;
static PeerToPeerAnwendung program;
static Object field(String name) throws Exception {
Field field = GUIApplicationPeerToPeerAnwendungWindow.class.getDeclaredField(name);
field.setAccessible(true);
return field.get(window);
}
@SuppressWarnings("unchecked")
static <T> T find(Container container, Class<T> kind) {
for (Component component : container.getComponents()) {
if (kind.isInstance(component)) return (T) component;
if (component instanceof Container) {
T found = find((Container) component, kind);
if (found != null) return found;
}
}
return null;
}
static void watchDialogs() {
Thread watcher = new Thread(() -> {
while (true) {
for (Window each : Window.getWindows()) {
if (!(each instanceof JDialog) || !each.isVisible()) continue;
JOptionPane pane = find((Container) each, JOptionPane.class);
if (pane == null) continue;
synchronized (dialogs) {
dialogs.add(String.valueOf(pane.getMessage()));
}
pane.setValue(JOptionPane.OK_OPTION);
each.setVisible(false);
}
try {
Thread.sleep(50);
} catch (InterruptedException error) {
return;
}
}
});
watcher.setDaemon(true);
watcher.start();
}
static void onEdt(Runnable action) throws Exception {
SwingUtilities.invokeLater(action);
Thread.sleep(500);
}
static List<List<String>> rows(JTable table) {
List<List<String>> rows = new ArrayList<>();
TableModel model = table.getModel();
for (int row = 0; row < model.getRowCount(); row++) {
List<String> cells = new ArrayList<>();
for (int column = 0; column < model.getColumnCount(); column++) cells.add(String.valueOf(model.getValueAt(row, column)));
rows.add(cells);
}
return rows;
}
static Map<String, Object> textField(String name) throws Exception {
JTextField each = (JTextField) field(name);
Map<String, Object> shown = new LinkedHashMap<>();
shown.put("text", each.getText());
shown.put("red", each.getForeground().equals(EingabenUeberpruefung.farbeFalsch));
return shown;
}
static void step(String name) throws Exception {
Map<String, Object> result = new LinkedHashMap<>();
result.put("step", name);
result.put("address", textField("networkIpField"));
result.put("peers", rows((JTable) field("netzwerkTabelle")));
result.put("search", ((JTextField) field("searchField")).getText());
result.put("results", rows((JTable) field("ergebnisTabelle")));
result.put("files", rows((JTable) field("dateiTabelle")));
result.put("maximum", textField("maxClientsField"));
result.put("programMaximum", program.getMaxTeilnehmerZahl());
result.put("programPeers", new ArrayList<>(program.holeBekanntePeerToPeerTeilnehmer()));
result.put("programResults", new ArrayList<>(program.holeErgebnisse()));
List<List<String>> folder = new ArrayList<>();
for (Datei each : program.getSystemSoftware().getDateisystem().holeDateien(program.holeVerzeichnis())) {
List<String> file = new ArrayList<>();
file.add(each.getName());
file.add(each.getDateiTyp());
file.add(each.getDateiInhalt());
folder.add(file);
}
result.put("programFiles", folder);
synchronized (dialogs) {
result.put("dialogs", new ArrayList<>(dialogs));
dialogs.clear();
}
steps.add(result);
}
/** Typed as a person types: the text, then a key released (the window checks the field then). */
static void type(String name, String text) throws Exception {
JTextField each = (JTextField) field(name);
onEdt(() -> {
each.setText(text);
KeyEvent event = new KeyEvent(each, KeyEvent.KEY_RELEASED, System.currentTimeMillis(), 0, KeyEvent.VK_A, 'a');
for (KeyListener listener : each.getKeyListeners()) listener.keyReleased(event);
});
}
/** Java's buttons here listen for the mouse pressed, not an action. */
static void press(String text, long wait) throws Exception {
List<JButton> found = new ArrayList<>();
collect(window, text, found);
JButton button = found.get(0);
onEdt(() -> {
MouseEvent event = new MouseEvent(button, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(), 0, 5, 5, 1, false);
for (MouseListener listener : button.getMouseListeners()) listener.mousePressed(event);
});
Thread.sleep(wait);
}
static void collect(Container container, String text, List<JButton> found) {
for (Component component : container.getComponents()) {
if (component instanceof JButton && text.equals(((JButton) component).getText())) found.add((JButton) component);
if (component instanceof Container) collect((Container) component, text, found);
}
}
public static void main(String[] args) throws Exception {
try {
run(args);
} catch (Throwable error) {
error.printStackTrace();
System.exit(1);
}
System.exit(0);
}
static void run(String[] args) throws Exception {
ScenarioNetwork network = ScenarioNetwork.read(args[0]);
Map<String, PeerToPeerAnwendung> programs = new LinkedHashMap<>();
for (String host : PEERS) {
network.system(host).installApp(CLASS, false);
PeerToPeerAnwendung each = (PeerToPeerAnwendung) network.system(host).holeSoftware(CLASS);
each.setMaxTeilnehmerZahl(host.equals(CLIENT) || host.equals(FAR) ? 1 : 2);
programs.put(host, each);
}
program = programs.get(CLIENT);
network.system(ECHO).getDateisystem().speicherDatei("/peer2peer", new Datei("notizen.txt", "txt", "Notiz"));
network.system(FAR).getDateisystem().speicherDatei("/peer2peer", new Datei("Lied der Woche.txt", "txt", "La la la"));
network.start();
Thread.sleep(500);
watchDialogs();
// The line .20 - .30 - .31 - 10.0.0.2: the others join first, as in GnutellaRun's story.
programs.get(WEB).beitretenNetzwerk("192.168.0.31");
Thread.sleep(2500);
programs.get(FAR).beitretenNetzwerk("192.168.0.31");
Thread.sleep(2500);
JFrame frame = new JFrame();
GUIDesktopPanel desktop = new GUIDesktopPanel((Betriebssystem) network.system(CLIENT));
frame.getContentPane().add(desktop);
window = (GUIApplicationPeerToPeerAnwendungWindow) desktop.starteAnwendung(CLASS);
SwingUtilities.invokeAndWait(() -> {
frame.pack();
frame.setVisible(true);
});
Thread.sleep(500);
step("open");
type("networkIpField", "192.168.0.300");
step("type an address that is none");
press("Netz beitreten", 1000);
step("join with an address that is none");
type("networkIpField", "192.168.0.20");
press("Netz beitreten", 2500);
step("join itself");
type("networkIpField", "192.168.0.30");
press("Netz beitreten", 3000);
step("join 192.168.0.30");
type("searchField", "woche");
press("Suchen", 3000);
step("search woche");
JTable results = (JTable) field("ergebnisTabelle");
onEdt(() -> results.setRowSelectionInterval(0, 0));
press("Herunterladen", 3000);
step("download row 0");
press("Herunterladen", 3000);
step("download row 0 again");
type("searchField", "xyz");
press("Suchen", 3000);
step("search xyz");
press("Liste leeren", 500);
step("empty the list");
type("searchField", "notiz");
press("Suchen", 0);
press("Stopp!", 3000);
step("search notiz and abort at once");
press("Suchen", 3000);
step("search notiz again");
onEdt(() -> results.clearSelection());
press("Herunterladen", 2000);
step("download with no row chosen");
// Another tab, as a person looks at the files: Java fills its tables anew.
JTabbedPane tabs = find(window, JTabbedPane.class);
onEdt(() -> tabs.setSelectedIndex(2));
step("files tab");
type("maxClientsField", "abc");
step("type a number of peers that is none");
press("Speichern", 500);
step("take a number of peers that is none");
type("maxClientsField", "0");
press("Speichern", 500);
step("take 0 peers");
type("maxClientsField", "99999999999");
press("Speichern", 500);
step("take a number beyond Java's int");
type("maxClientsField", "12");
press("Speichern", 500);
step("take 12 peers");
type("maxClientsField", "4");
press("Speichern", 500);
step("take 4 peers");
press("Netz verlassen", 1000);
step("leave the network");
// A file added to the folder beside the window: the table only shows it when it is filled anew.
Dateisystem files = network.system(CLIENT).getDateisystem();
onEdt(() -> files.speicherDatei("/peer2peer", new Datei("neu.txt", "txt", "neu")));
step("a file added beside the window");
Map<String, Object> output = new LinkedHashMap<>();
output.put("steps", steps);
try (FileOutputStream out = new FileOutputStream(args[1])) {
out.write((FlsOracle.Json.write(output, "") + "\n").getBytes(StandardCharsets.UTF_8));
}
network.stop();
System.out.println("gnutella window steps: " + steps.size());
}
}