/*
Copyright (C) 2009 Jiri Moskovcak (jmoskovc@redhat.com)
Copyright (C) 2009 RedHat 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, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#if HAVE_LOCALE_H
# include <locale.h>
#endif
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <gio/gdesktopappinfo.h>
#define GDK_DISABLE_DEPRECATION_WARNINGS
/* https://bugzilla.gnome.org/show_bug.cgi?id=734826 */
#include <gtk/gtk.h>
#ifdef HAVE_POLKIT
#include <polkit/polkit.h>
#endif
#include <libnotify/notify.h>
#include <glib.h>
#include <libreport/internal_abrt_dbus.h>
#include <libreport/event_config.h>
#include <libreport/internal_libreport_gtk.h>
#include <libreport/problem_utils.h>
#include "libabrt.h"
#include "problem_api.h"
#define APP_NAME "abrt-applet"
#define GS_SCHEMA_ID_PRIVACY "org.gnome.desktop.privacy"
#define GS_PRIVACY_OPT_AUTO_REPORTING "report-technical-problems"
/* libnotify action keys */
#define A_REPORT_REPORT "REPORT"
#define A_RESTART_APPLICATION "RESTART"
#define GUI_EXECUTABLE "gnome-abrt"
#define NOTIFICATION_ICON_NAME "face-sad-symbolic"
static GNetworkMonitor *netmon;
static GList *g_deferred_crash_queue;
static guint g_deferred_timeout;
static bool g_gnome_abrt_available;
static bool g_user_is_admin;
static bool is_autoreporting_enabled(void)
{
GSettings *settings;
gboolean ret;
settings = g_settings_new (GS_SCHEMA_ID_PRIVACY);
ret = g_settings_get_boolean (settings, GS_PRIVACY_OPT_AUTO_REPORTING);
g_object_unref (settings);
return ret;
}
static void migrate_auto_reporting_to_gsettings(void)
{
#define OPT_NAME "AutoreportingEnabled"
map_string_t *settings = new_map_string();
if (!load_app_conf_file(APP_NAME, settings))
goto finito;
/* Silently ignore not configured options */
int sv_logmode = logmode;
/* but only if we run in silent mode (no -v on command line) */
logmode = g_verbose == 0 ? 0 : sv_logmode;
int auto_reporting = 0;
int configured = try_get_map_string_item_as_bool(settings, OPT_NAME, &auto_reporting);
logmode = sv_logmode;
if (!configured)
goto finito;
/* Enable the GS option if AutoreportingEnabled is true because the user
* turned the Autoreporting in abrt-applet in a before GS.
*
* Do not disable the GS option if AutoreportingEvent is false because the
* GS option is false by default, thus disabling would revert the user's
* decision to automatically report technical problems.
*/
if (auto_reporting)
{
GSettings *settings = g_settings_new(GS_SCHEMA_ID_PRIVACY);
g_settings_set_boolean(settings, GS_PRIVACY_OPT_AUTO_REPORTING, TRUE);
g_object_unref(settings);
}
remove_map_string_item(settings, OPT_NAME);
save_app_conf_file(APP_NAME, settings);
log_warning("Successfully migrated "APP_NAME":"OPT_NAME" to "GS_SCHEMA_ID_PRIVACY":"GS_PRIVACY_OPT_AUTO_REPORTING);
#undef OPT_NAME
finito:
free_map_string(settings);
return;
}
static const char *get_autoreport_event_name(void)
{
load_user_settings(APP_NAME);
const char *configured = get_user_setting("AutoreportingEvent");
return configured ? configured : g_settings_autoreporting_event;
}
static bool is_networking_enabled(void)
{
if (!g_network_monitor_get_network_available(netmon))
return FALSE;
return g_network_monitor_get_connectivity(netmon) == G_NETWORK_CONNECTIVITY_FULL;
}
static void show_problem_list_notification(GList *problems);
static void problem_info_unref(gpointer data);
static gboolean process_deferred_queue_timeout_fn(void)
{
g_deferred_timeout = 0;
GList *tmp = g_deferred_crash_queue;
g_deferred_crash_queue = NULL;
/* this function calls push_to_deferred_queue() which appends data to
* g_deferred_crash_queue but the function also modifies the argument
* so we must reset g_deferred_crash_queue before the call */
show_problem_list_notification(tmp);
/* Remove this timeout fn from the main loop*/
return G_SOURCE_REMOVE;
}
static void connectivity_changed_cb(GObject *gobject,
GParamSpec *pspec,
gpointer user_data)
{
if (g_network_monitor_get_network_available(netmon) &&
g_network_monitor_get_connectivity(netmon) == G_NETWORK_CONNECTIVITY_FULL)
{
if (g_deferred_timeout)
g_source_remove(g_deferred_timeout);
g_deferred_timeout = g_idle_add ((GSourceFunc)process_deferred_queue_timeout_fn, NULL);
}
}
typedef struct problem_info {
problem_data_t *problem_data;
int refcount;
bool foreign;
guint count;
bool is_packaged;
char **envp;
pid_t pid;
bool known;
bool reported;
bool was_announced;
bool is_writable;
int time;
} problem_info_t;
static void push_to_deferred_queue(problem_info_t *pi)
{
g_deferred_crash_queue = g_list_append(g_deferred_crash_queue, pi);
}
static const char *problem_info_get_dir(problem_info_t *pi)
{
return problem_data_get_content_or_NULL(pi->problem_data, CD_DUMPDIR);
}
static const char *problem_info_get_command_line(problem_info_t *pi)
{
return problem_data_get_content_or_NULL(pi->problem_data, FILENAME_CMDLINE);
}
static int problem_info_get_time(problem_info_t *pi)
{
if (pi->time == -1)
{
const char *time_str = problem_data_get_content_or_NULL(pi->problem_data, FILENAME_TIME);
if (time_str == NULL)
error_msg_and_die("BUG: Problem info has data without the element time");
pi->time = atoi(time_str);
}
return pi->time;
}
static const char **problem_info_get_env(problem_info_t *pi)
{
if (pi->envp == NULL)
{
const char *env_str = problem_data_get_content_or_NULL(pi->problem_data, FILENAME_ENVIRON);
pi->envp = (env_str != NULL) ? g_strsplit (env_str, "\n", -1) : NULL;
}
return (const char **)pi->envp;
}
static int problem_info_get_pid(problem_info_t *pi)
{
if (pi->pid == -1)
{
const char *pid_str = problem_data_get_content_or_NULL(pi->problem_data, FILENAME_PID);
pi->pid = (pid_str != NULL) ? atoi (pid_str) : -1;
}
return pi->pid;
}
static int problem_info_get_count(problem_info_t *pi)
{
if (pi->count == -1)
{
const char *count_str = problem_data_get_content_or_NULL(pi->problem_data, FILENAME_COUNT);
pi->count = count_str ? atoi(count_str) : 1;
}
return pi->count;
}
static bool problem_info_is_reported(problem_info_t *pi)
{
return problem_data_get_content_or_NULL(pi->problem_data, FILENAME_REPORTED_TO) != NULL;
}
static void problem_info_set_dir(problem_info_t *pi, const char *dir)
{
problem_data_add_text_noteditable(pi->problem_data, CD_DUMPDIR, dir);
}
static bool problem_info_ensure_writable(problem_info_t *pi)
{
if (pi->is_writable)
return true;
/* chown the directory in any case, because kernel oopses are not foreign */
/* but their dump directories are not writable without chowning them or */
/* stealing them. The stealing is deprecated as it breaks the local */
/* duplicate search and root cannot see them */
const int res = chown_dir_over_dbus(problem_info_get_dir(pi));
if (pi->foreign && res != 0)
{
error_msg(_("Can't take ownership of '%s'"), problem_info_get_dir(pi));
return false;
}
pi->foreign = false;
struct dump_dir *dd = open_directory_for_writing(problem_info_get_dir(pi), /* don't ask */ NULL);
if (!dd)
{
error_msg(_("Can't open directory for writing '%s'"), problem_info_get_dir(pi));
return false;
}
problem_info_set_dir(pi, dd->dd_dirname);
pi->is_writable = true;
dd_close(dd);
return true;
}
static problem_info_t *problem_info_new(const char *dir)
{
problem_info_t *pi = g_new0(problem_info_t, 1);
pi->refcount = 1;
pi->time = -1;
pi->pid = -1;
pi->count = -1;
pi->problem_data = problem_data_new();
problem_info_set_dir(pi, dir);
return pi;
}
static void problem_info_unref(gpointer data)
{
problem_info_t *pi;
if (data == NULL)
return;
pi = data;
pi->refcount--;
if (pi->refcount > 0)
return;
problem_data_free(pi->problem_data);
g_free(pi);
}
static problem_info_t* problem_info_ref(problem_info_t *pi)
{
g_return_val_if_fail (pi != NULL, NULL);
pi->refcount++;
return pi;
}
static void run_event_async(problem_info_t *pi, const char *event_name);
struct event_processing_state
{
pid_t child_pid;
int child_stdout_fd;
struct strbuf *cmd_output;
problem_info_t *pi;
int flags;
};
static struct event_processing_state *new_event_processing_state(void)
{
struct event_processing_state *p = g_new0(struct event_processing_state, 1);
p->child_pid = -1;
p->child_stdout_fd = -1;
p->cmd_output = strbuf_new();
return p;
}
static void free_event_processing_state(struct event_processing_state *p)
{
if (!p)
return;
strbuf_free(p->cmd_output);
g_free(p);
}
/* Compares the problem directories to list saved in
* $XDG_CACHE_HOME/abrt/applet_dirlist and updates the applet_dirlist
* with updated list.
*
* @param new_dirs The list where new directories are stored if caller
* wishes it. Can be NULL.
*/
static void new_dir_exists(GList **new_dirs)
{
GList *dirlist = get_problems_over_dbus(/*don't authorize*/false);
if (dirlist == ERR_PTR)
return;
const char *cachedir = g_get_user_cache_dir();
char *dirlist_name = concat_path_file(cachedir, "abrt");
g_mkdir_with_parents(dirlist_name, 0777);
free(dirlist_name);
dirlist_name = concat_path_file(cachedir, "abrt/applet_dirlist");
FILE *fp = fopen(dirlist_name, "r+");
if (!fp)
fp = fopen(dirlist_name, "w+");
free(dirlist_name);
if (fp)
{
GList *old_dirlist = NULL;
char *line;
while ((line = xmalloc_fgetline(fp)) != NULL)
old_dirlist = g_list_prepend(old_dirlist, line);
old_dirlist = g_list_reverse(old_dirlist);
/* We will sort and compare current dir list with last known one.
* Possible combinations:
* DIR1 DIR1 - Both lists have the same element, advance both ptrs.
* DIR2 - Current dir list has new element. IOW: new dir exists!
* Advance only current dirlist ptr.
* DIR3 - Only old list has element. Advance only old ptr.
* DIR4 ==== - Old list ended, current one didn't. New dir exists!
* ====
*/
GList *l1 = dirlist = g_list_sort(dirlist, (GCompareFunc)strcmp);
GList *l2 = old_dirlist = g_list_sort(old_dirlist, (GCompareFunc)strcmp);
int different = 0;
while (l1 && l2)
{
int diff = strcmp(l1->data, l2->data);
different |= diff;
if (diff < 0)
{
if (new_dirs)
{
*new_dirs = g_list_prepend(*new_dirs, xstrdup(l1->data));
log_notice("New dir detected: %s", (char *)l1->data);
}
l1 = g_list_next(l1);
continue;
}
l2 = g_list_next(l2);
if (diff == 0)
l1 = g_list_next(l1);
}
different |= (l1 != NULL);
if (different && new_dirs)
{
while (l1)
{
*new_dirs = g_list_prepend(*new_dirs, xstrdup(l1->data));
log_notice("New dir detected: %s", (char *)l1->data);
l1 = g_list_next(l1);
}
}
if (different || l2)
{
rewind(fp);
if (ftruncate(fileno(fp), 0)) /* shut up gcc */;
l1 = dirlist;
while (l1)
{
fprintf(fp, "%s\n", (char*) l1->data);
l1 = g_list_next(l1);
}
}
fclose(fp);
list_free_with_free(old_dirlist);
}
list_free_with_free(dirlist);
}
static bool is_gnome_abrt_available(void)
{
GAppInfo *app;
GError *error = NULL;
bool ret = TRUE;
app = g_app_info_create_from_commandline (GUI_EXECUTABLE, GUI_EXECUTABLE,
G_APP_INFO_CREATE_SUPPORTS_STARTUP_NOTIFICATION,
&error);
if (!app)
{
log_debug("Cannot find " GUI_EXECUTABLE ": %s", error->message);
g_error_free(error);
ret = FALSE;
}
g_clear_object(&app);
return ret;
}
static bool is_user_admin(void)
{
#ifdef HAVE_POLKIT
GError *error = NULL;
bool ret = false;
GPermission *perm = polkit_permission_new_sync ("org.freedesktop.problems.getall",
NULL, NULL, &error);
if (!perm)
perror_msg_and_die("Can't get Polkit configuration: %s", error->message);
ret = g_permission_get_allowed (perm);
g_object_unref (perm);
return ret;
#else
return true;
#endif
}
static gboolean
is_app_running (GAppInfo *app)
{
/* FIXME ask gnome-shell about that */
return FALSE;
}
static void fork_exec_gui(const char *problem_id)
{
GAppInfo *app;
GError *error = NULL;
char *cmd;
cmd = g_strdup_printf (GUI_EXECUTABLE " -p %s", problem_id);
app = g_app_info_create_from_commandline (cmd, GUI_EXECUTABLE,
G_APP_INFO_CREATE_SUPPORTS_STARTUP_NOTIFICATION,
&error);
g_free(cmd);
if (!app)
error_msg_and_die("Cannot find " GUI_EXECUTABLE);
if (!g_app_info_launch(G_APP_INFO(app), NULL, NULL, &error))
perror_msg_and_die("Could not launch " GUI_EXECUTABLE ": %s", error->message);
/* Scan dirs and save new $XDG_CACHE_HOME/abrt/applet_dirlist.
* (Otherwise, after a crash, next time applet is started,
* it will show alert icon even if we did click on it
* "in previous life"). We ignore function return value.
*/
new_dir_exists(/* new dirs list */ NULL);
}
static pid_t spawn_event_handler_child(const char *dump_dir_name, const char *event_name, int *fdp)
{
char *args[7];
args[0] = (char *) LIBEXEC_DIR"/abrt-handle-event";
args[1] = (char *) "-i"; /* Interactive? - Sure, applet is like a user */
args[2] = (char *) "-e";
args[3] = (char *) event_name;
args[4] = (char *) "--";
args[5] = (char *) dump_dir_name;
args[6] = NULL;
int pipeout[2];
int flags = EXECFLG_INPUT_NUL | EXECFLG_OUTPUT | EXECFLG_QUIET | EXECFLG_ERR2OUT;
VERB1 flags &= ~EXECFLG_QUIET;
char *env_vec[2];
/* WTF? We use 'abrt-handle-event -i' but here we export REPORT_CLIENT_NONINTERACTIVE */
/* - Exactly, REPORT_CLIENT_NONINTERACTIVE causes that abrt-handle-event in */
/* interactive mode replies with empty responses to all event's questions. */
env_vec[0] = g_strdup("REPORT_CLIENT_NONINTERACTIVE=1");
env_vec[1] = NULL;
pid_t child = fork_execv_on_steroids(flags, args, fdp ? pipeout : NULL,
env_vec, /*dir:*/ NULL, /*uid(unused):*/ 0);
if (fdp)
*fdp = pipeout[0];
g_free(env_vec[0]);
return child;
}
//this action should open gnome-abrt
static void action_report(NotifyNotification *notification, gchar *action, gpointer user_data)
{
log_debug("Reporting a problem!");
/* must be closed before ask_yes_no dialog run */
GError *err = NULL;
notify_notification_close(notification, &err);
if (err != NULL)
{
error_msg(_("Can't close notification: %s"), err->message);
g_error_free(err);
}
problem_info_t *pi = (problem_info_t *)user_data;
if (problem_info_get_dir(pi))
fork_exec_gui(problem_info_get_dir(pi));
}
static void action_restart(NotifyNotification *notification, gchar *action, gpointer user_data)
{
GAppInfo *app;
log_debug("Restarting an application!");
/* must be closed before ask_yes_no dialog run */
GError *err = NULL;
notify_notification_close(notification, &err);
if (err != NULL)
{
error_msg(_("Can't close notification: %s"), err->message);
g_error_free(err);
}
problem_info_t *pi = (problem_info_t *)user_data;
app = problem_create_app_from_cmdline (problem_info_get_command_line(pi));
g_assert (app);
if (!g_app_info_launch(G_APP_INFO(app), NULL, NULL, &err))
{
perror_msg("Could not launch '%s': %s",
g_desktop_app_info_get_filename (G_DESKTOP_APP_INFO (app)),
err->message);
}
g_object_unref (app);
}
static void on_notify_close(NotifyNotification *notification, gpointer user_data)
{
log_debug("Notify closed!");
g_object_unref(notification);
/* Scan dirs and save new $XDG_CACHE_HOME/abrt/applet_dirlist.
* (Otherwise, after a crash, next time applet is started,
* it will show alert icon even if we did click on it
* "in previous life"). We ignore finction return value.
*/
new_dir_exists(/* new dirs list */ NULL);
}
static NotifyNotification *new_warn_notification(const char *body)
{
NotifyNotification *notification;
notification = notify_notification_new(_("Oops!"), body, NOTIFICATION_ICON_NAME);
g_signal_connect(notification, "closed", G_CALLBACK(on_notify_close), NULL);
notify_notification_set_urgency(notification, NOTIFY_URGENCY_NORMAL);
notify_notification_set_timeout(notification, NOTIFY_EXPIRES_DEFAULT);
notify_notification_set_hint(notification, "desktop-entry", g_variant_new_string(APP_NAME));
return notification;
}
static void
add_default_action (NotifyNotification *notification,
problem_info_t *pi)
{
if (!g_gnome_abrt_available)
return;
/* Using the same action as for report */
notify_notification_add_action(notification, "default", _("Report"),
NOTIFY_ACTION_CALLBACK(action_report),
problem_info_ref (pi), problem_info_unref);
}
static void
add_send_a_report_button (NotifyNotification *notification,
problem_info_t *pi)
{
if (!g_gnome_abrt_available)
return;
notify_notification_add_action(notification, A_REPORT_REPORT, _("Report"),
NOTIFY_ACTION_CALLBACK(action_report),
problem_info_ref (pi), problem_info_unref);
}
static void
add_restart_app_button (NotifyNotification *notification,
problem_info_t *pi)
{
notify_notification_add_action(notification, A_RESTART_APPLICATION, _("Restart"),
NOTIFY_ACTION_CALLBACK(action_restart),
problem_info_ref (pi), problem_info_unref);
}
/*
* Destroys the problems argument
*/
static void notify_problem_list(GList *problems)
{
if (problems == NULL)
{
log_debug("Not showing any notification bubble because the list of problems is empty.");
return;
}
/* For the whole system, we'll need to know:
* - Whether automatic reporting is enabled or not
* - Whether the network is available
*/
gboolean auto_reporting = is_autoreporting_enabled();
gboolean network_available = is_networking_enabled();
for (GList *iter = problems; iter; iter = g_list_next(iter))
{
char *notify_body = NULL;
GAppInfo *app;
problem_info_t *pi = iter->data;
if (pi->was_announced)
{
problem_info_unref (pi);
continue;
}
app = problem_create_app_from_env (problem_info_get_env(pi), problem_info_get_pid(pi));
if (!app)
{
const char *const cmd_line = problem_info_get_command_line(pi);
if (cmd_line != NULL)
app = problem_create_app_from_cmdline(cmd_line);
}
/* For each problem we'll need to know:
* - Whether or not the crash happened in an “app”
* - Whether the app is packaged (in Fedora) or not
* - Whether the app is back up and running
* - Whether the user is the one for which the app crashed
* - Whether the problem has already been reported on this machine
*/
gboolean is_app = (app != NULL);
gboolean is_packaged = pi->is_packaged;
gboolean is_running_again = is_app_running(app);
gboolean is_current_user = !pi->foreign;
gboolean already_reported = problem_info_get_count(pi) > 1;
gboolean report_button = FALSE;
gboolean restart_button = FALSE;
if (is_app)
{
if (auto_reporting)
{
if (is_packaged)
{
if (network_available)
{
notify_body = g_strdup_printf (_("We're sorry, it looks like %s crashed. The problem has been automatically reported."),
g_app_info_get_display_name (app));
}
else
{
notify_body = g_strdup_printf (_("We’re sorry, it looks like %s crashed. The problem will be reported when the internet is available."),
g_app_info_get_display_name (app));
}
}
else if (!already_reported)
{
notify_body = g_strdup_printf (_("We're sorry, it looks like %s crashed. Please contact the developer if you want to report the issue."),
g_app_info_get_display_name (app));
}
}
else
{
if (is_packaged)
{
notify_body = g_strdup_printf (_("We're sorry, it looks like %s crashed. If you'd like to help resolve the issue, please send a report."),
g_app_info_get_display_name (app));
report_button = TRUE;
}
else if (!already_reported)
{
notify_body = g_strdup_printf (_("We're sorry, it looks like %s crashed. Please contact the developer if you want to report the issue."),
g_app_info_get_display_name (app));
}
}
if (is_current_user && !is_running_again)
restart_button = TRUE;
} else {
if (!already_reported)
{
if (auto_reporting && is_packaged)
{
if (network_available)
{
notify_body = g_strdup (_("We're sorry, it looks like a problem occurred in a component. The problem has been automatically reported."));
}
else
{
notify_body = g_strdup (_("We're sorry, it looks like a problem occurred in a component. The problem will be reported when the internet is available."));
}
}
else if (!auto_reporting && is_packaged)
{
notify_body = g_strdup (_("We're sorry, it looks like a problem occurred. If you'd like to help resolve the issue, please send a report."));
report_button = TRUE;
}
else
{
char *binary = problem_get_argv0 (problem_info_get_command_line(pi));
notify_body = g_strdup_printf (_("We're sorry, it looks like %s crashed. Please contact the developer if you want to report the issue."),
binary);
g_free (binary);
}
}
}
if (!notify_body)
{
#define BOOL_AS_STR(x) x ? "true" : "false"
log_debug ("Not showing a notification, as we have no message to show:");
log_debug ("auto reporting: %s", BOOL_AS_STR(auto_reporting));
log_debug ("network available: %s", BOOL_AS_STR(network_available));
log_debug ("is app: %s", BOOL_AS_STR(is_app));
log_debug ("is packaged: %s", BOOL_AS_STR(is_packaged));
log_debug ("is running again: %s", BOOL_AS_STR(is_running_again));
log_debug ("is current user: %s", BOOL_AS_STR(is_current_user));
log_debug ("already reported: %s", BOOL_AS_STR(already_reported));
g_clear_object (&app);
problem_info_unref (pi);
continue;
}
NotifyNotification *notification = new_warn_notification(notify_body);
g_free(notify_body);
pi->was_announced = true;
if (report_button)
add_send_a_report_button (notification, pi);
if (restart_button)
add_restart_app_button (notification, pi);
add_default_action (notification, pi);
GError *err = NULL;
log_debug("Showing a notification");
notify_notification_show(notification, &err);
if (err != NULL)
{
error_msg(_("Can't show notification: %s"), err->message);
g_error_free(err);
}
problem_info_unref (pi);
}
g_list_free(problems);
}
static void notify_problem(problem_info_t *pi)
{
GList *problems = g_list_append(NULL, pi);
notify_problem_list(problems);
}
/* Event-processing child output handler */
static gboolean handle_event_output_cb(GIOChannel *gio, GIOCondition condition, gpointer ptr)
{
struct event_processing_state *state = ptr;
problem_info_t *pi = state->pi;
/* Read streamed data and split lines */
for (;;)
{
char buf[250]; /* usually we get one line, no need to have big buf */
errno = 0;
gsize r = 0;
GError *error = NULL;
GIOStatus stat = g_io_channel_read_chars(gio, buf, sizeof(buf) - 1, &r, &error);
if (stat == G_IO_STATUS_ERROR)
{ /* TODO: Terminate child's process? */
error_msg(_("Can't read from gio channel: '%s'"), error ? error->message : "");
g_error_free(error);
break;
}
if (stat == G_IO_STATUS_AGAIN)
{ /* We got all buffered data, but fd is still open. Done for now */
return TRUE; /* "glib, please don't remove this event (yet)" */
}
if (stat == G_IO_STATUS_EOF)
break;
buf[r] = '\0';
/* split lines in the current buffer */
char *raw = buf;
char *newline;
while ((newline = strchr(raw, '\n')) != NULL)
{
*newline = '\0';
strbuf_append_str(state->cmd_output, raw);
char *msg = state->cmd_output->buf;
log_debug("%s", msg);
strbuf_clear(state->cmd_output);
/* jump to next line */
raw = newline + 1;
}
/* beginning of next line. the line continues by next read */
strbuf_append_str(state->cmd_output, raw);
}
/* EOF/error */
/* Wait for child to actually exit, collect status */
int status = 1;
if (safe_waitpid(state->child_pid, &status, 0) <= 0)
perror_msg("waitpid(%d)", (int)state->child_pid);
if (WIFEXITED(status) && WEXITSTATUS(status) == EXIT_STOP_EVENT_RUN)
{
pi->known = 1;
status = 0;
}
if (status == 0)
{
pi->reported = 1;
log_debug("fast report finished successfully");
notify_problem(pi);
}
else
{
log_debug("fast report failed, deferring");
push_to_deferred_queue(pi);
}
free_event_processing_state(state);
/* We stop using this channel */
g_io_channel_unref(gio);
return FALSE;
}
static GIOChannel *my_io_channel_unix_new(int fd)
{
GIOChannel *ch = g_io_channel_unix_new(fd);
/* Need to set the encoding otherwise we get:
* "Invalid byte sequence in conversion input".
* According to manual "NULL" is safe for binary data.
*/
GError *error = NULL;
g_io_channel_set_encoding(ch, NULL, &error);
if (error)
perror_msg_and_die(_("Can't set encoding on gio channel: %s"), error->message);
g_io_channel_set_flags(ch, G_IO_FLAG_NONBLOCK, &error);
if (error)
perror_msg_and_die(_("Can't turn on nonblocking mode for gio channel: %s"), error->message);
g_io_channel_set_close_on_unref(ch, TRUE);
return ch;
}
static void export_event_configuration(const char *event_name)
{
static bool exported = false;
if (exported)
return;
exported = true;
event_config_t *event_config = get_event_config(event_name);
/* load event config data only for the event */
if (event_config != NULL)
load_single_event_config_data_from_user_storage(event_config);
GList *ex_env = export_event_config(event_name);
g_list_free(ex_env);
}
static void run_event_async(problem_info_t *pi, const char *event_name)
{
if (!problem_info_ensure_writable(pi))
{
problem_info_unref(pi);
return;
}
export_event_configuration(event_name);
struct event_processing_state *state = new_event_processing_state();
state->pi = pi;
state->child_pid = spawn_event_handler_child(problem_info_get_dir(state->pi), event_name, &state->child_stdout_fd);
GIOChannel *channel_event_output = my_io_channel_unix_new(state->child_stdout_fd);
g_io_add_watch(channel_event_output, G_IO_IN | G_IO_PRI | G_IO_HUP,
handle_event_output_cb, state);
}
/*
* Destroys the problems argument
*/
static void show_problem_list_notification(GList *problems)
{
if (is_autoreporting_enabled())
{
/* Automatically report only own problems */
/* and skip foreign problems */
for (GList *iter = problems; iter;)
{
problem_info_t *pi = (problem_info_t *)iter->data;
GList *next = g_list_next(iter);
if (!pi->foreign || g_user_is_admin)
{
if (is_networking_enabled ())
{
run_event_async(pi, get_autoreport_event_name());
problems = g_list_delete_link(problems, iter);
}
else
{
/* Don't remove from the list, we'll tell the user
* we'll report later, if it's not a dupe */
push_to_deferred_queue(pi);
}
}
iter = next;
}
}
/* report the rest:
* - only foreign if autoreporting is enabled
* - the whole list otherwise
*/
if (problems)
notify_problem_list(problems);
}
static void show_problem_notification(problem_info_t *pi)
{
GList *problems = g_list_append(NULL, pi);
show_problem_list_notification(problems);
}
static void Crash(GVariant *parameters)
{
const char *package_name, *dir, *uid_str, *uuid, *duphash;
log_debug("Crash recorded");
g_variant_get (parameters, "(&s&s&s&s&s)",
&package_name,
&dir,
&uid_str,
&uuid,
&duphash);
bool foreign_problem = false;
if (uid_str[0] != '\0')
{
char *end;
errno = 0;
unsigned long uid_num = strtoul(uid_str, &end, 10);
if (errno || *end != '\0' || uid_num != getuid())
{
foreign_problem = true;
log_notice("foreign problem %i", foreign_problem);
}
}
/* Non-admins shouldn't see other people's crashes */
if (foreign_problem && !g_user_is_admin)
return;
static const char *elements[] = {
FILENAME_CMDLINE,
FILENAME_COUNT,
FILENAME_UUID,
FILENAME_DUPHASH,
FILENAME_COMPONENT,
FILENAME_ENVIRON,
FILENAME_PID,
NULL,
};
problem_info_t *pi = problem_info_new(dir);
fill_problem_data_over_dbus(dir, elements, pi->problem_data);
pi->foreign = foreign_problem;
pi->is_packaged = (package_name != NULL);
/*
* Can't append dir to the seen list because of directory stealing
*
* append_dirlist(dir);
*
*/
show_problem_notification(pi);
}
static void handle_message(GDBusConnection *connection,
const gchar *sender_name,
const gchar *object_path,
const gchar *interface_name,
const gchar *signal_name,
GVariant *parameters,
gpointer user_data)
{
g_debug ("Received signal: sender_name: %s, object_path: %s, "
"interface_name: %s, signal_name: %s",
sender_name, object_path, interface_name, signal_name);
Crash(parameters);
}
static void
name_acquired_handler (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
static const char *elements[] = {
FILENAME_CMDLINE,
FILENAME_COUNT,
FILENAME_UUID,
FILENAME_DUPHASH,
FILENAME_COMPONENT,
FILENAME_UID,
FILENAME_TIME,
FILENAME_REPORTED_TO,
FILENAME_NOT_REPORTABLE,
NULL
};
/* If some new dirs appeared since our last run, let user know it */
GList *new_dirs = NULL;
GList *notify_list = NULL;
new_dir_exists(&new_dirs);
#define time_before_ndays(n) (time(NULL) - (n)*24*60*60)
/* Age limit = now - 3 days */
const unsigned long min_born_time = (unsigned long)(time_before_ndays(3));
for ( ; new_dirs != NULL; new_dirs = g_list_next(new_dirs))
{
const char *problem_id = (const char *)new_dirs->data;
problem_info_t *pi = problem_info_new(problem_id);
if (fill_problem_data_over_dbus(problem_id, elements, pi->problem_data) != 0)
{
log_notice("'%s' is not a dump dir - ignoring\n", problem_id);
problem_info_unref(pi);
continue;
}
/* TODO: add a filter for only complete problems to GetProblems D-Bus method */
if (!dbus_problem_is_complete(problem_id))
{
log_notice("Ignoring incomplete problem '%s'", problem_id);
problem_info_unref(pi);
continue;
}
/* TODO: add a filter for max-old reported problems to GetProblems D-Bus method */
if (problem_info_get_time(pi) < min_born_time)
{
log_notice("Ignoring outdated problem '%s'", problem_id);
problem_info_unref(pi);
continue;
}
/* TODO: add a filter for not-yet reported problems to GetProblems D-Bus method */
if (problem_info_is_reported(pi))
{
log_notice("Ignoring already reported problem '%s'", problem_id);
problem_info_unref(pi);
continue;
}
/* Can't be foreig because new_dir_exists() returns only own problems */
pi->foreign = false;
notify_list = g_list_prepend(notify_list, pi);
}
if (notify_list)
show_problem_list_notification(notify_list);
list_free_with_free(new_dirs);
/*
* We want to update "seen directories" list on SIGTERM.
* Updating it after each notification doesn't account for stealing directories:
* if directory is stolen after seen list is updated,
* on next startup applet will notify user about stolen directory. WRONG.
*
* SIGTERM handler simply stops GTK main loop and the applet saves user
* settings, releases notify resources, releases dbus resources and updates
* the seen list.
*/
}
static void
name_lost_handler (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
if (connection == NULL)
error_msg_and_die("Problem connecting to dbus");
gtk_main_quit ();
}
int main(int argc, char** argv)
{
/* I18n */
setlocale(LC_ALL, "");
#if ENABLE_NLS
bindtextdomain(PACKAGE, LOCALEDIR);
textdomain(PACKAGE);
#endif
abrt_init(argv);
notify_init("Problem detected");
/* Monitor NetworkManager state */
netmon = g_network_monitor_get_default ();
g_signal_connect (G_OBJECT (netmon), "notify::connectivity",
G_CALLBACK (connectivity_changed_cb), NULL);
g_signal_connect (G_OBJECT (netmon), "notify::network-available",
G_CALLBACK (connectivity_changed_cb), NULL);
g_set_prgname("abrt");
gtk_init(&argc, &argv);
/* Can't keep these strings/structs static: _() doesn't support that */
const char *program_usage_string = _(
"& [-v] [DIR]...\n"
"\n"
"Applet which notifies user when new problems are detected by ABRT\n"
);
enum {
OPT_v = 1 << 0,
};
/* Keep enum above and order of options below in sync! */
struct options program_options[] = {
OPT__VERBOSE(&g_verbose),
OPT_END()
};
/*unsigned opts =*/ parse_opts(argc, argv, program_options, program_usage_string);
migrate_to_xdg_dirs();
migrate_auto_reporting_to_gsettings();
export_abrt_envvars(0);
msg_prefix = g_progname;
load_abrt_conf();
load_event_config_data();
load_user_settings(APP_NAME);
/* Initialize our (dbus_abrt) machinery by filtering
* for signals:
* signal sender=:1.73 -> path=/org/freedesktop/problems; interface=org.freedesktop.problems; member=Crash
* string "coreutils-7.2-3.fc11"
* string "0"
*/
GError *error = NULL;
GDBusConnection *system_conn = g_bus_get_sync (G_BUS_TYPE_SYSTEM,
NULL, &error);
if (system_conn == NULL)
perror_msg_and_die("Can't connect to system dbus: %s", error->message);
guint filter_id = g_dbus_connection_signal_subscribe(system_conn,
NULL,
ABRT_DBUS_NAME,
"Crash",
ABRT_DBUS_OBJECT,
NULL,
G_DBUS_SIGNAL_FLAGS_NONE,
handle_message,
NULL, NULL);
guint name_own_id = g_bus_own_name (G_BUS_TYPE_SESSION,
ABRT_DBUS_NAME".applet",
G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | G_BUS_NAME_OWNER_FLAGS_REPLACE,
NULL,
name_acquired_handler,
name_lost_handler,
NULL, NULL);
g_user_is_admin = is_user_admin();
g_gnome_abrt_available = is_gnome_abrt_available();
/* Enter main loop
*/
gtk_main();
g_bus_unown_name (name_own_id);
g_dbus_connection_signal_unsubscribe(system_conn, filter_id);
/* new_dir_exists() is called for each notification and if user clicks on
* the abrt icon. Those calls cover 99.97% of detected crashes
*
* The rest of detected crashes:
*
* 0.01%
* applet doesn't append a repeated crash to the seen list if the crash was
* the last caught crash before exit (notification is not shown in case of
* repeated crash)
*
* 0.01%
* applet doesn't append a stolen directory to the seen list if
* notification was closed before the notified directory had been stolen
*
* 0.1%
* crashes of abrt-applet
*/
new_dir_exists(/* new dirs list */ NULL);
if (notify_is_initted())
notify_uninit();
/* It does not make much sense to save settings at exit and after
* introduction of system-config-abrt it is wrong to do that. abrt-applet
* is long-running application and user can modify the configuration files
* while abrt-applet run. Thus, saving configuration at desktop session
* exit would make someone's life really hard.
*
* abrt-applet saves configuration immediately after user input.
*
* save_user_settings();
*/
return 0;
}