volumeInterrogator_darwin.mjs

/**
 * @description Controls the collection of volume specific information and attributes to be published to homekit.
 * @copyright December 2020
 * @author Mike Price <dev.grumptech@gmail.com>
 * @module VolumeInterrogatorDarwinModule
 * @requires debug
 * @see {@link https://github.com/debug-js/debug#readme}
 * @requires plist
 * @see {@link https://github.com/TooTallNate/plist.js#readme}
 */

// Internal dependencies.
import {VolumeInterrogatorBase as _VolumeInterrogatorBase} from './volumeInterrogatorBase.mjs';
import {VOLUME_TYPES, VolumeData} from './volumeData.mjs';
import {default as SpawnHelper, SPAWN_HELPER_EVENTS as _SpawnHelperEvents} from 'grumptech-spawn-helper';

// External dependencies and imports.
import _debugModule from 'debug';
import _plistModule from 'plist';

/**
 * @private
 * @description Debugging function pointer for runtime related diagnostics.
 */
const _debug_process = _debugModule('vi_process');  // eslint-disable-line camelcase
/**
 * @private
 * @description Debugging function pointer for configuration related diagnostics.
 */
const _debug_config = _debugModule('vi_config');  // eslint-disable-line camelcase

// Bind debug to console.log
// eslint-disable-next-line camelcase, no-console
_debug_process.log = console.log.bind(console);
// eslint-disable-next-line camelcase, no-console
_debug_config.log  = console.log.bind(console);

// Helpful constants and conversion factors.
// Helpful constants and conversion factors.
/**
 * @description Conversion factor for 512byte blocks
 * @private
 */
const BLOCKS512_TO_BYTES    = 512;
/**
 * @description Regular expression pattern for white space.
 * @private
 */
const REGEX_WHITE_SPACE     = /\s+/;

/**
 * @description Derived class for darwin-based (OSX/macOS) volume interrogation
 * @augments _VolumeInterrogatorBase
 */
export class VolumeInterrogator_darwin extends _VolumeInterrogatorBase {    // eslint-disable-line camelcase
    /**
     * @description Constructor
     * @class
     * @param {object} [config] - The settings to use for creating the object.
     * @throws {Error}  - thrown if the operating system is not supported.
     */
    constructor(config) {
        // Sanity - ensure the Operating System is supported.
        const operatingSystem = process.platform;
        if ((operatingSystem === undefined) || (typeof(operatingSystem) !== 'string') ||
            (operatingSystem.length <= 0) || (operatingSystem.toLowerCase() !== 'darwin')) {
            throw new Error(`Operating system not supported. os:${operatingSystem}`);
        }

        // Initialize the base class.
        super(config);

        // Initialize data members.
        this._pendingFileSystems            = [];
        this._pendingVolumes                = [];
        this._theVisibleVolumeNames         = [];

        // Callbacks bound to this object.
        this._CB__list_known_virtual_filesystems_complete   = this._on_lsvfs_complete.bind(this);
        this._CB__display_free_disk_space_complete          = this._on_df_complete.bind(this);
        this._CB__visible_volumes                           = this._on_process_visible_volumes.bind(this);
        this._CB_process_diskUtil_info_complete             = this._on_process_diskutil_info_complete.bind(this);
        this._CB_process_disk_utilization_stats_complete    = this._on_process_disk_utilization_stats_complete.bind(this);
    }

    /**
     * @private
     * @description Helper function used to initiate an interrogation of the system volumes on darwin operating systems.
     * @returns {void}
     */
    _initiateInterrogation() {
        // Spawn a 'ls /Volumes' to get a listing of the 'visible' volumes.
        const lsVolumes = new SpawnHelper();
        lsVolumes.on(_SpawnHelperEvents.EVENT_COMPLETE, this._CB__visible_volumes);
        // eslint-disable-next-line new-cap
        lsVolumes.Spawn({command: 'ls', arguments: ['/Volumes']});
    }

    /**
     * @private
     * @description Helper function used to reset an interrogation.
     * @returns {void}
     */
    _doReset() {
        this._pendingFileSystems    = [];
        this._theVisibleVolumeNames = [];
        this._pendingVolumes        = [];
    }

