Skip to content

Commit aa880b8

Browse files
committed
Stabilize Manager: TagStore per-dir connections, sync folder previews, WinAPI crash log
Root-cause fixes for rapid folder-switch crashes: - TagStore: new connection per open + removeDatabase on close (fixed same-connection setDatabaseName leaving old QSqlQuery dangling -> QString corruption) - processNextFolderPreview: token check per iteration (dataChanged reentrancy) - FolderPreviewTask: sync applyFolderThumb (no event-queue pileup), QDirIterator takes first 4 images only (no full-dir entryList) - loadImageNoIcc: auto-detect format (was hardcoded png -> JPEG failed), QImage::fromData for stripped iCCP (no QBuffer dangling) - Crash log via raw WinAPI (immune to QString corruption), SEH handler added - Removed debug qWarning spam
1 parent 3e92ded commit aa880b8

4 files changed

Lines changed: 123 additions & 43 deletions

File tree

src/manager/TagStore.cpp

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ bool TagStore::open(const QString &folder) {
2929
QString dbPath = folder + "/qltag.db";
3030
if (!QDir(folder).exists()) return false;
3131

32-
// 用固定连接名,避免 addDatabase 同名反复替换导致旧查询悬垂
33-
const QString connName = QStringLiteral("qlens_tags_main");
34-
m_db = QSqlDatabase::contains(connName)
35-
? QSqlDatabase::database(connName, /*open=*/false)
36-
: QSqlDatabase::addDatabase("QSQLITE", connName);
32+
// 每次 open 用全新连接名:避免在同一连接上 setDatabaseName 换库导致旧 QSqlQuery 悬垂
33+
// (切目录时旧库的查询可能还在使用连接,换库即悬垂 → QString 损坏崩溃)
34+
static quint64 s_seq = 0;
35+
const QString connName = QStringLiteral("qlens_tags_%1").arg(++s_seq);
36+
m_db = QSqlDatabase::addDatabase("QSQLITE", connName);
3737
m_db.setDatabaseName(dbPath);
3838
if (!m_db.open()) {
3939
// 打开失败:可能是崩溃残留的损坏 wal。清理后重试一次。
@@ -72,8 +72,11 @@ bool TagStore::open(const QString &folder) {
7272
}
7373

7474
void TagStore::close() {
75+
QString connName = m_db.connectionName();
7576
if (m_db.isOpen()) m_db.close();
7677
m_db = QSqlDatabase();
78+
if (!connName.isEmpty())
79+
QSqlDatabase::removeDatabase(connName); // 移除注册表,防止连接泄漏
7780
m_folder.clear();
7881
m_tagNames.clear();
7982
}

src/manager/ThumbnailGrid.cpp

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ void ThumbModel::updatePix(int row, const QPixmap &pix) {
4949
m_items[row].pix = pix;
5050
QModelIndex idx = index(row);
5151
emit dataChanged(idx, idx, {PixRole});
52+
// IconMode + setUniformItemSizes 下 dataChanged 单行可能不触发重绘,强制刷新
53+
if (auto *v = qobject_cast<QListView*>(QObject::parent()))
54+
v->viewport()->update();
5255
}
5356

5457
void ThumbModel::updateHighlight(int row, bool hit) {
@@ -145,24 +148,41 @@ static QImage loadImageNoIcc(const QString &path, int maxPx)
145148
if (!f.open(QIODevice::ReadOnly))
146149
return {};
147150
QByteArray data = f.readAll();
148-
// 非 PNG 或没有 iCCP → 直接走 QImageReader
151+
// iCCP 被剥离 → 从内存深拷贝解码(QImage::fromData 会拷贝数据,无悬垂)
149152
if (data.startsWith("\x89PNG\r\n\x1a\n")) {
150-
QByteArray cleaned = data.left(8);
153+
bool stripped = false;
154+
QByteArray cleaned;
151155
qsizetype pos = 8;
152156
while (pos + 8 <= data.size()) {
153157
const quint32 len = qFromBigEndian<quint32>(data.constData() + pos);
154-
const QByteArray type = data.mid(pos + 4, 4);
155158
const qsizetype total = 12 + len;
156-
if (pos + total > data.size()) break; // 损坏,交给 Qt 处理
157-
if (type != "iCCP")
158-
cleaned += data.mid(pos, total);
159+
if (pos + total > data.size()) break;
160+
if (data.mid(pos + 4, 4) == "iCCP") {
161+
stripped = true;
162+
cleaned = data.left(8);
163+
break;
164+
}
159165
pos += total;
160166
}
161-
if (cleaned.size() > 8) data = cleaned; // 有 chunk 才替换
167+
if (stripped) {
168+
pos = 8;
169+
while (pos + 8 <= data.size()) {
170+
const quint32 len = qFromBigEndian<quint32>(data.constData() + pos);
171+
const QByteArray type = data.mid(pos + 4, 4);
172+
const qsizetype total = 12 + len;
173+
if (pos + total > data.size()) break;
174+
if (type != "iCCP")
175+
cleaned += data.mid(pos, total);
176+
pos += total;
177+
}
178+
QImage img = QImage::fromData(cleaned);
179+
if (!img.isNull() && maxPx > 0)
180+
img = img.scaled(maxPx, maxPx, Qt::KeepAspectRatio, Qt::SmoothTransformation);
181+
return img;
182+
}
162183
}
163-
QBuffer buf(&data);
164-
buf.open(QIODevice::ReadOnly);
165-
QImageReader rd(&buf, "png");
184+
// 无 iCCP:直接读文件路径(QImageReader 自管文件生命周期,无悬垂)
185+
QImageReader rd(path);
166186
rd.setAutoTransform(true);
167187
QSize orig = rd.size();
168188
if (orig.isValid() && orig.width() > 0 && maxPx > 0)
@@ -199,10 +219,15 @@ class FolderPreviewTask : public QRunnable {
199219
: m_grid(grid), m_folder(std::move(folder)), m_row(row), m_ts(ts), m_token(token) {}
200220

201221
void run() override {
202-
QDir sub(m_folder);
203-
sub.setNameFilters({"*.jpg","*.jpeg","*.png","*.webp","*.bmp","*.gif"});
204-
sub.setFilter(QDir::Files | QDir::Readable);
205-
QStringList previews = sub.entryList();
222+
// 只取前 4 个图片文件名(QDirIterator 前 4 个匹配即停,避免枚举整个目录)
223+
QStringList previews;
224+
QDirIterator it(m_folder,
225+
{"*.jpg","*.jpeg","*.png","*.webp","*.bmp","*.gif"},
226+
QDir::Files | QDir::Readable);
227+
while (it.hasNext() && previews.size() < 4) {
228+
it.next();
229+
previews << it.fileName();
230+
}
206231

207232
// 全程用 QImage(线程安全);QPixmap 只在主线程回调里创建
208233
QImage collage(m_ts, m_ts, QImage::Format_ARGB32_Premultiplied);
@@ -237,10 +262,12 @@ class FolderPreviewTask : public QRunnable {
237262
// 预览目标尺寸(cell 大小,最多 ~half ts)—— 用 setScaledSize 限流,
238263
// 避免解码 36MB 巨型 PNG 撑爆内存(之前崩溃根因)
239264
int targetPx = qMax(1, (int)(qMax(cellW, cellH) * 1.5));
265+
int decoded = 0;
240266
for (int pi = 0; pi < sample; ++pi) {
241267
// 剥离损坏 ICC 后解码(同 ThumbDecodeTask)
242-
QImage img = loadImageNoIcc(sub.path() + "/" + previews[pi], targetPx);
268+
QImage img = loadImageNoIcc(m_folder + "/" + previews[pi], targetPx);
243269
if (img.isNull()) continue;
270+
++decoded;
244271
int col = pi % cellCnt, row = pi / cellCnt;
245272
QRectF cell(inner.x() + col*cellW, inner.y() + row*cellH, cellW, cellH);
246273
QSize fit = img.size();
@@ -251,9 +278,9 @@ class FolderPreviewTask : public QRunnable {
251278
}
252279
p.end();
253280

254-
QMetaObject::invokeMethod(m_grid, [g = m_grid, r = m_row, im = collage, t = m_token]() {
255-
if (g) g->applyFolderThumb(r, im, t);
256-
}, Qt::QueuedConnection);
281+
// 主线程同步调用(本任务由主线程直接 run(),无需投递事件队列——避免连续切换时事件堆积)
282+
if (m_grid)
283+
m_grid->applyFolderThumb(m_row, collage, m_token);
257284
}
258285

259286
private:
@@ -352,10 +379,10 @@ void ThumbnailGrid::loadFolder(const QString &path) {
352379
int dirCount = (int)m_subDirs.size();
353380
int token = m_loadToken; // 本次加载令牌
354381

355-
// 文件夹预览任务
356-
// 文件夹预览任务
357-
for (int di = 0; di < dirCount; ++di)
358-
pool->start(new FolderPreviewTask(this, m_allItems[di].path, di, ts, token));
382+
// 文件夹预览任务(主线程分批处理,避免大目录并发抢线程池导致部分任务滞留)
383+
m_folderPreviewIdx = 0;
384+
m_folderPreviewToken = token;
385+
processNextFolderPreview(ts, token);
359386

360387
// 图片:缓存命中直接显示,未命中提交解码
361388
for (int i = 0; i < (int)m_imageFiles.size(); ++i) {
@@ -372,6 +399,29 @@ void ThumbnailGrid::loadFolder(const QString &path) {
372399
}
373400
}
374401

402+
// 主线程分批处理文件夹拼图(每次 2 个,避免 46 个大目录并发卡死)
403+
void ThumbnailGrid::processNextFolderPreview(int ts, int token) {
404+
// 每次进入都检查 token:用户已切换目录则丢弃(loadFolder 清空了 m_allItems)
405+
if (token != m_loadToken || m_folderPreviewIdx >= (int)m_subDirs.size())
406+
return;
407+
const int batch = 2;
408+
for (int n = 0; n < batch && m_folderPreviewIdx < (int)m_subDirs.size(); ++n, ++m_folderPreviewIdx) {
409+
// 每次迭代检查 token:task->run() 的 updatePix→dataChanged 可能重入 loadFolder
410+
// 清空 m_allItems,再访问就崩溃(SEH 0xc0000005 已证实)
411+
if (token != m_loadToken)
412+
return;
413+
int di = m_folderPreviewIdx;
414+
// 用路径快照(m_allItems 可能被新 loadFolder 重建,不能跨批持有索引)
415+
QString dirPath = m_allItems[di].path;
416+
FolderPreviewTask *task = new FolderPreviewTask(this, dirPath, di, ts, token);
417+
task->run();
418+
delete task;
419+
}
420+
if (m_folderPreviewIdx < (int)m_subDirs.size() && token == m_loadToken) {
421+
QTimer::singleShot(0, this, [this, ts, token]{ processNextFolderPreview(ts, token); });
422+
}
423+
}
424+
375425
// 按路径在模型可见行中查找
376426
int ThumbnailGrid::findModelRow(const QString &path) const {
377427
for (int i = 0; i < m_model->rowCount(); ++i) {

src/manager/ThumbnailGrid.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,16 @@ class ThumbnailGrid : public QListView {
8181

8282
int findModelRow(const QString &path) const;
8383
void applyFilter();
84+
void processNextFolderPreview(int ts, int token);
8485

8586
TagStore *m_store = nullptr;
8687
ThumbModel *m_model = nullptr;
8788
int m_thumbSize = 160;
8889
QStringList m_imageFiles;
8990
QStringList m_subDirs;
9091
QList<ThumbItem> m_allItems; // 全量(不过滤)
92+
int m_folderPreviewIdx = 0;
93+
int m_folderPreviewToken = 0;
9194
QString m_currentFolder;
9295
QString m_highlightTag;
9396
QString m_filterTag;

src/manager/main.cpp

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,35 +21,59 @@ static QMutex g_logMutex;
2121

2222
static void logHandler(QtMsgType type, const QMessageLogContext &ctx, const QString &msg)
2323
{
24-
QMutexLocker lock(&g_logMutex);
25-
QString line = QStringLiteral("[%1] [%4] %2 %3")
26-
.arg(QDateTime::currentDateTime().toString("HH:mm:ss.zzz"))
27-
.arg(msg)
28-
.arg(ctx.file ? QString::fromUtf8(ctx.file) : QStringLiteral("?"))
29-
.arg(QString::fromUtf8(ctx.category ? ctx.category : ""));
30-
if (g_logFile.isOpen()) {
31-
QTextStream ts(&g_logFile);
32-
ts << line << "\n";
33-
ts.flush();
24+
// 用 WinAPI 直接写文件,避免 QString/QTextStream 在内存损坏时二次崩溃
25+
#ifdef Q_OS_WIN
26+
QByteArray utf8 = msg.toUtf8();
27+
utf8.append('\n');
28+
HANDLE h = CreateFileW((LPCWSTR)QString(g_logFile.fileName()).utf16(),
29+
FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE,
30+
nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
31+
if (h != INVALID_HANDLE_VALUE) {
32+
DWORD written;
33+
WriteFile(h, utf8.constData(), (DWORD)utf8.size(), &written, nullptr);
34+
CloseHandle(h);
3435
}
3536
if (type == QtFatalMsg) {
36-
// 断言/致命错误:打印调用栈辅助定位
37-
#ifdef Q_OS_WIN
3837
void *stack[32];
3938
USHORT frames = CaptureStackBackTrace(0, 32, stack, nullptr);
39+
QByteArray hdr = QByteArray("--- call stack (") + QByteArray::number(frames) + QByteArray(" frames) ---\n");
40+
QByteArray dump;
41+
for (USHORT i = 0; i < frames; ++i)
42+
dump += QByteArray(" 0x") + QByteArray::number((quintptr)stack[i], 16) + '\n';
43+
HANDLE h2 = CreateFileW((LPCWSTR)QString(g_logFile.fileName()).utf16(),
44+
FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE,
45+
nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
46+
if (h2 != INVALID_HANDLE_VALUE) {
47+
DWORD w;
48+
WriteFile(h2, hdr.constData(), (DWORD)hdr.size(), &w, nullptr);
49+
WriteFile(h2, dump.constData(), (DWORD)dump.size(), &w, nullptr);
50+
CloseHandle(h2);
51+
}
52+
}
53+
#endif
54+
}
55+
56+
int main(int argc, char *argv[]) {
57+
#ifdef Q_OS_WIN
58+
// 抓 SEH 硬崩溃(访问违规等不走 qFatal),打印调用栈到日志
59+
SetUnhandledExceptionFilter([](EXCEPTION_POINTERS *ep) -> LONG {
60+
QMutexLocker lock(&g_logMutex);
4061
if (g_logFile.isOpen()) {
4162
QTextStream ts(&g_logFile);
63+
ts << "--- SEH exception code=0x"
64+
<< QString::number((quint32)ep->ExceptionRecord->ExceptionCode, 16)
65+
<< " ---\n";
66+
void *stack[32];
67+
USHORT frames = CaptureStackBackTrace(0, 32, stack, nullptr);
4268
ts << "--- call stack (" << frames << " frames) ---\n";
4369
for (USHORT i = 0; i < frames; ++i)
4470
ts << " " << stack[i] << "\n";
4571
ts.flush();
4672
}
73+
return EXCEPTION_CONTINUE_SEARCH;
74+
});
4775
#endif
48-
if (g_logFile.isOpen()) g_logFile.flush();
49-
}
50-
}
5176

52-
int main(int argc, char *argv[]) {
5377
QApplication app(argc, argv);
5478
app.setApplicationName("QLens Manager");
5579
app.setWindowIcon(QIcon(":/app.ico")); // 窗口/任务栏图标

0 commit comments

Comments
 (0)