tokio: Dropping a File does not close it

Version

tokio v0.2.13 tokio-macros v0.2.5 rust 1.42.0-nightly

Platform

Linux 64-bit Debian, kernel 4.19.0-5-amd64

Description

I’m trying to write a file to disk, and then execute it. To do both, I’m using tokio::fs::File and tokio::process::Command and the default executor. Here’s my code:

use std::os::unix::fs::OpenOptionsExt;
use tokio::io::AsyncWriteExt;

#[tokio::main]
async fn main() {

    let mut std_options = std::fs::OpenOptions::new();
    std_options.create(true).truncate(true).write(true).mode(0o750);
    let tokio_options = tokio::fs::OpenOptions::from(std_options);

    let mut file = tokio_options.open("/tmp/foo.sh").await.unwrap();
    file.write_all("#!/bin/sh\necho hello".as_bytes()).await.unwrap();
    drop(file); // drop file to close it

    let status = tokio::process::Command::new("/tmp/foo.sh").status().await.unwrap(); // line 15
    println!("Status: {:?}", status);
}

The docs here say that dropping a File will close it (which is why you see me trying to do that in the above code). When I run this code, it sometimes works, but most of the time it fails with this error:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 26, kind: Other, message: "Text file busy" }', src/main.rs:15:18
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

I think this means that my program still /tmp/foo.sh open when tokio::process::Command tries to execute it. This is surprising to me, since the docs suggest that dropping a file is how you close it (and I did not see any explicit “close” methods).

Am I missing something? How can I safely close this file so that it can be used?

Thanks!

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 15 (8 by maintainers)

Commits related to this issue

Most upvoted comments

Hey,

So the short version is that you should flush the file to make sure that all pending operations are completed at a specific point in your code, so adding the following should make it work as expected:

file.write_all("#!/bin/sh\necho hello".as_bytes()).await.unwrap();
file.flush().await.unwrap(); // <- add this after writing

The slightly longer version is that all filesystem operations are performed on a separate threadpool (accessed through spawn_blocking). This keeps the file handle alive while there are pending operations in the background even though the tokio File instance you created is dropped. A way that we wait for these pending operations to be applied for the File instance, is via AsyncWriteExt::flush.

This does sound like a good documentation item to fix.

I am working on a bigger doc change that addresses this (among other things).