vdi-cloud/terminal/kiosk-shell/src/monitor.cpp
宋一鸣 13f645a503 重构:完善串流就绪检测并补充 GPU 联调记录
变更详情:kiosk 会话就绪检测重构(Moonlight 输出解析器/事件化回调/单元测试);GPU 直通实操记录(vfio Subsystem 注入、sysfs vBIOS、7zr+pnputil 安装、566.36 驱动、Sunshine 配对断链修复)
2026-10-30 14:42:11 +08:00

210 lines
6.6 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "monitor.h"
#include <signal.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <string>
#include <thread>
#include <vector>
namespace vdi {
namespace {
Monitor* g_monitor = nullptr;
void onSignal(int) {
if (g_monitor) {
g_monitor->requestStop();
}
}
// 主进程写心跳文件;Monitor 读取其修改时间判断是否假死
bool heartbeatAgeMs(const std::string& path, long long& ageMs) {
struct stat st {};
if (stat(path.c_str(), &st) != 0) {
return false;
}
const auto now = std::chrono::system_clock::now();
const auto mtime = std::chrono::system_clock::from_time_t(st.st_mtime);
ageMs = std::chrono::duration_cast<std::chrono::milliseconds>(now - mtime).count();
return true;
}
} // namespace
Monitor::Monitor(MonitorConfig config) : config_(std::move(config)) {}
void Monitor::requestStop() {
stopRequested_ = true;
}
void Monitor::log(const std::string& message) const {
if (!config_.verbose) {
return;
}
const std::time_t now = std::time(nullptr);
char timeBuf[32] = {0};
std::strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%d %H:%M:%S", std::localtime(&now));
std::fprintf(stderr, "[monitor %s] %s\n", timeBuf, message.c_str());
std::fflush(stderr);
}
bool Monitor::isProcessAlive(pid_t pid) const {
if (pid <= 0) {
return false;
}
return kill(pid, 0) == 0 || errno == EPERM;
}
// 结束主进程及其整棵进程树。
// 主进程通常还会再 fork 子进程(例如启动器拉起的串流客户端),
// 只杀直接子进程会留下孤儿进程继续占用显示设备和串流会话,
// 因此子进程启动时独立成组,结束时按进程组终止。
namespace {
void killTree(pid_t pid, int sig) {
if (pid <= 0) {
return;
}
if (kill(-pid, sig) != 0) {
kill(pid, sig);
}
}
} // namespace
bool Monitor::isHung() const {
long long ageMs = 0;
if (!heartbeatAgeMs(config_.heartbeatFile, ageMs)) {
// 没有心跳文件:主进程还没写过心跳
return false;
}
return ageMs > config_.heartbeatTimeoutMs;
}
int Monitor::run(const std::string& command) {
if (command.empty()) {
log("用法: monitor -- <主进程命令行>");
return 2;
}
g_monitor = this;
signal(SIGTERM, onSignal);
signal(SIGINT, onSignal);
// 子进程退出时不发送 SIGPIPE,避免 Monitor 自身被杀
signal(SIGPIPE, SIG_IGN);
log("看门狗启动,监控: " + command);
pid_t child = -1;
auto startTime = std::chrono::steady_clock::now();
while (!stopRequested_) {
if (child <= 0) {
// 启动主进程
child = fork();
if (child == 0) {
// 子进程:恢复默认信号处理并 exec
signal(SIGTERM, SIG_DFL);
signal(SIGINT, SIG_DFL);
signal(SIGPIPE, SIG_DFL);
// 独立成组,便于结束整棵进程树
setpgid(0, 0);
execl("/bin/sh", "sh", "-c", command.c_str(), nullptr);
_exit(127);
}
if (child < 0) {
log(std::string("fork 失败: ") + std::strerror(errno));
return 1;
}
// 父子都调用一次 setpgid,避免父子竞争导致进程组 ID 未及时生效
setpgid(child, child);
startTime = std::chrono::steady_clock::now();
log("主进程已启动, pid=" + std::to_string(child));
}
std::this_thread::sleep_for(std::chrono::milliseconds(config_.checkIntervalMs));
if (stopRequested_) {
log("收到退出信号,终止主进程");
if (child > 0) {
killTree(child, SIGTERM);
// 等待退出,超时则强杀
for (int i = 0; i < 20 && isProcessAlive(child); ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
if (isProcessAlive(child)) {
killTree(child, SIGKILL);
}
waitpid(child, nullptr, 0);
}
log("看门狗退出");
return 0;
}
// 回收已退出的子进程
int status = 0;
const pid_t reaped = waitpid(child, &status, WNOHANG);
if (reaped == child) {
const int exitCode = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
const int signalNum = WIFSIGNALED(status) ? WTERMSIG(status) : 0;
log("主进程已退出, code=" + std::to_string(exitCode) +
(signalNum ? (", signal=" + std::to_string(signalNum)) : ""));
// shell 主进程退出时 Moonlight 等后代可能仍留在原进程组中。
// 重启前清理整个组,避免旧串流窗口与新实例并存。
killTree(child, SIGTERM);
std::this_thread::sleep_for(std::chrono::milliseconds(300));
killTree(child, SIGKILL);
if (++restartAttempts_ > config_.maxRestartAttempts) {
log("连续重启超过 " + std::to_string(config_.maxRestartAttempts) +
" 次,放弃重启(熔断)");
return 1;
}
log(std::to_string(config_.restartBackoffMs) + "ms 后重启");
std::this_thread::sleep_for(std::chrono::milliseconds(config_.restartBackoffMs));
child = -1;
continue;
}
// 假死检测
if (isHung()) {
const auto elapsed =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - startTime)
.count();
// 启动宽限期内不判定假死
if (elapsed > config_.startupGraceMs) {
log("检测到主进程假死(心跳超时),执行 kill 并重启");
killTree(child, SIGKILL);
waitpid(child, &status, 0);
child = -1;
if (++restartAttempts_ > config_.maxRestartAttempts) {
log("连续重启超过 " + std::to_string(config_.maxRestartAttempts) +
" 次,放弃重启(熔断)");
return 1;
}
std::this_thread::sleep_for(std::chrono::milliseconds(config_.restartBackoffMs));
continue;
}
} else {
// 进程健康,重置重启计数
restartAttempts_ = 0;
}
}
return 0;
}
} // namespace vdi