volumeInterrogatorBase.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 VolumeInterrogatorBaseModule
 * @requires debug
 * @see {@link https://github.com/debug-js/debug#readme}
 * @requires events
 * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/events.html#events}
 * @requires os
 * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/os.html}
 */

// External dependencies and imports.
import EventEmitter from 'events';
import _debugModule from 'debug';
import {uptime as _upTime} from 'os';

// Internal dependencies.
// eslint-disable-next-line no-unused-vars
import {VOLUME_TYPES, VolumeData, CONVERSION_BASES} from './volumeData.mjs';
import {VolumeWatcher as _volumeWatcher, VOLUME_WATCHER_EVENTS as _VOLUME_WATCHER_EVENTS} from './volumeWatchers.mjs';
import {default as SpawnHelper} from 'grumptech-spawn-helper';

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

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

// Helpful constants and conversion factors.
/**
 * @description Default period, in hours, for checking for changes in mounted volumes.
 * @private
 */
const DEFAULT_PERIOD_HR                 = 6.0;
/**
 * @description Minimum period, in hours, for checking for changes in mounted volumes.
 * @private
 */
const MIN_PERIOD_HR                     = (5.0 / 60.0);     // Once every 5 minutes.
/**
 * @description Maximum period, in hours, for checking for changes in mounted volumes.
 * @private
 */
const MAX_PERIOD_HR                     = (31.0 * 24.0);    // Once per month.
/**
 * @description Factor for converting from hours to milliseconds.
 * @private
 */
const CONVERT_HR_TO_MS                  = (60.0 * 60.0 * 1000.0);
/**
 * @description Flag indicating the identification of an invalid timeout.
 * @private
 */
const INVALID_TIMEOUT_ID                = -1;
/**
 * @description Timeout, in milliseconds, for retrying to detect volume changes.
 * @private
 */
const RETRY_TIMEOUT_MS                  = 250/* milliseconds */;
/**
 * @description Default threshold, in percent, for determining when free space is low.
 * @private
 */
const DEFAULT_LOW_SPACE_THRESHOLD       = 15.0;
/**
 * @description Minimum threshold, in percent, for detecting low free space.
 * @private
 */
const MIN_LOW_SPACE_THRESHOLD           = 0.0;
/**
 * @description Maximum threshold, in percent, for detecting low free space.
 * @private
 */
const MAX_LOW_SPACE_THRESHOLD           = 100.0;
/**
 * @description Maximum time, in milliseconds, for a volume detection to be initiated.
 * @private
 */
const MAX_RETRY_INIT_CHECK_TIME         = 120000;
/**
 * @description Time, in milliseconds, that must have elapsed since starting the operating system before starting volume detection.
 * @private
 */
const MIN_OS_UPTIME_TO_START_MS         = 600000/* milliseconds */;
/**
 * @description Time, in milliseconds, used to rescan for volume changes.
 * @private
 */
const FS_CHANGED_DETECTION_TIMEOUT_MS   = 1000/* milliseconds */;

/**
 * @description Enumeration of the methods for identifying volumes.
 * @private
 * @readonly
 * @enum {string}
 * @property {string} Name- Identify volume by name
 * @property {string} SerialNumber - Identify volume by serial number.
 */
const VOLUME_IDENTIFICATION_METHODS = {
    /* eslint-disable key-spacing */
    Name         : 'name',
    SerialNumber : 'serial_num',
    /* eslint-enable key-spacing */
};

/**
 * @description Enumeration of published events.
 * @readonly
 * @private
 * @enum {string}
 * @property {string} EVENT_SCANNING - Identification for the event published when scanning begins.
 * @property {string} EVENT_READY - Identification for the event published when scanning completes.
 */
const VOLUME_INTERROGATOR_BASE_EVENTS = {
    /* eslint-disable key-spacing */
    EVENT_SCANNING : 'scanning',
    EVENT_READY    : 'ready',
    /* eslint-enable key-spacing */
};

/**
 * @description Scanning initiated notification
 * @event module:VolumeInterrogatorBaseModule#event:scanning
 */
