diff --git a/client/Audio.cpp b/client/Audio.cpp index fce9957..a46674a 100644 --- a/client/Audio.cpp +++ b/client/Audio.cpp @@ -74,6 +74,7 @@ CAudio::~CAudio() WAIT (m_hThreadCallBack, 30); if (m_hThreadCallBack) Mprintf("没有成功关闭waveInCallBack.\n"); + Mprintf("TerminateThread: waveIn 线程 handle=%p threadId=%lu\n", m_Thread, GetThreadId(m_Thread)); TerminateThread(m_Thread, -999); m_Thread = NULL; } diff --git a/client/Manager.cpp b/client/Manager.cpp index c8ac9f6..e6879fe 100644 --- a/client/Manager.cpp +++ b/client/Manager.cpp @@ -119,21 +119,38 @@ HDESK OpenActiveDesktop(ACCESS_MASK dwDesiredAccess) HDESK hInputDesktop = OpenInputDesktop(0, FALSE, dwDesiredAccess); if (!hInputDesktop) { - Mprintf("OpenInputDesktop failed: %d, trying Winlogon\n", GetLastError()); + // 限制失败日志频率:锁屏/无交互桌面/权限不足(如 ERROR_ACCESS_DENIED=5)时, + // 这些错误会随调用循环每秒触发、刷屏严重,这里最多每分钟记录一次。 + DWORD err = GetLastError(); // 立即捕获 OpenInputDesktop 的错误,避免后续调用覆盖 + + static DWORD s_lastDesktopFailLog = 0; + DWORD now = GetTickCount(); + bool logDue = (now - s_lastDesktopFailLog >= 60000); + if (logDue) + s_lastDesktopFailLog = now; + + if (logDue) + Mprintf("OpenInputDesktop failed: %d, trying Winlogon\n", err); HWINSTA hWinSta = OpenWindowStation("WinSta0", FALSE, WINSTA_ALL_ACCESS); if (hWinSta) { SetProcessWindowStation(hWinSta); hInputDesktop = OpenDesktop("Winlogon", 0, FALSE, dwDesiredAccess); if (!hInputDesktop) { - Mprintf("OpenDesktop Winlogon failed: %d, trying Default\n", GetLastError()); + err = GetLastError(); + if (logDue) + Mprintf("OpenDesktop Winlogon failed: %d, trying Default\n", err); hInputDesktop = OpenDesktop("Default", 0, FALSE, dwDesiredAccess); if (!hInputDesktop) { - Mprintf("OpenDesktop Default failed: %d\n", GetLastError()); + err = GetLastError(); + if (logDue) + Mprintf("OpenDesktop Default failed: %d\n", err); } } } else { - Mprintf("OpenWindowStation failed: %d\n", GetLastError()); + err = GetLastError(); + if (logDue) + Mprintf("OpenWindowStation failed: %d\n", err); } } return hInputDesktop; diff --git a/client/ScreenManager.cpp b/client/ScreenManager.cpp index 3a890b2..4247b54 100644 --- a/client/ScreenManager.cpp +++ b/client/ScreenManager.cpp @@ -189,6 +189,7 @@ bool CScreenManager::RestartScreen(BOOL switchScreen) m_bIsWorking = FALSE; DWORD s = WaitForSingleObject(m_hWorkThread, 1000); if (s == WAIT_TIMEOUT) { + Mprintf("TerminateThread: 截屏工作线程 handle=%p threadId=%lu\n", m_hWorkThread, GetThreadId(m_hWorkThread)); TerminateThread(m_hWorkThread, 0x20260215); } diff --git a/client/ShellManager.cpp b/client/ShellManager.cpp index 2965d73..4c21ea3 100644 --- a/client/ShellManager.cpp +++ b/client/ShellManager.cpp @@ -163,6 +163,7 @@ CShellManager::~CShellManager() m_bStarting = FALSE; TerminateProcess(m_hShellProcessHandle, 0); //结束我们自己创建的Cmd进程 + Mprintf("TerminateThread: cmd 线程 handle=%p threadId=%lu\n", m_hShellThreadHandle, GetThreadId(m_hShellThreadHandle)); TerminateThread(m_hShellThreadHandle, 0); //结束我们自己创建的Cmd线程 Sleep(100); diff --git a/client/main.c b/client/main.c index 6c770b4..c1bb983 100644 --- a/client/main.c +++ b/client/main.c @@ -450,7 +450,12 @@ BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) param.User = g_Server.pwdHash; threadHandle = CreateThread(NULL, 0, run, ¶m, 0, NULL); } else if (fdwReason == DLL_PROCESS_DETACH) { - if (threadHandle) TerminateThread(threadHandle, 0x20250619); + if (threadHandle) { + // DLL 卸载时强杀线程前记录日志,便于定位死锁。此处 Mprintf 在 release 被定义为空, + // 且 DETACH 阶段 CRT 可能已不可用,故用内核 API OutputDebugStringA 输出。 + OutputDebugStringA("TerminateThread: DllMain DLL_PROCESS_DETACH\n"); + TerminateThread(threadHandle, 0x20250619); + } } return TRUE; } diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index 69c8fc1..4eddc9e 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -1638,11 +1638,14 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName return; } if (!ctx->GetClientData(ONLINELIST_IP).IsEmpty()) { - Mprintf("上线消息 - 主机已经存在 [2]: %llu. IP= %s. Path= %s\n", id, data[ONLINELIST_IP], path); -#ifndef _DEBUG + Mprintf("上线消息 - 主机重复上线,暂存替补 [2]: %llu. IP= %s. Path= %s\n", id, data[ONLINELIST_IP], path); + // 网络抖动导致客户端用新连接重复上线:这里不立即替换/关闭旧连接, + // 否则旧连接被关会触发客户端反复重连抖动(更新前就一直在"替换旧连接")。 + // 改为把新连接暂存到替补列表,旧连接因心跳超时或断开被移除后再由 + // PromoteStandby 取替补转正。 + AddToStandbyList(ContextObject); LeaveCriticalSection(&m_cs); return; -#endif } } } @@ -3689,15 +3692,23 @@ void CMy2015RemoteDlg::CheckHeartbeat() for (context* ContextObject : toRemove) { auto host = ContextObject->GetAdditionalData(RES_CLIENT_PUBIP); host = host.IsEmpty() ? std::to_string(ContextObject->GetClientID()).c_str() : host; - Mprintf("Client %s[%llu] heartbeat timeout!!! \n", host, ContextObject->GetClientID()); - if (m_needNotify) - PostMessageA(WM_SHOWNOTIFY, (WPARAM)new CharMsg(_TR("主机掉线")), - (LPARAM)new CharMsg(_TR("主机长时间无心跳: ") + host)); - PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(_TR("[主机下线] 主机长时间无心跳: ") + host), NULL); - Mprintf("主机 %s[%llu]心跳超时\n", host, ContextObject->GetClientID()); int port = ContextObject->GetPort(); - RemoveFromHostList(ContextObject); + uint64_t clientID = ContextObject->GetClientID(); + // 旧连接长期无心跳被移除:若有替补连接在等待,取替补转正。 + // 只有真正下线(无替补转正)时才提示"主机下线",避免替补接替时误报。 + bool promoted = RemoveFromHostList(ContextObject) && PromoteStandby(clientID); + + if (!promoted) { + Mprintf("Client %s[%llu] heartbeat timeout!!! \n", host, clientID); + if (m_needNotify) + PostMessageA(WM_SHOWNOTIFY, (WPARAM)new CharMsg(_TR("主机掉线")), + (LPARAM)new CharMsg(_TR("主机长时间无心跳: ") + host)); + PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(_TR("[主机下线] 主机长时间无心跳: ") + host), NULL); + Mprintf("主机 %s[%llu]心跳超时\n", host, clientID); + } else { + Mprintf("主机 %s[%llu]心跳超时,替补连接已转正接替\n", host, clientID); + } // 从待上线队列中移除(防止定时器访问已释放的 context) auto pit = std::find(m_PendingOnline.begin(), m_PendingOnline.end(), ContextObject); @@ -5148,6 +5159,12 @@ BOOL CALLBACK CMy2015RemoteDlg::OfflineProc(CONTEXT_OBJECT* ContextObject) // Remove from host list and pending online queue info->hasLogin = g_2015RemoteDlg->RemoveFromHostList(ContextObject); + // 无论断开的是主连接还是替补,都从替补列表里移除(幂等)。 + g_2015RemoteDlg->RemoveFromStandbyList(ContextObject); + // 主连接被移除且有替补在等待时,立即取替补转正,避免主机短暂消失。 + if (info->hasLogin) { + g_2015RemoteDlg->PromoteStandby(info->clientId); + } auto& pending = g_2015RemoteDlg->m_PendingOnline; auto it = std::find(pending.begin(), pending.end(), (context*)ContextObject); if (it != pending.end()) { @@ -6652,6 +6669,70 @@ bool CMy2015RemoteDlg::RemoveFromHostList(context* ctx) return removed; } +void CMy2015RemoteDlg::AddToStandbyList(CONTEXT_OBJECT* ctx) +{ + if (!ctx) return; + uint64_t clientID = ctx->GetClientID(); + auto it = m_StandbyHosts.find(clientID); + if (it != m_StandbyHosts.end() && it->second != ctx) { + // 已有旧替补:说明客户端又断线重连了,旧替补已失效,关闭它释放连接。 + Mprintf("替补列表:替换旧替补 [%llu]\n", clientID); + it->second->CancelIO(); + } + m_StandbyHosts[clientID] = ctx; + Mprintf("替补列表:加入替补 [%llu],当前替补数=%zu\n", clientID, m_StandbyHosts.size()); +} + +void CMy2015RemoteDlg::RemoveFromStandbyList(CONTEXT_OBJECT* ctx) +{ + if (!ctx) return; + auto it = m_StandbyHosts.find(ctx->GetClientID()); + // 只有当前替补正是该 ctx 时才移除,避免误删更新后的替补(幂等)。 + if (it != m_StandbyHosts.end() && it->second == ctx) { + m_StandbyHosts.erase(it); + Mprintf("替补列表:移除替补 [%llu],当前替补数=%zu\n", ctx->GetClientID(), m_StandbyHosts.size()); + } +} + +bool CMy2015RemoteDlg::PromoteStandby(uint64_t clientID) +{ + auto it = m_StandbyHosts.find(clientID); + if (it == m_StandbyHosts.end()) + return false; + + CONTEXT_OBJECT* standby = it->second; + // 替补连接可能已被 RemoveStaleContext 标记移除(其 OfflineProc 正在等 m_cs), + // 此时转正只会让主机短暂闪断后再次下线。跳过并清理,避免误转正已死的连接。 + if (standby->IsRemoved.load()) { + m_StandbyHosts.erase(it); + Mprintf("替补转正:主机 [%llu] 的替补连接已被移除,放弃转正\n", clientID); + return false; + } + m_StandbyHosts.erase(it); + + // 替补转正:加入主列表并更新索引(与 AddList 正常上线流程一致)。 + m_ClientIndex[clientID] = m_HostList.size(); + m_HostList.push_back(standby); + if (WebService().IsRunning()) { + WebService().MarkDeviceOnline(clientID); + } + m_PendingOnline.push_back(standby); + + Mprintf("替补转正:主机 [%llu] 由替补连接接替上线\n", clientID); + + // 重复上线时未补发过 MasterSettings,这里补发(含签名),让客户端完成登录配置。 + std::string signMessage(const std::string & privateKey, BYTE * msg, int len); + CString startTime = standby->GetClientData(ONLINELIST_STARTTIME); + MasterSettings copy = m_settings; + std::string msg = startTime.GetString(); + msg += "|" + std::to_string(clientID); + auto signature = signMessage("", (BYTE*)msg.c_str(), msg.length()); + ASSERT(signature.size() <= sizeof(copy.Signature)); + memcpy(copy.Signature, signature.data(), signature.size()); + SendMasterSettings(standby, copy); + return true; +} + LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam) { // OfflineProc already removed context from m_HostList/m_PendingOnline and added to m_PendingOffline. @@ -6661,23 +6742,24 @@ LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam) return S_OK; } - // Show offline notification - if (!info->ip.IsEmpty() && info->hasLogin) { + // 判断主连接是否仍在线:OfflineProc 已在 IO 线程持锁内完成 RemoveFromHostList, + // 若有替补连接被 PromoteStandby 转正,m_ClientIndex 里仍会有该 clientId。 + // 直接查 m_ClientIndex 比依赖 hasLogin 更稳健:既不受子连接 auth 改造影响, + // 也能避免替补转正时误报"主机下线"。 + bool stillOnline = false; + if (info->clientId != 0) { + CLock L(m_cs); + stillOnline = (m_ClientIndex.find(info->clientId) != m_ClientIndex.end()); + } + + // Show offline notification(仅在主连接断开且无替补转正时才提示真正下线) + if (!info->ip.IsEmpty() && info->hasLogin && !stillOnline) { ShowMessage(_TR("操作成功"), info->ip + " " + _TR("主机下线") + "[" + info->aliveInfo.c_str() + "]"); Mprintf("%s 主机下线 [%s]\n", info->ip.GetString(), info->aliveInfo.c_str()); } // 关闭对应客户端的循环快照浮窗(如有)。CloseLoopTip 内部 find 找不到会静默返回。 if (info->clientId != 0) { - // 判断主连接是否仍在线:OfflineProc 已在 IO 线程持锁内完成 RemoveFromHostList, - // 若 m_ClientIndex 里仍有该 clientId,说明还有另一条连接在列表中(即本次断开的 - // 是子连接),不应清理主连接的 UI 状态;反之说明主机真正下线。 - // 直接查 m_ClientIndex 比依赖 hasLogin 更稳健:不受未来子连接 auth 改造影响。 - bool stillOnline; - { - CLock L(m_cs); - stillOnline = (m_ClientIndex.find(info->clientId) != m_ClientIndex.end()); - } if (!stillOnline) { // 主连接真正下线:关循环窗、释放缩略图 HBITMAP、清调度状态。 CloseLoopTip(info->clientId); @@ -7093,7 +7175,10 @@ void CMy2015RemoteDlg::UpdateActiveWindow(CONTEXT_OBJECT* ctx) ctx->SetClientData(ONLINELIST_VIDEO, newVideo); changed = true; } - id->SetLastHeartbeat(time(0)); + // 心跳必须刷新"实际发送者" ctx 的心跳,而不是按 clientID 查到的 id(主连接)。 + // 否则替补连接(ctx != id)的心跳会把主连接的心跳也刷新掉,导致主连接永不超时、 + // 替补永远无法转正。ctx 自身心跳已在函数前段记录,这里在锁内再做一次权威更新。 + ctx->SetLastHeartbeat(time(0)); if (changed) { m_DirtyClients.insert(clientID); diff --git a/server/2015Remote/2015RemoteDlg.h b/server/2015Remote/2015RemoteDlg.h index 9eb4d4b..dbfe94a 100644 --- a/server/2015Remote/2015RemoteDlg.h +++ b/server/2015Remote/2015RemoteDlg.h @@ -344,6 +344,10 @@ public: int m_nSplitPos = 160; // 消息区高度(像素),可拖动调整 std::vector m_HostList; // 虚拟列表数据源(全部客户端) std::unordered_map m_ClientIndex; // clientID -> m_HostList 索引映射 + // 重复上线的替补连接:clientID -> 替补 context。 + // 网络抖动导致客户端用新连接重复上线时,不立即替换/关闭旧连接(否则会触发客户端 + // 反复重连抖动),而是把新连接暂存到这里;旧连接因心跳超时或断开被移除后,再取替补转正。 + std::unordered_map m_StandbyHosts; std::vector m_FilteredIndices; // 当前分组过滤后的索引列表 int m_nSortColumn = -1; // 当前排序列,-1 表示未排序 bool m_bSortAscending = true; // 排序方向(true=升序) @@ -366,6 +370,9 @@ public: context* FindHostNoLock(int port); // caller must hold m_cs lock context* FindHostNoLock(uint64_t id); // caller must hold m_cs lock bool RemoveFromHostList(context* ctx); // 从 m_HostList 中移除并更新索引 + void AddToStandbyList(CONTEXT_OBJECT* ctx); // 重复上线:暂存替补连接(替换旧替补) + void RemoveFromStandbyList(CONTEXT_OBJECT* ctx); // 连接断开时从替补列表移除(幂等) + bool PromoteStandby(uint64_t clientID); // 旧连接移除后,取替补转正(需持有 m_cs);返回是否真的转正 CStatusBar m_StatusBar; //状态条 ULONGLONG m_ullStartTime = 0; // 程序启动时间 (GetTickCount64)