Add minimized Windows autostart and preserve terminal output

This commit is contained in:
evgeny committed 2026-10-15 21:27:32 +02:00
1 parent abdca1acc7
commit 7c5b76b482
5 files changed
+69 -4

No files matched your search

+1 -1
View File
@@ -216,7 +216,7 @@ set_source_files_properties(${CMAKE_SOURCE_DIR}/../../src/radio/radio_audio.c PR
set_target_properties(utun_voice PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(utun_voice PROPERTIES POSITION_INDEPENDENT_CODE ON)
add_executable(vibechat add_executable(vibechat WIN32
src/main.cpp src/main.cpp
src/mainwindow.cpp src/mainwindow.cpp
src/channellist.cpp src/channellist.cpp
+16
View File
@@ -51,6 +51,18 @@ BAT-скрипты автоматически включают --installer. От
запускается от исходного пользователя. Входящие подключения разрешаются запускается от исходного пользователя. Входящие подключения разрешаются
правилом "VibeChat (installer)" только для установленного EXE. правилом "VibeChat (installer)" только для установленного EXE.
На странице дополнительных задач включена по умолчанию опция автозапуска
при входе в Windows для всех пользователей. Ярлык автозагрузки передаёт
--minimized: главное окно запускается свёрнутым, обычные ярлыки открывают его
как раньше. Выбор сохраняется при обновлении; снятие галочки удаляет прежний
ярлык автозагрузки. При удалении приложения этот ярлык также удаляется.
EXE собирается как GUI-приложение: запуск через Проводник, установщик или
автозагрузку не создаёт терминал. Запуск из терминала подключает родительскую
консоль и сохраняет перенаправление stdout/stderr, в том числе в файл или pipe.
Лог приложения показывает режим окна и наличие консоли, лог установки —
выбранный режим автозапуска и путь ярлыка.
При обновлении установщик предлагает закрыть приложение, использующее EXE/DLL. При обновлении установщик предлагает закрыть приложение, использующее EXE/DLL.
Обновление и удаление сохраняют конфиг, ключи, историю и медиа пользователя. Обновление и удаление сохраняют конфиг, ключи, историю и медиа пользователя.
Деинсталлятор удаляет своё правило firewall и установленные файлы программы. Деинсталлятор удаляет своё правило firewall и установленные файлы программы.
@@ -68,3 +80,7 @@ BAT-скрипты автоматически включают --installer. От
На чистой Windows без MSYS2/Qt: установка, запуск обычным пользователем, На чистой Windows без MSYS2/Qt: установка, запуск обычным пользователем,
сообщения/звуки/медиа, обновление с сохранением идентичности, удаление, сообщения/звуки/медиа, обновление с сохранением идентичности, удаление,
повторная установка. Проверить пути с пробелами и имя пользователя с кириллицей. повторная установка. Проверить пути с пробелами и имя пользователя с кириллицей.
Проверить автозапуск после входа в Windows, свёрнутость при ранее развёрнутом
окне, восстановление из трея, сохранение выбора и отключение при обновлении.
При запуске из Проводника не должно быть консоли; из терминала проверить
вывод, перенаправление и Ctrl+C. Дочерний процесс камеры также не создаёт консоль.
+15
View File
@@ -14,6 +14,7 @@ AppPublisher=uTun
DefaultDirName={autopf}\VibeChat DefaultDirName={autopf}\VibeChat
DisableDirPage=no DisableDirPage=no
DisableProgramGroupPage=yes DisableProgramGroupPage=yes
UsePreviousTasks=yes
ArchitecturesAllowed=x64compatible ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible ArchitecturesInstallIn64BitMode=x64compatible
MinVersion=10.0 MinVersion=10.0
@@ -36,6 +37,11 @@ Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"
[Tasks] [Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "autostart"; Description: "{cm:AutoStart}"; GroupDescription: "{cm:AutoStartGroup}"
[InstallDelete]
; Снятая при обновлении галочка удаляет только наш прежний ярлык автозагрузки.
Type: files; Name: "{autostartup}\VibeChat.lnk"; Tasks: not autostart
[Files] [Files]
Source: "{#PackageDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#PackageDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
@@ -43,11 +49,16 @@ Source: "{#PackageDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
[Icons] [Icons]
Name: "{autoprograms}\VibeChat"; Filename: "{app}\vibechat.exe"; WorkingDir: "{app}" Name: "{autoprograms}\VibeChat"; Filename: "{app}\vibechat.exe"; WorkingDir: "{app}"
Name: "{autodesktop}\VibeChat"; Filename: "{app}\vibechat.exe"; WorkingDir: "{app}"; Tasks: desktopicon Name: "{autodesktop}\VibeChat"; Filename: "{app}\vibechat.exe"; WorkingDir: "{app}"; Tasks: desktopicon
Name: "{autostartup}\VibeChat"; Filename: "{app}\vibechat.exe"; Parameters: "--minimized"; WorkingDir: "{app}"; Tasks: autostart
[Run] [Run]
Filename: "{app}\vibechat.exe"; Description: "{cm:LaunchProgram,VibeChat}"; WorkingDir: "{app}"; Flags: nowait postinstall skipifsilent runasoriginaluser Filename: "{app}\vibechat.exe"; Description: "{cm:LaunchProgram,VibeChat}"; WorkingDir: "{app}"; Flags: nowait postinstall skipifsilent runasoriginaluser
[CustomMessages] [CustomMessages]
english.AutoStartGroup=Startup:
russian.AutoStartGroup=Автозагрузка:
english.AutoStart=Start VibeChat minimized when signing in to Windows (for all users)
russian.AutoStart=Запускать VibeChat свёрнутым при входе в Windows (для всех пользователей)
english.FirewallFailed=Could not configure the VibeChat firewall rule. See the installation log for details. english.FirewallFailed=Could not configure the VibeChat firewall rule. See the installation log for details.
russian.FirewallFailed=Не удалось настроить правило брандмауэра VibeChat. Подробности в журнале установки. russian.FirewallFailed=Не удалось настроить правило брандмауэра VibeChat. Подробности в журнале установки.
@@ -75,6 +86,10 @@ var
begin begin
if CurStep <> ssPostInstall then Exit; if CurStep <> ssPostInstall then Exit;
Log('VibeChat: installed to ' + ExpandConstant('{app}') + '; user config, keys and history are preserved'); Log('VibeChat: installed to ' + ExpandConstant('{app}') + '; user config, keys and history are preserved');
if WizardIsTaskSelected('autostart') then
Log('Startup: enabled with --minimized; shortcut=' + ExpandConstant('{autostartup}\VibeChat.lnk'))
else
Log('Startup: disabled; shortcut=' + ExpandConstant('{autostartup}\VibeChat.lnk'));
RemoveFirewallRule; RemoveFirewallRule;
Parameters := 'advfirewall firewall add rule name="VibeChat (installer)" dir=in action=allow program=' + Parameters := 'advfirewall firewall add rule name="VibeChat (installer)" dir=in action=allow program=' +
AddQuotes(ExpandConstant('{app}\vibechat.exe')) + ' enable=yes profile=any'; AddQuotes(ExpandConstant('{app}\vibechat.exe')) + ' enable=yes profile=any';
+32 -2
View File
@@ -15,10 +15,12 @@
#include "call_camera_capture.h" #include "call_camera_capture.h"
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <cerrno>
#ifdef _WIN32 #ifdef _WIN32
#include <QStandardPaths> #include <QStandardPaths>
#include <windows.h> #include <windows.h>
#include <tlhelp32.h> #include <tlhelp32.h>
#include <io.h>
#endif #endif
#include "mainwindow.h" #include "mainwindow.h"
#include "../db/db_manager.h" #include "../db/db_manager.h"
@@ -30,6 +32,28 @@
static UtunNode* g_ctrl_node = nullptr; static UtunNode* g_ctrl_node = nullptr;
static MainWindow* g_mainWindow = 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) { static DWORD count_threads(void) {
DWORD pid = GetCurrentProcessId(), count = 0; DWORD pid = GetCurrentProcessId(), count = 0;
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
@@ -75,6 +99,10 @@ static struct ShutdownSentinel {
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
if (argc >= 2 && std::strcmp(argv[1], "--call-camera-worker") == 0) return call_camera_worker_main(argc, 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 #ifdef Q_OS_LINUX
/* На Wayland композитор владеет позицией окна (move() игнорируется) — /* На Wayland композитор владеет позицией окна (move() игнорируется) —
* для полноценного ручного move/resize с пропорцией запускаемся через XWayland. */ * для полноценного ручного move/resize с пропорцией запускаемся через XWayland. */
@@ -87,7 +115,7 @@ int main(int argc, char *argv[]) {
app.setDesktopFileName("org.utun.vibechat"); app.setDesktopFileName("org.utun.vibechat");
app.setWindowIcon(QIcon(":/icons/app_icon.png")); app.setWindowIcon(QIcon(":/icons/app_icon.png"));
app.setQuitOnLastWindowClosed(false); app.setQuitOnLastWindowClosed(false);
debug_config_init(); const bool minimized = app.arguments().contains(QStringLiteral("--minimized"));
QString cfgDir = QCoreApplication::applicationDirPath(); QString cfgDir = QCoreApplication::applicationDirPath();
#ifdef _WIN32 #ifdef _WIN32
@@ -218,6 +246,7 @@ int main(int argc, char *argv[]) {
debug_apply_global_level(lvl.toUtf8().constData()); debug_apply_global_level(lvl.toUtf8().constData());
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "vibechat: version=%s", qPrintable(app.applicationVersion())); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "vibechat: version=%s", qPrintable(app.applicationVersion()));
#ifdef _WIN32 #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", 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(storageReason), qPrintable(cfgPath), qPrintable(dbPath), qPrintable(debugFile),
qPrintable(cfgDir + "/vibechat.lock")); qPrintable(cfgDir + "/vibechat.lock"));
@@ -228,7 +257,8 @@ int main(int argc, char *argv[]) {
DbManager db; DbManager db;
MainWindow w(nullptr, &db, cfgPath, dbPath, debugFile, MainWindow w(nullptr, &db, cfgPath, dbPath, debugFile,
cfg.debugLevel(), cfg.debugCategories()); cfg.debugLevel(), cfg.debugCategories());
w.show(); if (minimized) w.showMinimized(); else w.show();
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "GUI startup: minimized_requested=%d minimized=%d", minimized, w.isMinimized());
#ifdef _WIN32 #ifdef _WIN32
g_ctrl_node = w.utunNode(); g_ctrl_node = w.utunNode();
+5 -1
View File
@@ -301,8 +301,10 @@ void MainWindow::restoreWindowGeometry() {
if (!visible) target.moveTo(0, 0); if (!visible) target.moveTo(0, 0);
setGeometry(target); setGeometry(target);
// Поздний ответ БД сохраняет свёрнутость при --minimized и не показывает скрытое в tray окно.
if (m_db->getUiStateInt("window_maximized", 0)) if (m_db->getUiStateInt("window_maximized", 0))
showMaximized(); setWindowState(windowState() | Qt::WindowMaximized);
GUI_DEBUG("window geometry restored: maximized=%d minimized=%d visible=%d", isMaximized(), isMinimized(), isVisible());
if (m_splitter) { if (m_splitter) {
const QStringList sp = m_db->getUiState("window_splitter").split(','); const QStringList sp = m_db->getUiState("window_splitter").split(',');
@@ -933,6 +935,7 @@ void MainWindow::setupTray() {
QAction *showAction = m_trayMenu->addAction(QCoreApplication::translate("ChatGui", "Show")); QAction *showAction = m_trayMenu->addAction(QCoreApplication::translate("ChatGui", "Show"));
connect(showAction, &QAction::triggered, this, [this]() { connect(showAction, &QAction::triggered, this, [this]() {
if (isMinimized()) setWindowState(windowState() & ~Qt::WindowMinimized);
show(); show();
raise(); raise();
activateWindow(); activateWindow();
@@ -999,6 +1002,7 @@ void MainWindow::onShutdownCheck() {
void MainWindow::onTrayActivated(QSystemTrayIcon::ActivationReason reason) { void MainWindow::onTrayActivated(QSystemTrayIcon::ActivationReason reason) {
if (reason == QSystemTrayIcon::Trigger || if (reason == QSystemTrayIcon::Trigger ||
reason == QSystemTrayIcon::DoubleClick) { reason == QSystemTrayIcon::DoubleClick) {
if (isMinimized()) setWindowState(windowState() & ~Qt::WindowMinimized);
show(); show();
raise(); raise();
activateWindow(); activateWindow();