You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

356 lines
16 KiB

#ifdef _WIN32
#include <winsock2.h>
#endif
#include <QCoreApplication>
#include <QApplication>
#include <QIcon>
#include <QPalette>
#include <QStyleFactory>
#include <QDir>
#include <QFile>
#include <QLockFile>
#include <QMessageBox>
#include <QTranslator>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QHostInfo>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include "gui_language.h"
#include "call_camera_capture.h"
#include "config_parser.h"
#include <cstdio>
#include <cstring>
#include <cerrno>
#ifdef _WIN32
#include <QStandardPaths>
#include <windows.h>
#include <tlhelp32.h>
#include <io.h>
#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<int>(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<int>(&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;
}