功能:完善通用云管工作台、审计与诊断能力

This commit is contained in:
songyiming committed 2026-11-11 18:46:16 +08:00
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 接口下发
+4 -2
View File
@@ -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 {
+29 -2
View File
@@ -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");