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:
@@ -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') {
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user