/**
 * @description Volume detection ready notification
 * @event module:VolumeInterrogatorBaseModule#event:ready
 * @type {object}
 * @param {VolumeData} e.results - Flag indicating if the spawned task completed successfully.
 * @param {Buffer} e.result - Buffer of result or error data returned by the spawned process.
 * @param {SpawnHelper} e.source - Reference to the spawn helper that raised the notification.
 * @private
 */
/**
 * @description Base class for volume interrogation (operating system agnostic).
 * @augments EventEmitter
 */
export class VolumeInterrogatorBase extends EventEmitter {
    /**
     * @description Constructor
     * @class
     * @param {object} [config] - The settings to use for creating the object.
     * @param {number} [config.period_hr] -The time (in hours) for periodically interrogating the system.
     * @param {number} [config.default_alarm_threshold] - The default low space threshold, in percent.
     * @param {object[]} [config.volume_customizations] - Array of objects for per-volume customizations.
     * @param {VOLUME_IDENTIFICATION_METHODS} [config.volume_customizations.volume_id_method] - The method for identifying the volume.
     * @param {string} [config.volume_customizations.volume_name] - The name of the volume (required when `config.volume_customizations.volume_id_method === VOLUME_IDENTIFICATION_METHODS.Name`)
     * @param {string} [config.volume_customizations.volume_serial_num] - The serial number of the volume
     *                                                                    (required when `config.volume_customizations.volume_id_method === VOLUME_IDENTIFICATION_METHODS.SerialNumber`)
     * @param {boolean} [config.volume_customizations.volume_low_space_alarm_active] - The flag indicating if the low space alarm is active or not.
     * @param {number} [config.volume_customizations.volume_alarm_threshold] - The  low space threshold, in percent
     *                                                                         (required when `config.volume_customizations.volume_low_space_alarm_active === true`)
     * @throws {TypeError}  - thrown if the configuration item is not the expected type.
     * @throws {RangeError} - thrown if the configuration parameters are out of bounds.
     */
    constructor(config) {
        let pollingPeriod           = DEFAULT_PERIOD_HR;
        let defaultAlarmThreshold   = DEFAULT_LOW_SPACE_THRESHOLD;
        const volumeCustomizations  = [];
        const exclusionMasks        = [];

        if (config !== undefined) {
            // Polling Period (hours)
            if (Object.prototype.hasOwnProperty.call(config, 'period_hr')) {
                if ((typeof(config.period_hr) === 'number') &&
                    (config.period_hr >= MIN_PERIOD_HR) && (config.period_hr <= MAX_PERIOD_HR)) {
                    pollingPeriod = config.period_hr;
                }
                else if (typeof(config.period_hr) !== 'number') {
                    throw new TypeError(`'config.period_hr' must be a number between ${MIN_PERIOD_HR} and ${MAX_PERIOD_HR}`);
                }
                else {
                    throw new RangeError(`'config.period_hr' must be a number between ${MIN_PERIOD_HR} and ${MAX_PERIOD_HR}`);
                }
            }
            // Default Alarm Threshold (percent)
            if (Object.prototype.hasOwnProperty.call(config, 'default_alarm_threshold')) {
                if ((typeof(config.period_hr) === 'number') &&
                    (config.default_alarm_threshold >= MIN_LOW_SPACE_THRESHOLD) &&
                    (config.default_alarm_threshold <= MAX_LOW_SPACE_THRESHOLD)) {
                    defaultAlarmThreshold = config.default_alarm_threshold;
                }
                else if (typeof(config.period_hr) !== 'number') {
                    throw new TypeError(`'config.default_alarm_threshold' must be a number between ${MIN_LOW_SPACE_THRESHOLD} and ${MAX_LOW_SPACE_THRESHOLD}`);
                }
                else {
                    throw new RangeError(`'config.default_alarm_threshold' must be a number between ${MIN_LOW_SPACE_THRESHOLD} and ${MAX_LOW_SPACE_THRESHOLD}`);
                }
            }
            // Exclusion Masks
            if (Object.prototype.hasOwnProperty.call(config, 'exclusion_masks')) {
                if (Array.isArray(config.exclusion_masks)) {
                    for (const mask of config.exclusion_masks) {
                        if (VolumeInterrogatorBase._validateVolumeExclusionMask(mask)) {
                            exclusionMasks.push(mask);
                        }
                        else {
                            throw new TypeError('\'config.volume_customizations\' item is not valid.');
                        }
                    }
                }
                else {
                    throw new TypeError('\'config.volume_customizations\' must be an array.');
                }
            }
            // Enable Volume Customizations
            if (Object.prototype.hasOwnProperty.call(config, 'volume_customizations')) {
                if (Array.isArray(config.volume_customizations)) {
                    for (const item of config.volume_customizations) {
                        if (VolumeInterrogatorBase._validateVolumeCustomization(item)) {
                            volumeCustomizations.push(item);
                        }
                        else {
                            throw new TypeError('\'config.volume_customizations\' item is not valid.');
                        }
                    }
                }
                else {
                    throw new TypeError('\'config.volume_customizations\' must be an array.');
                }
            }
        }

        // Initialize the base class.
        super();

        // Initialize data members.
        this._timeoutID                     = INVALID_TIMEOUT_ID;
        this._deferInitCheckTimeoutID       = INVALID_TIMEOUT_ID;
        this._decoupledStartTimeoutID       = INVALID_TIMEOUT_ID;
        this._checkInProgress               = false;
        this._period_hr                     = DEFAULT_PERIOD_HR;
        this._theVolumes                    = [];
        this._defaultAlarmThreshold         = defaultAlarmThreshold;
        this._volumeCustomizations          = volumeCustomizations;
        this._exclusionMasks                = exclusionMasks;

        // Callbacks bound to this object.
        this._CB__initiateCheck             = this._on_initiateCheck.bind(this);
        this._CB__ResetCheck                = this._on_reset_check.bind(this);
        this._DECOUPLE_Start                = this.Start.bind(this);
        this._CB__VolumeWatcherChange       = this._handleVolumeWatcherChangeDetected.bind(this);
        this._CB__VolumeWatcherAdded        = this._handleVolumeWatcherAdded.bind(this);

        // Set the polling period
        this.Period = pollingPeriod;

        // Get the list of watch folders.
        const watchFolders = this._watchFolders;
        // Compose the configuration for the volume watcher.
        const watcherConfig = [];
        for (const folder of watchFolders) {
            watcherConfig.push({target: folder, recursive: false, ignoreAccess: false});
        }
        // Create volume watchers and register for change notifications.
        this._volWatcher = new _volumeWatcher();
        this._volWatcher.on(_VOLUME_WATCHER_EVENTS.EVENT_CHANGE_DETECTED,  this._CB__VolumeWatcherChange);
        this._volWatcher.on(_VOLUME_WATCHER_EVENTS.EVENT_WATCH_ADD_RESULT, this._CB__VolumeWatcherAdded);
        // Add watches for the locations of interest.
        // Note: This is asynchronous and will be happening after the constructor completes.
        // eslint-disable-next-line new-cap
        this._volWatcher.AddWatches(watcherConfig);
    }

