Skip to content

places-sidebar: keep ejecting devices visible until the eject completes - #3816

Open
LucaBoschetto wants to merge 1 commit into
linuxmint:masterfrom
LucaBoschetto:fix-eject-data-loss
Open

places-sidebar: keep ejecting devices visible until the eject completes#3816
LucaBoschetto wants to merge 1 commit into
linuxmint:masterfrom
LucaBoschetto:fix-eject-data-loss

Conversation

@LucaBoschetto

Copy link
Copy Markdown

The problem

When a removable drive is ejected from the sidebar, its row disappears within about one second, while the kernel may still be flushing cached writes to the device for a minute or more. The only warning is a desktop notification ("Writing data..."), which is transient, easily missed, and may be disabled altogether; nothing in the file manager's own UI indicates the device is still busy. The user may reasonably conclude the eject is done, and unplugs. The files that were still in flight are truncated and the filesystem is left dirty.

This is easy to hit in real life: copying a large file to a USB stick returns almost immediately (the data lands in the page cache), the user clicks eject, the row vanishes, they pull the stick. On a 2 GB backlog I measured about 90 seconds between the row disappearing and the device actually being safe to remove (desktop system, source file on an NVMe SSD so the copy is cache-speed, ordinary consumer USB 3 flash sticks with exFAT sustaining roughly 20-50 MB/s; the size of the window is essentially backlog size over the stick's sequential write speed).

The eject operation itself (GVFS/udisks) is correct: it flushes everything and only reports completion when the device is truly safe. Only the sidebar UI reacts to the wrong signal.

Root cause

Verified at runtime with an isolation harness that makes the exact GIO calls Nemo makes, on Mint 22.3 / Nemo 6.6.3 / GVFS 1.54.4, with a 2 GB write-back backlog on an exFAT USB stick:

+0.8s   eject starts (g_mount_eject_with_operation)
+0.8s   GVolumeMonitor emits mount-removed        <- sidebar rebuilds here
+2.3s   show-unmount-progress: "Writing data ... should not be unplugged."
 ...    page-cache backlog drains for ~90 s ...
+94.6s  show-unmount-progress: "... can be safely unplugged." + eject completes
+94.7s  volume-removed

mount-removed fires at the start of the eject, not at completion. The GVolume however stays present the whole time, so on rebuild the device falls into the "unmounted volume" branch of update_places () and is rendered as a plain unmounted device: no eject button, a "Mount and open ..." tooltip, looks finished. The eject completion callbacks (mount_eject_cb and friends), the one place that knows the device is truly safe, do nothing on success.

Two extra consequences of the current behavior:

  • The row now behaves like an unmounted volume, so activating it mid-flush starts a re-mount of a device that is being flushed. This is a plausible trigger for the "an operation is already pending" message storms reported in USB UX problems: a welter of 'an operation is already pending' and similar messages #3463.
  • Nemo does send the urgent "Writing data" notification, but the false cue (row gone) appears about 1.5 s before it, and remains for the whole flush.

The fix

All in nemo-places-sidebar.c, UI-only; the eject operation itself is untouched.

  • Track devices with an eject in progress in a small process-global table keyed by unix-device id (process-global so that every window agrees on the state, not just the one where eject was clicked).
  • do_eject () records the device before starting the operation.
  • While the eject is in progress, the unmounted-volume render paths keep the device visible as busy: it keeps its normal device icon, the name gains an "(ejecting...)" suffix, the eject button is replaced by a busy indicator (xsi-emblem-synchronizing-symbolic) in the same spot the user clicked, and the tooltip reads "Still writing data to . Do not unplug it yet."
  • Row activation is ignored for a device in this state (no more accidental re-mount mid-flush).
  • Repeated eject requests for a device already ejecting (eject cell, context menu) are ignored while the operation is in flight, instead of spawning a second operation that fails with "an operation is already pending".
  • The eject completion callbacks (success or failure) remove the entry and refresh, so the row disappears exactly when the device is safe to remove, in every window. On failure the row heals back to its normal mounted state.
  • A sweep in update_places () drops entries whose volume is gone, so a physically yanked device can never leave a stale "ejecting" row.
  • The completion callbacks now receive a small context struct (window keep-alive as before, plus a weak pointer to the sidebar, which can legitimately be destroyed during a long eject, and the device key).

The resulting semantics are simple and truthful: row present and marked busy = not safe to unplug; row gone = safe. This also lines up with the existing "can be safely unplugged" notification, which fires at the same moment the row now disappears.

demo-final-zoom demo-tooltip

Testing

Tested on Linux Mint 22.3 (Cinnamon 6.6.9, X11), Nemo built from this branch at 6.6.3, GVFS 1.54.4, kernels 6.17 and 7.0, against exFAT USB sticks, driven both manually and by a scripted harness that timestamps every GVolumeMonitor signal alongside /proc/meminfo Dirty. The branch was subsequently rebased onto current master (6.7.4-dev, 932438f) and re-verified there with a live large-backlog eject cycle: busy state renders, the re-trigger guard holds, completion clears the row, no kernel I/O errors. Detailed results:

  • Large-backlog eject (1.4 GB dirty): row stays as "(ejecting...)" for the full ~35-90 s flush and disappears exactly at completion. MD5 of the copied files verified identical to the source after remount.
  • Idle-drive eject: near-instant completion, row clears promptly, no stuck state.
  • Clicking the ejecting row: inert (previously started a re-mount).
  • Two windows: both show the busy state during the flush and both clear on completion.
  • "Volume is busy" dialog: Cancel aborts the eject and the row heals back to the normal mounted state; letting the blocking process exit lets the eject proceed and complete safely.
  • Unmount (not eject): still renders as a normal unmounted volume with its "Mount and open ..." tooltip; activating it mounts and opens as before.
  • No kernel I/O errors across all cycles; no regressions observed in normal mount/unmount flows.

Notes for reviewers

  • Busy indicator placement: it lives in the eject cell, so the feedback appears exactly where the user clicked, the device keeps its identity icon, and the row's layout does not shift; a future animated spinner (see below) would occupy the same cell.
  • Busy indicator icon: an hourglass or spinner glyph might have been the clearer "busy" metaphor, but neither has a standard name in the freedesktop icon spec, so they would not resolve reliably across themes and distros. xsi-emblem-synchronizing-symbolic was chosen instead: the xsi set ships with libxapp (already a hard dependency) and matches the sidebar's existing icons, so it resolves everywhere Nemo runs and is the closest portable fit semantically.
  • The row text is deliberately kept short so it does not truncate at default sidebar widths; the full warning lives in the tooltip. The text matters for accessibility: screen readers announce the row name, not icon changes.
  • Two new translatable strings are introduced.
  • Follow-up candidates, intentionally out of scope to keep this minimal: an animated spinner in the eject cell (GtkCellRendererSpinner needs a model column plus animation plumbing), and determinate progress, which would require GVFS/udisks to emit real unmount progress (today show-unmount-progress fires only at start and completion; verified with the harness).

Related issues

The reproduction harness (Python/GIO, ~60 lines) is available on request; happy to attach it or adapt this patch based on feedback.

A GMount's mount-removed signal fires at the start of an eject, while
flushing cached writes to the device can take minutes. The sidebar
rebuilt on that early signal and rendered the device as a plain
unmounted volume ('Mount and open ...'), the strongest possible 'safe
to unplug' cue at exactly the moment it is least true - inviting
silent data loss and filesystem corruption on removable media.

Track devices with an eject in progress (process-global, so every
window agrees) and, while flushing:
- render the device as busy: '<name> (ejecting, do not remove)' with
  a synchronizing emblem and explanatory tooltip, in both unmounted-
  volume render paths;
- ignore activation on the row, which previously started a re-MOUNT
  of the device mid-flush (a likely trigger of the 'operation already
  pending' spam in linuxmint#3463);
- remove the entry in the eject completion callbacks (success or
  failure), the one place that knows the device is truly done, and
  refresh the sidebar;
- sweep entries whose volume is gone (physical yank) on every rebuild.

The completion callbacks now carry an EjectData (window keep-alive as
before, plus a weak pointer to the sidebar, which can be destroyed
during a long eject, and the device key).

No change to the eject operation itself, which was always correct;
only the UI no longer claims completion ~90s early.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant