Files
vdi-cloud/terminal/kiosk-shell/src/main_window.cpp
T

847 lines
32 KiB
C++

#include "main_window.h"
#include <QApplication>
#include <QDateTime>
#include <QDebug>
#include <QEvent>
#include <QEventLoop>
#include <QFile>
#include <QKeyEvent>
#include <QMenu>
#include <QShortcut>
#include <QScreen>
#include <QShowEvent>
#include <QStackedWidget>
#include <QStatusBar>
#include <QTimer>
#include <QWidget>
#include <QWindow>
#ifdef Q_OS_UNIX
#include <X11/Xlib.h>
#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<void(bool)> 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<Display*>(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<MoonlightSession>();
}
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<Display*>(grabDisplay_);
const Window window = static_cast<Window>(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<Display*>(grabDisplay_);
const Window window = static_cast<Window>(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<Display*>(grabDisplay_),
static_cast<Window>(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<Display*>(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<Display*>(grabDisplay_);
const int result = XGrabKeyboard(display, static_cast<Window>(winId()), False,
GrabModeAsync, GrabModeAsync, CurrentTime);
if (result == GrabSuccess) {
XSetInputFocus(display, static_cast<Window>(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<QKeyEvent*>(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<DesktopInfo> 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