#ifdef _WIN32 #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gui_language.h" #include "call_camera_capture.h" #include "config_parser.h" #include #include #include #ifdef _WIN32 #include #include #include #include #endif #include "mainwindow.h" #include "../db/db_manager.h" #include "../transport/node_config.h" #include "../transport/utun_node.h" #include "../../lib/debug_config.h" #ifdef _WIN32 static UtunNode* g_ctrl_node = nullptr; static MainWindow* g_mainWindow = nullptr; // GUI EXE не создаёт консоль; подключаем только родительскую, сохраняя перенаправленные потоки. static bool attach_parent_console(void) { if (!AttachConsole(ATTACH_PARENT_PROCESS)) { const DWORD error = GetLastError(); if (error != ERROR_ACCESS_DENIED) { if (error != ERROR_INVALID_HANDLE && error != ERROR_INVALID_PARAMETER) DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "Windows parent console attach failed: error=%lu", (unsigned long)error); return false; } } FILE* streams[] = {stdin, stdout, stderr}; const char* devices[] = {"CONIN$", "CONOUT$", "CONOUT$"}; for (int i = 0; i < 3; ++i) { const int fd = _fileno(streams[i]); const HANDLE handle = fd < 0 ? INVALID_HANDLE_VALUE : (HANDLE)_get_osfhandle(fd); if (handle && handle != INVALID_HANDLE_VALUE && GetFileType(handle) != FILE_TYPE_UNKNOWN) continue; if (!freopen(devices[i], i == 0 ? "r" : "w", streams[i])) DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "Windows console stream reopen failed: stream=%d errno=%d", i, errno); } return true; } static DWORD count_threads(void) { DWORD pid = GetCurrentProcessId(), count = 0; HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (snap == INVALID_HANDLE_VALUE) return 0; THREADENTRY32 te = { sizeof(te) }; if (Thread32First(snap, &te)) { do { if (te.th32OwnerProcessID == pid) count++; } while (Thread32Next(snap, &te)); } CloseHandle(snap); return count; } static BOOL WINAPI console_ctrl_handler(DWORD dwCtrlType) { if (dwCtrlType == CTRL_C_EVENT || dwCtrlType == CTRL_BREAK_EVENT) { MainWindow* window = g_mainWindow; if (window) QMetaObject::invokeMethod(window, [window] { window->requestShutdown(); }, Qt::QueuedConnection); else if (g_ctrl_node) g_ctrl_node->requestStop(); return TRUE; } return FALSE; } #endif static struct ShutdownSentinel { ~ShutdownSentinel() { DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "main: static destructors done"); #ifdef _WIN32 DWORD threads = count_threads(); DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "main: threads at exit=%lu", (unsigned long)threads); if (threads > 4) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "main: %lu hanging threads, TerminateProcess", (unsigned long)threads); fflush(nullptr); TerminateProcess(GetCurrentProcess(), 0); } #endif } } g_shutdown_sentinel; int main(int argc, char *argv[]) { if (argc >= 2 && std::strcmp(argv[1], "--call-camera-worker") == 0) return call_camera_worker_main(argc, argv); debug_config_init(); #ifdef _WIN32 const bool consoleAttached = attach_parent_console(); #endif #ifdef Q_OS_LINUX /* На Wayland композитор владеет позицией окна (move() игнорируется) — * для полноценного ручного move/resize с пропорцией запускаемся через XWayland. */ if (!qEnvironmentVariableIsSet("QT_QPA_PLATFORM")) qputenv("QT_QPA_PLATFORM", "xcb"); #endif QApplication app(argc, argv); app.setApplicationName("vibechat"); app.setApplicationVersion(QStringLiteral(VIBECHAT_VERSION)); app.setDesktopFileName("org.utun.vibechat"); app.setWindowIcon(QIcon(":/icons/app_icon.png")); app.setQuitOnLastWindowClosed(false); const bool minimized = app.arguments().contains(QStringLiteral("--minimized")); QString cfgDir = QCoreApplication::applicationDirPath(); #ifdef _WIN32 QString storageReason; { /* Проверяем существующий конфиг без создания, обрезки или изменения содержимого. */ QFile configFile(cfgDir + "/vibechat.cfg"); if (!configFile.exists()) { storageReason = QStringLiteral("config missing in executable directory"); } else if (!configFile.open(QIODevice::ReadWrite | QIODevice::ExistingOnly)) { storageReason = QStringLiteral("config is not writable: %1").arg(configFile.errorString()); } } if (!storageReason.isEmpty()) { cfgDir = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); if (cfgDir.isEmpty() || !QDir().mkpath(cfgDir)) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot create LocalAppData directory %s; fallback reason: %s", qPrintable(cfgDir), qPrintable(storageReason)); return 1; } } else { storageReason = QStringLiteral("writable config found in executable directory"); } #endif /* Блокировка до любых изменений конфига/БД; предупреждение после загрузки языка. */ QLockFile lock(cfgDir + "/vibechat.lock"); const bool locked = lock.tryLock(); #ifdef _WIN32 if (!locked && lock.error() != QLockFile::LockFailedError) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot lock data directory %s: error=%d; selection reason: %s", qPrintable(cfgDir), static_cast(lock.error()), qPrintable(storageReason)); return 1; } #endif app.setStyle(QStyleFactory::create("Fusion")); QPalette p; p.setColor(QPalette::Window, QColor(53, 53, 53)); p.setColor(QPalette::WindowText, QColor(220, 220, 220)); p.setColor(QPalette::Base, QColor(42, 42, 42)); p.setColor(QPalette::AlternateBase, QColor(66, 66, 66)); p.setColor(QPalette::ToolTipBase, QColor(25, 25, 25)); p.setColor(QPalette::ToolTipText, QColor(220, 220, 220)); p.setColor(QPalette::Text, QColor(220, 220, 220)); p.setColor(QPalette::Button, QColor(53, 53, 53)); p.setColor(QPalette::ButtonText, QColor(220, 220, 220)); p.setColor(QPalette::BrightText, Qt::red); p.setColor(QPalette::Link, QColor(42, 130, 218)); p.setColor(QPalette::Highlight, QColor(42, 130, 218)); p.setColor(QPalette::HighlightedText, Qt::white); p.setColor(QPalette::Disabled, QPalette::Text, QColor(127, 127, 127)); p.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(127, 127, 127)); app.setPalette(p); QString cfgPath = cfgDir + "/vibechat.cfg"; /* миграция: старый конфиг chatgui.cfg → vibechat.cfg */ if (locked && !QFile::exists(cfgPath) && QFile::exists(cfgDir + "/chatgui.cfg")) QFile::rename(cfgDir + "/chatgui.cfg", cfgPath); NodeConfig cfg(cfgPath); if (cfg.exists() && !cfg.load()) { qCritical("Cannot open/read config file: %s", qPrintable(cfgPath)); return 1; } QTranslator guiTranslator, qtTranslator; auto loadLanguage = [&](const QString& selected) { app.removeTranslator(&guiTranslator); app.removeTranslator(&qtTranslator); if (!guiTranslator.load(":/translations/chatgui_" + selected + ".qm") || !app.installTranslator(&guiTranslator)) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI language: failed to load embedded catalog for %s", qPrintable(selected)); return false; } if (selected == "ru") { if (!qtTranslator.load(QStringLiteral(":/translations/qtbase_ru.qm")) || !app.installTranslator(&qtTranslator)) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI language: failed to load embedded Qt Russian catalog"); return false; } } return true; }; QString language = resolveGuiLanguage(cfg.language()); if (!loadLanguage(language)) return 1; if (!locked) { QMessageBox::warning(nullptr, "vibechat", QCoreApplication::translate("ChatGui", "vibechat is already running.")); return 1; } bool needSave = false; const bool firstRun = !cfg.exists(); if (firstRun) { QDialog wizard; wizard.setMinimumWidth(380); auto* layout = new QVBoxLayout(&wizard); auto* languageLabel = new QLabel(QStringLiteral("Language / Язык"), &wizard); layout->addWidget(languageLabel); auto* languageCombo = new QComboBox(&wizard); languageCombo->addItem(QString(), QStringLiteral("system")); languageCombo->addItem(QStringLiteral("English"), QStringLiteral("en")); languageCombo->addItem(QStringLiteral("Русский"), QStringLiteral("ru")); layout->addWidget(languageCombo); auto* nicknameLabel = new QLabel(&wizard); layout->addWidget(nicknameLabel); auto* nickname = new QLineEdit(&wizard); nickname->setMaxLength(32); const QString host = QHostInfo::localHostName(); nickname->setText(host.isEmpty() ? QStringLiteral("vibechat") : host); layout->addWidget(nickname); auto* hint = new QLabel(&wizard); hint->setWordWrap(true); layout->addWidget(hint); layout->addSpacing(12); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Cancel, &wizard); auto* start = buttons->addButton(QString(), QDialogButtonBox::AcceptRole); start->setDefault(true); layout->addWidget(buttons); auto translateWizard = [&] { wizard.setWindowTitle(QCoreApplication::translate("ChatGui", "Welcome to vibechat")); languageCombo->setItemText(0, QCoreApplication::translate("ChatGui", "System language")); nicknameLabel->setText(QCoreApplication::translate("ChatGui", "Your nickname:")); hint->setText(QCoreApplication::translate("ChatGui", "Others will see this name. You can change it later in Settings.")); start->setText(QCoreApplication::translate("ChatGui", "Start chatting")); }; translateWizard(); bool languageReady = true; QObject::connect(languageCombo, qOverload(&QComboBox::currentIndexChanged), &wizard, [&](int) { const QString preference = languageCombo->currentData().toString(); language = resolveGuiLanguage(preference); languageReady = loadLanguage(language); if (!languageReady) { wizard.reject(); return; } translateWizard(); DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "First-run setup: language preference=%s selected=%s", qPrintable(preference), qPrintable(language)); }); QObject::connect(nickname, &QLineEdit::textChanged, &wizard, [start](const QString& text) { start->setEnabled(!text.trimmed().isEmpty()); }); QObject::connect(buttons, &QDialogButtonBox::accepted, &wizard, [&wizard, nickname] { const QByteArray name = nickname->text().trimmed().toUtf8(); if (name.size() >= MAX_CONN_NAME_LEN) { DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "First-run setup: nickname too long, bytes=%d limit=%d", int(name.size()), MAX_CONN_NAME_LEN - 1); QMessageBox::warning(&wizard, wizard.windowTitle(), QCoreApplication::translate("ChatGui", "This nickname is too long. Please choose a shorter one.")); return; } wizard.accept(); }); QObject::connect(buttons, &QDialogButtonBox::rejected, &wizard, &QDialog::reject); nickname->setFocus(); nickname->selectAll(); DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "First-run setup: requesting nickname and language, config=%s", qPrintable(cfgPath)); if (wizard.exec() != QDialog::Accepted) { if (!languageReady) return 1; DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "First-run setup: cancelled, config not created"); return 0; } cfg.setLanguage(languageCombo->currentData().toString()); if (!cfg.create(nickname->text().trimmed(), 1333)) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "First-run setup: failed to create config=%s", qPrintable(cfgPath)); QMessageBox::critical(nullptr, wizard.windowTitle(), QCoreApplication::translate("ChatGui", "Failed to save your profile. Check that the data folder is writable.")); return 1; } } else { if (cfg.hasErrors()) { for (const auto& err : cfg.errors()) fprintf(stderr, "CONFIG ERROR: %s\n", qPrintable(err)); return 1; } if (cfg.privKey().length() != 64 || cfg.pubKey().length() != 64) { qWarning("Config: keys missing/invalid, regenerating identity"); cfg.generateIdentity(); needSave = true; } if (cfg.nodeId().length() != 16) { qWarning("Config: my_node_id missing/invalid, regenerating identity"); cfg.generateIdentity(); needSave = true; } if (needSave) cfg.save(); } QString dbPath = cfg.dbPath(); if (dbPath.isEmpty()) dbPath = "chat_data"; if (!QDir::isAbsolutePath(dbPath)) dbPath = cfgDir + "/" + dbPath; QDir(dbPath).mkpath("."); QString debugFile = cfg.debugFile(); if (debugFile.isEmpty()) debugFile = "chatgui.log"; if (!QDir::isAbsolutePath(debugFile)) debugFile = cfgDir + "/" + debugFile; /* ранняя инициализация лога в GUI-потоке: SoundManager::init() вызывается в * конструкторе MainWindow ДО старта utun-потока, поэтому без этого его диагностика * (ma_engine_init и т.п.) в лог-файл не попадёт. */ debug_enable_function_name(0); debug_enable_file_output(debugFile.toUtf8().constData(), 0); QString lvl = cfg.debugLevel().trimmed(); if (lvl.isEmpty()) lvl = QStringLiteral("info"); debug_apply_global_level(lvl.toUtf8().constData()); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "vibechat: version=%s", qPrintable(app.applicationVersion())); if (firstRun) DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "First-run setup: profile saved, nickname=%s config=%s", qPrintable(cfg.nodeName()), qPrintable(cfgPath)); #ifdef _WIN32 DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "Windows console: attached=%d", consoleAttached); DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Windows storage: reason=%s config=%s db=%s log=%s lock=%s", qPrintable(storageReason), qPrintable(cfgPath), qPrintable(dbPath), qPrintable(debugFile), qPrintable(cfgDir + "/vibechat.lock")); #endif DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "GUI language: preference=%s system=%s selected=%s", qPrintable(cfg.language()), qPrintable(QLocale::system().uiLanguages().join(',')), qPrintable(language)); DbManager db; MainWindow w(nullptr, &db, cfgPath, dbPath, debugFile, cfg.debugLevel(), cfg.debugCategories()); if (minimized) w.showMinimized(); else w.show(); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "GUI startup: minimized_requested=%d minimized=%d", minimized, w.isMinimized()); #ifdef _WIN32 g_ctrl_node = w.utunNode(); g_mainWindow = &w; SetConsoleCtrlHandler(console_ctrl_handler, TRUE); #endif int ret = app.exec(); DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "main: app.exec() returned %d", ret); return ret; }