1
0
forked from BRT/arc

Remove database file handling from LogArchiveSender, revise emergency purge logic in monitoring, and enhance error reporting in BankSetupWizard.

This commit is contained in:
slava 2026-06-04 23:28:26 +05:00
parent 521101463b
commit d7fc838d1a
4 changed files with 117 additions and 73 deletions

View File

@ -216,24 +216,78 @@ def _purge_old() -> int:
return removed
DISK_EMERGENCY_THRESHOLD = 0.80 # чистить все скрины если диск > 80%
DISK_EMERGENCY_THRESHOLD = 0.80 # чистить файлы если диск > 80%
def _disk_pct() -> float:
import shutil
u = shutil.disk_usage(FILES_DIR)
return u.used / u.total
def _emergency_purge_screenshots() -> int:
"""Удаляет ВСЕ скриншоты истории экрана когда диск переполнен."""
import shutil
usage = shutil.disk_usage(FILES_DIR)
if usage.used / usage.total < DISK_EMERGENCY_THRESHOLD:
"""Постепенно удаляет файлы когда диск > 80%.
Порядок: сначала скрины (screenshots + screen_dumps) по одному от старых к новым,
потом zip-дампы (entries.file_path) от старых к новым.
"""
if _disk_pct() < DISK_EMERGENCY_THRESHOLD:
return 0
removed = 0
# 1. Скрины из таблицы screenshots (самые старые первыми)
with _db() as c:
rows = c.execute("SELECT image_path FROM screenshots").fetchall()
for r in rows:
_unlink_rel(r["image_path"])
removed = c.execute("DELETE FROM screenshots").rowcount
rows = c.execute(
"SELECT rowid, image_path FROM screenshots ORDER BY ts ASC"
).fetchall()
for r in rows:
if _disk_pct() < DISK_EMERGENCY_THRESHOLD:
break
_unlink_rel(r["image_path"])
with _db() as c:
c.execute("DELETE FROM screenshots WHERE rowid=?", (r["rowid"],))
removed += 1
if _disk_pct() < DISK_EMERGENCY_THRESHOLD:
if removed:
LOG.warning("disk emergency: removed %d screenshots, disk now %.0f%%", removed, _disk_pct() * 100)
return removed
# 2. Скрины из таблицы screen_dumps (самые старые первыми)
with _db() as c:
rows = c.execute(
"SELECT rowid, image_path, xml_path FROM screen_dumps ORDER BY ts ASC"
).fetchall()
for r in rows:
if _disk_pct() < DISK_EMERGENCY_THRESHOLD:
break
_unlink_rel(r["image_path"])
_unlink_rel(r["xml_path"])
with _db() as c:
c.execute("DELETE FROM screen_dumps WHERE rowid=?", (r["rowid"],))
removed += 1
if _disk_pct() < DISK_EMERGENCY_THRESHOLD:
LOG.warning("disk emergency: removed %d screen files, disk now %.0f%%", removed, _disk_pct() * 100)
return removed
# 3. Zip-дампы из entries (самые старые первыми)
with _db() as c:
rows = c.execute(
"SELECT rowid, file_path FROM entries WHERE file_path IS NOT NULL ORDER BY ts ASC"
).fetchall()
for r in rows:
if _disk_pct() < DISK_EMERGENCY_THRESHOLD:
break
_unlink_rel(r["file_path"])
with _db() as c:
c.execute("UPDATE entries SET file_path=NULL WHERE rowid=?", (r["rowid"],))
removed += 1
LOG.warning(
"disk emergency purge: removed %d screenshots (disk %.0f%% full)",
removed, usage.used / usage.total * 100,
"disk emergency purge: removed %d files total, disk now %.0f%%",
removed, _disk_pct() * 100,
)
return removed

View File

