feat(editor): F4.6c the software installation on a computer's desktop
A new oracle, InstallRun, installs each of Java's programs on new computers, all of them on one, each installed and removed again, and a DNS and an e-mail server installed again after their removal; Java saves the result as a project. A new program is switched off (a computer's firewall is on), makes the folders and files it lacks (the web server's start page in FILIUS' language and its picture, /peer2peer, /mailserver/konten.txt, /dns/hosts), and removing it takes its settings and leaves its files; a server installed again reads its records or accounts from them. Java installs in the order of its list and starts the program at once. The document's setSoftware does the same, checked against Java's saved installation. The core knows what a program makes in the files and installs or removes a program while the simulation runs; the room watches the document's programs and hands changes to the engine, so a new command line works at once. The desktop offers Java's dialog with two lists, arrows, double click and apply changes. Also: Java's file may name a number it rolled by the result of any getter (an idref, here a thread's priority); GetterDefaults now measures the priority of the saved thread classes, which the import read as 0 before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+6
-1
File diff suppressed because one or more lines are too long
@@ -19,6 +19,7 @@ export * from './runtime/capture.js';
|
|||||||
export * from './runtime/captureRows.js';
|
export * from './runtime/captureRows.js';
|
||||||
export * from './runtime/threads.js';
|
export * from './runtime/threads.js';
|
||||||
export * from './runtime/apps/program.js';
|
export * from './runtime/apps/program.js';
|
||||||
|
export * from './runtime/apps/installation.js';
|
||||||
export * from './runtime/apps/server.js';
|
export * from './runtime/apps/server.js';
|
||||||
export * from './runtime/apps/echo.js';
|
export * from './runtime/apps/echo.js';
|
||||||
export * from './runtime/apps/web.js';
|
export * from './runtime/apps/web.js';
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// What FILIUS' programs bring into a computer's files when the software
|
||||||
|
// installation installs them (measured with tools/java-oracle's InstallRun):
|
||||||
|
// a web server its folder /webserver with Java's start page and picture
|
||||||
|
// (WebServer.erzeugeStandardVerzeichnis, the page in the language FILIUS
|
||||||
|
// runs in), Gnutella its folder /peer2peer (PeerToPeerAnwendung), an e-mail
|
||||||
|
// server /mailserver with an empty konten.txt (EmailServer), a DNS server
|
||||||
|
// /dns with an empty hosts (DNSServer). Each is made only where nothing of
|
||||||
|
// the name is: a file left by a program removed before stays as it is, and
|
||||||
|
// the program reads it (a DNS server its records, an e-mail server its
|
||||||
|
// accounts). Removing a program leaves its files.
|
||||||
|
|
||||||
|
import type { InstallableProgram } from '../config.js';
|
||||||
|
import type { VirtualFileContent } from '../../model/virtualFileSystem.js';
|
||||||
|
import { javaBase64Decode } from '../../util/javaBase64.js';
|
||||||
|
import type { PageLanguage } from './routerPages.js';
|
||||||
|
|
||||||
|
export interface InstallationFile {
|
||||||
|
readonly path: string;
|
||||||
|
readonly content: VirtualFileContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** tmpl/webserver_index_<language>.txt as Java reads it: each line ended by "\n". */
|
||||||
|
const WEB_SERVER_INDEX: Record<PageLanguage, string> = {
|
||||||
|
de: "<html>\n <head>\n <title>Standardseite</title>\n </head>\n <body bgcolor=\"#ccddff\" style=\"font-family:Verdana; text-align:center;\">\n <h2> FILIUS - Webserver </h2>\n\n <p>Herzlich Willkommen auf dem Webserver der Anwendung FILIUS!</p>\n\n <p> Diese Seite wurde automatisch mit der Installation des \n Webservers eingerichtet, es lassen sich jedoch auch \n eigene Seiten hier unterbringen. </p>\n\n <p align=\"center\"> <img src=\"splashscreen-mini.png\"> </p>\n\n <p> https://www.lernsoftware-filius.de </p>\n </body>\n</html>\n",
|
||||||
|
en: "<html>\n <head>\n <title>Standardseite</title>\n </head>\n <body bgcolor=\"#ccddff\" style=\"font-family:Verdana; text-align:center;\">\n <h2> FILIUS - Webserver </h2>\n\n <p>Welcome at FILIUS!</p>\n\n <p> This page has been delivered as default page.\n Feel free to add your own contents! </p>\n\n <p align=\"center\"> <img src=\"splashscreen-mini.png\"> </p>\n\n <p> https://www.lernsoftware-filius.de </p>\n </body>\n</html>\n",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** gfx/allgemein/splashscreen-mini.png, the picture on the web server's start page. */
|
||||||
|
const SPLASH_PNG =
|
||||||
|
"iVBORw0KGgoAAAANSUhEUgAAAIAAAABgCAYAAADVenpJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUw" +
|
||||||
|
"AADqYAAAOpgAABdwnLpRPAAAAB90RVh0U29mdHdhcmUATWFjcm9tZWRpYSBGaXJld29ya3MgOLVo0ngAABebSURBVHhe7Z13sFRFFsYvWorigjxWBNct1/3D" +
|
||||||
|
"UmuroAxlSWHABVRETCgiKKJEwUTSV4gYQAQREAXEQBITKGYUMbEqggkzSRRFBcWAGZBwdn6N39iO83jzZu69b3jcruq6M307nvP1Oafjrfb6669bkLjtlwJv" +
|
||||||
|
"vvmmJX77pUHwxhtvWBheIAojr209D2iBZMVna0sx0SoIq/fT2FdeeSWRJr8zf9myZbZkyRIHgEwaz58/31599dWioFWwYMECK9TTwEWLFhmO37nkN2/evJzj" +
|
||||||
|
"5pJfrnFee+01R/xc4+cTDwZv2LDBfvjhh7+URdm//vqrffvtt0Zd8sk/zDShAACUg3YBABD4og+CICGoOO/eeustF/edd95Jh/OOOH5cNZRw0kEw5aMwpaE8" +
|
||||||
|
"nzD8Jy/SCJQQ/5tvvrHvv//eSSviKD/Vzf+vNvj1UP18oCseaSmDOOvWrbPvvvsuXb5f308++cQ+/PDDtHTw2yfJ4LfFp4vihgWCAGYU6qmMAMBvCPLFF184" +
|
||||||
|
"qUBj169fb1999ZUjDgxZs2aNAwDMIJ6YRRx6zurVq1086sWT/++//77rUcRBenz99df27rvvuncQG5FLPqSBAR9//LErF2aLWNRn8+bNtnHjRvvyyy9dGtKL" +
|
||||||
|
"mYsXL3b5kl5t+uyzz9JAIe5vv/3meq/y9NtKnpT50ksvuacAANDJl/JgIHl+9NFHLl/aRb7kR/t++eUXe++999x/tYV8ocuqVascvZYuXZp+XyjvgrffftsK" +
|
||||||
|
"9T4AqBCMhgByn376qfu5YsUK9w7G42AmYfRGmEgjpUpgHATinRxhMBYCy/3000+OITjKJg154sgLguJefvll958yYCKEhPE4mAGoeIcDWCqXuuu36rd27VoX" +
|
||||||
|
"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" +
|
||||||
|
"RdcUuS8BYL56PEaZ5gESAETMmCh6VLY8AQB7Bu699163rxMRT68mHEOQJwtl/oqoL/6jlACUkxiBERvBOp+www472LBhw9JnFfihhSGeshEkAbTTSAAI2wiU" +
|
||||||
|
"cZmogAglDUwTAPbaay875ZRTnL/77rvTNgHLxgAACUBcjEMdE9fhmqikFeUFDEkSXzgNpNfR55wqYijHGJ4JHR1Rb9GihZ199tl20UUX2fPPP+9UgQCg+YCH" +
|
||||||
|
"H37YRo0a5aTFPffc4/IBFFHwCIAGVCLxFaeBGALDdaYfMT558mS77rrrbNCgQTZu3Di76aabnNjv0qWLe86ePdv69OljMBrGAgDyuv/++61Ro0Z2+umnW7t2" +
|
||||||
|
"7eycc85xz6OPPtqBKAoeOQBEgayqnKd6uno1jLviiits8ODBTrQjtjm3CNM0vw/ja9WqZeeff7479yeH6AcAMAJAnHrqqdaxY0fnO3XqZJ07d7YzzjjDHTBh" +
|
||||||
|
"u3jYdE0kQAWlH0yAgezmKS0tdaJaR8cZ66O76anaJazfMLxhw4Z28sknOyYDGPYLwgAAwPAQtdG8efOsAGCnMCoibCkAmBMVkCMIYDxbqXr16uXENU5Gms/w" +
|
||||||
|
"TCbxTtflkPakk05yNsCzzz7rGMoEEXmjMs4888z4AcBYNPFl04BejevRo0ea8YShv3OhGxM9/j1H9Pa77rrLXfaABAAcQ4cOtZYtW1qHDh3+BADshlatWrlL" +
|
||||||
|
"LrA1cimvInEAZ0BDEv9XGtA7dUWMDDiJ+IrQizSPPvqojRgxIq37H3/88fTVNcOHDzeNDgSA8847z/CohLFjx7rRREXKzDWuAwB6LfF/poFuALnlllvsjjvu" +
|
||||||
|
"cIwjLB86odufeeYZ69mzp+vhDO+efPJJd2/Brbfe6piMxd++fXv3HsafdtppzmDU3UT5ll1efQF5AoCMDiDm33DDDemrawphAHYClztcfPHF1rt3bzcchOHY" +
|
||||||
|
"AkwKnXXWWW5uAM+w74gjjrAHHnjAgQ71UUjZCQAqKN0gNkO422+/3eltxveFMgAmYujBeIzACy+80PVyejwMb9u2rWM+kgCQcCEEewEKLbc85vM+kQAZAKG3" +
|
||||||
|
"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" +
|
||||||
|
"TkSuQmCC";
|
||||||
|
|
||||||
|
const folder = (path: string): InstallationFile => ({ path, content: { kind: 'directory' } });
|
||||||
|
const text = (path: string, value: string): InstallationFile => ({ path, content: { kind: 'text', text: value } });
|
||||||
|
|
||||||
|
/** The folders and files a program makes when it is installed, in the order Java makes them. */
|
||||||
|
export function installationFiles(program: InstallableProgram, language: PageLanguage): readonly InstallationFile[] {
|
||||||
|
switch (program) {
|
||||||
|
case 'webServer':
|
||||||
|
return [
|
||||||
|
folder('/webserver'),
|
||||||
|
text('/webserver/index.html', WEB_SERVER_INDEX[language]),
|
||||||
|
{ path: '/webserver/splashscreen-mini.png', content: { kind: 'image', data: javaBase64Decode(SPLASH_PNG)!, mediaType: 'image/png' } },
|
||||||
|
];
|
||||||
|
case 'gnutella':
|
||||||
|
return [folder('/peer2peer')];
|
||||||
|
case 'emailServer':
|
||||||
|
return [folder('/mailserver'), text('/mailserver/konten.txt', '')];
|
||||||
|
case 'dnsServer':
|
||||||
|
return [folder('/dns'), text('/dns/hosts', '')];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,6 +76,7 @@ import {
|
|||||||
FIREWALL_PROTOCOL,
|
FIREWALL_PROTOCOL,
|
||||||
firewallConfiguration,
|
firewallConfiguration,
|
||||||
interfaceKey,
|
interfaceKey,
|
||||||
|
type InstallableProgram,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
import { formatIPv4, maskAddress, parseIPv4, sameSubnet } from './ipv4.js';
|
import { formatIPv4, maskAddress, parseIPv4, sameSubnet } from './ipv4.js';
|
||||||
import { javaIpCheck, javaParseInt } from '../util/javaIp.js';
|
import { javaIpCheck, javaParseInt } from '../util/javaIp.js';
|
||||||
@@ -112,6 +113,7 @@ import { ICMP_TYPE_CODE } from './captureRows.js';
|
|||||||
import { JavaCompatibleRandom } from './random.js';
|
import { JavaCompatibleRandom } from './random.js';
|
||||||
import { routeTableRows, type RouteRow } from './routeTable.js';
|
import { routeTableRows, type RouteRow } from './routeTable.js';
|
||||||
import { EMPTY_TOPOLOGY_SNAPSHOT, withSnapshot, type NodeSnapshot, type TopologySnapshot } from './snapshot.js';
|
import { EMPTY_TOPOLOGY_SNAPSHOT, withSnapshot, type NodeSnapshot, type TopologySnapshot } from './snapshot.js';
|
||||||
|
import { installationFiles } from './apps/installation.js';
|
||||||
import { RuntimeSpeed } from './speed.js';
|
import { RuntimeSpeed } from './speed.js';
|
||||||
import { ApplicationThreads, waitUntil, type Steps, type Wait } from './threads.js';
|
import { ApplicationThreads, waitUntil, type Steps, type Wait } from './threads.js';
|
||||||
import { EchoServer, SimpleClient } from './apps/echo.js';
|
import { EchoServer, SimpleClient } from './apps/echo.js';
|
||||||
@@ -874,6 +876,10 @@ function getOrCreate<K, V>(map: Map<K, V>, key: K, create: () => V): V {
|
|||||||
|
|
||||||
// ── Engine ─────────────────────────────────────────────────────────────────
|
// ── Engine ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** The programs the engine runs (the others — file explorer, image viewer, text editor — are the editor's), in the order a start makes them. */
|
||||||
|
const RUNNING_PROGRAMS = ['echoServer', 'simpleClient', 'webServer', 'webBrowser', 'emailServer', 'commandPrompt', 'gnutella', 'emailClient'] as const;
|
||||||
|
type RunningProgram = (typeof RUNNING_PROGRAMS)[number];
|
||||||
|
|
||||||
export class NetworkRuntimeEngine {
|
export class NetworkRuntimeEngine {
|
||||||
private stateValue: RuntimeState;
|
private stateValue: RuntimeState;
|
||||||
private random: JavaCompatibleRandom;
|
private random: JavaCompatibleRandom;
|
||||||
@@ -1247,18 +1253,52 @@ export class NetworkRuntimeEngine {
|
|||||||
this.terminalsByNodeId.clear();
|
this.terminalsByNodeId.clear();
|
||||||
this.gnutellaPeersByNodeId.clear();
|
this.gnutellaPeersByNodeId.clear();
|
||||||
for (const node of snapshot.nodes) {
|
for (const node of snapshot.nodes) {
|
||||||
const echoServer = snapshot.echoServersByNodeId.get(node.id);
|
for (const program of RUNNING_PROGRAMS) if (this.hasProgram(node.id, program)) this.startProgram(node.id, program);
|
||||||
if (echoServer !== undefined) this.echoServersByNodeId.set(node.id, new EchoServer(this, node.id, echoServer));
|
if ((node.kind === 'router' || node.kind === 'gateway') && !this.swiftRouters) this.startRouterWebServer(node.id, node.kind);
|
||||||
const simpleClient = snapshot.simpleClientsByNodeId.get(node.id);
|
}
|
||||||
if (simpleClient !== undefined) this.simpleClientsByNodeId.set(node.id, new SimpleClient(this, node.id, simpleClient));
|
}
|
||||||
const webServer = snapshot.webServersByNodeId.get(node.id);
|
|
||||||
if (webServer !== undefined) {
|
/** Whether the snapshot gives a node a program the engine runs. */
|
||||||
this.webServersByNodeId.set(node.id, new WebServer(this, node.id, webServer, snapshot.fileSystemsByNodeId.get(node.id)));
|
private hasProgram(nodeId: Id, program: RunningProgram): boolean {
|
||||||
}
|
const snapshot = this.stateValue.topologySnapshot;
|
||||||
if (snapshot.webBrowserNodeIds.has(node.id)) this.webBrowsersByNodeId.set(node.id, new WebBrowser(this, node.id));
|
switch (program) {
|
||||||
const emailServer = snapshot.emailServersByNodeId.get(node.id);
|
case 'echoServer':
|
||||||
if (emailServer !== undefined) {
|
return snapshot.echoServersByNodeId.has(nodeId);
|
||||||
const nodeId = node.id;
|
case 'simpleClient':
|
||||||
|
return snapshot.simpleClientsByNodeId.has(nodeId);
|
||||||
|
case 'webServer':
|
||||||
|
return snapshot.webServersByNodeId.has(nodeId);
|
||||||
|
case 'webBrowser':
|
||||||
|
return snapshot.webBrowserNodeIds.has(nodeId);
|
||||||
|
case 'emailServer':
|
||||||
|
return snapshot.emailServersByNodeId.has(nodeId);
|
||||||
|
case 'commandPrompt':
|
||||||
|
return snapshot.terminalNodeIds.has(nodeId);
|
||||||
|
case 'gnutella':
|
||||||
|
return snapshot.gnutellaByNodeId.has(nodeId);
|
||||||
|
case 'emailClient':
|
||||||
|
return snapshot.emailClientsByNodeId.has(nodeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Starts a node's program from the snapshot (Anwendung.starten); what it shows starts empty. */
|
||||||
|
private startProgram(nodeId: Id, program: RunningProgram): void {
|
||||||
|
const snapshot = this.stateValue.topologySnapshot;
|
||||||
|
switch (program) {
|
||||||
|
case 'echoServer':
|
||||||
|
this.echoServersByNodeId.set(nodeId, new EchoServer(this, nodeId, snapshot.echoServersByNodeId.get(nodeId)!));
|
||||||
|
return;
|
||||||
|
case 'simpleClient':
|
||||||
|
this.simpleClientsByNodeId.set(nodeId, new SimpleClient(this, nodeId, snapshot.simpleClientsByNodeId.get(nodeId)!));
|
||||||
|
return;
|
||||||
|
case 'webServer':
|
||||||
|
this.webServersByNodeId.set(nodeId, new WebServer(this, nodeId, snapshot.webServersByNodeId.get(nodeId)!, snapshot.fileSystemsByNodeId.get(nodeId)));
|
||||||
|
return;
|
||||||
|
case 'webBrowser':
|
||||||
|
this.webBrowsersByNodeId.set(nodeId, new WebBrowser(this, nodeId));
|
||||||
|
return;
|
||||||
|
case 'emailServer': {
|
||||||
|
const emailServer = snapshot.emailServersByNodeId.get(nodeId)!;
|
||||||
this.emailServersByNodeId.set(
|
this.emailServersByNodeId.set(
|
||||||
nodeId,
|
nodeId,
|
||||||
new EmailServer(
|
new EmailServer(
|
||||||
@@ -1271,24 +1311,128 @@ export class NetworkRuntimeEngine {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
this.writeEmailServerAccountsFile(nodeId, emailServer.domain, emailServer.accounts);
|
this.writeEmailServerAccountsFile(nodeId, emailServer.domain, emailServer.accounts);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (snapshot.terminalNodeIds.has(node.id)) this.terminalsByNodeId.set(node.id, new Terminal(this, node.id, this.nodeFiles(node.id)));
|
case 'commandPrompt':
|
||||||
const gnutella = snapshot.gnutellaByNodeId.get(node.id);
|
this.terminalsByNodeId.set(nodeId, new Terminal(this, nodeId, this.nodeFiles(nodeId)));
|
||||||
if (gnutella !== undefined) {
|
return;
|
||||||
|
case 'gnutella': {
|
||||||
// PeerToPeerAnwendung.setSystemSoftware: the folder of the files to share.
|
// PeerToPeerAnwendung.setSystemSoftware: the folder of the files to share.
|
||||||
const files = this.nodeFiles(node.id);
|
const files = this.nodeFiles(nodeId);
|
||||||
if (!files.contains(GNUTELLA_FOLDER)) files.createDirectory(GNUTELLA_FOLDER);
|
if (!files.contains(GNUTELLA_FOLDER)) files.createDirectory(GNUTELLA_FOLDER);
|
||||||
this.gnutellaPeersByNodeId.set(node.id, new GnutellaPeer(this, node.id, gnutella));
|
this.gnutellaPeersByNodeId.set(nodeId, new GnutellaPeer(this, nodeId, snapshot.gnutellaByNodeId.get(nodeId)!));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const emailClient = snapshot.emailClientsByNodeId.get(node.id);
|
case 'emailClient':
|
||||||
if (emailClient !== undefined) {
|
this.emailClientsByNodeId.set(nodeId, new EmailClient(this, nodeId, snapshot.emailClientsByNodeId.get(nodeId)!, (folders) => this.setEmailClientFolders(nodeId, folders)));
|
||||||
const nodeId = node.id;
|
return;
|
||||||
this.emailClientsByNodeId.set(nodeId, new EmailClient(this, nodeId, emailClient, (folders) => this.setEmailClientFolders(nodeId, folders)));
|
|
||||||
}
|
|
||||||
if ((node.kind === 'router' || node.kind === 'gateway') && !this.swiftRouters) this.startRouterWebServer(node.id, node.kind);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A program installed while the simulation runs (Java's software
|
||||||
|
* installation: installAndStartAppIfAvailable): its settings, and the
|
||||||
|
* folders and files it makes where the node has none of the name, as
|
||||||
|
* `source` has them — the document's snapshot after the installation —,
|
||||||
|
* and the program started. A program installed already is left as it is.
|
||||||
|
*/
|
||||||
|
installProgram(nodeId: Id, program: InstallableProgram, source: TopologySnapshot): void {
|
||||||
|
const state = this.stateValue;
|
||||||
|
if (state.phase !== 'running' || !state.topologySnapshot.nodes.some((node) => node.id === nodeId)) return;
|
||||||
|
const running = (RUNNING_PROGRAMS as readonly string[]).includes(program) ? (program as RunningProgram) : undefined;
|
||||||
|
if (running !== undefined && this.hasProgram(nodeId, running)) return;
|
||||||
|
// The node's files first: a node without any gets them now, in the snapshot taken after.
|
||||||
|
const files = this.nodeFiles(nodeId);
|
||||||
|
const snapshot = state.topologySnapshot;
|
||||||
|
const from = source.fileSystemsByNodeId.get(nodeId);
|
||||||
|
for (const file of installationFiles(program, snapshot.language)) {
|
||||||
|
if (files.contains(file.path) || from?.contains(file.path) !== true) continue;
|
||||||
|
const content = from.entry(file.path).content;
|
||||||
|
if (content.kind === 'directory') files.createDirectory(file.path, true);
|
||||||
|
else if (content.kind === 'text') files.writeTextFile(file.path, content.text, false);
|
||||||
|
else if (content.kind === 'image') files.writeImageFile(file.path, content.data, content.mediaType, false);
|
||||||
|
else files.writeBinaryFile(file.path, content.data, content.mediaType, false);
|
||||||
|
}
|
||||||
|
const copied = <T>(own: ReadonlyMap<Id, T>, theirs: ReadonlyMap<Id, T>): ReadonlyMap<Id, T> => {
|
||||||
|
const value = theirs.get(nodeId);
|
||||||
|
return value === undefined ? own : new Map([...own, [nodeId, value]]);
|
||||||
|
};
|
||||||
|
const added = (own: ReadonlySet<Id>, theirs: ReadonlySet<Id>): ReadonlySet<Id> => (theirs.has(nodeId) ? new Set([...own, nodeId]) : own);
|
||||||
|
state.topologySnapshot = withSnapshot(snapshot, {
|
||||||
|
echoServersByNodeId: program === 'echoServer' ? copied(snapshot.echoServersByNodeId, source.echoServersByNodeId) : snapshot.echoServersByNodeId,
|
||||||
|
simpleClientsByNodeId: program === 'simpleClient' ? copied(snapshot.simpleClientsByNodeId, source.simpleClientsByNodeId) : snapshot.simpleClientsByNodeId,
|
||||||
|
webServersByNodeId: program === 'webServer' ? copied(snapshot.webServersByNodeId, source.webServersByNodeId) : snapshot.webServersByNodeId,
|
||||||
|
webBrowserNodeIds: program === 'webBrowser' ? added(snapshot.webBrowserNodeIds, source.webBrowserNodeIds) : snapshot.webBrowserNodeIds,
|
||||||
|
emailServersByNodeId: program === 'emailServer' ? copied(snapshot.emailServersByNodeId, source.emailServersByNodeId) : snapshot.emailServersByNodeId,
|
||||||
|
emailClientsByNodeId: program === 'emailClient' ? copied(snapshot.emailClientsByNodeId, source.emailClientsByNodeId) : snapshot.emailClientsByNodeId,
|
||||||
|
terminalNodeIds: program === 'commandPrompt' ? added(snapshot.terminalNodeIds, source.terminalNodeIds) : snapshot.terminalNodeIds,
|
||||||
|
gnutellaByNodeId: program === 'gnutella' ? copied(snapshot.gnutellaByNodeId, source.gnutellaByNodeId) : snapshot.gnutellaByNodeId,
|
||||||
|
dnsServerConfigurationsByNodeId: program === 'dnsServer' ? copied(snapshot.dnsServerConfigurationsByNodeId, source.dnsServerConfigurationsByNodeId) : snapshot.dnsServerConfigurationsByNodeId,
|
||||||
|
firewallConfigurationsByNodeId: program === 'firewall' ? copied(snapshot.firewallConfigurationsByNodeId, source.firewallConfigurationsByNodeId) : snapshot.firewallConfigurationsByNodeId,
|
||||||
|
});
|
||||||
|
if (running !== undefined && this.hasProgram(nodeId, running)) this.startProgram(nodeId, running);
|
||||||
|
this.applicationThreads.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A program removed while the simulation runs (entferneSoftware): a server
|
||||||
|
* is switched off, the program and its settings go; its files stay.
|
||||||
|
*/
|
||||||
|
removeProgram(nodeId: Id, program: InstallableProgram): void {
|
||||||
|
const state = this.stateValue;
|
||||||
|
if (state.phase !== 'running') return;
|
||||||
|
const snapshot = state.topologySnapshot;
|
||||||
|
const without = <T>(own: ReadonlyMap<Id, T>): ReadonlyMap<Id, T> => new Map([...own].filter(([id]) => id !== nodeId));
|
||||||
|
const withoutId = (own: ReadonlySet<Id>): ReadonlySet<Id> => new Set([...own].filter((id) => id !== nodeId));
|
||||||
|
switch (program) {
|
||||||
|
case 'echoServer':
|
||||||
|
this.echoServersByNodeId.get(nodeId)?.setActive(false);
|
||||||
|
this.echoServersByNodeId.delete(nodeId);
|
||||||
|
state.topologySnapshot = withSnapshot(snapshot, { echoServersByNodeId: without(snapshot.echoServersByNodeId) });
|
||||||
|
break;
|
||||||
|
case 'simpleClient':
|
||||||
|
this.simpleClientsByNodeId.delete(nodeId);
|
||||||
|
state.topologySnapshot = withSnapshot(snapshot, { simpleClientsByNodeId: without(snapshot.simpleClientsByNodeId) });
|
||||||
|
break;
|
||||||
|
case 'webServer':
|
||||||
|
this.webServersByNodeId.get(nodeId)?.setActive(false);
|
||||||
|
this.webServersByNodeId.delete(nodeId);
|
||||||
|
state.topologySnapshot = withSnapshot(snapshot, { webServersByNodeId: without(snapshot.webServersByNodeId) });
|
||||||
|
break;
|
||||||
|
case 'webBrowser':
|
||||||
|
this.webBrowsersByNodeId.delete(nodeId);
|
||||||
|
state.topologySnapshot = withSnapshot(snapshot, { webBrowserNodeIds: withoutId(snapshot.webBrowserNodeIds) });
|
||||||
|
break;
|
||||||
|
case 'emailServer':
|
||||||
|
this.emailServersByNodeId.get(nodeId)?.setActive(false);
|
||||||
|
this.emailServersByNodeId.delete(nodeId);
|
||||||
|
state.topologySnapshot = withSnapshot(snapshot, { emailServersByNodeId: without(snapshot.emailServersByNodeId) });
|
||||||
|
break;
|
||||||
|
case 'emailClient':
|
||||||
|
this.emailClientsByNodeId.delete(nodeId);
|
||||||
|
state.topologySnapshot = withSnapshot(snapshot, { emailClientsByNodeId: without(snapshot.emailClientsByNodeId) });
|
||||||
|
break;
|
||||||
|
case 'commandPrompt':
|
||||||
|
this.terminalsByNodeId.get(nodeId)?.interrupt();
|
||||||
|
this.terminalsByNodeId.delete(nodeId);
|
||||||
|
state.topologySnapshot = withSnapshot(snapshot, { terminalNodeIds: withoutId(snapshot.terminalNodeIds) });
|
||||||
|
break;
|
||||||
|
case 'gnutella':
|
||||||
|
this.gnutellaPeersByNodeId.delete(nodeId);
|
||||||
|
state.topologySnapshot = withSnapshot(snapshot, { gnutellaByNodeId: without(snapshot.gnutellaByNodeId) });
|
||||||
|
break;
|
||||||
|
case 'dnsServer':
|
||||||
|
state.topologySnapshot = withSnapshot(snapshot, { dnsServerConfigurationsByNodeId: without(snapshot.dnsServerConfigurationsByNodeId) });
|
||||||
|
break;
|
||||||
|
case 'firewall':
|
||||||
|
state.topologySnapshot = withSnapshot(snapshot, { firewallConfigurationsByNodeId: without(snapshot.firewallConfigurationsByNodeId) });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.applicationThreads.resume();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VermittlungsrechnerBetriebssystem, GatewayFirmware: a router and a home
|
* VermittlungsrechnerBetriebssystem, GatewayFirmware: a router and a home
|
||||||
* router switch on their web server on port 80 when they start, with
|
* router switch on their web server on port 80 when they start, with
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
// A program installed or removed while the simulation runs (Java's software
|
||||||
|
// installation): its settings and files from the document's snapshot, the
|
||||||
|
// program started at once; removed, a server is switched off and the program
|
||||||
|
// goes, its files staying.
|
||||||
|
|
||||||
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { installationFiles, topologySnapshot, VirtualFileSystem, type TopologySnapshot } from '../src/index.js';
|
||||||
|
import { device, pc, startedEngine, uuid } from './fixtures.js';
|
||||||
|
|
||||||
|
const PC = uuid(1);
|
||||||
|
const PORT = uuid(2);
|
||||||
|
|
||||||
|
/** The document's snapshot after a web server and a command line were installed. */
|
||||||
|
function source(files: VirtualFileSystem): TopologySnapshot {
|
||||||
|
for (const file of installationFiles('webServer', 'de')) {
|
||||||
|
if (file.content.kind === 'directory') files.createDirectory(file.path);
|
||||||
|
else if (file.content.kind === 'text') files.writeTextFile(file.path, file.content.text);
|
||||||
|
else if (file.content.kind === 'image') files.writeImageFile(file.path, file.content.data, file.content.mediaType);
|
||||||
|
}
|
||||||
|
return topologySnapshot({
|
||||||
|
nodes: [pc(PC, PORT)],
|
||||||
|
links: [],
|
||||||
|
deviceConfigurations: new Map([[PC, device('10.0.0.1')]]),
|
||||||
|
webServersByNodeId: new Map([[PC, { port: 80, documentRoot: '/webserver', virtualHosts: false, isActive: false }]]),
|
||||||
|
terminalNodeIds: new Set([PC]),
|
||||||
|
fileSystemsByNodeId: new Map([[PC, files]]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('a program installed while the simulation runs', () => {
|
||||||
|
it("takes its settings and the files it makes from the document's snapshot, and starts", () => {
|
||||||
|
const own = new VirtualFileSystem();
|
||||||
|
own.createDirectory('/webserver');
|
||||||
|
own.writeTextFile('/webserver/index.html', 'Meine Seite');
|
||||||
|
const engine = startedEngine({ nodes: [pc(PC, PORT)], links: [], deviceConfigurations: new Map([[PC, device('10.0.0.1')]]), fileSystemsByNodeId: new Map([[PC, own]]) });
|
||||||
|
expect([engine.webServer(PC), engine.terminal(PC)]).toEqual([undefined, undefined]);
|
||||||
|
const from = source(new VirtualFileSystem());
|
||||||
|
engine.installProgram(PC, 'webServer', from);
|
||||||
|
engine.installProgram(PC, 'commandPrompt', from);
|
||||||
|
expect(engine.webServer(PC)).toBeDefined();
|
||||||
|
expect(engine.terminal(PC)).toBeDefined();
|
||||||
|
expect(engine.state.topologySnapshot.webServersByNodeId.get(PC)).toEqual({ port: 80, documentRoot: '/webserver', virtualHosts: false, isActive: false });
|
||||||
|
// The running files keep what is there; what is missing comes from the document.
|
||||||
|
const files = engine.state.topologySnapshot.fileSystemsByNodeId.get(PC)!;
|
||||||
|
expect(files.textFile('/webserver/index.html')).toBe('Meine Seite');
|
||||||
|
expect(files.imageFile('/webserver/splashscreen-mini.png').mediaType).toBe('image/png');
|
||||||
|
// Installed again: nothing changes, the same program runs on.
|
||||||
|
const terminal = engine.terminal(PC);
|
||||||
|
engine.installProgram(PC, 'commandPrompt', from);
|
||||||
|
expect(engine.terminal(PC)).toBe(terminal);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goes when removed, its files staying, and nothing happens outside a run or on a device the run has not', () => {
|
||||||
|
const engine = startedEngine({ nodes: [pc(PC, PORT)], links: [], deviceConfigurations: new Map([[PC, device('10.0.0.1')]]) });
|
||||||
|
const from = source(new VirtualFileSystem());
|
||||||
|
engine.installProgram(uuid(9), 'webServer', from);
|
||||||
|
expect(engine.webServer(uuid(9))).toBeUndefined();
|
||||||
|
engine.installProgram(PC, 'webServer', from);
|
||||||
|
engine.installProgram(PC, 'commandPrompt', from);
|
||||||
|
engine.removeProgram(PC, 'webServer');
|
||||||
|
engine.removeProgram(PC, 'commandPrompt');
|
||||||
|
expect([engine.webServer(PC), engine.terminal(PC)]).toEqual([undefined, undefined]);
|
||||||
|
expect(engine.state.topologySnapshot.webServersByNodeId.has(PC)).toBe(false);
|
||||||
|
expect(engine.state.topologySnapshot.terminalNodeIds.has(PC)).toBe(false);
|
||||||
|
expect(engine.state.topologySnapshot.fileSystemsByNodeId.get(PC)!.contains('/webserver/index.html')).toBe(true);
|
||||||
|
engine.handle({ type: 'stop' });
|
||||||
|
engine.installProgram(PC, 'commandPrompt', from);
|
||||||
|
expect(engine.terminal(PC)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const templates = join(dirname(fileURLToPath(import.meta.url)), '../../../tools/java-oracle/.upstream/src/main/resources/tmpl');
|
||||||
|
|
||||||
|
describe("the files a program makes", () => {
|
||||||
|
it.skipIf(!existsSync(templates))("give the web server Java's start page in the language FILIUS runs in, as Java reads it (each line ended by a line break)", () => {
|
||||||
|
for (const [language, file] of [['de', 'webserver_index_de_DE.txt'], ['en', 'webserver_index_en_GB.txt']] as const) {
|
||||||
|
const lines = readFileSync(join(templates, file), 'utf8').split(/\r?\n/);
|
||||||
|
if (lines.at(-1) === '') lines.pop();
|
||||||
|
const page = installationFiles('webServer', language).find((each) => each.path === '/webserver/index.html')!;
|
||||||
|
expect(page.content, language).toEqual({ kind: 'text', text: lines.map((line) => `${line}\n`).join('') });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('are none for a program that makes none', () => {
|
||||||
|
for (const program of ['commandPrompt', 'webBrowser', 'textEditor', 'fileExplorer', 'imageViewer', 'echoServer', 'simpleClient', 'emailClient', 'firewall'] as const) {
|
||||||
|
expect(installationFiles(program, 'de'), program).toEqual([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('a server removed while the simulation runs', () => {
|
||||||
|
it('is switched off: its port listens no more', () => {
|
||||||
|
const engine = startedEngine({ nodes: [pc(PC, PORT)], links: [], deviceConfigurations: new Map([[PC, device('10.0.0.1')]]) });
|
||||||
|
const from = topologySnapshot({ ...source(new VirtualFileSystem()), webServersByNodeId: new Map([[PC, { port: 80, documentRoot: '/webserver', virtualHosts: false, isActive: true }]]) });
|
||||||
|
engine.installProgram(PC, 'webServer', from);
|
||||||
|
const listening = () => [...engine.state.socketsById.values()].filter((socket) => socket.nodeId === PC && socket.localPort === 80 && socket.tcpState === 'LISTEN');
|
||||||
|
expect(listening()).toHaveLength(1);
|
||||||
|
engine.removeProgram(PC, 'webServer');
|
||||||
|
// The server's thread closes the socket, as Java's does, a moment later.
|
||||||
|
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 100 });
|
||||||
|
expect(listening()).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -8,3 +8,4 @@ export * from './sheet.js';
|
|||||||
export * from './snapshot.js';
|
export * from './snapshot.js';
|
||||||
export * from './values.js';
|
export * from './values.js';
|
||||||
export * from './view.js';
|
export * from './view.js';
|
||||||
|
export * from './software.js';
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ function ownApplications(text: string): FlsOwnApplication[] {
|
|||||||
) as FlsOwnApplication[];
|
) as FlsOwnApplication[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileEntry(order: number, content: VirtualFileContent): Y.Map<unknown> {
|
export function fileEntry(order: number, content: VirtualFileContent): Y.Map<unknown> {
|
||||||
const entry = new Y.Map<unknown>();
|
const entry = new Y.Map<unknown>();
|
||||||
entry.set('order', order);
|
entry.set('order', order);
|
||||||
entry.set('kind', content.kind);
|
entry.set('kind', content.kind);
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
// A computer's software installation, after Java's (GUIInstallationsDialog
|
||||||
|
// on the desktop; measured with tools/java-oracle's InstallRun): the
|
||||||
|
// programs taken on with "apply changes" are installed — switched off, with
|
||||||
|
// the settings a new program of Java's has, and the folders and files it
|
||||||
|
// makes where nothing of the name is (installationFiles) —, those taken off
|
||||||
|
// are removed with their settings, their files staying. A program installed
|
||||||
|
// where a removed one left its files reads them, as Java's does: a DNS
|
||||||
|
// server its records from /dns/hosts, an e-mail server its accounts from
|
||||||
|
// /mailserver/konten.txt, an e-mail program its account from /konten.txt.
|
||||||
|
//
|
||||||
|
// Not adopted: Java's Gnutella rolls how many peers it knows (3 to 5); here a
|
||||||
|
// new one knows 3, as Java's file without the number reads.
|
||||||
|
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import {
|
||||||
|
DEFAULT_WEB_SERVER_PORT,
|
||||||
|
installationFiles,
|
||||||
|
JAVA_PERSONAL_FIREWALL_DEFAULTS,
|
||||||
|
type InstallableProgram,
|
||||||
|
type NodeKind,
|
||||||
|
type PageLanguage,
|
||||||
|
} from '@mdg-filius/core';
|
||||||
|
import { dnsRecordsOf, emailClientConfiguration, emailServerConfiguration, JAVA_WEB_ROOT, readDnsHosts, readEmailClientFile, readEmailServerFile, type FlsApplication } from '@mdg-filius/fls';
|
||||||
|
import { fileEntry, ROOTS } from './projectDoc.js';
|
||||||
|
import { toY, type PlainValue } from './values.js';
|
||||||
|
|
||||||
|
/** The programs the installation offers: Java's list (config/Desktop_*.txt); the DHCP server is set up in the device sheet. */
|
||||||
|
export type Software = Exclude<InstallableProgram, 'dhcpServer'>;
|
||||||
|
|
||||||
|
/** Where a program keeps its settings in a node's settings. */
|
||||||
|
const SETTINGS_KEY: Partial<Record<Software, string>> = {
|
||||||
|
webServer: 'webServer',
|
||||||
|
echoServer: 'echoServer',
|
||||||
|
simpleClient: 'simpleClient',
|
||||||
|
dnsServer: 'dnsServer',
|
||||||
|
emailServer: 'emailServer',
|
||||||
|
emailClient: 'emailClient',
|
||||||
|
gnutella: 'gnutella',
|
||||||
|
firewall: 'firewall',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The order of Java's list of programs in each language
|
||||||
|
* (config/Desktop_<language>.txt): the desktop shows them so, and "apply
|
||||||
|
* changes" installs them so (the English list is alphabetical, the German
|
||||||
|
* one is not).
|
||||||
|
*/
|
||||||
|
export const SOFTWARE_ORDER: Readonly<Record<PageLanguage, readonly Software[]>> = {
|
||||||
|
de: ['commandPrompt', 'imageViewer', 'fileExplorer', 'dnsServer', 'emailClient', 'emailServer', 'echoServer', 'simpleClient', 'firewall', 'gnutella', 'textEditor', 'webServer', 'webBrowser'],
|
||||||
|
en: ['commandPrompt', 'dnsServer', 'echoServer', 'emailServer', 'emailClient', 'fileExplorer', 'firewall', 'simpleClient', 'gnutella', 'imageViewer', 'textEditor', 'webBrowser', 'webServer'],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Java's new Gnutella rolls 3 to 5 peers; a file without the number reads 3. */
|
||||||
|
export const NEW_GNUTELLA_PEERS = 3;
|
||||||
|
|
||||||
|
const nodes = (doc: Y.Doc) => doc.getMap<Y.Map<unknown>>(ROOTS.nodes);
|
||||||
|
const ignore = () => {};
|
||||||
|
|
||||||
|
/** A text file of a node's files, undefined without it. */
|
||||||
|
function textOf(files: Y.Map<Y.Map<unknown>>, path: string): string | undefined {
|
||||||
|
const entry = files.get(path);
|
||||||
|
return entry?.get('kind') === 'text' ? (entry.get('text') as Y.Text).toString() : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The settings a new program of Java's has on a computer with these files. */
|
||||||
|
function newSettings(program: Software, files: Y.Map<Y.Map<unknown>>): PlainValue | undefined {
|
||||||
|
const application = (fields: Partial<FlsApplication>) => fields as FlsApplication;
|
||||||
|
switch (program) {
|
||||||
|
case 'webServer':
|
||||||
|
return { port: DEFAULT_WEB_SERVER_PORT, documentRoot: JAVA_WEB_ROOT, virtualHosts: false };
|
||||||
|
case 'echoServer':
|
||||||
|
return { port: 55_555 };
|
||||||
|
case 'simpleClient':
|
||||||
|
return { targetAddress: '', targetPort: 55_555 };
|
||||||
|
case 'dnsServer':
|
||||||
|
return { recursiveResolutionEnabled: false, records: dnsRecordsOf(readDnsHosts(textOf(files, '/dns/hosts') ?? ''), ignore) } as unknown as PlainValue;
|
||||||
|
case 'emailServer': {
|
||||||
|
const accounts = textOf(files, '/mailserver/konten.txt');
|
||||||
|
return emailServerConfiguration(application({ fileAccounts: accounts === undefined ? null : readEmailServerFile(accounts, ignore) }), ignore) as unknown as PlainValue;
|
||||||
|
}
|
||||||
|
case 'emailClient': {
|
||||||
|
const account = textOf(files, '/konten.txt');
|
||||||
|
return emailClientConfiguration(application({ fileAccount: account === undefined ? null : readEmailClientFile(account, ignore) }), ignore) as unknown as PlainValue;
|
||||||
|
}
|
||||||
|
case 'gnutella':
|
||||||
|
return { maximumKnownPeers: NEW_GNUTELLA_PEERS };
|
||||||
|
case 'firewall':
|
||||||
|
return JAVA_PERSONAL_FIREWALL_DEFAULTS as unknown as PlainValue;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A computer's installed programs; undefined for a device that has none (no PC or notebook). */
|
||||||
|
export function installedSoftware(doc: Y.Doc, id: string): readonly InstallableProgram[] | undefined {
|
||||||
|
const node = nodes(doc).get(id);
|
||||||
|
const kind = node?.get('kind') as NodeKind | undefined;
|
||||||
|
if (node === undefined || (kind !== 'pc' && kind !== 'notebook')) return undefined;
|
||||||
|
return [...(node.get('programs') as Y.Map<unknown>).keys()] as InstallableProgram[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Java's "apply changes": installs and removes a computer's programs in one
|
||||||
|
* step. A program already installed is not installed again, one not
|
||||||
|
* installed is not removed.
|
||||||
|
*/
|
||||||
|
export function setSoftware(
|
||||||
|
doc: Y.Doc,
|
||||||
|
id: string,
|
||||||
|
change: { readonly install: readonly Software[]; readonly remove: readonly Software[] },
|
||||||
|
language: PageLanguage,
|
||||||
|
origin: unknown,
|
||||||
|
): void {
|
||||||
|
const node = nodes(doc).get(id);
|
||||||
|
if (node === undefined || installedSoftware(doc, id) === undefined) return;
|
||||||
|
const programs = node.get('programs') as Y.Map<Y.Map<unknown>>;
|
||||||
|
const settings = node.get('settings') as Y.Map<unknown>;
|
||||||
|
const files = node.get('files') as Y.Map<Y.Map<unknown>>;
|
||||||
|
doc.transact(() => {
|
||||||
|
for (const program of change.remove) {
|
||||||
|
if (!programs.has(program)) continue;
|
||||||
|
programs.delete(program);
|
||||||
|
const key = SETTINGS_KEY[program];
|
||||||
|
if (key !== undefined) settings.delete(key);
|
||||||
|
}
|
||||||
|
// In the order of Java's list, as its dialog installs them.
|
||||||
|
for (const program of SOFTWARE_ORDER[language].filter((each) => change.install.includes(each))) {
|
||||||
|
if (programs.has(program)) continue;
|
||||||
|
let order = Math.max(-1, ...[...files.values()].map((entry) => entry.get('order') as number)) + 1;
|
||||||
|
// Java's web server makes its page and picture only where it has no page yet.
|
||||||
|
const keepsPage = program === 'webServer' && files.has('/webserver/index.html');
|
||||||
|
for (const file of installationFiles(program, language)) {
|
||||||
|
if (keepsPage && file.path !== '/webserver') continue;
|
||||||
|
if (!files.has(file.path)) files.set(file.path, fileEntry(order++, file.content));
|
||||||
|
}
|
||||||
|
const entry = new Y.Map<unknown>();
|
||||||
|
entry.set('active', false);
|
||||||
|
programs.set(program, entry);
|
||||||
|
const key = SETTINGS_KEY[program];
|
||||||
|
const value = newSettings(program, files);
|
||||||
|
if (key !== undefined && value !== undefined) settings.set(key, toY(value));
|
||||||
|
}
|
||||||
|
}, origin);
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,162 @@
|
|||||||
|
// The software installation against Java's (tools/java-oracle InstallRun,
|
||||||
|
// fixtures/installed-programs.fls): each program installed on a new computer,
|
||||||
|
// all on one, each installed and removed again, a DNS server and an e-mail
|
||||||
|
// server installed again where their removed ones left a record and an
|
||||||
|
// account — the same programs, settings and files as Java saved, read with
|
||||||
|
// the same reader. Named: Java's Gnutella rolls its number of peers.
|
||||||
|
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import type { Id, InstallableProgram, VirtualFileSystem } from '@mdg-filius/core';
|
||||||
|
import { openProjectArchive, PROGRAM_OF_CLASS, saveProjectArchive, type ImportedProject } from '@mdg-filius/fls';
|
||||||
|
import { addDevice, createUndoManager, emptyProject, installedSoftware, NEW_GNUTELLA_PEERS, readProject, ROOTS, setSoftware, writeProject, type Software } from '../src/index.js';
|
||||||
|
|
||||||
|
const fixture = join(dirname(fileURLToPath(import.meta.url)), 'fixtures/installed-programs.fls');
|
||||||
|
let counter = 0;
|
||||||
|
const java = openProjectArchive(new Uint8Array(readFileSync(fixture)), () => `90000000-0000-0000-0000-${(++counter).toString(16).padStart(12, '0')}`);
|
||||||
|
const PROGRAMS = Object.entries(PROGRAM_OF_CLASS).map(([javaClass, program]) => [javaClass.slice(javaClass.lastIndexOf('.') + 1), program as Software] as const);
|
||||||
|
|
||||||
|
/** A new PC in a new document. */
|
||||||
|
function computer() {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
writeProject(doc, emptyProject());
|
||||||
|
let ports = 0;
|
||||||
|
addDevice(doc, { kind: 'pc', id: 'pc', name: 'PC', x: 0, y: 0, newId: () => `port-${++ports}` }, 'me');
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Folders and files, each with its content: text as it is, pictures by kind and size. */
|
||||||
|
function filesOf(files: VirtualFileSystem | undefined): string[] {
|
||||||
|
const listed: string[] = [];
|
||||||
|
const walk = (folder: string) => {
|
||||||
|
for (const entry of files?.entriesInCreationOrder(folder) ?? []) {
|
||||||
|
const content = entry.content;
|
||||||
|
listed.push(`${entry.path} ${content.kind === 'text' ? JSON.stringify(content.text) : content.kind === 'directory' ? '/' : `${content.kind} ${content.data.length}`}`);
|
||||||
|
if (content.kind === 'directory') walk(entry.path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk('/');
|
||||||
|
return listed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What a computer holds of its programs, as the model has it. */
|
||||||
|
function slice(project: ImportedProject, id: Id) {
|
||||||
|
const gnutella = project.gnutellaConfigurations.get(id);
|
||||||
|
return {
|
||||||
|
programs: [...(project.installedPrograms.get(id) ?? [])].sort(),
|
||||||
|
active: [...(project.activePrograms.get(id) ?? [])],
|
||||||
|
webServer: project.webServerConfigurations.get(id),
|
||||||
|
echoServer: project.echoServerConfigurations.get(id),
|
||||||
|
simpleClient: project.simpleClientConfigurations.get(id),
|
||||||
|
dnsServer: project.dnsServerConfigurations.get(id),
|
||||||
|
emailServer: project.emailServerConfigurations.get(id),
|
||||||
|
emailClient: project.emailClientConfigurations.get(id),
|
||||||
|
// Java rolls it (3 to 5), and its file may name it by any number that came before (an idref).
|
||||||
|
gnutella: gnutella === undefined ? undefined : { maximumKnownPeers: NEW_GNUTELLA_PEERS },
|
||||||
|
firewall: project.firewallConfigurationsByNodeId.get(id),
|
||||||
|
files: filesOf(project.virtualFileSystems.get(id)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const javaComputer = (name: string) => java.graph.nodes.find((node) => node.displayName === name)!.id;
|
||||||
|
|
||||||
|
describe("the software installation, against Java's", () => {
|
||||||
|
it('installs each program with its settings and files', () => {
|
||||||
|
for (const [name, program] of PROGRAMS) {
|
||||||
|
const doc = computer();
|
||||||
|
setSoftware(doc, 'pc', { install: [program], remove: [] }, 'de', 'me');
|
||||||
|
expect(slice(readProject(doc), 'pc'), name).toEqual(slice(java, javaComputer(name)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('installs all programs on one computer, in the order of Java’s list', () => {
|
||||||
|
const doc = computer();
|
||||||
|
// Taken on in any order: the dialog installs them in the order of its list.
|
||||||
|
setSoftware(doc, 'pc', { install: PROGRAMS.map(([, program]) => program).reverse(), remove: [] }, 'de', 'me');
|
||||||
|
expect(slice(readProject(doc), 'pc')).toEqual(slice(java, javaComputer('Alle')));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes a program with its settings, its files staying', () => {
|
||||||
|
for (const [name, program] of PROGRAMS) {
|
||||||
|
const doc = computer();
|
||||||
|
setSoftware(doc, 'pc', { install: [program], remove: [] }, 'de', 'me');
|
||||||
|
setSoftware(doc, 'pc', { install: [], remove: [program] }, 'de', 'me');
|
||||||
|
expect(slice(readProject(doc), 'pc'), name).toEqual(slice(java, javaComputer(`Entfernt-${name}`)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reads the records and accounts a removed server left, when it is installed again', () => {
|
||||||
|
for (const [name, program, path, text] of [
|
||||||
|
['DNSServer', 'dnsServer', '/dns/hosts', 'www.filius.de. A 3600 1.2.3.4\n'],
|
||||||
|
['EmailServer', 'emailServer', '/mailserver/konten.txt', 'anna;filius.de;geheim;Muster;Anna;\n'],
|
||||||
|
] as const) {
|
||||||
|
const doc = computer();
|
||||||
|
setSoftware(doc, 'pc', { install: [program], remove: [] }, 'de', 'me');
|
||||||
|
// What Java's server wrote when it got the record or the account (its windows follow in F4.6g).
|
||||||
|
const files = doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get('pc')!.get('files') as Y.Map<Y.Map<unknown>>;
|
||||||
|
(files.get(path)!.get('text') as Y.Text).insert(0, text);
|
||||||
|
setSoftware(doc, 'pc', { install: [], remove: [program] }, 'de', 'me');
|
||||||
|
setSoftware(doc, 'pc', { install: [program], remove: [] }, 'de', 'me');
|
||||||
|
expect(slice(readProject(doc), 'pc'), name).toEqual(slice(java, javaComputer(`Wieder-${name}`)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('the software installation', () => {
|
||||||
|
it("writes the web server's start page in the language FILIUS runs in, and keeps a page that is there", () => {
|
||||||
|
const doc = computer();
|
||||||
|
setSoftware(doc, 'pc', { install: ['webServer'], remove: [] }, 'en', 'me');
|
||||||
|
const files = () => readProject(doc).virtualFileSystems.get('pc')!;
|
||||||
|
const page = files().entriesInCreationOrder('/webserver')[0]!;
|
||||||
|
expect(page.content.kind === 'text' && page.content.text).toContain('<p>Welcome at FILIUS!</p>');
|
||||||
|
// A page of its own: installed again, the server keeps it and brings no picture.
|
||||||
|
const entries = doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get('pc')!.get('files') as Y.Map<Y.Map<unknown>>;
|
||||||
|
setSoftware(doc, 'pc', { install: [], remove: ['webServer'] }, 'en', 'me');
|
||||||
|
entries.delete('/webserver/splashscreen-mini.png');
|
||||||
|
(entries.get('/webserver/index.html')!.get('text') as Y.Text).insert(0, 'Meine Seite ');
|
||||||
|
setSoftware(doc, 'pc', { install: ['webServer'], remove: [] }, 'de', 'me');
|
||||||
|
expect(filesOf(files()).filter((each) => each.startsWith('/webserver'))).toEqual(['/webserver /', expect.stringMatching(/^\/webserver\/index\.html "Meine Seite <html>/)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('installs nothing twice, removes nothing not there, takes one step of the undo, and installs only on computers', () => {
|
||||||
|
const doc = computer();
|
||||||
|
const undo = createUndoManager(doc, 'me');
|
||||||
|
setSoftware(doc, 'pc', { install: ['commandPrompt', 'webServer'], remove: ['textEditor'] }, 'de', 'me');
|
||||||
|
expect([...installedSoftware(doc, 'pc')!].sort()).toEqual(['commandPrompt', 'webServer']);
|
||||||
|
const before = readProject(doc);
|
||||||
|
setSoftware(doc, 'pc', { install: ['webServer'], remove: [] }, 'de', 'me');
|
||||||
|
expect(slice(readProject(doc), 'pc')).toEqual(slice(before, 'pc'));
|
||||||
|
undo.undo();
|
||||||
|
expect(installedSoftware(doc, 'pc')).toEqual([]);
|
||||||
|
expect(filesOf(readProject(doc).virtualFileSystems.get('pc'))).toEqual([]);
|
||||||
|
let ports = 0;
|
||||||
|
addDevice(doc, { kind: 'router', id: 'router', name: 'R', x: 0, y: 0, newId: () => `r-${++ports}` }, 'me');
|
||||||
|
setSoftware(doc, 'router', { install: ['commandPrompt'], remove: [] }, 'de', 'me');
|
||||||
|
expect(installedSoftware(doc, 'router')).toBeUndefined();
|
||||||
|
expect(readProject(doc).installedPrograms.has('router')).toBe(false);
|
||||||
|
setSoftware(doc, 'nobody', { install: ['commandPrompt'], remove: [] }, 'de', 'me');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gives a new Gnutella 3 peers, as Java's file without the number reads (Java rolls 3 to 5)", () => {
|
||||||
|
const doc = computer();
|
||||||
|
setSoftware(doc, 'pc', { install: ['gnutella'], remove: [] }, 'de', 'me');
|
||||||
|
expect(readProject(doc).gnutellaConfigurations.get('pc')).toEqual({ maximumKnownPeers: 3 });
|
||||||
|
let ids = 0;
|
||||||
|
const reopened = openProjectArchive(saveProjectArchive(readProject(doc)).archive, () => `70000000-0000-0000-0000-${(++ids).toString(16).padStart(12, '0')}`);
|
||||||
|
expect(reopened.gnutellaConfigurations.get(reopened.graph.nodes[0]!.id)).toEqual({ maximumKnownPeers: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goes into the file as Java would read it', () => {
|
||||||
|
const doc = computer();
|
||||||
|
setSoftware(doc, 'pc', { install: ['webServer', 'emailServer', 'commandPrompt'], remove: [] }, 'de', 'me');
|
||||||
|
const project = readProject(doc);
|
||||||
|
expect(project.installedPrograms.get('pc')).toEqual(new Set<InstallableProgram>(['webServer', 'emailServer', 'commandPrompt']));
|
||||||
|
let ids = 0;
|
||||||
|
const reopened = openProjectArchive(saveProjectArchive(project).archive, () => `80000000-0000-0000-0000-${(++ids).toString(16).padStart(12, '0')}`);
|
||||||
|
const again = reopened.graph.nodes[0]!.id;
|
||||||
|
expect(slice(reopened, again)).toEqual(slice(project, 'pc'));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -257,6 +257,25 @@ test.describe('on a phone', () => {
|
|||||||
await window.locator('input[name="terminalInput"]').press('Enter');
|
await window.locator('input[name="terminalInput"]').press('Enter');
|
||||||
await expect(window.locator('.terminal-text')).toContainText('IP Adresse . . . : 141.99.1.10');
|
await expect(window.locator('.terminal-text')).toContainText('IP Adresse . . . : 141.99.1.10');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('offers the installation’s programs as targets for a finger', async ({ page }) => {
|
||||||
|
await openEditor(page, server.base, 'internetwork_komplett.fls');
|
||||||
|
await simulationButton(page).tap();
|
||||||
|
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
|
||||||
|
await page.getByRole('button', { name: 'Alles zeigen' }).tap();
|
||||||
|
const id = await deviceNamed(page, 'Notebook 1.10');
|
||||||
|
const at = await onDevice(page, id);
|
||||||
|
await page.touchscreen.tap(at.x, at.y);
|
||||||
|
await desktop(page, id).locator('[data-program="installation"]').tap();
|
||||||
|
const panel = desktop(page, id).locator('[data-program-window="installation"]');
|
||||||
|
const first = (await panel.locator('[data-list="available"] [role="option"]').first().boundingBox())!;
|
||||||
|
expect(first.height).toBeGreaterThanOrEqual(44);
|
||||||
|
const arrow = (await panel.getByRole('button', { name: 'Installieren' }).boundingBox())!;
|
||||||
|
expect(arrow.height).toBeGreaterThanOrEqual(44);
|
||||||
|
await panel.locator('[data-list="available"] [role="option"]').first().tap();
|
||||||
|
await panel.getByRole('button', { name: 'Installieren' }).tap();
|
||||||
|
await expect(panel.locator('[data-list="installed"] [role="option"]')).toHaveText(['Befehlszeile', 'Bildbetrachter', 'Webbrowser']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Opens a computer's command line on its desktop. */
|
/** Opens a computer's command line on its desktop. */
|
||||||
@@ -343,6 +362,61 @@ test('keeps the command line over leaving it, over a return to the design, and e
|
|||||||
await expect(window).toBeVisible();
|
await expect(window).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("installs and removes programs as Java's installation does, and a program installed runs at once", async ({ page }) => {
|
||||||
|
await openEditor(page, server.base, 'internetwork_komplett.fls');
|
||||||
|
const id = await deviceNamed(page, 'Rechner 3.10');
|
||||||
|
expect((await desktopOf(page, id))!.programs).toEqual([]);
|
||||||
|
await start(page);
|
||||||
|
await tap(page, id);
|
||||||
|
const window = desktop(page, id);
|
||||||
|
await window.locator('[data-program="installation"]').click();
|
||||||
|
const panel = window.locator('[data-program-window="installation"]');
|
||||||
|
await expect(panel.getByRole('heading')).toHaveText('Software-Installation');
|
||||||
|
const installed = panel.locator('[data-list="installed"] [role="option"]');
|
||||||
|
const available = panel.locator('[data-list="available"] [role="option"]');
|
||||||
|
await expect(installed).toHaveCount(0);
|
||||||
|
await expect(available).toHaveText(['Befehlszeile', 'Bildbetrachter', 'Datei-Explorer', 'DNS-Server', 'E-Mail-Programm', 'E-Mail-Server', 'Echo-Server', 'Einfacher Client', 'Firewall', 'Gnutella', 'Text-Editor', 'Webserver', 'Webbrowser']);
|
||||||
|
// Chosen with a tap each, moved over with the arrow; the list keeps Java's order.
|
||||||
|
await available.filter({ hasText: 'Webserver' }).click();
|
||||||
|
await available.filter({ hasText: 'Befehlszeile' }).click();
|
||||||
|
await expect(available.filter({ hasText: 'Webserver' })).toHaveAttribute('aria-selected', 'true');
|
||||||
|
// A second tap lets a program go again.
|
||||||
|
await available.filter({ hasText: 'Gnutella' }).click();
|
||||||
|
await available.filter({ hasText: 'Gnutella' }).click();
|
||||||
|
await expect(available.filter({ hasText: 'Gnutella' })).toHaveAttribute('aria-selected', 'false');
|
||||||
|
await panel.getByRole('button', { name: 'Installieren' }).click();
|
||||||
|
await expect(installed).toHaveText(['Befehlszeile', 'Webserver']);
|
||||||
|
// Nothing changes before "apply changes".
|
||||||
|
expect((await desktopOf(page, id))!.programs).toEqual([]);
|
||||||
|
await panel.getByRole('button', { name: 'Änderungen annehmen' }).click();
|
||||||
|
await expect(panel).toHaveCount(0);
|
||||||
|
await expect(window.locator('.desktop-icon span')).toHaveText(['Software-Installation', 'Befehlszeile', 'Webserver']);
|
||||||
|
expect([...(await desktopOf(page, id))!.programs].sort()).toEqual(['commandPrompt', 'webServer']);
|
||||||
|
|
||||||
|
// The command line runs at once, and sees the web server's files.
|
||||||
|
await window.locator('[data-program="commandPrompt"]').click();
|
||||||
|
const input = window.locator('input[name="terminalInput"]');
|
||||||
|
await input.fill('ls /webserver');
|
||||||
|
await input.press('Enter');
|
||||||
|
await expect(window.locator('.terminal-text')).toContainText('index.html');
|
||||||
|
await window.getByRole('button', { name: 'Startbildschirm' }).click();
|
||||||
|
|
||||||
|
// A double click moves a program back; applied, it is gone.
|
||||||
|
await window.locator('[data-program="installation"]').click();
|
||||||
|
await installed.filter({ hasText: 'Webserver' }).dblclick();
|
||||||
|
await expect(installed).toHaveText(['Befehlszeile']);
|
||||||
|
await panel.getByRole('button', { name: 'Änderungen annehmen' }).click();
|
||||||
|
await expect(window.locator('.desktop-icon span')).toHaveText(['Software-Installation', 'Befehlszeile']);
|
||||||
|
expect((await desktopOf(page, id))!.programs).toEqual(['commandPrompt']);
|
||||||
|
// Taken on and given back before applying: nothing happens.
|
||||||
|
await window.locator('[data-program="installation"]').click();
|
||||||
|
await available.filter({ hasText: 'Gnutella' }).dblclick();
|
||||||
|
await installed.filter({ hasText: 'Gnutella' }).click();
|
||||||
|
await panel.getByRole('button', { name: 'Entfernen' }).click();
|
||||||
|
await panel.getByRole('button', { name: 'Änderungen annehmen' }).click();
|
||||||
|
expect((await desktopOf(page, id))!.programs).toEqual(['commandPrompt']);
|
||||||
|
});
|
||||||
|
|
||||||
test('says so when the simulation fails, and goes back to the design', async ({ page }) => {
|
test('says so when the simulation fails, and goes back to the design', async ({ page }) => {
|
||||||
// FILIUS' modem example: the core has no modems yet (remote links); a ping to the other side fails it.
|
// FILIUS' modem example: the core has no modems yet (remote links); a ping to the other side fails it.
|
||||||
await openEditor(page, server.base, 'modem_beispiel.fls');
|
await openEditor(page, server.base, 'modem_beispiel.fls');
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ import { ContextMenu, type MenuItem } from './design/ContextMenu.js';
|
|||||||
import { Palette, type PaletteKind } from './design/Palette.js';
|
import { Palette, type PaletteKind } from './design/Palette.js';
|
||||||
import { RouterDialog } from './design/RouterDialog.js';
|
import { RouterDialog } from './design/RouterDialog.js';
|
||||||
import { DeviceSheet } from './sheet/DeviceSheet.js';
|
import { DeviceSheet } from './sheet/DeviceSheet.js';
|
||||||
import { DesktopWindow } from './desktop/DesktopWindow.js';
|
import { DesktopWindow, type DesktopWindowName } from './desktop/DesktopWindow.js';
|
||||||
import type { DesktopProgram } from './desktop/programs.js';
|
|
||||||
import { useTranslate } from './i18n/i18n.js';
|
import { useTranslate } from './i18n/i18n.js';
|
||||||
import { ModeBar } from './simulation/ModeBar.js';
|
import { ModeBar } from './simulation/ModeBar.js';
|
||||||
import { useSimulation, type SimHostFactory } from './simulation/useSimulation.js';
|
import { useSimulation, type SimHostFactory } from './simulation/useSimulation.js';
|
||||||
@@ -80,7 +79,7 @@ interface OpenDesktop {
|
|||||||
readonly x: number;
|
readonly x: number;
|
||||||
readonly y: number;
|
readonly y: number;
|
||||||
readonly z: number;
|
readonly z: number;
|
||||||
readonly program: DesktopProgram | null;
|
readonly program: DesktopWindowName | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Brings a desktop to the top by its stacking, not its place in the page: moved in the page, it would lose the press on it. */
|
/** Brings a desktop to the top by its stacking, not its place in the page: moved in the page, it would lose the press on it. */
|
||||||
@@ -310,6 +309,7 @@ export function Editor({ doc, createSimHost }: { readonly doc: Y.Doc; readonly c
|
|||||||
y={desktop.y}
|
y={desktop.y}
|
||||||
z={desktop.z}
|
z={desktop.z}
|
||||||
program={desktop.program}
|
program={desktop.program}
|
||||||
|
origin={origin}
|
||||||
onProgram={(program) => setDesktops((open) => open.map((each) => (each.id === desktop.id ? { ...each, program } : each)))}
|
onProgram={(program) => setDesktops((open) => open.map((each) => (each.id === desktop.id ? { ...each, program } : each)))}
|
||||||
onMove={(x, y) => setDesktops((open) => open.map((each) => (each.id === desktop.id ? { ...each, x, y } : each)))}
|
onMove={(x, y) => setDesktops((open) => open.map((each) => (each.id === desktop.id ? { ...each, x, y } : each)))}
|
||||||
onFront={() => setDesktops((open) => toFront(open, desktop.id))}
|
onFront={() => setDesktops((open) => toFront(open, desktop.id))}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 616 B |
Binary file not shown.
|
After Width: | Height: | Size: 572 B |
@@ -24,6 +24,7 @@ import { useLanguage, useTranslate } from '../i18n/i18n.js';
|
|||||||
import type { Simulation } from '../simulation/useSimulation.js';
|
import type { Simulation } from '../simulation/useSimulation.js';
|
||||||
import { ProgramFrame } from './ProgramFrame.js';
|
import { ProgramFrame } from './ProgramFrame.js';
|
||||||
import { desktopPrograms, INSTALLATION_ICON, JAVA_PROGRAMS, type DesktopProgram } from './programs.js';
|
import { desktopPrograms, INSTALLATION_ICON, JAVA_PROGRAMS, type DesktopProgram } from './programs.js';
|
||||||
|
import { InstallationPanel } from './InstallationPanel.js';
|
||||||
import { TerminalPanel } from './TerminalPanel.js';
|
import { TerminalPanel } from './TerminalPanel.js';
|
||||||
import './desktop.css';
|
import './desktop.css';
|
||||||
|
|
||||||
@@ -39,14 +40,19 @@ export interface DesktopWindowProps {
|
|||||||
readonly y: number;
|
readonly y: number;
|
||||||
/** Its place in the stack of desktops. */
|
/** Its place in the stack of desktops. */
|
||||||
readonly z: number;
|
readonly z: number;
|
||||||
/** The program whose window is open on it, kept by the editor over a return to the design (Java's desktop keeps it). */
|
/** The window open on it — a program's or the installation's —, kept by the editor over a return to the design (Java's desktop keeps it). */
|
||||||
readonly program: DesktopProgram | null;
|
readonly program: DesktopWindowName | null;
|
||||||
readonly onProgram: (program: DesktopProgram | null) => void;
|
readonly onProgram: (program: DesktopWindowName | null) => void;
|
||||||
|
/** The person's origin, for what the installation changes in the document. */
|
||||||
|
readonly origin: object;
|
||||||
readonly onMove: (x: number, y: number) => void;
|
readonly onMove: (x: number, y: number) => void;
|
||||||
readonly onFront: () => void;
|
readonly onFront: () => void;
|
||||||
readonly onClose: () => void;
|
readonly onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** What can be open on a desktop: a program's window or the software installation. */
|
||||||
|
export type DesktopWindowName = DesktopProgram | 'installation';
|
||||||
|
|
||||||
/** The programs whose windows are built so far. */
|
/** The programs whose windows are built so far. */
|
||||||
const WITH_WINDOW: ReadonlySet<DesktopProgram> = new Set(['commandPrompt']);
|
const WITH_WINDOW: ReadonlySet<DesktopProgram> = new Set(['commandPrompt']);
|
||||||
|
|
||||||
@@ -62,7 +68,7 @@ function useDesktopView(doc: Y.Doc, id: string) {
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: open, onProgram: setOpen, onMove, onFront, onClose }: DesktopWindowProps) {
|
export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: open, onProgram: setOpen, origin, onMove, onFront, onClose }: DesktopWindowProps) {
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const language = useLanguage();
|
const language = useLanguage();
|
||||||
const view = useDesktopView(doc, id);
|
const view = useDesktopView(doc, id);
|
||||||
@@ -75,7 +81,7 @@ export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: o
|
|||||||
if (view === undefined) return null;
|
if (view === undefined) return null;
|
||||||
const programs = desktopPrograms(view.programs, language);
|
const programs = desktopPrograms(view.programs, language);
|
||||||
// A program removed while its window was open: the desktop shows.
|
// A program removed while its window was open: the desktop shows.
|
||||||
const shown = open !== null && programs.includes(open) ? open : null;
|
const shown = open === 'installation' || (open !== null && programs.includes(open)) ? open : null;
|
||||||
const address = settings?.ipAddress ?? '';
|
const address = settings?.ipAddress ?? '';
|
||||||
const title = view.addressAsName ? view.label : `${view.label} - ${address}`;
|
const title = view.addressAsName ? view.label : `${view.label} - ${address}`;
|
||||||
|
|
||||||
@@ -114,8 +120,7 @@ export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: o
|
|||||||
<div className="desktop-screen" style={{ backgroundImage: `url(${background})` }}>
|
<div className="desktop-screen" style={{ backgroundImage: `url(${background})` }}>
|
||||||
<ul className="desktop-icons">
|
<ul className="desktop-icons">
|
||||||
<li>
|
<li>
|
||||||
{/* The installation's window comes with F4.6c. */}
|
<button type="button" className="desktop-icon" data-program="installation" title={t('desktop.installation')} onClick={() => setOpen('installation')}>
|
||||||
<button type="button" className="desktop-icon" aria-disabled="true" title={t('desktop.installation')}>
|
|
||||||
<img src={INSTALLATION_ICON} alt="" />
|
<img src={INSTALLATION_ICON} alt="" />
|
||||||
<span>{t('desktop.installation')}</span>
|
<span>{t('desktop.installation')}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -136,7 +141,11 @@ export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: o
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
{shown === null ? null : (
|
{shown === 'installation' ? (
|
||||||
|
<ProgramFrame icon={INSTALLATION_ICON} title={t('desktop.installation')} program="installation">
|
||||||
|
<InstallationPanel doc={doc} node={id} installed={view.programs} origin={origin} onDone={home} />
|
||||||
|
</ProgramFrame>
|
||||||
|
) : shown === null ? null : (
|
||||||
<ProgramFrame icon={JAVA_PROGRAMS[shown].icon} title={t(`program.${shown}`)} program={shown}>
|
<ProgramFrame icon={JAVA_PROGRAMS[shown].icon} title={t(`program.${shown}`)} program={shown}>
|
||||||
{shown === 'commandPrompt' ? <TerminalPanel node={id} simulation={simulation} onExit={home} /> : null}
|
{shown === 'commandPrompt' ? <TerminalPanel node={id} simulation={simulation} onExit={home} /> : null}
|
||||||
</ProgramFrame>
|
</ProgramFrame>
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// A computer's software installation, after Java's GUIInstallationsDialog:
|
||||||
|
// the programs installed on the left, those to be had on the right, each in
|
||||||
|
// the order of Java's list; the arrows between them (or a double click) move
|
||||||
|
// the chosen ones over, "apply changes" installs and removes them in one
|
||||||
|
// step (setSoftware) and goes back to the desktop. Java's list of installed
|
||||||
|
// programs comes in the order of a hash map; here in the order of its list.
|
||||||
|
// A tap chooses a program, another tap lets it go, as Java's lists choose
|
||||||
|
// several.
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import type * as Y from 'yjs';
|
||||||
|
import { setSoftware, SOFTWARE_ORDER, type Software } from '@mdg-filius/doc';
|
||||||
|
import type { InstallableProgram } from '@mdg-filius/core';
|
||||||
|
import arrowLeft from '../assets/desktop/pfeil_links.png';
|
||||||
|
import arrowRight from '../assets/desktop/pfeil_rechts.png';
|
||||||
|
import { useLanguage, useTranslate } from '../i18n/i18n.js';
|
||||||
|
|
||||||
|
function ProgramList({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
programs,
|
||||||
|
chosen,
|
||||||
|
onChoose,
|
||||||
|
onMove,
|
||||||
|
}: {
|
||||||
|
readonly name: string;
|
||||||
|
readonly label: string;
|
||||||
|
readonly programs: readonly Software[];
|
||||||
|
readonly chosen: ReadonlySet<Software>;
|
||||||
|
readonly onChoose: (program: Software) => void;
|
||||||
|
readonly onMove: (program: Software) => void;
|
||||||
|
}) {
|
||||||
|
const t = useTranslate();
|
||||||
|
return (
|
||||||
|
<div className="installation-column">
|
||||||
|
<span id={`${name}-label`}>{label}</span>
|
||||||
|
<ul className="installation-list" role="listbox" aria-multiselectable="true" aria-labelledby={`${name}-label`} data-list={name}>
|
||||||
|
{programs.map((program) => (
|
||||||
|
<li key={program} role="option" aria-selected={chosen.has(program)} tabIndex={0} onClick={() => onChoose(program)} onDoubleClick={() => onMove(program)} onKeyDown={(event) => (event.key === ' ' || event.key === 'Enter') && (event.preventDefault(), onChoose(program))}>
|
||||||
|
{t(`program.${program}`)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InstallationPanel({ doc, node, installed, origin, onDone }: { readonly doc: Y.Doc; readonly node: string; readonly installed: readonly InstallableProgram[]; readonly origin: object; readonly onDone: () => void }) {
|
||||||
|
const t = useTranslate();
|
||||||
|
const language = useLanguage();
|
||||||
|
const order = SOFTWARE_ORDER[language];
|
||||||
|
const [kept, setKept] = useState<ReadonlySet<Software>>(() => new Set(order.filter((program) => installed.includes(program))));
|
||||||
|
const [chosen, setChosen] = useState<ReadonlySet<Software>>(new Set());
|
||||||
|
const left = order.filter((program) => kept.has(program));
|
||||||
|
const right = order.filter((program) => !kept.has(program));
|
||||||
|
const choose = (program: Software) =>
|
||||||
|
setChosen((before) => {
|
||||||
|
const next = new Set(before);
|
||||||
|
if (!next.delete(program)) next.add(program);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
/** Moves the chosen programs of one side to the other. */
|
||||||
|
const move = (toInstalled: boolean, programs: readonly Software[]) => {
|
||||||
|
setKept((before) => {
|
||||||
|
const next = new Set(before);
|
||||||
|
for (const program of programs) if (toInstalled) next.add(program);
|
||||||
|
else next.delete(program);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setChosen((before) => new Set([...before].filter((program) => !programs.includes(program))));
|
||||||
|
};
|
||||||
|
const apply = () => {
|
||||||
|
const install = order.filter((program) => kept.has(program) && !installed.includes(program));
|
||||||
|
const remove = order.filter((program) => !kept.has(program) && installed.includes(program));
|
||||||
|
setSoftware(doc, node, { install, remove }, language, origin);
|
||||||
|
onDone();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="installation">
|
||||||
|
<div className="installation-lists">
|
||||||
|
<ProgramList name="installed" label={t('installation.installed')} programs={left} chosen={chosen} onChoose={choose} onMove={(program) => move(false, [program])} />
|
||||||
|
<div className="installation-arrows">
|
||||||
|
<button type="button" aria-label={t('installation.add')} title={t('installation.add')} onClick={() => move(true, right.filter((program) => chosen.has(program)))}>
|
||||||
|
<img src={arrowLeft} alt="" />
|
||||||
|
</button>
|
||||||
|
<button type="button" aria-label={t('installation.remove')} title={t('installation.remove')} onClick={() => move(false, left.filter((program) => chosen.has(program)))}>
|
||||||
|
<img src={arrowRight} alt="" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ProgramList name="available" label={t('installation.available')} programs={right} chosen={chosen} onChoose={choose} onMove={(program) => move(true, [program])} />
|
||||||
|
</div>
|
||||||
|
<button type="button" className="installation-apply" onClick={apply}>
|
||||||
|
{t('installation.apply')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -295,3 +295,87 @@
|
|||||||
font: 14px ui-monospace, Menlo, Consolas, monospace;
|
font: 14px ui-monospace, Menlo, Consolas, monospace;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The software installation (GUIInstallationsDialog): two lists of 170
|
||||||
|
* pixels in Java, the arrows between them, "apply changes" below. */
|
||||||
|
.installation {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #eeeeee;
|
||||||
|
color: #000000;
|
||||||
|
font: 13px Dialog, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installation-lists {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 52px minmax(0, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installation-column {
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installation-list {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installation-list li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installation-list li[aria-selected='true'] {
|
||||||
|
background: rgb(184 207 229);
|
||||||
|
}
|
||||||
|
|
||||||
|
.installation-list li:focus-visible {
|
||||||
|
outline: 2px solid rgb(99 130 191);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installation-arrows {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installation-arrows button,
|
||||||
|
.installation-apply {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
border: 1px solid #a0a0a0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f8f8f8;
|
||||||
|
color: #000000;
|
||||||
|
font: 13px Dialog, sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installation-apply {
|
||||||
|
padding: 0 16px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
// set up in the device sheet, as in Java.
|
// set up in the device sheet, as in Java.
|
||||||
|
|
||||||
import type { InstallableProgram } from '@mdg-filius/core';
|
import type { InstallableProgram } from '@mdg-filius/core';
|
||||||
|
import { SOFTWARE_ORDER } from '@mdg-filius/doc';
|
||||||
import type { Language } from '../i18n/i18n.js';
|
import type { Language } from '../i18n/i18n.js';
|
||||||
import browser from '../assets/desktop/icon_browser.png';
|
import browser from '../assets/desktop/icon_browser.png';
|
||||||
import simpleClient from '../assets/desktop/icon_clientbaustein.png';
|
import simpleClient from '../assets/desktop/icon_clientbaustein.png';
|
||||||
@@ -43,11 +44,8 @@ export const JAVA_PROGRAMS: Readonly<Record<DesktopProgram, { readonly javaClass
|
|||||||
/** The software installation's icon, always first on the desktop (GUIDesktopPanel.initInstaller). */
|
/** The software installation's icon, always first on the desktop (GUIDesktopPanel.initInstaller). */
|
||||||
export const INSTALLATION_ICON = installation;
|
export const INSTALLATION_ICON = installation;
|
||||||
|
|
||||||
/** The order of Java's list in each language. */
|
/** The order of Java's list in each language (the installation's too). */
|
||||||
export const DESKTOP_ORDER: Readonly<Record<Language, readonly DesktopProgram[]>> = {
|
export const DESKTOP_ORDER: Readonly<Record<Language, readonly DesktopProgram[]>> = SOFTWARE_ORDER;
|
||||||
de: ['commandPrompt', 'imageViewer', 'fileExplorer', 'dnsServer', 'emailClient', 'emailServer', 'echoServer', 'simpleClient', 'firewall', 'gnutella', 'textEditor', 'webServer', 'webBrowser'],
|
|
||||||
en: ['commandPrompt', 'dnsServer', 'echoServer', 'emailServer', 'emailClient', 'fileExplorer', 'firewall', 'simpleClient', 'gnutella', 'imageViewer', 'textEditor', 'webBrowser', 'webServer'],
|
|
||||||
};
|
|
||||||
|
|
||||||
/** A computer's programs as its desktop shows them, in the language's order. */
|
/** A computer's programs as its desktop shows them, in the language's order. */
|
||||||
export function desktopPrograms(installed: readonly InstallableProgram[], language: Language): DesktopProgram[] {
|
export function desktopPrograms(installed: readonly InstallableProgram[], language: Language): DesktopProgram[] {
|
||||||
|
|||||||
@@ -79,6 +79,11 @@
|
|||||||
"forwarding.lanPortValue": "LAN-Port",
|
"forwarding.lanPortValue": "LAN-Port",
|
||||||
"forwarding.protocolValue": "Protokoll",
|
"forwarding.protocolValue": "Protokoll",
|
||||||
"forwarding.publicPortValue": "Portfreigabe",
|
"forwarding.publicPortValue": "Portfreigabe",
|
||||||
|
"installation.add": "Installieren",
|
||||||
|
"installation.apply": "Änderungen annehmen",
|
||||||
|
"installation.available": "Verfügbar:",
|
||||||
|
"installation.installed": "Installiert:",
|
||||||
|
"installation.remove": "Entfernen",
|
||||||
"menu.delete": "Löschen",
|
"menu.delete": "Löschen",
|
||||||
"menu.removeAllCables": "Alle Verbindungen entfernen",
|
"menu.removeAllCables": "Alle Verbindungen entfernen",
|
||||||
"menu.removeCable": "Verbindung entfernen",
|
"menu.removeCable": "Verbindung entfernen",
|
||||||
|
|||||||
@@ -79,6 +79,11 @@
|
|||||||
"forwarding.lanPortValue": "LAN Port",
|
"forwarding.lanPortValue": "LAN Port",
|
||||||
"forwarding.protocolValue": "Protocol",
|
"forwarding.protocolValue": "Protocol",
|
||||||
"forwarding.publicPortValue": "Mapped Port",
|
"forwarding.publicPortValue": "Mapped Port",
|
||||||
|
"installation.add": "Install",
|
||||||
|
"installation.apply": "Apply changes",
|
||||||
|
"installation.available": "Available:",
|
||||||
|
"installation.installed": "Installed:",
|
||||||
|
"installation.remove": "Remove",
|
||||||
"menu.delete": "Delete",
|
"menu.delete": "Delete",
|
||||||
"menu.removeAllCables": "Remove all cables",
|
"menu.removeAllCables": "Remove all cables",
|
||||||
"menu.removeCable": "Remove cable",
|
"menu.removeCable": "Remove cable",
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ const FROM_JAVA: Readonly<Record<string, string>> = {
|
|||||||
'connections.add': 'jvermittlungsrechnerkonfiguration_msg24',
|
'connections.add': 'jvermittlungsrechnerkonfiguration_msg24',
|
||||||
'connections.most': 'jvermittlungsrechnerkonfiguration_msg25',
|
'connections.most': 'jvermittlungsrechnerkonfiguration_msg25',
|
||||||
'mode.speed': 'guimainmemu_msg16',
|
'mode.speed': 'guimainmemu_msg16',
|
||||||
|
'installation.installed': 'installationsdialog_msg3',
|
||||||
|
'installation.available': 'installationsdialog_msg4',
|
||||||
|
'installation.apply': 'installationsdialog_msg2',
|
||||||
'mode.currentSpeed': 'guimainmemu_msg15',
|
'mode.currentSpeed': 'guimainmemu_msg15',
|
||||||
'menu.showDesktop': 'guievents_msg3',
|
'menu.showDesktop': 'guievents_msg3',
|
||||||
'desktop.home': 'desktoppanel_application',
|
'desktop.home': 'desktoppanel_application',
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ export * from './topologyWriter.js';
|
|||||||
export * from './zip.js';
|
export * from './zip.js';
|
||||||
export * from './exportProject.js';
|
export * from './exportProject.js';
|
||||||
export * from './projectSnapshot.js';
|
export * from './projectSnapshot.js';
|
||||||
|
export * from './importEmail.js';
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
{
|
{
|
||||||
|
"ClientBaustein#priority": 5,
|
||||||
"ClientBaustein#zielIPAdresse": null,
|
"ClientBaustein#zielIPAdresse": null,
|
||||||
"ClientBaustein#zielPort": 55555,
|
"ClientBaustein#zielPort": 55555,
|
||||||
"DHCPServer#port": 67,
|
"DHCPServer#port": 67,
|
||||||
|
"DHCPServer#priority": 5,
|
||||||
"DNSServer#aktiv": false,
|
"DNSServer#aktiv": false,
|
||||||
"DNSServer#port": 53,
|
"DNSServer#port": 53,
|
||||||
|
"DNSServer#priority": 5,
|
||||||
|
"EmailAnwendung#priority": 5,
|
||||||
|
"EmailServer#priority": 5,
|
||||||
|
"FileExplorer#priority": 5,
|
||||||
|
"Firewall#priority": 5,
|
||||||
"Gateway#maxLayerOfOperation": 2,
|
"Gateway#maxLayerOfOperation": 2,
|
||||||
|
"ImageViewer#priority": 5,
|
||||||
"JSidebarButton#debugGraphicsOptions": 0,
|
"JSidebarButton#debugGraphicsOptions": 0,
|
||||||
"JSidebarButton#displayedMnemonicIndex": -1,
|
"JSidebarButton#displayedMnemonicIndex": -1,
|
||||||
"JSidebarButton#enabled": true,
|
"JSidebarButton#enabled": true,
|
||||||
@@ -18,14 +26,21 @@
|
|||||||
"5"
|
"5"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"PeerToPeerAnwendung#priority": 5,
|
||||||
"RIPServer#port": 520,
|
"RIPServer#port": 520,
|
||||||
|
"RIPServer#priority": 5,
|
||||||
"Rechner#maxLayerOfOperation": 4,
|
"Rechner#maxLayerOfOperation": 4,
|
||||||
"ServerBaustein#aktiv": false,
|
"ServerBaustein#aktiv": false,
|
||||||
"ServerBaustein#port": 55555,
|
"ServerBaustein#port": 55555,
|
||||||
|
"ServerBaustein#priority": 5,
|
||||||
"SwingConstants.CENTER": 0,
|
"SwingConstants.CENTER": 0,
|
||||||
|
"Terminal#priority": 5,
|
||||||
|
"TextEditor#priority": 5,
|
||||||
"ThreadGroup#maxPriority": 10,
|
"ThreadGroup#maxPriority": 10,
|
||||||
"Vermittlungsrechner#maxLayerOfOperation": 2,
|
"Vermittlungsrechner#maxLayerOfOperation": 2,
|
||||||
|
"WebBrowser#priority": 5,
|
||||||
"WebServer#aktiv": false,
|
"WebServer#aktiv": false,
|
||||||
"WebServer#port": 80,
|
"WebServer#port": 80,
|
||||||
|
"WebServer#priority": 5,
|
||||||
"WebServer#useVHost": false
|
"WebServer#useVHost": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,26 @@
|
|||||||
// the next start goes on at the time it stopped, so that the capture's times
|
// the next start goes on at the time it stopped, so that the capture's times
|
||||||
// keep rising (plan §6.2). So do the command lines' windows, as Java's do
|
// keep rising (plan §6.2). So do the command lines' windows, as Java's do
|
||||||
// (TerminalWindow).
|
// (TerminalWindow).
|
||||||
|
//
|
||||||
|
// While the simulation runs, the document's programs are watched: a program
|
||||||
|
// installed or removed on the desktop (Java's software installation) is
|
||||||
|
// installed or removed in the running engine at once, as Java starts or ends
|
||||||
|
// it; any other change comes in at the next start.
|
||||||
|
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
import { DEFAULT_SPEED_PERCENT, NetworkRuntimeEngine, captureRows, interfaceKey, stableGeneratedMacAddress, type CapturedFrame, type CaptureRow, type Id, type LogLine } from '@mdg-filius/core';
|
import {
|
||||||
import { docSnapshot } from '@mdg-filius/doc';
|
DEFAULT_SPEED_PERCENT,
|
||||||
|
NetworkRuntimeEngine,
|
||||||
|
captureRows,
|
||||||
|
interfaceKey,
|
||||||
|
stableGeneratedMacAddress,
|
||||||
|
type CapturedFrame,
|
||||||
|
type CaptureRow,
|
||||||
|
type Id,
|
||||||
|
type InstallableProgram,
|
||||||
|
type LogLine,
|
||||||
|
} from '@mdg-filius/core';
|
||||||
|
import { docSnapshot, installedSoftware, ROOTS } from '@mdg-filius/doc';
|
||||||
import {
|
import {
|
||||||
parseClientMessage,
|
parseClientMessage,
|
||||||
SIM_PROTOCOL_VERSION,
|
SIM_PROTOCOL_VERSION,
|
||||||
@@ -77,12 +93,61 @@ export class SimulationRoom {
|
|||||||
private lastActivity: string[] = [];
|
private lastActivity: string[] = [];
|
||||||
/** Each computer's command line window, kept over stops. */
|
/** Each computer's command line window, kept over stops. */
|
||||||
private readonly terminalWindows = new Map<Id, TerminalWindow>();
|
private readonly terminalWindows = new Map<Id, TerminalWindow>();
|
||||||
|
/** Each computer's programs as the running engine has them. */
|
||||||
|
private software = new Map<Id, readonly InstallableProgram[]>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly doc: Y.Doc,
|
private readonly doc: Y.Doc,
|
||||||
private readonly options: RoomOptions,
|
private readonly options: RoomOptions,
|
||||||
) {
|
) {
|
||||||
this.engine = new NetworkRuntimeEngine(options.seed(), undefined, { wallClock: options.now });
|
this.engine = new NetworkRuntimeEngine(options.seed(), undefined, { wallClock: options.now });
|
||||||
|
doc.on('update', () => {
|
||||||
|
if (this.mode === 'simulation') this.guarded(() => this.syncSoftware());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The computers' programs in the document. */
|
||||||
|
private documentSoftware(): Map<Id, readonly InstallableProgram[]> {
|
||||||
|
const software = new Map<Id, readonly InstallableProgram[]>();
|
||||||
|
for (const id of this.doc.getMap(ROOTS.nodes).keys()) {
|
||||||
|
const programs = installedSoftware(this.doc, id);
|
||||||
|
if (programs !== undefined) software.set(id, programs);
|
||||||
|
}
|
||||||
|
return software;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Installs and removes in the running engine what the document's programs changed (Java's software installation). */
|
||||||
|
private syncSoftware(): void {
|
||||||
|
const now = this.documentSoftware();
|
||||||
|
const changes: { readonly node: Id; readonly installed: readonly InstallableProgram[]; readonly removed: readonly InstallableProgram[] }[] = [];
|
||||||
|
for (const [node, programs] of now) {
|
||||||
|
const before = this.software.get(node);
|
||||||
|
// A computer added since the start comes in at the next start.
|
||||||
|
if (before === undefined) continue;
|
||||||
|
const installed = programs.filter((program) => !before.includes(program));
|
||||||
|
const removed = before.filter((program) => !programs.includes(program));
|
||||||
|
if (installed.length > 0 || removed.length > 0) changes.push({ node, installed, removed });
|
||||||
|
}
|
||||||
|
if (changes.length === 0) return;
|
||||||
|
const source = docSnapshot(this.doc);
|
||||||
|
for (const { node, installed, removed } of changes) {
|
||||||
|
for (const program of removed) this.engine.removeProgram(node, program);
|
||||||
|
for (const program of installed) this.engine.installProgram(node, program, source);
|
||||||
|
this.software.set(node, now.get(node)!);
|
||||||
|
}
|
||||||
|
this.syncTerminalWindows();
|
||||||
|
this.flush([...this.clients.values()], false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A window for each command line the engine runs, the same over stops (TerminalWindow). */
|
||||||
|
private syncTerminalWindows(): void {
|
||||||
|
for (const node of this.engine.state.topologySnapshot.nodes) {
|
||||||
|
const terminal = this.engine.terminal(node.id);
|
||||||
|
if (terminal === undefined) continue;
|
||||||
|
const window = this.terminalWindows.get(node.id);
|
||||||
|
if (window === undefined) this.terminalWindows.set(node.id, new TerminalWindow(terminal));
|
||||||
|
else window.attach(terminal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A new client; it gets the room's state at once. */
|
/** A new client; it gets the room's state at once. */
|
||||||
@@ -168,13 +233,8 @@ export class SimulationRoom {
|
|||||||
this.engine.setSimulationSpeed(this.speed);
|
this.engine.setSimulationSpeed(this.speed);
|
||||||
if (!this.guarded(() => this.engine.handle({ type: 'start', snapshot, seed: this.options.seed(), initialTimeMilliseconds: this.engine.state.currentTimeMilliseconds }))) return;
|
if (!this.guarded(() => this.engine.handle({ type: 'start', snapshot, seed: this.options.seed(), initialTimeMilliseconds: this.engine.state.currentTimeMilliseconds }))) return;
|
||||||
this.linkOfPort = new Map(snapshot.links.flatMap((link) => [[link.sourcePortId, link.id] as const, [link.targetPortId, link.id] as const]));
|
this.linkOfPort = new Map(snapshot.links.flatMap((link) => [[link.sourcePortId, link.id] as const, [link.targetPortId, link.id] as const]));
|
||||||
for (const node of snapshot.nodes) {
|
this.software = this.documentSoftware();
|
||||||
const terminal = this.engine.terminal(node.id);
|
this.syncTerminalWindows();
|
||||||
if (terminal === undefined) continue;
|
|
||||||
const window = this.terminalWindows.get(node.id);
|
|
||||||
if (window === undefined) this.terminalWindows.set(node.id, new TerminalWindow(terminal));
|
|
||||||
else window.attach(terminal);
|
|
||||||
}
|
|
||||||
this.mode = 'simulation';
|
this.mode = 'simulation';
|
||||||
this.lastWall = this.options.now();
|
this.lastWall = this.options.now();
|
||||||
this.broadcast(this.state());
|
this.broadcast(this.state());
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export class TerminalWindow {
|
|||||||
|
|
||||||
/** The program of a start: the first time the window shows its start screen, later it goes on in the working directory it had. */
|
/** The program of a start: the first time the window shows its start screen, later it goes on in the working directory it had. */
|
||||||
attach(terminal: Terminal): void {
|
attach(terminal: Terminal): void {
|
||||||
|
if (this.terminal === terminal) return;
|
||||||
this.terminal = terminal;
|
this.terminal = terminal;
|
||||||
this.seen = 0;
|
this.seen = 0;
|
||||||
terminal.continueIn(this.directory);
|
terminal.continueIn(this.directory);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { dirname, join } from 'node:path';
|
|||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
import { addDevice, emptyProject, ROOTS, writeProject } from '@mdg-filius/doc';
|
import { addDevice, emptyProject, ROOTS, setSoftware, writeProject } from '@mdg-filius/doc';
|
||||||
import { historyLine, historyStep, terminalScreen, type HostMessage, type TerminalLine } from '@mdg-filius/protocol';
|
import { historyLine, historyStep, terminalScreen, type HostMessage, type TerminalLine } from '@mdg-filius/protocol';
|
||||||
import { SimulationRoom, TICK_MILLISECONDS } from '../src/index.js';
|
import { SimulationRoom, TICK_MILLISECONDS } from '../src/index.js';
|
||||||
|
|
||||||
@@ -214,3 +214,46 @@ describe("the window's history", () => {
|
|||||||
expect([historyLine(['a', 'b'], -1), historyLine(['a', 'b'], 0), historyLine(['a', 'b'], 1)]).toEqual(['', 'b', 'a']);
|
expect([historyLine(['a', 'b'], -1), historyLine(['a', 'b'], 0), historyLine(['a', 'b'], 1)]).toEqual(['', 'b', 'a']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('the software installation while the simulation runs', () => {
|
||||||
|
it("installs a program at once, as Java starts it: a command line to type into, a web server's files", () => {
|
||||||
|
const { doc, send, run, read, got } = setUp([]);
|
||||||
|
send({ kind: 'sub', topics: ['app:pc/terminal'] });
|
||||||
|
send({ kind: 'start' });
|
||||||
|
run(TICK_MILLISECONDS);
|
||||||
|
expect(read()).toEqual([]);
|
||||||
|
setSoftware(doc, 'pc', { install: ['commandPrompt', 'webServer'], remove: [] }, 'de', 'me');
|
||||||
|
run(TICK_MILLISECONDS);
|
||||||
|
expect(terminalScreen(read()).text).toContain("Verwende den Befehl 'help'");
|
||||||
|
send({ kind: 'input', node: 'pc', app: 'terminal', action: 'enter', args: { line: 'ls /webserver' }, reqId: 'a' });
|
||||||
|
run(400);
|
||||||
|
expect(terminalScreen(read()).text).toMatch(/index\.html[\s\S]*splashscreen-mini\.png/);
|
||||||
|
expect(got.filter((message) => message.kind === 'error')).toEqual([]);
|
||||||
|
|
||||||
|
// Removed: the window stays with its text, and takes no line.
|
||||||
|
setSoftware(doc, 'pc', { install: [], remove: ['commandPrompt'] }, 'de', 'me');
|
||||||
|
const shown = terminalScreen(read()).text;
|
||||||
|
send({ kind: 'input', node: 'pc', app: 'terminal', action: 'enter', args: { line: 'pwd' }, reqId: 'b' });
|
||||||
|
expect(got.filter((message) => message.kind === 'error')).toEqual([{ v: 1, kind: 'error', code: 'unknownApp', reqId: 'b' }]);
|
||||||
|
expect(terminalScreen(read()).text).toBe(shown);
|
||||||
|
// Installed again, it goes on in the same window.
|
||||||
|
setSoftware(doc, 'pc', { install: ['commandPrompt'], remove: [] }, 'de', 'me');
|
||||||
|
send({ kind: 'input', node: 'pc', app: 'terminal', action: 'enter', args: { line: 'pwd' }, reqId: 'c' });
|
||||||
|
run(400);
|
||||||
|
expect(terminalScreen(read()).text.startsWith(shown)).toBe(true);
|
||||||
|
expect(terminalScreen(read()).text.slice(shown.length)).toBe('\n/> pwd\n/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves the running engine alone in the design, and starts with the programs then', () => {
|
||||||
|
const { doc, send, run, read } = setUp([]);
|
||||||
|
send({ kind: 'sub', topics: ['app:pc/terminal'] });
|
||||||
|
send({ kind: 'start' });
|
||||||
|
send({ kind: 'stop' });
|
||||||
|
setSoftware(doc, 'pc', { install: ['commandPrompt'], remove: [] }, 'de', 'me');
|
||||||
|
expect(read()).toEqual([]);
|
||||||
|
send({ kind: 'start' });
|
||||||
|
run(TICK_MILLISECONDS);
|
||||||
|
expect(terminalScreen(read()).text).toContain("Verwende den Befehl 'help'");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1265,3 +1265,30 @@ Gemessen (03.10.2026):
|
|||||||
Javas Terminal setzt den Ordner nur bei der Installation
|
Javas Terminal setzt den Ordner nur bei der Installation
|
||||||
(`setSystemSoftware`), und das Fenster ist dasselbe Objekt.
|
(`setSystemSoftware`), und das Fenster ist dasselbe Objekt.
|
||||||
|
|
||||||
|
### Software-Installation (F4.6c)
|
||||||
|
|
||||||
|
`InstallRun` installiert jedes Programm aus Javas Liste, wie
|
||||||
|
`GUIInstallationsDialog.aenderungenSpeichern` es tut
|
||||||
|
(`installAndStartAppIfAvailable`), auf einem eigenen neuen Rechner, alle auf
|
||||||
|
einem („Alle“), jedes einmal mit anschließendem `entferneSoftware`
|
||||||
|
(„Entfernt-…“), dazu einen DNS-Server mit Eintrag und einen E-Mail-Server
|
||||||
|
mit Konto, die entfernt und neu installiert werden („Wieder-…“); Java
|
||||||
|
speichert das als Projekt (`packages/doc/test/fixtures/installed-programs.fls`).
|
||||||
|
|
||||||
|
Gemessen (03.10.2026):
|
||||||
|
|
||||||
|
* Ein neues Programm ist aus (kein Server läuft); die Firewall eines
|
||||||
|
Rechners dagegen ist sofort aktiv (verwerfen, UDP filtern).
|
||||||
|
* Es legt an, was fehlt: der Webserver `/webserver` mit Javas Startseite in
|
||||||
|
der Sprache, in der FILIUS läuft, und `splashscreen-mini.png` (nur, wenn
|
||||||
|
noch keine `index.html` da ist), Gnutella `/peer2peer`, der E-Mail-Server
|
||||||
|
`/mailserver/konten.txt` (leer), der DNS-Server `/dns/hosts` (leer).
|
||||||
|
* Entfernt, verschwinden Programm und Einstellungen; die Dateien bleiben.
|
||||||
|
Neu installiert, liest der DNS-Server seine Einträge aus `/dns/hosts`, der
|
||||||
|
E-Mail-Server seine Konten aus `konten.txt`.
|
||||||
|
* Gnutellas Teilnehmerzahl würfelt der Konstruktor (3 bis 5); die Datei
|
||||||
|
verweist darauf mit `idref` auf irgendeine frühere gleiche Zahl — hier den
|
||||||
|
Rückgabewert von `iconTextGap` (4) oder einer Thread-Priorität (5).
|
||||||
|
Darum misst `GetterDefaults` jetzt auch `#priority` der gespeicherten
|
||||||
|
Thread-Klassen (alle 5), die vorher unaufgelöst als 0 gelesen wurden.
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then
|
|||||||
fi
|
fi
|
||||||
classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
|
classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
|
||||||
mkdir -p "$here/.build"
|
mkdir -p "$here/.build"
|
||||||
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java" "$here/src/TerminalRun.java" "$here/src/FileCases.java" "$here/src/GnutellaRun.java" "$here/src/NewDeviceRun.java" "$here/src/SheetRun.java" "$here/src/TerminalWindowRun.java"
|
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java" "$here/src/TerminalRun.java" "$here/src/FileCases.java" "$here/src/GnutellaRun.java" "$here/src/NewDeviceRun.java" "$here/src/SheetRun.java" "$here/src/TerminalWindowRun.java" "$here/src/InstallRun.java"
|
||||||
# FILIUS writes its settings below $HOME; keep them out of the real one.
|
# FILIUS writes its settings below $HOME; keep them out of the real one.
|
||||||
mkdir -p "$here/.build/home"
|
mkdir -p "$here/.build/home"
|
||||||
# Removed first: a crashed run must not leave the previous result standing.
|
# Removed first: a crashed run must not leave the previous result standing.
|
||||||
@@ -57,6 +57,21 @@ HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" GetterDe
|
|||||||
filius.software.www.WebServer#aktiv \
|
filius.software.www.WebServer#aktiv \
|
||||||
filius.software.www.WebServer#useVHost \
|
filius.software.www.WebServer#useVHost \
|
||||||
filius.software.dateiaustausch.PeerToPeerAnwendung#maxTeilnehmerZahl \
|
filius.software.dateiaustausch.PeerToPeerAnwendung#maxTeilnehmerZahl \
|
||||||
|
filius.software.dhcp.DHCPServer#priority \
|
||||||
|
filius.software.rip.RIPServer#priority \
|
||||||
|
filius.software.lokal.Terminal#priority \
|
||||||
|
filius.software.lokal.ImageViewer#priority \
|
||||||
|
filius.software.lokal.FileExplorer#priority \
|
||||||
|
filius.software.dns.DNSServer#priority \
|
||||||
|
filius.software.email.EmailAnwendung#priority \
|
||||||
|
filius.software.email.EmailServer#priority \
|
||||||
|
filius.software.clientserver.ServerBaustein#priority \
|
||||||
|
filius.software.clientserver.ClientBaustein#priority \
|
||||||
|
filius.software.firewall.Firewall#priority \
|
||||||
|
filius.software.dateiaustausch.PeerToPeerAnwendung#priority \
|
||||||
|
filius.software.lokal.TextEditor#priority \
|
||||||
|
filius.software.www.WebServer#priority \
|
||||||
|
filius.software.www.WebBrowser#priority \
|
||||||
2>&1 | grep -v 'logback\|^$' || true
|
2>&1 | grep -v 'logback\|^$' || true
|
||||||
test -s "$defaults"
|
test -s "$defaults"
|
||||||
|
|
||||||
@@ -328,6 +343,14 @@ rm -f "$output"
|
|||||||
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" NewDeviceRun "$output" 2>&1 | grep -v 'logback\|^$' || true
|
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" NewDeviceRun "$output" 2>&1 | grep -v 'logback\|^$' || true
|
||||||
test -s "$output"
|
test -s "$output"
|
||||||
|
|
||||||
|
# FILIUS' programs as its software installation installs and removes them
|
||||||
|
# (InstallRun), saved as Java saves a project: the editor's installation
|
||||||
|
# (packages/doc/test/install.test.ts).
|
||||||
|
output="$root/packages/doc/test/fixtures/installed-programs.fls"
|
||||||
|
rm -f "$output"
|
||||||
|
HOME="$here/.build/home" xvfb-run -a java -Duser.language=de -Duser.country=DE -cp "$classpath:$here/.build" InstallRun "$output" 2>&1 | grep -v 'logback\|^$\|INFO' || true
|
||||||
|
test -s "$output"
|
||||||
|
|
||||||
# Text typed into FILIUS' settings panels of a host, a switch and a modem and
|
# Text typed into FILIUS' settings panels of a host, a switch and a modem and
|
||||||
# taken as they take it (SheetRun): what the editor's device sheet marks and
|
# taken as they take it (SheetRun): what the editor's device sheet marks and
|
||||||
# keeps (packages/doc/test/sheet.test.ts).
|
# keeps (packages/doc/test/sheet.test.ts).
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// Installs FILIUS' programs as its software installation does
|
||||||
|
// (GUIInstallationsDialog.aenderungenSpeichern: installAndStartAppIfAvailable,
|
||||||
|
// entferneSoftware) on new computers and saves them as Java saves a project
|
||||||
|
// (SzenarioVerwaltung.speichern): what a program brings along when it is
|
||||||
|
// installed — its settings, its folders and files — and what stays when it
|
||||||
|
// is removed. The reference for the editor's software installation
|
||||||
|
// (packages/doc/test/install.test.ts). One computer per program, named after
|
||||||
|
// its class's simple name; "Alle" has them all; "Entfernt-<name>" had the
|
||||||
|
// program installed and removed again; "Wieder-<name>" had a DNS server with
|
||||||
|
// a record or an e-mail server with an account installed, removed and
|
||||||
|
// installed again.
|
||||||
|
//
|
||||||
|
// Usage: java -cp filius.jar:lib/*:classes InstallRun <output fls>
|
||||||
|
|
||||||
|
import filius.gui.GUIContainer;
|
||||||
|
import filius.gui.netzwerksicht.GUIDesignSidebar;
|
||||||
|
import filius.gui.netzwerksicht.GUIDocuItem;
|
||||||
|
import filius.gui.netzwerksicht.GUIKabelItem;
|
||||||
|
import filius.gui.netzwerksicht.GUIKnotenItem;
|
||||||
|
import filius.hardware.knoten.Rechner;
|
||||||
|
import filius.rahmenprogramm.Information;
|
||||||
|
import filius.rahmenprogramm.SzenarioVerwaltung;
|
||||||
|
import filius.software.system.Betriebssystem;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class InstallRun {
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
try {
|
||||||
|
run(args);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
error.printStackTrace();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Rechner computer(List<GUIKnotenItem> items, String name, int place) {
|
||||||
|
Rechner host = new Rechner();
|
||||||
|
host.setName(name);
|
||||||
|
NewDeviceRun.add(items, host, GUIDesignSidebar.RECHNER, 100 + 150 * (place % 6), 100 + 150 * (place / 6));
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void run(String[] args) throws Exception {
|
||||||
|
GUIContainer.getGUIContainer();
|
||||||
|
List<GUIKnotenItem> items = new ArrayList<>();
|
||||||
|
List<Map<String, String>> programs = Information.getInformation().ladeProgrammListe();
|
||||||
|
int place = 0;
|
||||||
|
Rechner all = computer(items, "Alle", place++);
|
||||||
|
for (Map<String, String> program : programs) {
|
||||||
|
String className = program.get("Klasse");
|
||||||
|
String name = className.substring(className.lastIndexOf('.') + 1);
|
||||||
|
Betriebssystem system = (Betriebssystem) computer(items, name, place++).getSystemSoftware();
|
||||||
|
if (!system.installAndStartAppIfAvailable(className)) throw new IllegalStateException("not installed: " + className);
|
||||||
|
((Betriebssystem) all.getSystemSoftware()).installAndStartAppIfAvailable(className);
|
||||||
|
Betriebssystem removed = (Betriebssystem) computer(items, "Entfernt-" + name, place++).getSystemSoftware();
|
||||||
|
removed.installAndStartAppIfAvailable(className);
|
||||||
|
if (!removed.entferneSoftware(className)) throw new IllegalStateException("not removed: " + className);
|
||||||
|
}
|
||||||
|
// Installed again where a removed program left its files: a DNS server with a record, an e-mail server with an account.
|
||||||
|
Betriebssystem dns = (Betriebssystem) computer(items, "Wieder-DNSServer", place++).getSystemSoftware();
|
||||||
|
dns.installAndStartAppIfAvailable("filius.software.dns.DNSServer");
|
||||||
|
((filius.software.dns.DNSServer) dns.holeSoftware("filius.software.dns.DNSServer")).hinzuRecord("www.filius.de", "A", "1.2.3.4");
|
||||||
|
dns.entferneSoftware("filius.software.dns.DNSServer");
|
||||||
|
dns.installAndStartAppIfAvailable("filius.software.dns.DNSServer");
|
||||||
|
Betriebssystem mail = (Betriebssystem) computer(items, "Wieder-EmailServer", place++).getSystemSoftware();
|
||||||
|
mail.installAndStartAppIfAvailable("filius.software.email.EmailServer");
|
||||||
|
((filius.software.email.EmailServer) mail.holeSoftware("filius.software.email.EmailServer")).benutzerHinzufuegen("anna", "geheim", "Muster", "Anna");
|
||||||
|
mail.entferneSoftware("filius.software.email.EmailServer");
|
||||||
|
mail.installAndStartAppIfAvailable("filius.software.email.EmailServer");
|
||||||
|
// Let the programs' threads come to rest before the files are read.
|
||||||
|
Thread.sleep(1000);
|
||||||
|
if (!SzenarioVerwaltung.getInstance().speichern(args[0], items, new ArrayList<GUIKabelItem>(), new ArrayList<GUIDocuItem>())) {
|
||||||
|
throw new IllegalStateException("FILIUS could not save " + args[0]);
|
||||||
|
}
|
||||||
|
System.out.println("computers: " + items.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user