Skip to content

PartFile — part.met written on low disk space, risking truncation/corruption

Implementation

Landed in eMule-main commit 4b4087d (Harden part-file durability and disk-space handling): - CemuleApp::CanWritePartMetFiles(LPCTSTR pszPath, bool bForceRefresh) added to Emule.cpp/h - Checks GetDiskFreeSpaceEx per volume root; caches result to avoid per-file syscalls - PartFile.cpp:1180 guards SavePartFile() with CanWritePartMetFiles before attempting write - DownloadQueueDiskSpaceSeams.h added for testability - Disk-full pause logic added to DownloadQueue.cpp

Summary

PartFile.cpp and PartFileWriteThread.cpp attempt to write .part.met files with no check for available disk space. On a drive with very little free space, the write fails mid-file, producing a partial (truncated) metadata file. eMuleAI v1.3 adds a CanWritePartMetFiles() guard that checks free space per volume before each write, with a per-volume cache to avoid repeated syscalls.

Problem

The existing code attempts the write unconditionally:

// PartFile.cpp — no disk space check before attempting .met write
CStdioFile file;
if (!file.Open(strTmpFile, CFile::modeCreate | CFile::modeWrite | ...)) {
    ...
}
// write proceeds; if disk fills mid-write: partial .met file

A .part.met that is partially written and then not properly replaced (see BUG-009) can look like a valid file to the parser but contain truncated or zero-filled data — causing incorrect progress display, wrong AICH hashes, or download restart.

During shutdown, the write thread may already be stopped. Attempting to flush buffers after the write thread is gone can cause a hang or crash (see BUG-012).

Fix

Part 1: Disk space guard (from eMuleAI)

Add to CemuleApp:

// Minimum free space required for a .met write: MAXMETSIZE bytes
bool CanWritePartMetFiles(LPCTSTR pszPath, bool bForceRefresh = false);

Internally: - Extracts the volume root from pszPath via GetVolumeRootPath() - Queries GetDiskFreeSpaceEx for the volume - If free space < MAXMETSIZE: logs a warning, returns false - Caches result per volume root (avoids syscall per-file); force-refresh after any write failure

Call at the top of SavePartFile() and in PartFileWriteThread before each write:

if (!theApp.CanWritePartMetFiles(GetTmpPath()))
    return false;  // skip write, log, continue

Part 2: Shutdown flush guard (see also BUG-012)

In CPartFile::~CPartFile():

// Only flush if the write thread is still running
const CPartFileWriteThread* pThread = theApp.m_pPartFileWriteThread;
const bool bShutdownWithoutWriteThread =
    theApp.IsClosing() && (!pThread || !pThread->IsRunning());
if (!bShutdownWithoutWriteThread)
    FlushBuffer(false, true);

Acceptance Criteria

  • [x] CanWritePartMetFiles() implemented with volume-root extraction + free-space check
  • [x] SavePartFile() returns false early when disk space check fails
  • [x] PartFileWriteThread skips write when disk space check fails
  • [x] On failure: log line emitted ("Part.met disk-space guard blocked...")
  • [x] Cache invalidated on write failure (force-refresh on next call)
  • [x] No spurious write attempts during shutdown when write thread is already stopped

Current Evidence

  • CemuleApp::CanWritePartMetFiles() resolves the volume identity path, checks current free space against the effective minimum, caches per-volume decisions, and logs Part.met disk-space guard blocked metadata writes... when blocked.
  • CPartFile::SavePartFile() returns false before opening the temporary .part.met.backup write path when CanWritePartMetFiles() rejects the volume.
  • The buffered data flush path checks required free space before queuing writes to CPartFileWriteThread; on insufficient space it throws before handing buffers to the worker.
  • Write failures in SavePartFile() call theApp.InvalidatePartMetWriteGuardCache(m_fullname) before returning.
  • BUG-012 follow-up commit 75a92c2 now applies the shutdown write-thread guard before the first destructor-time flush attempt.
  • Native coverage in part_file_persistence.tests.cpp covers the part.met disk-space floor, cached write permission, force-refresh invalidation, and shutdown flush policy.
  • Latest validation:
  • python -m emule_workspace validate
  • python -m emule_workspace build app --config Debug --platform x64
  • python -m emule_workspace test all --config Debug --platform x64