From d20f621b4797751c768069433fb89a6cb6a90295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=AB=20Hoekstra?= Date: Sat, 30 Jul 2022 16:19:22 +0200 Subject: [PATCH] Add RFA backend --- src/RemoteFileAPI/MessageDefinitions.ts | 65 ++++++++++++ src/RemoteFileAPI/MessageHandlers.ts | 129 ++++++++++++++++++++++++ src/RemoteFileAPI/RFALogger.ts | 27 +++++ src/RemoteFileAPI/Remote.ts | 65 ++++++++++++ src/RemoteFileAPI/RemoteFileAPI.ts | 16 +++ src/index.tsx | 6 ++ 6 files changed, 308 insertions(+) create mode 100644 src/RemoteFileAPI/MessageDefinitions.ts create mode 100644 src/RemoteFileAPI/MessageHandlers.ts create mode 100644 src/RemoteFileAPI/RFALogger.ts create mode 100644 src/RemoteFileAPI/Remote.ts create mode 100644 src/RemoteFileAPI/RemoteFileAPI.ts diff --git a/src/RemoteFileAPI/MessageDefinitions.ts b/src/RemoteFileAPI/MessageDefinitions.ts new file mode 100644 index 000000000..15ec5740b --- /dev/null +++ b/src/RemoteFileAPI/MessageDefinitions.ts @@ -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' +} diff --git a/src/RemoteFileAPI/MessageHandlers.ts b/src/RemoteFileAPI/MessageHandlers.ts new file mode 100644 index 000000000..0075052ad --- /dev/null +++ b/src/RemoteFileAPI/MessageHandlers.ts @@ -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 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}); + } +} diff --git a/src/RemoteFileAPI/RFALogger.ts b/src/RemoteFileAPI/RFALogger.ts new file mode 100644 index 000000000..6a1d0f277 --- /dev/null +++ b/src/RemoteFileAPI/RFALogger.ts @@ -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); diff --git a/src/RemoteFileAPI/Remote.ts b/src/RemoteFileAPI/Remote.ts new file mode 100644 index 000000000..3cb689201 --- /dev/null +++ b/src/RemoteFileAPI/Remote.ts @@ -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); +} diff --git a/src/RemoteFileAPI/RemoteFileAPI.ts b/src/RemoteFileAPI/RemoteFileAPI.ts new file mode 100644 index 000000000..2bd0c894b --- /dev/null +++ b/src/RemoteFileAPI/RemoteFileAPI.ts @@ -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; diff --git a/src/index.tsx b/src/index.tsx index 3720f3eca..661d784b1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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(