Skip to content

PartFileWriteThread: catch CIOFailureException so disk-full doesn't abort the process - #499

Merged
mrjimenez merged 1 commit into
amule-project:masterfrom
got3nks:pr-d-write-thread-iofailure
Apr 29, 2026
Merged

PartFileWriteThread: catch CIOFailureException so disk-full doesn't abort the process#499
mrjimenez merged 1 commit into
amule-project:masterfrom
got3nks:pr-d-write-thread-iofailure

Conversation

@got3nks

@got3nks got3nks commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Summary

CPartFileWriteThread::Entry has no try/catch around pBuffer->area.FlushAt(...). CFileArea::FlushAtCFileAutoClose::WriteAt throws CIOFailureException on a disk-full / EIO / permission-denied write. The exception unwinds out of Entry, through wxThreadInternal::PthreadStart, into wxApp::OnUnhandledException, and std::set_terminate's MuleDebug.cpp:108 calls abort() → SIGABRT, the whole process dies.

Reproducer

Saw it in the wild on macOS during a 30 GB download to a near-full target disk. Crash report excerpt — the worker thread that died is the one that should be CPartFileWriteThread:

2026-04-29 13:40:18: PartFile.cpp(3085): WARNING: Not enough free disk-space! Pausing file: testfile-30gb.bin
…
Exception Type:    EXC_CRASH (SIGABRT)
Termination Reason:  Namespace SIGNAL, Code 6, Abort trap: 6
Application Specific Information:
  abort() called

Thread 2 Crashed:
  abort + 148                                  libsystem_c.dylib
  OnUnhandledException() + 840                 MuleDebug.cpp:108
  CamuleApp::OnUnhandledException() + 132      amule.cpp:2083
  wxAppConsoleBase::CallOnUnhandledException()
  wxThreadInternal::PthreadStart(wxThread*)

The disk-space check at PartFile.cpp:3083 is best-effort — it runs once per FlushBuffer on the main thread and only inspects the buffered total, so a write already queued to the worker can still hit the wall before the main thread pauses the file. The bug is latent; the conditions that exercise it are a slow main thread, a high in-flight write count, or both.

Fix

Catch CIOFailureException in the worker:

  • Log to logPartFile with the file name, offset, and length so the failure is diagnosable.
  • Decrement m_iWrites (so any main-thread m_iWrites <= 0 waits don't deadlock).
  • Mark the buffer PB_ERROR. FlushBuffer's existing Phase 2 PB_ERROR handler resets the item to PB_READY for retry. If the disk is genuinely exhausted the next FlushBuffer's CheckFreeDiskSpace pauses the file before the retry cycles further.

Backward compatibility

  • One-file change in CPartFileWriteThread::Entry. No API or wire changes.
  • The retry loop on PB_ERROR is the existing behaviour for the (already-handled) sync-fallback PB_ERROR case at the same call site.
  • No new dependencies; just #include "CFile.h" and <common/Format.h> in the worker TU.

mrjimenez pushed a commit that referenced this pull request Apr 29, 2026
Follow-up to PR #498 (b106e3a PartFile: serialise m_hpartfile
access against the hash thread).  When a download fills the disk,
PartFile.cpp:3083 logs "Not enough free disk-space! Pausing file:
…", calls PauseFile(true) (status -> PS_INSUFFICIENT), and returns.
PR #498's CDownloadQueue::Process paused-drain branch then drives
FlushBuffer for the file every Process tick (~100 ms) because
HasPendingHashWork() still returns true for the dirty m_aChangedPart
entries that were buffered before the disk filled.  Each call
re-enters the same disk-space check, re-logs the warning, and
re-pauses — producing tens of log lines per second until the file
is removed.  Manually clicking Stop on the GUI doesn't help: the
status flag stays PS_INSUFFICIENT, only m_stopped flips, and the
drain branch keys on status.

Drop PS_INSUFFICIENT from the drain set.  PS_PAUSED (user-clicked
pause) keeps its drain — that's the case the branch was added for.
Disk-full files have no productive hash work to do anyway: the
buffered items can't be written, so Phase 1 would just queue them
to the worker, which would catch CIOFailureException (PR #499)
and bounce them back as PB_ERROR.  Leftover m_aChangedPart entries
on a disk-full file are still covered by the destructor sync-hash
drain at shutdown.
…bort

CFileArea::FlushAt -> CFileAutoClose::WriteAt throws
CIOFailureException on a disk-full / EIO / permission-denied write,
and CPartFileWriteThread::Entry has no try/catch around it.  The
exception unwinds out of Entry, through wxThreadInternal::PthreadStart,
into wxApp::OnUnhandledException, which std::set_terminate's
MuleDebug aborts the process.

Reproducer: write enough into the buffered queue that the disk runs
out of space before the main-thread CheckFreeDiskSpace path on the
next FlushBuffer pauses the file (a slow main thread or a high
in-flight write count is enough).  The disk-space check at
PartFile.cpp:3083 is best-effort: it runs once per FlushBuffer on
the main thread and only inspects the buffered total, so a write
already queued to the worker thread can still hit the wall.

Catch the exception in the worker, log to logPartFile, decrement
m_iWrites so main-thread waiters don't deadlock, and mark the buffer
PB_ERROR.  FlushBuffer's existing Phase 2 PB_ERROR handler resets
the item to PB_READY for retry; if the disk is genuinely exhausted
the next FlushBuffer's CheckFreeDiskSpace pauses the file before the
retry cycles further.
@got3nks
got3nks force-pushed the pr-d-write-thread-iofailure branch from 2b514d0 to 6ca91a9 Compare April 29, 2026 13:48
@mrjimenez
mrjimenez merged commit 94a1327 into amule-project:master Apr 29, 2026
9 checks passed
@got3nks
got3nks deleted the pr-d-write-thread-iofailure branch May 3, 2026 15:19
mrjimenez pushed a commit to mrjimenez/amule that referenced this pull request Jul 16, 2026
…mule-project#499)

The interface bind added in amule-project#281 (IP_UNICAST_IF / IP_BOUND_IF) pins every
socket aMule opens — including the External Connection listener — to a single
interface, so ed2k/Kad cannot run over a VPN tunnel while the EC control port
stays on the LAN.

Decouple the EC listener with a new daemon-side setting,
/ExternalConnect/ECNetworkInterface (empty = any), that binds only aMule's EC
acceptor, independent of the global P2P interface pin. It sits beside the
existing ECAddress IP bind, giving the EC channel both its own IP and its own
interface.

CLibSocketServer gains a per-server interface override; only CExternalConnListener
uses it. ed2k/Kad TCP and UDP, outbound connections and HTTP keep following the
global setting untouched. A "Bind to network interface" selector is added to the
Remote Controls page (reusing the existing P2P label); like the other EC-listener
settings it is daemon-only — hidden in the remote GUI and not carried over EC —
and flagged restart-needed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants