11cb0ef41Sopenharmony_ciconst os = require('os') 21cb0ef41Sopenharmony_ciconst { join, dirname, basename } = require('path') 31cb0ef41Sopenharmony_ciconst { format } = require('util') 41cb0ef41Sopenharmony_ciconst { glob } = require('glob') 51cb0ef41Sopenharmony_ciconst { Minipass } = require('minipass') 61cb0ef41Sopenharmony_ciconst fsMiniPass = require('fs-minipass') 71cb0ef41Sopenharmony_ciconst fs = require('fs/promises') 81cb0ef41Sopenharmony_ciconst log = require('./log-shim') 91cb0ef41Sopenharmony_ciconst Display = require('./display') 101cb0ef41Sopenharmony_ci 111cb0ef41Sopenharmony_ciconst padZero = (n, length) => n.toString().padStart(length.toString().length, '0') 121cb0ef41Sopenharmony_ciconst globify = pattern => pattern.split('\\').join('/') 131cb0ef41Sopenharmony_ci 141cb0ef41Sopenharmony_ciclass LogFiles { 151cb0ef41Sopenharmony_ci // Default to a plain minipass stream so we can buffer 161cb0ef41Sopenharmony_ci // initial writes before we know the cache location 171cb0ef41Sopenharmony_ci #logStream = null 181cb0ef41Sopenharmony_ci 191cb0ef41Sopenharmony_ci // We cap log files at a certain number of log events per file. 201cb0ef41Sopenharmony_ci // Note that each log event can write more than one line to the 211cb0ef41Sopenharmony_ci // file. Then we rotate log files once this number of events is reached 221cb0ef41Sopenharmony_ci #MAX_LOGS_PER_FILE = null 231cb0ef41Sopenharmony_ci 241cb0ef41Sopenharmony_ci // Now that we write logs continuously we need to have a backstop 251cb0ef41Sopenharmony_ci // here for infinite loops that still log. This is also partially handled 261cb0ef41Sopenharmony_ci // by the config.get('max-files') option, but this is a failsafe to 271cb0ef41Sopenharmony_ci // prevent runaway log file creation 281cb0ef41Sopenharmony_ci #MAX_FILES_PER_PROCESS = null 291cb0ef41Sopenharmony_ci 301cb0ef41Sopenharmony_ci #fileLogCount = 0 311cb0ef41Sopenharmony_ci #totalLogCount = 0 321cb0ef41Sopenharmony_ci #path = null 331cb0ef41Sopenharmony_ci #logsMax = null 341cb0ef41Sopenharmony_ci #files = [] 351cb0ef41Sopenharmony_ci 361cb0ef41Sopenharmony_ci constructor ({ 371cb0ef41Sopenharmony_ci maxLogsPerFile = 50_000, 381cb0ef41Sopenharmony_ci maxFilesPerProcess = 5, 391cb0ef41Sopenharmony_ci } = {}) { 401cb0ef41Sopenharmony_ci this.#MAX_LOGS_PER_FILE = maxLogsPerFile 411cb0ef41Sopenharmony_ci this.#MAX_FILES_PER_PROCESS = maxFilesPerProcess 421cb0ef41Sopenharmony_ci this.on() 431cb0ef41Sopenharmony_ci } 441cb0ef41Sopenharmony_ci 451cb0ef41Sopenharmony_ci static format (count, level, title, ...args) { 461cb0ef41Sopenharmony_ci let prefix = `${count} ${level}` 471cb0ef41Sopenharmony_ci if (title) { 481cb0ef41Sopenharmony_ci prefix += ` ${title}` 491cb0ef41Sopenharmony_ci } 501cb0ef41Sopenharmony_ci 511cb0ef41Sopenharmony_ci return format(...args) 521cb0ef41Sopenharmony_ci .split(/\r?\n/) 531cb0ef41Sopenharmony_ci .map(Display.clean) 541cb0ef41Sopenharmony_ci .reduce((lines, line) => 551cb0ef41Sopenharmony_ci lines += prefix + (line ? ' ' : '') + line + os.EOL, 561cb0ef41Sopenharmony_ci '' 571cb0ef41Sopenharmony_ci ) 581cb0ef41Sopenharmony_ci } 591cb0ef41Sopenharmony_ci 601cb0ef41Sopenharmony_ci on () { 611cb0ef41Sopenharmony_ci this.#logStream = new Minipass() 621cb0ef41Sopenharmony_ci process.on('log', this.#logHandler) 631cb0ef41Sopenharmony_ci } 641cb0ef41Sopenharmony_ci 651cb0ef41Sopenharmony_ci off () { 661cb0ef41Sopenharmony_ci process.off('log', this.#logHandler) 671cb0ef41Sopenharmony_ci this.#endStream() 681cb0ef41Sopenharmony_ci } 691cb0ef41Sopenharmony_ci 701cb0ef41Sopenharmony_ci load ({ path, logsMax = Infinity } = {}) { 711cb0ef41Sopenharmony_ci // dir is user configurable and is required to exist so 721cb0ef41Sopenharmony_ci // this can error if the dir is missing or not configured correctly 731cb0ef41Sopenharmony_ci this.#path = path 741cb0ef41Sopenharmony_ci this.#logsMax = logsMax 751cb0ef41Sopenharmony_ci 761cb0ef41Sopenharmony_ci // Log stream has already ended 771cb0ef41Sopenharmony_ci if (!this.#logStream) { 781cb0ef41Sopenharmony_ci return 791cb0ef41Sopenharmony_ci } 801cb0ef41Sopenharmony_ci 811cb0ef41Sopenharmony_ci log.verbose('logfile', `logs-max:${logsMax} dir:${this.#path}`) 821cb0ef41Sopenharmony_ci 831cb0ef41Sopenharmony_ci // Pipe our initial stream to our new file stream and 841cb0ef41Sopenharmony_ci // set that as the new log logstream for future writes 851cb0ef41Sopenharmony_ci // if logs max is 0 then the user does not want a log file 861cb0ef41Sopenharmony_ci if (this.#logsMax > 0) { 871cb0ef41Sopenharmony_ci const initialFile = this.#openLogFile() 881cb0ef41Sopenharmony_ci if (initialFile) { 891cb0ef41Sopenharmony_ci this.#logStream = this.#logStream.pipe(initialFile) 901cb0ef41Sopenharmony_ci } 911cb0ef41Sopenharmony_ci } 921cb0ef41Sopenharmony_ci 931cb0ef41Sopenharmony_ci // Kickoff cleaning process, even if we aren't writing a logfile. 941cb0ef41Sopenharmony_ci // This is async but it will always ignore the current logfile 951cb0ef41Sopenharmony_ci // Return the result so it can be awaited in tests 961cb0ef41Sopenharmony_ci return this.#cleanLogs() 971cb0ef41Sopenharmony_ci } 981cb0ef41Sopenharmony_ci 991cb0ef41Sopenharmony_ci log (...args) { 1001cb0ef41Sopenharmony_ci this.#logHandler(...args) 1011cb0ef41Sopenharmony_ci } 1021cb0ef41Sopenharmony_ci 1031cb0ef41Sopenharmony_ci get files () { 1041cb0ef41Sopenharmony_ci return this.#files 1051cb0ef41Sopenharmony_ci } 1061cb0ef41Sopenharmony_ci 1071cb0ef41Sopenharmony_ci get #isBuffered () { 1081cb0ef41Sopenharmony_ci return this.#logStream instanceof Minipass 1091cb0ef41Sopenharmony_ci } 1101cb0ef41Sopenharmony_ci 1111cb0ef41Sopenharmony_ci #endStream (output) { 1121cb0ef41Sopenharmony_ci if (this.#logStream) { 1131cb0ef41Sopenharmony_ci this.#logStream.end(output) 1141cb0ef41Sopenharmony_ci this.#logStream = null 1151cb0ef41Sopenharmony_ci } 1161cb0ef41Sopenharmony_ci } 1171cb0ef41Sopenharmony_ci 1181cb0ef41Sopenharmony_ci #logHandler = (level, ...args) => { 1191cb0ef41Sopenharmony_ci // Ignore pause and resume events since we 1201cb0ef41Sopenharmony_ci // write everything to the log file 1211cb0ef41Sopenharmony_ci if (level === 'pause' || level === 'resume') { 1221cb0ef41Sopenharmony_ci return 1231cb0ef41Sopenharmony_ci } 1241cb0ef41Sopenharmony_ci 1251cb0ef41Sopenharmony_ci // If the stream is ended then do nothing 1261cb0ef41Sopenharmony_ci if (!this.#logStream) { 1271cb0ef41Sopenharmony_ci return 1281cb0ef41Sopenharmony_ci } 1291cb0ef41Sopenharmony_ci 1301cb0ef41Sopenharmony_ci const logOutput = this.#formatLogItem(level, ...args) 1311cb0ef41Sopenharmony_ci 1321cb0ef41Sopenharmony_ci if (this.#isBuffered) { 1331cb0ef41Sopenharmony_ci // Cant do anything but buffer the output if we dont 1341cb0ef41Sopenharmony_ci // have a file stream yet 1351cb0ef41Sopenharmony_ci this.#logStream.write(logOutput) 1361cb0ef41Sopenharmony_ci return 1371cb0ef41Sopenharmony_ci } 1381cb0ef41Sopenharmony_ci 1391cb0ef41Sopenharmony_ci // Open a new log file if we've written too many logs to this one 1401cb0ef41Sopenharmony_ci if (this.#fileLogCount >= this.#MAX_LOGS_PER_FILE) { 1411cb0ef41Sopenharmony_ci // Write last chunk to the file and close it 1421cb0ef41Sopenharmony_ci this.#endStream(logOutput) 1431cb0ef41Sopenharmony_ci if (this.#files.length >= this.#MAX_FILES_PER_PROCESS) { 1441cb0ef41Sopenharmony_ci // but if its way too many then we just stop listening 1451cb0ef41Sopenharmony_ci this.off() 1461cb0ef41Sopenharmony_ci } else { 1471cb0ef41Sopenharmony_ci // otherwise we are ready for a new file for the next event 1481cb0ef41Sopenharmony_ci this.#logStream = this.#openLogFile() 1491cb0ef41Sopenharmony_ci } 1501cb0ef41Sopenharmony_ci } else { 1511cb0ef41Sopenharmony_ci this.#logStream.write(logOutput) 1521cb0ef41Sopenharmony_ci } 1531cb0ef41Sopenharmony_ci } 1541cb0ef41Sopenharmony_ci 1551cb0ef41Sopenharmony_ci #formatLogItem (...args) { 1561cb0ef41Sopenharmony_ci this.#fileLogCount += 1 1571cb0ef41Sopenharmony_ci return LogFiles.format(this.#totalLogCount++, ...args) 1581cb0ef41Sopenharmony_ci } 1591cb0ef41Sopenharmony_ci 1601cb0ef41Sopenharmony_ci #getLogFilePath (count = '') { 1611cb0ef41Sopenharmony_ci return `${this.#path}debug-${count}.log` 1621cb0ef41Sopenharmony_ci } 1631cb0ef41Sopenharmony_ci 1641cb0ef41Sopenharmony_ci #openLogFile () { 1651cb0ef41Sopenharmony_ci // Count in filename will be 0 indexed 1661cb0ef41Sopenharmony_ci const count = this.#files.length 1671cb0ef41Sopenharmony_ci 1681cb0ef41Sopenharmony_ci try { 1691cb0ef41Sopenharmony_ci // Pad with zeros so that our log files are always sorted properly 1701cb0ef41Sopenharmony_ci // We never want to write files ending in `-9.log` and `-10.log` because 1711cb0ef41Sopenharmony_ci // log file cleaning is done by deleting the oldest so in this example 1721cb0ef41Sopenharmony_ci // `-10.log` would be deleted next 1731cb0ef41Sopenharmony_ci const f = this.#getLogFilePath(padZero(count, this.#MAX_FILES_PER_PROCESS)) 1741cb0ef41Sopenharmony_ci // Some effort was made to make the async, but we need to write logs 1751cb0ef41Sopenharmony_ci // during process.on('exit') which has to be synchronous. So in order 1761cb0ef41Sopenharmony_ci // to never drop log messages, it is easiest to make it sync all the time 1771cb0ef41Sopenharmony_ci // and this was measured to be about 1.5% slower for 40k lines of output 1781cb0ef41Sopenharmony_ci const logStream = new fsMiniPass.WriteStreamSync(f, { flags: 'a' }) 1791cb0ef41Sopenharmony_ci if (count > 0) { 1801cb0ef41Sopenharmony_ci // Reset file log count if we are opening 1811cb0ef41Sopenharmony_ci // after our first file 1821cb0ef41Sopenharmony_ci this.#fileLogCount = 0 1831cb0ef41Sopenharmony_ci } 1841cb0ef41Sopenharmony_ci this.#files.push(logStream.path) 1851cb0ef41Sopenharmony_ci return logStream 1861cb0ef41Sopenharmony_ci } catch (e) { 1871cb0ef41Sopenharmony_ci // If the user has a readonly logdir then we don't want to 1881cb0ef41Sopenharmony_ci // warn this on every command so it should be verbose 1891cb0ef41Sopenharmony_ci log.verbose('logfile', `could not be created: ${e}`) 1901cb0ef41Sopenharmony_ci } 1911cb0ef41Sopenharmony_ci } 1921cb0ef41Sopenharmony_ci 1931cb0ef41Sopenharmony_ci async #cleanLogs () { 1941cb0ef41Sopenharmony_ci // module to clean out the old log files 1951cb0ef41Sopenharmony_ci // this is a best-effort attempt. if a rm fails, we just 1961cb0ef41Sopenharmony_ci // log a message about it and move on. We do return a 1971cb0ef41Sopenharmony_ci // Promise that succeeds when we've tried to delete everything, 1981cb0ef41Sopenharmony_ci // just for the benefit of testing this function properly. 1991cb0ef41Sopenharmony_ci 2001cb0ef41Sopenharmony_ci try { 2011cb0ef41Sopenharmony_ci const logPath = this.#getLogFilePath() 2021cb0ef41Sopenharmony_ci const logGlob = join(dirname(logPath), basename(logPath) 2031cb0ef41Sopenharmony_ci // tell glob to only match digits 2041cb0ef41Sopenharmony_ci .replace(/\d/g, '[0123456789]') 2051cb0ef41Sopenharmony_ci // Handle the old (prior to 8.2.0) log file names which did not have a 2061cb0ef41Sopenharmony_ci // counter suffix 2071cb0ef41Sopenharmony_ci .replace(/-\.log$/, '*.log') 2081cb0ef41Sopenharmony_ci ) 2091cb0ef41Sopenharmony_ci 2101cb0ef41Sopenharmony_ci // Always ignore the currently written files 2111cb0ef41Sopenharmony_ci const files = await glob(globify(logGlob), { ignore: this.#files.map(globify), silent: true }) 2121cb0ef41Sopenharmony_ci const toDelete = files.length - this.#logsMax 2131cb0ef41Sopenharmony_ci 2141cb0ef41Sopenharmony_ci if (toDelete <= 0) { 2151cb0ef41Sopenharmony_ci return 2161cb0ef41Sopenharmony_ci } 2171cb0ef41Sopenharmony_ci 2181cb0ef41Sopenharmony_ci log.silly('logfile', `start cleaning logs, removing ${toDelete} files`) 2191cb0ef41Sopenharmony_ci 2201cb0ef41Sopenharmony_ci for (const file of files.slice(0, toDelete)) { 2211cb0ef41Sopenharmony_ci try { 2221cb0ef41Sopenharmony_ci await fs.rm(file, { force: true }) 2231cb0ef41Sopenharmony_ci } catch (e) { 2241cb0ef41Sopenharmony_ci log.silly('logfile', 'error removing log file', file, e) 2251cb0ef41Sopenharmony_ci } 2261cb0ef41Sopenharmony_ci } 2271cb0ef41Sopenharmony_ci } catch (e) { 2281cb0ef41Sopenharmony_ci // Disable cleanup failure warnings when log writing is disabled 2291cb0ef41Sopenharmony_ci if (this.#logsMax > 0) { 2301cb0ef41Sopenharmony_ci log.warn('logfile', 'error cleaning log files', e) 2311cb0ef41Sopenharmony_ci } 2321cb0ef41Sopenharmony_ci } finally { 2331cb0ef41Sopenharmony_ci log.silly('logfile', 'done cleaning log files') 2341cb0ef41Sopenharmony_ci } 2351cb0ef41Sopenharmony_ci } 2361cb0ef41Sopenharmony_ci} 2371cb0ef41Sopenharmony_ci 2381cb0ef41Sopenharmony_cimodule.exports = LogFiles 239