    /**
     * @private
     * @description Read-Only Property used to determine if a check is in progress.
     * @returns {boolean} - true if a check is in progress.
     */
    get _isCheckInProgress() {
        _debug_process(`_isCheckInProgress: Pending Volume Count - ${this._pendingVolumes.length}`);

        const checkInProgress = ((this._pendingVolumes.length !== 0) ||
                                 (this._pendingFileSystems.length !== 0));

        return checkInProgress;
    }

    /**
     * @private
     * @description Read-only property used to get an array of watch folders used to initiate an interrogation.
     * @returns {string[]} - Array of folders to be watched for changes.
     */
    get _watchFolders() {
        return (['/Volumes']);
    }

    /**
     * @private
     * @description Event handler for the SpawnHelper _SpawnHelperEvents.EVENT_COMPLETE Notification
     * @param {object} response - Spawn response.
     * @param {boolean} response.valid - - Flag indicating if the spoawned process was completed successfully.
     * @param {Buffer|string|*} response.result - Result or Error data provided by the spawned process.
     * @param {*} response.token - Client specified token intended to assist in processing the result.
     * @param {SpawnHelper} response.source - Reference to the SpawnHelper that provided the results.
     * @returns {void}
     */
    _on_process_visible_volumes(response) {
        _debug_config(`'${response.source.Command} ${response.source.Arguments}' Spawn Helper Result: valid:${response.valid}`);
        _debug_config(response.result.toString());

        // If a prior error was detected, ignore future processing
        if (!this._checkInProgress) {
            return;
        }

        if (response.valid &&
            (response.result !== undefined)) {
            // Update the list of visible volumes
            this._theVisibleVolumeNames = response.result.toString().split('\n');

            // Spawn a 'lsvfs' to determine the number and types of known file systems.
            const diskutilList = new SpawnHelper();
            diskutilList.on(_SpawnHelperEvents.EVENT_COMPLETE, this._CB__list_known_virtual_filesystems_complete);
            // eslint-disable-next-line new-cap
            diskutilList.Spawn({command: 'lsvfs'});
        }
        else {
            // Clear the check in progress.
            this._checkInProgress = false;
            _debug_process(`Error processing '${response.source.Command} ${response.source.Arguments}'. Err:${response.result}`);

            // Fire the ready event with no data.
            // This willl provide the client an opportunity to reset
            this.emit('ready', {results: []});
        }
    }

