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:
File diff suppressed because one or more lines are too long
@@ -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 {
|
private receiveTcpPayload(socketId: Id, segment: TcpSegment): void {
|
||||||
const session = this.stateValue.tcpSessionsById.get(socketId);
|
const session = this.stateValue.tcpSessionsById.get(socketId);
|
||||||
if (session === undefined) return;
|
if (session === undefined) return;
|
||||||
const nextRemoteSequence = nextTcpSequenceNumber(segment);
|
const nextRemoteSequence = nextTcpSequenceNumber(segment);
|
||||||
if (nextRemoteSequence > session.remoteSequenceNumber) {
|
if (nextRemoteSequence > session.remoteSequenceNumber || segment.payload.length === 0) {
|
||||||
session.remoteSequenceNumber = nextRemoteSequence;
|
session.remoteSequenceNumber = nextRemoteSequence;
|
||||||
session.receiveAssembly = concatBytes(session.receiveAssembly, segment.payload);
|
session.receiveAssembly = concatBytes(session.receiveAssembly, segment.payload);
|
||||||
if ((segment.flags & TCP_FLAG.push) !== 0) {
|
if ((segment.flags & TCP_FLAG.push) !== 0) {
|
||||||
|
|||||||
@@ -225,4 +225,20 @@ describe('the echo server and the generic client', () => {
|
|||||||
);
|
);
|
||||||
expect(received).toEqual(['zuletzt', 'notConnected']);
|
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']);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
// 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
|
// 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
|
// 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
|
// (F4.6f2), the file explorer and the image viewer (F4.6f3), the echo
|
||||||
// others follow. The browser's window takes its title from its page, the
|
// 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
|
// text editor's from its open file, the image viewer's from its picture, as
|
||||||
// Java's do.
|
// Java's do.
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@ import { InstallationPanel } from './InstallationPanel.js';
|
|||||||
import { TerminalPanel } from './TerminalPanel.js';
|
import { TerminalPanel } from './TerminalPanel.js';
|
||||||
import { TextEditorPanel, useTextEditorWindow } from './TextEditorPanel.js';
|
import { TextEditorPanel, useTextEditorWindow } from './TextEditorPanel.js';
|
||||||
import { FileExplorerPanel } from './FileExplorerPanel.js';
|
import { FileExplorerPanel } from './FileExplorerPanel.js';
|
||||||
|
import { EchoServerPanel, SimpleClientPanel } from './EchoPanels.js';
|
||||||
import { ImageViewerPanel, useImageViewer } from './ImageViewerPanel.js';
|
import { ImageViewerPanel, useImageViewer } from './ImageViewerPanel.js';
|
||||||
import { WebServerPanel } from './WebServerPanel.js';
|
import { WebServerPanel } from './WebServerPanel.js';
|
||||||
import './desktop.css';
|
import './desktop.css';
|
||||||
@@ -68,7 +69,7 @@ export interface DesktopWindowProps {
|
|||||||
export type DesktopWindowName = DesktopProgram | 'installation';
|
export type DesktopWindowName = DesktopProgram | 'installation';
|
||||||
|
|
||||||
/** The programs whose windows are built so far. */
|
/** The programs whose windows are built so far. */
|
||||||
const WITH_WINDOW: ReadonlySet<DesktopProgram> = new Set(['commandPrompt', 'webBrowser', 'webServer', 'emailClient', 'emailServer', 'textEditor', 'fileExplorer', 'imageViewer']);
|
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). */
|
/** The document's desktop of a computer, read anew after every change (a program installed, the label changed). */
|
||||||
function useDesktopView(doc: Y.Doc, id: string) {
|
function useDesktopView(doc: Y.Doc, id: string) {
|
||||||
@@ -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 === 'textEditor' ? <TextEditorPanel doc={doc} node={id} /> : null}
|
||||||
{shown === 'fileExplorer' ? <FileExplorerPanel doc={doc} node={id} /> : null}
|
{shown === 'fileExplorer' ? <FileExplorerPanel doc={doc} node={id} /> : null}
|
||||||
{shown === 'imageViewer' ? <ImageViewerPanel 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>
|
</ProgramFrame>
|
||||||
)}
|
)}
|
||||||
{info ? (
|
{info ? (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1026,3 +1026,60 @@
|
|||||||
.image-viewer-picture img {
|
.image-viewer-picture img {
|
||||||
max-width: 100%;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// A server's lines as Java's server windows write them (ServerAnwendung,
|
// A server's lines as Java's server windows write them (ServerAnwendung,
|
||||||
// ServerMitarbeiter, TCPSocket's failures), in the person's language: one
|
// ServerMitarbeiter, TCPSocket's failures), and the generic client's as its
|
||||||
// line each, as Java appends them to its window's text area.
|
// 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';
|
import type { Translate } from '../i18n/i18n.js';
|
||||||
|
|
||||||
export function serverLine(entry: ServerLogEntry, t: Translate): string {
|
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}`);
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -59,6 +59,10 @@
|
|||||||
"dhcp.static": "Statische Adresszuweisung",
|
"dhcp.static": "Statische Adresszuweisung",
|
||||||
"dhcp.subnetMask": "Netzmaske",
|
"dhcp.subnetMask": "Netzmaske",
|
||||||
"dhcp.upperBound": "Adress-Obergrenze",
|
"dhcp.upperBound": "Adress-Obergrenze",
|
||||||
|
"echoServer.log": "Ausgabe",
|
||||||
|
"echoServer.port": "Port:",
|
||||||
|
"echoServer.start": "Starten",
|
||||||
|
"echoServer.stop": "Anhalten",
|
||||||
"emailClient.account": "Konto einrichten",
|
"emailClient.account": "Konto einrichten",
|
||||||
"emailClient.accountHint": "E-Mail-Konto verwalten",
|
"emailClient.accountHint": "E-Mail-Konto verwalten",
|
||||||
"emailClient.accountInvalid": "Die Angaben sind entweder fehlerhaft oder unvollständig!",
|
"emailClient.accountInvalid": "Die Angaben sind entweder fehlerhaft oder unvollständig!",
|
||||||
@@ -293,6 +297,21 @@
|
|||||||
"sheet.title": "Einstellungen",
|
"sheet.title": "Einstellungen",
|
||||||
"sheet.wireless": "Drahtlos (WLAN)",
|
"sheet.wireless": "Drahtlos (WLAN)",
|
||||||
"sim.failed": "Die Simulation ist auf einen Fehler gestoßen und wurde beendet.",
|
"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.commands": "Befehle",
|
||||||
"terminal.ctrlC": "Strg+C",
|
"terminal.ctrlC": "Strg+C",
|
||||||
"terminal.input": "Befehl eingeben",
|
"terminal.input": "Befehl eingeben",
|
||||||
|
|||||||
@@ -59,6 +59,10 @@
|
|||||||
"dhcp.static": "Static Address Assignment",
|
"dhcp.static": "Static Address Assignment",
|
||||||
"dhcp.subnetMask": "Netmask",
|
"dhcp.subnetMask": "Netmask",
|
||||||
"dhcp.upperBound": "Upper bound of address",
|
"dhcp.upperBound": "Upper bound of address",
|
||||||
|
"echoServer.log": "Output",
|
||||||
|
"echoServer.port": "Port:",
|
||||||
|
"echoServer.start": "Start",
|
||||||
|
"echoServer.stop": "Stop",
|
||||||
"emailClient.account": "Account",
|
"emailClient.account": "Account",
|
||||||
"emailClient.accountHint": "Manage email account",
|
"emailClient.accountHint": "Manage email account",
|
||||||
"emailClient.accountInvalid": "Wrong or incomplete data!",
|
"emailClient.accountInvalid": "Wrong or incomplete data!",
|
||||||
@@ -293,6 +297,21 @@
|
|||||||
"sheet.title": "Settings",
|
"sheet.title": "Settings",
|
||||||
"sheet.wireless": "Wireless (Wi-Fi)",
|
"sheet.wireless": "Wireless (Wi-Fi)",
|
||||||
"sim.failed": "The simulation ran into an error and was stopped.",
|
"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.commands": "Commands",
|
||||||
"terminal.ctrlC": "Ctrl+C",
|
"terminal.ctrlC": "Ctrl+C",
|
||||||
"terminal.input": "Enter a command",
|
"terminal.input": "Enter a command",
|
||||||
|
|||||||
@@ -45,6 +45,22 @@ const FROM_JAVA: Readonly<Record<string, string>> = {
|
|||||||
'explorer.notImported': 'sw_fileexplorer_msg1',
|
'explorer.notImported': 'sw_fileexplorer_msg1',
|
||||||
'explorer.tooLarge': 'sw_fileexplorer_msg3',
|
'explorer.tooLarge': 'sw_fileexplorer_msg3',
|
||||||
'viewer.open': 'imageviewer_msg2',
|
'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.new': 'texteditor_msg3',
|
||||||
'textEditor.open': 'texteditor_msg4',
|
'textEditor.open': 'texteditor_msg4',
|
||||||
'textEditor.save': 'texteditor_msg5',
|
'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“). */
|
/** 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]>> = {
|
const FROM_JAVA_JOINED: Readonly<Record<string, readonly [string, string, string]>> = {
|
||||||
'server.connectionAccepted': ['sw_serveranwendung_msg4', '{address}', 'sw_serveranwendung_msg5'],
|
'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'],
|
'server.connectionClosed': ['sw_servermitarbeiter_msg1', '{address}', 'sw_servermitarbeiter_msg2'],
|
||||||
'emailServer.created': ['emailserver_msg22', '{name}', 'emailserver_msg23'],
|
'emailServer.created': ['emailserver_msg22', '{name}', 'emailserver_msg23'],
|
||||||
'mail.stored': ['sw_smtpmitarbeiter_msg12', '{name}', 'sw_smtpmitarbeiter_msg13'],
|
'mail.stored': ['sw_smtpmitarbeiter_msg12', '{name}', 'sw_smtpmitarbeiter_msg13'],
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { readFileSync } from 'node:fs';
|
|||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import type { ServerLogEntry } from '@mdg-filius/core';
|
import type { ServerLogEntry, SimpleClientLogEntry } from '@mdg-filius/core';
|
||||||
import { serverLine } from '../src/desktop/serverLines.js';
|
import { serverLine, simpleClientLine } from '../src/desktop/serverLines.js';
|
||||||
import { translate } from '../src/i18n/i18n.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 }[] };
|
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');
|
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!']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -2,5 +2,6 @@
|
|||||||
export * from './host.js';
|
export * from './host.js';
|
||||||
export * from './sim.js';
|
export * from './sim.js';
|
||||||
export * from './terminal.js';
|
export * from './terminal.js';
|
||||||
|
export * from './echo.js';
|
||||||
export * from './mail.js';
|
export * from './mail.js';
|
||||||
export * from './web.js';
|
export * from './web.js';
|
||||||
|
|||||||
@@ -75,8 +75,10 @@ export type HostMessage =
|
|||||||
/**
|
/**
|
||||||
* What a program's window shows besides its lines (plan §7.3), whole: on
|
* 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
|
* 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
|
* does not run, a browser that never ran): the browser's (BrowserView), the
|
||||||
* the web server's (WebServerView) so far.
|
* 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 }
|
| { readonly v: 1; readonly kind: 'view'; readonly node: string; readonly app: AppKind; readonly view: unknown }
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -20,8 +20,9 @@
|
|||||||
//
|
//
|
||||||
// A program's window may show more than its lines (plan §7.3): the browser's
|
// 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
|
// 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
|
// folders, the e-mail server's domain and accounts, the echo server's button
|
||||||
// view, whole, when they change.
|
// 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
|
// While the simulation runs, the document's programs are watched: a program
|
||||||
// installed or removed on the desktop (Java's software installation) is
|
// installed or removed on the desktop (Java's software installation) is
|
||||||
@@ -70,6 +71,8 @@ import {
|
|||||||
type Topic,
|
type Topic,
|
||||||
type EmailClientView,
|
type EmailClientView,
|
||||||
type EmailServerView,
|
type EmailServerView,
|
||||||
|
type EchoServerView,
|
||||||
|
type SimpleClientView,
|
||||||
type TraceFrame,
|
type TraceFrame,
|
||||||
type WebServerView,
|
type WebServerView,
|
||||||
} from '@mdg-filius/protocol';
|
} from '@mdg-filius/protocol';
|
||||||
@@ -237,7 +240,12 @@ export class SimulationRoom {
|
|||||||
settings.emailServer = emailServerAfterRun(configured as never, mailServerSettings);
|
settings.emailServer = emailServerAfterRun(configured as never, mailServerSettings);
|
||||||
}
|
}
|
||||||
const echo = this.engine.echoServer(id);
|
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.emailClient = snapshot.emailClientsByNodeId.get(id);
|
||||||
settings.firewall = snapshot.firewallConfigurationsByNodeId.get(id);
|
settings.firewall = snapshot.firewallConfigurationsByNodeId.get(id);
|
||||||
if (snapshot.dhcpClientConfigurationsByNodeId.get(id)?.isEnabled === true) settings.device = snapshot.deviceConfigurations.get(id);
|
if (snapshot.dhcpClientConfigurationsByNodeId.get(id)?.isEnabled === true) settings.device = snapshot.deviceConfigurations.get(id);
|
||||||
@@ -442,6 +450,49 @@ export class SimulationRoom {
|
|||||||
return this.actInEmailClient(input);
|
return this.actInEmailClient(input);
|
||||||
case 'emailServer':
|
case 'emailServer':
|
||||||
return this.actInEmailServer(input);
|
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:
|
default:
|
||||||
return 'unknownAction';
|
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) };
|
const view: EmailClientView = { account: client.account, inbox: client.inbox.map((entry) => entry.mail), sent: client.sent.map((entry) => entry.mail) };
|
||||||
return view;
|
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': {
|
case 'emailServer': {
|
||||||
const server = this.engine.emailServer(node);
|
const server = this.engine.emailServer(node);
|
||||||
if (server === undefined) return null;
|
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. */
|
/** A card's frames as the capture window shows them, both views. */
|
||||||
export function traceFramesOf(frames: readonly CapturedFrame[]): TraceFrame[] {
|
export function traceFramesOf(frames: readonly CapturedFrame[]): TraceFrame[] {
|
||||||
const byNumber = (rows: readonly CaptureRow[]) => {
|
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', 'setVirtualHost', { row: 0, column: 1, value: 2 }, 'badArgs'],
|
||||||
[server, 'webServer', 'restart', {}, 'unknownAction'],
|
[server, 'webServer', 'restart', {}, 'unknownAction'],
|
||||||
[notebook, 'webServer', 'setActive', { active: true }, 'unknownApp'],
|
[notebook, 'webServer', 'setActive', { active: true }, 'unknownApp'],
|
||||||
[server, 'echoServer', 'setActive', { active: true }, 'unknownAction'],
|
[server, 'gnutella', 'join', {}, 'unknownAction'],
|
||||||
];
|
];
|
||||||
for (const [target, app, action, args, code] of cases) {
|
for (const [target, app, action, args, code] of cases) {
|
||||||
send({ kind: 'input', node: target, app, action, args, reqId: `${app}.${action}.${JSON.stringify(args)}` });
|
send({ kind: 'input', node: target, app, action, args, reqId: `${app}.${action}.${JSON.stringify(args)}` });
|
||||||
|
|||||||
@@ -1460,3 +1460,36 @@ Gemessen (03.10.2026):
|
|||||||
* Der Bildbetrachter heißt nach dem Öffnen wie die Datei; jedes geöffnete
|
* 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,
|
Bild legt Java über das vorige, eine Textdatei zeigt es als leeres Bild,
|
||||||
ohne Fund bleibt alles.
|
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.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then
|
|||||||
fi
|
fi
|
||||||
classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
|
classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
|
||||||
mkdir -p "$here/.build"
|
mkdir -p "$here/.build"
|
||||||
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java" "$here/src/TerminalRun.java" "$here/src/FileCases.java" "$here/src/GnutellaRun.java" "$here/src/NewDeviceRun.java" "$here/src/SheetRun.java" "$here/src/TerminalWindowRun.java" "$here/src/InstallRun.java" "$here/src/BrowserWindowRun.java" "$here/src/MailWindowRun.java" "$here/src/SaveAfterRunRun.java" "$here/src/TextEditorWindowRun.java" "$here/src/FileExplorerWindowRun.java"
|
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.
|
# FILIUS writes its settings below $HOME; keep them out of the real one.
|
||||||
mkdir -p "$here/.build/home"
|
mkdir -p "$here/.build/home"
|
||||||
# Removed first: a crashed run must not leave the previous result standing.
|
# Removed first: a crashed run must not leave the previous result standing.
|
||||||
@@ -391,8 +391,9 @@ test -s "$output"
|
|||||||
|
|
||||||
# What a run leaves in FILIUS' project: an example started, a story played,
|
# 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
|
# stopped as the design mode stops it and saved (SaveAfterRunRun): the room's
|
||||||
# write-back into the document (packages/sim/test/writeBack.test.ts).
|
# write-back into the document (packages/sim/test/writeBack.test.ts; echo:
|
||||||
for run in email:email_komplett dhcp:einfaches_rechnernetz_komplett; do
|
# packages/sim/test/echoWindow.test.ts).
|
||||||
|
for run in email:email_komplett dhcp:einfaches_rechnernetz_komplett echo:email_komplett; do
|
||||||
story="${run%%:*}"
|
story="${run%%:*}"
|
||||||
output="$root/packages/sim/test/fixtures/after-run-$story.fls"
|
output="$root/packages/sim/test/fixtures/after-run-$story.fls"
|
||||||
rm -f "$output"
|
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
|
"$root/packages/core/test/scenarios/mail-lan.scenario.json" "$output" 2>&1 | grep -v 'logback\|^$\|^\s*at ' || true
|
||||||
test -s "$output"
|
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
|
# FILIUS' own examples as Java FILIUS runs them, opened and started as FILIUS
|
||||||
# does (ExampleRun): the acceptance of F2 (packages/fls/test/examplesJava.test.ts)
|
# does (ExampleRun): the acceptance of F2 (packages/fls/test/examplesJava.test.ts)
|
||||||
# and the classics of F3 (web, email, gnutella; packages/fls/test/classicsJava.test.ts).
|
# and the classics of F3 (web, email, gnutella; packages/fls/test/classicsJava.test.ts).
|
||||||
|
|||||||
@@ -0,0 +1,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
|
// domain set to senior.org
|
||||||
// dhcp einfaches_rechnernetz_komplett: the notebooks get their
|
// dhcp einfaches_rechnernetz_komplett: the notebooks get their
|
||||||
// addresses from the server (10 s)
|
// 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.GUIContainer;
|
||||||
import filius.gui.netzwerksicht.GUIKnotenItem;
|
import filius.gui.netzwerksicht.GUIKnotenItem;
|
||||||
import filius.hardware.knoten.Knoten;
|
import filius.hardware.knoten.Knoten;
|
||||||
import filius.rahmenprogramm.SzenarioVerwaltung;
|
import filius.rahmenprogramm.SzenarioVerwaltung;
|
||||||
|
import filius.software.clientserver.ClientBaustein;
|
||||||
|
import filius.software.clientserver.ServerBaustein;
|
||||||
import filius.software.email.Email;
|
import filius.software.email.Email;
|
||||||
import filius.software.email.EmailAnwendung;
|
import filius.software.email.EmailAnwendung;
|
||||||
import filius.software.email.EmailKonto;
|
import filius.software.email.EmailKonto;
|
||||||
@@ -93,6 +99,10 @@ public class SaveAfterRunRun {
|
|||||||
if (story.equals("email")) {
|
if (story.equals("email")) {
|
||||||
((Betriebssystem) system("Rechner 1")).installAndStartAppIfAvailable("filius.software.lokal.Terminal");
|
((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();
|
network.start();
|
||||||
Thread.sleep(2000);
|
Thread.sleep(2000);
|
||||||
switch (story) {
|
switch (story) {
|
||||||
@@ -130,6 +140,22 @@ public class SaveAfterRunRun {
|
|||||||
case "dhcp":
|
case "dhcp":
|
||||||
Thread.sleep(10000);
|
Thread.sleep(10000);
|
||||||
break;
|
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:
|
default:
|
||||||
throw new IllegalArgumentException("unknown story " + story);
|
throw new IllegalArgumentException("unknown story " + story);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user