Search code examples
rustgtkrust-rocket

Show Gtk GUI on HTTP request via Rocket


I want to show a Gtk Window upon a HTTP request to a Rocket server in my program.

Here's a MRE:

src/main.rs

use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Button};
use gtk4 as gtk;
use rocket::{get, launch, routes, Build, Rocket};

#[launch]
fn rocket() -> Rocket<Build> {
    rocket::build().mount("/", routes![do_get])
}

#[get("/")]
fn do_get() -> String {
    show_gui();
    "Gui shown!".to_string()
}

fn show_gui() {
    let application = Application::builder()
        .application_id("com.example.FirstGtkApp")
        .build();

    application.connect_activate(|app| {
        let window = ApplicationWindow::builder()
            .application(app)
            .title("First GTK Program")
            .default_width(350)
            .default_height(70)
            .build();

        let button = Button::with_label("Click me!");
        button.connect_clicked(|_| {
            eprintln!("Clicked!");
        });
        window.set_child(Some(&button));

        window.show();
    });

    application.run();
}

Cargo.toml

[package]
name = "gui"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
gtk4 = "0.6.6"
rocket = { version = "0.5.0-rc.3", features = ["json"] }

However, upon the second request, my program panics:

$ cargo run
   Compiling gui v0.1.0 (/home/neumann/gui)
    Finished dev [unoptimized + debuginfo] target(s) in 2.54s
     Running `target/debug/gui`
🔧 Configured for debug.
   >> address: 127.0.0.1
   >> port: 8000
   >> workers: 12
   >> max blocking threads: 512
   >> ident: Rocket
   >> IP header: X-Real-IP
   >> limits: bytes = 8KiB, data-form = 2MiB, file = 1MiB, form = 32KiB, json = 1MiB, msgpack = 1MiB, string = 8KiB
   >> temp dir: /tmp
   >> http/2: true
   >> keep-alive: 5s
   >> tls: disabled
   >> shutdown: ctrlc = true, force = true, signals = [SIGTERM], grace = 2s, mercy = 3s
   >> log level: normal
   >> cli colors: true
📬 Routes:
   >> (do_get) GET /
📡 Fairings:
   >> Shield (liftoff, response, singleton)
🛡️ Shield:
   >> X-Content-Type-Options: nosniff
   >> Permissions-Policy: interest-cohort=()
   >> X-Frame-Options: SAMEORIGIN
🚀 Rocket has launched from http://127.0.0.1:8000
GET / text/html:
   >> Matched: (do_get) GET /
   >> Outcome: Success
   >> Response succeeded.
GET / text/html:
   >> Matched: (do_get) GET /
thread 'rocket-worker-thread' panicked at 'Attempted to initialize GTK from two different threads.', /home/neumann/.cargo/registry/src/index.crates.io-6f17d22bba15001f/gtk4-0.6.6/src/rt.rs:95:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
   >> Handler do_get panicked.
   >> This is an application bug.
   >> A panic in Rust must be treated as an exceptional event.
   >> Panicking is not a suitable error handling mechanism.
   >> Unwinding, the result of a panic, is an expensive operation.
   >> Panics will degrade application performance.
   >> Instead of panicking, return `Option` and/or `Result`.
   >> Values of either type can be returned directly from handlers.
   >> A panic is treated as an internal server error.
   >> Outcome: Failure
   >> No 500 catcher registered. Using Rocket default.
   >> Response succeeded.

What is the correct way to resolve this issue?


Solution

  • Based on @Jmb's hint I came up with the following solution, that works for me:

    #![allow(clippy::let_underscore_untyped, clippy::no_effect_underscore_binding)]
    use gtk4::prelude::*;
    use gtk4::{Application, ApplicationWindow, Button};
    use rocket::{get, launch, routes, Build, Rocket, State};
    use std::sync::mpsc::{sync_channel, SyncSender};
    use std::thread;
    
    #[launch]
    fn rocket() -> Rocket<Build> {
        let sender = gtk_spawn();
        rocket::build().manage(sender).mount("/", routes![do_get])
    }
    
    #[allow(clippy::needless_pass_by_value)]
    #[get("/")]
    fn do_get(sender: &State<SyncSender<&'static str>>) -> String {
        sender.send("show").expect("cannot send to thread");
        "Gui shown!".to_string()
    }
    
    #[must_use]
    pub fn gtk_spawn() -> SyncSender<&'static str> {
        let (sender, receiver) = sync_channel::<&'static str>(32);
        thread::spawn(move || {
            while matches!(receiver.recv().expect("could not receive message"), "show") {
                show_gui();
            }
        });
        sender
    }
    
    fn show_gui() {
        let application = Application::builder()
            .application_id("com.example.FirstGtkApp")
            .build();
    
        application.connect_activate(|app| {
            let window = ApplicationWindow::builder()
                .application(app)
                .title("First GTK Program")
                .default_width(350)
                .default_height(70)
                .build();
    
            let button = Button::with_label("Click me!");
            button.connect_clicked(|_| {
                eprintln!("Clicked!");
            });
            window.set_child(Some(&button));
    
            window.show();
        });
    
        application.run();
    }