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
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; |
|
}
|
|
|