chipmunkbot3/util/console.js

180 lines
5 KiB
JavaScript
Raw Normal View History

2024-04-04 21:44:45 -04:00
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)) {
const inStream = fs.createReadStream(filename)
let outStreamRaw
let outStreamGzip
inStream.on('data', chunk => {
if (!outStreamRaw) {
// 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 = this.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...
outStreamRaw = fs.createWriteStream(outFilename)
outStreamGzip = zlib.createGzip()
outStreamGzip.on('data', chunk => outStreamRaw.write(chunk))
outStreamGzip.on('end', () => outStreamRaw.end())
}
outStreamGzip.write(chunk)
})
await stream.finished(inStream)
outStreamGzip?.end()
}
this.filename = filename
this.#writeStream = fs.createWriteStream(filename)
this.#writeStream.setDefaultEncoding('utf-8')
this.#writeStream.write(formatDate() + '\n')
}
}
function formatDate (date = new Date()) {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDay()
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