From 3fee57e84f02b951af0118f87509b50116333a26 Mon Sep 17 00:00:00 2001 From: Steve <48870638+svan71@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:50:33 -0400 Subject: [PATCH 1/2] Fix mount icons surviving unmount and all volumes sharing one identity _getMountName() computed a name and then returned the literal 'Volume', so every mount collapsed onto a single id and a single /tmp desktop entry. The entry was written only when absent, so its Name, Exec and Icon stayed frozen on the first drive mounted after boot, and no second drive could ever get an icon. Mount bookkeeping now reconciles against Gio.VolumeMonitor.get_mounts() instead of applying add/remove deltas. On 'mount-removed' the GMount has already lost its drive and volume, so any id derived from its name at that point can disagree with the id it was added under, leaving the icon behind for the rest of the session. Both handlers also refresh now - the dock only reconciles mount icons from layout(), which a plain animate() never reaches while the dock is autohidden. Also unmount via 'gio mount -u' rather than umount(8), which needs root for anything that is not a fuse mount and bypasses udisks when it does succeed, quote paths, and fall back to the location URI for network mounts. --- extension.js | 1 - services.js | 112 +++++++++++++++++++++------------------------------ 2 files changed, 47 insertions(+), 66 deletions(-) diff --git a/extension.js b/extension.js index 15a7410..db004c2 100644 --- a/extension.js +++ b/extension.js @@ -493,7 +493,6 @@ export default class Dash2DockLiteExt extends Extension { break; case 'mounted-icon': { this.services.checkMounts(); - this.services._commitMounts(); this.animate({ refresh: true }); break; } diff --git a/services.js b/services.js index 792400b..47be4f6 100644 --- a/services.js +++ b/services.js @@ -67,10 +67,6 @@ export const Services = class { 'ping', 1000 * 5, () => { - // deferred stuff is required when .desktop entry if first created - // check for deferred mounts - this._commitMounts(); - // notifications this.checkNotifications(); }, @@ -80,7 +76,6 @@ export const Services = class { this._disableNotifications = 0; - this._deferredMounts = []; this._volumeMonitor = Gio.VolumeMonitor.get(); this._volumeMonitor.connectObject( 'mount-added', @@ -119,16 +114,17 @@ export const Services = class { this.checkNotifications(); this.checkMounts(); - this._commitMounts(); } disable() { - this._downloadsMonitor.disconnectObject(this); + // guarded - a throw here used to abort the rest of disable() and leave the + // volume monitor connected to a dead Services instance + this._downloadsMonitor?.disconnectObject(this); this._downloadsMonitor = null; this._services = []; - this._volumeMonitor.disconnectObject(this); + this._volumeMonitor?.disconnectObject(this); this._volumeMonitor = null; - this._trashMonitor.disconnectObject(this); + this._trashMonitor?.disconnectObject(this); this._trashMonitor = null; this._trashDir = null; } @@ -171,37 +167,21 @@ export const Services = class { this._debounceCheckDownloads(); } - _commitMounts() { - if (this._deferredMounts && this._deferredMounts.length) { - let mounts = [...this._deferredMounts]; - this._deferredMounts = []; - mounts.forEach((m) => { - this._onMountAdded(null, m); - }); - } - } - _onMountAdded(monitor, mount) { if (!this.extension.mounted_icon) { return false; } this.last_mounted = mount; - let basename = this._getMountName(mount); // mount.get_default_location().get_basename(); - // let appname = `mount-${this._toSafeFileName(basename)}-dash2dock-lite.desktop`; - this.setupMountIcon(mount); - this.extension.animate(); + this.checkMounts(); + // refresh - the dock only reconciles its mount icons from layout() + this.extension.animate({ refresh: true }); return true; } _onMountRemoved(monitor, mount) { - let basename = this._getMountName(mount); //mount.get_default_location().get_basename(); - let appname = `mount-${this._toSafeFileName( - basename - )}-dash2dock-lite.desktop`; - let mount_id = tempPath(appname); - delete this._mounts[mount_id]; - this.extension.animate(); + this.checkMounts(); + this.extension.animate({ refresh: true }); } update(elapsed) { @@ -286,32 +266,32 @@ export const Services = class { // return; } let label = mount.get_name(); - let appname = `mount-${this._toSafeFileName( - basename - )}-dash2dock-lite.desktop`; - let fullpath = mount.get_default_location().get_path(); + let location = mount.get_default_location(); + // network mounts (smb://, sftp://...) have no local path + let fullpath = location.get_path() ?? location.get_uri(); let icon = 'drive-harddisk-solidstate'; if (mount.get_icon() && mount.get_icon().names) { icon = this.extension.lookup_icon_from_names(mount.get_icon().names) ?? icon; } let mount_exec = 'echo "not implemented"'; - let unmount_exec = `umount ${fullpath}`; - let mount_id = tempPath(appname); + // unmount through gio - a bare umount(8) needs root for anything that + // isn't a fuse mount, and it unmounts behind udisks' back when it does work + let unmount_exec = `gio mount -u "${fullpath}"`; + let mount_id = this._mountId(mount); let fn = Gio.File.new_for_path(mount_id); - if (!fn.query_exists(null)) { - let content = `[Desktop Entry]\nVersion=1.0\nTerminal=false\nType=Application\nName=${label}\nExec=xdg-open ${fullpath}\nIcon=${icon}\nStartupWMClass=mount-${this._toSafeFileName( - basename - )}-dash2dock-lite\nActions=unmount;\n\n[Desktop Action mount]\nName=Mount\nExec=${mount_exec}\n\n[Desktop Action unmount]\nName=Unmount\nExec=${unmount_exec}\n`; - const [, etag] = fn.replace_contents( - content, - null, - false, - Gio.FileCreateFlags.REPLACE_DESTINATION, - null - ); - } + // always rewrite - name, path and icon belong to whatever is mounted now + let content = `[Desktop Entry]\nVersion=1.0\nTerminal=false\nType=Application\nName=${label}\nExec=xdg-open "${fullpath}"\nIcon=${icon}\nStartupWMClass=mount-${this._toSafeFileName( + basename + )}-dash2dock-lite\nActions=unmount;\n\n[Desktop Action mount]\nName=Mount\nExec=${mount_exec}\n\n[Desktop Action unmount]\nName=Unmount\nExec=${unmount_exec}\n`; + const [, etag] = fn.replace_contents( + content, + null, + false, + Gio.FileCreateFlags.REPLACE_DESTINATION, + null + ); this._mounts[mount_id] = mount; } @@ -652,34 +632,36 @@ export const Services = class { } } - return 'Volume'; + return name || 'Volume'; + } + + _mountId(mount) { + let appname = `mount-${this._toSafeFileName( + this._getMountName(mount) + )}-dash2dock-lite.desktop`; + return tempPath(appname); } checkMounts() { if (!this.extension.mounted_icon) { - this._mounts = []; + this._mounts = {}; + this.mounts = []; return; } + // rebuild from the live mount list instead of tracking add/remove deltas. + // on 'mount-removed' the GMount has already lost its drive and volume, so + // the id derived from its name no longer matches the id it was added with - + // deltas leave icons of unmounted drives behind. let mounts = this._volumeMonitor.get_mounts() || []; - let mount_ids = mounts.map((mount) => { - let basename = this._getMountName(mount); - let appname = `mount-${this._toSafeFileName( - basename - )}-dash2dock-lite.desktop`; - return appname; - }); - - this.mounts = mounts; + let _mounts = {}; mounts.forEach((mount) => { - let basename = this._getMountName(mount); - let appname = `mount-${this._toSafeFileName( - basename - )}-dash2dock-lite.desktop`; - this._deferredMounts.push(mount); + this.setupMountIcon(mount); + _mounts[this._mountId(mount)] = mount; }); - // added devices will subsequently be on mounted events + this.mounts = mounts; + this._mounts = _mounts; } //! this is out of place - services should only do background process - no rendering From 3c4a1d185314b598aadad820f4efc630bb29378b Mon Sep 17 00:00:00 2001 From: Steve <48870638+svan71@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:59:50 -0400 Subject: [PATCH 2/2] Delete a mount's /tmp desktop entry once nothing is mounted there setupMountIcon() writes /tmp/-mount--dash2dock-lite.desktop per mount and nothing ever removed it, so entries for unmounted volumes survived the rest of the session - and any still present when the session ended stayed in /tmp indefinitely. checkMounts() now sweeps the entries against the reconciled mount table, which covers both cases without tracking removals separately. --- services.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/services.js b/services.js index 47be4f6..edae9f7 100644 --- a/services.js +++ b/services.js @@ -642,8 +642,41 @@ export const Services = class { return tempPath(appname); } + // the /tmp entries are written one per mount and nothing else removes them: + // drop every one no live mount claims, which also clears leftovers from a + // session that ended while something was still mounted + _sweepMountEntries(keep) { + let prefix = tempPath('mount-'); + let dirPath = GLib.path_get_dirname(prefix); + let iter = null; + try { + iter = Gio.File.new_for_path(dirPath).enumerate_children( + 'standard::name', + Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + null + ); + } catch (err) { + return; + } + + let info = null; + while ((info = iter.next_file(null))) { + let path = `${dirPath}/${info.get_name()}`; + if (!path.startsWith(prefix)) continue; + if (!path.endsWith('-dash2dock-lite.desktop')) continue; + if (keep[path]) continue; + try { + Gio.File.new_for_path(path).delete(null); + } catch (err) { + // already gone, or not ours to remove + } + } + iter.close(null); + } + checkMounts() { if (!this.extension.mounted_icon) { + this._sweepMountEntries({}); this._mounts = {}; this.mounts = []; return; @@ -662,6 +695,7 @@ export const Services = class { this.mounts = mounts; this._mounts = _mounts; + this._sweepMountEntries(_mounts); } //! this is out of place - services should only do background process - no rendering