Add RFA backend

This commit is contained in:
Zoë Hoekstra 2022-07-30 16:19:22 +02:00
parent 74f3d6507f
commit d20f621b47
No known key found for this signature in database
GPG Key ID: F9B7B7D8130F3323
6 changed files with 308 additions and 0 deletions

@ -0,0 +1,65 @@
export class RFAMessage {
jsonrpc = "2.0"; // Transmits version of JSON-RPC. Compliance maybe allows some funky interaction with external tools?
public method?:string; // Is defined when it's a request/notification, otherwise undefined
public result?:string; // Is defined when it's a response, otherwise undefined
public params?:FileMetadata; // Optional parameters to method
public error? :string; // Only defined on error
public id? :number; // ID to keep track of request -> response interaction, undefined with notifications, defined with request/response
constructor(obj: { method?: string; result?: string; params?: FileMetadata; error?: string; id?: number } = {}){
this.method = obj.method;
this.result = obj.result;
this.params = obj.params;
this.error = obj.error;
this.id = obj.id;
}
}
type FileMetadata = FileData
| FileContent
| FileLocation
| FileServer;
export interface FileData {
filename: string;
content: string;
server: string;
}
export interface FileContent {
filename: string;
content: string;
}
export interface FileLocation {
filename: string;
server: string;
}
export interface FileServer {
server: string;
}
export function isFileData(p: unknown): p is FileData {
const pf = p as FileData
return typeof pf.server === 'string' &&
typeof pf.filename === 'string' &&
typeof pf.content === 'string'
}
export function isFileLocation(p: unknown): p is FileLocation {
const pf = p as FileLocation
return typeof pf.server === 'string' &&
typeof pf.filename === 'string'
}
export function isFileContent(p: unknown): p is FileContent {
const pf = p as FileContent
return typeof pf.filename === 'string' &&
typeof pf.content === 'string'
}
export function isFileServer(p: unknown): p is FileServer {
const pf = p as FileServer
return typeof pf.server === 'string'
}

@ -0,0 +1,129 @@
import { Player } from "../Player";
import { isScriptFilename } from "../Script/isScriptFilename";
import { GetServer } from "../Server/AllServers";
import { isValidFilePath } from "../Terminal/DirectoryHelpers";
import { TextFile } from "../TextFile";
import { RFAMessage, FileData, FileContent, isFileServer, isFileLocation, FileLocation, isFileData } from "./MessageDefinitions";
//@ts-ignore: Complaint of import ending with .d.ts
import libSource from "!!raw-loader!../ScriptEditor/NetscriptDefinitions.d.ts";
function error(errorMsg: string, { id }: RFAMessage): RFAMessage {
console.error("[RFA-ERROR]" + (typeof (id) === "undefined" ? "" : `Request ${id}: `) + errorMsg);
return new RFAMessage({ error: errorMsg, id: id });
}
export const RFARequestHandler: Record<string, (message: RFAMessage) => void | RFAMessage> = {
pushFile: function (msg: RFAMessage): RFAMessage {
if (!isFileData(msg.params)) return error("pushFile message misses parameters", msg);
const fileData: FileData = msg.params;
if (!isValidFilePath(fileData.filename)) return error("pushFile with an invalid filename", msg);
const server = GetServer(fileData.server);
if (server == null) return error("Server hostname invalid", msg);
if (isScriptFilename(fileData.filename))
server.writeToScriptFile(Player, fileData.filename, fileData.content);
else // Assume it's a text file
server.writeToTextFile(fileData.filename, fileData.content);
// If and only if the content is actually changed correctly, send back an OK.
const savedCorrectly = server.getScript(fileData.filename)?.code === fileData.content
|| server.textFiles.filter((t: TextFile) => t.filename == fileData.filename).at(0)?.text === fileData.content;
if (!savedCorrectly) return error("File wasn't saved correctly", msg);
return new RFAMessage({ result: "OK", id: msg.id });
},
getFile: function (msg: RFAMessage): RFAMessage {
if (!isFileLocation(msg.params)) return error("getFile message misses parameters", msg);
const fileData: FileLocation = msg.params;
if (!isValidFilePath(fileData.filename)) return error("getFile with an invalid filename", msg);
const server = GetServer(fileData.server);
if (server == null) return error("Server hostname invalid", msg);
if (isScriptFilename(fileData.filename)) {
const scriptContent = server.getScript(fileData.filename);
if (!scriptContent) return error("Requested script doesn't exist", msg);
return new RFAMessage({ result: scriptContent.code, id: msg.id });
}
else { // Assume it's a text file
const file = server.textFiles.filter((t: TextFile) => t.filename == fileData.filename).at(0);
if (file === undefined) return error("Requested textfile doesn't exist", msg);
return new RFAMessage({ result: file.text, id: msg.id });
}
},
deleteFile: function (msg: RFAMessage): RFAMessage {
if (!isFileLocation(msg.params)) return error("deleteFile message misses parameters", msg);
const fileData: FileLocation = msg.params;
if (!isValidFilePath(fileData.filename)) return error("deleteFile with an invalid filename", msg);
const server = GetServer(fileData.server);
if (server == null) return error("Server hostname invalid", msg);
const fileExists = (): boolean => !!server.getScript(fileData.filename)
|| server.textFiles.some((t: TextFile) => t.filename === fileData.filename);
if (!fileExists()) return error("deleteFile file doesn't exist", msg);
server.removeFile(fileData.filename);
if (fileExists()) return error("deleteFile failed to delete the file", msg);
return new RFAMessage({ result: "OK", id: msg.id });
},
getFileNames: function (msg: RFAMessage): RFAMessage {
if (!isFileServer(msg.params)) return error("getFileNames message misses parameters", msg);
const server = GetServer(msg.params.server);
if (server == null) return error("Server hostname invalid", msg);
const fileList: FileContent[] = [
...server.textFiles.map((txt): FileContent => { return { filename: txt.filename, content: txt.text } }),
...server.scripts.map((scr): FileContent => { return { filename: scr.filename, content: scr.code } })
];
return new RFAMessage({ result: JSON.stringify(fileList), id: msg.id });
},
getAllFiles: function (msg: RFAMessage): RFAMessage {
if (!isFileServer(msg.params)) return error("getAllFiles message misses parameters", msg);
const server = GetServer(msg.params.server);
if (server == null) return error("Server hostname invalid", msg);
const fileNameList: string[] = [
...server.textFiles.map((txt): string => txt.filename),
...server.scripts.map((scr): string => scr.filename)
];
return new RFAMessage({ result: JSON.stringify(fileNameList), id: msg.id });
},
calculateRam: function (msg: RFAMessage): RFAMessage {
if (!isFileLocation(msg.params)) return error("calculateRam message misses parameters", msg);
const fileData: FileLocation = msg.params;
if (!isValidFilePath(fileData.filename)) return error("deleteFile with an invalid filename", msg);
const server = GetServer(fileData.server);
if (server == null) return error("Server hostname invalid", msg);
if (!isScriptFilename(fileData.filename)) return error("Filename isn't a script filename", msg);
const script = server.getScript(fileData.filename);
if (!script) return error("File doesn't exist", msg);
const ramUsage = script.ramUsage;
return new RFAMessage({result: String(ramUsage), id: msg.id});
},
getDefinitionFile: function (msg: RFAMessage): RFAMessage {
const source = (libSource + "").replace(/export /g, "");
console.log(source);
return new RFAMessage({result: source, id: msg.id});
}
}

