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:
2026-10-03 15:03:35 +00:00
co-authored by Claude Opus 5.5
parent ac48e969f4
commit f0e7c73ec7
29 changed files with 1264 additions and 52 deletions
+69 -9
View File
@@ -17,10 +17,26 @@
// 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
// (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 { DEFAULT_SPEED_PERCENT, NetworkRuntimeEngine, captureRows, interfaceKey, stableGeneratedMacAddress, type CapturedFrame, type CaptureRow, type Id, type LogLine } from '@mdg-filius/core';
import { docSnapshot } from '@mdg-filius/doc';
import {
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 {
parseClientMessage,
SIM_PROTOCOL_VERSION,
@@ -77,12 +93,61 @@ export class SimulationRoom {
private lastActivity: string[] = [];
/** Each computer's command line window, kept over stops. */
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(
private readonly doc: Y.Doc,
private readonly options: RoomOptions,
) {
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. */
@@ -168,13 +233,8 @@ export class SimulationRoom {
this.engine.setSimulationSpeed(this.speed);
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]));
for (const node of snapshot.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);
}
this.software = this.documentSoftware();
this.syncTerminalWindows();
this.mode = 'simulation';
this.lastWall = this.options.now();
this.broadcast(this.state());
+1
View File
@@ -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. */
attach(terminal: Terminal): void {
if (this.terminal === terminal) return;
this.terminal = terminal;
this.seen = 0;
terminal.continueIn(this.directory);
+44 -1
View File
@@ -10,7 +10,7 @@ import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
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 { 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']);
});
});
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'");
});
});