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:
@@ -76,6 +76,7 @@ import {
|
||||
FIREWALL_PROTOCOL,
|
||||
firewallConfiguration,
|
||||
interfaceKey,
|
||||
type InstallableProgram,
|
||||
} from './config.js';
|
||||
import { formatIPv4, maskAddress, parseIPv4, sameSubnet } from './ipv4.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 { routeTableRows, type RouteRow } from './routeTable.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 { ApplicationThreads, waitUntil, type Steps, type Wait } from './threads.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 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** 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 {
|
||||
private stateValue: RuntimeState;
|
||||
private random: JavaCompatibleRandom;
|
||||
@@ -1247,18 +1253,52 @@ export class NetworkRuntimeEngine {
|
||||
this.terminalsByNodeId.clear();
|
||||
this.gnutellaPeersByNodeId.clear();
|
||||
for (const node of snapshot.nodes) {
|
||||
const echoServer = snapshot.echoServersByNodeId.get(node.id);
|
||||
if (echoServer !== undefined) this.echoServersByNodeId.set(node.id, new EchoServer(this, node.id, echoServer));
|
||||
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) {
|
||||
this.webServersByNodeId.set(node.id, new WebServer(this, node.id, webServer, snapshot.fileSystemsByNodeId.get(node.id)));
|
||||
}
|
||||
if (snapshot.webBrowserNodeIds.has(node.id)) this.webBrowsersByNodeId.set(node.id, new WebBrowser(this, node.id));
|
||||
const emailServer = snapshot.emailServersByNodeId.get(node.id);
|
||||
if (emailServer !== undefined) {
|
||||
const nodeId = node.id;
|
||||
for (const program of RUNNING_PROGRAMS) if (this.hasProgram(node.id, program)) this.startProgram(node.id, program);
|
||||
if ((node.kind === 'router' || node.kind === 'gateway') && !this.swiftRouters) this.startRouterWebServer(node.id, node.kind);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the snapshot gives a node a program the engine runs. */
|
||||
private hasProgram(nodeId: Id, program: RunningProgram): boolean {
|
||||
const snapshot = this.stateValue.topologySnapshot;
|
||||
switch (program) {
|
||||
case 'echoServer':
|
||||
return snapshot.echoServersByNodeId.has(nodeId);
|
||||
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(
|
||||
nodeId,
|
||||
new EmailServer(
|
||||
@@ -1271,24 +1311,128 @@ export class NetworkRuntimeEngine {
|
||||
),
|
||||
);
|
||||
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)));
|
||||
const gnutella = snapshot.gnutellaByNodeId.get(node.id);
|
||||
if (gnutella !== undefined) {
|
||||
case 'commandPrompt':
|
||||
this.terminalsByNodeId.set(nodeId, new Terminal(this, nodeId, this.nodeFiles(nodeId)));
|
||||
return;
|
||||
case 'gnutella': {
|
||||
// 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);
|
||||
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);
|
||||
if (emailClient !== undefined) {
|
||||
const nodeId = node.id;
|
||||
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);
|
||||
case 'emailClient':
|
||||
this.emailClientsByNodeId.set(nodeId, new EmailClient(this, nodeId, snapshot.emailClientsByNodeId.get(nodeId)!, (folders) => this.setEmailClientFolders(nodeId, folders)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* router switch on their web server on port 80 when they start, with
|
||||
|
||||
Reference in New Issue
Block a user