变更详情:kiosk 会话就绪检测重构(Moonlight 输出解析器/事件化回调/单元测试);GPU 直通实操记录(vfio Subsystem 注入、sysfs vBIOS、7zr+pnputil 安装、566.36 驱动、Sunshine 配对断链修复)
210 lines
6.6 KiB
C++
210 lines
6.6 KiB
C++
#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
|