Command stdout - borrowed value does not live long enough

It's probably really simple, but I can't find a solution to my problem...

async fn watch_nvidia(tx_udp: Sender<String>) {
    let mut last_value = "..";
    loop {
        let result = Command::new("nvidia-smi")
            .args(["--query-gpu=temperature.gpu","--format=csv,noheader"])
            .output().unwrap();
        let output = str::from_utf8(&result.stdout).unwrap().trim();
        if output != last_value {
            let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
            let metric = format!(",zone=nvidia xps15manjaro.temperature={output} {current_time}\n");
            tx_udp.send(metric).await.ok();
            last_value = output;
        }
        println!("{output}");
        sleep(Duration::from_millis(400)).await;
    }
}
error[E0597]: `result.stdout` does not live long enough
  --> src/main.rs:36:37
   |
36 |         let output = str::from_utf8(&result.stdout).unwrap().trim();
   |                                     ^^^^^^^^^^^^^^ borrowed value does not live long enough
37 |         if output != last_value {
   |                      ---------- borrow later used here
...
50 |     }
   |     - `result.stdout` dropped here while still borrowed

Is there a friendly rust expert here that can help me out while I'll take a good night's sleep in the meantime? thank you! :slight_smile:

Your problem is that you're inside a loop, so result gets dropped before the loop starts over, but last_value will be read by the next loop iteration. Since result is dropped you can't assign a value that borrows from it to last_value.

The simple fix is to just make last_value an owned value String instead of a string slice

use std::{
    process::Command,
    time::{Duration, SystemTime, UNIX_EPOCH},
};
use tokio::{sync::mpsc::Sender, time::sleep};

async fn watch_nvidia(tx_udp: Sender<String>) {
    let mut last_value = String::from("..");
    loop {
        let result = Command::new("nvidia-smi")
            .args(["--query-gpu=temperature.gpu", "--format=csv,noheader"])
            .output()
            .unwrap();

        let output = String::from_utf8(result.stdout).unwrap();

        if output.trim() != last_value.trim() {
            let current_time = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap()
                .as_nanos();
            let metric = format!(",zone=nvidia xps15manjaro.temperature={output} {current_time}\n");
            tx_udp.send(metric).await.ok();
            println!("{output}");
            last_value = output;
        } else {
            // Duplicating the println to avoid cloning the output.
            println!("{output}");
        }

        sleep(Duration::from_millis(400)).await;
    }
}

String::from_utf8 works just like std::str::from_utf8, but it takes ownership of the Vec so you don't have to do any extra copies of the string data to get an owned value, which is nice!

I just have the trim as part of the comparison in this version, but depending on how you expect the output to be, it may be better to do a copy of the trimmed content instead of doing the trim again on every loop iteration for last_value

2 Likes

Thank you, I've got close to that before but I didn't think that the trim was the culprit.

I'm only interested in the trimmed value, but the trim method only takes an immutable reference and returns a &str slice, which again can only live as long as the untrimmed String.

But provided with the idea to create Strings from &str slices to get something I own (thank you!) I've managed to come up with this solution:

async fn watch_nvidia(tx_udp: Sender<String>) {
    let mut last_value = String::from("..");
    loop {
        let result = Command::new("nvidia-smi")
            .args(["--query-gpu=temperature.gpu","--format=csv,noheader"])
            .output().unwrap();
        let trimmed = String::from(str::from_utf8(&result.stdout).unwrap().trim());
        println!("{trimmed}");
        if last_value != trimmed {
            let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
            let metric = format!(",zone=nvidia xps15manjaro.temperature={trimmed} {current_time}\n");
            tx_udp.send(metric).await.ok();
            last_value = trimmed;
        }
        sleep(Duration::from_millis(400)).await;
    }
}

To be clear, trim wasn't the root issue. You can remove the trims from the original and it will still fail. The problem is how long the decoded &str lives.

Your solution is good though! Just wanted to clarify that point in case someone else lands on this question.

1 Like

yes of course, I meant I've already tried to replace the str::from_utf8(&result.stdout) with String::from_utf8(result.stdout) last night, but that failed because my .trim() behind it without storing the String somewhere it lives long enough gave me a very similar error.

Btw., there's only one trim to remove from the original, not multiples :wink:

What I still dislike about "solutions like these", is that (if I understood it correctly) we're sacrificing a bit of efficiency (=copying the data to a new String object) rather then just holding on to the memory location where that commands stdout output got written to initially - "just" to satisfy the borrow checker.

1 Like

Well it's not "just" to satisfy the borrow checker, because you're dropping the data backing the string. If you wanted to avoid copies you could do something like store the backing Vec outside the loop and use a raw string slice to store the trimmed portion so the borrow checker doesn't complain. But copying is definitely easier unless you have really strict memory requirements.

2 Likes