Skip to content

Commit 9bdd28c

Browse files
committed
Implement mount/unmount/autorun user interaction in Cinnamon.
Certain aspects of GtkMountOperation are broken under Wayland in Gtk3 and won't be fixed. Fortunately it can also use a dbus interface (org.gtk.MountOperationHandler) if a provider exists. This allows us to: - provide the 'device is in use' popup and showing which application is using it, when trying to eject a device. - provide the password/question dialog when mounting devices that require interaction before mounting. - eliminate a lot of code and translations from placesManager, and make the behavior identical when interacting with a device whether from a file manager or Cinnamon's drives applet. Translations are provided by Gtk, Gvfs instead (as when ejecting a device from a file manager). - Adds testMountDialogs.js for theme/dialog testing (activatable via looking-glass). Also replace mount-detection handling in cinnamon-settings-daemon. It was originally part of Cinnamon but mostly removed early on when Cinnamon was forked, and we've relied on csd-automount. With the implementation of CinnamonMountOperation for handling unmount operations, we can bring in the autorun dialog as well. - csd-automount is still used in fallback mode and managed by cinnamon-launcher in those situations (nm-applet, polkit agents are already handled here also). - linuxmint/cinnamon-settings-daemon#445 disables autostarting csd-automount at session startup. - Adds testAutorunDialog.js for theme/dialog testing (activatable via looking-glass).
1 parent c515147 commit 9bdd28c

18 files changed

Lines changed: 2071 additions & 253 deletions

data/org.cinnamon.gschema.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,15 @@
611611
<default>""</default>
612612
<summary>Stores the position of the hoverclick window so it can be restored there later - format is x::y</summary>
613613
</key>
614+
<key name="remember-mount-password" type="b">
615+
<default>false</default>
616+
<summary>Whether to remember password for mounting encrypted or remote filesystems</summary>
617+
<description>
618+
When an encrypted device or remote filesystem is mounted, a password
619+
dialog may include a "Remember Password" checkbox. This key stores the
620+
default state of that checkbox.
621+
</description>
622+
</key>
614623
</schema>
615624

616625
<schema id="org.cinnamon.theme" path="/org/cinnamon/theme/"

data/theme/cinnamon-sass/widgets/_dialogs.scss

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,16 @@
4141
.dialog-list-box {
4242
spacing: 1em;
4343

44+
border-radius: $base_border_radius;
45+
4446
.dialog-list-item {
4547
spacing: 1em;
48+
border-radius: $base_border_radius;
49+
padding: $base_padding $base_padding * 2;
50+
transition-duration: 100ms;
51+
52+
&:hover { background-color: $light_bg_color; }
53+
&:selected { background-color: $accent_bg_color; }
4654

4755
.dialog-list-item-title { font-weight: bold; }
4856
.dialog-list-item-description {
@@ -194,3 +202,59 @@
194202
}
195203
}
196204
}
205+
206+
// Shared grouped-list style for autorun and processes dialogs
207+
%grouped-dialog-list {
208+
.dialog-list-box {
209+
background-color: lighten($bg_color, 5%);
210+
spacing: 0;
211+
212+
.dialog-list-item {
213+
border-radius: 0;
214+
215+
&:first-child {
216+
border-radius: $base_border_radius $base_border_radius 0 0;
217+
}
218+
219+
&:last-child {
220+
border-radius: 0 0 $base_border_radius $base_border_radius;
221+
}
222+
}
223+
}
224+
}
225+
226+
.processes-dialog {
227+
.dialog-content-box {
228+
.dialog-list {
229+
@extend %grouped-dialog-list;
230+
}
231+
}
232+
}
233+
234+
.autorun-dialog {
235+
min-width: 30em;
236+
237+
.dialog-content-box {
238+
margin-top: $base_margin;
239+
margin-bottom: $base_margin * 3;
240+
spacing: $base_margin * 3;
241+
max-width: 28em;
242+
243+
.dialog-list {
244+
@extend %grouped-dialog-list;
245+
}
246+
247+
.autorun-dialog-heading {
248+
@extend %title_2;
249+
text-align: center;
250+
}
251+
252+
.autorun-dialog-subheading {
253+
@extend %title_4;
254+
text-align: center;
255+
}
256+
257+
.autorun-dialog-description {
258+
}
259+
}
260+
}

