unix_exec_piper/
lib.rs

1/*
2    MIT License
3
4    Copyright (c) 2020 Philipp Schuster
5
6    Permission is hereby granted, free of charge, to any person obtaining a copy
7    of this software and associated documentation files (the "Software"), to deal
8    in the Software without restriction, including without limitation the rights
9    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10    copies of the Software, and to permit persons to whom the Software is
11    furnished to do so, subject to the following conditions:
12
13    The above copyright notice and this permission notice shall be included in all
14    copies or substantial portions of the Software.
15
16    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22    SOFTWARE.
23*/
24
25pub use crate::data::{CmdChain, BasicCmd, CmdChainBuilder, BasicCmdBuilder, Builder, ProcessState};
26// public in case someone want to use this abstraction
27pub use crate::pipe::Pipe;
28
29mod libc_util;
30mod data;
31mod pipe;
32
33
34/// Runs a command chain. The parent process creates n childs and
35/// connects them (stdout => stdin) together via pipes.
36pub fn execute_piped_cmd_chain(cmds: &CmdChain) -> Vec<ProcessState> {
37    let mut pids: Vec<libc::pid_t> = vec![];
38
39    let mut pipe_to_current: Option<Pipe> = Option::None;
40    let mut pipe_to_next: Option<Pipe> = Option::None;
41    for i in 0..cmds.length() {
42        let cmd = &cmds.cmds()[i];
43
44        if pipe_to_next.is_some() {
45            pipe_to_current.replace(
46                pipe_to_next.take().unwrap()
47            );
48        }
49
50        if !cmd.is_last() {
51            pipe_to_next.replace(Pipe::new());
52        }
53
54        let pid = unsafe { libc::fork() };
55        if pid == -1 {
56            panic!("Fork failed! {}", errno::errno());
57        }
58
59        // parent code
60        if pid > 0 {
61            pids.push(pid);
62
63            // We MUST close all FDs in the Parent
64            if pipe_to_current.is_some() {
65                pipe_to_current.as_mut().unwrap().parent_close_all();
66            }
67        }
68        // child code
69        else {
70            // handle optional initial '< in.file' redirect
71            if cmd.is_first() && cmd.in_red_path().is_some() {
72                initial_ir(cmd);
73            }
74            // handle optional final '> out.file' redirect
75            if cmd.is_last() && cmd.out_red_path().is_some() {
76                final_or(cmd);
77            }
78
79            if pipe_to_current.is_some() {
80                pipe_to_current.as_mut().unwrap().as_read_end();
81            }
82            if pipe_to_next.is_some() {
83                pipe_to_next.as_mut().unwrap().as_write_end();
84            }
85
86            let _res = unsafe {
87                libc::execvp(
88                    cmd.executable_cstring().as_ptr(),
89                    cmd.args_to_c_argv()
90                )
91            };
92            panic!("Exec failed! {}", errno::errno());
93        }
94    }
95
96    let mut i = 0;
97    let mut process_states: Vec<ProcessState> = pids.into_iter()
98        .map(|pid| {
99            let executable = cmds.cmds()[i].executable().to_owned();
100            i += 1;
101            ProcessState::new(executable, pid)
102        })
103        .collect();
104
105    update_process_states(&mut process_states, cmds.background());
106
107    process_states
108}
109
110/// Updates the process state values if the pid is done running.
111/// Returns true if all pids are finished, otherwise false.
112///
113///  * `wnohang` if waitpid uses WNOHANG-flag. In other words: true means "wait blocking"
114///     and false means "update but don't block".
115pub fn update_process_states(states: &mut Vec<ProcessState>, wnohang: bool) -> bool {
116    // decide whether we wait blocking or non blocking
117    let wait_flags: libc::c_int = if wnohang { libc::WNOHANG } else { 0 };
118    let mut all_finished = true;
119
120    // only check those that are not finished yet!
121    // Important, otherwise failures happen
122    states.into_iter()
123        .filter(|state| !state.finished())
124        .for_each(|state| {
125            let mut status_code: libc::c_int = 0;
126            let status_code_ptr = &mut status_code as * mut libc::c_int;
127
128            let res = unsafe { libc::waitpid(state.pid(), status_code_ptr, wait_flags) };
129
130            // IDE doesn't find this functions but they exist
131            // returns true if the child terminated normally
132            let exited_normally: bool = unsafe { libc::WIFEXITED(status_code) };
133
134            if wait_flags == libc::WNOHANG && res == 0 {
135                all_finished = false;
136                // not done yet
137            } else if res == -1 {
138                panic!("Failure during waitpid! {}", errno::errno());
139            } else {
140                if !exited_normally {
141                    eprintln!("Process did not exited normally! {:#?}", state);
142                }
143                // exit code (only if exited_normally is true)
144                let exit_code: libc::c_int = unsafe { libc::WEXITSTATUS(status_code) };
145
146                state.finish(exit_code);
147                println!("Process {} finished with status code {}", state.pid(), status_code);
148            }
149        });
150    all_finished
151}
152
153/// Handles initial input redirect (from file).
154fn initial_ir(cmd: &BasicCmd) {
155    let fd = unsafe {
156        libc::open(
157            cmd.in_red_path_cstring().unwrap().as_ptr(),
158            libc::O_RDONLY,
159        )
160    };
161    if fd == -1 {
162        panic!("Input redirect path {} can't be opened/read! {}", cmd.in_red_path().as_ref().unwrap(), errno::errno());
163    }
164    let ret = unsafe { libc::dup2(fd, libc::STDIN_FILENO) };
165    if ret == -1 {
166        panic!("Error dup2() input redirect! {}", errno::errno());
167    }
168}
169
170/// Handles final output redirect (to file).
171fn final_or(cmd: &BasicCmd) {
172    let fd = unsafe {
173        // note that append won't work here because we only use the
174        // '> out.file' functionality but not '>> out.file' which
175        // would require the O_APPEND flag!
176
177        // open() doesn't work; file remains empty
178        // somehow fopen does some more magic..
179        /*libc::open(
180            cmd.out_red_path_cstring().unwrap().as_ptr(),
181            libc::O_WRONLY | libc::O_CREAT,
182            0644,
183        );*/
184        let file = libc::fopen(
185            cmd.out_red_path_cstring().unwrap().as_ptr(),
186            "w".as_ptr() as * const i8
187        );
188        // get file descriptor
189        libc::fileno(file)
190    };
191    if fd == -1 {
192        panic!("Output redirect path {} can't be opened/written! {}", cmd.out_red_path().as_ref().unwrap(), errno::errno());
193    }
194    let ret = unsafe { libc::dup2(fd, libc::STDOUT_FILENO) };
195    if ret == -1 {
196        panic!("Error dup2() output redirect! {}", errno::errno());
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use crate::data::{CmdChainBuilder, BasicCmdBuilder, Builder};
203    use crate::execute_piped_cmd_chain;
204
205    #[test]
206    fn test_execute_chain() {
207        // this test works if "2" is printed to stdout
208
209        let cmd_chain = CmdChainBuilder::new()
210            .add_cmd(
211                BasicCmdBuilder::new()
212                    .set_executable("echo")
213                    .add_arg("echo")
214                    .add_arg("Hallo\nAbc\n123\nAbc123")
215            ).add_cmd(
216            BasicCmdBuilder::new()
217                .set_executable("grep")
218                .add_arg("grep")
219                .add_arg("-i")
220                .add_arg("abc"))
221            .add_cmd(
222                BasicCmdBuilder::new()
223                    .set_executable("wc")
224                    .add_arg("wc")
225                    .add_arg("-l")
226            ).build();
227
228        execute_piped_cmd_chain(&cmd_chain);
229    }
230}