    /**
     * @private
     * @description Event handler for the SpawnHelper _SpawnHelperEvents.EVENT_COMPLETE Notification
     * @param {object} response - Spawn response.
     * @param {boolean} response.valid - - Flag indicating if the spoawned process was completed successfully.
     * @param {Buffer|string|*} response.result - Result or Error data provided by the spawned process.
     * @param {*} response.token - Client specified token intended to assist in processing the result.
     * @param {SpawnHelper} response.source - Reference to the SpawnHelper that provided the results.
     * @returns {void}
     */
    _on_lsvfs_complete(response) {
        _debug_config(`'${response.source.Command} ${response.source.Arguments}' Spawn Helper Result: valid:${response.valid}`);
        _debug_config(response.result.toString());

        // If a prior error was detected, ignore future processing
        if (!this._checkInProgress) {
            return;
        }

        const INDEX_FILE_SYSTEM_TYPE  = 0;
        const INDEX_FILE_SYSTEM_COUNT = 1;
        const INDEX_FILE_SYSTEM_FLAGS = 2;

        if (response.valid &&
            (response.result !== undefined)) {
            // Process the response from `lsvfs`.
            // There are two header rows and a dummy footer (as the result of the '\n' split), followd by lines with the following fields:
            // [virtual file system type], [number of file systems], [flags]
            const headerLines = 2;
            const footerLines = -1;
            const lines = response.result.toString().split('\n').slice(headerLines, footerLines);
            lines.forEach((element) => {
                // Break up the element based on white space.
                const fields = element.split(REGEX_WHITE_SPACE);

                // Does the response correspond to expectations?
                if (fields.length === 3) {
                    // Does this file system have any volumes?
                    const fsCount = Number.parseInt(fields[INDEX_FILE_SYSTEM_COUNT], 10);
                    if (fsCount > 0) {
                        const newFS = {type: fields[INDEX_FILE_SYSTEM_TYPE].toLowerCase(), count: fsCount, flags: fields[INDEX_FILE_SYSTEM_FLAGS].toLowerCase()};

                        // Sanity. Ensure this file system type is not already in the pending list.
                        const existingFSIndex = this._pendingFileSystems.findIndex((item) => {
                            const isMatch = (item.type.toLowerCase() === newFS.type);
                            return isMatch;
                        });
                        if (existingFSIndex < 0) {
                            // Add this file system type to the pending list.
                            this._pendingFileSystems.push(newFS);

                            // Spawn a 'diskutil list' to see all the disk/volume data
                            _debug_process(`Spawn df for fs type '${newFS.type}'.`);
                            const diskUsage = new SpawnHelper();
                            diskUsage.on(_SpawnHelperEvents.EVENT_COMPLETE, this._CB__display_free_disk_space_complete);
                            // eslint-disable-next-line new-cap
                            diskUsage.Spawn({command: 'df', arguments: ['-a', '-b', '-T', newFS.type], token: newFS});
                        }
                        else {
                            // Replace the existing item with this one
                            this._pendingFileSystems[existingFSIndex] = newFS;

                            _debug_process(`_on_lsvfs_complete: Duplicated file system type. '${newFS.type}'`);
                        }
                    }
                }
                else {
                    _debug_process(`_on_lsvfs_complete: Error processing line '${element}'`);
                    _debug_process(fields);
                }
            });
        }
        else {
            // Clear the check in progress.
            this._checkInProgress = false;
            _debug_process(`Error processing '${response.source.Command} ${response.source.Arguments}'. Err:${response.result}`);

            // Fire the ready event with no data.
            // This willl provide the client an opportunity to reset
            this.emit('ready', {results: []});
        }
    }