docs/reference/cinnamon/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
ignore = [
22
'cinnamon-recorder-src.h',
33
'cinnamon-recorder.h',
4+
'cinnamon-mount-operation.h',
45
st_headers,
56
st_private_headers,
67
tray_headers,

files/usr/bin/cinnamon-launcher

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class Launcher:
7373

7474
self.polkit_agent_proc = None
7575
self.nm_applet_proc = None
76+
self.automount_proc = None
7677
self.can_restart = False
7778
self.dialog = None
7879

@@ -161,6 +162,10 @@ class Launcher:
161162
print(f"Launching for fallback session: nm-applet")
162163
self.nm_applet_proc = subprocess.Popen(["nm-applet"])
163164

165+
if shutil.which("csd-automount"):
166+
print(f"Launching for fallback session: csd-automount")
167+
self.automount_proc = subprocess.Popen(["csd-automount"])
168+
164169
def kill_fallback_helpers(self):
165170
if self.polkit_agent_proc is not None:
166171
print("Killing fallback polkit agent")
@@ -180,6 +185,15 @@ class Launcher:
180185
self.nm_applet_proc.kill()
181186
self.nm_applet_proc = None
182187

188+
if self.automount_proc is not None:
189+
print("Killing fallback csd-automount")
190+
self.automount_proc.terminate()
191+
try:
192+
self.automount_proc.wait(timeout=5)
193+
except subprocess.TimeoutExpired:
194+
self.automount_proc.kill()
195+
self.automount_proc = None
196+
183197
@async_function
184198
def monitor_memory(self):
185199
if psutil.pid_exists(self.cinnamon_pid):

js/ui/automountManager.js

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
2+
/* exported Component */
3+
4+
const { Gio, GLib } = imports.gi;
5+
const Params = imports.misc.params;
6+
7+
const LoginManager = imports.misc.loginManager;
8+
const Main = imports.ui.main;
9+
const CinnamonMountOperation = imports.ui.cinnamonMountOperation;
10+
11+
// GSettings keys
12+
const SETTINGS_SCHEMA = 'org.cinnamon.desktop.media-handling';
13+
const SETTING_ENABLE_AUTOMOUNT = 'automount';
14+
15+
var AUTORUN_EXPIRE_TIMEOUT_SECS = 10;
16+
17+
var AutomountManager = class {
18+
constructor() {
19+
this._settings = new Gio.Settings({ schema_id: SETTINGS_SCHEMA });
20+
this._activeOperations = new Map();
21+
this._volumeQueue = [];
22+
23+
this._loginManager = LoginManager.getLoginManager();
24+
this._loginManager.connect('active-changed', (lm, active) => {
25+
if (active)
26+
this._drainVolumeQueue();
27+
});
28+
29+
Main.screensaverController.connect('locked-changed', (ctrl, locked) => {
30+
if (!locked)
31+
this._drainVolumeQueue();
32+
});
33+
34+
this._volumeMonitor = Gio.VolumeMonitor.get();
35+
this.enable();
36+
}
37+
38+
enable() {
39+
this._volumeMonitor.connectObject(
40+
'volume-added', this._onVolumeAdded.bind(this),
41+
'volume-removed', this._onVolumeRemoved.bind(this),
42+
'drive-connected', this._onDriveConnected.bind(this),
43+
'drive-disconnected', this._onDriveDisconnected.bind(this),
44+
'drive-eject-button', this._onDriveEjectButton.bind(this), this);
45+
46+
this._mountAllId = GLib.idle_add(GLib.PRIORITY_DEFAULT, this._startupMountAll.bind(this));
47+
GLib.Source.set_name_by_id(this._mountAllId, '[cinnamon] this._startupMountAll');
48+
}
49+
50+
disable() {
51+
this._volumeMonitor.disconnectObject(this);
52+
53+
if (this._mountAllId > 0) {
54+
GLib.source_remove(this._mountAllId);
55+
this._mountAllId = 0;
56+
}
57+
}
58+
59+
_drainVolumeQueue() {
60+
while (this._volumeQueue.length > 0) {
61+
let volume = this._volumeQueue.shift();
62+
this._checkAndMountVolume(volume, {
63+
checkSession: false,
64+
});
65+
}
66+
}
67+
68+
_startupMountAll() {
69+
let volumes = this._volumeMonitor.get_volumes();
70+
volumes.forEach(volume => {
71+
this._checkAndMountVolume(volume, {
72+
checkSession: false,
73+
useMountOp: false,
74+
allowAutorun: false,
75+
});
76+
});
77+
78+
this._mountAllId = 0;
79+
return GLib.SOURCE_REMOVE;
80+
}
81+
82+
_onDriveConnected() {
83+
if (!this._loginManager.sessionIsActive)
84+
return;
85+
86+
let player = global.display.get_sound_player();
87+
player.play_from_theme('device-added-media',
88+
_("External drive connected"),
89+
null);
90+
}
91+
92+
_onDriveDisconnected() {
93+
if (!this._loginManager.sessionIsActive)
94+
return;
95+
96+
let player = global.display.get_sound_player();
97+
player.play_from_theme('device-removed-media',
98+
_("External drive disconnected"),
99+
null);
100+
}
101+
102+
_onDriveEjectButton(monitor, drive) {
103+
if (!this._loginManager.sessionIsActive)
104+
return;
105+
106+
if (drive.can_stop()) {
107+
drive.stop(Gio.MountUnmountFlags.FORCE, null, null,
108+
(o, res) => {
109+
try {
110+
drive.stop_finish(res);
111+
} catch (e) {
112+
log(`Unable to stop the drive after drive-eject-button ${e.toString()}`);
113+
}
114+
});
115+
} else if (drive.can_eject()) {
116+
drive.eject_with_operation(Gio.MountUnmountFlags.FORCE, null, null,
117+
(o, res) => {
118+
try {
119+
drive.eject_with_operation_finish(res);
120+
} catch (e) {
121+
log(`Unable to eject the drive after drive-eject-button ${e.toString()}`);
122+
}
123+
});
124+
}
125+
}
126+
127+
_onVolumeAdded(monitor, volume) {
128+
this._checkAndMountVolume(volume);
129+
}
130+
131+
_checkAndMountVolume(volume, params) {
132+
params = Params.parse(params, {
133+
checkSession: true,
134+
useMountOp: true,
135+
allowAutorun: true,
136+
});
137+
138+
if (params.checkSession) {
139+
if (!this._loginManager.sessionIsActive)
140+
return;
141+
142+
if (Main.screensaverController.locked) {
143+
this._volumeQueue.push(volume);
144+
return;
145+
}
146+
}
147+
148+
if (volume.get_mount())
149+
return;
150+
151+
if (!this._settings.get_boolean(SETTING_ENABLE_AUTOMOUNT) ||
152+
!volume.should_automount() ||
153+
!volume.can_mount()) {
154+
this._allowAutorun(volume);
155+
this._allowAutorunExpire(volume);
156+
157+
return;
158+
}
159+
160+
if (params.useMountOp) {
161+
let operation = new CinnamonMountOperation.CinnamonMountOperation(volume);
162+
this._mountVolume(volume, operation, params.allowAutorun);
163+
} else {
164+
this._mountVolume(volume, null, params.allowAutorun);
165+
}
166+
}
167+
168+
_mountVolume(volume, operation, allowAutorun) {
169+
if (allowAutorun)
170+
this._allowAutorun(volume);
171+
172+
const mountOp = operation?.mountOp ?? null;
173+
this._activeOperations.set(volume, operation);
174+
175+
volume.mount(0, mountOp, null,
176+
this._onVolumeMounted.bind(this));
177+
}
178+
179+
_onVolumeMounted(volume, res) {
180+
this._allowAutorunExpire(volume);
181+
182+
try {
183+
volume.mount_finish(res);
184+
this._closeOperation(volume);
185+
} catch (e) {
186+
// Errors here do not have any specific codes we can parse, but the error message
187+
// comes from udisks and will not be translated, so should be reliable (used this way
188+
// in other projects as well).
189+
if (e.message.includes('No key available with this passphrase') ||
190+
e.message.includes('No key available to unlock device') ||
191+
e.message.includes('Failed to activate device: Incorrect passphrase') ||
192+
e.message.includes('Failed to load device\'s parameters: Invalid argument')) {
193+
this._reaskPassword(volume);
194+
} else {
195+
if (e.message.includes('Compiled against a version of libcryptsetup that does not support the VeraCrypt PIM setting')) {
196+
Main.notifyError(_("Unable to unlock volume"),
197+
_("The installed udisks version does not support the PIM setting"));
198+
}
199+
200+
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
201+
log(`Unable to mount volume ${volume.get_name()}: ${e.toString()}`);
202+
this._closeOperation(volume);
203+
}
204+
}
205+
}
206+
207+
_onVolumeRemoved(monitor, volume) {
208+
if (volume._allowAutorunExpireId && volume._allowAutorunExpireId > 0) {
209+
GLib.source_remove(volume._allowAutorunExpireId);
210+
delete volume._allowAutorunExpireId;
211+
}
212+
213+
this._volumeQueue = this._volumeQueue.filter(v => v !== volume);
214+
}
215+
216+
_reaskPassword(volume) {
217+
let prevOperation = this._activeOperations.get(volume);
218+
const existingDialog = prevOperation?.borrowDialog();
219+
let operation =
220+
new CinnamonMountOperation.CinnamonMountOperation(volume, { existingDialog });
221+
this._mountVolume(volume, operation);
222+
}
223+
224+
_closeOperation(volume) {
225+
let operation = this._activeOperations.get(volume);
226+
if (!operation)
227+
return;
228+
operation.close();
229+
this._activeOperations.delete(volume);
230+
}
231+
232+
_allowAutorun(volume) {
233+
volume.allowAutorun = true;
234+
}
235+
236+
_allowAutorunExpire(volume) {
237+
if (volume._allowAutorunExpireId && volume._allowAutorunExpireId > 0) {
238+
GLib.source_remove(volume._allowAutorunExpireId);
239+
delete volume._allowAutorunExpireId;
240+
}
241+
242+
let id = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, AUTORUN_EXPIRE_TIMEOUT_SECS, () => {
243+
volume.allowAutorun = false;
244+
delete volume._allowAutorunExpireId;
245+
return GLib.SOURCE_REMOVE;
246+
});
247+
volume._allowAutorunExpireId = id;
248+
GLib.Source.set_name_by_id(id, '[cinnamon] volume.allowAutorun');
249+
}
250+
};

0 commit comments

Comments
 (0)