feat: F4.6g5 the switch and NAT tables and "Pakete verwerfen", measured against Java's
ViewersRun takes the rows of Java's viewers from the firmware: a switch's table, a home router's NAT table, each after its reset, and the frames "Pakete verwerfen" drops while it is held. The core writes a frame as Java does and resets the NAT's dynamic entries; the room sends both tables as views, drops while any client holds the button, and sends the dropped frames, begun anew with each start, with the clock their times of day are written from; the editor opens a table with a tap on a switch or home router and holds "Pakete verwerfen" in its toolbar, which now wraps on a phone. This completes F4.6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
|||||||
| `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/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/viewers.ts`, `src/runtime/captureRows.ts` (`javaFrameText`), `src/runtime/engine.ts` (`portNumber` der SAT, `clearDynamicNatEntries`), `src/runtime/capture.ts` (`clearDropped`), `test/viewers.test.ts`, `test/viewers-switch.java.json`, `test/viewers-nat.java.json` | Java `SwitchFirmware` (`holeSAT`, `loescheSAT`), `GatewayFirmware` (`holeNAT`, `createEintrag`, `loescheNAT`), `EthernetFrame.toString`, `Lauscher.getDroppedDataUnits`, `Verbindung.setDrop` (gemessen mit `ViewersRun`) | neu (F4.6g5): was die Anzeigen der laufenden Simulation zeigen — die Nummer des Switch-Anschlusses („Port 1“), die NAT-Tabelle in Javas Worten (ICMP „0 (Identifier: 1)“, die Portweiterleitung mit „*“ und ohne Zeit), „Tabelle zurücksetzen“ lässt die Portweiterleitung stehen, ein verworfener Frame in Javas Textform; die Liste der verworfenen Frames lässt sich leeren, ihre Nummern laufen weiter. Gegen Java geprüft. Abweichend von Java: die Switch-Tabelle ist nach Adressen geordnet (Javas nach seiner Hash-Tabelle) |
|
||||||
| `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) |
|
||||||
@@ -168,6 +169,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
|||||||
| `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 |
|
| `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 |
|
||||||
|
| `src/viewers.ts`, `src/sim.ts` (`sat`, `nat`, `drop`, `dropped`, `clock`) | Java `SatViewer`, `NatViewer`, `SimulationControl` (gemessen mit `ViewersRun`) | neu (F4.6g5): die Tabellen eines Switches und eines Heimrouters als Bildschirmstände; die Nachricht `drop` (Javas „Pakete verwerfen“ gedrückt oder losgelassen), das Thema `dropped` mit den verworfenen Frames; der Zustand trägt `clock`, die Uhrzeit, für die die Zeit 0 der Simulation steht |
|
||||||
|
|
||||||
## packages/sim
|
## packages/sim
|
||||||
|
|
||||||
@@ -188,6 +190,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
|||||||
| `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 |
|
| `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 |
|
||||||
|
| `src/room.ts` (Anzeigen, „Pakete verwerfen“), `test/viewers.test.ts`, `test/room.test.ts` | Java `SatViewer`, `NatViewer`, `SimulationControl`, `Verbindung.setDrop` | erweitert (F4.6g5): der Raum schickt die Tabellen eines Switches und eines Heimrouters (ohne Zeilen) und nimmt ihr `reset`; jedes Kabel verwirft, solange irgendein Client „Pakete verwerfen“ hält (ein Client, der geht oder beim Stopp noch hält, zählt nicht mehr); die verworfenen Frames gehen an, wer sie beobachtet, mit jedem Start neu begonnen wie Javas Liste |
|
||||||
| `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
|
||||||
@@ -223,6 +226,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
|||||||
| `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/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/simulation/ViewerWindow.tsx`, `src/simulation/DropControl.tsx`, `src/simulation/timeOfDay.ts`, `src/simulation/useSimulation.ts` (`clock`, `dropped`, `drop`), `src/Editor.tsx`, `src/sheet/sheet.css` (`.editor-toolbar`) | Java `SatViewer`, `NatViewer` (`guievents_msg8`–`10`, `27`–`34`, `36`, `sw_switchfirmware_msg1`), `SimulationControl` (`simulation_control_drop_packets`), `GUIKnotenItem` (Klick im Aktionsmodus), gemessen mit `ViewersRun` | neu (F4.6g5): ein Tippen auf einen Switch zeigt seine Tabelle, auf einen Heimrouter seine NAT-Tabelle, in einem verschiebbaren Fenster mit Javas Spalten und Uhrzeiten; „Pakete verwerfen“ wird gehalten (Finger, Maus, Leertaste/Enter), daneben die Zahl und aufklappbar die Liste der verworfenen Frames mit Uhrzeit und Javas Text. Besser als Java: „Tabelle löschen“/„zurücksetzen“ ist ein Knopf (Java: Menü der rechten Maustaste), die Liste klappt weg; die Werkzeugleiste bricht auf schmalen Bildschirmen um |
|
||||||
| `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 |
|
||||||
@@ -234,6 +238,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
|||||||
| `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 |
|
| `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 |
|
||||||
|
| `browser/viewers.spec.ts`, `test/timeOfDay.test.ts` | — | neu (F4.6g5): auf `nat_with_mapped_port` die NAT-Tabelle (Portweiterleitung, dann ein Ping, zurückgesetzt), die Switch-Tabelle (gelernt, gelöscht), „Pakete verwerfen“ gehalten, während ein Ping läuft (Antworten verloren, Liste in Javas Worten); auf einem Handy bleibt „Pakete verwerfen“ erreichbar; die Uhrzeit wie Javas `HH:mm:ss(.SSS)` |
|
||||||
| `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 |
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export * from './runtime/speed.js';
|
|||||||
export * from './runtime/trace.js';
|
export * from './runtime/trace.js';
|
||||||
export * from './runtime/capture.js';
|
export * from './runtime/capture.js';
|
||||||
export * from './runtime/captureRows.js';
|
export * from './runtime/captureRows.js';
|
||||||
|
export * from './runtime/viewers.js';
|
||||||
export * from './runtime/threads.js';
|
export * from './runtime/threads.js';
|
||||||
export * from './runtime/apps/program.js';
|
export * from './runtime/apps/program.js';
|
||||||
export * from './runtime/apps/installation.js';
|
export * from './runtime/apps/installation.js';
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ class CaptureList {
|
|||||||
forgetIdentities(): void {
|
forgetIdentities(): void {
|
||||||
this.identities.clear();
|
this.identities.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Every frame forgotten, the numbers going on (the dropped frames' list, which keeps no identities). */
|
||||||
|
clear(): void {
|
||||||
|
this.frames.length = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PacketCapture {
|
export class PacketCapture {
|
||||||
@@ -107,6 +112,11 @@ export class PacketCapture {
|
|||||||
return this.dropped.frames;
|
return this.dropped.frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The dropped frames forgotten (SimulationControl begins its list anew), their numbers going on. */
|
||||||
|
clearDropped(): void {
|
||||||
|
this.dropped.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/** The simulation starts again: the lists stay, frames of the new run are new. */
|
/** The simulation starts again: the lists stay, frames of the new run are new. */
|
||||||
beginRun(): void {
|
beginRun(): void {
|
||||||
for (const list of this.listsByPortId.values()) list.forgetIdentities();
|
for (const list of this.listsByPortId.values()) list.forgetIdentities();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
// for an unknown transport protocol (all empty) does not occur.
|
// for an unknown transport protocol (all empty) does not occur.
|
||||||
|
|
||||||
import type { CapturedFrame } from './capture.js';
|
import type { CapturedFrame } from './capture.js';
|
||||||
import { IPV4_PROTOCOL, TCP_FLAG, type ArpPacket, type IcmpMessage, type IPv4Packet } from './packets.js';
|
import { IPV4_PROTOCOL, TCP_FLAG, type ArpPacket, type EthernetFrame, type IcmpMessage, type IPv4Packet } from './packets.js';
|
||||||
|
|
||||||
/** Java's four layers (Lauscher.PROTOKOLL_SCHICHTEN): network access, internet, transport, application. */
|
/** Java's four layers (Lauscher.PROTOKOLL_SCHICHTEN): network access, internet, transport, application. */
|
||||||
export type CaptureLayer = 'networkAccess' | 'internet' | 'transport' | 'application';
|
export type CaptureLayer = 'networkAccess' | 'internet' | 'transport' | 'application';
|
||||||
@@ -182,6 +182,17 @@ function packetText(packet: IPv4Packet): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EthernetFrame.toString: addresses, Java's type (0x800 for IP, 0x806 for
|
||||||
|
* ARP) and what the frame carries — an IP packet without its segment, an
|
||||||
|
* ICMP packet whole, an ARP packet. SimulationControl lists dropped frames so.
|
||||||
|
*/
|
||||||
|
export function javaFrameText(frame: EthernetFrame): string {
|
||||||
|
const payload = frame.payload;
|
||||||
|
const data = payload.kind === 'arp' ? arpText(payload.packet) : packetText(payload.packet);
|
||||||
|
return `[src=${frame.sourceMacAddress}, dest=${frame.destinationMacAddress}, type=${payload.kind === 'arp' ? '0x806' : '0x800'} | ${data}]`;
|
||||||
|
}
|
||||||
|
|
||||||
/** Java's String.trim: cuts characters up to U+0020 at both ends. */
|
/** Java's String.trim: cuts characters up to U+0020 at both ends. */
|
||||||
function javaTrim(text: string): string {
|
function javaTrim(text: string): string {
|
||||||
let start = 0;
|
let start = 0;
|
||||||
|
|||||||
@@ -276,6 +276,8 @@ export interface SwitchSatEntry {
|
|||||||
readonly macAddress: string;
|
readonly macAddress: string;
|
||||||
readonly portId: Id;
|
readonly portId: Id;
|
||||||
readonly portLabel: string;
|
readonly portLabel: string;
|
||||||
|
/** The port's place among the switch's, from 1 (SwitchFirmware.holeSAT: "Port <n>"). */
|
||||||
|
readonly portNumber: number;
|
||||||
readonly updatedAtMilliseconds: number;
|
readonly updatedAtMilliseconds: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4060,6 +4062,12 @@ export class NetworkRuntimeEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** GatewayFirmware.loescheNAT (the NAT viewer's "reset"): the dynamic entries go, the port forwarding's stay. */
|
||||||
|
clearDynamicNatEntries(nodeId: Id): void {
|
||||||
|
const table = this.stateValue.natTablesByNodeId.get(nodeId);
|
||||||
|
if (table !== undefined) this.stateValue.natTablesByNodeId.set(nodeId, table.filter((entry) => entry.type === 'static'));
|
||||||
|
}
|
||||||
|
|
||||||
/** The home router's NAT table as it stands: static entries, then the dynamic ones that have not expired. */
|
/** The home router's NAT table as it stands: static entries, then the dynamic ones that have not expired. */
|
||||||
natEntries(nodeId: Id): readonly NatEntry[] {
|
natEntries(nodeId: Id): readonly NatEntry[] {
|
||||||
this.expireNatEntries(nodeId);
|
this.expireNatEntries(nodeId);
|
||||||
@@ -4729,6 +4737,7 @@ export class NetworkRuntimeEngine {
|
|||||||
const node = this.nodeSnapshot(nodeId);
|
const node = this.nodeSnapshot(nodeId);
|
||||||
if (node === undefined || node.kind !== 'networkSwitch') return [];
|
if (node === undefined || node.kind !== 'networkSwitch') return [];
|
||||||
const labelsByPortId = new Map(node.ports.map((port) => [port.id, port.label]));
|
const labelsByPortId = new Map(node.ports.map((port) => [port.id, port.label]));
|
||||||
|
const numbersByPortId = new Map(node.ports.map((port, index) => [port.id, index + 1]));
|
||||||
const updatedAtByMac = this.stateValue.switchForwardingUpdatedAtMillisecondsByNodeId.get(nodeId) ?? new Map();
|
const updatedAtByMac = this.stateValue.switchForwardingUpdatedAtMillisecondsByNodeId.get(nodeId) ?? new Map();
|
||||||
const table = this.stateValue.switchForwardingTablesByNodeId.get(nodeId) ?? new Map<string, Id>();
|
const table = this.stateValue.switchForwardingTablesByNodeId.get(nodeId) ?? new Map<string, Id>();
|
||||||
return [...table]
|
return [...table]
|
||||||
@@ -4736,6 +4745,7 @@ export class NetworkRuntimeEngine {
|
|||||||
macAddress,
|
macAddress,
|
||||||
portId,
|
portId,
|
||||||
portLabel: labelsByPortId.get(portId) ?? '?',
|
portLabel: labelsByPortId.get(portId) ?? '?',
|
||||||
|
portNumber: numbersByPortId.get(portId) ?? 0,
|
||||||
updatedAtMilliseconds: updatedAtByMac.get(macAddress) ?? 0,
|
updatedAtMilliseconds: updatedAtByMac.get(macAddress) ?? 0,
|
||||||
}))
|
}))
|
||||||
.sort((lhs, rhs) =>
|
.sort((lhs, rhs) =>
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// The rows of FILIUS' viewers of a running simulation, as Java writes them:
|
||||||
|
// a home router's NAT table (GatewayFirmware.holeNAT, createEintrag, shown by
|
||||||
|
// NatViewer). A switch's table (SwitchFirmware.holeSAT) is the engine's
|
||||||
|
// switchSatEntries with the port's number; the time of an entry is the
|
||||||
|
// viewer's to write, in its clock.
|
||||||
|
|
||||||
|
import type { NatEntry } from './engine.js';
|
||||||
|
|
||||||
|
/** A NAT entry's cells: protocol, WAN port, remote address, LAN address, LAN port; and its last update, none for the port forwarding's. */
|
||||||
|
export interface NatViewRow {
|
||||||
|
readonly cells: readonly [string, string, string, string, string];
|
||||||
|
readonly updatedAtMilliseconds?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROTOCOL_NAMES: Readonly<Record<NatEntry['protocol'], string>> = { tcp: 'TCP', udp: 'UDP', icmp: 'ICMP' };
|
||||||
|
|
||||||
|
/** createEintrag: an ICMP entry's "port" is its identifier ("0 (Identifier: 1)"); the port forwarding's remote end is "*". */
|
||||||
|
export function natViewRow(entry: NatEntry): NatViewRow {
|
||||||
|
const port = (value: number) => (entry.protocol === 'icmp' ? `0 (Identifier: ${value})` : String(value));
|
||||||
|
const cells = [PROTOCOL_NAMES[entry.protocol], port(entry.port), entry.type === 'static' ? '*' : entry.remoteIPAddress, entry.lan.ipAddress, port(entry.lan.port)] as const;
|
||||||
|
return entry.type === 'static' ? { cells } : { cells, updatedAtMilliseconds: entry.updatedAtMilliseconds };
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"startEpoch": 1791063964929,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step": "start",
|
||||||
|
"at": 720,
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
"TCP",
|
||||||
|
"8080",
|
||||||
|
"*",
|
||||||
|
"192.168.0.10",
|
||||||
|
"80",
|
||||||
|
"-"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "after ping and datagram",
|
||||||
|
"at": 2926,
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
"TCP",
|
||||||
|
"8080",
|
||||||
|
"*",
|
||||||
|
"192.168.0.10",
|
||||||
|
"80",
|
||||||
|
"-"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"ICMP",
|
||||||
|
"0 (Identifier: 1)",
|
||||||
|
"42.0.0.20",
|
||||||
|
"192.168.0.10",
|
||||||
|
"0 (Identifier: 1)",
|
||||||
|
"21:46:06"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"UDP",
|
||||||
|
"61570",
|
||||||
|
"42.0.0.20",
|
||||||
|
"192.168.0.10",
|
||||||
|
"51179",
|
||||||
|
"21:46:06"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"answers": {
|
||||||
|
"ping 42.0.0.20": "ICMP 0/0",
|
||||||
|
"udp port": 51179
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "reset",
|
||||||
|
"at": 2926,
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
"TCP",
|
||||||
|
"8080",
|
||||||
|
"*",
|
||||||
|
"192.168.0.10",
|
||||||
|
"80",
|
||||||
|
"-"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"startEpoch": 1791063953143,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step": "after pings",
|
||||||
|
"at": 1632,
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
"02:00:00:00:01:11",
|
||||||
|
"Port 1",
|
||||||
|
"21:45:54"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"02:00:00:00:01:13",
|
||||||
|
"Port 3",
|
||||||
|
"21:45:54"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"02:00:00:00:01:12",
|
||||||
|
"Port 2",
|
||||||
|
"21:45:54"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"updated": {
|
||||||
|
"02:00:00:00:01:11": 1464,
|
||||||
|
"02:00:00:00:01:13": 1575,
|
||||||
|
"02:00:00:00:01:12": 1129
|
||||||
|
},
|
||||||
|
"ports": 24,
|
||||||
|
"answers": {
|
||||||
|
"ping .20": "ICMP 0/0",
|
||||||
|
"ping .30": "ICMP 0/0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "reset",
|
||||||
|
"at": 1632,
|
||||||
|
"rows": [],
|
||||||
|
"updated": {},
|
||||||
|
"ports": 24
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "ping .20 again",
|
||||||
|
"at": 1855,
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
"02:00:00:00:01:11",
|
||||||
|
"Port 1",
|
||||||
|
"21:45:54"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"02:00:00:00:01:12",
|
||||||
|
"Port 2",
|
||||||
|
"21:45:54"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"updated": {
|
||||||
|
"02:00:00:00:01:11": 1688,
|
||||||
|
"02:00:00:00:01:12": 1799
|
||||||
|
},
|
||||||
|
"ports": 24,
|
||||||
|
"answers": {
|
||||||
|
"ping .20": "ICMP 0/0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "after the retention",
|
||||||
|
"at": 7855,
|
||||||
|
"rows": [],
|
||||||
|
"updated": {},
|
||||||
|
"ports": 24
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "dropped while held",
|
||||||
|
"at": 10606,
|
||||||
|
"answer": "null",
|
||||||
|
"dropped": [
|
||||||
|
"21:46:01.054 : [src=02:00:00:00:01:11, dest=02:00:00:00:01:13, type=0x800 | [id=7; ttl=64; protocol=1; sender=192.168.0.10; target=192.168.0.30; icmpType=8; icmpCode=0; identifier=1; seqNr=4]]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "released",
|
||||||
|
"at": 10829,
|
||||||
|
"answer": "ICMP 0/0",
|
||||||
|
"dropped": [
|
||||||
|
"21:46:01.054 : [src=02:00:00:00:01:11, dest=02:00:00:00:01:13, type=0x800 | [id=7; ttl=64; protocol=1; sender=192.168.0.10; target=192.168.0.30; icmpType=8; icmpCode=0; identifier=1; seqNr=4]]"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
// What FILIUS' viewers of a running simulation show, against Java's
|
||||||
|
// (viewers-switch.java.json, viewers-nat.java.json; ViewersRun): a switch's
|
||||||
|
// source address table after pings, after its reset and after the retention;
|
||||||
|
// a home router's NAT table after a ping and a datagram, and after its reset
|
||||||
|
// (the port forwarding's entry stays); the frames "Pakete verwerfen" drops
|
||||||
|
// while it is held, in Java's words. Java's time of day is the viewer's to
|
||||||
|
// write (the editor's), and its table of a switch comes in the order of
|
||||||
|
// Java's hash map, here by address.
|
||||||
|
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { javaFrameText, natViewRow, NetworkRuntimeEngine, type Steps } from '../src/index.js';
|
||||||
|
import { snapshotOf, type Scenario } from './scenario.js';
|
||||||
|
|
||||||
|
interface JavaStep {
|
||||||
|
readonly step: string;
|
||||||
|
readonly at: number;
|
||||||
|
readonly rows?: readonly (readonly string[])[];
|
||||||
|
readonly updated?: Readonly<Record<string, number>>;
|
||||||
|
readonly answer?: string;
|
||||||
|
readonly answers?: Readonly<Record<string, string | number>>;
|
||||||
|
readonly dropped?: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const javaRun = (name: string) => JSON.parse(readFileSync(join(here, `viewers-${name}.java.json`), 'utf8')) as { readonly steps: readonly JavaStep[] };
|
||||||
|
const scenarioOf = (name: string) => JSON.parse(readFileSync(join(here, `scenarios/${name}.scenario.json`), 'utf8')) as Scenario;
|
||||||
|
|
||||||
|
function started(scenario: Scenario) {
|
||||||
|
const engine = new NetworkRuntimeEngine(scenario.seed);
|
||||||
|
engine.handle({ type: 'start', snapshot: snapshotOf(scenario), seed: scenario.seed, initialTimeMilliseconds: 0 });
|
||||||
|
const run = (milliseconds: number) => engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + milliseconds });
|
||||||
|
/** A ping, returning when its answer came or its wait ended, as Java's sendProbe blocks. */
|
||||||
|
const ping = (from: string, target: string, sequenceNumber: number): string => {
|
||||||
|
let answer: string | undefined;
|
||||||
|
engine.startThread(
|
||||||
|
(function* (): Steps {
|
||||||
|
const reply = yield* engine.icmpEchoSteps(from, target, { identifier: 1, sequenceNumber });
|
||||||
|
answer = reply === undefined ? 'null' : `ICMP ${reply.message.kind === 'echoReply' ? '0/0' : reply.message.kind}`;
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
for (let waited = 0; answer === undefined && waited < 30_000; waited += 10) run(10);
|
||||||
|
return answer ?? 'null';
|
||||||
|
};
|
||||||
|
return { engine, run, ping };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("a switch's table and the dropped frames, against Java's", () => {
|
||||||
|
const java = javaRun('switch');
|
||||||
|
const at = (name: string) => java.steps.find((step) => step.step === name)!;
|
||||||
|
const SWITCH = '00000000-0000-0000-0000-000000000104';
|
||||||
|
const PC10 = '00000000-0000-0000-0000-000000000101';
|
||||||
|
|
||||||
|
it('learns, is reset, ages, and lists what is dropped while it is held', () => {
|
||||||
|
const { engine, run, ping } = started(scenarioOf('switch-lan'));
|
||||||
|
run(500);
|
||||||
|
/** The table as Java's viewer shows it, without the time: address in capitals, "Port <n>". */
|
||||||
|
const table = () => engine.switchSatEntries(SWITCH).map((entry) => [entry.macAddress.toUpperCase(), `Port ${entry.portNumber}`]);
|
||||||
|
const javaTable = (name: string) => at(name).rows!.map(([mac = '', port = '']) => [mac, port]);
|
||||||
|
const sorted = (rows: string[][]) => [...rows].sort(([lhs], [rhs]) => (lhs! < rhs! ? -1 : 1));
|
||||||
|
expect([ping(PC10, '192.168.0.20', 1), ping(PC10, '192.168.0.30', 2)]).toEqual(Object.values(at('after pings').answers!));
|
||||||
|
// By address (Java's in its hash map's order).
|
||||||
|
expect(table()).toEqual(sorted(javaTable('after pings')));
|
||||||
|
expect(table()).not.toEqual(javaTable('after pings'));
|
||||||
|
engine.clearSwitchSat(SWITCH);
|
||||||
|
expect(table()).toEqual(javaTable('reset'));
|
||||||
|
expect(ping(PC10, '192.168.0.20', 3)).toBe(at('ping .20 again').answers!['ping .20']);
|
||||||
|
expect(table()).toEqual(sorted(javaTable('ping .20 again')));
|
||||||
|
run(6000);
|
||||||
|
expect(table()).toEqual(javaTable('after the retention'));
|
||||||
|
|
||||||
|
// "Pakete verwerfen" held while .10 pings .30: the request is dropped on its first cable; released, the ping is answered.
|
||||||
|
engine.setGlobalPacketLossEnabled(true);
|
||||||
|
const dropped = ping(PC10, '192.168.0.30', 4);
|
||||||
|
engine.setGlobalPacketLossEnabled(false);
|
||||||
|
const text = (step: JavaStep) => step.dropped!.map((line) => line.slice(line.indexOf(' : ') + 3));
|
||||||
|
const ours = () => engine.capture.droppedFrames().map((each) => javaFrameText(each.frame));
|
||||||
|
const sameButIdentity = (lines: readonly string[]) => lines.map((line) => line.replace(/\[id=\d+;/, '[id=ID;'));
|
||||||
|
expect(dropped).toBe(at('dropped while held').answer);
|
||||||
|
expect(sameButIdentity(ours())).toEqual(sameButIdentity(text(at('dropped while held'))));
|
||||||
|
expect(ping(PC10, '192.168.0.30', 5)).toBe(at('released').answer);
|
||||||
|
expect(sameButIdentity(ours())).toEqual(sameButIdentity(text(at('released'))));
|
||||||
|
// Java's time of the drop is the time of day with milliseconds.
|
||||||
|
expect(at('released').dropped![0]).toMatch(/^\d\d:\d\d:\d\d\.\d\d\d : \[src=/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("a home router's NAT table, against Java's", () => {
|
||||||
|
const java = javaRun('nat');
|
||||||
|
const at = (name: string) => java.steps.find((step) => step.step === name)!;
|
||||||
|
const GATEWAY = '00000000-0000-0000-0000-000000000502';
|
||||||
|
const LAN_PC = '00000000-0000-0000-0000-000000000501';
|
||||||
|
|
||||||
|
it('shows the port forwarding, a ping and a datagram, and keeps the port forwarding when reset', () => {
|
||||||
|
const { engine, run, ping } = started(scenarioOf('nat-home'));
|
||||||
|
run(500);
|
||||||
|
const rows = () =>
|
||||||
|
engine.natEntries(GATEWAY).map((entry) => {
|
||||||
|
const row = natViewRow(entry);
|
||||||
|
return [...row.cells, row.updatedAtMilliseconds === undefined ? '-' : 'TIME'];
|
||||||
|
});
|
||||||
|
/** Java's rows with a time of day as TIME, and the datagram's ports (drawn) as PORT. */
|
||||||
|
const javaRows = (name: string) =>
|
||||||
|
at(name).rows!.map((row) => (row[0] === 'UDP' ? [row[0], 'PORT', row[2], row[3], 'PORT', 'TIME'] : row.map((cell, index) => (index === 5 && cell !== '-' ? 'TIME' : cell))));
|
||||||
|
expect(rows()).toEqual(javaRows('start'));
|
||||||
|
expect(ping(LAN_PC, '42.0.0.20', 1)).toBe(at('after ping and datagram').answers!['ping 42.0.0.20']);
|
||||||
|
const socket = engine.bindUdpSocket(LAN_PC, { remoteIPAddress: '42.0.0.20', remotePort: 7 })!;
|
||||||
|
engine.sendUdp(socket, new TextEncoder().encode('one'));
|
||||||
|
run(1500);
|
||||||
|
expect(rows().map((row) => (row[0] === 'UDP' ? [row[0], 'PORT', row[2], row[3], 'PORT', row[5]] : row))).toEqual(javaRows('after ping and datagram'));
|
||||||
|
engine.clearDynamicNatEntries(GATEWAY);
|
||||||
|
expect(rows()).toEqual(javaRows('reset'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writes an entry as Java’s createEintrag does', () => {
|
||||||
|
expect([
|
||||||
|
natViewRow({ protocol: 'tcp', remoteIPAddress: '', port: 8080, lan: { ipAddress: '192.168.0.10', port: 80 }, type: 'static', updatedAtMilliseconds: 5 }),
|
||||||
|
natViewRow({ protocol: 'icmp', remoteIPAddress: '42.0.0.20', port: 3, lan: { ipAddress: '192.168.0.10', port: 4 }, type: 'dynamic', updatedAtMilliseconds: 7 }),
|
||||||
|
natViewRow({ protocol: 'udp', remoteIPAddress: '42.0.0.30', port: 8080, lan: { ipAddress: '192.168.0.10', port: 80 }, type: 'dynamicFromStatic', updatedAtMilliseconds: 9 }),
|
||||||
|
]).toEqual([
|
||||||
|
{ cells: ['TCP', '8080', '*', '192.168.0.10', '80'] },
|
||||||
|
{ cells: ['ICMP', '0 (Identifier: 3)', '42.0.0.20', '192.168.0.10', '0 (Identifier: 4)'], updatedAtMilliseconds: 7 },
|
||||||
|
{ cells: ['UDP', '8080', '42.0.0.30', '192.168.0.10', '80'], updatedAtMilliseconds: 9 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
// The viewers of a running simulation and "Pakete verwerfen" in Chromium
|
||||||
|
// (F4.6g5), on FILIUS' example nat_with_mapped_port: a tap on a home router
|
||||||
|
// shows its NAT table (the port forwarding's entry, then a ping's), a tap on
|
||||||
|
// a switch its table (the addresses a ping taught it), each reset; and
|
||||||
|
// "Pakete verwerfen" held while a ping runs: the ping loses answers, the
|
||||||
|
// list shows the dropped frames in Java's words.
|
||||||
|
|
||||||
|
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 tap(page: Page, id: string) {
|
||||||
|
const at = await onDevice(page, id);
|
||||||
|
await page.mouse.click(at.x, at.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
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('shows the tables of a home router and a switch, and drops while “Pakete verwerfen” is held', async ({ page }) => {
|
||||||
|
test.setTimeout(120_000);
|
||||||
|
await openEditor(page, server.base, 'nat_with_mapped_port.fls');
|
||||||
|
const nodes = (await drawing(page)).nodes;
|
||||||
|
const idOf = (name: string) => nodes.find((node) => node.name === name)!.id;
|
||||||
|
await page.getByRole('button', { name: 'Wechselt in den Aktionsmodus' }).click();
|
||||||
|
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
|
||||||
|
|
||||||
|
// The home router with the port forwarding: its entry, with Java's columns.
|
||||||
|
const gateway = idOf('Heimrouter Vincent');
|
||||||
|
await tap(page, gateway);
|
||||||
|
const nat = page.locator(`section[data-viewer="${gateway}"]`);
|
||||||
|
await expect(nat.locator('h2')).toHaveText('NAT-Tabelle Heimrouter Vincent');
|
||||||
|
await expect(nat.locator('th')).toHaveText(['Protokoll', 'Portfreigabe', 'WAN-Adresse', 'LAN-Adresse', 'LAN-Port', 'Letzte Aktualisierung']);
|
||||||
|
await expect(nat.locator('tbody tr')).toHaveCount(1);
|
||||||
|
await expect(nat.locator('tbody tr').first().locator('td')).toHaveText(['TCP', '80', '*', '192.168.0.10', '80', '-']);
|
||||||
|
await nat.locator('.desktop-close').click();
|
||||||
|
|
||||||
|
// The web server behind it pings out.
|
||||||
|
const web = idOf('Webserver - 192.168.0.10');
|
||||||
|
await tap(page, web);
|
||||||
|
const desktop = page.locator(`section[data-desktop="${web}"]`);
|
||||||
|
await install(desktop, 'Befehlszeile');
|
||||||
|
await desktop.locator('[data-program="commandPrompt"]').click();
|
||||||
|
const input = desktop.locator('input[name="terminalInput"]');
|
||||||
|
await input.fill('ping 141.99.4.10');
|
||||||
|
await input.press('Enter');
|
||||||
|
await expect(desktop.locator('.terminal-text')).toContainText('4 Paket(e) empfangen', { timeout: 30_000 });
|
||||||
|
await desktop.locator('.desktop-close').click();
|
||||||
|
|
||||||
|
await tap(page, gateway);
|
||||||
|
await expect(nat.locator('tbody tr')).toHaveCount(2);
|
||||||
|
await expect(nat.locator('tbody tr').nth(1).locator('td').nth(0)).toHaveText('ICMP');
|
||||||
|
await expect(nat.locator('tbody tr').nth(1).locator('td').nth(5)).toHaveText(/^\d\d:\d\d:\d\d$/);
|
||||||
|
await nat.getByRole('button', { name: 'Tabelle zurücksetzen' }).click();
|
||||||
|
await expect(nat.locator('tbody tr')).toHaveCount(1);
|
||||||
|
await nat.locator('.desktop-close').click();
|
||||||
|
|
||||||
|
// The switch the web server hangs on: the addresses the ping taught it.
|
||||||
|
const link = (await drawing(page)).links.find((each) => each.sourceNode === web || each.targetNode === web)!;
|
||||||
|
const lanSwitch = link.sourceNode === web ? link.targetNode : link.sourceNode;
|
||||||
|
await tap(page, lanSwitch);
|
||||||
|
const sat = page.locator(`section[data-viewer="${lanSwitch}"]`);
|
||||||
|
await expect(sat.locator('th')).toHaveText(['MAC', 'Port', 'Letzte Aktualisierung']);
|
||||||
|
await expect(sat.locator('tbody tr').first().locator('td')).toHaveText([/^[0-9A-F:]{17}$/, /^Port \d+$/, /^\d\d:\d\d:\d\d$/]);
|
||||||
|
await sat.getByRole('button', { name: 'Tabelle löschen' }).click();
|
||||||
|
await expect(sat.locator('tbody tr')).toHaveCount(0);
|
||||||
|
await sat.locator('.desktop-close').click();
|
||||||
|
|
||||||
|
// "Pakete verwerfen" held while a ping runs: answers lost, the frames listed.
|
||||||
|
await tap(page, web);
|
||||||
|
await desktop.locator('[data-program="commandPrompt"]').click();
|
||||||
|
await input.fill('ping 141.99.4.10');
|
||||||
|
await input.press('Enter');
|
||||||
|
const hold = page.getByRole('button', { name: 'Pakete verwerfen' });
|
||||||
|
const box = (await hold.boundingBox())!;
|
||||||
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||||
|
await page.mouse.down();
|
||||||
|
await expect(hold).toHaveAttribute('aria-pressed', 'true');
|
||||||
|
await page.waitForTimeout(2_500);
|
||||||
|
await page.mouse.up();
|
||||||
|
await expect(hold).toHaveAttribute('aria-pressed', 'false');
|
||||||
|
// The second ping's statistics: an answer lost.
|
||||||
|
await expect(desktop.locator('.terminal-text')).toContainText(/Paketstatistik ---[^]*Paketstatistik ---\n4 Paket\(e\) gesendet, [0-3] Paket\(e\) empfangen/, { timeout: 30_000 });
|
||||||
|
await expect(desktop.locator('.terminal-text')).toContainText('-- Timeout!');
|
||||||
|
const toggle = page.getByRole('button', { name: /^Verworfen: [1-9]/ });
|
||||||
|
await toggle.click();
|
||||||
|
await expect(page.getByLabel('Verworfene Pakete')).toContainText(/\d\d:\d\d:\d\d\.\d\d\d : \[src=[0-9A-F:]+, dest=[0-9A-F:]+, type=0x8/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps “Pakete verwerfen” in reach on a phone', async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 360, height: 740 });
|
||||||
|
await openEditor(page, server.base, 'nat_with_mapped_port.fls');
|
||||||
|
await page.getByRole('button', { name: 'Wechselt in den Aktionsmodus' }).click();
|
||||||
|
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
|
||||||
|
await expect(page.getByRole('button', { name: 'Pakete verwerfen' })).toBeInViewport({ ratio: 1 });
|
||||||
|
await expect(page.getByRole('button', { name: /^Verworfen: / })).toBeInViewport({ ratio: 1 });
|
||||||
|
// Every part of the toolbar within the screen (the dev page's own fields above the editor are wider; they are not the editor's).
|
||||||
|
const rights = await page.locator('.editor-toolbar > *').evaluateAll((parts) => parts.map((part) => part.getBoundingClientRect().right));
|
||||||
|
expect(Math.max(...rights)).toBeLessThanOrEqual(360);
|
||||||
|
});
|
||||||
@@ -22,6 +22,8 @@ import { DeviceSheet } from './sheet/DeviceSheet.js';
|
|||||||
import { DesktopWindow, type DesktopWindowName } from './desktop/DesktopWindow.js';
|
import { DesktopWindow, type DesktopWindowName } from './desktop/DesktopWindow.js';
|
||||||
import { useLanguage, useTranslate } from './i18n/i18n.js';
|
import { useLanguage, useTranslate } from './i18n/i18n.js';
|
||||||
import { ModeBar } from './simulation/ModeBar.js';
|
import { ModeBar } from './simulation/ModeBar.js';
|
||||||
|
import { DropControl } from './simulation/DropControl.js';
|
||||||
|
import { ViewerWindow, type ViewerKind } from './simulation/ViewerWindow.js';
|
||||||
import { useSimulation, type SimHostFactory } from './simulation/useSimulation.js';
|
import { useSimulation, type SimHostFactory } from './simulation/useSimulation.js';
|
||||||
|
|
||||||
/** The document's drawing, read anew after every change. */
|
/** The document's drawing, read anew after every change. */
|
||||||
@@ -73,9 +75,10 @@ const toolButton: CSSProperties = {
|
|||||||
/** Whether a key press belongs to a field being typed in. */
|
/** Whether a key press belongs to a field being typed in. */
|
||||||
const typing = (target: EventTarget | null) => target instanceof HTMLElement && (target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName));
|
const typing = (target: EventTarget | null) => target instanceof HTMLElement && (target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName));
|
||||||
|
|
||||||
/** A desktop open on the surface: whose, where its window lies, and how far up (the last one touched on top). */
|
/** A desktop open on the surface — or a switch's or home router's viewer (`viewer`) —: whose, where its window lies, and how far up (the last one touched on top). */
|
||||||
interface OpenDesktop {
|
interface OpenDesktop {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
|
readonly viewer?: ViewerKind;
|
||||||
readonly x: number;
|
readonly x: number;
|
||||||
readonly y: number;
|
readonly y: number;
|
||||||
readonly z: number;
|
readonly z: number;
|
||||||
@@ -130,18 +133,22 @@ export function Editor({ doc, createSimHost }: { readonly doc: Y.Doc; readonly c
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [simulation.failed]);
|
}, [simulation.failed]);
|
||||||
|
|
||||||
// A desktop whose computer is gone, or is no computer any more, closes (Java: reopenDialogs).
|
// A desktop whose computer is gone, or is no computer any more, closes (Java: reopenDialogs); so does a viewer whose switch or home router is.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const computers = new Set(view.nodes.filter((node) => node.kind === 'pc' || node.kind === 'notebook').map((node) => node.id));
|
const kinds = new Map(view.nodes.map((node) => [node.id, node.kind]));
|
||||||
setDesktops((open) => (open.every((each) => computers.has(each.id)) ? open : open.filter((each) => computers.has(each.id))));
|
const fits = (each: OpenDesktop) => {
|
||||||
|
const kind = kinds.get(each.id);
|
||||||
|
return each.viewer === 'sat' ? kind === 'networkSwitch' : each.viewer === 'nat' ? kind === 'gateway' : kind === 'pc' || kind === 'notebook';
|
||||||
|
};
|
||||||
|
setDesktops((open) => (open.every(fits) ? open : open.filter(fits)));
|
||||||
}, [view]);
|
}, [view]);
|
||||||
|
|
||||||
/** Opens a computer's desktop, or brings it to the front. */
|
/** Opens a computer's desktop, or a switch's or home router's viewer, or brings it to the front. */
|
||||||
const openDesktop = useCallback((id: string) => {
|
const openDesktop = useCallback((id: string, viewer?: ViewerKind) => {
|
||||||
setDesktops((open) => {
|
setDesktops((open) => {
|
||||||
if (open.some((each) => each.id === id)) return toFront(open, id);
|
if (open.some((each) => each.id === id)) return toFront(open, id);
|
||||||
// Java stacks a desktop that does not fit beside the others a little further on.
|
// Java stacks a desktop that does not fit beside the others a little further on.
|
||||||
return [...open, { id, x: 24 + 24 * open.length, y: 76 + 24 * open.length, z: Math.max(0, ...open.map((each) => each.z)) + 1, program: null }];
|
return [...open, { id, ...(viewer === undefined ? {} : { viewer }), x: 24 + 24 * open.length, y: 76 + 24 * open.length, z: Math.max(0, ...open.map((each) => each.z)) + 1, program: null }];
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -275,6 +282,9 @@ export function Editor({ doc, createSimHost }: { readonly doc: Y.Doc; readonly c
|
|||||||
onActivate={(id) => {
|
onActivate={(id) => {
|
||||||
const kind = view.nodes.find((node) => node.id === id)?.kind;
|
const kind = view.nodes.find((node) => node.id === id)?.kind;
|
||||||
if (kind === 'pc' || kind === 'notebook') openDesktop(id);
|
if (kind === 'pc' || kind === 'notebook') openDesktop(id);
|
||||||
|
// Java's click on a switch shows its table, on a home router its NAT table.
|
||||||
|
else if (kind === 'networkSwitch') openDesktop(id, 'sat');
|
||||||
|
else if (kind === 'gateway') openDesktop(id, 'nat');
|
||||||
}}
|
}}
|
||||||
onMenu={(target, at) => {
|
onMenu={(target, at) => {
|
||||||
if (simulating) {
|
if (simulating) {
|
||||||
@@ -285,9 +295,9 @@ export function Editor({ doc, createSimHost }: { readonly doc: Y.Doc; readonly c
|
|||||||
setMenu({ target, at });
|
setMenu({ target, at });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div role="toolbar" style={{ position: 'absolute', left: 12, top: 12, zIndex: 50, display: 'flex', gap: 6, alignItems: 'center' }}>
|
<div role="toolbar" className="editor-toolbar">
|
||||||
<ModeBar mode={simulation.mode} speed={simulation.speed} onMode={(mode) => (mode === 'simulation' ? simulation.start(language) : simulation.stop())} onSpeed={simulation.setSpeed} />
|
<ModeBar mode={simulation.mode} speed={simulation.speed} onMode={(mode) => (mode === 'simulation' ? simulation.start(language) : simulation.stop())} onSpeed={simulation.setSpeed} />
|
||||||
{simulating ? null : (
|
{simulating ? <DropControl simulation={simulation} /> : (
|
||||||
<>
|
<>
|
||||||
<button type="button" style={toolButton} disabled={!canUndo} onClick={() => manager?.undo()} aria-label={t('design.undo')} title={t('design.undo')}>
|
<button type="button" style={toolButton} disabled={!canUndo} onClick={() => manager?.undo()} aria-label={t('design.undo')} title={t('design.undo')}>
|
||||||
↶
|
↶
|
||||||
@@ -299,7 +309,22 @@ export function Editor({ doc, createSimHost }: { readonly doc: Y.Doc; readonly c
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{simulating
|
{simulating
|
||||||
? desktops.map((desktop) => (
|
? desktops.map((desktop) =>
|
||||||
|
desktop.viewer !== undefined ? (
|
||||||
|
<ViewerWindow
|
||||||
|
key={desktop.id}
|
||||||
|
kind={desktop.viewer}
|
||||||
|
id={desktop.id}
|
||||||
|
label={view.nodes.find((node) => node.id === desktop.id)?.label ?? ''}
|
||||||
|
simulation={simulation}
|
||||||
|
x={desktop.x}
|
||||||
|
y={desktop.y}
|
||||||
|
z={desktop.z}
|
||||||
|
onMove={(x, y) => setDesktops((open) => open.map((each) => (each.id === desktop.id ? { ...each, x, y } : each)))}
|
||||||
|
onFront={() => setDesktops((open) => toFront(open, desktop.id))}
|
||||||
|
onClose={() => setDesktops((open) => open.filter((each) => each.id !== desktop.id))}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<DesktopWindow
|
<DesktopWindow
|
||||||
key={desktop.id}
|
key={desktop.id}
|
||||||
simulation={simulation}
|
simulation={simulation}
|
||||||
@@ -316,7 +341,8 @@ export function Editor({ doc, createSimHost }: { readonly doc: Y.Doc; readonly c
|
|||||||
onFront={() => setDesktops((open) => toFront(open, desktop.id))}
|
onFront={() => setDesktops((open) => toFront(open, desktop.id))}
|
||||||
onClose={() => setDesktops((open) => open.filter((each) => each.id !== desktop.id))}
|
onClose={() => setDesktops((open) => open.filter((each) => each.id !== desktop.id))}
|
||||||
/>
|
/>
|
||||||
))
|
),
|
||||||
|
)
|
||||||
: null}
|
: null}
|
||||||
{menu === null ? null : <ContextMenu at={menu.at} items={menuItems(menu.target)} onClose={() => setMenu(null)} />}
|
{menu === null ? null : <ContextMenu at={menu.at} items={menuItems(menu.target)} onClose={() => setMenu(null)} />}
|
||||||
{routerAt === null ? null : (
|
{routerAt === null ? null : (
|
||||||
|
|||||||
@@ -1251,3 +1251,93 @@
|
|||||||
.gnutella-table tbody tr[tabindex] {
|
.gnutella-table tbody tr[tabindex] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A switch's or home router's viewer (SatViewer, NatViewer): a table and its
|
||||||
|
* reset, in a window as small as Java's dialog allows the NAT table's six columns. */
|
||||||
|
.viewer-window {
|
||||||
|
width: 600px;
|
||||||
|
height: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
overflow: auto;
|
||||||
|
background: var(--surface-card);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-table th,
|
||||||
|
.viewer-table td {
|
||||||
|
padding: 6px;
|
||||||
|
border-bottom: 1px solid var(--border-default);
|
||||||
|
text-align: left;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-table th {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-bar button,
|
||||||
|
.drop-control button {
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--surface-raised);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "Pakete verwerfen" (SimulationControl): held, then the list of dropped frames below. */
|
||||||
|
.drop-control {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-control-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-control-hold {
|
||||||
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-control button[aria-pressed='true'] {
|
||||||
|
background: var(--danger-soft);
|
||||||
|
border-color: var(--danger);
|
||||||
|
color: var(--danger-on-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-control-list {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
left: 0;
|
||||||
|
width: min(720px, calc(100vw - 32px));
|
||||||
|
max-height: 240px;
|
||||||
|
margin: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--surface-card);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font: 12px ui-monospace, Menlo, Consolas, monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|||||||
@@ -84,6 +84,9 @@
|
|||||||
"dnsServer.tabMX": "Mailaustausch (MX)",
|
"dnsServer.tabMX": "Mailaustausch (MX)",
|
||||||
"dnsServer.tabNS": "Nameserver (NS)",
|
"dnsServer.tabNS": "Nameserver (NS)",
|
||||||
"dnsServer.tables": "Einträge",
|
"dnsServer.tables": "Einträge",
|
||||||
|
"drop.hold": "Pakete verwerfen",
|
||||||
|
"drop.list": "Verworfen: {count}",
|
||||||
|
"drop.listLabel": "Verworfene Pakete",
|
||||||
"echoServer.log": "Ausgabe",
|
"echoServer.log": "Ausgabe",
|
||||||
"echoServer.port": "Port:",
|
"echoServer.port": "Port:",
|
||||||
"echoServer.start": "Starten",
|
"echoServer.start": "Starten",
|
||||||
@@ -386,6 +389,19 @@
|
|||||||
"simpleClient.port": "Server-Port:",
|
"simpleClient.port": "Server-Port:",
|
||||||
"simpleClient.receiveFailed": "Keine Verbindung hergestellt!",
|
"simpleClient.receiveFailed": "Keine Verbindung hergestellt!",
|
||||||
"simpleClient.send": "Senden",
|
"simpleClient.send": "Senden",
|
||||||
|
"tables.lanAddress": "LAN-Adresse",
|
||||||
|
"tables.lanPort": "LAN-Port",
|
||||||
|
"tables.lastUpdate": "Letzte Aktualisierung",
|
||||||
|
"tables.mac": "MAC",
|
||||||
|
"tables.natReset": "Tabelle zurücksetzen",
|
||||||
|
"tables.natTitle": "NAT-Tabelle",
|
||||||
|
"tables.port": "Port",
|
||||||
|
"tables.portMapping": "Portfreigabe",
|
||||||
|
"tables.portNumber": "Port",
|
||||||
|
"tables.protocol": "Protokoll",
|
||||||
|
"tables.satReset": "Tabelle löschen",
|
||||||
|
"tables.satTitle": "SAT Tabelle",
|
||||||
|
"tables.wanAddress": "WAN-Adresse",
|
||||||
"terminal.commands": "Befehle",
|
"terminal.commands": "Befehle",
|
||||||
"terminal.ctrlC": "Strg+C",
|
"terminal.ctrlC": "Strg+C",
|
||||||
"terminal.input": "Befehl eingeben",
|
"terminal.input": "Befehl eingeben",
|
||||||
|
|||||||
@@ -84,6 +84,9 @@
|
|||||||
"dnsServer.tabMX": "Mail exchange (MX)",
|
"dnsServer.tabMX": "Mail exchange (MX)",
|
||||||
"dnsServer.tabNS": "Name server (NS)",
|
"dnsServer.tabNS": "Name server (NS)",
|
||||||
"dnsServer.tables": "Records",
|
"dnsServer.tables": "Records",
|
||||||
|
"drop.hold": "Drop Packets",
|
||||||
|
"drop.list": "Dropped: {count}",
|
||||||
|
"drop.listLabel": "Dropped packets",
|
||||||
"echoServer.log": "Output",
|
"echoServer.log": "Output",
|
||||||
"echoServer.port": "Port:",
|
"echoServer.port": "Port:",
|
||||||
"echoServer.start": "Start",
|
"echoServer.start": "Start",
|
||||||
@@ -386,6 +389,19 @@
|
|||||||
"simpleClient.port": "Server port:",
|
"simpleClient.port": "Server port:",
|
||||||
"simpleClient.receiveFailed": "No connection established!",
|
"simpleClient.receiveFailed": "No connection established!",
|
||||||
"simpleClient.send": "Send",
|
"simpleClient.send": "Send",
|
||||||
|
"tables.lanAddress": "LAN Address",
|
||||||
|
"tables.lanPort": "LAN Port",
|
||||||
|
"tables.lastUpdate": "Last Update",
|
||||||
|
"tables.mac": "MAC",
|
||||||
|
"tables.natReset": "Reset table",
|
||||||
|
"tables.natTitle": "NAT Table",
|
||||||
|
"tables.port": "Port",
|
||||||
|
"tables.portMapping": "Port Mapping",
|
||||||
|
"tables.portNumber": "Port",
|
||||||
|
"tables.protocol": "Protocol",
|
||||||
|
"tables.satReset": "Clear Table",
|
||||||
|
"tables.satTitle": "SAT table",
|
||||||
|
"tables.wanAddress": "WAN Address",
|
||||||
"terminal.commands": "Commands",
|
"terminal.commands": "Commands",
|
||||||
"terminal.ctrlC": "Ctrl+C",
|
"terminal.ctrlC": "Ctrl+C",
|
||||||
"terminal.input": "Enter a command",
|
"terminal.input": "Enter a command",
|
||||||
|
|||||||
@@ -7,6 +7,26 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The mode buttons (and the design's undo and redo, the simulation's
|
||||||
|
* "Pakete verwerfen") over the surface: they wrap on a narrow screen; the
|
||||||
|
* space between them lets taps through to the surface. */
|
||||||
|
.editor-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
z-index: 50;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar > * {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-area {
|
.editor-area {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
// Java's "Pakete verwerfen" (SimulationControl): while the button is held,
|
||||||
|
// every cable drops what it carries; below it, the frames dropped in this
|
||||||
|
// simulation, each with its time of day and Java's words for it
|
||||||
|
// (EthernetFrame.toString). The button is held with a finger, the mouse, or
|
||||||
|
// Space or Enter; letting go (it lets the captured pointer go), a cancel or
|
||||||
|
// leaving the button by keyboard ends it.
|
||||||
|
//
|
||||||
|
// Better than Java: the list folds away and shows how many it holds (Java's
|
||||||
|
// panel takes the bottom of the window).
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState, type KeyboardEvent, type PointerEvent as ReactPointerEvent } from 'react';
|
||||||
|
import { useTranslate } from '../i18n/i18n.js';
|
||||||
|
import { timeOfDay } from './timeOfDay.js';
|
||||||
|
import type { Simulation } from './useSimulation.js';
|
||||||
|
|
||||||
|
export function DropControl({ simulation }: { readonly simulation: Simulation }) {
|
||||||
|
const t = useTranslate();
|
||||||
|
const { watch, drop, dropped, clock } = simulation;
|
||||||
|
useEffect(() => watch(['dropped']), [watch]);
|
||||||
|
const [held, setHeld] = useState(false);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const holding = useRef(false);
|
||||||
|
const hold = (on: boolean) => {
|
||||||
|
if (holding.current === on) return;
|
||||||
|
holding.current = on;
|
||||||
|
setHeld(on);
|
||||||
|
drop(on);
|
||||||
|
};
|
||||||
|
// Let go when the control goes (the simulation stopped while it was held).
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
if (holding.current) drop(false);
|
||||||
|
},
|
||||||
|
[drop],
|
||||||
|
);
|
||||||
|
const onPointerDown = (event: ReactPointerEvent<HTMLButtonElement>) => {
|
||||||
|
event.currentTarget.setPointerCapture(event.pointerId);
|
||||||
|
hold(true);
|
||||||
|
};
|
||||||
|
const onKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
|
||||||
|
if ((event.key === ' ' || event.key === 'Enter') && !event.repeat) {
|
||||||
|
event.preventDefault();
|
||||||
|
hold(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onKeyUp = (event: KeyboardEvent<HTMLButtonElement>) => {
|
||||||
|
if (event.key === ' ' || event.key === 'Enter') hold(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="drop-control">
|
||||||
|
<div className="drop-control-bar">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="drop-control-hold"
|
||||||
|
aria-pressed={held}
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerCancel={() => hold(false)}
|
||||||
|
onLostPointerCapture={() => hold(false)}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
|
onBlur={() => hold(false)}
|
||||||
|
onContextMenu={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
{t('drop.hold')}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="drop-control-toggle" aria-expanded={open} onClick={() => setOpen(!open)}>
|
||||||
|
{t('drop.list', { count: dropped.length })}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{open ? (
|
||||||
|
<pre className="drop-control-list" aria-label={t('drop.listLabel')} tabIndex={0}>
|
||||||
|
{dropped.map((unit) => `${timeOfDay(clock, unit.timeMilliseconds, true)} : ${unit.text}\n`).join('')}
|
||||||
|
</pre>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// A switch's or a home router's viewer while the simulation runs, after
|
||||||
|
// Java's SatViewer and NatViewer (measured with tools/java-oracle's
|
||||||
|
// ViewersRun): a window titled "SAT Tabelle <name>" or "NAT-Tabelle <name>"
|
||||||
|
// with the table — MAC, port, last update; protocol, port mapping, WAN
|
||||||
|
// address, LAN address, LAN port, last update — and the table reset. A tap on
|
||||||
|
// the device opens it, as Java's click does. The tables are the room's.
|
||||||
|
//
|
||||||
|
// Better than Java: the reset is a button (Java's is in a menu of the right
|
||||||
|
// mouse button), and a switch's addresses come in their order (Java's in its
|
||||||
|
// hash map's).
|
||||||
|
|
||||||
|
import { useEffect, useState, type PointerEvent as ReactPointerEvent } from 'react';
|
||||||
|
import type { NatView, SatView } from '@mdg-filius/protocol';
|
||||||
|
import { ICON_URLS } from '../canvas/icons.js';
|
||||||
|
import { useTranslate } from '../i18n/i18n.js';
|
||||||
|
import { timeOfDay } from './timeOfDay.js';
|
||||||
|
import type { Simulation } from './useSimulation.js';
|
||||||
|
|
||||||
|
export type ViewerKind = 'sat' | 'nat';
|
||||||
|
|
||||||
|
export interface ViewerWindowProps {
|
||||||
|
readonly kind: ViewerKind;
|
||||||
|
readonly id: string;
|
||||||
|
readonly label: string;
|
||||||
|
readonly simulation: Simulation;
|
||||||
|
readonly x: number;
|
||||||
|
readonly y: number;
|
||||||
|
readonly z: number;
|
||||||
|
readonly onMove: (x: number, y: number) => void;
|
||||||
|
readonly onFront: () => void;
|
||||||
|
readonly onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ViewerWindow({ kind, id, label, simulation, x, y, z, onMove, onFront, onClose }: ViewerWindowProps) {
|
||||||
|
const t = useTranslate();
|
||||||
|
const topic = `app:${id}/${kind}` as const;
|
||||||
|
const { watch, act, clock } = simulation;
|
||||||
|
useEffect(() => watch([topic]), [watch, topic]);
|
||||||
|
const view = simulation.views.get(topic) as SatView | NatView | null | undefined;
|
||||||
|
const [drag, setDrag] = useState<{ readonly pointer: number; readonly dx: number; readonly dy: number; moved: boolean } | null>(null);
|
||||||
|
const title = `${t(kind === 'sat' ? 'tables.satTitle' : 'tables.natTitle')} ${label}`;
|
||||||
|
|
||||||
|
// The title bar moves the window; the pointer is captured once it moves (pointer-capture trap).
|
||||||
|
const onPointerDown = (event: ReactPointerEvent<HTMLElement>) => {
|
||||||
|
if ((event.target as Element).closest('button') !== null) return;
|
||||||
|
setDrag({ pointer: event.pointerId, dx: event.clientX - x, dy: event.clientY - y, moved: false });
|
||||||
|
};
|
||||||
|
const onPointerMove = (event: ReactPointerEvent<HTMLElement>) => {
|
||||||
|
if (drag === null || drag.pointer !== event.pointerId) return;
|
||||||
|
if (!drag.moved) {
|
||||||
|
drag.moved = true;
|
||||||
|
event.currentTarget.setPointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
onMove(Math.max(0, event.clientX - drag.dx), Math.max(0, event.clientY - drag.dy));
|
||||||
|
};
|
||||||
|
const onPointerUp = () => setDrag(null);
|
||||||
|
|
||||||
|
const columns =
|
||||||
|
kind === 'sat' ? ['tables.mac', 'tables.port', 'tables.lastUpdate'] : ['tables.protocol', 'tables.portMapping', 'tables.wanAddress', 'tables.lanAddress', 'tables.lanPort', 'tables.lastUpdate'];
|
||||||
|
const rows: readonly (readonly string[])[] =
|
||||||
|
view === null || view === undefined
|
||||||
|
? []
|
||||||
|
: kind === 'sat'
|
||||||
|
? (view as SatView).rows.map((row) => [row.macAddress, `${t('tables.portNumber')} ${row.port}`, timeOfDay(clock, row.updatedAtMilliseconds)])
|
||||||
|
: (view as NatView).rows.map((row) => [...row.cells, row.updatedAtMilliseconds === undefined ? '-' : timeOfDay(clock, row.updatedAtMilliseconds)]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="desktop-window viewer-window" role="dialog" aria-label={title} data-viewer={id} style={{ left: x, top: y, zIndex: 5 + z }} onPointerDownCapture={onFront}>
|
||||||
|
<header className="desktop-titlebar" onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={onPointerUp} onPointerCancel={onPointerUp}>
|
||||||
|
<img src={ICON_URLS[kind === 'sat' ? 'switch' : 'router']} alt="" />
|
||||||
|
<h2>{title}</h2>
|
||||||
|
<button type="button" className="desktop-close" aria-label={t('sheet.close')} title={t('sheet.close')} onClick={onClose}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<div className="viewer-body">
|
||||||
|
<table className="viewer-table" aria-label={title}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{columns.map((column) => (
|
||||||
|
<th key={column}>{t(column)}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows.map((cells, row) => (
|
||||||
|
<tr key={row}>
|
||||||
|
{cells.map((cell, column) => (
|
||||||
|
<td key={column}>{cell}</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div className="viewer-bar">
|
||||||
|
<button type="button" onClick={() => act(id, kind, 'reset')}>
|
||||||
|
{t(kind === 'sat' ? 'tables.satReset' : 'tables.natReset')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// A time of the simulation as Java's viewers write the wall clock's: its
|
||||||
|
// hours, minutes and seconds (SimpleDateFormat "HH:mm:ss"), with the
|
||||||
|
// milliseconds where the list of dropped frames has them
|
||||||
|
// (Lauscher.formatTimestamp), in the viewer's time zone.
|
||||||
|
|
||||||
|
const two = (value: number) => String(value).padStart(2, '0');
|
||||||
|
|
||||||
|
/** The time of day of the simulation's time `milliseconds`, its time 0 standing for `clock` (milliseconds since 1970). */
|
||||||
|
export function timeOfDay(clock: number, milliseconds: number, withMilliseconds = false): string {
|
||||||
|
const at = new Date(clock + milliseconds);
|
||||||
|
const text = `${two(at.getHours())}:${two(at.getMinutes())}:${two(at.getSeconds())}`;
|
||||||
|
return withMilliseconds ? `${text}.${String(at.getMilliseconds()).padStart(3, '0')}` : text;
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import type * as Y from 'yjs';
|
import type * as Y from 'yjs';
|
||||||
import type { AppKind, NodeSettings, SimHost, SimMode, Topic } from '@mdg-filius/protocol';
|
import type { AppKind, DroppedUnit, NodeSettings, SimHost, SimMode, Topic } from '@mdg-filius/protocol';
|
||||||
import { LocalSimHost } from '@mdg-filius/sim';
|
import { LocalSimHost } from '@mdg-filius/sim';
|
||||||
import SimWorker from '@mdg-filius/sim/worker?worker';
|
import SimWorker from '@mdg-filius/sim/worker?worker';
|
||||||
import type { Language } from '../i18n/i18n.js';
|
import type { Language } from '../i18n/i18n.js';
|
||||||
@@ -40,10 +40,16 @@ export interface Simulation {
|
|||||||
readonly views: ReadonlyMap<string, unknown>;
|
readonly views: ReadonlyMap<string, unknown>;
|
||||||
/** Whether the core failed and the room went back to design mode, since the last start. */
|
/** Whether the core failed and the room went back to design mode, since the last start. */
|
||||||
readonly failed: boolean;
|
readonly failed: boolean;
|
||||||
|
/** The time of day (milliseconds since 1970) the simulation's time 0 stands for: a viewer's times of day are written from it. */
|
||||||
|
readonly clock: number;
|
||||||
|
/** The frames "Pakete verwerfen" dropped in this simulation, while `dropped` is watched. */
|
||||||
|
readonly dropped: readonly DroppedUnit[];
|
||||||
/** Starts in the language the person runs FILIUS in: the routers' pages come in it, as Java's in its locale. */
|
/** Starts in the language the person runs FILIUS in: the routers' pages come in it, as Java's in its locale. */
|
||||||
start(language?: Language): void;
|
start(language?: Language): void;
|
||||||
stop(): void;
|
stop(): void;
|
||||||
setSpeed(value: number): void;
|
setSpeed(value: number): void;
|
||||||
|
/** Java's "Pakete verwerfen": held (true) or let go (false). */
|
||||||
|
drop(on: boolean): void;
|
||||||
/** Watches these topics until the returned function is called; a topic two parts watch is sent for once. */
|
/** Watches these topics until the returned function is called; a topic two parts watch is sent for once. */
|
||||||
watch(topics: readonly Topic[]): () => void;
|
watch(topics: readonly Topic[]): () => void;
|
||||||
/** An action in a program's window (the room answers with its new lines). */
|
/** An action in a program's window (the room answers with its new lines). */
|
||||||
@@ -62,6 +68,8 @@ export function useSimulation(doc: Y.Doc, createHost: SimHostFactory = localSimH
|
|||||||
const [failed, setFailed] = useState(false);
|
const [failed, setFailed] = useState(false);
|
||||||
const [apps, setApps] = useState<ReadonlyMap<string, AppLines>>(new Map());
|
const [apps, setApps] = useState<ReadonlyMap<string, AppLines>>(new Map());
|
||||||
const [views, setViews] = useState<ReadonlyMap<string, unknown>>(new Map());
|
const [views, setViews] = useState<ReadonlyMap<string, unknown>>(new Map());
|
||||||
|
const [clock, setClock] = useState(0);
|
||||||
|
const [dropped, setDropped] = useState<readonly DroppedUnit[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const made = createHost(doc);
|
const made = createHost(doc);
|
||||||
@@ -71,6 +79,7 @@ export function useSimulation(doc: Y.Doc, createHost: SimHostFactory = localSimH
|
|||||||
case 'state':
|
case 'state':
|
||||||
setMode(message.mode);
|
setMode(message.mode);
|
||||||
setSpeedState(message.speed);
|
setSpeedState(message.speed);
|
||||||
|
setClock(message.clock);
|
||||||
if (message.mode === 'design') setActiveLinks(NO_LINKS);
|
if (message.mode === 'design') setActiveLinks(NO_LINKS);
|
||||||
return;
|
return;
|
||||||
case 'activity':
|
case 'activity':
|
||||||
@@ -88,6 +97,9 @@ export function useSimulation(doc: Y.Doc, createHost: SimHostFactory = localSimH
|
|||||||
return new Map(before).set(topic, { lines, live: message.from === 0 ? lines.length : (before.get(topic)?.live ?? 0) });
|
return new Map(before).set(topic, { lines, live: message.from === 0 ? lines.length : (before.get(topic)?.live ?? 0) });
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
case 'dropped':
|
||||||
|
setDropped((before) => (message.reset ? message.units : [...before, ...message.units]));
|
||||||
|
return;
|
||||||
case 'view':
|
case 'view':
|
||||||
setViews((before) => new Map(before).set(`app:${message.node}/${message.app}`, message.view));
|
setViews((before) => new Map(before).set(`app:${message.node}/${message.app}`, message.view));
|
||||||
return;
|
return;
|
||||||
@@ -112,6 +124,7 @@ export function useSimulation(doc: Y.Doc, createHost: SimHostFactory = localSimH
|
|||||||
}, []);
|
}, []);
|
||||||
const stop = useCallback(() => host.current?.send({ v: 1, kind: 'stop' }), []);
|
const stop = useCallback(() => host.current?.send({ v: 1, kind: 'stop' }), []);
|
||||||
const setSpeed = useCallback((value: number) => host.current?.send({ v: 1, kind: 'speed', value }), []);
|
const setSpeed = useCallback((value: number) => host.current?.send({ v: 1, kind: 'speed', value }), []);
|
||||||
|
const drop = useCallback((on: boolean) => host.current?.send({ v: 1, kind: 'drop', on }), []);
|
||||||
const watch = useCallback((topics: readonly Topic[]) => {
|
const watch = useCallback((topics: readonly Topic[]) => {
|
||||||
const fresh = topics.filter((topic) => {
|
const fresh = topics.filter((topic) => {
|
||||||
const count = watched.current.get(topic) ?? 0;
|
const count = watched.current.get(topic) ?? 0;
|
||||||
@@ -134,5 +147,5 @@ export function useSimulation(doc: Y.Doc, createHost: SimHostFactory = localSimH
|
|||||||
host.current?.send({ v: 1, kind: 'input', node, app, action, args, reqId: crypto.randomUUID() });
|
host.current?.send({ v: 1, kind: 'input', node, app, action, args, reqId: crypto.randomUUID() });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { mode, speed, activeLinks, nodes, apps, views, failed, start, stop, setSpeed, watch, act };
|
return { mode, speed, activeLinks, nodes, apps, views, failed, clock, dropped, start, stop, setSpeed, drop, watch, act };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,6 +206,20 @@ const FROM_JAVA: Readonly<Record<string, string>> = {
|
|||||||
'gnutella.networkTab': 'peertopeeranwendung_msg29',
|
'gnutella.networkTab': 'peertopeeranwendung_msg29',
|
||||||
'gnutella.leave': 'peertopeeranwendung_msg30',
|
'gnutella.leave': 'peertopeeranwendung_msg30',
|
||||||
'gnutella.leaveHint': 'peertopeeranwendung_msg31',
|
'gnutella.leaveHint': 'peertopeeranwendung_msg31',
|
||||||
|
'tables.satTitle': 'guievents_msg8',
|
||||||
|
'tables.mac': 'guievents_msg9',
|
||||||
|
'tables.port': 'guievents_msg10',
|
||||||
|
'tables.lastUpdate': 'guievents_msg27',
|
||||||
|
'tables.satReset': 'guievents_msg28',
|
||||||
|
'tables.natTitle': 'guievents_msg29',
|
||||||
|
'tables.protocol': 'guievents_msg30',
|
||||||
|
'tables.wanAddress': 'guievents_msg31',
|
||||||
|
'tables.portMapping': 'guievents_msg32',
|
||||||
|
'tables.lanAddress': 'guievents_msg33',
|
||||||
|
'tables.lanPort': 'guievents_msg34',
|
||||||
|
'tables.natReset': 'guievents_msg36',
|
||||||
|
'tables.portNumber': 'sw_switchfirmware_msg1',
|
||||||
|
'drop.hold': 'simulation_control_drop_packets',
|
||||||
'personalFirewall.hint': 'firewall_msg2',
|
'personalFirewall.hint': 'firewall_msg2',
|
||||||
'personalFirewall.port': 'firewall_msg3',
|
'personalFirewall.port': 'firewall_msg3',
|
||||||
'personalFirewall.allSenders': 'firewall_msg4',
|
'personalFirewall.allSenders': 'firewall_msg4',
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// A simulation time as Java's viewers write the wall clock: HH:mm:ss, and
|
||||||
|
// with milliseconds for the dropped frames (Lauscher.formatTimestamp), in the
|
||||||
|
// viewer's time zone.
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { timeOfDay } from '../src/simulation/timeOfDay.js';
|
||||||
|
|
||||||
|
describe('a time of day', () => {
|
||||||
|
it('pads hours, minutes, seconds and milliseconds as Java does', () => {
|
||||||
|
const clock = new Date(2026, 9, 3, 7, 5, 3, 4).getTime();
|
||||||
|
expect(timeOfDay(clock, 0)).toBe('07:05:03');
|
||||||
|
expect(timeOfDay(clock, 0, true)).toBe('07:05:03.004');
|
||||||
|
expect(timeOfDay(clock, 56_996 + 3_600_000 * 15, true)).toBe('22:06:00.000');
|
||||||
|
expect(timeOfDay(clock, 96, true)).toBe('07:05:03.100');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,5 +6,6 @@ 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 './gnutella.js';
|
||||||
|
export * from './viewers.js';
|
||||||
export * from './mail.js';
|
export * from './mail.js';
|
||||||
export * from './web.js';
|
export * from './web.js';
|
||||||
|
|||||||
@@ -8,19 +8,20 @@ import type { CaptureRow, PageLanguage } from '@mdg-filius/core';
|
|||||||
|
|
||||||
export const SIM_PROTOCOL_VERSION = 1;
|
export const SIM_PROTOCOL_VERSION = 1;
|
||||||
|
|
||||||
/** The programs whose windows a client can watch and act in. */
|
/** The programs whose windows a client can watch and act in; and a switch's and a home router's viewers (sat, nat). */
|
||||||
export const APP_KINDS = ['terminal', 'webBrowser', 'webServer', 'emailClient', 'emailServer', 'echoServer', 'simpleClient', 'gnutella', 'dnsServer', 'firewall'] as const;
|
export const APP_KINDS = ['terminal', 'webBrowser', 'webServer', 'emailClient', 'emailServer', 'echoServer', 'simpleClient', 'gnutella', 'dnsServer', 'firewall', 'sat', 'nat'] as const;
|
||||||
export type AppKind = (typeof APP_KINDS)[number];
|
export type AppKind = (typeof APP_KINDS)[number];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What a client subscribes to: which cables carry frames (`activity`), a
|
* What a client subscribes to: which cables carry frames (`activity`), a
|
||||||
* card's capture (`trace:<portId>`), a program's window
|
* card's capture (`trace:<portId>`), a program's window
|
||||||
* (`app:<nodeId>/<app>`), a computer's network settings as it runs with them
|
* (`app:<nodeId>/<app>`), a computer's network settings as it runs with them
|
||||||
* (`node:<nodeId>`: its desktop's title and network info).
|
* (`node:<nodeId>`: its desktop's title and network info), the frames
|
||||||
|
* "Pakete verwerfen" dropped (`dropped`).
|
||||||
*/
|
*/
|
||||||
export type Topic = 'activity' | `trace:${string}` | `app:${string}/${AppKind}` | `node:${string}`;
|
export type Topic = 'activity' | 'dropped' | `trace:${string}` | `app:${string}/${AppKind}` | `node:${string}`;
|
||||||
|
|
||||||
export const CLIENT_MESSAGE_KINDS = ['sub', 'unsub', 'start', 'stop', 'speed', 'input'] as const;
|
export const CLIENT_MESSAGE_KINDS = ['sub', 'unsub', 'start', 'stop', 'speed', 'drop', 'input'] as const;
|
||||||
export type ClientMessageKind = (typeof CLIENT_MESSAGE_KINDS)[number];
|
export type ClientMessageKind = (typeof CLIENT_MESSAGE_KINDS)[number];
|
||||||
|
|
||||||
export type ClientMessage =
|
export type ClientMessage =
|
||||||
@@ -36,6 +37,8 @@ export type ClientMessage =
|
|||||||
| { readonly v: 1; readonly kind: 'stop' }
|
| { readonly v: 1; readonly kind: 'stop' }
|
||||||
/** Java's speed slider, 1 to 100 per cent: how long a frame takes over a cable. */
|
/** Java's speed slider, 1 to 100 per cent: how long a frame takes over a cable. */
|
||||||
| { readonly v: 1; readonly kind: 'speed'; readonly value: number }
|
| { readonly v: 1; readonly kind: 'speed'; readonly value: number }
|
||||||
|
/** Java's "Pakete verwerfen" pressed (on) or let go: every cable drops what it carries while any client holds it. */
|
||||||
|
| { readonly v: 1; readonly kind: 'drop'; readonly on: boolean }
|
||||||
/** An action in a program's window, answered by `ack` or `error` with the same reqId. */
|
/** An action in a program's window, answered by `ack` or `error` with the same reqId. */
|
||||||
| {
|
| {
|
||||||
readonly v: 1;
|
readonly v: 1;
|
||||||
@@ -47,7 +50,7 @@ export type ClientMessage =
|
|||||||
readonly reqId: string;
|
readonly reqId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HOST_MESSAGE_KINDS = ['state', 'activity', 'trace', 'app', 'view', 'node', 'ack', 'error'] as const;
|
export const HOST_MESSAGE_KINDS = ['state', 'activity', 'trace', 'dropped', 'app', 'view', 'node', 'ack', 'error'] as const;
|
||||||
export type HostMessageKind = (typeof HOST_MESSAGE_KINDS)[number];
|
export type HostMessageKind = (typeof HOST_MESSAGE_KINDS)[number];
|
||||||
|
|
||||||
export type SimMode = 'design' | 'simulation';
|
export type SimMode = 'design' | 'simulation';
|
||||||
@@ -59,17 +62,31 @@ export interface TraceFrame {
|
|||||||
readonly layeredRows: readonly CaptureRow[];
|
readonly layeredRows: readonly CaptureRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A frame "Pakete verwerfen" dropped: its number among them, when, and what it was (EthernetFrame.toString). */
|
||||||
|
export interface DroppedUnit {
|
||||||
|
readonly number: number;
|
||||||
|
readonly timeMilliseconds: number;
|
||||||
|
readonly text: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** engineFailed: the core threw (a part it has not got yet, such as remote links); the room is back in design mode. */
|
/** engineFailed: the core threw (a part it has not got yet, such as remote links); the room is back in design mode. */
|
||||||
export const ERROR_CODES = ['badMessage', 'badVersion', 'unknownKind', 'badTopic', 'notRunning', 'unknownNode', 'unknownApp', 'unknownAction', 'badArgs', 'engineFailed'] as const;
|
export const ERROR_CODES = ['badMessage', 'badVersion', 'unknownKind', 'badTopic', 'notRunning', 'unknownNode', 'unknownApp', 'unknownAction', 'badArgs', 'engineFailed'] as const;
|
||||||
export type ErrorCode = (typeof ERROR_CODES)[number];
|
export type ErrorCode = (typeof ERROR_CODES)[number];
|
||||||
|
|
||||||
export type HostMessage =
|
export type HostMessage =
|
||||||
/** The room's mode, speed and time; sent to every client on a change and when it connects. */
|
/**
|
||||||
| { readonly v: 1; readonly kind: 'state'; readonly mode: SimMode; readonly speed: number; readonly t: number }
|
* The room's mode, speed and time; sent to every client on a change and
|
||||||
|
* when it connects. `clock`: the time of day, in milliseconds since 1970,
|
||||||
|
* that the simulation's time 0 stands for (a viewer writes its times of day
|
||||||
|
* from it, as Java's write the wall clock's).
|
||||||
|
*/
|
||||||
|
| { readonly v: 1; readonly kind: 'state'; readonly mode: SimMode; readonly speed: number; readonly t: number; readonly clock: number }
|
||||||
/** The cables that carried a frame since the last tick. */
|
/** The cables that carried a frame since the last tick. */
|
||||||
| { readonly v: 1; readonly kind: 'activity'; readonly links: readonly string[] }
|
| { readonly v: 1; readonly kind: 'activity'; readonly links: readonly string[] }
|
||||||
/** A card's new frames; on subscribing, all it holds (`reset`). */
|
/** A card's new frames; on subscribing, all it holds (`reset`). */
|
||||||
| { readonly v: 1; readonly kind: 'trace'; readonly portId: string; readonly frames: readonly TraceFrame[]; readonly reset: boolean }
|
| { readonly v: 1; readonly kind: 'trace'; readonly portId: string; readonly frames: readonly TraceFrame[]; readonly reset: boolean }
|
||||||
|
/** The frames "Pakete verwerfen" dropped since the start, new ones; on subscribing, all (`reset`): each its number, time and Java's text. */
|
||||||
|
| { readonly v: 1; readonly kind: 'dropped'; readonly units: readonly DroppedUnit[]; readonly reset: boolean }
|
||||||
/** A program's new window lines from line `from` on; on subscribing, all (from 0). */
|
/** A program's new window lines from line `from` on; on subscribing, all (from 0). */
|
||||||
| { readonly v: 1; readonly kind: 'app'; readonly node: string; readonly app: AppKind; readonly from: number; readonly lines: readonly unknown[] }
|
| { readonly v: 1; readonly kind: 'app'; readonly node: string; readonly app: AppKind; readonly from: number; readonly lines: readonly unknown[] }
|
||||||
/**
|
/**
|
||||||
@@ -79,7 +96,8 @@ export type HostMessage =
|
|||||||
* web server's (WebServerView), the e-mail program's and server's
|
* web server's (WebServerView), the e-mail program's and server's
|
||||||
* (EmailClientView, EmailServerView), the echo server's and the generic
|
* (EmailClientView, EmailServerView), the echo server's and the generic
|
||||||
* client's (EchoServerView, SimpleClientView), the DNS server's
|
* client's (EchoServerView, SimpleClientView), the DNS server's
|
||||||
* (DnsServerView).
|
* (DnsServerView), the firewall's (FirewallView), Gnutella's
|
||||||
|
* (GnutellaView), a switch's and a home router's tables (SatView, NatView).
|
||||||
*/
|
*/
|
||||||
| { readonly v: 1; readonly kind: 'view'; readonly node: string; readonly app: AppKind; readonly view: unknown }
|
| { readonly v: 1; readonly kind: 'view'; readonly node: string; readonly app: AppKind; readonly view: unknown }
|
||||||
/**
|
/**
|
||||||
@@ -106,7 +124,7 @@ const bad = (code: ErrorCode, detail: string) => ({ error: { code, detail } });
|
|||||||
|
|
||||||
/** Whether a topic is one the room knows. */
|
/** Whether a topic is one the room knows. */
|
||||||
export function isTopic(value: unknown): value is Topic {
|
export function isTopic(value: unknown): value is Topic {
|
||||||
if (value === 'activity') return true;
|
if (value === 'activity' || value === 'dropped') return true;
|
||||||
if (typeof value !== 'string') return false;
|
if (typeof value !== 'string') return false;
|
||||||
if (value.startsWith('trace:')) return value.length > 'trace:'.length;
|
if (value.startsWith('trace:')) return value.length > 'trace:'.length;
|
||||||
if (value.startsWith('node:')) return /^node:[^/]+$/.test(value);
|
if (value.startsWith('node:')) return /^node:[^/]+$/.test(value);
|
||||||
@@ -138,6 +156,9 @@ export function parseClientMessage(value: unknown): Parsed<ClientMessage> {
|
|||||||
case 'speed':
|
case 'speed':
|
||||||
if (typeof value.value !== 'number' || !Number.isFinite(value.value)) return bad('badMessage', 'speed is no number');
|
if (typeof value.value !== 'number' || !Number.isFinite(value.value)) return bad('badMessage', 'speed is no number');
|
||||||
return { message: { v: 1, kind: 'speed', value: value.value } };
|
return { message: { v: 1, kind: 'speed', value: value.value } };
|
||||||
|
case 'drop':
|
||||||
|
if (typeof value.on !== 'boolean') return bad('badMessage', 'on is no boolean');
|
||||||
|
return { message: { v: 1, kind: 'drop', on: value.on } };
|
||||||
case 'input': {
|
case 'input': {
|
||||||
const { node, app, action, args, reqId } = value;
|
const { node, app, action, args, reqId } = value;
|
||||||
if (typeof reqId !== 'string') return bad('badMessage', 'reqId is no string');
|
if (typeof reqId !== 'string') return bad('badMessage', 'reqId is no string');
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// What the viewers of a running simulation show, as the room keeps them and
|
||||||
|
// every client shows them (plan §7.3), after Java's SatViewer and NatViewer
|
||||||
|
// (measured with tools/java-oracle's ViewersRun): a switch's source address
|
||||||
|
// table and a home router's NAT table. A time is the simulation's; the
|
||||||
|
// viewer writes it as a time of day (the `state` message's `clock`).
|
||||||
|
|
||||||
|
/** A switch's learned addresses: the address (in capitals: the switch learns it so, as holeSAT writes it), the port's number (from 1), its last update. */
|
||||||
|
export interface SatView {
|
||||||
|
readonly rows: readonly { readonly macAddress: string; readonly port: number; readonly updatedAtMilliseconds: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A home router's NAT entries: protocol, WAN port, remote address, LAN address, LAN port in Java's words; the last update, none for the port forwarding's. */
|
||||||
|
export interface NatView {
|
||||||
|
readonly rows: readonly { readonly cells: readonly [string, string, string, string, string]; readonly updatedAtMilliseconds?: number }[];
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ describe("a client's messages", () => {
|
|||||||
{ v: 1, kind: 'start' },
|
{ v: 1, kind: 'start' },
|
||||||
{ v: 1, kind: 'stop' },
|
{ v: 1, kind: 'stop' },
|
||||||
{ v: 1, kind: 'speed', value: 50 },
|
{ v: 1, kind: 'speed', value: 50 },
|
||||||
|
{ v: 1, kind: 'drop', on: true },
|
||||||
{ v: 1, kind: 'input', node: 'node-1', app: 'terminal', action: 'enter', args: { line: 'ping 1.2.3.4' }, reqId: 'r1' },
|
{ v: 1, kind: 'input', node: 'node-1', app: 'terminal', action: 'enter', args: { line: 'ping 1.2.3.4' }, reqId: 'r1' },
|
||||||
];
|
];
|
||||||
expect(messages.map((each) => each.kind)).toEqual([...CLIENT_MESSAGE_KINDS]);
|
expect(messages.map((each) => each.kind)).toEqual([...CLIENT_MESSAGE_KINDS]);
|
||||||
@@ -31,6 +32,8 @@ describe("a client's messages", () => {
|
|||||||
expect(code({ v: 1, kind: 'sub', topics: ['activity', 'trace:'] })).toBe('badTopic');
|
expect(code({ v: 1, kind: 'sub', topics: ['activity', 'trace:'] })).toBe('badTopic');
|
||||||
expect(code({ v: 1, kind: 'unsub', topics: ['app:node/paint'] })).toBe('badTopic');
|
expect(code({ v: 1, kind: 'unsub', topics: ['app:node/paint'] })).toBe('badTopic');
|
||||||
expect(code({ v: 1, kind: 'start', language: 'fr' })).toBe('badMessage');
|
expect(code({ v: 1, kind: 'start', language: 'fr' })).toBe('badMessage');
|
||||||
|
expect(code({ v: 1, kind: 'drop', on: 'yes' })).toBe('badMessage');
|
||||||
|
expect(code({ v: 1, kind: 'sub', topics: ['dropped', 'app:node/sat', 'app:node/nat'] })).toBe('ok');
|
||||||
expect(code({ v: 1, kind: 'start', language: null })).toBe('badMessage');
|
expect(code({ v: 1, kind: 'start', language: null })).toBe('badMessage');
|
||||||
expect(code({ v: 1, kind: 'speed', value: '50' })).toBe('badMessage');
|
expect(code({ v: 1, kind: 'speed', value: '50' })).toBe('badMessage');
|
||||||
expect(code({ v: 1, kind: 'speed', value: Number.NaN })).toBe('badMessage');
|
expect(code({ v: 1, kind: 'speed', value: Number.NaN })).toBe('badMessage');
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ import {
|
|||||||
gnutellaPeerCountOfText,
|
gnutellaPeerCountOfText,
|
||||||
gnutellaResultRow,
|
gnutellaResultRow,
|
||||||
interfaceKey,
|
interfaceKey,
|
||||||
|
javaFrameText,
|
||||||
|
natViewRow,
|
||||||
IP_ADDRESS_PATTERN,
|
IP_ADDRESS_PATTERN,
|
||||||
portRuleSender,
|
portRuleSender,
|
||||||
stableGeneratedMacAddress,
|
stableGeneratedMacAddress,
|
||||||
@@ -80,6 +82,8 @@ import {
|
|||||||
type EchoServerView,
|
type EchoServerView,
|
||||||
type FirewallView,
|
type FirewallView,
|
||||||
type GnutellaView,
|
type GnutellaView,
|
||||||
|
type NatView,
|
||||||
|
type SatView,
|
||||||
type SimpleClientView,
|
type SimpleClientView,
|
||||||
type TraceFrame,
|
type TraceFrame,
|
||||||
type WebServerView,
|
type WebServerView,
|
||||||
@@ -99,6 +103,8 @@ export interface RoomOptions {
|
|||||||
readonly seed: () => number;
|
readonly seed: () => number;
|
||||||
/** How often a running simulation writes what it left into the document; WRITE_BACK_MILLISECONDS by default. */
|
/** How often a running simulation writes what it left into the document; WRITE_BACK_MILLISECONDS by default. */
|
||||||
readonly writeBackMilliseconds?: number;
|
readonly writeBackMilliseconds?: number;
|
||||||
|
/** The time of day, in milliseconds since 1970, for the viewers' times (Date.now by default). */
|
||||||
|
readonly epoch?: () => number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Client {
|
interface Client {
|
||||||
@@ -106,6 +112,8 @@ interface Client {
|
|||||||
readonly topics: Set<Topic>;
|
readonly topics: Set<Topic>;
|
||||||
/** Per card, the number of the last frame sent. */
|
/** Per card, the number of the last frame sent. */
|
||||||
readonly traceSent: Map<Id, number>;
|
readonly traceSent: Map<Id, number>;
|
||||||
|
/** The number of the last dropped frame sent; none before the first message (or after a start). */
|
||||||
|
droppedSent: number | undefined;
|
||||||
/** Per program window, how many of its program's lines it has; none while the program does not run (a stop empties it). */
|
/** Per program window, how many of its program's lines it has; none while the program does not run (a stop empties it). */
|
||||||
readonly appSent: Map<string, number>;
|
readonly appSent: Map<string, number>;
|
||||||
/** Per computer, the settings last sent (as text). */
|
/** Per computer, the settings last sent (as text). */
|
||||||
@@ -133,6 +141,8 @@ export class SimulationRoom {
|
|||||||
/** Per card, how many frames it held at the last tick. */
|
/** Per card, how many frames it held at the last tick. */
|
||||||
private framesSeen = new Map<Id, number>();
|
private framesSeen = new Map<Id, number>();
|
||||||
private lastActivity: string[] = [];
|
private lastActivity: string[] = [];
|
||||||
|
/** The clients holding "Pakete verwerfen": every cable drops while there is one. */
|
||||||
|
private readonly dropping = new Set<Client>();
|
||||||
/** Each computer's command line window, kept over stops. */
|
/** Each computer's command line window, kept over stops. */
|
||||||
private readonly terminalWindows = new Map<Id, TerminalWindow>();
|
private readonly terminalWindows = new Map<Id, TerminalWindow>();
|
||||||
/** Each computer's browser window, kept over stops. */
|
/** Each computer's browser window, kept over stops. */
|
||||||
@@ -313,13 +323,15 @@ export class SimulationRoom {
|
|||||||
/** A new client; it gets the room's state at once. */
|
/** A new client; it gets the room's state at once. */
|
||||||
connect(send: (message: HostMessage) => void): number {
|
connect(send: (message: HostMessage) => void): number {
|
||||||
const id = this.nextClient++;
|
const id = this.nextClient++;
|
||||||
this.clients.set(id, { send, topics: new Set(), traceSent: new Map(), appSent: new Map(), nodeSent: new Map(), viewSent: new Map() });
|
this.clients.set(id, { send, topics: new Set(), traceSent: new Map(), droppedSent: undefined, appSent: new Map(), nodeSent: new Map(), viewSent: new Map() });
|
||||||
send(this.state());
|
send(this.state());
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect(client: number): void {
|
disconnect(client: number): void {
|
||||||
|
const gone = this.clients.get(client);
|
||||||
this.clients.delete(client);
|
this.clients.delete(client);
|
||||||
|
if (gone !== undefined && this.dropping.delete(gone)) this.engine.setGlobalPacketLossEnabled(this.dropping.size > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** How many clients the room has (F stops a room nobody watches). */
|
/** How many clients the room has (F stops a room nobody watches). */
|
||||||
@@ -350,6 +362,7 @@ export class SimulationRoom {
|
|||||||
for (const topic of input.topics) {
|
for (const topic of input.topics) {
|
||||||
client.topics.add(topic);
|
client.topics.add(topic);
|
||||||
if (topic.startsWith('trace:')) client.traceSent.delete(topic.slice('trace:'.length));
|
if (topic.startsWith('trace:')) client.traceSent.delete(topic.slice('trace:'.length));
|
||||||
|
if (topic === 'dropped') client.droppedSent = undefined;
|
||||||
if (topic.startsWith('app:')) {
|
if (topic.startsWith('app:')) {
|
||||||
client.appSent.delete(topic);
|
client.appSent.delete(topic);
|
||||||
client.viewSent.delete(topic);
|
client.viewSent.delete(topic);
|
||||||
@@ -367,6 +380,15 @@ export class SimulationRoom {
|
|||||||
case 'stop':
|
case 'stop':
|
||||||
this.stop();
|
this.stop();
|
||||||
return;
|
return;
|
||||||
|
case 'drop':
|
||||||
|
if (this.mode !== 'simulation') {
|
||||||
|
client.send(message({ kind: 'error', code: 'notRunning' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (input.on) this.dropping.add(client);
|
||||||
|
else this.dropping.delete(client);
|
||||||
|
this.engine.setGlobalPacketLossEnabled(this.dropping.size > 0);
|
||||||
|
return;
|
||||||
case 'speed':
|
case 'speed':
|
||||||
this.speed = Math.min(100, Math.max(1, Math.trunc(input.value)));
|
this.speed = Math.min(100, Math.max(1, Math.trunc(input.value)));
|
||||||
this.engine.setSimulationSpeed(this.speed);
|
this.engine.setSimulationSpeed(this.speed);
|
||||||
@@ -384,7 +406,8 @@ export class SimulationRoom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private state(): HostMessage {
|
private state(): HostMessage {
|
||||||
return message({ kind: 'state', mode: this.mode, speed: this.speed, t: this.engine.state.currentTimeMilliseconds });
|
const t = this.engine.state.currentTimeMilliseconds;
|
||||||
|
return message({ kind: 'state', mode: this.mode, speed: this.speed, t, clock: (this.options.epoch ?? Date.now)() - t });
|
||||||
}
|
}
|
||||||
|
|
||||||
private broadcast(output: HostMessage): void {
|
private broadcast(output: HostMessage): void {
|
||||||
@@ -398,6 +421,9 @@ export class SimulationRoom {
|
|||||||
this.engine.setSimulationSpeed(this.speed);
|
this.engine.setSimulationSpeed(this.speed);
|
||||||
if (!this.guarded(() => this.engine.handle({ type: 'start', snapshot, seed: this.options.seed(), initialTimeMilliseconds: this.engine.state.currentTimeMilliseconds }))) return;
|
if (!this.guarded(() => this.engine.handle({ type: 'start', snapshot, seed: this.options.seed(), initialTimeMilliseconds: this.engine.state.currentTimeMilliseconds }))) return;
|
||||||
this.linkOfPort = new Map(snapshot.links.flatMap((link) => [[link.sourcePortId, link.id] as const, [link.targetPortId, link.id] as const]));
|
this.linkOfPort = new Map(snapshot.links.flatMap((link) => [[link.sourcePortId, link.id] as const, [link.targetPortId, link.id] as const]));
|
||||||
|
// Java's SimulationControl begins its list of dropped frames anew with each simulation.
|
||||||
|
this.engine.capture.clearDropped();
|
||||||
|
for (const client of this.clients.values()) client.droppedSent = undefined;
|
||||||
this.software = this.documentSoftware();
|
this.software = this.documentSoftware();
|
||||||
this.syncWindows();
|
this.syncWindows();
|
||||||
this.mode = 'simulation';
|
this.mode = 'simulation';
|
||||||
@@ -412,6 +438,7 @@ export class SimulationRoom {
|
|||||||
// A failure already stopped the engine and went back to the design.
|
// A failure already stopped the engine and went back to the design.
|
||||||
if (!this.guarded(() => this.writeBack())) return;
|
if (!this.guarded(() => this.writeBack())) return;
|
||||||
this.engine.handle({ type: 'stop' });
|
this.engine.handle({ type: 'stop' });
|
||||||
|
this.dropping.clear();
|
||||||
this.mode = 'design';
|
this.mode = 'design';
|
||||||
this.lastWall = undefined;
|
this.lastWall = undefined;
|
||||||
this.broadcast(this.state());
|
this.broadcast(this.state());
|
||||||
@@ -477,6 +504,9 @@ export class SimulationRoom {
|
|||||||
return this.actInFirewall(input);
|
return this.actInFirewall(input);
|
||||||
case 'gnutella':
|
case 'gnutella':
|
||||||
return this.actInGnutella(input);
|
return this.actInGnutella(input);
|
||||||
|
case 'sat':
|
||||||
|
case 'nat':
|
||||||
|
return this.actInViewer(input);
|
||||||
default:
|
default:
|
||||||
return 'unknownAction';
|
return 'unknownAction';
|
||||||
}
|
}
|
||||||
@@ -512,6 +542,19 @@ export class SimulationRoom {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A switch's or home router's viewer: its table reset (SatViewer, NatViewer: "Tabelle löschen", "Tabelle zurücksetzen"). */
|
||||||
|
private actInViewer(input: Extract<ClientMessage, { kind: 'input' }>): ErrorCode | undefined {
|
||||||
|
if (this.nodeKind(input.node) !== (input.app === 'sat' ? 'networkSwitch' : 'gateway')) return 'unknownApp';
|
||||||
|
if (input.action !== 'reset') return 'unknownAction';
|
||||||
|
if (input.app === 'sat') this.engine.clearSwitchSat(input.node);
|
||||||
|
else this.engine.clearDynamicNatEntries(input.node);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private nodeKind(node: Id): string | undefined {
|
||||||
|
return this.engine.state.topologySnapshot.nodes.find((each) => each.id === node)?.kind;
|
||||||
|
}
|
||||||
|
|
||||||
/** 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. */
|
/** 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 {
|
private actInGnutella(input: Extract<ClientMessage, { kind: 'input' }>): ErrorCode | undefined {
|
||||||
const peer = this.engine.gnutella(input.node);
|
const peer = this.engine.gnutella(input.node);
|
||||||
@@ -762,6 +805,16 @@ 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 'sat': {
|
||||||
|
if (this.nodeKind(node) !== 'networkSwitch') return null;
|
||||||
|
const view: SatView = { rows: this.engine.switchSatEntries(node).map((entry) => ({ macAddress: entry.macAddress, port: entry.portNumber, updatedAtMilliseconds: entry.updatedAtMilliseconds })) };
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
case 'nat': {
|
||||||
|
if (this.nodeKind(node) !== 'gateway') return null;
|
||||||
|
const view: NatView = { rows: this.engine.natEntries(node).map(natViewRow) };
|
||||||
|
return view;
|
||||||
|
}
|
||||||
case 'gnutella': {
|
case 'gnutella': {
|
||||||
const peer = this.engine.gnutella(node);
|
const peer = this.engine.gnutella(node);
|
||||||
if (peer === undefined) return null;
|
if (peer === undefined) return null;
|
||||||
@@ -845,6 +898,12 @@ export class SimulationRoom {
|
|||||||
for (const topic of client.topics) {
|
for (const topic of client.topics) {
|
||||||
if (topic === 'activity') {
|
if (topic === 'activity') {
|
||||||
if (subscribing) client.send(message({ kind: 'activity', links: this.lastActivity }));
|
if (subscribing) client.send(message({ kind: 'activity', links: this.lastActivity }));
|
||||||
|
} else if (topic === 'dropped') {
|
||||||
|
const reset = client.droppedSent === undefined;
|
||||||
|
const frames = this.engine.capture.droppedFrames().filter((each) => each.number > (client.droppedSent ?? 0));
|
||||||
|
if (frames.length === 0 && !reset) continue;
|
||||||
|
client.droppedSent = frames.at(-1)?.number ?? client.droppedSent ?? 0;
|
||||||
|
client.send(message({ kind: 'dropped', units: frames.map((each) => ({ number: each.number, timeMilliseconds: each.timeMilliseconds, text: javaFrameText(each.frame) })), reset }));
|
||||||
} else if (topic.startsWith('node:')) {
|
} else if (topic.startsWith('node:')) {
|
||||||
const node = topic.slice('node:'.length);
|
const node = topic.slice('node:'.length);
|
||||||
const settings = this.nodeSettings(node);
|
const settings = this.nodeSettings(node);
|
||||||
@@ -871,7 +930,7 @@ export class SimulationRoom {
|
|||||||
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 and Gnutella's windows have none.
|
// The browser's lines are its view; the DNS server's and Gnutella's windows have none.
|
||||||
if (app === 'webBrowser' || app === 'dnsServer' || app === 'gnutella') continue;
|
if (app === 'webBrowser' || app === 'dnsServer' || app === 'gnutella' || app === 'sat' || app === 'nat') continue;
|
||||||
}
|
}
|
||||||
const sent = client.appSent.get(topic);
|
const sent = client.appSent.get(topic);
|
||||||
if (app === 'terminal') {
|
if (app === 'terminal') {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ function exampleDoc(file = 'java-examples/internetwork_komplett.fls'): Y.Doc {
|
|||||||
/** A room on a clock the test moves, and a client that keeps what it gets. */
|
/** A room on a clock the test moves, and a client that keeps what it gets. */
|
||||||
function setUp(doc = exampleDoc()) {
|
function setUp(doc = exampleDoc()) {
|
||||||
let now = 1_000_000;
|
let now = 1_000_000;
|
||||||
const room = new SimulationRoom(doc, { now: () => now, seed: () => SEED });
|
const room = new SimulationRoom(doc, { now: () => now, seed: () => SEED, epoch: () => 1_700_000_000_000 + now });
|
||||||
const got: HostMessage[] = [];
|
const got: HostMessage[] = [];
|
||||||
const client = room.connect((message) => got.push(message));
|
const client = room.connect((message) => got.push(message));
|
||||||
const project = readProject(doc);
|
const project = readProject(doc);
|
||||||
@@ -58,13 +58,17 @@ const ofKind = <K extends HostMessage['kind']>(got: readonly HostMessage[], kind
|
|||||||
|
|
||||||
describe('a simulation room', () => {
|
describe('a simulation room', () => {
|
||||||
it('tells a new client its state, and every client a change', () => {
|
it('tells a new client its state, and every client a change', () => {
|
||||||
const { room, got, send } = setUp();
|
const { room, got, send, run } = setUp();
|
||||||
const other: HostMessage[] = [];
|
const other: HostMessage[] = [];
|
||||||
room.connect((message) => other.push(message));
|
room.connect((message) => other.push(message));
|
||||||
expect(got).toEqual([{ v: 1, kind: 'state', mode: 'design', speed: 90, t: 0 }]);
|
// The simulation's time 0 stands for the time of day then (the epoch here follows the room's clock).
|
||||||
|
expect(got).toEqual([{ v: 1, kind: 'state', mode: 'design', speed: 90, t: 0, clock: 1_700_001_000_000 }]);
|
||||||
send({ kind: 'start' });
|
send({ kind: 'start' });
|
||||||
|
run(3_000);
|
||||||
send({ kind: 'speed', value: 250 });
|
send({ kind: 'speed', value: 250 });
|
||||||
send({ kind: 'stop' });
|
send({ kind: 'stop' });
|
||||||
|
// The time of day of the simulation's time 0 stays while it runs.
|
||||||
|
expect(new Set(ofKind(got, 'state').map((state) => state.clock))).toEqual(new Set([1_700_001_000_000]));
|
||||||
for (const each of [got, other]) {
|
for (const each of [got, other]) {
|
||||||
expect(ofKind(each, 'state').map(({ mode, speed }) => [mode, speed])).toEqual([
|
expect(ofKind(each, 'state').map(({ mode, speed }) => [mode, speed])).toEqual([
|
||||||
['design', 90],
|
['design', 90],
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
// The viewers of a running simulation in the room, on FILIUS' example
|
||||||
|
// nat_with_mapped_port: a switch's table and a home router's NAT table as
|
||||||
|
// views (their reset an action), and "Pakete verwerfen": every cable drops
|
||||||
|
// while any client holds it, and the dropped frames go to who watches them,
|
||||||
|
// begun anew with each simulation. The tables' rows against Java's are
|
||||||
|
// packages/core/test/viewers.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 * as Y from 'yjs';
|
||||||
|
import { readProject, setSoftware, writeProject } from '@mdg-filius/doc';
|
||||||
|
import { openProjectArchive } from '@mdg-filius/fls';
|
||||||
|
import type { DroppedUnit, HostMessage, NatView, SatView } from '@mdg-filius/protocol';
|
||||||
|
import { SimulationRoom, TICK_MILLISECONDS } from '../src/index.js';
|
||||||
|
|
||||||
|
const corpus = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus/java-examples');
|
||||||
|
|
||||||
|
function setUp() {
|
||||||
|
let counter = 0;
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
writeProject(
|
||||||
|
doc,
|
||||||
|
openProjectArchive(new Uint8Array(readFileSync(join(corpus, 'nat_with_mapped_port.fls'))), () => `00000000-0000-0000-0000-${(++counter).toString(16).padStart(12, '0')}`),
|
||||||
|
);
|
||||||
|
const project = readProject(doc);
|
||||||
|
const idOf = (name: string) => project.graph.nodes.find((node) => node.displayName === name)!.id;
|
||||||
|
const gateway = [...project.portForwardingRowsByNodeId.keys()][0]!;
|
||||||
|
// The switch the web server behind the home router hangs on.
|
||||||
|
const webServer = idOf('Webserver - 192.168.0.10');
|
||||||
|
const link = project.graph.links.find((each) => each.sourceNodeId === webServer || each.targetNodeId === webServer)!;
|
||||||
|
const lanSwitch = link.sourceNodeId === webServer ? link.targetNodeId : link.sourceNodeId;
|
||||||
|
setSoftware(doc, webServer, { install: ['commandPrompt'], remove: [] }, 'de', 'me');
|
||||||
|
// The web server's card with an address in small letters (a file may hold one so): the switch's table writes it in capitals.
|
||||||
|
const webPort = (doc.getMap<Y.Map<unknown>>('nodes').get(webServer)!.get('ports') as Y.Array<Y.Map<unknown>>).get(0);
|
||||||
|
webPort.set('mac', 'aa:bb:cc:00:11:22');
|
||||||
|
let now = 0;
|
||||||
|
const room = new SimulationRoom(doc, { now: () => now, seed: () => 5 });
|
||||||
|
const views = new Map<string, unknown>();
|
||||||
|
const dropped: DroppedUnit[] = [];
|
||||||
|
const resets: number[] = [];
|
||||||
|
const messages: HostMessage[] = [];
|
||||||
|
const errors: string[] = [];
|
||||||
|
const connect = () =>
|
||||||
|
room.connect((message: HostMessage) => {
|
||||||
|
messages.push(message);
|
||||||
|
if (message.kind === 'view') views.set(`${message.node}/${message.app}`, message.view);
|
||||||
|
if (message.kind === 'dropped') {
|
||||||
|
if (message.reset) {
|
||||||
|
resets.push(message.units.length);
|
||||||
|
dropped.length = 0;
|
||||||
|
}
|
||||||
|
dropped.push(...message.units);
|
||||||
|
}
|
||||||
|
if (message.kind === 'error') errors.push(message.code);
|
||||||
|
});
|
||||||
|
const client = connect();
|
||||||
|
const send = (fields: object, who = client) => room.receive(who, { v: 1, ...fields });
|
||||||
|
const run = (milliseconds: number) => {
|
||||||
|
for (let elapsed = 0; elapsed < milliseconds; elapsed += TICK_MILLISECONDS) {
|
||||||
|
now += TICK_MILLISECONDS;
|
||||||
|
room.tick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const terminal = (node: string, line: string) => send({ kind: 'input', node, app: 'terminal', action: 'enter', args: { line }, reqId: line });
|
||||||
|
return { room, idOf, gateway, lanSwitch, webServer, views, dropped, resets, messages, errors, connect, send, run, terminal };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('the viewers in the room', () => {
|
||||||
|
it("shows a switch's table and a home router's NAT table, and resets them", () => {
|
||||||
|
const { gateway, lanSwitch, webServer, views, messages, errors, send, run, terminal } = setUp();
|
||||||
|
send({ kind: 'sub', topics: [`app:${lanSwitch}/sat`, `app:${gateway}/nat`, `app:${webServer}/sat`, `app:${lanSwitch}/nat`] });
|
||||||
|
send({ kind: 'start' });
|
||||||
|
run(TICK_MILLISECONDS);
|
||||||
|
const sat = () => views.get(`${lanSwitch}/sat`) as SatView;
|
||||||
|
const nat = () => views.get(`${gateway}/nat`) as NatView;
|
||||||
|
// The port forwarding's entry, before anything went through.
|
||||||
|
expect(nat()).toEqual({ rows: [{ cells: ['TCP', '80', '*', '192.168.0.10', '80'] }] });
|
||||||
|
expect(sat()).toEqual({ rows: [] });
|
||||||
|
// No switch, no home router: no table.
|
||||||
|
expect([views.get(`${webServer}/sat`), views.get(`${lanSwitch}/nat`)]).toEqual([null, null]);
|
||||||
|
terminal(webServer, 'ping 141.99.4.10');
|
||||||
|
run(10_000);
|
||||||
|
expect(sat().rows.length).toBeGreaterThanOrEqual(2);
|
||||||
|
expect(sat().rows.every((row) => Number.isInteger(row.port) && row.port >= 1 && row.updatedAtMilliseconds > 0)).toBe(true);
|
||||||
|
expect(sat().rows.map((row) => row.macAddress)).toContain('AA:BB:CC:00:11:22');
|
||||||
|
expect(nat().rows.map((row) => row.cells.slice(0, 1).concat(row.cells.slice(2, 4)))).toEqual([
|
||||||
|
['TCP', '*', '192.168.0.10'],
|
||||||
|
['ICMP', '141.99.4.10', '192.168.0.10'],
|
||||||
|
]);
|
||||||
|
expect(nat().rows[1]!.updatedAtMilliseconds).toBeGreaterThan(0);
|
||||||
|
send({ kind: 'input', node: lanSwitch, app: 'sat', action: 'reset', args: {}, reqId: 'sat' });
|
||||||
|
send({ kind: 'input', node: gateway, app: 'nat', action: 'reset', args: {}, reqId: 'nat' });
|
||||||
|
expect(sat()).toEqual({ rows: [] });
|
||||||
|
expect(nat()).toEqual({ rows: [{ cells: ['TCP', '80', '*', '192.168.0.10', '80'] }] });
|
||||||
|
// What the viewers never hand over.
|
||||||
|
send({ kind: 'input', node: webServer, app: 'sat', action: 'reset', args: {}, reqId: 'a' });
|
||||||
|
send({ kind: 'input', node: lanSwitch, app: 'nat', action: 'reset', args: {}, reqId: 'b' });
|
||||||
|
send({ kind: 'input', node: lanSwitch, app: 'sat', action: 'sort', args: {}, reqId: 'c' });
|
||||||
|
expect(errors).toEqual(['unknownApp', 'unknownApp', 'unknownAction']);
|
||||||
|
// The viewers have no lines: none are sent.
|
||||||
|
expect(messages.filter((message) => message.kind === 'app')).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops while any client holds "Pakete verwerfen", and lists what it dropped anew with each simulation', () => {
|
||||||
|
const { webServer, dropped, resets, messages, errors, connect, send, run, terminal, room } = setUp();
|
||||||
|
send({ kind: 'sub', topics: ['dropped'] });
|
||||||
|
// Not while the design is shown.
|
||||||
|
send({ kind: 'drop', on: true });
|
||||||
|
expect(errors).toEqual(['notRunning']);
|
||||||
|
send({ kind: 'start' });
|
||||||
|
run(TICK_MILLISECONDS);
|
||||||
|
terminal(webServer, 'ping 192.168.0.1');
|
||||||
|
run(12_000);
|
||||||
|
expect(dropped).toEqual([]);
|
||||||
|
// Nothing dropped, nothing sent after the subscription's (empty) list.
|
||||||
|
expect(messages.filter((message) => message.kind === 'dropped')).toHaveLength(2);
|
||||||
|
const other = connect();
|
||||||
|
send({ kind: 'drop', on: true });
|
||||||
|
send({ kind: 'drop', on: true }, other);
|
||||||
|
send({ kind: 'drop', on: false });
|
||||||
|
// The other still holds it: the ping is dropped.
|
||||||
|
terminal(webServer, 'ping 192.168.0.1');
|
||||||
|
run(12_000);
|
||||||
|
expect(dropped.length).toBeGreaterThan(0);
|
||||||
|
expect(dropped[0]!.text).toMatch(/^\[src=[0-9A-F:]+, dest=[0-9A-F:]+, type=0x8(00|06) \| \[/i);
|
||||||
|
expect(dropped.map((unit) => unit.number)).toEqual(dropped.map((_, index) => index + 1));
|
||||||
|
const count = dropped.length;
|
||||||
|
// Subscribed anew, the list comes whole.
|
||||||
|
send({ kind: 'unsub', topics: ['dropped'] });
|
||||||
|
send({ kind: 'sub', topics: ['dropped'] });
|
||||||
|
expect([resets.at(-1), dropped.length]).toEqual([count, count]);
|
||||||
|
// The other leaves: nothing is dropped any more.
|
||||||
|
room.disconnect(other);
|
||||||
|
terminal(webServer, 'ping 192.168.0.1');
|
||||||
|
run(12_000);
|
||||||
|
expect(dropped.length).toBe(count);
|
||||||
|
// A new simulation begins the list anew.
|
||||||
|
send({ kind: 'stop' });
|
||||||
|
send({ kind: 'start' });
|
||||||
|
run(TICK_MILLISECONDS);
|
||||||
|
expect(resets.at(-1)).toBe(0);
|
||||||
|
expect(dropped).toEqual([]);
|
||||||
|
expect(errors).toEqual(['notRunning']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forgets who held "Pakete verwerfen" when the simulation stops', () => {
|
||||||
|
const { webServer, dropped, connect, send, run, terminal } = setUp();
|
||||||
|
const other = connect();
|
||||||
|
send({ kind: 'sub', topics: ['dropped'] });
|
||||||
|
send({ kind: 'start' });
|
||||||
|
run(TICK_MILLISECONDS);
|
||||||
|
// Held through the stop: the letting go comes in the design, and is refused there.
|
||||||
|
send({ kind: 'drop', on: true }, other);
|
||||||
|
send({ kind: 'stop' });
|
||||||
|
send({ kind: 'drop', on: false }, other);
|
||||||
|
send({ kind: 'start' });
|
||||||
|
run(TICK_MILLISECONDS);
|
||||||
|
send({ kind: 'drop', on: true });
|
||||||
|
send({ kind: 'drop', on: false });
|
||||||
|
terminal(webServer, 'ping 192.168.0.1');
|
||||||
|
run(12_000);
|
||||||
|
expect(dropped).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1598,3 +1598,28 @@ Gemessen (03.10.2026):
|
|||||||
das Fenster auffrischt.
|
das Fenster auffrischt.
|
||||||
* Die Dateitabelle zeigt eine heruntergeladene Datei erst, wenn das
|
* Die Dateitabelle zeigt eine heruntergeladene Datei erst, wenn das
|
||||||
Programm das nächste Mal Bescheid gibt; eine daneben angelegte gar nicht.
|
Programm das nächste Mal Bescheid gibt; eine daneben angelegte gar nicht.
|
||||||
|
|
||||||
|
### SAT- und NAT-Anzeige, „Pakete verwerfen“ (F4.6g5)
|
||||||
|
|
||||||
|
`ViewersRun` misst, was Javas Anzeigen der laufenden Simulation zeigen. Die
|
||||||
|
Anzeigen (`SatViewer`, `NatViewer`, `SimulationControl`) zeigen nur Zeilen
|
||||||
|
der Firmware; die Sonde nimmt sie dort ab (`holeSAT`, `holeNAT`,
|
||||||
|
`getDroppedDataUnits`). Geschichte `switch` auf `switch-lan` (Haltezeit 5 s):
|
||||||
|
zwei Pings, die Tabelle, „Tabelle löschen“, ein Ping, nach der Haltezeit;
|
||||||
|
dann „Pakete verwerfen“ gedrückt (`Verbindung.setDrop`) während eines Pings
|
||||||
|
und losgelassen. Geschichte `nat` auf `nat-home`: die Portweiterleitung, ein
|
||||||
|
Ping und ein Datagramm hinaus, „Tabelle zurücksetzen“
|
||||||
|
(`packages/core/test/viewers-*.java.json`).
|
||||||
|
|
||||||
|
Gemessen (03.10.2026):
|
||||||
|
|
||||||
|
* Die Switch-Tabelle zeigt die Adresse groß, „Port <n>“ nach der Stelle des
|
||||||
|
Anschlusses und die Uhrzeit der letzten Meldung (`HH:mm:ss`), in der
|
||||||
|
Reihenfolge von Javas Hash-Tabelle; nach der Haltezeit ist sie leer.
|
||||||
|
* Die NAT-Tabelle zeigt die Portweiterleitung mit „*“ und „-“, einen Ping
|
||||||
|
als `0 (Identifier: 1)`, ein Datagramm mit gewürfeltem WAN-Port; „Tabelle
|
||||||
|
zurücksetzen“ lässt nur die Portweiterleitung stehen.
|
||||||
|
* „Pakete verwerfen“ wirkt nur, solange der Knopf gedrückt ist; die Liste
|
||||||
|
zeigt `HH:mm:ss.SSS : ` und den Frame in Javas Textform
|
||||||
|
(`[src=…, dest=…, type=0x800 | [id=…; ttl=64; …]]`; ein IP-Paket ohne sein
|
||||||
|
Segment).
|
||||||
|
|||||||
@@ -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" "$here/src/GnutellaWindowRun.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" "$here/src/ViewersRun.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.
|
||||||
@@ -464,6 +464,19 @@ HOME="$here/.build/home" xvfb-run -a java -Duser.language=de -Duser.country=DE -
|
|||||||
"$root/packages/core/test/scenarios/terminal-net.scenario.json" "$output" 2>&1 | grep -v 'logback\|^$\|^\s*at \|scrollpane' || true
|
"$root/packages/core/test/scenarios/terminal-net.scenario.json" "$output" 2>&1 | grep -v 'logback\|^$\|^\s*at \|scrollpane' || true
|
||||||
test -s "$output"
|
test -s "$output"
|
||||||
|
|
||||||
|
# What FILIUS' viewers of a running simulation show — a switch's table, a
|
||||||
|
# home router's NAT table, each after its reset — and the frames "Pakete
|
||||||
|
# verwerfen" drops while it is held (ViewersRun): the viewers
|
||||||
|
# (packages/core/test/viewers.test.ts).
|
||||||
|
for run in switch:switch-lan nat:nat-home; do
|
||||||
|
story="${run%%:*}"
|
||||||
|
output="$root/packages/core/test/viewers-$story.java.json"
|
||||||
|
rm -f "$output"
|
||||||
|
HOME="$here/.build/home" xvfb-run -a java -Duser.language=de -Duser.country=DE -cp "$classpath:$here/.build" ViewersRun \
|
||||||
|
"$root/packages/core/test/scenarios/${run##*:}.scenario.json" "$story" "$output" 2>&1 | grep -v 'logback\|^$\|^\s*at ' || true
|
||||||
|
test -s "$output"
|
||||||
|
done
|
||||||
|
|
||||||
# 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,169 @@
|
|||||||
|
// What FILIUS' viewers of a running simulation show, in Java FILIUS: a
|
||||||
|
// switch's source address table (SwitchFirmware.holeSAT, shown by
|
||||||
|
// SatViewer), a home router's NAT table (GatewayFirmware.holeNAT, shown by
|
||||||
|
// NatViewer), each also after its "reset" (loescheSAT, loescheNAT), and the
|
||||||
|
// frames "Pakete verwerfen" drops while it is held (Verbindung.setDrop,
|
||||||
|
// Lauscher.getDroppedDataUnits, shown by SimulationControl). The viewers only
|
||||||
|
// show these rows, so they are taken from the firmware directly. Times are
|
||||||
|
// written with the run's start (the rows hold wall-clock times).
|
||||||
|
//
|
||||||
|
// ViewersRun <scenario.json> <story> <out.json>
|
||||||
|
//
|
||||||
|
// switch switch-lan (retention 5 s): .10 pings .20 and .30; the table;
|
||||||
|
// reset; .10 pings .20 again; the table; aged after the retention;
|
||||||
|
// "Pakete verwerfen" held while .10 pings .30; the dropped frames
|
||||||
|
// nat nat-home (port forwarding TCP 8080 to 192.168.0.10:80):
|
||||||
|
// 192.168.0.10 pings 42.0.0.20 and sends a datagram to its port 7;
|
||||||
|
// the table; reset; the table
|
||||||
|
|
||||||
|
import filius.hardware.Verbindung;
|
||||||
|
import filius.hardware.knoten.Switch;
|
||||||
|
import filius.rahmenprogramm.nachrichten.Lauscher;
|
||||||
|
import filius.software.system.GatewayFirmware;
|
||||||
|
import filius.software.system.InternetKnotenBetriebssystem;
|
||||||
|
import filius.software.system.SwitchFirmware;
|
||||||
|
import filius.software.transportschicht.UDPSocket;
|
||||||
|
import filius.software.vermittlungsschicht.IcmpPaket;
|
||||||
|
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Vector;
|
||||||
|
|
||||||
|
public class ViewersRun {
|
||||||
|
static final String SWITCH = "00000000-0000-0000-0000-000000000104";
|
||||||
|
static final String PC10 = "00000000-0000-0000-0000-000000000101";
|
||||||
|
static final String GATEWAY = "00000000-0000-0000-0000-000000000502";
|
||||||
|
static final String LAN_PC = "00000000-0000-0000-0000-000000000501";
|
||||||
|
static final String SERVER = "00000000-0000-0000-0000-000000000504";
|
||||||
|
|
||||||
|
static ScenarioNetwork network;
|
||||||
|
static final List<Object> steps = new ArrayList<>();
|
||||||
|
|
||||||
|
static InternetKnotenBetriebssystem os(String id) {
|
||||||
|
return (InternetKnotenBetriebssystem) network.nodes.get(id).getSystemSoftware();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String ping(String from, String target, int sequence) {
|
||||||
|
IcmpPaket answer = os(from).holeICMP().sendProbe(target, 64, 1, sequence);
|
||||||
|
return answer == null ? "null" : "ICMP " + answer.getIcmpType() + "/" + answer.getIcmpCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<List<String>> rows(Vector<Vector<String>> table) {
|
||||||
|
List<List<String>> rows = new ArrayList<>();
|
||||||
|
for (Vector<String> row : table) rows.add(new ArrayList<>(row));
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Each learned address's last update, in milliseconds since the start. */
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
static Map<String, Long> satTimes(SwitchFirmware firmware) throws Exception {
|
||||||
|
Field field = SwitchFirmware.class.getDeclaredField("sat");
|
||||||
|
field.setAccessible(true);
|
||||||
|
Map<String, Object> sat = (Map<String, Object>) field.get(firmware);
|
||||||
|
Map<String, Long> times = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<String, Object> entry : sat.entrySet()) {
|
||||||
|
Method update = entry.getValue().getClass().getMethod("holeLetztesUpdate");
|
||||||
|
update.setAccessible(true);
|
||||||
|
times.put(entry.getKey().toUpperCase(), ((java.util.Date) update.invoke(entry.getValue())).getTime() - network.start);
|
||||||
|
}
|
||||||
|
return times;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void step(String name, Map<String, Object> shown) {
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("step", name);
|
||||||
|
result.put("at", System.currentTimeMillis() - network.start);
|
||||||
|
result.putAll(shown);
|
||||||
|
steps.add(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, Object> sat() throws Exception {
|
||||||
|
SwitchFirmware firmware = (SwitchFirmware) network.nodes.get(SWITCH).getSystemSoftware();
|
||||||
|
firmware.checkSAT();
|
||||||
|
Map<String, Object> shown = new LinkedHashMap<>();
|
||||||
|
shown.put("rows", rows(firmware.holeSAT()));
|
||||||
|
shown.put("updated", satTimes(firmware));
|
||||||
|
shown.put("ports", ((Switch) network.nodes.get(SWITCH)).getAnschluesse().size());
|
||||||
|
return shown;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, Object> nat() {
|
||||||
|
Map<String, Object> shown = new LinkedHashMap<>();
|
||||||
|
shown.put("rows", rows(((GatewayFirmware) network.nodes.get(GATEWAY).getSystemSoftware()).holeNAT()));
|
||||||
|
return shown;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
network = ScenarioNetwork.read(args[0]);
|
||||||
|
String story = args[1];
|
||||||
|
network.start();
|
||||||
|
Thread.sleep(500);
|
||||||
|
Map<String, Object> output = new LinkedHashMap<>();
|
||||||
|
output.put("startEpoch", network.start);
|
||||||
|
if (story.equals("switch")) {
|
||||||
|
Map<String, Object> answers = new LinkedHashMap<>();
|
||||||
|
answers.put("ping .20", ping(PC10, "192.168.0.20", 1));
|
||||||
|
answers.put("ping .30", ping(PC10, "192.168.0.30", 2));
|
||||||
|
Map<String, Object> shown = sat();
|
||||||
|
shown.put("answers", answers);
|
||||||
|
step("after pings", shown);
|
||||||
|
((SwitchFirmware) network.nodes.get(SWITCH).getSystemSoftware()).loescheSAT();
|
||||||
|
step("reset", sat());
|
||||||
|
Map<String, Object> again = sat();
|
||||||
|
again.put("answers", Map.of("ping .20", ping(PC10, "192.168.0.20", 3)));
|
||||||
|
again.putAll(sat());
|
||||||
|
step("ping .20 again", again);
|
||||||
|
Thread.sleep(6000);
|
||||||
|
step("after the retention", sat());
|
||||||
|
|
||||||
|
Lauscher.getLauscher().resetDroppedDataUnits();
|
||||||
|
Verbindung.setDrop(true);
|
||||||
|
String dropped = ping(PC10, "192.168.0.30", 4);
|
||||||
|
Verbindung.setDrop(false);
|
||||||
|
Map<String, Object> drop = new LinkedHashMap<>();
|
||||||
|
drop.put("answer", dropped);
|
||||||
|
drop.put("dropped", Lauscher.getLauscher().getDroppedDataUnits());
|
||||||
|
step("dropped while held", drop);
|
||||||
|
Map<String, Object> after = new LinkedHashMap<>();
|
||||||
|
after.put("answer", ping(PC10, "192.168.0.30", 5));
|
||||||
|
after.put("dropped", Lauscher.getLauscher().getDroppedDataUnits());
|
||||||
|
step("released", after);
|
||||||
|
} else {
|
||||||
|
step("start", nat());
|
||||||
|
Map<String, Object> answers = new LinkedHashMap<>();
|
||||||
|
answers.put("ping 42.0.0.20", ping(LAN_PC, "42.0.0.20", 1));
|
||||||
|
UDPSocket socket = new UDPSocket(os(LAN_PC), "42.0.0.20", 7);
|
||||||
|
socket.verbinden();
|
||||||
|
socket.senden("one");
|
||||||
|
answers.put("udp port", socket.holeLokalenPort());
|
||||||
|
Thread.sleep(1500);
|
||||||
|
Map<String, Object> shown = nat();
|
||||||
|
shown.put("answers", answers);
|
||||||
|
step("after ping and datagram", shown);
|
||||||
|
((GatewayFirmware) network.nodes.get(GATEWAY).getSystemSoftware()).loescheNAT();
|
||||||
|
step("reset", nat());
|
||||||
|
}
|
||||||
|
output.put("steps", steps);
|
||||||
|
try (FileOutputStream out = new FileOutputStream(args[2])) {
|
||||||
|
out.write((FlsOracle.Json.write(output, "") + "\n").getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
network.stop();
|
||||||
|
System.out.println("viewer steps: " + steps.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user