Search code examples
clinuxmultithreadinggtkgtk3

How can I update a section of a GTK+ GUI in a separate thread, which continuously reads data from another process?


I have a problem while coding a GTK+ GUI to manage other processes, under Linux. I'm definitely not an expert with GTK+, and I can't seem to solve this problem.

I'm trying to write a GTK+ application which should run other processes (in particular, an iPerf - a network measurement program - client and an iPerf server, which are managed using system() and popen()/pclose(), depending on the buttons which are clicked by the users.

There are some buttons related to launching a client and two buttons to start and stop a server, which call their respective callbacks.

The server start button, in particular, calls a callback which is responsible for starting a thread, which should read data from the server (quite asynchronously) and update a section of the GUI accordingly, while the GUI should be responsive to perform other operations (e.g. start a client).

In particular, iPerf is set to output new data every 1s, and each data lies on each line returned by iPerf, every second.

I tried reading the data from the server using popen().

If I launch the serverParserIdle() function (reported below), from within the GTK+ callback, using gdk_threads_add_idle(), it works, but with two big problems preventing the program from working properly:

1) iPerf output is buffered by popen() and the data is not parsed in real-time, as the program should do

2) The serverParserIdle() thread locks the GUI, and I cannot do other operations at the same time, such as running a client, which is something I need to do

Trying to solve (2), I tried changing with gdk_threads_add_idle() with gdk_threads_add_timeout(1000,...). In this case the GUI is no more locked, but popen is returning 0 and the server is not launched. Do you know why?

What can I do to solve all the problems listed above?

This is the serverParserIdle() function mentioned before:

static gboolean serverParserIdle(gpointer data) {
    FILE *iperfFp;
    char linebuf[STRSIZE_LINEBUF];
    double goodput, final_goodput;
    char unit_letter;
    int total_datagrams, prev_total_datagrams=-1;
    struct parser_data *parser_data_struct=data;
    gchar *gput_label_str=NULL, *final_gput_label_str=NULL;
    char first_char;


    iperfFp=popen(parser_data_struct->cmd,"r"); //parser_data_struct->cmd contains a string containing the command to launch the iperf server "iperf -s -u -i 1 ..."

    if(!iperfFp) {
        // We enter here if gdk_threads_add_timeout(1000,...) is used to call serverParserIdle()
        return FALSE;
    }

    while(fgets(linebuf,sizeof(linebuf),iperfFp)!=NULL) {
        sscanf(linebuf,"%c %*s %*s %*f %*s %*f %*s %lf %c%*s %*f %*s %*s %d %*s",&first_char,&goodput,&unit_letter,&total_datagrams); // Parse useful data on this line

        if(first_char!='[' || (unit_letter!='K' && unit_letter!='M')) {
            // This is just to discrimate the useful lines
            continue;
        }

        if(unit_letter=='K') {
            goodput=goodput/1000;
        }

        // This is again a way to distinguish the last line of a client-server session from all the other lines
        if(prev_total_datagrams!=-1 && total_datagrams>prev_total_datagrams*2) {
            if(final_gput_label_str) {
                g_free(final_gput_label_str);
            }
            // Update final goodput value in the GUI
            final_goodput=goodput;
            prev_total_datagrams=-1;
            final_gput_label_str=g_strdup_printf("<b><span font=\"70\" foreground=\"blue\">%.2f</span></b>",goodput);
            gtk_label_set_text(GTK_LABEL(parser_data_struct->gput_labels.final_gput_info_label),final_gput_label_str);
        } else {
            if(gput_label_str) {
                g_free(gput_label_str);
            }
            prev_total_datagrams=total_datagrams;

            // Update current goodput value in the GUI (every 1s only when a client is being connected to the server)
            gput_label_str=g_strdup_printf("<b><span font=\"70\" foreground=\"#018729\">%.2f</span></b>",goodput);
            gtk_label_set_text(GTK_LABEL(parser_data_struct->gput_labels.gput_info_label),gput_label_str);
        }

        //fflush(iperfFp); <- tried flushing, but it does not work
    }

    pclose(iperfFp);

    g_free(gput_label_str);
    g_free(final_gput_label_str);


    return FALSE;
}

gdk_threads_add_idle() or gdk_threads_add_timeout() are actually called from a callback (start_server()), which is assigned to a button in main() using:

g_signal_connect(button,"clicked",G_CALLBACK(start_server),&(data));

Thank you very much in advance.


Solution

  • I finally solved my problem by following pan-mroku's suggestion, i.e. by using IO Channels on a pipe opened with popen.

    This is the relevant code which is, in the end, able to read information from the server as new lines, with new data, are printed to stdout by the server itself, and update the GTK+ GUI accordingly.

    #include <gtk/gtk.h>
    #include <errno.h>
    // ...
    
    
    static gboolean serverParser(GIOChannel *source, GIOCondition condition, gpointer data) {
        gchar *linebuf; gsize strsize_linebuf;
        GIOStatus opstatus;
        int scan_retval=0;
        // ...
    
        opstatus=g_io_channel_read_line(source,&linebuf,&strsize_linebuf,NULL,NULL);
        if(opstatus==G_IO_STATUS_NORMAL && strsize_linebuf!=0) {
            scan_retval=sscanf(linebuf,"%c %*s %f%*[- *]%f %*s %*f %*s %lf %c%*s %*f %*s %*f%*[/ *]%d %*s",&field_1,&field_2,&field_3,&field_4,&field_5,&field_6);
    
            if(scan_retval==6) {
                // Work with the parsed server data, line by line
            }
        }
    
        // ...
    
        g_free(linebuf);
        return TRUE;
    }
    
    
    
    static void start_server(GtkWidget *widget, gpointer data) {
        // ...
        FILE *iperfFp;
        int iperfFd;
        GIOChannel *iperfIOchannel;
    
        // ...
        // Start server using stdbuf to get a line buffered output
        iperfFp=popen("stdbuf -o L iperf -s -u","r");
    
        if(!iperfFp) {
            g_print("Error in launching the server. errno = %d\n",errno);
            return;
        }
    
        iperfFd=fileno(iperfFp);
    
        iperfIOchannel=g_io_channel_unix_new(iperfFd);
        g_io_channel_set_flags(iperfIOchannel,G_IO_FLAG_NONBLOCK,NULL);
        g_io_channel_set_line_term(iperfIOchannel,NULL,-1);
        g_io_add_watch(iperfIOchannel,G_IO_IN,serverParser,&(data_struct->parser_pointers));
    
        // ...
    }
    
    // ...
    

    When the start button is clicked, the start_server callback is invoked, which starts the iPerf server (but the same could be done for any other external process) with popen and configures a new IO Channel. Then, every time a new line is generated by the server itself, serverParser is called to parse all the relevant data.

    I had to start the external iPerf process by invoking stdbuf first (with the argument -o L), in order to get a line buffered output and have serverParser called for each line generated by that process.