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:
2026-10-03 20:04:33 +00:00
co-authored by Claude Opus 5.5
parent 3dc2713f81
commit e31da62ae0
24 changed files with 1481 additions and 18 deletions
+5
View File
File diff suppressed because one or more lines are too long
+11 -1
View File
@@ -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) {
+16
View File
@@ -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']);
});
});
+86
View File
@@ -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<void> };
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<void> {
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>>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();
});
@@ -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<DesktopProgram> = new Set(['commandPrompt', 'webBrowser', 'webServer', 'emailClient', 'emailServer', 'textEditor', 'fileExplorer', 'imageViewer']);
const WITH_WINDOW: ReadonlySet<DesktopProgram> = 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' ? <TextEditorPanel doc={doc} node={id} /> : null}
{shown === 'fileExplorer' ? <FileExplorerPanel doc={doc} node={id} /> : null}
{shown === 'imageViewer' ? <ImageViewerPanel doc={doc} node={id} /> : null}
{shown === 'echoServer' ? <EchoServerPanel node={id} simulation={simulation} /> : null}
{shown === 'simpleClient' ? <SimpleClientPanel node={id} simulation={simulation} /> : null}
</ProgramFrame>
)}
{info ? (
+132
View File
@@ -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<never>[] = [];
/** A program's lines as text, the newest in sight as Java's text area scrolls. */
function useLines<Entry>(simulation: Simulation, topic: `app:${string}`, line: (entry: Entry) => string) {
const lines = (simulation.apps.get(topic as never)?.lines as readonly LogLine<Entry>[] | undefined) ?? NO_LINES;
const text = lines.map((each) => `${line(each)}\n`).join('');
const element = useRef<HTMLPreElement>(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<ServerLogEntry>(simulation, topic, (entry) => serverLine(entry, t));
if (view === null || view === undefined) return <div className="web-server" />;
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 (
<div className="web-server echo-server">
<div className="web-server-bar">
<button type="button" onClick={press}>
{t(view.active ? 'echoServer.stop' : 'echoServer.start')}
</button>
<label>
{t('echoServer.port')}
<input
value={port}
inputMode="numeric"
readOnly={view.active}
aria-invalid={typed === undefined ? 'true' : undefined}
onChange={(event) => setPort(event.target.value)}
onKeyDown={(event) => event.key === 'Enter' && press()}
/>
</label>
</div>
<pre className="web-server-log" ref={element} aria-label={t('echoServer.log')} tabIndex={0}>
{text}
</pre>
</div>
);
}
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<SimpleClientLogEntry>(simulation, topic, (entry) => simpleClientLine(entry, t));
if (view === null || view === undefined) return <div className="web-server" />;
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 (
<div className="web-server simple-client">
<label className="simple-client-field">
<span>{t('simpleClient.address')}</span>
<input value={address} readOnly={view.connected} autoCapitalize="off" autoCorrect="off" spellCheck={false} onChange={(event) => setAddress(event.target.value)} />
</label>
<label className="simple-client-field">
<span>{t('simpleClient.port')}</span>
<input value={port} inputMode="numeric" readOnly={view.connected} aria-invalid={typed === undefined ? 'true' : undefined} onChange={(event) => setPort(event.target.value)} />
</label>
<div className="web-server-bar">
<button type="button" onClick={connect}>
{t(view.connected ? 'simpleClient.disconnect' : 'simpleClient.connect')}
</button>
</div>
<label className="simple-client-message">
<span>{t('simpleClient.message')}</span>
<textarea value={message} onChange={(event) => setMessage(event.target.value)} />
</label>
<div className="web-server-bar">
<button type="button" disabled={!view.connected} onClick={send}>
{t('simpleClient.send')}
</button>
</div>
<pre className="web-server-log" ref={element} aria-label={t('simpleClient.output')} tabIndex={0}>
{text}
</pre>
</div>
);
}
+57
View File
@@ -1026,3 +1026,60 @@
.image-viewer-picture img {
max-width: 100%;
}
/* The echo server and the generic client (GUIApplicationServerBausteinWindow,
* GUIApplicationClientBausteinWindow), on the web server's look. */
.echo-server input,
.simple-client input,
.simple-client textarea {
min-height: 32px;
padding: 0 6px;
border: 1px solid #a0a0a0;
background: #ffffff;
color: #000000;
font: 16px Dialog, sans-serif;
}
.echo-server input {
width: 8em;
}
.echo-server [aria-invalid='true'],
.simple-client [aria-invalid='true'] {
border-color: #ff0000;
color: #ff0000;
}
.echo-server input:read-only,
.simple-client input:read-only {
background: #eeeeee;
}
.simple-client-field,
.simple-client-message {
display: flex;
align-items: center;
gap: 6px;
flex: none;
}
.simple-client-field span {
flex: none;
width: 140px;
}
.simple-client-field input {
flex: 1;
min-width: 0;
}
.simple-client-message {
flex-direction: column;
align-items: stretch;
}
.simple-client-message textarea {
min-height: 3.5em;
padding: 4px 6px;
resize: vertical;
}
+25 -3
View File
@@ -1,8 +1,10 @@
// A server's lines as Java's server windows write them (ServerAnwendung,
// ServerMitarbeiter, TCPSocket's failures), in the person's language: one
// line each, as Java appends them to its window's text area.
// ServerMitarbeiter, TCPSocket's failures), and the generic client's as its
// window writes them (ClientBaustein, Socket, TCPSocket), in the person's
// language: one line each (a failed connection two), as Java appends them to
// its window's text area.
import type { ServerLogEntry } from '@mdg-filius/core';
import type { ServerLogEntry, SimpleClientLogEntry } from '@mdg-filius/core';
import type { Translate } from '../i18n/i18n.js';
export function serverLine(entry: ServerLogEntry, t: Translate): string {
@@ -23,3 +25,23 @@ export function serverLine(entry: ServerLogEntry, t: Translate): string {
return t(`server.sendFailed.${entry.failure}`);
}
}
export function simpleClientLine(entry: SimpleClientLogEntry, t: Translate): string {
switch (entry.kind) {
case 'connected':
case 'disconnected':
case 'notConnected':
case 'receiveFailed':
return t(`simpleClient.${entry.kind}`);
case 'connectFailed':
return `${t('simpleClient.notConnected')}\n${t(`simpleClient.failure.${entry.failure}`)}`;
case 'connectionClosed':
return t('simpleClient.connectionClosed', { address: `${entry.address}:${entry.port}` });
case 'received':
return `>>${entry.text}`;
case 'sent':
return `<<${entry.text}`;
case 'sendFailed':
return t(`server.sendFailed.${entry.failure}`);
}
}
+19
View File
@@ -59,6 +59,10 @@
"dhcp.static": "Statische Adresszuweisung",
"dhcp.subnetMask": "Netzmaske",
"dhcp.upperBound": "Adress-Obergrenze",
"echoServer.log": "Ausgabe",
"echoServer.port": "Port:",
"echoServer.start": "Starten",
"echoServer.stop": "Anhalten",
"emailClient.account": "Konto einrichten",
"emailClient.accountHint": "E-Mail-Konto verwalten",
"emailClient.accountInvalid": "Die Angaben sind entweder fehlerhaft oder unvollständig!",
@@ -293,6 +297,21 @@
"sheet.title": "Einstellungen",
"sheet.wireless": "Drahtlos (WLAN)",
"sim.failed": "Die Simulation ist auf einen Fehler gestoßen und wurde beendet.",
"simpleClient.address": "Server-Adresse:",
"simpleClient.connect": "Verbinden",
"simpleClient.connected": "Verbindung hergestellt",
"simpleClient.connectionClosed": "Socket zu {address} getrennt",
"simpleClient.disconnect": "Trennen",
"simpleClient.disconnected": "Verbindung getrennt",
"simpleClient.failure.failed": "Aktives Öffnen des Sockets fehlgeschlagen",
"simpleClient.failure.timedOut": "Zeitüberschreitung beim Verbindungsaufbau",
"simpleClient.failure.unresolved": "Ziel konnte nicht bestimmt werden!",
"simpleClient.message": "Nachricht:",
"simpleClient.notConnected": "Fehler beim Verbindungsaufbau",
"simpleClient.output": "Ausgabe",
"simpleClient.port": "Server-Port:",
"simpleClient.receiveFailed": "Keine Verbindung hergestellt!",
"simpleClient.send": "Senden",
"terminal.commands": "Befehle",
"terminal.ctrlC": "Strg+C",
"terminal.input": "Befehl eingeben",
+19
View File
@@ -59,6 +59,10 @@
"dhcp.static": "Static Address Assignment",
"dhcp.subnetMask": "Netmask",
"dhcp.upperBound": "Upper bound of address",
"echoServer.log": "Output",
"echoServer.port": "Port:",
"echoServer.start": "Start",
"echoServer.stop": "Stop",
"emailClient.account": "Account",
"emailClient.accountHint": "Manage email account",
"emailClient.accountInvalid": "Wrong or incomplete data!",
@@ -293,6 +297,21 @@
"sheet.title": "Settings",
"sheet.wireless": "Wireless (Wi-Fi)",
"sim.failed": "The simulation ran into an error and was stopped.",
"simpleClient.address": "Server address:",
"simpleClient.connect": "Connect",
"simpleClient.connected": "Connection established",
"simpleClient.connectionClosed": "Socket to {address} disconnected",
"simpleClient.disconnect": "Disconnect",
"simpleClient.disconnected": "Disconnected",
"simpleClient.failure.failed": "Active opening of socket failed.",
"simpleClient.failure.timedOut": "Timeout while connecting",
"simpleClient.failure.unresolved": "Target does not exist!",
"simpleClient.message": "Message:",
"simpleClient.notConnected": "Error while connecting",
"simpleClient.output": "Output",
"simpleClient.port": "Server port:",
"simpleClient.receiveFailed": "No connection established!",
"simpleClient.send": "Send",
"terminal.commands": "Commands",
"terminal.ctrlC": "Ctrl+C",
"terminal.input": "Enter a command",
+17
View File
@@ -45,6 +45,22 @@ const FROM_JAVA: Readonly<Record<string, string>> = {
'explorer.notImported': 'sw_fileexplorer_msg1',
'explorer.tooLarge': 'sw_fileexplorer_msg3',
'viewer.open': 'imageviewer_msg2',
'echoServer.port': 'serverbaustein_msg1',
'echoServer.stop': 'serverbaustein_msg2',
'echoServer.start': 'serverbaustein_msg3',
'simpleClient.address': 'clientbaustein_msg1',
'simpleClient.port': 'clientbaustein_msg2',
'simpleClient.connect': 'clientbaustein_msg3',
'simpleClient.message': 'clientbaustein_msg4',
'simpleClient.send': 'clientbaustein_msg5',
'simpleClient.disconnect': 'clientbaustein_msg6',
'simpleClient.connected': 'sw_clientbaustein_msg2',
'simpleClient.disconnected': 'sw_clientbaustein_msg3',
'simpleClient.notConnected': 'sw_clientbaustein_msg4',
'simpleClient.receiveFailed': 'sw_tcpsocket_msg9',
'simpleClient.failure.unresolved': 'sw_socket_msg1',
'simpleClient.failure.timedOut': 'sw_tcpsocket_msg2',
'simpleClient.failure.failed': 'sw_tcpsocket_msg3',
'textEditor.new': 'texteditor_msg3',
'textEditor.open': 'texteditor_msg4',
'textEditor.save': 'texteditor_msg5',
@@ -269,6 +285,7 @@ const FROM_JAVA_MENDED: Readonly<Record<string, readonly [string, (java: string)
/** Java's words around a value, as its programs join them („Verbindung zu“ + address + „hergestellt“). */
const FROM_JAVA_JOINED: Readonly<Record<string, readonly [string, string, string]>> = {
'server.connectionAccepted': ['sw_serveranwendung_msg4', '{address}', 'sw_serveranwendung_msg5'],
'simpleClient.connectionClosed': ['sw_clientbaustein_msg5', '{address}', 'sw_clientbaustein_msg6'],
'server.connectionClosed': ['sw_servermitarbeiter_msg1', '{address}', 'sw_servermitarbeiter_msg2'],
'emailServer.created': ['emailserver_msg22', '{name}', 'emailserver_msg23'],
'mail.stored': ['sw_smtpmitarbeiter_msg12', '{name}', 'sw_smtpmitarbeiter_msg13'],
+41 -2
View File
@@ -6,8 +6,8 @@ import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import type { ServerLogEntry } from '@mdg-filius/core';
import { serverLine } from '../src/desktop/serverLines.js';
import type { ServerLogEntry, SimpleClientLogEntry } from '@mdg-filius/core';
import { serverLine, simpleClientLine } from '../src/desktop/serverLines.js';
import { translate } from '../src/i18n/i18n.js';
const java = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../../sim/test/browserWindow.java.json'), 'utf8')) as { steps: { step: string; log?: string }[] };
@@ -39,3 +39,42 @@ describe("a server's lines", () => {
expect(serverLine({ kind: 'connectionAccepted', address: '1.2.3.4', port: 5 }, (key, values) => translate('en', key, values))).toBe('Connection to 1.2.3.4:5 established');
});
});
// The echo windows' lines against Java's (packages/sim/test/echoWindow.java.json, EchoWindowRun).
const echo = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../../sim/test/echoWindow.java.json'), 'utf8')) as {
steps: { step: string; server: { log: string }; client: { output: string } }[];
};
describe("the echo windows' lines", () => {
it("are Java's, line by line, the server's and the client's", () => {
const last = echo.steps.at(-1)!;
const [, port] = /Verbindung zu 192\.168\.0\.20:(\d+) hergestellt/.exec(last.server.log)!;
const server: ServerLogEntry[] = [
{ kind: 'acceptingStarted' },
{ kind: 'connectionAccepted', address: '192.168.0.20', port: Number(port) },
{ kind: 'received', text: 'Zeile 1\nZeile 2' },
{ kind: 'sent', text: 'Zeile 1\nZeile 2' },
{ kind: 'acceptingStopped' },
{ kind: 'connectionClosed', address: '192.168.0.20', port: Number(port) },
{ kind: 'serverSocketClosed' },
];
for (const entry of server) expect(last.server.log, entry.kind).toContain(`${serverLine(entry, t)}\n`);
const client: SimpleClientLogEntry[] = [
{ kind: 'connected' },
{ kind: 'sent', text: 'Hallo Echo' },
{ kind: 'received', text: 'Hallo Echo' },
{ kind: 'connectionClosed', address: '192.168.0.30', port: 55555 },
{ kind: 'connectFailed', failure: 'timedOut' },
{ kind: 'disconnected' },
{ kind: 'connectFailed', failure: 'unresolved' },
];
for (const entry of client) expect(last.client.output, entry.kind).toContain(`${simpleClientLine(entry, t)}\n`);
// Not in this story, in Java's words all the same (sw_clientbaustein_msg4, sw_tcpsocket_msg3, msg9, msg6).
expect([
simpleClientLine({ kind: 'notConnected' }, t),
simpleClientLine({ kind: 'connectFailed', failure: 'failed' }, t),
simpleClientLine({ kind: 'receiveFailed' }, t),
simpleClientLine({ kind: 'sendFailed', failure: 'notConnected' }, t),
]).toEqual(['Fehler beim Verbindungsaufbau', 'Fehler beim Verbindungsaufbau\nAktives Öffnen des Sockets fehlgeschlagen', 'Keine Verbindung hergestellt!', 'Verbindung nicht aufgebaut!']);
});
});
+25
View File
@@ -0,0 +1,25 @@
// What the echo server's and the generic client's windows show besides their
// lines, as the room keeps them and every client shows them (plan §7.3),
// after GUIApplicationServerBausteinWindow and
// GUIApplicationClientBausteinWindow (measured with tools/java-oracle's
// EchoWindowRun).
/** The echo server: its button (running or not) and its port, which can be typed into only while it does not run. */
export interface EchoServerView {
readonly active: boolean;
readonly port: number;
}
/** The generic client: "Verbinden" or "Trennen", and the server's address and port, which can be typed into only while it is not connected. */
export interface SimpleClientView {
readonly connected: boolean;
readonly address: string;
readonly port: number;
}
/** A port as Java's windows take it (Integer.parseInt), and as a socket can have it: a whole number from 0 to 65535; undefined for any other text. */
export function portOfText(text: string): number | undefined {
if (!/^\d+$/.test(text)) return undefined;
const port = Number(text);
return port <= 65_535 ? port : undefined;
}
+1
View File
@@ -2,5 +2,6 @@
export * from './host.js';
export * from './sim.js';
export * from './terminal.js';
export * from './echo.js';
export * from './mail.js';
export * from './web.js';
+4 -2
View File
@@ -75,8 +75,10 @@ export type HostMessage =
/**
* What a program's window shows besides its lines (plan §7.3), whole: on
* subscribing and when it changes; null while it has nothing (its program
* does not run, a browser that never ran). The browser's (BrowserView) and
* the web server's (WebServerView) so far.
* does not run, a browser that never ran): the browser's (BrowserView), the
* web server's (WebServerView), the e-mail program's and server's
* (EmailClientView, EmailServerView), the echo server's and the generic
* client's (EchoServerView, SimpleClientView).
*/
| { readonly v: 1; readonly kind: 'view'; readonly node: string; readonly app: AppKind; readonly view: unknown }
/**
+71 -3
View File
@@ -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[]) => {
+424
View File
@@ -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": []
}
]
}
+251
View File
@@ -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 });
});
});
Binary file not shown.
+1 -1
View File
@@ -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)}` });
+33
View File
@@ -1460,3 +1460,36 @@ Gemessen (03.10.2026):
* Der Bildbetrachter heißt nach dem Öffnen wie die Datei; jedes geöffnete
Bild legt Java über das vorige, eine Textdatei zeigt es als leeres Bild,
ohne Fund bleibt alles.
### Echo-Server und Einfacher Client (F4.6g1)
`EchoWindowRun` öffnet Javas `GUIApplicationServerBausteinWindow` und
`GUIApplicationClientBausteinWindow` auf je einem `GUIDesktopPanel` im Netz
von mail-lan (Server 192.168.0.30, Client 192.168.0.20) und benutzt sie wie
eine Person: Port tippen und starten (auch „abc“), Adresse und Port tippen
und „Verbinden“ (auch Port „x“), Nachrichten senden (auch eine leere und
eine mit zwei Zeilen), den Server anhalten, während der Client verbunden
ist, „Trennen“, ein Port ohne Server, ein unbekannter Name. Je Schritt:
Knöpfe, Felder (und ob sie beschreibbar sind), Zeilen beider Fenster und die
Ausnahmen, die Javas Knöpfe warfen (`packages/sim/test/echoWindow.java.json`).
`SaveAfterRunRun echo` speichert danach, was Java vom Echo im Projekt
behält (`packages/sim/test/fixtures/after-run-echo.fls`).
Gemessen (03.10.2026):
* Server: „Starten“/„Anhalten“, Port 55555, das Feld nur angehalten
beschreibbar; ein Port, der keine Zahl ist, wirft
(`NumberFormatException`) und nichts geschieht.
* Client: Adresse leer, Port 55555; „Verbinden“/„Trennen“; „Senden“ nur
verbunden, danach ist das Nachrichtenfeld leer; die Felder sind verbunden
gesperrt und zeigen nach jeder Zeile wieder die Werte des Programms.
* Eine leere Nachricht geht als ein leeres Segment mit PSH hinaus
(`erstelleSegmente` macht mindestens eines) und kommt als „>>“, „<<“
zurück; Java beendet eine Nachricht mit jedem PSH, auch dem eines
wiederholten Segments.
* Der angehaltene Server schließt die Verbindung („Socket zu … geschlossen“,
beim Client „Socket zu … getrennt“). Ein Port ohne Server endet in
„Zeitüberschreitung beim Verbindungsaufbau“, ein unbekannter Name in „Ziel
konnte nicht bestimmt werden!“.
* Nach dem Lauf speichert Java den Port des Servers, dass er läuft, und
Adresse und Port, die der Client zuletzt nahm.
+14 -3
View File
@@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then
fi
classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
mkdir -p "$here/.build"
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java" "$here/src/TerminalRun.java" "$here/src/FileCases.java" "$here/src/GnutellaRun.java" "$here/src/NewDeviceRun.java" "$here/src/SheetRun.java" "$here/src/TerminalWindowRun.java" "$here/src/InstallRun.java" "$here/src/BrowserWindowRun.java" "$here/src/MailWindowRun.java" "$here/src/SaveAfterRunRun.java" "$here/src/TextEditorWindowRun.java" "$here/src/FileExplorerWindowRun.java"
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"
# FILIUS writes its settings below $HOME; keep them out of the real one.
mkdir -p "$here/.build/home"
# Removed first: a crashed run must not leave the previous result standing.
@@ -391,8 +391,9 @@ test -s "$output"
# What a run leaves in FILIUS' project: an example started, a story played,
# stopped as the design mode stops it and saved (SaveAfterRunRun): the room's
# write-back into the document (packages/sim/test/writeBack.test.ts).
for run in email:email_komplett dhcp:einfaches_rechnernetz_komplett; do
# write-back into the document (packages/sim/test/writeBack.test.ts; echo:
# packages/sim/test/echoWindow.test.ts).
for run in email:email_komplett dhcp:einfaches_rechnernetz_komplett echo:email_komplett; do
story="${run%%:*}"
output="$root/packages/sim/test/fixtures/after-run-$story.fls"
rm -f "$output"
@@ -421,6 +422,16 @@ HOME="$here/.build/home" xvfb-run -a java -Duser.language=de -Duser.country=DE -
"$root/packages/core/test/scenarios/mail-lan.scenario.json" "$output" 2>&1 | grep -v 'logback\|^$\|^\s*at ' || true
test -s "$output"
# FILIUS' echo server and generic client windows used as a person does —
# the port typed (also one that is no number), "Verbinden", "Senden" (also
# nothing), the server stopped while connected, failures (EchoWindowRun): the
# room's echo windows (packages/sim/test/echoWindow.test.ts).
output="$root/packages/sim/test/echoWindow.java.json"
rm -f "$output"
HOME="$here/.build/home" xvfb-run -a java -Duser.language=de -Duser.country=DE -cp "$classpath:$here/.build" EchoWindowRun \
"$root/packages/core/test/scenarios/mail-lan.scenario.json" "$output" 2>&1 | grep -v 'logback\|^$\|^\s*at ' || true
test -s "$output"
# FILIUS' own examples as Java FILIUS runs them, opened and started as FILIUS
# does (ExampleRun): the acceptance of F2 (packages/fls/test/examplesJava.test.ts)
# and the classics of F3 (web, email, gnutella; packages/fls/test/classicsJava.test.ts).
+197
View File
@@ -0,0 +1,197 @@
// Uses FILIUS' own echo server and generic client windows
// (GUIApplicationServerBausteinWindow, GUIApplicationClientBausteinWindow on
// their GUIDesktopPanels) on mail-lan's network as a person does: the
// server's port field and start button, the client's address and port
// fields, "Verbinden"/"Trennen", the message field and "Senden" — a port
// that is no number too, an empty message, the server stopped while the
// client is connected, a port nobody listens on, a name nobody knows. After
// each step it writes what both windows show: the buttons' texts, the
// fields (and whether they can be typed into), the lines. The reference for
// the editor's echo windows (packages/sim/test/echoWindow.test.ts).
//
// Usage: java -cp filius.jar:lib/*:classes EchoWindowRun <scenario.json> <output json>
import filius.gui.anwendungssicht.GUIApplicationClientBausteinWindow;
import filius.gui.anwendungssicht.GUIApplicationServerBausteinWindow;
import filius.gui.anwendungssicht.GUIDesktopPanel;
import filius.software.system.Betriebssystem;
import java.awt.FlowLayout;
import java.io.FileOutputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
public class EchoWindowRun {
static final String CLIENT = "00000000-0000-0000-0000-000000000201";
static final String SERVER = "00000000-0000-0000-0000-000000000203";
static final List<Object> steps = new ArrayList<>();
static GUIApplicationServerBausteinWindow server;
static GUIApplicationClientBausteinWindow client;
/** Exceptions the windows' buttons threw on the event thread since the last step. */
static final List<String> thrown = new ArrayList<>();
static Object field(Class<?> owner, Object object, String name) throws Exception {
Field field = owner.getDeclaredField(name);
field.setAccessible(true);
return field.get(object);
}
static void onEdt(Runnable action) throws Exception {
SwingUtilities.invokeLater(() -> {
try {
action.run();
} catch (RuntimeException error) {
synchronized (thrown) {
thrown.add(error.getClass().getSimpleName() + ": " + error.getMessage());
}
}
});
Thread.sleep(400);
}
static Map<String, Object> textField(JTextField field) {
Map<String, Object> shown = new LinkedHashMap<>();
shown.put("text", field.getText());
shown.put("editable", field.isEditable());
return shown;
}
static void step(String name) throws Exception {
Class<?> s = GUIApplicationServerBausteinWindow.class;
Class<?> c = GUIApplicationClientBausteinWindow.class;
Map<String, Object> result = new LinkedHashMap<>();
result.put("step", name);
Map<String, Object> serverShown = new LinkedHashMap<>();
serverShown.put("button", ((JButton) field(s, server, "bStartStop")).getText());
serverShown.put("port", textField((JTextField) field(s, server, "tfPort")));
serverShown.put("log", ((JTextArea) field(s, server, "taLog")).getText());
result.put("server", serverShown);
Map<String, Object> clientShown = new LinkedHashMap<>();
clientShown.put("connect", ((JButton) field(c, client, "btVerbinden")).getText());
clientShown.put("sendEnabled", ((JButton) field(c, client, "btSenden")).isEnabled());
clientShown.put("address", textField((JTextField) field(c, client, "tfServerAdresse")));
clientShown.put("port", textField((JTextField) field(c, client, "tfServerPort")));
clientShown.put("message", ((JTextArea) field(c, client, "taSenden")).getText());
clientShown.put("output", ((JTextArea) field(c, client, "taAusgabe")).getText());
result.put("client", clientShown);
synchronized (thrown) {
result.put("thrown", new ArrayList<>(thrown));
thrown.clear();
}
steps.add(result);
}
static void type(Class<?> owner, Object window, String name, String text) throws Exception {
Object component = field(owner, window, name);
onEdt(() -> ((javax.swing.text.JTextComponent) component).setText(text));
}
static void press(Class<?> owner, Object window, String name, long wait) throws Exception {
JButton button = (JButton) field(owner, window, name);
onEdt(button::doClick);
Thread.sleep(wait);
}
public static void main(String[] args) throws Exception {
try {
run(args);
} catch (Throwable error) {
error.printStackTrace();
System.exit(1);
}
System.exit(0);
}
static void run(String[] args) throws Exception {
ScenarioNetwork network = ScenarioNetwork.read(args[0]);
Betriebssystem serverSystem = (Betriebssystem) network.nodes.get(SERVER).getSystemSoftware();
Betriebssystem clientSystem = (Betriebssystem) network.nodes.get(CLIENT).getSystemSoftware();
serverSystem.installApp("filius.software.clientserver.ServerBaustein", false);
clientSystem.installApp("filius.software.clientserver.ClientBaustein", false);
network.start();
Thread.sleep(500);
JFrame frame = new JFrame();
GUIDesktopPanel serverDesktop = new GUIDesktopPanel(serverSystem);
GUIDesktopPanel clientDesktop = new GUIDesktopPanel(clientSystem);
frame.getContentPane().setLayout(new FlowLayout());
frame.getContentPane().add(serverDesktop);
frame.getContentPane().add(clientDesktop);
server = (GUIApplicationServerBausteinWindow) serverDesktop.starteAnwendung("filius.software.clientserver.ServerBaustein");
client = (GUIApplicationClientBausteinWindow) clientDesktop.starteAnwendung("filius.software.clientserver.ClientBaustein");
SwingUtilities.invokeAndWait(() -> {
frame.pack();
frame.setVisible(true);
});
Thread.sleep(500);
Class<?> s = GUIApplicationServerBausteinWindow.class;
Class<?> c = GUIApplicationClientBausteinWindow.class;
step("open");
type(s, server, "tfPort", "abc");
press(s, server, "bStartStop", 500);
step("server: start with port abc");
type(s, server, "tfPort", "55555");
press(s, server, "bStartStop", 1000);
step("server: start with port 55555");
type(c, client, "tfServerAdresse", "192.168.0.30");
type(c, client, "tfServerPort", "x");
press(c, client, "btVerbinden", 500);
step("client: connect with port x");
type(c, client, "tfServerAdresse", "192.168.0.30");
type(c, client, "tfServerPort", "55555");
press(c, client, "btVerbinden", 6000);
step("client: connect to 192.168.0.30:55555");
type(c, client, "taSenden", "Hallo Echo");
press(c, client, "btSenden", 4000);
step("client: send Hallo Echo");
type(c, client, "taSenden", "");
press(c, client, "btSenden", 4000);
step("client: send nothing");
type(c, client, "taSenden", "Zeile 1\nZeile 2");
press(c, client, "btSenden", 4000);
step("client: send two lines");
press(s, server, "bStartStop", 4000);
step("server: stop while connected");
type(c, client, "taSenden", "noch da?");
press(c, client, "btSenden", 8000);
step("client: send after the server stopped");
press(c, client, "btVerbinden", 3000);
step("client: Trennen");
press(s, server, "bStartStop", 1000);
step("server: start again");
press(c, client, "btVerbinden", 6000);
step("client: connect again");
press(c, client, "btVerbinden", 3000);
step("client: Trennen again");
type(c, client, "tfServerPort", "4711");
press(c, client, "btVerbinden", 12000);
step("client: connect to a port nobody listens on");
type(c, client, "tfServerAdresse", "gibtsnicht");
type(c, client, "tfServerPort", "55555");
press(c, client, "btVerbinden", 12000);
step("client: connect to a name nobody knows");
Map<String, Object> output = new LinkedHashMap<>();
output.put("steps", steps);
try (FileOutputStream out = new FileOutputStream(args[1])) {
out.write((FlsOracle.Json.write(output, "") + "\n").getBytes(StandardCharsets.UTF_8));
}
network.stop();
System.out.println("echo window steps: " + steps.size());
}
}
@@ -18,11 +18,17 @@
// domain set to senior.org
// dhcp einfaches_rechnernetz_komplett: the notebooks get their
// addresses from the server (10 s)
// echo email_komplett, with FILIUS' echo server installed on Rechner 2
// and its client on Rechner 1 before the start: the server on port
// 7777 (as its window sets it), the client connected to it, a
// message sent, the client left connected
import filius.gui.GUIContainer;
import filius.gui.netzwerksicht.GUIKnotenItem;
import filius.hardware.knoten.Knoten;
import filius.rahmenprogramm.SzenarioVerwaltung;
import filius.software.clientserver.ClientBaustein;
import filius.software.clientserver.ServerBaustein;
import filius.software.email.Email;
import filius.software.email.EmailAnwendung;
import filius.software.email.EmailKonto;
@@ -93,6 +99,10 @@ public class SaveAfterRunRun {
if (story.equals("email")) {
((Betriebssystem) system("Rechner 1")).installAndStartAppIfAvailable("filius.software.lokal.Terminal");
}
if (story.equals("echo")) {
((Betriebssystem) system("Rechner 2")).installAndStartAppIfAvailable("filius.software.clientserver.ServerBaustein");
((Betriebssystem) system("Rechner 1")).installAndStartAppIfAvailable("filius.software.clientserver.ClientBaustein");
}
network.start();
Thread.sleep(2000);
switch (story) {
@@ -130,6 +140,22 @@ public class SaveAfterRunRun {
case "dhcp":
Thread.sleep(10000);
break;
case "echo": {
// GUIApplicationServerBausteinWindow's start button, then its client's "Verbinden" and "Senden".
ServerBaustein server = (ServerBaustein) system("Rechner 2").holeSoftware("filius.software.clientserver.ServerBaustein");
server.setPort(7777);
server.setAktiv(true);
server.setPort(7777);
Thread.sleep(1000);
ClientBaustein client = (ClientBaustein) system("Rechner 1").holeSoftware("filius.software.clientserver.ClientBaustein");
client.setZielIPAdresse(system("Rechner 2").primaryIPAdresse());
client.setZielPort(7777);
client.verbinden();
Thread.sleep(6000);
client.senden("Hallo");
Thread.sleep(4000);
break;
}
default:
throw new IllegalArgumentException("unknown story " + story);
}