feature/node-rewrite
Sven Slootweg 2 years ago
parent 060e7d06e5
commit f08044baf3

@ -4,7 +4,8 @@
"description": "A VPS management panel", "description": "A VPS management panel",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"dev": "NODE_ENV=development nodemon --ext js,pug,jsx,gql,pegjs --ignore node_modules --ignore src/client --inspect=9229 bin/server.js" "dev": "NODE_ENV=development nodemon --ext js,pug,jsx,gql,pegjs --ignore node_modules --ignore src/client --inspect=9229 bin/server.js",
"dev:css": "sass --watch src/scss/style.scss public/css/style.css"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -36,6 +37,7 @@
"@validatem/required": "^0.1.1", "@validatem/required": "^0.1.1",
"@validatem/when": "^0.1.0", "@validatem/when": "^0.1.0",
"JSONStream": "^1.1.4", "JSONStream": "^1.1.4",
"ansi-to-html": "^0.7.2",
"argon2": "^0.27.0", "argon2": "^0.27.0",
"array.prototype.flat": "^1.2.1", "array.prototype.flat": "^1.2.1",
"as-expression": "^1.0.0", "as-expression": "^1.0.0",
@ -106,6 +108,7 @@
"nodemon": "^1.18.11", "nodemon": "^1.18.11",
"npm-check-licenses": "^1.0.5", "npm-check-licenses": "^1.0.5",
"react": "^16.8.6", "react": "^16.8.6",
"react-hot-loader": "^4.3.12" "react-hot-loader": "^4.3.12",
"sass": "^1.50.0"
} }
} }

