From f55c25f1bc4a2268fad6edb67f9720914ddd178c Mon Sep 17 00:00:00 2001 From: Jonas Hermsmeier Date: Thu, 8 Mar 2018 17:33:17 +0100 Subject: [PATCH 1/3] feat(lib): Use lsblk directly, parse json output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This switches from using a bash script on linux to shelling out to `lsblk` directly, using its `--json` option to obtain & parse machine-readable output – removing the need for external script files entirely. Change-Type: minor --- lib/diskutil.js | 100 +++++++-------- lib/drivelist.js | 18 +-- lib/exec.js | 59 +++++++++ lib/execute.js | 180 -------------------------- lib/lsblk/index.js | 59 +++++++++ lib/lsblk/json.js | 92 ++++++++++++++ lib/lsblk/pairs.js | 101 +++++++++++++++ lib/parse.js | 114 ----------------- lib/scripts.json | 7 - package.json | 2 - scripts/compile.js | 35 ----- scripts/linux.sh | 127 ------------------ tests/diskutil.spec.js | 1 + tests/drivelist.spec.js | 165 ++++++++++++------------ tests/execute.spec.js | 215 ------------------------------- tests/parse.spec.js | 276 ---------------------------------------- tests/scripts.spec.js | 47 ------- 17 files changed, 444 insertions(+), 1154 deletions(-) create mode 100644 lib/exec.js delete mode 100644 lib/execute.js create mode 100644 lib/lsblk/index.js create mode 100644 lib/lsblk/json.js create mode 100644 lib/lsblk/pairs.js delete mode 100644 lib/parse.js delete mode 100644 lib/scripts.json delete mode 100644 scripts/compile.js delete mode 100755 scripts/linux.sh delete mode 100644 tests/execute.spec.js delete mode 100644 tests/parse.spec.js delete mode 100644 tests/scripts.spec.js diff --git a/lib/diskutil.js b/lib/diskutil.js index d9e58c83..f402c0b8 100644 --- a/lib/diskutil.js +++ b/lib/diskutil.js @@ -17,57 +17,7 @@ 'use strict'; const plist = require('fast-plist'); -const childProcess = require('child_process'); - -const run = (cmd, argv, callback) => { - let stdout = ''; - let stderr = ''; - - const proc = childProcess.spawn(cmd, argv) - .on('error', callback) - .on('exit', (code, signal) => { - let error = null; - let data = null; - - if (code == null || code !== 0) { - error = error || new Error(`Command "${cmd} ${argv.join(' ')}" exited unexpectedly with "${code || signal}"`); - error.stderr = stderr; - } - - // NOTE: diskutil outputs error data as plist if something - // goes wrong, hence we still attempt to parse it here - try { - data = plist.parse(stdout); - } catch (e) { - error = error || e; - } - - // NOTE: `diskutil list -plist` can give back 'null' data when recovering - // from a system sleep / stand-by - if (data == null) { - error = error || new Error(`Command "${cmd} ${argv.join(' ')}" returned without data`); - } - - callback(error, data); - }); - - proc.stdout.setEncoding('utf8'); - proc.stderr.setEncoding('utf8'); - - proc.stdout.on('readable', function() { - let data = null; - while (data = this.read()) { - stdout += data; - } - }); - - proc.stderr.on('readable', function() { - let data = null; - while (data = this.read()) { - stderr += data; - } - }); -}; +const exec = require('./exec'); const asyncMap = (items, iter, callback) => { const results = []; @@ -150,8 +100,37 @@ const setMountpoints = (devices, list) => { }); }; +const listAll = (callback) => { + const cmd = 'diskutil'; + const argv = [ 'list', '-plist' ]; + exec(cmd, argv, (error, stdout) => { + if (error) { + callback(error); + return; + } + + let data = null; + + // NOTE: diskutil outputs error data as plist if something + // goes wrong, hence we still attempt to parse it here + try { + data = plist.parse(stdout); + } catch (e) { + error = error || e; + } + + // NOTE: `diskutil list -plist` can give back 'null' data when recovering + // from a system sleep / stand-by + if (data == null) { + error = error || new Error(`Command "${cmd} ${argv.join(' ')}" returned without data`); + } + + callback(error, data); + }); +}; + const list = (callback) => { - run('diskutil', [ 'list', '-plist' ], (listError, globalList) => { + listAll((listError, globalList) => { if (listError) { callback(listError); return; @@ -162,7 +141,22 @@ const list = (callback) => { }); asyncMap(tasks, (devicePath, next) => { - run('diskutil', [ 'info', '-plist', devicePath ], next); + exec('diskutil', [ 'info', '-plist', devicePath ], (error, stdout) => { + let data = null; + try { + data = plist.parse(stdout); + } catch (e) { + return next(e); + } + + // NOTE: `diskutil` can return 'null' when recovering + // from a system sleep / stand-by + if (data == null) { + error = error || new Error(`Command "diskutil info -plist ${devicePath}}" returned without data`); + } + + next(error, data); + }); }, (infoErrors, results) => { const devices = results.filter((device, index) => { return device && !infoErrors[index]; diff --git a/lib/drivelist.js b/lib/drivelist.js index b38fb329..f4080c46 100644 --- a/lib/drivelist.js +++ b/lib/drivelist.js @@ -22,10 +22,8 @@ const os = require('os'); const bindings = require('bindings'); -const parse = require('./parse'); -const execute = require('./execute'); -const scripts = require('./scripts.json'); const diskutil = require('./diskutil'); +const lsblk = require('./lsblk'); /** * @summary List available drives @@ -57,19 +55,7 @@ exports.list = (callback) => { diskutil.list(callback); break; case 'linux': - if (!scripts[os.platform()]) { - callback(new Error(`Your OS is not supported by this module: ${os.platform()}`)); - return; - } - - execute.extractAndRun(scripts[os.platform()], (error, output) => { - if (error) { - callback(error); - return; - } - - callback(null, parse(output)); - }); + lsblk(callback); break; default: return callback(new Error(`Your OS is not supported by this module: ${os.platform()}`)); diff --git a/lib/exec.js b/lib/exec.js new file mode 100644 index 00000000..d0ed76b9 --- /dev/null +++ b/lib/exec.js @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const childProcess = require('child_process'); + +const exec = (cmd, argv, callback) => { + let stdout = ''; + let stderr = ''; + + const proc = childProcess.spawn(cmd, argv) + .on('error', callback) + .on('exit', (code, signal) => { + let error = null; + + if (code == null || code !== 0) { + error = error || new Error(`Command "${cmd} ${argv.join(' ')}" exited unexpectedly with "${code || signal}"`); + error.code = code; + error.signal = signal; + error.stdout = stdout; + error.stderr = stderr; + } + + callback(error, stdout, stderr); + }); + + proc.stdout.setEncoding('utf8'); + proc.stderr.setEncoding('utf8'); + + proc.stdout.on('readable', function() { + let data = null; + while (data = this.read()) { + stdout += data; + } + }); + + proc.stderr.on('readable', function() { + let data = null; + while (data = this.read()) { + stderr += data; + } + }); +}; + +module.exports = exec; diff --git a/lib/execute.js b/lib/execute.js deleted file mode 100644 index 547c9c66..00000000 --- a/lib/execute.js +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright 2017 Resin.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -const packageInfo = require('../package.json'); -const debug = require('debug')(packageInfo.name); -const fs = require('fs'); -const childProcess = require('child_process'); -const path = require('path'); -const os = require('os'); -const crypto = require('crypto'); - -/** - * @summary A temporary directory to store scripts - * @type {String} - * @constant - * - * @description - * Some users mount their system temporary directory with the `noexec` - * option, which means we can't execute the files we put there. - * - * As a workaround, we try to use XDG_RUNTIME_DIR, which is probably - * a better bet. - */ -const TMP_DIRECTORY = process.env.XDG_RUNTIME_DIR || os.tmpdir(); - -/** - * @summary Generate a tmp filename with full path of OS' tmp dir - * @function - * @private - * - * @param {String} extension - temporary file extension - * @returns {String} filename - * - * @example - * const filename = tmpFilename('.sh'); - */ -const tmpFilename = (extension) => { - const random = crypto.randomBytes(6).toString('hex'); - const filename = `${packageInfo.name}-${random}${extension}`; - return path.join(TMP_DIRECTORY, filename); -}; - -/** - * @summary Create a temporary script file with the given contents - * @function - * @private - * - * @param {String} extension - temporary script file extension - * @param {String} contents - temporary script contents - * @param {Function} callback - callback(error, temporaryPath) - * - * @example - * createTemporaryScriptFile('.sh', '#!/bin/bash\necho "Foo"', (error, temporaryPath) => { - * if (error) { - * throw error; - * } - * console.log(temporaryPath); - * }) - */ -const createTemporaryScriptFile = (extension, contents, callback) => { - const temporaryPath = tmpFilename(extension); - fs.writeFile(temporaryPath, contents, { - mode: 0o755 - }, (error) => { - debug('write %s:', temporaryPath, error || 'OK'); - callback(error, temporaryPath); - }); -}; - -/** - * @summary Execute a script - * @function - * @private - * - * @param {String} scriptPath - script path - * @param {Function} callback - callback (error, output) - * @param {Number} [times=5] - retry times - * - * @example - * executeScript('path/to/script.sh', (error, output) => { - * if (error) throw error; - * console.log(output); - * }); - */ -const executeScript = (scriptPath, callback, times) => { - times = times === undefined ? 5 : times; - childProcess.execFile('bash', [ scriptPath ], (error, stdout, stderr) => { - if (error) { - if (error.code === 'EAGAIN') { - if (times <= 0) { - error.message = [ - 'Looks like you hit the limit of the number of processes you can have at any given time,', - 'so the child process needed to scan the available drives could not be spawned.' - ].join(' '); - return callback(error); - } - - debug('Got EAGAIN, retrying...'); - return setTimeout(() => { - executeScript(scriptPath, callback, times - 1); - }, 500); - } - - error.message += ` (code ${error.code}, signal ${error.signal || 'none'})`; - debug('error:', error); - debug('stderr: %s', stderr); - debug('stdout: %s', stdout); - return callback(error); - } - - // Don't throw an error if we get `stderr` output from - // the drive detection scripts at this point, given that - // if the script already exitted with code zero, then - // we consider them warnings that we can safely ignore. - if (stderr.trim().length) { - debug('stderr: %s', stderr); - } - - callback(null, stdout); - }); -}; - -/** - * @summary Extract and run an inline script - * @function - * @public - * - * @param {Object} script - script - * @param {String} script.content - script content - * @param {String} script.originalFilename - script original filename - * @param {Function} callback - callback(error, output) - * - * @example - * scripts.run({ - * content: '#!/bin/bash\necho Foo', - * originalFilename: 'myscript.sh' - * }, (error, output) => { - * if (error) { - * throw error; - * } - * - * console.log(output); - * }); - */ -exports.extractAndRun = (script, callback) => { - const extension = path.extname(script.originalFilename); - - createTemporaryScriptFile(extension, script.content, (writeError, temporaryPath) => { - if (writeError) { - return callback(writeError); - } - - executeScript(temporaryPath, (executeError, output) => { - - // Attempt to clean up, but ignore failure - fs.unlink(temporaryPath, (unlinkError) => { - if (unlinkError) { - debug('unlink error', unlinkError); - } - - callback(executeError, output); - }); - }); - }); -}; diff --git a/lib/lsblk/index.js b/lib/lsblk/index.js new file mode 100644 index 00000000..b3254c82 --- /dev/null +++ b/lib/lsblk/index.js @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const exec = require('../exec'); +const parsePairs = require('./pairs'); +const parseJSON = require('./json'); + +let SUPPORTS_JSON = true; + +const lsblk = (callback) => { + const cmd = 'lsblk'; + const argv = [ '--bytes' ]; + + if (SUPPORTS_JSON) { + argv.push('--all', '--json', '--paths', '--output-all'); + } else { + argv.push('--pairs', '--all'); + } + + exec(cmd, argv, (error, stdout) => { + if (error && SUPPORTS_JSON) { + SUPPORTS_JSON = false; + lsblk(callback); + return; + } else if (error) { + callback(error); + return; + } + + let devices = null; + + try { + devices = SUPPORTS_JSON ? parseJSON(stdout) : parsePairs(stdout); + } catch (e) { + callback(e); + return; + } + + callback(null, devices); + }); + +}; + +module.exports = lsblk; diff --git a/lib/lsblk/json.js b/lib/lsblk/json.js new file mode 100644 index 00000000..46d49b21 --- /dev/null +++ b/lib/lsblk/json.js @@ -0,0 +1,92 @@ +/* + * Copyright 2018 Resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const getMountpoints = (children) => { + return children.filter((child) => { + return child.mountpoint; + }).map((child) => { + return { + path: child.mountpoint, + label: child.label || child.partlabel + }; + }); +}; + +const getDescription = (device) => { + const description = [ + device.label || '', + device.vendor || '', + device.model || '' + ]; + if (device.children) { + let subLabels = device.children + .filter((c) => { + return c.label && c.label !== device.label; + }) + .map((c) => { + return c.label; + }); + subLabels = Array.from(new Set(subLabels)); + if (subLabels.length) { + description.push(`(${subLabels.join(', ')})`); + } + } + return description.join(' ').replace(/\s+/g, ' ').trim(); +}; + +const transform = (data) => { + return data.blockdevices.filter((device) => { + // Omit loop devices, CD/DVD drives, and RAM + return !device.name.startsWith('/dev/loop') + && !device.name.startsWith('/dev/sr') + && !device.name.startsWith('/dev/ram'); + }).map((device) => { + const isVirtual = /^(block)$/i.test(device.subsystems); + const isSCSI = /^(sata|scsi|ata|ide|pci)$/i.test(device.tran); + const isUSB = /^(usb)$/i.test(device.tran); + const isReadOnly = Number(device.ro) === 1; + const isRemovable = Number(device.rm) === 1 || isVirtual; + return { + enumerator: 'lsblk', + busType: (device.tran || 'UNKNOWN').toUpperCase(), + busVersion: null, + device: device.name, + raw: device.kname, + description: getDescription(device), + error: null, + size: Number(device.size) || null, + blockSize: Number(device['phy-sec']) || null, + logicalBlockSize: Number(device['log-sec']) || null, + mountpoints: device.children ? getMountpoints(device.children) : [], + isReadOnly: isReadOnly, + isSystem: !isRemovable && !isVirtual, + isVirtual: isVirtual, + isRemovable: isRemovable, + isCard: null, + isSCSI: isSCSI, + isUSB: isUSB, + isUAS: null + }; + }); +}; + +const parse = (stdout) => { + return transform(JSON.parse(stdout)); +}; + +module.exports = parse; diff --git a/lib/lsblk/pairs.js b/lib/lsblk/pairs.js new file mode 100644 index 00000000..44afcaa1 --- /dev/null +++ b/lib/lsblk/pairs.js @@ -0,0 +1,101 @@ +/* + * Copyright 2018 Resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const parseLsblkLine = (line) => { + + const data = {}; + let offset = 0; + let key = ''; + let value = ''; + + const keyChar = /[^"=]/; + const whitespace = /\s+/; + const escape = '\\'; + let state = 'key'; + + while (offset < line.length) { + if (state === 'key') { + while (keyChar.test(line[offset])) { + key += line[offset]; + offset += 1; + } + if (line[offset] === '=') { + state = 'value'; + offset += 1; + } + } else if (state === 'value') { + if (line[offset] !== '"') { + throw new Error(`Expected '"', saw "${line[offset]}"`); + } + offset += 1; + while (line[offset] !== '"' && line[offset - 1] !== escape) { + value += line[offset]; + offset += 1; + } + if (line[offset] !== '"') { + throw new Error(`Expected '"', saw "${line[offset]}"`); + } + offset += 1; + data[key.toLowerCase()] = value.trim(); + key = ''; + value = ''; + state = 'space'; + } else if (state === 'space') { + while (whitespace.test(line[offset])) { + offset += 1; + } + state = 'key'; + } else { + throw new Error(`Undefined state "${state}"`); + } + } + + return data; + +}; + +const parseLsblk = (output) => { + return output.trim().split(/\r?\n/g).map(parseLsblkLine); +}; + +const consolidate = (devices) => { + + const primaries = devices.filter((device) => { + return device.pkname === ''; + }); + + primaries.forEach((device) => { + device.mountpoints = devices.filter((child) => { + return child.pkname === device.kname; + }).map((child) => { + return { + path: child.mountpoint, + label: child.label + }; + }); + }); + + return primaries; + +}; + +const parse = (stdout) => { + return consolidate(parseLsblk(stdout)); +}; + +module.exports = parse; diff --git a/lib/parse.js b/lib/parse.js deleted file mode 100644 index aa0f5b6a..00000000 --- a/lib/parse.js +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2016 Resin.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -const yaml = require('js-yaml'); - -/** - * @summary Parse drivelist scripts output - * @function - * @public - * - * @param {String} input - input text - * @returns {Object} parsed drivelist script output - * - * @example - * const drives = parse([ - * 'device: /dev/disk1', - * 'description: Macintosh HD', - * 'size: 249.8 GB', - * 'mountpoint: /', - * '', - * 'device: /dev/disk2', - * 'description: elementary OS', - * 'size: 15.7 GB', - * 'mountpoint: /Volumes/Elementary' - * ].join('\n')); - * - * console.log(drives); - * - * > [ - * > { - * > device: '/dev/disk1', - * > description: 'Macintosh HD', - * > size: '249.8 GB', - * > mountpoint: '/' - * > } - * > , - * > { - * > device: '/dev/disk2', - * > description: 'elementary OS', - * > size: '15.7 GB', - * > mountpoint: '/Volumes/Elementary' - * > } - * > ] - */ -module.exports = (input) => { - if (!input || !input.trim()) { - return []; - } - - const allowedEscapes = [ 'b', 'f', 'n', 'r', 't', 'v' ]; - - return input.split(/\n\s*\n/g) - .map((device) => { - device = device.split(/\r?\n/g) - .filter((line) => { - return /^(\s\s-\s)?[a-z]+:/i.test(line); - }) - .map((line) => { - return line - .replace(/\\[^.\\]/g, (match, index, string) => { - const escapedCharacter = match[match.length - 1]; - - // Remove non printable ascii characters - // See http://stackoverflow.com/a/24229554 - if (string[index - 1] === '\\' || allowedEscapes.indexOf(escapedCharacter) !== -1) { - return match; - } - - return escapedCharacter; - }) - .replace(/[^\x20-\x7E]+/g, '') - .replace(/"/g, (match, index, string) => { - if (string.indexOf('"') === index || string.lastIndexOf('"') === index) { - return match; - } - - return '\\"'; - }); - }) - .join('\n'); - - const result = yaml.safeLoad(device); - - if (typeof result === 'string') { - const data = {}; - data[result] = null; - return data; - } - - if (!result || !result.device) { - return null; - } - - return result; - }) - .filter((result) => { - return Boolean(result); - }); -}; diff --git a/lib/scripts.json b/lib/scripts.json deleted file mode 100644 index 513a72dd..00000000 --- a/lib/scripts.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "linux": { - "content": "#!/bin/bash\n\nset -u\nset -e\n\nignore_first_line() {\n tail -n +2\n}\n\nget_uuids() {\n /sbin/blkid -s UUID -o value \"$1\"*\n}\n\nget_mountpoints() {\n grep \"^$1\" /proc/mounts | cut -d ' ' -f 2 | sed 's,\\\\040, ,g' | sed 's,\\\\011,\\t,g' | sed 's,\\\\012,\\\\n,g' | sed 's,\\\\134,\\\\\\\\,g'\n}\n\nget_path_id() {\n # udevadm test-builtin path_id /sys/block/sda | grep ID_PATH=\n find -L /dev/disk/by-path/ -samefile \"$1\" | cut -c 19-\n}\n\nDISKS=\"$(lsblk -a -d --output NAME | ignore_first_line)\"\n\nfor disk in $DISKS; do\n\n # Omit loop devices, CD/DVD drives, and RAM\n if [[ $disk == loop* ]] || [[ $disk == sr* ]] || [[ $disk == ram* ]]; then\n continue\n fi\n\n device=\"/dev/$disk\"\n diskinfo=($(lsblk -b -d -a \"$device\" --output SIZE,RO,RM,MODEL | ignore_first_line))\n\n # Omit drives for which `lsblk` failed, which means they\n # were unplugged right after we got the list of all drives\n if [ -z \"${diskinfo-}\" ]; then\n continue\n fi\n\n size=${diskinfo[0]}\n protected=${diskinfo[1]}\n removable=${diskinfo[2]}\n description=${diskinfo[*]:3}\n mountpoints=\"$(get_mountpoints \"$device\")\"\n devicePath=\"$(get_path_id \"$device\")\"\n\n # If we couldn't get the mount points as `/dev/$disk`,\n # get the disk UUIDs, and check as `/dev/disk/by-uuid/$uuid`\n if [ -z \"$mountpoints\" ]; then\n for uuid in $(get_uuids \"$device\"); do\n mountpoints=\"$mountpoints$(get_mountpoints \"/dev/disk/by-uuid/$uuid\")\"\n done\n fi\n\n # If we couldn't get the description from `lsblk`, see if we can get it\n # from sysfs (e.g. PCI-connected SD cards that appear as `/dev/mmcblk0`)\n if [ -z \"$description\" ]; then\n subdevice=\"$(echo \"$device\" | cut -d '/' -f 3)\"\n if [ -f \"/sys/class/block/$subdevice/device/name\" ]; then\n description=\"$(cat \"/sys/class/block/$subdevice/device/name\")\"\n fi\n fi\n\n echo \"enumerator: lsblk\"\n echo \"busType: UNKNOWN\"\n echo \"busVersion: \\\"0.0\\\"\"\n echo \"device: $device\"\n echo \"devicePath: $devicePath\"\n echo \"raw: $device\"\n echo \"description: \\\"$description\\\"\"\n echo \"error: null\"\n echo \"size: $size\"\n echo \"blockSize: null\"\n echo \"logicalBlockSize: null\"\n\n if [ -z \"$mountpoints\" ]; then\n echo \"mountpoints: []\"\n else\n echo \"mountpoints:\"\n echo \"$mountpoints\" | while read -r mountpoint ; do\n echo \" - path: \\\"$mountpoint\\\"\"\n done\n fi\n\n if [[ \"$protected\" == \"1\" ]]; then\n echo \"isReadOnly: True\"\n else\n echo \"isReadOnly: False\"\n fi\n\n eval \"$(udevadm info \\\n --query=property \\\n --export \\\n --export-prefix=UDEV_ \\\n --name=\"$disk\" \\\n | awk -F= '{gsub(\"\\\\.\",\"_\",$1); print $1 \"=\" $2}')\"\n\n set +u\n\n if [[ \"$removable\" == \"1\" ]] && \\\n [[ \"$UDEV_ID_DRIVE_FLASH_SD\" == \"1\" ]] || \\\n [[ \"$UDEV_ID_DRIVE_MEDIA_FLASH_SD\" == \"1\" ]] || \\\n [[ \"$UDEV_ID_BUS\" == \"usb\" ]]\n then\n echo \"isSystem: False\"\n else\n echo \"isSystem: True\"\n fi\n\n echo \"isVirtual: null\"\n echo \"isRemovable: null\"\n echo \"isCard: null\"\n echo \"isSCSI: null\"\n echo \"isUSB: null\"\n echo \"isUAS: null\"\n\n set -u\n\n # Unset UDEV variables used above to prevent them from\n # being interpreted as properties of another drive\n unset UDEV_ID_DRIVE_FLASH_SD\n unset UDEV_ID_DRIVE_MEDIA_FLASH_SD\n unset UDEV_ID_BUS\n\n echo \"\"\ndone\n", - "originalFilename": "linux.sh", - "type": "text" - } -} \ No newline at end of file diff --git a/package.json b/package.json index ad3e0c2f..b91de049 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ }, "scripts": { "test": "npm run lint && mocha --recursive tests -R spec", - "compile-scripts": "node scripts/compile.js", "lint": "eslint lib tests example && cpplint --recursive src", "readme": "jsdoc2md --template doc/README.hbs lib/drivelist.js > README.md", "configure": "node-gyp configure", @@ -48,7 +47,6 @@ "bindings": "^1.3.0", "debug": "^3.1.0", "fast-plist": "^0.1.2", - "js-yaml": "^3.11.0", "nan": "^2.10.0", "prebuild-install": "^4.0.0" } diff --git a/scripts/compile.js b/scripts/compile.js deleted file mode 100644 index c2d0aee2..00000000 --- a/scripts/compile.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2016 Resin.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -const fs = require('fs'); -const path = require('path'); - -const scripts = { - linux: path.join(__dirname, 'linux.sh') -}; - -const object = { - linux: { - content: fs.readFileSync(scripts.linux, { encoding: 'utf8' }), - originalFilename: path.basename(scripts.linux), - type: 'text' - } -}; - -fs.writeFileSync(path.join(__dirname, '..', 'lib', 'scripts.json'), JSON.stringify(object, null, 2)); - diff --git a/scripts/linux.sh b/scripts/linux.sh deleted file mode 100755 index bb9c0394..00000000 --- a/scripts/linux.sh +++ /dev/null @@ -1,127 +0,0 @@ -#!/bin/bash - -set -u -set -e - -ignore_first_line() { - tail -n +2 -} - -get_uuids() { - /sbin/blkid -s UUID -o value "$1"* -} - -get_mountpoints() { - grep "^$1" /proc/mounts | cut -d ' ' -f 2 | sed 's,\\040, ,g' | sed 's,\\011,\t,g' | sed 's,\\012,\\n,g' | sed 's,\\134,\\\\,g' -} - -get_path_id() { - # udevadm test-builtin path_id /sys/block/sda | grep ID_PATH= - find -L /dev/disk/by-path/ -samefile "$1" | cut -c 19- -} - -DISKS="$(lsblk -a -d --output NAME | ignore_first_line)" - -for disk in $DISKS; do - - # Omit loop devices, CD/DVD drives, and RAM - if [[ $disk == loop* ]] || [[ $disk == sr* ]] || [[ $disk == ram* ]]; then - continue - fi - - device="/dev/$disk" - diskinfo=($(lsblk -b -d -a "$device" --output SIZE,RO,RM,MODEL | ignore_first_line)) - - # Omit drives for which `lsblk` failed, which means they - # were unplugged right after we got the list of all drives - if [ -z "${diskinfo-}" ]; then - continue - fi - - size=${diskinfo[0]} - protected=${diskinfo[1]} - removable=${diskinfo[2]} - description=${diskinfo[*]:3} - mountpoints="$(get_mountpoints "$device")" - devicePath="$(get_path_id "$device")" - - # If we couldn't get the mount points as `/dev/$disk`, - # get the disk UUIDs, and check as `/dev/disk/by-uuid/$uuid` - if [ -z "$mountpoints" ]; then - for uuid in $(get_uuids "$device"); do - mountpoints="$mountpoints$(get_mountpoints "/dev/disk/by-uuid/$uuid")" - done - fi - - # If we couldn't get the description from `lsblk`, see if we can get it - # from sysfs (e.g. PCI-connected SD cards that appear as `/dev/mmcblk0`) - if [ -z "$description" ]; then - subdevice="$(echo "$device" | cut -d '/' -f 3)" - if [ -f "/sys/class/block/$subdevice/device/name" ]; then - description="$(cat "/sys/class/block/$subdevice/device/name")" - fi - fi - - echo "enumerator: lsblk" - echo "busType: UNKNOWN" - echo "busVersion: \"0.0\"" - echo "device: $device" - echo "devicePath: $devicePath" - echo "raw: $device" - echo "description: \"$description\"" - echo "error: null" - echo "size: $size" - echo "blockSize: null" - echo "logicalBlockSize: null" - - if [ -z "$mountpoints" ]; then - echo "mountpoints: []" - else - echo "mountpoints:" - echo "$mountpoints" | while read -r mountpoint ; do - echo " - path: \"$mountpoint\"" - done - fi - - if [[ "$protected" == "1" ]]; then - echo "isReadOnly: True" - else - echo "isReadOnly: False" - fi - - eval "$(udevadm info \ - --query=property \ - --export \ - --export-prefix=UDEV_ \ - --name="$disk" \ - | awk -F= '{gsub("\\.","_",$1); print $1 "=" $2}')" - - set +u - - if [[ "$removable" == "1" ]] && \ - [[ "$UDEV_ID_DRIVE_FLASH_SD" == "1" ]] || \ - [[ "$UDEV_ID_DRIVE_MEDIA_FLASH_SD" == "1" ]] || \ - [[ "$UDEV_ID_BUS" == "usb" ]] - then - echo "isSystem: False" - else - echo "isSystem: True" - fi - - echo "isVirtual: null" - echo "isRemovable: null" - echo "isCard: null" - echo "isSCSI: null" - echo "isUSB: null" - echo "isUAS: null" - - set -u - - # Unset UDEV variables used above to prevent them from - # being interpreted as properties of another drive - unset UDEV_ID_DRIVE_FLASH_SD - unset UDEV_ID_DRIVE_MEDIA_FLASH_SD - unset UDEV_ID_BUS - - echo "" -done diff --git a/tests/diskutil.spec.js b/tests/diskutil.spec.js index 68de0acd..0715e9c3 100644 --- a/tests/diskutil.spec.js +++ b/tests/diskutil.spec.js @@ -83,6 +83,7 @@ describe('Drivelist', function() { m.chai.expect(infoError).to.be.an('error'); m.chai.expect(infoError.message).to.startsWith('Command "'); m.chai.expect(infoError.message).to.endsWith('" returned without data'); + childProcess.spawn.restore(); }); }); diff --git a/tests/drivelist.spec.js b/tests/drivelist.spec.js index 740fb4d2..8319facd 100644 --- a/tests/drivelist.spec.js +++ b/tests/drivelist.spec.js @@ -18,95 +18,96 @@ const m = require('mochainon'); const os = require('os'); -const execute = require('../lib/execute'); -const drivelist = require('../lib/drivelist'); -const scripts = require('../lib/scripts.json'); +const assert = require('assert'); +const drivelist = require('..'); describe('Drivelist', function() { describe('.list()', function() { - describe('given scripts run succesfully', function() { - - beforeEach(function() { - this.executeExtractAndRunStub = m.sinon.stub(execute, 'extractAndRun'); - - this.executeExtractAndRunStub.withArgs(scripts.linux).yields(null, [ - 'enumerator: lsblk', - 'busType: UNKNOWN', - 'busVersion: "0.0"', - 'device: /dev/sda', - 'raw: /dev/sda', - 'description: "Samsung SSD 850"', - 'error: null', - 'size: 120034123776', - 'blockSize: null', - 'logicalBlockSize: null', - 'mountpoints:', - ' - path: "/"', - ' - path: "/boot/efi"', - 'isReadOnly: False', - 'isSystem: True', - 'isVirtual: null', - 'isRemovable: null', - 'isCard: null', - 'isSCSI: null', - 'isUSB: null', - 'isUAS: null' - ].join('\n')); - }); - - afterEach(function() { - this.executeExtractAndRunStub.restore(); - }); - - describe('given linux', function() { - - beforeEach(function() { - this.osPlatformStub = m.sinon.stub(os, 'platform'); - this.osPlatformStub.returns('linux'); - }); - - afterEach(function() { - this.osPlatformStub.restore(); + it('should yield results', function(done) { + drivelist.list((error, devices) => { + if (error) { + console.log('stdout:\n' + error.stdout); + console.log('stderr:\n' + error.stderr); + assert.ifError(error); + } + devices.forEach((device) => { + assert.ok( + device.enumerator, + `Invalid enumerator: ${device.enumerator}` + ); + assert.ok( + device.busType, + `Invalid busType: ${device.busType}` + ); + assert.ok( + device.device, + `Invalid device: ${device.device}` + ); + assert.ok( + device.raw, + `Invalid raw: ${device.raw}` + ); + assert.ok( + device.description, + `Invalid description: ${device.description}` + ); + assert.ok( + device.error === null, + `Invalid error: ${device.error}` + ); + assert.ok( + Number.isFinite(device.size), + `Invalid size: ${device.size}` + ); + assert.ok( + Number.isFinite(device.blockSize), + `Invalid blockSize: ${device.blockSize}` + ); + assert.ok( + Number.isFinite(device.logicalBlockSize), + `Invalid logicalBlockSize: ${device.logicalBlockSize}` + ); + assert.ok( + Array.isArray(device.mountpoints), + `Invalid mountpoints: ${device.mountpoints}` + ); + assert.ok( + device.isReadOnly == null || typeof device.isReadOnly === 'boolean', + `Invalid isReadOnly flag: ${device.isReadOnly}` + ); + assert.ok( + device.isSystem == null || typeof device.isSystem === 'boolean', + `Invalid isSystem flag: ${device.isSystem}` + ); + assert.ok( + device.isVirtual == null || typeof device.isVirtual === 'boolean', + `Invalid isVirtual flag: ${device.isVirtual}` + ); + assert.ok( + device.isRemovable == null || typeof device.isRemovable === 'boolean', + `Invalid isRemovable flag: ${device.isRemovable}` + ); + assert.ok( + device.isCard == null || typeof device.isCard === 'boolean', + `Invalid isCard flag: ${device.isCard}` + ); + assert.ok( + device.isSCSI == null || typeof device.isSCSI === 'boolean', + `Invalid isSCSI flag: ${device.isSCSI}` + ); + assert.ok( + device.isUSB == null || typeof device.isUSB === 'boolean', + `Invalid isUSB flag: ${device.isUSB}` + ); + assert.ok( + device.isUAS == null || typeof device.isUAS === 'boolean', + `Invalid isUAS flag: ${device.isUAS}` + ); }); - - it('should execute the linux script', function(done) { - drivelist.list((error, drives) => { - m.chai.expect(error).to.not.exist; - m.chai.expect(drives).to.deep.equal([ - { - enumerator: 'lsblk', - busType: 'UNKNOWN', - busVersion: '0.0', - device: '/dev/sda', - raw: '/dev/sda', - description: 'Samsung SSD 850', - error: null, - size: 120034123776, - blockSize: null, - logicalBlockSize: null, - mountpoints: [ { - path: '/' - }, { - path: '/boot/efi' - } ], - isReadOnly: false, - isSystem: true, - isVirtual: null, - isRemovable: null, - isCard: null, - isSCSI: null, - isUSB: null, - isUAS: null - } - ]); - done(); - }); - }); - + done(); }); - }); describe('given an unsupported os', function() { diff --git a/tests/execute.spec.js b/tests/execute.spec.js deleted file mode 100644 index 7f642ab1..00000000 --- a/tests/execute.spec.js +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright 2016 Resin.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -const m = require('mochainon'); -const os = require('os'); -const childProcess = require('child_process'); -const execute = require('../lib/execute'); - -describe('Execute', function() { - - describe('.extractAndRun()', function() { - - it('should be able to execute a script', function(done) { - this.timeout(5000); - - const script = (() => { - if (os.platform() === 'win32') { - return { - content: '@echo off\necho foo bar baz', - originalFilename: 'hello.bat' - }; - } - - return { - content: '#!/bin/bash\necho "foo bar baz"', - originalFilename: 'hello.sh' - }; - })(); - - execute.extractAndRun(script, (error, output) => { - m.chai.expect(error).to.not.exist; - - // The purpose of the trim is to get rid of - // operating system-specific line endings - m.chai.expect(output.trim()).to.equal('foo bar baz'); - - done(); - }); - }); - - describe('given an error when running the script', function() { - - beforeEach(function() { - this.childProcessExecFileStub = m.sinon.stub(childProcess, 'execFile'); - const error = new Error('script error'); - error.code = 27; - this.childProcessExecFileStub.yields(error); - }); - - afterEach(function() { - this.childProcessExecFileStub.restore(); - }); - - it('should yield the error', function(done) { - execute.extractAndRun({ - content: 'dummy content', - originalFilename: 'foo' - }, (error, output) => { - m.chai.expect(error).to.be.an.instanceof(Error); - m.chai.expect(error.message).to.equal('script error (code 27, signal none)'); - m.chai.expect(output).to.not.exist; - done(); - }); - }); - - }); - - describe('given EAGAIN errors and then success when running the script', function() { - - beforeEach(function() { - this.childProcessExecFileStub = m.sinon.stub(childProcess, 'execFile'); - const error = new Error('EAGAIN'); - error.code = 'EAGAIN'; - this.childProcessExecFileStub.onFirstCall().yields(error); - this.childProcessExecFileStub.onSecondCall().yields(error); - this.childProcessExecFileStub.onThirdCall().yields(error); - this.childProcessExecFileStub.yields(null, 'foo bar baz', ''); - }); - - afterEach(function() { - this.childProcessExecFileStub.restore(); - }); - - it('should eventually yield the command output', function(done) { - this.timeout(5000); - execute.extractAndRun({ - content: 'dummy content', - originalFilename: 'foo' - }, (error, output) => { - m.chai.expect(error).to.not.exist; - m.chai.expect(output).to.equal('foo bar baz'); - done(); - }); - }); - - }); - - describe('given EAGAIN errors when running the script', function() { - - beforeEach(function() { - this.childProcessExecFileStub = m.sinon.stub(childProcess, 'execFile'); - const error = new Error('EAGAIN'); - error.code = 'EAGAIN'; - this.childProcessExecFileStub.yields(error); - }); - - afterEach(function() { - this.childProcessExecFileStub.restore(); - }); - - it('should eventually yield the error', function(done) { - this.timeout(5000); - execute.extractAndRun({ - content: 'dummy content', - originalFilename: 'foo' - }, (error, output) => { - m.chai.expect(error).to.be.an.instanceof(Error); - m.chai.expect(error.code).to.equal('EAGAIN'); - m.chai.expect(output).to.not.exist; - done(); - }); - }); - - }); - - describe('given the script outputs to stderr with exit code 0', function() { - - beforeEach(function() { - this.childProcessExecFileStub = m.sinon.stub(childProcess, 'execFile'); - this.childProcessExecFileStub.yields(null, 'foo bar', 'script error'); - }); - - afterEach(function() { - this.childProcessExecFileStub.restore(); - }); - - it('should ignore stderr', function(done) { - execute.extractAndRun({ - content: 'dummy content', - originalFilename: 'foo' - }, (error, output) => { - m.chai.expect(error).to.not.exist; - m.chai.expect(output).to.equal('foo bar'); - done(); - }); - }); - - }); - - describe('given the script outputs to stdout and a blank string to stderr', function() { - - beforeEach(function() { - this.childProcessExecFileStub = m.sinon.stub(childProcess, 'execFile'); - this.childProcessExecFileStub.yields(null, 'foo bar', ' '); - }); - - afterEach(function() { - this.childProcessExecFileStub.restore(); - }); - - it('should yield the result', function(done) { - execute.extractAndRun({ - content: 'dummy content', - originalFilename: 'foo' - }, (error, output) => { - m.chai.expect(error).to.not.exist; - m.chai.expect(output).to.equal('foo bar'); - done(); - }); - }); - - }); - - describe('given the script outputs to stdout', function() { - - beforeEach(function() { - this.childProcessExecFileStub = m.sinon.stub(childProcess, 'execFile'); - this.childProcessExecFileStub.yields(null, 'foo bar', ''); - }); - - afterEach(function() { - this.childProcessExecFileStub.restore(); - }); - - it('should yield the result', function(done) { - execute.extractAndRun({ - content: 'dummy content', - originalFilename: 'foo' - }, (error, output) => { - m.chai.expect(error).to.not.exist; - m.chai.expect(output).to.equal('foo bar'); - done(); - }); - }); - - }); - - }); - -}); diff --git a/tests/parse.spec.js b/tests/parse.spec.js deleted file mode 100644 index b9834e97..00000000 --- a/tests/parse.spec.js +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright 2016 Resin.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -const m = require('mochainon'); -const parse = require('../lib/parse'); - -describe('Parse', function() { - - it('should return an empty object if no input', function() { - m.chai.expect(parse()).to.deep.equal([]); - }); - - it('should return an empty object if input is an empty string', function() { - m.chai.expect(parse('')).to.deep.equal([]); - }); - - it('should return an empty object if input is a string containing only spaces', function() { - m.chai.expect(parse(' ')).to.deep.equal([]); - }); - - it('should parse a single device', function() { - m.chai.expect(parse([ - 'device: /dev/disk1', - 'description: Macintosh HD', - 'size: 249.8 GB', - 'mountpoint: /' - ].join('\n'))).to.deep.equal([ - { - device: '/dev/disk1', - description: 'Macintosh HD', - size: '249.8 GB', - mountpoint: '/' - } - ]); - }); - - it('should parse multiple devices', function() { - m.chai.expect(parse([ - 'device: /dev/disk1', - 'description: Macintosh HD', - 'size: 249.8 GB', - 'mountpoint: /', - '', - 'device: /dev/disk2', - 'description: elementary OS', - 'size: 15.7 GB', - 'mountpoint: /Volumes/Elementary' - ].join('\n'))).to.deep.equal([ - { - device: '/dev/disk1', - description: 'Macintosh HD', - size: '249.8 GB', - mountpoint: '/' - }, - { - device: '/dev/disk2', - description: 'elementary OS', - size: '15.7 GB', - mountpoint: '/Volumes/Elementary' - } - ]); - }); - - it('should omit blank lines', function() { - m.chai.expect(parse([ - 'device: /dev/disk1', - 'description: Macintosh HD', - 'size: 249.8 GB', - 'mountpoint: /', - '', - 'device:', - 'description:', - 'size:', - 'mountpoint:' - ].join('\n'))).to.deep.equal([ - { - device: '/dev/disk1', - description: 'Macintosh HD', - size: '249.8 GB', - mountpoint: '/' - } - ]); - }); - - it('should ignore new lines after the output', function() { - m.chai.expect(parse([ - 'device: /dev/disk1', - 'description: Macintosh HD', - 'size: 249.8 GB', - 'mountpoint: /', - '', - 'device: /dev/disk2', - 'description: elementary OS', - 'size: 15.7 GB', - 'mountpoint: /Volumes/Elementary', - '', - '', - '' - ].join('\n'))).to.deep.equal([ - { - device: '/dev/disk1', - description: 'Macintosh HD', - size: '249.8 GB', - mountpoint: '/' - }, - { - device: '/dev/disk2', - description: 'elementary OS', - size: '15.7 GB', - mountpoint: '/Volumes/Elementary' - } - ]); - }); - - it('should parse a truthy boolean', function() { - m.chai.expect(parse([ - 'device: /dev/disk1', - 'hello: True' - ].join('\n'))).to.deep.equal([ - { - device: '/dev/disk1', - hello: true - } - ]); - }); - - it('should parse a falsy boolean', function() { - m.chai.expect(parse([ - 'device: /dev/disk1', - 'hello: False' - ].join('\n'))).to.deep.equal([ - { - device: '/dev/disk1', - hello: false - } - ]); - }); - - it('should ignore invalid keys', function() { - m.chai.expect(parse([ - '[0x7FFAC9E570E3] ANOMALY: use of REX.w', - 'device: /dev/disk2', - 'foo: foo', - 'this is a warning', - 'bar: bar' - ].join('\n'))).to.deep.equal([ - { - device: '/dev/disk2', - foo: 'foo', - bar: 'bar' - } - ]); - }); - - it('should parse multiple devices that are heterogeneous', function() { - m.chai.expect(parse([ - 'device: /dev/disk1', - 'hello: world', - 'foo: bar', - '', - 'device: /dev/disk2', - 'hey: there' - ].join('\n'))).to.deep.equal([ - { - device: '/dev/disk1', - hello: 'world', - foo: 'bar' - }, - { - device: '/dev/disk2', - hey: 'there' - } - ]); - }); - - it('should set null for values without keys', function() { - m.chai.expect(parse([ - 'device: /dev/disk1', - 'hello:' - ].join('\n'))).to.deep.equal([ - { - device: '/dev/disk1', - hello: null - } - ]); - }); - - it('should not interpret a word without colon as a key without value', function() { - m.chai.expect(parse([ 'hello' ].join('\n'))).to.deep.equal([]); - }); - - it('should not interpret multiple words without colon as a key without value', function() { - m.chai.expect(parse([ 'hello world' ].join('\n'))).to.deep.equal([]); - }); - - it('should handle a double quote inside a value', function() { - m.chai.expect(parse([ - 'device: /dev/disk1', - 'description: "SAMSUNG SSD PM810 2.5" 7mm 256GB"' - ].join('\n'))).to.deep.equal([ - { - device: '/dev/disk1', - description: 'SAMSUNG SSD PM810 2.5" 7mm 256GB' - } - ]); - }); - - it('should handle multiple double quotes inside a value', function() { - m.chai.expect(parse([ - 'device: /dev/disk1', - 'description: "SAMSUNG "SSD" PM810 2.5" 7mm 256GB"' - ].join('\n'))).to.deep.equal([ - { - device: '/dev/disk1', - description: 'SAMSUNG "SSD" PM810 2.5" 7mm 256GB' - } - ]); - }); - - it('should remove the backslash of unknown escape sequences inside values', function() { - m.chai.expect(parse([ - 'device: /dev/disk1', - 'description: "\\]=-01`23456\\78\\90\\=\\-0\\98e"' - ].join('\n'))).to.deep.equal([ - { - device: '/dev/disk1', - description: ']=-01`234567890=-098e' - } - ]); - }); - - it('should maintain known escape sequences', function() { - m.chai.expect(parse([ - 'device: /dev/disk1', - 'description: "foo\\bbar\\f\\nbaz\\r\\tqux\\v"' - ].join('\n'))).to.deep.equal([ - { - device: '/dev/disk1', - description: 'foo\bbar\f\nbaz\r\tqux\v' - } - ]); - }); - - it('should delete unicode characters inside a value', function() { - m.chai.expect(parse([ - 'device: /dev/disk1', - 'description: ""HCG8e\u0005" ^"', - 'foo: "StoreJet Transce\u0007\ufffd"', - 'printable: "\u0900\u04F5hello"' - - ].join('\n'))).to.deep.equal([ - { - device: '/dev/disk1', - description: '"HCG8e" ^', - foo: 'StoreJet Transce', - printable: 'hello' - } - ]); - }); - -}); diff --git a/tests/scripts.spec.js b/tests/scripts.spec.js deleted file mode 100644 index 591e09c8..00000000 --- a/tests/scripts.spec.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2017 Resin.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -const m = require('mochainon'); -const fs = require('fs'); -const path = require('path'); -const scripts = require('../lib/scripts.json'); - -describe('Scripts', function() { - - const checkCompiledScript = (script, scriptPath) => { - it(`should match ${path.basename(scriptPath)}`, function(done) { - fs.readFile(scriptPath, { - encoding: 'utf8' - }, (error, content) => { - m.chai.expect(error).to.not.exist; - m.chai.expect(script).to.deep.equal({ - originalFilename: path.basename(scriptPath), - content, - type: 'text' - }); - - done(); - }); - }); - }; - - describe('.linux', function() { - checkCompiledScript(scripts.linux, path.join(__dirname, '..', 'scripts', 'linux.sh')); - }); - -}); From fdee0f1b410b26f3ac06bc9ce4d82546be429772 Mon Sep 17 00:00:00 2001 From: Jonas Hermsmeier Date: Fri, 6 Jul 2018 22:30:10 +0200 Subject: [PATCH 2/3] fix(linux): Fix pair consolidation for lsblk This fixes consolidation & mapping of children on systems with an old version of `lsblk` (like Ubuntu 14.04), where only the `--pairs` option is available and no `pkname` or `kname` fields are exposed in the output. Change-Type: patch --- lib/lsblk/json.js | 14 +-- lib/lsblk/pairs.js | 66 +++++++++++- tests/data/lsblk/ubuntu-14.04-1.txt | 32 ++++++ tests/data/lsblk/ubuntu-14.04-2.txt | 30 ++++++ tests/lsblk.spec.js | 152 ++++++++++++++++++++++++++++ 5 files changed, 284 insertions(+), 10 deletions(-) create mode 100644 tests/data/lsblk/ubuntu-14.04-1.txt create mode 100644 tests/data/lsblk/ubuntu-14.04-2.txt create mode 100644 tests/lsblk.spec.js diff --git a/lib/lsblk/json.js b/lib/lsblk/json.js index 46d49b21..d98d1c7d 100644 --- a/lib/lsblk/json.js +++ b/lib/lsblk/json.js @@ -56,13 +56,13 @@ const transform = (data) => { && !device.name.startsWith('/dev/sr') && !device.name.startsWith('/dev/ram'); }).map((device) => { - const isVirtual = /^(block)$/i.test(device.subsystems); - const isSCSI = /^(sata|scsi|ata|ide|pci)$/i.test(device.tran); - const isUSB = /^(usb)$/i.test(device.tran); + const isVirtual = device.subsystems ? /^(block)$/i.test(device.subsystems) : null; + const isSCSI = device.tran ? /^(sata|scsi|ata|ide|pci)$/i.test(device.tran) : null; + const isUSB = device.tran ? /^(usb)$/i.test(device.tran) : null; const isReadOnly = Number(device.ro) === 1; - const isRemovable = Number(device.rm) === 1 || isVirtual; + const isRemovable = Number(device.rm) === 1 || Boolean(isVirtual); return { - enumerator: 'lsblk', + enumerator: 'lsblk:json', busType: (device.tran || 'UNKNOWN').toUpperCase(), busVersion: null, device: device.name, @@ -70,8 +70,8 @@ const transform = (data) => { description: getDescription(device), error: null, size: Number(device.size) || null, - blockSize: Number(device['phy-sec']) || null, - logicalBlockSize: Number(device['log-sec']) || null, + blockSize: Number(device['phy-sec']) || 512, + logicalBlockSize: Number(device['log-sec']) || 512, mountpoints: device.children ? getMountpoints(device.children) : [], isReadOnly: isReadOnly, isSystem: !isRemovable && !isVirtual, diff --git a/lib/lsblk/pairs.js b/lib/lsblk/pairs.js index 44afcaa1..27d4da1f 100644 --- a/lib/lsblk/pairs.js +++ b/lib/lsblk/pairs.js @@ -73,15 +73,23 @@ const parseLsblk = (output) => { return output.trim().split(/\r?\n/g).map(parseLsblkLine); }; +const getMajor = (device) => { + return device['maj:min'].substr(0, device['maj:min'].indexOf(':')); +}; + const consolidate = (devices) => { const primaries = devices.filter((device) => { - return device.pkname === ''; + return device.type === 'disk' + && !device.name.startsWith('ram') + && !device.name.startsWith('sr'); }); primaries.forEach((device) => { + const deviceMajor = getMajor(device); device.mountpoints = devices.filter((child) => { - return child.pkname === device.kname; + return child.type === 'part' + && getMajor(child) === deviceMajor; }).map((child) => { return { path: child.mountpoint, @@ -94,8 +102,60 @@ const consolidate = (devices) => { }; +const getDescription = (device) => { + const description = [ + device.label || '', + device.vendor || '', + device.model || '' + ]; + if (device.mountpoints.length) { + let subLabels = device.mountpoints + .filter((c) => { + return c.label && c.label !== device.label || c.path; + }) + .map((c) => { + return c.label || c.path; + }); + subLabels = Array.from(new Set(subLabels)); + if (subLabels.length) { + description.push(`(${subLabels.join(', ')})`); + } + } + return description.join(' ').replace(/\s+/g, ' ').trim(); +}; + const parse = (stdout) => { - return consolidate(parseLsblk(stdout)); + const devices = consolidate(parseLsblk(stdout)); + + return devices.map((device) => { + const isVirtual = device.subsystems ? /^(block)$/i.test(device.subsystems) : null; + const isSCSI = device.tran ? /^(sata|scsi|ata|ide|pci)$/i.test(device.tran) : null; + const isUSB = device.tran ? /^(usb)$/i.test(device.tran) : null; + const isReadOnly = Number(device.ro) === 1; + const isRemovable = Number(device.rm) === 1 || Boolean(isVirtual); + + return { + enumerator: 'lsblk:pairs', + busType: (device.tran || 'UNKNOWN').toUpperCase(), + busVersion: null, + device: '/dev/' + device.name, + raw: '/dev/' + device.name, + description: getDescription(device) || device.name, + error: null, + size: Number(device.size) || null, + blockSize: Number(device['phy-sec']) || 512, + logicalBlockSize: Number(device['log-sec']) || 512, + mountpoints: device.mountpoints, + isReadOnly: isReadOnly, + isSystem: !isRemovable && !isVirtual, + isVirtual: isVirtual, + isRemovable: isRemovable, + isCard: null, + isSCSI: isSCSI, + isUSB: isUSB, + isUAS: null + }; + }); }; module.exports = parse; diff --git a/tests/data/lsblk/ubuntu-14.04-1.txt b/tests/data/lsblk/ubuntu-14.04-1.txt new file mode 100644 index 00000000..33b6f498 --- /dev/null +++ b/tests/data/lsblk/ubuntu-14.04-1.txt @@ -0,0 +1,32 @@ +NAME="sda" MAJ:MIN="8:0" RM="0" SIZE="1024209543168" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="sda1" MAJ:MIN="8:1" RM="0" SIZE="524288000" RO="0" TYPE="part" MOUNTPOINT="/boot/efi" +NAME="sda2" MAJ:MIN="8:2" RM="0" SIZE="41943040" RO="0" TYPE="part" MOUNTPOINT="" +NAME="sda3" MAJ:MIN="8:3" RM="0" SIZE="3221225472" RO="0" TYPE="part" MOUNTPOINT="" +NAME="sda4" MAJ:MIN="8:4" RM="0" SIZE="235291017216" RO="0" TYPE="part" MOUNTPOINT="/" +NAME="sda5" MAJ:MIN="8:5" RM="0" SIZE="16980639744" RO="0" TYPE="part" MOUNTPOINT="[SWAP]" +NAME="sda6" MAJ:MIN="8:6" RM="0" SIZE="549755813888" RO="0" TYPE="part" MOUNTPOINT="/home" +NAME="sda7" MAJ:MIN="8:7" RM="0" SIZE="218392166400" RO="0" TYPE="part" MOUNTPOINT="" +NAME="ram0" MAJ:MIN="1:0" RM="0" SIZE="67108864" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram1" MAJ:MIN="1:1" RM="0" SIZE="67108864" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram2" MAJ:MIN="1:2" RM="0" SIZE="67108864" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram3" MAJ:MIN="1:3" RM="0" SIZE="67108864" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram4" MAJ:MIN="1:4" RM="0" SIZE="67108864" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram5" MAJ:MIN="1:5" RM="0" SIZE="67108864" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram6" MAJ:MIN="1:6" RM="0" SIZE="67108864" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram7" MAJ:MIN="1:7" RM="0" SIZE="67108864" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram8" MAJ:MIN="1:8" RM="0" SIZE="67108864" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram9" MAJ:MIN="1:9" RM="0" SIZE="67108864" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="loop0" MAJ:MIN="7:0" RM="0" SIZE="" RO="0" TYPE="loop" MOUNTPOINT="" +NAME="loop1" MAJ:MIN="7:1" RM="0" SIZE="" RO="0" TYPE="loop" MOUNTPOINT="" +NAME="loop2" MAJ:MIN="7:2" RM="0" SIZE="" RO="0" TYPE="loop" MOUNTPOINT="" +NAME="loop3" MAJ:MIN="7:3" RM="0" SIZE="" RO="0" TYPE="loop" MOUNTPOINT="" +NAME="loop4" MAJ:MIN="7:4" RM="0" SIZE="" RO="0" TYPE="loop" MOUNTPOINT="" +NAME="loop5" MAJ:MIN="7:5" RM="0" SIZE="" RO="0" TYPE="loop" MOUNTPOINT="" +NAME="loop6" MAJ:MIN="7:6" RM="0" SIZE="" RO="0" TYPE="loop" MOUNTPOINT="" +NAME="loop7" MAJ:MIN="7:7" RM="0" SIZE="" RO="0" TYPE="loop" MOUNTPOINT="" +NAME="ram10" MAJ:MIN="1:10" RM="0" SIZE="67108864" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram11" MAJ:MIN="1:11" RM="0" SIZE="67108864" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram12" MAJ:MIN="1:12" RM="0" SIZE="67108864" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram13" MAJ:MIN="1:13" RM="0" SIZE="67108864" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram14" MAJ:MIN="1:14" RM="0" SIZE="67108864" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram15" MAJ:MIN="1:15" RM="0" SIZE="67108864" RO="0" TYPE="disk" MOUNTPOINT="" diff --git a/tests/data/lsblk/ubuntu-14.04-2.txt b/tests/data/lsblk/ubuntu-14.04-2.txt new file mode 100644 index 00000000..8e4071ac --- /dev/null +++ b/tests/data/lsblk/ubuntu-14.04-2.txt @@ -0,0 +1,30 @@ +NAME="fd0" MAJ:MIN="2:0" RM="1" SIZE="" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="sda" MAJ:MIN="8:0" RM="0" SIZE="32G" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="sda1" MAJ:MIN="8:1" RM="0" SIZE="30G" RO="0" TYPE="part" MOUNTPOINT="/" +NAME="sda2" MAJ:MIN="8:2" RM="0" SIZE="1K" RO="0" TYPE="part" MOUNTPOINT="" +NAME="sda5" MAJ:MIN="8:5" RM="0" SIZE="2G" RO="0" TYPE="part" MOUNTPOINT="[SWAP]" +NAME="sr0" MAJ:MIN="11:0" RM="1" SIZE="1024M" RO="0" TYPE="rom" MOUNTPOINT="" +NAME="ram0" MAJ:MIN="1:0" RM="0" SIZE="64M" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram1" MAJ:MIN="1:1" RM="0" SIZE="64M" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram2" MAJ:MIN="1:2" RM="0" SIZE="64M" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram3" MAJ:MIN="1:3" RM="0" SIZE="64M" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram4" MAJ:MIN="1:4" RM="0" SIZE="64M" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram5" MAJ:MIN="1:5" RM="0" SIZE="64M" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram6" MAJ:MIN="1:6" RM="0" SIZE="64M" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram7" MAJ:MIN="1:7" RM="0" SIZE="64M" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram8" MAJ:MIN="1:8" RM="0" SIZE="64M" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram9" MAJ:MIN="1:9" RM="0" SIZE="64M" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="loop0" MAJ:MIN="7:0" RM="0" SIZE="" RO="0" TYPE="loop" MOUNTPOINT="" +NAME="loop1" MAJ:MIN="7:1" RM="0" SIZE="" RO="0" TYPE="loop" MOUNTPOINT="" +NAME="loop2" MAJ:MIN="7:2" RM="0" SIZE="" RO="0" TYPE="loop" MOUNTPOINT="" +NAME="loop3" MAJ:MIN="7:3" RM="0" SIZE="" RO="0" TYPE="loop" MOUNTPOINT="" +NAME="loop4" MAJ:MIN="7:4" RM="0" SIZE="" RO="0" TYPE="loop" MOUNTPOINT="" +NAME="loop5" MAJ:MIN="7:5" RM="0" SIZE="" RO="0" TYPE="loop" MOUNTPOINT="" +NAME="loop6" MAJ:MIN="7:6" RM="0" SIZE="" RO="0" TYPE="loop" MOUNTPOINT="" +NAME="loop7" MAJ:MIN="7:7" RM="0" SIZE="" RO="0" TYPE="loop" MOUNTPOINT="" +NAME="ram10" MAJ:MIN="1:10" RM="0" SIZE="64M" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram11" MAJ:MIN="1:11" RM="0" SIZE="64M" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram12" MAJ:MIN="1:12" RM="0" SIZE="64M" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram13" MAJ:MIN="1:13" RM="0" SIZE="64M" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram14" MAJ:MIN="1:14" RM="0" SIZE="64M" RO="0" TYPE="disk" MOUNTPOINT="" +NAME="ram15" MAJ:MIN="1:15" RM="0" SIZE="64M" RO="0" TYPE="disk" MOUNTPOINT="" diff --git a/tests/lsblk.spec.js b/tests/lsblk.spec.js new file mode 100644 index 00000000..9890a497 --- /dev/null +++ b/tests/lsblk.spec.js @@ -0,0 +1,152 @@ +/* + * Copyright 2018 Resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const m = require('mochainon'); +const parsePairs = require('../lib/lsblk/pairs.js'); + +describe('Drivelist', function() { + + context('lsblk', function() { + + it('can handle --pairs output on Ubuntu 14.04', function() { + + const listData = fs.readFileSync(path.join(__dirname, 'data', 'lsblk', 'ubuntu-14.04-1.txt'), 'utf8'); + const devices = parsePairs(listData); + + const expected = [ { + enumerator: 'lsblk:pairs', + busType: 'UNKNOWN', + busVersion: null, + device: '/dev/sda', + raw: '/dev/sda', + description: '(/boot/efi, /, [SWAP], /home)', + error: null, + size: 1024209543168, + blockSize: 512, + logicalBlockSize: 512, + mountpoints: [ { + path: '/boot/efi', + label: undefined + }, { + path: '', + label: undefined + }, { + path: '', + label: undefined + }, { + path: '/', + label: undefined + }, { + path: '[SWAP]', + label: undefined + }, { + path: '/home', + label: undefined + }, { + path: '', + label: undefined + } ], + isReadOnly: false, + isSystem: true, + isVirtual: null, + isRemovable: false, + isCard: null, + isSCSI: null, + isUSB: null, + isUAS: null + } ]; + + console.log(require('util').inspect(parsePairs(listData), { + colors: true, + depth: null + })); + + m.chai.expect(devices).to.deep.equal(expected); + + }); + + it('can handle --pairs output on Ubuntu 14.04, sample 2', function() { + + const listData = fs.readFileSync(path.join(__dirname, 'data', 'lsblk', 'ubuntu-14.04-2.txt'), 'utf8'); + const devices = parsePairs(listData); + + const expected = [ { + enumerator: 'lsblk:pairs', + busType: 'UNKNOWN', + busVersion: null, + device: '/dev/fd0', + raw: '/dev/fd0', + description: 'fd0', + error: null, + size: null, + blockSize: 512, + logicalBlockSize: 512, + mountpoints: [], + isReadOnly: false, + isSystem: false, + isVirtual: null, + isRemovable: true, + isCard: null, + isSCSI: null, + isUSB: null, + isUAS: null + }, { + enumerator: 'lsblk:pairs', + busType: 'UNKNOWN', + busVersion: null, + device: '/dev/sda', + raw: '/dev/sda', + description: '(/, [SWAP])', + error: null, + size: null, + blockSize: 512, + logicalBlockSize: 512, + mountpoints: [ { + path: '/', + label: undefined + }, { + path: '', + label: undefined + }, { + path: '[SWAP]', + label: undefined + } ], + isReadOnly: false, + isSystem: true, + isVirtual: null, + isRemovable: false, + isCard: null, + isSCSI: null, + isUSB: null, + isUAS: null + } ]; + + console.log(require('util').inspect(parsePairs(listData), { + colors: true, + depth: null + })); + + m.chai.expect(devices).to.deep.equal(expected); + + }); + + }); + +}); From c6534641b7319c75847b3c285b431d21c8b35356 Mon Sep 17 00:00:00 2001 From: "resin-io-modules-versionbot[bot]" Date: Mon, 9 Jul 2018 14:34:41 +0000 Subject: [PATCH 3/3] v6.3.0 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f219f5..56168291 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY! This project adheres to [Semantic Versioning](http://semver.org/). +## v6.3.0 - 2018-07-09 + +* Fix(linux): Fix pair consolidation for lsblk #255 [Jonas Hermsmeier] +* Feat(lib): Use lsblk directly, parse json output #255 [Jonas Hermsmeier] + ## v6.2.5 - 2018-07-06 * Fix(linux): Add flag to lsblk to list all devices #287 [Jonas Hermsmeier] diff --git a/package.json b/package.json index b91de049..7a6ddb0f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "drivelist", - "version": "6.2.5", + "version": "6.3.0", "description": "List all connected drives in your computer, in all major operating systems", "main": "lib/drivelist.js", "homepage": "https://github.com/resin-io-modules/drivelist",