Multi-thread basics: how to avoid message loops consuming entities needed further on?

Being a multi thread newbie, I want to create a solution that makes it possible to use my fancy macro keyboard (Blackmagic Speed Entry, originally created for controlling Davinci Resolve Video NLE) to control my music notation software (Steinberg Dorico) via its websocket Remote API.

The solution below does the basics:

  • it sets up a messaging channel for app-internal communication
  • it sets up the websocket connection to the software, and performs necessary handshaking
  • it sets up the macro keyboard connection, and uses its listener callbacks to broadcast messages with info about what keys are pressed.
  • it sets up an message reciever loop to deal with the messages, and uses the websocket to send commands back to the software

It listens to messages FROM the macro keyboard) and sends the corresponding commands TO the websocket/software.

// set up a messaging channel using the following AppMessage enum
#[derive(Debug)]
enum AppMessage {    
    SpeedKey(Key, bool),
}
// create a channel for sending messages to the websocket
let (app_message_sender, app_message_reciever) = mpsc::channel::<AppMessage>();

//--------------------------------------------------------------------
// setup websocket connection to Dorico notation software
let mut dorico_web_socket = connect("ws://127.0.0.1:4560").unwrap().0;
// ... and do some handshaking with the software

// <-- insert thread with listener loop for messages from the dorico_web_socket here?

//--------------------------------------------------------------------
// set up the Blackmagic Speed Editor macro keyboard HidDevice connection
let mut speed_editor_kbd = bmd_speededitor::new().unwrap();

// clone the app_message_sender for use in the on_key callback
let sender_kbd = app_message_sender.clone();
// add a onkey listener to the macro keyboard
speed_editor_kbd.on_key(move |key, down| {
    // use the cloned app_message_sender to send a message with key and down state
    sender_kbd.send(AppMessage::SpeedKey(key, down)).expect("On key error");
    Ok(())
});
// kickoff the speed editor keyboard
let handle_speed_editor = thread::spawn(move || {
    speed_editor_kbd.run().expect("Expected Speed Editor to run");
});

//--------------------------------------------------------------------
// listen to messages from the app_message_reciever
for app_message in app_message_reciever {
    match app_message {
        AppMessage::SpeedKey(key, down) => {            
            // match the current pressed key state
            match key {
                Key::Cam1 => {
                    // send a message via the websocket to the software
                    &dorico_web_socket.send(Message::Text("Dorico-command-123").into()));
                }
                Key::Cam2 => {
                    // 
                }                
            }
        }        
    }
}

However, I also want to listen to messages FROM the websocket/sofware and deal with these.

My problem is that I havent found a way to set up a listener loop for websocket messages without consuming away the websocket itself - thus making it unavailable when it's needed later on in the program. If I plug in the following snippet at the <-- insert thread with listener loop above, the &dorico_web_socket is consumed and can not be used later.

let handle_dorico_socket = thread::spawn(move || loop {      
    let msg = &dorico_web_socket.read().unwrap(); // websocket is consumed here       
    match msg {
        Message::Text(message_from_dorico) => {
            // broadcast an AppMessage or something...
        }
        _ => {}
    }
});

What is the best way to deal with this scenario?

what websocket library are you using? I've never seen an API where a read() operation will consume the socket. or do you mean something else that I misunderstood?

EDIT:

read the post again, and I think you are looking for a way to share the socket between threads. still, it depends on what library you are using.

if the implementation supports full-duplex communication, you can usually use Arc to share the socket between the sending thread and receiving thread. some libraries implements a split() method so you can use the sender in on thread and receiver in another.

The socket is not consumed, you're calling it multiple times in a loop. It's moved into the thread. Anyway, you need to clone the socket somehow, depends on the crate. Some crates allow to clone the socket right away, others require to wrap the socket into an Arc or Arc<Mutex>.

I'd recommend use async instead of threading.

Ah, sorry, forgot to mention that I'm using Tungstenite for the websocket.
Will try to find a library that supports .split(). Thanks!

If anybody is interrested, @nerditation's tips led me to a solution that seems to work fine:

I'm now using Websockets crate, version 0.3.0, wich has a split() method:

#[tokio::main]
async fn main() {
    ...
    let dorico_websocket = WebSocket::connect("ws://127.0.0.1:4560").await.unwrap();
    let (mut dorico_ws_read, mut dorico_ws_write) = dorico_websocket.split();

Then, I can use dorico_ws_read to listen to websocket messages in a tokio:spawn thread:

   let handle_dorico_socket = tokio::spawn(async move {
        loop {
            let frame = dorico_ws_read.receive().await.unwrap();
            // do whatever needed
        }
    });

Because of the split, dorico_ws_write is free to be used later on in the solution.

Thanks, @nerditation!