volumeInterrogator_linux.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 VolumeInterrogatorLinuxModule
 * @requires debug
 * @see {@link https://github.com/debug-js/debug#readme}
 * @requires os
 * @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}
 */
// 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 {userInfo as _userInfo} from 'os';

/**
 * @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.
/**
 * @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 linux-based volume interrogation
 * @augments _VolumeInterrogatorBase
 */
export class VolumeInterrogator_linux 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() !== 'linux')) {
            throw new Error(`Operating system not supported. os:${operatingSystem}`);
        }

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

        this._dfSpawnInProgress = false;

        this._CB__display_free_disk_space_complete = this._on_df_complete.bind(this);
    }

    /**
     * @private
     * @description Helper function used to initiate an interrogation of the system volumes on darwin operating systems.
     * @returns {void}
     */
    _initiateInterrogation() {
        // Set Check-in-Progress.
        this._dfSpawnInProgress = true;

        // Spawn a 'ls /Volumes' to get a listing of the 'visible' volumes.
        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: ['--block-size=512', '--portability', '--print-type', '--exclude-type=tmpfs', '--exclude-type=devtmpfs']});
    }

    /**
     * @private
     * @description Helper function used to reset an interrogation.
     * @returns {void}
     */
    _doReset() {
        // Clear Check-in-Progress.
        this._dfSpawnInProgress = false;
    }

    /**
     * @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() {
        return this._dfSpawnInProgress;
    }

    /**
     * @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() {
        const {username} = _userInfo();
        _debug_process(`Username: ${username}`);

        return ([`/media/${username}`, '/mnt']);
    }

    /**
     * @private
     * @description Event handler for the SpawnHelper _SpawnHelperEvents.EVENT_COMPLETE Notification
     * @param {object} response - Spawn response.
     * @param {boolean} response.valid - Flag indicating if the spawned 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);
        }

        // Clear the spawn in progress.
        this._dfSpawnInProgress = false;

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

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

        if (response.valid &&
            (response.result !== undefined)) {
            // 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(),
                    fs_type: fields[INDEC_FILE_SYSTEM_TYPE],
                    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],
                };

                // Is this list of file systems one of the types we handle?
                if (Object.values(VOLUME_TYPES).includes(newVol.fs_type)) {
                    // 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:        newVol.fs_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:            true,
                        shown:              this._isVolumeShown(newVol.mount_point),
                        low_space_alert:    lowSpaceAlert,
                    });
                    /* eslint-enable key-spacing */

                    // Add this volume to the list of volumes.
                    this._theVolumes.push(volData);
                }
            });

            // This is all we had to do.
            this._updateCheckInProgress();
        }
        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: []});
        }
    }
}
// eslint-disable-next-line camelcase
export default VolumeInterrogator_linux;