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 <noreply@anthropic.com>
This commit is contained in:
@@ -20,8 +20,9 @@
|
||||
//
|
||||
// A program's window may show more than its lines (plan §7.3): the browser's
|
||||
// page, the web server's button and table, the e-mail program's account and
|
||||
// folders, the e-mail server's domain and accounts go to the clients as a
|
||||
// view, whole, when they change.
|
||||
// folders, the e-mail server's domain and accounts, the echo server's button
|
||||
// and port, the generic client's connection go to the clients as a view,
|
||||
// whole, when they change.
|
||||
//
|
||||
// While the simulation runs, the document's programs are watched: a program
|
||||
// installed or removed on the desktop (Java's software installation) is
|
||||
@@ -70,6 +71,8 @@ import {
|
||||
type Topic,
|
||||
type EmailClientView,
|
||||
type EmailServerView,
|
||||
type EchoServerView,
|
||||
type SimpleClientView,
|
||||
type TraceFrame,
|
||||
type WebServerView,
|
||||
} from '@mdg-filius/protocol';
|
||||
@@ -237,7 +240,12 @@ export class SimulationRoom {
|
||||
settings.emailServer = emailServerAfterRun(configured as never, mailServerSettings);
|
||||
}
|
||||
const echo = this.engine.echoServer(id);
|
||||
if (echo !== undefined) active.echoServer = echo.isActive;
|
||||
if (echo !== undefined) {
|
||||
active.echoServer = echo.isActive;
|
||||
settings.echoServer = { port: echo.port };
|
||||
}
|
||||
const client = this.engine.simpleClient(id);
|
||||
if (client !== undefined) settings.simpleClient = { targetAddress: client.targetAddress, targetPort: client.targetPort };
|
||||
settings.emailClient = snapshot.emailClientsByNodeId.get(id);
|
||||
settings.firewall = snapshot.firewallConfigurationsByNodeId.get(id);
|
||||
if (snapshot.dhcpClientConfigurationsByNodeId.get(id)?.isEnabled === true) settings.device = snapshot.deviceConfigurations.get(id);
|
||||
@@ -442,6 +450,49 @@ export class SimulationRoom {
|
||||
return this.actInEmailClient(input);
|
||||
case 'emailServer':
|
||||
return this.actInEmailServer(input);
|
||||
case 'echoServer':
|
||||
return this.actInEchoServer(input);
|
||||
case 'simpleClient':
|
||||
return this.actInSimpleClient(input);
|
||||
default:
|
||||
return 'unknownAction';
|
||||
}
|
||||
}
|
||||
|
||||
/** The start button with the port typed (Java's window sets the port before and after it starts the server). */
|
||||
private actInEchoServer(input: Extract<ClientMessage, { kind: 'input' }>): ErrorCode | undefined {
|
||||
const server = this.engine.echoServer(input.node);
|
||||
if (server === undefined) return 'unknownApp';
|
||||
const { args } = input;
|
||||
if (input.action !== 'setActive') return 'unknownAction';
|
||||
if (typeof args.active !== 'boolean') return 'badArgs';
|
||||
if (!args.active) {
|
||||
server.setActive(false);
|
||||
return undefined;
|
||||
}
|
||||
if (!isPort(args.port)) return 'badArgs';
|
||||
server.setPort(args.port);
|
||||
server.setActive(true);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** "Verbinden" with the address and port typed, "Trennen", "Senden". */
|
||||
private actInSimpleClient(input: Extract<ClientMessage, { kind: 'input' }>): ErrorCode | undefined {
|
||||
const client = this.engine.simpleClient(input.node);
|
||||
if (client === undefined) return 'unknownApp';
|
||||
const { args } = input;
|
||||
switch (input.action) {
|
||||
case 'connect':
|
||||
if (typeof args.address !== 'string' || !isPort(args.port)) return 'badArgs';
|
||||
client.connect(args.address, args.port);
|
||||
return undefined;
|
||||
case 'disconnect':
|
||||
client.disconnect();
|
||||
return undefined;
|
||||
case 'send':
|
||||
if (typeof args.text !== 'string') return 'badArgs';
|
||||
client.send(args.text);
|
||||
return undefined;
|
||||
default:
|
||||
return 'unknownAction';
|
||||
}
|
||||
@@ -583,6 +634,18 @@ export class SimulationRoom {
|
||||
const view: EmailClientView = { account: client.account, inbox: client.inbox.map((entry) => entry.mail), sent: client.sent.map((entry) => entry.mail) };
|
||||
return view;
|
||||
}
|
||||
case 'echoServer': {
|
||||
const server = this.engine.echoServer(node);
|
||||
if (server === undefined) return null;
|
||||
const view: EchoServerView = { active: server.isActive, port: server.port };
|
||||
return view;
|
||||
}
|
||||
case 'simpleClient': {
|
||||
const client = this.engine.simpleClient(node);
|
||||
if (client === undefined) return null;
|
||||
const view: SimpleClientView = { connected: client.isConnected, address: client.targetAddress, port: client.targetPort };
|
||||
return view;
|
||||
}
|
||||
case 'emailServer': {
|
||||
const server = this.engine.emailServer(node);
|
||||
if (server === undefined) return null;
|
||||
@@ -732,6 +795,11 @@ export class SimulationRoom {
|
||||
}
|
||||
}
|
||||
|
||||
/** A port a socket can have. */
|
||||
function isPort(value: unknown): value is number {
|
||||
return Number.isInteger(value) && (value as number) >= 0 && (value as number) <= 65_535;
|
||||
}
|
||||
|
||||
/** A card's frames as the capture window shows them, both views. */
|
||||
export function traceFramesOf(frames: readonly CapturedFrame[]): TraceFrame[] {
|
||||
const byNumber = (rows: readonly CaptureRow[]) => {
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
{
|
||||
"steps": [
|
||||
{
|
||||
"step": "open",
|
||||
"server": {
|
||||
"button": "Starten",
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": true
|
||||
},
|
||||
"log": ""
|
||||
},
|
||||
"client": {
|
||||
"connect": "Verbinden",
|
||||
"sendEnabled": false,
|
||||
"address": {
|
||||
"text": "",
|
||||
"editable": true
|
||||
},
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": true
|
||||
},
|
||||
"message": "",
|
||||
"output": ""
|
||||
},
|
||||
"thrown": []
|
||||
},
|
||||
{
|
||||
"step": "server: start with port abc",
|
||||
"server": {
|
||||
"button": "Starten",
|
||||
"port": {
|
||||
"text": "abc",
|
||||
"editable": true
|
||||
},
|
||||
"log": ""
|
||||
},
|
||||
"client": {
|
||||
"connect": "Verbinden",
|
||||
"sendEnabled": false,
|
||||
"address": {
|
||||
"text": "",
|
||||
"editable": true
|
||||
},
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": true
|
||||
},
|
||||
"message": "",
|
||||
"output": ""
|
||||
},
|
||||
"thrown": [
|
||||
"NumberFormatException: For input string: \"abc\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "server: start with port 55555",
|
||||
"server": {
|
||||
"button": "Anhalten",
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": false
|
||||
},
|
||||
"log": "Annahme von Verbindungsanfragen gestartet\n"
|
||||
},
|
||||
"client": {
|
||||
"connect": "Verbinden",
|
||||
"sendEnabled": false,
|
||||
"address": {
|
||||
"text": "",
|
||||
"editable": true
|
||||
},
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": true
|
||||
},
|
||||
"message": "",
|
||||
"output": ""
|
||||
},
|
||||
"thrown": []
|
||||
},
|
||||
{
|
||||
"step": "client: connect with port x",
|
||||
"server": {
|
||||
"button": "Anhalten",
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": false
|
||||
},
|
||||
"log": "Annahme von Verbindungsanfragen gestartet\n"
|
||||
},
|
||||
"client": {
|
||||
"connect": "Verbinden",
|
||||
"sendEnabled": false,
|
||||
"address": {
|
||||
"text": "192.168.0.30",
|
||||
"editable": true
|
||||
},
|
||||
"port": {
|
||||
"text": "x",
|
||||
"editable": true
|
||||
},
|
||||
"message": "",
|
||||
"output": ""
|
||||
},
|
||||
"thrown": [
|
||||
"NumberFormatException: For input string: \"x\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "client: connect to 192.168.0.30:55555",
|
||||
"server": {
|
||||
"button": "Anhalten",
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": false
|
||||
},
|
||||
"log": "Annahme von Verbindungsanfragen gestartet\nVerbindung zu 192.168.0.20:57292 hergestellt\n"
|
||||
},
|
||||
"client": {
|
||||
"connect": "Trennen",
|
||||
"sendEnabled": true,
|
||||
"address": {
|
||||
"text": "192.168.0.30",
|
||||
"editable": false
|
||||
},
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": false
|
||||
},
|
||||
"message": "",
|
||||
"output": "Verbindung hergestellt\n"
|
||||
},
|
||||
"thrown": []
|
||||
},
|
||||
{
|
||||
"step": "client: send Hallo Echo",
|
||||
"server": {
|
||||
"button": "Anhalten",
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": false
|
||||
},
|
||||
"log": "Annahme von Verbindungsanfragen gestartet\nVerbindung zu 192.168.0.20:57292 hergestellt\n>>Hallo Echo\n<<Hallo Echo\n"
|
||||
},
|
||||
"client": {
|
||||
"connect": "Trennen",
|
||||
"sendEnabled": true,
|
||||
"address": {
|
||||
"text": "192.168.0.30",
|
||||
"editable": false
|
||||
},
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": false
|
||||
},
|
||||
"message": "",
|
||||
"output": "Verbindung hergestellt\n<<Hallo Echo\n>>Hallo Echo\n"
|
||||
},
|
||||
"thrown": []
|
||||
},
|
||||
{
|
||||
"step": "client: send nothing",
|
||||
"server": {
|
||||
"button": "Anhalten",
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": false
|
||||
},
|
||||
"log": "Annahme von Verbindungsanfragen gestartet\nVerbindung zu 192.168.0.20:57292 hergestellt\n>>Hallo Echo\n<<Hallo Echo\n>>\n<<\n"
|
||||
},
|
||||
"client": {
|
||||
"connect": "Trennen",
|
||||
"sendEnabled": true,
|
||||
"address": {
|
||||
"text": "192.168.0.30",
|
||||
"editable": false
|
||||
},
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": false
|
||||
},
|
||||
"message": "",
|
||||
"output": "Verbindung hergestellt\n<<Hallo Echo\n>>Hallo Echo\n<<\n>>\n"
|
||||
},
|
||||
"thrown": []
|
||||
},
|
||||
{
|
||||
"step": "client: send two lines",
|
||||
"server": {
|
||||
"button": "Anhalten",
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": false
|
||||
},
|
||||
"log": "Annahme von Verbindungsanfragen gestartet\nVerbindung zu 192.168.0.20:57292 hergestellt\n>>Hallo Echo\n<<Hallo Echo\n>>\n<<\n>>Zeile 1\nZeile 2\n<<Zeile 1\nZeile 2\n"
|
||||
},
|
||||
"client": {
|
||||
"connect": "Trennen",
|
||||
"sendEnabled": true,
|
||||
"address": {
|
||||
"text": "192.168.0.30",
|
||||
"editable": false
|
||||
},
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": false
|
||||
},
|
||||
"message": "",
|
||||
"output": "Verbindung hergestellt\n<<Hallo Echo\n>>Hallo Echo\n<<\n>>\n<<Zeile 1\nZeile 2\n>>Zeile 1\nZeile 2\n"
|
||||
},
|
||||
"thrown": []
|
||||
},
|
||||
{
|
||||
"step": "server: stop while connected",
|
||||
"server": {
|
||||
"button": "Starten",
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": true
|
||||
},
|
||||
"log": "Annahme von Verbindungsanfragen gestartet\nVerbindung zu 192.168.0.20:57292 hergestellt\n>>Hallo Echo\n<<Hallo Echo\n>>\n<<\n>>Zeile 1\nZeile 2\n<<Zeile 1\nZeile 2\nAnnahme von Verbindungsanfragen beendet\nSocket zu 192.168.0.20:57292 geschlossen\nServer-Socket geschlossen\n"
|
||||
},
|
||||
"client": {
|
||||
"connect": "Verbinden",
|
||||
"sendEnabled": false,
|
||||
"address": {
|
||||
"text": "192.168.0.30",
|
||||
"editable": true
|
||||
},
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": true
|
||||
},
|
||||
"message": "",
|
||||
"output": "Verbindung hergestellt\n<<Hallo Echo\n>>Hallo Echo\n<<\n>>\n<<Zeile 1\nZeile 2\n>>Zeile 1\nZeile 2\nSocket zu 192.168.0.30:55555 getrennt\n"
|
||||
},
|
||||
"thrown": []
|
||||
},
|
||||
{
|
||||
"step": "client: send after the server stopped",
|
||||
"server": {
|
||||
"button": "Starten",
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": true
|
||||
},
|
||||
"log": "Annahme von Verbindungsanfragen gestartet\nVerbindung zu 192.168.0.20:57292 hergestellt\n>>Hallo Echo\n<<Hallo Echo\n>>\n<<\n>>Zeile 1\nZeile 2\n<<Zeile 1\nZeile 2\nAnnahme von Verbindungsanfragen beendet\nSocket zu 192.168.0.20:57292 geschlossen\nServer-Socket geschlossen\n"
|
||||
},
|
||||
"client": {
|
||||
"connect": "Verbinden",
|
||||
"sendEnabled": false,
|
||||
"address": {
|
||||
"text": "192.168.0.30",
|
||||
"editable": true
|
||||
},
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": true
|
||||
},
|
||||
"message": "noch da?",
|
||||
"output": "Verbindung hergestellt\n<<Hallo Echo\n>>Hallo Echo\n<<\n>>\n<<Zeile 1\nZeile 2\n>>Zeile 1\nZeile 2\nSocket zu 192.168.0.30:55555 getrennt\n"
|
||||
},
|
||||
"thrown": []
|
||||
},
|
||||
{
|
||||
"step": "client: Trennen",
|
||||
"server": {
|
||||
"button": "Starten",
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": true
|
||||
},
|
||||
"log": "Annahme von Verbindungsanfragen gestartet\nVerbindung zu 192.168.0.20:57292 hergestellt\n>>Hallo Echo\n<<Hallo Echo\n>>\n<<\n>>Zeile 1\nZeile 2\n<<Zeile 1\nZeile 2\nAnnahme von Verbindungsanfragen beendet\nSocket zu 192.168.0.20:57292 geschlossen\nServer-Socket geschlossen\n"
|
||||
},
|
||||
"client": {
|
||||
"connect": "Verbinden",
|
||||
"sendEnabled": false,
|
||||
"address": {
|
||||
"text": "192.168.0.30",
|
||||
"editable": true
|
||||
},
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": true
|
||||
},
|
||||
"message": "noch da?",
|
||||
"output": "Verbindung hergestellt\n<<Hallo Echo\n>>Hallo Echo\n<<\n>>\n<<Zeile 1\nZeile 2\n>>Zeile 1\nZeile 2\nSocket zu 192.168.0.30:55555 getrennt\n"
|
||||
},
|
||||
"thrown": []
|
||||
},
|
||||
{
|
||||
"step": "server: start again",
|
||||
"server": {
|
||||
"button": "Anhalten",
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": false
|
||||
},
|
||||
"log": "Annahme von Verbindungsanfragen gestartet\nVerbindung zu 192.168.0.20:57292 hergestellt\n>>Hallo Echo\n<<Hallo Echo\n>>\n<<\n>>Zeile 1\nZeile 2\n<<Zeile 1\nZeile 2\nAnnahme von Verbindungsanfragen beendet\nSocket zu 192.168.0.20:57292 geschlossen\nServer-Socket geschlossen\nAnnahme von Verbindungsanfragen gestartet\n"
|
||||
},
|
||||
"client": {
|
||||
"connect": "Verbinden",
|
||||
"sendEnabled": false,
|
||||
"address": {
|
||||
"text": "192.168.0.30",
|
||||
"editable": true
|
||||
},
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": true
|
||||
},
|
||||
"message": "noch da?",
|
||||
"output": "Verbindung hergestellt\n<<Hallo Echo\n>>Hallo Echo\n<<\n>>\n<<Zeile 1\nZeile 2\n>>Zeile 1\nZeile 2\nSocket zu 192.168.0.30:55555 getrennt\n"
|
||||
},
|
||||
"thrown": []
|
||||
},
|
||||
{
|
||||
"step": "client: connect again",
|
||||
"server": {
|
||||
"button": "Anhalten",
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": false
|
||||
},
|
||||
"log": "Annahme von Verbindungsanfragen gestartet\nVerbindung zu 192.168.0.20:57292 hergestellt\n>>Hallo Echo\n<<Hallo Echo\n>>\n<<\n>>Zeile 1\nZeile 2\n<<Zeile 1\nZeile 2\nAnnahme von Verbindungsanfragen beendet\nSocket zu 192.168.0.20:57292 geschlossen\nServer-Socket geschlossen\nAnnahme von Verbindungsanfragen gestartet\nVerbindung zu 192.168.0.20:55719 hergestellt\n"
|
||||
},
|
||||
"client": {
|
||||
"connect": "Trennen",
|
||||
"sendEnabled": true,
|
||||
"address": {
|
||||
"text": "192.168.0.30",
|
||||
"editable": false
|
||||
},
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": false
|
||||
},
|
||||
"message": "noch da?",
|
||||
"output": "Verbindung hergestellt\n<<Hallo Echo\n>>Hallo Echo\n<<\n>>\n<<Zeile 1\nZeile 2\n>>Zeile 1\nZeile 2\nSocket zu 192.168.0.30:55555 getrennt\nFehler beim Verbindungsaufbau\nZeitüberschreitung beim Verbindungsaufbau\nVerbindung hergestellt\n"
|
||||
},
|
||||
"thrown": []
|
||||
},
|
||||
{
|
||||
"step": "client: Trennen again",
|
||||
"server": {
|
||||
"button": "Anhalten",
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": false
|
||||
},
|
||||
"log": "Annahme von Verbindungsanfragen gestartet\nVerbindung zu 192.168.0.20:57292 hergestellt\n>>Hallo Echo\n<<Hallo Echo\n>>\n<<\n>>Zeile 1\nZeile 2\n<<Zeile 1\nZeile 2\nAnnahme von Verbindungsanfragen beendet\nSocket zu 192.168.0.20:57292 geschlossen\nServer-Socket geschlossen\nAnnahme von Verbindungsanfragen gestartet\nVerbindung zu 192.168.0.20:55719 hergestellt\nSocket zu 192.168.0.20:55719 geschlossen\n"
|
||||
},
|
||||
"client": {
|
||||
"connect": "Verbinden",
|
||||
"sendEnabled": false,
|
||||
"address": {
|
||||
"text": "192.168.0.30",
|
||||
"editable": true
|
||||
},
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": true
|
||||
},
|
||||
"message": "noch da?",
|
||||
"output": "Verbindung hergestellt\n<<Hallo Echo\n>>Hallo Echo\n<<\n>>\n<<Zeile 1\nZeile 2\n>>Zeile 1\nZeile 2\nSocket zu 192.168.0.30:55555 getrennt\nFehler beim Verbindungsaufbau\nZeitüberschreitung beim Verbindungsaufbau\nVerbindung hergestellt\nVerbindung getrennt\nSocket zu 192.168.0.30:55555 getrennt\n"
|
||||
},
|
||||
"thrown": []
|
||||
},
|
||||
{
|
||||
"step": "client: connect to a port nobody listens on",
|
||||
"server": {
|
||||
"button": "Anhalten",
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": false
|
||||
},
|
||||
"log": "Annahme von Verbindungsanfragen gestartet\nVerbindung zu 192.168.0.20:57292 hergestellt\n>>Hallo Echo\n<<Hallo Echo\n>>\n<<\n>>Zeile 1\nZeile 2\n<<Zeile 1\nZeile 2\nAnnahme von Verbindungsanfragen beendet\nSocket zu 192.168.0.20:57292 geschlossen\nServer-Socket geschlossen\nAnnahme von Verbindungsanfragen gestartet\nVerbindung zu 192.168.0.20:55719 hergestellt\nSocket zu 192.168.0.20:55719 geschlossen\n"
|
||||
},
|
||||
"client": {
|
||||
"connect": "Verbinden",
|
||||
"sendEnabled": false,
|
||||
"address": {
|
||||
"text": "192.168.0.30",
|
||||
"editable": true
|
||||
},
|
||||
"port": {
|
||||
"text": "4711",
|
||||
"editable": true
|
||||
},
|
||||
"message": "noch da?",
|
||||
"output": "Verbindung hergestellt\n<<Hallo Echo\n>>Hallo Echo\n<<\n>>\n<<Zeile 1\nZeile 2\n>>Zeile 1\nZeile 2\nSocket zu 192.168.0.30:55555 getrennt\nFehler beim Verbindungsaufbau\nZeitüberschreitung beim Verbindungsaufbau\nVerbindung hergestellt\nVerbindung getrennt\nSocket zu 192.168.0.30:55555 getrennt\nFehler beim Verbindungsaufbau\nZeitüberschreitung beim Verbindungsaufbau\n"
|
||||
},
|
||||
"thrown": []
|
||||
},
|
||||
{
|
||||
"step": "client: connect to a name nobody knows",
|
||||
"server": {
|
||||
"button": "Anhalten",
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": false
|
||||
},
|
||||
"log": "Annahme von Verbindungsanfragen gestartet\nVerbindung zu 192.168.0.20:57292 hergestellt\n>>Hallo Echo\n<<Hallo Echo\n>>\n<<\n>>Zeile 1\nZeile 2\n<<Zeile 1\nZeile 2\nAnnahme von Verbindungsanfragen beendet\nSocket zu 192.168.0.20:57292 geschlossen\nServer-Socket geschlossen\nAnnahme von Verbindungsanfragen gestartet\nVerbindung zu 192.168.0.20:55719 hergestellt\nSocket zu 192.168.0.20:55719 geschlossen\n"
|
||||
},
|
||||
"client": {
|
||||
"connect": "Verbinden",
|
||||
"sendEnabled": false,
|
||||
"address": {
|
||||
"text": "gibtsnicht",
|
||||
"editable": true
|
||||
},
|
||||
"port": {
|
||||
"text": "55555",
|
||||
"editable": true
|
||||
},
|
||||
"message": "noch da?",
|
||||
"output": "Verbindung hergestellt\n<<Hallo Echo\n>>Hallo Echo\n<<\n>>\n<<Zeile 1\nZeile 2\n>>Zeile 1\nZeile 2\nSocket zu 192.168.0.30:55555 getrennt\nFehler beim Verbindungsaufbau\nZeitüberschreitung beim Verbindungsaufbau\nVerbindung hergestellt\nVerbindung getrennt\nSocket zu 192.168.0.30:55555 getrennt\nFehler beim Verbindungsaufbau\nZeitüberschreitung beim Verbindungsaufbau\nFehler beim Verbindungsaufbau\nZiel konnte nicht bestimmt werden!\n"
|
||||
},
|
||||
"thrown": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
// The echo server's and the generic client's windows in the room, against
|
||||
// Java's (echoWindow.java.json, EchoWindowRun): Java's steps played as the
|
||||
// editor's panels play them — the start button with the port typed,
|
||||
// "Verbinden" with the address and port typed (or "Trennen" while
|
||||
// connected), "Senden" only while connected —, the buttons, fields and lines
|
||||
// both windows show after each; and what Java's project keeps of them after
|
||||
// a run (fixtures/after-run-echo.fls, SaveAfterRunRun).
|
||||
//
|
||||
// Named: a port that is no number is not handed to the room (Java's window
|
||||
// throws and does nothing; the panel marks the field); a connection attempt
|
||||
// lasts three SYNs (F2.3c), so the one to the stopped server is awaited
|
||||
// before it starts again. The client's own port is the threads' draw, in
|
||||
// Java as here: written as PORT.
|
||||
|
||||
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 type { EchoServerLogEntry, SimpleClientLogEntry } from '@mdg-filius/core';
|
||||
import { addDevice, connect, emptyProject, readProject, setHostAddress, setSoftware, writeProject } from '@mdg-filius/doc';
|
||||
import { openProjectArchive } from '@mdg-filius/fls';
|
||||
import { portOfText, type EchoServerView, type HostMessage, type SimpleClientView } from '@mdg-filius/protocol';
|
||||
import { clientText, serverText } from '../../core/test/programLines.js';
|
||||
import { SimulationRoom, TICK_MILLISECONDS } from '../src/index.js';
|
||||
|
||||
interface WindowField {
|
||||
readonly text: string;
|
||||
readonly editable: boolean;
|
||||
}
|
||||
|
||||
interface JavaStep {
|
||||
readonly step: string;
|
||||
readonly server: { readonly button: string; readonly port: WindowField; readonly log: string };
|
||||
readonly client: { readonly connect: string; readonly sendEnabled: boolean; readonly address: WindowField; readonly port: WindowField; readonly message: string; readonly output: string };
|
||||
readonly thrown: readonly string[];
|
||||
}
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const java = JSON.parse(readFileSync(join(here, 'echoWindow.java.json'), 'utf8')) as { readonly steps: readonly JavaStep[] };
|
||||
const corpus = join(here, '../../../corpus/java-examples');
|
||||
|
||||
/** The client's own port, drawn by the threads. */
|
||||
const ownPort = (text: string) => text.replace(/192\.168\.0\.20:\d+/g, '192.168.0.20:PORT');
|
||||
|
||||
function setUp() {
|
||||
const doc = new Y.Doc();
|
||||
writeProject(doc, emptyProject());
|
||||
let counter = 0;
|
||||
const newId = () => `port-${++counter}`;
|
||||
addDevice(doc, { kind: 'pc', id: 'client', name: 'Client', x: 0, y: 0, newId }, 'me');
|
||||
addDevice(doc, { kind: 'pc', id: 'server', name: 'Server', x: 200, y: 0, newId }, 'me');
|
||||
addDevice(doc, { kind: 'networkSwitch', id: 'switch', name: 'Switch', x: 100, y: 100, newId }, 'me');
|
||||
connect(doc, { node: 'client' }, { node: 'switch' }, 'cable-1', 'me');
|
||||
connect(doc, { node: 'server' }, { node: 'switch' }, 'cable-2', 'me');
|
||||
setHostAddress(doc, 'client', 'ipAddress', '192.168.0.20', 'me');
|
||||
setHostAddress(doc, 'server', 'ipAddress', '192.168.0.30', 'me');
|
||||
setSoftware(doc, 'server', { install: ['echoServer'], remove: [] }, 'de', 'me');
|
||||
setSoftware(doc, 'client', { install: ['simpleClient'], remove: [] }, 'de', 'me');
|
||||
let now = 0;
|
||||
const room = new SimulationRoom(doc, { now: () => now, seed: () => 3 });
|
||||
const views = new Map<string, unknown>();
|
||||
const lines = new Map<string, unknown[]>();
|
||||
const errors: string[] = [];
|
||||
const id = room.connect((message: HostMessage) => {
|
||||
if (message.kind === 'view') views.set(message.app, message.view);
|
||||
if (message.kind === 'app') lines.set(message.app, [...(lines.get(message.app) ?? []).slice(0, message.from), ...message.lines]);
|
||||
if (message.kind === 'error') errors.push(message.code);
|
||||
});
|
||||
const send = (fields: object) => room.receive(id, { v: 1, ...fields });
|
||||
send({ kind: 'sub', topics: ['app:server/echoServer', 'app:client/simpleClient'] });
|
||||
send({ kind: 'start' });
|
||||
const run = (milliseconds: number) => {
|
||||
for (let elapsed = 0; elapsed < milliseconds; elapsed += TICK_MILLISECONDS) {
|
||||
now += TICK_MILLISECONDS;
|
||||
room.tick();
|
||||
}
|
||||
};
|
||||
const act = (node: string, app: string, action: string, args: object = {}) => send({ kind: 'input', node, app, action, args, reqId: action });
|
||||
return { doc, room, views, lines, errors, run, act, send };
|
||||
}
|
||||
|
||||
describe("the echo server's and the generic client's windows, against Java's", () => {
|
||||
it("play Java's steps as the panels play them", () => {
|
||||
const { views, lines, run, act, errors } = setUp();
|
||||
run(TICK_MILLISECONDS);
|
||||
// What the panels hold: the fields as typed, the message.
|
||||
let serverPortText = '';
|
||||
let clientAddressText = '';
|
||||
let clientPortText = '';
|
||||
let message = '';
|
||||
const server = () => views.get('echoServer') as EchoServerView;
|
||||
const client = () => views.get('simpleClient') as SimpleClientView;
|
||||
const serverLog = () => ((lines.get('echoServer') ?? []) as EchoServerLogEntry[]).map((line) => `${serverText(line)}\n`).join('');
|
||||
const clientOutput = () => ((lines.get('simpleClient') ?? []) as SimpleClientLogEntry[]).map((line) => `${clientText(line)}\n`).join('');
|
||||
// The fields follow the program when it changes (Java's aktualisieren sets them from it).
|
||||
let seenServer = server();
|
||||
let seenClient = client();
|
||||
const follow = () => {
|
||||
if (server() !== seenServer) serverPortText = String(server().port);
|
||||
if (client() !== seenClient) {
|
||||
clientAddressText = client().address;
|
||||
clientPortText = String(client().port);
|
||||
}
|
||||
seenServer = server();
|
||||
seenClient = client();
|
||||
};
|
||||
serverPortText = String(server().port);
|
||||
clientAddressText = client().address;
|
||||
clientPortText = String(client().port);
|
||||
const shown = () => {
|
||||
follow();
|
||||
return {
|
||||
server: { button: server().active ? 'Anhalten' : 'Starten', port: { text: serverPortText, editable: !server().active }, log: ownPort(serverLog()) },
|
||||
client: {
|
||||
connect: client().connected ? 'Trennen' : 'Verbinden',
|
||||
sendEnabled: client().connected,
|
||||
address: { text: clientAddressText, editable: !client().connected },
|
||||
port: { text: clientPortText, editable: !client().connected },
|
||||
message,
|
||||
output: clientOutput(),
|
||||
},
|
||||
};
|
||||
};
|
||||
// The failed attempt, awaited (see below), is in the window one step before Java's shows it.
|
||||
const timedOut = `${clientText({ kind: 'connectFailed', failure: 'timedOut' })}\n`;
|
||||
const expected = (step: JavaStep) => ({
|
||||
server: { ...step.server, log: ownPort(step.server.log) },
|
||||
client: step.step === 'server: start again' ? { ...step.client, output: step.client.output + timedOut } : step.client,
|
||||
});
|
||||
// The panels' buttons.
|
||||
const startStop = () => {
|
||||
if (server().active) act('server', 'echoServer', 'setActive', { active: false });
|
||||
else if (portOfText(serverPortText) !== undefined) act('server', 'echoServer', 'setActive', { active: true, port: portOfText(serverPortText) });
|
||||
};
|
||||
const connectButton = () => {
|
||||
if (client().connected) act('client', 'simpleClient', 'disconnect');
|
||||
else if (portOfText(clientPortText) !== undefined) act('client', 'simpleClient', 'connect', { address: clientAddressText, port: portOfText(clientPortText) });
|
||||
};
|
||||
const sendButton = () => {
|
||||
if (!client().connected) return;
|
||||
act('client', 'simpleClient', 'send', { text: message });
|
||||
message = '';
|
||||
};
|
||||
const plays: Record<string, () => void> = {
|
||||
'server: start with port abc': () => ((serverPortText = 'abc'), startStop(), run(500)),
|
||||
'server: start with port 55555': () => ((serverPortText = '55555'), startStop(), run(1000)),
|
||||
'client: connect with port x': () => ((clientAddressText = '192.168.0.30'), (clientPortText = 'x'), connectButton(), run(500)),
|
||||
'client: connect to 192.168.0.30:55555': () => ((clientAddressText = '192.168.0.30'), (clientPortText = '55555'), connectButton(), run(6000)),
|
||||
'client: send Hallo Echo': () => ((message = 'Hallo Echo'), sendButton(), run(4000)),
|
||||
'client: send nothing': () => ((message = ''), sendButton(), run(4000)),
|
||||
'client: send two lines': () => ((message = 'Zeile 1\nZeile 2'), sendButton(), run(4000)),
|
||||
'server: stop while connected': () => (startStop(), run(4000)),
|
||||
'client: send after the server stopped': () => ((message = 'noch da?'), sendButton(), run(8000)),
|
||||
'client: Trennen': () => (connectButton(), run(3000)),
|
||||
'server: start again': () => (startStop(), run(1000)),
|
||||
'client: connect again': () => (connectButton(), run(6000)),
|
||||
'client: Trennen again': () => (connectButton(), run(3000)),
|
||||
// Three SYNs (F2.3c) last longer than Java's one: until the attempt failed.
|
||||
'client: connect to a port nobody listens on': () => {
|
||||
clientPortText = '4711';
|
||||
connectButton();
|
||||
for (let waited = 0; !clientOutput().endsWith(timedOut) && waited < 60_000; waited += 1000) run(1000);
|
||||
},
|
||||
'client: connect to a name nobody knows': () => ((clientAddressText = 'gibtsnicht'), (clientPortText = '55555'), connectButton(), run(12000)),
|
||||
};
|
||||
for (const step of java.steps) {
|
||||
if (step.step !== 'open') plays[step.step]!();
|
||||
expect(shown(), step.step).toEqual(expected(step));
|
||||
// The press on "Verbinden" with the server stopped: Java's one SYN times out before the server starts again,
|
||||
// the core's three (named in F2.3c) only later — awaited here, so that the server starts after it as in Java.
|
||||
if (step.step === 'client: Trennen') {
|
||||
for (let waited = 0; !clientOutput().endsWith(timedOut) && waited < 60_000; waited += 1000) run(1000);
|
||||
}
|
||||
// Java's window threw for a port that is no number; the panel hands nothing over.
|
||||
expect(step.thrown.length > 0, step.step).toBe(step.step.includes('port abc') || step.step.includes('port x'));
|
||||
}
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("leave in the project what Java's leaves after a run: the server's port and that it runs, the client's server", () => {
|
||||
let ids = 0;
|
||||
const project = openProjectArchive(new Uint8Array(readFileSync(join(corpus, 'email_komplett.fls'))), () => `00000000-0000-0000-0000-${(++ids).toString(16).padStart(12, '0')}`);
|
||||
const doc = new Y.Doc();
|
||||
writeProject(doc, project);
|
||||
const id = (name: string) => readProject(doc).graph.nodes.find((node) => node.displayName === name)!.id;
|
||||
setSoftware(doc, id('Rechner 2'), { install: ['echoServer'], remove: [] }, 'de', 'me');
|
||||
setSoftware(doc, id('Rechner 1'), { install: ['simpleClient'], remove: [] }, 'de', 'me');
|
||||
let now = 0;
|
||||
const room = new SimulationRoom(doc, { now: () => now, seed: () => 5 });
|
||||
const client = room.connect(() => {});
|
||||
const act = (node: string, app: string, action: string, args: object = {}) => room.receive(client, { v: 1, kind: 'input', node: id(node), app, action, args, reqId: action });
|
||||
const run = (milliseconds: number) => {
|
||||
for (let elapsed = 0; elapsed < milliseconds; elapsed += TICK_MILLISECONDS) {
|
||||
now += TICK_MILLISECONDS;
|
||||
room.tick();
|
||||
}
|
||||
};
|
||||
room.receive(client, { v: 1, kind: 'start' });
|
||||
run(1000);
|
||||
act('Rechner 2', 'echoServer', 'setActive', { active: true, port: 7777 });
|
||||
run(1000);
|
||||
const address = readProject(doc).deviceConfigurations.get(id('Rechner 2'))!.ipAddress;
|
||||
act('Rechner 1', 'simpleClient', 'connect', { address, port: 7777 });
|
||||
run(6000);
|
||||
act('Rechner 1', 'simpleClient', 'send', { text: 'Hallo' });
|
||||
run(4000);
|
||||
room.receive(client, { v: 1, kind: 'stop' });
|
||||
|
||||
let javaIds = 0;
|
||||
const saved = openProjectArchive(new Uint8Array(readFileSync(join(here, 'fixtures/after-run-echo.fls'))), () => `00000000-0000-0000-0000-${(++javaIds).toString(16).padStart(12, '0')}`);
|
||||
const ours = readProject(doc);
|
||||
for (const name of ['Rechner 1', 'Rechner 2']) {
|
||||
const of = (each: typeof ours) => {
|
||||
const node = each.graph.nodes.find((candidate) => candidate.displayName === name)!.id;
|
||||
return { echoServer: each.echoServerConfigurations.get(node), simpleClient: each.simpleClientConfigurations.get(node), active: [...(each.activePrograms.get(node) ?? [])].sort() };
|
||||
};
|
||||
expect(of(ours), name).toEqual(of(saved));
|
||||
}
|
||||
});
|
||||
|
||||
it('refuses what the panels never hand over: a port that is no port, an action the program has not', () => {
|
||||
const { run, act, errors, views } = setUp();
|
||||
run(TICK_MILLISECONDS);
|
||||
act('server', 'echoServer', 'setActive', { active: true, port: 70_000 });
|
||||
act('server', 'echoServer', 'setActive', { active: true, port: 1.5 });
|
||||
act('server', 'echoServer', 'setActive', { active: 'yes' });
|
||||
act('server', 'echoServer', 'setActive', { active: 0 });
|
||||
act('server', 'echoServer', 'restart');
|
||||
act('client', 'simpleClient', 'connect', { address: '192.168.0.30', port: -1 });
|
||||
act('client', 'simpleClient', 'connect', { address: 5, port: 80 });
|
||||
act('client', 'simpleClient', 'send', {});
|
||||
act('client', 'simpleClient', 'reset');
|
||||
act('client', 'echoServer', 'setActive', { active: false });
|
||||
act('server', 'simpleClient', 'disconnect');
|
||||
expect(errors).toEqual(['badArgs', 'badArgs', 'badArgs', 'badArgs', 'unknownAction', 'badArgs', 'badArgs', 'badArgs', 'unknownAction', 'unknownApp', 'unknownApp']);
|
||||
expect([portOfText('0'), portOfText('65535'), portOfText('000080'), portOfText('65536'), portOfText('+5'), portOfText(' 5'), portOfText('123456')]).toEqual([
|
||||
0,
|
||||
65_535,
|
||||
80,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
]);
|
||||
// What is handed over is in the view: the port.
|
||||
act('server', 'echoServer', 'setActive', { active: true, port: 7777 });
|
||||
run(TICK_MILLISECONDS);
|
||||
expect(views.get('echoServer')).toEqual({ active: true, port: 7777 });
|
||||
});
|
||||
});
|
||||
BIN
Binary file not shown.
@@ -458,7 +458,7 @@ describe("a simulation room's browser and web server windows", () => {
|
||||
[server, 'webServer', 'setVirtualHost', { row: 0, column: 1, value: 2 }, 'badArgs'],
|
||||
[server, 'webServer', 'restart', {}, 'unknownAction'],
|
||||
[notebook, 'webServer', 'setActive', { active: true }, 'unknownApp'],
|
||||
[server, 'echoServer', 'setActive', { active: true }, 'unknownAction'],
|
||||
[server, 'gnutella', 'join', {}, 'unknownAction'],
|
||||
];
|
||||
for (const [target, app, action, args, code] of cases) {
|
||||
send({ kind: 'input', node: target, app, action, args, reqId: `${app}.${action}.${JSON.stringify(args)}` });
|
||||
|
||||
Reference in New Issue
Block a user