WebsocketConnection using libsoup-2.4 is sometimes blocking GTK ui thread and prevents opening the main window

252 Views Asked by At

I have a simple GTK app written in Vala with a websocket connection (using libsoup-2.4). Problem: my app sometimes freezes at startup (each ~10th startups) and doesn't show the gui window at all because of blocking somewhere in libsoup on socket connection.

Vala 0.40.25 with

  • gtk+-3.0
  • glib-2.0
  • gobject-2.0
  • libsoup-2.4

Here the code:

using Gtk;

class WebsocketConnection {
    private  Soup.WebsocketConnection websocket_connection;
    public signal void ws_message(int type, string message);
    public signal void connection_succeeded();
    public signal void connection_established();
    public signal void connection_failed();
    public signal void connection_disengaged();

    private string host;

    public WebsocketConnection(string host) {
        this.host = host;
    }

    private static string decode_bytes(Bytes byt, int n) {
        return (string)byt.get_data();
    }

    public void init_connection_for(string host) {
        MainLoop loop = new MainLoop();
        var socket_client = new Soup.Session();

        string url = "ws://%s:8080/".printf(host);
        message(@"connect to $url");
        var websocket_message = new Soup.Message("GET", url);
        socket_client.websocket_connect_async.begin(websocket_message, null, null, null, (obj, res) => {
            try {
                websocket_connection = socket_client.websocket_connect_async.end(res);
                message("Connected!");

                connection_succeeded();
                if (websocket_connection != null) {
                    websocket_connection.message.connect((type, m_message) => {
                        ws_message(type, decode_bytes(m_message, m_message.length));
                    });
                    websocket_connection.closed.connect(() => {
                        message("Connection closed");
                        connection_disengaged();
                    });
                }
            } catch (Error e) {
                message("Remote error: " + e.message + " " + e.code.to_string());
                connection_failed();
                loop.quit();
            }
            loop.quit();
        });

        loop.run();
    }
}


class Main : Gtk.Application {

    public Main() {
        Object(
                application_id: "com.github.syfds.websocket-libsoup-vala-example",
                flags : ApplicationFlags.FLAGS_NONE
        );
    }

    protected override void activate() {

        var window = new ApplicationWindow(this);
        window.title = "Hello, World!";
        window.border_width = 10;
        window.window_position = WindowPosition.CENTER;
        window.set_default_size(350, 70);
        window.destroy.connect(Gtk.main_quit);

        var grid  = new Grid();
        grid.orientation = Orientation.VERTICAL;
        grid.column_spacing = 5;
        grid.row_spacing = 5;
        var main_label = new Label("...");
        var websocket_host_input = new Entry();
        var send_message_btn = new Button.with_label("Connect");

        var websocket_host = "192.168.1.252";
        var connection = new WebsocketConnection(websocket_host);

        connection.connection_succeeded.connect(() => {
            message("Connection succeeded");
            main_label.set_text("Connection succeeded");
        });
        connection.connection_failed.connect(() => {
            message("Connection failed");
        });

        connection.ws_message.connect((type, msg) => {
            message("message received " + msg);
        });

        connection.init_connection_for(websocket_host);
        grid.add(main_label);
        window.add(grid);
        window.show_all();
    }

    public static int main(string[] args) {
        var app = new Main();
        return app.run(args);
    }
}

if I connect to the process over gdb I see following picture:

(gdb) info thr
  Id   Target Id         Frame 
* 1    Thread 0x7f38e6cbbac0 (LWP 29885) "com.github.syfd" 0x00007f38e53098f6 in __libc_recv (
    fd=14, buf=0x55890440bc00, len=1024, flags=0) at ../sysdeps/unix/sysv/linux/recv.c:28
  2    Thread 0x7f38d5d1c700 (LWP 29886) "gmain" 0x00007f38e52fbcb9 in __GI___poll (
    fds=0x558903d13b40, nfds=2, timeout=-1) at ../sysdeps/unix/sysv/linux/poll.c:29
  3    Thread 0x7f38d551b700 (LWP 29887) "gdbus" 0x00007f38e52fbcb9 in __GI___poll (
    fds=0x558903d30760, nfds=2, timeout=-1) at ../sysdeps/unix/sysv/linux/poll.c:29
  4    Thread 0x7f38cffff700 (LWP 29888) "dconf worker" 0x00007f38e52fbcb9 in __GI___poll (
    fds=0x558903f6ccb0, nfds=1, timeout=-1) at ../sysdeps/unix/sysv/linux/poll.c:29
