Cockatrice/cockatrice/src/main.cpp
BruebachL 6385f3fab3
[CrashReporting] Mock crash-report dialog and reporting preference (#7521)
* [CrashReporting] Mock dialog, sentry skeleton and CI prerequisites

Mock: DlgCrashReport preview dialog (no account needed, redacted log
preview), CrashReportingMode Ask/Always/Never setting in DebugSettings,
--simulate-crash flag and Help-menu preview entry. Send writes a local
mock JSON only.

Vendor sentry-native 0.17.3 via FetchContent behind
WITH_CRASH_REPORTING (breakpad everywhere, backend none under
sanitizers) plus a CrashReporter facade with early init and post-loop
shutdown. DSN is a placeholder: empty CMake default with
COCKATRICE_SENTRY_DSN env override, so SDK init is skipped and mock
behavior is unchanged until a real DSN is configured.

CI prerequisites riding here so every layer below builds green:
libcurl dev plus CA bundles in the Linux images and CodeQL env, sentry
Qt integration off (its own find_package cannot see the macOS thin Qt
layout), and the StrFunc Using construction makensis requires.

Tests: ctest green, format clean, code plus UI gates PASS.

* [CrashReporting] Forward mode changes to SDK and fix Linux CI opt-in

Connects PersonalSettings::crashReportingModeChanged to
giveConsent()/revokeConsent() so switching from AlwaysSend to
NeverSend (or Ask) mid-session reaches the SDK instead of leaving
consent given until restart.

Forwards WITH_CRASH_REPORTING into the docker container the way
CCACHE_DIR is, so the Linux package leg actually builds with crash
reporting ON instead of silently building OFF.

* [CrashReporting] Give crash-report logging its own category per review

* [CrashReporting] Drop unused DebugSettings constructor visibility per review

* [CrashReporting] Document reporting modes on the enum per review

* [CrashReporting] Match the dump scan to the writer's temp fallback

Review nit: when the handler's SHGetKnownFolderPath lookup fails it writes to
<temp>/../.., but the startup scan looked in QDir::tempPath(), so a dump
produced through the fallback would never be found and its crash would never
offer to be reported. Walk up the same two levels in the scan.

The path is Windows-only (#ifdef Q_OS_WIN); the statements were compiled and
run standalone against Qt 6 to confirm the API use.

* [CrashReporting] Complete the dump-scan fallback path

The previous commit walked the scan's fallback up two levels but dropped the
CrashDumps/Cockatrice suffix that the writer appends unconditionally, so a dump
written through the fallback was still invisible to the scan.

Evaluate the writer's expression verbatim rather than mirroring it with QDir:
cdUp() is existence-checked and silently leaves the path alone when the
directory is missing, parent_path() is not, and QDir::absoluteFilePath() joins
the shared suffix without risking a doubled separator when the walk lands on a
root. Verified by compiling the block verbatim against Qt 6 with -Wextra
-Werror and comparing it with the writer's join for both the normal and the
fallback case.

Also record that %LOCALAPPDATA% is the scan's stand-in for
FOLDERID_LocalAppData and why the two normally agree.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-19 14:57:03 +02:00

542 lines
23 KiB
C++

/***************************************************************************
* Copyright (C) 2008 by Max-Wilhelm Bruker *
* brukie@gmx.net *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#include "main.h"
#include "client/crash/crash_reporter.h"
#include "client/network/update/card_spoiler/spoiler_background_updater.h"
#include "client/settings/cache_settings.h"
#include "client/sound_engine.h"
#include "client/url_scheme_event_filter.h"
#include "database/interface/settings_card_preference_provider.h"
#include "interface/intents/intent.h"
#include "interface/intents/intent_open_local_deck.h"
#include "interface/logger.h"
#include "interface/pixel_map_generator.h"
#include "interface/theme_manager.h"
#include "interface/widgets/dialogs/dlg_crash_report.h"
#include "interface/window_main.h"
#include "libcockatrice/card/database/card_database.h"
#include "libcockatrice/rng/rng_abstract.h"
#include "single_instance_manager.h"
#include "version_string.h"
#include <QApplication>
#include <QByteArray>
#include <QByteArrayView>
#include <QCommandLineOption>
#include <QCommandLineParser>
#include <QCoreApplication>
#include <QCryptographicHash>
#include <QDebug>
#include <QDialog>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QGuiApplication>
#include <QIcon>
#include <QList>
#include <QLocale>
#include <QMessageBox>
#include <QMessageLogger>
#include <QNetworkInterface>
#include <QObject>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QSharedPointer>
#include <QString>
#include <QStringList>
#include <QStringLiteral>
#include <QTranslator>
#include <QUrl>
#include <QtGlobal>
#include <algorithm>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/rng/rng_sfmt.h>
#include <libcockatrice/settings/card_database_settings.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/network_settings.h>
#include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/utility/cryptoutil.h>
#include <libcockatrice/utility/translation_loader.h>
#include <qlogging.h>
#include <qnamespace.h>
#include <qsystemdetection.h>
QTranslator *translator, *qtTranslator;
RNG_Abstract *rng;
SoundEngine *soundEngine;
QSystemTrayIcon *trayIcon;
ThemeManager *themeManager;
const QString translationPrefix = "cockatrice";
QString translationPath;
static void CockatriceLogger(QtMsgType type, const QMessageLogContext &ctx, const QString &message)
{
QString logMessage = qFormatLogMessage(type, ctx, message);
// Regular expression to match the full path in the square brackets and extract only the filename and line number
QRegularExpression regex(R"(\[(?:.:)?[\/\\].*[\/\\]([^\/\\]+\:\d+)\])");
QRegularExpressionMatch match = regex.match(logMessage);
if (match.hasMatch()) {
// Extract the filename and line number (e.g., "main.cpp:211")
QString filenameLine = match.captured(1);
// Replace the full path in square brackets with just the filename and line number
logMessage.replace(match.captured(0), QString("[%1]").arg(filenameLine));
}
Logger::getInstance().log(type, ctx, logMessage);
}
#ifdef Q_OS_WIN
// clang-format off
#include <Windows.h>
#include <DbgHelp.h>
#include <ShlObj.h>
#include <ctime>
#include <filesystem>
#pragma comment(lib, "DbgHelp.lib") // Link the DbgHelp library
// clang-format on
LONG WINAPI CockatriceUnhandledExceptionFilter(EXCEPTION_POINTERS *exceptionPointers)
{
std::filesystem::path path;
// Find %LOCALAPPDATA% (or cheat at finding it)
wchar_t *localAppDataFolder;
if (SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_CREATE, NULL, &localAppDataFolder) != S_OK) {
path = std::filesystem::temp_directory_path().parent_path().parent_path();
} else {
path = std::filesystem::path(localAppDataFolder);
}
// Plan on writing crash files into %LOCALAPPDATA%/CrashDumps/Cockatrice/cockatrice.crash.*.dmp
path /= "CrashDumps";
path /= "Cockatrice";
if (!std::filesystem::exists(path)) {
std::filesystem::create_directories(path);
}
path /= "cockatrice.crash." + std::to_string(std::time(0)) + ".dmp";
// Create and write crash files
#ifdef UNICODE
HANDLE hDumpFile =
CreateFile(path.wstring().c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
#else
HANDLE hDumpFile =
CreateFile(path.string().c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
#endif
MINIDUMP_EXCEPTION_INFORMATION mei;
mei.ExceptionPointers = exceptionPointers;
mei.ThreadId = GetCurrentThreadId();
mei.ClientPointers = 1;
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hDumpFile, MiniDumpWithFullMemory, &mei, nullptr,
nullptr);
CloseHandle(hDumpFile);
return EXCEPTION_EXECUTE_HANDLER;
}
#endif
void installNewTranslator()
{
QString lang = SettingsCache::instance().personal().getLang();
// Qt's own strings and ours need a translator each: QTranslator discards its previous contents
// on every load, so loading both files into one translator silently drops the first of them.
TranslationLoader::loadQt(*qtTranslator, lang);
qApp->installTranslator(qtTranslator);
TranslationLoader::loadApplication(*translator, translationPrefix, lang,
TranslationLoader::applicationTranslationPaths(translationPath));
qApp->installTranslator(translator);
}
QString const generateClientID()
{
QString macList;
for (const QNetworkInterface &networkInterface : QNetworkInterface::allInterfaces()) {
if (networkInterface.hardwareAddress() != "") {
if (networkInterface.hardwareAddress() != "00:00:00:00:00:00:00:E0") {
macList += networkInterface.hardwareAddress() + ".";
}
}
}
QString strClientID = QCryptographicHash::hash(macList.toUtf8(), QCryptographicHash::Sha1).toHex().right(15);
return strClientID;
}
static QString redactActivationUrl(const QString &url)
{
// Activation URLs carry secrets in their query string (e.g. the deck share
// token); log only the scheme and the action (cockatrice://opendeck), never
// the parameters.
if (!url.startsWith(QStringLiteral("cockatrice://"))) {
return url;
}
const QUrl parsed(url);
return parsed.scheme() + "://" + parsed.host();
}
int main(int argc, char *argv[])
{
#ifdef Q_OS_WIN
SetUnhandledExceptionFilter(CockatriceUnhandledExceptionFilter);
#endif
// Logging setup
#ifdef Q_OS_APPLE
// <build>/cockatrice/cockatrice.app/Contents/MacOS/cockatrice
const QByteArray configPath = "../../../qtlogging.ini";
#elif defined(Q_OS_UNIX)
// <build>/cockatrice/cockatrice
const QByteArray configPath = "./qtlogging.ini";
#elif defined(Q_OS_WIN)
// <build>/cockatrice/Debug/cockatrice.exe
const QByteArray configPath = "../qtlogging.ini";
#else
const QByteArray configPath = "";
#endif
if (!qEnvironmentVariableIsSet(("QT_LOGGING_CONF"))) {
// Set the QT_LOGGING_CONF environment variable
qputenv("QT_LOGGING_CONF", configPath);
}
qSetMessagePattern(
"\033[0m[%{time yyyy-MM-dd h:mm:ss.zzz} "
"%{if-debug}\033[36mD%{endif}%{if-info}\033[32mI%{endif}%{if-warning}\033[33mW%{endif}%{if-critical}\033[31mC%{"
"endif}%{if-fatal}\033[1;31mF%{endif}\033[0m] [%{function}] - %{message} [%{file}:%{line}]");
QApplication app(argc, argv);
#ifdef Q_OS_MAC
UrlSchemeEventFilter cockatriceFilter(QStringList{QStringLiteral("cockatrice")});
QStringList pendingMacUrls;
const auto cocoaBufferConn =
QObject::connect(&cockatriceFilter, &UrlSchemeEventFilter::urlReceived,
[&pendingMacUrls](const QString &url) { pendingMacUrls.append(url); });
app.installEventFilter(&cockatriceFilter);
#endif
QObject::connect(&app, &QApplication::lastWindowClosed, &app, &QApplication::quit);
qInstallMessageHandler(CockatriceLogger);
#ifdef Q_OS_WIN
app.addLibraryPath(app.applicationDirPath() + "/plugins");
#endif
// These values are only used by the settings loader/saver
// Wrong or outdated values are kept to not break things
QCoreApplication::setOrganizationName("Cockatrice");
QCoreApplication::setOrganizationDomain("cockatrice.github.io");
QCoreApplication::setApplicationName("Cockatrice");
QCoreApplication::setApplicationVersion(VERSION_STRING);
#ifdef Q_OS_MAC
qApp->setAttribute(Qt::AA_DontShowIconsInMenus, true);
#endif
// Crash reporting (increment 1): init the SDK early so a crash anywhere below is captured into the
// local database. Uploads stay consent-gated (require_user_consent, armed in CrashReporter::init):
// AlwaysSend gives consent here so cached envelopes flush at startup; any other mode starts
// unconsented and the dialog below drives give/revoke. An empty DSN skips the SDK entirely and
// the mock fallback further below behaves exactly as before.
CrashReporter crashReporter;
const QString crashDatabasePath = SettingsCache::instance().getSettingsPath() + QStringLiteral("sentry");
{
QString crashDsn = QString::fromUtf8(qgetenv("COCKATRICE_SENTRY_DSN"));
if (crashDsn.isEmpty()) {
crashDsn = QString::fromLatin1(COCKATRICE_SENTRY_DSN);
}
crashReporter.init(crashDsn, QString::fromLatin1(VERSION_STRING), crashDatabasePath,
SettingsCache::instance().personal().getCrashReportingMode());
if (crashReporter.isReady() && SettingsCache::instance().personal().getCrashReportingMode() ==
PersonalSettings::CrashReportingMode::AlwaysSend) {
// Silent opt-in: cached crash envelopes flush automatically once consent is given.
crashReporter.giveConsent();
}
// Mid-session mode changes must reach the SDK: without this, switching from AlwaysSend to
// NeverSend (or Ask) leaves consent given until restart, so a later crash in this session
// would still upload after the user opted out. crashReporter outlives main(), so capturing
// it here is safe.
QObject::connect(&SettingsCache::instance().personal(), &PersonalSettings::crashReportingModeChanged,
[&crashReporter](PersonalSettings::CrashReportingMode mode) {
if (mode == PersonalSettings::CrashReportingMode::AlwaysSend) {
crashReporter.giveConsent();
} else {
crashReporter.revokeConsent();
}
});
}
// Persistent session log: keeps this run's logs on disk so the next-startup crash dialog can
// attach the crashed session's tail (the in-memory buffer is lost on crash). Opened in Append
// mode with a per-launch session marker; read back via Logger::getPreviousSessionTail().
const QString persistentLogPath =
SettingsCache::instance().getSettingsPath() + QStringLiteral("logs/cockatrice.log");
Logger::getInstance().setPersistentLogPath(persistentLogPath);
// Translations
#ifdef Q_OS_MAC
translationPath = qApp->applicationDirPath() + "/../Resources/translations";
#elif defined(Q_OS_WIN)
translationPath = qApp->applicationDirPath() + "/translations";
#else // linux
translationPath = qApp->applicationDirPath() + "/../share/cockatrice/translations";
#endif
// Command-line parser
QCommandLineParser parser;
parser.setApplicationDescription("Cockatrice Client");
parser.addHelpOption();
parser.addVersionOption();
parser.addOptions(
{{{"c", "connect"}, QCoreApplication::translate("main", "Connect on startup"), "user:pass@host:port"},
{{"d", "debug-output"}, QCoreApplication::translate("main", "Debug to file")},
{"simulate-crash",
QCoreApplication::translate("main", "Show the mock crash-report dialog (nothing is sent)")}});
parser.process(app);
if (parser.isSet("debug-output")) {
Logger::getInstance().logToFile(true);
}
// --- Handle files or URLs passed at startup ---
// Only positional arguments are treated as files/URLs, so options like
// --connect are never handed off to another instance.
const QStringList startupFiles = parser.positionalArguments();
const bool hasActivationFiles = !startupFiles.isEmpty();
SingleInstanceManager instance;
if (hasActivationFiles) {
QStringList redactedFiles;
redactedFiles.reserve(startupFiles.size());
for (const QString &file : startupFiles) {
redactedFiles.append(redactActivationUrl(file));
}
qCInfo(MainLog) << "Activation launch, files:" << redactedFiles;
// Activation launch: hand off to the primary instance if one is
// running, otherwise become the primary ourselves. Do this before
// constructing the main window so a hand-off exits cheaply.
if (!instance.tryRun(startupFiles)) {
qInfo() << "Handed off to a running instance, exiting";
// Sent successfully → exit
return 0;
}
// No primary instance → become server
qInfo() << "No existing instance found, becoming primary instance";
} else {
// Plain launch: if another instance is running, run independently
// instead of handing off and exiting.
if (!instance.tryRun(QStringList())) {
// Another instance is already running → just run independently
qInfo() << "Another instance exists, running independently";
} else {
qInfo() << "No existing instance found, starting server";
}
}
rng = new RNG_SFMT(CryptoUtil::randomUInt64());
themeManager = new ThemeManager;
soundEngine = new SoundEngine;
qtTranslator = new QTranslator;
translator = new QTranslator;
installNewTranslator();
QLocale::setDefault(QLocale::English);
// Dependency Injections
CardDatabaseManager::setCardPreferenceProvider(new SettingsCardPreferenceProvider());
CardDatabaseManager::setCardDatabasePathProvider(&SettingsCache::instance());
CardDatabaseManager::setCardSetPriorityController(&SettingsCache::instance().cardDatabase());
qCInfo(MainLog) << "Starting main program";
// Front-load the card database before constructing the main window. The
// binary-cache read is cheap when uncontended (~1s); doing it here -- while no
// GUI work yet competes for CPU -- avoids the multi-second, GUI-freezing
// contention that happens when the load runs alongside window construction.
// The CardDatabaseModel populates from the already-loaded data in its
// constructor, so the window appears fully populated with no startup lag.
// Note: checkUnknownSets() is deliberately not run for this front-loaded
// parse. It would run before MainWindow exists, so
// cardDatabaseNewSetsFound / cardDatabaseAllNewSetsEnabled would have no
// receivers, and enabling the sets up-front would suppress the first-run
// experience MainWindow::startupConfigCheck() and the onboarding wizard
// provide. Callers that load after startup own the check.
CardDatabaseManager::getInstance()->loadCardDatabases();
MainWindow ui;
// A URL launch must own the connection: the intent chain triggered by the
// URL connects to the server named in the URL, so the window's own startup
// auto-connect must not race against it (two connectToServer calls tear
// each other down via doDisconnectFromServer).
bool hasUrlActivation = std::any_of(startupFiles.begin(), startupFiles.end(), [](const QString &file) {
return file.startsWith(QStringLiteral("cockatrice://"));
});
#ifdef Q_OS_MAC
// On macOS the launch can arrive through the URL scheme instead of as a
// positional argument (captured in pendingMacUrls); count those too or the
// window would auto-connect into the link's own connection attempt.
hasUrlActivation = hasUrlActivation ||
std::any_of(pendingMacUrls.cbegin(), pendingMacUrls.cend(),
[](const QString &url) { return url.startsWith(QStringLiteral("cockatrice://")); });
#endif
ui.setSkipStartupAutoConnect(hasUrlActivation);
auto handleActivation = [&ui](const QString &file) {
if (file.startsWith("cockatrice://")) {
qCInfo(MainLog) << "Handling URL activation:" << redactActivationUrl(file);
// Route through the window's persistent url parser: it serializes
// link chains so activations handed over while another chain is
// still connecting do not connect concurrently.
ui.handleCockatriceLink(file);
} else if (QFileInfo(file).exists()) {
auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file);
QObject::connect(openDeckIntent, &Intent::failed, &ui, [&ui](const QString &reason) {
QMessageBox::warning(&ui, QObject::tr("Open deck"), reason);
});
openDeckIntent->execute();
}
};
#ifdef Q_OS_MAC
QObject::disconnect(cocoaBufferConn);
QObject::connect(&cockatriceFilter, &UrlSchemeEventFilter::urlReceived,
[&handleActivation](const QString &url) { handleActivation(url); });
#endif
if (parser.isSet("connect")) {
ui.setConnectTo(parser.value("connect"));
}
qCInfo(MainLog) << "MainWindow constructor finished";
ui.setWindowIcon(themePixmap(QStringLiteral("cockatrice")));
// Base name of the desktop file installed next to the binary, without the ".desktop"
// extension. Wayland reads the window icon from it, and xdg-desktop-portal looks up
// org.freedesktop.host.portal.Registry by this app ID, so it has to match the file name
// exactly, down to the case.
QGuiApplication::setDesktopFileName("cockatrice");
SettingsCache::instance().network().setClientID(generateClientID());
// If spoiler mode is enabled, we will download the spoilers
// then reload the DB. otherwise just reload the DB
SpoilerBackgroundUpdater spoilerBackgroundUpdater;
ui.show();
qCInfo(MainLog) << "ui.show() finished";
// MOCK crash-report flow (UI + settings only): no sentry-native upload, no network.
// Triggered by --simulate-crash or by leftover Windows crash dumps. Handled dumps are deleted
// after Send / Don't send (or a silent NeverSend skip) so one crash never prompts twice; on
// non-Windows nothing writes cockatrice.crash.*.dmp yet, so --simulate-crash is the trigger there.
{
QString crashDumpDir;
#ifdef Q_OS_WIN
// Mirrors the writer (CockatriceUnhandledExceptionFilter): it appends CrashDumps/Cockatrice
// to %LOCALAPPDATA%, or, when its known-folder lookup fails, to the temp directory walked up
// two parent_path() steps. %LOCALAPPDATA% is this scan's stand-in for FOLDERID_LocalAppData:
// both come from the same profile, so when the lookup fails the variable is normally unset.
QString crashDumpRootPath = QString::fromLocal8Bit(qgetenv("LOCALAPPDATA"));
if (crashDumpRootPath.isEmpty()) {
// The writer's fallback expression, verbatim — QDir::cdUp() is existence-checked and
// would silently leave the path alone here, parent_path() is not.
const std::filesystem::path fallback = std::filesystem::temp_directory_path().parent_path().parent_path();
crashDumpRootPath = QString::fromStdWString(fallback.wstring());
}
crashDumpDir = QDir(crashDumpRootPath).absoluteFilePath(QStringLiteral("CrashDumps/Cockatrice"));
#else
crashDumpDir = QDir::tempPath();
#endif
const QStringList crashDumps =
QDir(crashDumpDir).entryList({QStringLiteral("cockatrice.crash.*.dmp")}, QDir::Files);
if (parser.isSet("simulate-crash") || !crashDumps.isEmpty()) {
const PersonalSettings::CrashReportingMode crashMode =
SettingsCache::instance().personal().getCrashReportingMode();
if (crashMode == PersonalSettings::CrashReportingMode::AlwaysSend) {
crashReporter.giveConsent();
DlgCrashReport::writeMockReport(DlgCrashReport::buildRedactedPreview());
} else if (crashMode == PersonalSettings::CrashReportingMode::Ask) {
DlgCrashReport crashDialog(&ui);
if (crashDialog.exec() == QDialog::Accepted) {
crashReporter.giveConsent();
} else {
crashReporter.revokeConsent();
}
}
// NeverSend: skip silently (no dialog, nothing written or sent).
for (const QString &dump : crashDumps) {
QFile::remove(crashDumpDir + QStringLiteral("/") + dump);
}
}
}
// force shortcuts to be shown/hidden in right-click menus, regardless of system defaults
qApp->setAttribute(Qt::AA_DontShowShortcutsInContextMenus,
!SettingsCache::instance().userInterface().getShowShortcuts());
#ifdef Q_OS_MAC
for (const QString &url : pendingMacUrls) {
handleActivation(url);
}
pendingMacUrls.clear();
#endif
for (const QString &file : startupFiles) {
handleActivation(file);
}
// Connect to future file/URL events from other instances
QObject::connect(&instance, &SingleInstanceManager::filesReceived, [&handleActivation](const QStringList &files) {
for (const QString &file : files) {
handleActivation(file);
}
});
int ret = app.exec();
crashReporter.shutdown();
qCInfo(MainLog) << "Event loop finished, terminating...";
delete rng;
PingPixmapGenerator::clear();
CountryPixmapGenerator::clear();
UserLevelPixmapGenerator::clear();
ManaSymbolPixmapGenerator::clear();
return ret;
}