diff --git a/npx/bitburner-remote.js b/npx/bitburner-remote.js new file mode 100755 index 0000000..aa4ddb3 --- /dev/null +++ b/npx/bitburner-remote.js @@ -0,0 +1,4 @@ +#!/usr/bin/env node +import {start} from "../src/index.js" + +await start(); diff --git a/package-lock.json b/package-lock.json index f5acf3a..408a9d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,21 @@ { "name": "bitburner-remote", - "version": "1.0.0", + "version": "1.0.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bitburner-remote", - "version": "1.0.0", + "version": "1.0.6", "license": "Unlicense", "dependencies": { "cheap-watch": "^1.0.4", + "convict": "^6.2.3", "signal-js": "^3.0.1", "ws": "^8.8.1" + }, + "bin": { + "bitburner-remote": "npx/bitburner-remote.js" } }, "node_modules/cheap-watch": { @@ -22,6 +26,23 @@ "node": ">=8" } }, + "node_modules/convict": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/convict/-/convict-6.2.3.tgz", + "integrity": "sha512-mTY04Qr7WrqiXifdeUYXr4/+Te4hPFWDvz6J2FVIKCLc2XBhq63VOSSYAKJ+unhZAYOAjmEdNswTOeHt7s++pQ==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "yargs-parser": "^20.2.7" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "node_modules/signal-js": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/signal-js/-/signal-js-3.0.1.tgz", @@ -46,6 +67,14 @@ "optional": true } } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } } }, "dependencies": { @@ -54,6 +83,20 @@ "resolved": "https://registry.npmjs.org/cheap-watch/-/cheap-watch-1.0.4.tgz", "integrity": "sha512-QR/9FrtRL5fjfUJBhAKCdi0lSRQ3rVRRum3GF9wDKp2TJbEIMGhUEr2yU8lORzm9Isdjx7/k9S0DFDx+z5VGtw==" }, + "convict": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/convict/-/convict-6.2.3.tgz", + "integrity": "sha512-mTY04Qr7WrqiXifdeUYXr4/+Te4hPFWDvz6J2FVIKCLc2XBhq63VOSSYAKJ+unhZAYOAjmEdNswTOeHt7s++pQ==", + "requires": { + "lodash.clonedeep": "^4.5.0", + "yargs-parser": "^20.2.7" + } + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "signal-js": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/signal-js/-/signal-js-3.0.1.tgz", @@ -64,6 +107,11 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", "requires": {} + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" } } } diff --git a/package.json b/package.json index c44ee0e..2ac4eb0 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "bitburner-remote", - "version": "1.0.0", + "version": "1.0.7", "description": "Official implementation of the Bitburner Remote Server", "type": "module", - "main": "src/index.js", + "bin": "./npx/bitburner-remote.js", + "main": "./npx/bitburner-remote.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node src/index.js" @@ -25,6 +26,7 @@ "homepage": "https://github.com/Hoekstraa/bitburner-remote#readme", "dependencies": { "cheap-watch": "^1.0.4", + "convict": "^6.2.3", "signal-js": "^3.0.1", "ws": "^8.8.1" } diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..15ea160 --- /dev/null +++ b/src/config.js @@ -0,0 +1,74 @@ +import convict from "convict"; +import { existsSync } from "fs"; + +// Define a schema +export let config = convict({ + allowedFiletypes: { + doc: 'Filetypes that are synchronized to the game.', + format: 'Array', + default: [".js", ".script", ".txt"] + }, + allowDeletingFiles: { + doc: 'Allow deleting files in game if they get deleted off disk.', + format: 'Boolean', + default: false + }, + port: { + doc: 'The port to bind to.', + format: 'Number', + default: 12525, + env: 'BB_PORT', + arg: 'port' + }, + scriptsFolder: { + doc: 'The to be synchronized folder.', + format: 'String', + default: '.', + env: 'BB_SCRIPTFOLDER', + arg: "folder" + }, + quiet: { + doc: 'Log less internal events to stdout.', + format: 'Boolean', + env: 'BB_VERBOSE', + default: false + }, + dry: { + doc: 'Only print the files to be synchronised.', + format: 'Boolean', + env: 'BB_DRY', + default: false + }, + definitionFile: { + update: { + doc: 'Automatically pull the definition file from the game.', + format: 'Boolean', + env: 'BB_UPDATE_DEF', + default: false + }, + location: { + doc: 'Location/name of where the definition file gets placed.', + format: 'String', + env: 'BB_LOCATION_DEF', + default: "./NetScriptDefinitions.d.ts" + } + } +}); + +export function loadConfig() { + const configFile = "filesync.json"; + if (existsSync(configFile)) { + try { + config.loadFile(configFile); + } catch (e) { + throw new Error(`Unable to load configuration file at ${configFile}: ${e}`); + } + } else if (!config.get("quiet")) { + console.log("No configuration file found.") + } + + // Perform validation + config.validate({ allowed: 'strict' }); +} + + diff --git a/src/messages.js b/src/eventTypes.js similarity index 66% rename from src/messages.js rename to src/eventTypes.js index c056770..07e9ca0 100644 --- a/src/messages.js +++ b/src/eventTypes.js @@ -1,6 +1,6 @@ -class EventType { +export class EventType { static FileChanged = "FileChanged"; static FileDeleted = "FileDeleted"; static MessageReceived = "MessageReceived"; - static SendMessage = "SendMessage"; + static MessageSend = "MessageSend"; } \ No newline at end of file diff --git a/src/fileWatch.js b/src/fileWatch.js index 9abaf13..31abd3c 100644 --- a/src/fileWatch.js +++ b/src/fileWatch.js @@ -1,23 +1,32 @@ import CheapWatch from "cheap-watch"; -import * as settings from "./settings.js"; +import {config} from "./config.js"; +import {EventType} from "./eventTypes.js"; +import {resolve } from "path"; function fileFilter(event) { - if(settings.allowedFiletypes.some(extension => event.path.endsWith(extension))) + if(config.get("allowedFiletypes").some(extension => event.path.endsWith(extension))) return true; } export async function setupWatch(signaller) { const watch = new CheapWatch({ - dir: settings.scriptsFolder, + dir: config.get("scriptsFolder"), filter: fileFilter }); + if(!config.get("quiet")) console.log("Watching folder", resolve(config.get("scriptsFolder"))) + watch.on('+', fileEvent => signaller.emit(EventType.FileChanged, fileEvent)); watch.on('-', fileEvent => signaller.emit(EventType.FileDeleted, fileEvent)); // Wait 'till filewatcher is ready to go await watch.init(); + if(config.get("dry")) { + console.log("Watch would've synchronised:\n", watch.paths) + process.exit(); + } + return watch; } \ No newline at end of file diff --git a/src/index.js b/src/index.js index a028f4e..c760657 100644 --- a/src/index.js +++ b/src/index.js @@ -1,29 +1,35 @@ "use strict" import { setupWatch } from "./fileWatch.js"; -import * as settings from "./settings.js"; -import { setupSocket } from "./webSocket.js"; +import { config, loadConfig } from "./config.js"; +import { setupSocket } from "./networking/webSocket.js"; import signal from "signal-js"; -import { fileChangeEventToMsg, fileRemovalEventToMsg, requestDefinitionFile } from "./messageGenerators.js"; +import { fileChangeEventToMsg, fileRemovalEventToMsg, requestDefinitionFile } from "./networking/messageGenerators.js"; +import { EventType } from "./eventTypes.js"; +import { messageHandler } from "./networking/messageHandler.js"; -const watch = await setupWatch(signal); -const socket = setupSocket(signal); +export async function start() { + loadConfig(); + const watch = await setupWatch(signal); + const socket = setupSocket(signal); -signal.on("fileChange", fileEvent => { - console.log(fileEvent.path + " changed"); - signal.emit(EventType.SendMessage, fileChangeEventToMsg(fileEvent)) -}); + signal.on(EventType.MessageReceived, msg => messageHandler(msg)); -if(settings.allowDeletingFiles) - signal.on("fileDeletion", fileEvent => - signal.emit(EventType.SendMessage, fileRemovalEventToMsg(fileEvent))); + signal.on(EventType.FileChanged, fileEvent => { + if (!config.get("quiet")) console.log(fileEvent.path + " changed"); + signal.emit(EventType.MessageSend, fileChangeEventToMsg(fileEvent)) + }); + if (config.get("allowDeletingFiles")) + signal.on(EventType.FileDeleted, fileEvent => + signal.emit(EventType.MessageSend, fileRemovalEventToMsg(fileEvent))); -console.log(`Server is ready, running on ${settings.port}!`) + console.log(`Server is ready, running on ${config.get("port")}!`) -process.on('SIGINT', function() { - console.log("Shutting down!"); + process.on('SIGINT', function () { + console.log("Shutting down!"); - watch.close(); - socket.close(); - process.exit(); -}); + watch.close(); + socket.close(); + process.exit(); + }); +} \ No newline at end of file diff --git a/src/messageGenerators.js b/src/networking/messageGenerators.js similarity index 70% rename from src/messageGenerators.js rename to src/networking/messageGenerators.js index 6455bd4..9885ecb 100644 --- a/src/messageGenerators.js +++ b/src/networking/messageGenerators.js @@ -1,23 +1,24 @@ -import * as fs from "fs"; +import {readFileSync} from "fs"; +import {config} from "../config.js"; +import {join} from "path"; let messageCounter = 0; export function fileChangeEventToMsg({path}){ - const message = { + return { "jsonrpc":"2.0", "method":"pushFile", "params":{ "server":"home", "filename":path, - "content":fs.readFileSync(path).toString() + "content":readFileSync(join(config.get("scriptsFolder"), path)).toString() }, "id":messageCounter++ } - return JSON.stringify(message); } export function fileRemovalEventToMsg({path}){ - const message = { + return { "jsonrpc":"2.0", "method": "deleteFile", "params":{ @@ -25,14 +26,12 @@ export function fileRemovalEventToMsg({path}){ }, "id":messageCounter++ } - return JSON.stringify(message); } export function requestDefinitionFile(){ - const message = { + return { "jsonrpc": "2.0", "method": "getDefinitionFile", "id":messageCounter++ } - return JSON.stringify(message); } \ No newline at end of file diff --git a/src/networking/messageHandler.js b/src/networking/messageHandler.js new file mode 100644 index 0000000..42cbd01 --- /dev/null +++ b/src/networking/messageHandler.js @@ -0,0 +1,26 @@ +import { messageTracker } from "./messageTracker.js"; +import {writeFile} from "fs"; +import { config } from "../config.js"; + +export function messageHandler(msg) { + let incoming; + + try {incoming = JSON.parse(msg.toString());} + catch {return;} + console.log(incoming) + if (incoming.id == undefined) return; + + if (incoming.result) { + const request = messageTracker.get(incoming.id); + console.log(messageTracker.data); + console.log("REQUEST: ", request); + if (request.method && + request.method == "getDefinitionFile" + && incoming.result) { + writeFile(config.get("definitionFile").location, incoming.result, (err) => { + if (err) return console.log(err); + console.log("wrote definition") + }); + } + } +} \ No newline at end of file diff --git a/src/networking/messageTracker.js b/src/networking/messageTracker.js new file mode 100644 index 0000000..652a882 --- /dev/null +++ b/src/networking/messageTracker.js @@ -0,0 +1,19 @@ +class MessageTracker { + data = new Map() + #maxLength = 200 + + push(msg) { + this.data.set(msg.id, msg); + + if (this.data.size > this.#maxLength){ + const [firstKey] = map.keys(); + this.data.delete(firstKey); + } + } + + get(index) { + return this.data.get(index); + } +} + +export const messageTracker = new MessageTracker(); \ No newline at end of file diff --git a/src/networking/webSocket.js b/src/networking/webSocket.js new file mode 100644 index 0000000..66c8c2b --- /dev/null +++ b/src/networking/webSocket.js @@ -0,0 +1,34 @@ +import { WebSocketServer } from 'ws'; +import {config} from "../config.js"; +import {EventType} from "../eventTypes.js" +import { requestDefinitionFile } from './messageGenerators.js'; +import {messageTracker} from "./messageTracker.js" + +export function setupSocket(signaller){ + + const wss = new WebSocketServer({ port: config.get("port") }); + + wss.on('connection', function connection(ws) { + + function sendMessage(msg) { + messageTracker.push(msg); + ws.send(JSON.stringify(msg)); + } + + ws.on('message', (msg) => { + signaller.emit(EventType.MessageReceived, msg); + }); + + signaller.on(EventType.MessageSend, msg => { + sendMessage(msg); + }); + + if (config.get("definitionFile").update) { + sendMessage(requestDefinitionFile()); + } + + console.log("Connection made!"); + }); + + return wss; +} diff --git a/src/settings.js b/src/settings.js deleted file mode 100644 index c2f42ec..0000000 --- a/src/settings.js +++ /dev/null @@ -1,8 +0,0 @@ -// Folder your scripts are located in. -export const scriptsFolder = "./"; -// Allowed filetypes to synchronize. -export const allowedFiletypes = [".js", ".script", ".txt"]; - -export const allowDeletingFiles = false; -// Port the websocket is set up on. -export const port = 12525; diff --git a/src/webSocket.js b/src/webSocket.js deleted file mode 100644 index e12612b..0000000 --- a/src/webSocket.js +++ /dev/null @@ -1,17 +0,0 @@ -import { WebSocketServer } from 'ws'; -import * as settings from "./settings.js"; - -export function setupSocket(signaller){ - - const wss = new WebSocketServer({ port: settings.port }); - - wss.on('connection', function connection(ws) { - ws.on('message', function message(msg) { - signaller.emit(EventType.MessageReceived, msg); - }); - - signaller.on(EventType.SendMessage, data => ws.send(data)); - }); - - return wss; -}