diff --git a/.vscode/launch.json b/.vscode/launch.json index 9973bb2..b5031f7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -26,6 +26,15 @@ "skipFiles": [ "/**" ], + }, + { + "type": "node", + "request": "launch", + "name": "tree-find testcase", + "program": "${workspaceFolder}/src/packages/tree-find/example.js", + "skipFiles": [ + "/**" + ], } ] } diff --git a/src/api/data-sources/findmnt.js b/src/api/data-sources/findmnt.js deleted file mode 100644 index 8c3e4d7..0000000 --- a/src/api/data-sources/findmnt.js +++ /dev/null @@ -1,42 +0,0 @@ -"use strict"; - -const Promise = require("bluebird"); -const memoizee = require("memoizee"); -const fs = Promise.promisifyAll(require("fs")); -const findmnt = require("../../packages/exec-findmnt"); -const All = require("../../packages/graphql-interface/symbols/all"); -const treeMapAsync = require("../../packages/tree-map-async"); - -module.exports = function () { - let findmntOnce = memoizee(() => { - return Promise.try(() => { - return findmnt(); - }).then((mounts) => { - return treeMapAsync(mounts, async (mount) => { - if (mount.sourceDevice?.startsWith("/")) { - return { - ... mount, - sourceDevice: await fs.realpathAsync(mount.sourceDevice) - }; - } else { - // Skip mounts that don't exist at a path at all - return mount; - } - }, true); - }); - }); - - return function (mountpoints) { - return Promise.try(() => { - return findmntOnce(); - }).then(({tree, list}) => { - return mountpoints.map((mountpoint) => { - if (mountpoint === All) { - return tree; - } else { - return list.find((mount) => mount.mountpoint === mountpoint); - } - }); - }); - }; -}; diff --git a/src/api/data-sources/index.js b/src/api/data-sources/index.js index 649e111..17bd93b 100644 --- a/src/api/data-sources/index.js +++ b/src/api/data-sources/index.js @@ -32,21 +32,6 @@ function makeSingleCommand({ command, selectResult }) { module.exports = function createSources() { let sources = { - // lvmLogicalVolumes: evaluateAndPick({ - // command: lvm.getLogicalVolumes, - // selectResult: (result) => result.volumes, - // selectID: (volume) => volume.path - // }), - // lvmPhysicalVolumes: evaluateAndPick({ - // command: lvm.getPhysicalVolumes, - // selectResult: (result) => result.volumes, - // selectID: (device) => device.path - // }), - // lvmVolumeGroups: evaluateAndPick({ - // command: lvm.getVolumeGroups, - // selectResult: (result) => result.groups, - // selectID: (group) => group.name - // }), nvmeIdentifyController: makeSingleCommand({ command: (path) => nvmeCLI.identifyController({ devicePath: path }) }), @@ -70,10 +55,7 @@ module.exports = function createSources() { }; // TODO: Consider moving these to be inline as well, somehow - let factoryModules = { - lsblk: require("./lsblk")(), - findmnt: require("./findmnt")() - }; + let factoryModules = {}; return mapObj({ ... factoryModules, ... sources }, (name, factory) => { return [ diff --git a/src/api/data-sources/lsblk.js b/src/api/data-sources/lsblk.js deleted file mode 100644 index 5922404..0000000 --- a/src/api/data-sources/lsblk.js +++ /dev/null @@ -1,54 +0,0 @@ -"use strict"; - -const Promise = require("bluebird"); -const memoizee = require("memoizee"); -const asExpression = require("as-expression"); -const fs = Promise.promisifyAll(require("fs")); -const unreachable = require("@joepie91/unreachable")("cvm"); -const lsblk = require("../../packages/exec-lsblk"); -const All = require("../../packages/graphql-interface/symbols/all"); -const findInTree = require("../../packages/find-in-tree"); -const treeMapAsync = require("../../packages/tree-map-async"); - -module.exports = function () { - let lsblkOnce = memoizee(() => { - return Promise.try(() => { - return lsblk(); - }).then((tree) => { - return treeMapAsync(tree, async (device) => { - return { - ... device, - path: await fs.realpathAsync(device.path) - }; - }, true); - }); - }); - - return function (selectors) { - return Promise.try(() => { - return lsblkOnce(); - }).then(({tree, list}) => { - return selectors.map((selector) => { - if (selector === All) { - return tree; - // return list; - } else { - let { path, name } = selector; - - let predicate = asExpression(() => { - if (path != null) { - return (device) => device.path === path; - } else if (name != null) { - return (device) => device.name === name; - } else { - unreachable("No selector specified for lsblk"); - } - }); - - // TODO: Shouldn't this pick from the list instead? - return findInTree({ tree, predicate }); - } - }); - }); - }; -}; diff --git a/src/api/index.js b/src/api/index.js index 4924255..fb0764a 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -32,18 +32,16 @@ module.exports = function () { }; }, modules: [ - require("../packages/sysquery-lvm") + require("../packages/sysquery-lvm"), + require("../packages/sysquery-core"), ], schema: { hardware: { - drives: ({ paths }, { sources }) => { - return typeFromSource(sources.smartctlScan, paths, (device) => types.Drive({ path: device.path })); - } + // drives: ({ paths }, { sources }) => { + // return typeFromSource(sources.smartctlScan, paths, (device) => types.Drive({ path: device.path })); + // } }, resources: { - blockDevices: ({ names }, { sources }) => { - return typeFromSource(sources.lsblk, names, (device) => types.BlockDevice({ name: device.name })); - }, images: { installationMedia: [], vmImages: [] diff --git a/src/api/types/block-device.js b/src/api/types/block-device.js deleted file mode 100644 index bfb6a5d..0000000 --- a/src/api/types/block-device.js +++ /dev/null @@ -1,64 +0,0 @@ -"use strict"; - -const Promise = require("bluebird"); -const fs = Promise.promisifyAll(require("fs")); -const matchValue = require("match-value"); -const asyncpipe = require("../../packages/asyncpipe"); - -const dlayerSource = require("../../packages/dlayer-source"); -const All = require("../../packages/graphql-interface/symbols/all"); // FIXME: Move to dlayer-source? -const treecutter = require("../../packages/treecutter"); -const types = require("./"); - -module.exports = function BlockDevice({ name, path }) { - return dlayerSource.withSources({ - // TODO: Eventually make this produce a (filtered) tree instead? - mounts: function ({ type }, { $getProperty, $getPropertyPath, sources }) { - return Promise.try(() => { - return sources.findmnt.load(All); - }).then((mountTree) => { - return asyncpipe(mountTree, [ - (_) => treecutter.flatten(_), - (_) => _.map((mount) => types.Mount({ mountpoint: mount.mountpoint })), - (_) => Promise.filter(_, async (mount) => { - let sourcePath = await $getPropertyPath(mount, "sourceDevice.path"); - let sourceName = await $getPropertyPath(mount, "sourceDevice.name"); - - return ( - (sourcePath != null && sourcePath === path) - || (sourceName != null && sourceName === name) - ); - }) - ]); - }).then((relevantMounts) => { - if (type == null) { - return relevantMounts; - } else { - return Promise.filter(relevantMounts, async (mount) => { - return (await $getProperty(mount, "type") === type); - }); - } - }); - }, - $sources: { - lsblk: { - [dlayerSource.ID]: { name, path }, - name: "name", - path: (device) => fs.realpathAsync(device.path), - type: (device) => matchValue(device.type, { - partition: "PARTITION", - disk: "DISK", - loopDevice: "LOOP_DEVICE" - }), - size: "size", - mountpoint: "mountpoint", // FIXME: Isn't this obsoleted by `mounts`? - deviceNumber: "deviceNumber", - removable: "removable", - readOnly: "readOnly", - children: (device) => device.children.map((child) => { - return BlockDevice({ name: child.name }); - }) - } - } - }); -}; diff --git a/src/api/types/drive.js b/src/api/types/drive.js index 54f4d6a..1bff992 100644 --- a/src/api/types/drive.js +++ b/src/api/types/drive.js @@ -6,7 +6,7 @@ const treecutter = require("../../packages/treecutter"); const upperSnakeCase = require("../../packages/upper-snake-case"); const { B } = require("../../packages/unit-bytes-iec"); -const types = require("./"); +const types = require("."); module.exports = function Drive ({ path }) { return dlayerSource.withSources({ diff --git a/src/api/types/index.js b/src/api/types/index.js index adadb7d..57831e9 100644 --- a/src/api/types/index.js +++ b/src/api/types/index.js @@ -1,7 +1,5 @@ "use strict"; Object.assign(module.exports, { - Drive: require("./drive"), - BlockDevice: require("./block-device"), - Mount: require("./mount"), + // Drive: require("./drive"), }); diff --git a/src/api/types/lvm/logical-volume.js b/src/api/types/lvm/logical-volume.js deleted file mode 100644 index fa1d0bd..0000000 --- a/src/api/types/lvm/logical-volume.js +++ /dev/null @@ -1,58 +0,0 @@ -"use strict"; - -const dlayerSource = require("../../../packages/dlayer-source"); - -const types = require(".."); - -module.exports = function LVMLogicalVolume ({ path }) { - return dlayerSource.withSources({ - $sources: { - lvmLogicalVolumes: { - [dlayerSource.ID]: path, - path: "path", - name: "name", - fullName: "fullName", - size: "size", - uuid: "uuid", - deviceMapperPath: "deviceMapperPath", - layoutAttributes: "layoutAttributes", - roles: "roles", - tags: "tags", - configurationProfile: "configurationProfile", - creationTime: "creationTime", - creationHost: "creationHost", - neededKernelModules: "neededKernelModules", - dataVolume: "dataVolume", // FIXME: Reference? - metadataVolume: "metadataVolume", // FIXME: Reference? - poolVolume: "poolVolume", // FIXME: Reference? - persistentMajorNumber: "persistentMajorNumber", - persistentMinorNumber: "persistentMinorNumber", - type: "type", - isReadOnly: "isReadOnly", - isCurrentlyReadOnly: "isCurrentlyReadOnly", - isAllocationLocked: "isAllocationLocked", - allocationPolicy: "allocationPolicy", - status: "status", - healthStatus: "healthStatus", - isInitiallySynchronized: "isInitiallySynchronized", - isCurrentlySynchronized: "isCurrentlySynchronized", - isMerging: "isMerging", - isConverting: "isConverting", - isSuspended: "isSuspended", - isActivationSkipped: "isActivationSkipped", - isOpened: "isOpened", - isActiveLocally: "isActiveLocally", - isActiveRemotely: "isActiveRemotely", - isActiveExclusively: "isActiveExclusively", - isMergeFailed: "isMergeFailed", - isSnapshotInvalid: "isSnapshotInvalid", - isLiveTablePresent: "isLiveTablePresent", - isInactiveTablePresent: "isInactiveTablePresent", - isZeroFilled: "isZeroFilled", - hasFixedMinorNumber: "hasFixedMinorNumber", - outOfSpacePolicy: "outOfSpacePolicy", - volumeGroup: (volume) => types.LVMVolumeGroup({ name: volume.volumeGroup }) - } - } - }); -}; diff --git a/src/api/types/lvm/physical-volume.js b/src/api/types/lvm/physical-volume.js deleted file mode 100644 index 87791e3..0000000 --- a/src/api/types/lvm/physical-volume.js +++ /dev/null @@ -1,25 +0,0 @@ -"use strict"; - -const dlayerSource = require("../../../packages/dlayer-source"); - -const types = require("../"); - -module.exports = function LVMPhysicalVolume ({ path }) { - return dlayerSource.withSources({ - $sources: { - lvmPhysicalVolumes: { - [dlayerSource.ID]: path, - path: "path", - format: "format", - totalSpace: "totalSpace", - freeSpace: "freeSpace", - isExported: "isExported", - isMissing: "isMissing", - isAllocatable: "isAllocatable", - isDuplicate: "isDuplicate", - isUsed: "isUsed", - volumeGroup: (volume) => types.LVMVolumeGroup({ name: volume.volumeGroup }) - } - } - }); -}; diff --git a/src/api/types/lvm/volume-group.js b/src/api/types/lvm/volume-group.js deleted file mode 100644 index e0d1d68..0000000 --- a/src/api/types/lvm/volume-group.js +++ /dev/null @@ -1,47 +0,0 @@ -"use strict"; - -const Promise = require("bluebird"); -const dlayerSource = require("../../../packages/dlayer-source"); - -const types = require("../"); -const All = require("../../../packages/graphql-interface/symbols/all"); - -module.exports = function LVMVolumeGroup ({ name }) { - return dlayerSource.withSources({ - physicalVolumes: function (_args, { sources }) { - return Promise.try(() => { - return sources.lvmPhysicalVolumes.load(All); - }).filter((volume) => { - return (volume.volumeGroup === name); - }).map((volume) => { - return types.LVMPhysicalVolume({ path: volume.path }); - }); - }, - logicalVolumes: function (_args, { sources }) { - return Promise.try(() => { - return sources.lvmLogicalVolumes.load(All); - }).filter((volume) => { - return (volume.volumeGroup === name); - }).map((volume) => { - return types.LVMLogicalVolume({ path: volume.path }); - }); - }, - $sources: { - lvmVolumeGroups: { - [dlayerSource.ID]: name, - name: "name", - totalSpace: "totalSpace", - freeSpace: "freeSpace", - physicalVolumeCount: "physicalVolumeCount", - logicalVolumeCount: "logicalVolumeCount", - snapshotCount: "snapshotCount", - isReadOnly: "isReadOnly", - isResizeable: "isResizeable", - isExported: "isExported", - isIncomplete: "isIncomplete", - allocationPolicy: "allocationPolicy", - mode: "mode" - } - } - }); -}; diff --git a/src/api/types/mount.js b/src/api/types/mount.js deleted file mode 100644 index 22a048a..0000000 --- a/src/api/types/mount.js +++ /dev/null @@ -1,61 +0,0 @@ -"use strict"; - -const Promise = require("bluebird"); -const fs = Promise.promisifyAll(require("fs")); -const dlayerSource = require("../../packages/dlayer-source"); -const types = require("./"); - -module.exports = function Mount({ mountpoint }) { - return dlayerSource.withSources({ - mountpoint: mountpoint, - sourceDevice: async (_, { sources }) => { - let mount = await sources.findmnt.load(mountpoint); - - if (mount != null) { - if (mount.sourceDevice != null) { - let sourcePath = await fs.realpathAsync(mount.sourceDevice); - - if (await sources.lsblk.load({ path: sourcePath }) != null) { - return types.BlockDevice({ path: sourcePath }); - } else { - // This occurs when the `sourceDevice` is a valid device, but it is not a *block* device, eg. `/dev/fuse` - return null; - } - } else { - // This occurs when the mount is not backed by a device, eg. an sshfs FUSE mount - return null; - } - } else { - // TODO: Can this ever happen for any legitimate reason? - throw new Error(`Mountpoint '${mountpoint}' not found in findmnt output`); - } - }, - $sources: { - findmnt: { - [dlayerSource.ID]: mountpoint, - id: "id", - // FIXME: Aren't we inferring the below somewhere else in the code, using the square brackets? - type: (mount) => (mount.rootPath === "/") - ? "ROOT_MOUNT" - : "SUBMOUNT", - filesystem: "filesystem", - options: "options", - label: "label", - uuid: "uuid", - partitionLabel: "partitionLabel", - partitionUUID: "partitionUUID", - deviceNumber: "deviceNumber", - totalSpace: "totalSpace", - freeSpace: "freeSpace", - usedSpace: "usedSpace", - rootPath: "rootPath", - taskID: "taskID", - optionalFields: "optionalFields", - propagationFlags: "propagationFlags", - children: (mount) => mount.children.map((child) => { - return Mount({ mountpoint: child.mountpoint }); - }) - } - } - }); -}; diff --git a/src/packages/find-in-tree/index.js b/src/packages/find-in-tree/index.js deleted file mode 100644 index d6cd8a9..0000000 --- a/src/packages/find-in-tree/index.js +++ /dev/null @@ -1,39 +0,0 @@ -"use strict"; - -const assureArray = require("assure-array"); -const isIterable = require("is-iterable"); - -const { validateOptions } = require("@validatem/core"); -const required = require("@validatem/required"); -const isFunction = require("@validatem/is-function"); -const isString = require("@validatem/is-string"); - -module.exports = function findInTree(options) { - validateOptions(arguments, { - tree: [ required ], - predicate: [ required, isFunction ], - childrenProperty: [ isString ], - }); - - let childrenProperty = options.childrenProperty ?? "children"; - let topLevelItems = assureArray(options.tree); - let predicate = options.predicate; - - function find(items) { - if (isIterable(items)) { - for (let item of items) { - if (predicate(item)) { - return item; - } else { - let childResult = find(item[childrenProperty]); - - if (childResult !== undefined) { - return childResult; - } - } - } - } - } - - return find(topLevelItems); -}; diff --git a/src/packages/find/index.js b/src/packages/find/index.js new file mode 100644 index 0000000..4681bd3 --- /dev/null +++ b/src/packages/find/index.js @@ -0,0 +1,20 @@ +"use strict"; + +module.exports = function find(iterable, predicate) { + if (Array.isArray(iterable)) { + // An optimized implementation may be available + return iterable.find(predicate); + } else { + let i = 0; + + for (let value of iterable) { + let found = predicate(value, i, iterable); + + if (found) { + return value; + } + + i++; + } + } +}; diff --git a/src/packages/first-value/index.js b/src/packages/first-value/index.js new file mode 100644 index 0000000..0a4191c --- /dev/null +++ b/src/packages/first-value/index.js @@ -0,0 +1,17 @@ +"use strict"; + +// Kinda like `find`, but the ?? edition - evaluates items until one produces a not-nullish result + +module.exports = function firstValue(iterable, predicate) { + let i = 0; + + for (let value of iterable) { + let found = predicate(value, i, iterable); + + if (found != null) { + return found; + } + + i++; + } +}; diff --git a/src/packages/for-each/index.js b/src/packages/for-each/index.js new file mode 100644 index 0000000..04c5dc7 --- /dev/null +++ b/src/packages/for-each/index.js @@ -0,0 +1,17 @@ +"use strict"; + +// TODO: Document that there's no async version of this because an async version must always map (to a Promise) anyway + +module.exports = function forEach(iterable, mapper) { + if (Array.isArray(iterable)) { + // This may have an optimized implementation + return iterable.forEach(mapper); + } else { + let i = 0; + + for (let item of iterable) { + mapper(item, i, iterable); + i++; + } + } +}; diff --git a/src/packages/map-async/index.js b/src/packages/map-async/index.js new file mode 100644 index 0000000..0052433 --- /dev/null +++ b/src/packages/map-async/index.js @@ -0,0 +1,23 @@ +"use strict"; + +// TODO: Add concurrency control? + +const map = require("../map"); + +module.exports = async function mapAsync(iterable, mapper, options = {}) { + let concurrent = options.concurrent ?? true; + + if (concurrent) { + return Promise.all(map(iterable, mapper)); + } else { + let results = []; + let i = 0; + + for (let value of iterable) { + let result = await mapper(value, i, iterable); + results.push(result); + } + + return results; + } +}; diff --git a/src/packages/map/index.js b/src/packages/map/index.js new file mode 100644 index 0000000..6665e47 --- /dev/null +++ b/src/packages/map/index.js @@ -0,0 +1,18 @@ +"use strict"; + +module.exports = function map(iterable, mapper) { + if (Array.isArray(iterable)) { + // This may have an optimized implementation + return iterable.map(mapper); + } else { + let result = []; + let i = 0; + + for (let item of iterable) { + result.push(mapper(item, i, iterable)); + i++; + } + + return result; + } +}; diff --git a/src/packages/sysquery-core/findmnt.js b/src/packages/sysquery-core/findmnt.js new file mode 100644 index 0000000..f336819 --- /dev/null +++ b/src/packages/sysquery-core/findmnt.js @@ -0,0 +1,37 @@ +"use strict"; + +const memoizee = require("memoizee"); +const fs = require("fs").promises; +const findmnt = require("../exec-findmnt"); +const All = require("../graphql-interface/symbols/all"); +const treeMapAsync = require("../tree-map-async"); +const treeFind = require("../tree-find"); + +module.exports = function () { + let findmntOnce = memoizee(async () => { + return treeMapAsync(await findmnt(), async (mount) => { + return { + ... mount, + sourceDevice: (mount.sourceDevice?.startsWith("/")) + ? await fs.realpath(mount.sourceDevice) + : mount.sourceDevice + }; + + }); + }); + + return async function (mountpoints) { + let mounts = await findmntOnce(); + + // TODO: It's kind of strange that it sometimes returns a tree and sometimes a list, this can probably be improved? + let matches = mountpoints.map((mountpoint) => { + if (mountpoint === All) { + return mounts; + } else { + return treeFind(mounts, (mount) => mount.mountpoint === mountpoint); + } + }); + + return matches; + }; +}; diff --git a/src/packages/sysquery-core/index.js b/src/packages/sysquery-core/index.js new file mode 100644 index 0000000..59379b5 --- /dev/null +++ b/src/packages/sysquery-core/index.js @@ -0,0 +1,152 @@ +"use strict"; + +const fs = require("fs").promises; +const matchValue = require("match-value"); +const DataLoader = require("dataloader"); + +const mapFromSource = require("../map-from-source"); +const dlayerSource = require("../dlayer-source"); +const All = require("../graphql-interface/symbols/all"); +const treeFilterFlatAsync = require("../tree-filter-flat-async"); +const assert = require("assert"); + +// FIXME: Simplify findmnt/lsblk source definitions, and probably separate out mounts and block devices into their own separate modules + +module.exports = { + name: "sysquery.core", + makeContext: function () { + // MARKER: Complete sources migration, test, move smartctl + return { + lsblk: new DataLoader(require("./lsblk")()), + findmnt: new DataLoader(require("./findmnt")()) + }; + }, + types: { + "sysquery.core.Mount": function ({ mountpoint }) { + return dlayerSource.withSources({ + mountpoint: mountpoint, + sourceDevice: async (_, { lsblk, findmnt, $make }) => { + let mount = await findmnt.load(mountpoint); + + if (mount == null) { + // TODO: Can this ever happen for any legitimate reason? + throw new Error(`Mountpoint '${mountpoint}' not found in findmnt output`); + } else if (mount.sourceDevice == null) { + // This occurs when the mount is not backed by a device, eg. an sshfs FUSE mount + return null; + } else { + let sourcePath = await fs.realpath(mount.sourceDevice); + + if (await lsblk.load({ path: sourcePath }) != null) { + return $make("sysquery.core.BlockDevice", { path: sourcePath }); + } else { + // This occurs when the `sourceDevice` is a valid device, but it is not a *block* device, eg. `/dev/fuse` + return null; + } + } + }, + $sources: { + findmnt: { + [dlayerSource.ID]: mountpoint, + id: "id", + // FIXME: Aren't we inferring the below somewhere else in the code, using the square brackets? + type: (mount) => (mount.rootPath === "/") + ? "ROOT_MOUNT" + : "SUBMOUNT", + filesystem: "filesystem", + options: "options", + label: "label", + uuid: "uuid", + partitionLabel: "partitionLabel", + partitionUUID: "partitionUUID", + deviceNumber: "deviceNumber", + totalSpace: "totalSpace", + freeSpace: "freeSpace", + usedSpace: "usedSpace", + rootPath: "rootPath", + taskID: "taskID", + optionalFields: "optionalFields", + propagationFlags: "propagationFlags", + children: (mount, { $make }) => mount.children.map((child) => { + return $make("sysquery.core.Mount", { mountpoint: child.mountpoint }); + }) + } + } + }); + }, + "sysquery.core.BlockDevice": function ({ name, path }) { + return dlayerSource.withSources({ + // TODO: Eventually make this produce a (filtered) tree instead? + mounts: async function ({ type }, { findmnt, $make, $getProperty, $getPropertyPath }) { + let mountTree = await findmnt.load(All); + + let relevantMounts = await treeFilterFlatAsync(mountTree, async (mount) => { + let mountObject = $make("sysquery.core.Mount", { mountpoint: mount.mountpoint }); + // console.log({ sourceDevice: await $getProperty(mountObject, "sourceDevice") }); + let sourcePath = await $getPropertyPath(mountObject, "sourceDevice.path"); + let sourceName = await $getPropertyPath(mountObject, "sourceDevice.name"); + + // TODO: This logic looks strange. Is it actually correct, even when only one of name/path is specified upon BlockDevice construction? + let matchesDevice = ( + (sourcePath != null && sourcePath === path) + || (sourceName != null && sourceName === name) + ); + + let matchesType = ( + type == null + || await $getProperty(mountObject, "type" === type) + ); + + return matchesDevice && matchesType; + }, { recurseFilteredSubtrees: true }); + + // This is a bit hacky; this approach should probably be replaced by a map-filter instead. But for now, this will do - as this all happens in the same request context, there's no real penalty to re-creating the mount objects a second time. + return relevantMounts.map((mount) => { + return $make("sysquery.core.Mount", { mountpoint: mount.mountpoint }); + }); + }, + $sources: { + lsblk: { + [dlayerSource.ID]: { name, path }, + name: "name", + path: (device) => fs.realpath(device.path), + type: (device) => matchValue(device.type, { + partition: "PARTITION", + disk: "DISK", + loopDevice: "LOOP_DEVICE" + }), + size: "size", + mountpoint: "mountpoint", // FIXME: Isn't this obsoleted by `mounts`? + deviceNumber: "deviceNumber", + removable: "removable", + readOnly: "readOnly", + children: (device, { $make }) => device.children.map((child) => { + return $make("sysquery.core.BlockDevice", { name: child.name }); + }) + } + } + }); + } + }, + extensions: { + // None + }, + root: { + resources: { + blockDevices: ({ names, paths }, { lsblk, $make }) => { + // TODO: Design better abstraction for this sort of case + let selectors = (names == null && paths == null) + ? null + : [ + ... (names ?? []).map((name) => ({ name: name })), + ... (paths ?? []).map((path) => ({ path: path })) + ]; + + return mapFromSource(lsblk, selectors, (device) => { + return $make("sysquery.core.BlockDevice", { name: device.name }); + }); + } + // FIXME: Add mounts + } + } +}; diff --git a/src/packages/sysquery-core/lsblk.js b/src/packages/sysquery-core/lsblk.js new file mode 100644 index 0000000..340c356 --- /dev/null +++ b/src/packages/sysquery-core/lsblk.js @@ -0,0 +1,42 @@ +"use strict"; + +const memoizee = require("memoizee"); +const fs = require("fs").promises; +const unreachable = require("@joepie91/unreachable")("cvm"); +const lsblk = require("../exec-lsblk"); +const All = require("../graphql-interface/symbols/all"); +const treeFind = require("../tree-find"); +const treeMapAsync = require("../tree-map-async"); + +function makePredicate({ path, name }) { + if (path != null) { + return (device) => device.path === path; + } else if (name != null) { + return (device) => device.name === name; + } else { + unreachable("No selector specified for lsblk"); + } +} + +module.exports = function () { + let lsblkOnce = memoizee(async () => { + return treeMapAsync(await lsblk(), async (device) => { + return { + ... device, + path: await fs.realpath(device.path) + }; + }); + }); + + return async function (selectors) { + let blockDeviceTree = await lsblkOnce(); + + return selectors.map((selector) => { + if (selector === All) { + return blockDeviceTree; + } else { + return treeFind(blockDeviceTree, makePredicate(selector)); + } + }); + }; +}; diff --git a/src/packages/sysquery-lvm/index.js b/src/packages/sysquery-lvm/index.js index 2526693..412d513 100644 --- a/src/packages/sysquery-lvm/index.js +++ b/src/packages/sysquery-lvm/index.js @@ -9,7 +9,7 @@ const lvm = require("../exec-lvm"); const All = require("../graphql-interface/symbols/all"); module.exports = { - name: "LVM", + name: "sysquery.lvm", makeContext: function () { return { physicalVolumes: new DataLoader(evaluateAndPick({ diff --git a/src/packages/tree-filter-flat-async/index.js b/src/packages/tree-filter-flat-async/index.js new file mode 100644 index 0000000..6b7eb18 --- /dev/null +++ b/src/packages/tree-filter-flat-async/index.js @@ -0,0 +1,53 @@ +"use strict"; + +const isIterable = require("is-iterable"); +const mapAsync = require("../map-async"); + +const Omitted = Symbol("Omitted"); + +async function testValue(value, predicate) { + return (await predicate(value)) + ? value + : Omitted; +} + +module.exports = async function treeFilterFlatAsync(tree, filter, options = {}) { + let key = options.key ?? "children"; + let recurseFilteredSubtrees = options.recurseFilteredSubtrees ?? false; + let concurrent = options.concurrent ?? true; + let depthFirst = options.depthFirst ?? false; // NOTE: Only affects output order, not evaluation order + + let results = []; + + async function step(subtree) { + // NOTE: The reason that we're tracking promises rather than outcomes, is because we want to make sure that (in breadth-first mode) we *immediately* push it into the results array, before the first async yield point. This is to ensure that the output is always in a consistent breadth-first order, even if the evaluation itself (being async) is out-of-order. This array of promises is resolved and post-processed at the end. + // TODO: Consider rewriting this so that the step function *returns* a (correctly-ordered) array instead; however, this would require possibly a lot of array concatenations, which may negatively impact performance + let resultPromise = await testValue(subtree, filter); + + if (!depthFirst) { + results.push(resultPromise); + } + + if (recurseFilteredSubtrees || (await resultPromise) !== Omitted) { + if (isIterable(subtree[key])) { + await mapAsync(subtree[key], step, { concurrent: concurrent }); + } else if (subtree[key] != null && typeof subtree[key] === "object") { + await step(subtree[key]); + } + } + + if (depthFirst) { + results.push(resultPromise); + } + + return resultPromise; + } + + if (isIterable(tree)) { + await mapAsync(tree, step, { concurrent: concurrent }); + } else { + await step(tree); + } + + return results.filter((value) => value !== Omitted); +}; diff --git a/src/packages/tree-filter-flat/index.js b/src/packages/tree-filter-flat/index.js new file mode 100644 index 0000000..a009086 --- /dev/null +++ b/src/packages/tree-filter-flat/index.js @@ -0,0 +1,39 @@ +"use strict"; + +// FIXME: Untested + +const isIterable = require("is-iterable"); +const forEach = require("../for-each"); + +module.exports = function treeFilterFlat(tree, filter, options = {}) { + let key = options.key ?? "children"; + let recurseFilteredSubtrees = options.recurseFilteredSubtrees ?? false; + + let results = []; + + function step(subtree) { + let matchesFilter = filter(subtree); + + if (matchesFilter || recurseFilteredSubtrees) { + if (isIterable(subtree[key])) { + for (let item of subtree[key]) { + step(item); + } + } else if (subtree[key] != null && typeof subtree[key] === "object") { + step(subtree[key]); + } + } + + if (matchesFilter) { + results.push(subtree); + } + } + + if (isIterable(tree)) { + forEach(tree, step); + } else { + step(tree); + } + + return results; +}; diff --git a/src/packages/find-in-tree/example.js b/src/packages/tree-find/example.js similarity index 59% rename from src/packages/find-in-tree/example.js rename to src/packages/tree-find/example.js index bbd31bd..e29af74 100644 --- a/src/packages/find-in-tree/example.js +++ b/src/packages/tree-find/example.js @@ -18,5 +18,5 @@ let tree = [{ name: "b" }]; -console.log(findInTree({ tree, predicate: (item) => item.name === "a2" })); -console.log(findInTree({ tree, predicate: (item) => item.name === "nonexistent" })); +console.log(findInTree(tree, (item) => item.name === "a2")); +console.log(findInTree(tree, (item) => item.name === "nonexistent")); diff --git a/src/packages/tree-find/index.js b/src/packages/tree-find/index.js new file mode 100644 index 0000000..aaeb954 --- /dev/null +++ b/src/packages/tree-find/index.js @@ -0,0 +1,43 @@ +"use strict"; + +const isIterable = require("is-iterable"); +const firstValue = require("../first-value"); + +const { validateArguments } = require("@validatem/core"); +const required = require("@validatem/required"); +const isFunction = require("@validatem/is-function"); +const isString = require("@validatem/is-string"); +const defaultTo = require("@validatem/default-to"); + +module.exports = function findInTree(_tree, _predicate, _options) { + let [ tree, predicate, { key } ] = validateArguments(arguments, { + tree: [ required ], // TODO: Make stricter? + predicate: [ required, isFunction ], + options: [ defaultTo({}), { + key: [ defaultTo("children", isString) ] + }] + }); + + function step(subtree) { + if (predicate(subtree)) { + return subtree; + } else { + let nextChild = subtree[key]; + + if (isIterable(nextChild)) { + return firstValue(nextChild, step); + } else if (nextChild != null && typeof nextChild === "object") { + return step(nextChild); + } else { + // TODO: Should an explicit sentinel value be used for this? + return undefined; + } + } + } + + let finalResult = isIterable(tree) + ? firstValue(tree, step) + : step(tree); + + return finalResult; +}; diff --git a/src/packages/tree-map-async/index.js b/src/packages/tree-map-async/index.js index 0bea081..7a306b5 100644 --- a/src/packages/tree-map-async/index.js +++ b/src/packages/tree-map-async/index.js @@ -1,21 +1,29 @@ "use strict"; -const Promise = require("bluebird"); -const treecutter = require("../treecutter"); +const isIterable = require("is-iterable"); +const mapAsync = require("../map-async"); -module.exports = function treeMapAsync(tree, mapper, returnBoth = false) { - return Promise.map(treecutter.flatten(tree), (item) => { - return mapper(item); - }).then((items) => { - let newTree = treecutter.rebuild(items); +module.exports = async function treeMapAsync(tree, mapper, options = {}) { + let key = options.key ?? "children"; - if (returnBoth) { - return { - tree: newTree, - list: items - }; - } else { - return newTree; + async function step(subtree) { + let mapped = await mapper(subtree); + let modifiedProperties = {}; + + if (isIterable(mapped[key])) { + modifiedProperties[key] = await mapAsync(mapped[key], step); + } else if (mapped[key] != null && typeof mapped[key] === "object") { + modifiedProperties[key] = await step(mapped[key]); } - }); + + // We track modified properties separately and (immutably) merge them at the end, because it's the fastest way to ensure that we don't mutate the input object under any circumstances + return { + ... mapped, + ... modifiedProperties + }; + } + + return isIterable(tree) + ? mapAsync(tree, step) + : step(tree); }; diff --git a/src/packages/tree-map-async/old.js b/src/packages/tree-map-async/old.js new file mode 100644 index 0000000..e565f27 --- /dev/null +++ b/src/packages/tree-map-async/old.js @@ -0,0 +1,23 @@ +"use strict"; + +// TODO: Replace this with an implementation that doesn't use treecutter? As treecutter is pretty hacky + +const Promise = require("bluebird"); +const treecutter = require("../treecutter"); + +module.exports = function treeMapAsync(tree, mapper, returnBoth = false) { + return Promise.map(treecutter.flatten(tree), (item) => { + return mapper(item); + }).then((items) => { + let newTree = treecutter.rebuild(items); + + if (returnBoth) { + return { + tree: newTree, + list: items + }; + } else { + return newTree; + } + }); +}; diff --git a/src/packages/tree-map/index.js b/src/packages/tree-map/index.js new file mode 100644 index 0000000..3731208 --- /dev/null +++ b/src/packages/tree-map/index.js @@ -0,0 +1,31 @@ +"use strict"; + +const isIterable = require("is-iterable"); +const map = require("../map"); + +// FIXME: Untested + +module.exports = function treeMap(tree, mapper, options = {}) { + let key = options.key ?? "children"; + + function step(subtree) { + let mapped = mapper(subtree); + let modifiedProperties = {}; + + if (isIterable(mapped[key])) { + modifiedProperties[key] = mapped[key].map((item) => step(item)); + } else if (mapped[key] != null && typeof mapped[key] === "object") { + modifiedProperties[key] = step(mapped[key]); + } + + // We track modified properties separately and (immutably) merge them at the end, because it's the fastest way to ensure that we don't mutate the input object under any circumstances (even after we add multi-key support) + return { + ... mapped, + ... modifiedProperties + }; + } + + return isIterable(tree) + ? map(tree, step) + : step(tree); +}; diff --git a/try/api.js b/try/api.js index ff72c92..29685b2 100644 --- a/try/api.js +++ b/try/api.js @@ -39,16 +39,37 @@ const createAPI = require("../src/api"); // } // }; +// const query = { +// resources: { +// lvm: { +// createPhysicalVolume: { +// $arguments: { +// path: "/dev/loop3" +// }, +// path: true, +// totalSpace: true, +// freeSpace: true +// } +// } +// } +// }; + const query = { resources: { - lvm: { - createPhysicalVolume: { - $arguments: { - path: "/dev/loop3" - }, - path: true, + blockDevices: { + // $arguments: { names: ["sdb"] }, + name: true, + path: true, + mounts: { + mountpoint: true, + filesystem: true, totalSpace: true, - freeSpace: true + // children: { + // $recurse: true + // } + }, + children: { + $recurse: true } } }