-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlogging.cpp
More file actions
280 lines (244 loc) · 8.02 KB
/
Copy pathlogging.cpp
File metadata and controls
280 lines (244 loc) · 8.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
#include "logging.h"
#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QMutex>
#include <QMutexLocker>
#include <QSettings>
#include <QStandardPaths>
#include <QSysInfo>
#include <QTextStream>
namespace {
// One rotation, so the log is bounded at ~1 MB total without needing a cleanup
// policy. Big enough to cover a long session and still hold the startup lines
// that say which tracking backend was chosen; small enough to paste from.
constexpr qint64 k_maxBytes = 512 * 1024;
constexpr int k_maxTailLines = 2000; // ceiling on a tail() request
QMutex g_mutex;
QFile* g_file = nullptr; // owned; null when closed
QtMessageHandler g_previous = nullptr;
bool g_enabled = true;
bool g_installed = false;
QString settingsKey() { return QStringLiteral("diagnostics/logging"); }
QString dirPathLocked()
{
QString base = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
if (base.isEmpty()) // no writable app-data location
base = QDir::tempPath() + QStringLiteral("/TrackClick");
return base + QStringLiteral("/logs");
}
QString filePathLocked()
{
return dirPathLocked() + QStringLiteral("/trackclick.log");
}
// The single rotated predecessor.
QString rotatedPathLocked()
{
return filePathLocked() + QStringLiteral(".1");
}
void closeFileLocked()
{
delete g_file;
g_file = nullptr;
}
// Open the log for appending, creating the directory if needed. Returns false
// and leaves g_file null when the location is not writable — logging then
// degrades to "forwarded to the default handler only" rather than failing.
bool openFileLocked()
{
if (g_file)
return true;
if (!QDir().mkpath(dirPathLocked()))
return false;
auto* f = new QFile(filePathLocked());
if (!f->open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
delete f;
return false;
}
g_file = f;
return true;
}
void rotateIfNeededLocked()
{
if (!g_file || g_file->size() < k_maxBytes)
return;
closeFileLocked();
QFile::remove(rotatedPathLocked());
QFile::rename(filePathLocked(), rotatedPathLocked());
openFileLocked();
}
char levelChar(QtMsgType type)
{
switch (type) {
case QtDebugMsg: return 'D';
case QtInfoMsg: return 'I';
case QtWarningMsg: return 'W';
case QtCriticalMsg: return 'C';
case QtFatalMsg: return 'F';
}
return '?';
}
void writeLineLocked(const QString& line)
{
if (!openFileLocked())
return;
rotateIfNeededLocked();
if (!g_file)
return;
QTextStream out(g_file);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
out.setEncoding(QStringConverter::Utf8);
#else
out.setCodec("UTF-8");
#endif
out << line << '\n';
// Two flushes, and both are needed. QTextStream::flush() only pushes its own
// buffer into the QFile; QFile::flush() is what pushes the file engine's
// buffer out to the OS. Without the second one the bytes are invisible to
// anything that stats the path separately — which is exactly what
// sizeBytes() does, so rotation would trigger late or never.
out.flush();
g_file->flush();
// Flushed per line on purpose: the messages worth having are the ones written
// just before something goes wrong, and a buffered tail is exactly what gets
// lost if the app is killed or crashes. Volume here is a handful of lines
// per session, so the cost is irrelevant.
}
void messageHandler(QtMsgType type, const QMessageLogContext& ctx, const QString& msg)
{
// Forward first and unconditionally: a developer running from a terminal, and
// qFatal's abort behaviour, must both survive this handler existing.
if (g_previous)
g_previous(type, ctx, msg);
// A failure inside our own write path (QFile reporting an error, say) would
// re-enter the handler and recurse. One flag per thread breaks that without
// serialising the check.
static thread_local bool reentering = false;
if (reentering)
return;
reentering = true;
{
QMutexLocker lock(&g_mutex);
if (g_enabled) {
QString line = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz");
line += QStringLiteral(" [");
line += QLatin1Char(levelChar(type));
line += QStringLiteral("] ");
// Category is "default" for plain qWarning() calls — only worth
// printing once the codebase starts using QLoggingCategory.
if (ctx.category && qstrcmp(ctx.category, "default") != 0)
line += QLatin1Char('[') + QString::fromUtf8(ctx.category) + QStringLiteral("] ");
line += msg;
writeLineLocked(line);
}
}
reentering = false;
}
} // namespace
namespace logging {
void install()
{
QMutexLocker lock(&g_mutex);
if (g_installed)
return;
{
// Off unless the user asked for it. The cost is that a first bug report
// never carries a log — the user has to switch it on and reproduce — but
// an accessibility tool that already trips AV heuristics (§1) should not
// also start writing files nobody asked for.
QSettings s;
g_enabled = s.value(settingsKey(), false).toBool();
}
g_previous = qInstallMessageHandler(messageHandler);
g_installed = true;
if (!g_enabled)
return;
// Header on every launch, so a pasted log says what produced it without the
// user having to also report their version and platform.
writeLineLocked(QString());
writeLineLocked(QStringLiteral("=== TrackClick %1 starting — %2 / %3 ===")
.arg(QCoreApplication::applicationVersion(),
QSysInfo::prettyProductName(),
QSysInfo::currentCpuArchitecture()));
}
bool isEnabled()
{
QMutexLocker lock(&g_mutex);
return g_enabled;
}
void setEnabled(bool on)
{
QMutexLocker lock(&g_mutex);
if (on == g_enabled)
return;
g_enabled = on;
{
QSettings s;
s.setValue(settingsKey(), on);
}
// writeLineLocked() does not consult g_enabled, so the "stopping" line still
// reaches the file — a log that just ends is indistinguishable from one that
// was truncated by a crash.
if (on) {
writeLineLocked(QStringLiteral("=== logging enabled ==="));
} else {
writeLineLocked(QStringLiteral("=== logging disabled by user ==="));
closeFileLocked();
}
}
QString filePath()
{
QMutexLocker lock(&g_mutex);
return filePathLocked();
}
QString dirPath()
{
QMutexLocker lock(&g_mutex);
return dirPathLocked();
}
qint64 sizeBytes()
{
QMutexLocker lock(&g_mutex);
return QFileInfo(filePathLocked()).size(); // 0 when absent
}
QString tail(int maxLines)
{
if (maxLines <= 0)
return QString();
maxLines = qMin(maxLines, k_maxTailLines);
QMutexLocker lock(&g_mutex);
// Push anything still buffered out first, so the tail really is the tail —
// this reads the path rather than the open handle.
if (g_file)
g_file->flush();
QFile f(filePathLocked());
if (!f.open(QIODevice::ReadOnly | QIODevice::Text))
return QString();
// Read the whole file: it is capped at k_maxBytes, so this is bounded and far
// simpler than seeking backwards for line breaks.
QTextStream in(&f);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
in.setEncoding(QStringConverter::Utf8);
#else
in.setCodec("UTF-8");
#endif
QStringList lines;
while (!in.atEnd()) {
lines.append(in.readLine());
if (lines.size() > maxLines)
lines.removeFirst();
}
return lines.join(QLatin1Char('\n'));
}
void clear()
{
QMutexLocker lock(&g_mutex);
closeFileLocked();
QFile::remove(rotatedPathLocked());
QFile::remove(filePathLocked());
if (g_enabled)
writeLineLocked(QStringLiteral("=== log cleared ==="));
}
} // namespace logging