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:
+7
-5
File diff suppressed because one or more lines are too long
@@ -26,6 +26,8 @@ export * from './runtime/apps/routerPages.js';
|
||||
export * from './runtime/apps/firewallSettings.js';
|
||||
export * from './runtime/apps/emailServer.js';
|
||||
export * from './runtime/apps/emailClient.js';
|
||||
export * from './runtime/apps/terminal.js';
|
||||
export * from './runtime/apps/terminalTexts.js';
|
||||
export * from './runtime/email.js';
|
||||
export * from './runtime/http.js';
|
||||
export * from './util/equal.js';
|
||||
|
||||
@@ -261,6 +261,19 @@ export class VirtualFileSystem {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A directory's entries in the order they were made, as Java's file tree
|
||||
* keeps them (its command line lists them so); a file written anew keeps
|
||||
* its place.
|
||||
*/
|
||||
entriesInCreationOrder(rawDirectoryPath: string): VirtualFileEntry[] {
|
||||
const directoryPath = this.existingPath(rawDirectoryPath);
|
||||
if (this.get(directoryPath)?.kind !== 'directory') fail('expectedDirectory', directoryPath);
|
||||
return [...this.store.values()]
|
||||
.filter(({ path }) => !swiftEqual(path, directoryPath) && swiftEqual(parentPath(path), directoryPath))
|
||||
.map(({ path, content }) => ({ path, content }));
|
||||
}
|
||||
|
||||
textFile(rawPath: string): string {
|
||||
const entry = this.entry(rawPath);
|
||||
if (entry.content.kind !== 'text') fail('expectedTextFile', entry.path);
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# Cases for tools/java-oracle's TerminalCases: FILIUS' command line fed with
|
||||
# lines as typed, one per line. Read by terminal.test.ts.
|
||||
case listing
|
||||
ls
|
||||
dir
|
||||
ls Dokumente
|
||||
ls /Dokumente/Alt
|
||||
ls gibtsnicht
|
||||
ls ..
|
||||
dir /
|
||||
ls a b
|
||||
case navigate
|
||||
pwd
|
||||
cd Dokumente
|
||||
pwd
|
||||
cd Alt
|
||||
cd ..
|
||||
cd ../..
|
||||
cd /Dokumente/Alt
|
||||
cd /
|
||||
cd gibtsnicht
|
||||
cd DOKUMENTE
|
||||
pwd
|
||||
cd
|
||||
cd /
|
||||
cd notizen.txt
|
||||
pwd
|
||||
ls
|
||||
case files
|
||||
touch neu.txt
|
||||
touch neu.txt
|
||||
touch NEU.TXT
|
||||
touch Dokumente/neu2.txt
|
||||
touch /gibtsnicht/x.txt
|
||||
touch ..
|
||||
mkdir Neu
|
||||
mkdir Neu
|
||||
mkdir neu
|
||||
mkdir Dokumente/Alt/Tief
|
||||
mkdir /gibtsnicht/x
|
||||
mkdir ..
|
||||
ls
|
||||
case copy
|
||||
cp notizen.txt kopie.txt
|
||||
cp gibtsnicht.txt x.txt
|
||||
cp notizen.txt NOTIZEN.TXT
|
||||
copy bild.png Dokumente/bild2.png
|
||||
cp kurz.txt /leer/kurz.txt
|
||||
cp notizen.txt
|
||||
cp notizen.txt Dokumente
|
||||
ls
|
||||
case move
|
||||
mv notizen.txt umbenannt.txt
|
||||
mv kurz.txt Dokumente/kurz.txt
|
||||
mv gibtsnicht.txt y.txt
|
||||
move bild.png /leer/bild.png
|
||||
mv Dokumente Ordner
|
||||
ls
|
||||
case rename
|
||||
mv notizen.txt NOTIZEN.TXT
|
||||
ls
|
||||
mv Dokumente/brief.txt /brief.txt
|
||||
ls
|
||||
case delete
|
||||
rm notizen.txt
|
||||
rm notizen.txt
|
||||
del Dokumente
|
||||
rm /leer
|
||||
cd leer
|
||||
rm ../bild.png
|
||||
ls
|
||||
case show
|
||||
cat notizen.txt
|
||||
type Dokumente/brief.txt
|
||||
cat gibtsnicht
|
||||
cat
|
||||
cat Dokumente
|
||||
cat bild.png
|
||||
case info
|
||||
ipconfig
|
||||
ipconfig x
|
||||
route
|
||||
route x
|
||||
test
|
||||
ls
|
||||
case help
|
||||
help
|
||||
help ls
|
||||
help gibtsnicht
|
||||
help exit
|
||||
case window
|
||||
|
||||
|
||||
cls
|
||||
reset
|
||||
exit
|
||||
unbekannt
|
||||
LS
|
||||
move
|
||||
setInterrupt x
|
||||
initUsageInfo
|
||||
case spaces
|
||||
ls Dokumente
|
||||
cd Dokumente/
|
||||
pwd
|
||||
cd ./Alt/../Alt
|
||||
pwd
|
||||
case deleteInside
|
||||
cd Dokumente/Alt
|
||||
rm /Dokumente
|
||||
pwd
|
||||
ls
|
||||
cd ..
|
||||
pwd
|
||||
rm /
|
||||
ls
|
||||
case sameName
|
||||
mv notizen.txt notizen.txt
|
||||
cp kurz.txt kurz.txt
|
||||
ls
|
||||
case usage
|
||||
pwd x
|
||||
rm
|
||||
rm a b
|
||||
cd a b
|
||||
touch a b
|
||||
mkdir a b
|
||||
cp a b c
|
||||
mv a b c
|
||||
help type
|
||||
help rm
|
||||
help cp
|
||||
help mv
|
||||
\u0009
|
||||
case paths
|
||||
ls ./Dokumente
|
||||
ls notizen.txt
|
||||
cd Dokumente
|
||||
test
|
||||
ls
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,259 @@
|
||||
// The command line (apps/terminal.ts) against Java FILIUS' own:
|
||||
// tools/java-oracle's TerminalCases fed Java's Terminal, as its window does,
|
||||
// with the lines of terminal.cases.txt on host 10.0.0.2 of router-web
|
||||
// (terminal.<de|en>.java.json). After each line: what the window does
|
||||
// itself, what the program told it, the working directory, and every file
|
||||
// must be Java's, in both languages, except where Java errs.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
entryName,
|
||||
javaBase64,
|
||||
javaBase64Decode,
|
||||
TERMINAL_TEXTS,
|
||||
VirtualFileSystem,
|
||||
withSnapshot,
|
||||
type NetworkRuntimeEngine,
|
||||
type PageLanguage,
|
||||
type TerminalTextKey,
|
||||
type VirtualFileContent,
|
||||
} from '../src/index.js';
|
||||
import { watchedEngine } from './javaRun.js';
|
||||
import { snapshotOf, type Scenario } from './scenario.js';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const HOST = '00000000-0000-0000-0000-000000000301';
|
||||
|
||||
/** A file as TerminalCases writes it: path, and for a file its type, content and size. */
|
||||
type JavaNode = [string] | [string, string, string, number];
|
||||
type Told = ['text', string] | ['busy', boolean];
|
||||
|
||||
interface JavaStep {
|
||||
typed: string;
|
||||
window: 'blank' | 'exit' | 'clear' | null;
|
||||
told: Told[];
|
||||
directory: string;
|
||||
files: JavaNode[];
|
||||
}
|
||||
|
||||
interface JavaCase {
|
||||
name: string;
|
||||
start: JavaNode[];
|
||||
steps: JavaStep[];
|
||||
}
|
||||
|
||||
/** TerminalCases.files: the host's files at the start, in their order. */
|
||||
function startFiles(start: JavaNode[]): VirtualFileSystem {
|
||||
const files = new VirtualFileSystem();
|
||||
for (const node of start) {
|
||||
if (node.length === 1) files.createDirectory(node[0]);
|
||||
else if (node[1] === 'png') files.writeImageFile(node[0], javaBase64Decode(node[2])!, 'image/png');
|
||||
else files.writeTextFile(node[0], node[2]);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
/** The engine's files as TerminalCases writes Java's: preorder, each directory's entries in their order. */
|
||||
function engineFiles(files: VirtualFileSystem, directory = '/'): JavaNode[] {
|
||||
return files.entriesInCreationOrder(directory).flatMap((entry): JavaNode[] => {
|
||||
const content: VirtualFileContent = entry.content;
|
||||
if (content.kind === 'directory') return [[entry.path], ...engineFiles(files, entry.path)];
|
||||
const text = content.kind === 'text' ? content.text : javaBase64(content.data);
|
||||
return [[entry.path, content.kind === 'text' ? 'text' : 'binary', text, content.kind === 'text' ? content.text.length : content.data.length]];
|
||||
});
|
||||
}
|
||||
|
||||
/** Java's file types as the engine's kinds: an image FILIUS keeps in Base64, every other file as text. */
|
||||
const kindOf = (type: string) => (type === 'png' ? 'binary' : 'text');
|
||||
|
||||
/**
|
||||
* Java's sizes of text files: Datei.holeGroesse decodes the text as Base64
|
||||
* and counts the bytes ("Hallo Welt" is 6). As Java means it: the
|
||||
* characters. Applied to every step, in the file list and in each listing.
|
||||
*/
|
||||
function withTextSizes(step: JavaStep): JavaStep {
|
||||
const files = step.files.map((node): JavaNode => (node.length === 1 ? node : [node[0], kindOf(node[1]), node[2], kindOf(node[1]) === 'text' ? node[2].length : node[3]]));
|
||||
const sizes = new Map(files.flatMap((node) => (node.length === 4 && node[1] === 'text' ? [[entryName(node[0]), node[3]] as const] : [])));
|
||||
const told = step.told.map(([kind, value]): Told => {
|
||||
if (kind !== 'text') return [kind, value];
|
||||
return ['text', value.replace(/^(?!\[)(.+?)(\.+)(\d+)$/gm, (line, name: string, dots: string, size: string) => (sizes.has(name) ? `${name}${dots}${sizes.get(name)}` : line))];
|
||||
});
|
||||
return { ...step, files, told };
|
||||
}
|
||||
|
||||
type Fix = (steps: JavaStep[], text: (key: TerminalTextKey) => string) => JavaStep[];
|
||||
|
||||
const said = (step: JavaStep, ...texts: string[]): JavaStep => ({ ...step, told: texts.map((each): Told => ['text', each]) });
|
||||
const at = (steps: JavaStep[], changes: Record<number, (step: JavaStep) => JavaStep>) => steps.map((step, index) => changes[index]?.(step) ?? step);
|
||||
|
||||
/**
|
||||
* Where Java errs, the case as Java means it (the engine's departures, see
|
||||
* terminal.ts).
|
||||
*/
|
||||
const JAVA_BUGS: Record<string, Fix> = {
|
||||
// A folder that is not there: Java says "empty"; ls .. at the root lists
|
||||
// nothing (".." goes above the root); ls / names no path.
|
||||
listing: (steps, text) =>
|
||||
at(steps, {
|
||||
4: (step) => said(step, text('sw_terminal_msg20')),
|
||||
5: (step) => ({ ...step, told: steps[0]!.told }),
|
||||
6: (step) => said(step, (step.told[0]![1] as string).replace(/^(\S+) :/, '$1 /:')),
|
||||
}),
|
||||
// cd to a file makes it the working directory.
|
||||
navigate: (steps, text) =>
|
||||
at(steps, {
|
||||
13: (step) => ({ ...said(step, text('sw_terminal_msg20')), directory: '/' }),
|
||||
14: (step) => ({ ...said(step, '/'), directory: '/' }),
|
||||
15: (step) => ({ ...step, directory: '/', told: listingOfStart(text) }),
|
||||
}),
|
||||
// touch and mkdir in a folder that is not there: "made" and "there already".
|
||||
files: (steps, text) =>
|
||||
at(steps, {
|
||||
4: (step) => said(step, text('sw_terminal_msg12')),
|
||||
10: (step) => said(step, text('sw_terminal_msg16')),
|
||||
}),
|
||||
// cp onto a folder's name throws (ClassCastException).
|
||||
copy: (steps, text) => at(steps, { 6: (step) => said(step, text('sw_terminal_msg34')) }),
|
||||
// mv onto the file in another case copies it onto itself, then removes it.
|
||||
rename: (steps) => {
|
||||
const renamed: JavaNode = ['/NOTIZEN.TXT', 'text', 'Hallo Welt', 10];
|
||||
return at(steps, {
|
||||
0: (step) => ({ ...step, files: [...step.files, renamed] }),
|
||||
1: (step) => ({ ...step, files: [...step.files, renamed], told: [['text', addedFile(step.told, 'NOTIZEN.TXT', 10)]] }),
|
||||
2: (step) => ({ ...step, files: [...step.files.filter((node) => node[0] !== '/brief.txt'), renamed, step.files.find((node) => node[0] === '/brief.txt')!] }),
|
||||
3: (step) => ({
|
||||
...step,
|
||||
files: [...step.files.filter((node) => node[0] !== '/brief.txt'), renamed, step.files.find((node) => node[0] === '/brief.txt')!],
|
||||
told: [['text', addedFile(step.told, 'NOTIZEN.TXT', 10, 'brief.txt')]],
|
||||
}),
|
||||
});
|
||||
},
|
||||
// ".." above the root: Java's path evaluation throws (rm ../bild.png from the root).
|
||||
delete: (steps, text) =>
|
||||
at(steps, {
|
||||
5: (step) => ({ ...said(step, text('sw_terminal_msg37')), files: step.files.filter((node) => node[0] !== '/bild.png') }),
|
||||
6: (step) => ({
|
||||
...step,
|
||||
files: step.files.filter((node) => node[0] !== '/bild.png'),
|
||||
told: [['text', (step.told[0]![1] as string).replace(/\nbild\.png\.+\d+/, '').replace(/(:)2$/, '$11')]],
|
||||
}),
|
||||
}),
|
||||
// rm /Dokumente from inside: Java puts the working directory in front of
|
||||
// the path and finds nothing; rm / then removes the working directory.
|
||||
deleteInside: (steps, text) => {
|
||||
const withoutDocuments = (step: JavaStep) => step.files.filter((node) => !node[0].startsWith('/Dokumente'));
|
||||
const start = steps[0]!.files;
|
||||
const remaining = start.filter((node) => !node[0].startsWith('/Dokumente'));
|
||||
const rootListing = listing(text, '/', remaining);
|
||||
return at(steps, {
|
||||
1: (step) => ({ ...said(step, text('sw_terminal_msg37')), directory: '/', files: withoutDocuments(step) }),
|
||||
2: (step) => ({ ...said(step, '/'), directory: '/', files: remaining }),
|
||||
3: (step) => ({ ...said(step, rootListing), directory: '/', files: remaining }),
|
||||
4: (step) => ({ ...said(step, ''), directory: '/', files: remaining }),
|
||||
5: (step) => ({ ...said(step, '/'), directory: '/', files: remaining }),
|
||||
6: (step) => ({ ...said(step, text('sw_terminal_msg38')), directory: '/', files: remaining }),
|
||||
7: (step) => ({ ...said(step, rootListing), directory: '/', files: remaining }),
|
||||
});
|
||||
},
|
||||
// ls of a file: Java lists its children (none: "empty").
|
||||
paths: (steps, text) => at(steps, { 1: (step) => said(step, text('sw_terminal_msg20')) }),
|
||||
// mv onto the file itself: Java copies it onto itself, then removes it.
|
||||
sameName: (steps) => {
|
||||
const notes: JavaNode = ['/notizen.txt', 'text', 'Hallo Welt', 10];
|
||||
return at(steps, {
|
||||
0: (step) => ({ ...step, files: [...step.files, notes] }),
|
||||
1: (step) => ({ ...step, files: [...step.files, notes] }),
|
||||
2: (step) => ({ ...step, files: [...step.files, notes], told: [['text', addedFile(step.told, 'notizen.txt', 10)]] }),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/** A listing's text with a file added at its end (after `before`'s line when given), one file more counted. */
|
||||
function addedFile(told: Told[], name: string, size: number, before?: string): string {
|
||||
const text = told[0]![1] as string;
|
||||
const line = `\n${name}${'.'.repeat(40 - name.length)}${size}`;
|
||||
const withLine = before === undefined ? text.replace(/\n\n/, `${line}\n\n`) : text.replace(new RegExp(`\\n${before.replace('.', '\\.')}`), `${line}\n${before}`);
|
||||
return withLine.replace(/(\d+)$/, (count) => String(Number(count) + 1));
|
||||
}
|
||||
|
||||
/** Terminal.dir's listing of a directory as Java writes it, from TerminalCases' file list. */
|
||||
function listing(text: (key: TerminalTextKey) => string, directory: string, files: JavaNode[]): string {
|
||||
const children = files.filter((node) => node[0] !== directory && node[0].slice(0, node[0].lastIndexOf('/') || 1) === directory);
|
||||
const lines = children.map((node) => (node.length === 1 ? `[${entryName(node[0])}]` : `${entryName(node[0])}${'.'.repeat(Math.max(0, 40 - entryName(node[0]).length))}${node[3]}`));
|
||||
const directories = children.filter((node) => node.length === 1).length;
|
||||
return `${text('sw_terminal_msg9')} ${directory}:\n${lines.join('\n')}\n${text('sw_terminal_msg10')}${directories}${text('sw_terminal_msg11')}${children.length - directories}`;
|
||||
}
|
||||
|
||||
let startNodes: JavaNode[] = [];
|
||||
/** The root's listing of the start files (with the sizes as Java means them). */
|
||||
const listingOfStart = (text: (key: TerminalTextKey) => string): Told[] => [['text', listing(text, '/', startNodes)]];
|
||||
|
||||
function javaCases(language: PageLanguage): JavaCase[] {
|
||||
return (JSON.parse(readFileSync(join(here, `terminal.${language}.java.json`), 'utf8')) as { cases: JavaCase[] }).cases;
|
||||
}
|
||||
|
||||
function play(javaCase: JavaCase, language: PageLanguage): JavaStep[] {
|
||||
const scenario = JSON.parse(readFileSync(join(here, 'scenarios', 'router-web.scenario.json'), 'utf8')) as Scenario;
|
||||
const files = startFiles(javaCase.start);
|
||||
const snapshot = withSnapshot(snapshotOf(scenario), { language, terminalNodeIds: new Set([HOST]), fileSystemsByNodeId: new Map([[HOST, files]]) });
|
||||
const { engine } = watchedEngine(scenario, snapshot);
|
||||
const terminal = (each: NetworkRuntimeEngine) => each.terminal(HOST)!;
|
||||
let seen = 0;
|
||||
return javaCase.steps.map((step) => {
|
||||
const window = terminal(engine).enter(step.typed) ?? null;
|
||||
const told = terminal(engine)
|
||||
.log.all.slice(seen)
|
||||
.map((line): Told => (line.kind === 'text' ? ['text', line.text] : ['busy', line.busy]));
|
||||
seen = terminal(engine).log.all.length;
|
||||
return { typed: step.typed, window, told, directory: terminal(engine).directory, files: engineFiles(files) };
|
||||
});
|
||||
}
|
||||
|
||||
describe('the command line as Java FILIUS answers it', () => {
|
||||
for (const language of ['de', 'en'] as const) {
|
||||
const cases = javaCases(language);
|
||||
const text = (key: TerminalTextKey) => TERMINAL_TEXTS[language][key];
|
||||
|
||||
it(`corrects Java only where it errs (${language})`, () => {
|
||||
for (const [name, fix] of Object.entries(JAVA_BUGS)) {
|
||||
const javaCase = cases.find((each) => each.name === name)!;
|
||||
startNodes = withTextSizes({ ...javaCase.steps[0]!, files: javaCase.start }).files;
|
||||
const steps = javaCase.steps.map(withTextSizes);
|
||||
expect(JSON.stringify(fix(steps, text)), name).not.toBe(JSON.stringify(steps));
|
||||
}
|
||||
});
|
||||
|
||||
for (const javaCase of cases) {
|
||||
it(`${javaCase.name} (${language})`, () => {
|
||||
startNodes = withTextSizes({ ...javaCase.steps[0]!, files: javaCase.start }).files;
|
||||
const steps = javaCase.steps.map(withTextSizes);
|
||||
const expected = JAVA_BUGS[javaCase.name]?.(steps, text) ?? steps;
|
||||
const actual = play(javaCase, language);
|
||||
for (const [index, step] of expected.entries()) {
|
||||
const { typed, window, told, directory, files } = step;
|
||||
expect(actual[index], `step ${index}: ${JSON.stringify(typed)}`).toEqual({ typed, window, told, directory, files });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe('the command line beyond the cases', () => {
|
||||
it('copies an image as an image', () => {
|
||||
const scenario = JSON.parse(readFileSync(join(here, 'scenarios', 'router-web.scenario.json'), 'utf8')) as Scenario;
|
||||
const files = new VirtualFileSystem();
|
||||
files.writeImageFile('/bild.png', new Uint8Array([1, 2, 3]), 'image/png');
|
||||
const { engine } = watchedEngine(scenario, withSnapshot(snapshotOf(scenario), { terminalNodeIds: new Set([HOST]), fileSystemsByNodeId: new Map([[HOST, files]]) }));
|
||||
engine.terminal(HOST)!.enter('cp bild.png kopie.png');
|
||||
expect(files.entry('/kopie.png').content).toEqual({ kind: 'image', data: new Uint8Array([1, 2, 3]), mediaType: 'image/png' });
|
||||
});
|
||||
|
||||
it('lists in the order of making only a folder', () => {
|
||||
const files = new VirtualFileSystem();
|
||||
files.writeTextFile('/a.txt', 'a');
|
||||
expect(() => files.entriesInCreationOrder('/a.txt')).toThrow('expectedDirectory');
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,8 @@
|
||||
// A DNS server runs only when it is switched on (Java: DNSServer.aktiv); an
|
||||
// echo, web or e-mail server is there either way, switched on or off (it can be
|
||||
// switched at run time). The programs read the nodes' files (a web server its
|
||||
// pages).
|
||||
// pages); the command line changes them, and as they are the project's own,
|
||||
// a project saved after the run holds the change, as in Java.
|
||||
//
|
||||
// Back the other way: what the simulation changes that Java keeps in the
|
||||
// project (its objects are the project's), so a project saved after it holds
|
||||
@@ -38,6 +39,7 @@ export function projectSnapshot(project: ImportedProject): TopologySnapshot {
|
||||
emailServers.set(nodeId, { domain: configuration.domain, accounts: configuration.accounts, isActive: project.activePrograms.get(nodeId)?.has('emailServer') === true });
|
||||
}
|
||||
const emailClients = new Map([...project.emailClientConfigurations].filter(([nodeId]) => project.installedPrograms.get(nodeId)?.has('emailClient') === true));
|
||||
const terminals = new Set([...project.installedPrograms].filter(([, programs]) => programs.has('commandPrompt')).map(([nodeId]) => nodeId));
|
||||
const webBrowsers = new Set([...project.installedPrograms].filter(([, programs]) => programs.has('webBrowser')).map(([nodeId]) => nodeId));
|
||||
return topologySnapshot({
|
||||
nodes: project.graph.nodes.map((node) => ({
|
||||
@@ -63,6 +65,7 @@ export function projectSnapshot(project: ImportedProject): TopologySnapshot {
|
||||
webBrowserNodeIds: webBrowsers,
|
||||
emailServersByNodeId: emailServers,
|
||||
emailClientsByNodeId: emailClients,
|
||||
terminalNodeIds: terminals,
|
||||
fileSystemsByNodeId: project.virtualFileSystems,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -857,6 +857,22 @@ describe('importProject rules', () => {
|
||||
expect(client('Rechner 3').account).toEqual(project.emailClientConfigurations.get(receiver)!.account);
|
||||
});
|
||||
|
||||
it("keeps what the command line did to a node's files in the project saved after the run", () => {
|
||||
const name = 'java-examples/Internet_Komplett_mit_eMail_Webserver_Intranet_Portforwarding_Firewall_DHCP_DE.fls';
|
||||
const project = openProjectArchive(new Uint8Array(readFileSync(join(corpusRoot, name))), counter());
|
||||
const notebook = project.graph.nodes.find((each) => each.displayName === 'Notebook' && project.deviceConfigurations.get(each.id)?.ipAddress === '10.10.20.11')!.id;
|
||||
const snapshot = projectSnapshot(project);
|
||||
expect(snapshot.terminalNodeIds.has(notebook)).toBe(true);
|
||||
const engine = new NetworkRuntimeEngine(14);
|
||||
engine.handle({ type: 'start', snapshot, seed: 14, initialTimeMilliseconds: 0 });
|
||||
for (const line of ['mkdir Projekt', 'cd Projekt', 'touch plan.txt']) engine.terminal(notebook)!.enter(line);
|
||||
engine.handle({ type: 'advance', toMilliseconds: 1_000 });
|
||||
|
||||
const reopened = openProjectArchive(saveProjectArchive(projectWithSimulationChanges(project, engine.state.topologySnapshot)).archive, counter());
|
||||
const reopenedNotebook = reopened.graph.nodes.find((each) => each.displayName === 'Notebook' && reopened.deviceConfigurations.get(each.id)?.ipAddress === '10.10.20.11')!.id;
|
||||
expect(reopened.virtualFileSystems.get(reopenedNotebook)!.entry('/Projekt/plan.txt').content).toEqual({ kind: 'text', text: '' });
|
||||
});
|
||||
|
||||
it("keeps a node's IP forwarding where it differs from its kind's: a host that forwards, a router that does not", () => {
|
||||
const system = (ipForwardingEnabled: boolean) => ({ manualRoutes: [], ipForwardingEnabled });
|
||||
const project = importProject(
|
||||
|
||||
@@ -86,3 +86,5 @@ packages/core/test/routerWebJava.test.ts::konfig
|
||||
packages/core/src/runtime/apps/emailServer.ts::smtpmitarbeiter
|
||||
packages/core/src/runtime/apps/emailServer.ts::pop3mitarbeiter
|
||||
packages/core/src/runtime/apps/emailClient.ts::smtpclient
|
||||
# arpsend: a command only FILIUS' terminal has (send an ARP reply); named as Java names it.
|
||||
packages/core/src/runtime/apps/::arpsend
|
||||
|
||||
@@ -764,3 +764,32 @@ Gemessen (01.10.2026):
|
||||
* Wer von zwei fast gleichzeitig gebauten Sockets die nächste
|
||||
TCP-Startnummer zieht, entscheidet der Thread-Takt (Programm und
|
||||
Weiterleitung des Servers, wenige Millisekunden auseinander).
|
||||
|
||||
## Befehlszeile, lokale Befehle (F3.4a)
|
||||
|
||||
`TerminalCases` füttert Javas `Terminal` mit Zeilen, wie sie getippt
|
||||
werden, und zerlegt sie wie das Fenster (`GUIApplicationTerminalWindow`:
|
||||
erstes Wort der Befehl, dann die Parameter, mindestens drei leere dazu;
|
||||
`exit`, `cls`, `reset` und leere Zeilen behandelt das Fenster selbst). Ohne
|
||||
laufendes Netz, auf Rechner 10.0.0.2 von `router-web` mit festen Dateien.
|
||||
Geschrieben werden je Zeile, was das Programm dem Fenster sagt, der
|
||||
Arbeitsordner und alle Dateien, dazu die Terminal-Texte des Bündels, wie
|
||||
Java sie lädt (daraus `terminalTexts.ts`).
|
||||
|
||||
Gemessen (01.10.2026):
|
||||
|
||||
* Java ruft jede Methode des Terminals mit `String[]`-Parameter auf, deren
|
||||
Name der Befehl ist, mit Groß- und Kleinschreibung (`LS` ist unbekannt).
|
||||
* Ordner listet `ls` in der Reihenfolge, in der die Einträge entstanden;
|
||||
Pfade findet Java ohne Rücksicht auf Groß- und Kleinschreibung und zeigt
|
||||
die gespeicherte Schreibweise.
|
||||
* Die Größe einer Datei: Java dekodiert jeden Inhalt als Base64 und zählt
|
||||
die Bytes, nur bei einem Fehler die Zeichen; „Hallo Welt“ hat 6.
|
||||
* Fehler: `ls` auf einen fehlenden Ordner sagt „leer“, `dir /` schreibt
|
||||
„Verzeichnis :“; `cd` auf eine Datei macht sie zum Arbeitsordner; `touch`
|
||||
und `mkdir` in einem fehlenden Ordner melden „erstellt“ bzw. „bereits
|
||||
vorhanden“; `cp` auf einen Ordnernamen wirft; `..` über der Wurzel findet
|
||||
nichts oder wirft; `rm` (und das Löschen bei `mv`) hängt einen absoluten
|
||||
Pfad an den Arbeitsordner, `rm /` löscht den Arbeitsordner und meldet
|
||||
Erfolg; `mv` auf die Datei selbst (auch in anderer Schreibweise) löscht
|
||||
sie.
|
||||
|
||||
@@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then
|
||||
fi
|
||||
classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
|
||||
mkdir -p "$here/.build"
|
||||
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java"
|
||||
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"
|
||||
# FILIUS writes its settings below $HOME; keep them out of the real one.
|
||||
mkdir -p "$here/.build/home"
|
||||
# Removed first: a crashed run must not leave the previous result standing.
|
||||
@@ -276,6 +276,16 @@ for run in send:15000 fetch:10000 byName:35000 wrongPass:8000 noServer:15000 unk
|
||||
test -s "$output"
|
||||
done
|
||||
|
||||
# The command line fed with lines as typed, without a running network, in
|
||||
# both languages (TerminalCases; packages/core/test/terminal.test.ts).
|
||||
for language in de en; do
|
||||
output="$root/packages/core/test/terminal.$language.java.json"
|
||||
rm -f "$output"
|
||||
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" TerminalCases "$language" \
|
||||
"$root/packages/core/test/scenarios/router-web.scenario.json" "$root/packages/core/test/terminal.cases.txt" "$output" 2>&1 | grep -v 'logback\|^$' || true
|
||||
test -s "$output"
|
||||
done
|
||||
|
||||
# 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).
|
||||
for run in internetwork_komplett:routing:12000 einfaches_rechnernetz_komplett:dhcp:20000 dns_server:dns:12000 \
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
// Feeds FILIUS' command line (Terminal, with the input handling of its window
|
||||
// GUIApplicationTerminalWindow) with lines as typed, without a running
|
||||
// network, and writes for each line what the program told the window (each
|
||||
// text and each Boolean "busy" mark), what it returned, the working directory
|
||||
// after it, and every file and directory of the host: the reference for the
|
||||
// core's command line (packages/core/src/runtime/apps/terminal.ts). It also
|
||||
// writes the terminal's texts of the message bundle, as Java loads them.
|
||||
// Each case starts on host 10.0.0.2 of a scenario (ScenarioNetwork) with the
|
||||
// files below and a new Terminal. The cases file is ASCII, with Java-style
|
||||
// unicode escapes (backslash, u, four hex digits) for anything else:
|
||||
//
|
||||
// case <name>
|
||||
// <line as typed>
|
||||
//
|
||||
// Usage: java -cp filius.jar:lib/*:classes TerminalCases <de|en> <scenario.json> <cases txt> <output json>
|
||||
|
||||
import filius.rahmenprogramm.Base64;
|
||||
import filius.rahmenprogramm.Information;
|
||||
import filius.software.lokal.Terminal;
|
||||
import filius.software.system.Datei;
|
||||
import filius.software.system.Dateisystem;
|
||||
import filius.software.system.InternetKnotenBetriebssystem;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.TreeMap;
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
import javax.swing.tree.TreeNode;
|
||||
|
||||
public class TerminalCases {
|
||||
static final String HOST = "00000000-0000-0000-0000-000000000301";
|
||||
|
||||
/** A tiny PNG (1 x 1 pixel), as FILIUS keeps images: Base64. */
|
||||
static final String IMAGE = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
|
||||
|
||||
/** The image as FILIUS writes a file's bytes (Base64.encodeBytes, with its line breaks). */
|
||||
static String imageAsFiliusWritesIt() {
|
||||
try {
|
||||
return Base64.encodeBytes(Base64.decode(IMAGE));
|
||||
} catch (Exception error) {
|
||||
throw new IllegalStateException(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** The host's files at the start of each case, in this order. */
|
||||
static void files(Dateisystem files) {
|
||||
files.getRoot().removeAllChildren();
|
||||
files.speicherDatei("/", new Datei("notizen.txt", "txt", "Hallo Welt"));
|
||||
files.erstelleVerzeichnis("/", "Dokumente");
|
||||
files.speicherDatei("/Dokumente", new Datei("brief.txt", "txt", "Sehr geehrte Damen und Herren,\nviele Grüße"));
|
||||
files.erstelleVerzeichnis("/Dokumente", "Alt");
|
||||
files.speicherDatei("/", new Datei("bild.png", "png", imageAsFiliusWritesIt()));
|
||||
files.speicherDatei("/", new Datei("kurz.txt", "txt", "SGFsbG8="));
|
||||
files.erstelleVerzeichnis("/", "leer");
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
// Before any class reads the message bundle (I18n.messages is read once).
|
||||
Information.getInformation().setLocale(args[0].equals("en") ? new Locale("en", "GB") : Locale.GERMANY);
|
||||
ScenarioNetwork network = ScenarioNetwork.read(args[1]);
|
||||
InternetKnotenBetriebssystem system = network.system(HOST);
|
||||
List<Object> cases = new ArrayList<>();
|
||||
List<Object> steps = null;
|
||||
Terminal terminal = null;
|
||||
List<Object> told = new ArrayList<>();
|
||||
for (String line : Files.readAllLines(Path.of(args[2]), StandardCharsets.US_ASCII)) {
|
||||
if (line.startsWith("#")) continue;
|
||||
if (line.startsWith("case ")) {
|
||||
Map<String, Object> current = new LinkedHashMap<>();
|
||||
steps = new ArrayList<>();
|
||||
current.put("name", line.substring("case ".length()));
|
||||
current.put("steps", steps);
|
||||
cases.add(current);
|
||||
files(system.getDateisystem());
|
||||
system.getInstallierteAnwendungen().remove("filius.software.lokal.Terminal");
|
||||
system.installApp("filius.software.lokal.Terminal", false);
|
||||
terminal = (Terminal) system.holeSoftware("filius.software.lokal.Terminal");
|
||||
terminal.setSystemSoftware(system);
|
||||
terminal.hinzuBeobachter((observable, data) -> {
|
||||
if (data instanceof Boolean) told.add(Arrays.asList("busy", data));
|
||||
else if (data != null) told.add(Arrays.asList("text", data.toString()));
|
||||
});
|
||||
current.put("start", tree(system.getDateisystem()));
|
||||
continue;
|
||||
}
|
||||
String typed = IpCheckCases.unescape(line);
|
||||
Map<String, Object> step = new LinkedHashMap<>();
|
||||
step.put("typed", typed);
|
||||
told.clear();
|
||||
Object returned = null;
|
||||
String window = null;
|
||||
// GUIApplicationTerminalWindow: the first word is the command, the others its parameters, then at least three empty ones.
|
||||
if (typed.trim().isEmpty()) {
|
||||
window = "blank";
|
||||
} else {
|
||||
StringTokenizer tokens = new StringTokenizer(typed, " ");
|
||||
String command = tokens.nextToken();
|
||||
String[] parameters = new String[3 + tokens.countTokens()];
|
||||
Arrays.fill(parameters, "");
|
||||
for (int index = 0; tokens.hasMoreTokens(); index++) parameters[index] = tokens.nextToken();
|
||||
if (command.equals("exit")) {
|
||||
window = "exit";
|
||||
} else if (command.equals("reset") || command.equals("cls")) {
|
||||
window = "clear";
|
||||
} else {
|
||||
// Terminal.terminalEingabeAuswerten, but in this thread instead of the program's.
|
||||
try {
|
||||
Method method = Terminal.class.getDeclaredMethod(command, String[].class);
|
||||
terminal.setInterrupt(false);
|
||||
returned = method.invoke(terminal, (Object) parameters);
|
||||
} catch (NoSuchMethodException error) {
|
||||
terminal.benachrichtigeBeobachter(terminal.messages.getString("terminal_msg2") + "\n" + terminal.messages.getString("terminal_msg3"));
|
||||
} catch (InvocationTargetException error) {
|
||||
returned = "!" + error.getCause().getClass().getSimpleName();
|
||||
}
|
||||
}
|
||||
}
|
||||
step.put("window", window);
|
||||
step.put("told", new ArrayList<>(told));
|
||||
step.put("returned", returned == null ? null : returned.toString());
|
||||
step.put("directory", Dateisystem.absoluterPfad(terminal.getAktuellerOrdner()));
|
||||
step.put("files", tree(system.getDateisystem()));
|
||||
steps.add(step);
|
||||
}
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("cases", cases);
|
||||
ResourceBundle bundle = terminal.messages;
|
||||
Map<String, Object> texts = new TreeMap<>();
|
||||
for (String key : Collections.list(bundle.getKeys())) {
|
||||
if (key.startsWith("sw_terminal") || key.startsWith("terminal_msg")) texts.put(key, bundle.getString(key));
|
||||
}
|
||||
out.put("texts", texts);
|
||||
try (FileOutputStream stream = new FileOutputStream(args[3])) {
|
||||
stream.write((FlsOracle.Json.write(out, "") + "\n").getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
System.out.println("terminal cases (" + args[0] + "): " + cases.size());
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
/** Every node below the root in preorder: path, and for a file its type, content and size (Datei.holeGroesse). */
|
||||
static List<Object> tree(Dateisystem files) {
|
||||
List<Object> nodes = new ArrayList<>();
|
||||
Enumeration<TreeNode> all = files.getRoot().preorderEnumeration();
|
||||
while (all.hasMoreElements()) {
|
||||
DefaultMutableTreeNode node = (DefaultMutableTreeNode) all.nextElement();
|
||||
if (node == files.getRoot()) continue;
|
||||
if (node.getUserObject() instanceof Datei) {
|
||||
Datei file = (Datei) node.getUserObject();
|
||||
nodes.add(Arrays.asList(Dateisystem.absoluterPfad(node), file.getDateiTyp(), file.getDateiInhalt(), file.holeGroesse()));
|
||||
} else {
|
||||
nodes.add(Arrays.asList(Dateisystem.absoluterPfad(node)));
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user