UNPKG

journalctl-ts

Version:

A journalctl client for node, written in Typescript

263 lines 11.4 kB
"use strict"; /** * @author Thomas Novotny * @packageDocumentation */ var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); } var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var g = generator.apply(thisArg, _arguments || []), i, q = []; return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i; function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; } function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } function fulfill(value) { resume("next", value); } function reject(value) { resume("throw", value); } function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = __importDefault(require("events")); const child_process_1 = __importDefault(require("child_process")); const readline_1 = __importDefault(require("readline")); /** * Validata a fieldname as proper journald fieldName. Allowed are only A-Z, 0-9 and underscores * @param fieldName Field name to validate * @throws Error if field name is invalid * @returns The same field name */ function validateFieldName(fieldName) { const RegExp_ValidFieldName = /^[A-Z0-9_]+$/; if (!RegExp_ValidFieldName.test(fieldName)) throw new Error(`Not a valid journald Field Name: "${fieldName}"`); return fieldName; } /** * Validate a string as proper syslog identifier * @param identifier String to validate * @throws Error if field syslog identifier is invalid * @returns The same identifier */ function validateSyslogIdentifier(identifier) { const RegExp_SyslogIdentifier = /^[^\s]+$/; if (!RegExp_SyslogIdentifier.test(identifier)) throw new Error(`Not a valid SYSLOG_IDENTIFIER: "${identifier}"`); return identifier; } /** * Validate date as proper date * @param dateString date string * @throws Error if date string is invalid * @returns well-formed date string */ function validateDate(dateString) { const RegExp_DateString = /^\d\d\d\d-\d\d-\d\d(?: \d\d:\d\d(?::\d\d)?)?$/; if (!RegExp_DateString.test(dateString)) throw new Error(`Not a valid date: "${dateString}"`); if (isNaN(new Date(dateString).getTime())) throw new Error(`Not a valid date: "${dateString}"`); return dateString; } /** * Validate that a value is a positive integer * @throws Error if value is either not an integer or not positive * @param value the value that was tested */ function validatePositiveInteger(value) { if (!Number.isInteger(value) || isNaN(value)) throw new Error(`Not an integer: ${value}`); if (value < 0) throw new Error(`Not a positive integer: ${value}`); return value; } /** * Validates a journald priority. * If priority is an integer, the functions clamps the values to the next valid priority value * @param priority priority value * @throws Error if priority is not a number or not an integer * @returns valid journald priority */ function validatePriority(priority) { validatePositiveInteger(priority); if (priority < 0 || priority > 7) throw new Error(`Not a valid priority: ${priority}`); return priority; } /** * Validates a systemd unit name * @param unit Unit name to validate * @throws Error if unit name is not valid * @returns The unit name */ function validateUnit(unit) { const RegExp_Systemd_Unit_Name = /^[A-Za-z0-9:\-_\.\\]+$/; if (!RegExp_Systemd_Unit_Name.test(unit)) throw new Error(`Not a valid systemd unit name: "${unit}"`); return unit; } /** * This class is an EventEmitter that watches the local systemd journal for events and emits them. * @typeParam FieldNames List of field names that should be included in the JournalD messages. If left undefined, the messages will include all available fields. */ class JournalCtl extends events_1.default { /** * Create a new JournalCtl interface with the specified options * * @param options Optional options object * @throws Error if options are malformed or if the journalctl process can not be spawned */ constructor(options = {}, outputFields) { super(); /** Initial args: * -q (no error output) * -o json (output as newline delimited json) */ //const args: string[] = ['-q', '-o', 'json']; const args = ['-o', 'json']; if (options.until) { let dateString = typeof (options.until) === 'string' ? validateDate(options.until) : options.until.toISOString().replace(/T|\.\d\d\dZ/g, ' ').trim(); args.push('-U', `${dateString}`); } else { // If "until" is not set, set -f (tail) args.push("-f"); } if (options.all) args.push('-a'); // Unless otherwise defined, default lines are 10 if (!options.until && !options.since && options.lines === undefined) { args.push('-n', `${10}`); } else if (options.lines !== undefined) { args.push('-n', `${validatePositiveInteger(options.lines)}`); } if (options.since) { let dateString = typeof (options.since) === 'string' ? validateDate(options.since) : options.since.toISOString().replace(/T|\.\d\d\dZ/g, ' ').trim(); args.push('-S', `${dateString}`); } if (options.since && options.until) { const dSince = (typeof (options.since) == 'string' ? new Date(options.since) : options.since); const dUntil = (typeof (options.until) == 'string' ? new Date(options.until) : options.until); if (dSince.getTime() > dUntil.getTime()) throw new Error(`'Since' date can not be more recent than 'Until' date.`); } if (options.priority !== undefined) { args.push('-p'); if (typeof (options.priority) === 'number') { args.push(`${validatePriority(options.priority)}`); } else { args.push(`${validatePriority(options.priority.from)}..${validatePriority(options.priority.to)}`); } } if (options.identifier !== undefined) args.push('-t', validateSyslogIdentifier(options.identifier)); if (options.unit) args.push('-u', validateUnit(options.unit)); if (options.filter) { for (let key in options.filter) { args.push(`${validateFieldName(key)}=${options.filter[key]}`); } } if (outputFields) { outputFields.forEach((fieldName) => { args.push('--output-field', `${validateFieldName(fieldName)}`); }); if (!outputFields.includes("MESSAGE")) args.push('--output-field', `MESSAGE`); if (!outputFields.includes("SYSLOG_IDENTIFIER")) args.push('--output-field', `SYSLOG_IDENTIFIER`); if (!outputFields.includes("_HOSTNAME")) args.push('--output-field', `_HOSTNAME`); } // Start journalctl //console.log(['journalctl', ...args].join(' ')); this.journalCtl = child_process_1.default.spawn('journalctl', args); this.stdErrorText = ""; this.journalCtl.stderr.on('data', (chunk) => { this.stdErrorText += chunk.toString(); }); this.journalCtl.on("exit", (code) => { var _a; if (code) { this.emit('error', new Error((_a = this.stdErrorText) !== null && _a !== void 0 ? _a : `Process exited with code ${code}`)); } this.emit('exit'); }); this.journalCtl.on("error", (err) => { if (err.message.includes("ENOENT")) { this.emit('error', new Error(`journalctl executable not found. Is this a systemd distribution?`)); } else if (err.message.includes("EACCES")) { this.emit('error', new Error(`journalctl executable not accessible.`)); } else { this.emit('error', err); } }); const lineReader = readline_1.default.createInterface(this.journalCtl.stdout); lineReader.on('line', (line) => { try { // Note: apparently journald can in some cases end messages like ", }" which adds an invalid comma. The replace function removes it let message = JSON.parse(line.replace(/,\s*\}$/, ' }')); if (message.MESSAGE === undefined) message.MESSAGE = ""; if (message._HOSTNAME === undefined) message._HOSTNAME = ""; this.emit('message', message); } catch (err) { this.emit('error', new Error(`Could not parse JSON: ${line}`)); } }); this.generatorInstanceExtists = false; } on(event, callBack) { return super.on(event, callBack); } /** * Creates an AsyncGenerator for this instance * @returns AsyncGenerator which yields one journald message at a time as they come in */ createGenerator() { return __asyncGenerator(this, arguments, function* createGenerator_1() { if (this.generatorInstanceExtists) { throw new Error(`Only one instance of the async message generator can be created per JournalCtl instance.`); } this.generatorInstanceExtists = true; let totalEntries = 0; let resolve; //let reject: (err: Error) => void; const messagePromises = []; const createPromise = () => new Promise((res /*, rej */) => { resolve = res; /*reject = rej;*/ }); messagePromises.push(createPromise()); this.on('error', () => { //reject(err); resolve(undefined); }).on('message', message => { const oldResolve = resolve; messagePromises.push(createPromise()); totalEntries++; oldResolve(message); }).on('exit', () => { resolve(undefined); }); let result; while (result = yield __await(messagePromises.shift())) { yield yield __await(result); } return yield __await({ totalEntries: totalEntries }); }); } /** * Stops the journalctl process and causes this instance to eventually emit the 'exit' event * @returns the return value of ChildProcess.kill() */ stop() { return this.journalCtl.kill(); } } exports.default = JournalCtl; //# sourceMappingURL=JournalCtl.js.map