@ -6,10 +6,8 @@
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QProcess>
#include <QRegularExpression>
#include <QSettings>
#include <QStringList>
#include <QThread>
@ -18,16 +16,6 @@
namespace {
QString resolveDbPath() {
const QSettings settings("config.ini", QSettings::IniFormat);
QString dbPath = settings.value("db/dbPath").toString();
if (dbPath.startsWith("~"))
dbPath.replace(0, 1, QDir::homePath());
if (!dbPath.isEmpty() && QFileInfo(dbPath).isRelative())
dbPath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(dbPath);
return dbPath;
}
// Запускается в worker-потоке. Возвращает байты zip или пустой массив.
QByteArray buildArchive() {
const QString tmpDir = QDir::tempPath() + QStringLiteral("/arc_logs_auto_") +
@ -72,60 +60,46 @@ QByteArray buildArchive() {
bool LogArchiveSender::collectInto(const QString &destDir) {
const QString logPath = QCoreApplication::applicationDirPath() + "/application.log";
const QString dbPath = resolveDbPath();
const bool hasLog = QFile::exists(logPath);
const bool hasDb = !dbPath.isEmpty() && QFile::exists(dbPath);
if (!hasLog && !hasDb) {
qWarning() << "[LogArchiveSender] neither log nor db exists — nothing to collect";
if (!QFile::exists(logPath)) {
qWarning() << "[LogArchiveSender] log file not found — nothing to collect";
return false;
}
QDir().mkpath(destDir);
bool any = false;
if (hasLog) {
QFile srcLog(logPath);
if (srcLog.open(QIODevice::ReadOnly)) {
const QDateTime cutoff = QDateTime::currentDateTime().addSecs(-3600);
QFile srcLog(logPath);
if (srcLog.open(QIODevice::ReadOnly)) {
const QDateTime cutoff = QDateTime::currentDateTime().addSecs(-3600);
// Binary search for the first line with ts >= cutoff.
// Relies on monotonically non-decreasing timestamps in the log.
qint64 lo = 0, hi = srcLog.size();
while (lo < hi) {
const qint64 mid = lo + (hi - lo) / 2;
srcLog.seek(mid);
if (mid > 0) srcLog.readLine(); // align to next line boundary
const qint64 linePos = srcLog.pos();
if (linePos >= hi) { hi = mid; continue; }
const QByteArray probe = srcLog.readLine();
if (probe.isEmpty()) { hi = mid; continue; }
const QDateTime ts = QDateTime::fromString(
QString::fromLatin1(probe.left(23)), "yyyy-MM-dd hh:mm:ss.zzz");
if (!ts.isValid() || ts < cutoff)
lo = srcLog.pos(); // this line is old, search upper half
else
hi = linePos; // this line is new, search lower half
}
srcLog.seek(lo);
// Binary search for the first line with ts >= cutoff.
// Relies on monotonically non-decreasing timestamps in the log.
qint64 lo = 0, hi = srcLog.size();
while (lo < hi) {
const qint64 mid = lo + (hi - lo) / 2;
srcLog.seek(mid);
if (mid > 0) srcLog.readLine(); // align to next line boundary
const qint64 linePos = srcLog.pos();
if (linePos >= hi) { hi = mid; continue; }
const QByteArray probe = srcLog.readLine();
if (probe.isEmpty()) { hi = mid; continue; }
const QDateTime ts = QDateTime::fromString(
QString::fromLatin1(probe.left(23)), "yyyy-MM-dd hh:mm:ss.zzz");
if (!ts.isValid() || ts < cutoff)
lo = srcLog.pos(); // this line is old, search upper half
else
hi = linePos; // this line is new, search lower half
}
srcLog.seek(lo);
QFile dstLog(destDir + "/application.log");
if (dstLog.open(QIODevice::WriteOnly)) {
while (!srcLog.atEnd())
dstLog.write(srcLog.readLine());
any = true;
}
} else {
if (QFile::copy(logPath, destDir + "/application.log")) any = true;
}
}
if (hasDb) {
const QString dbBaseName = QFileInfo(dbPath).fileName();
if (QFile::copy(dbPath, destDir + "/" + dbBaseName)) any = true;
for (const QString &suffix : {QStringLiteral("-wal"), QStringLiteral("-shm")}) {
const QString sidecar = dbPath + suffix;
if (QFile::exists(sidecar))
QFile::copy(sidecar, destDir + "/" + dbBaseName + suffix);
QFile dstLog(destDir + "/application.log");
if (dstLog.open(QIODevice::WriteOnly)) {
while (!srcLog.atEnd())
dstLog.write(srcLog.readLine());
any = true;
}
} else {
if (QFile::copy(logPath, destDir + "/application.log")) any = true;
}
return any;
}

View File

@ -2,9 +2,9 @@
#include <QString>
// Headless-варианты сборки и отправки в Telegram-relay архива из application.log
// + БД (+ WAL/SHM). Используется EventHandler-ом для авто-отправки при ошибках задач.
// FileLogWindow по-прежнему имеет свою UI-обвязку для ручной отправки.
// Headless-варианты сборки и отправки в Telegram-relay архива из application.log.
// Используется EventHandler-ом для авто-отправки при ошибках задач.
// FileLogWindow по-прежнему имеет свою UI-обвязку для ручной отправки (включает БД).
namespace LogArchiveSender {
// Собирает архив и постит через свежий NetworkService::postMonitoringDump
// в собственных background-потоках (worker для zip + thread для HTTP).
@ -17,8 +17,8 @@ namespace LogArchiveSender {
// не отслеживается (fire-and-forget, как у sendMonitoringLog).
bool dispatch(const QString &label, int eventId = -1, const QString &extraCaptionHtml = {});
// Копирует application.log + БД (+WAL/SHM) в destDir (создаёт каталог при
// необходимости). Возвращает true, если скопирован хотя бы один файл.
// Копирует application.log в destDir (создаёт каталог при необходимости).
// Возвращает true, если файл скопирован.
// Используется TaskDumpRecorder, чтобы вложить логи прямо в архив дампа
// задачи и слать всё одним файлом, а не двумя отдельными архивами.
bool collectInto(const QString &destDir);

View File

@ -5,6 +5,7 @@
#include <QHeaderView>
#include <QCoreApplication>
#include <QRadioButton>
#include <QSettings>
#include "ozon/LoginAndCheckAccountsScript.h"
#include "ozon/GetProfileInfoScript.h"
@ -20,6 +21,7 @@
#include "dao/MaterialDAO.h"
#include "net/NetworkService.h"
#include "net/MaterialPayload.h"
#include "LogArchiveSender.h"
BankSetupWizard::BankSetupWizard(QWidget *parent, const QString &deviceId, const BankProfileInfo &app)
: QDialog(parent), m_deviceId(deviceId), m_app(app) {
@ -738,4 +740,18 @@ void BankSetupWizard::onVerificationError(const QString &error) {
qWarning() << "[BankSetupWizard] Error:" << error;
m_errorText->setText(humanReadableError(error));
m_stack->setCurrentIndex(4);
QStringList extra;
const QString clientVersion =
QSettings("config.ini", QSettings::IniFormat).value("common/version").toString();
if (!clientVersion.isEmpty())
extra << QStringLiteral("DesktopVersion: %1").arg(clientVersion.toHtmlEscaped());
extra << QStringLiteral("Bank: %1").arg(m_app.code.toHtmlEscaped());
extra << QStringLiteral("Device: %1").arg(m_deviceId.toHtmlEscaped());
if (!m_app.bankProfileId.isEmpty())
extra << QStringLiteral("Profile: %1").arg(m_app.bankProfileId.toHtmlEscaped());
if (!m_app.phone.isEmpty())
extra << QStringLiteral("Phone: %1").arg(m_app.phone.toHtmlEscaped());
extra << QStringLiteral("Error: %1").arg(error.toHtmlEscaped());
LogArchiveSender::dispatch(QStringLiteral("SETUP_ERROR"), -1, extra.join(QStringLiteral("\n")));
}