Как я могу обновить раздел графического интерфейса GTK + в отдельном потоке, который непрерывно считывает данные из другого процесса? - PullRequest
2 голосов
/ 14 мая 2019

У меня проблема при написании графического интерфейса GTK + для управления другими процессами в Linux. Я определенно не эксперт в GTK +, и я не могу решить эту проблему.

Я пытаюсь написать приложение GTK +, которое должно запускать другие процессы (в частности, iPerf - программа измерения сети - клиент и сервер iPerf, управление которыми осуществляется с помощью system() и popen() / pclose() , в зависимости от кнопок, которые нажимают пользователи.

Есть несколько кнопок, связанных с запуском клиента, и две кнопки для запуска и остановки сервера, которые вызывают их соответствующие обратные вызовы.

Кнопка запуска сервера, в частности, вызывает обратный вызов, который отвечает за запуск потока, который должен считывать данные с сервера (довольно асинхронно) и соответствующим образом обновлять часть графического интерфейса, в то время как графический интерфейс должен реагировать на выполнение. другие операции (например, запуск клиента).

В частности, iPerf настроен на вывод новых данных каждые 1 с, и каждая информация находится в каждой строке, возвращаемой iPerf, каждую секунду.

Я попытался прочитать данные с сервера, используя popen().

Если я запускаю функцию serverParserIdle() (об этом сообщается ниже) из-за обратного вызова GTK +, используя gdk_threads_add_idle(), она работает, но с двумя большими проблемами, мешающими нормальной работе программы:

1) Выход iPerf буферизируется с помощью popen(), и данные не анализируются в режиме реального времени, как должна делать программа

2) Поток serverParserIdle() блокирует графический интерфейс, и я не могу одновременно выполнять другие операции, такие как запуск клиента, что мне нужно сделать

Пытаясь решить (2), я попытался изменить с gdk_threads_add_idle() на gdk_threads_add_timeout(1000,...). В этом случае графический интерфейс больше не заблокирован, но popen возвращает 0, а сервер не запускается. Ты знаешь почему?

Что я могу сделать, чтобы решить все проблемы, перечисленные выше?

Это функция serverParserIdle(), упомянутая ранее:

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() или gdk_threads_add_timeout() фактически вызываются из обратного вызова (start_server()), который назначается кнопке в main() с использованием:

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

Заранее большое спасибо.

Ответы [ 2 ]

1 голос
/ 19 мая 2019

Я наконец решил свою проблему, следуя совету pan-mroku , т.е. используя Каналы ввода-вывода на канале, открытом с помощью popen .

Этосоответствующий код, который, в конце концов, способен считывать информацию с сервера в виде новых строк с новыми данными, которые печатаются на stdout самим сервером, и соответственно обновлять графический интерфейс GTK +.

#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));

    // ...
}

// ...

При нажатии кнопки запуска вызывается обратный вызов start_server, который запускает сервер iPerf (но то же самое можно было бы сделать для любого другого внешнего процесса) с popen и настраивает новый канал ввода-вывода.Затем каждый раз, когда сам сервер генерирует новую строку, serverParser вызывается для анализа всех соответствующих данных.

Мне пришлось запустить внешний процесс iPerf, сначала вызвав stdbuf (с аргументом-o L), для получения буферизованного вывода строки и вызова serverParser для каждой строки, сгенерированной этим процессом.

1 голос
/ 18 мая 2019

Вот пример на Perl на случай, если кому-то будет интересно.Это просто показывает основной принцип того, как выполнять асинхронные операции в цикле событий GTK:

#! /usr/bin/env perl
use feature qw(say);
use strict;
use warnings;

use Glib 'TRUE', 'FALSE';
use Gtk3 -init;
use AnyEvent;  # Important: load AnyEvent after Glib!
use AnyEvent::Subprocess;

use constant {
    GTK_STYLE_PROVIDER_PRIORITY_USER => 800,
};

my $window = Gtk3::Window->new( 'toplevel' );
my $grid1 = Gtk3::Grid->new();
$window->add( $grid1 );
my $frame1 = Gtk3::Frame->new('Output');
$frame1->set_size_request(800,600);
$grid1->attach($frame1, 0,0,1,1);
my $scrolled_window = Gtk3::ScrolledWindow->new();
$scrolled_window->set_border_width(5);
$scrolled_window->set_policy('automatic','automatic');
my $textview = Gtk3::TextView->new();
my $buffer = $textview->get_buffer();
$buffer->set_text ("Hello, this is some text\nHello world\n");
$textview->set_wrap_mode('none');
$textview->set_editable(FALSE);
$textview->set_cursor_visible(FALSE);
set_widget_property( $textview, 'font-size', '18px' );
my $bg_color = Gtk3::Gdk::RGBA::parse( "#411934" );
$textview->override_background_color('normal', $bg_color);
my $color = Gtk3::Gdk::RGBA::parse( "#e9e5e8" );
$textview->override_color('normal', $color);
$textview->set_monospace(TRUE);

$scrolled_window->add($textview);
$frame1->add($scrolled_window);
$window->set_border_width(5);
$window->set_default_size( 600, 400 );
$window->set_position('center_always');
$window->show_all();
setup_background_command( $buffer );  # start background command
my $condvar = AnyEvent->condvar;
$window->signal_connect( destroy  => sub { $condvar->send } );
my $done = $condvar->recv;  # enter main loop...

sub setup_background_command {
    my ( $buffer ) = @_;

    my $job = AnyEvent::Subprocess->new(
        delegates     => [ 'StandardHandles', 'CompletionCondvar' ],
        code          => sub { exec 'unbuffer', 'myscript.pl' }
    );
    my $run = $job->run;
    $run->delegate('stdout')->handle->on_read(
        sub {
            my ( $handle ) = @_;
            my $line = $handle->rbuf;
            chomp $line;
            my $iter = $buffer->get_end_iter();
            $buffer->insert( $iter, $line . "\n" );
            $handle->rbuf = ""; # clear buffer
        }
    );
}

sub set_widget_property {
    my ( $widget, $prop, $value ) = @_;

    my $context = $widget->get_style_context();
    my $cls_name = $prop . '_class';
    $context->add_class( $cls_name );
    my $provider = Gtk3::CssProvider->new();
    my $css = sprintf ".%s {%s: %s;}", $cls_name, $prop, $value;
    $provider->load_from_data( $css );
    $context->add_provider($provider, GTK_STYLE_PROVIDER_PRIORITY_USER);
}

Здесь командой для асинхронного выполнения в цикле событий GTK является сценарий myscript.pl:

#! /usr/bin/env perl
use feature qw(say);
use strict;
use warnings;

#STDOUT->autoflush(1);
sleep 1;
say "data 1";
sleep 1;
say "data 2";
sleep 1;
say "data 3";

Обратите внимание, что сценарий можно сделать небуферизованным, раскомментировав строку с autoflush(1).Но в целом мы должны предположить, что мы не можем изменить внутреннюю часть команды, поэтому я использовал unbuffer для запуска сценария.

...