    /**
     * @description Destructor
     * @returns {void}
     */
    Terminate() {
        // eslint-disable-next-line new-cap
        this.Stop();

        // Cleanup the volume watcher
        // eslint-disable-next-line new-cap
        this._volWatcher.Terminate();

        this.removeAllListeners(VOLUME_INTERROGATOR_BASE_EVENTS.EVENT_SCANNING);
        this.removeAllListeners(VOLUME_INTERROGATOR_BASE_EVENTS.EVENT_READY);
    }

    /**
     * @description Read Property accessor for the interrogation period.
     * @returns {number} - Time, in hours, for interrogating for changes.
     */
    get Period() {
        return this._period_hr;
    }

    /**
     * @description Write Property accessor for the interrogation period.
     * @param {number} periodHR - Time, in hours, for interrogating for changes.
     * @throws {TypeError}  - thrown if 'periodHR' is not a number.
     * @throws {RangeError} - thrown if 'periodHR' outside the allowed bounds.
     */
    set Period(periodHR) {
        if ((periodHR === undefined) || (typeof(periodHR) !== 'number')) {
            throw new TypeError(`'period_hr' must be a number between ${MIN_PERIOD_HR} and ${MAX_PERIOD_HR}`);
        }
        if ((periodHR < MIN_PERIOD_HR) && (periodHR > MAX_PERIOD_HR)) {
            throw new RangeError(`'period_hr' must be a number between ${MIN_PERIOD_HR} and ${MAX_PERIOD_HR}`);
        }

        // Update the polling period
        this._period_hr = periodHR;

        // Manage the timeout
        // eslint-disable-next-line new-cap
        this.Stop();
    }

