847 lines
32 KiB
C++
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
|