import { Lock, ObservableInterface } from "@hibas123/utils"; import * as fs from "fs"; import * as path from "path"; import { Adapter, Message, LoggingTypes } from "@hibas123/logging"; const MAX_FILE_SIZE = 500000000; export class LoggingFiles implements Adapter { file: Files; constructor(filename: string, private error = false, private maxFileSize = MAX_FILE_SIZE) { this.file = Files.getFile(filename); } init(observable: ObservableInterface) { observable.subscribe(this.onMessage.bind(this)); return this.file.init(this.maxFileSize); } 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.map(fmt => fmt.map(f => f.text).join("") + "\n").join(""); let msg = Buffer.from(txt); this.file.write(msg); } close() { this.file.close(); } } export class Files { private open = 0; 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); } file.open++; return file; } private maxFileSize = MAX_FILE_SIZE; private size: number = 0; private stream: fs.WriteStream = undefined; private lock = new Lock(); public initialized = false; private constructor(private file: string) { } public async init(maxFileSize: number) { if (this.initialized) return; let lock = await this.lock.getLock(); this.maxFileSize == maxFileSize; 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 >= this.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 async close() { await this.flush(false); this.open--; if (this.open <= 0) { this.stream.close() Files.files.delete(this.file); } } 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 < this.maxFileSize && this.size + data.byteLength > this.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() } public dispose() { } } 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()); }); }