    /**
     * @private
     * @description Event handler for the SpawnHelper _SpawnHelperEvents.EVENT_COMPLETE Notification
     * @param {object} response - Spawn response.
     * @param {boolean} response.valid - - Flag indicating if the spoawned process was completed successfully.
     * @param {Buffer|string|*} response.result - Result or Error data provided by the spawned process.
     * @param {*} response.token - Client specified token intended to assist in processing the result.
     * @param {SpawnHelper} response.source - Reference to the SpawnHelper that provided the results.
     * @returns {void}
     */
    _on_df_complete(response) {
        _debug_config(`'${response.source.Command} ${response.source.Arguments}' Spawn Helper Result: valid:${response.valid}`);
        _debug_config(response.result.toString());
        if (response.token !== undefined) {
            _debug_config('Spawn Token:');
            _debug_config(response.token);
        }

        // If a prior error was detected, ignore future processing
        if (!this._checkInProgress) {
            return;
        }

        const INDEX_FILE_SYSTEM_NAME  = 0;
        const INDEX_FILE_SYSTEM_512BLOCKS = 1;
        const INDEX_FILE_SYSTEM_USED_BLOCKS = 2;
        const INDEX_FILE_SYSTEM_AVAILABLE_BLOCKS = 3;
        const INDEX_FILE_SYSTEM_CAPACITY = 4;
        const INDEX_FILE_SYSTEM_MOUNT_PT = 5;

        if (response.valid &&
            (response.result !== undefined)) {
            // This spawn is expected to have been issued with a token.
            if (response.token !== undefined) {
                if (typeof(response.token) !== 'object') {
                    throw new TypeError('Spawn token is not a string.');
                }
                // Ensure that the pending file system list contains the token.
                if (this._pendingFileSystems.includes(response.token)) {
                    // Remove token from the list.
                    this._pendingFileSystems = this._pendingFileSystems.filter((item) => {
                        const isEqual = ((item.type !== response.token.type) ||
                                         (item.count !== response.token.count) ||
                                         (item.flags !== response.token.flags));
                        return isEqual;
                    });
                }
                else {
                    throw new Error('Spawn token is not in the pending file system list.');
                }
            }
            else {
                throw new Error('Spawn token is missing.');
            }

            // Is this list of file systems one of the types we handle?
            if (Object.values(VOLUME_TYPES).includes(response.token.type)) {
                // Process the response from `df`.
                // There are two header rows and a dummy footer (as the result of the '\n' split), followed by lines with the following fields:
                // [virtual file system type], [number of file systems], [flags]
                const headerLines = 1;
                const footerLines = -1;
                const lines = response.result.toString().split('\n').slice(headerLines, footerLines);
                lines.forEach((element) => {
                    // Break up the element based on white space.
                    const fields = element.split(REGEX_WHITE_SPACE);

                    // Note: The Mount Point may include white space. Handle that possibility
                    const fieldsMountPoint = fields.slice(INDEX_FILE_SYSTEM_MOUNT_PT, fields.length);
                    fields[INDEX_FILE_SYSTEM_MOUNT_PT] = fieldsMountPoint.join(' ');

                    const newVol = {
                        device_node: fields[INDEX_FILE_SYSTEM_NAME].toLowerCase(),
                        blocks: Number.parseInt(fields[INDEX_FILE_SYSTEM_512BLOCKS], 10),
                        used_blks: Number.parseInt(fields[INDEX_FILE_SYSTEM_USED_BLOCKS], 10),
                        avail_blks: Number.parseInt(fields[INDEX_FILE_SYSTEM_AVAILABLE_BLOCKS], 10),
                        capacity: Number.parseInt(fields[INDEX_FILE_SYSTEM_CAPACITY].slice(0, -1), 10),
                        mount_point: fields[INDEX_FILE_SYSTEM_MOUNT_PT],
                    };

                    // Determine the name of the volume (text following the last '/')
                    let volName = newVol.mount_point;
                    const volNameParts = newVol.mount_point.split('/');
                    if (volNameParts.length > 0) {
                        const candidateName = volNameParts[volNameParts.length - 1];
                        if (candidateName.length > 0) {
                            volName = volNameParts[volNameParts.length - 1];
                        }
                    }

                    // Determine if the low space alert threshold has been exceeded.
                    const lowSpaceAlert = this._determineLowSpaceAlert(volName, '~~ not used ~~', ((newVol.avail_blks / newVol.blocks) * 100.0));

                    // Create a new (and temporary) VolumeData item.
                    /* eslint-disable key-spacing */
                    const volData = new VolumeData({
                        name:               volName,
                        volume_type:        response.token.type,
                        mount_point:        newVol.mount_point,
                        volume_uuid:        'unknown',
                        device_node:        newVol.device_node,
                        capacity_bytes:     newVol.blocks * BLOCKS512_TO_BYTES,
                        free_space_bytes:   (newVol.blocks - newVol.used_blks) * BLOCKS512_TO_BYTES,
                        visible:            this._theVisibleVolumeNames.includes(volName),
                        low_space_alert:    lowSpaceAlert,
                    });
                    /* eslint-enable key-spacing */

                    // Get more informaion on this volume.
                    _debug_process(`Initiating 'diskutil info' for DiskId '${volData.DeviceNode}'`);
                    const diskutilInfo = new SpawnHelper();
                    diskutilInfo.on(_SpawnHelperEvents.EVENT_COMPLETE, this._CB_process_diskUtil_info_complete);
                    // eslint-disable-next-line new-cap
                    diskutilInfo.Spawn({command: 'diskutil', arguments: ['info', '-plist', volData.DeviceNode], token: volData});

                    // Add this volume to the list of pending volumes.
                    this._pendingVolumes.push(volData);
                });
            }
        }
        else {
            // Clear the check in progress.
            this._checkInProgress = false;
            _debug_process(`Error processing '${response.source.Command} ${response.source.Arguments}'. Err:${response.result}`);

            // Fire the ready event with no data.
            // This willl provide the client an opportunity to reset
            this.emit('ready', {results: []});
        }
    }

