#include "main_window.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "desktop_list_page.h" #include "login_page.h" namespace vdi { namespace { // FreeRDP 像素格式 -> QImage::Format // 注意:FreeRDP 的像素格式值是计算宏(FREERDP_PIXEL_FORMAT), // 不能硬编码数值,必须用宏直接比较。 // FreeRDP 主缓冲是 4 字节/像素(BGRX / BGRA / RGBA), // QImage 没有对应的 4 字节 BGRA 格式,因此统一转成 RGB888 输出。 QImage::Format toQImageFormat(uint32_t freerdpFormat) { (void)freerdpFormat; return QImage::Format_RGB888; } // FreeRDP BGRX32 在当前 ARM64 小端目标上与 QImage::Format_RGB32 的内存布局一致。 QImage convertFrame(const uint8_t* data, uint32_t width, uint32_t height, uint32_t bytesPerLine, uint32_t freerdpFormat) { if (!data || width == 0 || height == 0) { return QImage(); } if (freerdpFormat == PIXEL_FORMAT_BGRX32 || freerdpFormat == PIXEL_FORMAT_BGRA32) { QImage out(static_cast(width), static_cast(height), QImage::Format_RGB32); const size_t rowBytes = static_cast(width) * 4; for (uint32_t y = 0; y < height; ++y) { std::memcpy(out.scanLine(static_cast(y)), data + static_cast(y) * bytesPerLine, rowBytes); } return out; } QImage out(static_cast(width), static_cast(height), QImage::Format_RGB888); for (uint32_t y = 0; y < height; ++y) { const uint8_t* src = data + static_cast(y) * bytesPerLine; uint8_t* dst = out.scanLine(static_cast(y)); for (uint32_t x = 0; x < width; ++x) { const uint8_t* p = src + static_cast(x) * 4; dst[0] = p[0]; dst[1] = p[1]; dst[2] = p[2]; dst += 3; } } return out; } constexpr int kReconnectDelayMs = 3000; constexpr int kMaxReconnectAttempts = 5; } // namespace MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { fsm_ = StateMachine([this](State from, State to, const QString& detail) { onStateChanged(from, to, detail); }); setupUi(); setupHotkey(); setupHeartbeat(); keyboardLayout_ = freerdp_keyboard_init(0); config_ = loadConfig(); // 开机永远进入登录页;不再根据本地配置自动连接 RDP, // 会话只能由用户在桌面列表选择后启动。 fsm_.transitionTo(State::kConfig, QStringLiteral("等待登录")); showLogin(); } MainWindow::~MainWindow() { stopSessions(); } void MainWindow::setupUi() { setWindowTitle(QStringLiteral("VDI 云终端")); setWindowState(Qt::WindowFullScreen); setStyleSheet(QStringLiteral("QMainWindow { background: #0f1420; }")); 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; }" "QLineEdit:focus { border: 1px solid #2f6feb; }" "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); connect(login, &LoginPage::quitRequested, this, &MainWindow::onQuitRequested); // 桌面列表页 auto* list = new DesktopListPage(this); desktopListPage_ = list; list->setStyleSheet( QStringLiteral("QWidget { background: #0f1420; color: #e6e9ef; }" "QPushButton { background: #232d44; border: 1px solid #2b3654; " "border-radius: 6px; color: #e6e9ef; }" "QPushButton:hover { background: #2b3654; }")); stack_->addWidget(list); connect(list, &DesktopListPage::desktopSelected, this, &MainWindow::onDesktopSelected); connect(list, &DesktopListPage::logoutRequested, this, &MainWindow::onLogoutRequested); // 会话视图 auto* sessionWidget = new QWidget(this); auto* sessionLayout = new QVBoxLayout(sessionWidget); sessionLayout->setContentsMargins(0, 0, 0, 0); sessionView_ = new QLabel(sessionWidget); sessionView_->setFocusPolicy(Qt::StrongFocus); sessionView_->setMouseTracking(true); sessionView_->installEventFilter(this); sessionView_->setAlignment(Qt::AlignCenter); sessionView_->setStyleSheet(QStringLiteral("background: #000000; color: #e6e9ef;")); sessionView_->setText(QStringLiteral("正在建立远程会话…")); sessionOverlay_ = new QLabel(sessionWidget); sessionOverlay_->setAlignment(Qt::AlignCenter); sessionOverlay_->setStyleSheet( QStringLiteral("background: rgba(15,20,32,0.82); color: #e6e9ef; " "font-size: 20px; padding: 40px;")); sessionOverlay_->setText(QStringLiteral("正在连接…")); sessionOverlay_->hide(); sessionLayout->addWidget(sessionView_); sessionLayout->addWidget(sessionOverlay_); stack_->addWidget(sessionWidget); // 状态条 statusBar_ = new QStatusBar(this); statusBar_->setStyleSheet(QStringLiteral("QStatusBar { background: #161d2e; color: #8a94a6; }")); setStatusBar(statusBar_); // RDP 重连定时器 reconnectTimer_ = new QTimer(this); reconnectTimer_->setSingleShot(true); connect(reconnectTimer_, &QTimer::timeout, this, [this]() { if (fsm_.current() == State::kReconnect && reconnectAttempts_ < kMaxReconnectAttempts) { ++reconnectAttempts_; if (activeTicket_.protocol == Protocol::kMoonlight) { startMoonlightSession(activeTicket_); } else { startRdpSession(activeTicket_); } } else if (fsm_.current() == State::kReconnect) { stopSessions(); fsm_.transitionTo(State::kDesktopList, QStringLiteral("重连失败,已返回桌面列表")); } }); frameRefreshTimer_ = new QTimer(this); frameRefreshTimer_->setInterval(33); connect(frameRefreshTimer_, &QTimer::timeout, this, &MainWindow::onFrameRefresh); frameRefreshTimer_->start(); } void MainWindow::setupHotkey() { // 全局热键 Ctrl+Alt+Shift+M 呼出管理菜单(方案 3.2 节) auto* shortcut = new QShortcut(QKeySequence(QStringLiteral("Ctrl+Alt+Shift+M")), this); shortcut->setContext(Qt::ApplicationShortcut); connect(shortcut, &QShortcut::activated, this, [this]() { onMenuAction(0); }); } void MainWindow::setupHeartbeat() { // 定期写心跳文件,供看门狗 Monitor 判断主进程是否假死 // 对应会议纪要要求的双进程架构 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())); file.close(); } }); 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)); switch (to) { case State::kConfig: showLogin(); break; case State::kAuthenticating: break; case State::kDesktopList: showDesktopList(); refreshDesktopList(); break; case State::kConnecting: if (stack_->currentWidget() != sessionView_->parentWidget()) { stack_->setCurrentWidget(sessionView_->parentWidget()); } sessionView_->setFocus(Qt::OtherFocusReason); sessionOverlay_->setText(QStringLiteral("正在连接 %1 …").arg(activeTicket_.endpoint)); sessionOverlay_->show(); break; case State::kSession: sessionView_->setFocus(Qt::OtherFocusReason); sessionOverlay_->hide(); break; case State::kReconnect: sessionOverlay_->setText(QStringLiteral("连接已断开,正在重连…")); sessionOverlay_->show(); reconnectTimer_->start(kReconnectDelayMs); break; case State::kError: stopSessions(); fsm_.transitionTo(State::kDesktopList, QStringLiteral("连接失败")); if (desktopListPage_) { desktopListPage_->setBusy(false); desktopListPage_->setStatus(QStringLiteral("连接失败:%1").arg(detail), true); } break; case State::kBoot: break; } } 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() { desktopService_.listDesktops([this](std::vector list, const QString& error) { if (error.isEmpty()) { desktops_ = list; } if (desktopListPage_) { desktopListPage_->setDesktops(desktops_); if (!error.isEmpty()) { desktopListPage_->setStatus(error, true); } } }); } 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 (!fsm_.canConnect()) { return; } desktopService_.createTicket(desktopId, [this](const ConnectionTicket& ticket, const QString& error) { if (!error.isEmpty()) { if (desktopListPage_) { desktopListPage_->setBusy(false); desktopListPage_->setStatus(error, true); } return; } activeTicket_ = ticket; reconnectAttempts_ = 0; if (ticket.protocol == Protocol::kMoonlight) { startMoonlightSession(ticket); } else { startRdpSession(ticket); } }); } void MainWindow::onLogoutRequested() { stopSessions(); desktopService_.logout(); currentUser_.clear(); fsm_.transitionTo(State::kConfig, QStringLiteral("已退出登录")); } void MainWindow::startRdpSession(const ConnectionTicket& ticket) { ++sessionEpoch_; { QMutexLocker locker(&frameMutex_); latestFrame_ = QImage(); frameEpoch_ = sessionEpoch_; frameWakePending_ = false; } if (fsm_.current() == State::kSession) { fsm_.transitionTo(State::kReconnect, QStringLiteral("手动重新连接")); } if (!fsm_.transitionTo(State::kConnecting, ticket.endpoint)) { if (fsm_.current() != State::kConnecting) { return; } } RdpOptions opts; opts.server = ticket.endpoint.toStdString(); opts.username = ticket.username.toStdString(); opts.password = ticket.password.toStdString(); opts.keyboardLayout = keyboardLayout_; opts.width = ticket.width; opts.height = ticket.height; opts.bpp = 32; opts.fullscreen = true; // 与 xfreerdp3 相同的 GFX 能力组合语义: // FreeRDP 3.32.1 不接受单个管道名,必须成组。 opts.gfx = "progressive|RFX|AVC420|AVC444"; opts.ignoreCertificate = true; // POC 阶段;正式产品必须做证书校验 RdpCallbacks cb; cb.onFrame = [this](const uint8_t* data, uint32_t width, uint32_t height, uint32_t stride, uint32_t format) { // 在 FreeRDP 线程中复制帧,不能把其内部缓冲区裸指针排队到 GUI 线程。 const QImage image = convertFrame(data, width, height, stride, format); if (image.isNull()) { return; } QMutexLocker locker(&frameMutex_); latestFrame_ = image; frameEpoch_ = sessionEpoch_; if (frameWakePending_) { return; } frameWakePending_ = true; locker.unlock(); QMetaObject::invokeMethod(this, &MainWindow::onFrameWakeup, Qt::QueuedConnection); }; cb.onState = [this](bool connected, const std::string& message) { QMetaObject::invokeMethod( this, [this, connected, message]() { onSessionState(connected, QString::fromStdString(message)); }, Qt::QueuedConnection); }; rdpSession_ = std::make_unique(); rdpSession_->start(opts, cb); } void MainWindow::startMoonlightSession(const ConnectionTicket& ticket) { if (!fsm_.transitionTo(State::kConnecting, ticket.endpoint)) { if (fsm_.current() != State::kConnecting) { return; } } // Moonlight 是外部全屏进程:Kiosk 画面停在"正在连接"覆盖层, // 串流窗口由 moonlight-qt 自行置顶显示。 if (stack_->currentWidget() != sessionView_->parentWidget()) { stack_->setCurrentWidget(sessionView_->parentWidget()); } sessionOverlay_->setText(QStringLiteral("正在连接 %1 …").arg(ticket.endpoint)); sessionOverlay_->show(); MoonlightSession::Options opts; opts.decoder = QStringLiteral("software"); // RK3528 无可用 V4L2 硬解设备 opts.videoCodec = QStringLiteral("H.264"); opts.audioOnHost = true; moonlightSession_ = std::make_unique(/*parent=*/nullptr); moonlightSession_->setOptions(opts); moonlightSession_->start(ticket, [this](bool running, const QString& message) { QMetaObject::invokeMethod( this, [this, running, message]() { onMoonlightState(running, message); }, Qt::QueuedConnection); }); } void MainWindow::onMoonlightState(bool running, const QString& message) { if (running) { // 进程已拉起;串流窗口是否成功由 moonlight 自身展示 updateStatusBar(QStringLiteral("SESSION %1").arg(message)); return; } // 进程退出:回桌面列表 moonlightSession_.reset(); stopSessions(); if (fsm_.current() == State::kConnecting || fsm_.current() == State::kSession) { fsm_.transitionTo(State::kDesktopList, message); } if (desktopListPage_) { desktopListPage_->setBusy(false); desktopListPage_->setStatus(message); } } void MainWindow::onSessionState(bool connected, const QString& message) { if (connected) { reconnectAttempts_ = 0; fsm_.transitionTo(State::kSession, message); } else { if (fsm_.current() == State::kConnecting) { // 首次连接失败:回桌面列表并提示 stopSessions(); fsm_.transitionTo(State::kDesktopList, message); if (desktopListPage_) { desktopListPage_->setBusy(false); desktopListPage_->setStatus(QStringLiteral("连接失败:%1").arg(message), true); } } else if (fsm_.current() == State::kSession) { fsm_.transitionTo(State::kReconnect, message); } } } void MainWindow::onFrameWakeup() { QMutexLocker locker(&frameMutex_); frameWakePending_ = false; } void MainWindow::onFrameRefresh() { QImage image; uint64_t epoch = 0; { QMutexLocker locker(&frameMutex_); image = latestFrame_; epoch = frameEpoch_; latestFrame_ = QImage(); frameWakePending_ = false; } if (epoch == sessionEpoch_) { onFrameImage(image); } } void MainWindow::onFrameImage(const QImage& image) { if (image.isNull()) { return; } lastFrame_ = image; if (sessionView_) { const QSize targetSize = sessionView_->size(); if (lastFrame_.size() == targetSize) { sessionView_->setPixmap(QPixmap::fromImage(lastFrame_)); } else { sessionView_->setPixmap(QPixmap::fromImage(lastFrame_).scaled( targetSize, Qt::KeepAspectRatio, Qt::FastTransformation)); } } } void MainWindow::onQuitRequested() { stopSessions(); QApplication::quit(); } void MainWindow::stopSessions() { if (rdpSession_) { rdpSession_->stop(); rdpSession_.reset(); } if (moonlightSession_) { moonlightSession_->stop(); moonlightSession_.reset(); } QMutexLocker locker(&frameMutex_); latestFrame_ = QImage(); lastFrame_ = QImage(); frameWakePending_ = false; } void MainWindow::onMenuAction(int action) { switch (action) { case 1: // 重新连接当前桌面 if (fsm_.canConnect() && !activeTicket_.desktopId.isEmpty()) { reconnectAttempts_ = 0; stopSessions(); if (activeTicket_.protocol == Protocol::kMoonlight) { startMoonlightSession(activeTicket_); } else { startRdpSession(activeTicket_); } } break; case 2: // 断开回桌面列表 stopSessions(); if (fsm_.current() != State::kConfig) { fsm_.transitionTo(State::kDesktopList, "已断开"); } break; case 3: // 退出终端 onQuitRequested(); break; default: { QMenu menu(this); 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 == reconnect) { onMenuAction(1); } else if (chosen == disconnect) { onMenuAction(2); } else if (chosen == quit) { onMenuAction(3); } if (stack_->currentWidget() == sessionView_->parentWidget()) { sessionView_->setFocus(Qt::OtherFocusReason); } break; } } } bool MainWindow::eventFilter(QObject* watched, QEvent* event) { if (watched == sessionView_ && rdpSession_) { if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) { auto* keyEvent = static_cast(event); return forwardKeyEvent(keyEvent, event->type() == QEvent::KeyPress); } if (event->type() == QEvent::MouseMove || event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease) { return forwardMouseEvent(static_cast(event)); } if (event->type() == QEvent::Wheel) { return forwardMouseEvent(event); } } return QMainWindow::eventFilter(watched, event); } bool MainWindow::forwardKeyEvent(QKeyEvent* event, bool pressed) { if (!rdpSession_) { return false; } const DWORD rdpScanCode = freerdp_keyboard_get_rdp_scancode_from_x11_keycode(event->nativeScanCode()); if (rdpScanCode == RDP_SCANCODE_UNKNOWN) { return false; } return rdpSession_->sendKeyEvent(pressed, event->isAutoRepeat(), rdpScanCode); } bool MainWindow::forwardMouseEvent(QEvent* event) { if (!rdpSession_) { return false; } uint16_t flags = 0; QPoint pos = lastMousePosition_; if (event->type() == QEvent::MouseMove) { auto* mouse = static_cast(event); pos = mouse->pos(); flags = PTR_FLAGS_MOVE; } else if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease) { auto* mouse = static_cast(event); pos = mouse->pos(); if (mouse->button() == Qt::LeftButton) flags |= PTR_FLAGS_BUTTON1; if (mouse->button() == Qt::RightButton) flags |= PTR_FLAGS_BUTTON2; if (mouse->button() == Qt::MiddleButton) flags |= PTR_FLAGS_BUTTON3; if (event->type() == QEvent::MouseButtonPress) flags |= PTR_FLAGS_DOWN; } else if (event->type() == QEvent::Wheel) { auto* wheel = static_cast(event); pos = wheel->position().toPoint(); flags = PTR_FLAGS_WHEEL; if (wheel->angleDelta().y() < 0) flags |= PTR_FLAGS_WHEEL_NEGATIVE; } lastMousePosition_ = pos; if (lastFrame_.isNull() || !sessionView_) { return false; } const QSize sourceSize = lastFrame_.size(); const QSize viewSize = sessionView_->size(); const QSize displayedSize = sourceSize.scaled(viewSize, Qt::KeepAspectRatio); const int offsetX = (viewSize.width() - displayedSize.width()) / 2; const int offsetY = (viewSize.height() - displayedSize.height()) / 2; if (pos.x() < offsetX || pos.y() < offsetY || pos.x() >= offsetX + displayedSize.width() || pos.y() >= offsetY + displayedSize.height()) { return true; } const int remoteX = qBound(0, (pos.x() - offsetX) * sourceSize.width() / displayedSize.width(), sourceSize.width() - 1); const int remoteY = qBound(0, (pos.y() - offsetY) * sourceSize.height() / displayedSize.height(), sourceSize.height() - 1); return rdpSession_->sendMouseEvent(flags, static_cast(remoteX), static_cast(remoteY)); } } // namespace vdi