Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dd: open stdout from file descriptor when possible #4542

Closed

Conversation

jfinkels
Copy link
Collaborator

(Complements pull request #4189.)

Open stdout using its file descriptor so that a dd skip=N command in a subprocess does not close stdout.

For example, before this commit, multiple instances of dd writing to stdout and appearing in a single command line could incorrectly result in an empty stdout for each instance of dd after the first:

$ printf "abcde\n" | (dd bs=1 skip=1 count=1 && dd bs=1 skip=1)  2> /dev/null
b

After this commit, each dd process writes to the file descriptor referring to stdout:

$ printf "abcde\n" | (dd bs=1 skip=1 count=1 && dd bs=1 skip=1)  2> /dev/null
bde

Comment on lines 963 to 1173
None if is_stdout_redirected_to_seekable_file() => {
Output::new_file(Path::new(&stdout_canonicalized()), &settings)?
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this workaround is now handled more appropriately in the new_stdout() case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the seeking is different now, isn't it? Because StdoutFile copies 0's instead of seeking. Also does it matter that the StdoutFile is only available on unix, while this match expression works on all platforms?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jfinkels ping ? :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the seeking is different now, isn't it? Because StdoutFile copies 0's instead of seeking.

Hmm, yes I guess this removal was premature. It seems like there should be a way to unify these two code paths in a cleaner way, but maybe I'll just postpone that until a later pull request. For example, maybe the decision about whether the destination is seekable could move inside the new_stdout() constructor.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh actually, I forgot---if I leave this branch in, then the new test case fails:

$ printf "abcde\n" | (dd bs=1 skip=1 count=1 && dd bs=1 skip=1)  2> /dev/null
bde

(this comes from the GNU test suite). For reference, we added the if is_stdout_redirected_to_seekable_file() in pull request #3880.

Let me see if there's a way to reconcile the two.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just updated this branch to keep the None if is_stdout_redirected_to_seekable_file() => { branch. There seems to be an issue with the new test case I added, but I'm not sure what the problem is.

When I run the test case from the command line I get the expected behavior:

$ printf "abcde\n" | (./target/debug/dd bs=1 skip=1 count=1; ./target/debug/dd bs=1 skip=1) 2> /dev/null
bde

but when I run cargo test I get a failure:

$ cargo test test_dd::test_multiple_processes_writing_stdout
[...]
---- test_dd::test_multiple_processes_writing_stdout stdout ----
run: /bin/sh -c /home/jeffrey/src/coreutils/target/debug/coreutils printf 'abcde
' | ( /home/jeffrey/src/coreutils/target/debug/coreutils dd bs=1 skip=1 count=1; /home/jeffrey/src/coreutils/target/debug/coreutils dd bs=1 skip=1 ) 2> /dev/null
thread 'test_dd::test_multiple_processes_writing_stdout' panicked at 'assertion failed: `(left == right)`

Diff < left / right > :
<de
>bde

Copy link
Member

@tertsdiepraam tertsdiepraam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good apart from the comment above, which is more a question than a concern

@jfinkels
Copy link
Collaborator Author

jfinkels commented May 7, 2023 via email

@jfinkels jfinkels force-pushed the dd-stdout-from-file-descriptor branch from fc000ea to b613dce Compare May 7, 2023 18:35
@github-actions
Copy link

github-actions bot commented May 7, 2023

GNU testsuite comparison:

Congrats! The gnu test tests/tail-2/inotify-dir-recreate is no longer failing!

@github-actions
Copy link

GNU testsuite comparison:

Congrats! The gnu test tests/dd/nocache is no longer failing!
GNU test failed: tests/dd/nocache_eof. tests/dd/nocache_eof is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/tail-2/inotify-dir-recreate. tests/tail-2/inotify-dir-recreate is passing on 'main'. Maybe you have to rebase?

@sylvestre
Copy link
Contributor

Failed with:

running 1 test
run: /bin/sh -c /home/runner/work/coreutils/coreutils/target/debug/coreutils printf 'abcde
' | ( /home/runner/work/coreutils/coreutils/target/debug/coreutils dd bs=1 skip=1 count=1; /home/runner/work/coreutils/coreutils/target/debug/coreutils dd bs=1 skip=1 ) 2> /dev/null
test test_dd::test_multiple_processes_writing_stdout ... FAILED

failures:

failures:
    test_dd::test_multiple_processes_writing_stdout

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 2676 filtered out; finished in 0.08s

@jfinkels jfinkels force-pushed the dd-stdout-from-file-descriptor branch from 454e6da to fad41c2 Compare July 3, 2023 01:14
@jfinkels
Copy link
Collaborator Author

jfinkels commented Jul 3, 2023

Yeah I'm stuck here unfortunately, I don't know enough to diagnose the issue. It works as expected when I run the command manually on the command line:

$ printf "abcde\n" | (./target/debug/dd bs=1 skip=1 count=1; ./target/debug/dd bs=1 skip=1) 2> /dev/null
bde

but the same fails when running cargo test. Mentioned originally in a comment here: #4542 (comment)

@github-actions
Copy link

github-actions bot commented Jul 3, 2023

GNU testsuite comparison:

Skipping an intermittent issue tests/tail-2/inotify-dir-recreate

@tertsdiepraam
Copy link
Member

tertsdiepraam commented Jul 3, 2023

I've narrowed this down to UCommand and probably the way we deal with stdout there. It works fine with Command from the standard library.

Here's a theory: we pipe the stdout to a file, what if that file is not opened in append mode, but truncated on every new command that writes to stdout?

Update: it's a bit simpler. This gives de with uutils and bde with GNU:

/bin/sh -c "printf 'abcde\n' | ( dd bs=1 skip=1 count=1; dd bs=1 skip=1 ) 2> /dev/null > a.txt"

So possibly we're being too eager with trying to seek to the start of the file?

Update 2: I think treating the implementation of treating stdout as a file directly is wrong, because the seek, truncate and append defaults are different for piping to stdout. This diff fixes the behaviour for stdout (but obviously break the implementation for normal files). Does that give you enough info to fix it?

@@ -693,7 +693,7 @@ impl<'a> Output<'a> {
             opts.write(true)
                 .create(!cflags.nocreat)
                 .create_new(cflags.excl)
-                .append(oflags.append);
+                .append(true);
 
             #[cfg(any(target_os = "linux", target_os = "android"))]
             if let Some(libc_flags) = make_linux_oflags(oflags) {
@@ -714,9 +714,6 @@ impl<'a> Output<'a> {
         // suppress the error by calling `Result::ok()`. This matches
         // the behavior of GNU `dd` when given the command-line
         // argument `of=/dev/null`.
-        if !settings.oconv.notrunc {
-            dst.set_len(settings.seek).ok();
-        }
         let density = if settings.oconv.sparse {
             Density::Sparse
         } else {

Open stdout using its file descriptor so that a `dd skip=N` command in
a subprocess does not close stdout.

For example, before this commit, multiple instances of `dd` writing to
stdout and appearing in a single command line could incorrectly result
in an empty stdout for each instance of `dd` after the first:

    $ printf "abcde\n" | (dd bs=1 skip=1 count=1 && dd bs=1 skip=1)  2> /dev/null
    b

After this commit, each `dd` process writes to the file descriptor
referring to stdout:

    $ printf "abcde\n" | (dd bs=1 skip=1 count=1 && dd bs=1 skip=1)  2> /dev/null
    bde
@sylvestre sylvestre force-pushed the dd-stdout-from-file-descriptor branch from fad41c2 to c1ba7c9 Compare September 24, 2023 08:00
@sylvestre
Copy link
Contributor

@jfinkels still interested in finishing this?

@jfinkels jfinkels linked an issue Nov 27, 2023 that may be closed by this pull request
@jfinkels
Copy link
Collaborator Author

I've narrowed this down to UCommand and probably the way we deal with stdout there. It works fine with Command from the standard library.

I'll update the test to use the standard library Command as it seems a bit more predictable for this test case.

Update 2: I think treating the implementation of treating stdout as a file directly is wrong, because the seek, truncate and append defaults are different for piping to stdout. This diff fixes the behaviour for stdout (but obviously break the implementation for normal files). Does that give you enough info to fix it?

Yes, this is helpful. I'll try to adapt that idea to support all the necessary cases.

@sylvestre
Copy link
Contributor

seems that it is stuck - closing to remove it from the list
please reopen if you want to work on it again

@sylvestre sylvestre closed this Dec 25, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

dd fails on tests/dd/not-rewound.sh
3 participants