Works while debugging, doesn't work when run (also debug build)

Hi everyone.
This is exact port of the c app that uses onboard keyboard:

It is so exact that even replicates the strange behavior of the c app :rofl: :joy: :sweat_smile: , that is, it works as intended when debugging, but doesn't work when simply run (same build).
Could anyone take a look and perhaps figure out this very strange behavior?
Below is working example implemented by me in Rust.
Thank you in advance.
cargo.toml:

[package]
name = "rust_onboard"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
glib-sys = "0.15.10"
[dependencies.gtk]
version = "0.15.5"
features = ["v3_24"]
[dependencies.glib]
version = "0.15.10"
[dependencies.gio]
version = "0.15.11"

main.rs:

use glib::clone;
use glib::translate::ToGlibPtr;
use glib::{error::Error, Pid, SpawnFlags};
use glib_sys::{gpointer, GIOChannel, GIOStatus, GType};
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Button, Entry, Expander, Grid, Socket, Widget};
use std::rc::Rc;
use std::{env, path::Path};
use std::{
    io::Read,
    process::{Command, Stdio},
};

fn main() {
    let app = Application::builder()
        .application_id("org.example.FirstDesktopApp")
        .build();

    app.connect_activate(|app| {
        // We create the main window.
        let main_window = ApplicationWindow::builder()
            .application(app)
            .default_width(320)
            .default_height(200)
            .title("Hello, World!")
            .build();

        let entry: Entry = Entry::new();
        entry.set_hexpand(true);

        let socket: Socket = Socket::new();
        socket.set_size_request(500, 200);
        socket.set_hexpand(true);
        socket.set_vexpand(true);

        let expander: Expander = Expander::new(Some("Keyboard"));
        expander.add(&socket);

        let button: Button = Button::with_label("Get Keyboard");
        button.set_hexpand(true);
        button.connect_clicked(clone!(@weak socket => move |_| {
            add_plug(socket);
        }));

        let grid: Grid = Grid::new();
        main_window.add(&grid);

        grid.attach(&button, 0, 0, 1, 1);
        grid.attach(&entry, 0, 1, 1, 1);
        grid.attach(&expander, 0, 2, 1, 1);
        // Show the window.
        main_window.show_all();
    });

    app.run();
}

fn add_plug(socket: Socket) {
    println!("Add Plug");

    let working_directory = env::current_dir().unwrap();
    let argv = [
        Path::new("onboard"),
        Path::new("-e"),
        Path::new("-l"),
        Path::new("Phone"),
        Path::new("-t"),
        Path::new("ModelM"),
        Path::new("-s"),
        Path::new("400x300"),
    ];
    let environ = glib::environ();
    let envp: Vec<&Path> = environ.iter().map(Path::new).collect();
    let flags = SpawnFlags::DO_NOT_REAP_CHILD | SpawnFlags::SEARCH_PATH;
    let child_setup = None;
    let io: Result<(Pid, i32, i32, i32), Error> =
        glib::spawn_async_with_pipes(working_directory, &argv, &envp, flags, child_setup);
    match io {
        Ok(pid) => {
            println!(
                "Spawned with pipes: Pid: {:?}, input: {}, output: {}, error: {}",
                pid.0, pid.1, pid.2, pid.3
            );
            unsafe {
                let std_out_ch: *mut GIOChannel = glib_sys::g_io_channel_unix_new(pid.2);
                let condition: u32 = (glib::IOCondition::IN | glib::IOCondition::HUP).bits();
                let socket_ptr: *mut gtk::ffi::GtkSocket = socket.to_glib_none().0;
                let user_data: *mut std::ffi::c_void =
                    socket_ptr as *mut _ as *mut std::ffi::c_void;
                //glib_sys::g_io_add_watch(std_out_ch, condition, Some(watch_out_channel), user_data);
                glib_sys::g_io_add_watch_full(
                    std_out_ch,
                    1,
                    condition,
                    Some(watch_out_channel),
                    user_data,
                    None,
                );
            }
        }
        Err(e) => {
            eprintln!("io err:{}", e)
        }
    }
}

