diff --git a/notes.txt b/notes.txt index 8d349e3..bad427e 100644 --- a/notes.txt +++ b/notes.txt @@ -1,5 +1,6 @@ MARKER: - Move to pegjs-import +- Replace all 'this is a bug' errors with @joepie91/unreachable - LVM / mdraid support and tabs (+ complete refactoring LVM implementation) - Switch hashing to argon2id - Switch child_process to execa diff --git a/package.json b/package.json index 2309f68..33e4a1b 100644 --- a/package.json +++ b/package.json @@ -14,16 +14,19 @@ "license": "WTFPL", "dependencies": { "@babel/register": "^7.8.3", + "@invisible/pegjs-import": "^1.1.1", "@joepie91/express-react-views": "^1.0.1", "@joepie91/unreachable": "^1.0.0", "@validatem/allow-extra-properties": "^0.1.0", "@validatem/anything": "^0.1.0", "@validatem/array-of": "^0.1.2", "@validatem/core": "^0.3.15", + "@validatem/default-to": "^0.1.0", "@validatem/dynamic": "^0.1.2", "@validatem/either": "^0.1.9", "@validatem/error": "^1.1.0", "@validatem/is-array": "^0.1.1", + "@validatem/is-boolean": "^0.1.1", "@validatem/is-function": "^0.1.0", "@validatem/is-number": "^0.1.3", "@validatem/is-plain-object": "^0.1.1", diff --git a/src/app.js b/src/app.js index 02cc721..9f45412 100644 --- a/src/app.js +++ b/src/app.js @@ -101,6 +101,9 @@ module.exports = function () { // } + console.log(errorChain.getContext(sourceError)); + + res.render("error", { error: err }); diff --git a/src/packages/exec-binary/index.js b/src/packages/exec-binary/index.js index 91f2129..600dfbb 100644 --- a/src/packages/exec-binary/index.js +++ b/src/packages/exec-binary/index.js @@ -7,7 +7,7 @@ const util = require("util"); const execFileAsync = util.promisify(require("child_process").execFile); const debug = require("debug")("cvm:execBinary"); const asExpression = require("as-expression"); -const { rethrowAs } = require("error-chain"); +const { rethrowAs, chain } = require("error-chain"); const textParser = require("../text-parser"); const errors = require("./errors"); @@ -194,7 +194,7 @@ module.exports = function createBinaryInvocation(command, args = []) { if (!this._settings.expectedExitCodes.includes(exitCode)) { // FIXME: Can we actually pass `error` to be chained onto here, when there's a case where `error` is undefined? Namely, when requiring a non-zero exit code, but the process exits with 0. - throw new errors.NonZeroExitCode.chain(error, `Expected exit code to be one of ${JSON.stringify(this._settings.expectedExitCodes)}, but got '${exitCode}'`, { + throw chain(error, errors.NonZeroExitCode, `Expected exit code to be one of ${JSON.stringify(this._settings.expectedExitCodes)}, but got '${exitCode}'`, { exitCode: exitCode, stdout: stdout, stderr: stderr @@ -212,6 +212,7 @@ module.exports = function createBinaryInvocation(command, args = []) { return undefined; } } else { + // FIXME: use @joepie91/unreachable throw new Error(`Encountered expectation for unexpected channel '${expectation.channel}'; this is a bug, please report it`, { failedChannel: expectation.channel }); @@ -230,7 +231,7 @@ module.exports = function createBinaryInvocation(command, args = []) { return undefined; } } else { - throw errors.OutputParsingFailed.chain(error, `An error occurred while parsing '${expectation.channel}'`, { + throw chain(error, errors.OutputParsingFailed, `An error occurred while parsing '${expectation.channel}'`, { failedChannel: expectation.channel }); } @@ -270,7 +271,7 @@ module.exports = function createBinaryInvocation(command, args = []) { ? `Failed while processing ${error.failedChannel} of command` : "Failed while processing result of command execution"; - throw errors.CommandExecutionFailed.chain(error, message, { + throw chain(error, errors.CommandExecutionFailed, message, { exitCode: exitCode, stdout: stdout, stderr: stderr diff --git a/src/packages/exec-lvm/index.js b/src/packages/exec-lvm/index.js index 421bdee..df73f0d 100644 --- a/src/packages/exec-lvm/index.js +++ b/src/packages/exec-lvm/index.js @@ -1,6 +1,7 @@ "use strict"; const Promise = require("bluebird"); +const { chain } = require("error-chain"); const execBinary = require("../exec-binary"); const parseIECBytes = require("../parse-bytes-iec"); @@ -120,11 +121,11 @@ module.exports = { }).then((_output) => { return true; }).catch(hasFlag("deviceNotFound"), (error) => { - throw errors.InvalidPath.chain(error, `Specified device '${devicePath}' does not exist`, { + throw chain(error, errors.InvalidPath, `Specified device '${devicePath}' does not exist`, { path: devicePath }); }).catch(hasFlag("partitionTableExists"), (error) => { - throw errors.PartitionExists.chain(error, `Refused to create a Physical Volume, as a partition or partition table already exists on device '${devicePath}'`, { + throw chain(error, errors.PartitionExists, `Refused to create a Physical Volume, as a partition or partition table already exists on device '${devicePath}'`, { path: devicePath }); }); @@ -141,11 +142,11 @@ module.exports = { }).then((_output) => { return true; }).catch(hasFlag("deviceNotFound"), (error) => { - throw errors.InvalidPath.chain(error, `Specified device '${devicePath}' does not exist`, { + throw chain(error, errors.InvalidPath, `Specified device '${devicePath}' does not exist`, { path: devicePath }); }).catch(hasFlag("notAPhysicalVolume"), (error) => { - throw errors.InvalidPath.chain(error, `Specified device '${devicePath}' is not a Physical Volume`, { + throw chain(error, errors.InvalidPath, `Specified device '${devicePath}' is not a Physical Volume`, { path: devicePath }); }); @@ -178,17 +179,17 @@ module.exports = { }).catch(hasFlag("deviceNotFound"), (error) => { let failedDevices = error.getAllContext().result.deviceNotFound; - throw errors.InvalidPath.chain(error, `The following specified devices do not exist: ${failedDevices.join(", ")}`, { + throw chain(error, errors.InvalidPath, `The following specified devices do not exist: ${failedDevices.join(", ")}`, { paths: failedDevices }); }).catch(hasFlag("partitionTableExists"), (error) => { let failedDevices = error.getAllContext().result.partitionTableExists; - throw errors.PartitionExists.chain(error, `Refused to create a Volume Group, as partitions or partition tables already exist on the following devices: ${failedDevices.join(", ")}`, { + throw chain(error, errors.PartitionExists, `Refused to create a Volume Group, as partitions or partition tables already exist on the following devices: ${failedDevices.join(", ")}`, { paths: failedDevices }); }).catch(hasFlag("volumeGroupExists"), (error) => { - throw errors.VolumeGroupExists.chain(error, `A volume group with the name '${name}' already exists`, { + throw chain(error, errors.VolumeGroupExists, `A volume group with the name '${name}' already exists`, { volumeGroupName: name }); }).catch(hasFlag("physicalVolumeInUse"), (error) => { @@ -198,7 +199,7 @@ module.exports = { return `${device} (${volumeGroup})`; }).join(", "); - throw errors.PhysicalVolumeInUse.chain(error, `The following specified Physical Volumes are already in use in another Volume Group: ${failedItemString}`, { + throw chain(error, errors.PhysicalVolumeInUse, `The following specified Physical Volumes are already in use in another Volume Group: ${failedItemString}`, { volumes: failedItems }); }); @@ -218,21 +219,21 @@ module.exports = { }).then((_output) => { return true; }).catch(hasFlag("deviceNotFound"), (error) => { - throw errors.InvalidPath.chain(error, `Specified device '${physicalVolume}' does not exist`, { + throw chain(error, errors.InvalidPath, `Specified device '${physicalVolume}' does not exist`, { path: physicalVolume }); }).catch(hasFlag("volumeGroupNotFound"), (error) => { - throw errors.InvalidVolumeGroup.chain(error, `Specified Volume Group '${volumeGroup}' does not exist`, { + throw chain(error, errors.InvalidVolumeGroup, `Specified Volume Group '${volumeGroup}' does not exist`, { volumeGroupName: volumeGroup }); }).catch(hasFlag("physicalVolumeInUse"), (error) => { let volume = error.getAllContext().result.physicalVolumeInUse; - throw errors.PhysicalVolumeInUse.chain(error, `Specified Physical Volume '${physicalVolume}' is already in use in another Volume Group (${volume.volumeGroup})`, { + throw chain(error, errors.PhysicalVolumeInUse, `Specified Physical Volume '${physicalVolume}' is already in use in another Volume Group (${volume.volumeGroup})`, { volume: volume }); }).catch(hasFlag("partitionTableExists"), (error) => { - throw errors.PartitionExists.chain(error, `Refused to add device to Volume Group, as a partition or partition table already exists on device '${physicalVolume}'`, { + throw chain(error, errors.PartitionExists, `Refused to add device to Volume Group, as a partition or partition table already exists on device '${physicalVolume}'`, { path: physicalVolume }); }); diff --git a/src/packages/exec-smartctl/index.js b/src/packages/exec-smartctl/index.js index b567e3e..1e0edbc 100644 --- a/src/packages/exec-smartctl/index.js +++ b/src/packages/exec-smartctl/index.js @@ -8,22 +8,25 @@ const itemsToObject = require("../items-to-object"); /* FIXME: Error handling, eg. device not found errors */ -function outputParser(rootRule) { +function outputParser(parserPath) { return createPegParser({ - grammarFile: path.join(__dirname, "./parser.pegjs"), - options: { - allowedStartRules: [ rootRule ] - } + grammarFile: path.join(__dirname, parserPath) }); } +let attributesParser = outputParser("./parsers/commands/attributes.pegjs"); +let infoParser = outputParser("./parsers/commands/info.pegjs"); +let scanParser = outputParser("./parsers/commands/scan.pegjs"); + module.exports = { attributes: function ({ devicePath }) { return Promise.try(() => { + return attributesParser; + }).then((parser) => { return execBinary("smartctl", [devicePath]) .asRoot() .withFlags({ attributes: true }) - .requireOnStdout(outputParser("RootAttributes")) + .requireOnStdout(parser) .execute(); }).then((output) => { // NOTE: Ignore the header, for now @@ -32,10 +35,12 @@ module.exports = { }, info: function ({ devicePath }) { return Promise.try(() => { + return infoParser; + }).then((parser) => { return execBinary("smartctl", [devicePath]) .asRoot() .withFlags({ info: true }) - .requireOnStdout(outputParser("RootInfo")) + .requireOnStdout(parser) .execute(); }).then((output) => { // NOTE: Ignore the header, for now @@ -44,10 +49,12 @@ module.exports = { }, scan: function () { return Promise.try(() => { + return scanParser; + }).then((parser) => { return execBinary("smartctl") .asRoot() .withFlags({ scan: true }) - .requireOnStdout(outputParser("RootScan")) + .requireOnStdout(parser) .execute(); }).then((output) => { // NOTE: Ignore the header, for now diff --git a/src/packages/exec-smartctl/parser.pegjs b/src/packages/exec-smartctl/parser.pegjs deleted file mode 100644 index cac07b0..0000000 --- a/src/packages/exec-smartctl/parser.pegjs +++ /dev/null @@ -1,249 +0,0 @@ -{ - const matchValue = require("match-value"); - const syncpipe = require("syncpipe"); - - const {B} = require("../unit-bytes-iec"); - const mapAttributeFlags = require("./map-attribute-flags"); -} - -RootInfo - = header:Header infoSection:InfoSection Newline* { - return { ...header, fields: infoSection } - }; - -RootScan - = devices:ScanDevice* { - return { devices: devices }; - } - -RootAttributes - = header:Header attributesSection:AttributesSection Newline* { - return { ...header, attributes: attributesSection } - }; - -_ - = (" " / "\t")* - -RestOfLine - = content:$[^\n]+ Newline { - return content; - } - -Newline - = "\n" - / "\r\n" - -Header 'header' - = "smartctl " versionString:RestOfLine "Copyright" copyrightStatement:RestOfLine Newline { - return { versionString, copyrightStatement }; - } - -BytesValue - = value:SeparatedNumberValue { - return B(value); - } - -NumberValue - = value:$[0-9]+ { - return parseInt(value); - } - -SeparatedNumberValue - = value:$[0-9,]+ { - return syncpipe(value, [ - (_) => _.replace(/,/g, ""), - (_) => parseInt(_) - ]); - } - -HexNumberValue - = value:$[0-9A-Fa-f]+ { - return parseInt(value, 16); - } - -IdentifierValue - = value:$[a-zA-Z_-]+ { - return value; - } - -// smartctl --scan - -ScanDevice 'scanned device' - = path:$[^ ]+ _ "-d" _ interface_:$[^ ]+ _ RestOfLine { - return { path: path, interface: interface_ }; - } - -// smartctl --info - -InfoSection 'information section' - = "=== START OF INFORMATION SECTION ===" Newline fields:(InfoField+) { - return fields.filter((field) => field != null); - } - -InfoField 'information field' - = InfoFieldSimple - / InfoFieldIgnored - / InfoFieldSize - / InfoFieldRPM - / InfoFieldSectorSizes - / InfoFieldBoolean - / InfoFieldUnknown - -InfoFieldSimpleKey - = "Device Model" { return "model"; } - / "Model Number" { return "model"; } - / "Model Family" { return "modelFamily"; } - / "Serial Number" { return "serialNumber"; } - / "LU WWN Device Id" { return "wwn"; } - / "Firmware Version" { return "firmwareVersion"; } - / "Form Factor" { return "formFactor"; } - / "ATA Version is" { return "ataVersion"; } - / "SATA Version is" { return "sataVersion"; } - -InfoFieldSimple - = key:InfoFieldSimpleKey ":" _ value:RestOfLine { - return { key: key, value: value }; - } - -InfoFieldUnknown - = key:$[^:]+ ":" _ RestOfLine { - console.warn(`Encountered unrecognized SMART info key: ${key}`); - return null; - } - -InfoFieldIgnoredKey - = "Device is" - / "Local Time is" - -InfoFieldIgnored - = key:InfoFieldIgnoredKey ":" _ RestOfLine { - return null; - } - / "SMART support is:" _ ("Available" / "Unavailable") RestOfLine { - // We don't actually care about this entry, but have to specify its possible values explicitly, to distinguish it from the entry we *do* care about that (annoyingly) uses the same key; see InfoFieldBoolean - return null; - } - -InfoFieldSize - // NOTE: We don't actually care about the human-friendly display size after the 'bytes' specifier, hence the RestOfLine - = InfoFieldSizeKey _ value:SeparatedNumberValue _ "bytes"? _ RestOfLine { - return { - key: "size", - value: B(value) - }; - } - -InfoFieldSizeKey - = "User Capacity:" - / "Total NVM Capacity:" - -InfoFieldRPM - = "Rotation Rate:" _ value:NumberValue _ "rpm" Newline { - return { - key: "rpm", - value: value - }; - } - -InfoFieldSectorSizes - = "Sector Sizes:" _ logicalSize:BytesValue _ "bytes logical," _ physicalSize:BytesValue _ "bytes physical" Newline { - return { - key: "sectorSizes", - value: { - logical: logicalSize, - physical: physicalSize - } - }; - } - -InfoFieldBooleanKey - = "SMART support is" { return "smartEnabled"; } - -InfoFieldBoolean - = key:InfoFieldBooleanKey ":" _ value:RestOfLine { - return { - key: key, - value: matchValue(value, { - Enabled: true, - Disabled: false - }) - }; - } - -// smartctl --attributes - -AttributesSection - = AttributesSectionSATA - / AttributesSectionNVMe - -AttributesSectionSATA - = "=== START OF READ SMART DATA SECTION ===" Newline - "SMART Attributes Data Structure revision number:" _ NumberValue Newline - "Vendor Specific SMART Attributes with Thresholds:" Newline - "ID#" _ "ATTRIBUTE_NAME" _ "FLAG" _ "VALUE" _ "WORST" _ "THRESH" _ "TYPE" _ "UPDATED" _ "WHEN_FAILED" _ "RAW_VALUE" Newline - attributes:AttributeFieldSATA+ { - return attributes; - } - -AttributesSectionNVMe - = "=== START OF SMART DATA SECTION ===" Newline - "SMART/Health Information (NVMe Log 0x02)" Newline - attributes:AttributeFieldNVMe+ { - return attributes; - } - -AttributeFlags - = "0x" number:HexNumberValue { - return mapAttributeFlags(number); - } - -AttributeUpdatedWhen - = "Always" - / "Offline" - -AttributeFailedWhen - = "FAILING_NOW" - / "In_the_past" - / "-" - -AttributeFieldType - = "Pre-fail" - / "Old_age" - -AttributeFieldSATA - = _ id:NumberValue - _ attributeName:IdentifierValue - _ flags:AttributeFlags - _ value:NumberValue - _ worstValue:NumberValue - _ threshold:NumberValue - _ type:AttributeFieldType - _ updatedWhen:AttributeUpdatedWhen - _ failedWhen:AttributeFailedWhen - _ rawValue:RestOfLine { - return { - id, - attributeName, - flags, - value, - worstValue, - threshold, - rawValue, - updatedWhen: matchValue(updatedWhen, { - "Always": "always", - "Offline": "offline" - }), - type: matchValue(type, { - "Pre-fail": "preFail", - "Old_age": "oldAge" - }), - failingNow: (failedWhen === "FAILING_NOW"), - /* TODO: Should the below include the FAILING_NOW state? */ - failedBefore: (failedWhen === "In_the_past") - }; - } - -AttributeFieldNVMe - = label:$[^:]+ ":" _ value:RestOfLine { - return { label: label, value }; - } diff --git a/src/packages/exec-smartctl/parsers/commands/attributes.pegjs b/src/packages/exec-smartctl/parsers/commands/attributes.pegjs new file mode 100644 index 0000000..f25913c --- /dev/null +++ b/src/packages/exec-smartctl/parsers/commands/attributes.pegjs @@ -0,0 +1,88 @@ +import { _, RestOfLine, Newline, NumberValue, HexNumberValue, IdentifierValue } from "../primitives" +import { Header } from "../shared" + +{ + const matchValue = require("match-value"); + const mapAttributeFlags = require("../../map-attribute-flags"); +} + +RootAttributes + = header:Header attributesSection:AttributesSection Newline* { + return { ...header, attributes: attributesSection } + }; + +AttributesSection + = AttributesSectionSATA + / AttributesSectionNVMe + +AttributesSectionSATA + = "=== START OF READ SMART DATA SECTION ===" Newline + "SMART Attributes Data Structure revision number:" _ NumberValue Newline + "Vendor Specific SMART Attributes with Thresholds:" Newline + "ID#" _ "ATTRIBUTE_NAME" _ "FLAG" _ "VALUE" _ "WORST" _ "THRESH" _ "TYPE" _ "UPDATED" _ "WHEN_FAILED" _ "RAW_VALUE" Newline + attributes:AttributeFieldSATA+ { + return attributes; + } + +AttributesSectionNVMe + = "=== START OF SMART DATA SECTION ===" Newline + "SMART/Health Information (NVMe Log 0x02)" Newline + attributes:AttributeFieldNVMe+ { + return attributes; + } + +AttributeFlags + = "0x" number:HexNumberValue { + return mapAttributeFlags(number); + } + +AttributeUpdatedWhen + = "Always" + / "Offline" + +AttributeFailedWhen + = "FAILING_NOW" + / "In_the_past" + / "-" + +AttributeFieldType + = "Pre-fail" + / "Old_age" + +AttributeFieldSATA + = _ id:NumberValue + _ attributeName:IdentifierValue + _ flags:AttributeFlags + _ value:NumberValue + _ worstValue:NumberValue + _ threshold:NumberValue + _ type:AttributeFieldType + _ updatedWhen:AttributeUpdatedWhen + _ failedWhen:AttributeFailedWhen + _ rawValue:RestOfLine { + return { + id, + attributeName, + flags, + value, + worstValue, + threshold, + rawValue, + updatedWhen: matchValue(updatedWhen, { + "Always": "always", + "Offline": "offline" + }), + type: matchValue(type, { + "Pre-fail": "preFail", + "Old_age": "oldAge" + }), + failingNow: (failedWhen === "FAILING_NOW"), + /* TODO: Should the below include the FAILING_NOW state? */ + failedBefore: (failedWhen === "In_the_past") + }; + } + +AttributeFieldNVMe + = label:$[^:]+ ":" _ value:RestOfLine { + return { label: label, value }; + } diff --git a/src/packages/exec-smartctl/parsers/commands/info.pegjs b/src/packages/exec-smartctl/parsers/commands/info.pegjs new file mode 100644 index 0000000..1e1135b --- /dev/null +++ b/src/packages/exec-smartctl/parsers/commands/info.pegjs @@ -0,0 +1,106 @@ +import { _, RestOfLine, Newline, NumberValue, SeparatedNumberValue, BytesValue } from "../primitives" +import { Header } from "../shared" + +{ + const matchValue = require("match-value"); +} + +RootInfo + = header:Header infoSection:InfoSection Newline* { + return { ...header, fields: infoSection } + }; + +InfoSection 'information section' + = "=== START OF INFORMATION SECTION ===" Newline fields:(InfoField+) { + return fields.filter((field) => field != null); + } + +InfoField 'information field' + = InfoFieldSimple + / InfoFieldIgnored + / InfoFieldSize + / InfoFieldRPM + / InfoFieldSectorSizes + / InfoFieldBoolean + / InfoFieldUnknown + +InfoFieldSimpleKey + = "Device Model" { return "model"; } + / "Model Number" { return "model"; } + / "Model Family" { return "modelFamily"; } + / "Serial Number" { return "serialNumber"; } + / "LU WWN Device Id" { return "wwn"; } + / "Firmware Version" { return "firmwareVersion"; } + / "Form Factor" { return "formFactor"; } + / "ATA Version is" { return "ataVersion"; } + / "SATA Version is" { return "sataVersion"; } + +InfoFieldSimple + = key:InfoFieldSimpleKey ":" _ value:RestOfLine { + return { key: key, value: value }; + } + +InfoFieldUnknown + = key:$[^:]+ ":" _ RestOfLine { + console.warn(`Encountered unrecognized SMART info key: ${key}`); + return null; + } + +InfoFieldIgnoredKey + = "Device is" + / "Local Time is" + +InfoFieldIgnored + = key:InfoFieldIgnoredKey ":" _ RestOfLine { + return null; + } + / "SMART support is:" _ ("Available" / "Unavailable") RestOfLine { + // We don't actually care about this entry, but have to specify its possible values explicitly, to distinguish it from the entry we *do* care about that (annoyingly) uses the same key; see InfoFieldBoolean + return null; + } + +InfoFieldSize + // NOTE: We don't actually care about the human-friendly display size after the 'bytes' specifier, hence the RestOfLine + = InfoFieldSizeKey _ value:SeparatedNumberValue _ "bytes"? _ RestOfLine { + return { + key: "size", + value: B(value) + }; + } + +InfoFieldSizeKey + = "User Capacity:" + / "Total NVM Capacity:" + +InfoFieldRPM + = "Rotation Rate:" _ value:NumberValue _ "rpm" Newline { + return { + key: "rpm", + value: value + }; + } + +InfoFieldSectorSizes + = "Sector Sizes:" _ logicalSize:BytesValue _ "bytes logical," _ physicalSize:BytesValue _ "bytes physical" Newline { + return { + key: "sectorSizes", + value: { + logical: logicalSize, + physical: physicalSize + } + }; + } + +InfoFieldBooleanKey + = "SMART support is" { return "smartEnabled"; } + +InfoFieldBoolean + = key:InfoFieldBooleanKey ":" _ value:RestOfLine { + return { + key: key, + value: matchValue(value, { + Enabled: true, + Disabled: false + }) + }; + } diff --git a/src/packages/exec-smartctl/parsers/commands/scan.pegjs b/src/packages/exec-smartctl/parsers/commands/scan.pegjs new file mode 100644 index 0000000..9d36aea --- /dev/null +++ b/src/packages/exec-smartctl/parsers/commands/scan.pegjs @@ -0,0 +1,11 @@ +import { _, RestOfLine } from "../primitives" + +RootScan + = devices:ScanDevice* { + return { devices: devices }; + } + +ScanDevice 'scanned device' + = path:$[^ ]+ _ "-d" _ interface_:$[^ ]+ _ RestOfLine { + return { path: path, interface: interface_ }; + } diff --git a/src/packages/exec-smartctl/parsers/primitives.pegjs b/src/packages/exec-smartctl/parsers/primitives.pegjs new file mode 100644 index 0000000..c9cb78b --- /dev/null +++ b/src/packages/exec-smartctl/parsers/primitives.pegjs @@ -0,0 +1,44 @@ +{ + const syncpipe = require("syncpipe"); + const {B} = require("../../unit-bytes-iec"); +} + +_ + = (" " / "\t")* + +RestOfLine + = content:$[^\n]+ Newline { + return content; + } + +Newline + = "\n" + / "\r\n" + +BytesValue + = value:SeparatedNumberValue { + return B(value); + } + +NumberValue + = value:$[0-9]+ { + return parseInt(value); + } + +SeparatedNumberValue + = value:$[0-9,]+ { + return syncpipe(value, [ + (_) => _.replace(/,/g, ""), + (_) => parseInt(_) + ]); + } + +HexNumberValue + = value:$[0-9A-Fa-f]+ { + return parseInt(value, 16); + } + +IdentifierValue + = value:$[a-zA-Z_-]+ { + return value; + } diff --git a/src/packages/exec-smartctl/parsers/shared.pegjs b/src/packages/exec-smartctl/parsers/shared.pegjs new file mode 100644 index 0000000..daf9bfc --- /dev/null +++ b/src/packages/exec-smartctl/parsers/shared.pegjs @@ -0,0 +1,6 @@ +import { RestOfLine, Newline } from "./primitives" + +Header 'header' + = "smartctl " versionString:RestOfLine "Copyright" copyrightStatement:RestOfLine Newline { + return { versionString, copyrightStatement }; + } diff --git a/src/packages/text-parser-pegjs/index.js b/src/packages/text-parser-pegjs/index.js index 535d3c7..829a559 100644 --- a/src/packages/text-parser-pegjs/index.js +++ b/src/packages/text-parser-pegjs/index.js @@ -1,10 +1,11 @@ "use strict"; -const pegjs = require("pegjs"); -const fs = require("fs"); +const Promise = require("bluebird"); +const pegRedux = require("peg-redux"); const moduleEval = require("eval"); const vm = require("vm"); const asExpression = require("as-expression"); +const { chain } = require("error-chain"); const textParser = require("../text-parser"); const { validateOptions } = require("@validatem/core"); @@ -12,8 +13,8 @@ const isString = require("@validatem/is-string"); const isPlainObject = require("@validatem/is-plain-object"); const requireEither = require("@validatem/require-either"); -module.exports = function createPegParser({ grammar, grammarFile, options }) { - validateOptions(arguments, [ +module.exports = function createPegParser(_options) { + let { grammar, grammarFile, options } = validateOptions(arguments, [ { grammar: [ isString ], grammarFile: [ isString ], @@ -21,52 +22,53 @@ module.exports = function createPegParser({ grammar, grammarFile, options }) { }, requireEither([ "grammar", "grammarFile" ]) ]); - if (grammarFile != null) { - // FIXME: cache - grammar = fs.readFileSync(grammarFile, "utf8"); - } - - let parserCode = pegjs.generate(grammar, { + let parserOptions = { ... options, output: "source", format: "commonjs" - }); - - let parser = asExpression(() => { - if (grammarFile != null) { - return moduleEval(parserCode, grammarFile, {}, true); - } else { - let exports_ = {}; + }; - let sandbox = { - exports: exports_, - module: { + return Promise.try(() => { + return (grammarFile != null) + ? pegRedux.generateFromFile(grammarFile, parserOptions) + : pegRedux.generate(grammar, parserOptions); + }).then((parserCode) => { + let parser = asExpression(() => { + if (grammarFile != null) { + return moduleEval(parserCode, grammarFile, {}, true); + } else { + let exports_ = {}; + + let sandbox = { exports: exports_, - }, - require: function () { - throw new Error("You cannot use require() when loading a grammar as a string; use the `grammarFile` option instead"); - } - }; - - let script = new vm.Script(parserCode.replace(/^\#\!.*/, '')); - script.runInNewContext(sandbox); - - return sandbox.module.exports; - } - }); + module: { + exports: exports_, + }, + require: function () { + throw new Error("You cannot use require() when loading a grammar as a string; use the `grammarFile` option instead"); + } + }; + + let script = new vm.Script(parserCode.replace(/^\#\!.*/, '')); + script.runInNewContext(sandbox); - return { - supportsStreams: false, - parse: function (text) { - try { - return parser.parse(text); - } catch (error) { - if (error.name === "SyntaxError") { - throw textParser.NoResult.chain(error, "Parsing output failed"); - } else { - throw error; + return sandbox.module.exports; + } + }); + + return { + supportsStreams: false, + parse: function (text) { + try { + return parser.parse(text); + } catch (error) { + if (error.name === "SyntaxError") { + throw chain(error, textParser.NoResult, "Parsing output failed"); + } else { + throw error; + } } } - } - }; + }; + }); }; diff --git a/src/test-wrapper.js b/src/test-wrapper.js index 2e2d648..ed0189e 100644 --- a/src/test-wrapper.js +++ b/src/test-wrapper.js @@ -26,11 +26,11 @@ return Promise.try(() => { // return lvm.addVolumeToVolumeGroup({ volumeGroup: "vg-name", physicalVolume: "/dev/loop1" }); // return lvm.destroyPhysicalVolume({ devicePath: "/dev/loop0" }); // return lsblk(); - // return smartctl.scan(); + return smartctl.scan(); // return smartctl.info({ devicePath: "/dev/sda" }) // return smartctl.info({ devicePath: process.argv[2] }) // return smartctl.attributes({ devicePath: process.argv[2] }); - return findmnt(); + // return findmnt(); // return nvmeCli.listNamespaces({ devicePath: "/dev/nvme0" }); }).then((result) => { console.log(util.inspect(result, {colors: true, depth: null})); diff --git a/yarn.lock b/yarn.lock index 8403ebf..43a8d8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -949,6 +949,13 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@invisible/pegjs-import@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@invisible/pegjs-import/-/pegjs-import-1.1.1.tgz#1c5feb6fd768604cadd63efc9dfb999e8b0a0c7f" + integrity sha512-TiUoDxO08miDb6EQaWQeuBtkPPAsOpw55HBCbBN+EtIXy7URT1fwWNt/5k/k0pI+U58FnnGSj361oRSPATuSmw== + dependencies: + pegjs "^0.10.0" + "@joepie91/eslint-config@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@joepie91/eslint-config/-/eslint-config-1.1.0.tgz#9397e6ce0a010cb57dcf8aef8754d3a5ce0ae36a"