/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- * * Copyright (C) 2004-2006 William Jon McCann * * 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 St, Fifth Floor, Boston, MA 02110-1301, USA. * * Authors: William Jon McCann * */ #include "config.h" #include #include #include #include #include #include #include #if defined(HAVE_SETPRIORITY) && defined(PRIO_PROCESS) #include #endif #include #include #include #include #include "gs-debug.h" #include "gs-job.h" #include "subprocs.h" static void gs_job_class_init (GSJobClass *klass); static void gs_job_init (GSJob *job); static void gs_job_finalize (GObject *object); #define GS_JOB_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GS_TYPE_JOB, GSJobPrivate)) typedef enum { GS_JOB_INVALID, GS_JOB_RUNNING, GS_JOB_STOPPED, GS_JOB_KILLED, GS_JOB_DEAD } GSJobStatus; struct GSJobPrivate { GtkWidget *widget; GSJobStatus status; gint pid; guint watch_id; char *command; }; G_DEFINE_TYPE (GSJob, gs_job, G_TYPE_OBJECT) static char * widget_get_id_string (GtkWidget *widget) { char *id = NULL; g_return_val_if_fail (widget != NULL, NULL); id = g_strdup_printf ("0x%X", #if GTK_CHECK_VERSION (3, 0, 0) (guint32)GDK_WINDOW_XID (gtk_widget_get_window (widget))); #else (guint32)GDK_WINDOW_XID (widget->window)); #endif return id; } static void gs_job_class_init (GSJobClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = gs_job_finalize; g_type_class_add_private (klass, sizeof (GSJobPrivate)); } static void gs_job_init (GSJob *job) { job->priv = GS_JOB_GET_PRIVATE (job); } /* adapted from gspawn.c */ static int wait_on_child (int pid) { int status; wait_again: if (waitpid (pid, &status, 0) < 0) { if (errno == EINTR) { goto wait_again; } else if (errno == ECHILD) { ; /* do nothing, child already reaped */ } else { gs_debug ("waitpid () should not fail in 'GSJob'"); } } return status; } static void gs_job_died (GSJob *job) { if (job->priv->pid > 0) { int exit_status; gs_debug ("Waiting on process %d", job->priv->pid); exit_status = wait_on_child (job->priv->pid); job->priv->status = GS_JOB_DEAD; if (WIFEXITED (exit_status) && (WEXITSTATUS (exit_status) != 0)) { gs_debug ("Wait on child process failed"); } else { /* exited normally */ } } g_spawn_close_pid (job->priv->pid); job->priv->pid = 0; gs_debug ("Job died"); } static void gs_job_finalize (GObject *object) { GSJob *job; g_return_if_fail (object != NULL); g_return_if_fail (GS_IS_JOB (object)); job = GS_JOB (object); g_return_if_fail (job->priv != NULL); if (job->priv->pid > 0) { signal_pid (job->priv->pid, SIGTERM); gs_job_died (job); } g_free (job->priv->command); job->priv->command = NULL; G_OBJECT_CLASS (gs_job_parent_class)->finalize (object); } void gs_job_set_widget (GSJob *job, GtkWidget *widget) { g_return_if_fail (job != NULL); g_return_if_fail (GS_IS_JOB (job)); if (widget != job->priv->widget) { job->priv->widget = widget; /* restart job */ if (gs_job_is_running (job)) { gs_job_stop (job); gs_job_start (job); } } } gboolean gs_job_set_command (GSJob *job, const char *command) { g_return_val_if_fail (GS_IS_JOB (job), FALSE); gs_debug ("Setting command for job: '%s'", command != NULL ? command : "NULL"); g_free (job->priv->command); job->priv->command = g_strdup (command); return TRUE; } GSJob * gs_job_new (void) { GObject *job; job = g_object_new (GS_TYPE_JOB, NULL); return GS_JOB (job); } GSJob * gs_job_new_for_widget (GtkWidget *widget) { GObject *job; job = g_object_new (GS_TYPE_JOB, NULL); gs_job_set_widget (GS_JOB (job), widget); return GS_JOB (job); } static void nice_process (int pid, int nice_level) { g_return_if_fail (pid > 0); if (nice_level == 0) { return; } #if defined(HAVE_SETPRIORITY) && defined(PRIO_PROCESS) gs_debug ("Setting child process priority to: %d", nice_level); if (setpriority (PRIO_PROCESS, pid, nice_level) != 0) { gs_debug ("setpriority(PRIO_PROCESS, %lu, %d) failed", (unsigned long) pid, nice_level); } #else gs_debug ("don't know how to change process priority on this system."); #endif } static GPtrArray * get_env_vars (GtkWidget *widget) { GPtrArray *env; char *str; int i; static const char *allowed_env_vars [] = { "PATH", "SESSION_MANAGER", "XAUTHORITY", "XAUTHLOCALHOSTNAME", "LANG", "LANGUAGE", "DBUS_SESSION_BUS_ADDRESS" }; env = g_ptr_array_new (); str = gdk_screen_make_display_name (gtk_widget_get_screen (widget)); g_ptr_array_add (env, g_strdup_printf ("DISPLAY=%s", str)); g_free (str); g_ptr_array_add (env, g_strdup_printf ("HOME=%s", g_get_home_dir ())); for (i = 0; i < G_N_ELEMENTS (allowed_env_vars); i++) { const char *var; const char *val; var = allowed_env_vars [i]; val = g_getenv (var); if (val != NULL) { g_ptr_array_add (env, g_strdup_printf ("%s=%s", var, val)); } } str = widget_get_id_string (widget); g_ptr_array_add (env, g_strdup_printf ("XSCREENSAVER_WINDOW=%s", str)); g_free (str); g_ptr_array_add (env, NULL); return env; } static gboolean spawn_on_widget (GtkWidget *widget, const char *command, int *pid, GIOFunc watch_func, gpointer user_data, guint *watch_id) { char **argv; GPtrArray *env; gboolean result; GIOChannel *channel; GError *error = NULL; int standard_error; int child_pid; int id; int i; if (command == NULL) { return FALSE; } if (! g_shell_parse_argv (command, NULL, &argv, &error)) { gs_debug ("Could not parse command: %s", error->message); g_error_free (error); return FALSE; } env = get_env_vars (widget); error = NULL; #if GTK_CHECK_VERSION (3, 0, 0) result = g_spawn_async_with_pipes (NULL, argv, (char **)env->pdata, G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &child_pid, NULL, NULL, &standard_error, &error); #else result = gdk_spawn_on_screen_with_pipes (gtk_widget_get_screen (widget), NULL, argv, (char **)env->pdata, G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &child_pid, NULL, NULL, &standard_error, &error); #endif for (i = 0; i < env->len; i++) { g_free (g_ptr_array_index (env, i)); } g_ptr_array_free (env, TRUE); if (! result) { gs_debug ("Could not start command '%s': %s", command, error->message); g_error_free (error); g_strfreev (argv); return FALSE; } g_strfreev (argv); nice_process (child_pid, 10); if (pid != NULL) { *pid = child_pid; } else { g_spawn_close_pid (child_pid); } channel = g_io_channel_unix_new (standard_error); g_io_channel_set_close_on_unref (channel, TRUE); g_io_channel_set_flags (channel, g_io_channel_get_flags (channel) | G_IO_FLAG_NONBLOCK, NULL); id = g_io_add_watch (channel, G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL, watch_func, user_data); if (watch_id != NULL) { *watch_id = id; } g_io_channel_unref (channel); return result; } static gboolean command_watch (GIOChannel *source, GIOCondition condition, GSJob *job) { GIOStatus status; GError *error = NULL; gboolean done = FALSE; g_return_val_if_fail (job != NULL, FALSE); if (condition & G_IO_IN) { char *str; status = g_io_channel_read_line (source, &str, NULL, NULL, &error); if (status == G_IO_STATUS_NORMAL) { gs_debug ("command output: %s", str); } else if (status == G_IO_STATUS_EOF) { done = TRUE; } else if (error != NULL) { gs_debug ("command error: %s", error->message); g_error_free (error); } g_free (str); } else if (condition & G_IO_HUP) { done = TRUE; } if (done) { gs_job_died (job); job->priv->watch_id = 0; return FALSE; } return TRUE; } gboolean gs_job_is_running (GSJob *job) { gboolean running; g_return_val_if_fail (GS_IS_JOB (job), FALSE); running = (job->priv->pid > 0); return running; } gboolean gs_job_start (GSJob *job) { gboolean result; g_return_val_if_fail (job != NULL, FALSE); g_return_val_if_fail (GS_IS_JOB (job), FALSE); gs_debug ("starting job"); if (job->priv->pid != 0) { gs_debug ("Cannot restart active job."); return FALSE; } if (job->priv->widget == NULL) { gs_debug ("Could not start job: screensaver window is not set."); return FALSE; } if (job->priv->command == NULL) { /* no warning here because a NULL command is interpreted as a no-op job */ gs_debug ("No command set for job."); return FALSE; } result = spawn_on_widget (job->priv->widget, job->priv->command, &job->priv->pid, (GIOFunc)command_watch, job, &job->priv->watch_id); if (result) { job->priv->status = GS_JOB_RUNNING; } return result; } static void remove_command_watch (GSJob *job) { if (job->priv->watch_id != 0) { g_source_remove (job->priv->watch_id); job->priv->watch_id = 0; } } gboolean gs_job_stop (GSJob *job) { g_return_val_if_fail (job != NULL, FALSE); g_return_val_if_fail (GS_IS_JOB (job), FALSE); gs_debug ("stopping job"); if (job->priv->pid == 0) { gs_debug ("Could not stop job: pid not defined"); return FALSE; } if (job->priv->status == GS_JOB_STOPPED) { gs_job_suspend (job, FALSE); } remove_command_watch (job); signal_pid (job->priv->pid, SIGTERM); job->priv->status = GS_JOB_KILLED; gs_job_died (job); return TRUE; } gboolean gs_job_suspend (GSJob *job, gboolean suspend) { g_return_val_if_fail (job != NULL, FALSE); g_return_val_if_fail (GS_IS_JOB (job), FALSE); gs_debug ("suspending job"); if (job->priv->pid == 0) { return FALSE; } signal_pid (job->priv->pid, (suspend ? SIGSTOP : SIGCONT)); job->priv->status = (suspend ? GS_JOB_STOPPED : GS_JOB_RUNNING); return TRUE; }