diff --git a/gtk/Makefile.am b/gtk/Makefile.am index 6d7a03865d..5b27ff2aec 100644 --- a/gtk/Makefile.am +++ b/gtk/Makefile.am @@ -697,19 +697,23 @@ gtk_use_x11_c_sources = \ gtkplug-x11.c \ gtksocket-x11.c \ gtkxembed.c \ - gtktrayicon-x11.c + gtktrayicon-x11.c \ + gtkmountoperation-x11.c gtk_use_win32_c_sources = \ gtkplug-win32.c \ gtksocket-win32.c \ gtkwin32embed.c \ - gtkwin32embedwidget.c + gtkwin32embedwidget.c \ + gtkmountoperation-stub.c gtk_use_quartz_c_sources = \ gtksearchenginequartz.c \ gtkplug-stub.c \ - gtksocket-stub.c + gtksocket-stub.c \ + gtkmountoperation-stub.c gtk_use_stub_c_sources = \ gtkplug-stub.c \ - gtksocket-stub.c + gtksocket-stub.c \ + gtkmountoperation-stub.c gtk_all_c_sources += $(gtk_use_x11_c_sources) $(gtk_use_win32_c_sources) $(gtk_use_quartz_c_sources) $(gtk_use_stub_c_sources) if USE_X11 gtk_private_h_sources += gtkxembed.h gtktrayicon.h xembed.h diff --git a/gtk/gtkmountoperation-stub.c b/gtk/gtkmountoperation-stub.c new file mode 100644 index 0000000000..c7b8d1a9ed --- /dev/null +++ b/gtk/gtkmountoperation-stub.c @@ -0,0 +1,68 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* GTK - The GIMP Toolkit + * Copyright (C) David Zeuthen + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +/* + * Modified by the GTK+ Team and others 1997-2000. See the AUTHORS + * file for a list of people on the GTK+ Team. See the ChangeLog + * files for a list of changes. These files are distributed with + * GTK+ at ftp://ftp.gtk.org/pub/gtk/. + */ + +#include "config.h" + +#include +#include "gtkintl.h" + +#include "gtkmountoperationprivate.h" + +GtkMountOperationLookupContext * +_gtk_mount_operation_lookup_context_get (GdkDisplay *display) +{ + return NULL; +} + +void +_gtk_mount_operation_lookup_context_free (GtkMountOperationLookupContext *context) +{ +} + +gboolean +_gtk_mount_operation_lookup_info (GtkMountOperationLookupContext *context, + GPid pid, + gint size_pixels, + gchar **out_name, + gchar **out_command_line, + GdkPixbuf **out_pixbuf) +{ + return FALSE; +} + +gboolean +_gtk_mount_operation_kill_process (GPid pid, + GError **error) +{ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + _("Cannot kill process with pid %d. Operation is not implemented."), + pid); + return FALSE; +} + diff --git a/gtk/gtkmountoperation-x11.c b/gtk/gtkmountoperation-x11.c new file mode 100644 index 0000000000..655854ef92 --- /dev/null +++ b/gtk/gtkmountoperation-x11.c @@ -0,0 +1,967 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* GTK - The GIMP Toolkit + * Copyright (C) David Zeuthen + * Copyright (C) 2001 Havoc Pennington + * Copyright (C) 2005-2007 Vincent Untz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +/* + * Modified by the GTK+ Team and others 1997-2000. See the AUTHORS + * file for a list of people on the GTK+ Team. See the ChangeLog + * files for a list of changes. These files are distributed with + * GTK+ at ftp://ftp.gtk.org/pub/gtk/. + */ + +#include "config.h" + +#include +#include +#include +#include "x11/gdkx.h" +#include +#include +#include "gtkintl.h" + +/* for the kill(2) system call and errno - POSIX.1-2001 and later */ +#include +#include +#include + +#include "gtkmountoperationprivate.h" + + +/* ---------------------------------------------------------------------------------------------------- */ +/* these functions are based on code from libwnck (LGPLv2) */ + +static gboolean get_window_list (Display *xdisplay, + Window xwindow, + Atom atom, + Window **windows, + int *len); + +static char* get_utf8_property (Display *xdisplay, + Window xwindow, + Atom atom); + +static gboolean get_cardinal (Display *xdisplay, + Window xwindow, + Atom atom, + int *val); + +static gboolean read_rgb_icon (Display *xdisplay, + Window xwindow, + int ideal_width, + int ideal_height, + int *width, + int *height, + guchar **pixdata); + + +static gboolean +get_cardinal (Display *xdisplay, + Window xwindow, + Atom atom, + int *val) +{ + Atom type; + int format; + gulong nitems; + gulong bytes_after; + gulong *num; + int err, result; + + *val = 0; + + gdk_error_trap_push (); + type = None; + result = XGetWindowProperty (xdisplay, + xwindow, + atom, + 0, G_MAXLONG, + False, XA_CARDINAL, &type, &format, &nitems, + &bytes_after, (void*)&num); + XSync (xdisplay, False); + err = gdk_error_trap_pop (); + + if (err != Success || + result != Success) + return FALSE; + + if (type != XA_CARDINAL) + { + XFree (num); + return FALSE; + } + + *val = *num; + + XFree (num); + + return TRUE; +} + +static char* +get_utf8_property (Display *xdisplay, + Window xwindow, + Atom atom) +{ + Atom type; + int format; + gulong nitems; + gulong bytes_after; + gchar *val; + int err, result; + char *retval; + Atom utf8_string; + + utf8_string = gdk_x11_get_xatom_by_name ("UTF8_STRING"); + + gdk_error_trap_push (); + type = None; + val = NULL; + result = XGetWindowProperty (xdisplay, + xwindow, + atom, + 0, G_MAXLONG, + False, utf8_string, + &type, &format, &nitems, + &bytes_after, (guchar **)&val); + XSync (xdisplay, False); + err = gdk_error_trap_pop (); + + if (err != Success || + result != Success) + return NULL; + + if (type != utf8_string || + format != 8 || + nitems == 0) + { + if (val) + XFree (val); + return NULL; + } + + if (!g_utf8_validate (val, nitems, NULL)) + { + g_warning ("Property %s contained invalid UTF-8\n", + gdk_x11_get_xatom_name (atom)); + XFree (val); + return NULL; + } + + retval = g_strndup (val, nitems); + + XFree (val); + + return retval; +} + +static gboolean +find_largest_sizes (gulong *data, + gulong nitems, + int *width, + int *height) +{ + *width = 0; + *height = 0; + + while (nitems > 0) + { + int w, h; + gboolean replace; + + replace = FALSE; + + if (nitems < 3) + return FALSE; /* no space for w, h */ + + w = data[0]; + h = data[1]; + + if (nitems < ((w * h) + 2)) + return FALSE; /* not enough data */ + + *width = MAX (w, *width); + *height = MAX (h, *height); + + data += (w * h) + 2; + nitems -= (w * h) + 2; + } + + return TRUE; +} + +static gboolean +find_best_size (gulong *data, + gulong nitems, + int ideal_width, + int ideal_height, + int *width, + int *height, + gulong **start) +{ + int best_w; + int best_h; + gulong *best_start; + int max_width, max_height; + + *width = 0; + *height = 0; + *start = NULL; + + if (!find_largest_sizes (data, nitems, &max_width, &max_height)) + return FALSE; + + if (ideal_width < 0) + ideal_width = max_width; + if (ideal_height < 0) + ideal_height = max_height; + + best_w = 0; + best_h = 0; + best_start = NULL; + + while (nitems > 0) + { + int w, h; + gboolean replace; + + replace = FALSE; + + if (nitems < 3) + return FALSE; /* no space for w, h */ + + w = data[0]; + h = data[1]; + + if (nitems < ((w * h) + 2)) + break; /* not enough data */ + + if (best_start == NULL) + { + replace = TRUE; + } + else + { + /* work with averages */ + const int ideal_size = (ideal_width + ideal_height) / 2; + int best_size = (best_w + best_h) / 2; + int this_size = (w + h) / 2; + + /* larger than desired is always better than smaller */ + if (best_size < ideal_size && + this_size >= ideal_size) + replace = TRUE; + /* if we have too small, pick anything bigger */ + else if (best_size < ideal_size && + this_size > best_size) + replace = TRUE; + /* if we have too large, pick anything smaller + * but still >= the ideal + */ + else if (best_size > ideal_size && + this_size >= ideal_size && + this_size < best_size) + replace = TRUE; + } + + if (replace) + { + best_start = data + 2; + best_w = w; + best_h = h; + } + + data += (w * h) + 2; + nitems -= (w * h) + 2; + } + + if (best_start) + { + *start = best_start; + *width = best_w; + *height = best_h; + return TRUE; + } + else + return FALSE; +} + +static void +argbdata_to_pixdata (gulong *argb_data, + int len, + guchar **pixdata) +{ + guchar *p; + int i; + + *pixdata = g_new (guchar, len * 4); + p = *pixdata; + + /* One could speed this up a lot. */ + i = 0; + while (i < len) + { + guint argb; + guint rgba; + + argb = argb_data[i]; + rgba = (argb << 8) | (argb >> 24); + + *p = rgba >> 24; + ++p; + *p = (rgba >> 16) & 0xff; + ++p; + *p = (rgba >> 8) & 0xff; + ++p; + *p = rgba & 0xff; + ++p; + + ++i; + } +} + +static gboolean +read_rgb_icon (Display *xdisplay, + Window xwindow, + int ideal_width, + int ideal_height, + int *width, + int *height, + guchar **pixdata) +{ + Atom type; + int format; + gulong nitems; + gulong bytes_after; + int result, err; + gulong *data; + gulong *best; + int w, h; + + gdk_error_trap_push (); + type = None; + data = NULL; + result = XGetWindowProperty (xdisplay, + xwindow, + gdk_x11_get_xatom_by_name ("_NET_WM_ICON"), + 0, G_MAXLONG, + False, XA_CARDINAL, &type, &format, &nitems, + &bytes_after, (void*)&data); + + XSync (xdisplay, False); + err = gdk_error_trap_pop (); + + if (err != Success || + result != Success) + return FALSE; + + if (type != XA_CARDINAL) + { + XFree (data); + return FALSE; + } + + if (!find_best_size (data, nitems, + ideal_width, ideal_height, + &w, &h, &best)) + { + XFree (data); + return FALSE; + } + + *width = w; + *height = h; + + argbdata_to_pixdata (best, w * h, pixdata); + + XFree (data); + + return TRUE; +} + +static void +free_pixels (guchar *pixels, gpointer data) +{ + g_free (pixels); +} + +static GdkPixbuf* +scaled_from_pixdata (guchar *pixdata, + int w, + int h, + int new_w, + int new_h) +{ + GdkPixbuf *src; + GdkPixbuf *dest; + + src = gdk_pixbuf_new_from_data (pixdata, + GDK_COLORSPACE_RGB, + TRUE, + 8, + w, h, w * 4, + free_pixels, + NULL); + + if (src == NULL) + return NULL; + + if (w != h) + { + GdkPixbuf *tmp; + int size; + + size = MAX (w, h); + + tmp = gdk_pixbuf_new (GDK_COLORSPACE_RGB, TRUE, 8, size, size); + + if (tmp != NULL) + { + gdk_pixbuf_fill (tmp, 0); + gdk_pixbuf_copy_area (src, 0, 0, w, h, + tmp, + (size - w) / 2, (size - h) / 2); + + g_object_unref (src); + src = tmp; + } + } + + if (w != new_w || h != new_h) + { + dest = gdk_pixbuf_scale_simple (src, new_w, new_h, GDK_INTERP_BILINEAR); + + g_object_unref (G_OBJECT (src)); + } + else + { + dest = src; + } + + return dest; +} + +static gboolean +get_window_list (Display *xdisplay, + Window xwindow, + Atom atom, + Window **windows, + int *len) +{ + Atom type; + int format; + gulong nitems; + gulong bytes_after; + Window *data; + int err, result; + + *windows = NULL; + *len = 0; + + gdk_error_trap_push (); + type = None; + result = XGetWindowProperty (xdisplay, + xwindow, + atom, + 0, G_MAXLONG, + False, XA_WINDOW, &type, &format, &nitems, + &bytes_after, (void*)&data); + XSync (xdisplay, False); + err = gdk_error_trap_pop (); + + if (err != Success || + result != Success) + return FALSE; + + if (type != XA_WINDOW) + { + XFree (data); + return FALSE; + } + + *windows = g_new (Window, nitems); + memcpy (*windows, data, sizeof (Window) * nitems); + *len = nitems; + + XFree (data); + + return TRUE; +} + + +/* ---------------------------------------------------------------------------------------------------- */ + +struct _GtkMountOperationLookupContext +{ + /* Hash from pid (gint) -> XID (gint) + * + * Note that XIDs are at most 27 bits - however, also note that sizeof(XID) == 8 on + * x86_64 - that's just xlib brokenness. So it's safe to stuff the XID into a pointer. + */ + GHashTable *pid_to_window; + GdkDisplay *display; +}; + +GtkMountOperationLookupContext * +_gtk_mount_operation_lookup_context_get (GdkDisplay *display) +{ + GtkMountOperationLookupContext *context; + Window *mapping; + gint mapping_length; + gint n; + + context = g_new0 (GtkMountOperationLookupContext, 1); + + context->pid_to_window = g_hash_table_new (g_direct_hash, g_direct_equal); + context->display = display; + + mapping = NULL; + mapping_length = 0; + get_window_list (GDK_DISPLAY_XDISPLAY (context->display), + GDK_ROOT_WINDOW(), + gdk_x11_get_xatom_by_name_for_display (context->display, + "_NET_CLIENT_LIST"), + &mapping, + &mapping_length); + for (n = 0; n < mapping_length; n++) + { + gint pid; + + if (!get_cardinal (GDK_DISPLAY_XDISPLAY (context->display), + mapping[n], + gdk_x11_get_xatom_by_name_for_display (context->display, + "_NET_WM_PID"), + &pid)) + continue; + + g_hash_table_insert (context->pid_to_window, + GINT_TO_POINTER (pid), + GINT_TO_POINTER ((gint) mapping[n])); + } + g_free (mapping); + + return context; +} + +void +_gtk_mount_operation_lookup_context_free (GtkMountOperationLookupContext *context) +{ + g_hash_table_unref (context->pid_to_window); + g_free (context); +} + +/* ---------------------------------------------------------------------------------------------------- */ + +#ifdef __linux__ + +static GPid +pid_get_parent (GPid pid) +{ + GPid ppid; + gchar **tokens; + gchar *stat_filename; + gchar *stat_contents; + gsize stat_len; + + ppid = 0; + tokens = NULL; + stat_contents = NULL; + stat_filename = NULL; + + /* fail if trying to get the parent of the init process (no such thing) */ + if (pid == 1) + goto out; + + stat_filename = g_strdup_printf ("/proc/%d/status", pid); + if (g_file_get_contents (stat_filename, + &stat_contents, + &stat_len, + NULL)) + { + guint n; + + tokens = g_strsplit (stat_contents, "\n", 0); + + for (n = 0; tokens[n] != NULL; n++) + { + if (g_str_has_prefix (tokens[n], "PPid:")) + { + gchar *endp; + + endp = NULL; + ppid = strtoll (tokens[n] + sizeof "PPid:" - 1, &endp, 10); + if (endp == NULL || *endp != '\0') + { + g_warning ("Error parsing contents of `%s'. Parent pid is malformed.", + stat_filename); + ppid = 0; + goto out; + } + + break; + } + } + } + + out: + g_strfreev (tokens); + g_free (stat_contents); + g_free (stat_filename); + + return ppid; +} + +static gchar * +pid_get_env (GPid pid, + const gchar *key) +{ + gchar *ret; + gchar *env_filename; + gchar *env; + gsize env_len; + gsize key_len; + gchar *end; + + ret = NULL; + + key_len = strlen (key); + + env_filename = g_strdup_printf ("/proc/%d/environ", pid); + if (g_file_get_contents (env_filename, + &env, + &env_len, + NULL)) + { + guint n; + + /* /proc//environ in Linux is split at '\0' points, g_strsplit() can't handle that... */ + n = 0; + while (TRUE) + { + if (env[n] == '\0' || n >= env_len) + break; + + if (g_str_has_prefix (env + n, key) && (*(env + n + key_len) == '=')) + { + ret = g_strdup (env + n + key_len + 1); + + /* skip invalid UTF-8 */ + if (!g_utf8_validate (ret, -1, (const gchar *) &end)) + *end = '\0'; + break; + } + + for (; env[n] != '\0' && n < env_len; n++) + ; + n++; + } + g_free (env); + } + g_free (env_filename); + + return ret; +} + +static gchar * +pid_get_command_line (GPid pid) +{ + gchar *cmdline_filename; + gchar *cmdline_contents; + gsize cmdline_len; + guint n; + gchar *end; + + cmdline_contents = NULL; + + cmdline_filename = g_strdup_printf ("/proc/%d/cmdline", pid); + if (!g_file_get_contents (cmdline_filename, + &cmdline_contents, + &cmdline_len, + NULL)) + goto out; + + /* /proc//cmdline separates args by NUL-bytes - replace with spaces */ + for (n = 0; n < cmdline_len - 1; n++) + { + if (cmdline_contents[n] == '\0') + cmdline_contents[n] = ' '; + } + + /* skip invalid UTF-8 */ + if (!g_utf8_validate (cmdline_contents, -1, (const gchar *) &end)) + *end = '\0'; + + out: + g_free (cmdline_filename); + + return cmdline_contents; +} + +/* ---------------------------------------------------------------------------------------------------- */ +#else + +/* TODO: please implement for your OS - must return valid UTF-8 */ + +static GPid +pid_get_parent (GPid pid) +{ + return 0; +} + +static gchar * +pid_get_env (GPid pid, + const gchar *key) +{ + return NULL; +} + +static gchar * +pid_get_command_line (GPid pid) +{ + return NULL; +} + +#endif + +/* ---------------------------------------------------------------------------------------------------- */ + +static gchar * +get_name_for_window_with_pid (GtkMountOperationLookupContext *context, + GPid pid) +{ + Window window; + Window windowid_window; + gchar *ret; + + ret = NULL; + + window = GPOINTER_TO_INT (g_hash_table_lookup (context->pid_to_window, GINT_TO_POINTER (pid))); + if (window == None) + { + gchar *windowid_value; + + /* check for $WINDOWID (set by terminals) and see if we can get the title that way */ + windowid_value = pid_get_env (pid, "WINDOWID"); + if (windowid_value != NULL) + { + gchar *endp; + + endp = NULL; + windowid_window = (Window) g_ascii_strtoll (windowid_value, &endp, 10); + if (endp != NULL || *endp == '\0') + { + window = windowid_window; + } + g_free (windowid_value); + } + + /* otherwise, check for parents */ + if (window == None) + { + do + { + pid = pid_get_parent (pid); + if (pid == 0) + break; + + window = GPOINTER_TO_INT (g_hash_table_lookup (context->pid_to_window, GINT_TO_POINTER (pid))); + if (window != None) + break; + } + while (TRUE); + } + } + + if (window != None) + { + ret = get_utf8_property (GDK_DISPLAY_XDISPLAY (context->display), + window, + gdk_x11_get_xatom_by_name_for_display (context->display, + "_NET_WM_NAME")); + if (ret == NULL) + ret = get_utf8_property (GDK_DISPLAY_XDISPLAY (context->display), + window, gdk_x11_get_xatom_by_name_for_display (context->display, + "_NET_WM_ICON_NAME")); + } + + return ret; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static GdkPixbuf * +get_pixbuf_for_window_with_pid (GtkMountOperationLookupContext *context, + GPid pid, + gint size_pixels) +{ + Window window; + GdkPixbuf *ret; + + ret = NULL; + + window = GPOINTER_TO_INT (g_hash_table_lookup (context->pid_to_window, GINT_TO_POINTER (pid))); + if (window == None) + { + /* otherwise, check for parents */ + do + { + pid = pid_get_parent (pid); + if (pid == 0) + break; + + window = GPOINTER_TO_INT (g_hash_table_lookup (context->pid_to_window, GINT_TO_POINTER (pid))); + if (window != None) + break; + } + while (TRUE); + } + + if (window != None) + { + gint width; + gint height; + guchar *pixdata; + + if (read_rgb_icon (GDK_DISPLAY_XDISPLAY (context->display), + window, + size_pixels, size_pixels, + &width, &height, + &pixdata)) + { + /* steals pixdata */ + ret = scaled_from_pixdata (pixdata, + width, height, + size_pixels, size_pixels); + } + } + + return ret; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static const gchar *well_known_commands[] = +{ + "less", N_("Terminal Pager"), + "top", N_("Top Command"), + "bash", N_("Bourne Again Shell"), + "sh", N_("Bourne Shell"), + "zsh", N_("Z Shell"), + NULL, +}; + +gboolean +_gtk_mount_operation_lookup_info (GtkMountOperationLookupContext *context, + GPid pid, + gint size_pixels, + gchar **out_name, + gchar **out_command_line, + GdkPixbuf **out_pixbuf) +{ + g_return_val_if_fail (out_name != NULL && *out_name == NULL, FALSE); + g_return_val_if_fail (out_command_line != NULL && *out_command_line == NULL, FALSE); + g_return_val_if_fail (out_pixbuf != NULL && *out_pixbuf == NULL, FALSE); + + /* We perform two different lookups for name and icon size.. this is + * because we want the name from the window with WINDOWID and this + * normally does not give you an icon + * + * (the canonical example is a tab in gnome-terminal - the shell/command running + * in the shell will have WINDOWID set - but this window won't have an icon - so + * we want to continue up until the gnome-terminal window so we can get that icon) + */ + + *out_command_line = pid_get_command_line (pid); + + *out_name = get_name_for_window_with_pid (context, pid); + + *out_pixbuf = get_pixbuf_for_window_with_pid (context, pid, size_pixels); + + /* if we didn't manage to find the name via X, fall back to the basename + * of the first element of the command line and, for maximum geek-comfort, + * map a few well-known commands to proper translated names + */ + if (*out_name == NULL && *out_command_line != NULL && + strlen (*out_command_line) > 0 && (*out_command_line)[0] != ' ') + { + guint n; + gchar *s; + gchar *p; + + /* find the first character after the first argument */ + s = strchr (*out_command_line, ' '); + if (s == NULL) + s = *out_command_line + strlen (*out_command_line); + + for (p = s; p > *out_command_line; p--) + { + if (*p == '/') + { + p++; + break; + } + } + + *out_name = g_strndup (p, s - p); + + for (n = 0; well_known_commands[n] != NULL; n += 2) + { + /* sometimes the command is prefixed with a -, e.g. '-bash' instead + * of 'bash' - handle that as well + */ + if ((strcmp (well_known_commands[n], *out_name) == 0) || + ((*out_name)[0] == '-' && (strcmp (well_known_commands[n], (*out_name) + 1) == 0))) + { + g_free (*out_name); + *out_name = g_strdup (_(well_known_commands[n+1])); + break; + } + } + } + + return TRUE; +} + +gboolean +_gtk_mount_operation_kill_process (GPid pid, + GError **error) +{ + gboolean ret; + + ret = TRUE; + + if (kill ((pid_t) pid, SIGTERM) != 0) + { + /* TODO: On EPERM, we could use a setuid helper using polkit (very easy to implement + * via pkexec(1)) to allow the user to e.g. authenticate to gain the authorization + * to kill the process. But that's not how things currently work. + */ + + ret = FALSE; + g_set_error (error, + G_IO_ERROR, + g_io_error_from_errno (errno), + _("Cannot end process with pid %d: %s"), + pid, + strerror (errno)); + } + + return ret; +} diff --git a/gtk/gtkmountoperation.c b/gtk/gtkmountoperation.c index ace58ed90a..318e4aeb5b 100644 --- a/gtk/gtkmountoperation.c +++ b/gtk/gtkmountoperation.c @@ -29,6 +29,7 @@ #include +#include "gtkmountoperationprivate.h" #include "gtkalignment.h" #include "gtkbox.h" #include "gtkentry.h" @@ -44,6 +45,14 @@ #include "gtkstock.h" #include "gtktable.h" #include "gtkwindow.h" +#include "gtktreeview.h" +#include "gtktreeselection.h" +#include "gtkcellrenderertext.h" +#include "gtkcellrendererpixbuf.h" +#include "gtkscrolledwindow.h" +#include "gtkicontheme.h" +#include "gtkimagemenuitem.h" +#include "gtkmain.h" #include "gtkalias.h" /** @@ -58,14 +67,16 @@ * applications. */ -/** +/** * GtkMountOperation: * - * #GtkMountOperation is an implementation of #GMountOperation that + * #GtkMountOperation is an implementation of #GMountOperation that * can be used with GIO functions for mounting volumes such as - * g_file_mount_enclosing_volume() or g_file_mount_mountable(). + * g_file_mount_enclosing_volume(), g_file_mount_mountable(), + * g_volume_mount(), g_mount_unmount() and others. * - * When necessary, #GtkMountOperation shows dialogs to ask for passwords. + * When necessary, #GtkMountOperation shows dialogs to ask for + * passwords, questions or show processes blocking unmount. */ static void gtk_mount_operation_finalize (GObject *object); @@ -88,6 +99,11 @@ static void gtk_mount_operation_ask_question (GMountOperation *op, const char *message, const char *choices[]); +static void gtk_mount_operation_show_processes (GMountOperation *op, + const char *message, + GArray *processes, + const char *choices[]); + static void gtk_mount_operation_aborted (GMountOperation *op); G_DEFINE_TYPE (GtkMountOperation, gtk_mount_operation, G_TYPE_MOUNT_OPERATION); @@ -115,6 +131,10 @@ struct _GtkMountOperationPrivate { GAskPasswordFlags ask_flags; GPasswordSave password_save; gboolean anonymous; + + /* for the show-processes dialog */ + GtkWidget *process_tree_view; + GtkListStore *process_list_store; }; static void @@ -131,6 +151,7 @@ gtk_mount_operation_class_init (GtkMountOperationClass *klass) mount_op_class->ask_password = gtk_mount_operation_ask_password; mount_op_class->ask_question = gtk_mount_operation_ask_question; + mount_op_class->show_processes = gtk_mount_operation_show_processes; mount_op_class->aborted = gtk_mount_operation_aborted; g_object_class_install_property (object_class, @@ -747,6 +768,578 @@ gtk_mount_operation_ask_question (GMountOperation *op, g_object_ref (op); } +static void +show_processes_button_clicked (GtkDialog *dialog, + gint button_number, + GMountOperation *op) +{ + GtkMountOperationPrivate *priv; + GtkMountOperation *operation; + + operation = GTK_MOUNT_OPERATION (op); + priv = operation->priv; + + if (button_number >= 0) + { + g_mount_operation_set_choice (op, button_number); + g_mount_operation_reply (op, G_MOUNT_OPERATION_HANDLED); + } + else + g_mount_operation_reply (op, G_MOUNT_OPERATION_ABORTED); + + priv->dialog = NULL; + g_object_notify (G_OBJECT (operation), "is-showing"); + gtk_widget_destroy (GTK_WIDGET (dialog)); + g_object_unref (op); +} + +static gint +pid_equal (gconstpointer a, + gconstpointer b) +{ + GPid pa, pb; + + pa = *((GPid *) a); + pb = *((GPid *) b); + + return pb - pa; +} + +static void +diff_sorted_arrays (GArray *array1, + GArray *array2, + GCompareFunc compare, + GArray *added_indices, + GArray *removed_indices) +{ + gint order; + guint n1, n2; + guint elem_size; + + n1 = n2 = 0; + + elem_size = g_array_get_element_size (array1); + g_assert (elem_size == g_array_get_element_size (array2)); + + while (n1 < array1->len && n2 < array2->len) + { + order = (*compare) (((gconstpointer) array1->data) + n1 * elem_size, + ((gconstpointer) array2->data) + n2 * elem_size); + if (order < 0) + { + g_array_append_val (removed_indices, n1); + n1++; + } + else if (order > 0) + { + g_array_append_val (added_indices, n2); + n2++; + } + else + { /* same item */ + n1++; + n2++; + } + } + + while (n1 < array1->len) + { + g_array_append_val (removed_indices, n1); + n1++; + } + while (n2 < array2->len) + { + g_array_append_val (added_indices, n2); + n2++; + } +} + + +static void +add_pid_to_process_list_store (GtkMountOperation *mount_operation, + GtkMountOperationLookupContext *lookup_context, + GtkListStore *list_store, + GPid pid) +{ + gchar *command_line; + gchar *name; + GdkPixbuf *pixbuf; + gchar *markup; + GtkTreeIter iter; + + name = NULL; + pixbuf = NULL; + command_line = NULL; + _gtk_mount_operation_lookup_info (lookup_context, + pid, + 24, + &name, + &command_line, + &pixbuf); + + if (name == NULL) + name = g_strdup_printf (_("Unknown Application (pid %d)"), pid); + + if (command_line == NULL) + command_line = g_strdup (""); + + if (pixbuf == NULL) + { + GtkIconTheme *theme; + theme = gtk_icon_theme_get_for_screen (gtk_widget_get_screen (GTK_WIDGET (mount_operation->priv->dialog))); + pixbuf = gtk_icon_theme_load_icon (theme, + "application-x-executable", + 24, + 0, + NULL); + } + + markup = g_strdup_printf ("%s\n" + "%s", + name, + command_line); + + gtk_list_store_append (list_store, &iter); + gtk_list_store_set (list_store, &iter, + 0, pixbuf, + 1, markup, + 2, pid, + -1); + + if (pixbuf != NULL) + g_object_unref (pixbuf); + g_free (markup); + g_free (name); + g_free (command_line); +} + +static void +remove_pid_from_process_list_store (GtkMountOperation *mount_operation, + GtkListStore *list_store, + GPid pid) +{ + GtkTreeIter iter; + GPid pid_of_item; + + if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (list_store), &iter)) + { + do + { + gtk_tree_model_get (GTK_TREE_MODEL (list_store), + &iter, + 2, &pid_of_item, + -1); + + if (pid_of_item == pid) + { + gtk_list_store_remove (list_store, &iter); + break; + } + } + while (gtk_tree_model_iter_next (GTK_TREE_MODEL (list_store), &iter)); + } +} + + +static void +update_process_list_store (GtkMountOperation *mount_operation, + GtkListStore *list_store, + GArray *processes) +{ + guint n; + GtkMountOperationLookupContext *lookup_context; + GArray *current_pids; + GArray *pid_indices_to_add; + GArray *pid_indices_to_remove; + GtkTreeIter iter; + GPid pid; + + /* Just removing all items and adding new ones will screw up the + * focus handling in the treeview - so compute the delta, and add/remove + * items as appropriate + */ + current_pids = g_array_new (FALSE, FALSE, sizeof (GPid)); + pid_indices_to_add = g_array_new (FALSE, FALSE, sizeof (gint)); + pid_indices_to_remove = g_array_new (FALSE, FALSE, sizeof (gint)); + + if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (list_store), &iter)) + { + do + { + gtk_tree_model_get (GTK_TREE_MODEL (list_store), + &iter, + 2, &pid, + -1); + + g_array_append_val (current_pids, pid); + } + while (gtk_tree_model_iter_next (GTK_TREE_MODEL (list_store), &iter)); + } + + g_array_sort (current_pids, pid_equal); + g_array_sort (processes, pid_equal); + + diff_sorted_arrays (current_pids, processes, pid_equal, pid_indices_to_add, pid_indices_to_remove); + + if (pid_indices_to_add->len > 0) + lookup_context = _gtk_mount_operation_lookup_context_get (gtk_widget_get_display (mount_operation->priv->process_tree_view)); + for (n = 0; n < pid_indices_to_add->len; n++) + { + pid = g_array_index (processes, GPid, n); + add_pid_to_process_list_store (mount_operation, lookup_context, list_store, pid); + } + + for (n = 0; n < pid_indices_to_remove->len; n++) + { + pid = g_array_index (current_pids, GPid, n); + remove_pid_from_process_list_store (mount_operation, list_store, pid); + } + if (pid_indices_to_add->len > 0) + _gtk_mount_operation_lookup_context_free (lookup_context); + + /* select the first item, if we went from a zero to a non-zero amount of processes */ + if (current_pids->len == 0 && pid_indices_to_add->len > 0) + { + if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (list_store), &iter)) + { + GtkTreeSelection *tree_selection; + tree_selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (mount_operation->priv->process_tree_view)); + gtk_tree_selection_select_iter (tree_selection, &iter); + } + } + + g_array_unref (current_pids); + g_array_unref (pid_indices_to_add); + g_array_unref (pid_indices_to_remove); +} + +static void +on_end_process_activated (GtkMenuItem *item, + gpointer user_data) +{ + GtkMountOperation *op = GTK_MOUNT_OPERATION (user_data); + GtkTreeSelection *selection; + GtkTreeIter iter; + GPid pid_to_kill; + GError *error; + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (op->priv->process_tree_view)); + + if (!gtk_tree_selection_get_selected (selection, + NULL, + &iter)) + goto out; + + gtk_tree_model_get (GTK_TREE_MODEL (op->priv->process_list_store), + &iter, + 2, &pid_to_kill, + -1); + + /* TODO: We might want to either + * + * - Be smart about things and send SIGKILL rather than SIGTERM if + * this is the second time the user requests killing a process + * + * - Or, easier (but worse user experience), offer both "End Process" + * and "Terminate Process" options + * + * But that's not how things work right now.... + */ + error = NULL; + if (!_gtk_mount_operation_kill_process (pid_to_kill, &error)) + { + GtkWidget *dialog; + gint response; + + /* Use GTK_DIALOG_DESTROY_WITH_PARENT here since the parent dialog can be + * indeed be destroyed via the GMountOperation::abort signal... for example, + * this is triggered if the user yanks the device while we are showing + * the dialog... + */ + dialog = gtk_message_dialog_new (GTK_WINDOW (op->priv->dialog), + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, + _("Unable to end process")); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", + error->message); + + gtk_widget_show_all (dialog); + response = gtk_dialog_run (GTK_DIALOG (dialog)); + + /* GTK_RESPONSE_NONE means the dialog were programmatically destroy, e.g. that + * GTK_DIALOG_DESTROY_WITH_PARENT kicked in - so it would trigger a warning to + * destroy the dialog in that case + */ + if (response != GTK_RESPONSE_NONE) + gtk_widget_destroy (dialog); + + g_error_free (error); + } + + out: + ; +} + +static gboolean +do_popup_menu_for_process_tree_view (GtkWidget *widget, + GdkEventButton *event, + GtkMountOperation *op) +{ + GtkWidget *menu; + GtkWidget *item; + gint button; + gint event_time; + gboolean popped_up_menu; + + popped_up_menu = FALSE; + + menu = gtk_menu_new (); + + item = gtk_image_menu_item_new_with_mnemonic (_("_End Process")); + gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), + gtk_image_new_from_stock (GTK_STOCK_CLOSE, GTK_ICON_SIZE_MENU)); + g_signal_connect (item, "activate", + G_CALLBACK (on_end_process_activated), + op); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); + gtk_widget_show_all (menu); + + if (event != NULL) + { + GtkTreePath *path; + GtkTreeSelection *selection; + + if (gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (op->priv->process_tree_view), + (gint) event->x, + (gint) event->y, + &path, + NULL, + NULL, + NULL)) + { + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (op->priv->process_tree_view)); + gtk_tree_selection_select_path (selection, path); + gtk_tree_path_free (path); + } + else + { + /* don't popup a menu if the user right-clicked in an area with no rows */ + goto out; + } + + button = event->button; + event_time = event->time; + } + else + { + button = 0; + event_time = gtk_get_current_event_time (); + } + + gtk_menu_popup (GTK_MENU (menu), + NULL, + widget, + NULL, + NULL, + button, + event_time); + + popped_up_menu = TRUE; + + out: + return popped_up_menu; +} + +static gboolean +on_popup_menu_for_process_tree_view (GtkWidget *widget, + gpointer user_data) +{ + GtkMountOperation *op = GTK_MOUNT_OPERATION (user_data); + return do_popup_menu_for_process_tree_view (widget, NULL, op); +} + +static gboolean +on_button_press_event_for_process_tree_view (GtkWidget *widget, + GdkEventButton *event, + gpointer user_data) +{ + GtkMountOperation *op = GTK_MOUNT_OPERATION (user_data); + gboolean ret; + + ret = FALSE; + + /* Ignore double-clicks and triple-clicks */ + if (event->button == 3 && event->type == GDK_BUTTON_PRESS) + { + ret = do_popup_menu_for_process_tree_view (widget, event, op); + } + + return ret; +} + +static void +create_show_processes_dialog (GMountOperation *op, + const char *message, + const char *choices[]) +{ + GtkMountOperationPrivate *priv; + GtkWidget *dialog; + const char *secondary = NULL; + char *primary; + int count, len = 0; + GtkWidget *label; + GtkWidget *tree_view; + GtkWidget *scrolled_window; + GtkWidget *vbox; + GtkWidget *content_area; + GtkTreeViewColumn *column; + GtkCellRenderer *renderer; + GtkListStore *list_store; + gchar *s; + + priv = GTK_MOUNT_OPERATION (op)->priv; + + primary = strstr (message, "\n"); + if (primary) + { + secondary = primary + 1; + primary = g_strndup (message, primary - message); + } + + dialog = gtk_dialog_new (); + + if (priv->parent_window != NULL) + gtk_window_set_transient_for (GTK_WINDOW (dialog), priv->parent_window); + gtk_window_set_title (GTK_WINDOW (dialog), ""); + gtk_dialog_set_has_separator (GTK_DIALOG (dialog), FALSE); + + content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + vbox = gtk_vbox_new (FALSE, 12); + gtk_container_set_border_width (GTK_CONTAINER (vbox), 12); + gtk_box_pack_start (GTK_BOX (content_area), vbox, TRUE, TRUE, 0); + + if (secondary != NULL) + { + s = g_strdup_printf ("%s\n\n%s", primary, secondary); + } + else + { + s = g_strdup_printf ("%s", primary); + } + g_free (primary); + label = gtk_label_new (NULL); + gtk_label_set_markup (GTK_LABEL (label), s); + g_free (s); + gtk_box_pack_start (GTK_BOX (vbox), label, TRUE, TRUE, 0); + + /* First count the items in the list then + * add the buttons in reverse order */ + + while (choices[len] != NULL) + len++; + + for (count = len - 1; count >= 0; count--) + gtk_dialog_add_button (GTK_DIALOG (dialog), choices[count], count); + + g_signal_connect (G_OBJECT (dialog), "response", + G_CALLBACK (show_processes_button_clicked), op); + + priv->dialog = GTK_DIALOG (dialog); + g_object_notify (G_OBJECT (op), "is-showing"); + + if (priv->parent_window == NULL && priv->screen) + gtk_window_set_screen (GTK_WINDOW (dialog), priv->screen); + + tree_view = gtk_tree_view_new (); + /* TODO: should use EM's when gtk+ RI patches land */ + gtk_widget_set_size_request (tree_view, + 300, + 120); + + column = gtk_tree_view_column_new (); + renderer = gtk_cell_renderer_pixbuf_new (); + gtk_tree_view_column_pack_start (column, renderer, FALSE); + gtk_tree_view_column_set_attributes (column, renderer, + "pixbuf", 0, + NULL); + renderer = gtk_cell_renderer_text_new (); + g_object_set (renderer, + "ellipsize", PANGO_ELLIPSIZE_MIDDLE, + "ellipsize-set", TRUE, + NULL); + gtk_tree_view_column_pack_start (column, renderer, TRUE); + gtk_tree_view_column_set_attributes (column, renderer, + "markup", 1, + NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (tree_view), column); + gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (tree_view), FALSE); + + + scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), + GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window), GTK_SHADOW_IN); + + gtk_container_add (GTK_CONTAINER (scrolled_window), tree_view); + gtk_box_pack_start (GTK_BOX (vbox), scrolled_window, TRUE, TRUE, 0); + + g_signal_connect (tree_view, "popup-menu", + G_CALLBACK (on_popup_menu_for_process_tree_view), + op); + g_signal_connect (tree_view, "button-press-event", + G_CALLBACK (on_button_press_event_for_process_tree_view), + op); + + list_store = gtk_list_store_new (3, + GDK_TYPE_PIXBUF, + G_TYPE_STRING, + G_TYPE_INT); + + gtk_tree_view_set_model (GTK_TREE_VIEW (tree_view), GTK_TREE_MODEL (list_store)); + + priv->process_list_store = list_store; + priv->process_tree_view = tree_view; + /* set pointers to NULL when dialog goes away */ + g_object_add_weak_pointer (G_OBJECT (list_store), (gpointer *) &priv->process_list_store); + g_object_add_weak_pointer (G_OBJECT (tree_view), (gpointer *) &priv->process_tree_view); + + g_object_unref (list_store); + + gtk_widget_show_all (dialog); + g_object_ref (op); +} + +static void +gtk_mount_operation_show_processes (GMountOperation *op, + const char *message, + GArray *processes, + const char *choices[]) +{ + GtkMountOperationPrivate *priv; + + g_return_if_fail (GTK_IS_MOUNT_OPERATION (op)); + g_return_if_fail (message != NULL); + g_return_if_fail (processes != NULL); + g_return_if_fail (choices != NULL); + + priv = GTK_MOUNT_OPERATION (op)->priv; + + if (priv->process_list_store == NULL) + { + /* need to create the dialog */ + create_show_processes_dialog (op, message, choices); + } + + /* otherwise, we're showing the dialog, assume messages+choices hasn't changed */ + + update_process_list_store (GTK_MOUNT_OPERATION (op), + priv->process_list_store, + processes); +} + static void gtk_mount_operation_aborted (GMountOperation *op) { diff --git a/gtk/gtkmountoperationprivate.h b/gtk/gtkmountoperationprivate.h new file mode 100644 index 0000000000..89ce862690 --- /dev/null +++ b/gtk/gtkmountoperationprivate.h @@ -0,0 +1,53 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* GTK - The GIMP Toolkit + * Copyright (C) David Zeuthen + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +/* + * Modified by the GTK+ Team and others 1997-2000. See the AUTHORS + * file for a list of people on the GTK+ Team. See the ChangeLog + * files for a list of changes. These files are distributed with + * GTK+ at ftp://ftp.gtk.org/pub/gtk/. + */ + +#ifndef __GTK_MOUNT_OPERATION_PRIVATE_H__ +#define __GTK_MOUNT_OPERATION_PRIVATE_H__ + +#include +#include +#include + +struct _GtkMountOperationLookupContext; +typedef struct _GtkMountOperationLookupContext GtkMountOperationLookupContext; + +GtkMountOperationLookupContext *_gtk_mount_operation_lookup_context_get (GdkDisplay *display); + +gboolean _gtk_mount_operation_lookup_info (GtkMountOperationLookupContext *context, + GPid pid, + gint size_pixels, + gchar **out_name, + gchar **out_command_line, + GdkPixbuf **out_pixbuf); + +void _gtk_mount_operation_lookup_context_free (GtkMountOperationLookupContext *context); + +/* throw G_IO_ERROR_FAILED_HANDLED if a helper already reported the error to the user */ +gboolean _gtk_mount_operation_kill_process (GPid pid, + GError **error); + +#endif /* __GTK_MOUNT_OPERATION_PRIVATE_H__ */