const fs = require('fs') const path = require('path') const zlib = require('zlib') const stream = require('stream/promises') const EventEmitter = require('events') const fileExists = require('./file_exists') const colorCodeStringify = require('../util/chat/stringify/color_code') const ansiStringify = require('../util/chat/stringify/ansi') class Console extends EventEmitter { #writeStream = null #forks = [] logdir = null filename = null parent = null constructor ({ lang = {}, namespace = '', stdout = process.stdout, stderr = process.stderr, readline = null } = {}) { super() this.language = lang this.namespace = namespace this.stdout = stdout this.stderr = stderr this.readline = readline this.readline?.on('line', line => { this.emit('line', line) this.readline.prompt(true) }) this.on('local_error', error => this.emit('error', error)) this.on('error', error => this.#forks.forEach(fork => fork.emit('error', error))) this.on('line', line => this.#forks.forEach(fork => fork.emit('line', line))) this.on('end', () => this.#forks.forEach(fork => fork.emit('end'))) } info (text) { this.#log(text, '\xa72INFO', this.stdout) } warn (text) { this.#log(text, '\xa7eWARN', this.stderr) } error (text) { this.#log(text, '\xa7cERROR', this.stderr) } log (text) { this.info(text) } #log (text, prefix, stdout) { const raw = [`[${formatTimestamp()} ${prefix}\xa7r] `, text] if (this.namespace) raw[0] += `[${this.namespace}] ` const options = { lang: this.language } const colorStr = colorCodeStringify(raw, options) const ansiStr = ansiStringify(raw, options) if (stdout) stdout.write(ansiStr + '\x1b[0m\n') this.#writeToLog(colorStr + '\n') } #writeToLog (text) { if (this.parent) return this.parent.#writeToLog(text) this.#writeStream?.write(text) } fork (namespace) { if (this.namespace) namespace = this.namespace + '/' + namespace const fork = new Console({ lang: this.language, namespace }) fork.parent = this this.#forks.push(fork) fork.on('end', () => (this.#forks = this.#forks.filter(_fork => _fork !== fork))) return fork } end () { this.#writeStream?.end() this.stdout = null this.stderr = null this.parent = null this.emit('end') } async createLogFile (logdir) { this.logdir = logdir const filename = path.join(logdir, 'latest.log') if (await fileExists(filename)) await copyLatestLog(this.logdir, filename) this.filename = filename this.#writeStream = fs.createWriteStream(filename) this.#writeStream.setDefaultEncoding('utf-8') this.#writeStream.write(formatDate() + '\n') } } async function copyLatestLog (logdir, filename) { const input = fs.createReadStream(filename) let out let gzip input.on('data', chunk => { if (gzip) { gzip.write(chunk) return } // Attempt to read the first line (assumed to have a date) let idx = -1 for (let i = 0; i < chunk.length; i++) { if (chunk[i] === 0x0a || chunk[i] === 0x0d) { idx = i + 1 break } } if (chunk[idx] === 0x0d && chunk[idx + 1] === 0x0a) idx++ let date if (idx !== -1) { date = chunk.subarray(0, idx).toString().trim() // sanity checking const parts = date.split('-').map(Number) if (parts.length !== 3) idx = -1 else if (parts.some(part => Number.isNaN(part))) idx = -1 } if (idx !== -1) { // The sanity checks passed, so we can skip the date now chunk = chunk.subarray(idx) } else { // stat the file for the creation date const stat = fs.statSync(filename) date = formatDate(stat.birthtime) } // handle duplicates const prefix = logdir + '/' const suffix = '.log.gz' let outFilename = prefix + date + suffix let count = 1 while (fs.existsSync(outFilename)) { outFilename = prefix + date + '-' + count++ + suffix } // so much for just some output streams... out = fs.createWriteStream(outFilename) gzip = zlib.createGzip() gzip.pipe(out) gzip.write(chunk) }) await stream.finished(input) gzip?.end() } function formatDate (date = new Date()) { const year = date.getFullYear() const month = date.getMonth() + 1 const day = date.getDate() return `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}` } function formatTimestamp (date = new Date()) { const hours = date.getHours() const minutes = date.getMinutes() const seconds = date.getSeconds() return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}` } module.exports = Console