    /**
     * @description Read-Only Property accessor for the minumum period
     * @returns {number} - Time, in hours, for minimum value of the interrogation period
     */
    get MinimumPeriod() {
        return MIN_PERIOD_HR;
    }

    /**
     * @description Read-Only Property accessor for the maximum period
     * @returns {number} - Time, in hours, for maximum value of the interrogation period
     */
    get MaximumPeriod() {
        return MAX_PERIOD_HR;
    }

    /**
     * @description Read-Only Property accessor indicating if the checking of volume data is active.
     * @returns {boolean} - true if active.
     */
    get Active() {
        return (this._timeoutID !== INVALID_TIMEOUT_ID);
    }

    /**
     * @description Starts/Restart the interrogation process.
     * @returns {void}
     */
    Start() {
        // Stop the interrogation in case it is running.
        // eslint-disable-next-line new-cap
        this.Stop();

        // Get the current uptime of the operating system
        const uptime = _upTime() * 1000.0;

        // Has the operating system been running long enough?
        if (uptime < MIN_OS_UPTIME_TO_START_MS) {
            // No. So defer the start for a bit.
            this._decoupledStartTimeoutID = setTimeout(this._DECOUPLE_Start, (MIN_OS_UPTIME_TO_START_MS - uptime));
        }
        else {
            // Perform a check now.
            this._on_initiateCheck();
        }
    }

    /**
     * @description Stop the interrogation process.
     * @returns {void}
     */
    Stop() {
        // Clear the periodic innterrogation timer, if active.
        if (this._timeoutID !== INVALID_TIMEOUT_ID) {
            clearTimeout(this._timeoutID);
            this._timeoutID = INVALID_TIMEOUT_ID;
        }
        // Clear the decoupled start timer, if active.
        if (this._decoupledStartTimeoutID !== INVALID_TIMEOUT_ID) {
            clearTimeout(this._decoupledStartTimeoutID);
            this._decoupledStartTimeoutID = INVALID_TIMEOUT_ID;
        }
        // Clear the deferred interrogation initialization timer, if active.
        if (this._deferInitCheckTimeoutID !== INVALID_TIMEOUT_ID) {
            clearTimeout(this._deferInitCheckTimeoutID);
            this._deferInitCheckTimeoutID = INVALID_TIMEOUT_ID;
        }
    }

