Skip to content

Commit 42e5301

Browse files
bmehta001Copilot
andcommitted
TEMP DIAG: passive hang watchdog with thread stack dump (do not merge)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 933ad80 commit 42e5301

4 files changed

Lines changed: 238 additions & 4 deletions

File tree

‎.github/workflows/test-win-latest.yml‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ jobs:
3636
runs-on: ${{ matrix.os }}
3737
strategy:
3838
matrix:
39-
arch: [Win32, x64]
40-
build: [Release, Debug]
39+
arch: [Win32]
40+
build: [Release]
4141
os: [windows-2022]
4242

4343
steps:
@@ -53,8 +53,20 @@ jobs:
5353

5454
- name: Test ${{ matrix.arch }} ${{ matrix.build }}
5555
shell: cmd
56+
env:
57+
FUNCTESTS_WATCHDOG_SEC: '420'
5658
run: build-tests.cmd ${{ matrix.arch }} ${{ matrix.build }}
5759

60+
- name: Upload FuncTests binaries for offline symbolization
61+
if: always()
62+
uses: actions/upload-artifact@v4
63+
with:
64+
name: functests-${{ matrix.arch }}-${{ matrix.build }}
65+
path: |
66+
Solutions/out/${{ matrix.build }}/${{ matrix.arch }}/FuncTests/FuncTests.exe
67+
Solutions/out/${{ matrix.build }}/${{ matrix.arch }}/FuncTests/FuncTests.pdb
68+
if-no-files-found: warn
69+
5870
public-headers:
5971
name: Public header gate (MSVC)
6072
runs-on: windows-2022

‎tests/functests/FuncTests.vcxproj‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@
208208
<AdditionalOptions> /machine:X86 %(AdditionalOptions)</AdditionalOptions>
209209
<AdditionalDependencies>crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;wininet.lib;iphlpapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
210210
<AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
211-
<GenerateDebugInformation>No</GenerateDebugInformation>
211+
<GenerateDebugInformation>DebugFull</GenerateDebugInformation>
212212
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries)</IgnoreSpecificDefaultLibraries>
213213
<SubSystem>Console</SubSystem>
214214
<Version>
@@ -430,6 +430,7 @@
430430
<ClInclude Include="$(ProjectDir)..\common\MockISqlite3Proxy.hpp" />
431431
<ClInclude Include="$(ProjectDir)..\common\SocketTools.hpp" />
432432
<ClCompile Include="$(ProjectDir)\BasicFuncTests.cpp" />
433+
<ClCompile Include="$(ProjectDir)\HangWatchdog.cpp" />
433434
<ClCompile Include="$(ProjectDir)\Main.cpp" />
434435
<ClCompile Include="$(ProjectDir)\MultipleLogManagersTests.cpp" />
435436
<ClCompile Include="$(ProjectDir)..\common\Common.cpp" />
@@ -466,4 +467,4 @@
466467
<Target Name="CopyJson" AfterTargets="Build" Condition="exists('$(ProjectDir)..\..\lib\modules\exp\tests\functests\test.json')">
467468
<Copy SourceFiles="$(ProjectDir)..\..\lib\modules\exp\tests\functests\test.json" DestinationFolder="$(OutDir)" />
468469
</Target>
469-
</Project>
470+
</Project>

