pub mod tasks;
use crate::AnyRes;
use super::encode::auto_decode;
use serde::{de, Deserialize, Serialize};
use std::{
collections::HashMap,
ffi::OsStr,
path::{Path, PathBuf},
process::{Command, ExitStatus, Stdio},
};
use strum::*;
type DWORD = u32;
pub const NUMA_NO_PREFERRED_NODE: DWORD = 0x0;
pub const CREATE_NO_WINDOW: DWORD = 0x08000000;
pub const CREATE_NEW_PROCESS_GROUP: DWORD = 0x00000200;
pub const DEBUG_PROCESS: DWORD = 0x00000001;
pub const DEBUG_ONLY_THIS_PROCESS: DWORD = 0x00000002;
pub const CREATE_SUSPENDED: DWORD = 0x00000004;
pub const DETACHED_PROCESS: DWORD = 0x00000008;
pub const CREATE_NEW_CONSOLE: DWORD = 0x00000010;
pub const NORMAL_PRIORITY_CLASS: DWORD = 0x00000020;
pub const IDLE_PRIORITY_CLASS: DWORD = 0x00000040;
pub const HIGH_PRIORITY_CLASS: DWORD = 0x00000080;
pub const REALTIME_PRIORITY_CLASS: DWORD = 0x00000100;
pub const CREATE_UNICODE_ENVIRONMENT: DWORD = 0x00000400;
pub const CREATE_SEPARATE_WOW_VDM: DWORD = 0x00000800;
pub const CREATE_SHARED_WOW_VDM: DWORD = 0x00001000;
pub const CREATE_FORCEDOS: DWORD = 0x00002000;
pub const BELOW_NORMAL_PRIORITY_CLASS: DWORD = 0x00004000;
pub const ABOVE_NORMAL_PRIORITY_CLASS: DWORD = 0x00008000;
pub const INHERIT_PARENT_AFFINITY: DWORD = 0x00010000;
pub const INHERIT_CALLER_PRIORITY: DWORD = 0x00020000;
pub const CREATE_PROTECTED_PROCESS: DWORD = 0x00040000;
pub const EXTENDED_STARTUPINFO_PRESENT: DWORD = 0x00080000;
pub const PROCESS_MODE_BACKGROUND_BEGIN: DWORD = 0x00100000;
pub const PROCESS_MODE_BACKGROUND_END: DWORD = 0x00200000;
pub const CREATE_BREAKAWAY_FROM_JOB: DWORD = 0x01000000;
pub const CREATE_PRESERVE_CODE_AUTHZ_LEVEL: DWORD = 0x02000000;
pub const CREATE_DEFAULT_ERROR_MODE: DWORD = 0x04000000;
pub const PROFILE_USER: DWORD = 0x10000000;
pub const PROFILE_KERNEL: DWORD = 0x20000000;
pub const PROFILE_SERVER: DWORD = 0x40000000;
pub const CREATE_IGNORE_SYSTEM_DEFAULT: DWORD = 0x80000000;
#[derive(Debug, Clone)]
pub struct CmdOutput {
pub stdout: String,
pub status: ExitStatus,
pub stderr: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CmdResult<T> {
pub content: String,
pub status: bool,
pub opts: T,
}
impl<T> CmdResult<T> {
pub fn set_opts(mut self, opts: T) -> Self {
self.opts = opts;
self
}
pub fn merge(mut self, target: Self) -> Self {
self.content = target.content;
self.status = target.status;
self
}
pub fn set_status(mut self, state: bool) -> Self {
self.status = state;
self
}
pub fn set_content(mut self, content: String) -> Self {
self.content = content;
self
}
pub fn opts(&self) -> &T {
&self.opts
}
}
impl<'a, T: de::Deserialize<'a>> CmdResult<T> {
pub fn from_str(value: &'a str) -> crate::AnyResult<Self> {
let s = value.trim().trim_start_matches("R<").trim_end_matches(">R");
Ok(serde_json::from_str(s)?)
}
}
impl<T: Serialize> CmdResult<T> {
pub fn to_str(&self) -> crate::AnyResult<String> {
Ok(format!("R<{}>R", serde_json::to_string(self)?))
}
pub fn to_string_pretty(&self) -> crate::AnyResult<String> {
Ok(format!("R<{}>R", serde_json::to_string_pretty(self)?))
}
}
#[cfg(feature = "fs")]
pub fn shell_open(target: impl AsRef<str>) -> crate::AnyResult<()> {
let pathname = crate::fs::convert_path(target.as_ref());
#[cfg(target_os = "macos")]
Cmd::new("open").args(&["-R", &pathname]).spawn()?;
#[cfg(target_os = "windows")]
Cmd::new("explorer.exe").arg(pathname).spawn()?;
#[cfg(target_os = "linux")]
Cmd::new("xdg-open").arg(pathname).spawn()?;
Ok(())
}
#[cfg(all(feature = "fs", feature = "tokio"))]
pub async fn a_shell_open(target: impl AsRef<str>) -> crate::AnyResult<()> {
let pathname = crate::fs::convert_path(target.as_ref());
#[cfg(target_os = "macos")]
Cmd::new("open")
.args(&["-R", &pathname])
.a_spawn()?
.wait()
.await?;
#[cfg(target_os = "windows")]
Cmd::new("explorer.exe")
.arg(pathname)
.a_spawn()?
.wait()
.await?;
#[cfg(target_os = "linux")]
Cmd::new("xdg-open").arg(pathname).a_spawn()?.wait().await?;
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cmd {
exe: String,
args: Vec<String>,
cwd: Option<PathBuf>,
flags: DWORD,
env: Option<HashMap<String, String>>,
exe_type: ExeType,
}
impl Cmd {
pub fn get_exe_path(&self) -> PathBuf {
let cwd = self
.cwd
.clone()
.unwrap_or(std::env::current_dir().unwrap_or_default());
cwd.join(&self.exe)
}
pub fn check_exe_path(&self) -> crate::Result<PathBuf> {
let path = self.get_exe_path();
if !path.exists() {
return Err(format!("File not found: {}", path.display()).into());
}
Ok(path)
}
pub fn split_args(args: &str, key: char) -> Vec<String> {
let mut result = Vec::with_capacity(args.split_whitespace().count());
let mut start = 0;
let mut in_quotes = None;
let chars: Vec<_> = args.chars().collect();
for (i, &c) in chars.iter().enumerate() {
match c {
'"' | '\'' => {
if let Some(quote) = in_quotes {
if quote == c {
in_quotes = None;
if start < i {
result.push(chars[start..i].iter().collect());
start = i + 1;
}
}
} else {
in_quotes = Some(c);
start = i + 1;
}
}
_ if c == key && in_quotes.is_none() => {
if start < i {
result.push(chars[start..i].iter().collect());
}
start = i + 1;
}
_ if i == chars.len() - 1 => {
if start <= i {
result.push(chars[start..=i].iter().collect());
}
}
_ => {}
}
}
result
}
}
impl Cmd {
pub fn new<S: AsRef<OsStr>>(exe: S) -> Self {
Self {
exe: exe.as_ref().to_string_lossy().into_owned(),
args: Vec::new(),
cwd: None,
flags: CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP | NORMAL_PRIORITY_CLASS | INHERIT_PARENT_AFFINITY | INHERIT_CALLER_PRIORITY| CREATE_PRESERVE_CODE_AUTHZ_LEVEL| CREATE_DEFAULT_ERROR_MODE, env: None,
exe_type: ExeType::default(),
}
}
pub fn cwd(mut self, env_path: impl AsRef<std::path::Path>) -> Self {
let path = env_path.as_ref().to_path_buf();
let f = |myenv: &mut HashMap<String, String>, new_path: PathBuf| {
if let Some(p) = myenv.get_mut("Path") {
let mut paths = std::env::split_paths(&p).collect::<Vec<_>>();
paths.push(new_path);
if let Some(new_path_str) = std::env::join_paths(paths)
.ok()
.and_then(|x| x.into_string().ok())
{
*p = new_path_str;
}
}
};
self.cwd = Some(path.clone());
if let Some(ref mut myenv) = self.env {
f(myenv, path)
} else {
let mut myenv: HashMap<String, String> = std::env::vars().collect();
f(&mut myenv, path);
self.env = Some(myenv);
}
self
}
pub fn arg(mut self, arg: impl Into<String>) -> Self {
let arg = arg.into();
if !arg.is_empty() {
self.args.push(arg);
}
self
}
pub fn flags(mut self, flags: DWORD) -> Self {
self.flags = flags;
self
}
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
self.args.extend(
args
.into_iter()
.map(|s| s.as_ref().to_string_lossy().into_owned()),
);
self
}
pub fn set_args(&mut self, args: Vec<String>) -> &mut Self {
if !args.is_empty() {
self.args = args;
}
self
}
pub fn env<K, V>(mut self, key: K, val: V) -> Self
where
K: Into<String>,
V: Into<String>,
{
self
.env
.get_or_insert_with(HashMap::new)
.insert(key.into(), val.into());
self
}
pub fn envs<I, K, V>(mut self, vars: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
let env = self.env.get_or_insert_with(HashMap::new);
for (key, val) in vars {
env.insert(key.into(), val.into());
}
self
}
pub fn set_type(mut self, exe_type: ExeType) -> Self {
self.exe_type = exe_type;
self
}
fn prepare_command(&self) -> crate::Result<Command> {
self.prepare_generic_command(|cmd| Box::new(Command::new(cmd)))
}
#[cfg(feature = "tokio")]
fn prepare_tokio_command(&self) -> crate::Result<tokio::process::Command> {
self.prepare_generic_command(|cmd| Box::new(tokio::process::Command::new(cmd)))
}
fn prepare_generic_command<C>(&self, new_command: impl Fn(&str) -> Box<C>) -> crate::Result<C>
where
C: CommandTrait,
{
let exe_type = match self.exe_type {
ExeType::AutoShell => match ExeType::from_target(&self.exe) {
ExeType::Unknown => {
if cfg!(target_os = "windows") {
ExeType::PowerShell
} else {
ExeType::Shell
}
}
v => v,
},
other => other,
};
let mut cmd = match exe_type {
ExeType::AutoShell => return Err("AutoShell 无法执行".into()),
ExeType::PowerShell => new_command("powershell").args(["-NoProfile", "-Command", &self.exe]),
ExeType::Shell => new_command("sh").args(["-c", &self.exe]),
ExeType::Cmd => new_command("cmd.exe").args(["/C", &self.exe]),
ExeType::Ps1Script => {
new_command("powershell.exe").args(["-ExecutionPolicy", "Bypass", "-File", &self.exe])
}
ExeType::Vbs => new_command("cscript.exe").args(["/Nologo"]).arg(&self.exe),
ExeType::PythonScript => new_command("python").arg(&self.exe),
ExeType::MacOSApp => new_command("open").arg(&self.get_exe_path()),
ExeType::AndroidApk => new_command("adb").args(["shell", "am", "start", "-n", &self.exe]),
ExeType::IosApp => new_command("xcrun").args(["simctl", "launch", "booted", &self.exe]),
_ => *new_command(&self.check_exe_path()?.to_string_lossy()),
};
if !self.args.is_empty() {
cmd = cmd.args(&self.args);
}
if let Some(ref env) = self.env {
cmd = cmd.envs(env);
} else {
cmd = cmd.envs(std::env::vars());
}
if let Some(ref cwd) = self.cwd {
cmd = cmd.current_dir(cwd);
}
cmd = cmd.creation_flags(self.flags);
cmd = cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
Ok(cmd)
}
pub fn output(&self) -> crate::Result<CmdOutput> {
let output = self.prepare_command()?.output().any()?;
let stdout = auto_decode(&output.stdout)
.unwrap_or_else(|_| String::from_utf8_lossy(&output.stdout).to_string());
Ok(CmdOutput {
stdout,
status: output.status,
stderr: output.stderr,
})
}
#[cfg(feature = "tokio")]
pub async fn a_output(&self) -> crate::Result<CmdOutput> {
let output = self.prepare_tokio_command()?.output().await.any()?;
let stdout = auto_decode(&output.stdout)
.unwrap_or_else(|_| String::from_utf8_lossy(&output.stdout).to_string());
Ok(CmdOutput {
stdout,
status: output.status,
stderr: output.stderr,
})
}
pub fn spawn(&self) -> crate::Result<std::process::Child> {
self.prepare_command()?.spawn().any()
}
#[cfg(feature = "tokio")]
pub fn a_spawn(&self) -> crate::Result<tokio::process::Child> {
self.prepare_tokio_command()?.spawn().any()
}
}
pub trait CommandTrait<Target = Command> {
fn arg<S: AsRef<OsStr>>(self, arg: S) -> Self;
fn args<I, S>(self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>;
fn envs<I, K, V>(self, vars: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>;
fn current_dir<P: AsRef<std::path::Path>>(self, dir: P) -> Self;
fn stdin<T: Into<Stdio>>(self, cfg: T) -> Self;
fn stdout<T: Into<Stdio>>(self, cfg: T) -> Self;
fn stderr<T: Into<Stdio>>(self, cfg: T) -> Self;
fn creation_flags(self, flags: u32) -> Self;
}
impl CommandTrait for Command {
fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
Command::arg(&mut self, arg);
self
}
fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
Command::args(&mut self, args);
self
}
fn envs<I, K, V>(mut self, vars: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
Command::env_clear(&mut self).envs(vars);
self
}
fn current_dir<P: AsRef<std::path::Path>>(mut self, dir: P) -> Self {
Command::current_dir(&mut self, dir);
self
}
fn stdin<T: Into<Stdio>>(mut self, cfg: T) -> Self {
Command::stdin(&mut self, cfg);
self
}
fn stdout<T: Into<Stdio>>(mut self, cfg: T) -> Self {
Command::stdout(&mut self, cfg);
self
}
fn stderr<T: Into<Stdio>>(mut self, cfg: T) -> Self {
Command::stderr(&mut self, cfg);
self
}
fn creation_flags(mut self, flags: u32) -> Self {
#[cfg(target_os = "windows")]
{
std::os::windows::process::CommandExt::creation_flags(&mut self, flags);
}
self
}
}
#[cfg(feature = "tokio")]
impl CommandTrait for tokio::process::Command {
fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
tokio::process::Command::arg(&mut self, arg);
self
}
fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
tokio::process::Command::args(&mut self, args);
self
}
fn envs<I, K, V>(mut self, vars: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
tokio::process::Command::env_clear(&mut self).envs(vars);
self
}
fn current_dir<P: AsRef<std::path::Path>>(mut self, dir: P) -> Self {
tokio::process::Command::current_dir(&mut self, dir);
self
}
fn stdin<T: Into<Stdio>>(mut self, cfg: T) -> Self {
tokio::process::Command::stdin(&mut self, cfg);
self
}
fn stdout<T: Into<Stdio>>(mut self, cfg: T) -> Self {
tokio::process::Command::stdout(&mut self, cfg);
self
}
fn stderr<T: Into<Stdio>>(mut self, cfg: T) -> Self {
tokio::process::Command::stderr(&mut self, cfg);
self
}
fn creation_flags(mut self, flags: u32) -> Self {
#[cfg(target_os = "windows")]
tokio::process::Command::creation_flags(&mut self, flags);
self
}
}
#[allow(missing_docs)]
#[derive(
Default, Clone, Copy, Debug, Display, PartialEq, EnumString, VariantArray, Deserialize, Serialize,
)]
#[repr(i32)]
pub enum ExeType {
#[default]
#[strum(to_string = "Auto")]
AutoShell,
#[strum(to_string = "PS1")]
PowerShell,
#[strum(to_string = "SH")]
Shell,
#[strum(to_string = "CMD")]
Cmd,
#[strum(to_string = ".exe")]
WindowsExe,
#[strum(to_string = ".sh")]
ShellScript,
#[strum(to_string = ".ps1")]
Ps1Script,
#[strum(to_string = ".bat")]
Bat,
#[strum(to_string = ".vbs")]
Vbs,
#[strum(to_string = ".py")]
PythonScript,
#[strum(to_string = ".cmd")]
CmdScript,
#[strum(to_string = ".app")]
MacOSApp,
#[strum(to_string = ".LinuxEXE")]
LinuxExe,
#[strum(to_string = ".apk")]
AndroidApk,
#[strum(to_string = ".ipa")]
IosApp,
#[strum(to_string = ".so")]
So,
#[strum(to_string = ".dll")]
Dll,
#[strum(to_string = "Unknown")]
Unknown,
}
impl ExeType {
pub fn from_target(p: impl AsRef<Path>) -> Self {
match p
.as_ref()
.extension()
.and_then(|x| x.to_str())
.unwrap_or_default()
.to_lowercase()
.as_str()
{
"exe" => ExeType::WindowsExe,
"bat" => ExeType::Bat,
"cmd" => ExeType::CmdScript,
"vbs" => ExeType::Vbs,
"ps1" => ExeType::Ps1Script,
"sh" => ExeType::ShellScript,
"app" => ExeType::MacOSApp,
"apk" => ExeType::AndroidApk,
"ipa" => ExeType::IosApp,
"py" => ExeType::PythonScript,
"so" => ExeType::So,
"dll" => ExeType::Dll,
_ => ExeType::Unknown,
}
}
pub fn to_extension(&self) -> &'static str {
match self {
ExeType::WindowsExe => "exe",
ExeType::Bat => "bat",
ExeType::CmdScript => "cmd",
ExeType::Vbs => "vbs",
ExeType::Ps1Script => "ps1",
ExeType::ShellScript => "sh",
ExeType::MacOSApp => "app",
ExeType::AndroidApk => "apk",
ExeType::IosApp => "ipa",
ExeType::PythonScript => "py",
ExeType::So => "so",
ExeType::Dll => "dll",
_ => "",
}
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "tokio")]
mod a_async {
use crate::cmd::{Cmd, ExeType};
#[tokio::test]
#[cfg(not(target_os = "windows"))]
fn test_shell_open_unix() {
assert!(a_shell_open("/").await.is_ok());
}
#[tokio::test]
#[cfg(target_os = "windows")]
async fn test_shell_open_windows() {
use crate::cmd::a_shell_open;
assert!(a_shell_open("C:\\").await.is_ok());
}
#[tokio::test]
async fn test_cmd_bat() {
let cwd = std::env::current_dir().unwrap().join("examples");
let output = Cmd::new("test.bat").cwd(cwd).a_output().await.unwrap();
assert!(output.stdout.contains("test"));
assert!(!Cmd::new("test.bat")
.a_output()
.await
.unwrap()
.status
.success());
}
#[tokio::test]
async fn test_cmd_type() {
assert!(Cmd::new("echo Hello from cmd")
.set_type(ExeType::Cmd)
.output()
.is_ok());
assert!(Cmd::new("echo Hello from cmd")
.set_type(ExeType::AutoShell)
.output()
.is_ok());
assert!(Cmd::new("echo.exe")
.args(["Hello", "from", "cmd"])
.set_type(ExeType::IosApp)
.output()
.is_err());
assert!(Cmd::new("echo Hello from cmd")
.set_type(ExeType::WindowsExe)
.output()
.is_err());
}
#[tokio::test]
async fn test_cmd_zh() {
let output = Cmd::new("echo 你好Rust").a_output().await.unwrap();
assert_eq!(output.stdout, "你好Rust");
}
}
mod sync {
use crate::cmd::{shell_open, Cmd, CmdResult};
use serde::{Deserialize, Serialize};
#[test]
#[cfg(target_os = "windows")]
fn test_shell_open_windows() {
assert!(shell_open("C:\\").is_ok());
}
#[test]
#[cfg(not(target_os = "windows"))]
fn test_shell_open_unix() {
assert!(shell_open("/").is_ok());
}
#[test]
fn test_cmd() {
let output = Cmd::new("echo Hello from cmd").output().unwrap();
assert_eq!(output.stdout, "Hello from cmd");
assert!(Cmd::new("echo Hello from cmd").output().is_err());
}
#[test]
fn test_cmd_result_serialization() {
#[derive(Debug, Serialize, Deserialize)]
struct TestOpts {
value: String,
}
let result = CmdResult {
content: "Test content".to_string(),
status: true,
opts: TestOpts {
value: "test".to_string(),
},
};
let serialized = result.to_str().unwrap();
assert!(serialized.starts_with("R<") && serialized.ends_with(">R"));
let deserialized: CmdResult<TestOpts> = CmdResult::from_str(&serialized).unwrap();
assert_eq!(deserialized.content, "Test content");
assert_eq!(deserialized.status, true);
assert_eq!(deserialized.opts.value, "test");
}
#[test]
fn test_cmd_bat() {
let cwd = std::env::current_dir().unwrap().join("examples");
let output = Cmd::new("test.bat").cwd(cwd).output().unwrap();
assert!(output.stdout.contains("test"));
assert!(!Cmd::new("test.bat").output().unwrap().status.success());
}
}
}