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
+5
View File
@@ -18,6 +18,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `src/runtime/engine.ts` (leere Nachricht), `test/apps.test.ts` | Java `TCPSocket.erstelleSegmente`, `TCPSocket.run` (gemessen mit `EchoWindowRun`) | behoben (F4.6g1): ein leeres Segment mit PSH an der erwarteten Stelle ist eine leere Nachricht und wird ausgeliefert (vorher galt es als Wiederholung, weil seine Nummer die nächste nicht weiterzählt; Javas Echo beantwortet sie). Java beendet eine Nachricht mit jedem PSH, auch dem eines wiederholten Segments (eine leere Nachricht mehr, wenn ein ACK verloren geht); hier nur mit einem neuen — ein wiederholtes leeres ist an seiner Nummer nicht von einem neuen zu unterscheiden (benannt) | | `src/runtime/engine.ts` (leere Nachricht), `test/apps.test.ts` | Java `TCPSocket.erstelleSegmente`, `TCPSocket.run` (gemessen mit `EchoWindowRun`) | behoben (F4.6g1): ein leeres Segment mit PSH an der erwarteten Stelle ist eine leere Nachricht und wird ausgeliefert (vorher galt es als Wiederholung, weil seine Nummer die nächste nicht weiterzählt; Javas Echo beantwortet sie). Java beendet eine Nachricht mit jedem PSH, auch dem eines wiederholten Segments (eine leere Nachricht mehr, wenn ein ACK verloren geht); hier nur mit einem neuen — ein wiederholtes leeres ist an seiner Nummer nicht von einem neuen zu unterscheiden (benannt) |
| `src/runtime/apps/dnsServer.ts`, `src/runtime/dnsHosts.ts`, `src/runtime/engine.ts` (DNS-Server), `src/runtime/snapshot.ts` (`DnsServerSnapshot`), `test/dnsWindow.test.ts`, `test/dnsWindow.java.json` | Java `DNSServer` (`hinzuRecord`, `loescheResourceRecord`, `changeSingleEntry`, `leseRecordListe`, `schreibeRecordListe`), `ResourceRecord`, `GUIApplicationDNSServerWindow`, `DNSConfigTable` (gemessen mit `DnsWindowRun`) | neu (F4.6g2): der DNS-Server als Programm, das sein Fenster ändert — an und aus (sein Socket auf Port 53), rekursive Auflösung, und die Einträge, die `/dns/hosts` sind und bei jeder Anfrage gelesen werden wie in Java (ein anderes Programm, der Texteditor, ändert sie sofort); ohne Datei die der Einstellungen. Der Schnappschuss nimmt jetzt jeden installierten DNS-Server mit, ob er läuft (`isActive`). Der Leser von `/dns/hosts` zieht aus `packages/fls` hierher (`isHostsDomainName` statt `isDomainName`), dazu `newHostsRecord`, `dnsHostsText`, `hostsRecordsOf`. Gegen Javas Fenster geprüft, auch die Antworten des Resolvers. Abweichend von Java: ein hinzugefügter Eintrag behält, was ein anderes Programm inzwischen in die Datei schrieb (Java schreibt seine alte Liste zurück); eine Zelle ändert nur ihren Eintrag (Java auch die folgenden Einträge anderer Art); eine entfernte Zeile ist dieser Eintrag (Java der erste des Namens); ein Eintrag braucht einen Namen (Java nimmt den leeren) | | `src/runtime/apps/dnsServer.ts`, `src/runtime/dnsHosts.ts`, `src/runtime/engine.ts` (DNS-Server), `src/runtime/snapshot.ts` (`DnsServerSnapshot`), `test/dnsWindow.test.ts`, `test/dnsWindow.java.json` | Java `DNSServer` (`hinzuRecord`, `loescheResourceRecord`, `changeSingleEntry`, `leseRecordListe`, `schreibeRecordListe`), `ResourceRecord`, `GUIApplicationDNSServerWindow`, `DNSConfigTable` (gemessen mit `DnsWindowRun`) | neu (F4.6g2): der DNS-Server als Programm, das sein Fenster ändert — an und aus (sein Socket auf Port 53), rekursive Auflösung, und die Einträge, die `/dns/hosts` sind und bei jeder Anfrage gelesen werden wie in Java (ein anderes Programm, der Texteditor, ändert sie sofort); ohne Datei die der Einstellungen. Der Schnappschuss nimmt jetzt jeden installierten DNS-Server mit, ob er läuft (`isActive`). Der Leser von `/dns/hosts` zieht aus `packages/fls` hierher (`isHostsDomainName` statt `isDomainName`), dazu `newHostsRecord`, `dnsHostsText`, `hostsRecordsOf`. Gegen Javas Fenster geprüft, auch die Antworten des Resolvers. Abweichend von Java: ein hinzugefügter Eintrag behält, was ein anderes Programm inzwischen in die Datei schrieb (Java schreibt seine alte Liste zurück); eine Zelle ändert nur ihren Eintrag (Java auch die folgenden Einträge anderer Art); eine entfernte Zeile ist dieser Eintrag (Java der erste des Namens); ein Eintrag braucht einen Namen (Java nimmt den leeren) |
| `src/runtime/apps/personalFirewall.ts`, `src/runtime/engine.ts` (`personalFirewall`, `addFirewallPortRule`, `removeFirewallRule`, Firewall-Protokoll mit Zeit), `src/runtime/apps/routerPages.ts` (`portRuleAdded`, `portRuleRemoved`), `test/firewallWindow.test.ts`, `test/firewallWindow.java.json`, `test/scenarios/firewall-pc.scenario.json` | Java `GUIApplicationFirewallWindow` (`hinzuRegel`, `loescheRegel`, `ruleToVector`), `Firewall` (`addRule`, `deleteRule`), `FirewallRule` (gemessen mit `FirewallWindowRun`) | neu (F4.6g3): die Firewall eines Rechners, wie ihr Fenster sie ändert — eine Portregel hinzugefügt (TCP, UDP oder beide; alle Absender oder nur die der eigenen Netze; akzeptieren, hinten angefügt), eine Regel entfernt; beides steht im Protokoll der Firewall (Java schreibt es nur ins Fenster). Das Protokoll trägt jetzt die Zeit jeder Zeile. Gegen Javas Fenster geprüft, mit Ping, TCP-Verbindung und DNS-Frage aus dem eigenen und einem fremden Netz nach jedem Schritt. Abweichend von Java: ein Port ist 1 bis 65535, Leerzeichen drumherum erlaubt (Java nimmt jede Zahl: −1 öffnet jeden Port beider Protokolle, 0 und 70000 ergeben Regeln, die nie greifen; ein Leerzeichen davor lehnt Java still ab) | | `src/runtime/apps/personalFirewall.ts`, `src/runtime/engine.ts` (`personalFirewall`, `addFirewallPortRule`, `removeFirewallRule`, Firewall-Protokoll mit Zeit), `src/runtime/apps/routerPages.ts` (`portRuleAdded`, `portRuleRemoved`), `test/firewallWindow.test.ts`, `test/firewallWindow.java.json`, `test/scenarios/firewall-pc.scenario.json` | Java `GUIApplicationFirewallWindow` (`hinzuRegel`, `loescheRegel`, `ruleToVector`), `Firewall` (`addRule`, `deleteRule`), `FirewallRule` (gemessen mit `FirewallWindowRun`) | neu (F4.6g3): die Firewall eines Rechners, wie ihr Fenster sie ändert — eine Portregel hinzugefügt (TCP, UDP oder beide; alle Absender oder nur die der eigenen Netze; akzeptieren, hinten angefügt), eine Regel entfernt; beides steht im Protokoll der Firewall (Java schreibt es nur ins Fenster). Das Protokoll trägt jetzt die Zeit jeder Zeile. Gegen Javas Fenster geprüft, mit Ping, TCP-Verbindung und DNS-Frage aus dem eigenen und einem fremden Netz nach jedem Schritt. Abweichend von Java: ein Port ist 1 bis 65535, Leerzeichen drumherum erlaubt (Java nimmt jede Zahl: −1 öffnet jeden Port beider Protokolle, 0 und 70000 ergeben Regeln, die nie greifen; ein Leerzeichen davor lehnt Java still ab) |
| `src/runtime/apps/gnutella.ts` (`cancelSearch`, `setMaximumKnownPeers`, `fileRows`, `gnutellaResultRow`, `gnutellaPeerCountOfText`), `test/gnutellaWindow.test.ts`, `test/gnutellaWindow.java.json` | Java `PeerToPeerAnwendung` (`abbrechenSuche`, `setMaxTeilnehmerZahl`), `PeerToPeerClient`/`PeerToPeerClientMitarbeiter` (`beenden`), `GUIApplicationPeerToPeerAnwendungWindow` (`updateErgebnisTabelle`, `updateDateiTabelle`, `zahlPruefen`), gemessen mit `GnutellaWindowRun` | erweitert (F4.6g4): was Gnutellas Fenster vom Programm braucht — „Stopp!“ schließt jede Verbindung der eigenen Anfragen (Suchen, Beitreten, Herunterladen), die Treffer unterwegs sind verloren, weitergereichte fremde Anfragen bleiben; die Zahl der Teilnehmer gilt für die kommenden; die Dateien von `/peer2peer` (ohne Ordner) mit Typ; ein Ergebnis wie Javas Tabelle es zerlegt. Gegen Javas Fenster geprüft. Abweichend von Java: jede ganze Zahl ist eine Teilnehmerzahl (Javas Prüfung `musterNurZahl` nimmt nur eine Ziffer) |
| `src/runtime/engine.ts` (Installation), `src/runtime/apps/installation.ts`, `test/installation.test.ts` | Java `InternetKnotenBetriebssystem.installAndStartAppIfAvailable/entferneSoftware`, `WebServer.erzeugeStandardVerzeichnis`, `PeerToPeerAnwendung`/`EmailServer`/`DNSServer.setSystemSoftware/starten`, `tmpl/webserver_index_*`, `gfx/allgemein/splashscreen-mini.png` | neu (F4.6c): was ein Programm bei der Installation in die Dateien legt (Javas Startseite je Sprache, Bild, `/peer2peer`, `/mailserver/konten.txt`, `/dns/hosts`); `installProgram`/`removeProgram` im laufenden Betrieb — Einstellungen und fehlende Dateien aus dem Schnappschuss des Dokuments, Programm sofort gestartet; entfernt wird ein Server abgeschaltet, die Dateien bleiben. Die Programmerzeugung beim Start läuft über dieselbe `startProgram` in der bisherigen Reihenfolge | | `src/runtime/engine.ts` (Installation), `src/runtime/apps/installation.ts`, `test/installation.test.ts` | Java `InternetKnotenBetriebssystem.installAndStartAppIfAvailable/entferneSoftware`, `WebServer.erzeugeStandardVerzeichnis`, `PeerToPeerAnwendung`/`EmailServer`/`DNSServer.setSystemSoftware/starten`, `tmpl/webserver_index_*`, `gfx/allgemein/splashscreen-mini.png` | neu (F4.6c): was ein Programm bei der Installation in die Dateien legt (Javas Startseite je Sprache, Bild, `/peer2peer`, `/mailserver/konten.txt`, `/dns/hosts`); `installProgram`/`removeProgram` im laufenden Betrieb — Einstellungen und fehlende Dateien aus dem Schnappschuss des Dokuments, Programm sofort gestartet; entfernt wird ein Server abgeschaltet, die Dateien bleiben. Die Programmerzeugung beim Start läuft über dieselbe `startProgram` in der bisherigen Reihenfolge |
| `src/runtime/apps/web.ts` (F4.6d), `test/web.test.ts` | Java `WebBrowser.retrieveImages`, `GUIApplicationWebBrowserWindow.hyperLinkListener`, `WebServer.changeVHostTable/saveVHosts/setUseVHost` (gemessen mit `BrowserWindowRun`) | erweitert (F4.6d): die Zeile einer Seite nennt Host und Pfad, die eines Bildes trägt seine Daten (für das Fenster); `pageReference` und `linkTarget` — relative Bilder und Links zählen vom Ordner der Seite (Java vom Wurzelordner des Servers; für eine Seite im Wurzelordner sind die Anfragen Javas), eine Marke (`#…`) und andere Schemata führen nirgends hin; der Webserver schaltet seine virtuellen Hosts um und schreibt ihre Tabelle, nur mit vollständigen Zeilen (Java schreibt jede Zeile, und beim Lesen rückt eine Zelle einer unvollständigen in die Zeile davor) | | `src/runtime/apps/web.ts` (F4.6d), `test/web.test.ts` | Java `WebBrowser.retrieveImages`, `GUIApplicationWebBrowserWindow.hyperLinkListener`, `WebServer.changeVHostTable/saveVHosts/setUseVHost` (gemessen mit `BrowserWindowRun`) | erweitert (F4.6d): die Zeile einer Seite nennt Host und Pfad, die eines Bildes trägt seine Daten (für das Fenster); `pageReference` und `linkTarget` — relative Bilder und Links zählen vom Ordner der Seite (Java vom Wurzelordner des Servers; für eine Seite im Wurzelordner sind die Anfragen Javas), eine Marke (`#…`) und andere Schemata führen nirgends hin; der Webserver schaltet seine virtuellen Hosts um und schreibt ihre Tabelle, nur mit vollständigen Zeilen (Java schreibt jede Zeile, und beim Lesen rückt eine Zelle einer unvollständigen in die Zeile davor) |
| `src/runtime/apps/emailClient.ts`, `src/runtime/apps/emailServer.ts`, `src/runtime/emailAccountsFile.ts`, `src/runtime/engine.ts` (F4.6e) | Java `GUIApplicationEmailAnwendungWindow.kontoSpeichern/emailLoeschen`, `EmailAnwendung.setzeKonto/speichern/removeReceivedMail/removeSentMail`, `GUIApplicationEmailServerWindow`, `EmailServer.setMailDomain/benutzerHinzufuegen/kontoLoeschen` (gemessen mit `MailWindowRun`) | erweitert (F4.6e): das E-Mail-Programm setzt sein Konto (`accountOfForm` mit Javas Mustern für Ports und Benutzernamen, `splitName` wie Javas Dialog: letztes Wort Nachname) und schreibt es nach `/konten.txt` (`clientAccountFileText`, jetzt im Kern statt im Export), löscht eine Mail eines Ordners, prüft ein Formular wie Javas Fenster (`mailFormProblem`) und sendet mit dem Konto, mit dem die Mail geschrieben wurde; der Server setzt seine Domain (nicht leer, Javas Muster), legt ein Konto an (einen vorhandenen Namen in jeder Schreibweise nicht — Javas Fenster meldet es als angelegt) und entfernt eines, sein Startknopf schreibt die eigene Zeile des Fensters; die neue Domain steht sofort in Schnappschuss und Kontendatei (Java erst bei der nächsten Kontenänderung) | | `src/runtime/apps/emailClient.ts`, `src/runtime/apps/emailServer.ts`, `src/runtime/emailAccountsFile.ts`, `src/runtime/engine.ts` (F4.6e) | Java `GUIApplicationEmailAnwendungWindow.kontoSpeichern/emailLoeschen`, `EmailAnwendung.setzeKonto/speichern/removeReceivedMail/removeSentMail`, `GUIApplicationEmailServerWindow`, `EmailServer.setMailDomain/benutzerHinzufuegen/kontoLoeschen` (gemessen mit `MailWindowRun`) | erweitert (F4.6e): das E-Mail-Programm setzt sein Konto (`accountOfForm` mit Javas Mustern für Ports und Benutzernamen, `splitName` wie Javas Dialog: letztes Wort Nachname) und schreibt es nach `/konten.txt` (`clientAccountFileText`, jetzt im Kern statt im Export), löscht eine Mail eines Ordners, prüft ein Formular wie Javas Fenster (`mailFormProblem`) und sendet mit dem Konto, mit dem die Mail geschrieben wurde; der Server setzt seine Domain (nicht leer, Javas Muster), legt ein Konto an (einen vorhandenen Namen in jeder Schreibweise nicht — Javas Fenster meldet es als angelegt) und entfernt eines, sein Startknopf schreibt die eigene Zeile des Fensters; die neue Domain steht sofort in Schnappschuss und Kontendatei (Java erst bei der nächsten Kontenänderung) |
@@ -166,6 +167,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `src/echo.ts` | Java `GUIApplicationServerBausteinWindow`, `GUIApplicationClientBausteinWindow` (gemessen mit `EchoWindowRun`) | neu (F4.6g1): die Bildschirmstände von Echo-Server (läuft, Port) und Einfachem Client (verbunden, Adresse, Port); `portOfText`: ein Port, wie ein Socket ihn haben kann (Javas `Integer.parseInt` nimmt jede Zahl und wirft bei anderem Text) | | `src/echo.ts` | Java `GUIApplicationServerBausteinWindow`, `GUIApplicationClientBausteinWindow` (gemessen mit `EchoWindowRun`) | neu (F4.6g1): die Bildschirmstände von Echo-Server (läuft, Port) und Einfachem Client (verbunden, Adresse, Port); `portOfText`: ein Port, wie ein Socket ihn haben kann (Javas `Integer.parseInt` nimmt jede Zahl und wirft bei anderem Text) |
| `src/dns.ts`, `src/sim.ts` (`dnsServer`) | Java `GUIApplicationDNSServerWindow` (gemessen mit `DnsWindowRun`) | neu (F4.6g2): der Bildschirmstand des DNS-Servers — Knopf, Kästchen, die Zeilen der drei Tabellen, wie `/dns/hosts` sie hält | | `src/dns.ts`, `src/sim.ts` (`dnsServer`) | Java `GUIApplicationDNSServerWindow` (gemessen mit `DnsWindowRun`) | neu (F4.6g2): der Bildschirmstand des DNS-Servers — Knopf, Kästchen, die Zeilen der drei Tabellen, wie `/dns/hosts` sie hält |
| `src/firewall.ts`, `src/sim.ts` (`firewall`) | Java `GUIApplicationFirewallWindow` (gemessen mit `FirewallWindowRun`) | neu (F4.6g3): der Bildschirmstand der Firewall eines Rechners — die drei Kästchen und die Regeln, wie die Tabelle sie zeigt (mit dem ganzen Text einer Regel, die das Fenster nicht machen kann); das Protokoll kommt als Zeilen des Fensters | | `src/firewall.ts`, `src/sim.ts` (`firewall`) | Java `GUIApplicationFirewallWindow` (gemessen mit `FirewallWindowRun`) | neu (F4.6g3): der Bildschirmstand der Firewall eines Rechners — die drei Kästchen und die Regeln, wie die Tabelle sie zeigt (mit dem ganzen Text einer Regel, die das Fenster nicht machen kann); das Protokoll kommt als Zeilen des Fensters |
| `src/gnutella.ts` | Java `GUIApplicationPeerToPeerAnwendungWindow` (gemessen mit `GnutellaWindowRun`) | neu (F4.6g4): der Bildschirmstand von Gnutella — die bekannten Teilnehmer, die Ergebnisse wie die Tabelle sie zeigt, die Dateien von `/peer2peer`, die Zahl der Teilnehmer |
## packages/sim ## packages/sim
@@ -185,6 +187,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `src/room.ts` (Echo), `test/echoWindow.test.ts`, `test/echoWindow.java.json`, `test/fixtures/after-run-echo.fls`, `test/room.test.ts` | Java `GUIApplicationServerBausteinWindow`, `GUIApplicationClientBausteinWindow` (gemessen mit `EchoWindowRun`), `SaveAfterRunRun echo` | erweitert (F4.6g1): der Raum nimmt `setActive` mit Port beim Echo-Server und `connect`/`disconnect`/`send` beim Einfachen Client an, schickt ihre Bildschirmstände und schreibt nach dem Lauf Port und Ziel zurück, wie Javas Projekt sie behält. Gegen Javas Fenster und Javas gespeichertes Projekt geprüft; benannt: drei SYN statt einem (F2.3c) — ein Verbindungsversuch dauert länger | | `src/room.ts` (Echo), `test/echoWindow.test.ts`, `test/echoWindow.java.json`, `test/fixtures/after-run-echo.fls`, `test/room.test.ts` | Java `GUIApplicationServerBausteinWindow`, `GUIApplicationClientBausteinWindow` (gemessen mit `EchoWindowRun`), `SaveAfterRunRun echo` | erweitert (F4.6g1): der Raum nimmt `setActive` mit Port beim Echo-Server und `connect`/`disconnect`/`send` beim Einfachen Client an, schickt ihre Bildschirmstände und schreibt nach dem Lauf Port und Ziel zurück, wie Javas Projekt sie behält. Gegen Javas Fenster und Javas gespeichertes Projekt geprüft; benannt: drei SYN statt einem (F2.3c) — ein Verbindungsversuch dauert länger |
| `src/room.ts` (DNS-Server), `test/dnsWindow.test.ts` | Java `GUIApplicationDNSServerWindow`, `DNSConfigTable` | erweitert (F4.6g2): der Raum nimmt `setActive`, `setRecursive`, `addRecord`, `removeRecord`, `changeRecord` an, schickt den Bildschirmstand (ohne Zeilen; Javas Fenster hat keine) und schreibt nach dem Lauf zurück, ob der Server läuft, die rekursive Auflösung und die Einträge der Datei in seine Einstellungen | | `src/room.ts` (DNS-Server), `test/dnsWindow.test.ts` | Java `GUIApplicationDNSServerWindow`, `DNSConfigTable` | erweitert (F4.6g2): der Raum nimmt `setActive`, `setRecursive`, `addRecord`, `removeRecord`, `changeRecord` an, schickt den Bildschirmstand (ohne Zeilen; Javas Fenster hat keine) und schreibt nach dem Lauf zurück, ob der Server läuft, die rekursive Auflösung und die Einträge der Datei in seine Einstellungen |
| `src/room.ts` (Firewall), `test/firewallWindow.test.ts` | Java `GUIApplicationFirewallWindow` | erweitert (F4.6g3): der Raum nimmt `setActive`, `setIcmpAllowed`, `setFilterUdp`, `addRule`, `removeRule` an, schickt den Bildschirmstand und das Protokoll der Firewall als Zeilen (über Stopps behalten, wie die Engine es hält); zurück ins Projekt geht die Firewall wie schon bei Routern | | `src/room.ts` (Firewall), `test/firewallWindow.test.ts` | Java `GUIApplicationFirewallWindow` | erweitert (F4.6g3): der Raum nimmt `setActive`, `setIcmpAllowed`, `setFilterUdp`, `addRule`, `removeRule` an, schickt den Bildschirmstand und das Protokoll der Firewall als Zeilen (über Stopps behalten, wie die Engine es hält); zurück ins Projekt geht die Firewall wie schon bei Routern |
| `src/room.ts` (Gnutella), `test/gnutellaWindow.test.ts`, `test/room.test.ts` | Java `GUIApplicationPeerToPeerAnwendungWindow` | erweitert (F4.6g4): der Raum nimmt `join`, `leave`, `search`, `cancelSearch`, `clearResults`, `download`, `setMaximumPeers` an, schickt den Bildschirmstand (ohne Zeilen; Javas Fenster hat keine) und schreibt nach dem Lauf die Zahl der Teilnehmer zurück; eine heruntergeladene Datei steht sofort im Dokument. Der alte Raumtest nahm Gnutella als Programm ohne Aktionen und prüft dort jetzt ein nicht installiertes Programm |
| `browser/m9.spec.ts`, `browser/traceDigest.ts`, `browser/*Entry.ts`, `/playwright.config.ts` | Plan §13 M9 | neu (F4.2): M9 in Chromium — 17 Beispiele, je 30 s mit Eingaben, ergeben Zeichen für Zeichen denselben Mitschnitt wie in Node; der Raum in einem echten Worker | | `browser/m9.spec.ts`, `browser/traceDigest.ts`, `browser/*Entry.ts`, `/playwright.config.ts` | Plan §13 M9 | neu (F4.2): M9 in Chromium — 17 Beispiele, je 30 s mit Eingaben, ergeben Zeichen für Zeichen denselben Mitschnitt wie in Node; der Raum in einem echten Worker |
## packages/editor ## packages/editor
@@ -219,6 +222,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `src/desktop/EchoPanels.tsx`, `src/desktop/serverLines.ts` (`simpleClientLine`) | Java `GUIApplicationServerBausteinWindow` (`serverbaustein_msg1`–`3`), `GUIApplicationClientBausteinWindow` (`clientbaustein_msg1`–`6`), `ClientBaustein` (`sw_clientbaustein_msg2`–`6`), `Socket`/`TCPSocket` (`sw_socket_msg1`, `sw_tcpsocket_msg2`, `3`, `9`) | neu (F4.6g1): Echo-Server — Starten/Anhalten mit Port (nur angehalten beschreibbar) und seine Zeilen; Einfacher Client — Server-Adresse und -Port (verbunden gesperrt, nach einer Änderung des Programms wieder dessen Werte), Verbinden/Trennen, Nachricht und Senden (nur verbunden, danach leer), seine Zeilen in Javas Worten. Besser als Java: ein Port, der keiner ist, wird rot markiert und nicht übergeben (Javas Knopf wirft und tut nichts) | | `src/desktop/EchoPanels.tsx`, `src/desktop/serverLines.ts` (`simpleClientLine`) | Java `GUIApplicationServerBausteinWindow` (`serverbaustein_msg1`–`3`), `GUIApplicationClientBausteinWindow` (`clientbaustein_msg1`–`6`), `ClientBaustein` (`sw_clientbaustein_msg2`–`6`), `Socket`/`TCPSocket` (`sw_socket_msg1`, `sw_tcpsocket_msg2`, `3`, `9`) | neu (F4.6g1): Echo-Server — Starten/Anhalten mit Port (nur angehalten beschreibbar) und seine Zeilen; Einfacher Client — Server-Adresse und -Port (verbunden gesperrt, nach einer Änderung des Programms wieder dessen Werte), Verbinden/Trennen, Nachricht und Senden (nur verbunden, danach leer), seine Zeilen in Javas Worten. Besser als Java: ein Port, der keiner ist, wird rot markiert und nicht übergeben (Javas Knopf wirft und tut nichts) |
| `src/desktop/DnsServerPanel.tsx`, `src/desktop/WebServerPanel.tsx` (Zelle) | Java `GUIApplicationDNSServerWindow` (`dnsserver_msg1`–`23`), gemessen mit `DnsWindowRun` | neu (F4.6g2): der DNS-Server — Starten/Beenden, das Kästchen der rekursiven Auflösung, drei Reiter mit je zwei Feldern (das Adressfeld beginnt mit der eigenen Adresse), rot, solange die Prüfung sie nicht nimmt, „Hinzufügen“ (sonst Javas gültiges Beispiel), „Auswahl entfernen“ und die Tabelle, deren Zellen beim Verlassen geschrieben werden. Besser als Java: eine Zelle, die die Prüfung nicht nimmt, wird markiert, bevor sie zurückgeht. **Behoben, auch im Webserver (F4.6d):** eine Zelle übernahm den neuen Wert des Raums, der kurz nach einem Enter kommt, auch über das inzwischen Getippte; jetzt nur, solange seit Enter, Escape oder Verlassen nichts getippt ist | | `src/desktop/DnsServerPanel.tsx`, `src/desktop/WebServerPanel.tsx` (Zelle) | Java `GUIApplicationDNSServerWindow` (`dnsserver_msg1`–`23`), gemessen mit `DnsWindowRun` | neu (F4.6g2): der DNS-Server — Starten/Beenden, das Kästchen der rekursiven Auflösung, drei Reiter mit je zwei Feldern (das Adressfeld beginnt mit der eigenen Adresse), rot, solange die Prüfung sie nicht nimmt, „Hinzufügen“ (sonst Javas gültiges Beispiel), „Auswahl entfernen“ und die Tabelle, deren Zellen beim Verlassen geschrieben werden. Besser als Java: eine Zelle, die die Prüfung nicht nimmt, wird markiert, bevor sie zurückgeht. **Behoben, auch im Webserver (F4.6d):** eine Zelle übernahm den neuen Wert des Raums, der kurz nach einem Enter kommt, auch über das inzwischen Getippte; jetzt nur, solange seit Enter, Escape oder Verlassen nichts getippt ist |
| `src/desktop/PersonalFirewallPanel.tsx`, `src/desktop/serverLines.ts` (`firewallLine`), `src/desktop/DesktopWindow.tsx` | Java `GUIApplicationFirewallWindow` (`firewall_msg1`–`14`), `Firewall`/`FirewallThread` (`sw_firewall_msg5`, `msg7`, `msg8`, `firewallthread_msg1`), gemessen mit `FirewallWindowRun` | neu (F4.6g3): die Firewall eines Rechners — Reiter „Konfiguration“ und „Log“, die drei Kästchen (ICMP und UDP nur bei eingeschalteter Firewall), Port mit Protokoll und Absendern, „Hinzufügen“ (das Feld wird danach geleert), „Löschen“ der gewählten Zeile, die Tabelle; das Protokoll in Javas Worten. Besser als Java: ein Port, der keiner ist, wird rot markiert und nicht übergeben; Javas Erklärtext, den Java schreibt, aber nie zeigt, steht unter den Kästchen; eine Regel, die das Fenster nicht machen kann (aus einer Datei: verwerfend oder mit Adresse), zeigt in der Absenderspalte, was sie ist (Java zeigt sie als offenen Port für alle Absender); ins Protokoll kann man nicht tippen | | `src/desktop/PersonalFirewallPanel.tsx`, `src/desktop/serverLines.ts` (`firewallLine`), `src/desktop/DesktopWindow.tsx` | Java `GUIApplicationFirewallWindow` (`firewall_msg1`–`14`), `Firewall`/`FirewallThread` (`sw_firewall_msg5`, `msg7`, `msg8`, `firewallthread_msg1`), gemessen mit `FirewallWindowRun` | neu (F4.6g3): die Firewall eines Rechners — Reiter „Konfiguration“ und „Log“, die drei Kästchen (ICMP und UDP nur bei eingeschalteter Firewall), Port mit Protokoll und Absendern, „Hinzufügen“ (das Feld wird danach geleert), „Löschen“ der gewählten Zeile, die Tabelle; das Protokoll in Javas Worten. Besser als Java: ein Port, der keiner ist, wird rot markiert und nicht übergeben; Javas Erklärtext, den Java schreibt, aber nie zeigt, steht unter den Kästchen; eine Regel, die das Fenster nicht machen kann (aus einer Datei: verwerfend oder mit Adresse), zeigt in der Absenderspalte, was sie ist (Java zeigt sie als offenen Port für alle Absender); ins Protokoll kann man nicht tippen |
| `src/desktop/GnutellaPanel.tsx`, `src/desktop/DesktopWindow.tsx`, `src/assets/desktop/peertopeer_*.png`, `src/main.tsx` (`filiusDev.writeFile`) | Java `GUIApplicationPeerToPeerAnwendungWindow` (`peertopeeranwendung_msg1`–`31`), Javas Symbole der Reiter, gemessen mit `GnutellaWindowRun` | neu (F4.6g4): Gnutella — vier Reiter mit Javas Symbolen: Netzwerk (Adresse, anfangs Javas 192.168.0.1, rot, solange sie keine ist; „Netz beitreten“, Tabelle der Nachbarn, „Netz verlassen“), Suche (Suchbegriff, „Suchen“, „Stopp!“, Ergebnistabelle mit Wahl, „Herunterladen“, „Liste leeren“), Dateien, Einstellungen (Zahl der Teilnehmer, „Speichern“). Besser als Java: die Tabellen zeigen sofort, was das Programm hält (Javas Ergebnisse zeigen nach einer neuen Suche die alten, bis ein Treffer kommt; seine Dateien nur, was beim letzten Bescheid des Programms da war); jede ganze Zahl wird genommen; was nicht genommen wird, steht unter dem Feld statt in einem Dialog; „Herunterladen“ nur mit gewähltem Ergebnis. Die Dev-Seite schreibt mit `writeFile` eine Datei wie eine andere Person |
| `src/Editor.tsx`, `src/canvas/Canvas.tsx` (Aktionsmodus) | Java `GUIContainer.setActiveSite/showDesktop/closeDialogs/reopenDialogs`, `GUIKnotenItem` (Klick im Aktionsmodus), `GUIEvents.kontextMenueAktionsmodus` (`guievents_msg3`), `JCablePanel` (`farbeBlinken`) | erweitert (F4.6a): im Aktionsmodus ist der Entwurf gesperrt wie bei Java — keine Palette, kein Blatt, nichts bewegt sich, kein Rückgängig, kein Löschen —, Kabel mit Rahmen leuchten grün, ein Tipp auf einen Rechner öffnet seinen Desktop (Langdruck: „Desktop anzeigen“); offene Desktops überstehen den Entwurf und erscheinen beim nächsten Start wieder, die eines entfernten Rechners nicht | | `src/Editor.tsx`, `src/canvas/Canvas.tsx` (Aktionsmodus) | Java `GUIContainer.setActiveSite/showDesktop/closeDialogs/reopenDialogs`, `GUIKnotenItem` (Klick im Aktionsmodus), `GUIEvents.kontextMenueAktionsmodus` (`guievents_msg3`), `JCablePanel` (`farbeBlinken`) | erweitert (F4.6a): im Aktionsmodus ist der Entwurf gesperrt wie bei Java — keine Palette, kein Blatt, nichts bewegt sich, kein Rückgängig, kein Löschen —, Kabel mit Rahmen leuchten grün, ein Tipp auf einen Rechner öffnet seinen Desktop (Langdruck: „Desktop anzeigen“); offene Desktops überstehen den Entwurf und erscheinen beim nächsten Start wieder, die eines entfernten Rechners nicht |
| `browser/simulation.spec.ts`, `test/programs.test.ts` | — | neu (F4.6a): Moduswechsel, Sperren, Geschwindigkeit, Kabelaktivität, Desktop mit Titel, Programmen, Netzwerk-Info, Verschieben und Stapeln, Handy, Englisch; die Programmliste gegen Javas `Desktop_*.txt`; seit F4.6b die Befehlszeile (Ping über drei Router, Verlauf, Chips, Strg+C, Behalten, exit) und der Fehlerfall des Raums; seit F4.6c die Software-Installation (sofort lauffähig, Doppelklick, Abwählen) | | `browser/simulation.spec.ts`, `test/programs.test.ts` | — | neu (F4.6a): Moduswechsel, Sperren, Geschwindigkeit, Kabelaktivität, Desktop mit Titel, Programmen, Netzwerk-Info, Verschieben und Stapeln, Handy, Englisch; die Programmliste gegen Javas `Desktop_*.txt`; seit F4.6b die Befehlszeile (Ping über drei Router, Verlauf, Chips, Strg+C, Behalten, exit) und der Fehlerfall des Raums; seit F4.6c die Software-Installation (sofort lauffähig, Doppelklick, Abwählen) |
| `browser/web.spec.ts`, `browser/fixtures/web-pages.fls`, `test/webPagesFixture.test.ts`, `test/serverLines.test.ts`, `src/simulation/useSimulation.ts` (`views`), `src/Editor.tsx` (Sprache beim Start), `src/desktop/DesktopWindow.tsx` | — | neu (F4.6d): Browser und Webserver in Chromium auf einer eigenen Projektdatei (mit den Befehlen des Dokuments gebaut und bei jedem Lauf verglichen): Adresszeile, Titel, Sandbox, Bilder, Marke, Links im Ordner, beide Formulare bis in die Zeilen des Servers, Fehlerseite, nicht erreichbarer Server, Knopf, Kästchen und Tabelle, die Routerseiten auf Englisch; die Zeilen des Servers gegen Javas Fenster | | `browser/web.spec.ts`, `browser/fixtures/web-pages.fls`, `test/webPagesFixture.test.ts`, `test/serverLines.test.ts`, `src/simulation/useSimulation.ts` (`views`), `src/Editor.tsx` (Sprache beim Start), `src/desktop/DesktopWindow.tsx` | — | neu (F4.6d): Browser und Webserver in Chromium auf einer eigenen Projektdatei (mit den Befehlen des Dokuments gebaut und bei jedem Lauf verglichen): Adresszeile, Titel, Sandbox, Bilder, Marke, Links im Ordner, beide Formulare bis in die Zeilen des Servers, Fehlerseite, nicht erreichbarer Server, Knopf, Kästchen und Tabelle, die Routerseiten auf Englisch; die Zeilen des Servers gegen Javas Fenster |
@@ -229,6 +233,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `browser/echo.spec.ts`, `test/serverLines.test.ts` (Echo) | — | neu (F4.6g1): Echo in Chromium — im Lauf installiert, ein Port, der keiner ist, markiert, Server gestartet, Client verbunden, Felder gesperrt, Nachricht gesendet und zurückbekommen, Trennen; die Zeilen beider Fenster gegen Javas | | `browser/echo.spec.ts`, `test/serverLines.test.ts` (Echo) | — | neu (F4.6g1): Echo in Chromium — im Lauf installiert, ein Port, der keiner ist, markiert, Server gestartet, Client verbunden, Felder gesperrt, Nachricht gesendet und zurückbekommen, Trennen; die Zeilen beider Fenster gegen Javas |
| `browser/dns.spec.ts` | — | neu (F4.6g2): der DNS-Server in Chromium auf `email_komplett` — Knopf, Reiter, eigene Adresse im Feld, Javas Beispiel bei Ungültigem, Eintrag hinzugefügt, Zelle geschrieben und eine abgelehnt, Zeile entfernt, MX, Kästchen, Beenden; die Befehlszeile des Web-Servers löst den neuen Namen auf | | `browser/dns.spec.ts` | — | neu (F4.6g2): der DNS-Server in Chromium auf `email_komplett` — Knopf, Reiter, eigene Adresse im Feld, Javas Beispiel bei Ungültigem, Eintrag hinzugefügt, Zelle geschrieben und eine abgelehnt, Zeile entfernt, MX, Kästchen, Beenden; die Befehlszeile des Web-Servers löst den neuen Namen auf |
| `browser/firewall.spec.ts`, `test/serverLines.test.ts` (Firewall) | — | neu (F4.6g3): die Firewall in Chromium auf `email_komplett`, im Lauf auf dem Web-Server installiert — Javas Kästchen, ein Port abgelehnt und einer hinzugefügt, der Browser eines Notebooks durch die Regel gelassen, ICMP verboten und dessen Ping verworfen (beides im Protokoll), die Firewall aus (die Kästchen darunter gesperrt), die Regel gewählt und gelöscht; die Zeilen des Protokolls und der Tabelle Wort für Wort wie Javas | | `browser/firewall.spec.ts`, `test/serverLines.test.ts` (Firewall) | — | neu (F4.6g3): die Firewall in Chromium auf `email_komplett`, im Lauf auf dem Web-Server installiert — Javas Kästchen, ein Port abgelehnt und einer hinzugefügt, der Browser eines Notebooks durch die Regel gelassen, ICMP verboten und dessen Ping verworfen (beides im Protokoll), die Firewall aus (die Kästchen darunter gesperrt), die Regel gewählt und gelöscht; die Zeilen des Protokolls und der Tabelle Wort für Wort wie Javas |
| `browser/gnutella.spec.ts` | — | neu (F4.6g4): Gnutella in Chromium auf `email_komplett`, im Lauf auf zwei Notebooks installiert — vier Reiter, eine Adresse mit Javas Worten abgelehnt, ein Nachbar beigetreten, eine Datei des anderen gefunden und heruntergeladen (der Reiter Dateien zeigt sie), „Liste leeren“ hebt die Wahl auf, die Zahl der Teilnehmer gespeichert (12, die Java ablehnt; was keine Zahl ist, mit Javas Worten abgelehnt), das Netz verlassen |
| `src/sheet/ServicePanels.tsx`, `src/sheet/RowsEditor.tsx`, `src/sheet/SheetTabs.tsx`, `src/sheet/DeviceSheet.tsx` (Seiten) | Java `JDHCPKonfiguration` (`jdhcpkonfiguration_msg1`–`15`), `JPortForwardingDialog` (`jportforwarding_msg4`–`7`, `jgatewayconfiguration_msg19`) | neu (F4.5c): Javas Dialoge als Seiten des Blatts mit „Zurück“ — DHCP-Server (Grundeinstellungen, statische Adresszuweisung; beim Rechner, gesperrt während DHCP die Karte einrichtet, und auf dem LAN-Reiter des Heimrouters), Portfreigaben (Heimrouter); jedes Feld beim Verlassen übernommen statt mit Javas OK-Knopf. Die Zeilentabellen (Weiterleitung, Zuordnungen, Portfreigaben) teilen sich `RowsEditor`: eine neue Zeile, sobald alle Felder gültig sind, Zeile wählen, bearbeiten, löschen. Javas Tooltip zu „Manuelle Einstellungen“ als Text unter dem Kästchen (ein Finger hat kein Überfahren) | | `src/sheet/ServicePanels.tsx`, `src/sheet/RowsEditor.tsx`, `src/sheet/SheetTabs.tsx`, `src/sheet/DeviceSheet.tsx` (Seiten) | Java `JDHCPKonfiguration` (`jdhcpkonfiguration_msg1`–`15`), `JPortForwardingDialog` (`jportforwarding_msg4`–`7`, `jgatewayconfiguration_msg19`) | neu (F4.5c): Javas Dialoge als Seiten des Blatts mit „Zurück“ — DHCP-Server (Grundeinstellungen, statische Adresszuweisung; beim Rechner, gesperrt während DHCP die Karte einrichtet, und auf dem LAN-Reiter des Heimrouters), Portfreigaben (Heimrouter); jedes Feld beim Verlassen übernommen statt mit Javas OK-Knopf. Die Zeilentabellen (Weiterleitung, Zuordnungen, Portfreigaben) teilen sich `RowsEditor`: eine neue Zeile, sobald alle Felder gültig sind, Zeile wählen, bearbeiten, löschen. Javas Tooltip zu „Manuelle Einstellungen“ als Text unter dem Kästchen (ein Finger hat kein Überfahren) |
| `src/sheet/RouterSheets.tsx`, `src/sheet/NameField.tsx`, `src/canvas/Canvas.tsx` (`highlightedLink`), `src/theme/tokens.css` (`--canvas-cable-active`) | Java `JVermittlungsrechnerKonfiguration`, `JGatewayConfiguration`, `JWeiterleitungsTabelle` (Wörter `jvermittlungsrechnerkonfiguration_msg6`–`26`, `jweiterleitungstabelle_msg3`–`6`), `JCablePanel.farbeBlinken`, `Kabel.setAktiv` | neu (F4.5b): Blatt des Vermittlungsrechners — Reiter Allgemein (Name, Gateway, Automatisches Routing, IP-Weiterleitung nur angezeigt), je Karte (Reiter mit ihrer Adresse, „Verbunden mit …“, Adresse, Maske, MAC) und Weiterleitungstabelle (alle Einträge oder nur die manuellen, Javas Zeilen grau, eine neue Zeile, die mit vier gültigen Feldern übernommen wird, Zelle bearbeiten, Eintrag löschen; bei RIP gesperrt); Blatt des Heimrouters mit Allgemein, LAN, WAN (DHCP sperrt die WAN-Adresse). Das Kabel der offenen Karte leuchtet grün wie bei Java. Javas „Eintrag Löschen“ klein geschrieben | | `src/sheet/RouterSheets.tsx`, `src/sheet/NameField.tsx`, `src/canvas/Canvas.tsx` (`highlightedLink`), `src/theme/tokens.css` (`--canvas-cable-active`) | Java `JVermittlungsrechnerKonfiguration`, `JGatewayConfiguration`, `JWeiterleitungsTabelle` (Wörter `jvermittlungsrechnerkonfiguration_msg6`–`26`, `jweiterleitungstabelle_msg3`–`6`), `JCablePanel.farbeBlinken`, `Kabel.setAktiv` | neu (F4.5b): Blatt des Vermittlungsrechners — Reiter Allgemein (Name, Gateway, Automatisches Routing, IP-Weiterleitung nur angezeigt), je Karte (Reiter mit ihrer Adresse, „Verbunden mit …“, Adresse, Maske, MAC) und Weiterleitungstabelle (alle Einträge oder nur die manuellen, Javas Zeilen grau, eine neue Zeile, die mit vier gültigen Feldern übernommen wird, Zelle bearbeiten, Eintrag löschen; bei RIP gesperrt); Blatt des Heimrouters mit Allgemein, LAN, WAN (DHCP sperrt die WAN-Adresse). Das Kabel der offenen Karte leuchtet grün wie bei Java. Javas „Eintrag Löschen“ klein geschrieben |
+74 -9
View File
@@ -155,6 +155,28 @@ export function gnutellaPacket(text: string): GnutellaPacket | undefined {
return undefined; return undefined;
} }
/**
* A search result as the window's table shows it (updateErgebnisTabelle):
* the address before its first "/", the name up to the last ":" of what
* follows, the size after it (" 8 B", with Java's space).
*/
export function gnutellaResultRow(result: string): readonly [string, string, string] {
const [address = '', rest = ''] = result.split('/').filter((part) => part !== '');
const colon = rest.lastIndexOf(':');
return [address, colon < 0 ? rest : rest.slice(0, colon), colon < 0 ? '' : rest.slice(colon + 1)];
}
/**
* The number of peers typed into the window's settings, as it takes it: a
* whole number of Java's int; undefined for anything else. Java takes a
* single digit only (its pattern musterNurZahl is "\d"): 12 is refused.
*/
export function gnutellaPeerCountOfText(text: string): number | undefined {
if (!/^[0-9]+$/.test(text)) return undefined;
const count = Number(text);
return count <= 2_147_483_647 ? count : undefined;
}
/** What the program tells its window: its known peers, or its search results ("address/name: size B"), whenever they change. */ /** What the program tells its window: its known peers, or its search results ("address/name: size B"), whenever they change. */
export type GnutellaLogEntry = { readonly kind: 'peers'; readonly peers: readonly string[] } | { readonly kind: 'results'; readonly results: readonly string[] }; export type GnutellaLogEntry = { readonly kind: 'peers'; readonly peers: readonly string[] } | { readonly kind: 'results'; readonly results: readonly string[] };
@@ -221,7 +243,9 @@ export class GnutellaPeer {
readonly log: ProgramLog<GnutellaLogEntry>; readonly log: ProgramLog<GnutellaLogEntry>;
private peersValue: string[] = []; private peersValue: string[] = [];
private resultsValue: string[] = []; private resultsValue: string[] = [];
private readonly maximumKnownPeers: number; private maximumKnownPeersValue: number;
/** The connections of the program's own pings, queries and downloads (mitarbeiterEigeneAnfragen) while they are open. */
private readonly ownSockets = new Set<Id>();
/** /**
* The ids of the program's own pings and queries (eigeneAnfragen), and of * The ids of the program's own pings and queries (eigeneAnfragen), and of
* those it sent on (schonmalVerschicktListe). Java also keeps others' * those it sent on (schonmalVerschicktListe). Java also keeps others'
@@ -238,7 +262,7 @@ export class GnutellaPeer {
configuration: GnutellaConfiguration, configuration: GnutellaConfiguration,
) { ) {
this.log = new ProgramLog(engine); this.log = new ProgramLog(engine);
this.maximumKnownPeers = configuration.maximumKnownPeers; this.maximumKnownPeersValue = configuration.maximumKnownPeers;
this.server = new GnutellaServer(engine, nodeId, this); this.server = new GnutellaServer(engine, nodeId, this);
} }
@@ -252,6 +276,26 @@ export class GnutellaPeer {
return this.resultsValue; return this.resultsValue;
} }
/** How many peers the program keeps at most (getMaxTeilnehmerZahl). */
get maximumKnownPeers(): number {
return this.maximumKnownPeersValue;
}
/** setMaxTeilnehmerZahl: for the peers to come; those known stay. */
setMaximumKnownPeers(count: number): void {
this.maximumKnownPeersValue = count;
}
/**
* abbrechenSuche: every connection of the program's own requests closed —
* its searches' (hits still on their way are lost), its joins', its
* downloads' — as Java ends each worker and closes its socket.
*/
cancelSearch(): void {
// Each leaves the set when its thread ends.
for (const socketId of this.ownSockets) this.engine.requestTcpClose(socketId);
}
/** The node's first address (Betriebssystem.primaryIPAdresse). */ /** The node's first address (Betriebssystem.primaryIPAdresse). */
private get ownAddress(): string { private get ownAddress(): string {
return this.engine.networkInterfaces(this.nodeId)[0]?.ipAddress ?? ''; return this.engine.networkInterfaces(this.nodeId)[0]?.ipAddress ?? '';
@@ -313,7 +357,7 @@ export class GnutellaPeer {
/** hinzuTeilnehmer: a new peer while there is room. */ /** hinzuTeilnehmer: a new peer while there is room. */
private addPeer(address: string): void { private addPeer(address: string): void {
if (this.peersValue.includes(address) || this.peersValue.length >= this.maximumKnownPeers) return; if (this.peersValue.includes(address) || this.peersValue.length >= this.maximumKnownPeersValue) return;
this.peersValue = [...this.peersValue, address]; this.peersValue = [...this.peersValue, address];
this.log.write({ kind: 'peers', peers: this.peersValue }); this.log.write({ kind: 'peers', peers: this.peersValue });
} }
@@ -420,8 +464,10 @@ export class GnutellaPeer {
private *fetchFile(owner: string, name: string): Steps { private *fetchFile(owner: string, name: string): Steps {
const engine = this.engine; const engine = this.engine;
const socketId = engine.openTcpClientSocket(this.nodeId, owner, GNUTELLA_PORT); const socketId = engine.openTcpClientSocket(this.nodeId, owner, GNUTELLA_PORT);
if (socketId === undefined || (yield* engine.connectTcpSteps(socketId)) !== 'connected') { if (socketId === undefined) return;
if (socketId !== undefined) engine.requestTcpClose(socketId); this.ownSockets.add(socketId);
if ((yield* engine.connectTcpSteps(socketId)) !== 'connected') {
this.closeOwn(socketId);
return; return;
} }
yield* engine.sendTcpSteps(socketId, new TextEncoder().encode(formatHttpRequest({ method: HTTP_GET, path: name, host: owner }))); yield* engine.sendTcpSteps(socketId, new TextEncoder().encode(formatHttpRequest({ method: HTTP_GET, path: name, host: owner })));
@@ -431,7 +477,13 @@ export class GnutellaPeer {
const path = `${GNUTELLA_FOLDER}/${name}`; const path = `${GNUTELLA_FOLDER}/${name}`;
this.keepFile(path, fileContentOfJavaType(response.contentType ?? '', response.body ?? '', path)); this.keepFile(path, fileContentOfJavaType(response.contentType ?? '', response.body ?? '', path));
} }
engine.requestTcpClose(socketId); this.closeOwn(socketId);
}
/** A connection closed, and no longer one of the program's own (it may be so already). */
private closeOwn(socketId: Id): void {
this.engine.requestTcpClose(socketId);
this.ownSockets.delete(socketId);
} }
/** speicherDatei: over a file of that name in any case, keeping its name (as the file system does). */ /** speicherDatei: over a file of that name in any case, keeping its name (as the file system does). */
@@ -461,6 +513,17 @@ export class GnutellaPeer {
}); });
} }
/** The files of /peer2peer as the window's table shows them (updateDateiTabelle): name and type. */
fileRows(): readonly (readonly [string, string])[] {
const files = this.engine.state.topologySnapshot.fileSystemsByNodeId.get(this.nodeId);
if (files === undefined || !files.contains(GNUTELLA_FOLDER)) return [];
return files.entriesInCreationOrder(GNUTELLA_FOLDER).flatMap((entry) => {
const content = entry.content;
const name = entry.path.slice(entry.path.lastIndexOf('/') + 1);
return content.kind === 'directory' ? [] : [[name, javaFileTypeName(name, content)] as const];
});
}
/** A file at the path, found in any case (Dateisystem.holeDatei); none for a folder or nothing. */ /** A file at the path, found in any case (Dateisystem.holeDatei); none for a folder or nothing. */
private sharedFile(path: string): { readonly path: string; readonly name: string; readonly content: Exclude<VirtualFileContent, { kind: 'directory' }> } | undefined { private sharedFile(path: string): { readonly path: string; readonly name: string; readonly content: Exclude<VirtualFileContent, { kind: 'directory' }> } | undefined {
const files = this.engine.state.topologySnapshot.fileSystemsByNodeId.get(this.nodeId); const files = this.engine.state.topologySnapshot.fileSystemsByNodeId.get(this.nodeId);
@@ -480,8 +543,10 @@ export class GnutellaPeer {
private *sendTo(address: string, packet: GnutellaPacket): Steps { private *sendTo(address: string, packet: GnutellaPacket): Steps {
const engine = this.engine; const engine = this.engine;
const socketId = engine.openTcpClientSocket(this.nodeId, address, GNUTELLA_PORT); const socketId = engine.openTcpClientSocket(this.nodeId, address, GNUTELLA_PORT);
if (socketId === undefined || (yield* engine.connectTcpSteps(socketId)) !== 'connected') { if (socketId === undefined) return;
if (socketId !== undefined) engine.requestTcpClose(socketId); if (this.ownRequests.has(packet.guid)) this.ownSockets.add(socketId);
if ((yield* engine.connectTcpSteps(socketId)) !== 'connected') {
this.closeOwn(socketId);
return; return;
} }
yield* engine.sendTcpSteps(socketId, new TextEncoder().encode(gnutellaText(packet))); yield* engine.sendTcpSteps(socketId, new TextEncoder().encode(gnutellaText(packet)));
@@ -492,6 +557,6 @@ export class GnutellaPeer {
if (answer?.kind === 'pong') this.takePong(answer); if (answer?.kind === 'pong') this.takePong(answer);
else if (answer?.kind === 'queryHit') this.takeQueryHit(answer); else if (answer?.kind === 'queryHit') this.takeQueryHit(answer);
} }
engine.requestTcpClose(socketId); this.closeOwn(socketId);
} }
} }
+838
View File
@@ -0,0 +1,838 @@
{
"steps": [
{
"step": "open",
"address": {
"text": "192.168.0.1",
"red": false
},
"peers": [],
"search": "",
"results": [],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [],
"programResults": [],
"programFiles": [],
"dialogs": []
},
{
"step": "type an address that is none",
"address": {
"text": "192.168.0.300",
"red": true
},
"peers": [],
"search": "",
"results": [],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [],
"programResults": [],
"programFiles": [],
"dialogs": []
},
{
"step": "join with an address that is none",
"address": {
"text": "192.168.0.300",
"red": true
},
"peers": [],
"search": "",
"results": [],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [],
"programResults": [],
"programFiles": [],
"dialogs": [
"Dies ist keine gültige IP-Adresse"
]
},
{
"step": "join itself",
"address": {
"text": "192.168.0.20",
"red": false
},
"peers": [],
"search": "",
"results": [],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [],
"programResults": [],
"programFiles": [],
"dialogs": []
},
{
"step": "join 192.168.0.30",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "",
"results": [],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [],
"programFiles": [],
"dialogs": []
},
{
"step": "search woche",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "woche",
"results": [
[
"10.0.0.2",
"Lied der Woche.txt",
" 3 B"
]
],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"10.0.0.2/Lied der Woche.txt: 3 B"
],
"programFiles": [],
"dialogs": []
},
{
"step": "download row 0",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "woche",
"results": [
[
"10.0.0.2",
"Lied der Woche.txt",
" 3 B"
]
],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"10.0.0.2/Lied der Woche.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "download row 0 again",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "woche",
"results": [
[
"10.0.0.2",
"Lied der Woche.txt",
" 3 B"
]
],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"10.0.0.2/Lied der Woche.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "search xyz",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "xyz",
"results": [
[
"10.0.0.2",
"Lied der Woche.txt",
" 3 B"
]
],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "empty the list",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "xyz",
"results": [],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "search notiz and abort at once",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [],
"files": [],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "search notiz again",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "download with no row chosen",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "files tab",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "1",
"red": false
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "type a number of peers that is none",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "abc",
"red": true
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "take a number of peers that is none",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "abc",
"red": true
},
"programMaximum": 1,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": [
"Es sind nur Zahlen erlaubt!"
]
},
{
"step": "take 0 peers",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "0",
"red": false
},
"programMaximum": 0,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "take a number beyond Java's int",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "99999999999",
"red": true
},
"programMaximum": 0,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": [
"Es sind nur Zahlen erlaubt!"
]
},
{
"step": "take 12 peers",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "12",
"red": true
},
"programMaximum": 0,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": [
"Es sind nur Zahlen erlaubt!"
]
},
{
"step": "take 4 peers",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [
[
"1",
"192.168.0.30"
]
],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "4",
"red": false
},
"programMaximum": 4,
"programPeers": [
"192.168.0.30"
],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "leave the network",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "4",
"red": false
},
"programMaximum": 4,
"programPeers": [],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
]
],
"dialogs": []
},
{
"step": "a file added beside the window",
"address": {
"text": "192.168.0.30",
"red": false
},
"peers": [],
"search": "notiz",
"results": [
[
"192.168.0.31",
"notizen.txt",
" 3 B"
]
],
"files": [
[
"Lied der Woche.txt",
"txt"
]
],
"maximum": {
"text": "4",
"red": false
},
"programMaximum": 4,
"programPeers": [],
"programResults": [
"192.168.0.31/notizen.txt: 3 B"
],
"programFiles": [
[
"Lied der Woche.txt",
"txt",
"La la la"
],
[
"neu.txt",
"txt",
"neu"
]
],
"dialogs": []
}
]
}
+251
View File
@@ -0,0 +1,251 @@
// The Gnutella program as Java's window uses it, against Java's
// (gnutellaWindow.java.json, GnutellaWindowRun) on terminal-net beside the
// line of GnutellaRun's story "search": Java's steps played on the engine's
// program — joining (not with what is no address, nor itself), searching,
// "Stopp!", downloading, emptying the list, the number of peers, leaving —
// and after each what the program keeps (its peers, results, files of
// /peer2peer, number of peers) and what the window's tables show. Where Java
// errs, what the program and window do instead (named at each step): the
// tables show what the program keeps at once, a number of peers is not one
// digit only, a hit names a text's size in characters (gnutellaJava.test.ts).
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { GNUTELLA_FOLDER, gnutellaPeerCountOfText, gnutellaResultRow, IP_ADDRESS_PATTERN, javaFileTypeName, NetworkRuntimeEngine, VirtualFileSystem, withSnapshot } from '../src/index.js';
import { snapshotOf, type Scenario } from './scenario.js';
interface JavaStep {
readonly step: string;
readonly address: { readonly text: string; readonly red: boolean };
readonly peers: readonly (readonly string[])[];
readonly results: readonly (readonly string[])[];
readonly files: readonly (readonly string[])[];
readonly maximum: { readonly text: string; readonly red: boolean };
readonly programMaximum: number;
readonly programPeers: readonly string[];
readonly programResults: readonly string[];
readonly programFiles: readonly (readonly string[])[];
readonly dialogs: readonly string[];
}
const here = dirname(fileURLToPath(import.meta.url));
const java = JSON.parse(readFileSync(join(here, 'gnutellaWindow.java.json'), 'utf8')) as { readonly steps: readonly JavaStep[] };
const scenario = JSON.parse(readFileSync(join(here, 'scenarios/terminal-net.scenario.json'), 'utf8')) as Scenario;
const CLIENT = '00000000-0000-0000-0000-000000000401';
const WEB = '00000000-0000-0000-0000-000000000403';
const ECHO = '00000000-0000-0000-0000-000000000404';
const FAR = '00000000-0000-0000-0000-000000000406';
const PEERS = [CLIENT, WEB, ECHO, FAR];
/** Each shared text's size in characters, as the program names it in a hit (Java decodes the text as Base64: "La la la" and "Notiz" are 3 B). */
const CHARACTERS: Readonly<Record<string, number>> = { 'Lied der Woche.txt': 8, 'notizen.txt': 5 };
const inCharacters = (text: string) => text.replace(/([^/]+): 3 B$/, (_, name: string) => `${name}: ${CHARACTERS[name]} B`);
function setUp() {
const files = new Map(PEERS.map((host) => [host, new VirtualFileSystem()]));
for (const host of [ECHO, FAR]) files.get(host)!.createDirectory(GNUTELLA_FOLDER);
files.get(ECHO)!.writeTextFile(`${GNUTELLA_FOLDER}/notizen.txt`, 'Notiz');
files.get(FAR)!.writeTextFile(`${GNUTELLA_FOLDER}/Lied der Woche.txt`, 'La la la');
const engine = new NetworkRuntimeEngine(scenario.seed);
engine.handle({
type: 'start',
snapshot: withSnapshot(snapshotOf(scenario), {
gnutellaByNodeId: new Map(PEERS.map((host) => [host, { maximumKnownPeers: host === CLIENT || host === FAR ? 1 : 2 }])),
fileSystemsByNodeId: files,
}),
seed: scenario.seed,
initialTimeMilliseconds: 0,
});
const run = (milliseconds: number) => engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + milliseconds });
return { engine, files, run };
}
describe("the Gnutella window, against Java's", () => {
it("plays Java's steps: the same where Java is right, as named where it errs", () => {
const { engine, files, run } = setUp();
const program = engine.gnutella(CLIENT)!;
const folder = files.get(CLIENT)!;
const kept = () => ({
peers: program.peers,
results: program.results,
maximum: program.maximumKnownPeers,
files: folder.contains(GNUTELLA_FOLDER)
? folder.entriesInCreationOrder(GNUTELLA_FOLDER).map((entry) => {
const content = entry.content;
if (content.kind !== 'text') throw new Error(`${entry.path} is no text`);
const name = entry.path.slice(GNUTELLA_FOLDER.length + 1);
return [name, javaFileTypeName(name, content), content.text];
})
: [],
});
const at = (name: string) => java.steps.find((step) => step.step === name)!;
const javaKept = (step: JavaStep) => ({ peers: step.programPeers, results: step.programResults.map(inCharacters), maximum: step.programMaximum, files: step.programFiles });
const check = (name: string) => expect(kept(), name).toEqual(javaKept(at(name)));
/** The window's tables as they show what the program keeps (Java's lag behind it, named where they do). */
const tables = () => ({
peers: program.peers.map((peer, index) => [String(index + 1), peer]),
results: program.results.map((result) => [...gnutellaResultRow(result)]),
files: kept().files.map(([name, type]) => [name, type]),
});
const javaTables = (step: JavaStep) => ({ peers: step.peers, results: step.results.map(([address, name]) => [address, name, ` ${CHARACTERS[name!]} B`]), files: step.files });
const join = (address: string) => {
if (IP_ADDRESS_PATTERN.test(address)) program.join(address);
};
// The others form their line first, as Java's did (the window opens 5.5 s in).
run(500);
engine.gnutella(WEB)!.join('192.168.0.31');
run(2500);
engine.gnutella(FAR)!.join('192.168.0.31');
run(2500);
check('open');
expect(at('open').address).toEqual({ text: '192.168.0.1', red: false });
expect(at('open').maximum).toEqual({ text: '1', red: false });
// What is no address: red, and not taken (Java's message).
expect([IP_ADDRESS_PATTERN.test('192.168.0.300'), at('type an address that is none').address.red]).toEqual([false, true]);
join('192.168.0.300');
run(1000);
check('join with an address that is none');
expect(at('join with an address that is none').dialogs).toEqual(['Dies ist keine gültige IP-Adresse']);
join('192.168.0.20');
run(2500);
check('join itself');
join('192.168.0.30');
run(3000);
check('join 192.168.0.30');
expect(tables(), 'join 192.168.0.30').toEqual(javaTables(at('join 192.168.0.30')));
program.search('woche');
run(3000);
check('search woche');
expect(tables()).toEqual(javaTables(at('search woche')));
program.download(0);
run(3000);
check('download row 0');
// The file's table shows it at once (Java's only when the program next tells the window of a change).
expect(at('download row 0').files).toEqual([]);
expect(tables().files).toEqual([['Lied der Woche.txt', 'txt']]);
program.download(0);
run(3000);
check('download row 0 again');
program.search('xyz');
run(3000);
check('search xyz');
// The table shows no result, as the program keeps none (Java's still shows the last search's).
expect([at('search xyz').results.length, tables().results]).toEqual([1, []]);
program.clearResults();
run(500);
check('empty the list');
// "Stopp!" half a second after "Suchen" (as Java's press came): the hits on their way are lost; a new search finds them.
program.search('notiz');
run(500);
program.cancelSearch();
run(3000);
check('search notiz and abort at once');
program.search('notiz');
run(3000);
check('search notiz again');
expect(tables()).toEqual(javaTables(at('search notiz again')));
run(2000);
check('download with no row chosen');
check('files tab');
// The number of peers: what is no number refused (Java's message), 0 taken; 12 taken too (Java takes one digit only).
expect(['abc', '0', '99999999999', '12', '4', '-1', '', '2147483647', '2147483648'].map(gnutellaPeerCountOfText)).toEqual([
undefined,
0,
undefined,
12,
4,
undefined,
undefined,
2_147_483_647,
undefined,
]);
expect([at('take a number of peers that is none').dialogs, at('take 12 peers').dialogs]).toEqual([['Es sind nur Zahlen erlaubt!'], ['Es sind nur Zahlen erlaubt!']]);
program.setMaximumKnownPeers(0);
check('take 0 peers');
check("take a number beyond Java's int");
program.setMaximumKnownPeers(12);
expect(program.maximumKnownPeers).toBe(12);
program.setMaximumKnownPeers(4);
check('take 4 peers');
program.reset();
run(1000);
check('leave the network');
expect(tables().peers).toEqual([]);
// A file added beside the window: its table shows it at once (Java's not).
folder.writeTextFile(`${GNUTELLA_FOLDER}/neu.txt`, 'neu');
check('a file added beside the window');
expect([at('a file added beside the window').files.length, tables().files.length]).toEqual([1, 2]);
});
});
describe('the Gnutella program beyond the steps', () => {
it('splits a result as Java’s table does', () => {
expect(['10.0.0.2/Lied der Woche.txt: 8 B', '10.0.0.2/a:b.txt: 3 B', '//x/y', 'kein'].map(gnutellaResultRow)).toEqual([
['10.0.0.2', 'Lied der Woche.txt', ' 8 B'],
['10.0.0.2', 'a:b.txt', ' 3 B'],
['x', 'y', ''],
['kein', '', ''],
]);
});
it('keeps the peers it knows when the number shrinks, and takes no more', () => {
const { engine, run } = setUp();
const program = engine.gnutella(WEB)!;
program.setMaximumKnownPeers(1);
program.join('192.168.0.31');
run(3000);
engine.gnutella(CLIENT)!.join('192.168.0.30');
run(3000);
expect(program.peers).toEqual(['192.168.0.31']);
program.setMaximumKnownPeers(0);
expect(program.peers).toEqual(['192.168.0.31']);
});
it("cancels only the program's own requests: a query it passes on still brings its hits back", () => {
const { engine, run } = setUp();
engine.gnutella(WEB)!.join('192.168.0.31');
run(3000);
engine.gnutella(CLIENT)!.join('192.168.0.30');
run(3000);
engine.gnutella(CLIENT)!.search('notiz');
// .30 has the query (after about 330 ms) and has sent it on to .31 (about 390 ms) when it cancels its own requests
// (its join's connection): the hits on their way back through it still come.
run(450);
engine.gnutella(WEB)!.cancelSearch();
run(3000);
expect(engine.gnutella(CLIENT)!.results).toEqual(['192.168.0.31/notizen.txt: 5 B']);
});
it('shows the files of /peer2peer, not its folders, as Java lists them', () => {
const { engine, files } = setUp();
files.get(ECHO)!.createDirectory(`${GNUTELLA_FOLDER}/ordner`);
expect(engine.gnutella(ECHO)!.fileRows()).toEqual([['notizen.txt', 'txt']]);
expect(engine.gnutella(CLIENT)!.fileRows()).toEqual([]);
});
it('cancels the downloads too, and a cancel without requests changes nothing', () => {
const { engine, files, run } = setUp();
const program = engine.gnutella(CLIENT)!;
program.cancelSearch();
program.join('192.168.0.31');
run(3000);
program.search('notiz');
run(3000);
expect(program.results).toEqual(['192.168.0.31/notizen.txt: 5 B']);
program.download(0);
program.cancelSearch();
run(3000);
expect(files.get(CLIENT)!.contains(`${GNUTELLA_FOLDER}/notizen.txt`)).toBe(false);
program.download(0);
run(3000);
expect(files.get(CLIENT)!.textFile(`${GNUTELLA_FOLDER}/notizen.txt`)).toBe('Notiz');
});
});
+112
View File
@@ -0,0 +1,112 @@
// Gnutella on two notebooks' desktops in Chromium (F4.6g4), on FILIUS'
// example email_komplett with the program installed during the run: Java's
// four tabs, an address refused with Java's words, a peer joined, a file
// found on the other notebook and downloaded (the files tab shows it), the
// number of peers saved (also one Java refuses, and what is no number), the
// network left.
import { expect, test, type Locator, type Page } from '@playwright/test';
import { drawing, onDevice, openEditor, serveEditor } from './editorPage.js';
let server: { readonly base: string; readonly close: () => Promise<void> };
test.beforeAll(async () => {
server = await serveEditor();
});
test.afterAll(async () => {
await server.close();
});
async function idOf(page: Page, name: string) {
return (await drawing(page)).nodes.find((node) => node.name === name)!.id;
}
async function desktopOf(page: Page, name: string) {
const id = await idOf(page, name);
const at = await onDevice(page, id);
await page.mouse.click(at.x, at.y);
return page.locator(`section[data-desktop="${id}"]`);
}
async function install(desktop: Locator, program: string) {
await desktop.locator('[data-program="installation"]').click();
const installation = desktop.locator('[data-program-window="installation"]');
await installation.locator('[data-list="available"] [role="option"]').filter({ hasText: program }).click();
await installation.getByRole('button', { name: 'Installieren' }).click();
await installation.getByRole('button', { name: 'Änderungen annehmen' }).click();
}
test('joins, searches and downloads as Java’s Gnutella window does', async ({ page }) => {
test.setTimeout(120_000);
await openEditor(page, server.base, 'email_komplett.fls');
await page.getByRole('button', { name: 'Wechselt in den Aktionsmodus' }).click();
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
// The other notebook shares a file.
const other = await desktopOf(page, 'Rechner 2');
await install(other, 'Gnutella');
await expect(other.locator('[data-program="gnutella"]')).toBeVisible();
const otherId = await idOf(page, 'Rechner 2');
await page.evaluate(
(id) => (window as unknown as { filiusDev: { writeFile: (id: string, path: string, text: string) => void } }).filiusDev.writeFile(id, '/peer2peer/lied.txt', 'La la la'),
otherId,
);
await other.locator('.desktop-close').click();
const desktop = await desktopOf(page, 'Rechner 1');
await install(desktop, 'Gnutella');
await desktop.locator('[data-program="gnutella"]').click();
const window = desktop.locator('[data-program-window="gnutella"]');
await expect(window.locator('.program-titlebar h3')).toHaveText('Gnutella');
await expect(window.getByRole('tab')).toHaveText(['Netzwerk', 'Suche', 'Dateien', 'Einstellungen']);
// What is no address: red, and Java's words.
const address = window.getByRole('textbox', { name: 'Teilnehmer IP-Adresse:' });
await expect(address).toHaveValue('192.168.0.1');
await address.fill('141.99.50.300');
await expect(address).toHaveAttribute('aria-invalid', 'true');
await window.getByRole('button', { name: 'Netz beitreten' }).click();
await expect(window.getByRole('alert')).toHaveText('Dies ist keine gültige IP-Adresse');
await address.fill('141.99.50.182');
await window.getByRole('button', { name: 'Netz beitreten' }).click();
const peers = window.getByRole('table', { name: 'Liste der verbundenen Nachbarn:' }).locator('tbody tr');
await expect(peers).toHaveCount(1, { timeout: 30_000 });
await expect(peers.first().locator('td')).toHaveText(['1', '141.99.50.182']);
// The other's file found and downloaded.
await window.getByRole('tab', { name: 'Suche' }).click();
await window.getByRole('textbox', { name: 'Suchbegriff:' }).fill('lied');
await window.getByRole('button', { name: 'Suchen' }).click();
const results = window.getByRole('table', { name: 'Suche' }).locator('tbody tr');
await expect(results).toHaveCount(1, { timeout: 30_000 });
await expect(results.first().locator('td')).toHaveText(['141.99.50.182', 'lied.txt', '8 B']);
const download = window.getByRole('button', { name: 'Herunterladen' });
await expect(download).toBeDisabled();
await results.first().dispatchEvent('pointerdown');
await download.click();
// The list emptied: nothing is chosen any more.
await window.getByRole('button', { name: 'Liste leeren' }).click();
await expect(results).toHaveCount(0);
await expect(download).toBeDisabled();
await window.getByRole('tab', { name: 'Dateien' }).click();
await expect(window.getByRole('table', { name: 'Dateien' }).locator('tbody tr td')).toHaveText(['lied.txt', 'txt'], { timeout: 30_000 });
// The number of peers: 12 saved (Java takes one digit only), what is no number refused with Java's words.
await window.getByRole('tab', { name: 'Einstellungen' }).click();
const maximum = window.getByRole('textbox', { name: 'Maximale Anzahl direkter Nachbarn:' });
await expect(maximum).toHaveValue('3');
await maximum.fill('012');
await window.getByRole('button', { name: 'Speichern' }).click();
// The field shows the program's number once it is saved.
await expect(maximum).toHaveValue('12');
await expect(window.getByRole('alert')).toHaveCount(0);
await maximum.fill('x');
await expect(maximum).toHaveAttribute('aria-invalid', 'true');
await window.getByRole('button', { name: 'Speichern' }).click();
await expect(window.getByRole('alert')).toHaveText('Es sind nur Zahlen erlaubt!');
// The network left.
await window.getByRole('tab', { name: 'Netzwerk' }).click();
await window.getByRole('button', { name: 'Netz verlassen' }).click();
await expect(peers).toHaveCount(0);
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -42,6 +42,7 @@ import { FileExplorerPanel } from './FileExplorerPanel.js';
import { EchoServerPanel, SimpleClientPanel } from './EchoPanels.js'; import { EchoServerPanel, SimpleClientPanel } from './EchoPanels.js';
import { DnsServerPanel } from './DnsServerPanel.js'; import { DnsServerPanel } from './DnsServerPanel.js';
import { PersonalFirewallPanel } from './PersonalFirewallPanel.js'; import { PersonalFirewallPanel } from './PersonalFirewallPanel.js';
import { GnutellaPanel } from './GnutellaPanel.js';
import { ImageViewerPanel, useImageViewer } from './ImageViewerPanel.js'; import { ImageViewerPanel, useImageViewer } from './ImageViewerPanel.js';
import { WebServerPanel } from './WebServerPanel.js'; import { WebServerPanel } from './WebServerPanel.js';
import './desktop.css'; import './desktop.css';
@@ -72,7 +73,7 @@ export interface DesktopWindowProps {
export type DesktopWindowName = DesktopProgram | 'installation'; export type DesktopWindowName = DesktopProgram | 'installation';
/** The programs whose windows are built so far. */ /** The programs whose windows are built so far. */
const WITH_WINDOW: ReadonlySet<DesktopProgram> = new Set(['commandPrompt', 'webBrowser', 'webServer', 'emailClient', 'emailServer', 'textEditor', 'fileExplorer', 'imageViewer', 'echoServer', 'simpleClient', 'dnsServer', 'firewall']); const WITH_WINDOW: ReadonlySet<DesktopProgram> = new Set(['commandPrompt', 'webBrowser', 'webServer', 'emailClient', 'emailServer', 'textEditor', 'fileExplorer', 'imageViewer', 'echoServer', 'simpleClient', 'dnsServer', 'firewall', 'gnutella']);
/** The document's desktop of a computer, read anew after every change (a program installed, the label changed). */ /** The document's desktop of a computer, read anew after every change (a program installed, the label changed). */
function useDesktopView(doc: Y.Doc, id: string) { function useDesktopView(doc: Y.Doc, id: string) {
@@ -191,6 +192,7 @@ export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: o
{shown === 'simpleClient' ? <SimpleClientPanel node={id} simulation={simulation} /> : null} {shown === 'simpleClient' ? <SimpleClientPanel node={id} simulation={simulation} /> : null}
{shown === 'dnsServer' ? <DnsServerPanel node={id} simulation={simulation} /> : null} {shown === 'dnsServer' ? <DnsServerPanel node={id} simulation={simulation} /> : null}
{shown === 'firewall' ? <PersonalFirewallPanel node={id} simulation={simulation} /> : null} {shown === 'firewall' ? <PersonalFirewallPanel node={id} simulation={simulation} /> : null}
{shown === 'gnutella' ? <GnutellaPanel node={id} simulation={simulation} /> : null}
</ProgramFrame> </ProgramFrame>
)} )}
{info ? ( {info ? (
@@ -0,0 +1,253 @@
// A computer's Gnutella program, after Java's
// GUIApplicationPeerToPeerAnwendungWindow (measured with tools/java-oracle's
// GnutellaWindowRun): four tabs with Java's symbols. "Netzwerk": a peer's
// address (it starts as Java's, 192.168.0.1; red while it is no address)
// with "Netz beitreten", the table of known peers, "Netz verlassen".
// "Suche": the words with "Suchen" and "Stopp!", the table of results (a row
// chosen), "Herunterladen" and "Liste leeren". "Dateien": the files of
// /peer2peer. "Einstellungen": how many peers it keeps at most, with
// "Speichern". The program is the room's: the panel shows its view and hands
// over what is done.
//
// Better than Java:
// - the tables show what the program keeps at once: Java's results still
// show the last search's after a new one until a hit comes, and its files
// only what was there when the program last told the window of a change.
// - any whole number of peers is taken (Java's check takes one digit only).
// - what is not taken is said under its field (Java opens a dialog), and
// "Herunterladen" can be pressed only with a result chosen.
import { useEffect, useState } from 'react';
import { gnutellaPeerCountOfText, IP_ADDRESS_PATTERN } from '@mdg-filius/core';
import type { GnutellaView } from '@mdg-filius/protocol';
import networkIcon from '../assets/desktop/peertopeer_netzwerk_klein.png';
import searchIcon from '../assets/desktop/peertopeer_suchen_klein.png';
import filesIcon from '../assets/desktop/peertopeer_dateien_klein.png';
import settingsIcon from '../assets/desktop/peertopeer_einstellungen.png';
import { useTranslate } from '../i18n/i18n.js';
import type { Simulation } from '../simulation/useSimulation.js';
type Tab = 'network' | 'search' | 'files' | 'settings';
const TABS: readonly (readonly [Tab, string, string])[] = [
['network', 'gnutella.networkTab', networkIcon],
['search', 'gnutella.searchTab', searchIcon],
['files', 'gnutella.filesTab', filesIcon],
['settings', 'gnutella.settingsTab', settingsIcon],
];
export function GnutellaPanel({ node, simulation }: { readonly node: string; readonly simulation: Simulation }) {
const t = useTranslate();
const topic = `app:${node}/gnutella` as const;
const { watch, act } = simulation;
useEffect(() => watch([topic]), [watch, topic]);
const view = simulation.views.get(topic) as GnutellaView | null | undefined;
const [tab, setTab] = useState<Tab>('network');
const [address, setAddress] = useState('192.168.0.1');
const [criteria, setCriteria] = useState('');
const [chosen, setChosen] = useState<number | null>(null);
const [maximum, setMaximum] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const results = JSON.stringify(view?.results ?? []);
// Java's table is filled anew with each change and loses its choice.
useEffect(() => setChosen(null), [results]);
if (view === null || view === undefined) return <div className="web-server" />;
const addressValid = IP_ADDRESS_PATTERN.test(address);
const maximumText = maximum ?? String(view.maximumKnownPeers);
const count = gnutellaPeerCountOfText(maximumText);
const join = () => {
if (!addressValid) {
setNotice(t('gnutella.invalidAddress'));
return;
}
act(node, 'gnutella', 'join', { address });
setNotice(null);
};
const search = () => act(node, 'gnutella', 'search', { criteria });
const save = () => {
if (count === undefined) {
setNotice(t('gnutella.onlyNumbers'));
return;
}
act(node, 'gnutella', 'setMaximumPeers', { count });
setMaximum(null);
setNotice(null);
};
const noticeLine =
notice === null ? null : (
<p className="mail-notice" role="alert">
{notice}
</p>
);
return (
<div className="web-server gnutella">
<div className="mail-tabs" role="tablist">
{TABS.map(([each, label, icon]) => (
<button
key={each}
type="button"
role="tab"
aria-selected={tab === each}
onClick={() => {
setTab(each);
setNotice(null);
}}
>
<img src={icon} alt="" />
{t(label)}
</button>
))}
</div>
{tab === 'network' ? (
<div className="gnutella-page" role="tabpanel" aria-label={t('gnutella.networkTab')}>
<div className="web-server-bar">
<label>
{t('gnutella.address')}
<input
value={address}
inputMode="decimal"
autoCapitalize="off"
autoCorrect="off"
spellCheck={false}
aria-invalid={addressValid ? undefined : 'true'}
onChange={(event) => {
setAddress(event.target.value);
setNotice(null);
}}
onKeyDown={(event) => event.key === 'Enter' && join()}
/>
</label>
<button type="button" title={t('gnutella.joinHint')} onClick={join}>
{t('gnutella.join')}
</button>
</div>
{noticeLine}
<p className="gnutella-label">{t('gnutella.neighbours')}</p>
<table className="dns-server-table gnutella-table" aria-label={t('gnutella.neighbours')}>
<thead>
<tr>
<th>{t('gnutella.number')}</th>
<th>{t('gnutella.addressColumn')}</th>
</tr>
</thead>
<tbody>
{view.peers.map((peer, index) => (
<tr key={peer}>
<td>{index + 1}</td>
<td>{peer}</td>
</tr>
))}
</tbody>
</table>
<div className="web-server-bar">
<button type="button" title={t('gnutella.leaveHint')} onClick={() => act(node, 'gnutella', 'leave')}>
{t('gnutella.leave')}
</button>
</div>
</div>
) : tab === 'search' ? (
<div className="gnutella-page" role="tabpanel" aria-label={t('gnutella.searchTab')}>
<div className="web-server-bar">
<label>
{t('gnutella.searchTerm')}
<input
value={criteria}
title={t('gnutella.searchHint')}
autoCapitalize="off"
autoCorrect="off"
spellCheck={false}
onChange={(event) => setCriteria(event.target.value)}
onKeyDown={(event) => event.key === 'Enter' && search()}
/>
</label>
<button type="button" onClick={search}>
{t('gnutella.search')}
</button>
<button type="button" title={t('gnutella.stopHint')} onClick={() => act(node, 'gnutella', 'cancelSearch')}>
{t('gnutella.stop')}
</button>
</div>
<table className="dns-server-table gnutella-table" aria-label={t('gnutella.searchTab')}>
<thead>
<tr>
<th>{t('gnutella.resultAddress')}</th>
<th>{t('gnutella.resultName')}</th>
<th>{t('gnutella.resultSize')}</th>
</tr>
</thead>
<tbody>
{view.results.map((cells, row) => (
<tr
key={row}
tabIndex={0}
aria-selected={row === chosen}
onPointerDown={() => setChosen(row)}
onKeyDown={(event) => {
if (event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
setChosen(row);
}}
>
{cells.map((cell, column) => (
<td key={column}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
<div className="web-server-bar">
<button type="button" title={t('gnutella.downloadHint')} disabled={chosen === null} onClick={() => chosen !== null && act(node, 'gnutella', 'download', { row: chosen })}>
{t('gnutella.download')}
</button>
<button type="button" title={t('gnutella.clearHint')} onClick={() => act(node, 'gnutella', 'clearResults')}>
{t('gnutella.clear')}
</button>
</div>
</div>
) : tab === 'files' ? (
<div className="gnutella-page" role="tabpanel" aria-label={t('gnutella.filesTab')}>
<p className="gnutella-label">{t('gnutella.files')}</p>
<table className="dns-server-table gnutella-table" aria-label={t('gnutella.filesTab')}>
<thead>
<tr>
<th>{t('gnutella.fileName')}</th>
<th>{t('gnutella.fileType')}</th>
</tr>
</thead>
<tbody>
{view.files.map(([name, type]) => (
<tr key={name}>
<td>{name}</td>
<td>{type}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="gnutella-page" role="tabpanel" aria-label={t('gnutella.settingsTab')}>
<div className="web-server-bar">
<label>
{t('gnutella.maximum')}
<input
value={maximumText}
inputMode="numeric"
aria-invalid={count === undefined ? 'true' : undefined}
onChange={(event) => {
setMaximum(event.target.value);
setNotice(null);
}}
onKeyDown={(event) => event.key === 'Enter' && save()}
/>
</label>
<button type="button" onClick={save}>
{t('gnutella.save')}
</button>
</div>
{noticeLine}
</div>
)}
</div>
);
}
+49
View File
@@ -1202,3 +1202,52 @@
.personal-firewall-table tr { .personal-firewall-table tr {
cursor: pointer; cursor: pointer;
} }
/* Gnutella (GUIApplicationPeerToPeerAnwendungWindow): four tabs with Java's
* symbols — the network, the search, the files, the settings. */
.gnutella-page {
flex: 1;
min-height: 0;
overflow: auto;
display: flex;
flex-direction: column;
gap: 6px;
}
.gnutella .mail-tabs button {
display: inline-flex;
align-items: center;
gap: 4px;
}
.gnutella .mail-tabs img {
width: 24px;
height: 24px;
}
.gnutella-label {
margin: 0;
}
.gnutella input {
min-height: 32px;
padding: 0 6px;
border: 1px solid #a0a0a0;
background: #ffffff;
color: #000000;
font: 16px Dialog, sans-serif;
}
.gnutella [aria-invalid='true'] {
border-color: #ff0000;
color: #ff1414;
}
.gnutella-table td {
padding: 6px;
overflow-wrap: anywhere;
}
.gnutella-table tbody tr[tabindex] {
cursor: pointer;
}
+31
View File
@@ -220,6 +220,37 @@
"forwarding.lanPortValue": "LAN-Port", "forwarding.lanPortValue": "LAN-Port",
"forwarding.protocolValue": "Protokoll", "forwarding.protocolValue": "Protokoll",
"forwarding.publicPortValue": "Portfreigabe", "forwarding.publicPortValue": "Portfreigabe",
"gnutella.address": "Teilnehmer IP-Adresse:",
"gnutella.addressColumn": "Adresse",
"gnutella.clear": "Liste leeren",
"gnutella.clearHint": "Entfernt die bisherigen Suchergebnisse",
"gnutella.download": "Herunterladen",
"gnutella.downloadHint": "Herunterladen der markierten Datei",
"gnutella.fileName": "Dateiname",
"gnutella.fileType": "Dateityp",
"gnutella.files": "Dateien:",
"gnutella.filesTab": "Dateien",
"gnutella.invalidAddress": "Dies ist keine gültige IP-Adresse",
"gnutella.join": "Netz beitreten",
"gnutella.joinHint": "Versucht einem bestehenden Netzwerk beizutreten, nach Eingabe der IP-Adresse eines Teilnehmers.",
"gnutella.leave": "Netz verlassen",
"gnutella.leaveHint": "Zurücksetzen der Teilnehmerliste",
"gnutella.maximum": "Maximale Anzahl direkter Nachbarn:",
"gnutella.neighbours": "Liste der verbundenen Nachbarn:",
"gnutella.networkTab": "Netzwerk",
"gnutella.number": "Kennung",
"gnutella.onlyNumbers": "Es sind nur Zahlen erlaubt!",
"gnutella.resultAddress": "IP-Adresse",
"gnutella.resultName": "Dateiname",
"gnutella.resultSize": "Größe",
"gnutella.save": "Speichern",
"gnutella.search": "Suchen",
"gnutella.searchHint": "Suche nach enthaltener Zeichenkette",
"gnutella.searchTab": "Suche",
"gnutella.searchTerm": "Suchbegriff:",
"gnutella.settingsTab": "Einstellungen",
"gnutella.stop": "Stopp!",
"gnutella.stopHint": "Stoppt die laufende Suche",
"installation.add": "Installieren", "installation.add": "Installieren",
"installation.apply": "Änderungen annehmen", "installation.apply": "Änderungen annehmen",
"installation.available": "Verfügbar:", "installation.available": "Verfügbar:",
+31
View File
@@ -220,6 +220,37 @@
"forwarding.lanPortValue": "LAN Port", "forwarding.lanPortValue": "LAN Port",
"forwarding.protocolValue": "Protocol", "forwarding.protocolValue": "Protocol",
"forwarding.publicPortValue": "Mapped Port", "forwarding.publicPortValue": "Mapped Port",
"gnutella.address": "Member IP address:",
"gnutella.addressColumn": "address",
"gnutella.clear": "Clear list",
"gnutella.clearHint": "Clear results",
"gnutella.download": "Download",
"gnutella.downloadHint": "Downloads selected file",
"gnutella.fileName": "File",
"gnutella.fileType": "File type",
"gnutella.files": "Files:",
"gnutella.filesTab": "Files",
"gnutella.invalidAddress": "Invalid IP address.",
"gnutella.join": "Enter network",
"gnutella.joinHint": "Enter an existing network.",
"gnutella.leave": "Leave network",
"gnutella.leaveHint": "Clear list of connected neighbours.",
"gnutella.maximum": "Maximal number of direct neighbours:",
"gnutella.neighbours": "List of connected neighbours.",
"gnutella.networkTab": "Network",
"gnutella.number": "Identification",
"gnutella.onlyNumbers": "Just numbers are allowed!",
"gnutella.resultAddress": "IP address",
"gnutella.resultName": "File",
"gnutella.resultSize": "Size",
"gnutella.save": "Save",
"gnutella.search": "Search",
"gnutella.searchHint": "Search for substring",
"gnutella.searchTab": "Search",
"gnutella.searchTerm": "Search item:",
"gnutella.settingsTab": "Settings",
"gnutella.stop": "Stop!",
"gnutella.stopHint": "Stop current search",
"installation.add": "Install", "installation.add": "Install",
"installation.apply": "Apply changes", "installation.apply": "Apply changes",
"installation.available": "Available:", "installation.available": "Available:",
+2 -1
View File
@@ -7,7 +7,7 @@
import { StrictMode, useEffect, useState } from 'react'; import { StrictMode, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import * as Y from 'yjs'; import * as Y from 'yjs';
import { desktopView, deviceSheet, emptyProject, fileText, topologyView, writeProject } from '@mdg-filius/doc'; import { changeFiles, desktopView, deviceSheet, emptyProject, fileText, topologyView, writeProject } from '@mdg-filius/doc';
import { openProjectArchive } from '@mdg-filius/fls'; import { openProjectArchive } from '@mdg-filius/fls';
import { startBridge } from './bridge/dashboardBridge.js'; import { startBridge } from './bridge/dashboardBridge.js';
import { Editor } from './Editor.js'; import { Editor } from './Editor.js';
@@ -70,6 +70,7 @@ function App() {
sheet: (id: string) => deviceSheet(doc, id), sheet: (id: string) => deviceSheet(doc, id),
desktop: (id: string) => desktopView(doc, id), desktop: (id: string) => desktopView(doc, id),
insertText: (id: string, path: string, index: number, text: string) => doc.transact(() => fileText(doc, id, path)?.insert(index, text), 'other'), insertText: (id: string, path: string, index: number, text: string) => doc.transact(() => fileText(doc, id, path)?.insert(index, text), 'other'),
writeFile: (id: string, path: string, text: string) => changeFiles(doc, id, 'other', (files) => files.writeTextFile(path, text)),
}; };
}, [doc]); }, [doc]);
+31
View File
@@ -175,6 +175,37 @@ const FROM_JAVA: Readonly<Record<string, string>> = {
'firewall.dropIcmp': 'jfirewalldialog_msg40', 'firewall.dropIcmp': 'jfirewalldialog_msg40',
'firewall.dropIcmpHint': 'jfirewalldialog_msg41', 'firewall.dropIcmpHint': 'jfirewalldialog_msg41',
'firewall.filterSyn': 'jfirewalldialog_msg42', 'firewall.filterSyn': 'jfirewalldialog_msg42',
'gnutella.address': 'peertopeeranwendung_msg1',
'gnutella.join': 'peertopeeranwendung_msg2',
'gnutella.joinHint': 'peertopeeranwendung_msg3',
'gnutella.neighbours': 'peertopeeranwendung_msg4',
'gnutella.invalidAddress': 'peertopeeranwendung_msg5',
'gnutella.number': 'peertopeeranwendung_msg6',
'gnutella.addressColumn': 'peertopeeranwendung_msg7',
'gnutella.resultAddress': 'peertopeeranwendung_msg8',
'gnutella.resultName': 'peertopeeranwendung_msg9',
'gnutella.resultSize': 'peertopeeranwendung_msg10',
'gnutella.searchTerm': 'peertopeeranwendung_msg11',
'gnutella.searchHint': 'peertopeeranwendung_msg12',
'gnutella.search': 'peertopeeranwendung_msg13',
'gnutella.stop': 'peertopeeranwendung_msg14',
'gnutella.stopHint': 'peertopeeranwendung_msg15',
'gnutella.clear': 'peertopeeranwendung_msg16',
'gnutella.clearHint': 'peertopeeranwendung_msg17',
'gnutella.download': 'peertopeeranwendung_msg18',
'gnutella.downloadHint': 'peertopeeranwendung_msg19',
'gnutella.searchTab': 'peertopeeranwendung_msg20',
'gnutella.files': 'peertopeeranwendung_msg21',
'gnutella.fileName': 'peertopeeranwendung_msg22',
'gnutella.fileType': 'peertopeeranwendung_msg23',
'gnutella.filesTab': 'peertopeeranwendung_msg24',
'gnutella.maximum': 'peertopeeranwendung_msg25',
'gnutella.save': 'peertopeeranwendung_msg26',
'gnutella.onlyNumbers': 'peertopeeranwendung_msg27',
'gnutella.settingsTab': 'peertopeeranwendung_msg28',
'gnutella.networkTab': 'peertopeeranwendung_msg29',
'gnutella.leave': 'peertopeeranwendung_msg30',
'gnutella.leaveHint': 'peertopeeranwendung_msg31',
'personalFirewall.hint': 'firewall_msg2', 'personalFirewall.hint': 'firewall_msg2',
'personalFirewall.port': 'firewall_msg3', 'personalFirewall.port': 'firewall_msg3',
'personalFirewall.allSenders': 'firewall_msg4', 'personalFirewall.allSenders': 'firewall_msg4',
+14
View File
@@ -0,0 +1,14 @@
// What the Gnutella window shows, as the room keeps it and every client shows
// it (plan §7.3), after GUIApplicationPeerToPeerAnwendungWindow (measured
// with tools/java-oracle's GnutellaWindowRun): its known peers, its search
// results, the files of /peer2peer, and how many peers it keeps at most.
export interface GnutellaView {
/** The known peers' addresses, in the order they came. */
readonly peers: readonly string[];
/** Each result as the table shows it: the address, the file's name, its size (" 8 B", with Java's space). */
readonly results: readonly (readonly [string, string, string])[];
/** The files of /peer2peer: name and type. */
readonly files: readonly (readonly [string, string])[];
readonly maximumKnownPeers: number;
}
+1
View File
@@ -5,5 +5,6 @@ export * from './terminal.js';
export * from './dns.js'; export * from './dns.js';
export * from './echo.js'; export * from './echo.js';
export * from './firewall.js'; export * from './firewall.js';
export * from './gnutella.js';
export * from './mail.js'; export * from './mail.js';
export * from './web.js'; export * from './web.js';
+56 -3
View File
@@ -49,7 +49,10 @@ import {
NetworkRuntimeEngine, NetworkRuntimeEngine,
captureRows, captureRows,
firewallRuleText, firewallRuleText,
gnutellaPeerCountOfText,
gnutellaResultRow,
interfaceKey, interfaceKey,
IP_ADDRESS_PATTERN,
portRuleSender, portRuleSender,
stableGeneratedMacAddress, stableGeneratedMacAddress,
type CapturedFrame, type CapturedFrame,
@@ -76,6 +79,7 @@ import {
type DnsServerView, type DnsServerView,
type EchoServerView, type EchoServerView,
type FirewallView, type FirewallView,
type GnutellaView,
type SimpleClientView, type SimpleClientView,
type TraceFrame, type TraceFrame,
type WebServerView, type WebServerView,
@@ -218,7 +222,8 @@ export class SimulationRoom {
* What the run left of each node that Java's project keeps: its files (when * What the run left of each node that Java's project keeps: its files (when
* they changed since the last write), whether its servers run, a web * they changed since the last write), whether its servers run, a web
* server's virtual hosts, an e-mail server's domain and accounts, an * server's virtual hosts, an e-mail server's domain and accounts, an
* e-mail program's account and folders, a firewall, a DHCP client's address. * e-mail program's account and folders, a firewall, Gnutella's number of
* peers, a DHCP client's address.
*/ */
private runState(): Map<Id, NodeRunState> { private runState(): Map<Id, NodeRunState> {
const snapshot = this.engine.state.topologySnapshot; const snapshot = this.engine.state.topologySnapshot;
@@ -256,6 +261,8 @@ export class SimulationRoom {
} }
const client = this.engine.simpleClient(id); const client = this.engine.simpleClient(id);
if (client !== undefined) settings.simpleClient = { targetAddress: client.targetAddress, targetPort: client.targetPort }; if (client !== undefined) settings.simpleClient = { targetAddress: client.targetAddress, targetPort: client.targetPort };
const gnutella = this.engine.gnutella(id);
if (gnutella !== undefined) settings.gnutella = { maximumKnownPeers: gnutella.maximumKnownPeers };
settings.emailClient = snapshot.emailClientsByNodeId.get(id); settings.emailClient = snapshot.emailClientsByNodeId.get(id);
settings.firewall = snapshot.firewallConfigurationsByNodeId.get(id); settings.firewall = snapshot.firewallConfigurationsByNodeId.get(id);
if (snapshot.dhcpClientConfigurationsByNodeId.get(id)?.isEnabled === true) settings.device = snapshot.deviceConfigurations.get(id); if (snapshot.dhcpClientConfigurationsByNodeId.get(id)?.isEnabled === true) settings.device = snapshot.deviceConfigurations.get(id);
@@ -468,6 +475,8 @@ export class SimulationRoom {
return this.actInDnsServer(input); return this.actInDnsServer(input);
case 'firewall': case 'firewall':
return this.actInFirewall(input); return this.actInFirewall(input);
case 'gnutella':
return this.actInGnutella(input);
default: default:
return 'unknownAction'; return 'unknownAction';
} }
@@ -503,6 +512,44 @@ export class SimulationRoom {
} }
} }
/** The Gnutella window: "Netz beitreten" with the address, "Netz verlassen", "Suchen" with the words, "Stopp!", "Liste leeren", "Herunterladen" of the row chosen, the number of peers saved. */
private actInGnutella(input: Extract<ClientMessage, { kind: 'input' }>): ErrorCode | undefined {
const peer = this.engine.gnutella(input.node);
if (peer === undefined) return 'unknownApp';
const { args } = input;
switch (input.action) {
case 'join':
if (typeof args.address !== 'string' || !IP_ADDRESS_PATTERN.test(args.address)) return 'badArgs';
peer.join(args.address);
return undefined;
case 'leave':
peer.reset();
return undefined;
case 'search':
if (typeof args.criteria !== 'string') return 'badArgs';
peer.search(args.criteria);
return undefined;
case 'cancelSearch':
peer.cancelSearch();
return undefined;
case 'clearResults':
peer.clearResults();
return undefined;
case 'download':
if (!Number.isInteger(args.row) || (args.row as number) < 0 || (args.row as number) >= peer.results.length) return 'badArgs';
peer.download(args.row as number);
return undefined;
case 'setMaximumPeers': {
const count = Number.isInteger(args.count) ? gnutellaPeerCountOfText(String(args.count)) : undefined;
if (count === undefined) return 'badArgs';
peer.setMaximumKnownPeers(count);
return undefined;
}
default:
return 'unknownAction';
}
}
/** A computer's firewall window: its three boxes, "Hinzufügen" with the port, the protocol and the senders chosen, "Löschen" of the row chosen. */ /** A computer's firewall window: its three boxes, "Hinzufügen" with the port, the protocol and the senders chosen, "Löschen" of the row chosen. */
private actInFirewall(input: Extract<ClientMessage, { kind: 'input' }>): ErrorCode | undefined { private actInFirewall(input: Extract<ClientMessage, { kind: 'input' }>): ErrorCode | undefined {
const configuration = this.engine.personalFirewall(input.node); const configuration = this.engine.personalFirewall(input.node);
@@ -715,6 +762,12 @@ export class SimulationRoom {
const view: DnsServerView = { active: server.isActive, recursive: server.recursiveResolutionEnabled, rows: { A: rows('A'), MX: rows('MX'), NS: rows('NS') } }; const view: DnsServerView = { active: server.isActive, recursive: server.recursiveResolutionEnabled, rows: { A: rows('A'), MX: rows('MX'), NS: rows('NS') } };
return view; return view;
} }
case 'gnutella': {
const peer = this.engine.gnutella(node);
if (peer === undefined) return null;
const view: GnutellaView = { peers: peer.peers, results: peer.results.map(gnutellaResultRow), files: peer.fileRows(), maximumKnownPeers: peer.maximumKnownPeers };
return view;
}
case 'firewall': { case 'firewall': {
const configuration = this.engine.personalFirewall(node); const configuration = this.engine.personalFirewall(node);
if (configuration === undefined) return null; if (configuration === undefined) return null;
@@ -817,8 +870,8 @@ export class SimulationRoom {
client.viewSent.set(topic, key); client.viewSent.set(topic, key);
client.send(message({ kind: 'view', node: node!, app: app as AppKind, view })); client.send(message({ kind: 'view', node: node!, app: app as AppKind, view }));
} }
// The browser's lines are its view; the DNS server's window has none. // The browser's lines are its view; the DNS server's and Gnutella's windows have none.
if (app === 'webBrowser' || app === 'dnsServer') continue; if (app === 'webBrowser' || app === 'dnsServer' || app === 'gnutella') continue;
} }
const sent = client.appSent.get(topic); const sent = client.appSent.get(topic);
if (app === 'terminal') { if (app === 'terminal') {
+113
View File
@@ -0,0 +1,113 @@
// The Gnutella window in the room, on three computers of one network: its
// view (the known peers, the results as the table shows them, the files of
// /peer2peer, the number of peers), its actions, a downloaded file in the
// document, and what a run leaves in the project. The program's steps
// against Java's window are packages/core/test/gnutellaWindow.test.ts.
import { describe, expect, it } from 'vitest';
import * as Y from 'yjs';
import { addDevice, changeFiles, connect, emptyProject, fileText, readProject, setHostAddress, setSoftware, writeProject } from '@mdg-filius/doc';
import type { GnutellaView, HostMessage } from '@mdg-filius/protocol';
import { SimulationRoom, TICK_MILLISECONDS } from '../src/index.js';
function setUp() {
const doc = new Y.Doc();
writeProject(doc, emptyProject());
let counter = 0;
const newId = () => `port-${++counter}`;
addDevice(doc, { kind: 'networkSwitch', id: 'switch', name: 'Switch', x: 100, y: 100, newId }, 'me');
for (const [id, address] of [
['a', '192.168.0.20'],
['b', '192.168.0.30'],
['c', '192.168.0.31'],
] as const) {
addDevice(doc, { kind: 'pc', id, name: id, x: 0, y: 0, newId }, 'me');
connect(doc, { node: id }, { node: 'switch' }, `cable-${id}`, 'me');
setHostAddress(doc, id, 'ipAddress', address, 'me');
setSoftware(doc, id, { install: ['gnutella'], remove: [] }, 'de', 'me');
}
changeFiles(doc, 'c', 'me', (files) => files.writeTextFile('/peer2peer/notizen.txt', 'Notiz'));
let now = 0;
const room = new SimulationRoom(doc, { now: () => now, seed: () => 3 });
const views = new Map<string, GnutellaView | null>();
const errors: string[] = [];
const lines: unknown[] = [];
const client = room.connect((message: HostMessage) => {
if (message.kind === 'view' && message.app === 'gnutella') views.set(message.node, message.view as GnutellaView | null);
if (message.kind === 'app' && message.app === 'gnutella') lines.push(...message.lines);
if (message.kind === 'error') errors.push(message.code);
});
const send = (fields: object) => room.receive(client, { v: 1, ...fields });
send({ kind: 'sub', topics: ['app:a/gnutella', 'app:b/gnutella', 'app:switch/gnutella'] });
send({ kind: 'start' });
const run = (milliseconds: number) => {
for (let elapsed = 0; elapsed < milliseconds; elapsed += TICK_MILLISECONDS) {
now += TICK_MILLISECONDS;
room.tick();
}
};
run(TICK_MILLISECONDS);
const act = (node: string, action: string, args: object = {}) => send({ kind: 'input', node, app: 'gnutella', action, args, reqId: action });
return { doc, views, errors, lines, act, run, send };
}
describe('the Gnutella window in the room', () => {
it('shows the peers, the results and the files, and takes its actions', () => {
const { doc, views, errors, lines, act, run } = setUp();
const view = () => views.get('a')!;
expect(view()).toEqual({ peers: [], results: [], files: [], maximumKnownPeers: 3 });
act('b', 'join', { address: '192.168.0.31' });
run(3000);
act('a', 'join', { address: '192.168.0.30' });
run(3000);
// The ping goes on to .31, whose pong makes it a peer too.
expect(view().peers).toEqual(['192.168.0.30', '192.168.0.31']);
act('a', 'search', { criteria: 'notiz' });
run(3000);
expect(view().results).toEqual([['192.168.0.31', 'notizen.txt', ' 5 B']]);
act('a', 'download', { row: 0 });
run(3000);
expect(view().files).toEqual([['notizen.txt', 'txt']]);
// In the document's file at once.
expect(fileText(doc, 'a', '/peer2peer/notizen.txt')?.toString()).toBe('Notiz');
act('a', 'clearResults');
expect(view().results).toEqual([]);
// "Stopp!" at once: the hits on their way are lost.
act('a', 'search', { criteria: 'notiz' });
act('a', 'cancelSearch');
run(3000);
expect(view().results).toEqual([]);
act('a', 'setMaximumPeers', { count: 12 });
expect(view().maximumKnownPeers).toBe(12);
act('a', 'leave');
expect(view().peers).toEqual([]);
expect(errors).toEqual([]);
// The window has no lines: none are sent.
expect(lines).toEqual([]);
});
it('leaves in the project what the run made of the number of peers', () => {
const { doc, act, send } = setUp();
act('a', 'setMaximumPeers', { count: 7 });
send({ kind: 'stop' });
expect(readProject(doc).gnutellaConfigurations.get('a')).toEqual({ maximumKnownPeers: 7 });
expect(readProject(doc).gnutellaConfigurations.get('b')).toEqual({ maximumKnownPeers: 3 });
});
it('refuses what the window never hands over', () => {
const { views, act, errors } = setUp();
act('a', 'join', { address: '192.168.0.300' });
act('a', 'join', { address: 5 });
act('a', 'search', { criteria: 3 });
act('a', 'download', { row: 0 });
act('a', 'download', { row: -1 });
act('a', 'download', { row: 0.5 });
act('a', 'setMaximumPeers', { count: -1 });
act('a', 'setMaximumPeers', { count: '4' });
act('a', 'setMaximumPeers', { count: 2_147_483_648 });
act('a', 'restart');
act('switch', 'leave');
expect(errors).toEqual(['badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'unknownAction', 'unknownApp']);
expect(views.get('switch')).toBeNull();
});
});
+3 -2
View File
@@ -196,7 +196,8 @@ describe('a simulation room', () => {
expect(reason({ node: 'nobody', app: 'terminal', action: 'enter', args: { line: 'x' } })).toMatchObject({ code: 'unknownNode' }); expect(reason({ node: 'nobody', app: 'terminal', action: 'enter', args: { line: 'x' } })).toMatchObject({ code: 'unknownNode' });
expect(reason({ node: node('Switch 1').id, app: 'terminal', action: 'enter', args: { line: 'x' } })).toMatchObject({ code: 'unknownApp' }); expect(reason({ node: node('Switch 1').id, app: 'terminal', action: 'enter', args: { line: 'x' } })).toMatchObject({ code: 'unknownApp' });
expect(reason({ node: notebook, app: 'terminal', action: 'paint' })).toMatchObject({ code: 'unknownAction' }); expect(reason({ node: notebook, app: 'terminal', action: 'paint' })).toMatchObject({ code: 'unknownAction' });
expect(reason({ node: notebook, app: 'gnutella', action: 'search' })).toMatchObject({ code: 'unknownAction' }); // A program the computer does not have.
expect(reason({ node: notebook, app: 'gnutella', action: 'search' })).toMatchObject({ code: 'unknownApp' });
expect(reason({ node: notebook, app: 'terminal', action: 'enter', args: { line: 7 } })).toMatchObject({ code: 'badArgs' }); expect(reason({ node: notebook, app: 'terminal', action: 'enter', args: { line: 7 } })).toMatchObject({ code: 'badArgs' });
expect(reason({ node: notebook, app: 'terminal', action: 'interrupt' })).toEqual({ v: 1, kind: 'ack', reqId: 'r' }); expect(reason({ node: notebook, app: 'terminal', action: 'interrupt' })).toEqual({ v: 1, kind: 'ack', reqId: 'r' });
send({ kind: 'nonsense', reqId: 'q' }); send({ kind: 'nonsense', reqId: 'q' });
@@ -458,7 +459,7 @@ describe("a simulation room's browser and web server windows", () => {
[server, 'webServer', 'setVirtualHost', { row: 0, column: 1, value: 2 }, 'badArgs'], [server, 'webServer', 'setVirtualHost', { row: 0, column: 1, value: 2 }, 'badArgs'],
[server, 'webServer', 'restart', {}, 'unknownAction'], [server, 'webServer', 'restart', {}, 'unknownAction'],
[notebook, 'webServer', 'setActive', { active: true }, 'unknownApp'], [notebook, 'webServer', 'setActive', { active: true }, 'unknownApp'],
[server, 'gnutella', 'join', {}, 'unknownAction'], [server, 'gnutella', 'join', { address: '192.168.0.1' }, 'unknownApp'],
]; ];
for (const [target, app, action, args, code] of cases) { for (const [target, app, action, args, code] of cases) {
send({ kind: 'input', node: target, app, action, args, reqId: `${app}.${action}.${JSON.stringify(args)}` }); send({ kind: 'input', node: target, app, action, args, reqId: `${app}.${action}.${JSON.stringify(args)}` });
+33
View File
@@ -1565,3 +1565,36 @@ Gemessen (03.10.2026):
je Verbindung einen eigenen `TCPSocket`. Javas Echo-Server verträgt keine je Verbindung einen eigenen `TCPSocket`. Javas Echo-Server verträgt keine
sofort wieder geschlossene Verbindung (die folgenden laufen in die sofort wieder geschlossene Verbindung (die folgenden laufen in die
Zeitüberschreitung) — die Sonde hält sie eine halbe Sekunde. 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 fi
classpath="$upstream/target/filius.jar:$upstream/target/lib/*" classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
mkdir -p "$here/.build" 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. # FILIUS writes its settings below $HOME; keep them out of the real one.
mkdir -p "$here/.build/home" mkdir -p "$here/.build/home"
# Removed first: a crashed run must not leave the previous result standing. # 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 "$root/packages/core/test/scenarios/firewall-pc.scenario.json" "$output" 2>&1 | grep -v 'logback\|^$\|^\s*at ' || true
test -s "$output" 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 # 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) # 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). # 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());
}
}