功能:完善通用云管工作台、审计与诊断能力
This commit is contained in:
1 parent
5ac692724d
commit
9f05aa20ea
225 files changed
+14266
-141
No files matched your search
@@ -14,6 +14,7 @@ if [ -r "$render_conf" ]; then
|
||||
program) export VDI_MOONLIGHT_PROGRAM="${VDI_MOONLIGHT_PROGRAM:-$value}" ;;
|
||||
decoder) export VDI_MOONLIGHT_DECODER="${VDI_MOONLIGHT_DECODER:-$value}" ;;
|
||||
codec) export VDI_MOONLIGHT_CODEC="${VDI_MOONLIGHT_CODEC:-$value}" ;;
|
||||
bitrate_kbps) export VDI_MOONLIGHT_BITRATE_KBPS="${VDI_MOONLIGHT_BITRATE_KBPS:-$value}" ;;
|
||||
esac
|
||||
done < "$render_conf"
|
||||
fi
|
||||
|
||||
@@ -198,7 +198,7 @@ void DesktopListPage::rebuildCards() {
|
||||
const char* tone = "unassigned";
|
||||
switch (d.state) {
|
||||
case DesktopState::kAvailable:
|
||||
stateText = QStringLiteral("可连接"); tone = "available"; break;
|
||||
stateText = d.powerOnConnect && d.poweredOff ? QStringLiteral("连接时启动") : QStringLiteral("可连接"); tone = "available"; break;
|
||||
case DesktopState::kPending:
|
||||
stateText = QStringLiteral("任务进行中"); tone = "pending"; break;
|
||||
case DesktopState::kUnassigned:
|
||||
@@ -221,6 +221,7 @@ void DesktopListPage::rebuildCards() {
|
||||
connect(edit, &QPushButton::clicked, this, [this, id = d.id] { if (!busy_) emit editRequested(id); });
|
||||
buttons->addWidget(edit);
|
||||
auto* enter = new QPushButton(QStringLiteral("连接"), card);
|
||||
if (d.powerOnConnect && d.poweredOff) enter->setText(QStringLiteral("启动并连接"));
|
||||
enter->setObjectName(QStringLiteral("connectButton"));
|
||||
enter->setMinimumSize(82, 36);
|
||||
enter->setCursor(Qt::PointingHandCursor);
|
||||
|
||||
@@ -33,6 +33,8 @@ struct DesktopInfo {
|
||||
int width = 2560;
|
||||
int height = 1440;
|
||||
int fps = 20;
|
||||
bool powerOnConnect = false;
|
||||
bool poweredOff = false;
|
||||
};
|
||||
|
||||
// 一次会话的连接凭据;当前为本地构造,后续由平台 connect-ticket 接口下发
|
||||
|
||||
@@ -583,11 +583,13 @@ void MainWindow::onDesktopSelected(const QString& desktopId) {
|
||||
connectionPhase_ = ConnectionPhase::kTicket;
|
||||
logConnectionPhase(attempt, QStringLiteral("ticket"), QStringLiteral("started"));
|
||||
|
||||
if (!fsm_.transitionTo(State::kConnecting, QStringLiteral("正在获取连接信息"))) {
|
||||
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, kTicketTimeoutMs);
|
||||
armConnectionTimeout(attempt, ConnectionPhase::kTicket, powerOnConnect ? 210000 : kTicketTimeoutMs);
|
||||
|
||||
desktopService_.createTicket(desktopId, [this, attempt](const ConnectionTicket& ticket,
|
||||
const QString& error) {
|
||||
|
||||
@@ -139,12 +139,16 @@ MoonlightSession::Options MoonlightSession::optionsFromEnvironment(
|
||||
if (!program.isEmpty()) {
|
||||
options.program = program;
|
||||
}
|
||||
bool validBitrate = false;
|
||||
const int bitrate = environment.value(QStringLiteral("VDI_MOONLIGHT_BITRATE_KBPS")).toInt(&validBitrate);
|
||||
if (validBitrate && bitrate >= 1000 && bitrate <= 150000) options.bitrateKbps = bitrate;
|
||||
return options;
|
||||
}
|
||||
|
||||
QStringList MoonlightSession::buildStreamArguments(const ConnectionTicket& ticket,
|
||||
const Options& options) {
|
||||
const int bitrateKbps = ticket.width >= 2560 ? 30000 : 20000;
|
||||
const int bitrateKbps = options.bitrateKbps >= 1000 && options.bitrateKbps <= 150000
|
||||
? options.bitrateKbps : (ticket.width >= 2560 ? 30000 : 20000);
|
||||
QStringList args;
|
||||
args << QStringLiteral("stream")
|
||||
<< ticket.endpoint
|
||||
|
||||
@@ -52,6 +52,7 @@ public:
|
||||
bool audioOnHost = true;
|
||||
QString videoCodec = QStringLiteral("H.264");
|
||||
QString decoder = QStringLiteral("software");
|
||||
int bitrateKbps = 0; // Zero keeps the resolution-based default.
|
||||
};
|
||||
|
||||
enum class EventType {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <QUrl>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QDateTime>
|
||||
|
||||
namespace vdi {
|
||||
namespace {
|
||||
@@ -106,20 +107,46 @@ void PlatformClient::authenticate(std::function<void(const AuthResult&)> callbac
|
||||
}
|
||||
void PlatformClient::list(std::function<void(std::vector<DesktopInfo>,const QString&)> callback) {
|
||||
if (!authenticated_) { callback({},QStringLiteral("正在连接管理平台")); return; }
|
||||
request("GET","/terminal/desktops?pageSize=100",{},[callback](const QJsonObject& reply,const QString& error){
|
||||
request("GET","/terminal/desktops?pageSize=100",{},[this,callback](const QJsonObject& reply,const QString& error){
|
||||
std::vector<DesktopInfo> list;
|
||||
if (error.isEmpty()) exclusiveDesktops_.clear();
|
||||
for (const auto& item:reply.value("items").toArray()) {
|
||||
const auto obj=item.toObject(); DesktopInfo d;
|
||||
d.id=obj.value("id").toString(); d.name=obj.value("name").toString(); d.os=obj.value("os").toString();
|
||||
d.endpoint=obj.value("endpoint").toString(); d.app=obj.value("app").toString("Desktop");
|
||||
d.width=obj.value("width").toInt(2560); d.height=obj.value("height").toInt(1440); d.fps=obj.value("fps").toInt(30);
|
||||
d.state=obj.value("powerState").toString()=="running" && !d.endpoint.isEmpty()?DesktopState::kAvailable:DesktopState::kUnassigned;
|
||||
d.powerOnConnect=obj.value("powerOnConnect").toBool(false);
|
||||
d.poweredOff=obj.value("powerState").toString()!="running";
|
||||
if (d.powerOnConnect) exclusiveDesktops_.insert(d.id);
|
||||
d.state=(!d.poweredOff || d.powerOnConnect) && !obj.value("setupRequired").toBool(false) && !d.endpoint.isEmpty()?DesktopState::kAvailable:DesktopState::kUnassigned;
|
||||
list.push_back(d);
|
||||
}
|
||||
callback(std::move(list),error);
|
||||
});
|
||||
}
|
||||
void PlatformClient::ticket(const QString& id,std::function<void(const ConnectionTicket&,const QString&)> callback) {
|
||||
if (!exclusiveDesktops_.contains(id)) { issueTicket(id,std::move(callback)); return; }
|
||||
request("POST","/terminal/desktop-prepare",{{"desktopId",id}},[this,id,callback](const QJsonObject& result,const QString& error){
|
||||
if (!error.isEmpty()) { callback({},error); return; }
|
||||
const QString task=result.value("taskId").toString();
|
||||
if (task.isEmpty()) { callback({},QStringLiteral("平台未返回桌面启动任务")); return; }
|
||||
waitPrepared(id,task,QDateTime::currentMSecsSinceEpoch()+195000,callback);
|
||||
});
|
||||
}
|
||||
void PlatformClient::waitPrepared(const QString& id,const QString& task,qint64 deadline,
|
||||
std::function<void(const ConnectionTicket&,const QString&)> callback) {
|
||||
request("GET","/terminal/desktop-prepare/"+task,{},[this,id,task,deadline,callback](const QJsonObject& result,const QString& error){
|
||||
if (!error.isEmpty()) { callback({},error); return; }
|
||||
const QString status=result.value("status").toString();
|
||||
if (status=="success") { issueTicket(id,callback); return; }
|
||||
if (status!="queued" && status!="running") {
|
||||
callback({},result.value("message").toString(QStringLiteral("桌面启动失败,请联系管理员"))); return;
|
||||
}
|
||||
if (QDateTime::currentMSecsSinceEpoch()>=deadline) { callback({},QStringLiteral("桌面启动尚未确认,请稍后重试")); return; }
|
||||
QTimer::singleShot(1000,this,[this,id,task,deadline,callback]{waitPrepared(id,task,deadline,callback);});
|
||||
});
|
||||
}
|
||||
void PlatformClient::issueTicket(const QString& id,std::function<void(const ConnectionTicket&,const QString&)> callback) {
|
||||
request("POST","/terminal/connect-ticket",{{"desktopId",id}},[this,callback](const QJsonObject& reply,const QString& error){
|
||||
if (!error.isEmpty()) { callback({},error); return; }
|
||||
request("POST","/terminal/connect-ticket/consume",{{"ticket",reply.value("ticket")}},[this,callback](const QJsonObject& result,const QString& error){
|
||||
|
||||
@@ -26,6 +26,9 @@ public:
|
||||
std::function<QString()> kioskState;
|
||||
std::function<void(int)> desktopRefreshInterval;
|
||||
private:
|
||||
void issueTicket(const QString& desktopId, std::function<void(const ConnectionTicket&, const QString&)> callback);
|
||||
void waitPrepared(const QString& desktopId, const QString& taskId, qint64 deadline,
|
||||
std::function<void(const ConnectionTicket&, const QString&)> callback);
|
||||
void fetchPolicy();
|
||||
void applyPolicy(const QJsonObject& reply);
|
||||
void flushPolicyAck();
|
||||
@@ -48,6 +51,7 @@ private:
|
||||
bool policyInFlight_=false, policyAckInFlight_=false;
|
||||
QString origin_, token_, error_, activeSession_;
|
||||
QSet<QString> ending_, ackInFlight_;
|
||||
QSet<QString> exclusiveDesktops_;
|
||||
bool configured_=false, authenticated_=false, polling_=false, connected_=false;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ class DesktopListPageTest : public QObject {
|
||||
|
||||
private slots:
|
||||
void firstClickDisablesActionsAndBlocksDuplicate();
|
||||
void poweredOffSharedDesktopCanStart();
|
||||
};
|
||||
|
||||
void DesktopListPageTest::firstClickDisablesActionsAndBlocksDuplicate() {
|
||||
@@ -49,5 +50,26 @@ void DesktopListPageTest::firstClickDisablesActionsAndBlocksDuplicate() {
|
||||
QVERIFY(editButton->isEnabled());
|
||||
}
|
||||
|
||||
void DesktopListPageTest::poweredOffSharedDesktopCanStart() {
|
||||
DesktopListPage page;
|
||||
DesktopInfo desktop;
|
||||
desktop.id = QStringLiteral("shared-linux");
|
||||
desktop.name = QStringLiteral("Ubuntu");
|
||||
desktop.os = QStringLiteral("Ubuntu");
|
||||
desktop.state = DesktopState::kAvailable;
|
||||
desktop.powerOnConnect = true;
|
||||
desktop.poweredOff = true;
|
||||
page.setDesktops({desktop});
|
||||
auto* button = page.findChild<QPushButton*>(QStringLiteral("connectButton"));
|
||||
QVERIFY(button);
|
||||
QVERIFY(button->isEnabled());
|
||||
QCOMPARE(button->text(), QStringLiteral("启动并连接"));
|
||||
QSignalSpy spy(&page, &DesktopListPage::desktopSelected);
|
||||
button->click();
|
||||
QCOMPARE(spy.count(), 1);
|
||||
QCOMPARE(spy.at(0).at(0).toString(), desktop.id);
|
||||
QVERIFY(!button->isEnabled());
|
||||
}
|
||||
|
||||
QTEST_MAIN(DesktopListPageTest)
|
||||
#include "test_desktop_list_page.moc"
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include "desktop_list_page.h"
|
||||
#include "main_window.h"
|
||||
#include "platform_client.h"
|
||||
|
||||
namespace vdi {
|
||||
|
||||
@@ -26,6 +27,10 @@ public:
|
||||
int delayMs = 0;
|
||||
int authRequests = 0;
|
||||
int listRequests = 0;
|
||||
bool sharedGPU = false;
|
||||
bool prepareFailed = false;
|
||||
int preparePolls = 0;
|
||||
int ticketRequests = 0;
|
||||
|
||||
protected:
|
||||
void incomingConnection(qintptr descriptor) override {
|
||||
@@ -55,8 +60,24 @@ protected:
|
||||
++listRequests;
|
||||
body["items"] = QJsonArray{QJsonObject{
|
||||
{"id", "authorized-desktop"}, {"name", "Authorized Windows"},
|
||||
{"os", "Windows"}, {"powerState", "running"},
|
||||
{"os", "Windows"}, {"powerState", sharedGPU ? "stopped" : "running"},
|
||||
{"powerOnConnect", sharedGPU},
|
||||
{"endpoint", "127.0.0.1"}, {"app", "Desktop"}}};
|
||||
} else if (path == "/api/v1/terminal/desktop-prepare") {
|
||||
status = 202;
|
||||
body["taskId"] = "prepare-task";
|
||||
body["status"] = "queued";
|
||||
} else if (path == "/api/v1/terminal/desktop-prepare/prepare-task") {
|
||||
++preparePolls;
|
||||
body["status"] = prepareFailed ? "failed" : (preparePolls > 1 ? "success" : "running");
|
||||
body["message"] = "prepare test result";
|
||||
} else if (path == "/api/v1/terminal/connect-ticket") {
|
||||
++ticketRequests;
|
||||
status = 201;
|
||||
body["ticket"] = "prepared-ticket";
|
||||
} else if (path == "/api/v1/terminal/connect-ticket/consume") {
|
||||
body = QJsonObject{{"desktopId", "authorized-desktop"}, {"sessionId", "prepared-session"},
|
||||
{"endpoint", "127.0.0.1"}, {"width", 2560}, {"height", 1440}, {"fps", 45}};
|
||||
} else {
|
||||
body["items"] = QJsonArray{};
|
||||
}
|
||||
@@ -210,6 +231,38 @@ private slots:
|
||||
QVERIFY(selectionVisible(window));
|
||||
QVERIFY(!window.deviceAuthTimer_->isActive());
|
||||
}
|
||||
void sharedDesktopWaitsBeforeIssuingTicket() {
|
||||
for (bool fail : {false, true}) {
|
||||
TestPlatform server;
|
||||
QVERIFY(server.listen(QHostAddress::LocalHost, 0));
|
||||
configure(server, server.serverPort());
|
||||
server.sharedGPU = true;
|
||||
server.prepareFailed = fail;
|
||||
PlatformClient client;
|
||||
bool authenticated = false;
|
||||
client.authenticate([&](const AuthResult& result) { authenticated = result.ok; });
|
||||
QTRY_VERIFY(authenticated);
|
||||
bool listed = false;
|
||||
client.list([&](std::vector<DesktopInfo> desktops, const QString& error) {
|
||||
QVERIFY(error.isEmpty());
|
||||
QCOMPARE(desktops.size(), size_t(1));
|
||||
QVERIFY(desktops[0].powerOnConnect && desktops[0].poweredOff);
|
||||
QCOMPARE(desktops[0].state, DesktopState::kAvailable);
|
||||
listed = true;
|
||||
});
|
||||
QTRY_VERIFY(listed);
|
||||
bool completed = false;
|
||||
client.ticket("authorized-desktop", [&](const ConnectionTicket& ticket, const QString& error) {
|
||||
QCOMPARE(error.isEmpty(), !fail);
|
||||
if (!fail) QCOMPARE(ticket.sessionId, QString("prepared-session"));
|
||||
completed = true;
|
||||
});
|
||||
QTRY_VERIFY(server.preparePolls > 0);
|
||||
QCOMPARE(server.ticketRequests, 0);
|
||||
QTRY_VERIFY(completed);
|
||||
QCOMPARE(server.ticketRequests, fail ? 0 : 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace vdi
|
||||
|
||||
@@ -16,8 +16,22 @@ private slots:
|
||||
void buildsConsistentStreamArguments();
|
||||
void selectsProfileWithoutLosingStats();
|
||||
void rejectsInvalidDecoderOverride();
|
||||
void validatesConfiguredBitrate();
|
||||
};
|
||||
|
||||
void MoonlightSessionTest::validatesConfiguredBitrate() {
|
||||
ConnectionTicket ticket;
|
||||
ticket.width = 2560;
|
||||
ticket.height = 1440;
|
||||
QProcessEnvironment environment;
|
||||
for (const QString& value : {QString("25000"), QString("0"), QString("150001"), QString("invalid")}) {
|
||||
environment.insert("VDI_MOONLIGHT_BITRATE_KBPS", value);
|
||||
auto options = MoonlightSession::optionsFromEnvironment(environment);
|
||||
const auto arguments = MoonlightSession::buildStreamArguments(ticket, options);
|
||||
QCOMPARE(arguments.at(arguments.indexOf("--bitrate") + 1), value == "25000" ? QString("25000") : QString("30000"));
|
||||
}
|
||||
}
|
||||
|
||||
void MoonlightSessionTest::parsesSplitReadyMarkers() {
|
||||
MoonlightOutputParser parser;
|
||||
parser.append("Test decode succ");
|
||||
|
||||
@@ -41,6 +41,20 @@ gcc -shared -nostdlib -fPIC -O2 -Wall -Wextra -Werror -Wl,-z,defs \
|
||||
|
||||
每台主机单独的ed25519私钥和known_hosts留在盒子配置目录(目录700、私钥600),不进仓库。known_hosts是在工作站用用户提供的SSH凭据成功认证后取得的真实主机公钥,没有在运行时自动接受新主机密钥。JSON和密钥应在已验证0005的客户端部署后才启用。上例SHA为2026-11-05现场构建;重新构建后须先停用profile、完成对应验证,再更新SHA并启用,不能把旧值机械替换为任意新文件的SHA。
|
||||
|
||||
## NVIDIA 硬件采集(2026-11-11)
|
||||
|
||||
当前两台 Linux 的现场部署已切换为 GTX 965M 渲染、NvFBC/CUDA 采集与 NVENC H.264 编码;不再沿用上文历史 X11 软件编码基线。使用兼容 Maxwell 的 Sunshine v2025.924.154138;不得仅更新主二进制而不复测 CUDA 架构支持和 pinned manifest。
|
||||
|
||||
Ubuntu 的 NVIDIA→modesetting PRIME 路径会把服务端软件光标提前画进画面,仅设 NvFBC `bWithCursor=false` 仍会出现本地光标拖着视频光标。该主机在 `host.py` 同目录启用空标记文件 `hide-server-cursor`:捕获 guard 用独立 X11 连接持有 `XFixesHideCursor`,原生光标形状仍由另一个连接通过 `XFixesGetCursorImage` 读取。结束或异常退出时先停 Sunshine,再显式恢复并关闭隐藏连接;X server 也会在连接断开时释放该客户端的隐藏状态。标记仅在 Ubuntu 现场启用,麒麟保持原路径。撤销须先退出串流、停 guard,再移除标记后重连,不在同一串流中途切换。
|
||||
|
||||
适配器编译增加 `-DVDI_NVFBC -Ivendor`。麒麟缺少 libc 开发启动对象时另加 `-ffreestanding -nostdlib`,并显式链接系统版本化 libc/libdl。`vendor/NvFBC.h` 保留上游 MIT 许可。拦截 NvFBC 函数表时检查 SDK 参数版本,仅在租约内关闭视频内光标,不修改调用方参数;`nvfbc-ready-v1` 是加载就绪凭据,`nvfbc-v1` 才表示实际捕获会话创建成功。
|
||||
|
||||
麒麟的 Optimus 卡没有可用物理输出,旧内核 PRIME 显存导入也失败,因此显式启用 `VDI_NVFBC_HEADLESS=1`。适配器只在驱动报告支持采集、没有 RandR 输出且真实 X screen 为 2560×1440 时枚举整屏,标记 RandR 不可用,使 Sunshine 使用 `NVFBC_TRACKING_SCREEN`。它不生成真实显示输出,也不把不可采集屏幕报告为可用。UKUI 已目视验证桌面正常;Ubuntu GNOME 在相同无输出条件下黑屏,已恢复 NVIDIA + modesetting PRIME 输出,不能直接照搬麒麟配置。
|
||||
|
||||
麒麟使用 `/opt/vdi-sunshine-maxwell/runtime/` 的私有动态加载器和依赖库运行该 Sunshine,不替换系统 glibc;可执行文件、适配器和新增脚本按文件进行 KYSEC verified 认证。GPU 路径及测量边界见 [本轮实施证据](../../docs/evidence/linux-gpu-20261111/README.md)。
|
||||
|
||||
重启显示管理器可能使临时 guard 单元消失而遗留旧 ready/receipt;新租约在获得独占锁并确认旧 guard 停止后清理这两个标记,再等待新会话凭据。`test_native_cursor_host.py` 在实际 Ubuntu Linux 上覆盖该恢复情形及不完整 manifest 拒绝;`run_nvfbc_adapter.py` 配合 `nvfbc_adapter_stub.c` 在麒麟实际 glibc 上覆盖函数表 ABI、光标与整屏枚举边界。
|
||||
|
||||
## 停用与回退
|
||||
|
||||
退出当前Linux串流,等guard inactive且原vdi-sunshine active。将对应 `<IP>.json` 改名为 `.disabled`,下次从同一正常列表连接即使用原视频光标。临时诊断也可设置 `VDI_NATIVE_CURSOR=0`,但必须在当前租约结束、原服务恢复之后重连。
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <stddef.h>
|
||||
#ifdef VDI_NVFBC
|
||||
#include "NvFBC.h"
|
||||
#endif
|
||||
extern void *dlvsym(void *, const char *, const char *);
|
||||
extern char *getenv(const char *);
|
||||
extern int strcmp(const char *, const char *);
|
||||
@@ -24,6 +27,77 @@ typedef void *(*Lookup)(void *, const char *);
|
||||
typedef CursorImage *(*GetImage)(void *);
|
||||
static GetImage real_image;
|
||||
|
||||
static void receipt(const char *value, unsigned long size) {
|
||||
const char *path = getenv("VDI_CURSOR_RECEIPT");
|
||||
if (path) {
|
||||
int fd = open(path, 0101 | 01000 | 02000000, 0600);
|
||||
if (fd >= 0) { write(fd, value, size); close(fd); }
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef VDI_NVFBC
|
||||
static PNVFBCCREATEINSTANCE real_instance;
|
||||
static PNVFBCCREATECAPTURESESSION real_capture;
|
||||
static PNVFBCGETSTATUS real_status;
|
||||
|
||||
/* Some Optimus cards expose a real NVIDIA X screen without RandR outputs.
|
||||
* Sunshine's enumeration rejects that otherwise capturable screen. Explicit
|
||||
* single-seat mode enumerates the whole 2K X screen, with RandR unavailable,
|
||||
* so Sunshine selects NVFBC_TRACKING_SCREEN instead of an invented output ID.
|
||||
* Never report capture support or dimensions that the driver did not return.
|
||||
*/
|
||||
static NVFBCSTATUS NVFBCAPI screen_status(const NVFBC_SESSION_HANDLE session,
|
||||
NVFBC_GET_STATUS_PARAMS *params) {
|
||||
if (!params || params->dwVersion != NVFBC_GET_STATUS_PARAMS_VER) abort();
|
||||
PNVFBCGETSTATUS fn = __atomic_load_n(&real_status, __ATOMIC_ACQUIRE);
|
||||
if (!fn) abort();
|
||||
NVFBCSTATUS status = fn(session, params);
|
||||
if (status == NVFBC_SUCCESS && params->bIsCapturePossible &&
|
||||
params->dwOutputNum == 0 && params->screenSize.w == 2560 &&
|
||||
params->screenSize.h == 1440) {
|
||||
params->bXRandRAvailable = NVFBC_FALSE;
|
||||
params->dwOutputNum = 1;
|
||||
params->outputs[0] = (NVFBC_RANDR_OUTPUT_INFO){
|
||||
.dwId = 0, .name = "VDI-X-screen",
|
||||
.trackedBox = {0, 0, 2560, 1440}
|
||||
};
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
static NVFBCSTATUS NVFBCAPI capture_session(const NVFBC_SESSION_HANDLE session,
|
||||
NVFBC_CREATE_CAPTURE_SESSION_PARAMS *params) {
|
||||
if (!params || params->dwVersion != NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER)
|
||||
abort(); /* A new SDK ABI must be reviewed before suppressing its cursor. */
|
||||
NVFBC_CREATE_CAPTURE_SESSION_PARAMS local = *params;
|
||||
if (getenv("VDI_CURSOR_LEASE")) local.bWithCursor = NVFBC_FALSE;
|
||||
PNVFBCCREATECAPTURESESSION fn = __atomic_load_n(&real_capture, __ATOMIC_ACQUIRE);
|
||||
if (!fn) abort();
|
||||
NVFBCSTATUS status = fn(session, &local);
|
||||
if (status == NVFBC_SUCCESS) receipt("nvfbc-v1\n", 9);
|
||||
return status;
|
||||
}
|
||||
|
||||
static NVFBCSTATUS NVFBCAPI create_instance(NVFBC_API_FUNCTION_LIST *functions) {
|
||||
PNVFBCCREATEINSTANCE fn = __atomic_load_n(&real_instance, __ATOMIC_ACQUIRE);
|
||||
if (!fn) abort();
|
||||
NVFBCSTATUS status = fn(functions);
|
||||
if (status == NVFBC_SUCCESS) {
|
||||
if (!functions->nvFBCCreateCaptureSession) abort();
|
||||
__atomic_store_n(&real_capture, functions->nvFBCCreateCaptureSession, __ATOMIC_RELEASE);
|
||||
functions->nvFBCCreateCaptureSession = capture_session;
|
||||
if (getenv("VDI_NVFBC_HEADLESS")) {
|
||||
if (!functions->nvFBCGetStatus) abort();
|
||||
__atomic_store_n(&real_status, functions->nvFBCGetStatus, __ATOMIC_RELEASE);
|
||||
functions->nvFBCGetStatus = screen_status;
|
||||
}
|
||||
/* Startup probes do not create a capture session until Moonlight connects. */
|
||||
receipt("nvfbc-ready-v1\n", 15);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
#endif
|
||||
|
||||
static CursorImage *capture_image(void *display) {
|
||||
GetImage fn = __atomic_load_n(&real_image, __ATOMIC_ACQUIRE);
|
||||
if (!fn) abort();
|
||||
@@ -43,12 +117,15 @@ void *dlsym(void *handle, const char *name) {
|
||||
if (result && getenv("VDI_CURSOR_LEASE") &&
|
||||
strcmp(name, "XFixesGetCursorImage") == 0) {
|
||||
__atomic_store_n(&real_image, (GetImage)result, __ATOMIC_RELEASE);
|
||||
const char *receipt = getenv("VDI_CURSOR_RECEIPT");
|
||||
if (receipt) {
|
||||
int fd = open(receipt, 0101 | 01000 | 02000000, 0600);
|
||||
if (fd >= 0) { write(fd, "xfixes-v1\n", 10); close(fd); }
|
||||
}
|
||||
receipt("xfixes-v1\n", 10);
|
||||
return (void *)capture_image;
|
||||
}
|
||||
#ifdef VDI_NVFBC
|
||||
if (result && (getenv("VDI_CURSOR_LEASE") || getenv("VDI_NVFBC_HEADLESS")) &&
|
||||
strcmp(name, "NvFBCCreateInstance") == 0) {
|
||||
__atomic_store_n(&real_instance, (PNVFBCCREATEINSTANCE)result, __ATOMIC_RELEASE);
|
||||
return (void *)create_instance;
|
||||
}
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
@@ -49,10 +49,29 @@ def guard():
|
||||
if len(lease) != 32 or any(c not in '0123456789abcdef' for c in lease):
|
||||
raise ValueError('invalid lease')
|
||||
manifest = json.loads((ROOT / 'manifest.json').read_text())
|
||||
if set(manifest) != {'sunshine', 'capture-no-cursor.so'}:
|
||||
raise RuntimeError('incomplete pinned capture manifest')
|
||||
for name, expected in manifest.items():
|
||||
if name not in ('sunshine', 'capture-no-cursor.so') or hashlib.sha256(
|
||||
(ROOT / name).read_bytes()).hexdigest() != expected:
|
||||
raise RuntimeError('pinned capture build changed')
|
||||
if (ROOT / 'hide-server-cursor').exists():
|
||||
# PRIME can flatten Xorg's software sprite into the framebuffer before
|
||||
# NvFBC sees it. Keep a separate XFixes hide owned by the capture guard;
|
||||
# GetCursorImage still supplies the unmodified native shape to stream().
|
||||
with XCursor() as cursor:
|
||||
cursor.hide_server_cursor()
|
||||
try:
|
||||
guard_capture(lease)
|
||||
finally:
|
||||
# Stop the video producer before restoring its visible sprite.
|
||||
# ExecStopPost then restores normal, non-leased capture.
|
||||
systemctl('stop', SERVICE)
|
||||
else:
|
||||
guard_capture(lease)
|
||||
|
||||
|
||||
def guard_capture(lease):
|
||||
kms_config = ROOT / 'kms.json'
|
||||
scanout = None
|
||||
def checked_scanout(timeout=3):
|
||||
@@ -145,8 +164,13 @@ class XCursor:
|
||||
for name in ('XDisplayWidth', 'XDisplayHeight'):
|
||||
getattr(self.x, name).argtypes = [C.c_void_p, C.c_int]
|
||||
self.x.XFree.argtypes = [C.c_void_p]
|
||||
self.x.XSync.argtypes = [C.c_void_p, C.c_int]
|
||||
self.x.XCloseDisplay.argtypes = [C.c_void_p]
|
||||
self.f.XFixesGetCursorImage.argtypes = [C.c_void_p]
|
||||
self.f.XFixesGetCursorImage.restype = C.POINTER(Cursor)
|
||||
self.f.XFixesHideCursor.argtypes = [C.c_void_p, C.c_ulong]
|
||||
self.f.XFixesShowCursor.argtypes = [C.c_void_p, C.c_ulong]
|
||||
self.hidden = False
|
||||
self.display = self.x.XOpenDisplay(None)
|
||||
if not self.display:
|
||||
raise RuntimeError('no authenticated X11 display')
|
||||
@@ -154,6 +178,28 @@ class XCursor:
|
||||
self.root = self.x.XDefaultRootWindow(self.display)
|
||||
self.size = self.dimensions()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_):
|
||||
self.close()
|
||||
|
||||
def hide_server_cursor(self):
|
||||
if not self.hidden:
|
||||
self.f.XFixesHideCursor(self.display, self.root)
|
||||
self.x.XSync(self.display, 0)
|
||||
self.hidden = True
|
||||
|
||||
def close(self):
|
||||
if self.display:
|
||||
try:
|
||||
if self.hidden:
|
||||
self.f.XFixesShowCursor(self.display, self.root)
|
||||
self.x.XSync(self.display, 0)
|
||||
finally:
|
||||
self.x.XCloseDisplay(self.display)
|
||||
self.display = None
|
||||
|
||||
def dimensions(self):
|
||||
root = C.c_ulong()
|
||||
x, y = C.c_int(), C.c_int()
|
||||
@@ -214,6 +260,11 @@ def stream():
|
||||
# Fail before touching the service if the native shape is unsupported.
|
||||
x.read()
|
||||
systemctl('stop', GUARD)
|
||||
# A display-manager restart can remove the transient unit before its
|
||||
# ExecStopPost runs. The exclusive stream lock and confirmed stop make
|
||||
# it safe to discard the old producer's readiness/receipt here.
|
||||
for name in ('ready', 'receipt'):
|
||||
(RUN / name).unlink(missing_ok=True)
|
||||
(RUN / 'lease').write_text(lease)
|
||||
systemctl('start', GUARD)
|
||||
deadline = time.monotonic() + 40
|
||||
@@ -221,6 +272,8 @@ def stream():
|
||||
while not (RUN / 'ready').exists():
|
||||
if time.monotonic() > deadline:
|
||||
raise RuntimeError('guard not ready')
|
||||
if not (RUN / 'lease').exists():
|
||||
raise RuntimeError('capture guard ended before ready; inspect guard journal')
|
||||
os.utime(RUN / 'lease', None)
|
||||
time.sleep(.05)
|
||||
if (RUN / 'ready').read_text() != lease:
|
||||
|
||||
+1996
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user