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

@ -4,7 +4,8 @@
"description": "A VPS management panel",
"main": "index.js",
"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": {
"type": "git",
@ -36,6 +37,7 @@
"@validatem/required": "^0.1.1",
"@validatem/when": "^0.1.0",
"JSONStream": "^1.1.4",
"ansi-to-html": "^0.7.2",
"argon2": "^0.27.0",
"array.prototype.flat": "^1.2.1",
"as-expression": "^1.0.0",
@ -106,6 +108,7 @@
"nodemon": "^1.18.11",
"npm-check-licenses": "^1.0.5",
"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 {
background-color: #e4e4e4;
background-color: rgb(228, 228, 228);
margin: 0px;
font-family: sans-serif; }
font-family: sans-serif;
}
.content {
padding: 8px; }
padding: 8px;
}
label {
margin-right: 12px; }
margin-right: 12px;
}
.menu {
background-color: #000424; }
.menu h1, .menu .menuItem {
display: inline-block;
color: white; }
.menu h1 {
margin: 0px 16px; }
.menu .menuItem a {
color: white;
text-decoration: none;
padding: 15px 9px 5px 9px; }
.menu .menuItem.active a {
/* FIXME: Make this lighter when there is no submenu, to match the page background color */
background-color: #dddddd;
color: black; }
.menu .menuItem:not(.active) a:hover {
background-color: #afafaf;
color: black; }
background-color: rgb(0, 4, 36);
}
.menu h1, .menu .menuItem {
display: inline-block;
color: white;
}
.menu h1 {
margin: 0px 16px;
}
.menu .menuItem a {
color: white;
text-decoration: none;
padding: 15px 9px 5px 9px;
}
.menu .menuItem.active a {
/* FIXME: Make this lighter when there is no submenu, to match the page background color */
background-color: rgb(221, 221, 221);
color: black;
}
.menu .menuItem:not(.active) a:hover {
background-color: rgb(175, 175, 175);
color: black;
}
.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 {
height: 16px; }
height: 16px;
}
.submenu {
padding: .3em .2em 0 .2em;
border-bottom: 1px solid #000424; }
.submenu .menuItem {
display: inline-block;
margin-bottom: -1px;
padding: .3em .7em;
font-size: .95em; }
.submenu .menuItem a {
text-decoration: none;
color: black; }
.submenu .menuItem.active {
background-color: #e4e4e4;
border: 1px solid #000424;
border-bottom: none; }
padding: 0.3em 0.2em 0 0.2em;
border-bottom: 1px solid rgb(0, 4, 36);
}
.submenu .menuItem {
display: inline-block;
margin-bottom: -1px;
padding: 0.3em 0.7em;
font-size: 0.95em;
}
.submenu .menuItem a {
text-decoration: none;
color: black;
}
.submenu .menuItem.active {
background-color: rgb(228, 228, 228);
border: 1px solid rgb(0, 4, 36);
border-bottom: none;
}
table {
border-collapse: collapse; }
table th, table td {
padding: 6px 9px;
border: 1px solid black; }
table th {
text-align: left; }
table td.hidden {
border: none; }
border-collapse: collapse;
}
table th, table td {
padding: 6px 9px;
border: 1px solid black;
}
table th {
text-align: left;
}
table td.hidden {
border: none;
}
table.drives td {
vertical-align: top; }
vertical-align: top;
}
table.drives td.smart.HEALTHY {
background-color: #00a500; }
background-color: rgb(0, 165, 0);
}
table.drives td.smart.DETERIORATING {
background-color: #ff9100; }
background-color: rgb(255, 145, 0);
}
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) {
border-bottom-color: transparent; }
border-bottom-color: transparent;
}
table.drives .partition {
font-style: italic;
font-size: .8em; }
table.drives .partition td {
padding: 4px 9px; }
table.drives .partition .notMounted {
color: gray; }
font-size: 0.8em;
}
table.drives .partition td {
padding: 4px 9px;
}
table.drives .partition .notMounted {
color: gray;
}
table.drives tr.smartStatus {
font-size: .85em; }
table.drives tr.smartStatus td {
padding: 4px 9px; }
font-size: 0.85em;
}
table.drives tr.smartStatus td {
padding: 4px 9px;
}
table.drives th.healthy {
color: #006100; }
color: rgb(0, 97, 0);
}
table.drives th.atRisk {
color: #7c4600; }
color: rgb(124, 70, 0);
}
table.drives th.failing {
color: #c20000; }
color: rgb(194, 0, 0);
}
.stacktrace {
white-space: pre-wrap;
font-family: monospace; }
.stacktrace .irrelevant {
color: gray; }
font-family: monospace;
background-color: rgb(12, 12, 12);
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 smartctl = require("../../../packages/exec-smartctl");
const dlayerWrap = require("../../../packages/dlayer-wrap");
module.exports = function () {
return function (paths) {
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";
const Promise = require("bluebird");
const dlayerWrap = require("../../../packages/dlayer-wrap");
const smartctl = require("../../../packages/exec-smartctl");
module.exports = function () {
return function (paths) {
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: {
// lsblk: {
// [dlayerSource.ID]: { path },
// },
lsblk: {
[dlayerSource.ID]: { path },
size: "size"
},
smartctlScan: {
[dlayerSource.ID]: path,
interface: "interface"
},
smartctlInfo: {
[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",
modelFamily: "modelFamily",
smartAvailable: "smartAvailable",
@ -55,7 +58,7 @@ module.exports = function Drive ({ path }) {
serialNumber: "serialNumber",
wwn: "wwn",
firmwareVersion: "firmwareVersion",
size: "size",
// size: "size",
rpm: "rpm",
logicalSectorSize: (device) => device.sectorSizes.logical,
physicalSectorSize: (device) => device.sectorSizes.physical,
@ -65,30 +68,45 @@ module.exports = function Drive ({ path }) {
},
smartctlAttributes: {
[dlayerSource.ID]: path,
[dlayerSource.AllowErrors]: true,
smartFunctioning: (attributes) => {
return (attributes.isOK);
},
smartAttributes: (attributes) => {
return attributes.map((attribute) => {
return {
... attribute,
type: upperSnakeCase(attribute.type),
updatedWhen: upperSnakeCase(attribute.updatedWhen)
};
});
if (attributes.isOK) {
return attributes.map((attribute) => {
return {
... attribute,
type: upperSnakeCase(attribute.type),
updatedWhen: upperSnakeCase(attribute.updatedWhen)
};
});
} else {
return [];
}
},
smartHealth: (attributes) => {
let failed = attributes.filter((item) => {
return (item.failingNow === true || item.failedBefore === true);
});
let deteriorating = attributes.filter((item) => {
return (item.type === "preFail" && item.worstValueSeen < 100);
});
if (failed.length > 0) {
return "FAILING";
} else if (deteriorating.length > 0) {
return "DETERIORATING";
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) => {
return (item.failingNow === true || item.failedBefore === true);
});
let deteriorating = attributes.filter((item) => {
return (item.type === "preFail" && item.worstValueSeen < 100);
});
if (failed.length > 0) {
return "FAILING";
} else if (deteriorating.length > 0) {
return "DETERIORATING";
} else {
return "HEALTHY";
}
} else {
return "HEALTHY";
// 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");
});
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. */
let sourceError = (err instanceof graphql.GraphQLError && err.originalError != null)
? err.originalError
: 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 }));
}
// let sourceError = (err instanceof graphql.GraphQLError && err.originalError != null)
// ? err.originalError
// : err;
// if (sourceError.showChain != null) {
// console.log(sourceError.showChain());
// console.log("#####################");
// console.log(sourceError.getAllContext());
// } else {
// console.log(sourceError.stack);
// 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 }));
// }
console.log(errorChain.getContext(sourceError));
res.render("error", {
error: err
error: errorChain.render(err) + "\n\n ✂ ----------- \n\nError context: " + util.inspect(errorChain.getContext(err), { colors: true })
});
// debugger;

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

@ -3,8 +3,10 @@
const Promise = require("bluebird");
const syncpipe = require("syncpipe");
const util = require("util");
const Result = require("../result");
const ID = Symbol("dlayer-source object ID");
const AllowErrors = Symbol("dlayer-source allow-errors marker");
// TODO: Make more readable
@ -16,18 +18,6 @@ module.exports = {
(_) => Object.entries(_),
(_) => _.flatMap(([ source, properties ]) => {
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) {
return Promise.try(() => {
if (properties[ID] != null) {
@ -35,7 +25,7 @@ module.exports = {
if (dataSource != null) {
// console.log(`Calling source '${source}' with ID ${util.inspect(properties[ID])}`);
return dataSource.load(properties[ID]);
return Result.wrapAsync(() => dataSource.load(properties[ID]));
} else {
throw new Error(`Attempted to read from source '${source}', but no such source is registered`);
}
@ -46,10 +36,23 @@ module.exports = {
}).then((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?
if (result != null) {
return effectiveSelector(result);
if (result.isError) {
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 {
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
};
},
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 mapObject = require("map-obj");
const Result = require("../result");
// TODO: Bounded/unbounded recursion
// TODO: context
// TODO: $required query predicate
@ -50,11 +52,11 @@ function isObject(value) {
function mapMaybeArray(value, handler) {
// NOTE: This is async!
if (Array.isArray(value)) {
return Promise.map(value, (item) => {
return Promise.map(value, (item, i) => {
if (Array.isArray(item)) {
throw new Error(`Encountered a nested array, which is not allowed; maybe you forgot to flatten it?`);
} else {
return handler(item);
return handler(item, i);
}
});
} else {
@ -77,11 +79,12 @@ function asyncMapObject(object, handler) {
function analyzeSubquery(subquery) {
let isRecursive = (subquery?.$recurse === true);
let allowErrors = (subquery?.$allowErrors === true);
let hasChildKeys = isObject(subquery) && Object.keys(subquery).some((key) => !specialKeyRegex.test(key));
let isLeaf = (subquery === true || subquery === null || (!hasChildKeys && !isRecursive));
let args = subquery?.$arguments ?? {};
return { isRecursive, hasChildKeys, isLeaf, args };
return { isRecursive, allowErrors, hasChildKeys, isLeaf, args };
}
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) {
// map query object -> result object
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.
return mapObject.mapObjectSkip;
} 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) {
let promise = Promise.try(() => {
// This calls the data provider in the schema
return maybeCall(handler, [ args, context ], schemaObject);
return Result.wrapAsync(() => maybeCall(handler, [ args, context ], schemaObject));
}).then((result) => {
let nextQueryPath = queryPath.concat([ queryKey ]);
let nextSchemaPath = schemaPath.concat([ schemaKey ]);
return Promise.try(() => {
if (!isLeaf && result != null) {
let effectiveSubquery = (isRecursive)
? { ... queryObject, ... subquery }
: subquery;
return mapMaybeArray(result, (item) => {
return evaluate(item, effectiveSubquery, context, nextQueryPath, nextSchemaPath);
});
if (result.isOK) {
let value = result.value();
let nextQueryPath = queryPath.concat([ queryKey ]);
let nextSchemaPath = schemaPath.concat([ schemaKey ]);
return Promise.try(() => {
if (!isLeaf && value != null) {
let effectiveSubquery = (isRecursive)
? { ... queryObject, ... subquery }
: subquery;
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);
}
});
} else {
// null / undefined are returned as-is, so are leaves
return value;
}
}).then((evaluated) => {
if (allowErrors) {
return Result.ok(evaluated);
} else {
return evaluated;
}
}).tapCatch((error) => {
// FIXME: Chain properly
assignErrorPath(error, nextQueryPath, nextSchemaPath);
});
} else {
let error = result.error();
if (error.__dlayerAcceptableError === true) {
if (allowErrors === true) {
return Result.error(error.inner);
} else {
throw error.inner;
}
} else {
// null / undefined are returned as-is, so are leaves
return result;
}
}).catch((error) => {
// FIXME: Chain properly
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 = nextQueryPath;
error.message = error.message + ` (${stringifyPath(nextQueryPath, nextSchemaPath)})`;
throw error;
}
throw error;
});
}
});
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, [], []);
}
};
};
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 () {
return Promise.try(() => {
let effectiveCommand = command;
let effectiveArgs = flagsToArgs(this._settings.flags).concat(args);
let { effectiveCommand, subcommands } = Array.isArray(command)
? { 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) {
effectiveArgs = [ effectiveCommand ].concat(effectiveArgs);
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.
@ -348,7 +356,7 @@ module.exports = function createBinaryInvocation(command, args = []) {
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
}));
});

@ -3,7 +3,8 @@
const Promise = require("bluebird");
const matchValue = require("match-value");
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");
function parseBoolean(value) {
@ -49,7 +50,7 @@ function mapDeviceList(devices) {
deviceNumber: device["maj:min"],
removable: parseBoolean(device.rm),
readOnly: parseBoolean(device.ro),
size: parseIECBytes(device.size),
size: B(device.size),
children: (device.children != null) ? mapDeviceList(device.children) : []
};
});
@ -58,7 +59,7 @@ function mapDeviceList(devices) {
module.exports = function lsblk() {
return Promise.try(() => {
return execBinary("lsblk")
.withFlags({ json: true, "output-all": true })
.withFlags({ json: true, "output-all": true, bytes: true })
.requireOnStdout(createJSONParser())
.execute();
}).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 path = require("path");
const errorChain = require("error-chain");
const createPegParser = require("../text-parser-pegjs");
const execBinary = require("../exec-binary");
const itemsToObject = require("../items-to-object");
/* 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) {
return createPegParser({
@ -19,6 +24,8 @@ let infoParser = outputParser("./parsers/commands/info.pegjs");
let scanParser = outputParser("./parsers/commands/scan.pegjs");
module.exports = {
AttributesError: AttributesError,
InfoError: InfoError,
attributes: function ({ devicePath }) {
return Promise.try(() => {
return attributesParser;
@ -26,11 +33,18 @@ module.exports = {
return execBinary("smartctl", [devicePath])
.asRoot()
.withFlags({ attributes: true })
.withAllowedExitCodes([ 0, 2 ])
.requireOnStdout(parser)
.execute();
}).then((output) => {
// NOTE: Ignore the header, for now
return output.result.attributes;
let { error, attributes } = output.result;
if (error != null) {
throw new AttributesError(`smartctl returned an error: ${error}`);
} else {
// NOTE: Ignore the header, for now
return attributes;
}
});
},
info: function ({ devicePath }) {
@ -40,11 +54,18 @@ module.exports = {
return execBinary("smartctl", [devicePath])
.asRoot()
.withFlags({ info: true })
.withAllowedExitCodes([ 0, 2 ])
.requireOnStdout(parser)
.execute();
}).then((output) => {
// NOTE: Ignore the header, for now
return itemsToObject(output.result.fields);
let { error, fields } = output.result;
if (error != null) {
throw new InfoError(`smartctl returned an error: ${error}`);
} else {
// NOTE: Ignore the header, for now
return itemsToObject(fields);
}
});
},
scan: function () {

@ -5,7 +5,7 @@ import { SameLine as _ } from "../../../peg-whitespace"
import { RestOfLine } from "../../../peg-rest-of-line"
import { IdentifierValue } from "../primitives"
import { Header } from "../shared"
import { Header, Error } from "../shared"
{
const matchValue = require("match-value");
@ -13,13 +13,14 @@ import { Header } from "../shared"
}
RootAttributes
= header:Header attributesSection:AttributesSection Newline* {
return { ...header, attributes: attributesSection }
= header:Header attributesSection:(AttributesSection / Error) Newline* {
return { ...header, ... attributesSection }
};
AttributesSection
= AttributesSectionSATA
/ AttributesSectionNVMe
= attributes:AttributesSectionSATA { return { attributes: attributes }; }
/ attributes:AttributesSectionNVMe { return { attributes: attributes }; }
/ error:Error { return { error: error }; }
AttributesSectionSATA
= "=== 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 { BytesValue } from "../primitives"
import { Header } from "../shared"
import { Header, Error } from "../shared"
{
const matchValue = require("match-value");
@ -13,10 +13,14 @@ import { Header } from "../shared"
RootInfo
= 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+) {
return fields.filter((field) => field != null);
}

@ -5,3 +5,6 @@ Header 'header'
= "smartctl " versionString:RestOfLine "Copyright" copyrightStatement:RestOfLine Newline {
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
/* 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: {
// https://lwn.net/Articles/153366/
/* 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";
const { chain } = require("error-chain");
const ParseError = require("../text-parser");
const { ParseError } = require("../text-parser");
module.exports = function createJsonParser(resultMapper) {
return {

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

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

@ -1392,6 +1392,13 @@ ansi-styles@^4.1.0:
dependencies:
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:
version "2.0.0"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
@ -1400,6 +1407,14 @@ anymatch@^2.0.0:
micromatch "^3.1.4"
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:
version "1.2.0"
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"
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:
version "1.5.0"
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"
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:
version "1.1.0"
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"
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:
version "2.1.8"
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"
ws "~7.4.2"
entities@^2.0.0:
entities@^2.0.0, entities@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
@ -3556,6 +3598,13 @@ fill-range@^4.0.0:
repeat-string "^1.6.1"
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:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
@ -3738,6 +3787,11 @@ fsevents@^1.2.7:
bindings "^1.5.0"
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:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@ -3858,6 +3912,13 @@ glob-parent@^5.0.0:
dependencies:
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:
version "7.1.6"
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"
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:
version "3.3.0"
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:
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:
version "1.1.0"
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:
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:
version "0.1.0"
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:
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:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
@ -5408,7 +5493,7 @@ normalize-path@^2.1.1:
dependencies:
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"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
@ -6003,6 +6088,11 @@ pgpass@1.x:
dependencies:
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:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@ -6374,6 +6464,13 @@ readdirp@^2.2.1:
micromatch "^3.1.10"
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:
version "0.6.2"
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"
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:
version "1.2.4"
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-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:
version "0.5.3"
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"
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:
version "3.0.2"
resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"

Loading…
Cancel
Save