180 lines
5 KiB
JavaScript
180 lines
5 KiB
JavaScript
|
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
|