@ -1,104 +1,135 @@
body { body {
background-color: #e4e4e4; background-color: rgb(228, 228, 228);
margin: 0px; margin: 0px;
font-family: sans-serif; } font-family: sans-serif;
}
.content { .content {
padding: 8px; } padding: 8px;
}
label { label {
margin-right: 12px; } margin-right: 12px;
}
.menu { .menu {
background-color: #000424; } background-color: rgb(0, 4, 36);
.menu h1, .menu .menuItem { }
.menu h1, .menu .menuItem {
display: inline-block; display: inline-block;
color: white; } color: white;
.menu h1 { }
margin: 0px 16px; } .menu h1 {
.menu .menuItem a { margin: 0px 16px;
}
.menu .menuItem a {
color: white; color: white;
text-decoration: none; text-decoration: none;
padding: 15px 9px 5px 9px; } padding: 15px 9px 5px 9px;
.menu .menuItem.active a { }
.menu .menuItem.active a {
/* FIXME: Make this lighter when there is no submenu, to match the page background color */ /* FIXME: Make this lighter when there is no submenu, to match the page background color */
background-color: #dddddd; background-color: rgb(221, 221, 221);
color: black; } color: black;
.menu .menuItem:not(.active) a:hover { }
background-color: #afafaf; .menu .menuItem:not(.active) a:hover {
color: black; } background-color: rgb(175, 175, 175);
color: black;
}
.fakeSubmenu, .submenu { .fakeSubmenu, .submenu {
background: linear-gradient(to bottom, #dddddd, #dddddd 60%, #caccce); } background: linear-gradient(to bottom, rgb(221, 221, 221), rgb(221, 221, 221) 60%, rgb(202, 204, 206));
}
.fakeSubmenu { .fakeSubmenu {
height: 16px; } height: 16px;
}
.submenu { .submenu {
padding: .3em .2em 0 .2em; padding: 0.3em 0.2em 0 0.2em;
border-bottom: 1px solid #000424; } border-bottom: 1px solid rgb(0, 4, 36);
.submenu .menuItem { }
.submenu .menuItem {
display: inline-block; display: inline-block;
margin-bottom: -1px; margin-bottom: -1px;
padding: .3em .7em; padding: 0.3em 0.7em;
font-size: .95em; } font-size: 0.95em;
.submenu .menuItem a { }
.submenu .menuItem a {
text-decoration: none; text-decoration: none;
color: black; } color: black;
.submenu .menuItem.active { }
background-color: #e4e4e4; .submenu .menuItem.active {
border: 1px solid #000424; background-color: rgb(228, 228, 228);
border-bottom: none; } border: 1px solid rgb(0, 4, 36);
border-bottom: none;
}
table { table {
border-collapse: collapse; } border-collapse: collapse;
table th, table td { }
table th, table td {
padding: 6px 9px; padding: 6px 9px;
border: 1px solid black; } border: 1px solid black;
table th { }
text-align: left; } table th {
table td.hidden { text-align: left;
border: none; } }
table td.hidden {
border: none;
}
table.drives td { table.drives td {
vertical-align: top; } vertical-align: top;
}
table.drives td.smart.HEALTHY { table.drives td.smart.HEALTHY {
background-color: #00a500; } background-color: rgb(0, 165, 0);
}
table.drives td.smart.DETERIORATING { table.drives td.smart.DETERIORATING {
background-color: #ff9100; } background-color: rgb(255, 145, 0);
}
table.drives td.smart.FAILING { table.drives td.smart.FAILING {
background-color: #e60000; } background-color: rgb(230, 0, 0);
}
table.drives .hasPartitions td:not(.smart), table.drives .partition:not(.last) td:not(.smart) { table.drives .hasPartitions td:not(.smart), table.drives .partition:not(.last) td:not(.smart) {
border-bottom-color: transparent; } border-bottom-color: transparent;
}
table.drives .partition { table.drives .partition {
font-style: italic; font-style: italic;
font-size: .8em; } font-size: 0.8em;
table.drives .partition td { }
padding: 4px 9px; } table.drives .partition td {
table.drives .partition .notMounted { padding: 4px 9px;
color: gray; } }
table.drives .partition .notMounted {
color: gray;
}
table.drives tr.smartStatus { table.drives tr.smartStatus {
font-size: .85em; } font-size: 0.85em;
table.drives tr.smartStatus td { }
padding: 4px 9px; } table.drives tr.smartStatus td {
padding: 4px 9px;
}
table.drives th.healthy { table.drives th.healthy {
color: #006100; } color: rgb(0, 97, 0);
}
table.drives th.atRisk { table.drives th.atRisk {
color: #7c4600; } color: rgb(124, 70, 0);
}
table.drives th.failing { table.drives th.failing {
color: #c20000; } color: rgb(194, 0, 0);
}
.stacktrace { .stacktrace {
white-space: pre-wrap; white-space: pre-wrap;
font-family: monospace; } font-family: monospace;
.stacktrace .irrelevant { background-color: rgb(12, 12, 12);
color: gray; } border: 1px solid black;
padding: 0.8em;
max-width: 1200px;
}
.stacktrace .irrelevant {
color: gray;
}
/*# sourceMappingURL=style.css.map */

@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["../../src/scss/style.scss"],"names":[],"mappings":"AAIA;EACC,kBALqB;EAMrB;EACA;;;AAGD;EACC;;;AAOD;EACC;;;AAGD;EACC,kBAtBqB;;AAwBrB;EACC;EACA;;AAGD;EACC;;AAIA;EACC;EACA;EACA;;AAIA;AAEC;EACA,kBA3CqB;EA4CrB;;AAKD;EACC;EACA;;;AAMJ;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;AAEA;EACC;EACA;EACA;EACA;;AAEA;EACC;EACA;;AAKD;EACC,kBArFmB;EAsFnB;EACA;;;AAKH;EACC;;AAEA;EACC;EACA;;AAGD;EACC;;AAGD;EACC;;;AAKD;EACC;;AAIA;EACC;;AAGD;EACC;;AAGD;EACC;;AAKD;EACC;;AAIF;EACC;EACA;;AAEA;EACC;;AAGD;EACC;;AAIF;EACC;;AAEA;EACC;;AAKD;EACC;;AAGD;EACC;;AAGD;EACC;;;AAKH;EACC;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC","file":"style.css"}

@ -2,11 +2,14 @@
const Promise = require("bluebird"); const Promise = require("bluebird");
const smartctl = require("../../../packages/exec-smartctl"); const smartctl = require("../../../packages/exec-smartctl");
const dlayerWrap = require("../../../packages/dlayer-wrap");
module.exports = function () { module.exports = function () {
return function (paths) { return function (paths) {
return Promise.map(paths, (path) => { return Promise.map(paths, (path) => {
return smartctl.attributes({ devicePath: path }); return dlayerWrap(() => smartctl.attributes({ devicePath: path }), {
allowedErrors: [ smartctl.AttributesError ]
});
}); });
}; };
}; };

@ -1,12 +1,15 @@
"use strict"; "use strict";
const Promise = require("bluebird"); const Promise = require("bluebird");
const dlayerWrap = require("../../../packages/dlayer-wrap");
const smartctl = require("../../../packages/exec-smartctl"); const smartctl = require("../../../packages/exec-smartctl");
module.exports = function () { module.exports = function () {
return function (paths) { return function (paths) {
return Promise.map(paths, (path) => { return Promise.map(paths, (path) => {
return smartctl.info({ devicePath: path }); return dlayerWrap(() => smartctl.info({ devicePath: path }), {
allowedErrors: [ smartctl.InfoError ]
});
}); });
}; };
}; };

@ -39,15 +39,18 @@ module.exports = function Drive ({ path }) {
}); });
}, },
$sources: { $sources: {
// lsblk: { lsblk: {
// [dlayerSource.ID]: { path }, [dlayerSource.ID]: { path },
// }, size: "size"
},
smartctlScan: { smartctlScan: {
[dlayerSource.ID]: path, [dlayerSource.ID]: path,
interface: "interface" interface: "interface"
}, },
smartctlInfo: { smartctlInfo: {
[dlayerSource.ID]: path, [dlayerSource.ID]: path,
// NOTE: We allow allowable errors here because the SMART subsystem failing doesn't affect any other aspect of the drive's information, so the Drive object as a whole should not yield an error
[dlayerSource.AllowErrors]: true,
model: "model", model: "model",
modelFamily: "modelFamily", modelFamily: "modelFamily",
smartAvailable: "smartAvailable", smartAvailable: "smartAvailable",
@ -55,7 +58,7 @@ module.exports = function Drive ({ path }) {
serialNumber: "serialNumber", serialNumber: "serialNumber",
wwn: "wwn", wwn: "wwn",
firmwareVersion: "firmwareVersion", firmwareVersion: "firmwareVersion",
size: "size", // size: "size",
rpm: "rpm", rpm: "rpm",
logicalSectorSize: (device) => device.sectorSizes.logical, logicalSectorSize: (device) => device.sectorSizes.logical,
physicalSectorSize: (device) => device.sectorSizes.physical, physicalSectorSize: (device) => device.sectorSizes.physical,
@ -65,7 +68,12 @@ module.exports = function Drive ({ path }) {
}, },
smartctlAttributes: { smartctlAttributes: {
[dlayerSource.ID]: path, [dlayerSource.ID]: path,
[dlayerSource.AllowErrors]: true,
smartFunctioning: (attributes) => {
return (attributes.isOK);
},
smartAttributes: (attributes) => { smartAttributes: (attributes) => {
if (attributes.isOK) {
return attributes.map((attribute) => { return attributes.map((attribute) => {
return { return {
... attribute, ... attribute,
@ -73,8 +81,14 @@ module.exports = function Drive ({ path }) {
updatedWhen: upperSnakeCase(attribute.updatedWhen) updatedWhen: upperSnakeCase(attribute.updatedWhen)
}; };
}); });
} else {
return [];
}
}, },
smartHealth: (attributes) => { smartHealth: (attributes) => {
if (attributes.isOK) {
// FIXME: This is getting values in an inconsistent format? Different for SATA vs. NVMe
console.log("foo", {attributes});
let failed = attributes.filter((item) => { let failed = attributes.filter((item) => {
return (item.failingNow === true || item.failedBefore === true); return (item.failingNow === true || item.failedBefore === true);
}); });
@ -90,6 +104,10 @@ module.exports = function Drive ({ path }) {
} else { } else {
return "HEALTHY"; return "HEALTHY";
} }
} else {
// If we can't get SMART data, the only safe assumption is that it must be failing
return "FAILING";
}
} }
} }
} }

@ -85,34 +85,22 @@ module.exports = function () {
res.redirect("/hardware/storage-devices"); res.redirect("/hardware/storage-devices");
}); });
app.use((err, req, res, next) => { app.use((err, req, res, next) => {
/* GraphQL will wrap any data-resolving errors in its own error type, and that'll break our `showChain` logic below. Note that some GraphQL errors may not *have* an originalError (eg. schema violations), so we account for that as well. */ /* GraphQL will wrap any data-resolving errors in its own error type, and that'll break our `showChain` logic below. Note that some GraphQL errors may not *have* an originalError (eg. schema violations), so we account for that as well. */
let sourceError = (err instanceof graphql.GraphQLError && err.originalError != null) // let sourceError = (err instanceof graphql.GraphQLError && err.originalError != null)
? err.originalError // ? err.originalError
: err; // : err;
console.error(errorChain.render(sourceError));
// FIXME: Render full context instead, according to error-chain?
for (let key of Object.keys(err)) {
console.error(chalk.yellow.bold(`${key}: `) + util.inspect(err[key], { colors: true }));
}
// if (sourceError.showChain != null) { // console.error(errorChain.render(sourceError));
// console.log(sourceError.showChain());
// console.log("#####################");
// console.log(sourceError.getAllContext());
// } else {
// console.log(sourceError.stack);
// // FIXME: Render full context instead, according to error-chain?
// for (let key of Object.keys(err)) {
// console.error(chalk.yellow.bold(`${key}: `) + util.inspect(err[key], { colors: true }));
// } // }
console.log(errorChain.getContext(sourceError));
res.render("error", { res.render("error", {
error: err error: errorChain.render(err) + "\n\n ✂ ----------- \n\nError context: " + util.inspect(errorChain.getContext(err), { colors: true })
}); });
// debugger; // debugger;

@ -14,5 +14,6 @@ module.exports = {
ForbiddenError: errorChain.create("ForbiddenError", { ForbiddenError: errorChain.create("ForbiddenError", {
inheritsFrom: HttpError, inheritsFrom: HttpError,
context: { statusCode: 403 } context: { statusCode: 403 }
}, HttpError), }),
HardwareError: errorChain.create("HardwareError"),
}; };

@ -3,8 +3,10 @@
const Promise = require("bluebird"); const Promise = require("bluebird");
const syncpipe = require("syncpipe"); const syncpipe = require("syncpipe");
const util = require("util"); const util = require("util");
const Result = require("../result");
const ID = Symbol("dlayer-source object ID"); const ID = Symbol("dlayer-source object ID");
const AllowErrors = Symbol("dlayer-source allow-errors marker");
// TODO: Make more readable // TODO: Make more readable
@ -16,18 +18,6 @@ module.exports = {
(_) => Object.entries(_), (_) => Object.entries(_),
(_) => _.flatMap(([ source, properties ]) => { (_) => _.flatMap(([ source, properties ]) => {
return Object.entries(properties).map(([ property, selector ]) => { return Object.entries(properties).map(([ property, selector ]) => {
// This is to support property name shorthand used in place of a selector function
let effectiveSelector = (typeof selector === "string")
? (result) => {
// FIXME: Consider whether to add this check or not; currently, it would break stuff in CVM
// if (selector in result) {
return result[selector];
// } else {
// throw new Error(`Result object does not have a '${selector}' property`);
// }
}
: selector;
let getter = function (_args, context) { let getter = function (_args, context) {
return Promise.try(() => { return Promise.try(() => {
if (properties[ID] != null) { if (properties[ID] != null) {
@ -35,7 +25,7 @@ module.exports = {
if (dataSource != null) { if (dataSource != null) {
// console.log(`Calling source '${source}' with ID ${util.inspect(properties[ID])}`); // console.log(`Calling source '${source}' with ID ${util.inspect(properties[ID])}`);
return dataSource.load(properties[ID]); return Result.wrapAsync(() => dataSource.load(properties[ID]));
} else { } else {
throw new Error(`Attempted to read from source '${source}', but no such source is registered`); throw new Error(`Attempted to read from source '${source}', but no such source is registered`);
} }
@ -46,10 +36,23 @@ module.exports = {
}).then((result) => { }).then((result) => {
// console.log(`Result [${source}|${util.inspect(properties[ID])}] ${util.inspect(result)}`); // console.log(`Result [${source}|${util.inspect(properties[ID])}] ${util.inspect(result)}`);
// TODO: How to deal with null results? Allow them or not? Make it an option? // TODO: How to deal with null results? Allow them or not? Make it an option?
if (result != null) { if (result.isError) {
return effectiveSelector(result); if (properties[AllowErrors] === true) {
// This option is set when a source definition has its own way to deal with (allowable) errors. Instead of simply propagating the error for all affected attributes, it calls the attribute handlers with the Result (or returns `undefined` if only a property is specified).
return (typeof selector === "string")
? undefined
: selector(result);
} else {
// This is equivalent to a `throw`, and so we just propagate it
return result;
}
} else if (result.value() != null) {
// This is to support property name shorthand used in place of a selector function
return (typeof selector === "string")
? result.value()[selector]
: selector(result.value());
} else { } else {
throw new Error(`Null-ish result returned for ID '${properties[ID]}' from source '${source}'; this is not allowed, and there is probably a bug in your code. Please file a ticket if you have a good usecase for null-ish results!`); throw new Error(`Null-ish result returned for ID ${util.inspect(properties[ID])} from source '${source}'; this is not allowed, and there is probably a bug in your code. Please file a ticket if you have a good usecase for null-ish results!`);
} }
}); });
}; };
@ -66,7 +69,8 @@ module.exports = {
... rest ... rest
}; };
}, },
ID: ID ID: ID,
AllowErrors: AllowErrors
}; };

@ -0,0 +1,13 @@
"use strict";
const Promise = require("bluebird");
const dlayer = require("../dlayer");
const Result = require("../result");
module.exports = function dlayerWrap(callback, options = {}) {
return Promise.try(() => {
return Result.unwrapAsync(callback);
}).catch(... options.allowedErrors, (error) => {
return Result.error(dlayer.markAcceptableError(error));
});
};

@ -3,6 +3,8 @@
const Promise = require("bluebird"); const Promise = require("bluebird");
const mapObject = require("map-obj"); const mapObject = require("map-obj");
const Result = require("../result");
// TODO: Bounded/unbounded recursion // TODO: Bounded/unbounded recursion
// TODO: context // TODO: context
// TODO: $required query predicate // TODO: $required query predicate
@ -50,11 +52,11 @@ function isObject(value) {
function mapMaybeArray(value, handler) { function mapMaybeArray(value, handler) {
// NOTE: This is async! // NOTE: This is async!
if (Array.isArray(value)) { if (Array.isArray(value)) {
return Promise.map(value, (item) => { return Promise.map(value, (item, i) => {
if (Array.isArray(item)) { if (Array.isArray(item)) {
throw new Error(`Encountered a nested array, which is not allowed; maybe you forgot to flatten it?`); throw new Error(`Encountered a nested array, which is not allowed; maybe you forgot to flatten it?`);
} else { } else {
return handler(item); return handler(item, i);
} }
}); });
} else { } else {
@ -77,11 +79,12 @@ function asyncMapObject(object, handler) {
function analyzeSubquery(subquery) { function analyzeSubquery(subquery) {
let isRecursive = (subquery?.$recurse === true); let isRecursive = (subquery?.$recurse === true);
let allowErrors = (subquery?.$allowErrors === true);
let hasChildKeys = isObject(subquery) && Object.keys(subquery).some((key) => !specialKeyRegex.test(key)); let hasChildKeys = isObject(subquery) && Object.keys(subquery).some((key) => !specialKeyRegex.test(key));
let isLeaf = (subquery === true || subquery === null || (!hasChildKeys && !isRecursive)); let isLeaf = (subquery === true || subquery === null || (!hasChildKeys && !isRecursive));
let args = subquery?.$arguments ?? {}; let args = subquery?.$arguments ?? {};
return { isRecursive, hasChildKeys, isLeaf, args }; return { isRecursive, allowErrors, hasChildKeys, isLeaf, args };
} }
function analyzeQueryKey(schemaObject, queryObject, queryKey) { function analyzeQueryKey(schemaObject, queryObject, queryKey) {
@ -96,6 +99,14 @@ function analyzeQueryKey(schemaObject, queryObject, queryKey) {
}; };
} }
function assignErrorPath(error, queryPath, schemaPath) {
if (error.path == null) {
// Only assign the path if it hasn't already happened at a deeper level; this is a recursive function after all
error.path = queryPath;
error.message = error.message + ` (${stringifyPath(queryPath, schemaPath)})`;
}
}
function evaluate(schemaObject, queryObject, context, queryPath, schemaPath) { function evaluate(schemaObject, queryObject, context, queryPath, schemaPath) {
// map query object -> result object // map query object -> result object
return asyncMapObject(queryObject, (queryKey, subquery) => { return asyncMapObject(queryObject, (queryKey, subquery) => {
@ -105,38 +116,66 @@ function evaluate(schemaObject, queryObject, context, queryPath, schemaPath) {
// When constructing the result object, we only care about the 'real' keys, not about special meta-keys like $key; those get processed in the actual resolution logic itself. // When constructing the result object, we only care about the 'real' keys, not about special meta-keys like $key; those get processed in the actual resolution logic itself.
return mapObject.mapObjectSkip; return mapObject.mapObjectSkip;
} else { } else {
let { schemaKey, handler, args, isRecursive, isLeaf } = analyzeQueryKey(schemaObject, queryObject, queryKey); let { schemaKey, handler, args, isRecursive, allowErrors, isLeaf } = analyzeQueryKey(schemaObject, queryObject, queryKey);
if (handler != null) { if (handler != null) {
let promise = Promise.try(() => { let promise = Promise.try(() => {
// This calls the data provider in the schema // This calls the data provider in the schema
return maybeCall(handler, [ args, context ], schemaObject); return Result.wrapAsync(() => maybeCall(handler, [ args, context ], schemaObject));
}).then((result) => { }).then((result) => {
if (result.isOK) {
let value = result.value();
let nextQueryPath = queryPath.concat([ queryKey ]); let nextQueryPath = queryPath.concat([ queryKey ]);
let nextSchemaPath = schemaPath.concat([ schemaKey ]); let nextSchemaPath = schemaPath.concat([ schemaKey ]);
return Promise.try(() => { return Promise.try(() => {
if (!isLeaf && result != null) { if (!isLeaf && value != null) {
let effectiveSubquery = (isRecursive) let effectiveSubquery = (isRecursive)
? { ... queryObject, ... subquery } ? { ... queryObject, ... subquery }
: subquery; : subquery;
return mapMaybeArray(result, (item) => { return mapMaybeArray(value, (item, i) => {
if (i != null) {
let elementQueryPath = nextQueryPath.concat([i]);
let elementSchemaPath = nextSchemaPath.concat([i]);
return Promise.try(() => {
return evaluate(item, effectiveSubquery, context, elementQueryPath, elementSchemaPath);
}).tapCatch((error) => {
assignErrorPath(error, elementQueryPath, elementSchemaPath);
});
} else {
return evaluate(item, effectiveSubquery, context, nextQueryPath, nextSchemaPath); return evaluate(item, effectiveSubquery, context, nextQueryPath, nextSchemaPath);
}
}); });
} else { } else {
// null / undefined are returned as-is, so are leaves // null / undefined are returned as-is, so are leaves
return result; return value;
}
}).then((evaluated) => {
if (allowErrors) {
return Result.ok(evaluated);
} else {
return evaluated;
} }
}).catch((error) => { }).tapCatch((error) => {
// FIXME: Chain properly // FIXME: Chain properly
if (error.path == null) { assignErrorPath(error, nextQueryPath, nextSchemaPath);
// Only assign the path if it hasn't already happened at a deeper level; this is a recursive function after all });
error.path = nextQueryPath; } else {
error.message = error.message + ` (${stringifyPath(nextQueryPath, nextSchemaPath)})`; let error = result.error();
if (error.__dlayerAcceptableError === true) {
if (allowErrors === true) {
return Result.error(error.inner);
} else {
throw error.inner;
} }
} else {
throw error; throw error;
}); }
}
}); });
return [ queryKey, promise ]; return [ queryKey, promise ];
@ -191,9 +230,17 @@ module.exports = function createDLayer(options) {
} }
}; };
// FIXME: Currently, top-level errors do not get a path property assigned to them, because that assignment happens on nested calls above
return evaluate(options.schema, query, combinedContext, [], []); return evaluate(options.schema, query, combinedContext, [], []);
} }
}; };
}; };
module.exports.markAcceptableError = function (error) {
return {
__dlayerAcceptableError: true,
inner: error
};
};

@ -0,0 +1,4 @@
TODO:
- $call, for calling non-idempotent functions, requiring a (potentially empty) list of arguments
- $repeat modifier, accepting an array of attributes to repeat the given attribute/function with, the results are an array in the same order - share the top-level properties among all of them
- for named repeats, the user can use the alias feature instead? though no way to share properties in that case

@ -264,12 +264,20 @@ module.exports = function createBinaryInvocation(command, args = []) {
}, },
execute: function () { execute: function () {
return Promise.try(() => { return Promise.try(() => {
let effectiveCommand = command; let { effectiveCommand, subcommands } = Array.isArray(command)
let effectiveArgs = flagsToArgs(this._settings.flags).concat(args); ? { effectiveCommand: command[0], subcommands: command.slice(1) }
: { effectiveCommand: command, subcommands: [] };
// NOTE: subcommands can be specified as part of the command to ensure that they end up *before* any flags, as some tools require this
let effectiveArgs = [
... subcommands,
... flagsToArgs(this._settings.flags),
... args
];
if (this._settings.asRoot) { if (this._settings.asRoot) {
effectiveArgs = [ effectiveCommand ].concat(effectiveArgs);
effectiveCommand = "sudo"; effectiveCommand = "sudo";
effectiveArgs = [command].concat(effectiveArgs);
} }
// FIXME: Shouldn't we represent this in its original form, or at least an escaped form? And suffix 'Unsafe' to ensure it's not used in any actual execution code. // FIXME: Shouldn't we represent this in its original form, or at least an escaped form? And suffix 'Unsafe' to ensure it's not used in any actual execution code.
@ -348,7 +356,7 @@ module.exports = function createBinaryInvocation(command, args = []) {
stderr: stderr stderr: stderr
}); });
} }
}).catch(rethrowAs(errors.CommandExecutionFailed, `An error occurred while executing '${command}'`, { }).catch(rethrowAs(errors.CommandExecutionFailed, `An error occurred while executing ${util.inspect(command)}`, {
command: effectiveCompleteCommand command: effectiveCompleteCommand
})); }));
}); });

@ -3,7 +3,8 @@
const Promise = require("bluebird"); const Promise = require("bluebird");
const matchValue = require("match-value"); const matchValue = require("match-value");
const execBinary = require("../exec-binary"); const execBinary = require("../exec-binary");
const parseIECBytes = require("../parse-bytes-iec"); // const parseIECBytes = require("../parse-bytes-iec");
const { B } = require("../unit-bytes-iec");
const createJSONParser = require("../text-parser-json"); const createJSONParser = require("../text-parser-json");
function parseBoolean(value) { function parseBoolean(value) {
@ -49,7 +50,7 @@ function mapDeviceList(devices) {
deviceNumber: device["maj:min"], deviceNumber: device["maj:min"],
removable: parseBoolean(device.rm), removable: parseBoolean(device.rm),
readOnly: parseBoolean(device.ro), readOnly: parseBoolean(device.ro),
size: parseIECBytes(device.size), size: B(device.size),
children: (device.children != null) ? mapDeviceList(device.children) : [] children: (device.children != null) ? mapDeviceList(device.children) : []
}; };
}); });
@ -58,7 +59,7 @@ function mapDeviceList(devices) {
module.exports = function lsblk() { module.exports = function lsblk() {
return Promise.try(() => { return Promise.try(() => {
return execBinary("lsblk") return execBinary("lsblk")
.withFlags({ json: true, "output-all": true }) .withFlags({ json: true, "output-all": true, bytes: true })
.requireOnStdout(createJSONParser()) .requireOnStdout(createJSONParser())
.execute(); .execute();
}).then((output) => { }).then((output) => {

@ -0,0 +1,164 @@
"use strict";
const Promise = require("bluebird");
const execBinary = require("../exec-binary");
const { B } = require("../unit-bytes-iec");
const createJSONParser = require("../text-parser-json");
const thirdFourthByteMask = parseInt("11111111111111110000000000000000", 2);
const secondByteMask = parseInt("00000000000000001111111100000000", 2);
const firstByteMask = parseInt("00000000000000000000000011111111", 2);
const fieldMapping = {
vid: "vendorID",
ssvi: "subsystemVendorID",
sn: { name: "serialNumber", transform: (string) => string.trim() },
mn: { name: "modelNumber", transform: (string) => string.trim() },
fr: "firmwareRevision",
rab: { name: "recommendedArbitrationBurst", transform: (value) => 2 ** value }, // FIXME: Is this correct?
ieee: "ouiIdentifier",
cmic: { name: "cmicCapabilities", transform: (bitField) => {
return {
anar: bitField & 8,
virtualFunction: bitField & 4,
multipleControllers: bitField & 2,
multipleSubsystemPorts: bitField & 1
};
}},
mdts: { name: "maximumDataTransferSize", transform: (value) => {
if (value === 0) {
return null;
} else {
// Note: counted in multiples of the minimum memory page size
// TODO: Can we grab this from somewhere and return the value in bytes instead?
return 2 ** value;
}
}},
cntlid: "controllerID",
ver: { name: "protocolVersion", transform: (value) => {
return {
major: value & thirdFourthByteMask,
minor: value & secondByteMask,
tertiary: value & firstByteMask
};
}},
// MARKER
rtd3r: { name: "rtd3ResumeLatency", transform: (value) => {
if (value === 0) {
return null;
} else {
// TODO: Unit?
return value;
}
}},
rtd3e: { name: "rtd3EntryLatency", transform: (value) => {
if (value === 0) {
return null;
} else {
// TODO: Unit?
return value;
}
}},
// TODO:
// oaes,
// ctratt,
// rrls,
// crdt1,
// crdt2,
// crdt3,
// nvmsr,
// vwci,
// mec,
// oacs,
// acl,
// aerl,
// frmw,
// lpa,
// elpe,
// npss,
// avscc,
// apsta,
// wctemp,
// cctemp,
// mtfa,
// hmpre,
// hmmin,
tnvmcap: { name: "totalSpace", transformer: (value) => B(value) },
unvmcap: { name: "freeSpace", transformer: (value) => B(value) },
// TOOD:
// rpmbs,
// edstt,
// dsto,
// fwug,
// kas,
// hctma,
// mntmt,
// mxtmt,
// sanicap,
// hmminds,
// hmmaxd,
// nsetidmax,
// anatt,
// anacap,
// anagrpmax,
// nanagrpid,
// domainid,
// megcap,
// sqes,
// cqes,
// maxcmd,
// nn,
// oncs,
// fuses,
// fna,
// vwc,
// awun,
// awupf,
// icsvscc,
// nwpc,
// acwu,
// ocfs,
// sgls,
// maxdna,
// maxcna,
// ioccsz,
// iorcsz,
// icdoff,
// fcatt,
// msdbd,
// ofcs,
// psds
};
module.exports = {
identifyController: function (path) {
return Promise.try(() => {
return execBinary([ "nvme", "id-ctrl" ], [ path ])
.asRoot()
.withFlags({ "output-format": "json" })
.requireOnStderr(createJSONParser())
.execute();
}).then((output) => {
let result = {};
for (let key of Object.keys(output)) {
let mapping = fieldMapping[key];
if (mapping != null) {
let { name, transform } = (typeof mapping === "string")
? { name: mapping, transform: (value) => value }
: { name: mapping.name, transform: mapping.transform };
result[name] = transform(output[key]);
}
}
// TODO: Warn on unrecognized keys
return result;
}).catch((error) => {
console.dir(error);
});
}
};
// module.exports.identifyController("/dev/nvme0").then((result) => console.dir(result, { depth: null }))

@ -2,11 +2,16 @@
const Promise = require("bluebird"); const Promise = require("bluebird");
const path = require("path"); const path = require("path");
const errorChain = require("error-chain");
const createPegParser = require("../text-parser-pegjs"); const createPegParser = require("../text-parser-pegjs");
const execBinary = require("../exec-binary"); const execBinary = require("../exec-binary");
const itemsToObject = require("../items-to-object"); const itemsToObject = require("../items-to-object");
/* FIXME: Error handling, eg. device not found errors */ /* FIXME: Error handling, eg. device not found errors */
// TODO: Handle this case: "Read NVMe Identify Controller failed: scsi error medium or hardware error (serious)" in a more specific manner
let AttributesError = errorChain.create("AttributesError", {});
let InfoError = errorChain.create("InfoError", {});
function outputParser(parserPath) { function outputParser(parserPath) {
return createPegParser({ return createPegParser({
@ -19,6 +24,8 @@ let infoParser = outputParser("./parsers/commands/info.pegjs");
let scanParser = outputParser("./parsers/commands/scan.pegjs"); let scanParser = outputParser("./parsers/commands/scan.pegjs");
module.exports = { module.exports = {
AttributesError: AttributesError,
InfoError: InfoError,
attributes: function ({ devicePath }) { attributes: function ({ devicePath }) {
return Promise.try(() => { return Promise.try(() => {
return attributesParser; return attributesParser;
@ -26,11 +33,18 @@ module.exports = {
return execBinary("smartctl", [devicePath]) return execBinary("smartctl", [devicePath])
.asRoot() .asRoot()
.withFlags({ attributes: true }) .withFlags({ attributes: true })
.withAllowedExitCodes([ 0, 2 ])
.requireOnStdout(parser) .requireOnStdout(parser)
.execute(); .execute();
}).then((output) => { }).then((output) => {
let { error, attributes } = output.result;
if (error != null) {
throw new AttributesError(`smartctl returned an error: ${error}`);
} else {
// NOTE: Ignore the header, for now // NOTE: Ignore the header, for now
return output.result.attributes; return attributes;
}
}); });
}, },
info: function ({ devicePath }) { info: function ({ devicePath }) {
@ -40,11 +54,18 @@ module.exports = {
return execBinary("smartctl", [devicePath]) return execBinary("smartctl", [devicePath])
.asRoot() .asRoot()
.withFlags({ info: true }) .withFlags({ info: true })
.withAllowedExitCodes([ 0, 2 ])
.requireOnStdout(parser) .requireOnStdout(parser)
.execute(); .execute();
}).then((output) => { }).then((output) => {
let { error, fields } = output.result;
if (error != null) {
throw new InfoError(`smartctl returned an error: ${error}`);
} else {
// NOTE: Ignore the header, for now // NOTE: Ignore the header, for now
return itemsToObject(output.result.fields); return itemsToObject(fields);
}
}); });
}, },
scan: function () { scan: function () {

@ -5,7 +5,7 @@ import { SameLine as _ } from "../../../peg-whitespace"
import { RestOfLine } from "../../../peg-rest-of-line" import { RestOfLine } from "../../../peg-rest-of-line"
import { IdentifierValue } from "../primitives" import { IdentifierValue } from "../primitives"
import { Header } from "../shared" import { Header, Error } from "../shared"
{ {
const matchValue = require("match-value"); const matchValue = require("match-value");
@ -13,13 +13,14 @@ import { Header } from "../shared"
} }
RootAttributes RootAttributes
= header:Header attributesSection:AttributesSection Newline* { = header:Header attributesSection:(AttributesSection / Error) Newline* {
return { ...header, attributes: attributesSection } return { ...header, ... attributesSection }
}; };
AttributesSection AttributesSection
= AttributesSectionSATA = attributes:AttributesSectionSATA { return { attributes: attributes }; }
/ AttributesSectionNVMe / attributes:AttributesSectionNVMe { return { attributes: attributes }; }
/ error:Error { return { error: error }; }
AttributesSectionSATA AttributesSectionSATA
= "=== START OF READ SMART DATA SECTION ===" Newline = "=== START OF READ SMART DATA SECTION ===" Newline

@ -5,7 +5,7 @@ import { SameLine as _ } from "../../../peg-whitespace"
import { RestOfLine } from "../../../peg-rest-of-line" import { RestOfLine } from "../../../peg-rest-of-line"
import { BytesValue } from "../primitives" import { BytesValue } from "../primitives"
import { Header } from "../shared" import { Header, Error } from "../shared"
{ {
const matchValue = require("match-value"); const matchValue = require("match-value");
@ -13,10 +13,14 @@ import { Header } from "../shared"
RootInfo RootInfo
= header:Header infoSection:InfoSection Newline* { = header:Header infoSection:InfoSection Newline* {
return { ...header, fields: infoSection } return { ... header, ... infoSection }
}; };
InfoSection 'information section' InfoSection
= fields:InfoSectionSuccess { return { fields: fields }; }
/ error:Error { return { error: error }; }
InfoSectionSuccess 'information section'
= "=== START OF INFORMATION SECTION ===" Newline fields:(InfoField+) { = "=== START OF INFORMATION SECTION ===" Newline fields:(InfoField+) {
return fields.filter((field) => field != null); return fields.filter((field) => field != null);
} }

@ -5,3 +5,6 @@ Header 'header'
= "smartctl " versionString:RestOfLine "Copyright" copyrightStatement:RestOfLine Newline { = "smartctl " versionString:RestOfLine "Copyright" copyrightStatement:RestOfLine Newline {
return { versionString, copyrightStatement }; return { versionString, copyrightStatement };
} }
Error
= $ "Read NVMe Identify Controller failed: scsi error medium or hardware error (serious)"

@ -346,6 +346,10 @@ let mountOptionMap = {
// https://www.kernel.org/doc/Documentation/filesystems/sysfs.txt // https://www.kernel.org/doc/Documentation/filesystems/sysfs.txt
/* This pseudo-filesystem does not appear to have any specific mount options. */ /* This pseudo-filesystem does not appear to have any specific mount options. */
}, },
configfs: {
// https://www.kernel.org/doc/Documentation/filesystems/configfs/configfs.txt
/* This pseudo-filesystem does not appear to have any specific mount options. */
},
securityfs: { securityfs: {
// https://lwn.net/Articles/153366/ // https://lwn.net/Articles/153366/
/* This pseudo-filesystem does not appear to have any specific mount options. */ /* This pseudo-filesystem does not appear to have any specific mount options. */

@ -0,0 +1,136 @@
"use strict";
const assert = require("assert");
function createResultObject(isSuccessful, containedValue) {
return {
__isResultType: true,
isOK: isSuccessful,
isError: !isSuccessful,
error: function () {
if (!isSuccessful) {
return containedValue;
} else {
// FIXME: Clearer error message, definitely a bug!
throw new Error(`The Result is in a success state`);
}
},
value: function () {
// MARKER; either return value or throw the error it contains, to emulate standard throw behaviour
if (isSuccessful) {
return containedValue;
} else {
throw containedValue;
}
},
valueOr: function (defaultValue) {
if (isSuccessful) {
return containedValue;
} else {
return defaultValue;
}
},
mapTo: function ({ ok, error }) {
let okMapper = ok ?? ((value) => Result.ok(value));
let errorMapper = error ?? ((error) => Result.error(error));
if (this.isOK) {
let mapped = okMapper(containedValue);
return (Result.isResult(mapped))
? mapped
: Result.ok(mapped);
} else {
let mapped = errorMapper(containedValue);
return (Result.isResult(mapped))
? mapped
: Result.error(mapped);
}
},
// valueOr: function (errorCode, errorMessage) {
// if (isSuccessful) {
// return containedValue;
// } else {
// // FIXME: Integrate with error-chain somehow?
// let error = new Error(errorMessage);
// error.code = errorCode;
// throw error;
// }
// },
// FIXME: Chaining, Promise chain integration?
// FIXME: Serialization
// FIXME: Chaining with error filtering
};
}
let Result = module.exports = {
isResult: function (value) {
return (value != null && value.__isResultType === true);
},
ok: function (value) {
// Emulate what Promises do on `resolve(...)`
if (this.isResult(value)) {
return value;
} else {
return createResultObject(true, value);
}
},
error: function (error) {
return createResultObject(false, error);
},
wrap: function (callback) {
// Always returns a Result
try {
let result = callback();
return this.ok(result);
} catch (error) {
return this.error(error);
}
},
wrapAsync: function (callback) {
// Always returns a Promise that resolves to a Result
return new Promise((resolve, _reject) => {
resolve(callback());
}).then((result) => {
return this.ok(result);
}).catch((error) => {
return this.error(error);
});
},
// The below methods are used when it's unknown whether something will produce a Result or just return/throw
unwrapValue: function (value) {
if (Result.isResult(value)) {
return value.unwrap();
} else {
return value;
}
},
unwrap: function (callback) {
return Result.unwrapValue(callback());
},
unwrapAsync: function (callback) {
return new Promise((resolve, _reject) => {
resolve(callback());
}).then((result) => {
return Result.unwrapValue(result);
});
}
};
/* IDEA:
result.mapTo({
ok: (value) => value * 2,
error: (error) => chain(error, ErrorType, "Foo Bar")
})
result.mapTo({
// can return either a result or any value
ok: (value) => value * 2,
// can return either a result or an Error
error: (_error) => result.ok(0)
})
*/

@ -1,7 +1,7 @@
"use strict"; "use strict";
const { chain } = require("error-chain"); const { chain } = require("error-chain");
const ParseError = require("../text-parser"); const { ParseError } = require("../text-parser");
module.exports = function createJsonParser(resultMapper) { module.exports = function createJsonParser(resultMapper) {
return { return {

@ -171,6 +171,10 @@ table.drives {
.stacktrace { .stacktrace {
white-space: pre-wrap; white-space: pre-wrap;
font-family: monospace; font-family: monospace;
background-color: rgb(12, 12, 12);
border: 1px solid black;
padding: .8em;
max-width: 1200px;
.irrelevant { .irrelevant {
color: gray; color: gray;

@ -2,12 +2,19 @@
const React = require("react"); const React = require("react");
const entities = require("entities"); const entities = require("entities");
const ansiToHtml = require("ansi-to-html");
const Layout = require("./layout"); const Layout = require("./layout");
let converter = new ansiToHtml({
colors: {
1: "#F33"
}
});
module.exports = { module.exports = {
template: function ErrorPage({ error }) { template: function ErrorPage({ error }) {
let escapedStack = entities.escape(error.stack); let escapedStack = converter.toHtml(entities.escape(error));
let formattedStack = escapedStack let formattedStack = escapedStack
.split("\n") .split("\n")

@ -1392,6 +1392,13 @@ ansi-styles@^4.1.0:
dependencies: dependencies:
color-convert "^2.0.1" color-convert "^2.0.1"
ansi-to-html@^0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.7.2.tgz#a92c149e4184b571eb29a0135ca001a8e2d710cb"
integrity sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==
dependencies:
entities "^2.2.0"
anymatch@^2.0.0: anymatch@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
@ -1400,6 +1407,14 @@ anymatch@^2.0.0:
micromatch "^3.1.4" micromatch "^3.1.4"
normalize-path "^2.1.1" normalize-path "^2.1.1"
anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
aproba@^1.0.3: aproba@^1.0.3:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
@ -1686,6 +1701,11 @@ binary-extensions@^1.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
bindings@^1.5.0: bindings@^1.5.0:
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
@ -1780,6 +1800,13 @@ braces@^2.3.1, braces@^2.3.2:
split-string "^3.0.2" split-string "^3.0.2"
to-regex "^3.0.1" to-regex "^3.0.1"
braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
brorand@^1.0.1, brorand@^1.1.0: brorand@^1.0.1, brorand@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
@ -2141,6 +2168,21 @@ charenc@0.0.2:
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
"chokidar@>=3.0.0 <4.0.0":
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
chokidar@^2.0.4, chokidar@^2.1.1, chokidar@^2.1.8: chokidar@^2.0.4, chokidar@^2.1.1, chokidar@^2.1.8:
version "2.1.8" version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@ -2989,7 +3031,7 @@ engine.io@~3.5.0:
engine.io-parser "~2.2.0" engine.io-parser "~2.2.0"
ws "~7.4.2" ws "~7.4.2"
entities@^2.0.0: entities@^2.0.0, entities@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
@ -3556,6 +3598,13 @@ fill-range@^4.0.0:
repeat-string "^1.6.1" repeat-string "^1.6.1"
to-regex-range "^2.1.0" to-regex-range "^2.1.0"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
dependencies:
to-regex-range "^5.0.1"
finalhandler@~1.1.2: finalhandler@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
@ -3738,6 +3787,11 @@ fsevents@^1.2.7:
bindings "^1.5.0" bindings "^1.5.0"
nan "^2.12.1" nan "^2.12.1"
fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
function-bind@^1.1.1: function-bind@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@ -3858,6 +3912,13 @@ glob-parent@^5.0.0:
dependencies: dependencies:
is-glob "^4.0.1" is-glob "^4.0.1"
glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
glob@^7.0.0, glob@^7.1.0, glob@^7.1.3: glob@^7.0.0, glob@^7.1.0, glob@^7.1.3:
version "7.1.6" version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
@ -4151,6 +4212,11 @@ ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
immutable@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
import-fresh@^3.0.0: import-fresh@^3.0.0:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@ -4354,6 +4420,13 @@ is-binary-path@^1.0.0:
dependencies: dependencies:
binary-extensions "^1.0.0" binary-extensions "^1.0.0"
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-boolean-object@^1.0.1: is-boolean-object@^1.0.1:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0"
@ -4475,6 +4548,13 @@ is-glob@^4.0.0, is-glob@^4.0.1:
dependencies: dependencies:
is-extglob "^2.1.1" is-extglob "^2.1.1"
is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-installed-globally@^0.1.0: is-installed-globally@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80"
@ -4522,6 +4602,11 @@ is-number@^3.0.0:
dependencies: dependencies:
kind-of "^3.0.2" kind-of "^3.0.2"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-obj@^1.0.0: is-obj@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
@ -5408,7 +5493,7 @@ normalize-path@^2.1.1:
dependencies: dependencies:
remove-trailing-separator "^1.0.1" remove-trailing-separator "^1.0.1"
normalize-path@^3.0.0: normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
@ -6003,6 +6088,11 @@ pgpass@1.x:
dependencies: dependencies:
split2 "^3.1.1" split2 "^3.1.1"
picomatch@^2.0.4, picomatch@^2.2.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
pify@^2.0.0: pify@^2.0.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@ -6374,6 +6464,13 @@ readdirp@^2.2.1:
micromatch "^3.1.10" micromatch "^3.1.10"
readable-stream "^2.0.2" readable-stream "^2.0.2"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
rechoir@^0.6.2: rechoir@^0.6.2:
version "0.6.2" version "0.6.2"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
@ -6617,6 +6714,15 @@ safe-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sass@^1.50.0:
version "1.50.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.50.0.tgz#3e407e2ebc53b12f1e35ce45efb226ea6063c7c8"
integrity sha512-cLsD6MEZ5URXHStxApajEh7gW189kkjn4Rc8DQweMyF+o5HF5nfEz8QYLMlPsTOD88DknatTmBWkOcw5/LnJLQ==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
sax@^1.2.4: sax@^1.2.4:
version "1.2.4" version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
@ -6898,6 +7004,11 @@ socket.io@^2.0.4:
socket.io-client "2.4.0" socket.io-client "2.4.0"
socket.io-parser "~3.4.0" socket.io-parser "~3.4.0"
"source-map-js@>=0.6.2 <2.0.0":
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
source-map-resolve@^0.5.0: source-map-resolve@^0.5.0:
version "0.5.3" version "0.5.3"
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
@ -7437,6 +7548,13 @@ to-regex-range@^2.1.0:
is-number "^3.0.0" is-number "^3.0.0"
repeat-string "^1.6.1" repeat-string "^1.6.1"
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
to-regex@^3.0.1, to-regex@^3.0.2: to-regex@^3.0.1, to-regex@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"

Loading…
Cancel
Save