    /**
     * @private
     * @description Event handler for the SpawnHelper _SpawnHelperEvents.EVENT_COMPLETE Notification
     * @param {object} response - Spawn response.
     * @param {boolean} response.valid - - Flag indicating if the spoawned process was completed successfully.
     * @param {Buffer|string|*} response.result - Result or Error data provided by the spawned process.
     * @param {*} response.token - Client specified token intended to assist in processing the result.
     * @param {SpawnHelper} response.source - Reference to the SpawnHelper that provided the results.
     * @returns {void}
     */
    _on_process_diskutil_info_complete(response) {
        _debug_config(`'${response.source.Command} ${response.source.Arguments}' Spawn Helper Result: valid:${response.valid}`);
        _debug_config(response.result.toString());

        let errorEncountered = false;

        // If a prior error was detected, ignore future processing
        if (!this._checkInProgress) {
            return;
        }

        if (response.valid) {
            try {
                // Prurge the pending volumes listing.
                if (response.token instanceof VolumeData) {
                    if (this._pendingVolumes.includes(response.token)) {
                        // Remove this item from the pending list.
                        // eslint-disable-next-line arrow-body-style
                        this._pendingVolumes = this._pendingVolumes.filter((item) => {
                            return item !== response.token;
                        });
                    }
                    else {
                        // Unknown token !!

                        // Clear the check in progress.
                        this._checkInProgress = false;
                        // Flag the error
                        errorEncountered = true;
                        _debug_process('Unexpected call to _on_process_diskutil_info_complete. token not pending.');
                        _debug_process(response.token);
                    }
                }
                else {
                    // Unexpected token data !!

                    // Clear the check in progress.
                    this._checkInProgress = false;
                    // Flag the error
                    errorEncountered = true;
                    _debug_process('Unexpected call to _on_process_diskutil_info_complete. token is not instance of VolumeData.');
                    _debug_process(response.token);
                }

                if (!errorEncountered) {
                    // Attempt to parse the data as a plist.
                    const config = _plistModule.parse(response.result.toString());

                    // Determine if the volume should be shown.
                    const  isShown = this._isVolumeShown(response.token.MountPoint);

                    // Get the device identifier for this volume and manage the pending
                    // items.
                    if ((Object.prototype.hasOwnProperty.call(config, 'DeviceIdentifier')) && (typeof(config.DeviceIdentifier) === 'string')) {
                        // Validate the config data.
                        if ((Object.prototype.hasOwnProperty.call(config, 'VolumeName') &&          (typeof(config.VolumeName)          === 'string'))       &&
                            (Object.prototype.hasOwnProperty.call(config, 'FilesystemType') &&      (typeof(config.FilesystemType)      === 'string'))       &&
                            (Object.prototype.hasOwnProperty.call(config, 'DeviceIdentifier') &&    (typeof(config.DeviceIdentifier)    === 'string'))       &&
                            (Object.prototype.hasOwnProperty.call(config, 'MountPoint') &&          (typeof(config.MountPoint)          === 'string'))       &&
                            (Object.prototype.hasOwnProperty.call(config, 'DeviceNode') &&          (typeof(config.DeviceNode)          === 'string'))       &&
                            /* UDF volumes have no Volume UUID */
                            ((Object.prototype.hasOwnProperty.call(config, 'VolumeUUID') &&         (typeof(config.VolumeUUID)          === 'string'))       ||
                            (!Object.prototype.hasOwnProperty.call(config, 'VolumeUUID')))                                                                   &&
                            (Object.prototype.hasOwnProperty.call(config, 'Size') &&                 (typeof(config.Size)               === 'number'))       &&
                            // Free space is reported based on the file system type.
                            (((Object.prototype.hasOwnProperty.call(config, 'FreeSpace') &&          (typeof(config.FreeSpace)          === 'number')))      ||
                            (Object.prototype.hasOwnProperty.call(config, 'APFSContainerFree') && (typeof(config.APFSContainerFree)  === 'number'))))          {
                            // Then, process the data provided.
                            // Free space is reported based on the file system type.
                            const freeSpace  = ((config.FilesystemType === VOLUME_TYPES.TYPE_APFS) ? config.APFSContainerFree : config.FreeSpace);
                            // For volumes that do not have a volume UUID, use the device node.
                            const volumeUUID = ((Object.prototype.hasOwnProperty.call(config, 'VolumeUUID')) ? config.VolumeUUID : config.DeviceNode);
                            // Determine if the low space alert threshold has been exceeded.
                            const lowSpaceAlert = this._determineLowSpaceAlert(config.VolumeName, volumeUUID, ((freeSpace / config.Size) * 100.0));
                            /* eslint-disable key-spacing */
                            const volData = new VolumeData({
                                name:               config.VolumeName,
                                volume_type:        config.FilesystemType,
                                disk_id:            config.DeviceIdentifier,
                                mount_point:        config.MountPoint,
                                capacity_bytes:     config.Size,
                                device_node:        config.DeviceNode,
                                volume_uuid:        volumeUUID,
                                free_space_bytes:   freeSpace,
                                visible:            this._theVisibleVolumeNames.includes(config.VolumeName),
                                shown:              isShown,
                                low_space_alert:    lowSpaceAlert,
                            });
                            /* eslint-enable key-spacing */
                            this._theVolumes.push(volData);
                            /* eslint-disable */
/*
                            // APFS volumes may have some of their capacity consumed by purgable data. For example: APFS Snapshots.
                            // This purgable data can only be evaluated if the volume is mounted.
                            if ((volData.VolumeType === VOLUME_TYPES.TYPE_APFS) &&
                                (volData.IsMounted)) {
                                // Append the mount point to the 'pending volumes' list to keep us busy.
                                this._pendingVolumes.push(volData.MountPoint);

                                // Spawn a disk usage statistics ('du') process to see the accurate storage information for the
                                // APFS volumes.
                                const du_process = new SpawnHelper();
                                du_process.on(_SpawnHelperEvents.EVENT_COMPLETE, this._CB_process_disk_utilization_stats_complete);
                                du_process.Spawn({ command:'du', arguments:['-skHx', volData.MountPoint] });
                            }
*/
                            /* eslint-enable */
                        }
                        else {
                            // Ignore the inability to process this item if there is no valid volume name.
                            // eslint-disable-next-line no-lonely-if
                            if ((Object.prototype.hasOwnProperty.call(config, 'VolumeName') && (typeof(config.VolumeName) === 'string') &&
                                (config.VolumeName.length > 0))) {
                                _debug_process('_on_process_diskutil_info_complete: Unable to handle response from diskutil.');
                            }
                        }
                    }
                    else {
                        // Result did not contain a disk identifier. This may be ok, for example if the volume is mounted via SMB.
                        // eslint-disable-next-line no-lonely-if
                        if ((Object.prototype.hasOwnProperty.call(config, 'Error')) && (typeof(config.Error) === 'boolean') && (config.Error)) {
                            // We were unable to get more detailed informatopn. Just use what we have, but update the IsShown property.
                            /* eslint-disable key-spacing */
                            const volData = new VolumeData({
                                name:               response.token.Name,
                                volume_type:        response.token.VolumeType,
                                disk_id:            response.token.DeviceIdentifier,
                                mount_point:        response.token.MountPoint,
                                capacity_bytes:     response.token.Size,
                                device_node:        response.token.DeviceNode,
                                volume_uuid:        response.token.VolumeUUID,
                                free_space_bytes:   response.token.FreeSpace,
                                visible:            response.token.IsVisible,
                                shown:              isShown,
                                low_space_alert:    response.token.LowSpaceAlert,
                            });
                            /* eslint-enable key-spacing */
                            this._theVolumes.push(volData);
                        }
                        else {
                            // Unexpected result.

                            // Clear the check in progress.
                            this._checkInProgress = false;
                            // Flag the error
                            errorEncountered = true;
                            _debug_process('Unexpected call to _on_process_diskutil_info_complete. config.');
                            _debug_process(config);
                        }
                    }
                }

                // Finally, update the 'check in progress' flag.
                if (!errorEncountered) {
                    this._updateCheckInProgress();
                }
            }
            catch (error) {
                // Clear the check in progress.
                this._checkInProgress = false;
                // Flag the error
                errorEncountered = true;
                _debug_process(`Error processing 'diskutil info'. Err:${error}`);
            }
        }
        else {
            // Clear the check in progress.
            this._checkInProgress = false;
            // Flag the error
            errorEncountered = true;
            _debug_process(`Error processing '${response.source.Command} ${response.source.Arguments}'. Err:${response.result}`);
        }

        // Was a critical error encountered while processing the data?
        if (errorEncountered) {
            // Fire the ready event with no data.
            // This willl provide the client an opportunity to reset
            this.emit('ready', {results: []});
        }
    }

