├── .editorconfig ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .rustfmt.toml ├── CHANGELOG.md ├── CITATION.cff ├── COPYRIGHT ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── bin └── version ├── examples ├── async.rs ├── basic.rs ├── builder.rs ├── daemon.rs ├── kill_on_drop.rs └── with_flags.rs ├── logo.png ├── logo.svg ├── src ├── builder.rs ├── lib.rs ├── stdlib.rs ├── stdlib │ ├── child.rs │ ├── child │ │ ├── unix.rs │ │ └── windows.rs │ ├── erased.rs │ ├── unix.rs │ └── windows.rs ├── tokio.rs ├── tokio │ ├── child.rs │ ├── child │ │ ├── unix.rs │ │ └── windows.rs │ ├── erased.rs │ ├── unix.rs │ └── windows.rs ├── unix_ext.rs └── winres.rs └── tests ├── stdlib_unix.rs ├── stdlib_windows.rs ├── tokio_unix.rs └── tokio_windows.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | platform: 14 | - macos 15 | - ubuntu 16 | - windows 17 | toolchain: 18 | - stable 19 | - 1.68.0 20 | features: 21 | - default 22 | - with-tokio 23 | 24 | name: "Test on ${{ matrix.platform }} with Rust ${{ matrix.toolchain }} (feat: ${{ matrix.features }})" 25 | runs-on: "${{ matrix.platform }}-latest" 26 | 27 | steps: 28 | - uses: actions/checkout@v3 29 | 30 | - name: Configure Rust 31 | run: | 32 | rustup toolchain install --profile minimal --no-self-update ${{ matrix.toolchain }} 33 | rustup default ${{ matrix.toolchain }} 34 | rustup component add clippy 35 | 36 | - name: Configure caching 37 | uses: actions/cache@v3 38 | with: 39 | path: | 40 | ~/.cargo/registry/index/ 41 | ~/.cargo/registry/cache/ 42 | ~/.cargo/git/db/ 43 | target/ 44 | key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} 45 | 46 | - run: cargo test --features ${{ matrix.features }} --all-targets 47 | - run: cargo clippy --features ${{ matrix.features }} 48 | 49 | formatting: 50 | name: Fmt check 51 | runs-on: ubuntu-latest 52 | 53 | steps: 54 | - uses: actions/checkout@v3 55 | 56 | - name: Configure Rust 57 | run: | 58 | rustup toolchain install --profile minimal --no-self-update stable 59 | rustup default stable 60 | 61 | - run: cargo fmt --check 62 | 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Next (YYYY-MM-DD) 4 | 5 | ## v5.0.1 (2023-11-18) 6 | 7 | - Use [std's `process_group()`](doc.rust-lang.org/std/os/unix/process/trait.CommandExt.html#tymethod.process_group) ([#16](https://github.com/watchexec/command-group/issues/16)). 8 | 9 | ## v5.0.0 (2023-11-18) 10 | 11 | - Change `UnixChildExt::signal` to take `&self` instead of `&mut self`. 12 | - Grouped child `wait`s using upstream `::wait` and `::try_wait` in addition to the internal pgid-based logic, to help with cancellation. 13 | - Optimisations in `tokio::Child::wait()`. ([#25](https://github.com/watchexec/command-group/issues/25), [#26](https://github.com/watchexec/command-group/issues/26)) 14 | 15 | ## v4.1.0 (2023-11-05) 16 | 17 | - Add `ErasedChild::id()` method. 18 | 19 | ## v4.0.0 (2023-11-05) 20 | 21 | - Clarify why and in which situations `AsyncGroupChild::wait` may not behave as expected when cancelled. 22 | - Add `AsyncGroupChild::start_kill` to align with Tokio's `Child::start_kill`. 23 | - Change `AsyncGroupChild::kill` to also `wait()` on the child, to align with Tokio's `Child::kill`. 24 | - Add `ErasedChild` abstraction to allow using the same type for grouped and ungrouped children. 25 | 26 | ## v3.0.0 (2023-10-30) 27 | 28 | - Update `nix` to 0.27. 29 | - Increase MSRV to 1.68 (within policy). 30 | - Add note to documentation for Tokio `Child::wait` wrt cancel safety. ([#21](https://github.com/watchexec/command-group/issues/21)) 31 | 32 | ## v2.1.1 (2023-10-30) 33 | 34 | (Same as 3.0.0, but yanked due to breakage.) 35 | 36 | ## v2.1.0 (2023-03-04) 37 | 38 | - Add new `.group()` builder API to allow setting Windows flags and use `kill_on_drop`. ([#15](https://github.com/watchexec/command-group/issues/15), [#17](https://github.com/watchexec/command-group/issues/17), [#18](https://github.com/watchexec/command-group/issues/18)) 39 | 40 | ## v2.0.1 (2022-12-28) 41 | 42 | - Fix bug on Windows where the wrong pointer was being null checked, leading to timeout errors. ([#13](https://github.com/watchexec/command-group/pull/13)) 43 | 44 | ## v2.0.0 (2022-12-04) 45 | 46 | - Increase MSRV to 1.60.0 and change policy for increasing it (no longer a breaking change). 47 | - Wait for all processes in the process group, avoiding zombies. ([#7](https://github.com/watchexec/command-group/pull/7)) 48 | - Update `nix` to 0.26 and limit features. ([#8](https://github.com/watchexec/command-group/pull/8)) 49 | 50 | ## v1.0.8 (2021-10-16) 51 | 52 | - Bugfix: compiling would fail when Tokio was missing the `io-util` feature (not `io-std`). 53 | 54 | ## v1.0.7 (2021-10-16) (yanked) 55 | 56 | - Bugfix: compiling would fail when Tokio was missing the `io-std` feature. 57 | 58 | ## v1.0.6 (2021-08-26) 59 | 60 | - Correctly handle timeouts on Windows. ([#2](https://github.com/watchexec/command-group/issues/2), [#3](https://github.com/watchexec/command-group/pull/3)) 61 | 62 | ## v1.0.5 (2021-08-13) 63 | 64 | - Internal: change usage of `feature = "tokio"` to `feature = "with-tokio"`. 65 | - Documentation: remove wrong mention of blocking reads on `AsyncGroupChild::wait_with_output()`. 66 | 67 | ## v1.0.4 (2021-07-26) 68 | 69 | New: Tokio implementation, gated on the `with-tokio` feature. 70 | 71 | ## v1.0.3 (2021-07-21) 72 | 73 | Bugfix: `GroupChild::try_wait()` would error if called after a child exited by itself. 74 | 75 | ## v1.0.2 (2021-07-21) 76 | 77 | Bugfix: `GroupChild::try_wait()` and `::wait()` could not be called twice. 78 | 79 | ## v1.0.1 (2021-07-21) 80 | 81 | Implement `Send`+`Sync` on `GroupChild` on Windows, and add a `Drop` implementation to close handles 82 | too (whoops). Do our best when `.into_inner()` is used... 83 | 84 | ## v1.0.0 (2021-07-20) 85 | 86 | Initial release 87 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: If you use this software, please cite it using these metadata. 3 | title: "Command Group: extension to Command to spawn in a process group" 4 | 5 | version: "5.0.1" 6 | date-released: 2023-11-18 7 | 8 | repository-code: https://github.com/watchexec/command-group 9 | license: Apache-2.0 OR MIT 10 | 11 | authors: 12 | - family-names: Saparelli 13 | given-names: Félix 14 | orcid: https://orcid.org/0000-0002-2010-630X 15 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Short version for non-lawyers: 2 | 3 | This project is dual-licensed under Apache 2.0 and MIT terms. 4 | 5 | 6 | Longer version: 7 | 8 | Copyrights in this project are retained by their contributors. No copyright 9 | assignment is required to contribute. 10 | 11 | Some files include explicit copyright notices and/or license notices. 12 | For full authorship information, see the version control history. 13 | 14 | Except as otherwise noted (below and/or in individual files), the project is 15 | licensed under the Apache License, Version 2.0 or 16 | or the MIT license 17 | or , at your option. 18 | 19 | 20 | The read_both() method in src/child/windows.rs is adapted from the read2() 21 | function in the Rust stdlib at sys/unix/pipe.rs, copyright the Rust Contributors, 22 | licensed under Apache 2.0 and MIT. 23 | 24 | 25 | 26 | The job_object() and resume_threads() methods in src/stdlib/windows.rs are 27 | adapted from the Windows™ version of the new() method in watchexec at 28 | src/process.rs, copyright Matt Green, licensed under Apache 2.0 only. 29 | 30 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "command-group" 3 | version = "5.0.1" 4 | 5 | authors = ["Félix Saparelli "] 6 | license = "Apache-2.0 OR MIT" 7 | description = "Extension to Command to spawn in a process group" 8 | keywords = ["command", "extension", "process", "group"] 9 | 10 | documentation = "https://docs.rs/command-group" 11 | homepage = "https://github.com/watchexec/command-group" 12 | repository = "https://github.com/watchexec/command-group" 13 | readme = "README.md" 14 | 15 | edition = "2021" 16 | exclude = ["/bin", "/.github"] 17 | rust-version = "1.68.0" 18 | 19 | # there are a few windows-specific ones 20 | autoexamples = false 21 | 22 | [dependencies] 23 | async-trait = { version = "0.1.74", optional = true } 24 | 25 | [dependencies.tokio] 26 | version = "1.33.0" 27 | features = ["io-util", "macros", "process", "rt"] 28 | optional = true 29 | 30 | [target.'cfg(unix)'.dependencies.nix] 31 | version = "0.27.1" 32 | default-features = false 33 | features = ["fs", "poll", "signal"] 34 | 35 | [target.'cfg(windows)'.dependencies.winapi] 36 | version = "0.3.9" 37 | features = [ 38 | "impl-default", 39 | "handleapi", 40 | "ioapiset", 41 | "jobapi2", 42 | "processthreadsapi", 43 | "tlhelp32", 44 | "winbase", 45 | ] 46 | 47 | [features] 48 | default = [] 49 | with-tokio = ["async-trait", "tokio"] 50 | 51 | [dev-dependencies] 52 | tokio = { version = "1.10.0", features = ["io-util", "macros", "process", "rt", "rt-multi-thread", "time"] } 53 | 54 | [package.metadata.docs.rs] 55 | all-features = true 56 | 57 | [[example]] 58 | name = "basic" 59 | 60 | [[example]] 61 | name = "builder" 62 | 63 | [[example]] 64 | name = "async" 65 | required-features = ["with-tokio"] 66 | 67 | [[example]] 68 | name = "kill_on_drop" 69 | required-features = ["with-tokio"] 70 | 71 | [[example]] 72 | name = "daemon" 73 | required-features = ["with-tokio"] 74 | 75 | [[target.'cfg(windows)'.example]] 76 | name = "with_flags" 77 | required-features = ["with-tokio"] 78 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Crate release version](https://flat.badgen.net/crates/v/command-group)](https://crates.io/crates/command-group) 2 | [![Crate license: Apache 2.0 or MIT](https://flat.badgen.net/badge/license/Apache%202.0%20or%20MIT)][copyright] 3 | [![CI status](https://github.com/watchexec/command-group/actions/workflows/test.yml/badge.svg)](https://github.com/watchexec/command-group/actions/workflows/test.yml) 4 | 5 | ***The successor of command-group is [process-wrap](https://github.com/watchexec/process-wrap). No further work will be done on command-group.*** 6 | 7 | # Command Group 8 | 9 | _Extension to [`Command`](https://doc.rust-lang.org/std/process/struct.Command.html) to spawn in a process group._ 10 | 11 | - **[API documentation][docs]**. 12 | - [Dual-licensed][copyright] with Apache 2.0 and MIT. 13 | - Minimum Supported Rust Version: 1.68.0. 14 | - Only the last five stable versions are supported. 15 | - MSRV increases within that range at publish time will not incur major version bumps. 16 | 17 | [copyright]: ./COPYRIGHT 18 | [docs]: https://docs.rs/command-group 19 | 20 | ## Quick start 21 | 22 | ```toml 23 | [dependencies] 24 | command-group = "5.0.1" 25 | ``` 26 | 27 | ```rust 28 | use std::process::Command; 29 | use command_group::CommandGroup; 30 | 31 | let mut child = Command::new("watch").arg("ls").group_spawn()?; 32 | let status = child.wait()?; 33 | dbg!(status); 34 | ``` 35 | 36 | ### Async: Tokio 37 | 38 | ```toml 39 | [dependencies] 40 | command-group = { version = "5.0.1", features = ["with-tokio"] } 41 | tokio = { version = "1.10.0", features = ["full"] } 42 | ``` 43 | 44 | ```rust 45 | use tokio::process::Command; 46 | use command_group::AsyncCommandGroup; 47 | 48 | let mut child = Command::new("watch").arg("ls").group_spawn()?; 49 | let status = child.wait().await?; 50 | dbg!(status); 51 | ``` 52 | 53 | Also see the [Examples](./examples)! 54 | -------------------------------------------------------------------------------- /bin/version: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | app="command-group" 6 | mainbranch="main" 7 | upstream_rx="watchexec/" 8 | 9 | curbranch=$(git rev-parse --abbrev-ref HEAD) 10 | if [[ "$curbranch" != "$mainbranch" ]]; then 11 | echo "Current branch is not $mainbranch, abort!" 12 | exit 1 13 | fi 14 | 15 | gitstatus=$(git status --untracked-files=no --porcelain) 16 | if [[ ! -z "$gitstatus" ]]; then 17 | echo "Uncommited files and changes, abort!" 18 | exit 2 19 | fi 20 | 21 | upstream=$(git remote -v | grep -i "$upstream_rx" -m1 | awk '{print $1}') 22 | echo "Upstream remote discovered as: $upstream" 23 | 24 | echo "Pulling from upstream" 25 | git pull --rebase --autostash $upstream $mainbranch 26 | 27 | echo "Fetching tags from upstream" 28 | git fetch --tags "$upstream" 29 | 30 | extver=$(grep -P '^version =' Cargo.toml | head -n1 | cut -d'"' -f2) 31 | echo "(Version from Cargo.toml: $extver)" 32 | 33 | newver="$1" 34 | 35 | if [[ "$newver" == "$extver" ]]; then 36 | echo "New and existing versions are the same, abort!" 37 | exit 3 38 | fi 39 | 40 | date=$(date +%Y-%m-%d) 41 | echo "Next version to be $newver ($date), creating..." 42 | 43 | sed -E -i "s/^## Next.*$/## Next (YYYY-MM-DD)\n\n## v$newver ($date)/1" CHANGELOG.md 44 | sed -E -i "s/^command-group = \"$extver\"/command-group = \"$newver\"/1" README.md 45 | sed -E -i "s/^command-group = \{ version = \"$extver\"/command-group = \{ version = \"$newver\"/1" README.md 46 | sed -E -i "s/^version = \"$extver\"/version = \"$newver\"/1" Cargo.toml 47 | sed -E -i "s/^version: \"?$extver\"?/version: \"$newver\"/1" CITATION.cff 48 | sed -E -i "s/^date-released: .+$/date-released: $date/1" CITATION.cff 49 | 50 | cargo check 51 | cargo publish --dry-run --allow-dirty 52 | 53 | git commit -am "$newver" 54 | git tag -sam "$newver" "v$newver" 55 | 56 | git push --follow-tags $upstream $mainbranch 57 | cargo publish --no-verify 58 | -------------------------------------------------------------------------------- /examples/async.rs: -------------------------------------------------------------------------------- 1 | //! This example shows the basic usage of the async version 2 | //! of the `group_spawn` method, and collects the exit code. 3 | 4 | use command_group::AsyncCommandGroup; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | let mut handle = tokio::process::Command::new("ls").group_spawn().unwrap(); 9 | println!("{:?}", handle); 10 | let exit_code = handle.wait().await; 11 | println!("{:?}", exit_code); 12 | } 13 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | //! This example shows the basic usage of the sync version 2 | //! of the `group_spawn` method, and collects the exit code. 3 | 4 | use command_group::CommandGroup; 5 | 6 | fn main() { 7 | let mut handle = std::process::Command::new("ls").group_spawn().unwrap(); 8 | println!("{:?}", handle); 9 | let exit_code = handle.wait(); 10 | println!("{:?}", exit_code); 11 | } 12 | -------------------------------------------------------------------------------- /examples/builder.rs: -------------------------------------------------------------------------------- 1 | //! This example shows the basic usage of the sync version 2 | //! of the `group` method, and collects the exit code. The 3 | //! group builder gives more control over the process group. 4 | 5 | use command_group::CommandGroup; 6 | 7 | fn main() { 8 | let mut handle = std::process::Command::new("ls").group().spawn().unwrap(); 9 | println!("{:?}", handle); 10 | let exit_code = handle.wait(); 11 | println!("{:?}", exit_code); 12 | } 13 | -------------------------------------------------------------------------------- /examples/daemon.rs: -------------------------------------------------------------------------------- 1 | //! This example shows how to use the `group_spawn` method 2 | //! to spawn a python server daemon in the background. 3 | //! 4 | //! NOTE: This example will not work on Windows, as the 5 | //! `kill_on_drop` flag is not supported via this API. 6 | //! 7 | //! See the `kill_on_drop` example for a Windows-compatible 8 | //! example. 9 | 10 | use std::process::Stdio; 11 | 12 | use command_group::AsyncCommandGroup; 13 | 14 | #[tokio::main] 15 | async fn main() { 16 | tokio::process::Command::new("python3") 17 | .args(&["-m", "http.server", "8000"]) 18 | .stderr(Stdio::null()) 19 | .stdout(Stdio::null()) 20 | .group_spawn() 21 | .expect("failed to spawn server"); 22 | } 23 | -------------------------------------------------------------------------------- /examples/kill_on_drop.rs: -------------------------------------------------------------------------------- 1 | //! This example shows how to use the `group` builder 2 | //! to spawn a python server daemon in the background, 3 | //! while supporting windows by explicitly setting the 4 | //! `kill_on_drop` flag. 5 | 6 | use std::process::Stdio; 7 | 8 | use command_group::AsyncCommandGroup; 9 | 10 | #[tokio::main] 11 | async fn main() { 12 | tokio::process::Command::new("python3") 13 | .args(&["-m", "http.server", "8000"]) 14 | .stderr(Stdio::null()) 15 | .stdout(Stdio::null()) 16 | .group() 17 | .kill_on_drop(true) 18 | .spawn() 19 | .expect("failed to spawn server"); 20 | } 21 | -------------------------------------------------------------------------------- /examples/with_flags.rs: -------------------------------------------------------------------------------- 1 | //! This example shows how to use the `group` builder 2 | //! to spawn a python server daemon in the background, 3 | //! while setting creation flags on windows to hide the 4 | //! console window. 5 | 6 | use std::process::Stdio; 7 | 8 | use command_group::AsyncCommandGroup; 9 | use winapi::um::winbase::CREATE_NO_WINDOW; 10 | 11 | #[tokio::main] 12 | async fn main() { 13 | let group = tokio::process::Command::new("python3") 14 | .args(&["-m", "http.server", "8000"]) 15 | .stderr(Stdio::null()) 16 | .stdout(Stdio::null()) 17 | .group() 18 | .creation_flags(CREATE_NO_WINDOW) 19 | .spawn(); 20 | } 21 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/watchexec/command-group/bf3f6c94a84f7f5892e640d95339317c9268962d/logo.png -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 49 | 54 | 59 | 64 | 69 | 75 | 76 | 78 | 79 | 81 | image/svg+xml 82 | 84 | 85 | 86 | 87 | 88 | 94 | 98 | 104 | 110 | 121 | 122 | 127 | 132 | 138 | 144 | 150 | 156 | 157 | 161 | 167 | 173 | 179 | 185 | 186 | 190 | 196 | 202 | 208 | 214 | 215 | 219 | 225 | 231 | 237 | 243 | 244 | 248 | 254 | 260 | 266 | 267 | 271 | 277 | 283 | 289 | 290 | 296 | 301 | 306 | 307 | 308 | -------------------------------------------------------------------------------- /src/builder.rs: -------------------------------------------------------------------------------- 1 | //! 2 | 3 | /// CommandGroupBuilder is a builder for a group of processes. 4 | /// 5 | /// It is created via the `group` method on [`Command`](std::process::Command) or 6 | /// [`AsyncCommand`](tokio::process::Command). 7 | pub struct CommandGroupBuilder<'a, T> { 8 | pub(crate) command: &'a mut T, 9 | #[allow(dead_code)] 10 | pub(crate) kill_on_drop: bool, 11 | #[allow(dead_code)] 12 | pub(crate) creation_flags: u32, 13 | } 14 | 15 | impl<'a, T> CommandGroupBuilder<'a, T> { 16 | pub(crate) fn new(command: &'a mut T) -> Self { 17 | Self { 18 | command, 19 | kill_on_drop: false, 20 | creation_flags: 0, 21 | } 22 | } 23 | 24 | /// See [`tokio::process::Command::kill_on_drop`]. 25 | #[cfg(any(windows, feature = "with-tokio"))] 26 | pub fn kill_on_drop(&mut self, kill_on_drop: bool) -> &mut Self { 27 | self.kill_on_drop = kill_on_drop; 28 | self 29 | } 30 | 31 | /// Set the creation flags for the process. 32 | #[cfg(windows)] 33 | pub fn creation_flags(&mut self, creation_flags: u32) -> &mut Self { 34 | self.creation_flags = creation_flags; 35 | self 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! An extension to [`std::process::Command`] to support process groups on Unix and Windows. 2 | #![cfg_attr( 3 | feature = "with-tokio", 4 | doc = "With Tokio, the [`AsyncCommandGroup`] trait extends [`tokio::process::Command`](::tokio::process::Command)." 5 | )] 6 | #![doc = "\n"] 7 | #![cfg_attr( 8 | unix, 9 | doc = "On Unix, the [`UnixChildExt`] trait additionally provides support for sending signals to processes and process groups (it’s implemented on this crate’s [`GroupChild`]," 10 | )] 11 | #![cfg_attr( 12 | all(unix, feature = "with-tokio"), 13 | doc = "[`AsyncGroupChild`], Tokio’s [`Child`](::tokio::process::Child)" 14 | )] 15 | #![cfg_attr(unix, doc = "and std’s [`Child`](std::process::Child)).")] 16 | #![doc(html_favicon_url = "https://watchexec.github.io/logo:command-group.svg")] 17 | #![doc(html_logo_url = "https://watchexec.github.io/logo:command-group.svg")] 18 | #![warn(missing_docs)] 19 | 20 | pub mod stdlib; 21 | 22 | #[cfg(unix)] 23 | mod unix_ext; 24 | 25 | #[cfg(feature = "with-tokio")] 26 | pub mod tokio; 27 | 28 | pub mod builder; 29 | 30 | #[cfg(windows)] 31 | pub(crate) mod winres; 32 | 33 | #[cfg(unix)] 34 | #[doc(inline)] 35 | pub use crate::unix_ext::UnixChildExt; 36 | #[cfg(unix)] 37 | #[doc(no_inline)] 38 | pub use nix::sys::signal::Signal; 39 | 40 | #[doc(inline)] 41 | pub use crate::stdlib::child::GroupChild; 42 | pub use crate::stdlib::CommandGroup; 43 | 44 | #[cfg(feature = "with-tokio")] 45 | #[doc(inline)] 46 | pub use crate::tokio::child::AsyncGroupChild; 47 | #[cfg(feature = "with-tokio")] 48 | pub use crate::tokio::AsyncCommandGroup; 49 | -------------------------------------------------------------------------------- /src/stdlib.rs: -------------------------------------------------------------------------------- 1 | //! Implementation of process group extensions for the 2 | //! standard library’s [`Command` type](std::process::Command). 3 | 4 | use std::{ 5 | io::Result, 6 | process::{Command, ExitStatus, Output}, 7 | }; 8 | 9 | use crate::{builder::CommandGroupBuilder, GroupChild}; 10 | 11 | #[doc(inline)] 12 | pub use erased::ErasedChild; 13 | 14 | #[cfg(target_family = "windows")] 15 | mod windows; 16 | 17 | #[cfg(target_family = "unix")] 18 | mod unix; 19 | 20 | pub(crate) mod child; 21 | pub(crate) mod erased; 22 | 23 | /// Extensions for [`Command`](std::process::Command) adding support for process groups. 24 | pub trait CommandGroup { 25 | /// Executes the command as a child process group, returning a handle to it. 26 | /// 27 | /// By default, stdin, stdout and stderr are inherited from the parent. 28 | /// 29 | /// On Windows, this creates a job object instead of a POSIX process group. 30 | /// 31 | /// # Examples 32 | /// 33 | /// Basic usage: 34 | /// 35 | /// ```no_run 36 | /// use std::process::Command; 37 | /// use command_group::CommandGroup; 38 | /// 39 | /// Command::new("ls") 40 | /// .group_spawn() 41 | /// .expect("ls command failed to start"); 42 | /// ``` 43 | fn group_spawn(&mut self) -> Result { 44 | self.group().spawn() 45 | } 46 | 47 | /// Converts the implementor into a [`CommandGroupBuilder`](crate::CommandGroupBuilder), which can be used to 48 | /// set flags that are not available on the `Command` type. 49 | fn group(&mut self) -> CommandGroupBuilder; 50 | 51 | /// Executes the command as a child process group, waiting for it to finish and 52 | /// collecting all of its output. 53 | /// 54 | /// By default, stdout and stderr are captured (and used to provide the 55 | /// resulting output). Stdin is not inherited from the parent and any 56 | /// attempt by the child process to read from the stdin stream will result 57 | /// in the stream immediately closing. 58 | /// 59 | /// On Windows, this creates a job object instead of a POSIX process group. 60 | /// 61 | /// # Examples 62 | /// 63 | /// ```should_panic 64 | /// use std::process::Command; 65 | /// use std::io::{self, Write}; 66 | /// use command_group::CommandGroup; 67 | /// 68 | /// let output = Command::new("/bin/cat") 69 | /// .arg("file.txt") 70 | /// .group_output() 71 | /// .expect("failed to execute process"); 72 | /// 73 | /// println!("status: {}", output.status); 74 | /// io::stdout().write_all(&output.stdout).unwrap(); 75 | /// io::stderr().write_all(&output.stderr).unwrap(); 76 | /// 77 | /// assert!(output.status.success()); 78 | /// ``` 79 | fn group_output(&mut self) -> Result { 80 | self.group_spawn() 81 | .and_then(|child| child.wait_with_output()) 82 | } 83 | 84 | /// Executes a command as a child process group, waiting for it to finish and 85 | /// collecting its status. 86 | /// 87 | /// By default, stdin, stdout and stderr are inherited from the parent. 88 | /// 89 | /// On Windows, this creates a job object instead of a POSIX process group. 90 | /// 91 | /// # Examples 92 | /// 93 | /// ```should_panic 94 | /// use std::process::Command; 95 | /// use command_group::CommandGroup; 96 | /// 97 | /// let status = Command::new("/bin/cat") 98 | /// .arg("file.txt") 99 | /// .group_status() 100 | /// .expect("failed to execute process"); 101 | /// 102 | /// println!("process finished with: {}", status); 103 | /// 104 | /// assert!(status.success()); 105 | /// ``` 106 | fn group_status(&mut self) -> Result { 107 | self.group_spawn().and_then(|mut child| child.wait()) 108 | } 109 | } 110 | 111 | impl CommandGroup for Command { 112 | fn group(&mut self) -> CommandGroupBuilder<'_, Command> { 113 | CommandGroupBuilder::new(self) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/stdlib/child.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt, 3 | io::{Read, Result}, 4 | process::{Child, ExitStatus, Output}, 5 | }; 6 | 7 | #[cfg(unix)] 8 | use unix::ChildImp; 9 | #[cfg(windows)] 10 | use windows::ChildImp; 11 | 12 | #[cfg(unix)] 13 | use crate::UnixChildExt; 14 | 15 | #[cfg(unix)] 16 | use nix::sys::signal::Signal; 17 | 18 | #[cfg(windows)] 19 | use winapi::um::winnt::HANDLE; 20 | 21 | #[cfg(unix)] 22 | mod unix; 23 | #[cfg(windows)] 24 | mod windows; 25 | 26 | /// Representation of a running or exited child process group. 27 | /// 28 | /// This wraps the [`Child`] type in the standard library with methods that work 29 | /// with process groups. 30 | /// 31 | /// # Examples 32 | /// 33 | /// ```should_panic 34 | /// use std::process::Command; 35 | /// use command_group::CommandGroup; 36 | /// 37 | /// let mut child = Command::new("/bin/cat") 38 | /// .arg("file.txt") 39 | /// .group_spawn() 40 | /// .expect("failed to execute child"); 41 | /// 42 | /// let ecode = child.wait() 43 | /// .expect("failed to wait on child"); 44 | /// 45 | /// assert!(ecode.success()); 46 | /// ``` 47 | pub struct GroupChild { 48 | imp: ChildImp, 49 | exitstatus: Option, 50 | } 51 | 52 | impl fmt::Debug for GroupChild { 53 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 54 | f.debug_struct("GroupChild").finish() 55 | } 56 | } 57 | 58 | impl GroupChild { 59 | #[cfg(unix)] 60 | pub(crate) fn new(inner: Child) -> Self { 61 | Self { 62 | imp: ChildImp::new(inner), 63 | exitstatus: None, 64 | } 65 | } 66 | 67 | #[cfg(windows)] 68 | pub(crate) fn new(inner: Child, j: HANDLE, c: HANDLE) -> Self { 69 | Self { 70 | imp: ChildImp::new(inner, j, c), 71 | exitstatus: None, 72 | } 73 | } 74 | 75 | /// Returns the stdlib [`Child`] object. 76 | /// 77 | /// Note that the inner child may not be in the same state as this output child, due to how 78 | /// methods like `wait` and `kill` are implemented. It is not recommended to use this method 79 | /// _after_ using any of the other methods on this struct. 80 | /// 81 | /// # Examples 82 | /// 83 | /// Reading from stdout: 84 | /// 85 | /// ```no_run 86 | /// use std::io::Read; 87 | /// use std::process::{Command, Stdio}; 88 | /// use command_group::CommandGroup; 89 | /// 90 | /// let mut child = Command::new("ls").stdout(Stdio::piped()).group_spawn().expect("ls command didn't start"); 91 | /// let mut output = String::new(); 92 | /// if let Some(mut out) = child.inner().stdout.take() { 93 | /// out.read_to_string(&mut output).expect("failed to read from child"); 94 | /// } 95 | /// println!("output: {}", output); 96 | /// ``` 97 | pub fn inner(&mut self) -> &mut Child { 98 | self.imp.inner() 99 | } 100 | 101 | /// Consumes itself and returns the stdlib [`Child`] object. 102 | /// 103 | /// Note that the inner child may not be in the same state as this output child, due to how 104 | /// methods like `wait` and `kill` are implemented. It is not recommended to use this method 105 | /// _after_ using any of the other methods on this struct. 106 | /// 107 | #[cfg_attr( 108 | windows, 109 | doc = "On Windows, this unnavoidably leaves a handle unclosed. Prefer [`inner()`](Self::inner)." 110 | )] 111 | /// 112 | /// # Examples 113 | /// 114 | /// Writing to input: 115 | /// 116 | /// ```no_run 117 | /// use std::io::Write; 118 | /// use std::process::{Command, Stdio}; 119 | /// use command_group::CommandGroup; 120 | /// 121 | /// let mut child = Command::new("cat").stdin(Stdio::piped()).group_spawn().expect("cat command didn't start"); 122 | /// if let Some(mut din) = child.into_inner().stdin.take() { 123 | /// din.write_all(b"Woohoo!").expect("failed to write"); 124 | /// } 125 | /// ``` 126 | pub fn into_inner(self) -> Child { 127 | self.imp.into_inner() 128 | } 129 | 130 | /// Forces the child process group to exit. 131 | /// 132 | /// If the group has already exited, an [`InvalidInput`] error is returned. 133 | /// 134 | /// This is equivalent to sending a SIGKILL on Unix platforms. 135 | /// 136 | /// See [the stdlib documentation](Child::kill) for more. 137 | /// 138 | /// # Examples 139 | /// 140 | /// Basic usage: 141 | /// 142 | /// ```no_run 143 | /// use std::process::Command; 144 | /// use command_group::CommandGroup; 145 | /// 146 | /// let mut command = Command::new("yes"); 147 | /// if let Ok(mut child) = command.group_spawn() { 148 | /// child.kill().expect("command wasn't running"); 149 | /// } else { 150 | /// println!("yes command didn't start"); 151 | /// } 152 | /// ``` 153 | /// 154 | /// [`InvalidInput`]: std::io::ErrorKind::InvalidInput 155 | pub fn kill(&mut self) -> Result<()> { 156 | self.imp.kill() 157 | } 158 | 159 | /// Returns the OS-assigned process group identifier. 160 | /// 161 | /// See [the stdlib documentation](Child::id) for more. 162 | /// 163 | /// # Examples 164 | /// 165 | /// Basic usage: 166 | /// 167 | /// ```no_run 168 | /// use std::process::Command; 169 | /// use command_group::CommandGroup; 170 | /// 171 | /// let mut command = Command::new("ls"); 172 | /// if let Ok(child) = command.group_spawn() { 173 | /// println!("Child group's ID is {}", child.id()); 174 | /// } else { 175 | /// println!("ls command didn't start"); 176 | /// } 177 | /// ``` 178 | pub fn id(&self) -> u32 { 179 | self.imp.id() 180 | } 181 | 182 | /// Waits for the child group to exit completely, returning the status that 183 | /// the process leader exited with. 184 | /// 185 | /// See [the stdlib documentation](Child::wait) for more. 186 | /// 187 | /// # Examples 188 | /// 189 | /// Basic usage: 190 | /// 191 | /// ```no_run 192 | /// use std::process::Command; 193 | /// use command_group::CommandGroup; 194 | /// 195 | /// let mut command = Command::new("ls"); 196 | /// if let Ok(mut child) = command.group_spawn() { 197 | /// child.wait().expect("command wasn't running"); 198 | /// println!("Child has finished its execution!"); 199 | /// } else { 200 | /// println!("ls command didn't start"); 201 | /// } 202 | /// ``` 203 | pub fn wait(&mut self) -> Result { 204 | if let Some(es) = self.exitstatus { 205 | return Ok(es); 206 | } 207 | 208 | drop(self.imp.take_stdin()); 209 | let status = self.imp.wait()?; 210 | self.exitstatus = Some(status); 211 | Ok(status) 212 | } 213 | 214 | /// Attempts to collect the exit status of the child if it has already 215 | /// exited. 216 | /// 217 | /// See [the stdlib documentation](Child::try_wait) for more. 218 | /// 219 | /// # Examples 220 | /// 221 | /// Basic usage: 222 | /// 223 | /// ```no_run 224 | /// use std::process::Command; 225 | /// use command_group::CommandGroup; 226 | /// 227 | /// let mut child = Command::new("ls").group_spawn().unwrap(); 228 | /// 229 | /// match child.try_wait() { 230 | /// Ok(Some(status)) => println!("exited with: {}", status), 231 | /// Ok(None) => { 232 | /// println!("status not ready yet, let's really wait"); 233 | /// let res = child.wait(); 234 | /// println!("result: {:?}", res); 235 | /// } 236 | /// Err(e) => println!("error attempting to wait: {}", e), 237 | /// } 238 | /// ``` 239 | pub fn try_wait(&mut self) -> Result> { 240 | if self.exitstatus.is_some() { 241 | return Ok(self.exitstatus); 242 | } 243 | 244 | match self.imp.try_wait()? { 245 | Some(es) => { 246 | self.exitstatus = Some(es); 247 | Ok(Some(es)) 248 | } 249 | None => Ok(None), 250 | } 251 | } 252 | 253 | /// Simultaneously waits for the child to exit and collect all remaining 254 | /// output on the stdout/stderr handles, returning an `Output` 255 | /// instance. 256 | /// 257 | /// See [the stdlib documentation](Child::wait_with_output) for more. 258 | /// 259 | /// # Bugs 260 | /// 261 | /// On Windows, STDOUT is read before STDERR if both are piped, which may block. This is mostly 262 | /// because reading two outputs at the same time in synchronous code is horrendous. If you want 263 | /// this, please contribute a better version. Alternatively, prefer using the async API. 264 | /// 265 | /// # Examples 266 | /// 267 | /// Basic usage: 268 | /// 269 | /// ```should_panic 270 | /// use std::process::{Command, Stdio}; 271 | /// use command_group::CommandGroup; 272 | /// 273 | /// let child = Command::new("/bin/cat") 274 | /// .arg("file.txt") 275 | /// .stdout(Stdio::piped()) 276 | /// .group_spawn() 277 | /// .expect("failed to execute child"); 278 | /// 279 | /// let output = child 280 | /// .wait_with_output() 281 | /// .expect("failed to wait on child"); 282 | /// 283 | /// assert!(output.status.success()); 284 | /// ``` 285 | pub fn wait_with_output(mut self) -> Result { 286 | drop(self.imp.take_stdin()); 287 | 288 | let (mut stdout, mut stderr) = (Vec::new(), Vec::new()); 289 | match (self.imp.take_stdout(), self.imp.take_stderr()) { 290 | (None, None) => {} 291 | (Some(mut out), None) => { 292 | out.read_to_end(&mut stdout)?; 293 | } 294 | (None, Some(mut err)) => { 295 | err.read_to_end(&mut stderr)?; 296 | } 297 | (Some(out), Some(err)) => { 298 | let res = ChildImp::read_both(out, &mut stdout, err, &mut stderr); 299 | res.unwrap(); 300 | } 301 | } 302 | 303 | let status = self.imp.wait()?; 304 | Ok(Output { 305 | status, 306 | stdout, 307 | stderr, 308 | }) 309 | } 310 | } 311 | 312 | #[cfg(unix)] 313 | impl UnixChildExt for GroupChild { 314 | fn signal(&self, sig: Signal) -> Result<()> { 315 | self.imp.signal_imp(sig) 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/stdlib/child/unix.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | convert::TryInto, 3 | io::{Error, Read, Result}, 4 | os::{ 5 | fd::BorrowedFd, 6 | unix::{ 7 | io::{AsRawFd, RawFd}, 8 | process::ExitStatusExt, 9 | }, 10 | }, 11 | process::{Child, ChildStderr, ChildStdin, ChildStdout, ExitStatus}, 12 | }; 13 | 14 | use nix::{ 15 | errno::Errno, 16 | libc, 17 | poll::{poll, PollFd, PollFlags}, 18 | sys::{ 19 | signal::{killpg, Signal}, 20 | wait::WaitPidFlag, 21 | }, 22 | unistd::Pid, 23 | }; 24 | 25 | pub(super) struct ChildImp { 26 | pgid: Pid, 27 | inner: Child, 28 | } 29 | 30 | impl ChildImp { 31 | pub(super) fn new(inner: Child) -> Self { 32 | Self { 33 | pgid: Pid::from_raw(inner.id().try_into().expect("Command PID > i32::MAX")), 34 | inner, 35 | } 36 | } 37 | 38 | pub(super) fn take_stdin(&mut self) -> Option { 39 | self.inner.stdin.take() 40 | } 41 | 42 | pub(super) fn take_stdout(&mut self) -> Option { 43 | self.inner.stdout.take() 44 | } 45 | 46 | pub(super) fn take_stderr(&mut self) -> Option { 47 | self.inner.stderr.take() 48 | } 49 | 50 | pub fn inner(&mut self) -> &mut Child { 51 | &mut self.inner 52 | } 53 | 54 | pub fn into_inner(self) -> Child { 55 | self.inner 56 | } 57 | 58 | pub(super) fn signal_imp(&self, sig: Signal) -> Result<()> { 59 | killpg(self.pgid, sig).map_err(Error::from) 60 | } 61 | 62 | pub fn kill(&mut self) -> Result<()> { 63 | self.signal_imp(Signal::SIGKILL) 64 | } 65 | 66 | pub fn id(&self) -> u32 { 67 | self.inner.id() 68 | } 69 | 70 | fn wait_imp(&mut self, flag: WaitPidFlag) -> Result> { 71 | let negpid = Pid::from_raw(-self.pgid.as_raw()); 72 | 73 | // Wait for processes in a loop until every process in this 74 | // process group has exited (this ensures that we reap any 75 | // zombies that may have been created if the parent exited after 76 | // spawning children, but didn't wait for those children to 77 | // exit). 78 | let mut parent_exit_status: Option = None; 79 | loop { 80 | // we can't use the safe wrapper directly because it doesn't 81 | // return the raw status, and we need it to convert to the 82 | // std's ExitStatus. 83 | let mut status: i32 = 0; 84 | match unsafe { 85 | libc::waitpid(negpid.into(), &mut status as *mut libc::c_int, flag.bits()) 86 | } { 87 | 0 => { 88 | // Zero should only happen if WNOHANG was passed in, 89 | // and means that no processes have yet to exit. 90 | return Ok(None); 91 | } 92 | -1 => { 93 | match Errno::last() { 94 | Errno::ECHILD => { 95 | // No more children to reap; this is a 96 | // graceful exit. 97 | return Ok(parent_exit_status); 98 | } 99 | errno => { 100 | return Err(Error::from(errno)); 101 | } 102 | } 103 | } 104 | pid => { 105 | // *A* process exited. Was it the parent process 106 | // that we started? If so, collect the exit signal, 107 | // otherwise we reaped a zombie process and should 108 | // continue in the loop. 109 | if self.pgid.as_raw() == pid { 110 | parent_exit_status = Some(ExitStatus::from_raw(status)); 111 | } else { 112 | // Reaped a zombie child; keep looping. 113 | } 114 | } 115 | }; 116 | } 117 | } 118 | 119 | pub fn wait(&mut self) -> Result { 120 | if let Some(status) = self.try_wait()? { 121 | return Ok(status); 122 | } 123 | 124 | match self.wait_imp(WaitPidFlag::empty()).transpose() { 125 | None => self.inner.wait(), 126 | Some(status) => status, 127 | } 128 | } 129 | 130 | pub fn try_wait(&mut self) -> Result> { 131 | match self.wait_imp(WaitPidFlag::WNOHANG) { 132 | Ok(None) => self.inner.try_wait(), 133 | otherwise => otherwise, 134 | } 135 | } 136 | 137 | pub(super) fn read_both( 138 | mut out_r: ChildStdout, 139 | out_v: &mut Vec, 140 | mut err_r: ChildStderr, 141 | err_v: &mut Vec, 142 | ) -> Result<()> { 143 | let out_fd = out_r.as_raw_fd(); 144 | let err_fd = err_r.as_raw_fd(); 145 | set_nonblocking(out_fd, true)?; 146 | set_nonblocking(err_fd, true)?; 147 | 148 | // SAFETY: these are dropped at the same time as all other FDs here 149 | let out_bfd = unsafe { BorrowedFd::borrow_raw(out_fd) }; 150 | let err_bfd = unsafe { BorrowedFd::borrow_raw(err_fd) }; 151 | 152 | let mut fds = [ 153 | PollFd::new(&out_bfd, PollFlags::POLLIN), 154 | PollFd::new(&err_bfd, PollFlags::POLLIN), 155 | ]; 156 | 157 | loop { 158 | poll(&mut fds, -1)?; 159 | 160 | if fds[0].revents().is_some() && read(&mut out_r, out_v)? { 161 | set_nonblocking(err_fd, false)?; 162 | return err_r.read_to_end(err_v).map(drop); 163 | } 164 | if fds[1].revents().is_some() && read(&mut err_r, err_v)? { 165 | set_nonblocking(out_fd, false)?; 166 | return out_r.read_to_end(out_v).map(drop); 167 | } 168 | } 169 | 170 | fn read(r: &mut impl Read, dst: &mut Vec) -> Result { 171 | match r.read_to_end(dst) { 172 | Ok(_) => Ok(true), 173 | Err(e) => { 174 | if e.raw_os_error() == Some(libc::EWOULDBLOCK) 175 | || e.raw_os_error() == Some(libc::EAGAIN) 176 | { 177 | Ok(false) 178 | } else { 179 | Err(e) 180 | } 181 | } 182 | } 183 | } 184 | 185 | #[cfg(target_os = "linux")] 186 | fn set_nonblocking(fd: RawFd, nonblocking: bool) -> Result<()> { 187 | let v = nonblocking as libc::c_int; 188 | let res = unsafe { libc::ioctl(fd, libc::FIONBIO, &v) }; 189 | 190 | Errno::result(res).map_err(Error::from).map(drop) 191 | } 192 | 193 | #[cfg(not(target_os = "linux"))] 194 | fn set_nonblocking(fd: RawFd, nonblocking: bool) -> Result<()> { 195 | use nix::fcntl::{fcntl, FcntlArg, OFlag}; 196 | 197 | let mut flags = OFlag::from_bits_truncate(fcntl(fd, FcntlArg::F_GETFL)?); 198 | flags.set(OFlag::O_NONBLOCK, nonblocking); 199 | 200 | fcntl(fd, FcntlArg::F_SETFL(flags)) 201 | .map_err(Error::from) 202 | .map(drop) 203 | } 204 | } 205 | } 206 | 207 | pub trait UnixChildExt { 208 | fn signal(&self, sig: Signal) -> Result<()>; 209 | } 210 | 211 | impl UnixChildExt for ChildImp { 212 | fn signal(&self, sig: Signal) -> Result<()> { 213 | self.signal_imp(sig) 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/stdlib/child/windows.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{Read, Result}, 3 | mem, 4 | process::{Child, ChildStderr, ChildStdin, ChildStdout, ExitStatus}, 5 | }; 6 | use winapi::{ 7 | shared::{ 8 | basetsd::ULONG_PTR, 9 | minwindef::{DWORD, FALSE}, 10 | }, 11 | um::{ 12 | handleapi::CloseHandle, ioapiset::GetQueuedCompletionStatus, jobapi2::TerminateJobObject, 13 | minwinbase::OVERLAPPED, winbase::INFINITE, winnt::HANDLE, 14 | }, 15 | }; 16 | 17 | use crate::winres::*; 18 | 19 | pub(super) struct ChildImp { 20 | inner: Child, 21 | handles: JobPort, 22 | } 23 | 24 | impl ChildImp { 25 | pub fn new(inner: Child, job: HANDLE, completion_port: HANDLE) -> Self { 26 | Self { 27 | inner, 28 | handles: JobPort { 29 | job, 30 | completion_port, 31 | }, 32 | } 33 | } 34 | 35 | pub(super) fn take_stdin(&mut self) -> Option { 36 | self.inner.stdin.take() 37 | } 38 | 39 | pub(super) fn take_stdout(&mut self) -> Option { 40 | self.inner.stdout.take() 41 | } 42 | 43 | pub(super) fn take_stderr(&mut self) -> Option { 44 | self.inner.stderr.take() 45 | } 46 | 47 | pub fn inner(&mut self) -> &mut Child { 48 | &mut self.inner 49 | } 50 | 51 | pub fn into_inner(self) -> Child { 52 | // manually drop the completion port 53 | let its = mem::ManuallyDrop::new(self.handles); 54 | unsafe { CloseHandle(its.completion_port) }; 55 | // we leave the job handle unclosed, otherwise the Child is useless 56 | // (as closing it will terminate the job) 57 | 58 | // extract the Child 59 | self.inner 60 | } 61 | 62 | pub fn kill(&mut self) -> Result<()> { 63 | res_bool(unsafe { TerminateJobObject(self.handles.job, 1) }) 64 | } 65 | 66 | pub fn id(&self) -> u32 { 67 | self.inner.id() 68 | } 69 | 70 | fn wait_imp(&self, timeout: DWORD) -> Result<()> { 71 | let mut code: DWORD = 0; 72 | let mut key: ULONG_PTR = 0; 73 | let mut overlapped = mem::MaybeUninit::::uninit(); 74 | let mut lp_overlapped = overlapped.as_mut_ptr(); 75 | 76 | let result = unsafe { 77 | GetQueuedCompletionStatus( 78 | self.handles.completion_port, 79 | &mut code, 80 | &mut key, 81 | &mut lp_overlapped, 82 | timeout, 83 | ) 84 | }; 85 | 86 | // ignore timing out errors unless the timeout was specified to INFINITE 87 | // https://docs.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-getqueuedcompletionstatus 88 | if timeout != INFINITE && result == FALSE && lp_overlapped.is_null() { 89 | return Ok(()); 90 | } 91 | 92 | res_bool(result)?; 93 | 94 | Ok(()) 95 | } 96 | 97 | pub fn wait(&mut self) -> Result { 98 | self.wait_imp(INFINITE)?; 99 | self.inner.wait() 100 | } 101 | 102 | pub fn try_wait(&mut self) -> Result> { 103 | self.wait_imp(0)?; 104 | self.inner.try_wait() 105 | } 106 | 107 | pub(super) fn read_both( 108 | mut out_r: ChildStdout, 109 | out_v: &mut Vec, 110 | mut err_r: ChildStderr, 111 | err_v: &mut Vec, 112 | ) -> Result<()> { 113 | out_r.read_to_end(out_v)?; 114 | err_r.read_to_end(err_v)?; 115 | Ok(()) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/stdlib/erased.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::Result, 3 | process::{Child, ExitStatus, Output}, 4 | }; 5 | 6 | use super::GroupChild; 7 | 8 | /// Wrapper around a process child, be it grouped or ungrouped. 9 | /// 10 | /// This is a helper which erases that a [`std::process::Child`] is a different type than a 11 | /// [`GroupChild`]. It forwards to the corresponding method on the inner type. 12 | #[derive(Debug)] 13 | pub enum ErasedChild { 14 | /// A grouped process child. 15 | Grouped(GroupChild), 16 | 17 | /// An ungrouped process child. 18 | Ungrouped(Child), 19 | } 20 | 21 | impl ErasedChild { 22 | /// Returns the OS-assigned process (group) identifier. 23 | /// 24 | /// - Grouped: [`GroupChild::id`] 25 | /// - Ungrouped: [`Child::id`] 26 | pub fn id(&mut self) -> u32 { 27 | match self { 28 | Self::Grouped(c) => c.id(), 29 | Self::Ungrouped(c) => c.id(), 30 | } 31 | } 32 | 33 | /// Forces the child to exit. 34 | /// 35 | /// - Grouped: [`GroupChild::kill`] 36 | /// - Ungrouped: [`Child::kill`] 37 | pub fn kill(&mut self) -> Result<()> { 38 | match self { 39 | Self::Grouped(c) => c.kill(), 40 | Self::Ungrouped(c) => c.kill(), 41 | } 42 | } 43 | 44 | /// Attempts to collect the exit status of the child if it has already exited. 45 | /// 46 | /// - Grouped: [`GroupChild::try_wait`] 47 | /// - Ungrouped: [`Child::try_wait`] 48 | pub fn try_wait(&mut self) -> Result> { 49 | match self { 50 | Self::Grouped(c) => c.try_wait(), 51 | Self::Ungrouped(c) => c.try_wait(), 52 | } 53 | } 54 | 55 | /// Waits for the process to exit, and returns its exit status. 56 | /// 57 | /// - Grouped: [`GroupChild::wait`] 58 | /// - Ungrouped: [`Child::wait`] 59 | pub fn wait(&mut self) -> Result { 60 | match self { 61 | Self::Grouped(c) => c.wait(), 62 | Self::Ungrouped(c) => c.wait(), 63 | } 64 | } 65 | 66 | /// Waits for the process to exit, and returns its exit status. 67 | /// 68 | /// - Grouped: [`GroupChild::wait_with_output`] 69 | /// - Ungrouped: [`Child::wait_with_output`] 70 | pub fn wait_with_output(self) -> Result { 71 | match self { 72 | Self::Grouped(c) => c.wait_with_output(), 73 | Self::Ungrouped(c) => c.wait_with_output(), 74 | } 75 | } 76 | 77 | /// Sends a Unix signal to the process. 78 | /// 79 | /// - Grouped: [`GroupChild::signal`] 80 | /// - Ungrouped: [`Child::signal`] 81 | #[cfg(unix)] 82 | pub fn signal(&self, sig: crate::Signal) -> Result<()> { 83 | use crate::UnixChildExt; 84 | 85 | match self { 86 | Self::Grouped(c) => c.signal(sig), 87 | Self::Ungrouped(c) => c.signal(sig), 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/stdlib/unix.rs: -------------------------------------------------------------------------------- 1 | use std::{os::unix::process::CommandExt, process::Command}; 2 | 3 | use crate::{builder::CommandGroupBuilder, GroupChild}; 4 | 5 | impl CommandGroupBuilder<'_, Command> { 6 | /// Executes the command as a child process group, returning a handle to it. 7 | /// 8 | /// By default, stdin, stdout and stderr are inherited from the parent. 9 | /// 10 | /// On Windows, this creates a job object instead of a POSIX process group. 11 | /// 12 | /// # Examples 13 | /// 14 | /// Basic usage: 15 | /// 16 | /// ```no_run 17 | /// use std::process::Command; 18 | /// use command_group::CommandGroup; 19 | /// 20 | /// Command::new("ls") 21 | /// .group() 22 | /// .spawn() 23 | /// .expect("ls command failed to start"); 24 | /// ``` 25 | pub fn spawn(&mut self) -> std::io::Result { 26 | self.command.process_group(0).spawn().map(GroupChild::new) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/stdlib/windows.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | os::windows::{io::AsRawHandle, process::CommandExt}, 3 | process::Command, 4 | }; 5 | use winapi::um::winbase::CREATE_SUSPENDED; 6 | 7 | use crate::{builder::CommandGroupBuilder, winres::*, GroupChild}; 8 | 9 | impl CommandGroupBuilder<'_, Command> { 10 | /// Executes the command as a child process group, returning a handle to it. 11 | /// 12 | /// By default, stdin, stdout and stderr are inherited from the parent. 13 | /// 14 | /// On Windows, this creates a job object instead of a POSIX process group. 15 | /// 16 | /// # Examples 17 | /// 18 | /// Basic usage: 19 | /// 20 | /// ```no_run 21 | /// use std::process::Command; 22 | /// use command_group::CommandGroup; 23 | /// 24 | /// Command::new("ls") 25 | /// .group() 26 | /// .spawn() 27 | /// .expect("ls command failed to start"); 28 | /// ``` 29 | pub fn spawn(&mut self) -> std::io::Result { 30 | self.command 31 | .creation_flags(self.creation_flags | CREATE_SUSPENDED); 32 | 33 | let (job, completion_port) = job_object(self.kill_on_drop)?; 34 | let child = self.command.spawn()?; 35 | assign_child(child.as_raw_handle(), job)?; 36 | 37 | Ok(GroupChild::new(child, job, completion_port)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/tokio.rs: -------------------------------------------------------------------------------- 1 | //! Implementation of process group extensions for [Tokio](https://tokio.rs)’s 2 | //! asynchronous [`Command` type](::tokio::process::Command). 3 | 4 | use std::{ 5 | io::Result, 6 | process::{ExitStatus, Output}, 7 | }; 8 | 9 | use tokio::process::Command; 10 | 11 | use crate::{builder::CommandGroupBuilder, AsyncGroupChild}; 12 | 13 | #[doc(inline)] 14 | pub use erased::ErasedChild; 15 | 16 | #[cfg(target_family = "windows")] 17 | mod windows; 18 | 19 | #[cfg(target_family = "unix")] 20 | mod unix; 21 | 22 | pub(crate) mod child; 23 | pub(crate) mod erased; 24 | 25 | /// Extensions for [`Command`](::tokio::process::Command) adding support for process groups. 26 | /// 27 | /// This uses [`async_trait`] for now to provide async methods as a trait. 28 | #[async_trait::async_trait] 29 | pub trait AsyncCommandGroup { 30 | /// Executes the command as a child process group, returning a handle to it. 31 | /// 32 | /// By default, stdin, stdout and stderr are inherited from the parent. 33 | /// 34 | /// On Windows, this creates a job object instead of a POSIX process group. 35 | /// 36 | /// # Examples 37 | /// 38 | /// Basic usage: 39 | /// 40 | /// ```no_run 41 | /// # #[tokio::main] 42 | /// # async fn main() { 43 | /// use tokio::process::Command; 44 | /// use command_group::AsyncCommandGroup; 45 | /// 46 | /// Command::new("ls") 47 | /// .group_spawn() 48 | /// .expect("ls command failed to start"); 49 | /// # } 50 | /// ``` 51 | fn group_spawn(&mut self) -> Result { 52 | self.group().spawn() 53 | } 54 | 55 | /// Converts the implementor into a [`CommandGroupBuilder`](crate::CommandGroupBuilder), which can be used to 56 | /// set flags that are not available on the `Command` type. 57 | fn group(&mut self) -> crate::builder::CommandGroupBuilder; 58 | 59 | /// Executes the command as a child process group, waiting for it to finish and 60 | /// collecting all of its output. 61 | /// 62 | /// By default, stdout and stderr are captured (and used to provide the 63 | /// resulting output). Stdin is not inherited from the parent and any 64 | /// attempt by the child process to read from the stdin stream will result 65 | /// in the stream immediately closing. 66 | /// 67 | /// On Windows, this creates a job object instead of a POSIX process group. 68 | /// 69 | /// # Examples 70 | /// 71 | /// ```should_panic 72 | /// # #[tokio::main] 73 | /// # async fn main() { 74 | /// use tokio::process::Command; 75 | /// use std::io::{self, Write}; 76 | /// use command_group::AsyncCommandGroup; 77 | /// 78 | /// let output = Command::new("/bin/cat") 79 | /// .arg("file.txt") 80 | /// .group_output() 81 | /// .await 82 | /// .expect("failed to execute process"); 83 | /// 84 | /// println!("status: {}", output.status); 85 | /// io::stdout().write_all(&output.stdout).unwrap(); 86 | /// io::stderr().write_all(&output.stderr).unwrap(); 87 | /// 88 | /// assert!(output.status.success()); 89 | /// # } 90 | /// ``` 91 | async fn group_output(&mut self) -> Result { 92 | let child = self.group_spawn()?; 93 | child.wait_with_output().await 94 | } 95 | 96 | /// Executes a command as a child process group, waiting for it to finish and 97 | /// collecting its status. 98 | /// 99 | /// By default, stdin, stdout and stderr are inherited from the parent. 100 | /// 101 | /// On Windows, this creates a job object instead of a POSIX process group. 102 | /// 103 | /// # Examples 104 | /// 105 | /// ```should_panic 106 | /// # #[tokio::main] 107 | /// # async fn main() { 108 | /// use tokio::process::Command; 109 | /// use command_group::AsyncCommandGroup; 110 | /// 111 | /// let status = Command::new("/bin/cat") 112 | /// .arg("file.txt") 113 | /// .group_status() 114 | /// .await 115 | /// .expect("failed to execute process"); 116 | /// 117 | /// println!("process finished with: {}", status); 118 | /// 119 | /// assert!(status.success()); 120 | /// # } 121 | /// ``` 122 | async fn group_status(&mut self) -> Result { 123 | let mut child = self.group_spawn()?; 124 | child.wait().await 125 | } 126 | } 127 | 128 | #[async_trait::async_trait] 129 | impl AsyncCommandGroup for Command { 130 | fn group<'a>(&'a mut self) -> CommandGroupBuilder<'a, Command> { 131 | CommandGroupBuilder::new(self) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/tokio/child.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt, 3 | io::Result, 4 | process::{ExitStatus, Output}, 5 | }; 6 | 7 | use tokio::{io::AsyncReadExt, process::Child}; 8 | 9 | #[cfg(unix)] 10 | pub(self) use unix::ChildImp; 11 | #[cfg(windows)] 12 | pub(self) use windows::ChildImp; 13 | 14 | #[cfg(unix)] 15 | use nix::sys::signal::Signal; 16 | 17 | #[cfg(windows)] 18 | use winapi::um::winnt::HANDLE; 19 | 20 | #[cfg(unix)] 21 | mod unix; 22 | #[cfg(windows)] 23 | mod windows; 24 | 25 | /// Representation of a running or exited child process group (Tokio variant). 26 | /// 27 | /// This wraps Tokio’s [`Child`] type with methods that work with process groups. 28 | /// 29 | /// # Examples 30 | /// 31 | /// ```should_panic 32 | /// # #[tokio::main] 33 | /// # async fn main() { 34 | /// use tokio::process::Command; 35 | /// use command_group::AsyncCommandGroup; 36 | /// 37 | /// let mut child = Command::new("/bin/cat") 38 | /// .arg("file.txt") 39 | /// .group_spawn() 40 | /// .expect("failed to execute child"); 41 | /// 42 | /// let ecode = child.wait() 43 | /// .await 44 | /// .expect("failed to wait on child"); 45 | /// 46 | /// assert!(ecode.success()); 47 | /// # } 48 | /// ``` 49 | pub struct AsyncGroupChild { 50 | imp: ChildImp, 51 | exitstatus: Option, 52 | } 53 | 54 | impl fmt::Debug for AsyncGroupChild { 55 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 56 | f.debug_struct("AsyncGroupChild").finish() 57 | } 58 | } 59 | 60 | impl AsyncGroupChild { 61 | #[cfg(unix)] 62 | pub(crate) fn new(inner: Child) -> Self { 63 | Self { 64 | imp: ChildImp::new(inner), 65 | exitstatus: None, 66 | } 67 | } 68 | 69 | #[cfg(windows)] 70 | pub(crate) fn new(inner: Child, j: HANDLE, c: HANDLE) -> Self { 71 | Self { 72 | imp: ChildImp::new(inner, j, c), 73 | exitstatus: None, 74 | } 75 | } 76 | 77 | /// Returns the stdlib [`Child`] object. 78 | /// 79 | /// Note that the inner child may not be in the same state as this output child, due to how 80 | /// methods like `wait` and `kill` are implemented. It is not recommended to use this method 81 | /// _after_ using any of the other methods on this struct. 82 | /// 83 | /// # Examples 84 | /// 85 | /// Reading from stdout: 86 | /// 87 | /// ```no_run 88 | /// # #[tokio::main] 89 | /// # async fn main() { 90 | /// use std::process::Stdio; 91 | /// use tokio::{io::AsyncReadExt, process::Command}; 92 | /// use command_group::AsyncCommandGroup; 93 | /// 94 | /// let mut child = Command::new("ls").stdout(Stdio::piped()).group_spawn().expect("ls command didn't start"); 95 | /// let mut output = String::new(); 96 | /// if let Some(mut out) = child.inner().stdout.take() { 97 | /// out.read_to_string(&mut output).await.expect("failed to read from child"); 98 | /// } 99 | /// println!("output: {}", output); 100 | /// # } 101 | /// ``` 102 | pub fn inner(&mut self) -> &mut Child { 103 | self.imp.inner() 104 | } 105 | 106 | /// Consumes itself and returns the stdlib [`Child`] object. 107 | /// 108 | /// Note that the inner child may not be in the same state as this output child, due to how 109 | /// methods like `wait` and `kill` are implemented. It is not recommended to use this method 110 | /// _after_ using any of the other methods on this struct. 111 | /// 112 | #[cfg_attr( 113 | windows, 114 | doc = "On Windows, this unnavoidably leaves a handle unclosed. Prefer [`inner()`](Self::inner)." 115 | )] 116 | /// 117 | /// # Examples 118 | /// 119 | /// Writing to input: 120 | /// 121 | /// ```no_run 122 | /// # #[tokio::main] 123 | /// # async fn main() { 124 | /// use std::process::Stdio; 125 | /// use tokio::{io::AsyncWriteExt, process::Command}; 126 | /// use command_group::AsyncCommandGroup; 127 | /// 128 | /// let mut child = Command::new("cat").stdin(Stdio::piped()).group_spawn().expect("cat command didn't start"); 129 | /// if let Some(mut din) = child.into_inner().stdin.take() { 130 | /// din.write_all(b"Woohoo!").await.expect("failed to write"); 131 | /// } 132 | /// # } 133 | /// ``` 134 | pub fn into_inner(self) -> Child { 135 | self.imp.into_inner() 136 | } 137 | 138 | /// Forces the child process group to exit. 139 | /// 140 | /// If the group has already exited, an [`InvalidInput`] error is returned. 141 | /// 142 | /// This is equivalent to sending a SIGKILL on Unix platforms. 143 | /// 144 | /// See [the Tokio documentation](Child::kill) for more. 145 | /// 146 | /// # Examples 147 | /// 148 | /// Basic usage: 149 | /// 150 | /// ```no_run 151 | /// # #[tokio::main] 152 | /// # async fn main() { 153 | /// use tokio::process::Command; 154 | /// use command_group::AsyncCommandGroup; 155 | /// 156 | /// let mut command = Command::new("yes"); 157 | /// if let Ok(mut child) = command.group_spawn() { 158 | /// child.kill().await.expect("command wasn't running"); 159 | /// } else { 160 | /// println!("yes command didn't start"); 161 | /// } 162 | /// # } 163 | /// ``` 164 | /// 165 | /// [`InvalidInput`]: std::io::ErrorKind::InvalidInput 166 | pub async fn kill(&mut self) -> Result<()> { 167 | self.start_kill()?; 168 | self.wait().await?; 169 | Ok(()) 170 | } 171 | 172 | /// Attempts to force the child to exit, but does not wait for the request to take effect. 173 | /// 174 | /// This is equivalent to sending a SIGKILL on Unix platforms. 175 | /// 176 | /// Note that on Unix platforms it is possible for a zombie process to remain after a kill is 177 | /// sent; to avoid this, the caller should ensure that either `child.wait().await` or 178 | /// `child.try_wait()` is invoked successfully. 179 | /// 180 | /// See [the Tokio documentation](Child::start_kill) for more. 181 | /// 182 | /// [`InvalidInput`]: std::io::ErrorKind::InvalidInput 183 | pub fn start_kill(&mut self) -> Result<()> { 184 | self.imp.start_kill() 185 | } 186 | 187 | /// Returns the OS-assigned process group identifier. 188 | /// 189 | /// Like Tokio, this returns `None` if the child process group has alread exited, to avoid 190 | /// holding onto an expired (and possibly reused) PGID. 191 | /// 192 | /// See [the Tokio documentation](Child::id) for more. 193 | /// 194 | /// # Examples 195 | /// 196 | /// Basic usage: 197 | /// 198 | /// ```no_run 199 | /// # #[tokio::main] 200 | /// # async fn main() { 201 | /// use tokio::process::Command; 202 | /// use command_group::AsyncCommandGroup; 203 | /// 204 | /// let mut command = Command::new("ls"); 205 | /// if let Ok(child) = command.group_spawn() { 206 | /// if let Some(pgid) = child.id() { 207 | /// println!("Child group's ID is {}", pgid); 208 | /// } else { 209 | /// println!("Child group is gone"); 210 | /// } 211 | /// } else { 212 | /// println!("ls command didn't start"); 213 | /// } 214 | /// # } 215 | /// ``` 216 | pub fn id(&self) -> Option { 217 | self.imp.id() 218 | } 219 | 220 | /// Waits for the child group to exit completely, returning the status that the process leader 221 | /// exited with. 222 | /// 223 | /// See [the Tokio documentation](Child::wait) for more. 224 | /// 225 | /// The current implementation spawns a blocking task on the Tokio thread pool; contributions 226 | /// are welcome for a better version. 227 | /// 228 | /// An important consideration on Unix platforms is that there is no way to cancel the `wait` 229 | /// syscall. _Cancelling this future_ will **not** cancel that underlying `wait` call. That has 230 | /// consequences: a `wait`ed process that exits will have its resources cleaned up by the kernel. 231 | /// If the application is no longer listening for that `wait` returning, it will not know that 232 | /// the process has been cleaned up, and will try to wait on it again. That in turn may fail, or 233 | /// could even attach to a recycled PID which would then point to a completely different process. 234 | /// 235 | /// # Examples 236 | /// 237 | /// Basic usage: 238 | /// 239 | /// ```no_run 240 | /// # #[tokio::main] 241 | /// # async fn main() { 242 | /// use tokio::process::Command; 243 | /// use command_group::AsyncCommandGroup; 244 | /// 245 | /// let mut command = Command::new("ls"); 246 | /// if let Ok(mut child) = command.group_spawn() { 247 | /// child.wait().await.expect("command wasn't running"); 248 | /// println!("Child has finished its execution!"); 249 | /// } else { 250 | /// println!("ls command didn't start"); 251 | /// } 252 | /// # } 253 | /// ``` 254 | pub async fn wait(&mut self) -> Result { 255 | if let Some(es) = self.exitstatus { 256 | return Ok(es); 257 | } 258 | 259 | drop(self.imp.take_stdin()); 260 | let status = self.imp.wait().await?; 261 | self.exitstatus = Some(status); 262 | Ok(status) 263 | } 264 | 265 | /// Attempts to collect the exit status of the child if it has already exited. 266 | /// 267 | /// See [the Tokio documentation](Child::try_wait) for more. 268 | /// 269 | /// # Examples 270 | /// 271 | /// Basic usage: 272 | /// 273 | /// ```no_run 274 | /// # #[tokio::main] 275 | /// # async fn main() { 276 | /// use tokio::process::Command; 277 | /// use command_group::AsyncCommandGroup; 278 | /// 279 | /// let mut child = Command::new("ls").group_spawn().unwrap(); 280 | /// 281 | /// match child.try_wait() { 282 | /// Ok(Some(status)) => println!("exited with: {}", status), 283 | /// Ok(None) => { 284 | /// println!("status not ready yet, let's really wait"); 285 | /// let res = child.wait().await; 286 | /// println!("result: {:?}", res); 287 | /// } 288 | /// Err(e) => println!("error attempting to wait: {}", e), 289 | /// } 290 | /// # } 291 | /// ``` 292 | pub fn try_wait(&mut self) -> Result> { 293 | if self.exitstatus.is_some() { 294 | return Ok(self.exitstatus); 295 | } 296 | 297 | match self.imp.try_wait()? { 298 | Some(es) => { 299 | self.exitstatus = Some(es); 300 | Ok(Some(es)) 301 | } 302 | None => Ok(None), 303 | } 304 | } 305 | 306 | /// Simultaneously waits for the child to exit and collect all remaining output on the 307 | /// stdout/stderr handles, returning an `Output` instance. 308 | /// 309 | /// See [the Tokio documentation](Child::wait_with_output) for more. 310 | /// 311 | /// # Examples 312 | /// 313 | /// Basic usage: 314 | /// 315 | /// ```should_panic 316 | /// # #[tokio::main] 317 | /// # async fn main() { 318 | /// use std::process::Stdio; 319 | /// use tokio::process::Command; 320 | /// use command_group::AsyncCommandGroup; 321 | /// 322 | /// let child = Command::new("/bin/cat") 323 | /// .arg("file.txt") 324 | /// .stdout(Stdio::piped()) 325 | /// .group_spawn() 326 | /// .expect("failed to execute child"); 327 | /// 328 | /// let output = child 329 | /// .wait_with_output() 330 | /// .await 331 | /// .expect("failed to wait on child"); 332 | /// 333 | /// assert!(output.status.success()); 334 | /// # } 335 | /// ``` 336 | pub async fn wait_with_output(mut self) -> Result { 337 | drop(self.imp.take_stdin()); 338 | 339 | let (mut stdout, mut stderr) = (Vec::new(), Vec::new()); 340 | match (self.imp.take_stdout(), self.imp.take_stderr()) { 341 | (None, None) => {} 342 | (Some(mut out), None) => { 343 | out.read_to_end(&mut stdout).await?; 344 | } 345 | (None, Some(mut err)) => { 346 | err.read_to_end(&mut stderr).await?; 347 | } 348 | (Some(mut out), Some(mut err)) => { 349 | // TODO: replace with futures crate usage 350 | // and drop macros feature from tokio 351 | tokio::try_join!(out.read_to_end(&mut stdout), err.read_to_end(&mut stderr),)?; 352 | } 353 | } 354 | 355 | let status = self.imp.wait().await?; 356 | Ok(Output { 357 | status, 358 | stdout, 359 | stderr, 360 | }) 361 | } 362 | } 363 | 364 | #[cfg(unix)] 365 | impl crate::UnixChildExt for AsyncGroupChild { 366 | fn signal(&self, sig: Signal) -> Result<()> { 367 | self.imp.signal_imp(sig) 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /src/tokio/child/unix.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | convert::TryInto, 3 | io::{Error, Result}, 4 | ops::ControlFlow, 5 | os::unix::process::ExitStatusExt, 6 | process::ExitStatus, 7 | }; 8 | 9 | use nix::{ 10 | errno::Errno, 11 | libc, 12 | sys::{ 13 | signal::{killpg, Signal}, 14 | wait::WaitPidFlag, 15 | }, 16 | unistd::Pid, 17 | }; 18 | use tokio::{ 19 | process::{Child, ChildStderr, ChildStdin, ChildStdout}, 20 | task::spawn_blocking, 21 | }; 22 | 23 | pub(super) struct ChildImp { 24 | pgid: Pid, 25 | inner: Child, 26 | } 27 | 28 | impl ChildImp { 29 | pub(super) fn new(inner: Child) -> Self { 30 | let pid = inner 31 | .id() 32 | .expect("Command was reaped before we could read its PID") 33 | .try_into() 34 | .expect("Command PID > i32::MAX"); 35 | Self { 36 | pgid: Pid::from_raw(pid), 37 | inner, 38 | } 39 | } 40 | 41 | pub(super) fn take_stdin(&mut self) -> Option { 42 | self.inner.stdin.take() 43 | } 44 | 45 | pub(super) fn take_stdout(&mut self) -> Option { 46 | self.inner.stdout.take() 47 | } 48 | 49 | pub(super) fn take_stderr(&mut self) -> Option { 50 | self.inner.stderr.take() 51 | } 52 | 53 | pub fn inner(&mut self) -> &mut Child { 54 | &mut self.inner 55 | } 56 | 57 | pub fn into_inner(self) -> Child { 58 | self.inner 59 | } 60 | 61 | pub(super) fn signal_imp(&self, sig: Signal) -> Result<()> { 62 | killpg(self.pgid, sig).map_err(Error::from) 63 | } 64 | 65 | pub fn start_kill(&mut self) -> Result<()> { 66 | self.signal_imp(Signal::SIGKILL) 67 | } 68 | 69 | pub fn id(&self) -> Option { 70 | self.inner.id() 71 | } 72 | 73 | fn wait_imp(pgid: i32, flag: WaitPidFlag) -> Result>> { 74 | // Wait for processes in a loop until every process in this 75 | // process group has exited (this ensures that we reap any 76 | // zombies that may have been created if the parent exited after 77 | // spawning children, but didn't wait for those children to 78 | // exit). 79 | let mut parent_exit_status: Option = None; 80 | loop { 81 | // we can't use the safe wrapper directly because it doesn't 82 | // return the raw status, and we need it to convert to the 83 | // std's ExitStatus. 84 | let mut status: i32 = 0; 85 | match unsafe { libc::waitpid(-pgid, &mut status as *mut libc::c_int, flag.bits()) } { 86 | 0 => { 87 | // Zero should only happen if WNOHANG was passed in, 88 | // and means that no processes have yet to exit. 89 | return Ok(ControlFlow::Continue(())); 90 | } 91 | -1 => { 92 | match Errno::last() { 93 | Errno::ECHILD => { 94 | // No more children to reap; this is a 95 | // graceful exit. 96 | return Ok(ControlFlow::Break(parent_exit_status)); 97 | } 98 | errno => { 99 | return Err(Error::from(errno)); 100 | } 101 | } 102 | } 103 | pid => { 104 | // *A* process exited. Was it the parent process 105 | // that we started? If so, collect the exit signal, 106 | // otherwise we reaped a zombie process and should 107 | // continue in the loop. 108 | if pgid == pid { 109 | parent_exit_status = Some(ExitStatus::from_raw(status)); 110 | } else { 111 | // Reaped a zombie child; keep looping. 112 | } 113 | } 114 | }; 115 | } 116 | } 117 | 118 | pub async fn wait(&mut self) -> Result { 119 | const MAX_RETRY_ATTEMPT: usize = 10; 120 | 121 | // Always wait for parent to exit first. 122 | // 123 | // It's likely that all its children has already exited and reaped by 124 | // the time the parent exits. 125 | let status = self.inner.wait().await?; 126 | 127 | let pgid = self.pgid.as_raw(); 128 | 129 | // Try reaping all children, if there are some that are still alive after 130 | // several attempts, then spawn a blocking task to reap them. 131 | for retry_attempt in 1..=MAX_RETRY_ATTEMPT { 132 | if Self::wait_imp(pgid, WaitPidFlag::WNOHANG)?.is_break() { 133 | break; 134 | } else if retry_attempt == MAX_RETRY_ATTEMPT { 135 | spawn_blocking(move || Self::wait_imp(pgid, WaitPidFlag::empty())).await??; 136 | } 137 | } 138 | 139 | Ok(status) 140 | } 141 | 142 | pub fn try_wait(&mut self) -> Result> { 143 | match Self::wait_imp(self.pgid.as_raw(), WaitPidFlag::WNOHANG)? { 144 | ControlFlow::Break(res) => Ok(res), 145 | ControlFlow::Continue(()) => self.inner.try_wait(), 146 | } 147 | } 148 | } 149 | 150 | impl crate::UnixChildExt for ChildImp { 151 | fn signal(&self, sig: Signal) -> Result<()> { 152 | self.signal_imp(sig) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/tokio/child/windows.rs: -------------------------------------------------------------------------------- 1 | use std::{io::Result, mem, ops::ControlFlow, process::ExitStatus}; 2 | use tokio::{ 3 | process::{Child, ChildStderr, ChildStdin, ChildStdout}, 4 | task::spawn_blocking, 5 | }; 6 | use winapi::{ 7 | shared::{ 8 | basetsd::ULONG_PTR, 9 | minwindef::{DWORD, FALSE}, 10 | }, 11 | um::{ 12 | handleapi::CloseHandle, ioapiset::GetQueuedCompletionStatus, jobapi2::TerminateJobObject, 13 | minwinbase::OVERLAPPED, winbase::INFINITE, winnt::HANDLE, 14 | }, 15 | }; 16 | 17 | use crate::winres::*; 18 | 19 | pub(super) struct ChildImp { 20 | inner: Child, 21 | handles: JobPort, 22 | } 23 | 24 | impl ChildImp { 25 | pub fn new(inner: Child, job: HANDLE, completion_port: HANDLE) -> Self { 26 | Self { 27 | inner, 28 | handles: JobPort { 29 | job, 30 | completion_port, 31 | }, 32 | } 33 | } 34 | 35 | pub(super) fn take_stdin(&mut self) -> Option { 36 | self.inner.stdin.take() 37 | } 38 | 39 | pub(super) fn take_stdout(&mut self) -> Option { 40 | self.inner.stdout.take() 41 | } 42 | 43 | pub(super) fn take_stderr(&mut self) -> Option { 44 | self.inner.stderr.take() 45 | } 46 | 47 | pub fn inner(&mut self) -> &mut Child { 48 | &mut self.inner 49 | } 50 | 51 | pub fn into_inner(self) -> Child { 52 | let its = mem::ManuallyDrop::new(self.handles); 53 | 54 | // manually drop the completion port 55 | unsafe { CloseHandle(its.completion_port) }; 56 | // we leave the job handle unclosed, otherwise the Child is useless 57 | // (as closing it will terminate the job) 58 | 59 | self.inner 60 | } 61 | 62 | pub fn start_kill(&mut self) -> Result<()> { 63 | res_bool(unsafe { TerminateJobObject(self.handles.job, 1) }) 64 | } 65 | 66 | pub fn id(&self) -> Option { 67 | self.inner.id() 68 | } 69 | 70 | fn wait_imp(completion_port: ThreadSafeRawHandle, timeout: DWORD) -> Result> { 71 | let mut code: DWORD = 0; 72 | let mut key: ULONG_PTR = 0; 73 | let mut overlapped = mem::MaybeUninit::::uninit(); 74 | let mut lp_overlapped = overlapped.as_mut_ptr(); 75 | 76 | let result = unsafe { 77 | GetQueuedCompletionStatus( 78 | completion_port.0, 79 | &mut code, 80 | &mut key, 81 | &mut lp_overlapped, 82 | timeout, 83 | ) 84 | }; 85 | 86 | // ignore timing out errors unless the timeout was specified to INFINITE 87 | // https://docs.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-getqueuedcompletionstatus 88 | if timeout != INFINITE && result == FALSE && lp_overlapped.is_null() { 89 | return Ok(ControlFlow::Continue(())); 90 | } 91 | 92 | res_bool(result)?; 93 | 94 | Ok(ControlFlow::Break(())) 95 | } 96 | 97 | pub async fn wait(&mut self) -> Result { 98 | const MAX_RETRY_ATTEMPT: usize = 10; 99 | 100 | // Always wait for parent to exit first. 101 | // 102 | // It's likely that all its children has already exited and reaped by 103 | // the time the parent exits. 104 | let status = self.inner.wait().await?; 105 | 106 | let completion_port = ThreadSafeRawHandle(self.handles.completion_port); 107 | 108 | // Try waiting for group exit, if it is still alive after several 109 | // attempts, then spawn a blocking task to reap them. 110 | for retry_attempt in 1..=MAX_RETRY_ATTEMPT { 111 | if Self::wait_imp(completion_port, 0)?.is_break() { 112 | break; 113 | } else if retry_attempt == MAX_RETRY_ATTEMPT { 114 | spawn_blocking(move || Self::wait_imp(completion_port, INFINITE)).await??; 115 | } 116 | } 117 | 118 | Ok(status) 119 | } 120 | 121 | pub fn try_wait(&mut self) -> Result> { 122 | Self::wait_imp(ThreadSafeRawHandle(self.handles.completion_port), 0)?; 123 | self.inner.try_wait() 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/tokio/erased.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::Result, 3 | process::{ExitStatus, Output}, 4 | }; 5 | 6 | use super::AsyncGroupChild; 7 | use tokio::process::Child; 8 | 9 | /// Wrapper around a process child, be it grouped or ungrouped. 10 | /// 11 | /// This is a helper which erases that a [`tokio::process::Child`] is a different type than an 12 | /// [`AsyncGroupChild`]. It forwards to the corresponding method on the inner type. 13 | #[derive(Debug)] 14 | pub enum ErasedChild { 15 | /// A grouped process child. 16 | Grouped(AsyncGroupChild), 17 | 18 | /// An ungrouped process child. 19 | Ungrouped(Child), 20 | } 21 | 22 | impl ErasedChild { 23 | /// Returns the OS-assigned process (group) identifier. 24 | /// 25 | /// - Grouped: [`AsyncGroupChild::id`] 26 | /// - Ungrouped: [`Child::id`] 27 | pub fn id(&mut self) -> Option { 28 | match self { 29 | Self::Grouped(c) => c.id(), 30 | Self::Ungrouped(c) => c.id(), 31 | } 32 | } 33 | 34 | /// Forces the child to exit. 35 | /// 36 | /// - Grouped: [`AsyncGroupChild::kill`] 37 | /// - Ungrouped: [`Child::kill`] 38 | pub async fn kill(&mut self) -> Result<()> { 39 | match self { 40 | Self::Grouped(c) => c.kill().await, 41 | Self::Ungrouped(c) => c.kill().await, 42 | } 43 | } 44 | 45 | /// Attempts to force the child to exit, but does not wait for the request to take effect. 46 | /// 47 | /// - Grouped: [`AsyncGroupChild::start_kill`] 48 | /// - Ungrouped: [`Child::start_kill`] 49 | pub fn start_kill(&mut self) -> Result<()> { 50 | match self { 51 | Self::Grouped(c) => c.start_kill(), 52 | Self::Ungrouped(c) => c.start_kill(), 53 | } 54 | } 55 | 56 | /// Attempts to collect the exit status of the child if it has already exited. 57 | /// 58 | /// - Grouped: [`AsyncGroupChild::try_wait`] 59 | /// - Ungrouped: [`Child::try_wait`] 60 | pub fn try_wait(&mut self) -> Result> { 61 | match self { 62 | Self::Grouped(c) => c.try_wait(), 63 | Self::Ungrouped(c) => c.try_wait(), 64 | } 65 | } 66 | 67 | /// Waits for the process to exit, and returns its exit status. 68 | /// 69 | /// - Grouped: [`AsyncGroupChild::wait`] 70 | /// - Ungrouped: [`Child::wait`] 71 | pub async fn wait(&mut self) -> Result { 72 | match self { 73 | Self::Grouped(c) => c.wait().await, 74 | Self::Ungrouped(c) => c.wait().await, 75 | } 76 | } 77 | 78 | /// Waits for the process to exit, and returns its exit status. 79 | /// 80 | /// - Grouped: [`AsyncGroupChild::wait_with_output`] 81 | /// - Ungrouped: [`Child::wait_with_output`] 82 | pub async fn wait_with_output(self) -> Result { 83 | match self { 84 | Self::Grouped(c) => c.wait_with_output().await, 85 | Self::Ungrouped(c) => c.wait_with_output().await, 86 | } 87 | } 88 | 89 | /// Sends a Unix signal to the process. 90 | /// 91 | /// - Grouped: [`AsyncGroupChild::signal`] 92 | /// - Ungrouped: [`Child::signal`] 93 | #[cfg(unix)] 94 | pub fn signal(&self, sig: crate::Signal) -> Result<()> { 95 | use crate::UnixChildExt; 96 | 97 | match self { 98 | Self::Grouped(c) => c.signal(sig), 99 | Self::Ungrouped(c) => c.signal(sig), 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/tokio/unix.rs: -------------------------------------------------------------------------------- 1 | use crate::builder::CommandGroupBuilder; 2 | use crate::AsyncGroupChild; 3 | 4 | impl CommandGroupBuilder<'_, tokio::process::Command> { 5 | /// Executes the command as a child process group, returning a handle to it. 6 | /// 7 | /// By default, stdin, stdout and stderr are inherited from the parent. 8 | /// 9 | /// On Windows, this creates a job object instead of a POSIX process group. 10 | /// 11 | /// # Examples 12 | /// 13 | /// Basic usage: 14 | /// 15 | /// ```no_run 16 | /// use tokio::process::Command; 17 | /// use command_group::AsyncCommandGroup; 18 | /// 19 | /// Command::new("ls") 20 | /// .group() 21 | /// .spawn() 22 | /// .expect("ls command failed to start"); 23 | /// ``` 24 | pub fn spawn(&mut self) -> std::io::Result { 25 | #[cfg(tokio_unstable)] 26 | { 27 | self.command.process_group(0); 28 | } 29 | 30 | #[cfg(not(tokio_unstable))] 31 | unsafe { 32 | use nix::unistd::{setpgid, Pid}; 33 | use std::io::Error; 34 | self.command.pre_exec(|| { 35 | setpgid(Pid::this(), Pid::from_raw(0)) 36 | .map_err(Error::from) 37 | .map(|_| ()) 38 | }); 39 | } 40 | 41 | self.command.spawn().map(AsyncGroupChild::new) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/tokio/windows.rs: -------------------------------------------------------------------------------- 1 | use tokio::process::Command; 2 | use winapi::um::winbase::CREATE_SUSPENDED; 3 | 4 | use crate::{builder::CommandGroupBuilder, winres::*, AsyncGroupChild}; 5 | 6 | impl CommandGroupBuilder<'_, Command> { 7 | /// Executes the command as a child process group, returning a handle to it. 8 | /// 9 | /// By default, stdin, stdout and stderr are inherited from the parent. 10 | /// 11 | /// On Windows, this creates a job object instead of a POSIX process group. 12 | /// 13 | /// # Examples 14 | /// 15 | /// Basic usage: 16 | /// 17 | /// ```no_run 18 | /// use tokio::process::Command; 19 | /// use command_group::CommandGroup; 20 | /// 21 | /// Command::new("ls") 22 | /// .group() 23 | /// .spawn() 24 | /// .expect("ls command failed to start"); 25 | /// ``` 26 | pub fn spawn(&mut self) -> std::io::Result { 27 | let (job, completion_port) = job_object(self.kill_on_drop)?; 28 | self.command 29 | .creation_flags(self.creation_flags | CREATE_SUSPENDED); 30 | 31 | let child = self.command.spawn()?; 32 | assign_child( 33 | child 34 | .raw_handle() 35 | .expect("child has exited but it has not even started"), 36 | job, 37 | )?; 38 | 39 | Ok(AsyncGroupChild::new(child, job, completion_port)) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/unix_ext.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | convert::TryInto, 3 | io::{Error, Result}, 4 | process::Child, 5 | }; 6 | 7 | use nix::{ 8 | sys::signal::{kill, Signal}, 9 | unistd::Pid, 10 | }; 11 | 12 | /// Unix-specific extensions to process [`Child`]ren. 13 | pub trait UnixChildExt { 14 | /// Sends a signal to the child process. If the process has already exited, an [`InvalidInput`] 15 | /// error is returned. 16 | /// 17 | /// # Examples 18 | /// 19 | /// Basic usage: 20 | /// 21 | /// ```no_run 22 | /// use std::process::Command; 23 | /// use command_group::{UnixChildExt, Signal}; 24 | /// 25 | /// let mut command = Command::new("yes"); 26 | /// if let Ok(mut child) = command.spawn() { 27 | /// child.signal(Signal::SIGTERM).expect("command wasn't running"); 28 | /// } else { 29 | /// println!("yes command didn't start"); 30 | /// } 31 | /// ``` 32 | /// 33 | /// With a process group: 34 | /// 35 | /// ```no_run 36 | /// use std::process::Command; 37 | /// use command_group::{CommandGroup, UnixChildExt, Signal}; 38 | /// 39 | /// let mut command = Command::new("yes"); 40 | /// if let Ok(mut child) = command.group_spawn() { 41 | /// child.signal(Signal::SIGTERM).expect("command wasn't running"); 42 | /// } else { 43 | /// println!("yes command didn't start"); 44 | /// } 45 | /// ``` 46 | /// 47 | /// [`InvalidInput`]: std::io::ErrorKind::InvalidInput 48 | fn signal(&self, sig: Signal) -> Result<()>; 49 | } 50 | 51 | impl UnixChildExt for Child { 52 | fn signal(&self, sig: Signal) -> Result<()> { 53 | let pid = Pid::from_raw(self.id().try_into().expect("Command PID > i32::MAX")); 54 | kill(pid, sig).map_err(Error::from) 55 | } 56 | } 57 | 58 | #[cfg(feature = "with-tokio")] 59 | impl UnixChildExt for ::tokio::process::Child { 60 | fn signal(&self, sig: Signal) -> Result<()> { 61 | if let Some(id) = self.id() { 62 | let pid = Pid::from_raw(id.try_into().expect("Command PID > i32::MAX")); 63 | kill(pid, sig).map_err(Error::from) 64 | } else { 65 | Ok(()) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/winres.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | convert::TryInto, 3 | io::{Error, Result}, 4 | mem, 5 | os::windows::io::RawHandle, 6 | ptr, 7 | }; 8 | use winapi::{ 9 | shared::minwindef::{BOOL, DWORD, FALSE, LPVOID}, 10 | um::{ 11 | handleapi::{CloseHandle, INVALID_HANDLE_VALUE}, 12 | ioapiset::CreateIoCompletionPort, 13 | jobapi2::{AssignProcessToJobObject, CreateJobObjectW, SetInformationJobObject}, 14 | processthreadsapi::{GetProcessId, OpenThread, ResumeThread}, 15 | tlhelp32::{ 16 | CreateToolhelp32Snapshot, Thread32First, Thread32Next, TH32CS_SNAPTHREAD, THREADENTRY32, 17 | }, 18 | winnt::{ 19 | JobObjectAssociateCompletionPortInformation, JobObjectExtendedLimitInformation, HANDLE, 20 | JOBOBJECT_ASSOCIATE_COMPLETION_PORT, JOBOBJECT_EXTENDED_LIMIT_INFORMATION, 21 | JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, 22 | }, 23 | }, 24 | }; 25 | 26 | #[derive(Clone)] 27 | pub(crate) struct JobPort { 28 | pub job: HANDLE, 29 | pub completion_port: HANDLE, 30 | } 31 | 32 | impl Drop for JobPort { 33 | fn drop(&mut self) { 34 | unsafe { CloseHandle(self.job) }; 35 | unsafe { CloseHandle(self.completion_port) }; 36 | } 37 | } 38 | 39 | unsafe impl Send for JobPort {} 40 | unsafe impl Sync for JobPort {} 41 | 42 | #[derive(Copy, Clone)] 43 | #[repr(transparent)] 44 | pub(crate) struct ThreadSafeRawHandle(pub HANDLE); 45 | 46 | unsafe impl Send for ThreadSafeRawHandle {} 47 | unsafe impl Sync for ThreadSafeRawHandle {} 48 | 49 | pub(crate) fn res_null(handle: HANDLE) -> Result { 50 | if handle.is_null() { 51 | Err(Error::last_os_error()) 52 | } else { 53 | Ok(handle) 54 | } 55 | } 56 | 57 | pub(crate) fn res_bool(ret: BOOL) -> Result<()> { 58 | if ret == FALSE { 59 | Err(Error::last_os_error()) 60 | } else { 61 | Ok(()) 62 | } 63 | } 64 | 65 | pub(crate) fn res_neg(ret: DWORD) -> Result { 66 | if ret == DWORD::MAX { 67 | Err(Error::last_os_error()) 68 | } else { 69 | Ok(ret) 70 | } 71 | } 72 | 73 | pub(crate) fn job_object(kill_on_drop: bool) -> Result<(HANDLE, HANDLE)> { 74 | let job = res_null(unsafe { CreateJobObjectW(ptr::null_mut(), ptr::null()) })?; 75 | 76 | let completion_port = 77 | res_null(unsafe { CreateIoCompletionPort(INVALID_HANDLE_VALUE, ptr::null_mut(), 0, 1) })?; 78 | 79 | let mut associate_completion = JOBOBJECT_ASSOCIATE_COMPLETION_PORT { 80 | CompletionKey: job, 81 | CompletionPort: completion_port, 82 | }; 83 | 84 | res_bool(unsafe { 85 | SetInformationJobObject( 86 | job, 87 | JobObjectAssociateCompletionPortInformation, 88 | &mut associate_completion as *mut _ as LPVOID, 89 | mem::size_of_val(&associate_completion) 90 | .try_into() 91 | .expect("cannot safely cast to DWORD"), 92 | ) 93 | })?; 94 | 95 | let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default(); 96 | 97 | if kill_on_drop { 98 | info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; 99 | } 100 | 101 | res_bool(unsafe { 102 | SetInformationJobObject( 103 | job, 104 | JobObjectExtendedLimitInformation, 105 | &mut info as *mut _ as LPVOID, 106 | mem::size_of_val(&info) 107 | .try_into() 108 | .expect("cannot safely cast to DWORD"), 109 | ) 110 | })?; 111 | 112 | Ok((job, completion_port)) 113 | } 114 | 115 | // This is pretty terrible, but it's either this or we re-implement all of Rust's std::process just 116 | // to get at PROCESS_INFORMATION! 117 | fn resume_threads(child_process: HANDLE) -> Result<()> { 118 | let child_id = unsafe { GetProcessId(child_process) }; 119 | 120 | let h = res_null(unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) })?; 121 | let mut entry = THREADENTRY32 { 122 | dwSize: 28, 123 | cntUsage: 0, 124 | th32ThreadID: 0, 125 | th32OwnerProcessID: 0, 126 | tpBasePri: 0, 127 | tpDeltaPri: 0, 128 | dwFlags: 0, 129 | }; 130 | 131 | let mut res = res_bool(unsafe { Thread32First(h, &mut entry) }); 132 | while res.is_ok() { 133 | if entry.th32OwnerProcessID == child_id { 134 | let thread_handle = res_null(unsafe { OpenThread(0x0002, 0, entry.th32ThreadID) })?; 135 | res_neg(unsafe { ResumeThread(thread_handle) })?; 136 | res_bool(unsafe { CloseHandle(thread_handle) })?; 137 | } 138 | 139 | res = res_bool(unsafe { Thread32Next(h, &mut entry) }); 140 | } 141 | 142 | res_bool(unsafe { CloseHandle(h) }) 143 | } 144 | 145 | pub(crate) fn assign_child(handle: RawHandle, job: HANDLE) -> Result<()> { 146 | let handle = handle as _; 147 | res_bool(unsafe { AssignProcessToJobObject(job, handle) })?; 148 | resume_threads(handle)?; 149 | Ok(()) 150 | } 151 | -------------------------------------------------------------------------------- /tests/stdlib_unix.rs: -------------------------------------------------------------------------------- 1 | #![cfg(unix)] 2 | 3 | use command_group::{CommandGroup, Signal, UnixChildExt}; 4 | use std::{ 5 | io::{Read, Result, Write}, 6 | os::unix::process::ExitStatusExt, 7 | process::{Command, Stdio}, 8 | thread::sleep, 9 | time::Duration, 10 | }; 11 | 12 | const DIE_TIME: Duration = Duration::from_millis(100); 13 | 14 | // each test has a _normal variant that uses the stdlib non-group API for comparison/debugging. 15 | 16 | #[test] 17 | fn inner_read_stdout_normal() -> Result<()> { 18 | let mut child = Command::new("echo") 19 | .arg("hello") 20 | .stdout(Stdio::piped()) 21 | .spawn()?; 22 | 23 | let mut output = String::new(); 24 | if let Some(mut out) = child.stdout.take() { 25 | out.read_to_string(&mut output)?; 26 | } 27 | 28 | assert_eq!(output.as_str(), "hello\n"); 29 | Ok(()) 30 | } 31 | 32 | #[test] 33 | fn inner_read_stdout_group() -> Result<()> { 34 | let mut child = Command::new("echo") 35 | .arg("hello") 36 | .stdout(Stdio::piped()) 37 | .group_spawn()?; 38 | 39 | let mut output = String::new(); 40 | if let Some(mut out) = child.inner().stdout.take() { 41 | out.read_to_string(&mut output)?; 42 | } 43 | 44 | assert_eq!(output.as_str(), "hello\n"); 45 | Ok(()) 46 | } 47 | 48 | #[test] 49 | fn into_inner_write_stdin_normal() -> Result<()> { 50 | let mut child = Command::new("cat") 51 | .stdin(Stdio::piped()) 52 | .stdout(Stdio::piped()) 53 | .spawn()?; 54 | 55 | if let Some(mut din) = child.stdin.take() { 56 | din.write_all(b"hello")?; 57 | } 58 | 59 | let mut output = String::new(); 60 | if let Some(mut out) = child.stdout.take() { 61 | out.read_to_string(&mut output)?; 62 | } 63 | 64 | assert_eq!(output.as_str(), "hello"); 65 | Ok(()) 66 | } 67 | 68 | #[test] 69 | fn into_inner_write_stdin_group() -> Result<()> { 70 | let mut child = Command::new("cat") 71 | .stdin(Stdio::piped()) 72 | .stdout(Stdio::piped()) 73 | .group_spawn()? 74 | .into_inner(); 75 | 76 | if let Some(mut din) = child.stdin.take() { 77 | din.write_all(b"hello")?; 78 | } 79 | 80 | let mut output = String::new(); 81 | if let Some(mut out) = child.stdout.take() { 82 | out.read_to_string(&mut output)?; 83 | } 84 | 85 | assert_eq!(output.as_str(), "hello"); 86 | Ok(()) 87 | } 88 | 89 | #[test] 90 | fn kill_and_try_wait_normal() -> Result<()> { 91 | let mut child = Command::new("yes").stdout(Stdio::null()).spawn()?; 92 | assert!(child.try_wait()?.is_none()); 93 | child.kill()?; 94 | sleep(DIE_TIME); 95 | assert!(child.try_wait()?.is_some()); 96 | sleep(DIE_TIME); 97 | assert!(child.try_wait()?.is_some()); 98 | Ok(()) 99 | } 100 | 101 | #[test] 102 | fn kill_and_try_wait_group() -> Result<()> { 103 | let mut child = Command::new("yes").stdout(Stdio::null()).group_spawn()?; 104 | assert!(child.try_wait()?.is_none()); 105 | child.kill()?; 106 | sleep(DIE_TIME); 107 | assert!(child.try_wait()?.is_some()); 108 | sleep(DIE_TIME); 109 | assert!(child.try_wait()?.is_some()); 110 | Ok(()) 111 | } 112 | 113 | #[test] 114 | fn try_wait_twice_after_sigterm_normal() -> Result<()> { 115 | let mut child = Command::new("yes").stdout(Stdio::null()).spawn()?; 116 | assert!(child.try_wait()?.is_none(), "pre try_wait"); 117 | child.signal(Signal::SIGTERM)?; 118 | sleep(DIE_TIME); 119 | assert!(child.try_wait()?.is_some(), "first try_wait"); 120 | sleep(DIE_TIME); 121 | assert!(child.try_wait()?.is_some(), "second try_wait"); 122 | Ok(()) 123 | } 124 | 125 | #[test] 126 | fn try_wait_twice_after_sigterm_group() -> Result<()> { 127 | let mut child = Command::new("yes").stdout(Stdio::null()).group_spawn()?; 128 | assert!(child.try_wait()?.is_none(), "pre try_wait"); 129 | child.signal(Signal::SIGTERM)?; 130 | sleep(DIE_TIME); 131 | assert!(child.try_wait()?.is_some(), "first try_wait"); 132 | sleep(DIE_TIME); 133 | assert!(child.try_wait()?.is_some(), "second try_wait"); 134 | Ok(()) 135 | } 136 | 137 | #[test] 138 | fn wait_twice_after_sigterm_normal() -> Result<()> { 139 | let mut child = Command::new("yes").stdout(Stdio::null()).spawn()?; 140 | assert!(child.try_wait()?.is_none(), "pre try_wait"); 141 | child.signal(Signal::SIGTERM)?; 142 | let status = child.wait()?; 143 | assert_eq!( 144 | status.signal(), 145 | Some(Signal::SIGTERM as i32), 146 | "first wait status" 147 | ); 148 | let status = child.wait()?; 149 | assert_eq!( 150 | status.signal(), 151 | Some(Signal::SIGTERM as i32), 152 | "second wait status" 153 | ); 154 | Ok(()) 155 | } 156 | 157 | #[test] 158 | fn wait_twice_after_sigterm_group() -> Result<()> { 159 | let mut child = Command::new("yes").stdout(Stdio::null()).group_spawn()?; 160 | assert!(child.try_wait()?.is_none(), "pre try_wait"); 161 | child.signal(Signal::SIGTERM)?; 162 | let status = child.wait()?; 163 | assert_eq!( 164 | status.signal(), 165 | Some(Signal::SIGTERM as i32), 166 | "first wait status" 167 | ); 168 | let status = child.wait()?; 169 | assert_eq!( 170 | status.signal(), 171 | Some(Signal::SIGTERM as i32), 172 | "second wait status" 173 | ); 174 | Ok(()) 175 | } 176 | 177 | #[test] 178 | fn wait_after_die_normal() -> Result<()> { 179 | let mut child = Command::new("echo").stdout(Stdio::null()).spawn()?; 180 | sleep(DIE_TIME); 181 | 182 | let status = child.wait()?; 183 | assert!(status.success()); 184 | 185 | Ok(()) 186 | } 187 | 188 | #[test] 189 | fn wait_after_die_group() -> Result<()> { 190 | let mut child = Command::new("echo").stdout(Stdio::null()).group_spawn()?; 191 | sleep(DIE_TIME); 192 | 193 | let status = child.wait()?; 194 | assert!(status.success()); 195 | 196 | Ok(()) 197 | } 198 | 199 | #[test] 200 | fn try_wait_after_die_normal() -> Result<()> { 201 | let mut child = Command::new("echo").stdout(Stdio::null()).spawn()?; 202 | sleep(DIE_TIME); 203 | 204 | let status = child.try_wait()?; 205 | assert!(status.is_some()); 206 | assert!(status.unwrap().success()); 207 | 208 | Ok(()) 209 | } 210 | 211 | #[test] 212 | fn try_wait_after_die_group() -> Result<()> { 213 | let mut child = Command::new("echo").stdout(Stdio::null()).group_spawn()?; 214 | sleep(DIE_TIME); 215 | 216 | let status = child.try_wait()?; 217 | assert!(status.is_some()); 218 | assert!(status.unwrap().success()); 219 | 220 | Ok(()) 221 | } 222 | 223 | #[test] 224 | fn wait_normal() -> Result<()> { 225 | let mut command = Command::new("echo"); 226 | let mut child = command.spawn()?; 227 | let status = child.wait()?; 228 | assert!(status.success()); 229 | let status = child.wait()?; 230 | assert!(status.success()); 231 | Ok(()) 232 | } 233 | 234 | #[test] 235 | fn wait_group() -> Result<()> { 236 | let mut command = Command::new("echo"); 237 | let mut child = command.group_spawn()?; 238 | let status = child.wait()?; 239 | assert!(status.success()); 240 | let status = child.wait()?; 241 | assert!(status.success()); 242 | Ok(()) 243 | } 244 | 245 | #[test] 246 | fn wait_with_output_normal() -> Result<()> { 247 | let child = Command::new("echo") 248 | .arg("hello") 249 | .stdout(Stdio::piped()) 250 | .spawn()?; 251 | 252 | let output = child.wait_with_output()?; 253 | assert!(output.status.success()); 254 | assert_eq!(output.stdout, b"hello\n".to_vec()); 255 | assert_eq!(output.stderr, Vec::new()); 256 | Ok(()) 257 | } 258 | 259 | #[test] 260 | fn wait_with_output_group() -> Result<()> { 261 | let child = Command::new("echo") 262 | .arg("hello") 263 | .stdout(Stdio::piped()) 264 | .group_spawn()?; 265 | 266 | let output = child.wait_with_output()?; 267 | assert!(output.status.success()); 268 | assert_eq!(output.stdout, b"hello\n".to_vec()); 269 | assert_eq!(output.stderr, Vec::new()); 270 | Ok(()) 271 | } 272 | 273 | #[test] 274 | fn id_same_as_inner_group() -> Result<()> { 275 | let mut command = Command::new("echo"); 276 | let mut child = command.group_spawn()?; 277 | assert_eq!(child.id(), child.inner().id()); 278 | Ok(()) 279 | } 280 | 281 | #[test] 282 | fn signal_normal() -> Result<()> { 283 | let mut child = Command::new("yes").stdout(Stdio::null()).spawn()?; 284 | 285 | child.signal(Signal::SIGCONT)?; 286 | sleep(DIE_TIME); 287 | assert!(child.try_wait()?.is_none(), "not exited with sigcont"); 288 | 289 | child.signal(Signal::SIGTERM)?; 290 | sleep(DIE_TIME); 291 | assert!(child.try_wait()?.is_some(), "exited with sigterm"); 292 | 293 | Ok(()) 294 | } 295 | 296 | #[test] 297 | fn signal_group() -> Result<()> { 298 | let mut child = Command::new("yes").stdout(Stdio::null()).group_spawn()?; 299 | 300 | child.signal(Signal::SIGCONT)?; 301 | sleep(DIE_TIME); 302 | assert!(child.try_wait()?.is_none(), "not exited with sigcont"); 303 | 304 | child.signal(Signal::SIGTERM)?; 305 | sleep(DIE_TIME); 306 | assert!(child.try_wait()?.is_some(), "exited with sigterm"); 307 | 308 | Ok(()) 309 | } 310 | -------------------------------------------------------------------------------- /tests/stdlib_windows.rs: -------------------------------------------------------------------------------- 1 | #![cfg(windows)] 2 | 3 | use command_group::CommandGroup; 4 | use std::{ 5 | io::{Read, Result, Write}, 6 | process::{Command, Stdio}, 7 | thread::sleep, 8 | time::Duration, 9 | }; 10 | 11 | const DIE_TIME: Duration = Duration::from_millis(1000); 12 | 13 | // each test has a _normal variant that uses the stdlib non-group API for comparison/debugging. 14 | 15 | #[test] 16 | fn inner_read_stdout_normal() -> Result<()> { 17 | let mut child = Command::new("powershell.exe") 18 | .arg("/C") 19 | .arg("echo hello") 20 | .stdout(Stdio::piped()) 21 | .spawn()?; 22 | 23 | let mut output = String::new(); 24 | if let Some(mut out) = child.stdout.take() { 25 | out.read_to_string(&mut output)?; 26 | } 27 | 28 | assert_eq!(output.as_str(), "hello\r\n"); 29 | Ok(()) 30 | } 31 | 32 | #[test] 33 | fn inner_read_stdout_group() -> Result<()> { 34 | let mut child = Command::new("powershell.exe") 35 | .arg("/C") 36 | .arg("echo hello") 37 | .stdout(Stdio::piped()) 38 | .group_spawn()?; 39 | 40 | let mut output = String::new(); 41 | if let Some(mut out) = child.inner().stdout.take() { 42 | out.read_to_string(&mut output)?; 43 | } 44 | 45 | assert_eq!(output.as_str(), "hello\r\n"); 46 | Ok(()) 47 | } 48 | 49 | #[test] 50 | fn into_inner_write_stdin_normal() -> Result<()> { 51 | let mut child = Command::new("findstr") 52 | .arg("^") 53 | .stdin(Stdio::piped()) 54 | .stdout(Stdio::piped()) 55 | .spawn()?; 56 | 57 | if let Some(mut din) = child.stdin.take() { 58 | din.write_all(b"hello")?; 59 | } 60 | 61 | let mut output = String::new(); 62 | if let Some(mut out) = child.stdout.take() { 63 | out.read_to_string(&mut output)?; 64 | } 65 | 66 | assert_eq!(output.as_str(), "hello\r\n"); 67 | Ok(()) 68 | } 69 | 70 | #[test] 71 | fn into_inner_write_stdin_group() -> Result<()> { 72 | let mut child = Command::new("findstr") 73 | .arg("^") 74 | .stdin(Stdio::piped()) 75 | .stdout(Stdio::piped()) 76 | .group_spawn()? 77 | .into_inner(); 78 | 79 | if let Some(mut din) = child.stdin.take() { 80 | din.write_all(b"hello")?; 81 | } 82 | 83 | let mut output = String::new(); 84 | if let Some(mut out) = child.stdout.take() { 85 | out.read_to_string(&mut output)?; 86 | } 87 | 88 | assert_eq!(output.as_str(), "hello\r\n"); 89 | Ok(()) 90 | } 91 | 92 | #[test] 93 | fn kill_and_try_wait_normal() -> Result<()> { 94 | let mut child = Command::new("powershell.exe") 95 | .arg("/C") 96 | .arg("pause") 97 | .spawn()?; 98 | assert!(child.try_wait()?.is_none()); 99 | child.kill()?; 100 | sleep(DIE_TIME); 101 | assert!(child.try_wait()?.is_some()); 102 | sleep(DIE_TIME); 103 | assert!(child.try_wait()?.is_some()); 104 | Ok(()) 105 | } 106 | 107 | #[test] 108 | fn kill_and_try_wait_group() -> Result<()> { 109 | let mut child = Command::new("powershell.exe") 110 | .arg("/C") 111 | .arg("pause") 112 | .group_spawn()?; 113 | assert!(child.try_wait()?.is_none()); 114 | child.kill()?; 115 | sleep(DIE_TIME); 116 | assert!(child.try_wait()?.is_some()); 117 | sleep(DIE_TIME); 118 | assert!(child.try_wait()?.is_some()); 119 | Ok(()) 120 | } 121 | 122 | #[test] 123 | fn wait_after_die_normal() -> Result<()> { 124 | let mut child = Command::new("powershell.exe") 125 | .arg("/C") 126 | .arg("echo hello") 127 | .spawn()?; 128 | sleep(DIE_TIME); 129 | 130 | let status = child.wait()?; 131 | assert!(status.success()); 132 | 133 | Ok(()) 134 | } 135 | 136 | #[test] 137 | fn wait_after_die_group() -> Result<()> { 138 | let mut child = Command::new("powershell.exe") 139 | .arg("/C") 140 | .arg("echo hello") 141 | .group_spawn()?; 142 | sleep(DIE_TIME); 143 | 144 | let status = child.wait()?; 145 | assert!(status.success()); 146 | 147 | Ok(()) 148 | } 149 | 150 | #[test] 151 | fn try_wait_after_die_normal() -> Result<()> { 152 | let mut child = Command::new("powershell.exe") 153 | .arg("/C") 154 | .arg("echo hello") 155 | .spawn()?; 156 | sleep(DIE_TIME * 10); 157 | 158 | let status = child.try_wait()?; 159 | assert!(status.is_some()); 160 | assert!(status.unwrap().success()); 161 | 162 | Ok(()) 163 | } 164 | 165 | #[test] 166 | fn try_wait_after_die_group() -> Result<()> { 167 | let mut child = Command::new("powershell.exe") 168 | .arg("/C") 169 | .arg("echo hello") 170 | .group_spawn()?; 171 | sleep(DIE_TIME * 10); 172 | 173 | let status = child.try_wait()?; 174 | assert!(status.is_some()); 175 | assert!(status.unwrap().success()); 176 | 177 | Ok(()) 178 | } 179 | 180 | #[test] 181 | fn wait_normal() -> Result<()> { 182 | let mut child = Command::new("powershell.exe") 183 | .arg("/C") 184 | .arg("echo hello") 185 | .spawn()?; 186 | let status = child.wait()?; 187 | assert!(status.success()); 188 | let status = child.wait()?; 189 | assert!(status.success()); 190 | Ok(()) 191 | } 192 | 193 | #[test] 194 | fn wait_group() -> Result<()> { 195 | let mut child = Command::new("powershell.exe") 196 | .arg("/C") 197 | .arg("echo hello") 198 | .group_spawn()?; 199 | let status = child.wait()?; 200 | assert!(status.success()); 201 | let status = child.wait()?; 202 | assert!(status.success()); 203 | Ok(()) 204 | } 205 | 206 | #[test] 207 | fn wait_with_output_normal() -> Result<()> { 208 | let child = Command::new("powershell.exe") 209 | .arg("/C") 210 | .arg("echo hello") 211 | .stdout(Stdio::piped()) 212 | .spawn()?; 213 | 214 | let output = child.wait_with_output()?; 215 | assert!(output.status.success()); 216 | assert_eq!(output.stdout, b"hello\r\n".to_vec()); 217 | assert_eq!(output.stderr, Vec::new()); 218 | Ok(()) 219 | } 220 | 221 | #[test] 222 | fn wait_with_output_group() -> Result<()> { 223 | let child = Command::new("powershell.exe") 224 | .arg("/C") 225 | .arg("echo hello") 226 | .stdout(Stdio::piped()) 227 | .group_spawn()?; 228 | 229 | let output = child.wait_with_output()?; 230 | assert!(output.status.success()); 231 | assert_eq!(output.stdout, b"hello\r\n".to_vec()); 232 | assert_eq!(output.stderr, Vec::new()); 233 | Ok(()) 234 | } 235 | 236 | #[test] 237 | fn id_same_as_inner_group() -> Result<()> { 238 | let mut child = Command::new("powershell.exe") 239 | .arg("/C") 240 | .arg("echo hello") 241 | .group_spawn()?; 242 | assert_eq!(child.id(), child.inner().id()); 243 | Ok(()) 244 | } 245 | -------------------------------------------------------------------------------- /tests/tokio_unix.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(unix, feature = "with-tokio"))] 2 | 3 | use command_group::{AsyncCommandGroup, Signal, UnixChildExt}; 4 | use std::{io::Result, os::unix::process::ExitStatusExt, process::Stdio, time::Duration}; 5 | use tokio::{ 6 | io::{AsyncReadExt, AsyncWriteExt}, 7 | process::Command, 8 | time::sleep, 9 | }; 10 | 11 | const DIE_TIME: Duration = Duration::from_millis(100); 12 | 13 | // each test has a _normal variant that uses the Tokio non-group API for comparison/debugging. 14 | 15 | #[tokio::test] 16 | async fn inner_read_stdout_normal() -> Result<()> { 17 | let mut child = Command::new("echo") 18 | .arg("hello") 19 | .stdout(Stdio::piped()) 20 | .spawn()?; 21 | 22 | let mut output = String::new(); 23 | if let Some(mut out) = child.stdout.take() { 24 | out.read_to_string(&mut output).await?; 25 | } 26 | 27 | assert_eq!(output.as_str(), "hello\n"); 28 | Ok(()) 29 | } 30 | 31 | #[tokio::test] 32 | async fn inner_read_stdout_group() -> Result<()> { 33 | let mut child = Command::new("echo") 34 | .arg("hello") 35 | .stdout(Stdio::piped()) 36 | .group_spawn()?; 37 | 38 | let mut output = String::new(); 39 | if let Some(mut out) = child.inner().stdout.take() { 40 | out.read_to_string(&mut output).await?; 41 | } 42 | 43 | assert_eq!(output.as_str(), "hello\n"); 44 | Ok(()) 45 | } 46 | 47 | #[tokio::test] 48 | async fn into_inner_write_stdin_normal() -> Result<()> { 49 | let mut child = Command::new("cat") 50 | .stdin(Stdio::piped()) 51 | .stdout(Stdio::piped()) 52 | .spawn()?; 53 | 54 | if let Some(mut din) = child.stdin.take() { 55 | din.write_all(b"hello").await?; 56 | } 57 | 58 | let mut output = String::new(); 59 | if let Some(mut out) = child.stdout.take() { 60 | out.read_to_string(&mut output).await?; 61 | } 62 | 63 | assert_eq!(output.as_str(), "hello"); 64 | Ok(()) 65 | } 66 | 67 | #[tokio::test] 68 | async fn into_inner_write_stdin_group() -> Result<()> { 69 | let mut child = Command::new("cat") 70 | .stdin(Stdio::piped()) 71 | .stdout(Stdio::piped()) 72 | .group_spawn()? 73 | .into_inner(); 74 | 75 | if let Some(mut din) = child.stdin.take() { 76 | din.write_all(b"hello").await?; 77 | } 78 | 79 | let mut output = String::new(); 80 | if let Some(mut out) = child.stdout.take() { 81 | out.read_to_string(&mut output).await?; 82 | } 83 | 84 | assert_eq!(output.as_str(), "hello"); 85 | Ok(()) 86 | } 87 | 88 | #[tokio::test] 89 | async fn kill_and_try_wait_normal() -> Result<()> { 90 | let mut child = Command::new("yes").stdout(Stdio::null()).spawn()?; 91 | assert!(child.try_wait()?.is_none()); 92 | child.kill().await?; 93 | sleep(DIE_TIME).await; 94 | assert!(child.try_wait()?.is_some()); 95 | sleep(DIE_TIME).await; 96 | assert!(child.try_wait()?.is_some()); 97 | Ok(()) 98 | } 99 | 100 | #[tokio::test] 101 | async fn kill_and_try_wait_group() -> Result<()> { 102 | let mut child = Command::new("yes").stdout(Stdio::null()).group_spawn()?; 103 | assert!(child.try_wait()?.is_none()); 104 | child.kill().await?; 105 | sleep(DIE_TIME).await; 106 | assert!(child.try_wait()?.is_some()); 107 | sleep(DIE_TIME).await; 108 | assert!(child.try_wait()?.is_some()); 109 | Ok(()) 110 | } 111 | 112 | #[tokio::test] 113 | async fn try_wait_twice_after_sigterm_normal() -> Result<()> { 114 | let mut child = Command::new("yes").stdout(Stdio::null()).spawn()?; 115 | assert!(child.try_wait()?.is_none(), "pre try_wait"); 116 | child.signal(Signal::SIGTERM)?; 117 | sleep(DIE_TIME).await; 118 | assert!(child.try_wait()?.is_some(), "first try_wait"); 119 | sleep(DIE_TIME).await; 120 | assert!(child.try_wait()?.is_some(), "second try_wait"); 121 | Ok(()) 122 | } 123 | 124 | #[tokio::test] 125 | async fn try_wait_twice_after_sigterm_group() -> Result<()> { 126 | let mut child = Command::new("yes").stdout(Stdio::null()).group_spawn()?; 127 | assert!(child.try_wait()?.is_none(), "pre try_wait"); 128 | child.signal(Signal::SIGTERM)?; 129 | sleep(DIE_TIME).await; 130 | assert!(child.try_wait()?.is_some(), "first try_wait"); 131 | sleep(DIE_TIME).await; 132 | assert!(child.try_wait()?.is_some(), "second try_wait"); 133 | Ok(()) 134 | } 135 | 136 | #[tokio::test] 137 | async fn wait_twice_after_sigterm_normal() -> Result<()> { 138 | let mut child = Command::new("yes").stdout(Stdio::null()).spawn()?; 139 | assert!(child.try_wait()?.is_none(), "pre try_wait"); 140 | child.signal(Signal::SIGTERM)?; 141 | let status = child.wait().await?; 142 | assert_eq!( 143 | status.signal(), 144 | Some(Signal::SIGTERM as i32), 145 | "first wait status" 146 | ); 147 | let status = child.wait().await?; 148 | assert_eq!( 149 | status.signal(), 150 | Some(Signal::SIGTERM as i32), 151 | "second wait status" 152 | ); 153 | Ok(()) 154 | } 155 | 156 | #[tokio::test] 157 | async fn wait_twice_after_sigterm_group() -> Result<()> { 158 | let mut child = Command::new("yes").stdout(Stdio::null()).group_spawn()?; 159 | assert!(child.try_wait()?.is_none(), "pre try_wait"); 160 | child.signal(Signal::SIGTERM)?; 161 | let status = child.wait().await?; 162 | assert_eq!( 163 | status.signal(), 164 | Some(Signal::SIGTERM as i32), 165 | "first wait status" 166 | ); 167 | let status = child.wait().await?; 168 | assert_eq!( 169 | status.signal(), 170 | Some(Signal::SIGTERM as i32), 171 | "second wait status" 172 | ); 173 | Ok(()) 174 | } 175 | 176 | #[tokio::test] 177 | async fn wait_after_die_normal() -> Result<()> { 178 | let mut child = Command::new("echo").stdout(Stdio::null()).spawn()?; 179 | sleep(DIE_TIME).await; 180 | 181 | let status = child.wait().await?; 182 | assert!(status.success()); 183 | 184 | Ok(()) 185 | } 186 | 187 | #[tokio::test] 188 | async fn wait_after_die_group() -> Result<()> { 189 | let mut child = Command::new("echo").stdout(Stdio::null()).group_spawn()?; 190 | sleep(DIE_TIME).await; 191 | 192 | let status = child.wait().await?; 193 | assert!(status.success()); 194 | 195 | Ok(()) 196 | } 197 | 198 | #[tokio::test] 199 | async fn try_wait_after_die_normal() -> Result<()> { 200 | let mut child = Command::new("echo").stdout(Stdio::null()).spawn()?; 201 | sleep(DIE_TIME).await; 202 | 203 | let status = child.try_wait()?; 204 | assert!(status.is_some()); 205 | assert!(status.unwrap().success()); 206 | 207 | Ok(()) 208 | } 209 | 210 | #[tokio::test] 211 | async fn try_wait_after_die_group() -> Result<()> { 212 | let mut child = Command::new("echo").stdout(Stdio::null()).group_spawn()?; 213 | sleep(DIE_TIME).await; 214 | 215 | let status = child.try_wait()?; 216 | assert!(status.is_some()); 217 | assert!(status.unwrap().success()); 218 | 219 | Ok(()) 220 | } 221 | 222 | #[tokio::test] 223 | async fn wait_normal() -> Result<()> { 224 | let mut command = Command::new("echo"); 225 | let mut child = command.spawn()?; 226 | let status = child.wait().await?; 227 | assert!(status.success()); 228 | let status = child.wait().await?; 229 | assert!(status.success()); 230 | Ok(()) 231 | } 232 | 233 | #[tokio::test] 234 | async fn wait_group() -> Result<()> { 235 | let mut command = Command::new("echo"); 236 | let mut child = command.group_spawn()?; 237 | let status = child.wait().await?; 238 | assert!(status.success()); 239 | let status = child.wait().await?; 240 | assert!(status.success()); 241 | Ok(()) 242 | } 243 | 244 | #[tokio::test] 245 | async fn wait_with_output_normal() -> Result<()> { 246 | let child = Command::new("echo") 247 | .arg("hello") 248 | .stdout(Stdio::piped()) 249 | .spawn()?; 250 | 251 | let output = child.wait_with_output().await?; 252 | assert!(output.status.success()); 253 | assert_eq!(output.stdout, b"hello\n".to_vec()); 254 | assert_eq!(output.stderr, Vec::new()); 255 | Ok(()) 256 | } 257 | 258 | #[tokio::test] 259 | async fn wait_with_output_group() -> Result<()> { 260 | let child = Command::new("echo") 261 | .arg("hello") 262 | .stdout(Stdio::piped()) 263 | .group_spawn()?; 264 | 265 | let output = child.wait_with_output().await?; 266 | assert!(output.status.success()); 267 | assert_eq!(output.stdout, b"hello\n".to_vec()); 268 | assert_eq!(output.stderr, Vec::new()); 269 | Ok(()) 270 | } 271 | 272 | #[tokio::test] 273 | async fn id_same_as_inner_group() -> Result<()> { 274 | let mut command = Command::new("echo"); 275 | let mut child = command.group_spawn()?; 276 | assert_eq!(child.id(), child.inner().id()); 277 | Ok(()) 278 | } 279 | 280 | #[tokio::test] 281 | async fn signal_normal() -> Result<()> { 282 | let mut child = Command::new("yes").stdout(Stdio::null()).spawn()?; 283 | 284 | child.signal(Signal::SIGCONT)?; 285 | sleep(DIE_TIME).await; 286 | assert!(child.try_wait()?.is_none(), "not exited with sigcont"); 287 | 288 | child.signal(Signal::SIGTERM)?; 289 | sleep(DIE_TIME).await; 290 | assert!(child.try_wait()?.is_some(), "exited with sigterm"); 291 | 292 | Ok(()) 293 | } 294 | 295 | #[tokio::test] 296 | async fn signal_group() -> Result<()> { 297 | let mut child = Command::new("yes").stdout(Stdio::null()).group_spawn()?; 298 | 299 | child.signal(Signal::SIGCONT)?; 300 | sleep(DIE_TIME).await; 301 | assert!(child.try_wait()?.is_none(), "not exited with sigcont"); 302 | 303 | child.signal(Signal::SIGTERM)?; 304 | sleep(DIE_TIME).await; 305 | assert!(child.try_wait()?.is_some(), "exited with sigterm"); 306 | 307 | Ok(()) 308 | } 309 | -------------------------------------------------------------------------------- /tests/tokio_windows.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(windows, feature = "with-tokio"))] 2 | 3 | use command_group::AsyncCommandGroup; 4 | use std::{io::Result, process::Stdio, time::Duration}; 5 | use tokio::{ 6 | io::{AsyncReadExt, AsyncWriteExt}, 7 | process::Command, 8 | time::sleep, 9 | }; 10 | 11 | const DIE_TIME: Duration = Duration::from_millis(1000); 12 | 13 | // each test has a _normal variant that uses the Tokio non-group API for comparison/debugging. 14 | 15 | #[tokio::test] 16 | async fn inner_read_stdout_normal() -> Result<()> { 17 | let mut child = Command::new("powershell.exe") 18 | .arg("/C") 19 | .arg("echo hello") 20 | .stdout(Stdio::piped()) 21 | .spawn()?; 22 | 23 | let mut output = String::new(); 24 | if let Some(mut out) = child.stdout.take() { 25 | out.read_to_string(&mut output).await?; 26 | } 27 | 28 | assert_eq!(output.as_str(), "hello\r\n"); 29 | Ok(()) 30 | } 31 | 32 | #[tokio::test] 33 | async fn inner_read_stdout_group() -> Result<()> { 34 | let mut child = Command::new("powershell.exe") 35 | .arg("/C") 36 | .arg("echo hello") 37 | .stdout(Stdio::piped()) 38 | .group_spawn()?; 39 | 40 | let mut output = String::new(); 41 | if let Some(mut out) = child.inner().stdout.take() { 42 | out.read_to_string(&mut output).await?; 43 | } 44 | 45 | assert_eq!(output.as_str(), "hello\r\n"); 46 | Ok(()) 47 | } 48 | 49 | #[tokio::test] 50 | async fn into_inner_write_stdin_normal() -> Result<()> { 51 | let mut child = Command::new("findstr") 52 | .arg("^") 53 | .stdin(Stdio::piped()) 54 | .stdout(Stdio::piped()) 55 | .spawn()?; 56 | 57 | if let Some(mut din) = child.stdin.take() { 58 | din.write_all(b"hello").await?; 59 | } 60 | 61 | let mut output = String::new(); 62 | if let Some(mut out) = child.stdout.take() { 63 | out.read_to_string(&mut output).await?; 64 | } 65 | 66 | assert_eq!(output.as_str(), "hello\r\n"); 67 | Ok(()) 68 | } 69 | 70 | #[tokio::test] 71 | async fn into_inner_write_stdin_group() -> Result<()> { 72 | let mut child = Command::new("findstr") 73 | .arg("^") 74 | .stdin(Stdio::piped()) 75 | .stdout(Stdio::piped()) 76 | .group_spawn()? 77 | .into_inner(); 78 | 79 | if let Some(mut din) = child.stdin.take() { 80 | din.write_all(b"hello").await?; 81 | } 82 | 83 | let mut output = String::new(); 84 | if let Some(mut out) = child.stdout.take() { 85 | out.read_to_string(&mut output).await?; 86 | } 87 | 88 | assert_eq!(output.as_str(), "hello\r\n"); 89 | Ok(()) 90 | } 91 | 92 | #[tokio::test] 93 | async fn kill_and_try_wait_normal() -> Result<()> { 94 | let mut child = Command::new("powershell.exe") 95 | .arg("/C") 96 | .arg("pause") 97 | .spawn()?; 98 | assert!(child.try_wait()?.is_none()); 99 | child.kill().await?; 100 | sleep(DIE_TIME).await; 101 | assert!(child.try_wait()?.is_some()); 102 | sleep(DIE_TIME).await; 103 | assert!(child.try_wait()?.is_some()); 104 | Ok(()) 105 | } 106 | 107 | #[tokio::test] 108 | async fn kill_and_try_wait_group() -> Result<()> { 109 | let mut child = Command::new("powershell.exe") 110 | .arg("/C") 111 | .arg("pause") 112 | .group_spawn()?; 113 | assert!(child.try_wait()?.is_none()); 114 | child.kill().await?; 115 | sleep(DIE_TIME).await; 116 | assert!(child.try_wait()?.is_some()); 117 | sleep(DIE_TIME).await; 118 | assert!(child.try_wait()?.is_some()); 119 | Ok(()) 120 | } 121 | 122 | #[tokio::test] 123 | async fn wait_after_die_normal() -> Result<()> { 124 | let mut child = Command::new("powershell.exe") 125 | .arg("/C") 126 | .arg("echo hello") 127 | .spawn()?; 128 | sleep(DIE_TIME).await; 129 | 130 | let status = child.wait().await?; 131 | assert!(status.success()); 132 | 133 | Ok(()) 134 | } 135 | 136 | #[tokio::test] 137 | async fn wait_after_die_group() -> Result<()> { 138 | let mut child = Command::new("powershell.exe") 139 | .arg("/C") 140 | .arg("echo hello") 141 | .group_spawn()?; 142 | sleep(DIE_TIME).await; 143 | 144 | let status = child.wait().await?; 145 | assert!(status.success()); 146 | 147 | Ok(()) 148 | } 149 | 150 | #[tokio::test] 151 | async fn try_wait_after_die_normal() -> Result<()> { 152 | let mut child = Command::new("powershell.exe") 153 | .arg("/C") 154 | .arg("echo hello") 155 | .spawn()?; 156 | sleep(DIE_TIME * 10).await; 157 | 158 | let status = child.try_wait()?; 159 | assert!(status.is_some()); 160 | assert!(status.unwrap().success()); 161 | 162 | Ok(()) 163 | } 164 | 165 | #[tokio::test] 166 | async fn try_wait_after_die_group() -> Result<()> { 167 | let mut child = Command::new("powershell.exe") 168 | .arg("/C") 169 | .arg("echo hello") 170 | .group_spawn()?; 171 | sleep(DIE_TIME * 10).await; 172 | 173 | let status = child.try_wait()?; 174 | assert!(status.is_some()); 175 | assert!(status.unwrap().success()); 176 | 177 | Ok(()) 178 | } 179 | 180 | #[tokio::test] 181 | async fn wait_normal() -> Result<()> { 182 | let mut child = Command::new("powershell.exe") 183 | .arg("/C") 184 | .arg("echo hello") 185 | .spawn()?; 186 | let status = child.wait().await?; 187 | assert!(status.success()); 188 | let status = child.wait().await?; 189 | assert!(status.success()); 190 | Ok(()) 191 | } 192 | 193 | #[tokio::test] 194 | async fn wait_group() -> Result<()> { 195 | let mut child = Command::new("powershell.exe") 196 | .arg("/C") 197 | .arg("echo hello") 198 | .group_spawn()?; 199 | let status = child.wait().await?; 200 | assert!(status.success()); 201 | let status = child.wait().await?; 202 | assert!(status.success()); 203 | Ok(()) 204 | } 205 | 206 | #[tokio::test] 207 | async fn wait_with_output_normal() -> Result<()> { 208 | let child = Command::new("powershell.exe") 209 | .arg("/C") 210 | .arg("echo hello") 211 | .stdout(Stdio::piped()) 212 | .spawn()?; 213 | 214 | let output = child.wait_with_output().await?; 215 | assert!(output.status.success()); 216 | assert_eq!(output.stdout, b"hello\r\n".to_vec()); 217 | assert_eq!(output.stderr, Vec::new()); 218 | Ok(()) 219 | } 220 | 221 | #[tokio::test] 222 | async fn wait_with_output_group() -> Result<()> { 223 | let child = Command::new("powershell.exe") 224 | .arg("/C") 225 | .arg("echo hello") 226 | .stdout(Stdio::piped()) 227 | .group_spawn()?; 228 | 229 | let output = child.wait_with_output().await?; 230 | assert!(output.status.success()); 231 | assert_eq!(output.stdout, b"hello\r\n".to_vec()); 232 | assert_eq!(output.stderr, Vec::new()); 233 | Ok(()) 234 | } 235 | 236 | #[tokio::test] 237 | async fn id_same_as_inner_group() -> Result<()> { 238 | let mut child = Command::new("powershell.exe") 239 | .arg("/C") 240 | .arg("echo hello") 241 | .group_spawn()?; 242 | assert_eq!(child.id(), child.inner().id()); 243 | Ok(()) 244 | } 245 | --------------------------------------------------------------------------------