use std::cell::RefCell;
use std::ffi::OsString;
use std::fs::File;
use std::io::ErrorKind;
use std::time::Duration;
use subprocess;
use crate::function::OptionalArg;
use crate::obj::objbytes::PyBytesRef;
use crate::obj::objlist::PyListRef;
use crate::obj::objstr::{self, PyStringRef};
use crate::obj::objtype::PyClassRef;
use crate::pyobject::{Either, IntoPyObject, PyObjectRef, PyRef, PyResult, PyValue};
use crate::stdlib::io::io_open;
use crate::stdlib::os::{convert_io_error, raw_file_number, rust_file};
use crate::vm::VirtualMachine;
#[derive(Debug)]
struct Popen {
process: RefCell<subprocess::Popen>,
args: PyObjectRef,
}
impl PyValue for Popen {
fn class(vm: &VirtualMachine) -> PyClassRef {
vm.class("_subprocess", "Popen")
}
}
type PopenRef = PyRef<Popen>;
#[derive(FromArgs)]
#[allow(dead_code)]
struct PopenArgs {
#[pyarg(positional_only)]
args: Either<PyStringRef, PyListRef>,
#[pyarg(positional_or_keyword, default = "None")]
stdin: Option<i64>,
#[pyarg(positional_or_keyword, default = "None")]
stdout: Option<i64>,
#[pyarg(positional_or_keyword, default = "None")]
stderr: Option<i64>,
#[pyarg(positional_or_keyword, default = "None")]
close_fds: Option<bool>,
#[pyarg(positional_or_keyword, default = "None")]
cwd: Option<PyStringRef>,
#[pyarg(positional_or_keyword, default = "None")]
start_new_session: Option<bool>,
}
impl IntoPyObject for subprocess::ExitStatus {
fn into_pyobject(self, vm: &VirtualMachine) -> PyResult {
let status: i32 = match self {
subprocess::ExitStatus::Exited(status) => status as i32,
subprocess::ExitStatus::Signaled(status) => -i32::from(status),
subprocess::ExitStatus::Other(status) => status as i32,
_ => return Err(vm.new_os_error("Unknown exist status".to_owned())),
};
Ok(vm.new_int(status))
}
}
#[cfg(windows)]
const NULL_DEVICE: &str = "nul";
#[cfg(unix)]
const NULL_DEVICE: &str = "/dev/null";
fn convert_redirection(arg: Option<i64>, vm: &VirtualMachine) -> PyResult<subprocess::Redirection> {
match arg {
Some(fd) => match fd {
-1 => Ok(subprocess::Redirection::Pipe),
-2 => Ok(subprocess::Redirection::Merge),
-3 => Ok(subprocess::Redirection::File(
File::open(NULL_DEVICE).unwrap(),
)),
fd => {
if fd < 0 {
Err(vm.new_value_error(format!("Invalid fd: {}", fd)))
} else {
Ok(subprocess::Redirection::File(rust_file(fd)))
}
}
},
None => Ok(subprocess::Redirection::None),
}
}
fn convert_to_file_io(file: &Option<File>, mode: String, vm: &VirtualMachine) -> PyResult {
match file {
Some(ref stdin) => io_open(
vm,
vec![
vm.new_int(raw_file_number(stdin.try_clone().unwrap())),
vm.new_str(mode),
]
.into(),
),
None => Ok(vm.get_none()),
}
}
impl PopenRef {
fn new(cls: PyClassRef, args: PopenArgs, vm: &VirtualMachine) -> PyResult<PopenRef> {
let stdin = convert_redirection(args.stdin, vm)?;
let stdout = convert_redirection(args.stdout, vm)?;
let stderr = convert_redirection(args.stderr, vm)?;
let command_list = match &args.args {
Either::A(command) => vec![command.as_str().to_owned()],
Either::B(command_list) => command_list
.borrow_elements()
.iter()
.map(|x| objstr::clone_value(x))
.collect(),
};
let cwd = args.cwd.map(|x| OsString::from(x.as_str()));
let process = subprocess::Popen::create(
&command_list,
subprocess::PopenConfig {
stdin,
stdout,
stderr,
cwd,
..Default::default()
},
)
.map_err(|s| vm.new_os_error(format!("Could not start program: {}", s)))?;
Popen {
process: RefCell::new(process),
args: args.args.into_object(),
}
.into_ref_with_type(vm, cls)
}
fn poll(self) -> Option<subprocess::ExitStatus> {
self.process.borrow_mut().poll()
}
fn return_code(self) -> Option<subprocess::ExitStatus> {
self.process.borrow().exit_status()
}
fn wait(self, timeout: OptionalArg<u64>, vm: &VirtualMachine) -> PyResult<()> {
let timeout = match timeout.into_option() {
Some(timeout) => self
.process
.borrow_mut()
.wait_timeout(Duration::new(timeout, 0)),
None => self.process.borrow_mut().wait().map(Some),
}
.map_err(|s| vm.new_os_error(format!("Could not start program: {}", s)))?;
if timeout.is_none() {
let timeout_expired = vm.try_class("_subprocess", "TimeoutExpired")?;
Err(vm.new_exception_msg(timeout_expired, "Timeout".to_owned()))
} else {
Ok(())
}
}
fn stdin(self, vm: &VirtualMachine) -> PyResult {
convert_to_file_io(&self.process.borrow().stdin, "wb".to_owned(), vm)
}
fn stdout(self, vm: &VirtualMachine) -> PyResult {
convert_to_file_io(&self.process.borrow().stdout, "rb".to_owned(), vm)
}
fn stderr(self, vm: &VirtualMachine) -> PyResult {
convert_to_file_io(&self.process.borrow().stderr, "rb".to_owned(), vm)
}
fn terminate(self, vm: &VirtualMachine) -> PyResult<()> {
self.process
.borrow_mut()
.terminate()
.map_err(|err| convert_io_error(vm, err))
}
fn kill(self, vm: &VirtualMachine) -> PyResult<()> {
self.process
.borrow_mut()
.kill()
.map_err(|err| convert_io_error(vm, err))
}
#[allow(clippy::type_complexity)]
fn communicate(
self,
args: PopenCommunicateArgs,
vm: &VirtualMachine,
) -> PyResult<(Option<Vec<u8>>, Option<Vec<u8>>)> {
let bytes = match args.input {
OptionalArg::Present(ref bytes) => Some(bytes.get_value().to_vec()),
OptionalArg::Missing => None,
};
let mut communicator = self.process.borrow_mut().communicate_start(bytes);
if let OptionalArg::Present(timeout) = args.timeout {
communicator = communicator.limit_time(Duration::new(timeout, 0));
}
communicator.read().map_err(|err| {
if err.error.kind() == ErrorKind::TimedOut {
let timeout_expired = vm.try_class("_subprocess", "TimeoutExpired").unwrap();
vm.new_exception_msg(timeout_expired, "Timeout".to_owned())
} else {
convert_io_error(vm, err.error)
}
})
}
fn pid(self) -> Option<u32> {
self.process.borrow().pid()
}
fn enter(self) -> Self {
self
}
fn exit(
self,
_exception_type: PyObjectRef,
_exception_value: PyObjectRef,
_traceback: PyObjectRef,
) {
let mut process = self.process.borrow_mut();
process.stdout.take();
process.stdin.take();
process.stderr.take();
}
fn args(self) -> PyObjectRef {
self.args.clone()
}
}
#[derive(FromArgs)]
#[allow(dead_code)]
struct PopenCommunicateArgs {
#[pyarg(positional_or_keyword, optional = true)]
input: OptionalArg<PyBytesRef>,
#[pyarg(positional_or_keyword, optional = true)]
timeout: OptionalArg<u64>,
}
pub fn make_module(vm: &VirtualMachine) -> PyObjectRef {
let ctx = &vm.ctx;
let subprocess_error = ctx.new_class("SubprocessError", ctx.exceptions.exception_type.clone());
let timeout_expired = ctx.new_class("TimeoutExpired", subprocess_error.clone());
let popen = py_class!(ctx, "Popen", ctx.object(), {
(slot new) => PopenRef::new,
"poll" => ctx.new_method(PopenRef::poll),
"returncode" => ctx.new_readonly_getset("returncode", PopenRef::return_code),
"wait" => ctx.new_method(PopenRef::wait),
"stdin" => ctx.new_readonly_getset("stdin", PopenRef::stdin),
"stdout" => ctx.new_readonly_getset("stdout", PopenRef::stdout),
"stderr" => ctx.new_readonly_getset("stderr", PopenRef::stderr),
"terminate" => ctx.new_method(PopenRef::terminate),
"kill" => ctx.new_method(PopenRef::kill),
"communicate" => ctx.new_method(PopenRef::communicate),
"pid" => ctx.new_readonly_getset("pid", PopenRef::pid),
"__enter__" => ctx.new_method(PopenRef::enter),
"__exit__" => ctx.new_method(PopenRef::exit),
"args" => ctx.new_readonly_getset("args", PopenRef::args),
});
py_module!(vm, "_subprocess", {
"Popen" => popen,
"SubprocessError" => subprocess_error,
"TimeoutExpired" => timeout_expired,
"PIPE" => ctx.new_int(-1),
"STDOUT" => ctx.new_int(-2),
"DEVNULL" => ctx.new_int(-3),
})
}