feat(fls): import installed programs and their settings

importProject carries a host's installed programs into the model with
the settings Swift keeps (web server port, Gnutella peer limit, DNS
recursion and forwarder, personal firewall) and two it does not (the echo
server's port, the generic client's target), plus Java's "switched on".
The Swift oracle writes programs and settings; they match on the 13 files
Swift reads completely.

Intended differences, named in the test:
- every FILIUS program is kept; Swift drops command prompt, text
  editor, file explorer, image viewer, echo server and generic client;
- a web server's files stay in /webserver, its document root; Swift
  moves them to /www. No program state goes into a host's files.
- multicast_server.fls: maxTeilnehmerZahl refers to a shared Integer
  (a button's icon text gap, 4); Swift falls back to 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 06:12:15 +00:00
co-authored by Claude Opus 5.5
parent 8080e7dcb2
commit c3fe56ab16
6 changed files with 800 additions and 92 deletions
+114 -2
View File
@@ -27,7 +27,13 @@
// - a PC or notebook whose file has no file system gets an empty one, as in
// Java — Swift gives it the demo files of a new device;
// - a file system's root is Java's root, whatever its user object says.
// Not here yet: applications (with them the web server's /webserver → /www).
// - every installed application of FILIUS is kept: Swift drops the command
// prompt, text editor, file explorer, image viewer, echo server and
// generic client, and their settings (port, target) have a place here;
// - a web server's files stay in /webserver (its document root), where Swift
// moves them to /www; and no program state is written into a host's files
// (Swift keeps Gnutella and e-mail state there as hidden JSON files).
// Not here yet: DNS records (from /dns/hosts) and e-mail accounts.
import {
canonicalMacAddress,
@@ -35,6 +41,7 @@ import {
defaultSwitchConfiguration,
dhcpServerConfiguration,
DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS,
DEFAULT_WEB_SERVER_PORT,
deterministicDocumentationUuid,
documentationColor,
documentationItem,
@@ -54,6 +61,12 @@ import {
type DeviceConfiguration,
type DhcpClientConfiguration,
type DhcpServerConfiguration,
type DnsServerConfiguration,
type EchoServerConfiguration,
type GnutellaConfiguration,
type InstallableProgram,
type SimpleClientConfiguration,
type WebServerConfiguration,
type GatewayPortForwardingRow,
type RemoteLinkConfiguration,
type FirewallAction,
@@ -75,7 +88,7 @@ import {
type TopologyNode,
type VirtualFileContent,
} from '@mdg-filius/core';
import type { FlsFileTree, FlsFirewall } from './systemSoftware.js';
import type { FlsApplicationClass, FlsFileTree, FlsFirewall } from './systemSoftware.js';
import type { FlsDocumentationItem, FlsInterface, FlsNode, FlsNodeClass, FlsTopology } from './topology.js';
export interface ImportedProject {
@@ -96,6 +109,14 @@ export interface ImportedProject {
remoteLinkConfigurations: Map<Id, RemoteLinkConfiguration>;
/** Every PC and notebook. */
virtualFileSystems: Map<Id, VirtualFileSystem>;
installedPrograms: Map<Id, Set<InstallableProgram>>;
/** Server programs Java had switched on (aktiv). */
activePrograms: Map<Id, Set<InstallableProgram>>;
webServerConfigurations: Map<Id, WebServerConfiguration>;
gnutellaConfigurations: Map<Id, GnutellaConfiguration>;
dnsServerConfigurations: Map<Id, DnsServerConfiguration>;
echoServerConfigurations: Map<Id, EchoServerConfiguration>;
simpleClientConfigurations: Map<Id, SimpleClientConfiguration>;
/** In render order: rectangles below text, then by order. */
documentationItems: DocumentationItem[];
/** The topology's warnings, then this step's. */
@@ -132,6 +153,13 @@ export function importProject(topology: FlsTopology, newId: () => Id): ImportedP
portForwardingRowsByNodeId: new Map(),
remoteLinkConfigurations: new Map(),
virtualFileSystems: new Map(),
installedPrograms: new Map(),
activePrograms: new Map(),
webServerConfigurations: new Map(),
gnutellaConfigurations: new Map(),
dnsServerConfigurations: new Map(),
echoServerConfigurations: new Map(),
simpleClientConfigurations: new Map(),
};
const nodes = topology.nodes.map((source) => {
const node = importNode(source, newId, warnings);
@@ -140,6 +168,7 @@ export function importProject(topology: FlsTopology, newId: () => Id): ImportedP
registerServiceConfigurations(source, node, system, newId, warnings);
if (node.kind === 'pc' || node.kind === 'notebook') {
system.virtualFileSystems.set(node.id, importFileSystem(source.system.fileSystem, node.displayName, warnings));
registerPrograms(source, node, system, warnings);
}
return node;
});
@@ -331,8 +360,91 @@ type SystemConfigurations = Pick<
| 'portForwardingRowsByNodeId'
| 'remoteLinkConfigurations'
| 'virtualFileSystems'
| 'installedPrograms'
| 'activePrograms'
| 'webServerConfigurations'
| 'gnutellaConfigurations'
| 'dnsServerConfigurations'
| 'echoServerConfigurations'
| 'simpleClientConfigurations'
>;
/** Java's application classes as Swift's programs. */
export const PROGRAM_OF_CLASS: Readonly<Record<FlsApplicationClass, InstallableProgram>> = {
'filius.software.clientserver.ClientBaustein': 'simpleClient',
'filius.software.clientserver.ServerBaustein': 'echoServer',
'filius.software.dateiaustausch.PeerToPeerAnwendung': 'gnutella',
'filius.software.dns.DNSServer': 'dnsServer',
'filius.software.email.EmailAnwendung': 'emailClient',
'filius.software.email.EmailServer': 'emailServer',
'filius.software.firewall.Firewall': 'firewall',
'filius.software.lokal.FileExplorer': 'fileExplorer',
'filius.software.lokal.ImageViewer': 'imageViewer',
'filius.software.lokal.Terminal': 'commandPrompt',
'filius.software.lokal.TextEditor': 'textEditor',
'filius.software.www.WebBrowser': 'webBrowser',
'filius.software.www.WebServer': 'webServer',
};
/** The web server's document root: where Java keeps its files. */
export const JAVA_WEB_ROOT = '/webserver';
/** The program parts of Swift's finalizeCurrentNodeCandidate, for a PC or notebook. */
function registerPrograms(source: FlsNode, node: TopologyNode, system: SystemConfigurations, warnings: string[]): void {
const installed = new Set<InstallableProgram>();
const active = new Set<InstallableProgram>();
for (const application of source.system.applications ?? []) {
const program = PROGRAM_OF_CLASS[application.javaClass];
installed.add(program);
if (application.isActive === true) active.add(program);
switch (program) {
case 'webServer':
system.webServerConfigurations.set(node.id, {
port: application.port ?? DEFAULT_WEB_SERVER_PORT,
documentRoot: JAVA_WEB_ROOT,
});
break;
case 'gnutella':
// Without one in the file Java rolls 3, 4 or 5; Swift's default is 3.
system.gnutellaConfigurations.set(node.id, { maximumKnownPeers: application.maxPeers ?? 3 });
break;
case 'dnsServer': {
// Java's DNSServer forwards to the host's own DNS server setting;
// only one that differs from the host's own address is a forwarder.
const recursive = application.recursiveResolutionEnabled === true;
const forwarder = trimmedOrEmpty(source.interfaces[0]?.dns);
const primary = trimmedOrEmpty(source.interfaces[0]?.ip);
const forwarding = recursive && forwarder !== '' && forwarder !== primary;
system.dnsServerConfigurations.set(
node.id,
forwarding
? { recursiveResolutionEnabled: recursive, forwardingServerIPAddress: forwarder }
: { recursiveResolutionEnabled: recursive },
);
break;
}
case 'firewall':
if (application.firewall !== undefined) {
system.firewallConfigurationsByNodeId.set(node.id, firewallConfigurationOf(application.firewall, `node '${node.displayName}'`, warnings));
}
break;
case 'echoServer':
system.echoServerConfigurations.set(node.id, { port: application.port ?? 55_555 });
break;
case 'simpleClient':
system.simpleClientConfigurations.set(node.id, {
targetAddress: trimmedOrEmpty(application.targetAddress),
targetPort: application.targetPort ?? 55_555,
});
break;
default:
break;
}
}
if (installed.size > 0) system.installedPrograms.set(node.id, installed);
if (active.size > 0) system.activePrograms.set(node.id, active);
}
/** A row of text cells, trimmed, or undefined unless it has exactly four. */
function fourCells(row: (string | null)[]): [string, string, string, string] | undefined {
if (row.length !== 4 || row.some((cell) => cell === null)) return undefined;