重构:完善串流就绪检测并补充 GPU 联调记录
变更详情:kiosk 会话就绪检测重构(Moonlight 输出解析器/事件化回调/单元测试);GPU 直通实操记录(vfio Subsystem 注入、sysfs vBIOS、7zr+pnputil 安装、566.36 驱动、Sunshine 配对断链修复)
This commit is contained in:
1 parent
b08486046b
commit
13f645a503
14 files changed
+743
-225
No files matched your search
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QMenu>
|
||||
#include <QShortcut>
|
||||
@@ -17,12 +18,9 @@
|
||||
namespace vdi {
|
||||
namespace {
|
||||
|
||||
// 首帧检测:moonlight-qt 是独立进程,kiosk 拿不到解码回调。
|
||||
// 软解 1080p60 的 CPU 占用远高于启动握手期,因此轮询进程 CPU 时间,
|
||||
// 累计超过阈值即认为串流已出画;超时则强制淡出兜底(时钟频率 100 tick/秒)。
|
||||
constexpr int kFirstFramePollMs = 500;
|
||||
constexpr qint64 kFirstFrameCpuTicks = 120;
|
||||
constexpr qint64 kFirstFrameTimeoutMs = 15000;
|
||||
constexpr int kTicketTimeoutMs = 10000;
|
||||
constexpr int kProcessStartTimeoutMs = 5000;
|
||||
constexpr int kStreamReadyTimeoutMs = 30000;
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -35,10 +33,6 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
setupHeartbeat();
|
||||
|
||||
config_ = loadConfig();
|
||||
|
||||
// 开机直进桌面列表:当前无正式账号体系,设备身份(SN)即用户,自动登录。
|
||||
// 登录页代码保留——接入管理平台认证后,由 DesktopService 决定是否回到登录流程;
|
||||
// 会话只能由用户在桌面列表选择后启动。
|
||||
fsm_.transitionTo(State::kConfig, QStringLiteral("就绪"));
|
||||
const QString deviceUser = QString::fromStdString(config_.sn).trimmed();
|
||||
desktopService_.login(deviceUser.isEmpty() ? QStringLiteral("terminal") : deviceUser,
|
||||
@@ -64,7 +58,6 @@ void MainWindow::setupUi() {
|
||||
stack_->setStyleSheet(QStringLiteral("background: #0f1420;"));
|
||||
setCentralWidget(stack_);
|
||||
|
||||
// 登录页
|
||||
auto* login = new LoginPage(config_, this);
|
||||
loginPage_ = login;
|
||||
login->setStyleSheet(
|
||||
@@ -76,10 +69,8 @@ void MainWindow::setupUi() {
|
||||
"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;
|
||||
list->setStyleSheet(
|
||||
@@ -88,35 +79,43 @@ void MainWindow::setupUi() {
|
||||
"border-radius: 6px; color: #e6e9ef; }"
|
||||
"QPushButton:hover { background: #2b3654; }"));
|
||||
stack_->addWidget(list);
|
||||
|
||||
connect(list, &DesktopListPage::desktopSelected, this, &MainWindow::onDesktopSelected);
|
||||
connect(list, &DesktopListPage::editRequested, this, &MainWindow::onEditDesktopRequested);
|
||||
|
||||
// 过场层:独立置顶窗口,盖住 moonlight-qt 启动期的小窗口闪烁与分辨率切换,
|
||||
// 首帧检测到后淡出。不属于 stack_(串流期间整个 kiosk 都被 moonlight 覆盖)。
|
||||
overlay_ = new TransitionOverlay(this);
|
||||
|
||||
// 状态条
|
||||
statusBar_ = new QStatusBar(this);
|
||||
statusBar_->setStyleSheet(QStringLiteral("QStatusBar { background: #161d2e; color: #8a94a6; }"));
|
||||
setStatusBar(statusBar_);
|
||||
|
||||
// 串流首帧检测
|
||||
firstFrameTimer_ = new QTimer(this);
|
||||
firstFrameTimer_->setInterval(kFirstFramePollMs);
|
||||
connect(firstFrameTimer_, &QTimer::timeout, this, &MainWindow::watchFirstFrame);
|
||||
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() {
|
||||
// 全局热键 Ctrl+Alt+Shift+M 呼出管理菜单(方案 3.2 节)
|
||||
auto* menuShortcut = new QShortcut(QKeySequence(QStringLiteral("Ctrl+Alt+Shift+M")), this);
|
||||
menuShortcut->setContext(Qt::ApplicationShortcut);
|
||||
connect(menuShortcut, &QShortcut::activated, this, [this]() { onMenuAction(0); });
|
||||
|
||||
// Ctrl+Alt+Shift+Q 退出串流回桌面列表。
|
||||
// 主路径是 moonlight-qt 自带的同一快捷键(串流独占键盘时由它退出,
|
||||
// 进程退出后 onMoonlightState 自动回列表);这里兜底处理
|
||||
// 串流尚未建立、假死或焦点仍在 kiosk 的情况。
|
||||
auto* quitShortcut = new QShortcut(QKeySequence(QStringLiteral("Ctrl+Alt+Shift+Q")), this);
|
||||
quitShortcut->setContext(Qt::ApplicationShortcut);
|
||||
connect(quitShortcut, &QShortcut::activated, this, [this]() {
|
||||
@@ -133,8 +132,6 @@ void MainWindow::setupHotkey() {
|
||||
}
|
||||
|
||||
void MainWindow::setupHeartbeat() {
|
||||
// 定期写心跳文件,供看门狗 Monitor 判断主进程是否假死
|
||||
// 对应会议纪要要求的双进程架构
|
||||
const QString path = QStringLiteral("/tmp/vdi-kiosk.heartbeat");
|
||||
heartbeatTimer_ = new QTimer(this);
|
||||
heartbeatTimer_->setInterval(5000);
|
||||
@@ -142,7 +139,6 @@ void MainWindow::setupHeartbeat() {
|
||||
QFile file(path);
|
||||
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
file.write(QByteArray::number(QDateTime::currentSecsSinceEpoch()));
|
||||
file.close();
|
||||
}
|
||||
});
|
||||
heartbeatTimer_->start();
|
||||
@@ -164,13 +160,9 @@ void MainWindow::onStateChanged(State from, State to, const QString& detail) {
|
||||
refreshDesktopList();
|
||||
break;
|
||||
case State::kConnecting:
|
||||
firstFrameClock_.restart();
|
||||
overlay_->start(QStringLiteral("正在进入云桌面"));
|
||||
firstFrameTimer_->start();
|
||||
break;
|
||||
case State::kSession:
|
||||
// 串流画面由 moonlight-qt 全屏呈现;过场层由首帧检测负责淡出
|
||||
break;
|
||||
case State::kReconnect:
|
||||
break;
|
||||
case State::kError:
|
||||
@@ -239,25 +231,44 @@ void MainWindow::onLoginRequested(const QString& username, const QString& passwo
|
||||
|
||||
void MainWindow::onDesktopSelected(const QString& desktopId) {
|
||||
if (!fsm_.canConnect()) {
|
||||
if (desktopListPage_) {
|
||||
desktopListPage_->setBusy(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
desktopService_.createTicket(desktopId, [this](const ConnectionTicket& ticket,
|
||||
const QString& error) {
|
||||
if (!error.isEmpty()) {
|
||||
if (desktopListPage_) {
|
||||
desktopListPage_->setBusy(false);
|
||||
desktopListPage_->setStatus(error, true);
|
||||
}
|
||||
|
||||
const quint64 attempt = ++connectionAttempt_;
|
||||
connectingDesktopId_ = desktopId;
|
||||
connectionClock_.restart();
|
||||
phaseClock_.restart();
|
||||
connectionPhase_ = ConnectionPhase::kTicket;
|
||||
logConnectionPhase(attempt, QStringLiteral("ticket"), QStringLiteral("started"));
|
||||
|
||||
if (!fsm_.transitionTo(State::kConnecting, QStringLiteral("正在获取连接信息"))) {
|
||||
failConnection(attempt, QStringLiteral("当前状态无法发起连接"));
|
||||
return;
|
||||
}
|
||||
armConnectionTimeout(attempt, ConnectionPhase::kTicket, kTicketTimeoutMs);
|
||||
|
||||
desktopService_.createTicket(desktopId, [this, attempt](const ConnectionTicket& ticket,
|
||||
const QString& error) {
|
||||
if (attempt != connectionAttempt_ || connectionPhase_ != ConnectionPhase::kTicket) {
|
||||
return;
|
||||
}
|
||||
connectionTimer_->stop();
|
||||
if (!error.isEmpty()) {
|
||||
failConnection(attempt, error);
|
||||
return;
|
||||
}
|
||||
logConnectionPhase(attempt, QStringLiteral("ticket"), QStringLiteral("ready"));
|
||||
activeTicket_ = ticket;
|
||||
startMoonlightSession(ticket);
|
||||
if (!startMoonlightSession(ticket, attempt)) {
|
||||
failConnection(attempt, QStringLiteral("已有串流进程正在运行"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::onEditDesktopRequested(const QString& desktopId) {
|
||||
// 桌面连接设置:编辑地址/应用名/分辨率/帧率,保存后立即刷新列表。
|
||||
// 现场环境地址不同或走 VPN 隧道时,在这里改地址即可,无需改代码。
|
||||
DesktopSettingsDialog dialog(desktops_, desktopId, this);
|
||||
dialog.setStyleSheet(
|
||||
QStringLiteral("QWidget { background: #0f1420; color: #e6e9ef; }"
|
||||
@@ -277,71 +288,126 @@ void MainWindow::onEditDesktopRequested(const QString& desktopId) {
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::startMoonlightSession(const ConnectionTicket& ticket) {
|
||||
if (!fsm_.transitionTo(State::kConnecting, ticket.endpoint)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool MainWindow::startMoonlightSession(const ConnectionTicket& ticket, quint64 attempt) {
|
||||
MoonlightSession::Options opts;
|
||||
opts.decoder = QStringLiteral("software"); // RK3528 无可用 V4L2 硬解设备
|
||||
opts.decoder = QStringLiteral("software");
|
||||
opts.videoCodec = QStringLiteral("H.264");
|
||||
opts.audioOnHost = true;
|
||||
|
||||
moonlightSession_ = std::make_unique<MoonlightSession>(/*parent=*/nullptr);
|
||||
moonlightSession_->setOptions(opts);
|
||||
moonlightSession_->start(ticket, [this](bool running, const QString& message) {
|
||||
|
||||
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, running, message]() { onMoonlightState(running, message); },
|
||||
this, [this, attempt, event]() { onMoonlightEvent(attempt, event); },
|
||||
Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::onMoonlightState(bool running, const QString& message) {
|
||||
if (running) {
|
||||
// 进程已拉起:转入会话态;过场层等首帧检测淡出
|
||||
fsm_.transitionTo(State::kSession, message);
|
||||
updateStatusBar(QStringLiteral("SESSION %1").arg(message));
|
||||
void MainWindow::onMoonlightEvent(quint64 attempt, const MoonlightSession::Event& event) {
|
||||
if (attempt != connectionAttempt_) {
|
||||
return;
|
||||
}
|
||||
// 进程退出:回桌面列表
|
||||
moonlightSession_.reset();
|
||||
stopSessions();
|
||||
if (fsm_.current() == State::kConnecting || fsm_.current() == State::kSession) {
|
||||
fsm_.transitionTo(State::kDesktopList, message);
|
||||
|
||||
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: {
|
||||
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;
|
||||
}
|
||||
if (!moonlightSession_->activateStreamWindow()) {
|
||||
QTimer::singleShot(100, this, [this, attempt]() { waitForMoonlightWindow(attempt); });
|
||||
return;
|
||||
}
|
||||
|
||||
connectionTimer_->stop();
|
||||
logConnectionPhase(attempt, QStringLiteral("stream_ready"), QStringLiteral("ready"));
|
||||
connectionPhase_ = ConnectionPhase::kSession;
|
||||
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();
|
||||
}
|
||||
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(message);
|
||||
desktopListPage_->setStatus(QStringLiteral("连接失败:%1").arg(message), true);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::watchFirstFrame() {
|
||||
if (!moonlightSession_ || !moonlightSession_->isRunning()) {
|
||||
return; // 进程退出路径由 onMoonlightState 收尾
|
||||
}
|
||||
qint64 cpuTicks = -1;
|
||||
// /proc/<pid>/stat 的 comm 字段可含空格,从最后一个 ')' 后取字段:
|
||||
// 第 0 个是 state(整行第 3 字段),utime/stime 是第 11/12 个。
|
||||
QFile statFile(QStringLiteral("/proc/%1/stat").arg(moonlightSession_->processId()));
|
||||
if (statFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
const QString content = QString::fromLatin1(statFile.readAll());
|
||||
const QStringList fields =
|
||||
content.mid(content.lastIndexOf(')') + 1).split(' ', Qt::SkipEmptyParts);
|
||||
if (fields.size() > 12) {
|
||||
cpuTicks = fields.value(11).toLongLong() + fields.value(12).toLongLong();
|
||||
}
|
||||
}
|
||||
if (cpuTicks >= kFirstFrameCpuTicks || firstFrameClock_.elapsed() >= kFirstFrameTimeoutMs) {
|
||||
onFirstFrameDetected();
|
||||
void MainWindow::armConnectionTimeout(quint64 attempt, ConnectionPhase phase, int timeoutMs) {
|
||||
if (attempt != connectionAttempt_) {
|
||||
return;
|
||||
}
|
||||
connectionPhase_ = phase;
|
||||
connectionTimer_->start(timeoutMs);
|
||||
}
|
||||
|
||||
void MainWindow::onFirstFrameDetected() {
|
||||
firstFrameTimer_->stop();
|
||||
if (overlay_) {
|
||||
overlay_->onFirstFrame();
|
||||
}
|
||||
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() {
|
||||
@@ -350,12 +416,17 @@ void MainWindow::onQuitRequested() {
|
||||
}
|
||||
|
||||
void MainWindow::stopSessions() {
|
||||
if (firstFrameTimer_) {
|
||||
firstFrameTimer_->stop();
|
||||
stopSessions(true);
|
||||
}
|
||||
|
||||
void MainWindow::stopSessions(bool invalidateAttempt) {
|
||||
connectionTimer_->stop();
|
||||
if (invalidateAttempt) {
|
||||
++connectionAttempt_;
|
||||
}
|
||||
connectionPhase_ = ConnectionPhase::kIdle;
|
||||
if (moonlightSession_) {
|
||||
moonlightSession_->stop();
|
||||
moonlightSession_.reset();
|
||||
}
|
||||
if (overlay_) {
|
||||
overlay_->finish();
|
||||
@@ -364,19 +435,24 @@ void MainWindow::stopSessions() {
|
||||
|
||||
void MainWindow::onMenuAction(int action) {
|
||||
switch (action) {
|
||||
case 1: // 重新连接当前桌面
|
||||
case 1:
|
||||
if (fsm_.canConnect() && !activeTicket_.desktopId.isEmpty()) {
|
||||
stopSessions();
|
||||
startMoonlightSession(activeTicket_);
|
||||
const quint64 attempt = ++connectionAttempt_;
|
||||
connectingDesktopId_ = activeTicket_.desktopId;
|
||||
connectionClock_.restart();
|
||||
if (fsm_.transitionTo(State::kConnecting, activeTicket_.endpoint) &&
|
||||
!startMoonlightSession(activeTicket_, attempt)) {
|
||||
failConnection(attempt, QStringLiteral("已有串流进程正在运行"));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 2: // 断开回桌面列表
|
||||
case 2:
|
||||
stopSessions();
|
||||
if (fsm_.current() != State::kConfig) {
|
||||
fsm_.transitionTo(State::kDesktopList, "已断开");
|
||||
fsm_.transitionTo(State::kDesktopList, QStringLiteral("已断开"));
|
||||
}
|
||||
break;
|
||||
case 3: // 退出终端(维护功能,只保留在管理菜单)
|
||||
case 3:
|
||||
onQuitRequested();
|
||||
break;
|
||||
default: {
|
||||
|
||||
Reference in new issue
Block a user