From 9616baa45ea8c3352c5e3eece35cf03e25833996 Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Thu, 16 Oct 2025 14:46:08 -0400 Subject: [PATCH 1/2] Implement mount/unmount 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). --- docs/reference/cinnamon/meson.build | 1 + js/ui/cinnamonMountOperation.js | 795 ++++++++++++++++++++++++++++ js/ui/main.js | 3 + js/ui/placesManager.js | 160 +----- src/cinnamon-mount-operation.c | 189 +++++++ src/cinnamon-mount-operation.h | 41 ++ src/meson.build | 2 + 7 files changed, 1037 insertions(+), 154 deletions(-) create mode 100644 js/ui/cinnamonMountOperation.js create mode 100644 src/cinnamon-mount-operation.c create mode 100644 src/cinnamon-mount-operation.h diff --git a/docs/reference/cinnamon/meson.build b/docs/reference/cinnamon/meson.build index 4514ff5d2c..eaa011ce79 100644 --- a/docs/reference/cinnamon/meson.build +++ b/docs/reference/cinnamon/meson.build @@ -1,6 +1,7 @@ ignore = [ 'cinnamon-recorder-src.h', 'cinnamon-recorder.h', + 'cinnamon-mount-operation.h', st_headers, st_private_headers, tray_headers, diff --git a/js/ui/cinnamonMountOperation.js b/js/ui/cinnamonMountOperation.js new file mode 100644 index 0000000000..98c01504e3 --- /dev/null +++ b/js/ui/cinnamonMountOperation.js @@ -0,0 +1,795 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported CinnamonMountOperation, CinnamonMountOpHandler */ + +const { Clutter, Gio, GLib, GObject, Pango, Cinnamon, St } = imports.gi; + +// const Animation = imports.ui.animation; +const CheckBox = imports.ui.checkBox; +const Dialog = imports.ui.dialog; +const Main = imports.ui.main; +const MessageTray = imports.ui.messageTray; +const ModalDialog = imports.ui.modalDialog; +const Params = imports.misc.params; +const CinnamonEntry = imports.ui.cinnamonEntry; + +const Util = imports.misc.util; + +var LIST_ITEM_ICON_SIZE = 48; +var WORK_SPINNER_ICON_SIZE = 16; + +const REMEMBER_MOUNT_PASSWORD_KEY = 'remember-mount-password'; + +const MountOperationHandlerIface = +' \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +'; + +/* ------ Common Utils ------- */ +function _setButtonsForChoices(dialog, oldChoices, choices) { + let buttons = []; + let buttonsChanged = oldChoices.length !== choices.length; + + for (let idx = 0; idx < choices.length; idx++) { + let button = idx; + + buttonsChanged ||= oldChoices[idx] !== choices[idx]; + + buttons.unshift({ + label: choices[idx], + action: () => dialog.emit('response', button), + }); + } + + if (buttonsChanged) + dialog.setButtons(buttons); +} + +function _setLabelsForMessage(content, message) { + let labels = message.split('\n'); + + content.title = labels.shift(); + content.description = labels.join('\n'); +} + +/* -------------------------------------------------------- */ + +var CinnamonMountOperation = class { + constructor(source, params) { + params = Params.parse(params, { existingDialog: null }); + + this._dialog = null; + this._existingDialog = params.existingDialog; + this._processesDialog = null; + + this.mountOp = new Cinnamon.MountOperation(); + + this.mountOp.connect('ask-question', + this._onAskQuestion.bind(this)); + this.mountOp.connect('ask-password', + this._onAskPassword.bind(this)); + this.mountOp.connect('show-processes-2', + this._onShowProcesses2.bind(this)); + this.mountOp.connect('aborted', + this.close.bind(this)); + this.mountOp.connect('show-unmount-progress', + this._onShowUnmountProgress.bind(this)); + } + + _closeExistingDialog() { + if (!this._existingDialog) + return; + + this._existingDialog.close(); + this._existingDialog = null; + } + + _onAskQuestion(op, message, choices) { + global.log("askQuestion", message, choices); + this._closeExistingDialog(); + this._dialog = new CinnamonMountQuestionDialog(); + + this._dialog.connectObject('response', + (object, choice) => { + this.mountOp.set_choice(choice); + this.mountOp.reply(Gio.MountOperationResult.HANDLED); + + this.close(); + }, this); + + this._dialog.update(message, choices); + this._dialog.open(); + } + + _onAskPassword(op, message, defaultUser, defaultDomain, flags) { + global.log("askPassword", message, defaultUser, defaultDomain, flags); + if (this._existingDialog) { + this._dialog = this._existingDialog; + this._dialog.reaskPassword(); + } else { + this._dialog = new CinnamonMountPasswordDialog(message, flags); + } + + this._dialog.connectObject('response', + (object, choice, password, remember, hiddenVolume, systemVolume, pim) => { + if (choice == -1) { + this.mountOp.reply(Gio.MountOperationResult.ABORTED); + } else { + if (remember) + this.mountOp.set_password_save(Gio.PasswordSave.PERMANENTLY); + else + this.mountOp.set_password_save(Gio.PasswordSave.NEVER); + + this.mountOp.set_password(password); + this.mountOp.set_is_tcrypt_hidden_volume(hiddenVolume); + this.mountOp.set_is_tcrypt_system_volume(systemVolume); + this.mountOp.set_pim(pim); + this.mountOp.reply(Gio.MountOperationResult.HANDLED); + } + }, this); + this._dialog.open(); + } + + close(_op) { + this._closeExistingDialog(); + this._processesDialog = null; + + if (this._dialog) { + this._dialog.close(); + this._dialog = null; + } + + if (this._notifier) { + this._notifier.done(); + this._notifier = null; + } + } + + _onShowProcesses2(op) { + this._closeExistingDialog(); + global.log("showProcesses"); + let processes = op.get_show_processes_pids(); + let choices = op.get_show_processes_choices(); + let message = op.get_show_processes_message(); + + if (!this._processesDialog) { + this._processesDialog = new CinnamonProcessesDialog(); + this._dialog = this._processesDialog; + + this._processesDialog.connectObject('response', + (object, choice) => { + if (choice == -1) { + this.mountOp.reply(Gio.MountOperationResult.ABORTED); + } else { + this.mountOp.set_choice(choice); + this.mountOp.reply(Gio.MountOperationResult.HANDLED); + } + + this.close(); + }, this); + this._processesDialog.open(); + } + + this._processesDialog.update(message, processes, choices); + } + + _onShowUnmountProgress(op, message, timeLeft, bytesLeft) { + global.log("show unmount prog", message, timeLeft, bytesLeft); + if (!this._notifier) + this._notifier = new CinnamonUnmountNotifier(); + + if (bytesLeft == 0) + this._notifier.done(message); + else + this._notifier.show(message); + } + + borrowDialog() { + this._dialog?.disconnectObject(this); + return this._dialog; + } +}; + +var CinnamonUnmountNotifier = class extends MessageTray.Source { + constructor() { + super('unmount-notifier'); + + this._notification = null; + Main.messageTray.add(this); + } + + show(message) { + let [header, text] = message.split('\n', 2); + + if (!this._notification) { + this._notification = new MessageTray.Notification(this, header, text); + this._notification.setTransient(true); + this._notification.setUrgency(MessageTray.Urgency.CRITICAL); + } else { + this._notification.update(header, text); + } + + this.notify(this._notification); + } + + done(message) { + if (this._notification) { + this._notification.destroy(); + this._notification = null; + } + + if (message) { + let [header, text] = message.split('\n', 2); + let notification = new MessageTray.Notification(this, header, text); + notification.setTransient(true); + + this.notify(notification); + } + } + + createNotificationIcon () { + return new St.Icon({ + icon_name: 'xapp-media-removable', + icon_type: St.IconType.SYMBOLIC, + icon_size: this.ICON_SIZE + }); + } +}; + +var CinnamonMountQuestionDialog = GObject.registerClass({ + Signals: { 'response': { param_types: [GObject.TYPE_INT] } }, +}, class CinnamonMountQuestionDialog extends ModalDialog.ModalDialog { + _init() { + super._init({ styleClass: 'mount-question-dialog' }); + + this._oldChoices = []; + + this._content = new Dialog.MessageDialogContent(); + this.contentLayout.add_child(this._content); + } + + vfunc_key_release_event(event) { + if (event.keyval === Clutter.KEY_Escape) { + this.emit('response', -1); + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; + } + + update(message, choices) { + _setLabelsForMessage(this._content, message); + _setButtonsForChoices(this, this._oldChoices, choices); + this._oldChoices = choices; + } +}); + +var CinnamonMountPasswordDialog = GObject.registerClass({ + Signals: { + 'response': { + param_types: [ + GObject.TYPE_INT, + GObject.TYPE_STRING, + GObject.TYPE_BOOLEAN, + GObject.TYPE_BOOLEAN, + GObject.TYPE_BOOLEAN, + GObject.TYPE_UINT, + ], + }, + }, +}, class CinnamonMountPasswordDialog extends ModalDialog.ModalDialog { + _init(message, flags) { + let strings = message.split('\n'); + let title = strings.shift() || null; + let description = strings.shift() || null; + super._init({ styleClass: 'prompt-dialog' }); + + let disksApp = Cinnamon.AppSystem.get_default().lookup_app('org.gnome.DiskUtility.desktop'); + + let content = new Dialog.MessageDialogContent({ title, description }); + + let passwordGridLayout = new Clutter.GridLayout({ orientation: Clutter.Orientation.VERTICAL }); + let passwordGrid = new St.Widget({ + style_class: 'prompt-dialog-password-grid', + layout_manager: passwordGridLayout, + }); + passwordGridLayout.hookup_style(passwordGrid); + + let rtl = passwordGrid.get_text_direction() === Clutter.TextDirection.RTL; + let curGridRow = 0; + + if (flags & Gio.AskPasswordFlags.TCRYPT) { + this._hiddenVolume = new CheckBox.CheckBox(_("Hidden Volume")); + content.add_child(this._hiddenVolume); + + this._systemVolume = new CheckBox.CheckBox(_("Windows System Volume")); + content.add_child(this._systemVolume); + + this._keyfilesCheckbox = new CheckBox.CheckBox(_("Uses Keyfiles")); + this._keyfilesCheckbox.connect("clicked", this._onKeyfilesCheckboxClicked.bind(this)); + content.add_child(this._keyfilesCheckbox); + + this._keyfilesLabel = new St.Label({ visible: false }); + if (disksApp) { + this._keyfilesLabel.clutter_text.set_markup( + /* Translators: %s is the Disks application */ + _('To unlock a volume that uses keyfiles, use the %s utility instead.') + .format(disksApp.get_name())); + } else { + this._keyfilesLabel.clutter_text.set_markup( + _('You need an external utility like Disks to unlock a volume that uses keyfiles.')); + } + this._keyfilesLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + this._keyfilesLabel.clutter_text.line_wrap = true; + content.add_child(this._keyfilesLabel); + + this._pimEntry = new St.PasswordEntry({ + style_class: 'prompt-dialog-password-entry', + hint_text: _('PIM Number'), + can_focus: true, + x_expand: true, + }); + this._pimEntry.clutter_text.connect('activate', this._onEntryActivate.bind(this)); + CinnamonEntry.addContextMenu(this._pimEntry); + + if (rtl) + passwordGridLayout.attach(this._pimEntry, 1, curGridRow, 1, 1); + else + passwordGridLayout.attach(this._pimEntry, 0, curGridRow, 1, 1); + curGridRow += 1; + } else { + this._hiddenVolume = null; + this._systemVolume = null; + this._pimEntry = null; + } + + this._passwordEntry = new St.PasswordEntry({ + style_class: 'prompt-dialog-password-entry', + hint_text: _('Password'), + can_focus: true, + x_expand: true, + }); + this._passwordEntry.clutter_text.connect('activate', this._onEntryActivate.bind(this)); + this.setInitialKeyFocus(this._passwordEntry); + CinnamonEntry.addContextMenu(this._passwordEntry); + + // this._workSpinner = new Animation.Spinner(WORK_SPINNER_ICON_SIZE, { + // animate: true, + // }); + + if (rtl) { + // passwordGridLayout.attach(this._workSpinner, 0, curGridRow, 1, 1); + passwordGridLayout.attach(this._passwordEntry, 1, curGridRow, 1, 1); + } else { + passwordGridLayout.attach(this._passwordEntry, 0, curGridRow, 1, 1); + // passwordGridLayout.attach(this._workSpinner, 1, curGridRow, 1, 1); + } + curGridRow += 1; + + let warningBox = new St.BoxLayout({ vertical: true }); + + let capsLockWarning = new CinnamonEntry.CapsLockWarning(); + warningBox.add_child(capsLockWarning); + + this._errorMessageLabel = new St.Label({ + style_class: 'prompt-dialog-error-label', + opacity: 0, + }); + this._errorMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + this._errorMessageLabel.clutter_text.line_wrap = true; + warningBox.add_child(this._errorMessageLabel); + + passwordGridLayout.attach(warningBox, 0, curGridRow, 2, 1); + + content.add_child(passwordGrid); + + // if (flags & Gio.AskPasswordFlags.SAVING_SUPPORTED) { + // this._rememberChoice = new CheckBox.CheckBox(_("Remember Password")); + // // this._rememberChoice.checked = + // // global.settings.get_boolean(REMEMBER_MOUNT_PASSWORD_KEY); + // content.add_child(this._rememberChoice); + // } else { + // this._rememberChoice = null; + // } + + this.contentLayout.add_child(content); + + this._defaultButtons = [{ + label: _("Cancel"), + action: this._onCancelButton.bind(this), + key: Clutter.KEY_Escape, + }, { + label: _("Unlock"), + action: this._onUnlockButton.bind(this), + default: true, + }]; + + this._usesKeyfilesButtons = [{ + label: _("Cancel"), + action: this._onCancelButton.bind(this), + key: Clutter.KEY_Escape, + }]; + + if (disksApp) { + this._usesKeyfilesButtons.push({ + /* Translators: %s is the Disks application */ + label: _('Open %s').format(disksApp.get_name()), + action: () => { + disksApp.activate(); + this._onCancelButton(); + }, + default: true, + }); + } + + this.setButtons(this._defaultButtons); + } + + reaskPassword() { + // this._workSpinner.stop(); + this._passwordEntry.set_text(''); + this._errorMessageLabel.text = _('Sorry, that didn’t work. Please try again.'); + this._errorMessageLabel.opacity = 255; + + Util.wiggle(this._passwordEntry); + } + + _onCancelButton() { + this.emit('response', -1, '', false, false, false, 0); + } + + _onUnlockButton() { + this._onEntryActivate(); + } + + _onEntryActivate() { + let pim = 0; + if (this._pimEntry !== null) { + pim = this._pimEntry.get_text(); + + if (isNaN(pim)) { + this._pimEntry.set_text(''); + this._errorMessageLabel.text = _('The PIM must be a number or empty.'); + this._errorMessageLabel.opacity = 255; + return; + } + + this._errorMessageLabel.opacity = 0; + } + + // global.settings.set_boolean(REMEMBER_MOUNT_PASSWORD_KEY, + // this._rememberChoice && this._rememberChoice.checked); + + // this._workSpinner.play(); + this.emit('response', 1, + this._passwordEntry.get_text(), + this._rememberChoice && + this._rememberChoice.checked, + this._hiddenVolume && + this._hiddenVolume.checked, + this._systemVolume && + this._systemVolume.checked, + parseInt(pim)); + } + + _onKeyfilesCheckboxClicked() { + let useKeyfiles = this._keyfilesCheckbox.checked; + this._passwordEntry.reactive = !useKeyfiles; + this._passwordEntry.can_focus = !useKeyfiles; + this._pimEntry.reactive = !useKeyfiles; + this._pimEntry.can_focus = !useKeyfiles; + this._rememberChoice.reactive = !useKeyfiles; + this._rememberChoice.can_focus = !useKeyfiles; + this._keyfilesLabel.visible = useKeyfiles; + this.setButtons(useKeyfiles ? this._usesKeyfilesButtons : this._defaultButtons); + } +}); + +var CinnamonProcessesDialog = GObject.registerClass({ + Signals: { 'response': { param_types: [GObject.TYPE_INT] } }, +}, class CinnamonProcessesDialog extends ModalDialog.ModalDialog { + _init() { + super._init({ styleClass: 'processes-dialog' }); + + this._oldChoices = []; + + this._content = new Dialog.MessageDialogContent(); + this.contentLayout.add_child(this._content); + + this._applicationSection = new Dialog.ListSection(); + this._applicationSection.hide(); + this.contentLayout.add_child(this._applicationSection); + } + + vfunc_key_release_event(event) { + if (event.keyval === Clutter.KEY_Escape) { + this.emit('response', -1); + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; + } + + _setAppsForPids(pids) { + // remove all the items + this._applicationSection.list.destroy_all_children(); + global.log(pids); + pids.forEach(pid => { + let tracker = Cinnamon.WindowTracker.get_default(); + let app = tracker.get_app_from_pid(pid); + + if (!app) + return; + + let listItem = new Dialog.ListSectionItem({ + icon_actor: app.create_icon_texture(LIST_ITEM_ICON_SIZE), + title: app.get_name(), + }); + this._applicationSection.list.add_child(listItem); + }); + + this._applicationSection.visible = + this._applicationSection.list.get_n_children() > 0; + } + + update(message, processes, choices) { + this._setAppsForPids(processes); + _setLabelsForMessage(this._content, message); + _setButtonsForChoices(this, this._oldChoices, choices); + this._oldChoices = choices; + } +}); + +var CinnamonMountOperationType = { + NONE: 0, + ASK_PASSWORD: 1, + ASK_QUESTION: 2, + SHOW_PROCESSES: 3, +}; + +var CinnamonMountOpHandler = class { + constructor() { + this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(MountOperationHandlerIface, this); + this._dbusImpl.export(Gio.DBus.session, '/org/gtk/MountOperationHandler'); + Gio.bus_own_name_on_connection(Gio.DBus.session, 'org.gtk.MountOperationHandler', + Gio.BusNameOwnerFlags.REPLACE, null, null); + + this._dialog = null; + + this._ensureEmptyRequest(); + } + + _ensureEmptyRequest() { + this._currentId = null; + this._currentInvocation = null; + this._currentType = CinnamonMountOperationType.NONE; + } + + _clearCurrentRequest(response, details) { + if (this._currentInvocation) { + this._currentInvocation.return_value( + GLib.Variant.new('(ua{sv})', [response, details])); + } + + this._ensureEmptyRequest(); + } + + _setCurrentRequest(invocation, id, type) { + let oldId = this._currentId; + let oldType = this._currentType; + let requestId = `${id}@${invocation.get_sender()}`; + + this._clearCurrentRequest(Gio.MountOperationResult.UNHANDLED, {}); + + this._currentInvocation = invocation; + this._currentId = requestId; + this._currentType = type; + + if (this._dialog && (oldId == requestId) && (oldType == type)) + return true; + + return false; + } + + _closeDialog() { + if (this._dialog) { + this._dialog.close(); + this._dialog = null; + } + } + + /** + * AskPassword: + * @param {Array} params + * {string} id: an opaque ID identifying the object for which + * the operation is requested + * {string} message: the message to display + * {string} icon_name: the name of an icon to display + * {string} default_user: the default username for display + * {string} default_domain: the default domain for display + * {Gio.AskPasswordFlags} flags: a set of GAskPasswordFlags + * {Gio.MountOperationResults} response: a GMountOperationResult + * {Object} response_details: a dictionary containing response details as + * entered by the user. The dictionary MAY contain the following + * properties: + * - "password" -> (s): a password to be used to complete the mount operation + * - "password_save" -> (u): a GPasswordSave + * @param {Gio.DBusMethodInvocation} invocation + * The ID must be unique in the context of the calling process. + * + * The dialog will stay visible until clients call the Close() method, or + * another dialog becomes visible. + * Calling AskPassword again for the same id will have the effect to clear + * the existing dialog and update it with a message indicating the previous + * attempt went wrong. + */ + AskPasswordAsync(params, invocation) { + let [id, message, iconName_, defaultUser_, defaultDomain_, flags] = params; + global.log("ask pass", message); + if (this._setCurrentRequest(invocation, id, CinnamonMountOperationType.ASK_PASSWORD)) { + this._dialog.reaskPassword(); + return; + } + + this._closeDialog(); + + this._dialog = new CinnamonMountPasswordDialog(message, flags); + this._dialog.connect('response', + (object, choice, password, remember, hiddenVolume, systemVolume, pim) => { + let details = {}; + let response; + + if (choice == -1) { + response = Gio.MountOperationResult.ABORTED; + } else { + response = Gio.MountOperationResult.HANDLED; + + let passSave = remember ? Gio.PasswordSave.PERMANENTLY : Gio.PasswordSave.NEVER; + details['password_save'] = GLib.Variant.new('u', passSave); + details['password'] = GLib.Variant.new('s', password); + details['hidden_volume'] = GLib.Variant.new('b', hiddenVolume); + details['system_volume'] = GLib.Variant.new('b', systemVolume); + details['pim'] = GLib.Variant.new('u', pim); + } + + this._clearCurrentRequest(response, details); + }); + this._dialog.open(); + } + + /** + * AskQuestion: + * @param {Array} params - params + * {string} id: an opaque ID identifying the object for which + * the operation is requested + * The ID must be unique in the context of the calling process. + * {string} message: the message to display + * {string} icon_name: the name of an icon to display + * {string[]} choices: an array of choice strings + * @param {Gio.DBusMethodInvocation} invocation - invocation + * + * The dialog will stay visible until clients call the Close() method, or + * another dialog becomes visible. + * Calling AskQuestion again for the same id will have the effect to clear + * update the dialog with the new question. + */ + AskQuestionAsync(params, invocation) { + let [id, message, iconName_, choices] = params; + global.log("ask question", message); + + if (this._setCurrentRequest(invocation, id, CinnamonMountOperationType.ASK_QUESTION)) { + this._dialog.update(message, choices); + return; + } + + this._closeDialog(); + + this._dialog = new CinnamonMountQuestionDialog(message); + this._dialog.connect('response', (object, choice) => { + let response; + let details = {}; + + if (choice == -1) { + response = Gio.MountOperationResult.ABORTED; + } else { + response = Gio.MountOperationResult.HANDLED; + details['choice'] = GLib.Variant.new('i', choice); + } + + this._clearCurrentRequest(response, details); + }); + + this._dialog.update(message, choices); + this._dialog.open(); + } + + /** + * ShowProcesses: + * @param {Array} params - params + * {string} id: an opaque ID identifying the object for which + * the operation is requested + * The ID must be unique in the context of the calling process. + * {string} message: the message to display + * {string} icon_name: the name of an icon to display + * {number[]} application_pids: the PIDs of the applications to display + * {string[]} choices: an array of choice strings + * @param {Gio.DBusMethodInvocation} invocation - invocation + * + * The dialog will stay visible until clients call the Close() method, or + * another dialog becomes visible. + * Calling ShowProcesses again for the same id will have the effect to clear + * the existing dialog and update it with the new message and the new list + * of processes. + */ + ShowProcessesAsync(params, invocation) { + let [id, message, iconName_, applicationPids, choices] = params; + + if (this._setCurrentRequest(invocation, id, CinnamonMountOperationType.SHOW_PROCESSES)) { + this._dialog.update(message, applicationPids, choices); + return; + } + + this._closeDialog(); + global.log("show processes "); + this._dialog = new CinnamonProcessesDialog(); + this._dialog.connect('response', (object, choice) => { + let response; + let details = {}; + + if (choice == -1) { + response = Gio.MountOperationResult.ABORTED; + } else { + response = Gio.MountOperationResult.HANDLED; + details['choice'] = GLib.Variant.new('i', choice); + } + + this._clearCurrentRequest(response, details); + }); + + this._dialog.update(message, applicationPids, choices); + this._dialog.open(); + } + + /** + * Close: + * @param {Array} _params - params + * @param {Gio.DBusMethodInvocation} _invocation - invocation + * + * Closes a dialog previously opened by AskPassword, AskQuestion or ShowProcesses. + * If no dialog is open, does nothing. + */ + Close(_params, _invocation) { + this._clearCurrentRequest(Gio.MountOperationResult.UNHANDLED, {}); + this._closeDialog(); + } +}; diff --git a/js/ui/main.js b/js/ui/main.js index 18f74fa092..da00193dbb 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -116,6 +116,7 @@ const NetworkAgent = imports.ui.networkAgent; const NotificationDaemon = imports.ui.notificationDaemon; const WindowAttentionHandler = imports.ui.windowAttentionHandler; const CinnamonDBus = imports.ui.cinnamonDBus; +const CinnamonMountOperation = imports.ui.cinnamonMountOperation; const Screenshot = imports.ui.screenshot; const ThemeManager = imports.ui.themeManager; const Magnifier = imports.ui.magnifier; @@ -162,6 +163,7 @@ var windowAttentionHandler = null; var screenRecorder = null; var cinnamonAudioSelectionDBusService = null; var cinnamonDBusService = null; +var cinnamonMountOpDBusService = null; var screenshotService = null; var modalCount = 0; var modalActorFocusStack = []; @@ -331,6 +333,7 @@ function start() { new CinnamonPortalHandler(); cinnamonAudioSelectionDBusService = new AudioDeviceSelection.AudioDeviceSelectionDBus(); cinnamonDBusService = new CinnamonDBus.CinnamonDBus(); + cinnamonMountOpDBusService = new CinnamonMountOperation.CinnamonMountOpHandler(); setRunState(RunState.STARTUP); screenshotService = new Screenshot.ScreenshotService(); diff --git a/js/ui/placesManager.js b/js/ui/placesManager.js index f96a95c648..f8740dc48d 100644 --- a/js/ui/placesManager.js +++ b/js/ui/placesManager.js @@ -8,6 +8,7 @@ const Mainloop = imports.mainloop; const Signals = imports.signals; const St = imports.gi.St; +const CinnamonMountOperation = imports.ui.cinnamonMountOperation; const Main = imports.ui.main; const MessageTray = imports.ui.messageTray; const Params = imports.misc.params; @@ -125,172 +126,23 @@ PlaceDeviceInfo.prototype = { if (!this.isRemovable()) return; - let mountOp = new Gio.MountOperation(); + let mountOp = new CinnamonMountOperation.CinnamonMountOperation(); let drive = this._mount.get_drive(); let volume = this._mount.get_volume(); if (drive && drive.get_start_stop_type() == Gio.DriveStartStopType.SHUTDOWN && drive.can_stop()) { - drive.stop(0, mountOp, null, Lang.bind(this, this._stopFinish)); + drive.stop(0, mountOp.mountOp, null, null); } else { - if (drive && drive.can_eject()) - drive.eject_with_operation(0, mountOp, null, Lang.bind(this, this._ejectFinish, true)); - else if (volume && volume.can_eject()) - volume.eject_with_operation(0, mountOp, null, Lang.bind(this, this._ejectFinish, false)); - else if (this._mount.can_eject()) - this._mount.eject_with_operation(0, mountOp, null, Lang.bind(this, this._ejectFinish, false)); + if ((drive && drive.can_eject()) || (volume && volume.can_eject()) || this._mount.can_eject()) + drive.eject_with_operation(0, mountOp.mountOp, null, null); else if (this._mount.can_unmount()) - this._mount.unmount_with_operation(0, mountOp, null, Lang.bind(this, this._removeFinish)); + this._mount.unmount_with_operation(0, mountOp.mountOp, null, null); } this.busyWaitId = 0; return false; - }, - - _sendNotification: function(msg1, msg2 = null, withButton = false, persistent = false) { - if (Main.messageTray) { - if (persistent && this.busyNotification != null) { - return; - } - - if (!persistent && this.busyNotification) { - this.busyNotification.destroy(); - this.busyNotification = null; - } - - let source = new MessageTray.SystemNotificationSource(); - Main.messageTray.add(source); - let notification = new MessageTray.Notification(source, msg1, msg2); - notification.setTransient(true); - notification.setUrgency(persistent ? MessageTray.Urgency.CRITICAL : MessageTray.Urgency.NORMAL); - if (withButton) { - notification.addButton('system-undo', _("Retry")); - notification.connect('action-invoked', Lang.bind(this, this.remove)); - } - source.notify(notification); - if (persistent) { - this.busyNotification = notification; - this.destroySignalId = notification.connect("destroy", () => { - this.busyNotification.disconnect(this.destroySignalId); - this.busyNotification = null; - this.destroySignalId = 0; - }) - } - } else { - if (msg2) - global.log(msg1 + ': ' + msg2); - else - global.log(msg1); - } - }, - - _stopFinish: function(drive, res) { - if (DEBUG) global.log("PlacesManager: **_stopFinish**"); - let driveName = drive.get_name(); // Ex: USB Flash Drive - let unixDevice = drive.get_identifier('unix-device'); // Ex: /dev/sdc - let msg1 = _("%s (%s) has just been stopped.").format(driveName, this.name); - let msg2 = _("Device %s can be turned off, if necessary.").format(unixDevice); - let btn = false; // Show the 'Retry' button? - try { - drive.stop_finish(res); - } catch(e) { - if (e.code == Gio.IOErrorEnum.BUSY) { - msg1 = _("Device %s is busy, please wait.".format(drive.get_name())); - msg2 = _("Do not disconnect or data loss may occur."); - - this._sendNotification(msg1, msg2, false, true); - this.busyWaitId = Mainloop.timeout_add_seconds(2, ()=>this._tryRemove()); - return; - } - btn = true; - msg1 = _("Unable to stop the drive %s (%s)").format(drive.get_name(), this.name); - msg2 = e.message; - } - if (DEBUG) global.log(msg1 + ": " + msg2); - this._sendNotification(msg1, msg2, btn); - }, - - _ejectFinish: function(source, res, is_drive) { - if (DEBUG) global.log("PlacesManager: **_ejectFinish**"); - let msg1; - let msg2 = null; - let btn = false; - - if (is_drive) { - let driveName = source.get_name(); // Ex: USB Flash Drive - let unixDevice = source.get_identifier('unix-device'); // Ex: /dev/sdc - msg1 = _("%s (%s) can be safely unplugged.").format(driveName, this.name); - msg2 = _("Device %s can be removed.").format(unixDevice); - } else { - msg1 = _("%s (%s) has just been ejected.").format(source.get_name(), this.name); - } - try { - source.eject_with_operation_finish(res); - } catch(e) { - if (e.code == Gio.IOErrorEnum.BUSY) { - msg1 = _("Device %s is busy, please wait.".format(source.get_name())); - msg2 = _("Do not remove or data loss may occur."); - - this._sendNotification(msg1, msg2, false, true); - this.busyWaitId = Mainloop.timeout_add_seconds(2, ()=>this._tryRemove()); - return; - } - btn = true; - msg1 = _("Unable to eject the drive %s (%s)").format(source.get_name(), this.name); - msg2 = e.message; - } - if (DEBUG) global.log(msg1 + ": " + msg2); - this._sendNotification(msg1, msg2, btn); - }, - - _removeFinish: function(o, res, data) { - if (DEBUG) global.log("PlacesManager: **_removeFinish**"); - let msg1 = _("Successfully unmounted %s (%s)").format(o.get_name(), this.name); - let msg2 = null; - let btn = false; - - // 'this._mount.can_eject()' seems to be ever false. Thus, only the 'else' part will be used. - // If no issues are reported, these 19 lines of code commented below can be deleted. - //~ if (this._mount.can_eject()) { - //~ msg1 = _("%s (%s) can be safely unplugged").format(o.get_name(), this.name); - //~ msg2 = _("Device can be removed"); - //~ try { - //~ this._mount.eject_with_operation_finish(res); - //~ } catch(e) { - //~ btn = true; - //~ msg1 = _("Failed to eject %s (%s)").format(o.get_name(), this.name); - //~ msg2 = e.message; - //~ } - //~ } else { - //~ try { - //~ this._mount.unmount_with_operation_finish(res); - //~ } catch(e) { - //~ btn = true; - //~ msg1 = _("Failed to unmount %s (%s)").format(o.get_name(), this.name); - //~ msg2 = e.message; - //~ } - //~ } - // <--Beginning of the code replacing the 19 lines above: - try { - this._mount.unmount_with_operation_finish(res); - } catch(e) { - if (e.code == Gio.IOErrorEnum.BUSY) { - msg1 = _("Device %s is busy, please wait.".format(o.get_name())); - msg2 = _("Do not disconnect or data loss may occur."); - - this._sendNotification(msg1, msg2, false, true); - this.busyWaitId = Mainloop.timeout_add_seconds(2, ()=>this._tryRemove()); - return; - } - btn = true; - msg1 = _("Failed to unmount %s (%s)").format(o.get_name(), this.name); - msg2 = e.message; - } - // End of this code.--> - - if (DEBUG) global.log(msg1 + ": " + msg2); - this._sendNotification(msg1, msg2, btn); } }; diff --git a/src/cinnamon-mount-operation.c b/src/cinnamon-mount-operation.c new file mode 100644 index 0000000000..d7096886eb --- /dev/null +++ b/src/cinnamon-mount-operation.c @@ -0,0 +1,189 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Author: Cosimo Cecchi + * + */ + +#include "cinnamon-mount-operation.h" + +/* This is a dummy class; we would like to be able to subclass the + * object from JS but we can't yet; the default GMountOperation impl + * automatically calls g_mount_operation_reply(UNHANDLED) after an idle, + * in interactive methods. We want to handle the reply ourselves + * instead, so we just override the default methods with empty ones, + * except for ask-password, as we don't want to handle that. + * + * Also, we need to workaround the fact that gjs doesn't support type + * annotations for signals yet (so we can't effectively forward e.g. + * the GPid array to JS). + * See https://bugzilla.gnome.org/show_bug.cgi?id=645978 + */ + +enum { + SHOW_PROCESSES_2, + NUM_SIGNALS +}; + +static guint signals[NUM_SIGNALS] = { 0, }; + +typedef struct _CinnamonMountOperationPrivate CinnamonMountOperationPrivate; + +struct _CinnamonMountOperation +{ + GMountOperation parent_instance; + + CinnamonMountOperationPrivate *priv; +}; + +struct _CinnamonMountOperationPrivate { + GArray *pids; + gchar **choices; + gchar *message; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (CinnamonMountOperation, cinnamon_mount_operation, G_TYPE_MOUNT_OPERATION); + +static void +cinnamon_mount_operation_init (CinnamonMountOperation *self) +{ + self->priv = cinnamon_mount_operation_get_instance_private (self); +} + +static void +cinnamon_mount_operation_ask_password (GMountOperation *op, + const char *message, + const char *default_user, + const char *default_domain, + GAskPasswordFlags flags) +{ + /* do nothing */ +} + +static void +cinnamon_mount_operation_ask_question (GMountOperation *op, + const char *message, + const char *choices[]) +{ + /* do nothing */ +} + +static void +cinnamon_mount_operation_show_processes (GMountOperation *operation, + const gchar *message, + GArray *processes, + const gchar *choices[]) +{ + CinnamonMountOperation *self = CINNAMON_MOUNT_OPERATION (operation); + + if (self->priv->pids != NULL) + { + g_array_unref (self->priv->pids); + self->priv->pids = NULL; + } + + g_free (self->priv->message); + g_strfreev (self->priv->choices); + + /* save the parameters */ + self->priv->pids = g_array_ref (processes); + self->priv->choices = g_strdupv ((gchar **) choices); + self->priv->message = g_strdup (message); + + g_signal_emit (self, signals[SHOW_PROCESSES_2], 0); +} + +static void +cinnamon_mount_operation_finalize (GObject *obj) +{ + CinnamonMountOperation *self = CINNAMON_MOUNT_OPERATION (obj); + + g_strfreev (self->priv->choices); + g_free (self->priv->message); + + if (self->priv->pids != NULL) + { + g_array_unref (self->priv->pids); + self->priv->pids = NULL; + } + + G_OBJECT_CLASS (cinnamon_mount_operation_parent_class)->finalize (obj); +} + +static void +cinnamon_mount_operation_class_init (CinnamonMountOperationClass *klass) +{ + GMountOperationClass *mclass; + GObjectClass *oclass; + + mclass = G_MOUNT_OPERATION_CLASS (klass); + mclass->show_processes = cinnamon_mount_operation_show_processes; + mclass->ask_question = cinnamon_mount_operation_ask_question; + mclass->ask_password = cinnamon_mount_operation_ask_password; + + oclass = G_OBJECT_CLASS (klass); + oclass->finalize = cinnamon_mount_operation_finalize; + + signals[SHOW_PROCESSES_2] = + g_signal_new ("show-processes-2", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +GMountOperation * +cinnamon_mount_operation_new (void) +{ + return g_object_new (CINNAMON_TYPE_MOUNT_OPERATION, NULL); +} + +/** + * cinnamon_mount_operation_get_show_processes_pids: + * @self: a #CinnamonMountOperation + * + * Returns: (transfer full) (element-type GPid): a #GArray + */ +GArray * +cinnamon_mount_operation_get_show_processes_pids (CinnamonMountOperation *self) +{ + return g_array_ref (self->priv->pids); +} + +/** + * cinnamon_mount_operation_get_show_processes_choices: + * @self: a #CinnamonMountOperation + * + * Returns: (transfer full): + */ +gchar ** +cinnamon_mount_operation_get_show_processes_choices (CinnamonMountOperation *self) +{ + return g_strdupv (self->priv->choices); +} + +/** + * cinnamon_mount_operation_get_show_processes_message: + * @self: a #CinnamonMountOperation + * + * Returns: (transfer full): + */ +gchar * +cinnamon_mount_operation_get_show_processes_message (CinnamonMountOperation *self) +{ + return g_strdup (self->priv->message); +} diff --git a/src/cinnamon-mount-operation.h b/src/cinnamon-mount-operation.h new file mode 100644 index 0000000000..ed79929cd6 --- /dev/null +++ b/src/cinnamon-mount-operation.h @@ -0,0 +1,41 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Author: Cosimo Cecchi + * + */ + +#ifndef __CINNAMON_MOUNT_OPERATION_H__ +#define __CINNAMON_MOUNT_OPERATION_H__ + +#include + +G_BEGIN_DECLS + +#define CINNAMON_TYPE_MOUNT_OPERATION (cinnamon_mount_operation_get_type ()) +G_DECLARE_FINAL_TYPE (CinnamonMountOperation, cinnamon_mount_operation, + CINNAMON, MOUNT_OPERATION, GMountOperation) + +GMountOperation *cinnamon_mount_operation_new (void); + +GArray * cinnamon_mount_operation_get_show_processes_pids (CinnamonMountOperation *self); +gchar ** cinnamon_mount_operation_get_show_processes_choices (CinnamonMountOperation *self); +gchar * cinnamon_mount_operation_get_show_processes_message (CinnamonMountOperation *self); + +G_END_DECLS + +#endif /* __CINNAMON_MOUNT_OPERATION_H__ */ diff --git a/src/meson.build b/src/meson.build index a44cb7c0e9..93b34202bf 100644 --- a/src/meson.build +++ b/src/meson.build @@ -19,6 +19,7 @@ cinnamon_headers = [ 'cinnamon-glsl-effect.h', 'cinnamon-gtk-embed.h', 'cinnamon-global.h', + 'cinnamon-mount-operation.h', 'cinnamon-perf-log.h', 'cinnamon-screen.h', 'cinnamon-screenshot.h', @@ -53,6 +54,7 @@ cinnamon_sources = [ 'cinnamon-global.c', 'cinnamon-keyring-prompt.c', 'cinnamon-keyring-prompt.h', + 'cinnamon-mount-operation.c', 'cinnamon-perf-log.c', 'cinnamon-polkit-authentication-agent.c', 'cinnamon-polkit-authentication-agent.h', From fbca1471c8fbf44aa209cffc5b39d206fba0a6b0 Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Thu, 16 Oct 2025 14:50:15 -0400 Subject: [PATCH 2/2] 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. --- js/ui/automountManager.js | 272 +++++++++++++++++ js/ui/autorunManager.js | 313 ++++++++++++++++++++ js/ui/main.js | 7 + src/hotplug-sniffer/cinnamon-mime-sniffer.c | 104 +++---- src/hotplug-sniffer/cinnamon-mime-sniffer.h | 31 +- src/hotplug-sniffer/hotplug-sniffer.c | 19 +- 6 files changed, 646 insertions(+), 100 deletions(-) create mode 100644 js/ui/automountManager.js create mode 100644 js/ui/autorunManager.js diff --git a/js/ui/automountManager.js b/js/ui/automountManager.js new file mode 100644 index 0000000000..a8b93c1e45 --- /dev/null +++ b/js/ui/automountManager.js @@ -0,0 +1,272 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported Component */ + +const { Gio, GLib } = imports.gi; +const Params = imports.misc.params; + +const GnomeSession = imports.misc.gnomeSession; +const Main = imports.ui.main; +const CinnamonMountOperation = imports.ui.cinnamonMountOperation; + +var GNOME_SESSION_AUTOMOUNT_INHIBIT = 16; + +// GSettings keys +const SETTINGS_SCHEMA = 'org.cinnamon.desktop.media-handling'; +const SETTING_ENABLE_AUTOMOUNT = 'automount'; + +var AUTORUN_EXPIRE_TIMEOUT_SECS = 10; + +var AutomountManager = class { + constructor() { + this._settings = new Gio.Settings({ schema_id: SETTINGS_SCHEMA }); + this._activeOperations = new Map(); + + GnomeSession.SessionManager((proxy, error) => { + if (error) + return; + + this._session = proxy; + this.actor.show(); + this.updateStatus(); + + this._session.connectSignal( + "InhibitorAdded", + this._InhibitorsChanged.bind(this) + ); + + this._session.connectSignal( + "InhibitorRemoved", + this._InhibitorsChanged.bind(this) + ); + }); + + this._inhibited = false; + + this._volumeMonitor = Gio.VolumeMonitor.get(); + this.enable(); + } + + enable() { + this._volumeMonitor.connectObject( + 'volume-added', this._onVolumeAdded.bind(this), + 'volume-removed', this._onVolumeRemoved.bind(this), + 'drive-connected', this._onDriveConnected.bind(this), + 'drive-disconnected', this._onDriveDisconnected.bind(this), + 'drive-eject-button', this._onDriveEjectButton.bind(this), this); + + this._mountAllId = GLib.idle_add(GLib.PRIORITY_DEFAULT, this._startupMountAll.bind(this)); + GLib.Source.set_name_by_id(this._mountAllId, '[cinnamon] this._startupMountAll'); + } + + disable() { + this._volumeMonitor.disconnectObject(this); + + if (this._mountAllId > 0) { + GLib.source_remove(this._mountAllId); + this._mountAllId = 0; + } + } + + async _InhibitorsChanged(_object, _senderName, [_inhibitor]) { + try { + const [inhibited] = + await this._session.IsInhibitedAsync(GNOME_SESSION_AUTOMOUNT_INHIBIT); + this._inhibited = inhibited; + } catch (e) {} + } + + _startupMountAll() { + let volumes = this._volumeMonitor.get_volumes(); + volumes.forEach(volume => { + this._checkAndMountVolume(volume, { + checkSession: false, + useMountOp: false, + allowAutorun: false, + }); + }); + + this._mountAllId = 0; + return GLib.SOURCE_REMOVE; + } + + _onDriveConnected() { + // if we're not in the current ConsoleKit session, + // or screensaver is active, don't play sounds + // if (!this._session.SessionIsActive) + // return; + + let player = global.display.get_sound_player(); + player.play_from_theme('device-added-media', + _("External drive connected"), + null); + } + + _onDriveDisconnected() { + // if we're not in the current ConsoleKit session, + // or screensaver is active, don't play sounds + // if (!this._session.SessionIsActive) + // return; + + let player = global.display.get_sound_player(); + player.play_from_theme('device-removed-media', + _("External drive disconnected"), + null); + } + + _onDriveEjectButton(monitor, drive) { + // TODO: this code path is not tested, as the GVfs volume monitor + // doesn't emit this signal just yet. + // if (!this._session.SessionIsActive) + // return; + + // we force stop/eject in this case, so we don't have to pass a + // mount operation object + if (drive.can_stop()) { + drive.stop(Gio.MountUnmountFlags.FORCE, null, null, + (o, res) => { + try { + drive.stop_finish(res); + } catch (e) { + log(`Unable to stop the drive after drive-eject-button ${e.toString()}`); + } + }); + } else if (drive.can_eject()) { + drive.eject_with_operation(Gio.MountUnmountFlags.FORCE, null, null, + (o, res) => { + try { + drive.eject_with_operation_finish(res); + } catch (e) { + log(`Unable to eject the drive after drive-eject-button ${e.toString()}`); + } + }); + } + } + + _onVolumeAdded(monitor, volume) { + this._checkAndMountVolume(volume); + } + + _checkAndMountVolume(volume, params) { + global.log("check and mount"); + params = Params.parse(params, { + checkSession: true, + useMountOp: true, + allowAutorun: true, + }); + + if (params.checkSession) { + // if we're not in the current ConsoleKit session, + // don't attempt automount + // if (!this._session.SessionIsActive) + // return; + } + + if (this._inhibited) + return; + + // Volume is already mounted, don't bother. + if (volume.get_mount()) + return; + + if (!this._settings.get_boolean(SETTING_ENABLE_AUTOMOUNT) || + !volume.should_automount() || + !volume.can_mount()) { + // allow the autorun to run anyway; this can happen if the + // mount gets added programmatically later, even if + // should_automount() or can_mount() are false, like for + // blank optical media. + this._allowAutorun(volume); + this._allowAutorunExpire(volume); + + return; + } + + if (params.useMountOp) { + let operation = new CinnamonMountOperation.CinnamonMountOperation(volume); + this._mountVolume(volume, operation, params.allowAutorun); + } else { + this._mountVolume(volume, null, params.allowAutorun); + } + } + + _mountVolume(volume, operation, allowAutorun) { + if (allowAutorun) + this._allowAutorun(volume); + + const mountOp = operation?.mountOp ?? null; + this._activeOperations.set(volume, operation); + + volume.mount(0, mountOp, null, + this._onVolumeMounted.bind(this)); + } + + _onVolumeMounted(volume, res) { + global.log("on volume mounted"); + this._allowAutorunExpire(volume); + + try { + volume.mount_finish(res); + this._closeOperation(volume); + } catch (e) { + // FIXME: we will always get G_IO_ERROR_FAILED from the gvfs udisks + // backend, see https://bugs.freedesktop.org/show_bug.cgi?id=51271 + // To reask the password if the user input was empty or wrong, we + // will check for corresponding error messages. However, these + // error strings are not unique for the cases in the comments below. + if (e.message.includes('No key available with this passphrase') || // cryptsetup + e.message.includes('No key available to unlock device') || // udisks (no password) + // libblockdev wrong password opening LUKS device + e.message.includes('Failed to activate device: Incorrect passphrase') || + // cryptsetup returns EINVAL in many cases, including wrong TCRYPT password/parameters + e.message.includes('Failed to load device\'s parameters: Invalid argument')) { + this._reaskPassword(volume); + } else { + if (e.message.includes('Compiled against a version of libcryptsetup that does not support the VeraCrypt PIM setting')) { + Main.notifyError(_("Unable to unlock volume"), + _("The installed udisks version does not support the PIM setting")); + } + + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED)) + log(`Unable to mount volume ${volume.get_name()}: ${e.toString()}`); + this._closeOperation(volume); + } + } + } + + _onVolumeRemoved(monitor, volume) { + if (volume._allowAutorunExpireId && volume._allowAutorunExpireId > 0) { + GLib.source_remove(volume._allowAutorunExpireId); + delete volume._allowAutorunExpireId; + } + } + + _reaskPassword(volume) { + let prevOperation = this._activeOperations.get(volume); + const existingDialog = prevOperation?.borrowDialog(); + let operation = + new CinnamonMountOperation.CinnamonMountOperation(volume, { existingDialog }); + this._mountVolume(volume, operation); + } + + _closeOperation(volume) { + let operation = this._activeOperations.get(volume); + if (!operation) + return; + operation.close(); + this._activeOperations.delete(volume); + } + + _allowAutorun(volume) { + volume.allowAutorun = true; + } + + _allowAutorunExpire(volume) { + let id = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, AUTORUN_EXPIRE_TIMEOUT_SECS, () => { + volume.allowAutorun = false; + delete volume._allowAutorunExpireId; + return GLib.SOURCE_REMOVE; + }); + volume._allowAutorunExpireId = id; + GLib.Source.set_name_by_id(id, '[cinnamon] volume.allowAutorun'); + } +}; diff --git a/js/ui/autorunManager.js b/js/ui/autorunManager.js new file mode 100644 index 0000000000..d35a212596 --- /dev/null +++ b/js/ui/autorunManager.js @@ -0,0 +1,313 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported Component */ + +const { Clutter, Gio, GObject, St } = imports.gi; + +const GnomeSession = imports.misc.gnomeSession; +const Main = imports.ui.main; +const MessageTray = imports.ui.messageTray; + +Gio._promisify(Gio.Mount.prototype, 'guess_content_type'); + +const hotplugSnifferIface = +' \ + \ + \ + \ + \ + \ + \ + \ + \ +'; + +// GSettings keys +const SETTINGS_SCHEMA = 'org.cinnamon.desktop.media-handling'; +const SETTING_DISABLE_AUTORUN = 'autorun-never'; +const SETTING_START_APP = 'autorun-x-content-start-app'; +const SETTING_IGNORE = 'autorun-x-content-ignore'; +const SETTING_OPEN_FOLDER = 'autorun-x-content-open-folder'; + +var AutorunSetting = { + RUN: 0, + IGNORE: 1, + FILES: 2, + ASK: 3, +}; + +// misc utils +function shouldAutorunMount(mount) { + let root = mount.get_root(); + let volume = mount.get_volume(); + + if (!volume || !volume.allowAutorun) + return false; + + if (root.is_native() && isMountRootHidden(root)) + return false; + + return true; +} + +function isMountRootHidden(root) { + let path = root.get_path(); + + // skip any mounts in hidden directory hierarchies + return path.includes('/.'); +} + +function isMountNonLocal(mount) { + // If the mount doesn't have an associated volume, that means it's + // an uninteresting filesystem. Most devices that we care about will + // have a mount, like media players and USB sticks. + let volume = mount.get_volume(); + if (volume == null) + return true; + + return volume.get_identifier("class") == "network"; +} + +function startAppForMount(app, mount) { + let files = []; + let root = mount.get_root(); + let retval = false; + + files.push(root); + + try { + retval = app.launch(files, global.create_app_launch_context()); + } catch (e) { + log(`Unable to launch the app ${app.get_name()}: ${e}`); + } + + return retval; +} + +const HotplugSnifferProxy = Gio.DBusProxy.makeProxyWrapper(hotplugSnifferIface); +function HotplugSniffer() { + return new HotplugSnifferProxy(Gio.DBus.session, + 'org.Cinnamon.HotplugSniffer', + '/org/Cinnamon/HotplugSniffer'); +} + +var ContentTypeDiscoverer = class { + constructor() { + this._settings = new Gio.Settings({ schema_id: SETTINGS_SCHEMA }); + } + + async guessContentTypes(mount) { + let autorunEnabled = !this._settings.get_boolean(SETTING_DISABLE_AUTORUN); + let shouldScan = autorunEnabled && !isMountNonLocal(mount); + + let contentTypes = []; + if (shouldScan) { + try { + contentTypes = await mount.guess_content_type(false, null); + } catch (e) { + log(`Unable to guess content types on added mount ${mount.get_name()}: ${e}`); + } + + if (contentTypes.length === 0) { + const root = mount.get_root(); + const hotplugSniffer = new HotplugSniffer(); + [contentTypes] = await hotplugSniffer.SniffURIAsync(root.get_uri()); + } + } + + // we're not interested in win32 software content types here + contentTypes = contentTypes.filter( + type => type !== 'x-content/win32-software'); + + const apps = []; + contentTypes.forEach(type => { + const app = Gio.app_info_get_default_for_type(type, false); + + if (app) + apps.push(app); + }); + + if (apps.length === 0) + apps.push(Gio.app_info_get_default_for_type('inode/directory', false)); + + return [apps, contentTypes]; + } +}; + +var AutorunManager = class { + constructor() { + // this._session = new GnomeSession.SessionManager(); + this._volumeMonitor = Gio.VolumeMonitor.get(); + + this._dispatcher = new AutorunDispatcher(this); + this.enable(); + } + + enable() { + this._volumeMonitor.connectObject( + 'mount-added', this._onMountAdded.bind(this), + 'mount-removed', this._onMountRemoved.bind(this), this); + } + + disable() { + this._volumeMonitor.disconnectObject(this); + } + + async _onMountAdded(monitor, mount) { + // don't do anything if our session is not the currently + // active one + // if (!this._session.SessionIsActive) + // return; + + const discoverer = new ContentTypeDiscoverer(); + const [apps, contentTypes] = await discoverer.guessContentTypes(mount); + this._dispatcher.addMount(mount, apps, contentTypes); + } + + _onMountRemoved(monitor, mount) { + this._dispatcher.removeMount(mount); + } +}; + +var AutorunDispatcher = class { + constructor(manager) { + this._manager = manager; + this._sources = []; + this._settings = new Gio.Settings({ schema_id: SETTINGS_SCHEMA }); + } + + _getAutorunSettingForType(contentType) { + let runApp = this._settings.get_strv(SETTING_START_APP); + if (runApp.includes(contentType)) + return AutorunSetting.RUN; + + let ignore = this._settings.get_strv(SETTING_IGNORE); + if (ignore.includes(contentType)) + return AutorunSetting.IGNORE; + + let openFiles = this._settings.get_strv(SETTING_OPEN_FOLDER); + if (openFiles.includes(contentType)) + return AutorunSetting.FILES; + + return AutorunSetting.ASK; + } + + _getSourceForMount(mount) { + let filtered = this._sources.filter(source => source.mount == mount); + + // we always make sure not to add two sources for the same + // mount in addMount(), so it's safe to assume filtered.length + // is always either 1 or 0. + if (filtered.length == 1) + return filtered[0]; + + return null; + } + + _addSource(mount, apps) { + // if we already have a source showing for this + // mount, return + if (this._getSourceForMount(mount)) + return; + + // add a new source + this._sources.push(new AutorunSource(this._manager, mount, apps)); + } + + addMount(mount, apps, contentTypes) { + // if autorun is disabled globally, return + if (this._settings.get_boolean(SETTING_DISABLE_AUTORUN)) + return; + + // if the mount doesn't want to be autorun, return + if (!shouldAutorunMount(mount)) + return; + + let setting; + if (contentTypes.length > 0) + setting = this._getAutorunSettingForType(contentTypes[0]); + else + setting = AutorunSetting.ASK; + + // check at the settings for the first content type + // to see whether we should ask + if (setting == AutorunSetting.IGNORE) + return; // return right away + + let success = false; + let app = null; + + if (setting == AutorunSetting.RUN) + app = Gio.app_info_get_default_for_type(contentTypes[0], false); + else if (setting == AutorunSetting.FILES) + app = Gio.app_info_get_default_for_type('inode/directory', false); + + if (app) + success = startAppForMount(app, mount); + + // we fallback here also in case the settings did not specify 'ask', + // but we failed launching the default app or the default file manager + if (!success) + this._addSource(mount, apps); + } + + removeMount(mount) { + let source = this._getSourceForMount(mount); + + // if we aren't tracking this mount, don't do anything + if (!source) + return; + + // destroy the notification source + source.destroy(); + } +}; + +var AutorunSource = class extends MessageTray.Source { + constructor(manager, mount, apps) { + super(mount.get_name()); + + this._manager = manager; + this.mount = mount; + this.apps = apps; + + this._notification = new AutorunNotification(this._manager, this); + + // add ourselves as a source, and popup the notification + Main.messageTray.add(this); + this.notify(this._notification); + } + + createNotificationIcon () { + return new St.Icon({ + gicon: this.mount.get_symbolic_icon(), + icon_type: St.IconType.SYMBOLIC, + icon_size: this.ICON_SIZE + }); + } +}; + +var AutorunNotification = class extends MessageTray.Notification { + constructor(manager, source) { + super(source, source.title); + + this._manager = manager; + this._mount = source.mount; + + this.source.apps.forEach(app => { + this.addButton(app.get_id(), _("Open with %s").format(app.get_name())); + }); + + this.connect("action-invoked", (notification, id) => { + let app = this.source.apps.find(a => a.get_id() == id); + if (app) + startAppForMount(app, this._mount); + }); + } + + activate() { + super.activate(); + + let app = Gio.app_info_get_default_for_type('inode/directory', false); + startAppForMount(app, this._mount); + } +}; diff --git a/js/ui/main.js b/js/ui/main.js index da00193dbb..1e7cd8e738 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -91,6 +91,8 @@ const GObject = imports.gi.GObject; const XApp = imports.gi.XApp; const PointerTracker = imports.misc.pointerTracker; +const AutomountManager = imports.ui.automountManager; +const AutorunManager = imports.ui.autorunManager; const AudioDeviceSelection = imports.ui.audioDeviceSelection; const SoundManager = imports.ui.soundManager; const BackgroundManager = imports.ui.backgroundManager; @@ -164,6 +166,8 @@ var screenRecorder = null; var cinnamonAudioSelectionDBusService = null; var cinnamonDBusService = null; var cinnamonMountOpDBusService = null; +var automountManager = null; +var autorunManager = null; var screenshotService = null; var modalCount = 0; var modalActorFocusStack = []; @@ -334,6 +338,9 @@ function start() { cinnamonAudioSelectionDBusService = new AudioDeviceSelection.AudioDeviceSelectionDBus(); cinnamonDBusService = new CinnamonDBus.CinnamonDBus(); cinnamonMountOpDBusService = new CinnamonMountOperation.CinnamonMountOpHandler(); + automountManager = new AutomountManager.AutomountManager(); + autorunManager = new AutorunManager.AutorunManager(); + setRunState(RunState.STARTUP); screenshotService = new Screenshot.ScreenshotService(); diff --git a/src/hotplug-sniffer/cinnamon-mime-sniffer.c b/src/hotplug-sniffer/cinnamon-mime-sniffer.c index aaa50066c8..c17ca210eb 100644 --- a/src/hotplug-sniffer/cinnamon-mime-sniffer.c +++ b/src/hotplug-sniffer/cinnamon-mime-sniffer.c @@ -14,9 +14,7 @@ * General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street - Suite 500, Boston, MA - * 02110-1335, USA. + * along with this program; if not, see . * * Author: Cosimo Cecchi * @@ -45,8 +43,6 @@ #define DIRECTORY_LOAD_ITEMS_PER_CALLBACK 100 #define HIGH_SCORE_RATIO 0.10 -G_DEFINE_TYPE (CinnamonMimeSniffer, cinnamon_mime_sniffer, G_TYPE_OBJECT); - enum { PROP_FILE = 1, NUM_PROPERTIES @@ -74,16 +70,26 @@ typedef struct { gint total_items; } DeepCountState; +typedef struct _CinnamonMimeSnifferPrivate CinnamonMimeSnifferPrivate; + +struct _CinnamonMimeSniffer +{ + GObject parent_instance; + + CinnamonMimeSnifferPrivate *priv; +}; + struct _CinnamonMimeSnifferPrivate { GFile *file; GCancellable *cancellable; guint watchdog_id; - GSimpleAsyncResult *async_result; - gchar **sniffed_mime; + GTask *task; }; +G_DEFINE_TYPE_WITH_PRIVATE (CinnamonMimeSniffer, cinnamon_mime_sniffer, G_TYPE_OBJECT); + static void deep_count_load (DeepCountState *state, GFile *file); @@ -181,6 +187,7 @@ prepare_async_result (DeepCountState *state) GArray *results; GPtrArray *sniffed_mime; SniffedResult result; + char **mimes; sniffed_mime = g_ptr_array_new (); results = g_array_new (TRUE, TRUE, sizeof (SniffedResult)); @@ -222,16 +229,16 @@ prepare_async_result (DeepCountState *state) out: g_ptr_array_add (sniffed_mime, NULL); - self->priv->sniffed_mime = (gchar **) g_ptr_array_free (sniffed_mime, FALSE); + mimes = (gchar **) g_ptr_array_free (sniffed_mime, FALSE); g_array_free (results, TRUE); - g_simple_async_result_complete_in_idle (self->priv->async_result); + g_task_return_pointer (self->priv->task, mimes, (GDestroyNotify)g_strfreev); } /* adapted from nautilus/libnautilus-private/nautilus-directory-async.c */ static void deep_count_one (DeepCountState *state, - GFileInfo *info) + GFileInfo *info) { GFile *subdir; const char *content_type; @@ -242,11 +249,13 @@ deep_count_one (DeepCountState *state, subdir = g_file_get_child (state->file, g_file_info_get_name (info)); state->deep_count_subdirectories = g_list_append (state->deep_count_subdirectories, subdir); - } + } else { content_type = g_file_info_get_content_type (info); - add_content_type_to_cache (state, content_type); + + if (content_type) + add_content_type_to_cache (state, content_type); } } @@ -297,8 +306,8 @@ deep_count_next_dir (DeepCountState *state) static void deep_count_more_files_callback (GObject *source_object, - GAsyncResult *res, - gpointer user_data) + GAsyncResult *res, + gpointer user_data) { DeepCountState *state; GList *files, *l; @@ -311,10 +320,10 @@ deep_count_more_files_callback (GObject *source_object, deep_count_finish (state); return; } - + files = g_file_enumerator_next_files_finish (state->enumerator, res, NULL); - + for (l = files; l != NULL; l = l->next) { info = l->data; @@ -345,8 +354,8 @@ deep_count_more_files_callback (GObject *source_object, static void deep_count_callback (GObject *source_object, - GAsyncResult *res, - gpointer user_data) + GAsyncResult *res, + gpointer user_data) { DeepCountState *state; GFileEnumerator *enumerator; @@ -361,7 +370,7 @@ deep_count_callback (GObject *source_object, enumerator = g_file_enumerate_children_finish (G_FILE (source_object), res, NULL); - + if (enumerator == NULL) { deep_count_next_dir (state); @@ -418,21 +427,18 @@ query_info_async_ready_cb (GObject *source, if (error != NULL) { - g_simple_async_result_take_error (self->priv->async_result, - error); - g_simple_async_result_complete_in_idle (self->priv->async_result); + g_task_return_error (self->priv->task, error); return; } if (g_file_info_get_file_type (info) != G_FILE_TYPE_DIRECTORY) { - g_simple_async_result_set_error (self->priv->async_result, - G_IO_ERROR, - G_IO_ERROR_NOT_DIRECTORY, - "Not a directory"); - g_simple_async_result_complete_in_idle (self->priv->async_result); - g_object_unref(info); + g_task_return_new_error (self->priv->task, + G_IO_ERROR, + G_IO_ERROR_NOT_DIRECTORY, + "Not a directory"); + return; } @@ -477,27 +483,13 @@ cinnamon_mime_sniffer_dispose (GObject *object) g_clear_object (&self->priv->file); g_clear_object (&self->priv->cancellable); - g_clear_object (&self->priv->async_result); + g_clear_object (&self->priv->task); - if (self->priv->watchdog_id != 0) - { - g_source_remove (self->priv->watchdog_id); - self->priv->watchdog_id = 0; - } + g_clear_handle_id (&self->priv->watchdog_id, g_source_remove); G_OBJECT_CLASS (cinnamon_mime_sniffer_parent_class)->dispose (object); } -static void -cinnamon_mime_sniffer_finalize (GObject *object) -{ - CinnamonMimeSniffer *self = CINNAMON_MIME_SNIFFER (object); - - g_strfreev (self->priv->sniffed_mime); - - G_OBJECT_CLASS (cinnamon_mime_sniffer_parent_class)->finalize (object); -} - static void cinnamon_mime_sniffer_get_property (GObject *object, guint prop_id, @@ -541,7 +533,6 @@ cinnamon_mime_sniffer_class_init (CinnamonMimeSnifferClass *klass) oclass = G_OBJECT_CLASS (klass); oclass->dispose = cinnamon_mime_sniffer_dispose; - oclass->finalize = cinnamon_mime_sniffer_finalize; oclass->get_property = cinnamon_mime_sniffer_get_property; oclass->set_property = cinnamon_mime_sniffer_set_property; @@ -550,19 +541,15 @@ cinnamon_mime_sniffer_class_init (CinnamonMimeSnifferClass *klass) "File", "The loaded file", G_TYPE_FILE, - G_PARAM_READWRITE); + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); - g_type_class_add_private (klass, sizeof (CinnamonMimeSnifferPrivate)); g_object_class_install_properties (oclass, NUM_PROPERTIES, properties); } static void cinnamon_mime_sniffer_init (CinnamonMimeSniffer *self) { - self->priv = - G_TYPE_INSTANCE_GET_PRIVATE (self, - CINNAMON_TYPE_MIME_SNIFFER, - CinnamonMimeSnifferPrivate); + self->priv = cinnamon_mime_sniffer_get_instance_private (self); init_mimetypes (); } @@ -580,18 +567,16 @@ cinnamon_mime_sniffer_sniff_async (CinnamonMimeSniffer *self, gpointer user_data) { g_assert (self->priv->watchdog_id == 0); - g_assert (self->priv->async_result == NULL); - - self->priv->async_result = - g_simple_async_result_new (G_OBJECT (self), - callback, user_data, - cinnamon_mime_sniffer_sniff_finish); + g_assert (self->priv->task == NULL); self->priv->cancellable = g_cancellable_new (); + self->priv->task = g_task_new (self, self->priv->cancellable, + callback, user_data); self->priv->watchdog_id = g_timeout_add (WATCHDOG_TIMEOUT, watchdog_timeout_reached_cb, self); + g_source_set_name_by_id (self->priv->watchdog_id, "[gnome-shell] watchdog_timeout_reached_cb"); start_loading_file (self); } @@ -601,8 +586,5 @@ cinnamon_mime_sniffer_sniff_finish (CinnamonMimeSniffer *self, GAsyncResult *res, GError **error) { - if (g_simple_async_result_propagate_error (self->priv->async_result, error)) - return NULL; - - return g_strdupv (self->priv->sniffed_mime); + return g_task_propagate_pointer (self->priv->task, error); } diff --git a/src/hotplug-sniffer/cinnamon-mime-sniffer.h b/src/hotplug-sniffer/cinnamon-mime-sniffer.h index d6e47cd074..dcfcf934a1 100644 --- a/src/hotplug-sniffer/cinnamon-mime-sniffer.h +++ b/src/hotplug-sniffer/cinnamon-mime-sniffer.h @@ -13,9 +13,7 @@ * General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street - Suite 500, Boston, MA - * 02110-1335, USA. + * along with this program; if not, see . * * Author: Cosimo Cecchi * @@ -29,30 +27,9 @@ G_BEGIN_DECLS -#define CINNAMON_TYPE_MIME_SNIFFER (cinnamon_mime_sniffer_get_type ()) -#define CINNAMON_MIME_SNIFFER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CINNAMON_TYPE_MIME_SNIFFER, CinnamonMimeSniffer)) -#define CINNAMON_IS_MIME_SNIFFER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CINNAMON_TYPE_MIME_SNIFFER)) -#define CINNAMON_MIME_SNIFFER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CINNAMON_TYPE_MIME_SNIFFER, CinnamonMimeSnifferClass)) -#define CINNAMON_IS_MIME_SNIFFER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CINNAMON_TYPE_MIME_SNIFFER)) -#define CINNAMON_MIME_SNIFFER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CINNAMON_TYPE_MIME_SNIFFER, CinnamonMimeSnifferClass)) - -typedef struct _CinnamonMimeSniffer CinnamonMimeSniffer; -typedef struct _CinnamonMimeSnifferPrivate CinnamonMimeSnifferPrivate; -typedef struct _CinnamonMimeSnifferClass CinnamonMimeSnifferClass; - -struct _CinnamonMimeSniffer -{ - GObject parent_instance; - - CinnamonMimeSnifferPrivate *priv; -}; - -struct _CinnamonMimeSnifferClass -{ - GObjectClass parent_class; -}; - -GType cinnamon_mime_sniffer_get_type (void) G_GNUC_CONST; +#define CINNAMON_TYPE_MIME_SNIFFER (cinnamon_mime_sniffer_get_type ()) +G_DECLARE_FINAL_TYPE (CinnamonMimeSniffer, cinnamon_mime_sniffer, + CINNAMON, MIME_SNIFFER, GObject) CinnamonMimeSniffer *cinnamon_mime_sniffer_new (GFile *file); diff --git a/src/hotplug-sniffer/hotplug-sniffer.c b/src/hotplug-sniffer/hotplug-sniffer.c index 343d2e9ca4..6682db84fc 100644 --- a/src/hotplug-sniffer/hotplug-sniffer.c +++ b/src/hotplug-sniffer/hotplug-sniffer.c @@ -13,9 +13,7 @@ * General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street - Suite 500, Boston, MA - * 02110-1335, USA. + * along with this program; if not, see . * * Authors: David Zeuthen * Cosimo Cecchi @@ -62,11 +60,7 @@ ensure_autoquit_off (void) if (g_getenv ("HOTPLUG_SNIFFER_PERSIST") != NULL) return; - if (autoquit_id != 0) - { - g_source_remove (autoquit_id); - autoquit_id = 0; - } + g_clear_handle_id (&autoquit_id, g_source_remove); } static void @@ -78,6 +72,7 @@ ensure_autoquit_on (void) autoquit_id = g_timeout_add_seconds (AUTOQUIT_TIMEOUT, autoquit_timeout_cb, NULL); + g_source_set_name_by_id (autoquit_id, "[cinnamon] autoquit_timeout_cb"); } typedef struct { @@ -91,7 +86,7 @@ invocation_data_new (GVariant *params, { InvocationData *ret; - ret = g_slice_new0 (InvocationData); + ret = g_new0 (InvocationData, 1); ret->parameters = g_variant_ref (params); ret->invocation = g_object_ref (invocation); @@ -104,7 +99,7 @@ invocation_data_free (InvocationData *data) g_variant_unref (data->parameters); g_clear_object (&data->invocation); - g_slice_free (InvocationData, data); + g_free (data); } static void @@ -128,9 +123,9 @@ sniff_async_ready_cb (GObject *source, g_dbus_method_invocation_return_value (data->invocation, g_variant_new ("(^as)", types)); + g_strfreev (types); out: - g_strfreev (types); invocation_data_free (data); ensure_autoquit_on (); } @@ -267,7 +262,7 @@ main (int argc, /* ---------------------------------------------------------------------------------------------------- */ -static void +static void __attribute__((format(printf, 1, 0))) print_debug (const gchar *format, ...) { g_autofree char *s = NULL;