import fs from 'fs'; import path from 'path'; import { Handler } from '../../Server'; import { Error, HandlerConfig, Log } from '../../type'; const enum FileType { Log = 'log', Error = 'error' } const FILE_CLOSE_COUNTDOWN = 60 * 10; const FILE_CLOSE_CHECK_DELAY = 1; const LOG_LEVEL_TITLE = [ null, 'LOG', 'WARN', 'ERROR' ] export default class LogSave implements Handler { private readonly fileDir: string = null; private timer: NodeJS.Timer = null; private fileCache: { [id: string]: { countdown: number, stream: fs.WriteStream } } = null; constructor(config: HandlerConfig) { this.fileCache = {}; if (config.path) this.fileDir = config.path as string; else this.fileDir = path.join(__dirname, '..', 'log'); } private mkdir(dir: string): void { if (!fs.existsSync(dir)) { let parentDir = path.dirname(dir); this.mkdir(parentDir); fs.mkdirSync(dir); } } public onServerStart(): void { this.timer = setInterval(() => this.checkFileCache(), FILE_CLOSE_CHECK_DELAY * 1000); } public onServerStop(): void { this.timer && clearInterval(this.timer); this.timer = null; this.checkFileCache(true); } public onClientConnected(id: string): void { } public onClientLogMessage(id: string, logs: Log[]): void { const fileStream = this.openFile(id, FileType.Log); logs.forEach(log => { const date = new Date(log.time); const timeString = `${date.getHours()}:${('0' + date.getMinutes()).slice(-2)}:${('0' + date.getSeconds()).slice(-2)}`; fileStream.write(`[${timeString}][${LOG_LEVEL_TITLE[log.level]}] ${log.text}\n`); }); } public onClientErrorMessage(id: string, error: Error): void { const fileStream = this.openFile(id, FileType.Error); const date = new Date(error.time); const timeString = `${date.getHours()}:${('0' + date.getMinutes()).slice(-2)}:${('0' + date.getSeconds()).slice(-2)}`; let errorString = `time: ${timeString}\n`; for (const key in error) { key !== 'time' && (errorString += `${key}: ${error[key]}\n`); } errorString += '\n' fileStream.write(errorString); } public onClientDisconnect(id: string): void { this.closeFile(id, FileType.Log); this.closeFile(id, FileType.Error); } private getFilePath(id: string, fileType: FileType): string { const date = new Date(); const dateString = `${date.getFullYear()}-${('0' + (date.getMonth() + 1)).slice(-2)}-${('0' + date.getDate()).slice(-2)}`; const dirPath = path.join(this.fileDir, dateString, fileType); this.mkdir(dirPath); return path.join(dirPath, `${id}.txt`); } private openFile(id: string, fileType: FileType): fs.WriteStream { const fileName = this.getFilePath(id, fileType); let fileData = this.fileCache[fileName]; if (!fileData) { const stream = fs.createWriteStream(fileName, { flags: 'a' }); fileData = this.fileCache[fileName] = { countdown: FILE_CLOSE_COUNTDOWN, stream }; } fileData.countdown = FILE_CLOSE_COUNTDOWN; return fileData.stream; } private closeFile(id: string, fileType: FileType): void { const fileName = this.getFilePath(id, fileType); const fileData = this.fileCache[fileName]; if (!fileData) return; fileData.stream.close(); this.fileCache[fileName] = null; delete this.fileCache[fileName]; } private checkFileCache(focusClose: boolean = false): void { for (const fileName in this.fileCache) { const fileData = this.fileCache[fileName]; if (!fileData) continue; if (!focusClose && fileData.countdown-- > 0) continue; fileData.stream.close(); this.fileCache[fileName] = null; delete this.fileCache[fileName]; } } }