How to add a shutdown hook to a Rust program?
Asked Answered
A

1

10

I'm writing a Rust program which needs to save some data at the end of its execution, whatever happens.

In the Java world, I would do that with a shutdown hook. There is a crate, aptly called shutdown_hooks, but it seems only able to register extern "C" functions (and I'm not totally sure it will run on panic!(...)).

How can I implement a shutdown hook that triggers on normal exit as well on panic?

Anthology answered 9/9, 2019 at 20:9 Comment(1)
You should not be relying on a program terminating the way you want: the user might kill it, the computer might be powered off, etc. shutdown_hooks uses atexit, which is about as good as it gets on POSIX systems. It will run on most panics, but not on eg. segfaults.Hourihan
D
16

In the general case, it's impossible. Even ignoring effects from outside of the program (as mentioned by mcarton), whoever compiles the final binary can choose if a panic actually triggers stack unwinding or if it simply aborts the program. In the latter case, there's nothing you can do.

In the case of unwinding panic or normal exit, you can implement Drop and use the conventional aspects of RAII:

struct Cleanup;

impl Drop for Cleanup {
    fn drop(&mut self) {
        eprintln!("Doing some final cleanup");
    }
}

fn main() {
    let _cleanup = Cleanup;

    panic!("Oh no!");
}
thread 'main' panicked at 'Oh no!', src/main.rs:12:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
Doing some final cleanup

It appears that Java's shutdown hooks allow running multiple pieces of code in parallel in threads. You could do something similar with some small modifications:

use std::{
    sync::{Arc, Condvar, Mutex},
    thread,
};

#[derive(Debug, Default)]
struct Cleanup {
    hooks: Vec<thread::JoinHandle<()>>,
    run: Arc<Mutex<bool>>,
    go: Arc<Condvar>,
}

impl Cleanup {
    fn add(&mut self, f: impl FnOnce() + Send + 'static) {
        let run = self.run.clone();
        let go = self.go.clone();

        let t = thread::spawn(move || {
            let mut run = run.lock().unwrap();

            while !*run {
                run = go.wait(run).unwrap();
            }

            f();
        });
        self.hooks.push(t);
    }
}

impl Drop for Cleanup {
    fn drop(&mut self) {
        eprintln!("Starting final cleanup");

        *self.run.lock().unwrap() = true;
        self.go.notify_all();

        for h in self.hooks.drain(..) {
            h.join().unwrap();
        }

        eprintln!("Final cleanup complete");
    }
}

fn main() {
    let mut cleanup = Cleanup::default();

    cleanup.add(|| {
        eprintln!("Cleanup #1");
    });

    cleanup.add(|| {
        eprintln!("Cleanup #2");
    });

    panic!("Oh no!");
}

See also:

Dennard answered 9/9, 2019 at 20:20 Comment(2)
Such a clever solution. @Dennard is it okay if I use this method to block the main thread for user input at the end? Like "Press any key to exit". I just want to keep the console window open in case the user launched my application directly.Rice
"there's nothing you can do" you could trap SIGABRT, couldn't you?Culvert

© 2022 - 2024 — McMap. All rights reserved.