feat(core): the command line with its local commands after Java, measured against Java
The terminal splits a line as Java's window does and runs ls, cd, pwd, mkdir, touch, cp, mv, rm, cat, test, help, ipconfig and route on the node's files, which are the project's own. Its texts are generated from the bundle Java loads. TerminalCases feeds Java's Terminal without a network (16 cases, de and en); eight cases name Java bugs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
// FILIUS' command line (filius.software.lokal.Terminal, with the input
|
||||
// handling of GUIApplicationTerminalWindow): a line's first word is the
|
||||
// command, the others its parameters; the program tells its window texts,
|
||||
// in the project's language, as Java writes them. The files are the node's
|
||||
// (the snapshot's file system, the same the project keeps).
|
||||
//
|
||||
// Departures from Java (terminal.test.ts names each):
|
||||
// - a text file's size is its length in characters (Java decodes the text as
|
||||
// Base64 and counts the bytes: "Hallo Welt" is 6);
|
||||
// - a folder that is not there is "not there" for ls (Java: "empty"), and
|
||||
// ls / names the root (Java leaves the path out);
|
||||
// - cd to a file is "not there" (Java makes the file the working directory);
|
||||
// - touch and mkdir in a folder that is not there fail (Java says the file
|
||||
// was made, or the folder is there already); cp onto a folder's name
|
||||
// fails (Java throws);
|
||||
// - ".." at the root stays there (Java finds nothing, or throws);
|
||||
// - rm and mv take an absolute path as absolute (Java puts the working
|
||||
// directory in front of the path it removes); the root cannot be removed (Java removes the working
|
||||
// directory and says it removed the file); after the working directory
|
||||
// is removed, the nearest folder above it that is left is the working
|
||||
// directory (Java goes on in the removed one);
|
||||
// - mv onto the file itself, also in another case, renames it (Java copies
|
||||
// the file onto itself and removes it).
|
||||
|
||||
import type { Id } from '../../ids.js';
|
||||
import type { VirtualFileContent, VirtualFileSystem } from '../../model/virtualFileSystem.js';
|
||||
import { entryName, parentPath } from '../../model/virtualFileSystem.js';
|
||||
import type { NetworkRuntimeEngine } from '../engine.js';
|
||||
import { javaTrim } from '../http.js';
|
||||
import type { Steps } from '../threads.js';
|
||||
import { javaBase64 } from '../../util/javaBase64.js';
|
||||
import { CommandThread, ProgramLog } from './program.js';
|
||||
import type { PageLanguage } from './routerPages.js';
|
||||
import { TERMINAL_TEXTS, type TerminalTextKey } from './terminalTexts.js';
|
||||
|
||||
/** What the command line tells its window: a text, or whether a command runs on (Java's Boolean). */
|
||||
export type TerminalLogEntry = { readonly kind: 'text'; readonly text: string } | { readonly kind: 'busy'; readonly busy: boolean };
|
||||
|
||||
/** What the window does itself with a line (GUIApplicationTerminalWindow). */
|
||||
export type TerminalWindowAction = 'blank' | 'exit' | 'clear';
|
||||
|
||||
/** GUIApplicationTerminalWindow.MENU_LINE. */
|
||||
export const TERMINAL_MENU_LINE = '==========================================================================\n';
|
||||
|
||||
/** The usage texts' keys by command (Terminal.initUsageInfo). */
|
||||
const USAGE: Record<string, string> = {
|
||||
arp: 'arp',
|
||||
arpsend: 'arpsend',
|
||||
cat: 'cat',
|
||||
cd: 'cd',
|
||||
copy: 'copy',
|
||||
cp: 'copy',
|
||||
del: 'del',
|
||||
dir: 'dir',
|
||||
help: 'help',
|
||||
host: 'host',
|
||||
ipconfig: 'ipconfig',
|
||||
ls: 'dir',
|
||||
mkdir: 'mkdir',
|
||||
move: 'move',
|
||||
mv: 'move',
|
||||
netstat: 'netstat',
|
||||
nslookup: 'nslookup',
|
||||
ping: 'ping',
|
||||
pwd: 'pwd',
|
||||
rm: 'del',
|
||||
route: 'route',
|
||||
tcpdump: 'tcpdump',
|
||||
touch: 'touch',
|
||||
traceroute: 'traceroute',
|
||||
type: 'cat',
|
||||
};
|
||||
|
||||
/**
|
||||
* Dateisystem.evaluatePathString for an absolute path: "." and empty parts
|
||||
* left out, ".." one up (at the root it stays there).
|
||||
*/
|
||||
function evaluatedPath(path: string): string {
|
||||
const parts: string[] = [];
|
||||
for (const part of path.split('/')) {
|
||||
if (part === '..') parts.pop();
|
||||
else if (part !== '.' && part !== '') parts.push(part);
|
||||
}
|
||||
return `/${parts.join('/')}`;
|
||||
}
|
||||
|
||||
/** A path's last part (Dateisystem.getBasename). */
|
||||
const basenameOf = (path: string) => path.slice(path.lastIndexOf('/') + 1);
|
||||
|
||||
export class Terminal {
|
||||
readonly log: ProgramLog<TerminalLogEntry>;
|
||||
private directoryValue = '/';
|
||||
/** The program's thread (Anwendung.ausfuehren): one command after the other. */
|
||||
private readonly commands: CommandThread;
|
||||
|
||||
constructor(
|
||||
private readonly engine: NetworkRuntimeEngine,
|
||||
private readonly nodeId: Id,
|
||||
private readonly files: VirtualFileSystem,
|
||||
) {
|
||||
this.log = new ProgramLog(engine);
|
||||
this.commands = new CommandThread(engine);
|
||||
}
|
||||
|
||||
private get language(): PageLanguage {
|
||||
return this.engine.state.topologySnapshot.language;
|
||||
}
|
||||
|
||||
private text(key: TerminalTextKey): string {
|
||||
return TERMINAL_TEXTS[this.language][key];
|
||||
}
|
||||
|
||||
/** The working directory, as Dateisystem.absoluterPfad writes it. */
|
||||
get directory(): string {
|
||||
return this.directoryValue;
|
||||
}
|
||||
|
||||
/** The window's prompt. */
|
||||
get prompt(): string {
|
||||
return `${this.directoryValue}> `;
|
||||
}
|
||||
|
||||
/** GUIApplicationTerminalWindow.showStartScreen. */
|
||||
get startScreen(): string {
|
||||
return `${this.text('sw_terminal_msg57')}\n${TERMINAL_MENU_LINE}${this.text('sw_terminal_msg26')}\n${TERMINAL_MENU_LINE}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* A line typed into the window: blank, exit, cls and reset are the
|
||||
* window's; any other line is the program's command, run by its thread.
|
||||
*/
|
||||
enter(line: string): TerminalWindowAction | undefined {
|
||||
const words = line.split(' ').filter((word) => word !== '');
|
||||
if (javaTrim(line) === '' || words.length === 0) return 'blank';
|
||||
const [command, ...parameters] = words as [string, ...string[]];
|
||||
if (command === 'exit') return 'exit';
|
||||
if (command === 'reset' || command === 'cls') return 'clear';
|
||||
this.commands.hand(() => this.run(command, parameters));
|
||||
this.engine.resumeThreads();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private tell(text: string): void {
|
||||
this.log.write({ kind: 'text', text });
|
||||
}
|
||||
|
||||
/** Terminal.terminalEingabeAuswerten: the method of the command's name, if Java has one. */
|
||||
private *run(command: string, parameters: readonly string[]): Steps {
|
||||
const local: Record<string, (parameters: readonly string[]) => void> = {
|
||||
cat: (each) => this.cat(each),
|
||||
cd: (each) => this.cd(each),
|
||||
copy: (each) => this.copy(each, 'cp'),
|
||||
cp: (each) => this.copy(each, 'cp'),
|
||||
del: (each) => this.remove(each),
|
||||
dir: (each) => this.list(each),
|
||||
help: (each) => this.usage(each[0] ?? ''),
|
||||
ipconfig: (each) => this.ipconfig(each),
|
||||
ls: (each) => this.list(each),
|
||||
mkdir: (each) => this.makeDirectory(each),
|
||||
move: (each) => this.move(each),
|
||||
mv: (each) => this.move(each),
|
||||
pwd: (each) => (each.length === 0 ? this.tell(this.directoryValue) : this.usage('pwd')),
|
||||
rm: (each) => this.remove(each),
|
||||
route: (each) => this.route(each),
|
||||
test: () => this.test(),
|
||||
touch: (each) => this.touch(each),
|
||||
type: (each) => this.cat(each),
|
||||
};
|
||||
const handler = Object.hasOwn(local, command) ? local[command] : undefined;
|
||||
if (handler === undefined) {
|
||||
this.tell(`${this.text('terminal_msg2')}\n${this.text('terminal_msg3')}`);
|
||||
return;
|
||||
}
|
||||
handler(parameters);
|
||||
}
|
||||
|
||||
/** Terminal.usage: the command's usage, or the list of commands. */
|
||||
private usage(command: string): void {
|
||||
const key = Object.hasOwn(USAGE, command) ? USAGE[command] : undefined;
|
||||
if (key === undefined) {
|
||||
this.tell(this.text('sw_terminal_msg25'));
|
||||
return;
|
||||
}
|
||||
const texts = TERMINAL_TEXTS[this.language] as Record<string, string>;
|
||||
this.tell(`${texts[`sw_terminal_${key}`]}\n${texts[`sw_terminal_usage_${key}`]}\n`);
|
||||
}
|
||||
|
||||
/** A parameter as an absolute path: from the root, or from the working directory. */
|
||||
private absolute(path: string): string {
|
||||
return evaluatedPath(path.startsWith('/') ? path : `${this.directoryValue}/${path}`);
|
||||
}
|
||||
|
||||
/** The content at a path, found in any case; undefined when there is none. */
|
||||
private content(path: string): { readonly path: string; readonly content: VirtualFileContent } | undefined {
|
||||
try {
|
||||
return this.files.entry(path);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Datei.holeGroesse, a text in characters (see the departures above). */
|
||||
private static size(content: VirtualFileContent): number {
|
||||
return content.kind === 'text' ? content.text.length : content.kind === 'directory' ? 0 : content.data.length;
|
||||
}
|
||||
|
||||
/** Terminal.dir. */
|
||||
private list(parameters: readonly string[]): void {
|
||||
if (parameters.length > 1) return this.usage('dir');
|
||||
const shown = parameters.length === 0 ? this.directoryValue : this.absolute(parameters[0]!);
|
||||
const found = this.content(shown);
|
||||
if (found === undefined || found.content.kind !== 'directory') return this.tell(this.text('sw_terminal_msg20'));
|
||||
const entries = this.files.entriesInCreationOrder(found.path);
|
||||
if (entries.length === 0) return this.tell(this.text('sw_terminal_msg8'));
|
||||
let text = `${this.text('sw_terminal_msg9')} ${shown}:`;
|
||||
let directories = 0;
|
||||
let files = 0;
|
||||
for (const entry of entries) {
|
||||
const name = entryName(entry.path);
|
||||
if (entry.content.kind === 'directory') {
|
||||
directories += 1;
|
||||
text += `\n[${name}]`;
|
||||
} else {
|
||||
files += 1;
|
||||
text += `\n${name}${'.'.repeat(Math.max(0, 40 - name.length))}${Terminal.size(entry.content)}`;
|
||||
}
|
||||
}
|
||||
this.tell(`${text}\n${this.text('sw_terminal_msg10')}${directories}${this.text('sw_terminal_msg11')}${files}`);
|
||||
}
|
||||
|
||||
/** Terminal.cd. */
|
||||
private cd(parameters: readonly string[]): void {
|
||||
if (parameters.length > 1) return this.usage('cd');
|
||||
if (parameters.length === 0) return this.tell(this.directoryValue);
|
||||
const found = this.content(this.absolute(parameters[0]!));
|
||||
if (found === undefined || found.content.kind !== 'directory') return this.tell(this.text('sw_terminal_msg20'));
|
||||
this.directoryValue = found.path;
|
||||
this.tell('');
|
||||
}
|
||||
|
||||
/** A new item at a path (touch, mkdir): the parent must be a folder, the name new. */
|
||||
private create(parameters: readonly string[], command: 'touch' | 'mkdir', make: (path: string) => void): void {
|
||||
if (parameters.length !== 1) return this.usage(command);
|
||||
const keys =
|
||||
command === 'touch'
|
||||
? ({ noName: 'sw_terminal_msg15', exists: 'sw_terminal_msg14', made: 'sw_terminal_msg13', failed: 'sw_terminal_msg12' } as const)
|
||||
: ({ noName: 'sw_terminal_msg19', exists: 'sw_terminal_msg18', made: 'sw_terminal_msg17', failed: 'sw_terminal_msg16' } as const);
|
||||
const path = this.absolute(parameters[0]!);
|
||||
if (basenameOf(path) === '') return this.tell(this.text(keys.noName));
|
||||
if (this.content(path) !== undefined) return this.tell(this.text(keys.exists));
|
||||
// The file system takes nothing in a folder that is not there (or is a file).
|
||||
try {
|
||||
make(path);
|
||||
this.tell(this.text(keys.made));
|
||||
} catch {
|
||||
this.tell(this.text(keys.failed));
|
||||
}
|
||||
}
|
||||
|
||||
private touch(parameters: readonly string[]): void {
|
||||
this.create(parameters, 'touch', (path) => this.files.writeTextFile(path, '', false));
|
||||
}
|
||||
|
||||
private makeDirectory(parameters: readonly string[]): void {
|
||||
this.create(parameters, 'mkdir', (path) => this.files.createDirectory(path));
|
||||
}
|
||||
|
||||
/** Writes content as a new file or over a file of that name (Dateisystem.speicherDatei). */
|
||||
private write(path: string, content: VirtualFileContent): void {
|
||||
if (content.kind === 'text') this.files.writeTextFile(path, content.text);
|
||||
else if (content.kind === 'image') this.files.writeImageFile(path, content.data, content.mediaType);
|
||||
else if (content.kind === 'binary') this.files.writeBinaryFile(path, content.data, content.mediaType);
|
||||
}
|
||||
|
||||
/** Terminal.pureCopy: a file to a new name or over a file; false where it cannot. */
|
||||
private copied(parameters: readonly string[]): { readonly source: string; readonly destination: string } | undefined {
|
||||
const source = this.content(this.absolute(parameters[0]!));
|
||||
const destination = this.absolute(parameters[1]!);
|
||||
if (source === undefined || source.content.kind === 'directory' || basenameOf(destination) === '') return undefined;
|
||||
// The file system writes over a file only, in a folder that is there; onto the file itself it changes nothing.
|
||||
try {
|
||||
this.write(destination, source.content);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return { source: source.path, destination };
|
||||
}
|
||||
|
||||
private copy(parameters: readonly string[], command: 'cp'): void {
|
||||
if (parameters.length !== 2) return this.usage(command);
|
||||
this.tell(this.text(this.copied(parameters) === undefined ? 'sw_terminal_msg34' : 'sw_terminal_msg33'));
|
||||
}
|
||||
|
||||
/** Terminal.mv: copied, then the source removed; onto the file itself, renamed. */
|
||||
private move(parameters: readonly string[]): void {
|
||||
if (parameters.length !== 2) return this.usage('mv');
|
||||
const done = this.copied(parameters);
|
||||
if (done === undefined) return this.tell(this.text('sw_terminal_msg36'));
|
||||
if (done.source.toLowerCase() === done.destination.toLowerCase()) {
|
||||
const content = this.files.entry(done.source).content;
|
||||
this.files.deleteItem(done.source);
|
||||
this.write(done.destination, content);
|
||||
} else {
|
||||
this.files.deleteItem(done.source, true);
|
||||
}
|
||||
this.tell(this.text('sw_terminal_msg35'));
|
||||
}
|
||||
|
||||
/** Terminal.del: a file or a folder with all in it. */
|
||||
private remove(parameters: readonly string[]): void {
|
||||
if (parameters.length !== 1) return this.usage('del');
|
||||
const found = this.content(this.absolute(parameters[0]!));
|
||||
if (found === undefined || found.path === '/') return this.tell(this.text('sw_terminal_msg38'));
|
||||
this.files.deleteItem(found.path, true);
|
||||
while (this.content(this.directoryValue) === undefined) this.directoryValue = parentPath(this.directoryValue);
|
||||
this.tell(this.text('sw_terminal_msg37'));
|
||||
}
|
||||
|
||||
/** Terminal.cat: a file's content as Java keeps it (an image or binary file in Base64). */
|
||||
private cat(parameters: readonly string[]): void {
|
||||
if (parameters.length === 0) {
|
||||
this.usage('cat');
|
||||
this.tell('');
|
||||
return;
|
||||
}
|
||||
const found = this.content(this.absolute(parameters[0]!));
|
||||
if (found === undefined || found.content.kind === 'directory') return this.tell(this.text('sw_terminal_msg54'));
|
||||
this.tell(found.content.kind === 'text' ? found.content.text : javaBase64(found.content.data));
|
||||
}
|
||||
|
||||
/** Terminal.test: test.txt in the working directory. */
|
||||
private test(): void {
|
||||
try {
|
||||
this.files.writeTextFile(`${this.directoryValue === '/' ? '' : this.directoryValue}/test.txt`, 'blaaa');
|
||||
this.tell(this.text('sw_terminal_msg24'));
|
||||
} catch {
|
||||
this.tell(this.text('sw_terminal_msg23'));
|
||||
}
|
||||
}
|
||||
|
||||
/** Terminal.ipconfig: the node's first address, mask, MAC, gateway and DNS server. */
|
||||
private ipconfig(parameters: readonly string[]): void {
|
||||
if (parameters.length !== 0) return this.usage('ipconfig');
|
||||
const configuration = this.engine.state.topologySnapshot.deviceConfigurations.get(this.nodeId);
|
||||
const card = this.engine.networkInterfaces(this.nodeId)[0];
|
||||
this.tell(
|
||||
`${this.text('sw_terminal_msg4')} ${card?.ipAddress ?? configuration?.ipAddress ?? ''}\n` +
|
||||
`${this.text('sw_terminal_msg5')} ${card?.subnetMask ?? configuration?.subnetMask ?? ''}\n` +
|
||||
`${this.text('sw_terminal_msg6666')} ${card?.macAddress ?? ''}\n` +
|
||||
`${this.text('sw_terminal_msg6')} ${configuration?.defaultGateway ?? ''}\n` +
|
||||
`${this.text('sw_terminal_msg27')} ${configuration?.dnsServer ?? ''}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Terminal.route: the forwarding table, each cell padded to 15. */
|
||||
private route(parameters: readonly string[]): void {
|
||||
if (parameters.length !== 0) return this.usage('route');
|
||||
const rows = this.engine.forwardingTable(this.nodeId);
|
||||
const cell = (text: string) => `${text}${' '.repeat(Math.max(0, 15 - text.length))} | `;
|
||||
const table = rows.map((row) => `| ${[row.destinationNetwork, row.subnetMask, row.nextHop, row.interfaceIPAddress].map(cell).join('')}\n`).join('');
|
||||
this.tell(this.text('sw_terminal_msg7') + table);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
// The command line's texts as Java FILIUS's message bundle gives them
|
||||
// (MessagesBundle_de_DE, _en_GB; the keys sw_terminal_* and terminal_msg*),
|
||||
// written from what tools/java-oracle's TerminalCases read from Java (texts
|
||||
// of terminal.<de|en>.java.json), so that continued lines and spacing are
|
||||
// Java's own.
|
||||
|
||||
import type { PageLanguage } from './routerPages.js';
|
||||
|
||||
export const TERMINAL_TEXTS = {
|
||||
de: {
|
||||
sw_terminal_arp: 'arp - show or manipulate Address Resolution Protocol (ARP) table',
|
||||
sw_terminal_arpsend: 'arpsend - send an ARP reply',
|
||||
sw_terminal_cat: 'cat / type - show file contents',
|
||||
sw_terminal_cd: 'cd - change directory',
|
||||
sw_terminal_copy: 'copy / cp - copy file',
|
||||
sw_terminal_del: 'del / rm - delete file/directory',
|
||||
sw_terminal_dir: 'dir / ls - show list of files in current directory',
|
||||
sw_terminal_exit: 'exit - exit terminal application',
|
||||
sw_terminal_help: 'help - show the list of commands or info on one command',
|
||||
sw_terminal_host: 'host - resolve hostname to IP address',
|
||||
sw_terminal_ipconfig: 'ipconfig - show network configuration',
|
||||
sw_terminal_mkdir: 'mkdir - create directory',
|
||||
sw_terminal_move: 'move / mv - move/rename file',
|
||||
sw_terminal_msg10: '\nAnzahl Verzeichnisse:',
|
||||
sw_terminal_msg11: '\nAnzahl Dateien:',
|
||||
sw_terminal_msg12: 'Fehler bei Touch\n',
|
||||
sw_terminal_msg13: 'Datei erstellt\n',
|
||||
sw_terminal_msg14: 'Datei bereits vorhanden\n',
|
||||
sw_terminal_msg15: 'Bitte einen Dateinamen angeben!\n',
|
||||
sw_terminal_msg16: 'Fehler beim Ordner erstellen\n',
|
||||
sw_terminal_msg17: 'Ordner erstellt\n',
|
||||
sw_terminal_msg18: 'Ordner bereits vorhanden\n',
|
||||
sw_terminal_msg19: 'Bitte einen Ordnernamen angeben!\n',
|
||||
sw_terminal_msg20: 'Verzeichnis nicht vorhanden\n',
|
||||
sw_terminal_msg23: 'Test\n',
|
||||
sw_terminal_msg24: 'Erfolgreich erstellt!',
|
||||
sw_terminal_msg25:
|
||||
'Liste verfügbarer Befehle:\n arp zeige Adresstabelle des "Address Resolution Protocol" (ARP)\n arpsend versende eine ARP-Antwort\n cat / type zeige Dateiinhalt an \n cd Verzeichnis wechseln\n cls Anzeige zurücksetzen\n copy / cp Datei kopieren\n del / rm lösche Datei/Verzeichnis\n dir / ls zeige Liste der Dateien im aktuellen Verzeichnis\n exit beende Terminal-Anwendung\n help zeige diese Befehlsliste\n host löse Hostnamen zu IP-Adresse auf\n ipconfig Netzwerkkonfiguration anzeigen\n mkdir erstelle Verzeichnis\n move / mv Datei verschieben/umbenennen\n netstat zeige Liste aller Verbindungen\n nslookup löse Hostnamen zu IP-Adresse auf\n ping teste Verbindung zu anderem Rechner\n pwd gib Pfad des aktuellen Arbeitsverzeichnisses aus\n route Routing-/Weiterleitungstabelle anzeigen\n tcpdump Datenaustausch anzeigen\n touch erstelle Datei\n traceroute analysiere Stationen des Übertragungsweges \n',
|
||||
sw_terminal_msg26: "Verwende den Befehl 'help' um Liste der verfügbaren Befehle anzuzeigen\n oder nutze 'help <Befehl>' für weitere Infos zum Befehl.",
|
||||
sw_terminal_msg27: 'DNS-Server . . . :',
|
||||
sw_terminal_msg28: 'hat die IP-Adresse',
|
||||
sw_terminal_msg29: 'Ungültige Host-Angabe!\nNur korrekte URLs ohne Pfadangabe erlaubt.\n',
|
||||
sw_terminal_msg30: 'Hostname konnte nicht aufgelöst werden!\nEventuell ist kein DNS-Server verfügbar.\n',
|
||||
sw_terminal_msg31: 'Zeitüberschreitung! Keine Antwort empfangen.',
|
||||
sw_terminal_msg33: 'Datei erfolgreich kopiert\n',
|
||||
sw_terminal_msg34: 'Datei konnte nicht kopiert werden\n',
|
||||
sw_terminal_msg35: 'Datei erfolgreich verschoben/umbenannt\n',
|
||||
sw_terminal_msg36: 'Datei konnte nicht verschoben/umbenannt werden\n',
|
||||
sw_terminal_msg37: 'Datei erfolgreich gelöscht\n',
|
||||
sw_terminal_msg38: 'Datei konnte nicht gelöscht werden\n',
|
||||
sw_terminal_msg4: 'IP Adresse . . . :',
|
||||
sw_terminal_msg45: 'Paketstatistik',
|
||||
sw_terminal_msg47: 'Paket(e) empfangen',
|
||||
sw_terminal_msg48: 'Paketverlust',
|
||||
sw_terminal_msg49: '| Proto | Lokale Adresse | Entfernte Adresse | Zustand |\n',
|
||||
sw_terminal_msg5: 'Netzmaske. . . . :',
|
||||
sw_terminal_msg50: '| Internetadresse | Physische Adresse |\n',
|
||||
sw_terminal_msg52: 'Zieladresse nicht erreichbar',
|
||||
sw_terminal_msg53: 'Ping zu Broadcast-Adressen wird nicht unterstützt.',
|
||||
sw_terminal_msg54: 'Datei nicht vorhanden',
|
||||
sw_terminal_msg55: 'Beenden mit Strg+C',
|
||||
sw_terminal_msg57:
|
||||
"\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n",
|
||||
sw_terminal_msg58: 'Mails werden behandelt durch',
|
||||
sw_terminal_msg6: 'Standardgateway. :',
|
||||
sw_terminal_msg6666: 'Physische Adresse:',
|
||||
sw_terminal_msg7: '| Ziel | Netzmaske | Gateway | Schnittstelle |\n -----------------------------------------------------------------------\n',
|
||||
sw_terminal_msg8: 'Verzeichnis ist leer\n',
|
||||
sw_terminal_msg9: 'Verzeichnis',
|
||||
sw_terminal_netstat: 'netstat - show list of connections',
|
||||
sw_terminal_nslookup: 'nslookup - resolve hostname to IP address',
|
||||
sw_terminal_ping: 'ping - test connection to other computer',
|
||||
sw_terminal_pwd: 'pwd - print working directory',
|
||||
sw_terminal_route: 'route - show routing table',
|
||||
sw_terminal_tcpdump: 'tcpdump - show data exchange',
|
||||
sw_terminal_touch: 'touch - create file',
|
||||
sw_terminal_trace_msg10: 'Fehler: %s antwortet "ICMP Destination Unreachable"',
|
||||
sw_terminal_trace_msg11: '%s wurde nach einem Sprung (hop) erreicht.',
|
||||
sw_terminal_trace_msg12: '%s wurde nach %s Sprüngen (hops) erreicht.',
|
||||
sw_terminal_trace_msg13: 'Fehler: nächste Station konnte mehrfach nicht erreicht werden.',
|
||||
sw_terminal_trace_msg2: 'Fehler: Verbindung zum DNS-Server nicht möglich.',
|
||||
sw_terminal_trace_msg3: 'Fehler: Domainname konnte nicht aufgelöst werden.',
|
||||
sw_terminal_trace_msg4: 'Verbindung nach %s wird bestimmt (max. %s Stationen).',
|
||||
sw_terminal_trace_msg5: 'Verbindung nach %s (%s) wird bestimmt (max. %s Stationen).',
|
||||
sw_terminal_trace_msg6: '%s konnte über %s Stationen (hops) nicht erreicht werden.',
|
||||
sw_terminal_trace_msg7: 'Analyse abgebrochen!',
|
||||
sw_terminal_trace_msg8: 'Fehler: %s antwortet "ICMP Network Unreachable"',
|
||||
sw_terminal_trace_msg9: 'Fehler: %s antwortet "ICMP Host Unreachable"',
|
||||
sw_terminal_traceroute: 'traceroute - analyse hops of connecting path',
|
||||
sw_terminal_usage_arp:
|
||||
'Usage: arp [-d [<IP address>]] [-a <IP address>] \n -d \t delete ARP entry for IP address or reset ARP table \n -a \t show ARP entry for specified IP address',
|
||||
sw_terminal_usage_arpsend:
|
||||
'Usage: arpsend <sender IP address> <target IP address>\n <sender IP address> address to be published for own MAC address\n <target IP address> target the reply must be sent to',
|
||||
sw_terminal_usage_cat: 'Usage: cat <file name>',
|
||||
sw_terminal_usage_cd: 'Usage: cd <directory path>',
|
||||
sw_terminal_usage_copy: 'Usage: copy <original file> <new file name>',
|
||||
sw_terminal_usage_del: 'Usage: del <file | folder> \n <file | folder> relative path to current directory',
|
||||
sw_terminal_usage_dir: 'Usage: dir',
|
||||
sw_terminal_usage_exit: 'Usage: exit',
|
||||
sw_terminal_usage_help: 'Usage: help [<command>]',
|
||||
sw_terminal_usage_host: 'Usage: host <domain> \n <domain> the domain for which to look up A and MX records',
|
||||
sw_terminal_usage_ipconfig: 'Usage: ipconfig',
|
||||
sw_terminal_usage_mkdir: 'Usage: mkdir <directory name> \n <directory name> name of new directory either as absolute path \n or relative path',
|
||||
sw_terminal_usage_move: 'Usage: move <original file> <new file name>',
|
||||
sw_terminal_usage_netstat: 'Usage: netstat',
|
||||
sw_terminal_usage_nslookup: 'Usage: nslookup <domain> \n <domain> the domain for which to look up A (and related NS) records',
|
||||
sw_terminal_usage_ping: 'Usage: ping <remote host> \n <remote host> the remote address as IP address or host name',
|
||||
sw_terminal_usage_pwd: 'Usage: pwd',
|
||||
sw_terminal_usage_route: 'Usage: route',
|
||||
sw_terminal_usage_tcpdump: 'Usage: tcpdump',
|
||||
sw_terminal_usage_touch: 'Usage: touch <file name>',
|
||||
sw_terminal_usage_traceroute: 'Usage: traceroute <remote host> \n <remote host> the remote address as IP address or host name',
|
||||
terminal_msg1: 'root>',
|
||||
terminal_msg2: 'Unbekannter Befehl!',
|
||||
terminal_msg3: "Mit 'help' erhalten Sie eine Liste bekannter Befehle.",
|
||||
},
|
||||
en: {
|
||||
sw_terminal_arp: 'arp - show or manipulate Address Resolution Protocol (ARP) table',
|
||||
sw_terminal_arpsend: 'arpsend - send an ARP reply',
|
||||
sw_terminal_cat: 'cat / type - show file contents',
|
||||
sw_terminal_cd: 'cd - change directory',
|
||||
sw_terminal_copy: 'copy / cp - copy file',
|
||||
sw_terminal_del: 'del / rm - delete file/directory',
|
||||
sw_terminal_dir: 'dir / ls - show list of files in current directory',
|
||||
sw_terminal_exit: 'exit - exit terminal application',
|
||||
sw_terminal_help: 'help - show the list of commands or info on one command',
|
||||
sw_terminal_host: 'host - resolve hostname to IP address',
|
||||
sw_terminal_ipconfig: 'ipconfig - show network configuration',
|
||||
sw_terminal_mkdir: 'mkdir - create directory',
|
||||
sw_terminal_move: 'move / mv - move/rename file',
|
||||
sw_terminal_msg10: '\nNumber of directories:',
|
||||
sw_terminal_msg11: '\nNumber of files:',
|
||||
sw_terminal_msg12: 'Error in Touch\n',
|
||||
sw_terminal_msg13: 'File created\n',
|
||||
sw_terminal_msg14: 'File does already exist\n',
|
||||
sw_terminal_msg15: 'Please enter a file name!\n',
|
||||
sw_terminal_msg16: 'Error while creating new folder\n',
|
||||
sw_terminal_msg17: 'Folder is created\n',
|
||||
sw_terminal_msg18: 'Folder does already exist\n',
|
||||
sw_terminal_msg19: 'Please enter folder name!\n',
|
||||
sw_terminal_msg20: 'Directory does not exist\n',
|
||||
sw_terminal_msg23: 'Test\n',
|
||||
sw_terminal_msg24: 'Successfully built!',
|
||||
sw_terminal_msg25:
|
||||
'List of available commands:\n arp show or manipulate Address Resolution Protocol (ARP) table\n arpsend send an ARP reply\n cat / type show file contents \n cd change directory\n cls clear screen \n copy / cp copy file\n del / rm delete file/directory\n dir / ls show list of files in current directory\n exit exit terminal application\n help show this list of commands\n host resolve hostname to IP address\n ipconfig show network configuration\n mkdir create directory\n move / mv move/rename file\n netstat show list of connections\n nslookup resolve hostname to IP address\n ping test connection to other computer\n pwd print working directory\n route show routing table\n tcpdump show data exchange\n touch create file\n traceroute analyse hops of connecting path \n',
|
||||
sw_terminal_msg26: "Use command 'help' to show list of available commands \n or 'help <command>' for further information on the command.",
|
||||
sw_terminal_msg27: 'DNS server . . .:',
|
||||
sw_terminal_msg28: 'has the IP address',
|
||||
sw_terminal_msg29: 'Invalid host string!\nOnly correct URLs without paths are allowed.\n',
|
||||
sw_terminal_msg30: 'Hostname could not be resolved!\nPossibly no DNS server is available.\n',
|
||||
sw_terminal_msg31: 'Timeout! No response received during waiting period.',
|
||||
sw_terminal_msg33: 'File successfully copied\n',
|
||||
sw_terminal_msg34: 'File could not be copied\n',
|
||||
sw_terminal_msg35: 'File successfully moved/renamed\n',
|
||||
sw_terminal_msg36: 'File could not be moved/renamed\n',
|
||||
sw_terminal_msg37: 'File successfully deleted\n',
|
||||
sw_terminal_msg38: 'File could not be deleted\n',
|
||||
sw_terminal_msg4: 'IP address . . .:',
|
||||
sw_terminal_msg45: 'packet statistics',
|
||||
sw_terminal_msg47: 'packet(s) received',
|
||||
sw_terminal_msg48: 'packet loss',
|
||||
sw_terminal_msg49: '| Proto | Local Address | Remote Address | State |\n',
|
||||
sw_terminal_msg5: 'Netmask. . . . .:',
|
||||
sw_terminal_msg50: '| Internet Address | Physical Address |\n',
|
||||
sw_terminal_msg52: 'Destination not reachable',
|
||||
sw_terminal_msg53: 'Ping to Broadcast is not supported.',
|
||||
sw_terminal_msg54: 'No such file',
|
||||
sw_terminal_msg55: 'Cancel with Ctrl+C',
|
||||
sw_terminal_msg57:
|
||||
"\n\n sSSs .S S. .S .S S. sSSs \n d%%SP .SS SS. .SS .SS SS. d%%SP \n d%S' S%S S%S S%S S%S S%S d%S' \n S%S S%S S%S S%S S%S S%S S%| \n S&S S&S S&S S&S S&S S&S S&S \n S&S_Ss S&S S&S S&S S&S S&S Y&Ss \n S&S~SP S&S S&S S&S S&S S&S `S&&S \n S&S S&S S&S S&S S&S S&S `S*S \n S*b S*S S*b S*S S*b d*S l*S \n S*S S*S S*S. S*S S*S. .S*S .S*P \n S*S S*S SSSbs S*S SSSbs_sdSSS sSS*S \n S*S S*S YSSP S*S YSSP~YSSY YSS' \n SP SP SP \n Y Y Y \n\n",
|
||||
sw_terminal_msg58: 'mail is handled by',
|
||||
sw_terminal_msg6: 'Standard gateway:',
|
||||
sw_terminal_msg6666: 'Physical address:',
|
||||
sw_terminal_msg7: '| Destination | Netmask | Gateway | Interface |\n -------------------------------------------------------------------------\n',
|
||||
sw_terminal_msg8: 'Directory is empty\n',
|
||||
sw_terminal_msg9: 'Directory',
|
||||
sw_terminal_netstat: 'netstat - show list of connections',
|
||||
sw_terminal_nslookup: 'nslookup - resolve hostname to IP address',
|
||||
sw_terminal_ping: 'ping - test connection to other computer',
|
||||
sw_terminal_pwd: 'pwd - print working directory',
|
||||
sw_terminal_route: 'route - show routing table',
|
||||
sw_terminal_tcpdump: 'tcpdump - show data exchange',
|
||||
sw_terminal_touch: 'touch - create file',
|
||||
sw_terminal_trace_msg10: 'Error: response from %s was "ICMP Destination Unreachable"',
|
||||
sw_terminal_trace_msg11: '%s was reached with one hop.',
|
||||
sw_terminal_trace_msg12: '%s was reached with %s hops.',
|
||||
sw_terminal_trace_msg13: 'Error: next hop could not be processed.',
|
||||
sw_terminal_trace_msg2: 'Error: could not connect to DNS server.',
|
||||
sw_terminal_trace_msg3: 'Error: domain could not be resolved.',
|
||||
sw_terminal_trace_msg4: 'trace route to %s started (max. %s hops).',
|
||||
sw_terminal_trace_msg5: 'trace route to %s (%s) started (max. %s hops).',
|
||||
sw_terminal_trace_msg6: '%s could not be reached via %s hops.',
|
||||
sw_terminal_trace_msg7: 'Analysis interrupted!',
|
||||
sw_terminal_trace_msg8: 'Error: response from %s was "ICMP Network Unreachable"',
|
||||
sw_terminal_trace_msg9: 'Error: response from %s was "ICMP Host Unreachable"',
|
||||
sw_terminal_traceroute: 'traceroute - analyse hops of connecting path',
|
||||
sw_terminal_usage_arp:
|
||||
'Usage: arp [-d [<IP address>]] [-a <IP address>] \n -d \t delete ARP entry for IP address or reset ARP table \n -a \t show ARP entry for specified IP address',
|
||||
sw_terminal_usage_arpsend:
|
||||
'Usage: arpsend <sender IP address> <target IP address>\n <sender IP address> address to be published for own MAC address\n <target IP address> target the reply must be sent to',
|
||||
sw_terminal_usage_cat: 'Usage: cat <file name>',
|
||||
sw_terminal_usage_cd: 'Usage: cd <directory path>',
|
||||
sw_terminal_usage_copy: 'Usage: copy <original file> <new file name>',
|
||||
sw_terminal_usage_del: 'Usage: del <file | folder> \n <file | folder> relative path to current directory',
|
||||
sw_terminal_usage_dir: 'Usage: dir',
|
||||
sw_terminal_usage_exit: 'Usage: exit',
|
||||
sw_terminal_usage_help: 'Usage: help [<command>]',
|
||||
sw_terminal_usage_host: 'Usage: host <domain> \n <domain> the domain for which to look up A and MX records',
|
||||
sw_terminal_usage_ipconfig: 'Usage: ipconfig',
|
||||
sw_terminal_usage_mkdir: 'Usage: mkdir <directory name> \n <directory name> name of new directory either as absolute path \n or relative path',
|
||||
sw_terminal_usage_move: 'Usage: move <original file> <new file name>',
|
||||
sw_terminal_usage_netstat: 'Usage: netstat',
|
||||
sw_terminal_usage_nslookup: 'Usage: nslookup <domain> \n <domain> the domain for which to look up A (and related NS) records',
|
||||
sw_terminal_usage_ping: 'Usage: ping <remote host> \n <remote host> the remote address as IP address or host name',
|
||||
sw_terminal_usage_pwd: 'Usage: pwd',
|
||||
sw_terminal_usage_route: 'Usage: route',
|
||||
sw_terminal_usage_tcpdump: 'Usage: tcpdump',
|
||||
sw_terminal_usage_touch: 'Usage: touch <file name>',
|
||||
sw_terminal_usage_traceroute: 'Usage: traceroute <remote host> \n <remote host> the remote address as IP address or host name',
|
||||
terminal_msg1: 'root>',
|
||||
terminal_msg2: 'Unknown command!',
|
||||
terminal_msg3: "Type 'help' to get a list of available commands.",
|
||||
},
|
||||
} as const satisfies Record<PageLanguage, Record<string, string>>;
|
||||
|
||||
export type TerminalTextKey = keyof (typeof TERMINAL_TEXTS)['de'];
|
||||
@@ -118,6 +118,7 @@ import { EchoServer, SimpleClient } from './apps/echo.js';
|
||||
import { WebBrowser, WebServer, type WebServerPage } from './apps/web.js';
|
||||
import { EmailClient, type EmailClientFolders } from './apps/emailClient.js';
|
||||
import { EmailServer } from './apps/emailServer.js';
|
||||
import { Terminal } from './apps/terminal.js';
|
||||
import { firewallLogPage, forwardingTablePage, ripTablePage, routerIndexPage } from './apps/routerPages.js';
|
||||
import { applyFirewallSettings, firewallSettingsPage } from './apps/firewallSettings.js';
|
||||
import { VirtualFileSystem } from '../model/virtualFileSystem.js';
|
||||
@@ -915,6 +916,7 @@ export class NetworkRuntimeEngine {
|
||||
private readonly webBrowsersByNodeId = new Map<Id, WebBrowser>();
|
||||
private readonly emailServersByNodeId = new Map<Id, EmailServer>();
|
||||
private readonly emailClientsByNodeId = new Map<Id, EmailClient>();
|
||||
private readonly terminalsByNodeId = new Map<Id, Terminal>();
|
||||
/** The time of day a program writes into a mail (Java's new Date()). */
|
||||
private readonly wallClock: () => number;
|
||||
/** Each node's firewall log; it outlives a stop and a new start, as Java's log page does. */
|
||||
@@ -1046,6 +1048,7 @@ export class NetworkRuntimeEngine {
|
||||
this.webBrowsersByNodeId.clear();
|
||||
this.emailServersByNodeId.clear();
|
||||
this.emailClientsByNodeId.clear();
|
||||
this.terminalsByNodeId.clear();
|
||||
this.arpLookups = [];
|
||||
this.protocolThreads.clear();
|
||||
this.dnsQueriesBySocketId.clear();
|
||||
@@ -1136,6 +1139,11 @@ export class NetworkRuntimeEngine {
|
||||
return this.emailServersByNodeId.get(nodeId);
|
||||
}
|
||||
|
||||
/** The node's command line while the simulation runs, if it has one installed. */
|
||||
terminal(nodeId: Id): Terminal | undefined {
|
||||
return this.terminalsByNodeId.get(nodeId);
|
||||
}
|
||||
|
||||
/** The node's e-mail program while the simulation runs, if it has one installed. */
|
||||
emailClient(nodeId: Id): EmailClient | undefined {
|
||||
return this.emailClientsByNodeId.get(nodeId);
|
||||
@@ -1171,6 +1179,7 @@ export class NetworkRuntimeEngine {
|
||||
this.webBrowsersByNodeId.clear();
|
||||
this.emailServersByNodeId.clear();
|
||||
this.emailClientsByNodeId.clear();
|
||||
this.terminalsByNodeId.clear();
|
||||
for (const node of snapshot.nodes) {
|
||||
const echoServer = snapshot.echoServersByNodeId.get(node.id);
|
||||
if (echoServer !== undefined) this.echoServersByNodeId.set(node.id, new EchoServer(this, node.id, echoServer));
|
||||
@@ -1196,6 +1205,17 @@ export class NetworkRuntimeEngine {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (snapshot.terminalNodeIds.has(node.id)) {
|
||||
let files = snapshot.fileSystemsByNodeId.get(node.id);
|
||||
if (files === undefined) {
|
||||
// A node without files gets an empty file system, kept in the snapshot.
|
||||
files = new VirtualFileSystem();
|
||||
this.stateValue.topologySnapshot = withSnapshot(this.stateValue.topologySnapshot, {
|
||||
fileSystemsByNodeId: new Map([...this.stateValue.topologySnapshot.fileSystemsByNodeId, [node.id, files]]),
|
||||
});
|
||||
}
|
||||
this.terminalsByNodeId.set(node.id, new Terminal(this, node.id, files));
|
||||
}
|
||||
const emailClient = snapshot.emailClientsByNodeId.get(node.id);
|
||||
if (emailClient !== undefined) {
|
||||
const nodeId = node.id;
|
||||
|
||||
@@ -103,6 +103,8 @@ export interface TopologySnapshot {
|
||||
readonly webBrowserNodeIds: ReadonlySet<Id>;
|
||||
/** The installed e-mail servers, by node; a run's deliveries change their mailboxes here. */
|
||||
readonly emailServersByNodeId: ReadonlyMap<Id, EmailServerSnapshot>;
|
||||
/** The nodes with the command line installed (Terminal); it works on the node's files. */
|
||||
readonly terminalNodeIds: ReadonlySet<Id>;
|
||||
/** The installed e-mail programs, by node; a run's sent and fetched mails change their folders here. */
|
||||
readonly emailClientsByNodeId: ReadonlyMap<Id, EmailClientConfiguration>;
|
||||
/** Each node's files, which its programs read (a web server its pages). */
|
||||
@@ -131,6 +133,7 @@ export interface TopologySnapshotInput {
|
||||
webBrowserNodeIds?: ReadonlySet<Id>;
|
||||
emailServersByNodeId?: ReadonlyMap<Id, EmailServerSnapshot>;
|
||||
emailClientsByNodeId?: ReadonlyMap<Id, EmailClientConfiguration>;
|
||||
terminalNodeIds?: ReadonlySet<Id>;
|
||||
fileSystemsByNodeId?: ReadonlyMap<Id, VirtualFileSystem>;
|
||||
language?: PageLanguage;
|
||||
}
|
||||
@@ -156,6 +159,7 @@ export function topologySnapshot(input: TopologySnapshotInput): TopologySnapshot
|
||||
webBrowserNodeIds: input.webBrowserNodeIds ?? new Set(),
|
||||
emailServersByNodeId: input.emailServersByNodeId ?? new Map(),
|
||||
emailClientsByNodeId: input.emailClientsByNodeId ?? new Map(),
|
||||
terminalNodeIds: input.terminalNodeIds ?? new Set(),
|
||||
fileSystemsByNodeId: input.fileSystemsByNodeId ?? new Map(),
|
||||
language: input.language ?? 'de',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user