Skip to content

Commit c06b618

Browse files
authored
feat: run programs on hosts (#88)
Related to #51 This PR changes the "Send packet" button to instead have a dropdown list of "programs", each with a different functionality. Pressing the "Start program" button starts the program. For now we have three choices: - No program: just stops the program - Send ICMP packet: sends a single ICMP packet - Start echo server: sends ICMP echo packets every X seconds ![image](https://github.com/user-attachments/assets/5ace3471-12bd-4ab9-b1a1-7414f9de764b) This still has a problem when changing layers: the program continues running, but with wrong object references. It should be fixed in #89
1 parent 05fbfeb commit c06b618

File tree

5 files changed

+109
-72
lines changed

5 files changed

+109
-72
lines changed

src/graphics/renderables/device_info.ts

Lines changed: 24 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import { Device } from "../../types/devices";
22
import { DeviceType } from "../../types/devices/device";
33
import { CreateDevice } from "../../types/devices/utils";
44
import { RoutingTableEntry } from "../../types/graphs/datagraph";
5-
import { ViewGraph } from "../../types/graphs/viewgraph";
6-
import { sendPacket } from "../../types/packet";
75
import { RemoveDeviceMove } from "../../types/undo-redo";
86
import { urManager } from "../../types/viewportManager";
97
import {
@@ -13,6 +11,13 @@ import {
1311
} from "../right_bar";
1412
import { StyledInfo } from "./styled_info";
1513

14+
export interface ProgramInfo {
15+
name: string;
16+
inputs?: Node[];
17+
18+
start(): void;
19+
}
20+
1621
export class DeviceInfo extends StyledInfo {
1722
readonly device: Device;
1823
inputFields: Node[] = [];
@@ -61,29 +66,25 @@ export class DeviceInfo extends StyledInfo {
6166
);
6267
}
6368

64-
addSendPacketButton() {
65-
const { id, viewgraph } = this.device;
66-
67-
const adjacentDevices = viewgraph
68-
.getDeviceIds()
69-
.filter((adjId) => adjId !== id)
70-
.map((id) => ({ value: id.toString(), text: `Device ${id}` }));
69+
addProgramList(programs: ProgramInfo[]) {
70+
const programOptions = programs.map(({ name }, i) => {
71+
return { value: i.toString(), text: name };
72+
});
73+
const inputsContainer = document.createElement("div");
74+
let selectedProgram = programs[0];
7175
this.inputFields.push(
72-
// Dropdown for selecting packet type
73-
createDropdown(
74-
"Packet Type",
75-
[
76-
{ value: "IP", text: "IP" },
77-
{ value: "ICMP", text: "ICMP" },
78-
],
79-
"packet-type",
80-
),
81-
// Dropdown for selecting destination
82-
createDropdown("Destination", adjacentDevices, "destination"),
76+
// Dropdown for selecting program
77+
createDropdown("Program", programOptions, "program-selector", (v) => {
78+
selectedProgram = programs[parseInt(v)];
79+
const programInputs = selectedProgram.inputs || [];
80+
inputsContainer.replaceChildren(...programInputs);
81+
}),
82+
inputsContainer,
8383
// Button to send a packet
84-
createRightBarButton("Send Packet", () =>
85-
sendSelectedPacket(viewgraph, id),
86-
),
84+
createRightBarButton("Start program", () => {
85+
console.log("Started program: ", selectedProgram.name);
86+
selectedProgram.start();
87+
}),
8788
);
8889
}
8990

@@ -122,20 +123,3 @@ function getTypeName(device: Device): string {
122123
return "Host";
123124
}
124125
}
125-
126-
function sendSelectedPacket(viewgraph: ViewGraph, id: number): void {
127-
// Get the selected packet type and destination ID
128-
const packetType = (
129-
document.getElementById("packet-type") as HTMLSelectElement
130-
)?.value;
131-
const destinationId = Number(
132-
(document.getElementById("destination") as HTMLSelectElement)?.value,
133-
);
134-
135-
// Call the sendPacket method with the selected values
136-
if (packetType && !isNaN(destinationId)) {
137-
sendPacket(viewgraph, packetType, id, destinationId);
138-
} else {
139-
console.warn("Please select both a packet type and a destination.");
140-
}
141-
}

src/graphics/right_bar.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ export function createDropdown(
241241
label: string,
242242
options: { value: string; text: string }[],
243243
selectId?: string,
244+
onchange: (value: string, event: Event) => void = () => undefined,
244245
) {
245246
const container = document.createElement("div");
246247
container.classList.add("dropdown-container");
@@ -261,8 +262,9 @@ export function createDropdown(
261262
});
262263

263264
// Default onchange behavior: logs the selected value
264-
select.onchange = () => {
265+
select.onchange = (e) => {
265266
console.log(`Selected ${label}:`, select.value);
267+
onchange(select.value, e);
266268
};
267269

268270
container.appendChild(labelElement);

src/packets/ip.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,18 @@ export class IpAddress {
2929
}
3030

3131
// Parse IP address from a string representation (10.25.34.42)
32-
static parse(addrString: string): IpAddress {
32+
static parse(addrString: string): IpAddress | null {
3333
const octets = new Uint8Array(4);
34-
addrString.split(".").forEach((octet, i) => {
34+
const splits = addrString.split(".");
35+
if (splits.length !== 4) {
36+
console.error("Invalid IP address. Length: ", splits.length);
37+
return null;
38+
}
39+
splits.forEach((octet, i) => {
3540
const octetInt = parseInt(octet);
3641
if (isNaN(octetInt) || octetInt < 0 || octetInt > 255) {
37-
throw new Error(`Invalid IP address: ${addrString}`);
42+
console.error("Invalid IP address. value: ", octetInt);
43+
return null;
3844
}
3945
octets[i] = octetInt;
4046
});

src/types/devices/host.ts

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@ import { ViewGraph } from "../graphs/viewgraph";
33
import PcImage from "../../assets/pc.svg";
44
import { Position } from "../common";
55
import { IpAddress } from "../../packets/ip";
6-
import { DeviceInfo, RightBar } from "../../graphics/right_bar";
6+
import { createDropdown, DeviceInfo, RightBar } from "../../graphics/right_bar";
7+
import { ProgramInfo } from "../../graphics/renderables/device_info";
8+
import { sendPacket } from "../packet";
9+
import { Ticker } from "pixi.js";
10+
11+
const DEFAULT_ECHO_DELAY = 250; // ms
712

813
export class Host extends Device {
14+
currentProgram: (ticker: Ticker) => void = undefined;
15+
916
constructor(
1017
id: number,
1118
viewgraph: ViewGraph,
@@ -19,7 +26,7 @@ export class Host extends Device {
1926
showInfo(): void {
2027
const info = new DeviceInfo(this);
2128
info.addField("IP Address", this.ip.octets.join("."));
22-
info.addSendPacketButton();
29+
info.addProgramList(this.getProgramList());
2330
RightBar.getInstance().renderInfo(info);
2431
}
2532

@@ -30,4 +37,62 @@ export class Host extends Device {
3037
getType(): DeviceType {
3138
return DeviceType.Host;
3239
}
40+
41+
getProgramList() {
42+
const adjacentDevices = this.viewgraph
43+
.getDeviceIds()
44+
.filter((adjId) => adjId !== this.id)
45+
.map((id) => ({ value: id.toString(), text: `Device ${id}` }));
46+
47+
const dropdownContainer = createDropdown(
48+
"Destination",
49+
adjacentDevices,
50+
"destination",
51+
);
52+
const destination = dropdownContainer.querySelector("select");
53+
54+
const programList: ProgramInfo[] = [
55+
{ name: "No program", start: () => this.stopProgram() },
56+
{
57+
name: "Send ICMP echo",
58+
inputs: [dropdownContainer],
59+
start: () => this.sendSingleEcho(destination.value),
60+
},
61+
{
62+
name: "Echo server",
63+
inputs: [dropdownContainer],
64+
start: () => this.startEchoServer(destination.value),
65+
},
66+
];
67+
return programList;
68+
}
69+
70+
sendSingleEcho(id: string) {
71+
this.stopProgram();
72+
const dst = parseInt(id);
73+
sendPacket(this.viewgraph, "ICMP", this.id, dst);
74+
}
75+
76+
startEchoServer(id: string) {
77+
this.stopProgram();
78+
const dst = parseInt(id);
79+
let progress = 0;
80+
const send = (ticker: Ticker) => {
81+
const delay = DEFAULT_ECHO_DELAY;
82+
progress += ticker.deltaMS;
83+
if (progress < delay) {
84+
return;
85+
}
86+
sendPacket(this.viewgraph, "ICMP", this.id, dst);
87+
progress -= delay;
88+
};
89+
Ticker.shared.add(send, this);
90+
this.currentProgram = send;
91+
}
92+
93+
stopProgram() {
94+
if (this.currentProgram) {
95+
Ticker.shared.remove(this.currentProgram, this);
96+
}
97+
}
3398
}

src/types/packet.ts

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@ export class Packet extends Graphics {
2929
currentStart: number;
3030
color: number;
3131
type: string;
32-
sourceId: number;
33-
destinationId: number;
34-
private detailsVisible = false;
3532

3633
rawPacket: IPv4Packet;
3734

@@ -45,13 +42,7 @@ export class Packet extends Graphics {
4542
Packet.animationPaused = false;
4643
}
4744

48-
constructor(
49-
viewgraph: ViewGraph,
50-
type: string,
51-
rawPacket: IPv4Packet,
52-
sourceid: number,
53-
destinationid: number,
54-
) {
45+
constructor(viewgraph: ViewGraph, type: string, rawPacket: IPv4Packet) {
5546
super();
5647

5748
this.viewgraph = viewgraph;
@@ -61,8 +52,6 @@ export class Packet extends Graphics {
6152
this.zIndex = ZIndexLevels.Packet;
6253

6354
this.rawPacket = rawPacket;
64-
this.sourceId = sourceid;
65-
this.destinationId = destinationid;
6655

6756
this.interactive = true;
6857
this.cursor = "pointer";
@@ -134,8 +123,6 @@ export class Packet extends Graphics {
134123

135124
const info = new StyledInfo("Packet Information");
136125
info.addField("Type", this.type);
137-
info.addField("Source ID", this.sourceId.toString());
138-
info.addField("Destination ID", this.destinationId.toString());
139126
info.addField("Source IP Address", this.rawPacket.sourceAddress.toString());
140127
info.addField(
141128
"Destination IP Address",
@@ -271,6 +258,7 @@ export class Packet extends Graphics {
271258
}
272259
}
273260

261+
// TODO: maybe make this receive the packet directly?
274262
export function sendPacket(
275263
viewgraph: ViewGraph,
276264
packetType: string,
@@ -297,18 +285,10 @@ export function sendPacket(
297285
console.warn("Tipo de paquete no reconocido");
298286
return;
299287
}
300-
const rawPacket = new IPv4Packet(
301-
originDevice.ip,
302-
destinationDevice.ip,
303-
payload,
304-
);
305-
const packet = new Packet(
306-
viewgraph,
307-
packetType,
308-
rawPacket,
309-
originId,
310-
destinationId,
311-
);
288+
const dstIp = destinationDevice.ip;
289+
const rawPacket = new IPv4Packet(originDevice.ip, dstIp, payload);
290+
const packet = new Packet(viewgraph, packetType, rawPacket);
291+
312292
const originConnections = viewgraph.getConnections(originId);
313293
if (originConnections.length === 0) {
314294
console.warn(`No se encontró un dispositivo con ID ${originId}.`);

0 commit comments

Comments
 (0)