(gdb) bt
#0  0x00007f38e53098f6 in __libc_recv (fd=14, buf=0x55890440bc00, len=1024, flags=0)
    at ../sysdeps/unix/sysv/linux/recv.c:28
#1  0x00007f38e5eb54f4 in  () at /usr/lib/x86_64-linux-gnu/libgio-2.0.so.0
#2  0x00007f38e5667558 in  () at /usr/lib/x86_64-linux-gnu/libsoup-2.4.so.1
#3  0x00007f38e59173a5 in g_main_context_dispatch () at /usr/lib/x86_64-linux-gnu/libglib-2.0.so.0
#4  0x00007f38e5917770 in  () at /usr/lib/x86_64-linux-gnu/libglib-2.0.so.0
#5  0x00007f38e59177fc in g_main_context_iteration () at /usr/lib/x86_64-linux-gnu/libglib-2.0.so.0
#6  0x00007f38e5ed8f3d in g_application_run () at /usr/lib/x86_64-linux-gnu/libgio-2.0.so.0
#7  0x0000558902d69e8f in main_main (args=0x7ffeb2b775f8, args_length1=1)
    at /home/sergej/workspace/websocket-libsoup-vala-example-v2/src/Main.vala:121
#8  0x0000558902d69ed2 in main (argc=1, argv=0x7ffeb2b775f8)
    at /home/sergej/workspace/websocket-libsoup-vala-example-v2/src/Main.vala:119

It looks like __libc_recv at ../sysdeps/unix/sysv/linux/recv.c:28 is blocking here but I can't understand why and how to do the connection on non-blocking way.

I appreciate any help/hints how to solve the issue with blocked UI or how to create non-blocking websocket connection with libsoup.

1

There are 1 best solutions below

0
On

You're using async methods, but it needs a bit of work. Make your init_connection_for method async:

public async void init_connection_for(string host) { ... }

then yield to the call to make the websocket connection:

var websocket_connection = yield socket_client.websocket_connect_async(websocket_message, null, null, null);

Then you call your function to set things up at the beginning before it then yields and gets called back by the asynchronous connection:

connection.init_connection_for.begin(websocket_host);

You then also need to compile with GIO, --pkg gio-2.0, for the async methods.

This will get you moving forwards, but there is a lot more that can be done to simplify and re-architect your code. You don't need the extra MainLoop for example.

This is a diff of the changes to help you progress:

--- original.vala   2021-08-08 15:59:41.757378207 +0100
+++ modifed.vala    2021-08-08 17:18:58.680837130 +0100
@@ -18,35 +18,33 @@
         return (string)byt.get_data();
     }
 
-    public void init_connection_for(string host) {
+    public async void init_connection_for(string host) {
         MainLoop loop = new MainLoop();
         var socket_client = new Soup.Session();
 
         string url = "ws://%s:8080/".printf(host);
         message(@"connect to $url");
         var websocket_message = new Soup.Message("GET", url);
-        socket_client.websocket_connect_async.begin(websocket_message, null, null, null, (obj, res) => {
-            try {
-                websocket_connection = socket_client.websocket_connect_async.end(res);
-                message("Connected!");
-
-                connection_succeeded();
-                if (websocket_connection != null) {
-                    websocket_connection.message.connect((type, m_message) => {
-                        ws_message(type, decode_bytes(m_message, m_message.length));
-                    });
-                    websocket_connection.closed.connect(() => {
-                        message("Connection closed");
-                        connection_disengaged();
-                    });
-                }
-            } catch (Error e) {
-                message("Remote error: " + e.message + " " + e.code.to_string());
-                connection_failed();
-                loop.quit();
+        var websocket_connection = yield socket_client.websocket_connect_async(websocket_message, null, null, null);
+        try {
+            message("Connected!");
+
+            connection_succeeded();
+            if (websocket_connection != null) {
+                websocket_connection.message.connect((type, m_message) => {
+                    ws_message(type, decode_bytes(m_message, m_message.length));
+                });
+                websocket_connection.closed.connect(() => {
+                    message("Connection closed");
+                    connection_disengaged();
+                });
             }
+        } catch (Error e) {
+            message("Remote error: " + e.message + " " + e.code.to_string());
+            connection_failed();
             loop.quit();
-        });
+        }
+        loop.quit();
 
         loop.run();
     }
@@ -94,7 +92,7 @@
             message("message received " + msg);
         });
 
-        connection.init_connection_for(websocket_host);
+        connection.init_connection_for.begin(websocket_host);
         grid.add(main_label);
         window.add(grid);
         window.show_all();