‎tests/functests/HangWatchdog.cpp‎

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
//
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
// TEMPORARY CI DIAGNOSTIC -- not for merge.
6+
// Dumps native stacks of every thread when a single test exceeds a deadline,
7+
// so a CI-only hang can be root-caused without a debugger on the runner.
8+
//
9+
#ifdef _WIN32
10+
11+
#include <windows.h>
12+
#include <dbghelp.h>
13+
#include <tlhelp32.h>
14+
15+
#include <atomic>
16+
#include <chrono>
17+
#include <cstdio>
18+
#include <cstdlib>
19+
#include <thread>
20+
#include <vector>
21+
22+
#pragma comment(lib, "dbghelp.lib")
23+
24+
namespace {
25+
26+
std::atomic<long long> g_testStartMs{0};
27+
28+
long long NowMs()
29+
{
30+
return std::chrono::duration_cast<std::chrono::milliseconds>(
31+
std::chrono::steady_clock::now().time_since_epoch())
32+
.count();
33+
}
34+
35+
void DumpAllThreadStacks()
36+
{
37+
HANDLE proc = ::GetCurrentProcess();
38+
::SymSetOptions(SYMOPT_UNDNAME | SYMOPT_LOAD_LINES);
39+
char exeDir[MAX_PATH] = {0};
40+
::GetModuleFileNameA(NULL, exeDir, MAX_PATH);
41+
char* lastSlash = strrchr(exeDir, '\\');
42+
if (lastSlash != nullptr)
43+
{
44+
*lastSlash = '\0';
45+
}
46+
::SymInitialize(proc, exeDir, TRUE);
47+
48+
DWORD pid = ::GetCurrentProcessId();
49+
DWORD selfTid = ::GetCurrentThreadId();
50+
HANDLE snap = ::CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
51+
if (snap == INVALID_HANDLE_VALUE)
52+
{
53+
printf("watchdog: CreateToolhelp32Snapshot failed %lu\n", ::GetLastError());
54+
return;
55+
}
56+
57+
THREADENTRY32 te;
58+
te.dwSize = sizeof(te);
59+
if (!::Thread32First(snap, &te))
60+
{
61+
::CloseHandle(snap);
62+
return;
63+
}
64+
65+
do
66+
{
67+
if (te.th32OwnerProcessID != pid || te.th32ThreadID == selfTid)
68+
{
69+
continue;
70+
}
71+
72+
HANDLE th = ::OpenThread(
73+
THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION | THREAD_SUSPEND_RESUME,
74+
FALSE, te.th32ThreadID);
75+
if (th == NULL)
76+
{
77+
continue;
78+
}
79+
80+
std::vector<DWORD64> frames;
81+
::SuspendThread(th);
82+
CONTEXT ctx;
83+
memset(&ctx, 0, sizeof(ctx));
84+
ctx.ContextFlags = CONTEXT_FULL;
85+
if (::GetThreadContext(th, &ctx))
86+
{
87+
STACKFRAME64 f;
88+
memset(&f, 0, sizeof(f));
89+
#ifdef _M_IX86
90+
DWORD machine = IMAGE_FILE_MACHINE_I386;
91+
f.AddrPC.Offset = ctx.Eip;
92+
f.AddrFrame.Offset = ctx.Ebp;
93+
f.AddrStack.Offset = ctx.Esp;
94+
#else
95+
DWORD machine = IMAGE_FILE_MACHINE_AMD64;
96+
f.AddrPC.Offset = ctx.Rip;
97+
f.AddrFrame.Offset = ctx.Rbp;
98+
f.AddrStack.Offset = ctx.Rsp;
99+
#endif
100+
f.AddrPC.Mode = AddrModeFlat;
101+
f.AddrFrame.Mode = AddrModeFlat;
102+
f.AddrStack.Mode = AddrModeFlat;
103+
104+
for (int i = 0; i < 64; i++)
105+
{
106+
if (!::StackWalk64(machine, proc, th, &f, &ctx, NULL,
107+
::SymFunctionTableAccess64, ::SymGetModuleBase64, NULL))
108+
{
109+
break;
110+
}
111+
if (f.AddrPC.Offset == 0)
112+
{
113+
break;
114+
}
115+
frames.push_back(f.AddrPC.Offset);
116+
}
117+
}
118+
::ResumeThread(th);
119+
::CloseHandle(th);
120+
121+
printf("\n--- thread %lu (%zu frames) ---\n",
122+
static_cast<unsigned long>(te.th32ThreadID), frames.size());
123+
for (size_t i = 0; i < frames.size(); i++)
124+
{
125+
alignas(8) char buf[sizeof(SYMBOL_INFO) + 512];
126+
memset(buf, 0, sizeof(buf));
127+
SYMBOL_INFO* sym = reinterpret_cast<SYMBOL_INFO*>(buf);
128+
sym->SizeOfStruct = sizeof(SYMBOL_INFO);
129+
sym->MaxNameLen = 500;
130+
131+
char modName[MAX_PATH] = "?";
132+
DWORD64 modBase = 0;
133+
IMAGEHLP_MODULE64 mi;
134+
memset(&mi, 0, sizeof(mi));
135+
mi.SizeOfStruct = sizeof(mi);
136+
int symType = -1;
137+
if (::SymGetModuleInfo64(proc, frames[i], &mi))
138+
{
139+
strncpy_s(modName, mi.ModuleName, _TRUNCATE);
140+
modBase = mi.BaseOfImage;
141+
symType = static_cast<int>(mi.SymType);
142+
}
143+
unsigned long long rva = (modBase != 0)
144+
? static_cast<unsigned long long>(frames[i] - modBase) : 0ull;
145+
146+
DWORD64 disp = 0;
147+
if (::SymFromAddr(proc, frames[i], &disp, sym))
148+
{
149+
printf(" %02zu %s+0x%llx %s!%s+0x%llx\n", i, modName, rva, modName, sym->Name,
150+
static_cast<unsigned long long>(disp));
151+
}
152+
else
153+
{
154+
printf(" %02zu %s+0x%llx (symtype=%d)\n", i, modName, rva, symType);
155+
}
156+
}
157+
fflush(stdout);
158+
} while (::Thread32Next(snap, &te));
159+
160+
::CloseHandle(snap);
161+
}
162+
163+
} // namespace
164+
165+
void FuncTestsWatchdog_NoteTestStart()
166+
{
167+
g_testStartMs.store(NowMs());
168+
}
169+
170+
void FuncTestsWatchdog_Start()
171+
{
172+
const char* env = nullptr;
173+
size_t len = 0;
174+
char buffer[32] = {0};
175+
if (::getenv_s(&len, buffer, sizeof(buffer), "FUNCTESTS_WATCHDOG_SEC") == 0 && len > 1)
176+
{
177+
env = buffer;
178+
}
179+
unsigned seconds = (env != nullptr) ? static_cast<unsigned>(atoi(env)) : 0u;
180+
if (seconds == 0)
181+
{
182+
return;
183+
}
184+
185+
g_testStartMs.store(NowMs());
186+
std::thread([seconds]() {
187+
for (;;)
188+
{
189+
std::this_thread::sleep_for(std::chrono::seconds(5));
190+
long long started = g_testStartMs.load();
191+
if (started == 0)
192+
{
193+
continue;
194+
}
195+
if ((NowMs() - started) > static_cast<long long>(seconds) * 1000)
196+
{
197+
printf("\n#### WATCHDOG: current test exceeded %u s -- dump #1 ####\n", seconds);
198+
fflush(stdout);
199+
DumpAllThreadStacks();
200+
std::this_thread::sleep_for(std::chrono::seconds(45));
201+
printf("\n#### WATCHDOG: dump #2 (45 s later; identical stacks == truly stuck) ####\n");
202+
fflush(stdout);
203+
DumpAllThreadStacks();
204+
printf("\n#### WATCHDOG: terminating process ####\n");
205+
fflush(stdout);
206+
::TerminateProcess(::GetCurrentProcess(), 99);
207+
}
208+
}
209+
}).detach();
210+
}
211+
212+
#else
213+
void FuncTestsWatchdog_NoteTestStart() {}
214+
void FuncTestsWatchdog_Start() {}
215+
#endif

‎tests/functests/Main.cpp‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@
99
extern "C" unsigned long __stdcall IsDebuggerPresent();
1010
#endif
1111

12+
// TEMPORARY CI DIAGNOSTIC -- see HangWatchdog.cpp
13+
void FuncTestsWatchdog_NoteTestStart();
14+
void FuncTestsWatchdog_Start();
15+
1216
class TestStatusLogger : public testing::EmptyTestEventListener {
1317
public:
1418
virtual void OnTestStart(testing::TestInfo const& test) override
1519
{
1620
std::ignore = test;
21+
FuncTestsWatchdog_NoteTestStart();
1722
LOG_INFO("--- %s.%s", test.test_case_name(), test.name());
1823
}
1924

@@ -35,6 +40,7 @@ int MAIN_CDECL main(int argc, char** argv)
3540
{
3641
::testing::InitGoogleMock(&argc, argv);
3742
::testing::UnitTest::GetInstance()->listeners().Append(new TestStatusLogger());
43+
FuncTestsWatchdog_Start();
3844

3945
int result = RUN_ALL_TESTS();
4046

0 commit comments

Comments
 (0)