Port basic lib modules to TypeScript

This commit is contained in:
Hornwitser committed 2023-08-06 01:03:13 +02:00
1 parent 2988cd962c
commit e0c197b9bc
25 files changed
+1370 -1324

No files matched your search

+1
View File
@@ -36,6 +36,7 @@
"devDependencies": {
"@types/fs-extra": "^11.0.1",
"@types/node": "^20.4.5",
"@types/triple-beam": "^1.3.2",
"typescript": "^5.1.6"
}
}
@@ -1,6 +1,3 @@
"use strict";
/**
* Helper class for implementing the exponential backoff algorithm
*
@@ -14,49 +11,48 @@
* the passed value of `max`. The value for the delay will reset back to
* `base` if more time than the value for `reset` has passed between two
* invocations of delay.
*
* @alias module:lib.ExponentialBackoff
*/
class ExponentialBackoff {
export default class ExponentialBackoff {
/**
* Base delay in seconds. The first delay will on average be this
* long, but can be anywhere between 0 and 2 times this value.
*/
base = 1;
/**
* Maximum delay in seconds. A delay will not take more than this
* value.
*/
max = 60;
/**
* Time in seconds between two invocations of delay which will cause
* it to start over with `base` instead of doubling.
*/
reset: number;
private _exp = 0;
private _lastInvocationTime = Date.now();
/**
* Construct ExponentialBackoff helper
*
* The options passed may also be modified on the instance at runtime.
*
* @param {object=} options -
* @param options -
* Options for this exponential backoff helper
* @param {number=} [options.base=1] -
* @param [options.base=1] -
* Base delay in seconds. The first delay will on average be this
* long, but can be anywhere between 0 and 2 times this value.
* @param {number=} [options.max=60] -
* @param [options.max=60] -
* Maximum delay in seconds. A delay will not take more than this
* value.
* @param {number=} [options.reset=2*max] -
* @param [options.reset=2*max] -
* Time in seconds between two invocations of delay which will cause
* it to start over with `base` instead of doubling.
*/
constructor(options = {}) {
/**
* Base delay in seconds. The first delay will on average be this
* long, but can be anywhere between 0 and 2 times this value.
* @type {number}
*/
constructor(options: { base?: number, max?: number, reset?: number } = {}) {
this.base = options.base || 1;
/**
* Maximum delay in seconds. A delay will not take more than this
* value.
* @type {number}
*/
this.max = options.max || 60;
/**
* Time in seconds between two invocations of delay which will cause
* it to start over with `base` instead of doubling.
* @type {number}
*/
this.reset = options.reset || 2 * this.max;
this._exp = 0;
this._lastInvocationTime = Date.now();
}
/**
@@ -66,7 +62,7 @@ class ExponentialBackoff {
* algorithm. This is a value between 0 and min(2 * options.base *
* 2<sup>retries</sup>, optinos.max).
*
* @returns {number} time in milliseconds to wait.
* @returns time in milliseconds to wait.
*/
delay() {
let invocationTime = Date.now();
@@ -1,39 +1,31 @@
"use strict";
/**
* Stastics collected about a player.
* @alias module:lib.PlayerStats
*/
class PlayerStats {
export default class PlayerStats {
/**
* Count of the number of times this player has been seen joining this server.
* @type {number}
*/
joinCount = 0;
/**
* Time in ms this player has been seen online on this instance.
* @type {number}
*/
onlineTimeMs = 0;
/**
* Timestamp the player was last seen joining.
* @type {Date=}
*/
lastJoinAt;
lastJoinAt?: Date;
/**
* Timestamp the player was last seen leaving.
* @type {Date=}
*/
lastLeaveAt;
lastLeaveAt?: Date;
/**
* Reason the player was last seen leaving with.
* @type {string=}
*/
lastLeaveReason;
lastLeaveReason?: string;
static jsonSchema = {
type: "object",
@@ -62,7 +54,7 @@ class PlayerStats {
}
}
static fromJSON(json) {
static fromJSON(json: any) {
return new this(json);
}
@@ -84,5 +76,3 @@ class PlayerStats {
return json;
}
}
module.exports = PlayerStats;
@@ -1,6 +1,3 @@
"use strict";
/**
* Helper class for rate limiting actions
*
@@ -8,6 +5,7 @@
* limited on invocations.
*
* @example
* ```ts
* const rateLimiter = RateLimiter({
* action: () => { console.log("action"); },
* maxRate: 1,
@@ -19,36 +17,36 @@
* // and one more time 1 second later for 99 other activations that were
* // exceeding the rate limit.
* }
* @alias module:lib.RateLimiter
* ```
*/
class RateLimiter {
export default class RateLimiter {
/**
* Maximum rate of activations per second.
*/
maxRate: number;
/**
* Callback invoked at most `maxRate` per second on activation.
*/
action: Function | null;
private _lastRun = 0;
private _runTimeout = null;
private lastProgressBroadcast = Date.now();
/**
* Construct RateLimiter helper
*
* The options passed may also be modified on the instance at runtime.
*
* @param {object=} options - Options for this rate limiter
* @param {number=} options.maxRate -
* @param options - Options for this rate limiter
* @param options.maxRate -
* Maximum rate of activations per second.
* @param {function()=} options.action -
* @param options.action -
* Callback invoked at maste `maxRate` per second on activation.
*/
constructor(options = {}) {
/**
* Maximum rate of activations per second.
* @type {number}
*/
constructor(options: { maxRate?: number, action?: Function } = {}) {
this.maxRate = options.maxRate || 1;
/**
* Callback invoked at most `maxRate` per second on activation.
* @type {function()}
*/
this.action = options.action || null;
this._lastRun = 0;
this._runTimeout = null;
this.lastProgressBroadcast = Date.now();
}
/**
@@ -59,7 +57,7 @@ class RateLimiter {
* invoked immediatly, otherwise a timeout is registered to invoke the
* action at next point in time where it will be within the rate limit.
*
* @returns {boolean} true if activation was within the rate limit.
* @returns true if activation was within the rate limit.
*/
activate() {
let now = Date.now();
@@ -100,5 +98,3 @@ class RateLimiter {
}
}
}
module.exports = RateLimiter;
@@ -6,29 +6,37 @@
* @author Hornwitser
* @module lib/command
*/
"use strict";
const libData = require("./data");
const libLink = require("./link");
const libErrors = require("./errors");
import * as libData from "./data";
import * as libErrors from "./errors";
import type { Link } from "./link";
export type CommandHandler = (args: Record<string, unknown>, control: Link) => Promise<void>;
export type CommandDefinition = [string | string[], string?, Function?];
/**
* Represents a command that can be runned by clusterioctl
* @static
*/
class Command {
export class Command {
private _handler: CommandHandler;
private _definition: CommandDefinition;
name: string;
alias: string[];
/**
* Define an executable command.
*
* @param {Object} cmd - Command definiton.
* @param {Array} cmd.definiton -
* @param cmd - Command definiton.
* @param cmd.definiton -
* Arguments to pass to yargs .command method to define this
* command.
* @param {function(args, control)} handler -
* @param cmd.handler -
* Async function invoked when the command is executed. Is given
* the parsed args and a reference to the {@link
* module:ctl/ctl.Control} instance.
*/
constructor({ definition, handler }) {
constructor({ definition, handler }: { definition: CommandDefinition, handler: CommandHandler }) {
if (definition[0] instanceof Array) {
this.name = definition[0][0].split(" ")[0];
this.alias = definition[0].slice(1);
@@ -40,11 +48,11 @@ class Command {
this._handler = handler;
}
register(yargs) {
register(yargs: any) {
yargs.command(...this._definition);
}
async run(args, control) {
async run(args: Record<string, unknown>, control: Link) {
await this._handler(args, control);
}
}
@@ -56,20 +64,23 @@ class Command {
* CommandTrees. This is used by clusterioctl to hold the full tree of
* available commands, you may extend this tree by using a control plugin,
* see {@link module:lib.BaseControlPlugin#addCommands}
*
* @static
*/
class CommandTree {
export class CommandTree {
public name: string;
public alias: string[];
public description: string;
public subCommands: Map<string, Command | CommandTree>;
/**
* Define a command containing sub commands.
*
* @param {Object} cmd - Command definition
* @param {string} cmd.name - Name of the command.
* @param {Array<string>} cmd.alias - Aliases for this tree.
* @param {description} cmd.description -
* @param cmd - Command definition
* @param cmd.name - Name of the command.
* @param cmd.alias - Aliases for this tree.
* @param cmd.description -
* Descripton to provide for this command tree node.
*/
constructor({ name, alias, description }) {
constructor({ name, alias, description }: { name: string, alias: string[], description: string }) {
if (typeof name !== "string") {
throw new Error("name must be a string");
}
@@ -83,10 +94,10 @@ class CommandTree {
/**
* Add a command or command tree to this tree
*
* @param {module:lib.Command|module:lib.CommandTree} command -
* @param command -
* The command to add to this command tree.
*/
add(command) {
add(command: Command | CommandTree) {
if (this.subCommands.has(command.name)) {
throw new Error(`Command ${command.name} already exists`);
}
@@ -102,13 +113,13 @@ class CommandTree {
/**
* Get a command or command tree from this tree
*
* @param {string} name -
* @param name -
* The name of the command or command tree to retrieve.
* @return {?module:lib.Command|module:lib.CommandTree}
* @return
* The command to add to this command tree.
*/
get(name) {
return this.subCommands.get(name) || null;
get(name: string) {
return this.subCommands.get(name);
}
/**
@@ -117,10 +128,10 @@ class CommandTree {
* Traverses the the tree of sub-commands recursively and adds all of
* them to the given yargs parser.
*
* @param {Object} yargs - yargs parser to register with.
* @param yargs - yargs parser to register with.
*/
register(yargs) {
yargs.command([this.name].concat(this.alias), this.description, (yargs) => {
register(yargs: any) {
yargs.command([this.name].concat(this.alias), this.description, (yargs: any) => {
for (let [name, command] of this.subCommands) {
// Check if the entry is not an alias.
if (name === command.name) {
@@ -141,13 +152,12 @@ class CommandTree {
* Resolves a string with either an host name or an id into an integer with
* the host ID.
*
* @param {module:lib.Link} client - link to controller to query host on.
* @param {string} hostName - string with name or id of host.
* @returns {Promise<number>} host ID.
* @static
* @param client - link to controller to query host on.
* @param hostName - string with name or id of host.
* @returns host ID.
*/
async function resolveHost(client, hostName) {
let hostId;
export async function resolveHost(client: Link, hostName: string) {
let hostId: number;
if (/^-?\d+$/.test(hostName)) {
hostId = parseInt(hostName, 10);
} else {
@@ -173,13 +183,12 @@ async function resolveHost(client, hostName) {
* Resolves a string with either an instance name or an id into an integer
* with the instance ID.
*
* @param {module:lib.Link} client - link to controller to query instance on.
* @param {string} instanceName - string with name or id of instance.
* @returns {Promise<number>} instance ID.
* @static
* @param client - link to controller to query instance on.
* @param instanceName - string with name or id of instance.
* @returns instance ID.
*/
async function resolveInstance(client, instanceName) {
let instanceId;
export async function resolveInstance(client: Link, instanceName: string) {
let instanceId: number;
if (/^-?\d+$/.test(instanceName)) {
instanceId = parseInt(instanceName, 10);
} else {
@@ -205,14 +214,13 @@ async function resolveInstance(client, instanceName) {
* Resolevs a string with either a mod pack name or an id into an integer
* with the mod pack ID.
*
* @param {module:lib.Link} client -
* @param client -
* link to controller to query mod pack on.
* @param {string} modPackName - string with name or id of mod pack.
* @returns {Promise<number>} mod pack ID.
* @static
* @param modPackName - string with name or id of mod pack.
* @returns mod pack ID.
*/
async function resolveModPack(client, modPackName) {
let modPackId;
export async function resolveModPack(client: Link, modPackName: string) {
let modPackId: number;
if (/^-?\d+/.test(modPackName)) {
modPackId = parseInt(modPackName, 10);
} else {
@@ -238,15 +246,14 @@ async function resolveModPack(client, modPackName) {
* Resolves a string with either a role name or an id into an object
* representing the role.
*
* @param {module:lib.Link} client - link to controller to query role on.
* @param {string} roleName - string with name or id of role.
* @returns {Promise<object>} Role info.
* @static
* @param client - link to controller to query role on.
* @param roleName - string with name or id of role.
* @returns Role info.
*/
async function retrieveRole(client, roleName) {
export async function retrieveRole(client: Link, roleName: string) {
let roles = await client.sendTo("controller", new libData.RoleListRequest());
let resolvedRole;
let resolvedRole: libData.RawRole;
if (/^-?\d+$/.test(roleName)) {
let roleId = parseInt(roleName, 10);
for (let role of roles) {
@@ -271,13 +278,3 @@ async function retrieveRole(client, roleName) {
return resolvedRole;
}
module.exports = {
Command,
CommandTree,
resolveHost,
resolveInstance,
resolveModPack,
retrieveRole,
};
+1 -1
View File
@@ -1,5 +1,5 @@
"use strict";
const PlayerStats = require("../PlayerStats");
const PlayerStats = require("../PlayerStats").default;
const { JsonString, jsonArray } = require("./composites");
/** @memberof module:lib */
+1 -1
View File
@@ -1,5 +1,5 @@
"use strict";
const PlayerStats = require("../PlayerStats");
const PlayerStats = require("../PlayerStats").default;
const { jsonArray, JsonNumber } = require("./composites");
/* @memberof module:lib */
@@ -4,23 +4,21 @@
* @module lib/database
* @author Hornwitser
*/
import fs from "fs-extra";
"use strict";
const fs = require("fs-extra");
const libFileOps = require("./file_ops");
const { basicType } = require("./helpers");
import * as libFileOps from "./file_ops";
import { basicType } from "./helpers";
/**
* Converts a Map with only string keys to a JavaScript object.
*
* @param {Map} map - the Map to convert to an object.
* @return {object} - Object with the mapping's key-values as properties.
* @param map - the Map to convert to an object.
* @return Object with the mapping's key-values as properties.
* @throws {Error} if there are non-string keys in map.
*/
function mapToObject(map) {
let obj = {};
export function mapToObject<T>(map: Map<string, T>) {
let obj: Record<string, T> = {};
for (let [key, value] of map) {
if (typeof key !== "string") {
throw new Error(
@@ -40,14 +38,15 @@ function mapToObject(map) {
* properties as key value pairs and converts it into a JavaScript Map.
* Returns an empty Map if the file does not exist.
*
* @param {string} filePath - The path to the JSON file to load.
* @param filePath - The path to the JSON file to load.
* @returns promise resolving to the loaded map
* @throws {Error} If JSON file did not contain an object.
* @throws {Error} If an error occured reading the file.
*/
async function loadJsonAsMap(filePath) {
export async function loadJsonAsMap(filePath: string): Promise<Map<string, unknown>> {
try {
let content = await fs.readFile(filePath);
let parsed = JSON.parse(content);
let parsed = JSON.parse(content.toString());
if (basicType(parsed) !== "object") {
throw new Error(`Expected object but got ${basicType(parsed)}`);
@@ -72,12 +71,12 @@ async function loadJsonAsMap(filePath) {
* If the directory the file is to be saved into doesn't exist it will be
* created.
*
* @param {string} filePath - The path to the JSON file that will be saved.
* @param {Map} map - Map with only string keys to save.
* @param filePath - The path to the JSON file that will be saved.
* @param map - Map with only string keys to save.
* @throws {Error} if there are non-string keys in map.
* @throws {Error} if an error occured writing to the file.
*/
async function saveMapAsJson(filePath, map) {
export async function saveMapAsJson(filePath: string, map: Map<string, unknown>) {
let obj = mapToObject(map);
await libFileOps.safeOutputFile(filePath, JSON.stringify(obj, null, 4));
}
@@ -88,15 +87,15 @@ async function saveMapAsJson(filePath, map) {
* Loads the JSON file specified by path containing an array of objects that
* each have an id property into a Map from the id to the object.
*
* @param {string} filePath - The path to the JSON file to load.
* @param filePath - The path to the JSON file to load.
* @throws {Error} If JSON file did not contain an array.
* @throws {Error} If there are objects in the array without an id property.
* @throws {Error} If an error occured reading the file.
*/
async function loadJsonArrayAsMap(filePath) {
export async function loadJsonArrayAsMap(filePath: string): Promise<Map<unknown, unknown>> {
try {
let content = await fs.readFile(filePath);
let parsed = JSON.parse(content);
let parsed = JSON.parse(content.toString());
if (basicType(parsed) !== "array") {
throw new Error(`Expected array but got ${basicType(parsed)}`);
@@ -132,24 +131,24 @@ async function loadJsonArrayAsMap(filePath) {
* Save the values of a Map with objects containing an id property that is
* equal to the key the objects are stored at into a JSON file.
*
* @param {string} filePath - The path to the JSON file that will be saved.
* @param {Map} map -
* @param filePath - The path to the JSON file that will be saved.
* @param map -
* Map with objects containing an id property equal to the key they are
* stored under.
* @throws {Error} if an error occured writing to the file.
*/
async function saveMapAsJsonArray(filePath, map) {
export async function saveMapAsJsonArray(filePath: string, map: Map<unknown, { id: unknown }>) {
await libFileOps.safeOutputFile(filePath, JSON.stringify([...map.values()], null, 4));
}
function checkName(name) {
function checkName(name: unknown): asserts name is string {
if (typeof name !== "string") {
throw new Error("name must be a string");
}
}
function checkCount(count) {
function checkCount(count: unknown): asserts count is number {
if (typeof count !== "number" || isNaN(count)) {
throw new Error("count must be a number");
}
@@ -164,7 +163,8 @@ function checkCount(count) {
* serialized the database discards entries with a zero counts, and when
* deserialized the content is verified.
*/
class ItemDatabase {
export class ItemDatabase {
private _items: Map<string, number> = new Map();
/**
* Create a new item database
@@ -174,9 +174,7 @@ class ItemDatabase {
* database from. An empty database will be created if this parameter
* is left undefined.
*/
constructor(serialized) {
this._items = new Map();
constructor(serialized?: object) {
// Verify the content of the serialized database
if (serialized !== undefined) {
for (let [name, count] of Object.entries(serialized)) {
@@ -194,10 +192,10 @@ class ItemDatabase {
* Serialize the item database into a plain JavaScript object that can be
* turned inta a string with JSON.stringify().
*
* @returns {object} Serialized representation of the database
* @returns Serialized representation of the database
*/
serialize() {
let obj = {};
let obj: Record<string, number> = {};
for (let [name, count] of this._items) {
if (count !== 0) {
obj[name] = count;
@@ -223,10 +221,10 @@ class ItemDatabase {
* database. If the item has not been previously stored into the database
* then 0 is returned.
*
* @param {string} name - The name of the item to get the count of.
* @returns {number} The count of the item stored.
* @param name - The name of the item to get the count of.
* @returns The count of the item stored.
*/
getItemCount(name) {
getItemCount(name: string) {
checkName(name);
if (!this._items.has(name)) {
@@ -241,10 +239,10 @@ class ItemDatabase {
*
* Add count copies of the item specified by name to the item database.
*
* @param {string} name - The name of the item to add.
* @param {number} count - The count of item to remove.
* @param name - The name of the item to add.
* @param count - The count of item to remove.
*/
addItem(name, count) {
addItem(name: string, count: number) {
checkName(name);
checkCount(count);
@@ -262,10 +260,10 @@ class ItemDatabase {
* If count is greater than the currently stored count the stored item
* count will become negative.
*
* @param {string} name - The name of the item to remove.
* @param {number} count - The count of items to remove.
* @param name - The name of the item to remove.
* @param count - The count of items to remove.
*/
removeItem(name, count) {
removeItem(name: string, count: number) {
checkName(name);
checkCount(count);
@@ -277,12 +275,3 @@ class ItemDatabase {
this._items.set(name, count);
};
}
module.exports = {
mapToObject,
loadJsonAsMap,
saveMapAsJson,
loadJsonArrayAsMap,
saveMapAsJsonArray,
ItemDatabase,
};
-92
View File
@@ -1,92 +0,0 @@
/**
* Errors thrown in Clusterio
* @module lib/errors
*/
"use strict";
/**
* Thrown by commands when they fail
* @static
*/
class CommandError extends Error { }
/**
* Thrown from requests sent when an error occured handling it
* @static
*/
class RequestError extends Error {
constructor(message, code, stack) {
super(message);
if (code) { this.code = code; }
if (stack) { this.stack = stack; }
}
}
/**
* Thrown when a permission check fails.
*
* Is a subclass of RequestError to prevent logging stack traces when
* requests fail due to permission denied.
*
* @static
*/
class PermissionError extends RequestError { }
/**
* Signal for messages that fail validation
* @static
*/
class InvalidMessage extends Error {
constructor(msg, errors) {
super(msg);
this.errors = errors || null;
}
}
/**
* Thrown from requests when the session it was sent on was lost
* @static
*/
class SessionLost extends Error { }
/**
* Thrown when WebSocket authentication failed
* @static
*/
class AuthenticationFailed extends Error { }
/**
* Errror class for known errors occuring during startup
* @static
*/
class StartupError extends Error { }
/**
* Errors outside of our control
* @static
*/
class EnvironmentError extends Error { }
/**
* Errors caused by plugins
* @static
*/
class PluginError extends Error {
constructor(pluginName, original) {
super(`PluginError: ${original.message}`);
this.pluginName = pluginName;
this.original = original;
}
}
module.exports = {
CommandError,
RequestError,
PermissionError,
InvalidMessage,
SessionLost,
AuthenticationFailed,
StartupError,
EnvironmentError,
PluginError,
};
+75
View File
@@ -0,0 +1,75 @@
/**
* Errors thrown in Clusterio
* @module lib/errors
*/
/**
* Thrown by commands when they fail
*/
export class CommandError extends Error { }
/**
* Thrown from requests sent when an error occured handling it
*/
export class RequestError extends Error {
constructor(
message: string,
public code?: string,
public stack?: string,
) {
super(message);
}
}
/**
* Thrown when a permission check fails.
*
* Is a subclass of RequestError to prevent logging stack traces when
* requests fail due to permission denied.
*
*/
export class PermissionError extends RequestError { }
/**
* Signal for messages that fail validation
*/
export class InvalidMessage extends Error {
constructor(
message: string,
public errors: object = null,
) {
super(message);
}
}
/**
* Thrown from requests when the session it was sent on was lost
*/
export class SessionLost extends Error { }
/**
* Thrown when WebSocket authentication failed
*/
export class AuthenticationFailed extends Error { }
/**
* Errror class for known errors occuring during startup
*/
export class StartupError extends Error { }
/**
* Errors outside of our control
*/
export class EnvironmentError extends Error { }
/**
* Errors caused by plugins
*/
export class PluginError extends Error {
constructor(
public pluginName: string,
public original: Error
) {
super(`PluginError: ${original.message}`);
}
}
@@ -1,25 +1,24 @@
/**
* @module lib/file_ops
*/
"use strict";
const fs = require("fs-extra");
const path = require("path");
const crypto = require("crypto"); // needed for getTempFile
import fs from "fs-extra";
import path from "path";
import crypto from "crypto"; // needed for getTempFile
/**
* Returns the newest file in a directory
*
* @param {string} directory - The directory to look for the newest file in.
* @param {function(string): boolean} filter -
* @param directory - The directory to look for the newest file in.
* @param filter -
* Optional function to filter out files to ignore. Should return true
* if the file is to be considered.
* @returns {Promise<string|undefined>}
* @returns
* Name of the file with the newest timestamp or null if the
* directory contains no files.
*/
async function getNewestFile(directory, filter = (name) => true) {
export async function getNewestFile(directory: string, filter = (_name: string) => true) {
let newestTime = new Date(0);
let newestFile;
let newestFile: string | undefined;
for (let entry of await fs.readdir(directory, { withFileTypes: true })) {
if (entry.isFile()) {
let stat = await fs.stat(path.join(directory, entry.name));
@@ -40,11 +39,11 @@ async function getNewestFile(directory, filter = (name) => true) {
* Error reading the size of files are ignored, and error reading the
* directory will result in 0 being returned.
*
* @param {string} directory - The directory to sum files in.
* @returns {Promise<number>} The size in bytes of all files in the directory.
* @param directory - The directory to sum files in.
* @returns The size in bytes of all files in the directory.
*/
async function directorySize(directory) {
let dirEntries;
export async function directorySize(directory: string) {
let dirEntries: fs.Dirent[];
try {
dirEntries = await fs.readdir(directory, { withFileTypes: true });
} catch (err) {
@@ -53,7 +52,7 @@ async function directorySize(directory) {
}
throw err;
}
let statTasks = [];
let statTasks: Promise<number>[] = [];
for (let entry of dirEntries) {
if (entry.isFile()) {
statTasks.push(fs
@@ -77,13 +76,13 @@ async function directorySize(directory) {
* racy and a file or folder may have been created in the folder by the time
* this function returns.
*
* @param {string} directory - directory to check in.
* @param {string} name - file name to check for, may have extension.
* @param {string} extension - dot extension used for the file name.
* @returns {Promise<string>} modified name with extension that likely does
* @param directory - directory to check in.
* @param name - file name to check for, may have extension.
* @param extension - dot extension used for the file name.
* @returns modified name with extension that likely does
* not exist in the folder
*/
async function findUnusedName(directory, name, extension = "") {
export async function findUnusedName(directory: string, name: string, extension = "") {
if (extension && name.endsWith(extension)) {
name = name.slice(0, -extension.length);
}
@@ -101,25 +100,27 @@ async function findUnusedName(directory, name, extension = "") {
}
}
}
/**
* Generate collision resistante temporary file name
*
* Warning: this function should not be relied upon to be accurate for
* security sensitive applications. The selection process is inherently
* racy and a file or folder may have been created in the folder by the time
* this function returns.
*
* @param {string} prefix - Prefix for file
* @param {string} suffix - Suffix for file
* @param {string} tmpdir - Directory for temp file
* @param prefix - Prefix for file
* @param suffix - Suffix for file
* @param tmpdir - Directory for temp file
* @returns Temporary file name
*/
async function getTempFile(prefix, suffix, tmpdir) {
prefix = (typeof prefix !== "undefined") ? prefix : "tmp.";
suffix = (typeof suffix !== "undefined") ? suffix : "";
tmpdir = (typeof tmpdir !== "undefined") ? tmpdir : "./";
export async function getTempFile(prefix = "tmp.", suffix = "", tmpdir = "./") {
let fileName = path.join(prefix + crypto.randomBytes(16).toString("hex") + suffix);
let freeFile = await findUnusedName(tmpdir, fileName);
let fullPath = path.join(tmpdir, freeFile);
return fullPath;
}
/**
* Safely write data to a file
*
@@ -131,11 +132,11 @@ async function getTempFile(prefix, suffix, tmpdir) {
* should not be too much of an issue as the next time the same file is
* written the temporary will be overwritten and renamed to the target file.
*
* @param {string} file - Path to file to write.
* @param {string|Buffer} data - Content to write.
* @param {object|string} options - see fs.writeFile, `flag` must not be set.
* @param file - Path to file to write.
* @param data - Content to write.
* @param options - see fs.writeFile, `flag` must not be set.
*/
async function safeOutputFile(file, data, options={}) {
export async function safeOutputFile(file: string, data: string | Buffer, options: fs.WriteFileOptions ={}) {
let directory = path.dirname(file);
if (!await fs.pathExists(directory)) {
await fs.mkdirs(directory);
@@ -153,7 +154,7 @@ async function safeOutputFile(file, data, options={}) {
}
// Reserved names by allmost all filesystems
// Reserved names by almost all filesystems
const badNames = [".", ".."];
// Reserved namespaces in Windows
@@ -167,10 +168,10 @@ const badWinNamespaces = [
/**
* Check if a string is a valid file name
*
* @param {string} name - Name to check
* @param name - Name to check
* @throws Error if the name is unsuitable.
*/
function checkFilename(name) {
export function checkFilename(name: string) {
// All of these are bad in Windows only, except for /, . and ..
// See: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
const badChars = /[<>:"\/\\|?*\x00-\x1f]/g;
@@ -208,10 +209,10 @@ function checkFilename(name) {
/**
* Clean up string to be suitable for use as filename
*
* @param {string} name - Arbitrary name string
* @returns {string} Filename suitable to use in the filesystem
* @param name - Arbitrary name string
* @returns Filename suitable to use in the filesystem
*/
function cleanFilename(name) {
export function cleanFilename(name: string) {
// copied from checkFilename due to RegExp with global flag containing state.
const badChars = /[<>:"\/\\|?*\x00-\x1f]/g;
const badEnd = /[. ]$/;
@@ -233,14 +234,3 @@ function cleanFilename(name) {
return name;
}
module.exports = {
getNewestFile,
directorySize,
findUnusedName,
getTempFile,
safeOutputFile,
checkFilename,
cleanFilename,
};
@@ -2,18 +2,17 @@
* Hashing functions
* @module lib/hash
*/
"use strict";
const crypto = require("crypto");
const fs = require("fs-extra");
import crypto from "crypto";
import fs from "fs-extra";
/**
* Returns a promise that resolves to the SHA1 hash of the stream given
*
* @param {Stream} stream - Node stream of the content to hash.
* @returns {Promise<string>} hash of the stream.
* @param stream - Node stream of the content to hash.
* @returns hash of the stream.
*/
function hashStream(stream) {
export function hashStream(stream: NodeJS.ReadableStream): Promise<string> {
return new Promise((resolve, reject) => {
let hasher = crypto.createHash("sha1");
hasher.setEncoding("hex");
@@ -33,14 +32,9 @@ function hashStream(stream) {
/**
* Returns a promise that resolves to the SHA1 hash of the file given by path
*
* @param {string} path - Path to the file to hash.
* @returns {Promise<string>} hash of the file given.
* @param path - Path to the file to hash.
* @returns hash of the file given.
*/
function hashFile(path) {
export function hashFile(path: string) {
return hashStream(fs.createReadStream(path));
}
module.exports = {
hashStream,
hashFile,
};
@@ -5,17 +5,14 @@
* @author Hornwitser
*/
"use strict";
/**
* Parse INI file into object of sections
* @param {string} input - content of INI file to parse.
* @returns {object<string, object<string, string>>}
* sections parsed from the ini file.
* @param input - content of INI file to parse.
* @returns sections parsed from the ini file.
*/
function parse(input) {
const sections = {};
let currentSection = sections;
export function parse(input: string) {
const sections: Record<string, string | Record<string, string>> = {};
let currentSection: Record<string, string> = sections as Record<string, string>;
const lines = input.split(/\r\n|\n/);
for (let number = 1; number <= lines.length; number++) {
let line = lines[number - 1].replace(/^[\t ]+/, "");
@@ -56,11 +53,11 @@ function parse(input) {
/**
* Serialize an object of section definitions into an INI file
* @param {object<string, object<string, string>>} sections -
* @param sections -
* Sections to encode into ini format
* @returns {string} sections serialized into INI format
* @returns sections serialized into INI format
*/
function stringify(sections) {
export function stringify(sections: Record<string, string | Record<string, string>>) {
let serialized = [];
let first = true;
@@ -87,8 +84,3 @@ function stringify(sections) {
if (serialized.length) { serialized.push(""); }
return serialized.join("\n");
}
module.exports = {
parse,
stringify,
};
+1
View File
@@ -0,0 +1 @@
export default function isDeepStrictEqual(a: any, b: any): boolean
@@ -1,9 +1,9 @@
"use strict";
const winston = require("winston");
const Transport = require("winston-transport");
const { LEVEL, MESSAGE } = require("triple-beam");
import winston from "winston";
import Transport from "winston-transport";
import { LEVEL, MESSAGE } from "triple-beam";
const levels = Object.freeze({
export const levels = Object.freeze({
fatal: 0,
error: 1,
warn: 2,
@@ -13,7 +13,7 @@ const levels = Object.freeze({
verbose: 6,
});
const colors = {
export const colors = {
fatal: "inverse red",
error: "bold brightRed",
warn: "bold brightYellow",
@@ -21,21 +21,31 @@ const colors = {
info: "bold brightBlue",
server: "bold",
verbose: "bold grey",
};
} as const;
winston.addColors(colors);
// The Console Transport is replicated here because it's overly complicated
// in winston, writes to stdout/err instead of console and doesn't work in
// the browser.
class ConsoleTransport extends Transport {
constructor(options = {}) {
export class ConsoleTransport extends Transport {
errorLevels: Set<string>;
warnLevels: Set<string>;
filter: (info: object) => boolean;
constructor(
options: Transport.TransportStreamOptions & {
errorLevels?: string[],
warnLevels?: string[],
filter?: ConsoleTransport["filter"]
} = {}
) {
super(options);
this.errorLevels = new Set(options.errorLevels || ["fatal", "error"]);
this.warnLevels = new Set(options.warnLevels || ["warn"]);
this.filter = options.filter || (() => true);
}
log(info, callback) {
log(info: any, callback: Function) {
if (!this.filter(info)) {
return callback();
}
@@ -56,12 +66,12 @@ class ConsoleTransport extends Transport {
/**
* Formats winston log messages for the web console
*/
class WebConsoleFormat {
constructor(options = {}) {
this.options = options;
}
export class WebConsoleFormat {
constructor(
public options = {}
) { }
transform(info, options) {
transform(info: any, _options: {}) {
let src = " ";
if (info.host_id !== undefined) {
src += `s:${info.host_name} - `;
@@ -84,31 +94,41 @@ class WebConsoleFormat {
/**
* Filter object for logs
* @typedef {Object} LogFilter
* @property {string} [maxLevel] -
* Maximum log level to include. Higher levels are more verbose.
* @property {boolean} [all] -
* Include log entries from controller, all hosts and all instances.
* @property {boolean} [controller] -
* Include log entries from the controller.
* @property {Array<number>} [hostIds] -
* Include log entries for the given hosts and instances of those
* hosts by id.
* @property {Array<number>} [instanceIds] -
* Include log entries for the given instances by id.
*/
export interface LogFilter {
/**
* Maximum log level to include. Higher levels are more verbose.
*/
maxLevel?: keyof typeof levels;
/**
* Include log entries from controller, all hosts and all instances.
*/
all?: boolean;
/**
* Include log entries from the controller.
*/
controller?: boolean;
/**
* Include log entries for the given hosts and instances of those
* hosts by id.
*/
hostIds?: number[];
/**
* Include log entries for the given instances by id.
*/
instanceIds?: number[];
}
/**
* Create log filter by level and source.
*
* @param {LogFilter} filter -
* @param filter -
* Filter to filter log entries by.
* @returns {function(object): boolean}
* @returns
* filter returning true for log entries that match it.
* @static
*/
function logFilter({ all, controller, hostIds, instanceIds, maxLevel }) {
return info => {
export function logFilter({ all, controller, hostIds, instanceIds, maxLevel }: LogFilter) {
return (info: any) => {
// Note: reversed to filter out undefined levels
if (maxLevel && !(levels[info.level] <= levels[maxLevel])) {
return false;
@@ -135,17 +155,23 @@ function logFilter({ all, controller, hostIds, instanceIds, maxLevel }) {
};
}
const logger = winston.createLogger({
export const logger = winston.createLogger({
level: "verbose",
levels,
format: winston.format.timestamp(),
});
}) as unknown as Logger;
module.exports = {
ConsoleTransport,
WebConsoleFormat,
colors,
levels,
logFilter,
logger,
// Who in their right mind thought that log levels should be hard coded
// into the type when you can define custom levels you want to support?
export type Logger = Omit<winston.Logger,
"error" | "warn" | "help" | "data" | "info" | "debug" | "prompt" | "http" | "verbose" | "input" | "silly" |
"emerg" | "alert" | "crit" | "warning" | "notice"
> & {
fatal: winston.LeveledLogMethod,
error: winston.LeveledLogMethod,
warn: winston.LeveledLogMethod,
audit: winston.LeveledLogMethod,
info: winston.LeveledLogMethod,
server: winston.LeveledLogMethod,
verbose: winston.LeveledLogMethod,
};
@@ -5,24 +5,95 @@
* @module lib/logging_utils
*/
"use strict";
const events = require("events");
const fs = require("fs-extra");
const path = require("path");
const stream = require("stream");
const util = require("util");
const winston = require("winston");
const Transport = require("winston-transport");
const { LEVEL, MESSAGE } = require("triple-beam");
const chalk = require("chalk");
import fs from "fs-extra";
import path from "path";
import stream from "stream";
import util from "util";
import winston from "winston";
import Transport from "winston-transport";
import { LEVEL, MESSAGE } from "triple-beam";
import chalk from "chalk";
const libData = require("./data");
const libErrors = require("./errors");
const libFileOps = require("./file_ops");
const { levels, logFilter, logger } = require("./logging");
const libStream = require("./stream");
import * as libData from "./data";
import * as libErrors from "./errors";
import * as libFileOps from "./file_ops";
import { type LogFilter, levels, logFilter, logger } from "./logging";
import * as libStream from "./stream";
import type { Link } from "./link";
const finished = util.promisify(stream.finished);
interface SecondsLogOutput {
/** Where the message came from, one of "stdout" and "stderr". */
source: "stdout" | "stderr";
/** Timestamp format, one of "date", "seconds" and "none". */
format: "seconds";
/** Timestamp of the message. Not present if format is "none". */
time: string;
/** Type of message, one of "log", "action" and "generic". */
type: "log";
/** Log level for "log" type. i.e "Info" normally. */
level: string;
/** File reported for "log" type. */
file: string;
/** Main content of the line. */
message: string;
}
interface SecondsGenericOutput {
/** Where the message came from, one of "stdout" and "stderr". */
source: "stdout" | "stderr";
/** Timestamp format, one of "date", "seconds" and "none". */
format: "seconds";
/** Timestamp of the message. Not present if format is "none". */
time: string;
/** Type of message, one of "log", "action" and "generic". */
type: "generic";
/** Main content of the line. */
message: string;
}
interface DateActionOutput {
/** Where the message came from, one of "stdout" and "stderr". */
source: "stdout" | "stderr";
/** Timestamp format, one of "date", "seconds" and "none". */
format: "date";
/** Timestamp of the message. Not present if format is "none". */
time: string;
/** Type of message, one of "log", "action" and "generic". */
type: "action";
/** Kind of action for "action" type. i.e "CHAT" for chat. */
action: string;
/** Main content of the line. */
message: string;
}
interface DateGenericOutput {
/** Where the message came from, one of "stdout" and "stderr". */
source: "stdout" | "stderr";
/** Timestamp format, one of "date", "seconds" and "none". */
format: "date";
/** Timestamp of the message. Not present if format is "none". */
time: string;
/** Type of message, one of "log", "action" and "generic". */
type: "generic";
/** Main content of the line. */
message: string;
}
interface UnformattedOutput {
/** Where the message came from, one of "stdout" and "stderr". */
source: "stdout" | "stderr";
/** Timestamp format, one of "date", "seconds" and "none". */
format: "none";
/** Type of message, one of "log", "action" and "generic". */
type: "generic";
/** Main content of the line. */
message: string;
}
export type ParsedFactorioOutput =
| SecondsLogOutput
| SecondsGenericOutput
| DateActionOutput
| DateGenericOutput
| UnformattedOutput
;
/**
* Format a parsed Factorio message with colors
@@ -30,11 +101,11 @@ const finished = util.promisify(stream.finished);
* Formats a parsed Factorio output from lib/factorio into a readable
* colorized output using terminal escape codes that can be printed.
*
* @param {Object} parsed - Parsed Factorio server output.
* @returns {string} terminal colorized message.
* @param parsed - Parsed Factorio server output.
* @returns terminal colorized message.
* @private
*/
function formatServerOutput(parsed) {
function formatServerOutput(parsed: ParsedFactorioOutput) {
let time = "";
if (parsed.format === "seconds") {
time = `${chalk.yellow(parsed.time.padStart(8))} `;
@@ -71,15 +142,17 @@ function formatServerOutput(parsed) {
/**
* Formats winston log messages for a character terminal.
* @static
*/
class TerminalFormat {
constructor(options = {}) {
this.options = options;
export class TerminalFormat {
colorize: winston.Logform.Colorizer;
constructor(
public options: object = {}
) {
this.colorize = winston.format.colorize(options);
}
transform(info, options) {
transform(info: any, options: { showTimestamp: boolean }) {
info = this.colorize.transform(info, this.colorize.options);
let ts = "";
if (options.showTimestamp && info.timestamp) {
@@ -112,17 +185,24 @@ class TerminalFormat {
/**
* Sends logs over a lib/link connection.
* @static
*/
class LinkTransport extends Transport {
constructor(options) {
export class LinkTransport extends Transport {
link: Link;
filter?: (info: any) => boolean;
constructor(
options: Transport.TransportStreamOptions & {
link: Link,
filter?: (info: any) => boolean,
}
) {
super(options);
this.link = options.link;
this.filter = options.filter || null;
this.filter = options.filter;
}
log(info, callback) {
log(info: any, callback: Function) {
if (this.filter && !this.filter(info)) {
return callback();
}
@@ -144,18 +224,26 @@ const logIndexVersion = 2;
/**
* Keeps an index over a log directory to speed up queries to it
* @static
*/
class LogIndex {
constructor(logDirectory, serialized) {
this.logDirectory = logDirectory;
export class LogIndex {
index: Map<string, {
levels: Set<keyof typeof levels>,
controller: boolean,
hostIds: Set<number>,
instanceIds: Set<number>,
}>;
constructor(
public logDirectory: string,
serialized: any,
) {
this.index = new Map();
if (serialized.version !== logIndexVersion) {
return;
}
for (let [file, serializedEntry] of Object.entries(serialized.files)) {
for (let [file, serializedEntry] of Object.entries(serialized.files) as [string, any][]) {
this.index.set(file, {
levels: new Set(serializedEntry.levels),
controller: serializedEntry.controller,
@@ -190,13 +278,13 @@ class LogIndex {
* data about previously indexed files saved with {@link
* module:lib.LogIndex#save}.
*
* @param {string} logDirectory - Path to directory to load from.
* @returns {Promise<module:lib.LogIndex>} loaded or created log index.
* @param logDirectory - Path to directory to load from.
* @returns loaded or created log index.
*/
static async load(logDirectory) {
static async load(logDirectory: string) {
try {
let content = await fs.readFile(path.join(logDirectory, "index.json"));
return new LogIndex(logDirectory, JSON.parse(content));
return new LogIndex(logDirectory, JSON.parse(content.toString()));
} catch (err) {
if (err.code !== "ENOENT") {
logger.warn(`Failed to load ${path.join(logDirectory, "index.json")}: ${err}`);
@@ -218,19 +306,19 @@ class LogIndex {
);
}
async indexFile(file) {
async indexFile(file: string) {
let filePath = path.join(this.logDirectory, file);
let lineStream = new libStream.LineSplitter({ readableObjectMode: true });
let fileStream = fs.createReadStream(filePath);
fileStream.pipe(lineStream);
let entry = {
levels: new Set(),
levels: new Set() as Set<keyof typeof levels>,
controller: false,
hostIds: new Set(),
instanceIds: new Set(),
hostIds: new Set() as Set<number>,
instanceIds: new Set() as Set<number>,
};
for await (let line of lineStream) {
let info;
let info: any;
try {
info = JSON.parse(line);
} catch (err) {
@@ -273,14 +361,14 @@ class LogIndex {
/**
* Check if a file includes entries for the given filter
*
* @param {string} file -
* @param file -
* Name of file in the log directory to check for.
* @param {LogFilter} filter -
* @param filter -
* Filter to check index if file contains entries for.
* @returns {boolean}
* @returns
* true if the file may contain entries included by the filter.
*/
filterIncludesFile(file, { maxLevel, all, controller, instanceIds, hostIds }) {
filterIncludesFile(file: string, { maxLevel, all, controller, instanceIds, hostIds }: LogFilter) {
let entry = this.index.get(file);
if (!entry) {
return true;
@@ -314,35 +402,51 @@ class LogIndex {
/**
* Filter object for querying logs
* @typedef {Object} QueryLogFilter
* @property {number} [limit=100] - Maximum number of entries to return.
* @property {string} [order="asc"] -
* Return entries in ascending ("asc") date order or desceding ("desc")
* date order.
* @property {string} [maxLevel] -
* Maximum log level to include. Higher levels are more verbose.
* @property {boolean} [all] -
* Include log entries from controller, all hosts and all instances.
* @property {boolean} [controller] -
* Include log entries from the controller.
* @property {Array<number>} [hostIds] -
* Include log entries for the given hosts and instances of those
* hosts by id.
* @property {Array<number>} [instanceIds] -
* Include log entries for the given instances by id.
*/
export interface QueryLogFilter {
/**
* Maximum number of entries to return. Defaults to 100.
*/
limit?: number;
/**
* Return entries in ascending ("asc") date order or desceding ("desc")
* date order. Defaults to "asc".
*/
order: "asc" | "desc";
/**
* Maximum log level to include. Higher levels are more verbose.
*/
maxLevel: keyof typeof levels;
/**
* Include log entries from controller, all hosts and all instances.
*/
all: boolean;
/**
* Include log entries from the controller.
*/
controller: boolean;
/**
* Include log entries for the given hosts and instances of those
* hosts by id.
*/
hostIds: number[];
/**
* Include log entries for the given instances by id.
*/
instanceIds: number[];
}
/**
* Query log directory
*
* @param {string} logDirectory - path to directory with logs.
* @param {QueryLogFilter} filter -
* @param logDirectory - path to directory with logs.
* @param filter -
* Filter to limit logs by.
* @param {module:lib.LogIndex=} index -
* @param index -
* Index to speed up query with.
* @returns {Promise<Array<Object>>} log entries matching the filter
* @returns log entries matching the filter
*/
async function queryLog(logDirectory, filter, index) {
export async function queryLog(logDirectory: string, filter: QueryLogFilter, index: LogIndex) {
let files = (await fs.readdir(logDirectory)).filter(entry => logFileGlob.test(entry));
filter = {
@@ -363,8 +467,8 @@ async function queryLog(logDirectory, filter, index) {
continue;
}
let filePath = path.join(logDirectory, file);
let fileStream;
let lineStream;
let fileStream: fs.ReadStream;
let lineStream: libStream.LineSplitter;
if (filter.order === "asc") {
lineStream = new libStream.LineSplitter({ readableObjectMode: true });
fileStream = fs.createReadStream(filePath);
@@ -373,7 +477,7 @@ async function queryLog(logDirectory, filter, index) {
fileStream = await libStream.createReverseReadStream(filePath);
}
lineStream.on("data", line => {
let info;
let info: any;
try {
info = JSON.parse(line);
} catch (err) {
@@ -403,27 +507,18 @@ async function queryLog(logDirectory, filter, index) {
return log;
}
function handleUnhandledErrors() {
export function handleUnhandledErrors() {
/* eslint-disable node/no-process-exit */
process.on("uncaughtException", err => {
logger.fatal(`Uncaught exception:\n${err.stack}`);
process.exit(1);
});
process.on("unhandledRejection", (reason, promise) => {
process.on("unhandledRejection", (reason: Error, _promise) => {
logger.fatal(`Unhandled rejection:\n${reason.stack ? reason.stack : reason}`);
process.exit(1);
});
/* eslint-enable node/no-process-exit */
}
const _formatServerOutput = formatServerOutput;
module.exports = {
TerminalFormat,
LinkTransport,
LogIndex,
handleUnhandledErrors,
queryLog,
// for testing only
_formatServerOutput,
};
// for testing only
export const _formatServerOutput = formatServerOutput;
@@ -2,15 +2,14 @@
* Utilities for dealing with Lua
* @module lib/lua_tools
*/
"use strict";
/**
* Escapes a string for inclusion into a lua string
*
* @param {string} content - String to escape.
* @returns {string} Escaped string.
* @param content - String to escape.
* @returns Escaped string.
*/
function escapeString(content) {
export function escapeString(content: string) {
return content
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
@@ -20,7 +19,3 @@ function escapeString(content) {
.replace(/\r/g, "\\r")
;
}
module.exports = {
escapeString,
};
@@ -2,18 +2,61 @@
* Plugin interfaces and utilities.
* @module lib/plugin
*/
"use strict";
import * as libHelpers from "./helpers";
import type { Logger } from "./logging";
import type { ConfigGroup } from "./config";
import type { CollectorResult } from "./prometheus";
import type { ParsedFactorioOutput } from "./logging_utils";
import type { ModPack, ModInfo } from "./data";
import type { Link } from "./link";
import type { CommandTree } from "./command";
const helpers = require("./helpers");
/* TODO fix when converting to ts
/**
* Conceptual base for controller and instance plugins.
* @typedef {
* BaseControllerPlugin
* |BaseInstancePlugin
* |BaseControlPlugin
* } BasePlugin
*/
export type BasePlugin =
| BaseControllerPlugin
| BaseInstancePlugin
| BaseControlPlugin
| BaseWebPlugin
;
// TODO Add proper typing for plugins
export type PluginInfo = any;
type Controller = any;
type Instance = any;
type Host = any;
type InstanceInfo = any;
type HostConnection = any;
type ControlConnection = any;
type Event = any;
type React = any;
/**
* Information about the event.
*/
interface PlayerEvent {
type: "join" | "leave" | "import";
/** Name of the player that joined/left */
name: string,
/**
* Only present for type "leave". Reason for player leaving the
* game, one of the possible reasons in defines.disconnect_reason
* or "server_quit" if the server exits while the player is online.
*/
reason?: string;
}
type InstanceStatus =
| "unassigned"
| "unknown"
| "stopped"
| "starting"
| "running"
| "stopping"
| "creating_save"
| "exporting_data"
| "deleted"
;
/**
* Base class for instance plugins
@@ -27,40 +70,43 @@ const helpers = require("./helpers");
* Instances may be started and stopped many times, and many instances may
* be running at the same time, each of which will have their own instance
* of the InstancePlugin class.
*
* @static
*/
class BaseInstancePlugin {
constructor(info, instance, host) {
export class BaseInstancePlugin {
/**
* Logger for this plugin
*
* Instance of winston Logger for sending log messages from this
* plugin. Supported methods and their corresponding log levels are
* `error`, `warn`, `audit`, `info` and `verbose`.
*/
logger: Logger;
private _pendingRconMessages: {
resolve: (result: string) => void,
reject: (err: Error) => void,
message: string,
expectEmpty: boolean,
}[] = [];
private _sendingRconMessages = false;
constructor(
/**
* The plugin's own info module
*/
this.info = info;
public info: PluginInfo,
/**
* Instance the plugin started for
* @type {module:host/src/Instance}
*/
this.instance = instance;
public instance: Instance,
/**
* Host running the instance
*
* With the exepction of accessing the host's config you should
* avoid ineracting with the host object directly.
*
* @type {module:host/src/Host}
*/
this.host = host;
/**
* Logger for this plugin
*
* Instance of winston Logger for sending log messages from this
* plugin. Supported methods and their corresponding log levels are
* `error`, `warn`, `audit`, `info` and `verbose`.
*/
this.logger = instance.logger.child({ plugin: this.info.name });
public host: Host,
) {
this.logger = instance.logger.child({ plugin: this.info.name }) as unknown as Logger;
this._pendingRconMessages = [];
this._sendingRconMessages = false;
@@ -77,12 +123,12 @@ class BaseInstancePlugin {
* Invoked after the value of the config field given by `field` has
* changed.
*
* @param {module:lib.ConfigGroup} group -
* @param group -
* The group who's field got changed.
* @param {string} field - Name of the field that changed.
* @param {*} prev - The previous value of the field.
* @param field - Name of the field that changed.
* @param prev - The previous value of the field.
*/
async onInstanceConfigFieldChanged(group, field, prev) { }
async onInstanceConfigFieldChanged(group: ConfigGroup, field: string, prev: unknown) { }
/**
* Called before collecting Prometheus metrics
@@ -97,9 +143,9 @@ class BaseInstancePlugin {
* the Gauges/Counters/etc in onMetric and let Clusterio deal will
* collecting the values.
*
* @returns {*} an async iterator of prometheus metric results or undefined.
* @returns an async iterator of prometheus metric results or undefined.
*/
async onMetrics() { }
async onMetrics(): Promise<void | AsyncIterator<CollectorResult>> { }
/**
* Called after the Factorio server is started
@@ -130,24 +176,10 @@ class BaseInstancePlugin {
* stderr. The output is a parsed form of the line and contains the
* following properties:
*
* @param {Object} parsed - parsed server output.
* @param {string} parsed.source -
* Where the message came from, one of "stdout" and "stderr".
* @param {string} parsed.format -
* Timestamp format, one of "date", "seconds" and "none".
* @param {string=} parsed.time -
* Timestamp of the message. Not present if format is "none".
* @param {string} parsed.type -
* Type of message, one of "log", "action" and "generic".
* @param {string=} parsed.level -
* Log level for "log" type. i.e "Info" normally.
* @param {string=} parsed.file - File reported for "log" type.
* @param {string=} parsed.action -
* Kind of action for "action" type. i.e "CHAT" for chat.
* @param {string} parsed.message - Main content of the line.
* @param {string} line - raw line of server output.
* @param parsed - parsed server output.
* @param line - raw line of server output.
*/
async onOutput(parsed, line) { }
async onOutput(parsed: ParsedFactorioOutput, line: string) { }
/**
* Called when an event on the controller connection happens
@@ -181,9 +213,9 @@ class BaseInstancePlugin {
* send any messages that goes to or via the controller after the
* connection has been closed and before a new one is established.
*
* @param {string} event - one of connect, drop, resume and close
* @param event - one of connect, drop, resume and close
*/
onControllerConnectionEvent(event) { }
onControllerConnectionEvent(event: "connect" | "drop" | "resume" | "close") { }
/**
* Called when the controller is preparing to disconnect from the host
@@ -195,25 +227,19 @@ class BaseInstancePlugin {
* Plugins must stop sending messages to the controller, or are forwarded
* via the controller after the prepare disconnect has been handled.
*
* @param {module:controller/src/HostConnection} connection -
* @param connection -
* The connection to the host preparing to disconnect.
*/
async onPrepareControllerDisconnect(connection) { }
async onPrepareControllerDisconnect(connection: HostConnection) { }
/**
* Called when a player joins or leaves the game
*
* Invoked when a player either joins or leaves the instance.
*
* @param {Object} event - Information about the event.
* @param {string} event.type - Either "join", "leave" or "import".
* @param {string} event.name - Name of the player that joined/left.
* @param {string=} event.reason -
* Only present for type "leave". Reason for player leaving the
* game, one of the possible reasons in defines.disconnect_reason
* or "server_quit" if the server exits while the player is online.
* @param event - Information about the event.
*/
async onPlayerEvent(event) { }
async onPlayerEvent(event: PlayerEvent) { }
/**
* Send RCON message to instance
@@ -227,13 +253,13 @@ class BaseInstancePlugin {
* running some time after `onStart` and some time before `onStop` is
* invoked.
*
* @param {string} message - message to send to server over RCON.
* @param {boolean=} [expectEmpty=false] -
* @param message - message to send to server over RCON.
* @param expectEmpty -
* if true throw if the response is not empty. Useful for detecting
* errors that might have been sent in response.
* @returns {Promise<string>} response from server.
* @returns response from server.
*/
async sendRcon(message, expectEmpty=false) {
async sendRcon(message: string, expectEmpty=false): Promise<string> {
return await this.instance.sendRcon(message, expectEmpty, this.info.name);
}
@@ -252,14 +278,14 @@ class BaseInstancePlugin {
* running some time after `onStart` and some time before `onStop` is
* invoked.
*
* @param {string} message - message to send to server over RCON.
* @param {boolean=} [expectEmpty=false] -
* @param message - message to send to server over RCON.
* @param [expectEmpty=false] -
* if true throw if the response is not empty. Useful for detecting
* errors that might have been sent in response.
* @returns {Promise<string>} response from server.
* @returns response from server.
*/
async sendOrderedRcon(message, expectEmpty=false) {
let promise = new Promise((resolve, reject) => {
async sendOrderedRcon(message: string, expectEmpty=false) {
let promise = new Promise<string>((resolve, reject) => {
this._pendingRconMessages.push({resolve, reject, message, expectEmpty});
});
if (!this._sendingRconMessages) {
@@ -291,28 +317,24 @@ class BaseInstancePlugin {
* To be discovered the class must be exported under the name `ControllerPlugin`
* in the module specified by the `controllerEntrypoint` in the plugin's info.js
* file.
*
* @static
*/
class BaseControllerPlugin {
constructor(info, controller, metrics, logger) {
this.info = info;
export class BaseControllerPlugin {
/**
* Logger for this plugin
*
* Instance of winston Logger for sending log messages from this
* plugin. Supported methods and their corresponding log levels are
* `error`, `warn`, `audit`, `info` and `verbose`.
*/
logger: Logger;
/**
* Controller
* @type {module:controller/src/Controller}
*/
this.controller = controller;
this.metrics = metrics;
/**
* Logger for this plugin
*
* Instance of winston Logger for sending log messages from this
* plugin. Supported methods and their corresponding log levels are
* `error`, `warn`, `audit`, `info` and `verbose`.
*/
this.logger = logger.child({ plugin: this.info.name });
constructor(
public info: PluginInfo,
public controller: Controller,
public metrics: any[],
logger: Logger
) {
this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger;
}
/**
@@ -354,11 +376,11 @@ class BaseControllerPlugin {
* the case of network outages if reconnect fails to re-establish the
* session between the controller and the host.
*
* @param {module:controller/src/InstanceInfo} instance -
* @param instance -
* The instance that changed.
* @param {?string} prev - the previous status of the instance.
* @param prev - the previous status of the instance.
*/
async onInstanceStatusChanged(instance, prev) { }
async onInstanceStatusChanged(instance: InstanceInfo, prev?: InstanceStatus) { }
/**
* Called when the value of a controller config field changed.
@@ -366,12 +388,12 @@ class BaseControllerPlugin {
* Invoked after the value of the config field given by `field` has
* changed on the controller.
*
* @param {module:lib.ConfigGroup} group -
* @param group -
* The group who's field got changed.
* @param {string} field - Name of the field that changed.
* @param {*} prev - The previous value of the field.
* @param field - Name of the field that changed.
* @param prev - The previous value of the field.
*/
async onControllerConfigFieldChanged(group, field, prev) { }
async onControllerConfigFieldChanged(group: ConfigGroup, field: string, prev: unknown) { }
/**
* Called when the value of an instance config field changed.
@@ -379,14 +401,14 @@ class BaseControllerPlugin {
* Invoked after the value of the config field given by `field` has
* changed on an instance.
*
* @param {module:controller/src/InstanceInfo} instance -
* @param instance -
* The instance the config changed on.
* @param {module:lib.ConfigGroup} group -
* @param group -
* The group who's field got changed.
* @param {string} field - Name of the field that changed.
* @param {*} prev - The previous value of the field.
* @param field - Name of the field that changed.
* @param prev - The previous value of the field.
*/
async onInstanceConfigFieldChanged(instance, group, field, prev) { }
async onInstanceConfigFieldChanged(instance: InstanceInfo, group: ConfigGroup, field: string, prev: unknown) { }
/**
* Called before collecting Prometheus metrics
@@ -401,9 +423,9 @@ class BaseControllerPlugin {
* the Gauges/Counters/etc in onMetric and let Clusterio deal will
* collecting the values.
*
* @returns {*} an async iterator of prometheus metric results or undefined.
* @returns an async iterator of prometheus metric results or undefined.
*/
async onMetrics() { }
async onMetrics(): Promise<void | AsyncIterator<CollectorResult>> { }
/**
* Called when the controller is shutting down
@@ -439,11 +461,11 @@ class BaseControllerPlugin {
*
* Invoked when the connection to the host has been closed.
*
* @param {module:controller/src/HostConnection} connection -
* @param connection -
* The connection the event occured on.
* @param {string} event - one of connect, drop, resume and close
* @param event - one of connect, drop, resume and close
*/
onHostConnectionEvent(connection, event) { }
onHostConnectionEvent(connection: HostConnection, event: "connect" | "drop" | "resume" | "close") { }
/**
* Called when an avent on a control connection happens
@@ -475,11 +497,11 @@ class BaseControllerPlugin {
*
* Invoked when the connection to the control has been closed.
*
* @param {module:controller/src/ControlConnection} connection -
* @param connection -
* The connection the event occured on.
* @param {string} event - one of connect, drop, resume, and close.
* @param event - one of connect, drop, resume, and close.
*/
onControlConnectionEvent(connection, event) { }
onControlConnectionEvent(connection: ControlConnection, event: "connect" | "drop" | "resume" | "close") { }
/**
* Called when a host is preparing to disconnect from the controller
@@ -490,10 +512,10 @@ class BaseControllerPlugin {
* Plugins must stop sending messages to the host in question after the
* prepare disconnect has been handled.
*
* @param {module:controller/src/HostConnection} connection -
* @param connection -
* The connection to the host preparing to disconnect.
*/
async onPrepareHostDisconnect(connection) { }
async onPrepareHostDisconnect(connection: HostConnection) { }
/**
* Called when a mod pack is updated
@@ -504,9 +526,9 @@ class BaseControllerPlugin {
* If the mod pack has been deleted its `.isDeleted` property will be
* true.
*
* @param {module:lib.ModPack} modPack - Mod pack that updated.
* @param modPack - Mod pack that updated.
*/
async onModPackUpdated(modPack) { }
async onModPackUpdated(modPack: ModPack) { }
/**
* Called when a mod stored on the controller is updated
@@ -516,9 +538,9 @@ class BaseControllerPlugin {
*
* If a mod has been deleted its `.isDeleted` property will be true.
*
* @param {module:lib.ModInfo} mod - Mod that updated.
* @param mod - Mod that updated.
*/
async onModUpdated(mod) { }
async onModUpdated(mod: ModInfo) { }
/**
* Called when a player joins or leaves an instance
@@ -526,17 +548,11 @@ class BaseControllerPlugin {
* Invoked when a player either joins or leaves an instance in the
* cluster.
*
* @param {module:controller/src/InstanceInfo} instance -
* @param instance -
* The instance it occured on.
* @param {Object} event - Information about the event.
* @param {string} event.type - Either "join", "leave" or "import".
* @param {string} event.name - Name of the player that joined/left.
* @param {string=} event.reason -
* Only present for type "leave". Reason for player leaving the
* game, one of the possible reasons in defines.disconnect_reason
* or "server_quit" if the server exits while the player is online.
* @param event - Information about the event.
*/
async onPlayerEvent(instance, event) { }
async onPlayerEvent(instance: InstanceInfo, event: PlayerEvent) { }
/**
* Broadcast event to all connected hosts
@@ -545,20 +561,15 @@ class BaseControllerPlugin {
* This does not include hosts that are in the process of closing the
* connection, which typically happens when they are shutting down.
*
* @param {*} event - Event to send
* @param {Object} data - Data ta pass with the event.
* @param event - Event to send
*/
broadcastEventToHosts(event, data={}) {
if (event.type !== "event") {
this.logger.warn(`Ignoring broadcast of ${event.type}`);
return;
}
broadcastEventToHosts(event: Event) {
for (let hostConnection of this.controller.wsServer.hostConnections.values()) {
if (
!hostConnection.connector.closing
&& (!event.plugin || hostConnection.plugins.has(event.plugin))
&& (!event.constructor.plugin || hostConnection.plugins.has(event.constructor.plugin))
) {
event.send(hostConnection, data);
hostConnection.send(event);
}
}
}
@@ -571,24 +582,25 @@ class BaseControllerPlugin {
* clusterioctl in order to extend its functionallity. To be discovered the
* class must be exported under the name `ControlPlugin` in the module
* specified by the `controlEntrypoint` in the plugin's info.js file.
*
* @static
*/
class BaseControlPlugin {
constructor(info, logger) {
export class BaseControlPlugin {
/**
* Logger for this plugin
*
* Instance of winston Logger for sending log messages from this
* plugin. Supported methods and their corresponding log levels are
* `error`, `warn`, `audit`, `info` and `verbose`.
*/
logger: Logger;
constructor(
/**
* The plugin's own info module
*/
this.info = info;
/**
* Logger for this plugin
*
* Instance of winston Logger for sending log messages from this
* plugin. Supported methods and their corresponding log levels are
* `error`, `warn`, `audit`, `info` and `verbose`.
*/
this.logger = logger.child({ plugin: this.info.name });
public info: PluginInfo,
logger: Logger,
) {
this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger;
}
/**
@@ -601,136 +613,153 @@ class BaseControlPlugin {
*
* Invoked by clusterioctl to let plugins add commands. `rootCommand` is
* the top level command node which the plugin should add its own {@link
* module:lib.CommandTree} to.
* CommandTree} to.
*
* @param {module:lib.CommandTree} rootCommand -
* @param rootCommand -
* Root of the clusterioctl command tree.
*/
async addCommands(rootCommand) { }
async addCommands(rootCommand: CommandTree) { }
}
/**
* Plugin supplied login form
* @typedef {Object} LoginForm
* @property {string} name -
* Internal name of the login form, this should start with the
* plugin name followed by a dot.
* @property {string} title -
* Name displayed above this form in the login window.
* @property {React.Component} Component -
* React component that's rendered for this login form. This is
* supplied the setToken function via its props which should be
* called when an authentication token is aquired via this
* form.
*/
export interface PluginLoginForm {
/**
* Internal name of the login form, this should start with the
* plugin name followed by a dot.
*/
name: string;
/** Name displayed above this form in the login window. */
title: string;
/**
* React component that's rendered for this login form. This is
* supplied the setToken function via its props which should be called
* when an authentication token is aquired via this form.
*/
Component: React["Component"];
};
/**
* Plugin supplied pages
* @typedef {Object} Page
* @property {string} path - URL path to this page.
* @property {string=} sidebarPath -
* If present and this path matches one of the pages in the sidebar it
* will cause that sidebar entry to be highlighted as active.
* @property {string=} sidebarGroup -
* If present group this entry under a group of the given name in the
* sidebar.
* @property {string=} sidebarName -
* If present creates an entry in the sidebar for this page with the
* given text.
* @property {ReactNode} content -
* A react node which is rendered when this page is navigated to.
* Should render a PageLayout.
*/
export interface PluginPage {
/** URL path to this page. */
path: string;
/**
* If present and this path matches one of the pages in the sidebar it
* will cause that sidebar entry to be highlighted as active.
*/
sidebarPath?: string;
/**
* If present group this entry under a group of the given name in the
* sidebar.
*/
sidebarGroup?: string;
/**
* If present creates an entry in the sidebar for this page with the
* given text.
*/
sidebarName?: string;
/**
* A react node which is rendered when this page is navigated to.
* Should render a PageLayout.
*/
content?: React["ReactNode"];
};
/**
* Base class for web interface plugins
*
* @static
*/
class BaseWebPlugin {
constructor(container, packageData, info, logger) {
export class BaseWebPlugin {
/**
* Contents of the plugin's package.json file
*/
package: any;
/**
* Control link to the controller, not available until the
* connect event in onControllerConnectionEvent is signaled.
*/
control?: Link;
/**
* Logger for this plugin
*
* Instance of winston Logger for sending log messages from this
* plugin. Supported methods and their corresponding log levels are
* `error`, `warn`, `audit`, `info` and `verbose`.
*/
logger: Logger;
/**
* List of login forms provided by this plugin
*/
loginForms: PluginLoginForm[] = [];
/**
* List of pages provided by this plugin
*/
pages: PluginPage[] = [];
/**
* Extra react component to add to core components
*
* Interface to augment core components of the web UI. Setting a
* component as one of the supported properties of this object will
* cause the web UI to render it when displaying that component,
* usually at the end. Each component will receive a `plugin` param
* which is the instance of the web plugin that contained the
* component extra.
*/
componentExtra: {
/** Placed at the end of the controller page. */
ControllerPage?: React["ComponentType"],
/** Placed at the end of the hosts list page. */
HostsPage?: React["ComponentType"],
/**
* Placed at the end of each host page. Takes a `host` param which
* is the host the page is displayed for.
*/
HostViewPage?: React["ComponentType"],
/** Placed at the end of the instance list page. */
InstancesPage?: React["ComponentType"],
/**
* Placed at the end of each instance page. Takes an `instance`
* param which is the instance the page is displayed for.
*/
InstanceViewPage?: React["ComponentType"],
/** Placed at the end of the users list page. */
UsersPage?: React["ComponentType"],
/**
* Placed at the end of each user page. Takes a `user` param which
* is the user object the page is displayed for.
*/
UserViewPage?: React["ComponentType"],
/** Placed at the end of the roles list page. */
RolesPage?: React["ComponentType"],
/**
* Placed at the end of each role page. Takes a `role` param which
* is the role object the page is displayed for.
*/
RoleViewPage?: React["ComponentType"],
} = {};
constructor(
/**
* Webpack container for this plugin
*/
this.container = container;
/**
* Contents of the plugin's package.json file
* @type {Object}
*/
this.package = packageData;
public container: any,
packageData: any,
/**
* The plugin's own info module
*/
this.info = info;
/**
* Control link to the controller, not available until the
* connect event in onControllerConnectionEvent is signaled.
* @type {?module:lib.Link}
*/
this.control = null;
/**
* Logger for this plugin
*
* Instance of winston Logger for sending log messages from this
* plugin. Supported methods and their corresponding log levels are
* `error`, `warn`, `audit`, `info` and `verbose`.
*/
this.logger = logger.child({ plugin: this.info.name });
/**
* List of login forms provided by this plugin
*
* @type {Array<module:lib~LoginForm>}
*/
this.loginForms = [];
/**
* List of pages provided by this plugin
*
* @type {Array<module:lib~Page>}
*/
this.pages = [];
/**
* Extra react component to add to core components
*
* Interface to augment core components of the web UI. Setting a
* component as one of the supported properties of this object will
* cause the web UI to render it when displaying that component,
* usually at the end. Each component will receive a `plugin` param
* which is the instance of the web plugin that contained the
* component extra.
*
* @type {object}
* @property {React.ComponentType} ControllerPage -
* Placed at the end of the controller page.
* @property {React.ComponentType} HostsPage -
* Placed at the end of the hosts list page.
* @property {React.ComponentType} HostViewPage -
* Placed at the end of each host page. Takes a `host` param
* which is the host the page is displayed for.
* @property {React.ComponentType} InstancesPage -
* Placed at the end of the instance list page.
* @property {React.ComponentType} InstanceViewPage -
* Placed at the end of each instance page. Takes an `instance`
* param which is the instance the page is displayed for.
* @property {React.ComponentType} UsersPage -
* Placed at the end of the users list page.
* @property {React.ComponentType} UserViewPage -
* Placed at the end of each user page. Takes a `user` param
* which is the user object the page is displayed for.
* @property {React.ComponentType} RolesPage -
* Placed at the end of the roles list page.
* @property {React.ComponentType} RoleViewPage -
* Placed at the end of each role page. Takes a `role` param
* which is the role object the page is displayed for.
*/
this.componentExtra = {};
public info: PluginInfo,
logger: Logger,
) {
this.package = packageData;
this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger;
}
/**
@@ -770,27 +799,27 @@ class BaseWebPlugin {
* send any messages that goes to or via the controller after the
* connection has been closed and before a new one is established.
*
* @param {string} event - one of connect, drop, resume and close
* @param event - one of connect, drop, resume and close
*/
onControllerConnectionEvent(event) { }
onControllerConnectionEvent(event: "connect" | "drop" | "resume" | "close") { }
}
/**
* Invokes the given hook on all plugins
*
* @param {Map<string, Object>} plugins -
* @param plugins -
* Mapping of plugin names to plugins to invoke the hook on.
* @param {string} hook - Name of hook to invoke.
* @param {...*} args - Arguments to pass on to the hook.
* @returns {Promise<Array>} Non-empty return values from the hooks.
* @param hook - Name of hook to invoke.
* @param args - Arguments to pass on to the hook.
* @returns Non-undefined return values from the hooks.
*/
async function invokeHook(plugins, hook, ...args) {
let results = [];
export async function invokeHook(plugins: Map<string, BasePlugin>, hook: string, ...args: any[]) {
let results: unknown[] = [];
for (let [name, plugin] of plugins) {
try {
// Use an object to detect if the hook failed on timeout or tried to return a value looking like an error
const timeout = {};
let result = await helpers.timeout(
let result = await libHelpers.timeout(
plugin[hook](...args),
15000,
timeout
@@ -806,13 +835,3 @@ async function invokeHook(plugins, hook, ...args) {
}
return results;
}
module.exports = {
BaseInstancePlugin,
BaseControllerPlugin,
BaseControlPlugin,
BaseWebPlugin,
invokeHook,
};
@@ -3,12 +3,10 @@
* Helpers for loading plugins in Node.js
* @module lib/plugin_loader
*/
"use strict";
const libErrors = require("./errors");
const libPlugin = require("./plugin");
const { logger } = require("./logging");
const path = require("path");
import path from "path";
import * as libErrors from "./errors";
import * as libPlugin from "./plugin";
import { logger } from "./logging";
/**
@@ -18,16 +16,15 @@ const path = require("path");
* loaded the info modules will not be reloaded should this function be
* called again.
*
* @param {Map<string, string>} pluginList -
* @param pluginList -
* Mapping of plugin name to require path for the plugins to load.
* @returns {Promise<Array<Object>>} Array of plugin info modules.
* @static
* @returns Array of plugin info modules.
*/
async function loadPluginInfos(pluginList) {
let plugins = [];
export async function loadPluginInfos(pluginList: Map<string, string>) {
let plugins: libPlugin.PluginInfo[] = [];
for (let [pluginName, pluginPath] of pluginList) {
let pluginInfo;
let pluginPackage;
let pluginInfo: libPlugin.PluginInfo;
let pluginPackage: { version: string };
try {
pluginInfo = require(path.posix.join(pluginPath, "info"));
@@ -56,7 +53,12 @@ async function loadPluginInfos(pluginList) {
return plugins;
}
function loadPluginClass(entrypointName, className, pluginClass, pluginInfo) {
function loadPluginClass(
entrypointName: string,
className: string,
pluginClass: any,
pluginInfo: libPlugin.PluginInfo
) {
let resolvedPath = path.posix.join(pluginInfo.requirePath, pluginInfo[entrypointName]);
let entrypoint = require(resolvedPath);
if (!entrypoint[className]) {
@@ -75,45 +77,41 @@ function loadPluginClass(entrypointName, className, pluginClass, pluginInfo) {
/**
* Load controller plugin class of a plugin
*
* @param {Object} pluginInfo -
* @param pluginInfo -
* Plugin info object returned from {@link
* module:lib.loadPluginInfos} to load class from.
* @returns {Promise<function>} plugin class
* @static
* loadPluginInfos} to load class from.
* @returns plugin class
*/
async function loadControllerPluginClass(pluginInfo) {
export async function loadControllerPluginClass(
pluginInfo: libPlugin.PluginInfo
): Promise<libPlugin.BaseControllerPlugin> {
return loadPluginClass("controllerEntrypoint", "ControllerPlugin", libPlugin.BaseControllerPlugin, pluginInfo);
}
/**
* Load instance plugin class of a plugin
*
* @param {Object} pluginInfo -
* @param pluginInfo -
* Plugin info object returned from {@link
* module:lib.loadPluginInfos} to load class from.
* @returns {Promise<function>} plugin class
* @static
* loadPluginInfos} to load class from.
* @returns plugin class
*/
async function loadInstancePluginClass(pluginInfo) {
export async function loadInstancePluginClass(
pluginInfo: libPlugin.PluginInfo
): Promise<libPlugin.BaseInstancePlugin> {
return loadPluginClass("instanceEntrypoint", "InstancePlugin", libPlugin.BaseInstancePlugin, pluginInfo);
}
/**
* Load control plugin class of a plugin
*
* @param {Object} pluginInfo -
* @param pluginInfo -
* Plugin info object returned from {@link
* module:lib.loadPluginInfos} to load class from.
* @returns {Promise<function>} plugin class
* @static
* loadPluginInfos} to load class from.
* @returns plugin class
*/
async function loadControlPluginClass(pluginInfo) {
export async function loadControlPluginClass(
pluginInfo: libPlugin.PluginInfo
): Promise<libPlugin.BaseControlPlugin> {
return loadPluginClass("controlEntrypoint", "ControlPlugin", libPlugin.BaseControlPlugin, pluginInfo);
}
module.exports = {
loadPluginInfos,
loadControllerPluginClass,
loadInstancePluginClass,
loadControlPluginClass,
};
File diff suppressed because it is too large. Load diff
@@ -2,8 +2,7 @@
* JSON schemas used for validating link messages
* @module lib/schema
*/
"use strict";
const Ajv = require("ajv");
import Ajv, { JSONSchemaType, ValidateFunction } from "ajv";
const ajv = new Ajv({
// These are duplicated in scripts/compile_validator
allowUnionTypes: true,
@@ -14,17 +13,16 @@ const ajv = new Ajv({
/**
* Compile JSON schema into validator
* @function
* @param {Object} schema - JSON schema to create validator for.
* @returns {Function} Validator for the schema.
* @param schema - JSON schema to create validator for.
* @returns Validator for the schema.
*/
function compile(schema) {
export function compile<T>(schema: JSONSchemaType<T>) {
if (typeof global !== "object" || !global.lazySchemaCompilation) {
return ajv.compile(schema);
return ajv.compile<T>(schema);
}
let doValidate;
function validate(data) {
let doValidate: ValidateFunction<T>;
const validate = <ValidateFunction<T>>function (data) {
if (!doValidate) {
doValidate = ajv.compile(schema);
}
@@ -32,9 +30,5 @@ function compile(schema) {
validate.errors = doValidate.errors;
return result;
}
return validate;
return validate
}
module.exports = {
compile,
};
@@ -3,15 +3,15 @@
* @module lib/shared_commands
*/
"use strict";
const path = require("path");
import path from "path";
const libConfig = require("./config");
const libFileOps = require("./file_ops");
const { logger } = require("./logging");
const libHelpers = require("./helpers");
import * as libConfig from "./config";
import * as libFileOps from "./file_ops";
import { logger } from "./logging";
import * as libHelpers from "./helpers";
function print(...content) {
function print(...content: any[]) {
// eslint-disable-next-line no-console
console.log(...content);
}
@@ -22,9 +22,9 @@ function print(...content) {
* Can be passed to yargs.command to implement a plugin list management
* command. Use handlePluginCommand to do the requested action.
*
* @param {Object} yargs - yargs command builder.
* @param yargs - yargs command builder.
*/
function pluginCommand(yargs) {
export function pluginCommand(yargs: any) {
yargs
.command("add <path>", "Add plugin by require path")
.command("remove <name>", "Remove plugin by name")
@@ -40,20 +40,24 @@ function pluginCommand(yargs) {
*
* Handle the actions that are made available by pluginCommand.
*
* @param {Object} args - yargs args object.
* @param {Map<string, string>} pluginList - Current list of plugins.
* @param {string} pluginListPath - Path to plugin list config file.
* @param args - yargs args object.
* @param pluginList - Current list of plugins.
* @param pluginListPath - Path to plugin list config file.
*/
async function handlePluginCommand(args, pluginList, pluginListPath) {
export async function handlePluginCommand(
args: Record<string, unknown>,
pluginList: Map<string, string>,
pluginListPath: string
) {
let command = args._[1];
if (command === "add") {
let pluginPath = args.path;
let pluginPath = args.path as string;
if (/^\.\.?[\/\\]/.test(pluginPath)) {
pluginPath = path.resolve(pluginPath);
}
let pluginInfo;
let pluginInfo: { name: string };
try {
// eslint-disable-next-line node/global-require
pluginInfo = require(path.posix.join(pluginPath, "info"));
@@ -74,7 +78,7 @@ async function handlePluginCommand(args, pluginList, pluginListPath) {
print(`Added ${pluginInfo.name}`);
} else if (command === "remove") {
if (!pluginList.delete(args.name)) {
if (!pluginList.delete(args.name as string)) {
logger.error(`Plugin with name ${args.name} does not exist`);
process.exitCode = 1;
return;
@@ -97,11 +101,11 @@ async function handlePluginCommand(args, pluginList, pluginListPath) {
* Can be passed to yargs.command to implement a config command. Use
* handleConfigCommand to do the requested action.
*
* @param {Object} yargs - yargs command builder.
* @param yargs - yargs command builder.
*/
function configCommand(yargs) {
export function configCommand(yargs: any) {
yargs
.command("set <field> [value]", "Set config field", yargs => {
.command("set <field> [value]", "Set config field", (yargs: any) => {
yargs.options({
"stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" },
});
@@ -119,15 +123,19 @@ function configCommand(yargs) {
*
* Handle the actions that are made available by configCommand.
*
* @param {Object} args - yargs args object.
* @param {module:lib.Config} instance - Config instance.
* @param {string} configPath - Path to configuration file.
* @param args - yargs args object.
* @param instance - Config instance.
* @param configPath - Path to configuration file.
*/
async function handleConfigCommand(args, instance, configPath) {
export async function handleConfigCommand(
args: Record<string, unknown>,
instance: libConfig.Config,
configPath: string
) {
let command = args._[1];
if (command === "list") {
for (let GroupClass of instance.constructor.groups.values()) {
for (let GroupClass of (instance as any).constructor.groups.values()) {
for (let def of GroupClass.definitions.values()) {
let value = instance.get(def.fullName);
print(`${def.fullName} ${JSON.stringify(value)}`);
@@ -136,7 +144,7 @@ async function handleConfigCommand(args, instance, configPath) {
} else if (command === "show") {
try {
print(instance.get(args.field));
print(instance.get(args.field as string));
} catch (err) {
if (err instanceof libConfig.InvalidField) {
logger.error(err.message);
@@ -154,7 +162,7 @@ async function handleConfigCommand(args, instance, configPath) {
}
try {
instance.set(args.field, args.value);
instance.set(args.field as string, args.value);
await libFileOps.safeOutputFile(configPath, JSON.stringify(instance.serialize(), null, 4));
} catch (err) {
if (err instanceof libConfig.InvalidField || err instanceof libConfig.InvalidValue) {
@@ -165,12 +173,3 @@ async function handleConfigCommand(args, instance, configPath) {
}
}
}
module.exports = {
pluginCommand,
handlePluginCommand,
configCommand,
handleConfigCommand,
};
+1 -1
View File
@@ -138,7 +138,7 @@ type ReadStreamOptions = Parameters<typeof fs.createReadStream>[1];
* Options to pass to fs.createReadStream. Values passed for `fd` and
* `fs` will be ignored.
*/
export async function createReverseReadStream(path: string, options: ReadStreamOptions) {
export async function createReverseReadStream(path: string, options?: ReadStreamOptions) {
if (typeof options === "string") {
options = { encoding: options };
}
@@ -7,24 +7,23 @@
* @module lib/users
*/
"use strict";
const libErrors = require("./errors");
import * as libErrors from "./errors";
const PlayerStats = require("./PlayerStats");
import PlayerStats from "./PlayerStats";
/**
* Represents a permission that can be granted
* @static
*/
class Permission {
constructor(name, title, description, grantByDefault) {
this.name = name;
this.title = title;
this.description = description;
this.grantByDefault = grantByDefault;
}
export class Permission {
constructor(
public name: string,
public title: string,
public description: string,
public grantByDefault: boolean,
) { }
}
const permissions = new Map();
export const permissions = new Map<string, Permission>();
/**
* Define a new user permission for Clusterio
@@ -33,15 +32,25 @@ const permissions = new Map();
* granted to roles, and checked on users. Name should be `lower_case`
* style and start with the plugin name followed by a dot.
*
* @param {Object} def - The definition for the permission.
* @param {string} def.name - The internal name for the permission.
* @param {string} def.title - User facing name for permission.
* @param {string} def.description - User facing description.
* @param {boolean} def.grantByDefault -
* @param def - The definition for the permission.
* @param def.name - The internal name for the permission.
* @param def.title - User facing name for permission.
* @param def.description - User facing description.
* @param def.grantByDefault -
* If true this role is granted by the generated Player role by default.
*
*/
function definePermission({ name, title, description, grantByDefault=false }) {
export function definePermission({
name,
title,
description,
grantByDefault = false,
}: {
name: string,
title: string,
description: string,
grantByDefault?: boolean,
}) {
if (typeof name !== "string" || !name.length) {
throw new Error("Expected name to be a non-empty string");
}
@@ -64,10 +73,14 @@ function definePermission({ name, title, description, grantByDefault=false }) {
/**
* Represents a collection of granted permissions
* @static
*/
class Role {
constructor(serializedRole) {
export class Role {
id: number;
name: string;
description: string;
permissions: Set<string>;
constructor(serializedRole: ReturnType<Role["serialize"]>) {
this.id = serializedRole.id;
this.name = serializedRole.name;
this.description = serializedRole.description;
@@ -98,14 +111,18 @@ class Role {
*
* Ensures the role with id 0 exisits and has the core.admin permission.
*
* @param {Map<number, module:lib.Role>} roles - role storage to modify.
* @returns {module:lib.Role} the default admin role
* @static
* @param roles - role storage to modify.
* @returns the default admin role
*/
function ensureDefaultAdminRole(roles) {
export function ensureDefaultAdminRole(roles: Map<number, Role>) {
let admin = roles.get(0);
if (!admin) {
admin = new Role({ id: 0, name: "Cluster Admin", description: "Cluster wide administrator." });
admin = new Role({
id: 0,
name: "Cluster Admin",
description: "Cluster wide administrator.",
permissions: [],
});
roles.set(0, admin);
}
admin.permissions.add("core.admin");
@@ -119,13 +136,17 @@ function ensureDefaultAdminRole(roles) {
* assigned by default. Note that this may not be the same role as the one
* that is assigned to new users by default.
*
* @param {Map<number, module:lib.Role>} roles - role storage to modify.
* @static
* @param roles - role storage to modify.
*/
function ensureDefaultPlayerRole(roles) {
export function ensureDefaultPlayerRole(roles: Map<number, Role>) {
let player = roles.get(1);
if (!player) {
player = new Role({ id: 1, name: "Player", description: "Default player role." });
player = new Role({
id: 1,
name: "Player",
description: "Default player role.",
permissions: [],
});
roles.set(1, player);
}
player.grantDefaultPermissions();
@@ -136,56 +157,42 @@ function ensureDefaultPlayerRole(roles) {
* Represeents a user in the cluster
*
* Holds data about a Factorio user in the cluster.
* @static
*/
class User {
constructor(serializedUser, loadedRoles) {
/**
* Factorio user name.
* @type {string}
*/
export class User {
/** Factorio user name. */
name: string;
/** Instances this user is online on. */
instances: Set<number>;
/** Unix time in seconds the user token must be issued after to be valid. */
tokenValidAfter: number;
/** True if the user is promoted to admin on the Factorio instances. */
isAdmin: boolean;
/** True if the user is whitelisted on the Factorio instances. */
isWhitelisted: boolean;
/** True if the user is banned from Factorio instances. */
isBanned: boolean;
/** Reason for being banned. Ignored if isBanned is false. */
banReason: string;
/** Roles this user has */
roles: Set<Role>;
/** Per instance statistics for the player this user account is tied to. */
instanceStats: Map<number, PlayerStats>;
/** Combined statistics for the player this user account is tied to. */
playerStats: PlayerStats;
/** True if this user object has been removed from the cluster. */
isDeleted: boolean;
constructor(
serializedUser: ReturnType<User["serialize"]>,
loadedRoles: Map<number, Role>
) {
this.name = serializedUser.name;
/**
* Instances this user is online on.
* @type {Set<number>}
*/
this.instances = new Set();
/**
* Unix time in seconds the user token must be issued after to be valid.
* @type {number}
*/
this.tokenValidAfter = serializedUser.token_valid_after || 0;
/**
* True if the user is promoted to admin on the Factorio instances.
* @type {boolean}
*/
this.isAdmin = Boolean(serializedUser.is_admin);
/**
* True if the user is whitelisted on the Factorio instances.
* @type {boolean}
*/
this.isWhitelisted = Boolean(serializedUser.is_whitelisted);
/**
* True if the user is banned from Factorio instances.
* @type {boolean}
*/
this.isBanned = Boolean(serializedUser.is_banned);
/**
* Reason for being banned. Ignored if isBanned is false.
* @type {string}
*/
this.banReason = serializedUser.ban_reason || "";
/**
* Roles this user has
* @type {Set<module:lib.Role>}
*/
this.roles = new Set();
if (serializedUser.roles) {
for (let roleId of serializedUser.roles) {
@@ -195,32 +202,26 @@ class User {
}
}
}
/**
* Per instance statistics for the player this user account is tied to.
* @type {Map<number, module:lib.PlayerStats>}
*/
this.instanceStats = new Map(
(serializedUser.instance_stats ? serializedUser.instance_stats : []).map(
([id, stats]) => [id, new PlayerStats(stats)]
)
);
/**
* Combined statistics for the player this user account is tied to.
* @type {module:lib.PlayerStats}
*/
this.playerStats = this._calculatePlayerStats();
/**
* True if this user object has been removed from the cluster.
* @type {boolean}
*/
this.isDeleted = false;
}
serialize() {
let serialized = {
let serialized: {
name: string,
roles?: number[],
token_valid_after?: number,
is_admin?: boolean,
is_whitelisted?: boolean,
is_banned?: boolean,
ban_reason?: string,
instance_stats?: [number, object][],
} = {
name: this.name,
};
@@ -272,11 +273,11 @@ class User {
* permission. If the permission is not granted for the user a
* "Permission denied" error is thrown.
*
* @param {string} permission - The permission to check for.
* @param permission - The permission to check for.
* @throws {Error} If the given permission does not exist.
* @throws {module:lib.PermissionError} if the user does noh have the given permission.
* @throws {libErrors.PermissionError} if the user does noh have the given permission.
*/
checkPermission(permission) {
checkPermission(permission: string) {
if (!permissions.has(permission)) {
throw new Error(`permission ${permission} does not exist`);
}
@@ -290,12 +291,12 @@ class User {
throw new libErrors.PermissionError("Permission denied");
}
notifyJoin(instance_id) {
notifyJoin(instance_id: number) {
this.instances.add(instance_id);
User.onlineUsers.add(this);
}
notifyLeave(instance_id) {
notifyLeave(instance_id: number) {
this.instances.delete(instance_id);
if (!this.instances.size) {
User.onlineUsers.delete(this);
@@ -321,12 +322,11 @@ class User {
}
return playerStats;
}
/**
* Set of users currently online in the cluster.
*/
static onlineUsers = new Set<User>();
}
/**
* Set of users currently online in the cluster.
* @type {module:lib.User}
*/
User.onlineUsers = new Set();
// Definitions for the built in permissions used in Clusterio.
// description should answer "this permission allows you to ___"
@@ -677,14 +677,3 @@ definePermission({
title: "Dump WebSocket",
description: "Dump all WebSocket communicatation from the controller.",
});
module.exports = {
Permission,
Role,
User,
permissions,
definePermission,
ensureDefaultAdminRole,
ensureDefaultPlayerRole,
};
@@ -1,4 +1,4 @@
"use strict";
import type JSZip from "jszip";
/**
* Returns the root folder in the zip file
@@ -6,13 +6,10 @@
* Returns the name of the folder that all files in the zip file is
* contained in. Throws an error if there are multiple such folders.
*
* @param {JSZip} zip - Zip to search through.
* @returns {string} name of the root folder.
* @memberof module:lib
* @private
* @inner
* @param zip - Zip to search through.
* @returns name of the root folder.
*/
function findRoot(zip) {
export function findRoot(zip: JSZip) {
let root = null;
zip.forEach((relativePath, file) => {
let index = relativePath.indexOf("/");
@@ -34,7 +31,3 @@ function findRoot(zip) {
return root;
}
module.exports = {
findRoot,
};