    /**
     * @private
     * @description Event handler for the SpawnHelper _SpawnHelperEvents.EVENT_COMPLETE Notification
     * @param {object} response - Spawn response.
     * @param {boolean} response.valid - - Flag indicating if the spoawned process was completed successfully.
     * @param {Buffer|string|*} response.result - Result or Error data provided by the spawned process.
     * @param {*} response.token - Client specified token intended to assist in processing the result.
     * @param {SpawnHelper} response.source - Reference to the SpawnHelper that provided the results.
     * @returns {void}
     */
    _on_process_disk_utilization_stats_complete(response) {
        _debug_config(`'${response.source.Command} ${response.source.Arguments}' Spawn Helper Result: valid:${response.valid}`);

        // If a prior error was detected, ignore future processing
        if (!this._checkInProgress) {
            return;
        }

        // We expect the 'du' process to encounter errors.
        // So get the results from the object directly.
        const rawResult = response.source.Result.toString();
        // Break the result up into an array of lines.
        const lines = rawResult.split('\r');
        for (const line of lines) {
            // Split the lines by white space.
            const fields = line.split('\t');
            // Verify that there are 2 fields. These are the size and the volume name (mount point)
            if (fields.length === 2) {
                // eslint-disable-next-line new-cap
                const usedBytes     = VolumeData.ConvertFrom1KBlockaToBytes(Number.parseInt(fields[0], 10));
                const volumeName    = fields[1].trim();
                // eslint-disable-next-line new-cap
                _debug_process(`du results: Name:${volumeName} Used:${VolumeData.ConvertFromBytesToGB(usedBytes)} raw:${Number.parseInt(fields[0], 10)}`);

                // Verify that we were looking for this mount point.
                if (this._pendingVolumes.includes(volumeName)) {
                    // First, remove this item from the pending list.
                    this._pendingVolumes = this._pendingVolumes.filter((item) => (item !== volumeName));

                    // Find the matching volume in the list.
                    let matchedIndex;
                    const matchedItem = this._theVolumes.filter((item, index) => {
                        const match = (item.MountPoint === volumeName);
                        // Cache the index of the match as well.
                        if (match) {
                            matchedIndex = index;
                        }
                        return match;
                    });
                    if ((matchedItem === undefined) || (matchedItem.length !== 1) || (matchedIndex < this._theVolumes.length)) {
                        // Clear the check in progress.
                        this._checkInProgress = false;
                        _debug_process('Unable to identify unique volumeData item.');
                        throw new Error('Unable to identify unique volumeData item.');
                    }

                    // Create a new volume data item to replace the original
                    const volData = new VolumeData({
                        /* eslint-disable key-spacing */
                        name:               matchedItem[0].Name,
                        volume_type:        matchedItem[0].VolumeType,
                        disk_id:            matchedItem[0].DiskId,
                        mount_point:        matchedItem[0].MountPoint,
                        capacity_bytes:     matchedItem[0].Size,
                        device_node:        matchedItem[0].DeviceNode,
                        volume_uuid:        matchedItem[0].VolumeUUID,
                        free_space_bytes:   matchedItem[0].FreeSpace,
                        visible:            matchedItem[0].IsVisible,
                        low_space_alert:    matchedItem[0].LowSpaceAlert,
                        used_space_bytes:   usedBytes,
                        /* eslint-enable key-spacing */
                    });

                    // Replace the item in the array with the updated one.
                    this._theVolumes[matchedIndex] = volData;

                    // Finally, update the 'check in progress' flag.
                    this._updateCheckInProgress();
                }
                else {
                    // Clear the check in progress.
                    this._checkInProgress = false;
                    _debug_process(`Unexpected call to _on_process_disk_utilization_stats_complete. mount_point:${fields[1]}`);
                    throw new Error(`Unexpected call to _on_process_disk_utilization_stats_complete. mount_point:${fields[1]}`);
                }
            }
            else {
                // Clear the check in progress.
                this._checkInProgress = false;
                _debug_process(`Unable to paese '${response.source.Command} ${response.source.Arguments}' results`);
                throw Error(`Unable to paese '${response.source.Command} ${response.source.Arguments}' results`);
            }
        }
    }

