重构:完善串流就绪检测并补充 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
@@ -1,17 +1,104 @@
|
||||
#include "moonlight_session.h"
|
||||
|
||||
#include <QDate>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QTextStream>
|
||||
#include <QTimer>
|
||||
|
||||
namespace vdi {
|
||||
namespace {
|
||||
|
||||
MoonlightSession::MoonlightSession(QObject* parent) : QObject(parent) {}
|
||||
constexpr int kReadyStableMs = 750;
|
||||
constexpr int kTerminateGraceMs = 2000;
|
||||
|
||||
bool isTestDecodeLine(const QByteArray& line) {
|
||||
return line.toLower().contains("test decode successful");
|
||||
}
|
||||
|
||||
bool isRendererLine(const QByteArray& line) {
|
||||
const QByteArray lower = line.toLower();
|
||||
return lower.contains("renderer '") && lower.contains(" chosen");
|
||||
}
|
||||
|
||||
bool isTerminalErrorLine(const QByteArray& line) {
|
||||
const QByteArray lower = line.toLower();
|
||||
if (lower.contains("会话已终止")) {
|
||||
return true;
|
||||
}
|
||||
const QByteArray marker("connection terminated:");
|
||||
const int markerPos = lower.indexOf(marker);
|
||||
if (markerPos < 0) {
|
||||
return false;
|
||||
}
|
||||
bool ok = false;
|
||||
const int code = lower.mid(markerPos + marker.size()).trimmed().toInt(&ok);
|
||||
return !ok || code != 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void MoonlightOutputParser::reset() {
|
||||
buffer_.clear();
|
||||
hasTestDecode_ = false;
|
||||
hasRenderer_ = false;
|
||||
hasTerminalError_ = false;
|
||||
}
|
||||
|
||||
void MoonlightOutputParser::append(const QByteArray& output) {
|
||||
buffer_.append(output);
|
||||
int newline = -1;
|
||||
while ((newline = buffer_.indexOf('\n')) >= 0) {
|
||||
const QByteArray line = buffer_.left(newline).trimmed();
|
||||
buffer_.remove(0, newline + 1);
|
||||
hasTestDecode_ = hasTestDecode_ || isTestDecodeLine(line);
|
||||
hasRenderer_ = hasRenderer_ || isRendererLine(line);
|
||||
hasTerminalError_ = hasTerminalError_ || isTerminalErrorLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
bool MoonlightOutputParser::isReady() const {
|
||||
return hasTestDecode_ && hasRenderer_ && !hasTerminalError_;
|
||||
}
|
||||
|
||||
MoonlightSession::MoonlightSession(QObject* parent) : QObject(parent) {
|
||||
process_ = new QProcess(this);
|
||||
process_->setProcessChannelMode(QProcess::MergedChannels);
|
||||
connect(process_, &QProcess::started, this, &MoonlightSession::onProcessStarted);
|
||||
connect(process_,
|
||||
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this,
|
||||
&MoonlightSession::onProcessFinished);
|
||||
connect(process_, &QProcess::errorOccurred, this, &MoonlightSession::onProcessError);
|
||||
connect(process_, &QProcess::readyReadStandardOutput, this,
|
||||
&MoonlightSession::onProcessOutput);
|
||||
|
||||
readyTimer_ = new QTimer(this);
|
||||
readyTimer_->setSingleShot(true);
|
||||
connect(readyTimer_, &QTimer::timeout, this, [this]() {
|
||||
if (!readyEventDelivered_ && !terminalEventDelivered_ && isRunning()) {
|
||||
readyEventDelivered_ = true;
|
||||
emitEvent(EventType::kStreamReady, QStringLiteral("串流画面已就绪"));
|
||||
}
|
||||
});
|
||||
|
||||
killTimer_ = new QTimer(this);
|
||||
killTimer_->setSingleShot(true);
|
||||
connect(killTimer_, &QTimer::timeout, this, [this]() {
|
||||
if (isRunning()) {
|
||||
process_->kill();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
MoonlightSession::~MoonlightSession() {
|
||||
stop();
|
||||
callback_ = nullptr;
|
||||
readyTimer_->stop();
|
||||
killTimer_->stop();
|
||||
if (isRunning()) {
|
||||
process_->kill();
|
||||
process_->waitForFinished(1000);
|
||||
}
|
||||
}
|
||||
|
||||
void MoonlightSession::setOptions(const Options& options) {
|
||||
@@ -19,23 +106,55 @@ void MoonlightSession::setOptions(const Options& options) {
|
||||
}
|
||||
|
||||
bool MoonlightSession::isRunning() const {
|
||||
return process_ && process_->state() != QProcess::NotRunning;
|
||||
return process_->state() != QProcess::NotRunning;
|
||||
}
|
||||
|
||||
qint64 MoonlightSession::processId() const {
|
||||
return isRunning() ? process_->processId() : 0;
|
||||
}
|
||||
|
||||
bool MoonlightSession::start(const ConnectionTicket& ticket, StateCallback callback) {
|
||||
bool MoonlightSession::activateStreamWindow() {
|
||||
if (!isRunning()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QProcess finder;
|
||||
finder.start(QStringLiteral("xdotool"),
|
||||
{QStringLiteral("search"), QStringLiteral("--onlyvisible"),
|
||||
QStringLiteral("--pid"), QString::number(process_->processId()),
|
||||
QStringLiteral("--class"), QStringLiteral("com.moonlight_stream.Moonlight")});
|
||||
if (!finder.waitForFinished(500) || finder.exitCode() != 0) {
|
||||
return false;
|
||||
}
|
||||
const QString windowId = QString::fromUtf8(finder.readAllStandardOutput())
|
||||
.split(QLatin1Char('\n'), Qt::SkipEmptyParts)
|
||||
.value(0).trimmed();
|
||||
if (windowId.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return QProcess::execute(QStringLiteral("xdotool"),
|
||||
{QStringLiteral("windowactivate"), QStringLiteral("--sync"),
|
||||
windowId}) == 0;
|
||||
}
|
||||
|
||||
bool MoonlightSession::start(const ConnectionTicket& ticket, EventCallback callback) {
|
||||
if (isRunning()) {
|
||||
return false;
|
||||
}
|
||||
callback_ = std::move(callback);
|
||||
|
||||
// moonlight-qt 需要 X11 会话环境;由 kiosk-shell 继承当前 DISPLAY/XAUTHORITY
|
||||
process_ = new QProcess(this);
|
||||
callback_ = std::move(callback);
|
||||
stopRequested_ = false;
|
||||
terminalEventDelivered_ = false;
|
||||
readyEventDelivered_ = false;
|
||||
outputParser_.reset();
|
||||
readyTimer_->stop();
|
||||
killTimer_->stop();
|
||||
|
||||
process_->setProgram(QStringLiteral("moonlight-qt"));
|
||||
process_->setProcessChannelMode(QProcess::MergedChannels);
|
||||
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
|
||||
environment.insert(QStringLiteral("GL_IS_SLOW"), QStringLiteral("0"));
|
||||
process_->setProcessEnvironment(environment);
|
||||
|
||||
QStringList args;
|
||||
args << QStringLiteral("stream")
|
||||
@@ -50,96 +169,109 @@ bool MoonlightSession::start(const ConnectionTicket& ticket, StateCallback callb
|
||||
<< 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");
|
||||
}
|
||||
args << (options_.vsync ? QStringLiteral("--vsync") : QStringLiteral("--no-vsync"));
|
||||
args << (options_.framePacing ? QStringLiteral("--frame-pacing")
|
||||
: 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");
|
||||
}
|
||||
args << (options_.audioOnHost ? QStringLiteral("--audio-on-host")
|
||||
: QStringLiteral("--no-audio-on-host"));
|
||||
process_->setArguments(args);
|
||||
|
||||
// 日志追加到 moonlight-yyyyMMdd.log,便于现场排查
|
||||
const QString logDir =
|
||||
QDir::homePath() + QStringLiteral("/vdi-terminal/logs");
|
||||
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")));
|
||||
logPath_ = logDir + QStringLiteral("/moonlight-%1.log")
|
||||
.arg(QDate::currentDate().toString(QStringLiteral("yyyyMMdd")));
|
||||
|
||||
// QProcess::finished 在 Qt5 里有两个重载((int) 已废弃),必须用 QOverload 消歧
|
||||
connect(process_,
|
||||
QOverload<int, QProcess::ExitStatus>::of(&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::onProcessStarted() {
|
||||
emitEvent(EventType::kStarted, QStringLiteral("串流进程已启动"));
|
||||
}
|
||||
|
||||
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);
|
||||
readyTimer_->stop();
|
||||
killTimer_->stop();
|
||||
|
||||
if (stopRequested_) {
|
||||
emitTerminalEvent(EventType::kFinished, QStringLiteral("串流已结束"));
|
||||
return;
|
||||
}
|
||||
|
||||
const QString message = exitStatus == QProcess::CrashExit
|
||||
? QStringLiteral("串流异常退出 (code=%1)").arg(exitCode)
|
||||
: QStringLiteral("串流已结束 (code=%1)").arg(exitCode);
|
||||
emitTerminalEvent(exitStatus == QProcess::CrashExit ? EventType::kFailed
|
||||
: EventType::kFinished,
|
||||
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 (error == QProcess::Crashed) {
|
||||
return;
|
||||
}
|
||||
if (callback_) {
|
||||
callback_(false, message);
|
||||
|
||||
const QString message = error == QProcess::FailedToStart
|
||||
? QStringLiteral("无法启动 moonlight-qt,请检查安装")
|
||||
: QStringLiteral("串流进程错误 (%1)").arg(static_cast<int>(error));
|
||||
emitTerminalEvent(EventType::kFailed, message);
|
||||
}
|
||||
|
||||
void MoonlightSession::onProcessOutput() {
|
||||
const QByteArray output = process_->readAllStandardOutput();
|
||||
if (output.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QFile log(logPath_);
|
||||
if (log.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
|
||||
log.write(output);
|
||||
}
|
||||
|
||||
const bool wasReady = outputParser_.isReady();
|
||||
outputParser_.append(output);
|
||||
if (outputParser_.hasTerminalError()) {
|
||||
readyTimer_->stop();
|
||||
emitTerminalEvent(EventType::kFailed, QStringLiteral("云桌面连接已中断"));
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
if (!wasReady && outputParser_.isReady()) {
|
||||
scheduleReadyEvent();
|
||||
}
|
||||
}
|
||||
|
||||
void MoonlightSession::stop() {
|
||||
if (!process_) {
|
||||
if (!isRunning()) {
|
||||
return;
|
||||
}
|
||||
QProcess* proc = process_;
|
||||
process_ = nullptr; // 先断开自身引用,避免 finished 回调再触发状态迁移
|
||||
disconnect(proc, nullptr, this, nullptr);
|
||||
proc->terminate();
|
||||
if (!proc->waitForFinished(2000)) {
|
||||
proc->kill();
|
||||
proc->waitForFinished(1000);
|
||||
stopRequested_ = true;
|
||||
readyTimer_->stop();
|
||||
process_->terminate();
|
||||
killTimer_->start(kTerminateGraceMs);
|
||||
}
|
||||
|
||||
void MoonlightSession::emitEvent(EventType type, const QString& message) {
|
||||
if (callback_) {
|
||||
callback_(Event{type, message});
|
||||
}
|
||||
}
|
||||
|
||||
void MoonlightSession::emitTerminalEvent(EventType type, const QString& message) {
|
||||
if (terminalEventDelivered_) {
|
||||
return;
|
||||
}
|
||||
terminalEventDelivered_ = true;
|
||||
emitEvent(type, message);
|
||||
}
|
||||
|
||||
void MoonlightSession::scheduleReadyEvent() {
|
||||
if (!readyEventDelivered_ && !terminalEventDelivered_ && isRunning()) {
|
||||
readyTimer_->start(kReadyStableMs);
|
||||
}
|
||||
proc->deleteLater();
|
||||
}
|
||||
|
||||
} // namespace vdi
|
||||
Reference in new issue
Block a user