#pragma once #include "fgc/Logger.h" #include #include #include #include namespace fgc { // A rotating, size-capped log file for the tower's own operation. // // A tower runs detached for days inside a tmux session, and in TUI mode every // log line goes into a 500-line ring buffer on screen and nowhere else - so the // record of what it did dies with the session. That is the one thing you want // when somebody asks why an axis is not moving. // // This is deliberately a plain appender rather than anything clever: // // * It runs INSIDE Logger's lock. Logger holds a non-recursive mutex while it // calls the sink, so every method here MUST be free of LOG_* - one logging // call from this file deadlocks the whole process. Failures are recorded and // reported through failed()/lastError(), which the caller polls from OUTSIDE // the lock. That constraint is why paths::writeLogFile() is not reused: it // LOG_WARNs on failure, and it truncates rather than appends. // * It flushes every line but never fsyncs. A crashed process keeps everything // (the bytes are already in the page cache); only a power cut loses the tail. // fsync per line on a tower's eMMC would be far worse than that trade. // * Rotation is by size, checked before each write. Files are pruned by count, // oldest first. struct FileLogPolicy { // Empty => paths::defaultLogDir(). std::string dir; // Rotate once a write would push the file past this. 0 disables rotation. long long max_bytes = 20LL * 1024 * 1024; // How many _*.log to keep in `dir`, counting every run's files and // every rotation of them. Oldest are deleted. 0 disables pruning. int keep_files = 10; // File name prefix. Deliberately NOT "test": Application::latestPriorTestReport() // scans this same directory for test_*.log and would otherwise pick up ours. std::string prefix = "fgc"; }; class FileLogSink { public: FileLogSink() = default; ~FileLogSink(); FileLogSink(const FileLogSink&) = delete; FileLogSink& operator=(const FileLogSink&) = delete; // Create the directory if needed and open a fresh file for this run. `header` // is written first and re-written after every rotation, so each file on disk // says for itself which build and tower produced it. Returns false and sets // failed() if the file cannot be opened. bool open(const FileLogPolicy& policy, const std::string& header); // Append one already-formatted Logger line (no trailing newline, and it // begins "HH:MM:SS.mmm"). A date is prepended here rather than in Logger, // because the on-screen panes are width-constrained and a file that may span // days is the only place the date is actually needed. // // `cat` distinguishes a wire-trace line from an ordinary one; the caller uses // it to decide routing, and it is recorded here only for completeness. // Never throws, never logs. A no-op once failed(). void write(LogLevel level, LogCat cat, const std::string& line); void close(); bool isOpen() const { return fp_ != nullptr; } // The file being written now. Safe from any thread: rotation changes it // under Logger's lock, while the dashboard reads it for the LOG title. std::string path() const { std::lock_guard lock(path_mutex_); return path_; } // Set when an open, write or rotate failed. The sink then stops writing // rather than retrying every line. Poll this from outside Logger's lock. bool failed() const { return failed_; } std::string lastError() const { return last_error_; } // Exposed for tests: the files this policy would prune, newest first. static std::vector existingFiles(const FileLogPolicy& policy); private: std::string segmentPath(int n) const; bool openFile(const std::string& path); void rotate(); void prune(); void fail(const std::string& what); std::FILE* fp_ = nullptr; FileLogPolicy policy_; std::string header_; std::string path_; // written under path_mutex_; read freely by this sink mutable std::mutex path_mutex_; std::string base_; // path without the "..log" suffix long long bytes_ = 0; long long lines_ = 0; // lines in the CURRENT file; guards rotation loops int rotation_ = 0; bool failed_ = false; std::string last_error_; }; } // namespace fgc