Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: move programs outside Host #111

Merged
merged 17 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions src/graphics/renderables/device_info.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ProgramRunner } from "../../programs";
import { Device } from "../../types/devices";
import { DeviceType } from "../../types/devices/device";
import { ViewGraph } from "../../types/graphs/viewgraph";
Expand All @@ -8,14 +9,10 @@ import {
createToggleTable,
createRightBarButton,
} from "../right_bar";
import { ProgramInfo } from "./program_info";
import { StyledInfo } from "./styled_info";

export interface ProgramInfo {
name: string;
inputs?: Node[];

start(): void;
}
export { ProgramInfo } from "./program_info";

export class DeviceInfo extends StyledInfo {
readonly device: Device;
Expand Down Expand Up @@ -59,24 +56,27 @@ export class DeviceInfo extends StyledInfo {
);
}

addProgramList(programs: ProgramInfo[]) {
// First argument is to avoid a circular dependency
addProgramList(runner: ProgramRunner, programs: ProgramInfo[]) {
const programOptions = programs.map(({ name }, i) => {
return { value: i.toString(), text: name };
});
const inputsContainer = document.createElement("div");
let selectedProgram = programs[0];
inputsContainer.replaceChildren(...selectedProgram.toHTML());
this.inputFields.push(
// Dropdown for selecting program
createDropdown("Program", programOptions, "program-selector", (v) => {
selectedProgram = programs[parseInt(v)];
const programInputs = selectedProgram.inputs || [];
inputsContainer.replaceChildren(...programInputs);
inputsContainer.replaceChildren(...selectedProgram.toHTML());
}),
inputsContainer,
// Button to send a packet
createRightBarButton("Start program", () => {
console.log("Started program: ", selectedProgram.name);
selectedProgram.start();
const { name } = selectedProgram;
console.log("Started program: ", name);
const inputs = selectedProgram.getInputValues();
runner.addRunningProgram(name, inputs);
}),
);
}
Expand Down
29 changes: 29 additions & 0 deletions src/graphics/renderables/program_info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createDropdown, Renderable } from "../right_bar";

interface HasValue {
value: string;
}

export class ProgramInfo implements Renderable {
readonly name: string;
private inputs: Node[] = [];
private inputsValues: HasValue[] = [];

constructor(name: string) {
this.name = name;
}

withDropdown(name: string, options: { value: string; text: string }[]) {
const dropdown = createDropdown(name, options);
this.inputs.push(dropdown);
this.inputsValues.push(dropdown.querySelector("select"));
}

getInputValues() {
return this.inputsValues.map(({ value }) => value);
}

toHTML() {
return this.inputs;
}
}
93 changes: 93 additions & 0 deletions src/programs/echo_sender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Ticker } from "pixi.js";
import { DeviceId } from "../types/graphs/datagraph";
import { sendRawPacket } from "../types/packet";
import { ProgramBase } from "./program_base";
import { ViewGraph } from "../types/graphs/viewgraph";
import { ProgramInfo } from "../graphics/renderables/device_info";
import { EchoRequest } from "../packets/icmp";
import { IPv4Packet } from "../packets/ip";

function adjacentDevices(viewgraph: ViewGraph, srcId: DeviceId) {
const adjacentDevices = viewgraph
.getAdjacentDeviceIds(srcId)
.map((id) => ({ value: id.toString(), text: `Device ${id}` }));

return adjacentDevices;
}

export abstract class EchoSender extends ProgramBase {
protected dstId: DeviceId;

protected _parseInputs(inputs: string[]): void {
if (inputs.length !== 1) {
console.error(
"Program requires 1 input. " + inputs.length + " were given.",
);
return;
}
this.dstId = parseInt(inputs[0]);
}

protected sendSingleEcho() {
const dstDevice = this.viewgraph.getDevice(this.dstId);
const srcDevice = this.viewgraph.getDevice(this.srcId);
if (!dstDevice) {
console.error("Destination device not found");
return;
}
const echoRequest = new EchoRequest(0);
const ipPacket = new IPv4Packet(srcDevice.ip, dstDevice.ip, echoRequest);
sendRawPacket(this.viewgraph, this.srcId, ipPacket);
}
}

export class SingleEcho extends EchoSender {
static readonly PROGRAM_NAME = "Send ICMP echo";

protected _run() {
this.sendSingleEcho();
this.signalStop();
}

protected _stop() {
// Nothing to do
}

static getProgramInfo(viewgraph: ViewGraph, srcId: DeviceId): ProgramInfo {
const programInfo = new ProgramInfo(this.PROGRAM_NAME);
programInfo.withDropdown("Destination", adjacentDevices(viewgraph, srcId));
return programInfo;
}
}

const DEFAULT_ECHO_DELAY_MS = 250;

