diff options
| author | Bitflash <btf01@proton.me> | 2026-07-29 12:46:33 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-29 12:46:33 -0300 |
| commit | 6e836ed5cb5f41391e335b4f508dc6c0e8915ba5 (patch) | |
| tree | 77d9175f60192242bbca4ad43b3ae44c7eb827a5 /src | |
| parent | 1227068a504418521f3238acf9c4dca297141d27 (diff) | |
util: stop debug.log dropping lines under concurrency (#29)
OutputDebugStringF resolved its path lazily with no lock:
static string strDebugFile;
if (strDebugFile.empty()) { ... strDebugFile = dir + "\\" + "debug.log"; }
FILE* fileout = fopen(strDebugFile.c_str(), "a");
if (fileout) { ... }
One thread could read that string while another was still assigning to it, and
at startup three threads log within milliseconds of each other. When it raced,
fopen received a torn path, returned NULL, and the `if (fileout)` dropped the
line with no error anywhere. Several threads calling fopen on the same file at
once had no protection either.
Measured over six runs, counting first-statement printfs that are unconditional
and must appear every time:
ThreadBtfAccept started 4/6
ThreadNostrSeed started 5/6
btfpeers: (cached peers) 2/6
Over eight runs after the change, all three are 8/8.
This matters beyond tidiness. A missing line was read as a thread that never
started, and a real investigation was spent on it. Any reasoning of the form
"it is not in the log, so it did not happen" was unsound against this build.
Path resolution and the write now share one critical section. The rest of the
function already had its own for the OutputDebugString accumulation path; only
the file half was unguarded.
Diffstat (limited to 'src')
| -rw-r--r-- | src/util.h | 56 |
1 files changed, 36 insertions, 20 deletions
| @@ -269,29 +269,45 @@ string GetAppDir(); | |||
| 269 | inline int OutputDebugStringF(const char* pszFormat, ...) | 269 | inline int OutputDebugStringF(const char* pszFormat, ...) |
| 270 | { | 270 | { |
| 271 | #if 1 // debug.log always enabled | 271 | #if 1 // debug.log always enabled |
| 272 | // Write to data directory so it's always findable | 272 | // Write to data directory so it's always findable. |
| 273 | static string strDebugFile; | 273 | // |
| 274 | if (strDebugFile.empty()) { | 274 | // Path resolution and the write are both under the lock, because neither |
| 275 | string dir = GetAppDir(); | 275 | // used to be under anything. strDebugFile was filled in lazily inside an |
| 276 | if (!dir.empty()) | 276 | // `if (empty())`, so one thread could read the path while another was |
| 277 | strDebugFile = dir + | 277 | // still assigning to it -- and at startup three threads log within |
| 278 | // milliseconds of each other. When that raced, fopen got a torn path, | ||
| 279 | // returned NULL, and the `if (fileout)` below dropped the line silently. | ||
| 280 | // Measured before this change: unconditional first-statement printfs in | ||
| 281 | // ThreadBtfAccept, ThreadNostrSeed and the cached-peer thread reached the | ||
| 282 | // file in only 4/6, 5/6 and 2/6 of runs respectively. Concluding a thread | ||
| 283 | // never ran because its line was absent was not sound, and the lock also | ||
| 284 | // stops several threads calling fopen on the same file at once. | ||
| 285 | static CCriticalSection cs_debugFile; | ||
| 286 | CRITICAL_BLOCK(cs_debugFile) | ||
| 287 | { | ||
| 288 | static string strDebugFile; | ||
| 289 | if (strDebugFile.empty()) { | ||
| 290 | string dir = GetAppDir(); | ||
| 291 | if (!dir.empty()) | ||
| 292 | strDebugFile = dir + | ||
| 278 | #ifdef _WIN32 | 293 | #ifdef _WIN32 |
| 279 | "\\" | 294 | "\\" |
| 280 | #else | 295 | #else |
| 281 | "/" | 296 | "/" |
| 282 | #endif | 297 | #endif |
| 283 | + "debug.log"; | 298 | + "debug.log"; |
| 284 | else | 299 | else |
| 285 | strDebugFile = "debug.log"; | 300 | strDebugFile = "debug.log"; |
| 286 | } | 301 | } |
| 287 | FILE* fileout = fopen(strDebugFile.c_str(), "a"); | 302 | FILE* fileout = fopen(strDebugFile.c_str(), "a"); |
| 288 | if (fileout) | 303 | if (fileout) |
| 289 | { | 304 | { |
| 290 | va_list arg_ptr; | 305 | va_list arg_ptr; |
| 291 | va_start(arg_ptr, pszFormat); | 306 | va_start(arg_ptr, pszFormat); |
| 292 | vfprintf(fileout, pszFormat, arg_ptr); | 307 | vfprintf(fileout, pszFormat, arg_ptr); |
| 293 | va_end(arg_ptr); | 308 | va_end(arg_ptr); |
| 294 | fclose(fileout); | 309 | fclose(fileout); |
| 310 | } | ||
| 295 | } | 311 | } |
| 296 | 312 | ||
| 297 | #ifndef _WIN32 | 313 | #ifndef _WIN32 |