Files
logos-basecamp/app/utils/LogRedirector.cpp
T
Dario Lipicar 009284a22c write console output to files (#159)
* write console output to files

* PR comments
2026-04-20 17:45:41 -03:00

222 lines
5.7 KiB
C++

#include "LogRedirector.h"
#include <QDateTime>
#include <QDir>
#include <cerrno>
#include <cstdio>
#if !defined(Q_OS_WIN)
# include <fcntl.h>
# include <unistd.h>
#endif
namespace LogosBasecampLog {
LogRedirector& LogRedirector::instance()
{
static LogRedirector s;
return s;
}
LogRedirector::~LogRedirector()
{
stop();
}
#if defined(Q_OS_WIN)
bool LogRedirector::start(const QString&, int) { return false; }
void LogRedirector::stop() {}
void LogRedirector::readerLoop() {}
void LogRedirector::openNewFile() {}
#else
bool LogRedirector::start(const QString& logsDir, int maxLinesPerFile)
{
if (m_started)
return true;
if (maxLinesPerFile <= 0)
maxLinesPerFile = 10000;
m_logsDir = logsDir;
m_maxLinesPerFile = maxLinesPerFile;
if (!QDir().mkpath(m_logsDir))
return false;
m_sessionStamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
m_rotationIndex = 0;
m_linesInCurrentFile = 0;
auto cleanup = [this]() {
if (m_originalStdout >= 0) { ::close(m_originalStdout); m_originalStdout = -1; }
if (m_originalStderr >= 0) { ::close(m_originalStderr); m_originalStderr = -1; }
if (m_currentFile) {
m_currentFile->close();
m_currentFile.reset();
}
};
openNewFile();
if (!m_currentFile || !m_currentFile->isOpen()) {
cleanup();
return false;
}
m_originalStdout = ::dup(fileno(stdout));
m_originalStderr = ::dup(fileno(stderr));
if (m_originalStdout < 0 || m_originalStderr < 0) {
cleanup();
return false;
}
int fds[2];
if (::pipe(fds) != 0) {
cleanup();
return false;
}
std::fflush(stdout);
std::fflush(stderr);
if (::dup2(fds[1], fileno(stdout)) == -1 ||
::dup2(fds[1], fileno(stderr)) == -1) {
::close(fds[0]);
::close(fds[1]);
cleanup();
return false;
}
// Line-buffer so each line is forwarded through the pipe promptly, even
// when stdout is not attached to a terminal.
std::setvbuf(stdout, nullptr, _IOLBF, 0);
std::setvbuf(stderr, nullptr, _IOLBF, 0);
::close(fds[1]);
m_readFd = fds[0];
m_running = true;
m_readerThread = std::thread(&LogRedirector::readerLoop, this);
m_started = true;
return true;
}
void LogRedirector::stop()
{
if (!m_started)
return;
m_running = false;
std::fflush(stdout);
std::fflush(stderr);
// Restoring via dup2 closes the previous target fd, i.e. the pipe's
// write end held by stdout/stderr. Once both are restored, the reader
// thread sees EOF on the read end and exits. Keep the duplicated
// originals open until after the reader thread has finished, since
// readerLoop() may still be writing to m_originalStdout — closing it
// concurrently could race with fd-number reuse.
if (m_originalStdout >= 0)
::dup2(m_originalStdout, fileno(stdout));
if (m_originalStderr >= 0)
::dup2(m_originalStderr, fileno(stderr));
if (m_readerThread.joinable())
m_readerThread.join();
if (m_originalStdout >= 0) { ::close(m_originalStdout); m_originalStdout = -1; }
if (m_originalStderr >= 0) { ::close(m_originalStderr); m_originalStderr = -1; }
if (m_readFd >= 0) {
::close(m_readFd);
m_readFd = -1;
}
if (m_currentFile) {
m_currentFile->flush();
m_currentFile->close();
m_currentFile.reset();
}
m_started = false;
}
void LogRedirector::openNewFile()
{
QString name;
if (m_rotationIndex == 0) {
name = QStringLiteral("basecamp_%1.log").arg(m_sessionStamp);
} else {
name = QStringLiteral("basecamp_%1.%2.log")
.arg(m_sessionStamp)
.arg(m_rotationIndex, 3, 10, QChar('0'));
}
auto f = std::make_unique<QFile>(m_logsDir + QStringLiteral("/") + name);
if (!f->open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
m_currentFile.reset();
return;
}
m_currentFile = std::move(f);
m_linesInCurrentFile = 0;
++m_rotationIndex;
}
void LogRedirector::readerLoop()
{
char buf[4096];
while (true) {
ssize_t n = ::read(m_readFd, buf, sizeof(buf));
if (n < 0) {
if (errno == EINTR)
continue;
break;
}
if (n == 0)
break; // EOF: all writers (stdout/stderr replacements) are gone.
// Mirror to the original stdout so an attached terminal keeps seeing
// output. Failures here are ignored (e.g. stdout was /dev/null).
if (m_originalStdout >= 0) {
ssize_t off = 0;
while (off < n) {
ssize_t w = ::write(m_originalStdout, buf + off, n - off);
if (w < 0) {
if (errno == EINTR)
continue;
break;
}
off += w;
}
}
if (!m_currentFile)
continue;
ssize_t start = 0;
for (ssize_t i = 0; i < n; ++i) {
if (buf[i] != '\n')
continue;
m_currentFile->write(buf + start, i - start + 1);
++m_linesInCurrentFile;
start = i + 1;
if (m_linesInCurrentFile >= m_maxLinesPerFile) {
m_currentFile->flush();
m_currentFile->close();
openNewFile();
if (!m_currentFile)
return; // Rotation failed; stop writing to files.
}
}
if (start < n && m_currentFile)
m_currentFile->write(buf + start, n - start);
}
if (m_currentFile)
m_currentFile->flush();
}
#endif // Q_OS_WIN
} // namespace LogosBasecampLog