Rust, signals, and rowing machines

Recently I have started using Rust again. I had a brief interaction with it a few years ago and found it lacking (pre-1.0). This time around has been far better!

I started working on a program to interact with my rowing machine. It is a WaterRower with an S4 monitor. This comes with a serial interface on which the rower will spit out loads of raw data. The plan is to take that output and do fun things with it. Logging workouts, analyzing data, and possibly even a basic game for it.

I created a Rust project for interacting with the rowing machine. It is very basic right now, mostly helping me get my brain around the, admittedly ugly, Rust syntax. In v0.1.0, the idea is to have one thread for interacting with the serial device, one thread for displaying the data, and the main thread for keeping track of these other threads.

To initiate the connection with the waterrower and have it actually start sending out data over the serial port I have to send it a very specific line, at UTF-8 encoded string that reads “USB\r\n”. It will continue to spit out data until I send it another string, “EXIT\r\n”. If I do not send that last string, the rowers serial buffer will fill up and it will reset and crash, maybe not in that order. Either way, it is less that ideal.

The problem is, when I send Control+C or otherwise kill the program from Linux, it doesn’t properly exit. This is were signals come in. When I send ctrl+c, it is actually sending a signal, SIGINT. When killing the process, it sends SIGTERM. Ideally, I want to handle these signals gracefully inside my program. Unfortunately, there is no good way to handle signals in Rust from what I have seen.

The best way I have found involves a global bool. Here is a small snippet of the basic structure I used to handle signals. I have annotated the code, so consider it part of the post.

extern crate nix;

use nix::sys::signal;
use std::sync::atomic::{AtomicBool, Ordering, ATOMIC_BOOL_INIT};
use std::thread;
use std::time::Duration;

// define EXIT_NOW bool (ATOMIC_BOOL_INIT is false)
static EXIT_NOW: AtomicBool = ATOMIC_BOOL_INIT;

// define what we do when we receive a signal
extern fn early_exit(_: i32) {
    println!("Caught signal, exiting!");
    // set EXIT_NOW bool to true, Ordering::Relaxed);

fn main() {
    // define an action to take (the key here is 'signal::SigHandler::Handler(early_exit)'
    //    early_exit being the function we defined above
    let sig_action = signal::SigAction::new(signal::SigHandler::Handler(early_exit),
    // use sig_action for SIGINT
    unsafe { signal::sigaction(signal::SIGINT, &sig_action); }
    // use sig_action for SIGTERM
    unsafe { signal::sigaction(signal::SIGTERM, &sig_action); }

    // spawn a new thread
    let handle = thread::spawn(move || {
        // loop forever
        loop {
            // check if bool, EXIT_NOW, is true
            if EXIT_NOW.load(Ordering::Relaxed) {
                println!("Cleaning up");
            println!("Doing something...");
        } // end loop

    // block until thread ends (until user sends signal)

Notice how we had to declare the sigactions as unsafe? that is because it is “unsafe” in the Rust world. It allows me to do things that Rust cannot guarantee memory access is safe. Now, it is completely possible to use this to write code that does _not_ have any race conditions, but it does make it more difficult. Depending on what the thread is doing, you may need to have a special EXIT_NOW if statement in each thread you spawn to ensure a proper exit.

If anyone has a better way to handle signals in Rust, now or in the future, please let me know! I am not super happy about the way EXIT_NOW is declared and used, but it is safe enough. I am always looking for better ways though!

All in all, I am fairly happy with Rust so far. It hasn’t prevented me from doing anything and it has already stopped me from making a few mistakes that would have resulted in bugs due to race conditions that may have been very difficult to track down.

Leave a Reply

Your email address will not be published. Required fields are marked *