#include "main_window.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_UNIX #include #undef KeyPress #undef KeyRelease #endif #include "desktop_list_page.h" #include "desktop_settings_dialog.h" #include "login_page.h" #include "transition_overlay.h" namespace vdi { namespace { constexpr int kTicketTimeoutMs = 10000; constexpr int kProcessStartTimeoutMs = 5000; constexpr int kStreamReadyTimeoutMs = 30000; } // namespace MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { fsm_ = StateMachine([this](State from, State to, const QString& detail) { onStateChanged(from, to, detail); }); setupUi(); setupHotkey(); setupHeartbeat(); config_ = loadConfig(); desktopService_.setRuntimeHandlers([this](std::function done) { showReturnCover(); stopSessions(); if (!fsm_.isIn(State::kDesktopList)) fsm_.transitionTo(State::kDesktopList,QStringLiteral("管理员已结束会话")); presentKioskSurface(); releaseReturnCoverWhenReady(); if (desktopListPage_) desktopListPage_->setBusy(true); auto* wait=new QTimer(this); wait->setInterval(100); connect(wait,&QTimer::timeout,this,[this,wait,done,attempts=0]() mutable { const bool stopped=!moonlightSession_ || !moonlightSession_->isRunning(); if (!stopped && ++attempts<100) return; wait->stop(); wait->deleteLater(); if (desktopListPage_) desktopListPage_->setBusy(false); done(stopped); }); wait->start(); },[this]() { return QString::fromLatin1(stateToString(fsm_.current())); }); if (desktopService_.platformManaged()) { auto* refresh=new QTimer(this); refresh->setInterval(15000); connect(refresh,&QTimer::timeout,this,[this]{ if (fsm_.isIn(State::kDesktopList)) refreshDesktopList(); }); refresh->start(); desktopService_.setPolicyHandler([refresh](int seconds){ refresh->setInterval(seconds*1000); }); } deviceAuthTimer_ = new QTimer(this); deviceAuthTimer_->setSingleShot(true); connect(deviceAuthTimer_, &QTimer::timeout, this, &MainWindow::authenticateDevice); deviceAuthMessage_ = QStringLiteral("正在连接管理平台,请稍候…"); // Paint the product page before even a synchronous configuration failure. // Device authentication has no username/password recovery flow. fsm_.transitionTo(State::kDesktopList, QStringLiteral("请选择要进入的云桌面")); QTimer::singleShot(0, this, &MainWindow::authenticateDevice); } void MainWindow::authenticateDevice() { if (deviceAuthenticated_ || deviceAuthInFlight_) return; deviceAuthTimer_->stop(); deviceAuthInFlight_ = true; const QString deviceUser = QString::fromStdString(config_.sn).trimmed(); desktopService_.login(deviceUser.isEmpty() ? QStringLiteral("terminal") : deviceUser, QStringLiteral("kiosk"), [this](const AuthResult& result) { deviceAuthInFlight_ = false; deviceAuthenticated_ = result.ok; if (result.ok) { deviceAuthRetryMs_ = 2000; qInfo() << "vdi_device_auth result=ready"; currentUser_ = result.displayName; if (desktopListPage_) { desktopListPage_->setCurrentUser(currentUser_); } if (fsm_.isIn(State::kDesktopList)) refreshDesktopList(); return; } // A cold boot may precede Wi-Fi, clock synchronization or the API. // Stay on the selection page and never substitute local authorization. deviceAuthMessage_ = QStringLiteral("%1;正在自动重试,无需重启盒子") .arg(result.message.isEmpty() ? QStringLiteral("管理平台暂不可用") : result.message); qWarning() << "vdi_device_auth result=waiting retry_ms=" << deviceAuthRetryMs_; if (fsm_.isIn(State::kDesktopList)) { refreshDesktopList(); } deviceAuthTimer_->start(deviceAuthRetryMs_); deviceAuthRetryMs_ = qMin(deviceAuthRetryMs_ * 2, 30000); }); } MainWindow::~MainWindow() { qApp->removeEventFilter(this); setKioskInputGuard(false); #ifdef Q_OS_UNIX if (grabDisplay_) { XCloseDisplay(static_cast(grabDisplay_)); grabDisplay_ = nullptr; } #endif stopSessions(); delete returnCover_; returnCover_ = nullptr; } void MainWindow::setupUi() { setWindowTitle(QStringLiteral("VDI 云终端")); // The login/list pages are the security boundary of the terminal. Keep a // borderless, top-level surface over the desktop panels while the guest // stream is not active. X11BypassWindowManagerHint also prevents MATE's // panel from being raised above the shell after a global shortcut. Qt::WindowFlags kioskFlags = Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint; #ifdef Q_OS_UNIX kioskFlags |= Qt::X11BypassWindowManagerHint; #endif setWindowFlags(kioskFlags); setWindowState(Qt::WindowFullScreen); setStyleSheet(QStringLiteral("QMainWindow { background: #0f1420; }")); qApp->installEventFilter(this); // The display-guard service may restore the physical output mode after // this process has already mapped its window. Track screen geometry so // the opaque kiosk surface immediately follows that mode change instead // of leaving an exposed root-desktop strip at the old size. for (QScreen* screen : QApplication::screens()) { connect(screen, &QScreen::geometryChanged, this, [this](const QRect&) { if (isVisible()) { applyKioskSurface(); } }); } Qt::WindowFlags coverFlags = Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool; #ifdef Q_OS_UNIX coverFlags |= Qt::X11BypassWindowManagerHint; #endif returnCover_ = new QWidget(nullptr, coverFlags); returnCover_->setAttribute(Qt::WA_ShowWithoutActivating); returnCover_->setStyleSheet(QStringLiteral("QWidget { background: #0f1420; }")); returnCover_->hide(); stack_ = new QStackedWidget(this); stack_->setStyleSheet(QStringLiteral("background: #0f1420;")); setCentralWidget(stack_); auto* login = new LoginPage(config_, this); loginPage_ = login; login->setStyleSheet( QStringLiteral("QWidget { background: #0f1420; color: #e6e9ef; }" "QLineEdit { background: #1a2233; border: 1px solid #2b3654; " "border-radius: 4px; padding: 8px; color: #e6e9ef; }" "QPushButton { background: #232d44; border: 1px solid #2b3654; " "border-radius: 6px; color: #e6e9ef; }" "QPushButton:hover { background: #2b3654; }")); stack_->addWidget(login); connect(login, &LoginPage::loginRequested, this, &MainWindow::onLoginRequested); auto* list = new DesktopListPage(this); desktopListPage_ = list; stack_->addWidget(list); connect(list, &DesktopListPage::desktopSelected, this, &MainWindow::onDesktopSelected); connect(list, &DesktopListPage::editRequested, this, &MainWindow::onEditDesktopRequested); overlay_ = new TransitionOverlay(this); statusBar_ = new QStatusBar(this); statusBar_->setStyleSheet(QStringLiteral("QStatusBar { background: #161d2e; color: #8a94a6; }")); setStatusBar(statusBar_); // Product feedback is shown in the page/overlay; internal FSM names are // retained in diagnostics rather than displayed along the screen edge. statusBar_->hide(); connectionTimer_ = new QTimer(this); connectionTimer_->setSingleShot(true); connect(connectionTimer_, &QTimer::timeout, this, [this]() { QString message; switch (connectionPhase_) { case ConnectionPhase::kTicket: message = QStringLiteral("获取连接信息超时"); break; case ConnectionPhase::kProcessStart: message = QStringLiteral("Moonlight 启动超时"); break; case ConnectionPhase::kStreamReady: message = QStringLiteral("云桌面画面就绪超时"); break; default: return; } failConnection(connectionAttempt_, message); }); moonlightSession_ = std::make_unique(); } void MainWindow::setupHotkey() { auto* menuShortcut = new QShortcut(QKeySequence(QStringLiteral("Ctrl+Alt+Shift+M")), this); menuShortcut->setContext(Qt::ApplicationShortcut); connect(menuShortcut, &QShortcut::activated, this, [this]() { onMenuAction(0); }); auto* quitShortcut = new QShortcut(QKeySequence(QStringLiteral("Ctrl+Alt+Shift+Q")), this); quitShortcut->setContext(Qt::ApplicationShortcut); connect(quitShortcut, &QShortcut::activated, this, [this]() { if (fsm_.isIn(State::kConnecting) || fsm_.isIn(State::kSession) || fsm_.isIn(State::kReconnect)) { // Put the opaque kiosk surface back on top before asking // Moonlight to terminate. This prevents one compositor frame of // the box desktop from showing while the stream window closes. showReturnCover(); fsm_.transitionTo(State::kDesktopList, QStringLiteral("已退出串流")); presentKioskSurface(); stopSessions(); if (desktopListPage_) { desktopListPage_->setBusy(false); desktopListPage_->setStatus(QStringLiteral("已退出串流")); } releaseReturnCoverWhenReady(); } }); } void MainWindow::setupHeartbeat() { const QString path = QStringLiteral("/tmp/vdi-kiosk.heartbeat"); heartbeatTimer_ = new QTimer(this); heartbeatTimer_->setInterval(5000); connect(heartbeatTimer_, &QTimer::timeout, this, [path]() { QFile file(path); if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { file.write(QByteArray::number(QDateTime::currentSecsSinceEpoch())); } }); heartbeatTimer_->start(); } void MainWindow::onStateChanged(State from, State to, const QString& detail) { Q_UNUSED(from); const QString stateName = QString::fromLatin1(stateToString(to)); updateStatusBar(detail.isEmpty() ? stateName : QStringLiteral("%1 %2").arg(stateName, detail)); // Only the Moonlight window may receive system shortcuts. Every other // state keeps the kiosk keyboard grab, including the connecting overlay. const bool streamSurface = to == State::kSession || to == State::kReconnect; if (streamSurface) { // Moonlight owns Ctrl+Alt+Shift+Q while streaming and can destroy its // window before QProcess notifies us. Keep our painted, full-screen // list mapped underneath it: restoring a hidden window here is too // late to prevent a frame of the host desktop from being scanned out. setKioskInputGuard(false); } else { setKioskInputGuard(true); } switch (to) { case State::kConfig: showLogin(); break; case State::kAuthenticating: break; case State::kDesktopList: showDesktopList(); refreshDesktopList(); break; case State::kConnecting: overlay_->start(QStringLiteral("正在进入云桌面")); break; case State::kSession: case State::kReconnect: break; case State::kError: stopSessions(); fsm_.transitionTo(State::kDesktopList, QStringLiteral("连接失败")); if (desktopListPage_) { desktopListPage_->setBusy(false); desktopListPage_->setStatus(QStringLiteral("连接失败:%1").arg(detail), true); } return; case State::kBoot: break; } // Select the destination page before mapping/painting the window. In // particular, the first boot frame must never paint the old login page. if (!streamSurface && to != State::kBoot) { presentKioskSurface(); } } void MainWindow::showEvent(QShowEvent* event) { QMainWindow::showEvent(event); // With the production X11 kiosk session there is no window manager to // perform the initial activation handshake. Merely raising the Qt // surface leaves _NET_ACTIVE_WINDOW unset and pointer/keyboard events can // keep going to the desktop root. Use the same direct X11 map/focus path // as the return-from-stream transition on the first paint as well. presentKioskSurface(); // HDMI mode restoration can happen shortly after kiosk-shell maps. A // delayed pass covers that race even on X servers that do not emit a // geometryChanged signal for the mode switch. QTimer::singleShot(1200, this, [this]() { if (isVisible()) { applyKioskSurface(); } }); if (kioskInputGuard_) { retryKeyboardGrab(); } } void MainWindow::applyKioskSurface() { if (!isVisible()) { return; } // Geometry updates and delayed show callbacks must not steal the stream's // focus or raise this persistent backing surface over Moonlight. if (kioskInputGuard_) { raise(); activateWindow(); if (overlay_ && overlay_->isVisible()) { overlay_->raise(); } } if (QScreen* screen = windowHandle() ? windowHandle()->screen() : QApplication::primaryScreen()) { // An override-redirect window does not receive the WM fullscreen // negotiation; explicitly cover the complete output geometry. setGeometry(screen->geometry()); } } void MainWindow::presentKioskSurface() { if (!isVisible()) { showFullScreen(); } // Let Qt map and paint the opaque surface before changing the X11 stack. // The direct X11 calls are needed because Moonlight uses an override/full- // screen window and a WM-only raise can otherwise be deferred one frame. applyKioskSurface(); QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); #ifdef Q_OS_UNIX if (!grabDisplay_) { grabDisplay_ = XOpenDisplay(nullptr); } if (grabDisplay_) { auto* display = static_cast(grabDisplay_); const Window window = static_cast(winId()); XMapRaised(display, window); XRaiseWindow(display, window); XSetInputFocus(display, window, RevertToParent, CurrentTime); XSync(display, False); } #endif applyKioskSurface(); } void MainWindow::showReturnCover() { if (!returnCover_) { return; } QScreen* screen = QApplication::primaryScreen(); if (screen) { returnCover_->setGeometry(screen->geometry()); } returnCover_->show(); returnCover_->raise(); QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); #ifdef Q_OS_UNIX if (!grabDisplay_) { grabDisplay_ = XOpenDisplay(nullptr); } if (grabDisplay_) { auto* display = static_cast(grabDisplay_); const Window window = static_cast(returnCover_->winId()); XMapRaised(display, window); XRaiseWindow(display, window); XSync(display, False); } #endif } void MainWindow::hideReturnCover() { if (!returnCover_) { return; } returnCover_->hide(); QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); } void MainWindow::releaseReturnCoverWhenReady() { if (!returnCover_ || !returnCover_->isVisible()) { return; } bool ready = isVisible(); #ifdef Q_OS_UNIX if (ready && grabDisplay_) { XWindowAttributes attributes{}; ready = XGetWindowAttributes(static_cast(grabDisplay_), static_cast(winId()), &attributes) != 0 && attributes.map_state == IsViewable; } #endif if (ready) { presentKioskSurface(); hideReturnCover(); return; } // The X server may need a few event-loop turns to map the Qt surface // after Moonlight relinquishes its fullscreen window. Keep the opaque // cover in place until that mapping is observable; never expose the root // desktop during this interval. QTimer::singleShot(20, this, [this]() { releaseReturnCoverWhenReady(); }); } void MainWindow::setKioskInputGuard(bool enabled) { kioskInputGuard_ = enabled; if (!enabled) { #ifdef Q_OS_UNIX if (keyboardGrabActive_ && grabDisplay_) { auto* display = static_cast(grabDisplay_); XUngrabKeyboard(display, CurrentTime); XFlush(display); } #endif keyboardGrabActive_ = false; return; } if (isVisible()) { retryKeyboardGrab(); } } void MainWindow::retryKeyboardGrab() { if (!kioskInputGuard_ || !isVisible() || keyboardGrabActive_) { return; } #ifdef Q_OS_UNIX if (!grabDisplay_) { grabDisplay_ = XOpenDisplay(nullptr); } if (grabDisplay_) { auto* display = static_cast(grabDisplay_); const int result = XGrabKeyboard(display, static_cast(winId()), False, GrabModeAsync, GrabModeAsync, CurrentTime); if (result == GrabSuccess) { XSetInputFocus(display, static_cast(winId()), RevertToParent, CurrentTime); } XFlush(display); keyboardGrabActive_ = result == GrabSuccess; } #else keyboardGrabActive_ = true; #endif if (!keyboardGrabActive_ && !keyboardGrabRetryPending_) { keyboardGrabRetryPending_ = true; QTimer::singleShot(200, this, [this]() { keyboardGrabRetryPending_ = false; retryKeyboardGrab(); }); } } bool MainWindow::eventFilter(QObject* watched, QEvent* event) { Q_UNUSED(watched); if (kioskInputGuard_ && (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease || event->type() == QEvent::ShortcutOverride)) { auto* keyEvent = static_cast(event); const Qt::KeyboardModifiers modifiers = keyEvent->modifiers(); const int key = keyEvent->key(); const bool metaShortcut = modifiers.testFlag(Qt::MetaModifier) || key == Qt::Key_Meta; const bool altWindowShortcut = modifiers.testFlag(Qt::AltModifier) && (key == Qt::Key_Tab || key == Qt::Key_F4); const bool vtShortcut = modifiers.testFlag(Qt::ControlModifier) && modifiers.testFlag(Qt::AltModifier) && (key == Qt::Key_Delete || (key >= Qt::Key_F1 && key <= Qt::Key_F12)); const bool screenshotKey = key == Qt::Key_Print; if (metaShortcut || altWindowShortcut || vtShortcut || screenshotKey) { return true; } } return QMainWindow::eventFilter(watched, event); } void MainWindow::updateStatusBar(const QString& text) { statusBar_->showMessage(text); } void MainWindow::showLogin() { stack_->setCurrentWidget(loginPage_); if (loginPage_) { loginPage_->setBusy(false); loginPage_->setStatus(QStringLiteral("请输入用户名和密码登录")); } } void MainWindow::showDesktopList() { if (!desktopListPage_) { return; } stack_->setCurrentWidget(desktopListPage_); desktopListPage_->setCurrentUser(currentUser_); desktopListPage_->setBusy(false); } void MainWindow::refreshDesktopList() { if (!deviceAuthenticated_) { desktops_.clear(); if (desktopListPage_) { desktopListPage_->setDesktops(desktops_); desktopListPage_->setBusy(true); desktopListPage_->setStatus(deviceAuthMessage_); } return; } desktopService_.listDesktops([this](std::vector list, const QString& error) { if (error.isEmpty()) { desktops_ = list; } else if (desktopService_.platformManaged()) { desktops_.clear(); } if (desktopListPage_) { desktopListPage_->setDesktops(desktops_); desktopListPage_->setBusy(!fsm_.isIn(State::kDesktopList)); if (!error.isEmpty()) { desktopListPage_->setStatus(error, true); } else { desktopListPage_->setStatus(QString()); } } }); } void MainWindow::onLoginRequested(const QString& username, const QString& password) { desktopService_.login(username, password, [this](const AuthResult& result) { if (!loginPage_) { return; } if (!result.ok) { loginPage_->setBusy(false); loginPage_->setStatus(result.message, true); return; } currentUser_ = result.displayName; fsm_.transitionTo(State::kAuthenticating, QStringLiteral("认证成功")); fsm_.transitionTo(State::kDesktopList, QStringLiteral("欢迎 %1").arg(currentUser_)); }); } void MainWindow::onDesktopSelected(const QString& desktopId) { if (!deviceAuthenticated_) return; if (!fsm_.canConnect()) { if (desktopListPage_) { desktopListPage_->setBusy(false); } return; } const quint64 attempt = ++connectionAttempt_; connectingDesktopId_ = desktopId; connectionClock_.restart(); phaseClock_.restart(); connectionPhase_ = ConnectionPhase::kTicket; logConnectionPhase(attempt, QStringLiteral("ticket"), QStringLiteral("started")); bool powerOnConnect=false; for (const auto& d:desktops_) if (d.id==desktopId) powerOnConnect=d.powerOnConnect; if (!fsm_.transitionTo(State::kConnecting, powerOnConnect ? QStringLiteral("正在准备云桌面,请等待启动") : QStringLiteral("正在获取连接信息"))) { failConnection(attempt, QStringLiteral("当前状态无法发起连接")); return; } armConnectionTimeout(attempt, ConnectionPhase::kTicket, powerOnConnect ? 210000 : kTicketTimeoutMs); desktopService_.createTicket(desktopId, [this, attempt](const ConnectionTicket& ticket, const QString& error) { if (attempt != connectionAttempt_ || connectionPhase_ != ConnectionPhase::kTicket) { desktopService_.sessionEnded(ticket.sessionId); return; } connectionTimer_->stop(); if (!error.isEmpty()) { failConnection(attempt, error); return; } logConnectionPhase(attempt, QStringLiteral("ticket"), QStringLiteral("ready")); activeTicket_ = ticket; if (!startMoonlightSession(ticket, attempt)) { failConnection(attempt, QStringLiteral("已有串流进程正在运行")); } }); } void MainWindow::onEditDesktopRequested(const QString& desktopId) { if (desktopService_.platformManaged()) { if (desktopListPage_) desktopListPage_->setStatus(QStringLiteral("桌面与连接参数由管理平台维护")); return; } DesktopSettingsDialog dialog(desktops_, desktopId, this); dialog.setStyleSheet( QStringLiteral("QWidget { background: #0f1420; color: #e6e9ef; }" "QLineEdit { background: #1a2233; border: 1px solid #2b3654; " "border-radius: 4px; padding: 8px; color: #e6e9ef; }" "QComboBox { background: #1a2233; border: 1px solid #2b3654; " "border-radius: 4px; padding: 8px; color: #e6e9ef; }" "QPushButton { background: #232d44; border: 1px solid #2b3654; " "border-radius: 6px; color: #e6e9ef; }" "QLabel { color: #e6e9ef; }")); if (dialog.exec() == QDialog::Accepted) { desktops_ = dialog.desktops(); if (desktopListPage_) { desktopListPage_->setDesktops(desktops_); desktopListPage_->setStatus(QStringLiteral("连接配置已保存")); } } } bool MainWindow::startMoonlightSession(const ConnectionTicket& ticket, quint64 attempt) { moonlightSession_->setOptions(MoonlightSession::optionsFromEnvironment( QProcessEnvironment::systemEnvironment())); phaseClock_.restart(); connectionPhase_ = ConnectionPhase::kProcessStart; logConnectionPhase(attempt, QStringLiteral("process_start"), QStringLiteral("started")); armConnectionTimeout(attempt, ConnectionPhase::kProcessStart, kProcessStartTimeoutMs); return moonlightSession_->start(ticket, [this, attempt](const MoonlightSession::Event& event) { QMetaObject::invokeMethod( this, [this, attempt, event]() { onMoonlightEvent(attempt, event); }, Qt::QueuedConnection); }); } void MainWindow::onMoonlightEvent(quint64 attempt, const MoonlightSession::Event& event) { if (attempt != connectionAttempt_) { return; } switch (event.type) { case MoonlightSession::EventType::kStarted: if (connectionPhase_ != ConnectionPhase::kProcessStart) { return; } connectionTimer_->stop(); logConnectionPhase(attempt, QStringLiteral("process_start"), QStringLiteral("ready")); phaseClock_.restart(); armConnectionTimeout(attempt, ConnectionPhase::kStreamReady, kStreamReadyTimeoutMs); updateStatusBar(QStringLiteral("CONNECTING 正在初始化云桌面画面")); break; case MoonlightSession::EventType::kStreamReady: if (connectionPhase_ != ConnectionPhase::kStreamReady) { return; } waitForMoonlightWindow(attempt); break; case MoonlightSession::EventType::kFailed: failConnection(attempt, event.message); break; case MoonlightSession::EventType::kFinished: { desktopService_.sessionEnded(activeTicket_.sessionId); connectionTimer_->stop(); logConnectionPhase(attempt, QStringLiteral("session"), QStringLiteral("finished")); ++connectionAttempt_; connectionPhase_ = ConnectionPhase::kIdle; overlay_->finish(); if (fsm_.isIn(State::kConnecting) || fsm_.isIn(State::kSession) || fsm_.isIn(State::kReconnect)) { fsm_.transitionTo(State::kDesktopList, event.message); } if (desktopListPage_) { desktopListPage_->setBusy(false); desktopListPage_->setStatus(event.message); } break; } } } void MainWindow::waitForMoonlightWindow(quint64 attempt) { if (attempt != connectionAttempt_ || connectionPhase_ != ConnectionPhase::kStreamReady) { return; } // Paint the return destination while our surface is still mapped, before // handing the top of the X11 stack to Moonlight. A native client shortcut, // normal disconnect, or client crash then reveals this page immediately. showDesktopList(); desktopListPage_->setBusy(true); desktopListPage_->setStatus(QString()); repaint(); QGuiApplication::sync(); if (!moonlightSession_->activateStreamWindow()) { QTimer::singleShot(100, this, [this, attempt]() { waitForMoonlightWindow(attempt); }); return; } connectionTimer_->stop(); logConnectionPhase(attempt, QStringLiteral("stream_ready"), QStringLiteral("ready")); connectionPhase_ = ConnectionPhase::kSession; desktopService_.sessionConnected(activeTicket_.sessionId); fsm_.transitionTo(State::kSession, QStringLiteral("云桌面画面已就绪")); overlay_->onFirstFrame(); } void MainWindow::failConnection(quint64 attempt, const QString& message) { if (attempt != connectionAttempt_) { return; } connectionTimer_->stop(); logConnectionPhase(attempt, QStringLiteral("connection"), QStringLiteral("failed"), message); ++connectionAttempt_; connectionPhase_ = ConnectionPhase::kIdle; if (moonlightSession_) { moonlightSession_->stop(); } releasePlatformSessionAfterStop(activeTicket_.sessionId); overlay_->finish(); if (fsm_.isIn(State::kConnecting) || fsm_.isIn(State::kSession) || fsm_.isIn(State::kReconnect)) { fsm_.transitionTo(State::kDesktopList, QStringLiteral("连接失败")); } if (desktopListPage_) { desktopListPage_->setBusy(false); desktopListPage_->setStatus(QStringLiteral("连接失败:%1").arg(message), true); } } void MainWindow::armConnectionTimeout(quint64 attempt, ConnectionPhase phase, int timeoutMs) { if (attempt != connectionAttempt_) { return; } connectionPhase_ = phase; connectionTimer_->start(timeoutMs); } void MainWindow::logConnectionPhase(quint64 attempt, const QString& phase, const QString& result, const QString& error) { const qint64 totalMs = connectionClock_.isValid() ? connectionClock_.elapsed() : 0; const qint64 phaseMs = phaseClock_.isValid() ? phaseClock_.elapsed() : 0; qInfo().noquote() << QStringLiteral("vdi_connection attempt=%1 desktop=%2 phase=%3 result=%4 phase_ms=%5 total_ms=%6 error=%7") .arg(attempt) .arg(connectingDesktopId_, phase, result) .arg(phaseMs) .arg(totalMs) .arg(error); } void MainWindow::onQuitRequested() { stopSessions(); QApplication::quit(); } void MainWindow::stopSessions() { stopSessions(true); } void MainWindow::stopSessions(bool invalidateAttempt) { connectionTimer_->stop(); if (invalidateAttempt) { ++connectionAttempt_; } connectionPhase_ = ConnectionPhase::kIdle; if (moonlightSession_) { moonlightSession_->stop(); } releasePlatformSessionAfterStop(activeTicket_.sessionId); if (overlay_) { overlay_->finish(); } } void MainWindow::releasePlatformSessionAfterStop(const QString& id) { if (id.isEmpty()) return; const qint64 pid=moonlightSession_?moonlightSession_->processId():0; if (!pid) { desktopService_.sessionEnded(id); return; } auto* wait=new QTimer(this); wait->setInterval(100); connect(wait,&QTimer::timeout,this,[this,wait,id,pid] { if (moonlightSession_ && moonlightSession_->isRunning() && moonlightSession_->processId()==pid) return; wait->stop(); wait->deleteLater(); desktopService_.sessionEnded(id); }); wait->start(); } void MainWindow::onMenuAction(int action) { switch (action) { case 1: if (fsm_.canConnect() && !activeTicket_.desktopId.isEmpty()) { onDesktopSelected(activeTicket_.desktopId); } break; case 2: showReturnCover(); if (fsm_.current() != State::kConfig) { fsm_.transitionTo(State::kDesktopList, QStringLiteral("已断开")); } stopSessions(); releaseReturnCoverWhenReady(); break; case 3: onQuitRequested(); break; default: { QMenu menu(this); QAction* settings = menu.addAction(QStringLiteral("桌面连接设置")); menu.addSeparator(); QAction* reconnect = menu.addAction(QStringLiteral("重新连接当前桌面")); QAction* disconnect = menu.addAction(QStringLiteral("断开并返回桌面列表")); menu.addSeparator(); QAction* quit = menu.addAction(QStringLiteral("退出终端")); QAction* chosen = menu.exec(QCursor::pos()); if (chosen == settings) { if (fsm_.current() == State::kDesktopList || fsm_.current() == State::kConfig) { onEditDesktopRequested(QString()); } } else if (chosen == reconnect) { onMenuAction(1); } else if (chosen == disconnect) { onMenuAction(2); } else if (chosen == quit) { onMenuAction(3); } break; } } } } // namespace vdi