-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathScannerManager.cpp
More file actions
322 lines (243 loc) · 11 KB
/
ScannerManager.cpp
File metadata and controls
322 lines (243 loc) · 11 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
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2026 Reikooters <https://github.com/Reikooters>
#include <QProcess>
#include <QCoreApplication>
#include <QDataStream>
#include <QDebug>
#include <QMessageBox>
#include <limits>
#include "ScannerManager.h"
ScannerManager::ScannerManager(QObject *parent) : QObject(parent) {}
ScannerManager::~ScannerManager() {
// If the manager is destroyed while a scan is running
// (e.g. user closed the dialog window), request a cancel.
requestCancel();
}
std::optional<ScannerEngine::SearchDatabase> ScannerManager::scanDevice(const QString &devicePath, const QString &fsType) {
m_isRunning = true;
m_cancelRequested = false;
Q_EMIT scannerStarted();
Q_EMIT progressMessage("Authenticating and starting scanner...");
// Using pkexec triggers the system authentication dialog (Native KDE feel)
// We assume 'kerything-scanner-helper' is in the same directory as our app
// Use unique_ptr to prevent leaks on early returns
auto helper = std::make_unique<QProcess>();
QString helperPath = QCoreApplication::applicationDirPath() + "/kerything-scanner-helper";
// Launch helper
qDebug() << "Launching helper:" << helperPath << "on" << devicePath << "type:" << fsType;
helper->start("pkexec", {helperPath, devicePath, fsType});
QByteArray rawData;
rawData.reserve(1024 * 1024 * 16); // start with 16 MiB to reduce early reallocations
QByteArray stderrBuf;
stderrBuf.reserve(4096);
int lastPct = -1;
// Percent completion updates from the helper are written to stderr
// on a line beginning with KERYTHING_PROGRESS then the integer value.
// Parse those as we get them and update the progress bar,
// while ignoring any other lines written to stderr.
auto drainAndParseStderr = [&](bool flushPartialLine = false) {
stderrBuf += helper->readAllStandardError();
std::optional<int> latestPctSeen;
auto consumeLine = [&](QByteArrayView lineView) {
static constexpr QByteArrayView kPrefix("KERYTHING_PROGRESS ");
if (!lineView.startsWith(kPrefix)) {
return;
}
bool ok = false;
int pct = lineView.mid(kPrefix.size()).trimmed().toInt(&ok);
if (!ok) {
return;
}
pct = std::clamp(pct, 0, 100);
latestPctSeen = pct;
};
while (true) {
const int nl = stderrBuf.indexOf('\n');
if (nl < 0) {
break;
}
QByteArrayView lineView(stderrBuf.constData(), nl);
consumeLine(lineView);
stderrBuf.remove(0, nl + 1);
}
// If the process has finished, treat the remaining bytes as the last line.
if (flushPartialLine && !stderrBuf.isEmpty()) {
consumeLine(stderrBuf);
stderrBuf.clear();
}
// If a progress update was received, update the percentage progress displayed,
// but only if the new percentage value is different from the one currently shown.
if (latestPctSeen && *latestPctSeen != lastPct) {
lastPct = *latestPctSeen;
Q_EMIT progressValue(lastPct);
Q_EMIT progressMessage(QStringLiteral("Scanning device... %1%").arg(lastPct));
}
};
// Loop until finished, keeping the UI responsive
while (!helper->waitForFinished(100)) {
QCoreApplication::processEvents();
// drain stdout continuously to avoid deadlock if helper writes a lot.
rawData += helper->readAllStandardOutput();
// Drain stderr for progress updates (and ignore other stderr output)
drainAndParseStderr();
// Check if user clicked the "Cancel" button or closed the dialog
if (m_cancelRequested) {
qDebug() << "Cancellation requested. Abandoning process...";
helper->kill(); // Send SIGKILL
// Do not call waitForFinished here, otherwise the GUI will freeze.
// We tell the process to delete itself whenever it finally manages to exit.
// Ownership transfer: We release from unique_ptr because the process
// needs to live long enough to die properly in the background.
QProcess* helperPtr = helper.release();
connect(helperPtr, &QProcess::finished, helperPtr, &QObject::deleteLater);
m_isRunning = false;
Q_EMIT progressMessage("Scanner cancelled.");
Q_EMIT scannerFinished();
return std::nullopt;
}
}
// Drain any remaining stdout/stderr after exit
rawData += helper->readAllStandardOutput();
drainAndParseStderr();
if (helper->exitCode() != 0) {
m_isRunning = false;
qDebug() << "Helper failed with exit code" << helper->exitCode();
Q_EMIT progressMessage("Scanner failed.");
Q_EMIT errorMessage("Scanner Helper Failed",
QString("The scanner process exited with code %1.\n\n"
"This usually means the partition is busy or pkexec was cancelled.")
.arg(helper->exitCode()));
Q_EMIT scannerFinished();
return std::nullopt;
}
// Helper process has finished, now we process the data
qDebug() << "Helper finished with exit code" << helper->exitCode();
Q_EMIT progressMessage("Processing data from helper...");
QCoreApplication::processEvents();
if (rawData.isEmpty()) {
m_isRunning = false;
qDebug() << "No data received from helper.";
Q_EMIT progressMessage("No data received.");
Q_EMIT errorMessage("No Data Received",
"The scanner helper finished but sent no data. If this partition is very large, "
"it might have run out of memory.");
Q_EMIT scannerFinished();
return std::nullopt;
}
// Use a QDataStream directly on the process's stdout
QDataStream stream(rawData);
stream.setByteOrder(QDataStream::LittleEndian);
ScannerEngine::SearchDatabase db;
auto readVal = [&](auto& val) {
return stream.readRawData(reinterpret_cast<char*>(&val), sizeof(val)) == sizeof(val);
};
// 1. Read record count
quint64 recordCount = 0;
if (!readVal(recordCount)) {
m_isRunning = false;
qDebug() << "Failed to read record count";
Q_EMIT progressMessage("Data stream error.");
Q_EMIT errorMessage("Data Stream Error", "Failed to read record count from the helper stream.");
Q_EMIT scannerFinished();
return std::nullopt;
}
qDebug() << "Helper reporting" << recordCount << "records. Allocating memory...";
// Sanity: prevent absurd allocations
static constexpr quint64 kMaxRecords = 500'000'000ULL; // 500M entries
if (recordCount == 0 || recordCount > kMaxRecords) {
m_isRunning = false;
Q_EMIT progressMessage("Data stream error.");
Q_EMIT errorMessage("Data Stream Error",
QString("Helper returned an invalid record count: %1").arg(recordCount));
Q_EMIT scannerFinished();
return std::nullopt;
}
// 2. Read records (size checks first)
const quint64 recordBytes64 = recordCount * static_cast<quint64>(sizeof(ScannerEngine::FileRecord));
if (recordBytes64 / static_cast<quint64>(sizeof(ScannerEngine::FileRecord)) != recordCount) {
m_isRunning = false;
Q_EMIT progressMessage("Data stream error.");
Q_EMIT errorMessage("Data Stream Error", "Record byte size overflow.");
Q_EMIT scannerFinished();
return std::nullopt;
}
if (recordBytes64 > static_cast<quint64>(std::numeric_limits<qint64>::max())) {
m_isRunning = false;
Q_EMIT progressMessage("Data stream error.");
Q_EMIT errorMessage("Data Stream Error", "Record data is too large to read safely.");
Q_EMIT scannerFinished();
return std::nullopt;
}
try {
db.records.resize(recordCount);
} catch (const std::exception& e) {
m_isRunning = false;
qDebug() << "CRASH prevented! Attempted to resize to" << recordCount << "records. Error:" << e.what();
Q_EMIT progressMessage("Memory allocation failed.");
Q_EMIT errorMessage("Memory Allocation Failed",
QString("Failed to allocate memory for %1 records.\nError: %2")
.arg(recordCount).arg(e.what()));
Q_EMIT scannerFinished();
return std::nullopt;
}
if (stream.readRawData(reinterpret_cast<char*>(db.records.data()),
static_cast<qint64>(recordBytes64)) != static_cast<qint64>(recordBytes64)) {
m_isRunning = false;
Q_EMIT progressMessage("Data stream error.");
Q_EMIT errorMessage("Data Stream Error", "Truncated stream while reading records.");
Q_EMIT scannerFinished();
return std::nullopt;
}
qDebug() << "Records transfer complete.";
// 3. Read string pool size
quint64 poolSize = 0;
if (!readVal(poolSize)) {
m_isRunning = false;
Q_EMIT progressMessage("Data stream error.");
Q_EMIT errorMessage("Data Stream Error", "Failed to read string pool size.");
Q_EMIT scannerFinished();
return std::nullopt;
}
// Sanity: prevent absurd allocations
static constexpr quint64 kMaxPoolBytes = 8ULL * 1024 * 1024 * 1024; // 8 GiB
if (poolSize == 0 || poolSize > kMaxPoolBytes) {
m_isRunning = false;
Q_EMIT progressMessage("Data stream error.");
Q_EMIT errorMessage("Data Stream Error",
QString("Helper returned an invalid string pool size: %1 bytes").arg(poolSize));
Q_EMIT scannerFinished();
return std::nullopt;
}
// Redundant extra check, just kept here in case the 8 GiB limit is ever removed
if (poolSize > static_cast<quint64>(std::numeric_limits<qint64>::max())) {
m_isRunning = false;
Q_EMIT progressMessage("Data stream error.");
Q_EMIT errorMessage("Data Stream Error", "String pool is too large to read safely.");
Q_EMIT scannerFinished();
return std::nullopt;
}
qDebug() << "String pool size:" << poolSize;
// 4. Read string pool
db.stringPool.resize(poolSize);
if (stream.readRawData(db.stringPool.data(), static_cast<qint64>(poolSize)) != static_cast<qint64>(poolSize)) {
m_isRunning = false;
Q_EMIT progressMessage("Data stream error.");
Q_EMIT errorMessage("Data Stream Error", "Truncated stream while reading string pool.");
Q_EMIT scannerFinished();
return std::nullopt;
}
// 5. Pre-sort data by name ascending
qDebug() << "Data processing complete. Sorting data...";
Q_EMIT progressMessage("Data processing complete. Sorting data...");
QCoreApplication::processEvents();
db.sortByNameAscendingParallel();
// 6. Build the trigram index
qDebug() << "Building trigrams index...";
Q_EMIT progressMessage("Building search index...");
QCoreApplication::processEvents();
db.buildTrigramIndexParallel();
qDebug() << "Index generation complete.";
m_isRunning = false;
Q_EMIT scannerFinished();
return db;
}