Dropping a struct behind a Mutex lock

I used to be able to drop stdout with drop(stdout), but since I'm using stdout in two different threads, I had to put stdout behind a Mutex lock.

use termion::input::TermRead;
use termion::raw::IntoRawMode;
use termion::{event::Key, raw::RawTerminal};

use std::io::{stdin, Write};
use std::sync::{Arc, Mutex};
use std::time::Duration;

struct RawStdout {
    buffer: RawTerminal<std::io::Stdout>,
}

impl RawStdout {
    fn new() -> std::io::Result<Self> {
        let buffer = std::io::stdout().into_raw_mode()?;
        Ok(Self { buffer })
    }
}

impl Write for RawStdout {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        self.buffer.write(buf)
    }

    fn flush(&mut self) -> std::io::Result<()> {
        self.buffer.flush()
    }
}

fn main() {
    let mut stdout = RawStdout::new().unwrap();

    stdout.flush().unwrap();

    let stdout_mutex = Arc::new(Mutex::new(stdout));
    let stdout_clone = Arc::clone(&stdout_mutex);

    std::thread::spawn(move || {
        let stdout_clone = Arc::clone(&stdout_mutex);

        for c in stdin().keys() {
            match c.unwrap() {
                Key::Ctrl('c') => {
                    println!("Ctrl + C\r\n");

                    let stdout = stdout_clone.lock().unwrap();

                    println!("The main thread released the lock!");

                    // Drop `stdout` to exit raw mode in the terminal.
                    drop(stdout);

                    std::process::exit(0);
                }
                _ => {}
            }
        }
    });

    let mut stdout = stdout_clone.lock().unwrap();

    writeln!(stdout, "Command is executing...").unwrap();
    stdout.flush().unwrap();
    std::thread::sleep(Duration::from_millis(5000));
}

Now, the problem is that when I do drop(stdout);, I'm not dropping stdout but the Mutex lock. How can I drop stdout like I used to?

Rust Explorer

You can use Mutex<Option<Stdout>>, then overwrite the option with None.

9 Likes

it seams a XY problem to me, your example code is just trying to emulate a signal handler? in that case, you can just use the signal-hook crate, or if you only cares about SIGINT (a.k.a. Ctrl-C), you can use ctrlc, which is also much easier.

    // this example uses an atomic bool and main thread polls the value,
    // can also use other sync primitives, like park and unpark the main thread;
    // call `process:exit()` from other thread or an signal handler not recomendded
    static QUIT: AtomicBool = AtomicBool::new(false);
    ctrlc::set_handler(|| {
        QUIT.store(true, Ordering::SeqCst);
    }).expect("Error setting Ctrl-C handler");

    println!("Waiting for Ctrl-C...");
    while !QUIT.load(Ordering::SeqCst) {
        // just yield a little bit CPU
        sleep(Duration::from_millis(5));
    }
    println!("Got it! Exiting...");
}

also, use stdout for keyboard input is a strong smell to me! if you are writing an interactive application, maybe use a higher level tui event loop library, instead of directly read the raw terminal input?

std::process:exit() should be used with caution, as it doens't unwind the stack, all desctructors are skipped so the side effect (such as set the terminal to raw mode in your example) will still persist after the program exits.

1 Like

Thanks for the suggestion.

Are you sure ctrlc::set_handler will work in raw mode? I tried it before, and I remember it didn't detect Ctrl + C. Raw mode isn't supposed to react to normal keyboard combinations.

no, SIGINT is not delivered in raw mode cause the tty driver to translate key strokes to signals is bypassed in raw mode.

what I mean was, you can avoid using raw mode if you only need to install a signal hook, since cooked mode is easier to deal with after all (e.g. in raw mode, println!() will not work as expected, since it only write LF but no CR).

if you are writing an interactive application, I suggest to use a higher level library that handles the event loop for you, do you don't need to deal with the raw input yourself.

however, I was not saying you should never use raw tty, I was just suggesting alternative ways. if you need to handle raw tty events, that's fine, you just need to be more careful.

PS: I believe Stdout is internally synchronized with some locking mechanism, so you don't need to use mutex in the first place, just get an Stdout handle for each thread, this way you don't need to use Option to drop the handle. you just need to synchronize your threads when you enter/exit raw mode, so your screen isn't garbled.

thread::spawn(move || {
    let raw_tty = std::io::stdout().into_raw_mode();
    // do your stuff in raw mode
});
// main thread can use stdout without mutex
let stdout = std::io::stdout();
// synchronize with other thread, make sure raw mode is entered
stdout.write(b"\x1b[1;31mhello raw tty\r\n");
1 Like

Mm, every interesting solution. Thanks, Ill give it a try.

To be honest, this Mutex thing is adding a lot of complexity to my code.