export class EchoServer extends EchoSender {
static readonly PROGRAM_NAME = "Echo server";

progress = 0;

protected _run() {
Ticker.shared.add(this.tick, this);
}

private tick(ticker: Ticker) {
const delay = DEFAULT_ECHO_DELAY_MS;
this.progress += ticker.deltaMS;
if (this.progress < delay) {
return;
}
this.sendSingleEcho();
this.progress -= delay;
}

protected _stop() {
Ticker.shared.remove(this.tick, this);
}

static getProgramInfo(viewgraph: ViewGraph, srcId: DeviceId): ProgramInfo {
const programInfo = new ProgramInfo(this.PROGRAM_NAME);
programInfo.withDropdown("Destination", adjacentDevices(viewgraph, srcId));
return programInfo;
}
}
81 changes: 81 additions & 0 deletions src/programs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { ProgramInfo } from "../graphics/renderables/program_info";
import { DeviceId } from "../types/graphs/datagraph";
import { ViewGraph } from "../types/graphs/viewgraph";
import { EchoServer, SingleEcho } from "./echo_sender";

export type Pid = number;

export interface RunningProgram {
pid: Pid;
name: string;
inputs: string[];
}

// Currently used only for Host, due to a circular dependency
export interface ProgramRunner {
addRunningProgram(name: string, inputs: string[]): void;
}

export interface Program {
/**
* Starts running the program.
* @param signalStop Function to call when the program should stop
*/
run(signalStop: () => void): void;

/**
* Stops running the program.
*/
stop(): void;
}

/**
* This type matches a class having a constructor with the given signature
*/
type ProgramConstructor = new (
viewgraph: ViewGraph,
srcId: DeviceId,
inputs: string[],
) => Program;

// List of all programs.
// Each one has to:
// - Implement the Program interface
// - Have a static readonly PROGRAM_NAME property
// - Have a constructor with the signature (viewgraph, srcId, inputs)
// - Have a getProgramInfo static method
const programList = [SingleEcho, EchoServer];

// Map of program name to program constructor
const programMap = new Map<string, ProgramConstructor>(
programList.map((p) => [p.PROGRAM_NAME, p]),
);

export function getProgramList(
viewgraph: ViewGraph,
srcId: DeviceId,
): ProgramInfo[] {
return programList.map((p) => p.getProgramInfo(viewgraph, srcId));
}

/**
* Creates a new program instance.
* @param viewgraph Viegraph instance the device is on
* @param sourceId ID of the device running the program
* @param runningProgram data of the running program
* @returns a new Program instance if the data is valid, undefined otherwise
*/
export function newProgram(
viewgraph: ViewGraph,
sourceId: DeviceId,
runningProgram: RunningProgram,
): Program | undefined {
const { name, inputs } = runningProgram;
const GivenProgram = programMap.get(name);

if (!GivenProgram) {
console.error("Unknown program: ", name);
return undefined;
}
return new GivenProgram(viewgraph, sourceId, inputs);
}
55 changes: 55 additions & 0 deletions src/programs/program_base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Program } from ".";
import { DeviceId } from "../types/graphs/datagraph";
import { ViewGraph } from "../types/graphs/viewgraph";

/**
* Base class for all programs.
* Provides a basic structure for programs to be run.
*/
export abstract class ProgramBase implements Program {
protected viewgraph: ViewGraph;
protected srcId: DeviceId;

protected signalStop: () => void;

constructor(viewgraph: ViewGraph, srcId: DeviceId, inputs: string[]) {
this.viewgraph = viewgraph;
this.srcId = srcId;

this._parseInputs(inputs);
}

run(signalStop: () => void) {
if (this.signalStop) {
console.error("Program already running");
return;
}
this.signalStop = signalStop;

this._run();
}

stop(): void {
// This function could be useful
console.debug("Program stopping");
this._stop();
}

// Functions to be implemented by subclasses

/**
* Parses the given inputs and sets any subclass fields.
* @param inputs program inputs to be parsed
*/
protected abstract _parseInputs(inputs: string[]): void;

/**
* Starts running the program.
*/
protected abstract _run(): void;

/**
* Stops running the program.
*/
protected abstract _stop(): void;
}
20 changes: 5 additions & 15 deletions src/types/devices/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { IpAddress, IPv4Packet } from "../../packets/ip";
import { DeviceId } from "../graphs/datagraph";
import { DragDeviceMove, AddEdgeMove } from "../undo-redo";
import { Layer } from "./layer";
import { Packet, sendPacket } from "../packet";
import { Packet, sendRawPacket } from "../packet";
import { EchoReply } from "../../packets/icmp";
import { CreateDevice } from "./utils";

Expand Down Expand Up @@ -135,23 +135,13 @@ export abstract class Device extends Sprite {
handlePacket(packet: Packet) {
switch (packet.type) {
case "ICMP-8": {
const destinationDevice = this.viewgraph.getDeviceByIP(
const dstDevice = this.viewgraph.getDeviceByIP(
packet.rawPacket.sourceAddress,
);
if (destinationDevice) {
if (dstDevice) {
const echoReply = new EchoReply(0);
const ipPacket = new IPv4Packet(
this.ip,
destinationDevice.ip,
echoReply,
);
sendPacket(
this.viewgraph,
ipPacket,
echoReply.getPacketType(),
this.id,
destinationDevice.id,
);
const ipPacket = new IPv4Packet(this.ip, dstDevice.ip, echoReply);
sendRawPacket(this.viewgraph, this.id, ipPacket);
}
break;
}
Expand Down
Loading