/*************************************************************************** spawn_async_with_pipes_gtk.c ---------------------------- begin : Sun Jan 11 2004 copyright : (C) 2004 by Tim-Philipp Müller email : t.i.m at orange dot net ***************************************************************************/ /*************************************************************************** * * * 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 code demonstrates the usage of g_spawn_async_with_pipes() and * * GIOChannels as part of the Gtk+ main loop (ie. it demonstrates how * * to spawn a child process, read its output, and at the same time keep * * the GUI responsive). * * * ***************************************************************************/ #include #include #include #ifdef G_OS_UNIX #include #include #endif /* We need at least GLib >= 2.4.0 for the child watch and GPid stuff */ #if !GLIB_CHECK_VERSION(2,4,0) # error You need at least Gtk/GLib 2.4.0 to run this code. #endif #define BUFSIZE 1024 static GtkWidget *button; /* NULL */ static GtkWidget *progressbar; /* NULL */ static GtkWidget *textview; /* NULL */ static GtkWidget *label; /* NULL */ static guint8 *databuf; /* NULL */ static gsize datalen; /* 0 */ /***************************************************************** * * get_human_size * * (note: not thread-safe!) * *****************************************************************/ const gchar * get_human_size(gsize len) { static gchar buf[256]; if (len < 1024) g_snprintf(buf, 256, "%u bytes", len ); else if (datalen < (1024*1024)) g_snprintf(buf, 256, "%u kB", (len >> 10)); else g_snprintf(buf, 256, "%.1f MB", (gfloat)len /(1024.0*1024.0)); return buf; } /***************************************************************** * * stdout_done * * Called when the stdout pipe was closed * (ie. wget terminated). * * Always returns FALSE (to make it nicer * to use in the callback) * *****************************************************************/ static gboolean stdout_done (GIOChannel *ioc) { gchar buf[512]; g_snprintf(buf, 512, " Download finished. Received %u bytes (%s)", datalen, get_human_size(datalen)); gtk_label_set_text(GTK_LABEL(label), buf); /* ... if datalen is 0, an error has probably occured --- */ /* (it's really hard to know though what happened) */ /* ... do something with the received data here ... */ /* free received data and reset for next use */ g_free(databuf); databuf = NULL; datalen = 0; gtk_widget_set_sensitive(button, TRUE); return FALSE; } /***************************************************************** * * stderr_done * * Called when the stderr pipe was closed * (ie. wget terminated). * * Always returns FALSE (to make it nicer * to use in the callback) * *****************************************************************/ static gboolean stderr_done (GIOChannel *ioc) { gtk_widget_set_sensitive(button, TRUE); return FALSE; } /***************************************************************** * * iofunc_stdout * * called when there's data to read from the stdin pipe * or when the pipe is closed (ie. the program terminated) * *****************************************************************/ static gboolean iofunc_stdout (GIOChannel *ioc, GIOCondition cond, gpointer data) { /* data for us to read? */ if (cond & (G_IO_IN | G_IO_PRI)) { GIOStatus ret; guint8 buf[BUFSIZE]; gsize len = 0; memset(buf, 0x00, BUFSIZE); ret = g_io_channel_read_chars(ioc, buf, BUFSIZE, &len, NULL); if (len <= 0 || ret != G_IO_STATUS_NORMAL) return stdout_done(ioc); /* = return FALSE */ databuf = g_realloc(databuf, datalen + len + 1); memcpy(databuf + datalen, buf, len); databuf[datalen+len] = 0x00; datalen += len; gtk_progress_bar_pulse(GTK_PROGRESS_BAR(progressbar)); /* abuse buf */ g_snprintf(buf, BUFSIZE, " Received %s", get_human_size(datalen)); gtk_label_set_text(GTK_LABEL(label), buf); } if (cond & (G_IO_ERR | G_IO_HUP | G_IO_NVAL)) return stdout_done(ioc); /* = return FALSE */ return TRUE; } /***************************************************************** * * iofunc_stderr * * called when there's data to read from the stderr pipe * or when the pipe is closed (ie. the program terminated) * *****************************************************************/ static gboolean iofunc_stderr (GIOChannel *ioc, GIOCondition cond, gpointer data) { GtkTextBuffer *textbuf; GtkTextIter enditer; /* data for us to read? */ if (cond & (G_IO_IN | G_IO_PRI)) { GIOStatus ret; guint8 buf[BUFSIZE]; gsize len = 0; ret = g_io_channel_read_chars(ioc, buf, BUFSIZE, &len, NULL); if (len <= 0 || ret != G_IO_STATUS_NORMAL) return stderr_done(ioc); /* = return FALSE */ textbuf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview)); gtk_text_buffer_get_end_iter(textbuf, &enditer); /* Check if messages are in UTF8. If not, assume * they are in current locale and try to convert. * We assume we're getting the stream in a 1-byte * encoding here, ie. that we do not have cut-off * characters at the end of our buffer (=BAD) */ if (g_utf8_validate(buf,len,NULL)) { gtk_text_buffer_insert(textbuf, &enditer, buf, len); } else { const gchar *charset; gchar *utf8; (void) g_get_charset(&charset); utf8 = g_convert_with_fallback(data, len, "UTF-8", charset, NULL, NULL, NULL, NULL); if (utf8) { gtk_text_buffer_insert(textbuf, &enditer, utf8, len); g_free(utf8); } else { g_warning("Wget Message Output is not in UTF-8 nor in locale charset.\n"); } } /* A bit awkward, but better than nothing. Scroll text view to end */ gtk_text_buffer_get_end_iter(textbuf, &enditer); gtk_text_buffer_move_mark_by_name(textbuf, "insert", &enditer); gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(textview), gtk_text_buffer_get_mark(textbuf, "insert"), 0.0, FALSE, 0.0, 0.0); g_free(data); } if (cond & (G_IO_ERR | G_IO_HUP | G_IO_NVAL)) return stderr_done(ioc); /* = return FALSE */ return TRUE; } /***************************************************************** * * set_up_io_channel * * sets up callback to call whenever something interesting * happens on a pipe (ie. we get data, or wget output) * *****************************************************************/ static GIOChannel * set_up_io_channel (gint fd, GIOCondition cond, GIOFunc func, gpointer data) { GIOChannel *ioc; /* set up handler for data */ ioc = g_io_channel_unix_new (fd); /* Set IOChannel encoding to none to make * it fit for binary data */ g_io_channel_set_encoding (ioc, NULL, NULL); g_io_channel_set_buffered (ioc, FALSE); /* Tell the io channel to close the file descriptor * when the io channel gets destroyed */ g_io_channel_set_close_on_unref (ioc, TRUE); /* g_io_add_watch() adds its own reference, * which will be dropped when the watch source * is removed from the main loop (which happens * when we return FALSE from the callback) */ g_io_add_watch (ioc, cond, func, data); g_io_channel_unref (ioc); return ioc; } /***************************************************************** * * spawn_wget * * spawns wget and hooks up stdout/stderr of wget * *****************************************************************/ static GPid spawn_wget (const gchar *url) { GError *error = NULL; gchar **argv; GPid child_pid; gint stdout_fd; gint stderr_fd; /* It's global, so we can only spawn one wget at * a time the way we've set up our routines */ g_assert (databuf == NULL); /* Spawn wget and make it retrieve the given * URL. The data is outputted to stdout (so * it's piped to us directly rather than being * written to a file) */ argv = g_new (gchar *, 5); argv[0] = g_strdup ("wget"); /* program to call */ argv[1] = g_strdup ("-v"); /* argument passed to wget */ argv[2] = g_strdup ("--output-document=-"); /* make wget output data to stdout */ argv[3] = g_strdup (url); /* the URL to fetch */ argv[4] = NULL; /* end of argument list */ if (!g_spawn_async_with_pipes( NULL, /* use current working directory */ argv, /* the program we want to run and parameters for it */ NULL, /* use the environment variables that are set for the parent */ (GSpawnFlags) G_SPAWN_SEARCH_PATH /* look for wget in $PATH */ | G_SPAWN_DO_NOT_REAP_CHILD, /* we'll check the exit status ourself */ NULL, /* don't need a child setup function either */ NULL, /* and therefore no child setup func data argument */ &child_pid, /* where to store the child's PID */ NULL, /* don't need standard input (=> will be /dev/null) */ &stdout_fd, /* where to put wget's stdout file descriptor */ &stderr_fd, /* where to put wget's stderr file descriptor */ &error)) { g_warning ("g_spawn_async_with_pipes() failed: %s\n", error->message); g_strfreev (argv); g_error_free (error); error = NULL; return (GPid) 0; } /* Now use GIOChannels to monitor wget's stdout and stderr */ set_up_io_channel(stdout_fd, G_IO_IN|G_IO_PRI|G_IO_ERR|G_IO_HUP|G_IO_NVAL, iofunc_stdout, NULL); set_up_io_channel(stderr_fd, G_IO_IN|G_IO_PRI|G_IO_ERR|G_IO_HUP|G_IO_NVAL, iofunc_stderr, NULL); g_strfreev (argv); return (GPid) child_pid; } /***************************************************************** * * wget_exit_cb * * Our child process has exited. * *****************************************************************/ static void wget_exit_cb (GPid child_pid, gint status, gpointer your_user_data) { g_print ("Child PID: %lu exit status: %d\n", (gulong) child_pid, status); /* Not sure how the exit status works on win32. Unix code follows */ #ifdef G_OS_UNIX if (WIFEXITED (status)) /* Did child terminate in a normal way? */ { if (WEXITSTATUS (status) == EXIT_SUCCESS) { g_print ("Child PID: %lu exited normally without errors.\n", (gulong) child_pid); } else { g_print ("Child PID: %lu exited with an error (code %d).\n", (gulong) child_pid, WEXITSTATUS (status)); } } else if (WIFSIGNALED (status)) /* was it terminated by a signal */ { g_print ("Child PID: %lu was terminated by signal %d\n", (gulong) child_pid, WTERMSIG (status)); } else { g_print ("Child PID: %lu was terminated in some other way.\n", (gulong) child_pid); } #endif /* G_OS_UNIX */ g_spawn_close_pid (child_pid); /* does nothing on unix, needed on win32 */ } /***************************************************************** * * onButtonClick * * The button has clicked. Fetch the URL entered into * the entry. * *****************************************************************/ static void onButtonClick (GtkButton *button, gpointer entry) { gchar *url; GPid child_pid; /* Get URL from entry (we get a copy here) */ url = gtk_editable_get_chars(GTK_EDITABLE(entry), 0, -1); /* Check if entry is empty */ if (url == NULL || *url == 0x00) { g_warning("No URL specified!\n"); return; } child_pid = spawn_wget (url); if (child_pid != (GPid) 0) { g_print ("Child PID: %lu\n", (gulong) child_pid); gtk_widget_set_sensitive (GTK_WIDGET (button), FALSE); /* Watch the child, so we get the exit status */ g_child_watch_add (child_pid, wget_exit_cb, NULL); } g_free (url); } /***************************************************************** * * onEntryActivated * * Enter has been pressed in the entry field * * We could have used g_signal_connect_swapped as * well, and connected to onButtonClick, but this is * a bit clearer, even if awkward. * *****************************************************************/ static void onEntryActivated (GtkEntry *entry, gpointer button) { g_assert(GTK_IS_BUTTON(button)); if (GTK_WIDGET_SENSITIVE(button)) { onButtonClick (GTK_BUTTON(button), entry); } } /***************************************************************** * * create_window * * Sets up the window and its contents (entry, button, etc.) * *****************************************************************/ static void create_window (void) { GtkWidget *window, *entry, *hbox, *vbox, *scrollwin; window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_default_size(GTK_WINDOW(window), 300, 400); gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); g_signal_connect(window, "delete-event", (GCallback) gtk_main_quit, NULL); /* dirty*/ vbox = gtk_vbox_new(FALSE, 2); hbox = gtk_hbox_new(FALSE, 2); entry = gtk_entry_new(); gtk_entry_set_text(GTK_ENTRY(entry), "http://scentric.net/tutorial/treeview-tutorial.pdf"); button = gtk_button_new_with_label("Fetch URL using wget"); g_signal_connect(button, "clicked", (GCallback) onButtonClick, entry); gtk_box_pack_start(GTK_BOX(hbox), entry, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0); progressbar = gtk_progress_bar_new(); gtk_progress_bar_set_pulse_step(GTK_PROGRESS_BAR(progressbar), 0.01); gtk_box_pack_start(GTK_BOX(vbox), progressbar, FALSE, FALSE, 0); scrollwin = gtk_scrolled_window_new(NULL,NULL); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrollwin), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); textview = gtk_text_view_new(); gtk_text_view_set_editable(GTK_TEXT_VIEW(textview), FALSE); gtk_container_add(GTK_CONTAINER(scrollwin), textview); gtk_box_pack_start(GTK_BOX(vbox), scrollwin, TRUE, TRUE, 0); label = gtk_label_new(NULL); gtk_misc_set_alignment(GTK_MISC(label), 0.0, 0.5); gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0); gtk_container_add(GTK_CONTAINER(window), vbox); /* need to do this after button is valid */ g_signal_connect(entry, "activate", (GCallback) onEntryActivated, button); gtk_widget_show_all(window); } /***************************************************************** * * main * * Everything starts here... * *****************************************************************/ gint main (gint argc, gchar **argv) { gtk_init (&argc,&argv); create_window (); gtk_main (); return 0; }