test(java-oracle): TcpRun records TCP as Java FILIUS runs it

Three stories of socket calls on tcp-lan's network (exchange, closedPort,
lostData): every frame with TCP flags and numbers, when each call started,
returned or failed, and each socket's states. Network building and the
packet log move from DhcpRun to ScenarioNetwork.

Measured: 3 RTT per attempt (8250 ms at 90 %), an orderly close after a
failed send, TIME_WAIT of 3 RTT, the passive side closing by itself on FIN,
one SYN only (Java's retry loop ends after the first timeout).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 11:08:43 +00:00
co-authored by Claude Opus 5.5
parent ed437f9b5c
commit cd3c6a432c
8 changed files with 1740 additions and 385 deletions
+14 -384
View File
@@ -1,396 +1,26 @@
// Runs a core scenario's network in Java FILIUS and records what DHCP really
// does: every frame each network card saw (FILIUS' own packet log, the
// Lauscher) and each host's address configuration at the end.
// Runs a core scenario's network in Java FILIUS (ScenarioNetwork) and records
// what DHCP really does: every frame each network card saw and each host's
// address configuration at the end.
//
// DhcpRun <scenario.json> <milliseconds> <out.json>
//
// The network is built from the scenario (nodes, links, devices,
// dhcpServers, dhcpClients; steps are ignored) the way the core reads it: a
// port's MAC is its `mac`, else the core's stableGeneratedMacAddress. The
// simulation then runs as the action mode starts it (every system software's
// starten()), in real time, for the given milliseconds. Threads make the
// times vary from run to run; the order of one client's messages does not.
import filius.gui.GUIContainer;
import filius.hardware.Kabel;
import filius.hardware.NetzwerkInterface;
import filius.hardware.Port;
import filius.hardware.knoten.InternetKnoten;
import filius.hardware.knoten.Knoten;
import filius.hardware.knoten.LokalerKnoten;
import filius.hardware.knoten.Notebook;
import filius.hardware.knoten.Rechner;
import filius.hardware.knoten.Switch;
import filius.rahmenprogramm.nachrichten.Lauscher;
import filius.software.dhcp.DHCPServer;
import filius.software.netzzugangsschicht.EthernetFrame;
import filius.software.system.InternetKnotenBetriebssystem;
import filius.software.transportschicht.Segment;
import filius.software.vermittlungsschicht.ArpPaket;
import filius.software.vermittlungsschicht.IcmpPaket;
import filius.software.vermittlungsschicht.IpPaket;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
// The scenario's steps are ignored: the network runs, in real time, for the
// given milliseconds. Threads make the times vary from run to run; the order
// of one client's messages does not.
public class DhcpRun {
public static void main(String[] args) throws Exception {
@SuppressWarnings("unchecked")
Map<String, Object> scenario = (Map<String, Object>) new JsonReader(
new String(Files.readAllBytes(Paths.get(args[0])), StandardCharsets.UTF_8)).value();
long duration = Long.parseLong(args[1]);
Map<String, Knoten> nodes = new LinkedHashMap<>();
Map<String, Port> ports = new HashMap<>();
Map<String, String> macByNode = new LinkedHashMap<>();
for (Object each : list(scenario.get("nodes"))) {
Map<String, Object> node = map(each);
String id = (String) node.get("id");
String kind = (String) node.get("kind");
List<Object> nodePorts = list(node.get("ports"));
Knoten knoten;
switch (kind) {
case "pc":
knoten = new Rechner();
break;
case "notebook":
knoten = new Notebook();
break;
case "networkSwitch":
knoten = new Switch();
break;
default:
throw new IllegalArgumentException("node kind " + kind + " is not built by this tool");
}
knoten.setName(id);
if (knoten instanceof InternetKnoten) {
if (nodePorts.size() != 1) throw new IllegalArgumentException("a host has one port here");
Map<String, Object> port = map(nodePorts.get(0));
NetzwerkInterface nic = ((InternetKnoten) knoten).getNetzwerkInterfaces().get(0);
String mac = port.containsKey("mac") ? (String) port.get("mac") : stableMac((String) port.get("id"));
nic.setMac(mac);
ports.put((String) port.get("id"), nic.getPort());
macByNode.put(id, mac);
} else {
LinkedList<Port> switchPorts = ((LokalerKnoten) knoten).getAnschluesse();
for (int index = 0; index < nodePorts.size(); index++) {
ports.put((String) map(nodePorts.get(index)).get("id"), switchPorts.get(index));
}
}
nodes.put(id, knoten);
}
for (Map.Entry<String, Object> entry : map(scenario.getOrDefault("devices", new HashMap<>())).entrySet()) {
Map<String, Object> device = map(entry.getValue());
NetzwerkInterface nic = ((InternetKnoten) nodes.get(entry.getKey())).getNetzwerkInterfaces().get(0);
nic.setIp((String) device.get("ip"));
nic.setSubnetzMaske((String) device.get("mask"));
nic.setGateway((String) device.getOrDefault("gateway", ""));
nic.setDns((String) device.getOrDefault("dns", ""));
}
for (Object each : list(scenario.get("links"))) {
Map<String, Object> link = map(each);
Kabel cable = new Kabel();
cable.setAnschluesse(new Port[] { ports.get(link.get("sourcePort")), ports.get(link.get("targetPort")) });
}
List<String> servers = new ArrayList<>();
for (Map.Entry<String, Object> entry : map(scenario.getOrDefault("dhcpServers", new HashMap<>())).entrySet()) {
Map<String, Object> config = map(entry.getValue());
DHCPServer server = system(nodes.get(entry.getKey())).getDHCPServer();
server.setUntergrenze((String) config.getOrDefault("lower", "0.0.0.0"));
server.setObergrenze((String) config.getOrDefault("upper", "0.0.0.0"));
server.setGatewayip((String) config.getOrDefault("gateway", "0.0.0.0"));
server.setDnsserverip((String) config.getOrDefault("dns", "0.0.0.0"));
server.setOwnSettings(Boolean.TRUE.equals(config.get("own")));
for (Object assignment : list(config.getOrDefault("staticAssignments", new ArrayList<>()))) {
server.addStaticAssignment((String) list(assignment).get(0), (String) list(assignment).get(1));
}
server.setAktiv(Boolean.TRUE.equals(config.get("active")));
servers.add(entry.getKey());
}
for (Object each : list(scenario.getOrDefault("dhcpClients", new ArrayList<>()))) {
system(nodes.get((String) each)).setDHCPKonfiguration(true);
}
// Switches and DHCP servers first: FILIUS' client waits for every active
// server to have started (waitUntilDhcpServerStarted, through the GUI's
// node list, which this tool has not got).
// FILIUS' window exists before a simulation starts; created lazily and
// unsynchronized, the first clients to ask for it would race.
GUIContainer.getGUIContainer();
long start = System.currentTimeMillis();
for (Map.Entry<String, Knoten> entry : nodes.entrySet()) {
if (entry.getValue() instanceof Switch || servers.contains(entry.getKey())) entry.getValue().getSystemSoftware().starten();
}
Thread.sleep(200);
for (Map.Entry<String, Knoten> entry : nodes.entrySet()) {
if (!(entry.getValue() instanceof Switch || servers.contains(entry.getKey()))) entry.getValue().getSystemSoftware().starten();
}
Thread.sleep(duration);
Field field = Lauscher.class.getDeclaredField("datenEinheiten");
field.setAccessible(true);
@SuppressWarnings("unchecked")
Map<String, LinkedList<Object[]>> log = (Map<String, LinkedList<Object[]>>) field.get(Lauscher.getLauscher());
Json out = new Json();
ScenarioNetwork network = ScenarioNetwork.read(args[0]);
network.start();
Thread.sleep(Long.parseLong(args[1]));
ScenarioNetwork.Json out = new ScenarioNetwork.Json();
out.open('{');
out.key("frames").open('{');
for (Map.Entry<String, String> entry : macByNode.entrySet()) {
out.key(entry.getKey()).open('[');
LinkedList<Object[]> frames = log.get(entry.getValue());
if (frames != null) {
synchronized (frames) {
for (Object[] stamped : frames) frame(out, ((Long) stamped[0]) - start, (EthernetFrame) stamped[1]);
}
}
out.close(']');
}
network.writeFrames(out);
network.writeDevices(out);
out.close('}');
out.key("devices").open('{');
for (Map.Entry<String, Knoten> entry : nodes.entrySet()) {
if (!(entry.getValue() instanceof InternetKnoten)) continue;
NetzwerkInterface nic = ((InternetKnoten) entry.getValue()).getNetzwerkInterfaces().get(0);
out.key(entry.getKey()).open('[').string(nic.getIp()).string(nic.getSubnetzMaske()).string(nic.getGateway())
.string(nic.getDns()).close(']');
}
out.close('}');
out.close('}');
for (Knoten knoten : nodes.values()) {
try {
knoten.getSystemSoftware().beenden();
} catch (Exception e) {}
}
try (Writer writer = new OutputStreamWriter(new FileOutputStream(args[2]), StandardCharsets.UTF_8)) {
writer.write(out.toString());
writer.write('\n');
}
network.stop();
ScenarioNetwork.write(args[2], out);
System.exit(0);
}
private static void frame(Json out, long time, EthernetFrame frame) {
out.open('{').key("time").number(time).key("source").string(frame.getQuellMacAdresse()).key("destination")
.string(frame.getZielMacAdresse());
Object data = frame.getDaten();
if (data instanceof ArpPaket) {
ArpPaket arp = (ArpPaket) data;
out.key("arp").number(arp.getOperation()).key("senderIp").string(arp.getSenderIP()).key("targetIp")
.string(arp.getTargetIP());
} else if (data instanceof IcmpPaket) {
out.key("icmp").number(((IcmpPaket) data).getIcmpType());
} else if (data instanceof IpPaket) {
IpPaket ip = (IpPaket) data;
Segment segment = ip.getSegment();
out.key("sourceIp").string(ip.getSender()).key("destinationIp").string(ip.getEmpfaenger()).key("protocol")
.number(ip.getProtocol()).key("sourcePort").number(segment.getQuellPort()).key("destinationPort")
.number(segment.getZielPort()).key("payload").string(segment.getDaten());
}
out.close('}');
}
private static InternetKnotenBetriebssystem system(Knoten knoten) {
return (InternetKnotenBetriebssystem) knoten.getSystemSoftware();
}
/** The core's stableGeneratedMacAddress (packages/core/src/model/node.ts). */
static String stableMac(String portId) {
String compact = portId.toUpperCase().replace("-", "");
String suffix = compact.substring(compact.length() - 10);
StringBuilder mac = new StringBuilder("02");
for (int offset = 0; offset < suffix.length(); offset += 2) mac.append(':').append(suffix, offset, offset + 2);
return mac.toString();
}
@SuppressWarnings("unchecked")
private static Map<String, Object> map(Object value) {
return (Map<String, Object>) value;
}
@SuppressWarnings("unchecked")
private static List<Object> list(Object value) {
return (List<Object>) value;
}
/** Just enough JSON for a scenario: objects, arrays, strings, numbers, booleans, null. */
static final class JsonReader {
private final String text;
private int position;
JsonReader(String text) {
this.text = text;
}
Object value() {
skip();
char c = text.charAt(position);
if (c == '{') {
Map<String, Object> result = new TreeMap<>();
position++;
skip();
if (text.charAt(position) == '}') {
position++;
return result;
}
for (;;) {
skip();
String key = (String) value();
skip();
expect(':');
result.put(key, value());
skip();
if (text.charAt(position++) == '}') return result;
}
}
if (c == '[') {
List<Object> result = new ArrayList<>();
position++;
skip();
if (text.charAt(position) == ']') {
position++;
return result;
}
for (;;) {
result.add(value());
skip();
if (text.charAt(position++) == ']') return result;
}
}
if (c == '"') {
StringBuilder result = new StringBuilder();
position++;
for (char each = text.charAt(position++); each != '"'; each = text.charAt(position++)) {
if (each == '\\') {
char escaped = text.charAt(position++);
switch (escaped) {
case 'n':
result.append('\n');
break;
case 't':
result.append('\t');
break;
case 'u':
result.append((char) Integer.parseInt(text.substring(position, position + 4), 16));
position += 4;
break;
default:
result.append(escaped);
}
} else {
result.append(each);
}
}
return result.toString();
}
if (text.startsWith("true", position)) {
position += 4;
return Boolean.TRUE;
}
if (text.startsWith("false", position)) {
position += 5;
return Boolean.FALSE;
}
if (text.startsWith("null", position)) {
position += 4;
return null;
}
int begin = position;
while (position < text.length() && "+-.eE0123456789".indexOf(text.charAt(position)) >= 0) position++;
return Double.parseDouble(text.substring(begin, position));
}
private void skip() {
while (position < text.length() && Character.isWhitespace(text.charAt(position))) position++;
}
private void expect(char c) {
if (text.charAt(position++) != c) throw new IllegalArgumentException("expected " + c + " at " + (position - 1));
}
}
/** A JSON writer with commas and indentation. */
static final class Json {
private final StringBuilder text = new StringBuilder();
private final LinkedList<Boolean> first = new LinkedList<>();
private boolean afterKey;
private void separate() {
if (afterKey) {
afterKey = false;
return;
}
if (first.isEmpty()) return;
if (!first.peek()) text.append(',');
first.pop();
first.push(false);
text.append('\n');
for (int i = 0; i < first.size(); i++) text.append(" ");
}
Json open(char bracket) {
separate();
text.append(bracket);
first.push(true);
return this;
}
Json close(char bracket) {
boolean empty = first.pop();
if (!empty) {
text.append('\n');
for (int i = 0; i < first.size(); i++) text.append(" ");
}
text.append(bracket);
return this;
}
Json key(String key) {
separate();
quote(key);
text.append(": ");
afterKey = true;
return this;
}
Json string(String value) {
separate();
if (value == null) text.append("null");
else quote(value);
return this;
}
Json number(long value) {
separate();
text.append(value);
return this;
}
private void quote(String value) {
text.append('"');
for (char c : value.toCharArray()) {
if (c == '"' || c == '\\') text.append('\\').append(c);
else if (c == '\n') text.append("\\n");
else if (c < 0x20) text.append(String.format("\\u%04x", (int) c));
else text.append(c);
}
text.append('"');
}
@Override
public String toString() {
return text.toString();
}
}
}
+418
View File
@@ -0,0 +1,418 @@
// A core scenario's network (packages/core/test/scenarios/*.scenario.json:
// nodes, links, devices, dhcpServers, dhcpClients) built in Java FILIUS the
// way the core reads it: a port's MAC is its `mac`, else the core's
// stableGeneratedMacAddress. Started as the action mode starts it (every
// system software's starten()); afterwards every frame each network card saw
// (FILIUS' own packet log, the Lauscher) and each host's addresses. Shared by
// DhcpRun and TcpRun.
import filius.gui.GUIContainer;
import filius.hardware.Kabel;
import filius.hardware.NetzwerkInterface;
import filius.hardware.Port;
import filius.hardware.knoten.InternetKnoten;
import filius.hardware.knoten.Knoten;
import filius.hardware.knoten.LokalerKnoten;
import filius.hardware.knoten.Notebook;
import filius.hardware.knoten.Rechner;
import filius.hardware.knoten.Switch;
import filius.rahmenprogramm.nachrichten.Lauscher;
import filius.software.dhcp.DHCPServer;
import filius.software.netzzugangsschicht.EthernetFrame;
import filius.software.system.InternetKnotenBetriebssystem;
import filius.software.transportschicht.Segment;
import filius.software.transportschicht.TcpSegment;
import filius.software.vermittlungsschicht.ArpPaket;
import filius.software.vermittlungsschicht.IcmpPaket;
import filius.software.vermittlungsschicht.IpPaket;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class ScenarioNetwork {
final Map<String, Knoten> nodes = new LinkedHashMap<>();
final Map<String, String> macByNode = new LinkedHashMap<>();
final List<String> servers = new ArrayList<>();
long start;
@SuppressWarnings("unchecked")
static ScenarioNetwork read(String path) throws Exception {
Map<String, Object> scenario = (Map<String, Object>) new JsonReader(
new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8)).value();
ScenarioNetwork network = new ScenarioNetwork();
network.build(scenario);
return network;
}
private void build(Map<String, Object> scenario) {
Map<String, Port> ports = new HashMap<>();
for (Object each : list(scenario.get("nodes"))) {
Map<String, Object> node = map(each);
String id = (String) node.get("id");
String kind = (String) node.get("kind");
List<Object> nodePorts = list(node.get("ports"));
Knoten knoten;
switch (kind) {
case "pc":
knoten = new Rechner();
break;
case "notebook":
knoten = new Notebook();
break;
case "networkSwitch":
knoten = new Switch();
break;
default:
throw new IllegalArgumentException("node kind " + kind + " is not built by this tool");
}
knoten.setName(id);
if (knoten instanceof InternetKnoten) {
if (nodePorts.size() != 1) throw new IllegalArgumentException("a host has one port here");
Map<String, Object> port = map(nodePorts.get(0));
NetzwerkInterface nic = ((InternetKnoten) knoten).getNetzwerkInterfaces().get(0);
String mac = port.containsKey("mac") ? (String) port.get("mac") : stableMac((String) port.get("id"));
nic.setMac(mac);
ports.put((String) port.get("id"), nic.getPort());
macByNode.put(id, mac);
} else {
LinkedList<Port> switchPorts = ((LokalerKnoten) knoten).getAnschluesse();
for (int index = 0; index < nodePorts.size(); index++) {
ports.put((String) map(nodePorts.get(index)).get("id"), switchPorts.get(index));
}
}
nodes.put(id, knoten);
}
for (Map.Entry<String, Object> entry : map(scenario.getOrDefault("devices", new HashMap<>())).entrySet()) {
Map<String, Object> device = map(entry.getValue());
NetzwerkInterface nic = ((InternetKnoten) nodes.get(entry.getKey())).getNetzwerkInterfaces().get(0);
nic.setIp((String) device.get("ip"));
nic.setSubnetzMaske((String) device.get("mask"));
nic.setGateway((String) device.getOrDefault("gateway", ""));
nic.setDns((String) device.getOrDefault("dns", ""));
}
for (Object each : list(scenario.get("links"))) {
Map<String, Object> link = map(each);
Kabel cable = new Kabel();
cable.setAnschluesse(new Port[] { ports.get(link.get("sourcePort")), ports.get(link.get("targetPort")) });
}
for (Map.Entry<String, Object> entry : map(scenario.getOrDefault("dhcpServers", new HashMap<>())).entrySet()) {
Map<String, Object> config = map(entry.getValue());
DHCPServer server = system(nodes.get(entry.getKey())).getDHCPServer();
server.setUntergrenze((String) config.getOrDefault("lower", "0.0.0.0"));
server.setObergrenze((String) config.getOrDefault("upper", "0.0.0.0"));
server.setGatewayip((String) config.getOrDefault("gateway", "0.0.0.0"));
server.setDnsserverip((String) config.getOrDefault("dns", "0.0.0.0"));
server.setOwnSettings(Boolean.TRUE.equals(config.get("own")));
for (Object assignment : list(config.getOrDefault("staticAssignments", new ArrayList<>()))) {
server.addStaticAssignment((String) list(assignment).get(0), (String) list(assignment).get(1));
}
server.setAktiv(Boolean.TRUE.equals(config.get("active")));
servers.add(entry.getKey());
}
for (Object each : list(scenario.getOrDefault("dhcpClients", new ArrayList<>()))) {
system(nodes.get((String) each)).setDHCPKonfiguration(true);
}
}
/** Starts the simulation; times in the log count from here. */
void start() throws InterruptedException {
// Switches and DHCP servers first: FILIUS' client waits for every active
// server to have started (waitUntilDhcpServerStarted, through the GUI's
// node list, which this tool has not got).
// FILIUS' window exists before a simulation starts; created lazily and
// unsynchronized, the first clients to ask for it would race.
GUIContainer.getGUIContainer();
start = System.currentTimeMillis();
for (Map.Entry<String, Knoten> entry : nodes.entrySet()) {
if (entry.getValue() instanceof Switch || servers.contains(entry.getKey())) entry.getValue().getSystemSoftware().starten();
}
Thread.sleep(200);
for (Map.Entry<String, Knoten> entry : nodes.entrySet()) {
if (!(entry.getValue() instanceof Switch || servers.contains(entry.getKey()))) entry.getValue().getSystemSoftware().starten();
}
}
InternetKnotenBetriebssystem system(String nodeId) {
return (InternetKnotenBetriebssystem) nodes.get(nodeId).getSystemSoftware();
}
/** Every frame each host's card saw, by node. */
void writeFrames(Json out) throws Exception {
Field field = Lauscher.class.getDeclaredField("datenEinheiten");
field.setAccessible(true);
@SuppressWarnings("unchecked")
Map<String, LinkedList<Object[]>> log = (Map<String, LinkedList<Object[]>>) field.get(Lauscher.getLauscher());
out.key("frames").open('{');
for (Map.Entry<String, String> entry : macByNode.entrySet()) {
out.key(entry.getKey()).open('[');
LinkedList<Object[]> frames = log.get(entry.getValue());
if (frames != null) {
synchronized (frames) {
for (Object[] stamped : frames) frame(out, ((Long) stamped[0]) - start, (EthernetFrame) stamped[1]);
}
}
out.close(']');
}
out.close('}');
}
/** Each host's address, mask, gateway and DNS server. */
void writeDevices(Json out) {
out.key("devices").open('{');
for (Map.Entry<String, Knoten> entry : nodes.entrySet()) {
if (!(entry.getValue() instanceof InternetKnoten)) continue;
NetzwerkInterface nic = ((InternetKnoten) entry.getValue()).getNetzwerkInterfaces().get(0);
out.key(entry.getKey()).open('[').string(nic.getIp()).string(nic.getSubnetzMaske()).string(nic.getGateway())
.string(nic.getDns()).close(']');
}
out.close('}');
}
void stop() {
for (Knoten knoten : nodes.values()) {
try {
knoten.getSystemSoftware().beenden();
} catch (Exception e) {}
}
}
static void write(String path, Json out) throws Exception {
try (Writer writer = new OutputStreamWriter(new FileOutputStream(path), StandardCharsets.UTF_8)) {
writer.write(out.toString());
writer.write('\n');
}
}
private static void frame(Json out, long time, EthernetFrame frame) {
out.open('{').key("time").number(time).key("source").string(frame.getQuellMacAdresse()).key("destination")
.string(frame.getZielMacAdresse());
Object data = frame.getDaten();
if (data instanceof ArpPaket) {
ArpPaket arp = (ArpPaket) data;
out.key("arp").number(arp.getOperation()).key("senderIp").string(arp.getSenderIP()).key("targetIp")
.string(arp.getTargetIP());
} else if (data instanceof IcmpPaket) {
out.key("icmp").number(((IcmpPaket) data).getIcmpType());
} else if (data instanceof IpPaket) {
IpPaket ip = (IpPaket) data;
Segment segment = ip.getSegment();
out.key("sourceIp").string(ip.getSender()).key("destinationIp").string(ip.getEmpfaenger()).key("protocol")
.number(ip.getProtocol()).key("sourcePort").number(segment.getQuellPort()).key("destinationPort")
.number(segment.getZielPort()).key("payload").string(segment.getDaten());
if (segment instanceof TcpSegment) {
TcpSegment tcp = (TcpSegment) segment;
String flags = (tcp.isSyn() ? "S" : "") + (tcp.isAck() ? "A" : "") + (tcp.isFin() ? "F" : "")
+ (tcp.isPush() ? "P" : "") + (tcp.isRst() ? "R" : "");
out.key("tcpFlags").string(flags).key("sequence").number(tcp.getSeqNummer()).key("acknowledgement")
.number(tcp.getAckNummer());
}
}
out.close('}');
}
private static InternetKnotenBetriebssystem system(Knoten knoten) {
return (InternetKnotenBetriebssystem) knoten.getSystemSoftware();
}
/** The core's stableGeneratedMacAddress (packages/core/src/model/node.ts). */
static String stableMac(String portId) {
String compact = portId.toUpperCase().replace("-", "");
String suffix = compact.substring(compact.length() - 10);
StringBuilder mac = new StringBuilder("02");
for (int offset = 0; offset < suffix.length(); offset += 2) mac.append(':').append(suffix, offset, offset + 2);
return mac.toString();
}
@SuppressWarnings("unchecked")
private static Map<String, Object> map(Object value) {
return (Map<String, Object>) value;
}
@SuppressWarnings("unchecked")
private static List<Object> list(Object value) {
return (List<Object>) value;
}
/** Just enough JSON for a scenario: objects, arrays, strings, numbers, booleans, null. */
static final class JsonReader {
private final String text;
private int position;
JsonReader(String text) {
this.text = text;
}
Object value() {
skip();
char c = text.charAt(position);
if (c == '{') {
Map<String, Object> result = new TreeMap<>();
position++;
skip();
if (text.charAt(position) == '}') {
position++;
return result;
}
for (;;) {
skip();
String key = (String) value();
skip();
expect(':');
result.put(key, value());
skip();
if (text.charAt(position++) == '}') return result;
}
}
if (c == '[') {
List<Object> result = new ArrayList<>();
position++;
skip();
if (text.charAt(position) == ']') {
position++;
return result;
}
for (;;) {
result.add(value());
skip();
if (text.charAt(position++) == ']') return result;
}
}
if (c == '"') {
StringBuilder result = new StringBuilder();
position++;
for (char each = text.charAt(position++); each != '"'; each = text.charAt(position++)) {
if (each == '\\') {
char escaped = text.charAt(position++);
switch (escaped) {
case 'n':
result.append('\n');
break;
case 't':
result.append('\t');
break;
case 'u':
result.append((char) Integer.parseInt(text.substring(position, position + 4), 16));
position += 4;
break;
default:
result.append(escaped);
}
} else {
result.append(each);
}
}
return result.toString();
}
if (text.startsWith("true", position)) {
position += 4;
return Boolean.TRUE;
}
if (text.startsWith("false", position)) {
position += 5;
return Boolean.FALSE;
}
if (text.startsWith("null", position)) {
position += 4;
return null;
}
int begin = position;
while (position < text.length() && "+-.eE0123456789".indexOf(text.charAt(position)) >= 0) position++;
return Double.parseDouble(text.substring(begin, position));
}
private void skip() {
while (position < text.length() && Character.isWhitespace(text.charAt(position))) position++;
}
private void expect(char c) {
if (text.charAt(position++) != c) throw new IllegalArgumentException("expected " + c + " at " + (position - 1));
}
}
/** A JSON writer with commas and indentation. */
static final class Json {
private final StringBuilder text = new StringBuilder();
private final LinkedList<Boolean> first = new LinkedList<>();
private boolean afterKey;
private void separate() {
if (afterKey) {
afterKey = false;
return;
}
if (first.isEmpty()) return;
if (!first.peek()) text.append(',');
first.pop();
first.push(false);
text.append('\n');
for (int i = 0; i < first.size(); i++) text.append(" ");
}
Json open(char bracket) {
separate();
text.append(bracket);
first.push(true);
return this;
}
Json close(char bracket) {
boolean empty = first.pop();
if (!empty) {
text.append('\n');
for (int i = 0; i < first.size(); i++) text.append(" ");
}
text.append(bracket);
return this;
}
Json key(String key) {
separate();
quote(key);
text.append(": ");
afterKey = true;
return this;
}
Json string(String value) {
separate();
if (value == null) text.append("null");
else quote(value);
return this;
}
Json number(long value) {
separate();
text.append(value);
return this;
}
private void quote(String value) {
text.append('"');
for (char c : value.toCharArray()) {
if (c == '"' || c == '\\') text.append('\\').append(c);
else if (c == '\n') text.append("\\n");
else if (c < 0x20) text.append(String.format("\\u%04x", (int) c));
else text.append(c);
}
text.append('"');
}
@Override
public String toString() {
return text.toString();
}
}
}
+213
View File
@@ -0,0 +1,213 @@
// Runs a story of TCP calls on a core scenario's network in Java FILIUS
// (ScenarioNetwork) and records what really happens: every frame each card
// saw, when each call started, returned or failed, and every state each
// socket went through (polled every few milliseconds).
//
// TcpRun <scenario.json> <story> <milliseconds> <out.json>
//
// The stories use the network of tcp-lan (server 192.168.0.10, clients
// 192.168.0.20 and .30) and run the calls as FILIUS' applications do, the
// server's in a thread of its own:
// exchange the client connects to port 80, sends "GET /", the server
// answers with 3000 x (three segments); the client closes,
// the server closes a second after it saw CLOSE_WAIT
// closedPort a client connects to port 81, where nothing listens
// lostData connected, all frames are lost (Verbindung.setDrop) while
// the client sends; loss ends when senden fails
import filius.hardware.Verbindung;
import filius.software.transportschicht.ServerSocket;
import filius.software.transportschicht.Socket;
import filius.software.transportschicht.TCPSocket;
import filius.software.vermittlungsschicht.IpPaket;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class TcpRun {
static final String SERVER = "00000000-0000-0000-0000-000000000101";
static final String CLIENT = "00000000-0000-0000-0000-000000000102";
static final String OTHER_CLIENT = "00000000-0000-0000-0000-000000000103";
static ScenarioNetwork network;
static final List<Object[]> events = new ArrayList<>();
static final Map<String, List<Object[]>> states = new LinkedHashMap<>();
static final Map<String, TCPSocket> watched = new LinkedHashMap<>();
static synchronized void event(String what) {
events.add(new Object[] { System.currentTimeMillis() - network.start, what });
}
static synchronized void watch(String label, Socket socket) {
watched.put(label, (TCPSocket) socket);
states.put(label, new ArrayList<>());
poll();
}
static synchronized void poll() {
for (Map.Entry<String, TCPSocket> entry : watched.entrySet()) {
List<Object[]> seen = states.get(entry.getKey());
String state = entry.getValue().getStateAsString();
if (seen.isEmpty() || !seen.get(seen.size() - 1)[1].equals(state)) {
seen.add(new Object[] { System.currentTimeMillis() - network.start, state });
}
}
}
interface Call {
Object run() throws Exception;
}
/** A call with its start and its end (result or exception) in the events. */
static Object call(String label, Call call) {
event(label + " called");
try {
Object result = call.run();
event(label + " returned" + (result instanceof String ? " " + ((String) result).length() + " characters" : ""));
return result;
} catch (Exception e) {
event(label + " failed " + e.getClass().getSimpleName());
return null;
}
}
static Thread server(Call body) {
Thread thread = new Thread(() -> {
try {
body.run();
} catch (Exception e) {
event("server failed " + e.getClass().getSimpleName());
}
});
thread.start();
return thread;
}
static String repeat(String text, int count) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < count; i++) result.append(text);
return result.toString();
}
public static void main(String[] args) throws Exception {
network = ScenarioNetwork.read(args[0]);
String story = args[1];
long duration = Long.parseLong(args[2]);
network.start();
Thread poller = new Thread(() -> {
while (true) {
poll();
try {
Thread.sleep(2);
} catch (InterruptedException e) {
return;
}
}
});
poller.setDaemon(true);
poller.start();
long end = network.start + duration;
switch (story) {
case "exchange": {
server(() -> {
ServerSocket listener = new ServerSocket(network.system(SERVER), 80, IpPaket.TCP, new ArrayList<>());
Socket accepted = (Socket) call("server oeffnen", listener::oeffnen);
watch("server", accepted);
call("server empfangen", accepted::empfangen);
call("server senden", () -> {
accepted.senden(repeat("x", 3000));
return null;
});
while (!((TCPSocket) accepted).getStateAsString().equals("CLOSE_WAIT")) Thread.sleep(2);
Thread.sleep(1000);
call("server schliessen", () -> {
accepted.schliessen();
return null;
});
return null;
});
Thread.sleep(500);
TCPSocket client = new TCPSocket(network.system(CLIENT), "192.168.0.10", 80);
watch("client", client);
call("client verbinden", () -> {
client.verbinden();
return null;
});
call("client senden", () -> {
client.senden("GET /");
return null;
});
call("client empfangen", client::empfangen);
call("client schliessen", () -> {
client.schliessen();
return null;
});
break;
}
case "closedPort": {
Thread.sleep(500);
TCPSocket client = new TCPSocket(network.system(OTHER_CLIENT), "192.168.0.10", 81);
watch("client", client);
call("client verbinden", () -> {
client.verbinden();
return null;
});
break;
}
case "lostData": {
server(() -> {
ServerSocket listener = new ServerSocket(network.system(SERVER), 80, IpPaket.TCP, new ArrayList<>());
Socket accepted = (Socket) call("server oeffnen", listener::oeffnen);
watch("server", accepted);
return null;
});
Thread.sleep(500);
TCPSocket client = new TCPSocket(network.system(CLIENT), "192.168.0.10", 80);
watch("client", client);
call("client verbinden", () -> {
client.verbinden();
return null;
});
Thread.sleep(500);
event("loss on");
Verbindung.setDrop(true);
call("client senden", () -> {
client.senden("verloren");
return null;
});
Verbindung.setDrop(false);
event("loss off");
break;
}
default:
throw new IllegalArgumentException("unknown story " + story);
}
long rest = end - System.currentTimeMillis();
if (rest > 0) Thread.sleep(rest);
poll();
ScenarioNetwork.Json out = new ScenarioNetwork.Json();
out.open('{');
network.writeFrames(out);
synchronized (TcpRun.class) {
out.key("events").open('[');
for (Object[] each : events) out.open('[').number((Long) each[0]).string((String) each[1]).close(']');
out.close(']');
out.key("states").open('{');
for (Map.Entry<String, List<Object[]>> entry : states.entrySet()) {
out.key(entry.getKey()).open('[');
for (Object[] each : entry.getValue()) out.open('[').number((Long) each[0]).string((String) each[1]).close(']');
out.close(']');
}
out.close('}');
}
out.close('}');
network.stop();
ScenarioNetwork.write(args[3], out);
System.exit(0);
}
}