[Bug]: App crashes after Confirm Delete / File Duplicate Bulk Actions send-to-trash on Windows (files deleted, no completion dialog) #170
Labels
No labels
Kind/Bug
Kind/Feature
Priority/High
Priority/Medium
Reviewed/Confirmed
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
ai-collab/bulk-image-organizer#170
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
App version
Packaged Windows build (reported against a build around 0.6.1/v0.7.0-era).
Platform
Windows
Report (from user, in chat, not yet reproduced by the user on a second attempt)
"Delete file bulk action" is the File Duplicate Bulk Actions dialog (
_on_dupe_bulk_actionsinui/main_window.py) with "send to trash" selected — it skips the normalDeleteConfirmDialogand calls_start_confirm_delete(trash_paths, remove_empty_dirs=True)directly. The underlyingDeleteWorker(core/delete_worker.py) ran to completion (files were actually sent to the recycle bin), but the app crashed before thefinishedsignal's handler (_on_delete_finished) could show a summary or return control to the UI. No dialog, no apparent log output described.Investigation (code-level, not yet confirmed against an actual Windows crash log)
This matches a failure shape this project has hit before: DR-027/DR-028 (issue #111) — a native crash inside a
QThreadPoolworker that bypasses Python's exception handling entirely (no traceback, no dialog), traced both times to a native/COM dependency being touched for the first time inside worker code.DeleteWorker._run_delete()callssend2trash.send2trash()once per file, in a loop, entirely inside aQRunnable.run()executing on aQThreadPoolworker thread. On Windows,send2trashauto-selects its backend (send2trash/win/__init__.py): ifpywin32is importable, it unconditionally usessend2trash/win/modern.py, which per-call does:i.e. it initializes a COM STA apartment on whatever OS thread
QThreadPoolhanded it, creates a ShellIFileOperationCOM object with anIFileOperationProgressSinkcallback, performs the operation, and tears the apartment down — repeated once per file.QThreadPoolworker threads never pump a Windows message loop, which STA COM apartments and Explorer's shell-operation callbacks generally expect to have available. This is a well-known source of instability for COM shell operations invoked from bare worker threads, and unlike a normal Python exception (whichDeleteWorker.run()'sexcept Exceptionalready catches and reports via theerrorsignal), a native/COM-level fault here would bypass that handling entirely — consistent with "crashed before alerting the user."pywin32is not a direct dependency of this project (pyproject.tomlonly listssend2trash>=1.8) —send2trashonly requires it via its own optional[nativelib]extra, which we don't request — but nothing prevents it from being present transitively or in a given build environment, andsend2trash's own fallback logic silently switches to the COM path whenever it's importable, with no app-level control over that choice today.By contrast,
send2trash/win/legacy.py(the fallback whenpywin32is absent) uses onlyctypes+SHFileOperationW— no COM, no apartment/message-pump requirements, safe to call from an arbitrary thread, and any failure surfaces as a normalOSError/WindowsErrorthatDeleteWorker's existingexcept OSErroralready handles correctly.Proposed fix
Force
DeleteWorkerto always usesend2trash.win.legacy.send2trashonsys.platform == "win32", instead of the package's own auto-detection, so the COM-based backend is never reachable from this app regardless of whetherpywin32happens to be present in a given build/environment. Non-Windows platforms are unaffected.Acceptance criteria
DeleteWorkeron Windows never invokessend2trash.win.modern(COM/IFileOperation) — always thectypes/SHFileOperationWlegacy backend.Status
Not yet reproduced by the user on a retry; root cause identified via code inspection and precedent (DR-027/028), not confirmed via an actual crash log or dump from the user's machine. Proceeding with the fix per the same pattern DR-027 used (implement the well-evidenced fix, await confirmation). If this recurs after the fix ships, the next step is capturing
%APPDATA%\bulk-image-organizer\logsand/or anuitka-crash-report.xmlfrom the crash for further diagnosis.Implemented per the proposed fix, documented as DR-037 in
docs/DECISIONS.md.core/delete_worker.pynow forcessend2trash.win.legacy(ctypes/SHFileOperationW, no COM) onsys.platform == "win32"instead of lettingsend2trashauto-select its COM-basedIFileOperationbackend wheneverpywin32happens to be importable. macOS/Linux unaffected.Regression test
tests/test_delete_worker_windows_backend.pyverifies the module resolves to the legacy backend on a simulatedwin32platform (stubbingsend2trash.win.legacy/.modernand reloading the module) and never reaches the modern/COM backend on other platforms. Full suite (705 passed) and ruff pass; UI exit smoke test clean.Committed to
0.6.1/v0.7.0(the same branch #163/#167/#168 are on) rather than a separate branch, per direction. Still unconfirmed against an actual Windows crash — no crash log or dump was available, and the root cause was identified by reading the installedsend2trashsource and this project's DR-027/DR-028 precedent, not by reproducing the crash. If it recurs after this ships, next step is capturing%APPDATA%\bulk-image-organizer\logsand/or a Nuitka crash report.Reverting the send2trash backend change (commit
3ccdad0on0.6.1/v0.7.0), including DR-037 and its test — per direction, since:_start_confirm_delete/_on_delete_progress/_on_delete_finished/_on_delete_errorare untouched by any commit here.Treating this as a one-time issue on the reporter's system rather than an application bug for now. Leaving this issue open as a placeholder with no code change — reopen investigation here if it happens again, ideally with the app log (
%APPDATA%\bulk-image-organizer\logs\bulk-image-organizer.log) from that session.Haven't reproduced, closing.