diff --git a/monitoring/app.py b/monitoring/app.py index d6e4295..eb6b331 100644 --- a/monitoring/app.py +++ b/monitoring/app.py @@ -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 diff --git a/services/LogArchiveSender.cpp b/services/LogArchiveSender.cpp index 8e4a883..ca0161d 100644 --- a/services/LogArchiveSender.cpp +++ b/services/LogArchiveSender.cpp @@ -6,10 +6,8 @@ #include #include #include -#include #include #include -#include #include #include @@ -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; } diff --git a/services/LogArchiveSender.h b/services/LogArchiveSender.h index f1fbefd..17d09bb 100644 --- a/services/LogArchiveSender.h +++ b/services/LogArchiveSender.h @@ -2,9 +2,9 @@ #include -// 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); diff --git a/views/bank/BankSetupWizard.cpp b/views/bank/BankSetupWizard.cpp index b1dfbcf..31e41fa 100644 --- a/views/bank/BankSetupWizard.cpp +++ b/views/bank/BankSetupWizard.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #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"))); }