    /**
     * @private
     * @description  Helper for extracting the disk identifiers from the data provided by 'diskutil list' for HFS+ volumes.
     * @param {object} disks - list of disk data provided by 'diskutil list' for non-APFS disks.
     * @returns {string[]} - Array of disk identifiers.
     * @throws {TypeError} - thrown for enteries that are not specific to Partitions or if the input is not an array
     * @todo - Not used at present time.
     */
    _partitionDiskIdentifiers(disks) {
        if ((disks === undefined) || (!Array.isArray(disks))) {
            throw new TypeError('disk must be an array');
        }

        const diskIdentifiers = [];

        for (const disk of disks) {
            if ((Object.prototype.hasOwnProperty.call(disk, 'Partitions')) &&
                (disk.Partitions.length > 0)) {
                for (const partition of disk.Partitions) {
                    // Validate that we can access the disk identifier.
                    if ((Object.prototype.hasOwnProperty.call(partition, 'DeviceIdentifier') &&
                        (typeof(partition.DeviceIdentifier) === 'string'))) {
                        // Record the identifier.
                        diskIdentifiers.push(partition.DeviceIdentifier);
                    }
                    else {
                        _debug_process('_partitionDiskIdentifiers(): partition is not as expected. Missing or Invalid Disk Identifier.');
                        throw new TypeError('partition is not as expected. Missing or Invalid Disk Identifier.');
                    }
                }
            }
            else {
                _debug_process('_partitionDiskIdentifiers(): drive is not as expected. No partitions.');
                throw new TypeError('drive is not as expected. No partitions.');
            }
        }

        return diskIdentifiers;
    }

