import { Lock, ObservableInterface } from "@hibas123/utils"; import * as fs from "fs"; import * as path from "path"; import { Adapter, Message, LoggingTypes } from "@hibas123/logging"; const maxFileSize = 500000000; export class LoggingFiles implements Adapter { file: Files; constructor(filename: string, private error = false) { this.file = Files.getFile(filename); } init(observable: ObservableInterface) { observable.subscribe(this.onMessage.bind(this)); return this.file.init(); } flush(sync: boolean) { this.file.flush(sync); } onMessage(message: Message) { // Just ignore all non error messages, if this.error is set if (this.error && message.type !== LoggingTypes.Error) return; let txt = message.text.formatted.join("\n") + "\n"; txt = txt.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ""); let index = txt.indexOf("\x1b"); while (index >= 0) { txt = txt.substring(0, index) + txt.substring(index + 5, txt.length); index = txt.indexOf("\x1b"); } let msg = Buffer.from(txt); this.file.write(msg); } } export class Files { private static files = new Map(); static getFile(filename: string): Files { filename = path.resolve(filename); let file = this.files.get(filename); if (!file) { file = new Files(filename); this.files.set(filename, file); } return file; } private size: number = 0; private stream: fs.WriteStream = undefined; private lock = new Lock(); public initialized = false; private constructor(private file: string) { } public async init() { if (this.initialized) return; let lock = await this.lock.getLock(); await this.initializeFile() this.initialized = true; lock.release(); this.checkQueue() } private async initializeFile(new_file = false) { try { if (this.stream) { this.stream.close(); } const folder = path.dirname(this.file); if (folder) { if (!await fsExists(folder)) { await fsMkDir(folder).catch(() => { }); //Could happen, if two seperate instances want to create the same folder so ignoring } } let size = 0; if (await fsExists(this.file)) { let stats = await fsStat(this.file); if (new_file || stats.size >= maxFileSize) { if (await fsExists(this.file + ".old")) await fsUnlink(this.file + ".old"); await fsMove(this.file, this.file + ".old") } else { size = stats.size; } } this.stream = fs.createWriteStream(this.file, { flags: "a" }) this.size = size; } catch (e) { console.log(e); //ToDo is this the right behavior? process.exit(1); } } private queue: Buffer[] = []; async checkQueue() { if (this.lock.locked) return; let lock = await this.lock.getLock(); let msg: Buffer; while (msg = this.queue.shift()) { await this.write_to_file(msg); } lock.release(); } public flush(sync: boolean) { if (sync) { // if sync flush, the process most likely is in failstate, so checkQueue stopped its work. let msg: Buffer; while (msg = this.queue.shift()) { this.stream.write(msg); } } else { return Promise.resolve().then(async () => { const lock = await this.lock.getLock(); lock.release(); await this.checkQueue(); }) } } private async write_to_file(data: Buffer) { try { if (data.byteLength < maxFileSize && this.size + data.byteLength > maxFileSize) { await this.initializeFile(true) } this.size += data.byteLength; this.stream.write(data); } catch (err) { // TODO: Better error handling! console.error(err); this.initializeFile(false); this.write_to_file(data); } } public write(data: Buffer) { this.queue.push(data); this.checkQueue() } } function fsUnlink(path) { return new Promise((resolve, reject) => { fs.unlink(path, (err) => { if (err) reject(err); else resolve(); }) }) } function fsStat(path: string) { return new Promise((resolve, reject) => { fs.stat(path, (err, stats) => { if (err) reject(err); else resolve(stats); }) }) } function fsMove(oldPath: string, newPath: string) { return new Promise((resolve, reject) => { let callback = (err?) => { if (err) reject(err) else resolve() } fs.rename(oldPath, newPath, function (err) { if (err) { if (err.code === 'EXDEV') { copy(); } else { callback(err) } return; } callback() }); function copy() { fs.copyFile(oldPath, newPath, (err) => { if (err) callback(err) else fs.unlink(oldPath, callback); }) } }) } function fsExists(path: string) { return new Promise((resolve, reject) => { fs.exists(path, resolve); }); } function fsMkDir(path: string) { return new Promise((resolve, reject) => { fs.mkdir(path, (err) => err ? reject(err) : resolve()); }); }