From f08044baf3f0e1dd83e0eb15fd66768c14cac81a Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Sat, 21 May 2022 14:42:44 +0200 Subject: [PATCH] WIP --- package.json | 7 +- public/css/style.css | 177 ++++++++++-------- public/css/style.css.map | 1 + src/api/data-sources/smartctl/attributes.js | 5 +- src/api/data-sources/smartctl/info.js | 5 +- src/api/types/drive.js | 66 ++++--- src/app.js | 30 +-- src/errors.js | 3 +- src/packages/dlayer-source/index.js | 38 ++-- src/packages/dlayer-wrap/index.js | 13 ++ src/packages/dlayer/index.js | 103 +++++++--- src/packages/dlayer/notes.txt | 4 + src/packages/exec-binary/index.js | 16 +- src/packages/exec-lsblk/index.js | 7 +- src/packages/exec-nvme/index.js | 164 ++++++++++++++++ src/packages/exec-smartctl/index.js | 29 ++- .../parsers/commands/attributes.pegjs | 11 +- .../exec-smartctl/parsers/commands/info.pegjs | 10 +- .../exec-smartctl/parsers/shared.pegjs | 3 + src/packages/parse-mount-options/index.js | 4 + src/packages/result/index.js | 136 ++++++++++++++ src/packages/text-parser-json/index.js | 2 +- src/scss/style.scss | 4 + src/views/error.jsx | 9 +- yarn.lock | 122 +++++++++++- 25 files changed, 778 insertions(+), 191 deletions(-) create mode 100644 public/css/style.css.map create mode 100644 src/packages/dlayer-wrap/index.js create mode 100644 src/packages/dlayer/notes.txt create mode 100644 src/packages/exec-nvme/index.js create mode 100644 src/packages/result/index.js diff --git a/package.json b/package.json index 5c05ddc..ac5e953 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/public/css/style.css b/public/css/style.css index 36e6bbd..7ccf896 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -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 */ diff --git a/public/css/style.css.map b/public/css/style.css.map new file mode 100644 index 0000000..98a4f43 --- /dev/null +++ b/public/css/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"} \ No newline at end of file diff --git a/src/api/data-sources/smartctl/attributes.js b/src/api/data-sources/smartctl/attributes.js index 51407ba..a60ebdf 100644 --- a/src/api/data-sources/smartctl/attributes.js +++ b/src/api/data-sources/smartctl/attributes.js @@ -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 ] + }); }); }; }; diff --git a/src/api/data-sources/smartctl/info.js b/src/api/data-sources/smartctl/info.js index 2444035..06c9b1e 100644 --- a/src/api/data-sources/smartctl/info.js +++ b/src/api/data-sources/smartctl/info.js @@ -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 ] + }); }); }; }; diff --git a/src/api/types/drive.js b/src/api/types/drive.js index 865c51a..64b2966 100644 --- a/src/api/types/drive.js +++ b/src/api/types/drive.js @@ -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"; } } } diff --git a/src/app.js b/src/app.js index fbc9dd1..1f9de6f 100644 --- a/src/app.js +++ b/src/app.js @@ -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; diff --git a/src/errors.js b/src/errors.js index e64415e..776aecd 100644 --- a/src/errors.js +++ b/src/errors.js @@ -14,5 +14,6 @@ module.exports = { ForbiddenError: errorChain.create("ForbiddenError", { inheritsFrom: HttpError, context: { statusCode: 403 } - }, HttpError), + }), + HardwareError: errorChain.create("HardwareError"), }; diff --git a/src/packages/dlayer-source/index.js b/src/packages/dlayer-source/index.js index a5dc281..27bc161 100644 --- a/src/packages/dlayer-source/index.js +++ b/src/packages/dlayer-source/index.js @@ -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 }; diff --git a/src/packages/dlayer-wrap/index.js b/src/packages/dlayer-wrap/index.js new file mode 100644 index 0000000..457cb3f --- /dev/null +++ b/src/packages/dlayer-wrap/index.js @@ -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)); + }); +}; diff --git a/src/packages/dlayer/index.js b/src/packages/dlayer/index.js index 36c422b..33be9cd 100644 --- a/src/packages/dlayer/index.js +++ b/src/packages/dlayer/index.js @@ -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 + }; +}; + diff --git a/src/packages/dlayer/notes.txt b/src/packages/dlayer/notes.txt new file mode 100644 index 0000000..0c88395 --- /dev/null +++ b/src/packages/dlayer/notes.txt @@ -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 diff --git a/src/packages/exec-binary/index.js b/src/packages/exec-binary/index.js index 480b747..4c00e75 100644 --- a/src/packages/exec-binary/index.js +++ b/src/packages/exec-binary/index.js @@ -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 })); }); diff --git a/src/packages/exec-lsblk/index.js b/src/packages/exec-lsblk/index.js index 91c3df2..872f712 100644 --- a/src/packages/exec-lsblk/index.js +++ b/src/packages/exec-lsblk/index.js @@ -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) => { diff --git a/src/packages/exec-nvme/index.js b/src/packages/exec-nvme/index.js new file mode 100644 index 0000000..bd4befc --- /dev/null +++ b/src/packages/exec-nvme/index.js @@ -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 })) diff --git a/src/packages/exec-smartctl/index.js b/src/packages/exec-smartctl/index.js index 0bae53f..0f6c0fe 100644 --- a/src/packages/exec-smartctl/index.js +++ b/src/packages/exec-smartctl/index.js @@ -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 () { diff --git a/src/packages/exec-smartctl/parsers/commands/attributes.pegjs b/src/packages/exec-smartctl/parsers/commands/attributes.pegjs index 27f1a9f..79eeb83 100644 --- a/src/packages/exec-smartctl/parsers/commands/attributes.pegjs +++ b/src/packages/exec-smartctl/parsers/commands/attributes.pegjs @@ -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 diff --git a/src/packages/exec-smartctl/parsers/commands/info.pegjs b/src/packages/exec-smartctl/parsers/commands/info.pegjs index af55576..a8768f8 100644 --- a/src/packages/exec-smartctl/parsers/commands/info.pegjs +++ b/src/packages/exec-smartctl/parsers/commands/info.pegjs @@ -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); } diff --git a/src/packages/exec-smartctl/parsers/shared.pegjs b/src/packages/exec-smartctl/parsers/shared.pegjs index 4cb1960..b9ea974 100644 --- a/src/packages/exec-smartctl/parsers/shared.pegjs +++ b/src/packages/exec-smartctl/parsers/shared.pegjs @@ -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)" diff --git a/src/packages/parse-mount-options/index.js b/src/packages/parse-mount-options/index.js index 7d23e44..c763b88 100644 --- a/src/packages/parse-mount-options/index.js +++ b/src/packages/parse-mount-options/index.js @@ -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. */ diff --git a/src/packages/result/index.js b/src/packages/result/index.js new file mode 100644 index 0000000..788e01f --- /dev/null +++ b/src/packages/result/index.js @@ -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) +}) + +*/ diff --git a/src/packages/text-parser-json/index.js b/src/packages/text-parser-json/index.js index 8fdd759..2da8df2 100644 --- a/src/packages/text-parser-json/index.js +++ b/src/packages/text-parser-json/index.js @@ -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 { diff --git a/src/scss/style.scss b/src/scss/style.scss index 41bf37c..ec9cae1 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -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; diff --git a/src/views/error.jsx b/src/views/error.jsx index 8e192d7..4f4b1ad 100644 --- a/src/views/error.jsx +++ b/src/views/error.jsx @@ -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") diff --git a/yarn.lock b/yarn.lock index eef1ab4..fb49ced 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"