    /**
     * @private
     * @description Helper for extracting the disk identifiers from the data provided by 'diskutil list' for AFPS volumes.
     * @param {object[]} containers - list of AFPS container data provided by 'diskutil list'
     * @returns {string[]} - Array of disk identifiers.
     * @throws {TypeError} - thrown for enteries that are not specific to Partitions or if the input is not an array
     * @todo - Not used at present time.
     */
    _apfsDiskIdentifiers(containers) {
        if ((containers === undefined) || (!Array.isArray(containers))) {
            throw new TypeError('containers must be an array');
        }

        const diskIdentifiers = [];

        for (const container of containers) {
            if ((Object.prototype.hasOwnProperty.call(container, 'APFSVolumes')) &&
                Array.isArray(container.APFSVolumes)) {
                // The data of interest is stored in the APFS volumes entry.
                for (const volume of container.APFSVolumes) {
                    // Validate that we can access the disk identifier.
                    if ((Object.prototype.hasOwnProperty.call(volume, 'DeviceIdentifier') &&
                        (typeof(volume.DeviceIdentifier) === 'string'))) {
                        // Record the identifier.
                        diskIdentifiers.push(volume.DeviceIdentifier);
                    }
                    else {
                        _debug_process('_apfsDiskIdentifiers(): volume is not as expected. Missing or Invalid Disk Identifier.');
                        throw new TypeError('volume is not as expected. Missing or Invalid Disk Identifier.');
                    }
                }
            }
            else {
                _debug_process('_apfsDiskIdentifiers(): volume is not as expected. (AFPS)');
                throw new TypeError('volume is not as expected. (AFPS)');
            }
        }

        return diskIdentifiers;
    }
}
// eslint-disable-next-line camelcase
export default VolumeInterrogator_darwin;