mirror of
https://github.com/feigeCode/navop.git
synced 2026-09-22 08:01:27 +00:00
排查文档记录的结论是:RDP 关闭超时时会把整个 adapter 连同 COM 资源一起 Box::leak,SSH 文件编辑器的分离加载/保存任务持有窗口强引用,两者都会让 已关闭的会话残留内存。本次按文档建议分三档落地,并刻意保持关闭/释放语义 不变——先建立可观测性,再改所有权。 第 1 档:低频诊断,不改任何语义 - 新增 windows_rdp_host::lifecycle_stats:进程级原子计数,明确区分 created / destroyed / quarantined,覆盖 hosts_*、wrong_thread_drops、 overlays_* 与 adapters_retired / retired_destroyed / retired_leaked_at_exit, 并派生 live_hosts() / live_overlays() / quarantined_adapters()。 - close 超时、overlay abandon、wrong-thread drop 的日志补 close_mode / retry_count / elapsed_ms / last_stage / last_error 关联字段。 - C++ 侧原先 DestroyWindow(host_window) 调用后不看结果,现挪进新的 destroy_host_window() noexcept:先取 GetWindowThreadProcessId(窗口销毁 之后 Win32 已不再报线程 id),SetLastError 后调用,再回读 GetLastError() 与 IsWindow()。失败行始终打印,成功行受 NAVOP_REMOTE_DESKTOP_DIAGNOSTICS 约束。刻意不做重试:失败只可能是跨线程调用或句柄已失效,而重试存在句柄 被复用后二次释放的风险。 第 2 档:编辑器分离任务改用弱引用 - reload_tab / save_tab 的分离任务由 cx.entity().clone() 改为 downgrade()。 根因就在这里:慢的 SFTP 往返会把已关闭的窗口连同其全部 tab 一起钉住。 - save 语义保持不变:写操作照常执行完毕,只有 UI 更新退化为 no-op。 - 新增 impl Drop for RemoteFileEditorWindow。窗口被移除时仍开着的 tab 不走 close_clean_tab,视图与 tab 计数必须在这里归还,否则计数会永久漂移。 - 结果被丢弃时打 log_task_result_discarded,用于区分「任务干净排空」与 「任务仍挂在后台」。 第 3 档:Box::leak 兜底改为 owner 线程 retirement 队列 - 三处 deadline 分支(close 超时、初始化清理超时、drain 的 poll_view_owner) 不再 Box::leak,改为交给 windows_native_retirement:owner 线程按 RETIREMENT_RETRY_INTERVAL=1s 重试 force_close,沿用已有的 borrow-free 三段式(Phase B 释放借用之后才泵 COM 消息,避免重入 App 借用), RETIREMENT_MAX_AGE=60s 有界宽限到期、或 on_app_quit 时才兜底 leak 并计数。 - WindowsNativeCloseOperation::into_leaked_adapter 更名 into_adapter,语义 仍是无 COM 调用的所有权移交,只是不再隐含「泄漏」。 - 队列本身(RetirementPolicy / Retired / RetirementQueue)是纯数据且跨平台 单测覆盖,只有 GPUI/native 接线是 Windows 专属。 验证: - cargo test -p windows_rdp_host:153 lib + 39 contract passed / 0 failed - cargo test -p remote_desktop_view --features windows-native-rdp: 280 passed / 0 failed - cargo test -p remote_file_editor:62 passed / 0 failed - 同步更新三处源文本契约测试:删去 Box::leak / into_leaked_adapter 断言, 新增「三处 deadline 路径都不得再出现 Box::leak」「retirement driver 不得 在 update_global 借用内 spawn」「销毁必须观测 DestroyWindow 结果」等不变量 未验证:真实 RDP 会话卡住 COM 回调、真实 SFTP 断连等真机路径仍是 #[ignore] + NAVOP_LIVE_* 环境变量用例,需要真实服务器。另有报告的 RDP 终端卡死(#118)根因仍未定位,本次未触碰该链路。
341 lines
9.3 KiB
C++
341 lines
9.3 KiB
C++
#include "host_internal.h"
|
|
|
|
#include <cinttypes>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
|
|
namespace {
|
|
|
|
constexpr const char* kTracePrefix = "RDP_NATIVE_TRACE";
|
|
constexpr char kReplacementCharacter[] = "\xEF\xBF\xBD";
|
|
|
|
// Native stage traces are diagnostics for white-screen / z-order issues and
|
|
// run on every presentation-state poll. Keep them off by default so normal
|
|
// operation does not flood stderr; set `NAVOP_REMOTE_DESKTOP_DIAGNOSTICS`
|
|
// (the same switch as the Rust-side remote desktop diagnostics) to re-enable.
|
|
bool native_trace_enabled() noexcept {
|
|
static const bool enabled = []() noexcept {
|
|
char* value = nullptr;
|
|
const errno_t error = _dupenv_s(
|
|
&value,
|
|
nullptr,
|
|
"NAVOP_REMOTE_DESKTOP_DIAGNOSTICS");
|
|
if (error != 0 || value == nullptr) {
|
|
return false;
|
|
}
|
|
std::free(value);
|
|
return true;
|
|
}();
|
|
return enabled;
|
|
}
|
|
|
|
void flush_trace() noexcept {
|
|
std::fflush(stderr);
|
|
}
|
|
|
|
void write_replacement_character() noexcept {
|
|
static_cast<void>(std::fwrite(
|
|
kReplacementCharacter,
|
|
1,
|
|
sizeof(kReplacementCharacter) - 1,
|
|
stderr));
|
|
}
|
|
|
|
void write_byte(uint8_t byte) noexcept {
|
|
static_cast<void>(std::fputc(static_cast<int>(byte), stderr));
|
|
}
|
|
|
|
void write_utf8_code_point(uint32_t code_point) noexcept {
|
|
if (code_point <= UINT32_C(0x7f)) {
|
|
write_byte(static_cast<uint8_t>(code_point));
|
|
return;
|
|
}
|
|
|
|
if (code_point <= UINT32_C(0x7ff)) {
|
|
write_byte(static_cast<uint8_t>(
|
|
UINT32_C(0xc0) | (code_point >> 6U)));
|
|
write_byte(static_cast<uint8_t>(
|
|
UINT32_C(0x80) | (code_point & UINT32_C(0x3f))));
|
|
return;
|
|
}
|
|
|
|
if (code_point <= UINT32_C(0xffff)) {
|
|
write_byte(static_cast<uint8_t>(
|
|
UINT32_C(0xe0) | (code_point >> 12U)));
|
|
write_byte(static_cast<uint8_t>(
|
|
UINT32_C(0x80) |
|
|
((code_point >> 6U) & UINT32_C(0x3f))));
|
|
write_byte(static_cast<uint8_t>(
|
|
UINT32_C(0x80) | (code_point & UINT32_C(0x3f))));
|
|
return;
|
|
}
|
|
|
|
write_byte(static_cast<uint8_t>(
|
|
UINT32_C(0xf0) | (code_point >> 18U)));
|
|
write_byte(static_cast<uint8_t>(
|
|
UINT32_C(0x80) |
|
|
((code_point >> 12U) & UINT32_C(0x3f))));
|
|
write_byte(static_cast<uint8_t>(
|
|
UINT32_C(0x80) |
|
|
((code_point >> 6U) & UINT32_C(0x3f))));
|
|
write_byte(static_cast<uint8_t>(
|
|
UINT32_C(0x80) | (code_point & UINT32_C(0x3f))));
|
|
}
|
|
|
|
void write_utf16(const uint16_t* text, uint32_t text_len) noexcept {
|
|
uint32_t index = 0;
|
|
while (index < text_len) {
|
|
const uint16_t first = text[index++];
|
|
switch (first) {
|
|
case u'\\':
|
|
static_cast<void>(std::fputs("\\\\", stderr));
|
|
continue;
|
|
case u'"':
|
|
static_cast<void>(std::fputs("\\\"", stderr));
|
|
continue;
|
|
case u'\n':
|
|
static_cast<void>(std::fputs("\\n", stderr));
|
|
continue;
|
|
case u'\r':
|
|
static_cast<void>(std::fputs("\\r", stderr));
|
|
continue;
|
|
case u'\t':
|
|
static_cast<void>(std::fputs("\\t", stderr));
|
|
continue;
|
|
default:
|
|
break;
|
|
}
|
|
if (first < UINT16_C(0x20) || first == UINT16_C(0x7f)) {
|
|
static_cast<void>(std::fprintf(
|
|
stderr,
|
|
"\\u%04" PRIX16,
|
|
static_cast<unsigned int>(first)));
|
|
continue;
|
|
}
|
|
if (first >= UINT16_C(0xd800) &&
|
|
first <= UINT16_C(0xdbff)) {
|
|
if (index >= text_len) {
|
|
write_replacement_character();
|
|
break;
|
|
}
|
|
const uint16_t second = text[index];
|
|
if (second < UINT16_C(0xdc00) ||
|
|
second > UINT16_C(0xdfff)) {
|
|
write_replacement_character();
|
|
continue;
|
|
}
|
|
++index;
|
|
const uint32_t code_point =
|
|
UINT32_C(0x10000) +
|
|
((static_cast<uint32_t>(first) -
|
|
UINT32_C(0xd800))
|
|
<< 10U) +
|
|
(static_cast<uint32_t>(second) -
|
|
UINT32_C(0xdc00));
|
|
write_utf8_code_point(code_point);
|
|
continue;
|
|
}
|
|
if (first >= UINT16_C(0xdc00) &&
|
|
first <= UINT16_C(0xdfff)) {
|
|
write_replacement_character();
|
|
continue;
|
|
}
|
|
write_utf8_code_point(static_cast<uint32_t>(first));
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void trace_native_stage(const char* stage) noexcept {
|
|
if (!native_trace_enabled()) {
|
|
return;
|
|
}
|
|
std::fprintf(
|
|
stderr,
|
|
"%s stage=%s\n",
|
|
kTracePrefix,
|
|
stage == nullptr ? "<null>" : stage);
|
|
flush_trace();
|
|
}
|
|
|
|
void trace_native_hresult(
|
|
const char* stage,
|
|
int32_t hresult) noexcept {
|
|
if (!native_trace_enabled()) {
|
|
return;
|
|
}
|
|
std::fprintf(
|
|
stderr,
|
|
"%s stage=%s hresult=0x%08" PRIX32 "\n",
|
|
kTracePrefix,
|
|
stage == nullptr ? "<null>" : stage,
|
|
static_cast<uint32_t>(hresult));
|
|
flush_trace();
|
|
}
|
|
|
|
void trace_native_result(
|
|
const char* stage,
|
|
NavopRdpResult result) noexcept {
|
|
if (!native_trace_enabled()) {
|
|
return;
|
|
}
|
|
std::fprintf(
|
|
stderr,
|
|
"%s stage=%s result=%" PRId32 "\n",
|
|
kTracePrefix,
|
|
stage == nullptr ? "<null>" : stage,
|
|
result);
|
|
flush_trace();
|
|
}
|
|
|
|
void trace_native_win32(
|
|
const char* stage,
|
|
uint32_t win32_code) noexcept {
|
|
if (!native_trace_enabled()) {
|
|
return;
|
|
}
|
|
std::fprintf(
|
|
stderr,
|
|
"%s stage=%s win32=0x%08" PRIX32 "\n",
|
|
kTracePrefix,
|
|
stage == nullptr ? "<null>" : stage,
|
|
win32_code);
|
|
flush_trace();
|
|
}
|
|
|
|
void trace_native_pointer(
|
|
const char* stage,
|
|
uintptr_t pointer) noexcept {
|
|
if (!native_trace_enabled()) {
|
|
return;
|
|
}
|
|
std::fprintf(
|
|
stderr,
|
|
"%s stage=%s pointer=0x%" PRIXPTR "\n",
|
|
kTracePrefix,
|
|
stage == nullptr ? "<null>" : stage,
|
|
pointer);
|
|
flush_trace();
|
|
}
|
|
|
|
void trace_native_rect(
|
|
const char* stage,
|
|
int32_t left,
|
|
int32_t top,
|
|
int32_t right,
|
|
int32_t bottom) noexcept {
|
|
if (!native_trace_enabled()) {
|
|
return;
|
|
}
|
|
std::fprintf(
|
|
stderr,
|
|
"%s stage=%s rect={left=%" PRId32 ",top=%" PRId32
|
|
",right=%" PRId32 ",bottom=%" PRId32 "}\n",
|
|
kTracePrefix,
|
|
stage == nullptr ? "<null>" : stage,
|
|
left,
|
|
top,
|
|
right,
|
|
bottom);
|
|
flush_trace();
|
|
}
|
|
|
|
void trace_native_window(
|
|
const char* stage,
|
|
uint32_t index,
|
|
uintptr_t window,
|
|
uintptr_t parent,
|
|
uint32_t visible,
|
|
uintptr_t style,
|
|
uintptr_t ex_style,
|
|
int32_t left,
|
|
int32_t top,
|
|
int32_t right,
|
|
int32_t bottom,
|
|
const uint16_t* class_name,
|
|
uint32_t class_name_len) noexcept {
|
|
if (!native_trace_enabled()) {
|
|
return;
|
|
}
|
|
std::fprintf(
|
|
stderr,
|
|
"%s stage=%s index=%" PRIu32 " pointer=0x%" PRIXPTR
|
|
" parent=0x%" PRIXPTR " visible=%" PRIu32
|
|
" style=0x%" PRIXPTR " ex_style=0x%" PRIXPTR
|
|
" rect={left=%" PRId32 ",top=%" PRId32
|
|
",right=%" PRId32 ",bottom=%" PRId32 "} class=\"",
|
|
kTracePrefix,
|
|
stage == nullptr ? "<null>" : stage,
|
|
index,
|
|
window,
|
|
parent,
|
|
visible,
|
|
style,
|
|
ex_style,
|
|
left,
|
|
top,
|
|
right,
|
|
bottom);
|
|
if (class_name != nullptr && class_name_len != UINT32_C(0)) {
|
|
write_utf16(class_name, class_name_len);
|
|
}
|
|
static_cast<void>(std::fputs("\"\n", stderr));
|
|
flush_trace();
|
|
}
|
|
|
|
void trace_native_utf16(
|
|
const char* stage,
|
|
int32_t hresult,
|
|
const uint16_t* text,
|
|
uint32_t text_len) noexcept {
|
|
if (!native_trace_enabled()) {
|
|
return;
|
|
}
|
|
std::fprintf(
|
|
stderr,
|
|
"%s stage=%s hresult=0x%08" PRIX32 " text=\"",
|
|
kTracePrefix,
|
|
stage == nullptr ? "<null>" : stage,
|
|
static_cast<uint32_t>(hresult));
|
|
if (text != nullptr && text_len != UINT32_C(0)) {
|
|
write_utf16(text, text_len);
|
|
}
|
|
static_cast<void>(std::fputs("\"\n", stderr));
|
|
flush_trace();
|
|
}
|
|
|
|
void log_native_host_window_destroy(
|
|
const char* stage,
|
|
uintptr_t host_window,
|
|
uintptr_t parent_window,
|
|
uint64_t generation,
|
|
uint32_t window_thread_id,
|
|
int32_t destroyed,
|
|
uint32_t win32_code,
|
|
uint32_t window_still_alive) noexcept {
|
|
// A successful destroy is routine and stays behind the diagnostics switch.
|
|
// A failed one means the ATL host HWND outlives its owning object, which
|
|
// leaks a top-level window (and its subclasses) for the rest of the
|
|
// process, so it is always reported even in a release build.
|
|
if (destroyed != 0 && !native_trace_enabled()) {
|
|
return;
|
|
}
|
|
std::fprintf(
|
|
stderr,
|
|
"%s stage=%s host_window=0x%" PRIXPTR " parent_window=0x%" PRIXPTR
|
|
" generation=%" PRIu64 " destroy_window_called=1 destroy_window_result=%"
|
|
PRId32 " win32=0x%08" PRIX32 " owner_thread=%" PRIu32
|
|
" current_thread=%" PRIu32 " window_still_alive=%" PRIu32 "\n",
|
|
kTracePrefix,
|
|
stage == nullptr ? "<null>" : stage,
|
|
host_window,
|
|
parent_window,
|
|
generation,
|
|
destroyed,
|
|
win32_code,
|
|
window_thread_id,
|
|
static_cast<uint32_t>(GetCurrentThreadId()),
|
|
window_still_alive);
|
|
flush_trace();
|
|
}
|