功能:实现桌面选择流程并接入 Moonlight 会话

变更详情:feat(kiosk-shell): 登录/桌面列表产品流程 + Moonlight 会话 + 看门狗进程组修复

- 登录页改为用户认证(本地演示认证,账号密码非空通过)
- 新增桌面列表页:Windows/Moonlight 可连接,Ubuntu/麒麟待分配
- 新增 DesktopService 适配层(登录/桌面列表/连接票据,后续接 API)
- 新增 MoonlightSession:QProcess 参数数组直启 moonlight-qt,退出回列表
- 状态机扩展 AUTHENTICATING / DESKTOP_LIST
- 移除开机自动 RDP,收敛为 monitor -> kiosk-shell 单一自启链
- Monitor 按进程组结束整棵进程树,修复孤儿串流进程
- vdi-moonlight 启动器新增 vsync/frame-pacing/overlay 低延迟开关
This commit is contained in:
songyiming committed 2026-10-28 20:48:12 +08:00
1 parent f74d3ceadf
commit 61127fb714
25 files changed
+1982 -246

No files matched your search

@@ -0,0 +1,138 @@
#include "moonlight_session.h"
#include <QDate>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QTextStream>
namespace vdi {
MoonlightSession::MoonlightSession(QObject* parent) : QObject(parent) {}
MoonlightSession::~MoonlightSession() {
stop();
}
void MoonlightSession::setOptions(const Options& options) {
options_ = options;
}
bool MoonlightSession::isRunning() const {
return process_ && process_->state() != QProcess::NotRunning;
}
bool MoonlightSession::start(const ConnectionTicket& ticket, StateCallback callback) {
if (isRunning()) {
return false;
}
callback_ = std::move(callback);
// moonlight-qt 需要 X11 会话环境;由 kiosk-shell 继承当前 DISPLAY/XAUTHORITY
process_ = new QProcess(this);
process_->setProgram(QStringLiteral("moonlight-qt"));
process_->setProcessChannelMode(QProcess::MergedChannels);
QStringList args;
args << QStringLiteral("stream")
<< ticket.endpoint
<< ticket.app
<< QStringLiteral("--resolution")
<< QStringLiteral("%1x%2").arg(ticket.width).arg(ticket.height)
<< QStringLiteral("--fps") << QString::number(ticket.fps)
<< QStringLiteral("--bitrate") << QString::number(20000)
<< QStringLiteral("--display-mode") << QStringLiteral("fullscreen")
<< QStringLiteral("--video-codec") << options_.videoCodec
<< QStringLiteral("--video-decoder") << options_.decoder
<< QStringLiteral("--absolute-mouse")
<< QStringLiteral("--keep-awake");
if (options_.vsync) {
args << QStringLiteral("--vsync");
} else {
args << QStringLiteral("--no-vsync");
}
if (options_.framePacing) {
args << QStringLiteral("--frame-pacing");
} else {
args << QStringLiteral("--no-frame-pacing");
}
if (options_.performanceOverlay) {
args << QStringLiteral("--performance-overlay");
}
if (options_.audioOnHost) {
args << QStringLiteral("--audio-on-host");
} else {
args << QStringLiteral("--no-audio-on-host");
}
process_->setArguments(args);
// 日志追加到 moonlight-yyyyMMdd.log,便于现场排查
const QString logDir =
QDir::homePath() + QStringLiteral("/vdi-terminal/logs");
QDir().mkpath(logDir);
const QString logPath = logDir + QStringLiteral("/moonlight-%1.log")
.arg(QDate::currentDate().toString(QStringLiteral("yyyyMMdd")));
connect(process_, &QProcess::finished, this, &MoonlightSession::onProcessFinished);
connect(process_, &QProcess::errorOccurred, this, &MoonlightSession::onProcessError);
connect(process_, &QProcess::readyReadStandardOutput, this, [this, logPath]() {
QFile log(logPath);
if (log.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
QTextStream(&log) << QString::fromUtf8(process_->readAllStandardOutput());
}
});
if (callback_) {
callback_(true, QStringLiteral("串流进程已启动"));
}
process_->start();
return true;
}
void MoonlightSession::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) {
QString message;
if (exitStatus == QProcess::CrashExit) {
message = QStringLiteral("串流异常退出 (code=%1)").arg(exitCode);
} else {
message = QStringLiteral("串流已结束 (code=%1)").arg(exitCode);
}
process_->deleteLater();
process_ = nullptr;
if (callback_) {
callback_(false, message);
}
}
void MoonlightSession::onProcessError(QProcess::ProcessError error) {
QString message;
switch (error) {
case QProcess::FailedToStart:
message = QStringLiteral("无法启动 moonlight-qt,请检查安装");
break;
case QProcess::Crashed:
return; // 由 finished(CrashExit) 统一上报
default:
message = QStringLiteral("串流进程错误 (%1)").arg(static_cast<int>(error));
break;
}
if (callback_) {
callback_(false, message);
}
}
void MoonlightSession::stop() {
if (!process_) {
return;
}
QProcess* proc = process_;
process_ = nullptr; // 先断开自身引用,避免 finished 回调再触发状态迁移
disconnect(proc, nullptr, this, nullptr);
proc->terminate();
if (!proc->waitForFinished(2000)) {
proc->kill();
proc->waitForFinished(1000);
}
proc->deleteLater();
}
} // namespace vdi