feat: F4.6g2 the DNS server window, its records being /dns/hosts

Oracle DnsWindowRun uses Java's DNS server window as a person does and
asks a computer's resolver after each step. The core's DNS server is a
program its window changes: on and off (its socket on port 53),
recursive resolution, and records that are /dns/hosts, read on every
query as Java's server reads them (the reader moves from fls to the
core). The snapshot carries every installed DNS server with whether it
runs. The room takes the window's actions, sends its view and writes
the server back after the run; the editor shows Java's window with its
three tabs. Not taken from Java: a cell edit spilling into records of
another type, an add losing what another program wrote into the file,
an empty mail domain, a removal hitting the first record of the name.
A table cell no longer takes the room's value over what is being typed
(the web server's too).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 20:36:17 +00:00
co-authored by Claude Opus 5.5
parent e31da62ae0
commit 065b42430d
27 changed files with 2850 additions and 158 deletions
+48 -2
View File
@@ -71,6 +71,7 @@ import {
type Topic,
type EmailClientView,
type EmailServerView,
type DnsServerView,
type EchoServerView,
type SimpleClientView,
type TraceFrame,
@@ -244,6 +245,12 @@ export class SimulationRoom {
active.echoServer = echo.isActive;
settings.echoServer = { port: echo.port };
}
const dns = this.engine.dnsServer(id);
const dnsSettings = setting('dnsServer');
if (dns !== undefined && dnsSettings !== undefined) {
active.dnsServer = dns.isActive;
settings.dnsServer = { ...(dnsSettings as object), recursiveResolutionEnabled: dns.recursiveResolutionEnabled, records: dns.answering };
}
const client = this.engine.simpleClient(id);
if (client !== undefined) settings.simpleClient = { targetAddress: client.targetAddress, targetPort: client.targetPort };
settings.emailClient = snapshot.emailClientsByNodeId.get(id);
@@ -454,6 +461,38 @@ export class SimulationRoom {
return this.actInEchoServer(input);
case 'simpleClient':
return this.actInSimpleClient(input);
case 'dnsServer':
return this.actInDnsServer(input);
default:
return 'unknownAction';
}
}
/** The start button, the box for recursive resolution, a table's "Hinzufügen", "Auswahl entfernen" and a cell written. */
private actInDnsServer(input: Extract<ClientMessage, { kind: 'input' }>): ErrorCode | undefined {
const server = this.engine.dnsServer(input.node);
if (server === undefined) return 'unknownApp';
const { args } = input;
const type = args.type === 'A' || args.type === 'MX' || args.type === 'NS' ? args.type : undefined;
const row = Number.isInteger(args.row) ? (args.row as number) : undefined;
switch (input.action) {
case 'setActive':
if (typeof args.active !== 'boolean') return 'badArgs';
server.setActive(args.active);
return undefined;
case 'setRecursive':
if (typeof args.enabled !== 'boolean') return 'badArgs';
server.setRecursiveResolutionEnabled(args.enabled);
return undefined;
case 'addRecord':
if (type === undefined || typeof args.name !== 'string' || typeof args.data !== 'string') return 'badArgs';
return server.addRecord(type, args.name, args.data) ? undefined : 'badArgs';
case 'removeRecord':
if (type === undefined || row === undefined) return 'badArgs';
return server.removeRecord(type, row) ? undefined : 'badArgs';
case 'changeRecord':
if (type === undefined || row === undefined || (args.column !== 0 && args.column !== 1) || typeof args.value !== 'string') return 'badArgs';
return server.changeRecord(type, row, args.column, args.value) ? undefined : 'badArgs';
default:
return 'unknownAction';
}
@@ -634,6 +673,13 @@ export class SimulationRoom {
const view: EmailClientView = { account: client.account, inbox: client.inbox.map((entry) => entry.mail), sent: client.sent.map((entry) => entry.mail) };
return view;
}
case 'dnsServer': {
const server = this.engine.dnsServer(node);
if (server === undefined) return null;
const rows = (type: 'A' | 'MX' | 'NS') => server.rows(type).map(({ record }) => [record.domainName, record.rdata] as const);
const view: DnsServerView = { active: server.isActive, recursive: server.recursiveResolutionEnabled, rows: { A: rows('A'), MX: rows('MX'), NS: rows('NS') } };
return view;
}
case 'echoServer': {
const server = this.engine.echoServer(node);
if (server === undefined) return null;
@@ -725,8 +771,8 @@ export class SimulationRoom {
client.viewSent.set(topic, key);
client.send(message({ kind: 'view', node: node!, app: app as AppKind, view }));
}
// The browser's lines are its view.
if (app === 'webBrowser') continue;
// The browser's lines are its view; the DNS server's window has none.
if (app === 'webBrowser' || app === 'dnsServer') continue;
}
const sent = client.appSent.get(topic);
if (app === 'terminal') {
+113
View File
@@ -0,0 +1,113 @@
// The DNS server's window in the room, on FILIUS' example email_komplett:
// its view (button, box, the three tables as /dns/hosts holds them), its
// actions, /dns/hosts written by another into the document and shown at
// once, and what a run leaves in the project (the file, whether it runs,
// recursive resolution, the records in its settings). The server's steps
// against Java's window are packages/core/test/dnsWindow.test.ts.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import * as Y from 'yjs';
import { changeFiles, fileText, readProject, writeProject } from '@mdg-filius/doc';
import { openProjectArchive } from '@mdg-filius/fls';
import type { DnsServerView, HostMessage } from '@mdg-filius/protocol';
import { SimulationRoom, TICK_MILLISECONDS } from '../src/index.js';
const here = dirname(fileURLToPath(import.meta.url));
const corpus = join(here, '../../../corpus/java-examples');
function setUp() {
let ids = 0;
const doc = new Y.Doc();
writeProject(
doc,
openProjectArchive(new Uint8Array(readFileSync(join(corpus, 'email_komplett.fls'))), () => `00000000-0000-0000-0000-${(++ids).toString(16).padStart(12, '0')}`),
);
const id = readProject(doc).graph.nodes.find((node) => node.displayName === 'DNS-Server')!.id;
let now = 0;
const room = new SimulationRoom(doc, { now: () => now, seed: () => 5 });
let view: DnsServerView | null | undefined;
const errors: string[] = [];
const lines: HostMessage[] = [];
const client = room.connect((message: HostMessage) => {
if (message.kind === 'app') lines.push(message);
if (message.kind === 'view' && message.app === 'dnsServer') view = message.view as DnsServerView | null;
if (message.kind === 'error') errors.push(message.code);
});
const send = (fields: object) => room.receive(client, { v: 1, ...fields });
send({ kind: 'sub', topics: [`app:${id}/dnsServer`] });
send({ kind: 'start' });
const run = (milliseconds: number) => {
for (let elapsed = 0; elapsed < milliseconds; elapsed += TICK_MILLISECONDS) {
now += TICK_MILLISECONDS;
room.tick();
}
};
run(TICK_MILLISECONDS);
const act = (action: string, args: object = {}) => send({ kind: 'input', node: id, app: 'dnsServer', action, args, reqId: action });
return { doc, id, room, view: () => view, errors, lines, act, run, send };
}
describe("the DNS server's window in the room", () => {
it('shows the server and its tables, takes its actions, and shows what another writes into /dns/hosts at once', () => {
const { doc, id, view, act, run, errors, lines } = setUp();
// Java's DNS window has no lines: none are sent.
expect(lines).toEqual([]);
const hosts = () => fileText(doc, id, '/dns/hosts')!.toString();
expect(view()!.active).toBe(true);
const before = view()!.rows.A.length;
act('addRecord', { type: 'A', name: 'neu.schule.de', data: '10.0.0.1' });
act('addRecord', { type: 'MX', name: 'schule.de', data: 'mail.schule.de' });
act('addRecord', { type: 'NS', name: 'schule.de', data: 'ns.schule.de' });
expect(view()!.rows.A.at(-1)).toEqual(['neu.schule.de.', '10.0.0.1']);
expect([view()!.rows.MX.at(-1), view()!.rows.NS.at(-1)]).toEqual([
['schule.de.', 'mail.schule.de.'],
['schule.de.', 'ns.schule.de.'],
]);
// In the document's file at once.
expect(hosts().endsWith('neu.schule.de. A 3600 10.0.0.1\nschule.de. MX 3600 mail.schule.de.\nschule.de. NS 3600 ns.schule.de.\n')).toBe(true);
act('changeRecord', { type: 'A', row: before, column: 1, value: '10.0.0.2' });
expect(view()!.rows.A.at(-1)).toEqual(['neu.schule.de.', '10.0.0.2']);
act('removeRecord', { type: 'NS', row: 0 });
expect(view()!.rows.NS).toEqual([]);
act('setRecursive', { enabled: true });
act('setActive', { active: false });
expect([view()!.active, view()!.recursive]).toEqual([false, true]);
expect(errors).toEqual([]);
// The text editor writes the file: the table shows it.
changeFiles(doc, id, 'me', (files) => files.writeTextFile('/dns/hosts', `${files.textFile('/dns/hosts')}extern.schule.de. A 3600 10.0.0.9\n`));
run(TICK_MILLISECONDS);
expect(view()!.rows.A.at(-1)).toEqual(['extern.schule.de.', '10.0.0.9']);
});
it('leaves in the project what the run made of the server', () => {
const { doc, id, act, send } = setUp();
act('addRecord', { type: 'A', name: 'neu.schule.de', data: '10.0.0.1' });
act('setRecursive', { enabled: true });
act('setActive', { active: false });
send({ kind: 'stop' });
const project = readProject(doc);
expect(project.activePrograms.get(id)?.has('dnsServer') ?? false).toBe(false);
expect(project.dnsServerConfigurations.get(id)!.recursiveResolutionEnabled).toBe(true);
expect(project.dnsServerConfigurations.get(id)!.records.at(-1)).toEqual({ name: 'neu.schule.de', type: 'A', ttlSeconds: 3600, target: '10.0.0.1' });
expect(project.virtualFileSystems.get(id)!.textFile('/dns/hosts').endsWith('neu.schule.de. A 3600 10.0.0.1\n')).toBe(true);
});
it('refuses what the window never hands over', () => {
const { act, errors } = setUp();
act('setActive', { active: 'no' });
act('setRecursive', {});
act('addRecord', { type: 'TXT', name: 'a.de', data: 'x' });
act('addRecord', { type: 'A', name: '', data: '10.0.0.1' });
act('addRecord', { type: 'A', name: 'a.de', data: 5 });
act('removeRecord', { type: 'A', row: 1.5 });
act('removeRecord', { type: 'NS', row: 0 });
act('changeRecord', { type: 'A', row: 0, column: 2, value: 'x' });
act('changeRecord', { type: 'A', row: 0, column: 1, value: 'kein' });
act('changeRecord', { type: 'A', row: 0, column: 1, value: 3 });
act('restart');
expect(errors).toEqual(['badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'unknownAction']);
});
});