diff options
author | Andrew Walbran <qwandor@google.com> | 2021-05-04 14:32:38 +0000 |
---|---|---|
committer | Andrew Walbran <qwandor@google.com> | 2021-05-19 11:36:54 +0000 |
commit | 9d6ae1aa286fa6788a71f3072b0c6ba33ee321d8 (patch) | |
tree | 025ddd060f0898ea511a83862896dbd7bf7f7795 | |
parent | 419ee4b117b9188216b22a0e586c54ae2e9292e4 (diff) | |
download | command-fds-9d6ae1aa286fa6788a71f3072b0c6ba33ee321d8.tar.gz |
Import command-fds crate.
Bug: 188443660
Test: mm
Change-Id: Ie9fe9d316d3aa5f73c922f616dcbaf6177dbb3e7
-rw-r--r-- | .cargo_vcs_info.json | 5 | ||||
-rw-r--r-- | .github/workflows/rust.yml | 61 | ||||
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Android.bp | 32 | ||||
-rw-r--r-- | CONTRIBUTING.md | 29 | ||||
-rw-r--r-- | Cargo.toml | 27 | ||||
-rw-r--r-- | Cargo.toml.orig | 14 | ||||
-rw-r--r-- | LICENSE | 201 | ||||
-rw-r--r-- | METADATA | 19 | ||||
-rw-r--r-- | MODULE_LICENSE_APACHE2 | 0 | ||||
-rw-r--r-- | OWNERS | 1 | ||||
-rw-r--r-- | README.md | 49 | ||||
-rw-r--r-- | examples/spawn.rs | 75 | ||||
-rw-r--r-- | patches/Android.bp.patch | 15 | ||||
-rw-r--r-- | src/lib.rs | 334 | ||||
-rw-r--r-- | testdata/file1.txt | 1 | ||||
-rw-r--r-- | testdata/file2.txt | 1 |
17 files changed, 866 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..0ecc3ba --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,5 @@ +{ + "git": { + "sha1": "21ffd14511b405853cd2468d6e8f7f9d17135154" + } +} diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..3d98a16 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,61 @@ +name: Rust + +on: + push: + branches: [main] + pull_request: + +env: + CARGO_TERM_COLOR: always + grcov-version: 0.8.0 + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Build + run: cargo build + - name: Run tests + run: cargo test + - name: Run clippy + uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features + + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Format Rust code + run: cargo fmt --all -- --check + + coverage: + runs-on: ubuntu-latest + env: + RUSTC_BOOTSTRAP: 1 + steps: + - uses: actions/checkout@v2 + - name: Install dependencies + run: sudo apt-get install libdbus-1-dev + - name: Install grcov + run: curl -L https://github.com/mozilla/grcov/releases/latest/download/grcov-linux-x86_64.tar.bz2 | tar jxf - + - name: Install llvm-tools + run: rustup component add llvm-tools-preview + - name: Build for coverage + run: cargo build --all-features + env: + RUSTFLAGS: "-Zinstrument-coverage" + - name: Run tests with coverage + run: cargo test --all-features + env: + RUSTFLAGS: "-Zinstrument-coverage" + LLVM_PROFILE_FILE: "test-coverage-%p-%m.profraw" + - name: Convert coverage + run: ./grcov . -s . --binary-path target/debug/ -t lcov --branch --ignore-not-existing -o target/debug/lcov.info + - name: Upload coverage to codecov.io + uses: codecov/codecov-action@v1 + with: + directory: ./target/debug + fail_ci_if_error: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..fea27bd --- /dev/null +++ b/Android.bp @@ -0,0 +1,32 @@ +// This file is generated by cargo2android.py --run --device --dependencies --patch=patches/Android.bp.patch. +// Do not modify this file as changes will be overridden on upgrade. + + + +rust_library { + name: "libcommand_fds", + host_supported: true, + crate_name: "command_fds", + srcs: ["src/lib.rs"], + edition: "2018", + rustlibs: [ + "libnix", + "libthiserror", + ], + apex_available: [ + "//apex_available:platform", + "com.android.virt", + ], +} + +// dependent_library ["feature_list"] +// bitflags-1.2.1 "default" +// cfg-if-1.0.0 +// libc-0.2.94 "default,extra_traits,std" +// nix-0.20.0 +// proc-macro2-1.0.26 "default,proc-macro" +// quote-1.0.9 "default,proc-macro" +// syn-1.0.72 "clone-impls,default,derive,parsing,printing,proc-macro,quote" +// thiserror-1.0.24 +// thiserror-impl-1.0.24 +// unicode-xid-0.2.2 "default" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..22b241c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,29 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement (CLA). You (or your employer) retain the copyright to your +contribution; this simply gives us permission to use and redistribute your +contributions as part of the project. Head over to +<https://cla.developers.google.com/> to see your current agreements on file or +to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Community Guidelines + +This project follows +[Google's Open Source Community Guidelines](https://opensource.google/conduct/). diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bcc6791 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies +# +# If you believe there's an error in this file please file an +# issue against the rust-lang/cargo repository. If you're +# editing this file be aware that the upstream Cargo.toml +# will likely look very different (and much more reasonable) + +[package] +edition = "2018" +name = "command-fds" +version = "0.1.0" +authors = ["Andrew Walbran <qwandor@google.com>"] +description = "A library for passing arbitrary file descriptors when spawning child processes." +keywords = ["command", "process", "child", "subprocess", "fd"] +categories = ["os::unix-apis"] +license = "Apache-2.0" +repository = "https://github.com/google/command-fds/" +[dependencies.nix] +version = "0.20.0" + +[dependencies.thiserror] +version = "1.0.24" diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..ea39b35 --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,14 @@ +[package] +name = "command-fds" +version = "0.1.0" +edition = "2018" +authors = ["Andrew Walbran <qwandor@google.com>"] +license = "Apache-2.0" +description = "A library for passing arbitrary file descriptors when spawning child processes." +repository = "https://github.com/google/command-fds/" +keywords = ["command", "process", "child", "subprocess", "fd"] +categories = ["os::unix-apis"] + +[dependencies] +nix = "0.20.0" +thiserror = "1.0.24" @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..dde0db4 --- /dev/null +++ b/METADATA @@ -0,0 +1,19 @@ +name: "command-fds" +description: "A library for passing arbitrary file descriptors when spawning child processes." +third_party { + url { + type: HOMEPAGE + value: "https://crates.io/crates/command-fds" + } + url { + type: ARCHIVE + value: "https://static.crates.io/crates/command-fds/command-fds-0.1.0.crate" + } + version: "0.1.0" + license_type: NOTICE + last_upgrade_date { + year: 2021 + month: 5 + day: 4 + } +} diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MODULE_LICENSE_APACHE2 @@ -0,0 +1 @@ +include platform/prebuilts/rust:/OWNERS diff --git a/README.md b/README.md new file mode 100644 index 0000000..896c2ab --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# command-fds + +[![crates.io page](https://img.shields.io/crates/v/command-fds.svg)](https://crates.io/crates/command-fds) +[![docs.rs page](https://docs.rs/command-fds/badge.svg)](https://docs.rs/command-fds) + +A library for passing arbitrary file descriptors when spawning child processes. + +## Example + +```rust +use command_fds::{CommandFdExt, FdMapping}; +use std::fs::File; +use std::os::unix::io::AsRawFd; +use std::process::Command; + +// Open a file. +let file = File::open("Cargo.toml").unwrap(); + +// Prepare to run `ls -l /proc/self/fd` with some FDs mapped. +let mut command = Command::new("ls"); +command.arg("-l").arg("/proc/self/fd"); +command + .fd_mappings(vec![ + // Map `file` as FD 3 in the child process. + FdMapping { + parent_fd: file.as_raw_fd(), + child_fd: 3, + }, + // Map this process's stdin as FD 5 in the child process. + FdMapping { + parent_fd: 0, + child_fd: 5, + }, + ]) + .unwrap(); + +// Spawn the child process. +let mut child = command.spawn().unwrap(); +child.wait().unwrap(); +``` + +## License + +Licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). + +## Contributing + +If you want to contribute to the project, see details of +[how we accept contributions](CONTRIBUTING.md). diff --git a/examples/spawn.rs b/examples/spawn.rs new file mode 100644 index 0000000..b399dc7 --- /dev/null +++ b/examples/spawn.rs @@ -0,0 +1,75 @@ +// Copyright 2021, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use command_fds::{CommandFdExt, FdMapping}; +use std::fs::{read_dir, read_link, File}; +use std::os::unix::io::AsRawFd; +use std::os::unix::process::CommandExt; +use std::process::Command; +use std::thread::sleep; +use std::time::Duration; + +/// Print out a list of all open file descriptors. +fn list_fds() { + let dir = read_dir("/proc/self/fd").unwrap(); + for entry in dir { + let entry = entry.unwrap(); + let target = read_link(entry.path()).unwrap(); + println!("{:?} {:?}", entry, target); + } +} + +fn main() { + list_fds(); + + // Open a file. + let file = File::open("Cargo.toml").unwrap(); + println!("File: {:?}", file); + list_fds(); + + // Prepare to run `ls -l /proc/self/fd` with some FDs mapped. + let mut command = Command::new("ls"); + command.arg("-l").arg("/proc/self/fd"); + command + .fd_mappings(vec![ + // Map `file` as FD 3 in the child process. + FdMapping { + parent_fd: file.as_raw_fd(), + child_fd: 3, + }, + // Map this process's stdin as FD 5 in the child process. + FdMapping { + parent_fd: 0, + child_fd: 5, + }, + ]) + .unwrap(); + unsafe { + command.pre_exec(move || { + println!("pre_exec"); + list_fds(); + Ok(()) + }); + } + + // Spawn the child process. + println!("Spawning command"); + let mut child = command.spawn().unwrap(); + sleep(Duration::from_millis(100)); + println!("Spawned"); + list_fds(); + + println!("Waiting for command"); + println!("{:?}", child.wait().unwrap()); +} diff --git a/patches/Android.bp.patch b/patches/Android.bp.patch new file mode 100644 index 0000000..94c1fcc --- /dev/null +++ b/patches/Android.bp.patch @@ -0,0 +1,15 @@ +diff --git a/Android.bp b/Android.bp +index 0a57129..a6ff574 100644 +--- a/Android.bp ++++ b/Android.bp +@@ -39,6 +39,10 @@ rust_library { + "libnix", + "libthiserror", + ], ++ apex_available: [ ++ "//apex_available:platform", ++ "com.android.virt", ++ ], + } + + // dependent_library ["feature_list"] diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8ce1071 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,334 @@ +// Copyright 2021, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A library for passing arbitrary file descriptors when spawning child processes. +//! +//! # Example +//! +//! ```rust +//! use command_fds::{CommandFdExt, FdMapping}; +//! use std::fs::File; +//! use std::os::unix::io::AsRawFd; +//! use std::process::Command; +//! +//! // Open a file. +//! let file = File::open("Cargo.toml").unwrap(); +//! +//! // Prepare to run `ls -l /proc/self/fd` with some FDs mapped. +//! let mut command = Command::new("ls"); +//! command.arg("-l").arg("/proc/self/fd"); +//! command +//! .fd_mappings(vec![ +//! // Map `file` as FD 3 in the child process. +//! FdMapping { +//! parent_fd: file.as_raw_fd(), +//! child_fd: 3, +//! }, +//! // Map this process's stdin as FD 5 in the child process. +//! FdMapping { +//! parent_fd: 0, +//! child_fd: 5, +//! }, +//! ]) +//! .unwrap(); +//! +//! // Spawn the child process. +//! let mut child = command.spawn().unwrap(); +//! child.wait().unwrap(); +//! ``` + +use nix::fcntl::{fcntl, FcntlArg}; +use nix::unistd::dup2; +use std::cmp::max; +use std::io::{self, ErrorKind}; +use std::os::unix::io::RawFd; +use std::os::unix::process::CommandExt; +use std::process::Command; +use thiserror::Error; + +/// A mapping from a file descriptor in the parent to a file descriptor in the child, to be applied +/// when spawning a child process. +/// +/// The parent_fd must be kept open until after the child is spawned. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FdMapping { + pub parent_fd: RawFd, + pub child_fd: RawFd, +} + +/// Error setting up FD mappings, because there were two or more mappings for the same child FD. +#[derive(Copy, Clone, Debug, Eq, Error, PartialEq)] +#[error("Two or more mappings for the same child FD")] +pub struct FdMappingCollision; + +/// Extension to add file descriptor mappings to a [`Command`]. +pub trait CommandFdExt { + /// Adds the given set of file descriptor to the command. + /// + /// Calling this more than once on the same command may result in unexpected behaviour. + fn fd_mappings(&mut self, mappings: Vec<FdMapping>) -> Result<(), FdMappingCollision>; +} + +impl CommandFdExt for Command { + fn fd_mappings(&mut self, mappings: Vec<FdMapping>) -> Result<(), FdMappingCollision> { + // Validate that there are no conflicting mappings to the same child FD. + let mut child_fds: Vec<RawFd> = mappings.iter().map(|mapping| mapping.child_fd).collect(); + child_fds.sort_unstable(); + child_fds.dedup(); + if child_fds.len() != mappings.len() { + return Err(FdMappingCollision); + } + + // Register the callback to apply the mappings after forking but before execing. + unsafe { + self.pre_exec(move || map_fds(&mappings)); + } + + Ok(()) + } +} + +fn map_fds(mappings: &[FdMapping]) -> io::Result<()> { + if mappings.is_empty() { + // No need to do anything, and finding first_unused_fd would fail. + return Ok(()); + } + + // Find the first FD which is higher than any parent or child FD in the mapping, so we can + // safely use it and higher FDs as temporary FDs. There may be other files open with these FDs, + // so we still need to ensure we don't conflict with them. + let first_safe_fd = mappings + .iter() + .map(|mapping| max(mapping.parent_fd, mapping.child_fd)) + .max() + .unwrap() + + 1; + + // If any parent FDs conflict with child FDs, then first duplicate them to a temporary FD which + // is clear of either range. + let child_fds: Vec<RawFd> = mappings.iter().map(|mapping| mapping.child_fd).collect(); + let mappings = mappings + .iter() + .map(|mapping| { + Ok(if child_fds.contains(&mapping.parent_fd) { + let temporary_fd = + fcntl(mapping.parent_fd, FcntlArg::F_DUPFD_CLOEXEC(first_safe_fd))?; + FdMapping { + parent_fd: temporary_fd, + child_fd: mapping.child_fd, + } + } else { + mapping.to_owned() + }) + }) + .collect::<nix::Result<Vec<_>>>() + .map_err(nix_to_io_error)?; + + // Now we can actually duplicate FDs to the desired child FDs. + for mapping in mappings { + // This closes child_fd if it is already open as something else, and clears the FD_CLOEXEC + // flag on child_fd. + dup2(mapping.parent_fd, mapping.child_fd).map_err(nix_to_io_error)?; + } + + Ok(()) +} + +/// Convert a [`nix::Error`] to a [`std::io::Error`]. +fn nix_to_io_error(error: nix::Error) -> io::Error { + if let nix::Error::Sys(errno) = error { + io::Error::from_raw_os_error(errno as i32) + } else { + io::Error::new(ErrorKind::Other, error) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nix::unistd::close; + use std::collections::HashSet; + use std::fs::{read_dir, File}; + use std::os::unix::io::AsRawFd; + use std::process::Output; + use std::str; + use std::sync::Once; + + static SETUP: Once = Once::new(); + + #[test] + fn conflicting_mappings() { + setup(); + + let mut command = Command::new("ls"); + + // The same mapping can't be included twice. + assert_eq!( + command.fd_mappings(vec![ + FdMapping { + child_fd: 4, + parent_fd: 5, + }, + FdMapping { + child_fd: 4, + parent_fd: 5, + }, + ]), + Err(FdMappingCollision) + ); + + // Mapping two different FDs to the same FD isn't allowed either. + assert_eq!( + command.fd_mappings(vec![ + FdMapping { + child_fd: 4, + parent_fd: 5, + }, + FdMapping { + child_fd: 4, + parent_fd: 6, + }, + ]), + Err(FdMappingCollision) + ); + } + + #[test] + fn no_mappings() { + setup(); + + let mut command = Command::new("ls"); + command.arg("/proc/self/fd"); + + assert_eq!(command.fd_mappings(vec![]), Ok(())); + + let output = command.output().unwrap(); + expect_fds(&output, &[0, 1, 2, 3], 0); + } + + #[test] + fn one_mapping() { + setup(); + + let mut command = Command::new("ls"); + command.arg("/proc/self/fd"); + + let file = File::open("testdata/file1.txt").unwrap(); + // Map the file an otherwise unused FD. + assert_eq!( + command.fd_mappings(vec![FdMapping { + parent_fd: file.as_raw_fd(), + child_fd: 5, + },]), + Ok(()) + ); + + let output = command.output().unwrap(); + expect_fds(&output, &[0, 1, 2, 3, 5], 0); + } + + #[test] + fn swap_mappings() { + setup(); + + let mut command = Command::new("ls"); + command.arg("/proc/self/fd"); + + let file1 = File::open("testdata/file1.txt").unwrap(); + let file2 = File::open("testdata/file2.txt").unwrap(); + let fd1 = file1.as_raw_fd(); + let fd2 = file2.as_raw_fd(); + // Map files to each other's FDs, to ensure that the temporary FD logic works. + assert_eq!( + command.fd_mappings(vec![ + FdMapping { + parent_fd: fd1, + child_fd: fd2, + }, + FdMapping { + parent_fd: fd2, + child_fd: fd1, + }, + ]), + Ok(()) + ); + + let output = command.output().unwrap(); + // Expect one more Fd for the /proc/self/fd directory. We can't predict what number it will + // be assigned, because 3 might or might not be taken already by fd1 or fd2. + expect_fds(&output, &[0, 1, 2, fd1, fd2], 1); + } + + #[test] + fn map_stdin() { + setup(); + + let mut command = Command::new("cat"); + + let file = File::open("testdata/file1.txt").unwrap(); + // Map the file to stdin. + assert_eq!( + command.fd_mappings(vec![FdMapping { + parent_fd: file.as_raw_fd(), + child_fd: 0, + },]), + Ok(()) + ); + + let output = command.output().unwrap(); + assert!(output.status.success()); + assert_eq!(output.stdout, b"test 1"); + } + + /// Parse the output of ls into a set of filenames + fn parse_ls_output(output: &[u8]) -> HashSet<String> { + str::from_utf8(output) + .unwrap() + .split_terminator("\n") + .map(str::to_owned) + .collect() + } + + /// Check that the output of `ls /proc/self/fd` contains the expected set of FDs, plus exactly + /// `extra` extra FDs. + fn expect_fds(output: &Output, expected_fds: &[RawFd], extra: usize) { + assert!(output.status.success()); + let expected_fds: HashSet<String> = expected_fds.iter().map(RawFd::to_string).collect(); + let fds = parse_ls_output(&output.stdout); + if extra == 0 { + assert_eq!(fds, expected_fds); + } else { + assert!(expected_fds.is_subset(&fds)); + assert_eq!(fds.len(), expected_fds.len() + extra); + } + } + + fn setup() { + SETUP.call_once(close_excess_fds); + } + + /// Close all file descriptors apart from stdin, stdout and stderr. + /// + /// This is necessary because GitHub Actions opens a bunch of others for some reason. + fn close_excess_fds() { + let dir = read_dir("/proc/self/fd").unwrap(); + for entry in dir { + let entry = entry.unwrap(); + let fd: RawFd = entry.file_name().to_str().unwrap().parse().unwrap(); + if fd > 3 { + close(fd).unwrap(); + } + } + } +} diff --git a/testdata/file1.txt b/testdata/file1.txt new file mode 100644 index 0000000..4f67a83 --- /dev/null +++ b/testdata/file1.txt @@ -0,0 +1 @@ +test 1
\ No newline at end of file diff --git a/testdata/file2.txt b/testdata/file2.txt new file mode 100644 index 0000000..81403e4 --- /dev/null +++ b/testdata/file2.txt @@ -0,0 +1 @@ +test 2
\ No newline at end of file |