Source: journalctl.js

const childProcess = require('child_process')
const EventEmitter = require('events')
const JSONStream = require('./json-stream.js')

/**
 * @typedef {Object} JournalctlOptions
 * @property {boolean} [all] - Include all fields in the output.
 * @property {number} [lines] - Number of lines to show from the journal.
 * @property {string} [since] - Show entries since the specified date.
 * @property {string} [identifier] - Show entries with the specified identifier.
 * @property {string} [unit] - Show entries from the specified unit.
 * @property {string|string[]} [filter] - Filter entries by the specified criteria.
 * @property {boolean} [emitRawMessages] - Emit raw messages instead of parsed JSON.
 * @property {boolean} [disableJSONMessages] - Disable JSON message parsing.
 * @property {childProcess.SpawnOptions} [spawnOptions] - Options to pass to the child process spawn.
 * @property {string[]} [commandPrefix] - Command prefix to use instead of 'journalctl'.
 * @property {boolean} [quoteArgs] - When using a command prefix, wrap the original command and arguments in quotes.
 */

class Journalctl extends EventEmitter {
  #cmd = 'journalctl'
  /** @type {childProcess.ChildProcessWithoutNullStreams} */
  #journalctl
  /** @type {boolean} */
  #emitRawMessages = false
  /** @type {boolean} */
  #disableJSONMessages = false
  /** @type {childProcess.SpawnOptions|undefined} */
  #spawnOptions = undefined
  /** @type {string[]} */
  #args

  /**
  * Creates an instance of Journalctl.
  * @param {JournalctlOptions} [opts={}] - Options to configure journalctl.
  */
  constructor (opts = {}) {
    super()

    // Decode opts
    this.#args = ['-f', '-o', 'json']
    if (opts.all) this.#args.push('-a')
    if (opts.lines) this.#args.push('-n', opts.lines)
    if (opts.since) this.#args.push('-S', opts.since)
    if (opts.identifier) this.#args.push('-t', opts.identifier)
    if (opts.unit) this.#args.push('-u', opts.unit)
    if (opts.emitRawMessages) this.#emitRawMessages = true
    if (opts.disableJSONMessages) this.#disableJSONMessages = true
    if (opts.spawnOptions) this.#spawnOptions = opts.spawnOptions

    if (opts.filter) {
      if (!(opts.filter instanceof Array)) {
        opts.filter = [opts.filter]
      }
      opts.filter.forEach(f => this.#args.push(f))
    }

    if (opts.commandPrefix && opts.commandPrefix.length > 0) {
      const newArgs = [this.#cmd, ...this.#args]
      if (opts.quoteArgs) {
        this.#args = [`"${newArgs.join(' ')}"`]
      }
      this.#args = opts.commandPrefix.slice(1).concat(this.#args)
      this.#cmd = opts.commandPrefix[0]
    }

    // Start journalctl
    this.#journalctl = childProcess.spawn(this.#cmd, this.#args, this.#spawnOptions)
      .on('error', this.#onError.bind(this))
      .on('close', this.#onClose.bind(this))

    if (this.#emitRawMessages) {
      this.#journalctl.stdout.on('data', chunk => {
        this.emit('raw-message', chunk)
      })
    }
    if (!this.#disableJSONMessages) {
      // Setup decoder
      const decoder = new JSONStream(e => {
        this.emit('json-message', e)
      })
      this.#journalctl.stdout.on('data', chunk => {
        decoder.decode(chunk.toString())
      })
    }
  }

  /**
   * Gets the stdout stream of the journalctl process.
   * @returns {node:stream}  The stdout stream.
   */
  getStdout () {
    return this.#journalctl.stdout
  }

  /**
   * Gets the stderr stream of the journalctl process.
   * @returns {node:stream} The stderr stream.
   */
  getStderr () {
    return this.#journalctl.stderr
  }

  #onError (err) {
    this.emit('error', err)
  }

  #onClose (code) {
    if (code !== 0) {
      this.emit('error', new Error(`journalctl exited with code ${code}`))
    }
    this.emit('close')
  }

  /**
  * Stops the journalctl process.
  * @param {Function} [cb] - Callback function to be called on process exit.
  */
  stop (cb) {
    // Kill the process
    if (cb) this.#journalctl.on('exit', cb)
    this.#journalctl.kill()
  }
}

module.exports = Journalctl