From e31da62ae0aa59d713400ef6ab171348b760a34c Mon Sep 17 00:00:00 2001 From: Julian Kiedaisch Date: Sat, 3 Oct 2026 20:04:33 +0000 Subject: [PATCH] feat: F4.6g1 the echo server and the generic client windows Oracle EchoWindowRun uses Java's echo server and client windows as a person does; SaveAfterRunRun echo saves what Java keeps after the run. The room takes setActive with a port, connect/disconnect/send, sends both views and writes the server's port and the client's target back. The editor's windows show Java's buttons, fields and lines; a port that is no port is marked and not handed over (Java's button throws). Core fix: an empty message (one empty segment with the push, as Java's erstelleSegmente makes) counted as a repeat and never arrived. Co-Authored-By: Claude Opus 5.5 --- CHANGES.md | 5 + packages/core/src/runtime/engine.ts | 12 +- packages/core/test/apps.test.ts | 16 + packages/editor/browser/echo.spec.ts | 86 ++++ packages/editor/src/desktop/DesktopWindow.tsx | 9 +- packages/editor/src/desktop/EchoPanels.tsx | 132 ++++++ packages/editor/src/desktop/desktop.css | 57 +++ packages/editor/src/desktop/serverLines.ts | 28 +- packages/editor/src/i18n/de.json | 19 + packages/editor/src/i18n/en.json | 19 + packages/editor/test/i18n.test.ts | 17 + packages/editor/test/serverLines.test.ts | 43 +- packages/protocol/src/echo.ts | 25 ++ packages/protocol/src/index.ts | 1 + packages/protocol/src/sim.ts | 6 +- packages/sim/src/room.ts | 74 ++- packages/sim/test/echoWindow.java.json | 424 ++++++++++++++++++ packages/sim/test/echoWindow.test.ts | 251 +++++++++++ packages/sim/test/fixtures/after-run-echo.fls | Bin 0 -> 10894 bytes packages/sim/test/room.test.ts | 2 +- tools/java-oracle/README.md | 33 ++ tools/java-oracle/run.sh | 17 +- tools/java-oracle/src/EchoWindowRun.java | 197 ++++++++ tools/java-oracle/src/SaveAfterRunRun.java | 26 ++ 24 files changed, 1481 insertions(+), 18 deletions(-) create mode 100644 packages/editor/browser/echo.spec.ts create mode 100644 packages/editor/src/desktop/EchoPanels.tsx create mode 100644 packages/protocol/src/echo.ts create mode 100644 packages/sim/test/echoWindow.java.json create mode 100644 packages/sim/test/echoWindow.test.ts create mode 100644 packages/sim/test/fixtures/after-run-echo.fls create mode 100644 tools/java-oracle/src/EchoWindowRun.java diff --git a/CHANGES.md b/CHANGES.md index f7957ef..67be343 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, DHCP, Portweiterleitung, Remote Link ohne LAN-Transport, Programme und ihre Konfiguration, Firewall, DNS-Einträge aus `State/TopologyDNSResolver.swift`, E-Mail-Konten und -Nachrichten aus `State/TopologyEmail.swift`); dazu Echo-Server- und Client-Konfiguration, die Swift nicht speichert | übertragen; Java-Beschriftungen der Oberfläche weggelassen | | `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4472–5187 (UDP), ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall, UDP-Sockets (binden, freie Ports nach Javas Zufall, senden samt Broadcast, zustellen, empfangen mit Wartezeit, schließen), TCP (Z. ~5187–5913: Server- und Client-Sockets, Handschlag mit Ergebnis, Segmente zu 1460 Byte bis zum PSH, höchstens drei Versuche, Abbau über alle Java-Zustände samt `TIME_WAIT`; seit F2.3c nach Javas `TCPSocket` an Java-Filius gemessen: je Versuch und für `TIME_WAIT` 3 RTT nach der Geschwindigkeit (8250 ms bei 90 %, Swift fest 7500 bzw. 2500 ms), der Zeitablauf zählt ab der Übergabe des Segments, vor ARP; ein Segment beendet `TIME_WAIT`; die Gegenseite schließt beim FIN von selbst; nicht bestätigte Daten schließen mit FIN statt sofort; der angenommene Socket hat die Startnummer des Listeners, der Listener eine neue. seit F2.3e wie Java: SYN+ACK und FIN gehen nie ein zweites Mal hinaus; der Server gibt einen Handschlag nach 3 RTT auf und schließt mit FIN; nach dem FIN wartet ein Socket höchstens fünfmal 3 RTT, jedes Segment beendet eine Wartezeit, dann CLOSED (Java lässt den Zustand stehen). Abweichend von Java: drei SYN statt einem, weil Javas Wiederholungsschleife nach dem ersten Zeitablauf abbricht, und ein wiederholtes SYN in `SYN_RCVD` bekommt das SYN+ACK noch einmal, statt den Socket zu schließen), DHCP (Z. ~5913–6694: Server mit festen Zuordnungen, Adressbereich, Angeboten, Leases und Sperrliste; Client seit F2.3b nach Javas `DHCPClient` neu gebaut und an Java-Filius gemessen: je Versuch eine Nachricht, eine Antwort, die nicht passt, wird gelesen und verworfen; die ARP-Prüfung wartet eine RTT, eine Antwort für die Adresse beendet sie sofort, dann DECLINE; nach einem Zeitablauf wiederholt der Zustand seine Nachricht, nach dem fünften nimmt der Client eine Link-Local-Adresse `169.254.1.x/16` (ARP-Probe, ARP-Reply an alle) und gibt keine alte Adresse zurück; Swift sendete doppelt, wartete nicht auf ARP und kehrte nach zehn Versuchen zur alten Adresse zurück. Abweichend von Java: Nach fünf belegten Link-Local-Adressen gibt der Client auf (Java sucht endlos), und ein ARP-Paket für eine andere Adresse beendet die Prüfung nicht. Seit F2.3d befördert ein Kabel einen Frame nach dem anderen wie Javas `SimplexVerbindung` (ein Frame wartet im Ausgangspuffer, bis der vorige angekommen ist, und erscheint im Protokoll, wenn er losgeht); Swift schickt alle zugleich, die Option `swiftCables` behält das für den Swift-Vergleich. Die RTT der DHCP-Zeiten (Warten, Angebote, Sperrliste) folgt wie in Java der Geschwindigkeit (50 Kabel, 2750 ms bei 90 %); Swift rechnet fest mit 2500 ms. ARP seit F2.4b nach Javas `holeARPTabellenEintrag`, an Java-Filius gemessen: eine Auflösung fragt, wartet eine RTT, fragt ein zweites Mal und gibt nach einer weiteren RTT auf; dann „Host unreachable" beim Absender selbst oder vom Router zurück (bei ICMP nur für Echo-Anfragen); ein wartender Absender wacht nach den übrigen Ereignissen desselben Zeitpunkts auf; der Ping wartet danach eine RTT auf die Antwort. Swift schickt in der Ereignisverarbeitung beide Anfragen zugleich und lässt das Paket ohne Ende warten, wenn die Adresse irgendwo im Netz vorkommt (Wissen, das kein Knoten hat), und blockiert direkte Aufrufe ohne Wartezeit; die Option `swiftArp` behält das für den Swift-Vergleich. Seit F2.4b-2 kehrt ein Senden wie in Java sofort zurück, die Frames reisen, während die Uhr läuft (Swift: der Aufruf kehrt erst zurück, wenn alle Frames angekommen sind; die Option `swiftSends` behält das für den Swift-Vergleich), und UDP und TCP senden über Javas Transport-Thread je Knoten und Protokoll: ein Segment nach dem anderen; solange IP auf ARP wartet, blockiert das nächste `senden`. Wartende Absender kommen in der Reihenfolge an die Reihe, in der sie kamen (Javas Sperre überlässt das dem Zufall). Seit F2.4b-3 geht jedes ankommende Paket wie in Java durch den IP-Thread (UDP, TCP) oder den ICMP-Thread (ICMP) seines Knotens: zustellen, Echo-Reply, weiterleiten, eins nach dem anderen; solange ARP wartet, warten die folgenden. Abweichend von Java weckt eine ARP-Antwort alle, die auf die Adresse warten (Javas `notify` weckt einen beliebigen). DNS seit F2.4a nach Javas `DNSServer`/`Resolver`, an Java-Filius gemessen: Server auf Port 53 mit Einträgen aus dem Modell (die Antwort wiederholt die Frage nicht, RCODE immer 0, ein Verweis mit NS und A im Antwortteil), iterativer Resolver mit 5 RTT je Anfrage, der nur die Server behält, an die er verwiesen wurde; seit F2.4c auf den Ereignissen statt blockierend und mit rekursivem Server nach `DNSServer.answerWithRemoteData` (Nameserver der Zone, dann der eigene DNS-Server, sonst Verweis), abweichend von Java fragt er sich nie selbst (Java endlos). Gateway-NAT seit F2.6 (Z. ~3585–3900) nach Javas `filius.software.nat`, an Java-Filius gemessen: die Übersetzung vor dem IP-Thread (wie Javas NAT-Threads zwischen Karte und Ethernet), ausgehend WAN-Adresse und der Port des Eintrags (ICMP behält die Kennung), eingehend nach Gegenstelle, Port und Protokoll, sonst nach der Portweiterleitung (legt einen dynamischen Eintrag an), ein Ping an die WAN-Adresse beantwortet der Heimrouter, Pakete von außen an das LAN verschwinden, beide Seiten durch die Firewall des Heimrouters, ausgehend TTL 1 bekommt „Zeit überschritten“; dynamische Einträge verfallen nach 300 s mal Javas Verzögerungsfaktor (Swift fest 300 s), aufgefrischt nur eingehend, seit 01.10.2026 genau in diesem Alter (Java findet sie erst bei seiner sekündlichen Prüfung, bis zu 1 s später; so auch der Switch: Javas `checkSAT` prüft jede Sekunde, der Kern verfällt genau zur Lernzeit); der Heimrouter leitet weiter wie ein Router. Abweichend von Java: ein LAN-Endpunkt bekommt für jede neue Gegenstelle einen Eintrag auf demselben WAN-Port (Java behält nur den für die erste, die Antworten weiterer bleiben am Heimrouter hängen). RIP seit F2.5a (Z. ~4700–5090) nach Javas `filius.software.rip`, seit F2.5b an Java-Filius gemessen (die Pause in Javas `float`-Rechnung): eine Route mit 0 Hops je eigenem Netz, die nie verfällt; der erste Beacon nach einer Sekunde, dann alle 25,2–35,2 s aus einem Zufall je Router (Java: ein ungesetzter `Random` je Router; der Kern setzt ihn aus Szenario und Router-ID, `ripSeed`; Swift: ein Zufall für alle); eine Nachricht je Schnittstelle ohne die Routen über sie (Split Horizon); eine neue Route oder mehr Hops vom selben Router senden sofort; eine gelernte Route wird genau nach ihren 75 s auf 16 gesetzt und sofort gemeldet (Java erst beim nächsten eigenen Beacon, bis zu gut 30 s später; Swift bei jedem Ereignis; seit 01.10.2026, vorher wie Java); Nachrichten von jedem Quellport (Swift: nur 521); ein RIP-Router leitet nur nach seiner Tabelle weiter, wenige Hops vor langer Maske. `receiveUdp` kehrt wie Javas `empfangen` zurück, sobald ein Datagramm da ist. Seit der Abnahme F2 nach Javas `ARPThread`: ein Eintrag entsteht nur aus einer Anfrage nach der eigenen Adresse oder aus einer Antwort (Swift: aus jedem Paket; `swiftArp` behält das), ein Paket von der Netzadresse wird verworfen; ein Rechner leitet weiter, wenn seine IP-Weiterleitung an ist (`ipForwardingByNodeId`). Seit F2.8b nach Javas Tabellen gemessen: eine ARP-Anfrage trägt als Ziel-MAC die Broadcast-Adresse (Swift: Nullen; `swiftArp` behält sie), eine ICMP-Fehlermeldung Identifier und Sequenznummer 0 (RFC 792, Java; Swift kopiert die des Echos, ein Ping erkennt die Meldung am eingebetteten Echo), und „Host nicht erreichbar“ vom Router trägt das Paket, wie er es weitergeleitet hat (TTL gesenkt). Die Broadcast-Adresse schreibt sie immer groß (Java in der Antwort an 0.0.0.0 klein). Den Mitschnitt je Karte (`capture.ts`) schreibt sie seit F2.8a dort, wo Java ihn schreibt: beim Abgang auf das Kabel und bei der Ankunft an der Karte. Seit F3.4a läuft die Befehlszeile mit (`terminal`, auf den Dateien des Knotens). Seit F3.4b nach Javas `ICMP`/`ICMPThread`, an Java-Filius gemessen: ein Echo im Programm-Thread (`icmpEchoSteps`: Warteschlange leeren, senden mit ARP-Wartezeit, eine RTT auf die erste ICMP-Nachricht warten, gleich welche), ein ICMP-Paket an den Broadcast eines eigenen Netzes geht mit TTL 1 an alle und wird von dort beantwortet, einer für ein fremdes Netz verworfen; Javas Routenwahl (`routeRow`), die ARP-Tabelle mit Javas Broadcast-Zeile (`arpTable`, `removeArpEntry`, `resetArpTable`), DNS im Programm-Thread mit Antwort- und Autoritätsteil (`resolveDnsSteps`), ein UDP-Socket weiß, dass er ohne Gegenstelle lauscht (`listening`); ein Rechner nimmt Pakete an 127.0.0.1 an. Eine ARP-Wartezeit endet wie Javas `wait` nach allem, was zur selben Zeit sonst geschieht (eine Antwort zur selben Millisekunde zählt noch). Seit F3.4c die Uhrzeit eines Frames (`wallTime`) für `tcpdump`. Seit F3.6a läuft das Gnutella-Programm mit (`gnutella`, Schnappschuss `gnutellaByNodeId`), mit eigener Zufallsquelle für die GUIDs (`drawGnutellaGuid`). Seit F3.5a schreibt der Mailserver seine Kontendatei `/mailserver/konten.txt` beim Start und bei jeder Änderung seiner Konten in die Dateien des Knotens (Java schreibt die Benachrichtigung an einen lokalen Absender erst beim nächsten Speichern hinein). Abweichend von Java: eine Echo-Antwort kommt von der gefragten Adresse (RFC 792; Java antwortet von 127.0.0.1, wenn die eigene Adresse gefragt war, und von der nahen Karte, wenn die ferne eines Routers gefragt war), und „Netz nicht erreichbar“ vom Router trägt wie „Host nicht erreichbar“ das Paket, wie er es weitergeleitet hätte, mit Absender (Java bettet ein halb gebautes ohne Absender ein). Seit F3.3b läuft das E-Mail-Programm mit (`emailClient`), seine gesendeten und abgeholten Mails stehen im Schnappschuss (`emailClientsByNodeId`); ein Programm-Thread kann MX nachschlagen (`resolveDnsMailServerSteps`). Seit F3.3a läuft der E-Mail-Server mit (`emailServer`); was er annimmt oder löscht, steht im Schnappschuss (`emailServersByNodeId`), aus dem es zurück ins Projekt geht; die Uhrzeit für seine Benachrichtigungen kommt aus der Option `wallClock`. Seit F3.2c ändert die Seite `konfig` der Router und Heimrouter die Firewall zur Laufzeit; die Änderung steht im Schnappschuss (`firewallConfiguration`), aus dem sie zurück ins Projekt geht. Seit F3.2b starten Router und Heimrouter ihren Webserver auf Port 80 mit Javas Seiten (Javas Server lauscht wegen eines Wettlaufs in `setAktiv` manchmal nie, der Kern immer; `swiftRouters` lässt ihn für den Swift-Vergleich weg), die Firewall führt ein Protokoll je Knoten (Regeltreffer bei TCP und UDP, verworfenes ICMP), das wie Javas Protokollseite einen Neustart überlebt, eine RIP-Route kennt die öffentliche Adresse des meldenden Routers (`hopPublicIp`), und TCP zählt Sequenznummern und Segmentgröße wie Java in Zeichen (UTF-16-Einheiten) statt in Bytes, ohne ein Zeichenpaar zu zerreißen; Bytes, die kein Text sind, zählen als Bytes. Seit F3.2a schließt ein Programm wie Javas `schliessen()` nur auf Bitte (`requestTcpClose`): Das FIN geht zur selben Zeit, nach dem, was der Programm-Thread noch tut. Seit F3.1 laufen Verbinden, Senden, Empfangen und Annehmen auch als Schritte eines Programm-Threads (`connectTcpSteps`, `sendTcpSteps`, `receiveTcpSteps`, `acceptTcpSteps`, `resolveHostSteps`); die blockierenden Aufrufe sind nur noch eine Hülle darum. Abweichend von Java: Ein Verbindungsaufbau ohne Route scheitert sofort (Javas Socket-Thread stirbt an der `RouteNotFoundException`, `verbinden` wartet ewig), eine Nachricht, die vor dem Schließen ankam, wird noch ausgegeben (Java verwirft sie), und Senden unterscheidet wie Java „nicht verbunden", „unterbrochen" und „Zeitüberschreitung". WLAN seit F2.7 an Java-Filius gemessen: zur Laufzeit ist ein Funkkabel ein Kabel wie jedes andere, und eine SSID verbindet nichts (nach ihr legt nur Javas Editor das Funkkabel, `JHostKonfiguration.updateWifiConnection`); Swifts Verbindung über die SSID zur Laufzeit, auch ohne Kabel (`wirelessAssociations`), nicht übernommen. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Remote Link | | `src/runtime/engine.ts` (ganze Millisekunden) | Java `System.currentTimeMillis` (long) | behoben (F4.6b): `start` und `advance` runden auf ganze Millisekunden ab, wie Javas Uhr zählt. Gefunden im Browser: der Raum gab `performance.now()` mit Bruchteilen weiter, und ein Ping blieb nach der ersten Zeitüberschreitung hängen; alle Tests liefen zuvor mit glatten Takten | +| `src/runtime/engine.ts` (leere Nachricht), `test/apps.test.ts` | Java `TCPSocket.erstelleSegmente`, `TCPSocket.run` (gemessen mit `EchoWindowRun`) | behoben (F4.6g1): ein leeres Segment mit PSH an der erwarteten Stelle ist eine leere Nachricht und wird ausgeliefert (vorher galt es als Wiederholung, weil seine Nummer die nächste nicht weiterzählt; Javas Echo beantwortet sie). Java beendet eine Nachricht mit jedem PSH, auch dem eines wiederholten Segments (eine leere Nachricht mehr, wenn ein ACK verloren geht); hier nur mit einem neuen — ein wiederholtes leeres ist an seiner Nummer nicht von einem neuen zu unterscheiden (benannt) | | `src/runtime/engine.ts` (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/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) | @@ -158,6 +159,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/terminal.ts` | Java `GUIApplicationTerminalWindow` (gemessen mit `TerminalWindowRun`) | neu (F4.6b): die Zeilen eines Befehlszeilen-Fensters (Text, Eingabe nach ihrem Prompt, cls, Beschäftigt, Prompt, exit) und wie ein Client sie liest — Text ab dem letzten cls, Prompt, ob er sichtbar ist (verborgen, solange ein Befehl läuft), Verlauf; Javas Pfeiltasten (`commandHistoryPointer`) | | `src/web.ts`, `src/sim.ts` (`view`, `start.language`) | Java `GUIApplicationWebBrowserWindow`, `GUIApplicationWebServerWindow` (gemessen mit `BrowserWindowRun`) | neu (F4.6d): der Bildschirmstand eines Programmfensters (`view`, ganz, wenn er sich ändert) — der des Browsers (Adresszeile, Adresse des letzten Abrufs, Seite: Start, nicht erreichbar, Fehler mit Status, HTML mit den gekommenen Bildern, Text, Bild) und der des Webservers (Knopf, Kästchen, Tabelle); `browserTitle` wie Javas Fenster (Programmname, dann die Adresse, dann der Titel der Seite), `htmlTitle`; der Start nennt die Sprache, in der FILIUS für die startende Person läuft | | `src/mail.ts` | Java `GUIApplicationEmailAnwendungWindow` (`posteingangAktualisieren`, `gesendeteAktualisieren`, `replyLayout`), `GUIApplicationEmailServerWindow` (gemessen mit `MailWindowRun`) | neu (F4.6e): die Bildschirmstände von E-Mail-Programm (Konto, Posteingang, Gesendete) und E-Mail-Server (Knopf, Domain, Konten mit Zahl der Mails); die Zeilen der Ordner und die Antwort (an den Absender, „RE:“, zitierter Text) wie Javas Fenster | +| `src/echo.ts` | Java `GUIApplicationServerBausteinWindow`, `GUIApplicationClientBausteinWindow` (gemessen mit `EchoWindowRun`) | neu (F4.6g1): die Bildschirmstände von Echo-Server (läuft, Port) und Einfachem Client (verbunden, Adresse, Port); `portOfText`: ein Port, wie ein Socket ihn haben kann (Javas `Integer.parseInt` nimmt jede Zahl und wirft bei anderem Text) | ## packages/sim @@ -174,6 +176,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/room.ts` (E-Mail), `test/mailWindow.test.ts`, `test/mailWindow.java.json` | Java `GUIApplicationEmailAnwendungWindow`, `GUIApplicationEmailServerWindow` (gemessen mit `MailWindowRun`) | erweitert (F4.6e): der Raum schickt die Bildschirmstände von E-Mail-Programm und -Server zu ihren Zeilen und nimmt `setAccount`/`send`/`fetch`/`removeMail` und `setActive`/`setDomain` (nur bei angehaltenem Server)/`addAccount`/`removeAccount` an. Gegen Javas Fenster geprüft; benannt: ein vorhandener Benutzername, Löschen im falschen Ordner, die Domain in der Kontendatei | | `src/room.ts` (Rückweg), `test/writeBack.test.ts`, `test/fixtures/after-run-*.fls`, `test/terminalWindow.test.ts` | Java `GUIMainMenu.stopSimulation`, `SzenarioVerwaltung.speichern` (gemessen mit `SaveAfterRunRun`) | erweitert (F4.6f1): der Raum schreibt, was ein Lauf hinterlässt (Dateien, laufende Server, virtuelle Hosts, Domain und Konten der E-Mail-Server, Konto und Ordner der E-Mail-Programme, Firewalls, die Adresse eines DHCP-Clients), als `origin: 'sim'` ins Dokument: höchstens einmal je Sekunde (ab dem Start gezählt), nach jeder Aktion in einem Fenster und beim Stopp. Gegen Javas nach dem Lauf gespeichertes Projekt geprüft; die Befehlszeile geht nach einem Neustart im vorher angelegten Ordner weiter wie Javas (die benannte Abweichung aus F4.6b ist weg) | | `src/room.ts` (Dateien in beide Richtungen), `test/writeBack.test.ts` | Plan §7.3 | erweitert (F4.6f2): was eine Person ins Dokument schreibt (der Texteditor), geht im Lauf sofort in die Dateien der Engine; was die Engine an ihren Dateien ändert, geht im Takt der Änderung ins Dokument (nicht erst nach einer Sekunde), damit das Dokument jede Änderung der Engine hat, bevor die nächste einer Person kommt — so behält ein Text, den beide zugleich ändern, beides. Ein Rechner, dessen Programme keine Dateien benutzen, liest seine beim nächsten Start | +| `src/room.ts` (Echo), `test/echoWindow.test.ts`, `test/echoWindow.java.json`, `test/fixtures/after-run-echo.fls`, `test/room.test.ts` | Java `GUIApplicationServerBausteinWindow`, `GUIApplicationClientBausteinWindow` (gemessen mit `EchoWindowRun`), `SaveAfterRunRun echo` | erweitert (F4.6g1): der Raum nimmt `setActive` mit Port beim Echo-Server und `connect`/`disconnect`/`send` beim Einfachen Client an, schickt ihre Bildschirmstände und schreibt nach dem Lauf Port und Ziel zurück, wie Javas Projekt sie behält. Gegen Javas Fenster und Javas gespeichertes Projekt geprüft; benannt: drei SYN statt einem (F2.3c) — ein Verbindungsversuch dauert länger | | `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 @@ -205,6 +208,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/desktop/EmailClientPanel.tsx`, `src/desktop/EmailServerPanel.tsx`, `src/desktop/mailLines.ts`, `src/assets/desktop/email_*.png` | Java `GUIApplicationEmailAnwendungWindow` (`emailanwendung_msg1`–`47`), `GUIApplicationEmailServerWindow` (`emailserver_msg1`–`29`), `SMTPMitarbeiter`/`SMTPClient` (`sw_smtpmitarbeiter_msg12`–`14`, `sw_smtpclient_msg1`–`4`), Javas Symbole (`gfx/desktop`, `img/email_icon.png`) | neu (F4.6e): das E-Mail-Programm — Abrufen, Verfassen, Antworten, Löschen, Konto einrichten, Posteingang und Gesendete, darunter die gewählte Mail (sonst Javas Willkommen), Verfassen und Konto als Fenster über dem Programm, Javas Dialoge als Hinweis mit OK, ein Fehler des Programms wie Javas Dialog; die gewählte Mail gehört zu ihrem Ordner (Java löschte die zuvor im anderen Ordner gewählte). Der E-Mail-Server — Starten/Anhalten, Maildomain (nur angehalten, rot, wo Javas Muster sie nicht nimmt), Neues Konto mit Namensfeld (Java hat das Etikett ohne Feld; ohne Namen Javas Platzhalter), Konten-Liste mit Javas Frage vor dem Entfernen, Log Fenster in Javas Worten | | `src/desktop/textEditor.ts`, `src/desktop/TextEditorPanel.tsx`, `src/desktop/FileDialog.tsx`, `src/desktop/textBinding.ts`, `src/assets/desktop/datei.png`, `src/assets/desktop/ordner.png` | Java `GUIApplicationTextEditorWindow` (`texteditor_msg3`–`6`), `DMTNFileChooser` (`dmtnfilechooser_msg1`–`11`), `OrdnerInhaltListRenderer` (Javas Symbole aus `gfx/desktop`), gemessen mit `TextEditorWindowRun` | neu (F4.6f2): der Texteditor — „Neu“, „Öffnen“, „Speichern“, „Speichern unter“ über Javas Dateidialog, der Titel mit dem Namen der offenen Datei, Javas Courier; der Text der offenen Datei ist ihr `Y.Text` im Dokument (Plan §7.3): Getipptes steht sofort in der Datei, für alle, die sie offen haben, und für die laufende Simulation (Java hält eine Kopie bis „Speichern“). Strg+Z/Strg+Y nehmen das eigene Getippte zurück. Besser als Java: „Speichern unter“ auf eine vorhandene Datei schreibt sie und bleibt in ihr (Java bleibt in einer Kopie, die kein Ordner hält, jedes spätere „Speichern“ ging verloren); eine gelöschte offene Datei lässt ihren Text als neuen Text, „Speichern“ fragt nach einem Namen (Javas ging verloren); was der Dialog nicht findet oder kein Text ist, sagt er, die offene Datei bleibt; der Dialog zeigt den Pfad des Ordners (Java nur den Namen, die Wurzel als „root“), ein Tippen öffnet einen Ordner oder nimmt einen Dateinamen (Java will einen Doppelklick) | | `src/desktop/fileExplorer.ts`, `src/desktop/windowState.ts`, `src/desktop/FileExplorerPanel.tsx`, `src/desktop/ImageViewerPanel.tsx` | Java `GUIApplicationFileExplorerWindow` (`fileexplorer_msg2`–`19`, `sw_fileexplorer_msg1`–`3`, `sw_fileimporter_msg3`), `GUIApplicationImageViewerWindow` (`imageviewer_msg2`), gemessen mit `FileExplorerWindowRun` | neu (F4.6f3): der Datei-Explorer — Baum aus Ordnern und Dateien, die Einträge des gewählten Ordners mit Javas Symbolen, Javas Menü per Rechtsklick (auf einem Eintrag Löschen, Ausschneiden, Kopieren, Umbenennen; auf der freien Fläche Neuer Ordner, Einfügen), Javas Fragen und das Importfenster; Kopiertes bleibt zum nächsten Einfügen, Ausgeschnittenes nicht. Der Bildbetrachter — „Öffnen“ über Javas Dateidialog, das Bild, sein Name als Titel. Die Dateien sind die des Dokuments, geändert über die Operationen des Kerns. Besser als Java: Baum und Liste folgen den Dateien sofort („Aktualisieren“ entfällt); die Aktionen des Menüs sind auch Knöpfe, ein Finger braucht keinen Rechtsklick; ein Ordner der Liste öffnet mit Doppelklick; eine im Baum gewählte Datei wird in ihrem Ordner gewählt (Java hält sie für einen Ordner, ein neuer Ordner landete unter ihr); was eine Aktion ablehnt, wird gesagt; der neue Name beginnt mit dem alten; der Bildbetrachter zeigt ein Bild statt alle übereinander und sagt, was kein Bild ist. `windowState.ts`: was die Fenster der lokalen Programme teilen (auch der Texteditor) | +| `src/desktop/EchoPanels.tsx`, `src/desktop/serverLines.ts` (`simpleClientLine`) | Java `GUIApplicationServerBausteinWindow` (`serverbaustein_msg1`–`3`), `GUIApplicationClientBausteinWindow` (`clientbaustein_msg1`–`6`), `ClientBaustein` (`sw_clientbaustein_msg2`–`6`), `Socket`/`TCPSocket` (`sw_socket_msg1`, `sw_tcpsocket_msg2`, `3`, `9`) | neu (F4.6g1): Echo-Server — Starten/Anhalten mit Port (nur angehalten beschreibbar) und seine Zeilen; Einfacher Client — Server-Adresse und -Port (verbunden gesperrt, nach einer Änderung des Programms wieder dessen Werte), Verbinden/Trennen, Nachricht und Senden (nur verbunden, danach leer), seine Zeilen in Javas Worten. Besser als Java: ein Port, der keiner ist, wird rot markiert und nicht übergeben (Javas Knopf wirft und tut nichts) | | `src/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/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 | @@ -212,6 +216,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `browser/simulation.spec.ts` (Rückweg) | — | erweitert (F4.6f1): ein in der Befehlszeile angelegter Ordner ist nach Entwurf und Neustart da, ein entfernter weg | | `browser/text.spec.ts`, `test/textEditor.test.ts`, `test/textBinding.test.ts`, `src/main.tsx` (`filiusDev.insertText`) | — | neu (F4.6f2): Javas Schritte am Fenster des Texteditors nachgespielt (gleich, wo Java recht hat, benannt, wo es irrt); in Chromium: im Lauf installiert, eine Seite über Javas Dialog in den Webserver gespeichert, vom anderen Rechner abgerufen, im Lauf geändert und wieder abgerufen, der Cursor bleibt, wo man tippt, Rückgängig, Ablehnungen im Dialog, eine von der Befehlszeile gelöschte offene Datei, „Neu“, Weitertippen nach einer fremden Änderung vor dem Cursor (die Entwicklungsseite schreibt dafür wie eine andere Person in eine Datei); wohin Cursor und Auswahl bei fremden Änderungen gehen | | `browser/files.spec.ts`, `test/fileExplorer.test.ts` | — | neu (F4.6f3): Javas Schritte an Explorer und Bildbetrachter nachgespielt (gleich, wo Java recht hat, benannt, wo es irrt); in Chromium: im Lauf installiert, Ordner angelegt, über Javas Rechtsklick-Menü kopiert und eingefügt, Javas Meldung beim Einfügen in sich selbst, per Knopf umbenannt und nach „Sicher?“ gelöscht, was die Befehlszeile anlegt sofort sichtbar, ein lokales Bild importiert und im Bildbetrachter gezeigt, eine Textdatei dort abgelehnt | +| `browser/echo.spec.ts`, `test/serverLines.test.ts` (Echo) | — | neu (F4.6g1): Echo in Chromium — im Lauf installiert, ein Port, der keiner ist, markiert, Server gestartet, Client verbunden, Felder gesperrt, Nachricht gesendet und zurückbekommen, Trennen; die Zeilen beider Fenster gegen Javas | | `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 | diff --git a/packages/core/src/runtime/engine.ts b/packages/core/src/runtime/engine.ts index 2d8bb8f..e7b65b1 100644 --- a/packages/core/src/runtime/engine.ts +++ b/packages/core/src/runtime/engine.ts @@ -2799,11 +2799,21 @@ export class NetworkRuntimeEngine { } } + /** + * A data segment (TCPSocket's receive loop): new data goes into the + * message, a push ends it. An empty message is one empty segment with the + * push (TCPSocket.erstelleSegmente makes at least one; an empty segment + * comes here only with it). Java ends a message with every push, also with + * a repeated segment's (an ACK lost: an empty message more); here only + * with a new one's — but an empty segment repeated cannot be told from a + * new one by its number, which it does not count on, and is an empty + * message again (named). + */ private receiveTcpPayload(socketId: Id, segment: TcpSegment): void { const session = this.stateValue.tcpSessionsById.get(socketId); if (session === undefined) return; const nextRemoteSequence = nextTcpSequenceNumber(segment); - if (nextRemoteSequence > session.remoteSequenceNumber) { + if (nextRemoteSequence > session.remoteSequenceNumber || segment.payload.length === 0) { session.remoteSequenceNumber = nextRemoteSequence; session.receiveAssembly = concatBytes(session.receiveAssembly, segment.payload); if ((segment.flags & TCP_FLAG.push) !== 0) { diff --git a/packages/core/test/apps.test.ts b/packages/core/test/apps.test.ts index 005eb0f..50f40ff 100644 --- a/packages/core/test/apps.test.ts +++ b/packages/core/test/apps.test.ts @@ -225,4 +225,20 @@ describe('the echo server and the generic client', () => { ); expect(received).toEqual(['zuletzt', 'notConnected']); }); + + // Measured with EchoWindowRun (packages/sim/test/echoWindow.java.json): Java's echo answers an empty message. + it('echoes an empty message: one empty segment with the push, once, and the next message after it', () => { + const engine = startedEngine(network()); + const client = engine.simpleClient(CLIENT)!; + client.connect('10.0.0.1', 55555); + advance(engine, 6_000); + client.send(''); + advance(engine, 10_000); + client.send('danach'); + advance(engine, 14_000); + const server = engine.echoServer(SERVER)!; + expect(server.log.all.filter((line) => line.kind === 'received' || line.kind === 'sent').map((line) => `${line.kind}:${'text' in line ? line.text : ''}`)).toEqual(['received:', 'sent:', 'received:danach', 'sent:danach']); + expect(client.log.all.filter((line) => line.kind === 'received').map((line) => ('text' in line ? line.text : ''))).toEqual(['', 'danach']); + }); }); + diff --git a/packages/editor/browser/echo.spec.ts b/packages/editor/browser/echo.spec.ts new file mode 100644 index 0000000..a1a2781 --- /dev/null +++ b/packages/editor/browser/echo.spec.ts @@ -0,0 +1,86 @@ +// The echo server and the generic client on two computers' desktops in +// Chromium (F4.6g1), on browser/fixtures/web-pages.fls: installed while the +// simulation runs; the server started on a port typed (one that is no port +// marked and not handed over), the client connected, a message sent and +// echoed, the fields locked while connected, the server's lines, "Trennen". + +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { expect, test, type Page } from '@playwright/test'; +import { drawing, onDevice, serveEditor } from './editorPage.js'; + +let server: { readonly base: string; readonly close: () => Promise }; + +test.beforeAll(async () => { + server = await serveEditor(); +}); + +test.afterAll(async () => { + await server.close(); +}); + +const FIXTURE = join(dirname(fileURLToPath(import.meta.url)), 'fixtures/web-pages.fls'); + +async function openPages(page: Page): Promise { + await page.goto(`${server.base}?example=new`); + await page.waitForSelector('svg.filius-canvas'); + await page.locator('input[type="file"]').first().setInputFiles(FIXTURE); + await expect.poll(async () => (await drawing(page).catch(() => ({ nodes: [] }))).nodes.length).toBe(4); + await page.getByRole('button', { name: 'Wechselt in den Aktionsmodus' }).click(); + await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation'); +} + +/** A computer's desktop opened with a tap, a program installed on it, and its window opened. */ +async function install(page: Page, name: string, program: string, label: string) { + const id = (await drawing(page)).nodes.find((node) => node.name === name)!.id; + const at = await onDevice(page, id); + await page.mouse.click(at.x, at.y); + const desktop = page.locator(`section[data-desktop="${id}"]`); + 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: label }).click(); + await installation.getByRole('button', { name: 'Installieren' }).click(); + await installation.getByRole('button', { name: 'Änderungen annehmen' }).click(); + await desktop.locator(`[data-program="${program}"]`).click(); + return { desktop, window: desktop.locator(`[data-program-window="${program}"]`) }; +} + +test('echoes a message from the generic client, as Java’s windows do', async ({ page }) => { + test.setTimeout(120_000); + await openPages(page); + const echo = await install(page, 'Server', 'echoServer', 'Echo-Server'); + await expect(echo.window.locator('.program-titlebar h3')).toHaveText('Echo-Server'); + const port = echo.window.getByRole('textbox', { name: 'Port:' }); + await expect(port).toHaveValue('55555'); + // A port that is no port: marked, the button does nothing. + await port.fill('abc'); + await expect(port).toHaveAttribute('aria-invalid', 'true'); + await echo.window.getByRole('button', { name: 'Starten' }).click(); + await expect(echo.window.getByRole('button', { name: 'Starten' })).toBeVisible(); + await port.fill('7777'); + await echo.window.getByRole('button', { name: 'Starten' }).click(); + await expect(echo.window.getByRole('button', { name: 'Anhalten' })).toBeVisible(); + await expect(port).toHaveAttribute('readonly', ''); + const serverLog = echo.window.getByLabel('Ausgabe'); + await expect(serverLog).toHaveText('Annahme von Verbindungsanfragen gestartet\n'); + await echo.desktop.locator('.desktop-close').click(); + + const client = await install(page, 'Browser', 'simpleClient', 'Einfacher Client'); + const send = client.window.getByRole('button', { name: 'Senden' }); + await expect(send).toBeDisabled(); + await client.window.getByRole('textbox', { name: 'Server-Adresse:' }).fill('192.168.0.20'); + await client.window.getByRole('textbox', { name: 'Server-Port:' }).fill('7777'); + await client.window.getByRole('button', { name: 'Verbinden' }).click(); + await expect(client.window.getByRole('button', { name: 'Trennen' })).toBeVisible({ timeout: 30_000 }); + await expect(client.window.getByRole('textbox', { name: 'Server-Adresse:' })).toHaveAttribute('readonly', ''); + const output = client.window.getByLabel('Ausgabe'); + await expect(output).toHaveText('Verbindung hergestellt\n'); + await client.window.getByRole('textbox', { name: 'Nachricht:' }).fill('Hallo Echo'); + await send.click(); + await expect(client.window.getByRole('textbox', { name: 'Nachricht:' })).toHaveValue(''); + await expect(output).toHaveText('Verbindung hergestellt\n<>Hallo Echo\n', { timeout: 30_000 }); + await client.window.getByRole('button', { name: 'Trennen' }).click(); + await expect(client.window.getByRole('button', { name: 'Verbinden' })).toBeVisible({ timeout: 30_000 }); + await expect(output).toContainText('Verbindung getrennt'); + await expect(send).toBeDisabled(); +}); diff --git a/packages/editor/src/desktop/DesktopWindow.tsx b/packages/editor/src/desktop/DesktopWindow.tsx index 9c7d7ee..87feec8 100644 --- a/packages/editor/src/desktop/DesktopWindow.tsx +++ b/packages/editor/src/desktop/DesktopWindow.tsx @@ -13,8 +13,8 @@ // icon opens its window over the desktop (ProgramFrame), the home button // leaves it; so far the command line (F4.6b), the web browser and the web // server (F4.6d), the e-mail program and server (F4.6e), the text editor -// (F4.6f2), the file explorer and the image viewer (F4.6f3) have one, the -// others follow. The browser's window takes its title from its page, the +// (F4.6f2), the file explorer and the image viewer (F4.6f3), the echo +// server and the generic client (F4.6g1) have one, the others follow. The browser's window takes its title from its page, the // text editor's from its open file, the image viewer's from its picture, as // Java's do. @@ -38,6 +38,7 @@ import { InstallationPanel } from './InstallationPanel.js'; import { TerminalPanel } from './TerminalPanel.js'; import { TextEditorPanel, useTextEditorWindow } from './TextEditorPanel.js'; import { FileExplorerPanel } from './FileExplorerPanel.js'; +import { EchoServerPanel, SimpleClientPanel } from './EchoPanels.js'; import { ImageViewerPanel, useImageViewer } from './ImageViewerPanel.js'; import { WebServerPanel } from './WebServerPanel.js'; import './desktop.css'; @@ -68,7 +69,7 @@ export interface DesktopWindowProps { export type DesktopWindowName = DesktopProgram | 'installation'; /** The programs whose windows are built so far. */ -const WITH_WINDOW: ReadonlySet = new Set(['commandPrompt', 'webBrowser', 'webServer', 'emailClient', 'emailServer', 'textEditor', 'fileExplorer', 'imageViewer']); +const WITH_WINDOW: ReadonlySet = new Set(['commandPrompt', 'webBrowser', 'webServer', 'emailClient', 'emailServer', 'textEditor', 'fileExplorer', 'imageViewer', 'echoServer', 'simpleClient']); /** The document's desktop of a computer, read anew after every change (a program installed, the label changed). */ function useDesktopView(doc: Y.Doc, id: string) { @@ -183,6 +184,8 @@ export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: o {shown === 'textEditor' ? : null} {shown === 'fileExplorer' ? : null} {shown === 'imageViewer' ? : null} + {shown === 'echoServer' ? : null} + {shown === 'simpleClient' ? : null} )} {info ? ( diff --git a/packages/editor/src/desktop/EchoPanels.tsx b/packages/editor/src/desktop/EchoPanels.tsx new file mode 100644 index 0000000..f5c3cd1 --- /dev/null +++ b/packages/editor/src/desktop/EchoPanels.tsx @@ -0,0 +1,132 @@ +// A computer's echo server and generic client, after Java's +// GUIApplicationServerBausteinWindow and GUIApplicationClientBausteinWindow +// (measured with tools/java-oracle's EchoWindowRun). The server: its start +// button ("Starten"/"Anhalten") with the port beside it, which can be typed +// into only while it does not run, and its lines. The client: the server's +// address and port (typed into only while it is not connected; they show the +// program's again when it changes, as Java's window sets them from it), +// "Verbinden"/"Trennen", the message and "Senden" (only while connected; the +// message is emptied once it is sent) and the lines. The programs are the +// room's: the panels show their views and lines and hand over what is done. +// +// Better than Java: a port that is no port (Java's Integer.parseInt throws, +// and the button does nothing) is marked red, and the button hands it not +// over. + +import { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import type { LogLine, ServerLogEntry, SimpleClientLogEntry } from '@mdg-filius/core'; +import { portOfText, type EchoServerView, type SimpleClientView } from '@mdg-filius/protocol'; +import { useTranslate } from '../i18n/i18n.js'; +import type { Simulation } from '../simulation/useSimulation.js'; +import { serverLine, simpleClientLine } from './serverLines.js'; + +const NO_LINES: readonly LogLine[] = []; + +/** A program's lines as text, the newest in sight as Java's text area scrolls. */ +function useLines(simulation: Simulation, topic: `app:${string}`, line: (entry: Entry) => string) { + const lines = (simulation.apps.get(topic as never)?.lines as readonly LogLine[] | undefined) ?? NO_LINES; + const text = lines.map((each) => `${line(each)}\n`).join(''); + const element = useRef(null); + useLayoutEffect(() => { + if (element.current !== null) element.current.scrollTop = element.current.scrollHeight; + }, [text]); + return { text, element }; +} + +export function EchoServerPanel({ node, simulation }: { readonly node: string; readonly simulation: Simulation }) { + const t = useTranslate(); + const topic = `app:${node}/echoServer` as const; + const { watch, act } = simulation; + useEffect(() => watch([topic]), [watch, topic]); + const view = simulation.views.get(topic) as EchoServerView | null | undefined; + const [port, setPort] = useState(view === null || view === undefined ? '' : String(view.port)); + useEffect(() => { + if (view !== null && view !== undefined) setPort(String(view.port)); + }, [view]); + const { text, element } = useLines(simulation, topic, (entry) => serverLine(entry, t)); + if (view === null || view === undefined) return
; + const typed = portOfText(port); + const press = () => { + if (view.active) act(node, 'echoServer', 'setActive', { active: false }); + else if (typed !== undefined) act(node, 'echoServer', 'setActive', { active: true, port: typed }); + }; + return ( +
+
+ + +
+
+        {text}
+      
+
+ ); +} + +export function SimpleClientPanel({ node, simulation }: { readonly node: string; readonly simulation: Simulation }) { + const t = useTranslate(); + const topic = `app:${node}/simpleClient` as const; + const { watch, act } = simulation; + useEffect(() => watch([topic]), [watch, topic]); + const view = simulation.views.get(topic) as SimpleClientView | null | undefined; + const [address, setAddress] = useState(view?.address ?? ''); + const [port, setPort] = useState(view === null || view === undefined ? '' : String(view.port)); + const [message, setMessage] = useState(''); + useEffect(() => { + if (view === null || view === undefined) return; + setAddress(view.address); + setPort(String(view.port)); + }, [view]); + const { text, element } = useLines(simulation, topic, (entry) => simpleClientLine(entry, t)); + if (view === null || view === undefined) return
; + const typed = portOfText(port); + const connect = () => { + if (view.connected) act(node, 'simpleClient', 'disconnect'); + else if (typed !== undefined) act(node, 'simpleClient', 'connect', { address, port: typed }); + }; + const send = () => { + act(node, 'simpleClient', 'send', { text: message }); + setMessage(''); + }; + return ( +
+ + +
+ +
+