    /**
     * @description Helper function used to reset an ongoing check.
     * @summary Used to recover from unexpected errors.
     * @param {boolean} issueReady - Flag indicating if a Ready event should be emitted.
     * @returns {void}
     * @private
     */
    _on_reset_check(issueReady) {
        if ((issueReady === undefined) || (typeof(issueReady) !== 'boolean')) {
            throw new TypeError('issueReadyEvent is not a boolean.');
        }

        // Mark that the check is no longer in progress.
        this._checkInProgress = false;

        // Reset the timer id, now that it has tripped.
        this._deferInitCheckTimeoutID = INVALID_TIMEOUT_ID;

        // Clear the previously known volune data.
        this._theVolumes = [];

        // Perform operating system specific reset actions.
        this._doReset();

        if (this._decoupledStartTimeoutID !== INVALID_TIMEOUT_ID) {
            clearTimeout(this._decoupledStartTimeoutID);
            this._decoupledStartTimeoutID = INVALID_TIMEOUT_ID;
        }

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

    /**
     * @description Helper function used to initiate an interrogation of the system volumes.
     * @summary Called periodically by a timeout timer.
     * @returns {void}
     * @private
     */
    _on_initiateCheck() {
        // Is there a current volume check underway?
        const isPriorCheckInProgress = this._checkInProgress;

        _debug_process(`_on_initiateCheck(): Initiating a scan. CheckInProgress=${isPriorCheckInProgress}`);

        if (!this._checkInProgress) {
            // Alert interested clients that the scan was initiated.
            this.emit(VOLUME_INTERROGATOR_BASE_EVENTS.EVENT_SCANNING);

            // Mark that the check is in progress.
            this._checkInProgress = true;

            // Clear the previously known volune data.
            this._theVolumes = [];

            // Perform operating system specific reset actions.
            this._doReset();

            // Let the interrogation begin.
            this._initiateInterrogation();
        }
        else if (this._deferInitCheckTimeoutID === INVALID_TIMEOUT_ID) {
            this._deferInitCheckTimeoutID = setTimeout(this._CB__ResetCheck, MAX_RETRY_INIT_CHECK_TIME, true);
        }

        // Compute the number of milliseconds for the timeout.
        // Note: If there was a check in progress when we got here, try again in a little bit,
        //       do not wait for the full timeout.
        const theDelay = (isPriorCheckInProgress ? RETRY_TIMEOUT_MS : (this._period_hr * CONVERT_HR_TO_MS));
        // Queue another check
        this._timeoutID = setTimeout(this._CB__initiateCheck, theDelay);
    }

    /**
     * @description Abstract method used to initiate interrogation on derived classes.
     * @returns {void}
     * @private
     * @throws {Error} - Always thrown. Should only be invoked on derived classes.
     */
    _initiateInterrogation() {
        throw new Error('Abstract Method _initiateInterrogation() invoked!');
    }

    /**
     * @description Abstract method used to reset an interrogation.
     * @returns {void}
     * @private
     * @throws {Error} - Always thrown. Should only be invoked on derived classes.
     */
    _doReset() {
        throw new Error('Abstract Method _doReset() invoked!');
    }

    /**
     * @description Abstract property used to determine if a check is in progress.
     * @returns {void}
     * @private
     * @throws {Error} - Always thrown. Should only be invoked on derived classes.
     */
    get _isCheckInProgress() {
        throw new Error('Abstract Property _checkInProgress() invoked!');
    }

    /**
     * @description Abstract property used to get an array of watch folders used to initiate an interrogation.
     * @returns {void}
     * @private
     * @throws {Error} - Always thrown. Should only be invoked on derived classes.
     */
    get _watchFolders() {
        throw new Error('Abstract Property _watchFolders() invoked!');
    }

    /**
     * @description Helper for managing the "in progress" flag and 'ready' event
     * @returns {void}
     * @private
     */
    _updateCheckInProgress() {
        const wasCheckInProgress = this._checkInProgress;
        this._checkInProgress = this._isCheckInProgress;
        if (wasCheckInProgress && !this._checkInProgress) {
            // Fire Ready event
            this.emit(VOLUME_INTERROGATOR_BASE_EVENTS.EVENT_READY, {results: this._theVolumes});

            _debug_process('Ready event.');
            for (const volume of this._theVolumes) {
                _debug_process(`Volume Name: ${volume.Name}`);
                _debug_process(`\tVisible:    ${volume.IsVisible}`);
                _debug_process(`\tShown:      ${volume.IsShown}`);
                _debug_process(`\tMountPoint: ${volume.MountPoint}`);
                _debug_process(`\tDevNode:    ${volume.DeviceNode}`);
                // eslint-disable-next-line new-cap
                _debug_process(`\tCapacity:   ${VolumeData.ConvertFromBytesToGB(volume.Size).toFixed(4)} GB`);
                // eslint-disable-next-line new-cap
                _debug_process(`\tFree:       ${VolumeData.ConvertFromBytesToGB(volume.FreeSpace).toFixed(4)} GB`);
                // eslint-disable-next-line new-cap
                _debug_process(`\tUsed:       ${VolumeData.ConvertFromBytesToGB(volume.UsedSpace).toFixed(4)} GB`);
                _debug_process(`\t% Used:     ${((volume.UsedSpace / volume.Size) * 100.0).toFixed(2)}%`);
            }
        }
    }

    /**
     * @description Helper to compute the alert for a specific volume.
     * @param {string} volumeName - Name of the volume
     * @param {string} volumeUUID - Unique Identifier (serial number) of the volume
     * @param {number} volumePercentFree - Percentage of free space (0...100)
     * @returns {boolean} true if the alert is active
     * @private
     * @throws {TypeError} - thrown for invalid arguments
     * @throws {RangeError} - thrown when 'volumePercentFree' is outside the range of 0...100
     */
    _determineLowSpaceAlert(volumeName, volumeUUID, volumePercentFree) {
        // Validate arguments
        if ((volumeName === undefined) || (typeof(volumeName) !== 'string') || (volumeName.length <= 0)) {
            throw new TypeError('\'volumeName\' must be a non-zero length string');
        }
        if ((volumeUUID === undefined) || (typeof(volumeUUID) !== 'string') || (volumeUUID.length <= 0)) {
            throw new TypeError('\'volumeUUID\' must be a non-zero length string');
        }
        if ((volumePercentFree === undefined) || (typeof(volumePercentFree) !== 'number')) {
            throw new TypeError('\'volumePercentFree\' must be a number');
        }
        else if ((volumePercentFree < MIN_LOW_SPACE_THRESHOLD) || (volumePercentFree > MAX_LOW_SPACE_THRESHOLD)) {
            throw new RangeError(`'volumePercentFree' must be in the range of ${MIN_LOW_SPACE_THRESHOLD}...${MAX_LOW_SPACE_THRESHOLD}. ${volumePercentFree}`);
        }

        // Determine the default alert state.
        let alert = (volumePercentFree < this._defaultAlarmThreshold);

        // Does this volume have a customization?
        const volCustomizations = this._volumeCustomizations.filter((item) => {
            const match = (((item.volume_id_method === VOLUME_IDENTIFICATION_METHODS.Name) &&
                             (item.volume_name.toLowerCase() === volumeName.toLowerCase())) ||
                            ((item.volume_id_method === VOLUME_IDENTIFICATION_METHODS.SerialNumber) &&
                             (item.volume_serial_num.toLowerCase() === volumeUUID.toLowerCase())));
            return match;
        });
        if ((volCustomizations !== undefined) && (volCustomizations.length > 0)) {
            // There is at least one customization.

            // Filter for the matching customizations that indicate an alert.
            const trippedAlerts = volCustomizations.filter((item) => {
                const alertTripped = ((item.volume_low_space_alarm_active) &&
                                      (volumePercentFree < item.volume_alarm_threshold));

                return alertTripped;
            });

            // If any alerts were set, then indicate that.
            alert = (trippedAlerts.length > 0);
        }

        return alert;
    }

    /**
     * @description Event handler for file system change detections.
     *              Called when the contents of the watched folder(s) change(s).
     * @param {*} eventType - Type of change detected ('rename' or 'change')
     * @param {*} fileName - Name of the file or directory with the change.
     * @private
     * @returns {void}
     */
    _handleVolumeWatcherChangeDetected(eventType, fileName) {
        // Decouple the automatic refresh.
        setImmediate((eType, fName) => {
            _debug_process(`Volume Watcher Change Detected: type:${eType} name:${fName} active:${this.Active} chkInProgress:${this._checkInProgress}`);
            // Initiate a re-scan (decoupled from the notification event), if active (even if there
            // is a scan already in progress.)
            if (this.Active) {
                if (this._decoupledStartTimeoutID !== INVALID_TIMEOUT_ID) {
                    clearTimeout(this._decoupledStartTimeoutID);
                }
                this._decoupledStartTimeoutID = setTimeout(this._DECOUPLE_Start, FS_CHANGED_DETECTION_TIMEOUT_MS);
            }
        }, eventType, fileName);
    }

    // eslint-disable-next-line class-methods-use-this
    /**
     * @description Event handler for file system change detections.
     *              Called when the contents of the watched folder(s) change(s).
     * @param {object} result - Result of the request to add a watcher.
     * @param {string} result.target - Target of the watch
     * @param {boolean} result.success - Status of the add operation.
     * @returns {void}
     */
    _handleVolumeWatcherAdded(result) {
        if (result !== undefined) {
            _debug_process(`AddWatch Results: target:${result.target} status:${result.success}`);
        }
    }

    /**
     * @description Helper to determine if the volume should be shown or not.
     * @param {string} mountPoint - Mount Point of the volume
     * @returns {boolean} - true if shown. false otherwise.
     * @throws { TypeError } - thrown if mountPoint is not a non-null string.
     * @private
     */
    _isVolumeShown(mountPoint) {
        if ((mountPoint === undefined) ||
            (typeof(mountPoint) !== 'string') || (mountPoint.length <= 0)) {
            throw new TypeError(`_isVolumeShown. mountPoint is not valid. ${mountPoint}`);
        }

        let isShown = true;
        for (const mask of this._exclusionMasks) {
            _debug_process(`Evaluating exclusion mask '${mask}' for mount point '${mountPoint}'`);
            const reMask = new RegExp(mask);
            const matches = mountPoint.match(reMask);
            _debug_process(matches);
            isShown = isShown && (matches === null);
        }

        return isShown;
    }

    /**
     * @description Helper to evaluate the validity of the custom configuration settings.
     * @param {object} customConfig - Custom per-volume configuration settings.
     * @param {string} customConfig.volume_id_method - The method for identifying the volume.
     * @param {string} customConfig.volume_name - The name of the volume.
     *                                      (required when `config.volume_id_method === VOLUME_IDENTIFICATION_METHODS.Name`)
     * @param {string} customConfig.volume_serial_num - The serial number of the volume.
     *                                            (required when `config.volume_id_method === VOLUME_IDENTIFICATION_METHODS.SerialNumber`)
     * @param {boolean} customConfig.volume_low_space_alarm_active - The flag indicating if the low space alarm is active or not.
     * @param {number} customConfig.volume_alarm_threshold - The low space threshold, in percent.
     *                                                 (required when `config.volume_low_space_alarm_active === true`)
     * @returns {boolean} `true` if the configuration is valid. `false` otherwise.
     * @private
     */
    static _validateVolumeCustomization(customConfig) {
        // Initial sanoty check.
        let valid = (customConfig !== undefined);

        if (valid) {
            // Volume Id Method
            if ((!Object.prototype.hasOwnProperty.call(customConfig, 'volume_id_method')) ||
                (typeof(customConfig.volume_id_method) !== 'string')                      ||
                (Object.values(VOLUME_IDENTIFICATION_METHODS).indexOf(customConfig.volume_id_method) < 0)) {
                valid = false;
            }
            // Volume Name
            if (valid &&
                (customConfig.volume_id_method === VOLUME_IDENTIFICATION_METHODS.Name) &&
                ((!Object.prototype.hasOwnProperty.call(customConfig, 'volume_name')) ||
                 (typeof(customConfig.volume_name) !== 'string')                      ||
                 (customConfig.volume_name.length <= 0))) {
                valid = false;
            }
            // Volume Serial Number
            if (valid &&
                (customConfig.volume_id_method === VOLUME_IDENTIFICATION_METHODS.SerialNumber) &&
                ((!Object.prototype.hasOwnProperty.call(customConfig, 'volume_serial_num')) ||
                 (typeof(customConfig.volume_serial_num) !== 'string')                      ||
                 (customConfig.volume_serial_num.length <= 0))) {
                valid = false;
            }
            // Low Space Alarm Active
            if ((!Object.prototype.hasOwnProperty.call(customConfig, 'volume_low_space_alarm_active')) ||
                (typeof(customConfig.volume_low_space_alarm_active) !== 'boolean')) {
                valid = false;
            }
            // Low Space Alarm Threshold
            if (valid &&
                customConfig.volume_low_space_alarm_active &&
                ((!Object.prototype.hasOwnProperty.call(customConfig, 'volume_alarm_threshold')) ||
                 (typeof(customConfig.volume_alarm_threshold) !== 'number')                      ||
                 (customConfig.volume_alarm_threshold <= MIN_LOW_SPACE_THRESHOLD)                ||
                 (customConfig.volume_alarm_threshold >= MAX_LOW_SPACE_THRESHOLD))) {
                valid = false;
            }
        }

        return valid;
    }

    /**
     * @description Helper to evaluate the validity of the volume exclusion configuration.
     * @param {object} maskConfig - Volume exclusion mask.
     * @returns {boolean} - `true` if the exclusion mask is valid. `false` otherwise.
     */
    static _validateVolumeExclusionMask(maskConfig) {
        let valid = (maskConfig !== undefined);

        if (valid) {
            valid = (typeof(maskConfig) === 'string');
        }
        return valid;
    }
}
export default VolumeInterrogatorBase;