Files
泡芙 3a240395aa fix(rdp): RDP 关闭超时与编辑器分离任务的生命周期治理
排查文档记录的结论是: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)根因仍未定位,本次未触碰该链路。
2026-09-13 22:48:42 +08:00

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();
}