@ -0,0 +1,27 @@
class RemoteFileAPILogger {
_enabled = true;
_prefix = "[RFA]";
_error_prefix = "[RFA-ERROR]";
constructor(enabled: boolean){
this._enabled = enabled;
}
public error(...message: any[]) : void {
if (this._enabled) console.error(this._error_prefix, ...message);
}
public log(...message: any[]) : void {
if (this._enabled) console.log(this._prefix, ...message);
}
public disable() : void {
this._enabled = false;
}
public enable() : void {
this._enabled = true;
}
}
export const RFALogger = new RemoteFileAPILogger(true);

@ -0,0 +1,65 @@
import { RFALogger } from "./RFALogger";
import { RFAMessage } from "./MessageDefinitions"
import { RFARequestHandler } from "./MessageHandlers";
export class Remote {
connection? : WebSocket;
protocol = "ws";
ipaddr: string;
port: number;
constructor(ip : string, port : number) {
this.ipaddr = ip;
this.port = port;
}
public startConnection() : void {
this.startConnectionRecurse(1, 5);
}
handleCloseEvent():void {
delete this.connection;
RFALogger.log("Connection closed.");
}
startConnectionRecurse(retryN : number, retryMax : number) : void {
RFALogger.log("Trying to connect.");
this.connection = new WebSocket(this.protocol + "://" + this.ipaddr + ":" + this.port);
this.connection.addEventListener("error", (e:Event) => {
if(!this.connection) return;
// When having trouble connecting, try again.
if (this.connection.readyState === 3) {
RFALogger.log(`Connection lost, retrying (try #${retryN}).`);
if (retryN <= retryMax) this.startConnectionRecurse(retryN + 1, retryMax);
return;
}
// Else handle the error normally
RFALogger.error(e);
});
this.connection.addEventListener("message", handleMessageEvent);
this.connection.addEventListener("open", () => RFALogger.log("Connection established: ", this.ipaddr, ":", this.port));
this.connection.addEventListener("close", this.handleCloseEvent);
}
}
function handleMessageEvent(e: MessageEvent):void {
const msg : RFAMessage = JSON.parse(e.data);
RFALogger.log("Message received:", msg);
//TODO: Needs more safety, send error on invalid input, send error when msg.method isn´t in handlers, ...
if (msg.method)
{
const response = RFARequestHandler[msg.method](msg);
RFALogger.log("Sending response: ", response);
if(response) this.send(JSON.stringify(response));
}
else if (msg.result) RFALogger.error("Result handling not implemented yet.");
else if (msg.error) RFALogger.error("Received an error from server", msg);
else RFALogger.error("Incorrect Message", msg);
}

@ -0,0 +1,16 @@
import { Remote } from "./Remote";
class RemoteFileAPI {
server : Remote;
constructor(){
this.server = new Remote("localhost", 12525);
return;
}
enable() : void {
this.server.startConnection();
}
}
export const RFA = new RemoteFileAPI;

@ -4,6 +4,9 @@ import ReactDOM from "react-dom";
import { TTheme as Theme, ThemeEvents, refreshTheme } from "./Themes/ui/Theme";
import { LoadingScreen } from "./ui/LoadingScreen";
import { initElectron } from "./Electron";
import { RFA } from "./RemoteFileAPI/RemoteFileAPI";
initElectron();
globalThis["React"] = React;
globalThis["ReactDOM"] = ReactDOM;
@ -14,6 +17,9 @@ ReactDOM.render(
document.getElementById("root"),
);
console.log("[RFA] Fix this hack ASAP");
RFA.enable();
function rerender(): void {
refreshTheme();
ReactDOM.render(