Improve: Park duplicate online connections as standby, promote on old removal

Replacing the old connection on a duplicate login made the client
reconnect in a tight loop. Instead, park the new connection in a standby
list keyed by clientID and promote it into the main host list only after
the old connection is removed by heartbeat timeout or disconnect.

Fix heartbeat attribution so a parked standby refreshes its own
last-heartbeat time rather than the main connection's, otherwise the main
never times out and the standby can never take over. Gate the offline
notification on an actual removal with no promoted standby to avoid a
spurious "host offline" when takeover happens. Skip promoting a standby
already marked removed, and reuse the normal login sign/settings path when
a standby is promoted.

Co-Authored-By: deepseek-v4-pro
This commit is contained in:
yuanyuanxiang
2026-08-21 19:35:43 +02:00
parent d359148841
commit 8de5c00a39
7 changed files with 144 additions and 27 deletions

View File

@@ -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;
}

View File

@@ -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;

View File

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

View File

@@ -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);

View File

@@ -450,7 +450,12 @@ BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
param.User = g_Server.pwdHash;
threadHandle = CreateThread(NULL, 0, run, &param, 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;
}

View File

@@ -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);

View File

@@ -344,6 +344,10 @@ public:
int m_nSplitPos = 160; // 消息区高度(像素),可拖动调整
std::vector<context*> m_HostList; // 虚拟列表数据源(全部客户端)
std::unordered_map<uint64_t, size_t> m_ClientIndex; // clientID -> m_HostList 索引映射
// 重复上线的替补连接clientID -> 替补 context。
// 网络抖动导致客户端用新连接重复上线时,不立即替换/关闭旧连接(否则会触发客户端
// 反复重连抖动),而是把新连接暂存到这里;旧连接因心跳超时或断开被移除后,再取替补转正。
std::unordered_map<uint64_t, CONTEXT_OBJECT*> m_StandbyHosts;
std::vector<size_t> 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)