🦭 Seal of approval for regular use

This commit is contained in:
Zoë Hoekstra 2022-08-24 15:50:01 +02:00
parent 7c571687bf
commit 10f274ed52
No known key found for this signature in database
GPG key ID: F9B7B7D8130F3323
13 changed files with 257 additions and 61 deletions

4
npx/bitburner-remote.js Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env node
import {start} from "../src/index.js"
await start();

52
package-lock.json generated
View file

@ -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=="
}
}
}

View file

@ -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"
}

74
src/config.js Normal file
View file

@ -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' });
}

View file

@ -1,6 +1,6 @@
class EventType {
export class EventType {
static FileChanged = "FileChanged";
static FileDeleted = "FileDeleted";
static MessageReceived = "MessageReceived";
static SendMessage = "SendMessage";
static MessageSend = "MessageSend";
}

View file

@ -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;
}

View file

@ -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() {
process.on('SIGINT', function () {
console.log("Shutting down!");
watch.close();
socket.close();
process.exit();
});
});
}

View file

@ -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);
}

View file

@ -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")
});
}
}
}

View file

@ -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();

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}