273 lines
11 KiB
C++
273 lines
11 KiB
C++
#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 "gui_language.h"
|
|
#include "call_camera_capture.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;
|
|
const QString language = resolveGuiLanguage(cfg.language());
|
|
if (!guiTranslator.load(":/translations/chatgui_" + language + ".qm") || !app.installTranslator(&guiTranslator)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI language: failed to load embedded catalog for %s", qPrintable(language));
|
|
return 1;
|
|
}
|
|
if (language == "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 1;
|
|
}
|
|
}
|
|
if (!locked) {
|
|
QMessageBox::warning(nullptr, "vibechat", QCoreApplication::translate("ChatGui", "vibechat is already running."));
|
|
return 1;
|
|
}
|
|
bool needSave = false;
|
|
|
|
if (!cfg.exists()) {
|
|
if (!cfg.create("", 1333)) {
|
|
qCritical("Failed to create config: %s", qPrintable(cfgPath));
|
|
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()));
|
|
#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;
|
|
}
|