unsafe extern "C" fn watch_out_channel(channel: *mut GIOChannel, cond: u32, data: gpointer) -> i32 {
    if let Ok(window_id) = read_lines_from_channel(channel) {
        let filtered = window_id
            .chars()
            .filter(|d| {
                //println!("{d}");
                if d.is_ascii_digit() {
                    true
                } else {
                    false
                }
            })
            .collect::<String>();
        println!("window_id: {window_id}");
        let window_id = filtered.parse().unwrap();
        let gtk_socket: *mut gtk::ffi::GtkSocket = data as *mut _ as *mut gtk::ffi::GtkSocket;
        let socket: Socket = glib::translate::FromGlibPtrNone::from_glib_none(gtk_socket);
        socket.add_id(window_id);
    }
    1
}

use glib_sys::GError;
use std::{
    error,
    ffi::{CStr, CString},
    os::raw::c_char,
    ptr, slice, str,
};
unsafe fn into_message(error: *mut GError) -> String {
    assert!(!error.is_null());
    let message: &CStr = CStr::from_ptr((*error).message);
    let message: String = message.to_str().unwrap().to_owned();
    glib_sys::g_error_free(error);
    message
}

fn read_lines(filename: &str) -> Result<Vec<String>, Box<dyn error::Error>> {
    let filename: CString = CString::new(filename)?;
    let mut error: *mut GError = ptr::null_mut();
    let channel: *mut GIOChannel = unsafe {
        glib_sys::g_io_channel_new_file(filename.as_ptr(), b"r\0".as_ptr().cast(), &mut error)
    };
    if channel.is_null() {
        return Err(unsafe { into_message(error) }.into());
    }
    let mut lines: Vec<String> = Vec::new();
    loop {
        let mut line_raw: *mut c_char = ptr::null_mut();
        let mut length: usize = 0;
        let status: GIOStatus = unsafe {
            glib_sys::g_io_channel_read_line(
                channel,
                &mut line_raw,
                &mut length,
                ptr::null_mut(),
                &mut error,
            )
        };
        match status {
            glib_sys::G_IO_STATUS_ERROR => {
                unsafe { glib_sys::g_io_channel_unref(channel) };
                return Err(unsafe { into_message(error) }.into());
            }
            glib_sys::G_IO_STATUS_NORMAL => {
                let line_bytes: &[u8] = if length == 0 {
                    &[]
                } else {
                    unsafe { slice::from_raw_parts(line_raw.cast(), length) }
                };
                lines.push(str::from_utf8(line_bytes).unwrap().to_owned());
                unsafe { glib_sys::g_free(line_raw.cast()) };
            }
            glib_sys::G_IO_STATUS_EOF => break,
            glib_sys::G_IO_STATUS_AGAIN => {}
            _ => unreachable!(),
        }
    }
    unsafe { glib_sys::g_io_channel_unref(channel) };
    Ok(lines)
}
fn read_lines_from_channel(channel: *mut GIOChannel) -> Result<String, Box<dyn error::Error>> {
    let mut error: *mut GError = ptr::null_mut();

    if channel.is_null() {
        return Err(unsafe { into_message(error) }.into());
    }
    let mut window_id = String::new();
    /*loop */
    {
        let mut line_raw: *mut c_char = ptr::null_mut();
        let mut length: usize = 0;
        let status: GIOStatus = unsafe {
            glib_sys::g_io_channel_read_line(
                channel,
                &mut line_raw,
                &mut length,
                ptr::null_mut(),
                &mut error,
            )
        };
        match status {
            glib_sys::G_IO_STATUS_ERROR => {
                unsafe { glib_sys::g_io_channel_unref(channel) };
                return Err(unsafe { into_message(error) }.into());
            }
            glib_sys::G_IO_STATUS_NORMAL => {
                let line_bytes: &[u8] = if length == 0 {
                    &[]
                } else {
                    unsafe { slice::from_raw_parts(line_raw.cast(), length) }
                };
                window_id = str::from_utf8(line_bytes).unwrap().to_owned();
                // lines.push(str::from_utf8(line_bytes).unwrap().to_owned());
                unsafe { glib_sys::g_free(line_raw.cast()) };
            }
            //glib_sys::G_IO_STATUS_EOF => break,
            glib_sys::G_IO_STATUS_AGAIN => {}
            _ => unreachable!(),
        }
    }
    // unsafe { glib_sys::g_io_channel_unref(channel) };
    Ok(window_id)
}

