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
+1
View File
@@ -8,3 +8,4 @@ export * from './sheet.js';
export * from './snapshot.js';
export * from './values.js';
export * from './view.js';
export * from './software.js';
+1 -1
View File
@@ -98,7 +98,7 @@ function ownApplications(text: string): 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>();
entry.set('order', order);
entry.set('kind', content.kind);
+144
View File
@@ -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);
}