// 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; /** Where a program keeps its settings in a node's settings. */ const SETTINGS_KEY: Partial> = { 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_.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> = { 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>(ROOTS.nodes); const ignore = () => {}; /** A text file of a node's files, undefined without it. */ function textOf(files: Y.Map>, 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>): PlainValue | undefined { const application = (fields: Partial) => 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).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>; const settings = node.get('settings') as Y.Map; const files = node.get('files') as Y.Map>; 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(); 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); }