[Update]
I actually had a very small progress in investigating the issue and it appears that the โ€œfocusโ€ is the problem.
If I start that application, switch to other and switch back to my app it works. But if I just run it and even I click on the window to โ€œgrab the focusโ€ it is simply not working.

So my question is, how to make the application loose focus and then regain it?

I'm not sure what platform you are running on, and thus whether this will help,

but debugging focus issues can be tricky without perturbing the issue, the only thing I can say that helps is, Even debugging on the console, and switching between the window and console will cause events to be fired etc.

  • Find a trackball, they have the handy lock mouse button so you don't have to hold the mouse button down.
  • run the debugger through an SSH session, and attach to the process via process id instead of starting the process in the debugger. possibly putting in a sleep or 'read a char from stdin' at a convenient point so you can start the debugger at some appropriate place if necessary.
  • From your description, It doesn't sound like an address space randomization issue which is usually my first guess when I hear that it works in the debugger.

I don't know if it is your case, but the less intuitive bug I've had with GTK was that some variables were dropped (by a GC in the binding I guess?), so I had to put a useless reference somewhere in the Rust code to avoid that.

For example:

fn build_ui(
    app: &gtk::Application,
    /* ... */
) {
    // ...
    let graph_gesture_drag = gtk::GestureDrag::new(&graph_viewport);
    graph_gesture_drag.set_touch_only(false);
    graph_gesture_drag.connect_drag_begin({/* ... */});
    graph_gesture_drag.connect_drag_update({/* ... */});
    graph_gesture_drag.connect_drag_end({/* ... */});
    // ...
    graph_area.connect_key_press_event({
        // ...
        move |_, event| {
            #[allow(clippy::no_effect, unused_must_use)]
            {
                &graph_gesture_drag; // avoid GC
            }
            //  ...
        }
    });
    // ...
}

(complete source code -- search for graph_gesture_drag)

Hi and thank you.
Tbh, I thought there isn't GC (garbage collector) involved anywhere in Rust.
Am I missing something?

Not in Rust, but maybe in GTK. And maybe it's not a GC but just reference counting or even something in the compiler, I'm not sure. But I just verified, in my program if you remove the &graph_gesture_drag reference, this object is removed and its events are never called.

@tuxmain GTK also doesn't have GC. Unfortunately as helpful as you try to be you'd send me on wild goose chase with your suggestion.
If you've read my update, I actually found out what the root cause of the issue is. The problem is that the window is not being seen by the system as an active window even when I press on it. I have to loose focus, than gain it in order for that app to work.
Why don't you try that code and see for yourself? It is just a one file and really small amount of code.

When the window opens, I click on "Get Keyboard", click on the text input, click on "> Keyboard", then the graphical keyboard appears and when I click on a key, the character is appended to the text input. If the text input hasn't focus, the characters don't appear. I don't need to do focus tricks between windows.

Is this the expected behavior?

Yes, that is the expected behavior. So you are saying that it works on your machine? Did you try the code?

Yes I compiled it on my machine. (ArchLinux, XFCE4, rust 1.63.0, gtk 3.24.34-1, onboard 1.4.1-7)

OK, thanks, in that case my window manager (kde) seems to be messing something.
Thank you very much for your time and help.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.