UNPKG

hb-lib-tools

Version:

homebridge-lib Command-Line Tools`

331 lines (298 loc) 10.6 kB
// hb-lib-tools/lib/CommandLineUtility.js // // Library for Homebridge plugins. // Copyright © 2018-2025 Erik Baauw. All rights reserved. import { format } from 'node:util' import { formatError, timeout } from 'hb-lib-tools' import { chalk } from 'hb-lib-tools/chalk' import { OptionParser } from 'hb-lib-tools/OptionParser' /** Command-line tool. * <br>See {@link CommandLineTool}. * @name CommandLineTool * @type {Class} * @memberof module:hb-lib-tools */ /** Command-line tool. */ class CommandLineTool { /** Make text bold. * @param {string} text - The text. * @returns {string} - The bold text. */ static b (text) { return chalk.bold(text) } /** Make text underlined. * @param {string} text - The text. * @returns {string} - The underlined text. */ static u (text) { return chalk.underline(text) } /** Create a new instance of a command line utility. */ constructor (options = { mode: 'command' }) { // We need to handle errors here, for subtype or caller won't be able // to log the error/exception without our log functions. try { // Set program name // argv[0]: node executable, argv[1]: javascript file this.name = process.argv[1] // Use {mode: "command"} as default for logging. this._options = { chalk: false, debug: false, program: true, timestamp: false } // Set logging options. this.setOptions(options) process .on('SIGHUB', this._onSignal.bind(this)) .on('SIGINT', this._onSignal.bind(this)) .on('SIGTERM', this._onSignal.bind(this)) .on('SIGABRT', this._onSignal.bind(this)) .on('uncaughtException', async (error) => { await this.fatal('uncaught exception: %s', error.stack) }) .removeAllListeners('unhandledRejection') .on('unhandledRejection', async (error) => { await this.fatal('unhandled rejection: %s', error.stack) }) } catch (error) { this._log({ label: 'fatal', chalk: chalk.bold.red }, error) } } /** Set logging options. * @param {object} options - Loggin options. * @parameter {string} mode - Mode in which utlity is run:<br> * - `command` - From the command line. * - `daemon` - As a daemon. * - `service` - As a service. * @parameter {boolean} [debug=false] - Output debug messages. * @parameter {boolean} [vdebug=false] - Output verbose debug messages. * @parameter {boolean} [vvdebug=false] - Output very verbose debug messages. * @returns {object} - The old options. */ setOptions (options) { if (this.optionParser == null) { this.optionParser = new OptionParser(this._options) this.optionParser .boolKey('chalk') // Use chalk to colour messages. .boolKey('debug') // Show debug messages. .boolKey('vdebug') // Show verbose debug messages. .boolKey('vvdebug') // Show very verbose debug messages. .boolKey('program') // Include program name. .boolKey('timestamp') // Include timestamp. .enumKey('mode') .enumKeyValue('mode', 'command', () => { // Command-line program. this._options.chalk = false this._options.program = true this._options.timestamp = false }) .enumKeyValue('mode', 'daemon', () => { // Program runs as standalone daemon. this._options.chalk = true this._options.program = false this._options.timestamp = true }) .enumKeyValue('mode', 'service', () => { // Program runs as systemctl service. this._options.chalk = true this._options.program = false this._options.timestamp = false }) } const oldOptions = this._options this.optionParser.parse(options) if (this._options.debug) { this._options.chalk = true } return oldOptions } /** Do cleanup before exit. * @abstract */ async destroy () {} // Signal handler. async _onSignal (signal, signalNum) { this.log('got %s - exiting', signal) try { await this.destroy() } catch (error) { this.error(error) } setImmediate(() => { process.exit(128 + signalNum) }) // Prevent _onSignal() from returning. await timeout(1000) } /** Program name. * @type {string} */ get name () { return this._name } set name (name) { const list = name.split('/') this._name = list[list.length - 1] process.title = this._name } /** Debug mode is enabled. * @type {boolean} */ get debugEnabled () { return !!this._options.debug } /** Usage string. * @type {string} */ get usage () { return this._usage } set usage (usage) { this._usage = usage } /** Verbose debug mode is enabled. * @type {boolean} */ get vdebugEnabled () { return !!this._options.vdebug } // ===== Logging ============================================================= /** Print debug message to stderr. * @param {string|Error} format - The printf-style message or an instance of * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). * @param {...string} args - Arguments to the printf-style message. */ debug (format, ...args) { if (this._options.debug) { this._log({ chalk: chalk.grey }, format, ...args) } } /** Print error message to stderr. * @param {string|Error} format - The printf-style message or an instance of * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). * @param {...string} args - Arguments to the printf-style message. */ error (format, ...args) { this._log({ label: 'error', chalk: chalk.bold.red }, format, ...args) } /** Print error message to stderr and abort program. * @param {string|Error} format - The printf-style message or an instance of * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). * @param {...string} args - Arguments to the printf-style message. */ async fatal (format, ...args) { this._log({ label: 'fatal', chalk: chalk.bold.red }, format, ...args) try { await this.destroy() } catch (error) { this.error(error) } setImmediate(() => { process.exit(-1) }) // Prevent fatal() from returning. await timeout(1000) } /** Print log message to stderr. * @param {string|Error} format - The printf-style message or an instance of * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). * @param {...string} args - Arguments to the printf-style message. */ log (format, ...args) { this._log({}, format, ...args) } /** Print log message continuation to stderr. * @param {string|Error} format - The printf-style message or an instance of * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). * @param {...string} args - Arguments to the printf-style message. */ logc (format, ...args) { this._log({ noLabel: true }, format, ...args) } /** Print message to stdout. * @param {string|Error} format - The printf-style message or an instance of * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). * @param {...string} args - Arguments to the printf-style message. */ print (format, ...args) { this._log({ noLabel: true, stdout: true }, format, ...args) } /** Print verbose debug message to stderr. * @param {string|Error} format - The printf-style message or an instance of * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). * @param {...string} args - Arguments to the printf-style message. */ vdebug (format, ...args) { if (this._options.vdebug) { this._log({ chalk: chalk.grey }, format, ...args) } } /** Print very verbose debug message to stderr. * @param {string|Error} format - The printf-style message or an instance of * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). * @param {...string} args - Arguments to the printf-style message. */ vvdebug (format, ...args) { if (this._options.vvdebug) { this._log({ chalk: chalk.grey }, format, ...args) } } /** Print warning message to stderr. * @param {string|Error} format - The printf-style message or an instance of * [Error](https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_error). * @param {...string} args - Arguments to the printf-style message. */ warn (format, ...args) { this._log({ label: 'warning', chalk: chalk.yellow }, format, ...args) } // Do the heavy lifting for debug(), error(), fatal(), log(), and warn(), // taking into account the options, and errors vs exceptions. _log (params = {}, ...args) { const output = params.stdout ? process.stdout : process.stderr let timestamp = '' let message = '' let usage // If last argument is Error convert it to string. if (args.length > 0) { let lastArg = args.pop() if (lastArg instanceof Error) { if (lastArg.constructor.name === 'UsageError') { usage = true } lastArg = formatError(lastArg, this._options.chalk) } args.push(lastArg) } // Format message. if (args[0] == null) { message = '' } else if (typeof args[0] === 'string') { message = format(...args) } else { message = format('%o', ...args) } // Handle newline. if (message.substring(message.length - 2) === '\\c') { message = message.substring(0, message.length - 2) } else { message += '\n' } // Handle labels. if (!params.noLabel) { if (params.label != null) { message = params.label + ': ' + message } if (this._options.program) { message = this._name + ': ' + message } if (this._options.timestamp) { timestamp = '[' + String(new Date()).substring(0, 24) + '] ' if (this._options.chalk) { timestamp = chalk.white(timestamp) } } } // Handle colours. if (params.chalk != null && this._options.chalk) { message = params.chalk(message) } output.write(timestamp + message) if (usage && this._usage != null) { this.logc('usage: %s', this._usage) } } } export { CommandLineTool }