Skip to content

Commit fbca147

Browse files
committed
Implement automount/autorun detection and interaction in Cinnamon.
This aims to replace cinnamon-settings-daemon's automount manager. 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.
1 parent 9616baa commit fbca147

6 files changed

Lines changed: 646 additions & 100 deletions

File tree

js/ui/automountManager.js

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
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 GnomeSession = imports.misc.gnomeSession;
8+
const Main = imports.ui.main;
9+
const CinnamonMountOperation = imports.ui.cinnamonMountOperation;
10+
11+
var GNOME_SESSION_AUTOMOUNT_INHIBIT = 16;
12+
13+
// GSettings keys
14+
const SETTINGS_SCHEMA = 'org.cinnamon.desktop.media-handling';
15+
const SETTING_ENABLE_AUTOMOUNT = 'automount';
16+
17+
var AUTORUN_EXPIRE_TIMEOUT_SECS = 10;
18+
19+
var AutomountManager = class {
20+
constructor() {
21+
this._settings = new Gio.Settings({ schema_id: SETTINGS_SCHEMA });
22+
this._activeOperations = new Map();
23+
24+
GnomeSession.SessionManager((proxy, error) => {
25+
if (error)
26+
return;
27+
28+
this._session = proxy;
29+
this.actor.show();
30+
this.updateStatus();
31+
32+
this._session.connectSignal(
33+
"InhibitorAdded",
34+
this._InhibitorsChanged.bind(this)
35+
);
36+
37+
this._session.connectSignal(
38+
"InhibitorRemoved",
39+
this._InhibitorsChanged.bind(this)
40+
);
41+
});
42+
43+
this._inhibited = false;
44+
45+
this._volumeMonitor = Gio.VolumeMonitor.get();
46+
this.enable();
47+
}
48+
49+
enable() {
50+
this._volumeMonitor.connectObject(
51+
'volume-added', this._onVolumeAdded.bind(this),
52+
'volume-removed', this._onVolumeRemoved.bind(this),
53+
'drive-connected', this._onDriveConnected.bind(this),
54+
'drive-disconnected', this._onDriveDisconnected.bind(this),
55+
'drive-eject-button', this._onDriveEjectButton.bind(this), this);
56+
57+
this._mountAllId = GLib.idle_add(GLib.PRIORITY_DEFAULT, this._startupMountAll.bind(this));
58+
GLib.Source.set_name_by_id(this._mountAllId, '[cinnamon] this._startupMountAll');
59+
}
60+
61+
disable() {
62+
this._volumeMonitor.disconnectObject(this);
63+
64+
if (this._mountAllId > 0) {
65+
GLib.source_remove(this._mountAllId);
66+
this._mountAllId = 0;
67+
}
68+
}
69+
70+
async _InhibitorsChanged(_object, _senderName, [_inhibitor]) {
71+
try {
72+
const [inhibited] =
73+
await this._session.IsInhibitedAsync(GNOME_SESSION_AUTOMOUNT_INHIBIT);
74+
this._inhibited = inhibited;
75+
} catch (e) {}
76+
}
77+
78+
_startupMountAll() {
79+
let volumes = this._volumeMonitor.get_volumes();
80+
volumes.forEach(volume => {
81+
this._checkAndMountVolume(volume, {
82+
checkSession: false,
83+
useMountOp: false,
84+
allowAutorun: false,
85+
});
86+
});
87+
88+
this._mountAllId = 0;
89+
return GLib.SOURCE_REMOVE;
90+
}
91+
92+
_onDriveConnected() {
93+
// if we're not in the current ConsoleKit session,
94+
// or screensaver is active, don't play sounds
95+
// if (!this._session.SessionIsActive)
96+
// return;
97+
98+
let player = global.display.get_sound_player();
99+
player.play_from_theme('device-added-media',
100+
_("External drive connected"),
101+
null);
102+
}
103+
104+
_onDriveDisconnected() {
105+
// if we're not in the current ConsoleKit session,
106+
// or screensaver is active, don't play sounds
107+
// if (!this._session.SessionIsActive)
108+
// return;
109+
110+
let player = global.display.get_sound_player();
111+
player.play_from_theme('device-removed-media',
112+
_("External drive disconnected"),
113+
null);
114+
}
115+
116+
_onDriveEjectButton(monitor, drive) {
117+
// TODO: this code path is not tested, as the GVfs volume monitor
118+
// doesn't emit this signal just yet.
119+
// if (!this._session.SessionIsActive)
120+
// return;
121+
122+
// we force stop/eject in this case, so we don't have to pass a
123+
// mount operation object
124+
if (drive.can_stop()) {
125+
drive.stop(Gio.MountUnmountFlags.FORCE, null, null,
126+
(o, res) => {
127+
try {
128+
drive.stop_finish(res);
129+
} catch (e) {
130+
log(`Unable to stop the drive after drive-eject-button ${e.toString()}`);
131+
}
132+
});
133+
} else if (drive.can_eject()) {
134+
drive.eject_with_operation(Gio.MountUnmountFlags.FORCE, null, null,
135+
(o, res) => {
136+
try {
137+
drive.eject_with_operation_finish(res);
138+
} catch (e) {
139+
log(`Unable to eject the drive after drive-eject-button ${e.toString()}`);
140+
}
141+
});
142+
}
143+
}
144+
145+
_onVolumeAdded(monitor, volume) {
146+
this._checkAndMountVolume(volume);
147+
}
148+
149+
_checkAndMountVolume(volume, params) {
150+
global.log("check and mount");
151+
params = Params.parse(params, {
152+
checkSession: true,
153+
useMountOp: true,
154+
allowAutorun: true,
155+
});
156+
157+
if (params.checkSession) {
158+
// if we're not in the current ConsoleKit session,
159+
// don't attempt automount
160+
// if (!this._session.SessionIsActive)
161+
// return;
162+
}
163+
164+
if (this._inhibited)
165+
return;
166+
167+
// Volume is already mounted, don't bother.
168+
if (volume.get_mount())
169+
return;
170+
171+
if (!this._settings.get_boolean(SETTING_ENABLE_AUTOMOUNT) ||
172+
!volume.should_automount() ||
173+
!volume.can_mount()) {
174+
// allow the autorun to run anyway; this can happen if the
175+
// mount gets added programmatically later, even if
176+
// should_automount() or can_mount() are false, like for
177+
// blank optical media.
178+
this._allowAutorun(volume);
179+
this._allowAutorunExpire(volume);
180+
181+
return;
182+
}
183+
184+
if (params.useMountOp) {
185+
let operation = new CinnamonMountOperation.CinnamonMountOperation(volume);
186+
this._mountVolume(volume, operation, params.allowAutorun);
187+
} else {
188+
this._mountVolume(volume, null, params.allowAutorun);
189+
}
190+
}
191+
192+
_mountVolume(volume, operation, allowAutorun) {
193+
if (allowAutorun)
194+
this._allowAutorun(volume);
195+
196+
const mountOp = operation?.mountOp ?? null;
197+
this._activeOperations.set(volume, operation);
198+
199+
volume.mount(0, mountOp, null,
200+
this._onVolumeMounted.bind(this));
201+
}
202+
203+
_onVolumeMounted(volume, res) {
204+
global.log("on volume mounted");
205+
this._allowAutorunExpire(volume);
206+
207+
try {
208+
volume.mount_finish(res);
209+
this._closeOperation(volume);
210+
} catch (e) {
211+
// FIXME: we will always get G_IO_ERROR_FAILED from the gvfs udisks
212+
// backend, see https://bugs.freedesktop.org/show_bug.cgi?id=51271
213+
// To reask the password if the user input was empty or wrong, we
214+
// will check for corresponding error messages. However, these
215+
// error strings are not unique for the cases in the comments below.
216+
if (e.message.includes('No key available with this passphrase') || // cryptsetup
217+
e.message.includes('No key available to unlock device') || // udisks (no password)
218+
// libblockdev wrong password opening LUKS device
219+
e.message.includes('Failed to activate device: Incorrect passphrase') ||
220+
// cryptsetup returns EINVAL in many cases, including wrong TCRYPT password/parameters
221+
e.message.includes('Failed to load device\'s parameters: Invalid argument')) {
222+
this._reaskPassword(volume);
223+
} else {
224+
if (e.message.includes('Compiled against a version of libcryptsetup that does not support the VeraCrypt PIM setting')) {
225+
Main.notifyError(_("Unable to unlock volume"),
226+
_("The installed udisks version does not support the PIM setting"));
227+
}
228+
229+
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
230+
log(`Unable to mount volume ${volume.get_name()}: ${e.toString()}`);
231+
this._closeOperation(volume);
232+
}
233+
}
234+
}
235+
236+
_onVolumeRemoved(monitor, volume) {
237+
if (volume._allowAutorunExpireId && volume._allowAutorunExpireId > 0) {
238+
GLib.source_remove(volume._allowAutorunExpireId);
239+
delete volume._allowAutorunExpireId;
240+
}
241+
}
242+
243+
_reaskPassword(volume) {
244+
let prevOperation = this._activeOperations.get(volume);
245+
const existingDialog = prevOperation?.borrowDialog();
246+
let operation =
247+
new CinnamonMountOperation.CinnamonMountOperation(volume, { existingDialog });
248+
this._mountVolume(volume, operation);
249+
}
250+
251+
_closeOperation(volume) {
252+
let operation = this._activeOperations.get(volume);
253+
if (!operation)
254+
return;
255+
operation.close();
256+
this._activeOperations.delete(volume);
257+
}
258+
259+
_allowAutorun(volume) {
260+
volume.allowAutorun = true;
261+
}
262+
263+
_allowAutorunExpire(volume) {
264+
let id = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, AUTORUN_EXPIRE_TIMEOUT_SECS, () => {
265+
volume.allowAutorun = false;
266+
delete volume._allowAutorunExpireId;
267+
return GLib.SOURCE_REMOVE;
268+
});
269+
volume._allowAutorunExpireId = id;
270+
GLib.Source.set_name_by_id(id, '[cinnamon] volume.allowAutorun');
271+
}
272+
};

0 commit comments

Comments
 (0)