shell.rs 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use super::InvokeContext;
  5. use crate::{api::ipc::CallbackFn, Runtime};
  6. #[cfg(shell_execute)]
  7. use crate::{Manager, Scopes};
  8. use serde::Deserialize;
  9. use tauri_macros::{module_command_handler, CommandModule};
  10. #[cfg(shell_scope)]
  11. use crate::ExecuteArgs;
  12. #[cfg(not(shell_scope))]
  13. type ExecuteArgs = ();
  14. #[cfg(shell_execute)]
  15. use std::sync::{Arc, Mutex};
  16. use std::{collections::HashMap, path::PathBuf};
  17. type ChildId = u32;
  18. #[cfg(shell_execute)]
  19. type ChildStore = Arc<Mutex<HashMap<ChildId, crate::api::process::CommandChild>>>;
  20. #[cfg(shell_execute)]
  21. fn command_childs() -> &'static ChildStore {
  22. use once_cell::sync::Lazy;
  23. static STORE: Lazy<ChildStore> = Lazy::new(Default::default);
  24. &STORE
  25. }
  26. #[derive(Debug, Clone, Deserialize)]
  27. #[serde(untagged)]
  28. pub enum Buffer {
  29. Text(String),
  30. Raw(Vec<u8>),
  31. }
  32. #[allow(clippy::unnecessary_wraps)]
  33. fn default_env() -> Option<HashMap<String, String>> {
  34. Some(HashMap::default())
  35. }
  36. #[allow(dead_code)]
  37. #[derive(Debug, Clone, Default, Deserialize)]
  38. #[serde(rename_all = "camelCase")]
  39. pub struct CommandOptions {
  40. #[serde(default)]
  41. sidecar: bool,
  42. cwd: Option<PathBuf>,
  43. // by default we don't add any env variables to the spawned process
  44. // but the env is an `Option` so when it's `None` we clear the env.
  45. #[serde(default = "default_env")]
  46. env: Option<HashMap<String, String>>,
  47. }
  48. /// The API descriptor.
  49. #[derive(Deserialize, CommandModule)]
  50. #[serde(tag = "cmd", rename_all = "camelCase")]
  51. pub enum Cmd {
  52. /// The execute script API.
  53. #[serde(rename_all = "camelCase")]
  54. Execute {
  55. program: String,
  56. args: ExecuteArgs,
  57. on_event_fn: CallbackFn,
  58. #[serde(default)]
  59. options: CommandOptions,
  60. },
  61. StdinWrite {
  62. pid: ChildId,
  63. buffer: Buffer,
  64. },
  65. KillChild {
  66. pid: ChildId,
  67. },
  68. Open {
  69. path: String,
  70. with: Option<String>,
  71. },
  72. }
  73. impl Cmd {
  74. #[allow(unused_variables)]
  75. fn execute<R: Runtime>(
  76. context: InvokeContext<R>,
  77. program: String,
  78. args: ExecuteArgs,
  79. on_event_fn: CallbackFn,
  80. options: CommandOptions,
  81. ) -> crate::Result<ChildId> {
  82. let mut command = if options.sidecar {
  83. #[cfg(not(shell_sidecar))]
  84. return Err(crate::Error::ApiNotAllowlisted(
  85. "shell > sidecar".to_string(),
  86. ));
  87. #[cfg(shell_sidecar)]
  88. {
  89. let program = PathBuf::from(program);
  90. let program_as_string = program.display().to_string();
  91. let program_no_ext_as_string = program.with_extension("").display().to_string();
  92. let is_configured = context
  93. .config
  94. .tauri
  95. .bundle
  96. .external_bin
  97. .as_ref()
  98. .map(|bins| {
  99. bins
  100. .iter()
  101. .any(|b| b == &program_as_string || b == &program_no_ext_as_string)
  102. })
  103. .unwrap_or_default();
  104. if is_configured {
  105. context
  106. .window
  107. .state::<Scopes>()
  108. .shell
  109. .prepare(&program.to_string_lossy(), args, true)
  110. .map_err(Box::new)?
  111. } else {
  112. return Err(crate::Error::SidecarNotAllowed(program));
  113. }
  114. }
  115. } else {
  116. #[cfg(not(shell_execute))]
  117. return Err(crate::Error::ApiNotAllowlisted(
  118. "shell > execute".to_string(),
  119. ));
  120. #[cfg(shell_execute)]
  121. match context
  122. .window
  123. .state::<Scopes>()
  124. .shell
  125. .prepare(&program, args, false)
  126. {
  127. Ok(cmd) => cmd,
  128. Err(_) => return Err(crate::Error::ProgramNotAllowed(PathBuf::from(program))),
  129. }
  130. };
  131. #[cfg(any(shell_execute, shell_sidecar))]
  132. {
  133. if let Some(cwd) = options.cwd {
  134. command = command.current_dir(cwd);
  135. }
  136. if let Some(env) = options.env {
  137. command = command.envs(env);
  138. } else {
  139. command = command.env_clear();
  140. }
  141. let (mut rx, child) = command.spawn()?;
  142. let pid = child.pid();
  143. command_childs().lock().unwrap().insert(pid, child);
  144. crate::async_runtime::spawn(async move {
  145. while let Some(event) = rx.recv().await {
  146. if matches!(event, crate::api::process::CommandEvent::Terminated(_)) {
  147. command_childs().lock().unwrap().remove(&pid);
  148. }
  149. let js = crate::api::ipc::format_callback(on_event_fn, &event)
  150. .expect("unable to serialize CommandEvent");
  151. let _ = context.window.eval(js.as_str());
  152. }
  153. });
  154. Ok(pid)
  155. }
  156. }
  157. #[cfg(any(shell_execute, shell_sidecar))]
  158. fn stdin_write<R: Runtime>(
  159. _context: InvokeContext<R>,
  160. pid: ChildId,
  161. buffer: Buffer,
  162. ) -> crate::Result<()> {
  163. if let Some(child) = command_childs().lock().unwrap().get_mut(&pid) {
  164. match buffer {
  165. Buffer::Text(t) => child.write(t.as_bytes())?,
  166. Buffer::Raw(r) => child.write(&r)?,
  167. }
  168. }
  169. Ok(())
  170. }
  171. #[cfg(not(any(shell_execute, shell_sidecar)))]
  172. fn stdin_write<R: Runtime>(
  173. _context: InvokeContext<R>,
  174. _pid: ChildId,
  175. _buffer: Buffer,
  176. ) -> crate::Result<()> {
  177. Err(crate::Error::ApiNotAllowlisted(
  178. "shell > execute or shell > sidecar".into(),
  179. ))
  180. }
  181. #[cfg(any(shell_execute, shell_sidecar))]
  182. fn kill_child<R: Runtime>(_context: InvokeContext<R>, pid: ChildId) -> crate::Result<()> {
  183. if let Some(child) = command_childs().lock().unwrap().remove(&pid) {
  184. child.kill()?;
  185. }
  186. Ok(())
  187. }
  188. #[cfg(not(any(shell_execute, shell_sidecar)))]
  189. fn kill_child<R: Runtime>(_context: InvokeContext<R>, _pid: ChildId) -> crate::Result<()> {
  190. Err(crate::Error::ApiNotAllowlisted(
  191. "shell > execute or shell > sidecar".into(),
  192. ))
  193. }
  194. /// Open a (url) path with a default or specific browser opening program.
  195. ///
  196. /// See [`crate::api::shell::open`] for how it handles security-related measures.
  197. #[module_command_handler(shell_open, "shell > open")]
  198. fn open<R: Runtime>(
  199. context: InvokeContext<R>,
  200. path: String,
  201. with: Option<String>,
  202. ) -> crate::Result<()> {
  203. use std::str::FromStr;
  204. with
  205. .as_deref()
  206. // only allow pre-determined programs to be specified
  207. .map(crate::api::shell::Program::from_str)
  208. .transpose()
  209. .map_err(Into::into)
  210. // validate and open path
  211. .and_then(|with| {
  212. crate::api::shell::open(&context.window.state::<Scopes>().shell, path, with)
  213. .map_err(Into::into)
  214. })
  215. }
  216. }
  217. #[cfg(test)]
  218. mod tests {
  219. use super::{Buffer, ChildId, CommandOptions, ExecuteArgs};
  220. use crate::api::ipc::CallbackFn;
  221. use quickcheck::{Arbitrary, Gen};
  222. impl Arbitrary for CommandOptions {
  223. fn arbitrary(g: &mut Gen) -> Self {
  224. Self {
  225. sidecar: false,
  226. cwd: Option::arbitrary(g),
  227. env: Option::arbitrary(g),
  228. }
  229. }
  230. }
  231. impl Arbitrary for Buffer {
  232. fn arbitrary(g: &mut Gen) -> Self {
  233. Buffer::Text(String::arbitrary(g))
  234. }
  235. }
  236. #[cfg(shell_scope)]
  237. impl Arbitrary for ExecuteArgs {
  238. fn arbitrary(_: &mut Gen) -> Self {
  239. ExecuteArgs::None
  240. }
  241. }
  242. #[tauri_macros::module_command_test(shell_execute, "shell > execute")]
  243. #[quickcheck_macros::quickcheck]
  244. fn execute(
  245. _program: String,
  246. _args: ExecuteArgs,
  247. _on_event_fn: CallbackFn,
  248. _options: CommandOptions,
  249. ) {
  250. }
  251. #[tauri_macros::module_command_test(shell_execute, "shell > execute or shell > sidecar")]
  252. #[quickcheck_macros::quickcheck]
  253. fn stdin_write(_pid: ChildId, _buffer: Buffer) {}
  254. #[tauri_macros::module_command_test(shell_execute, "shell > execute or shell > sidecar")]
  255. #[quickcheck_macros::quickcheck]
  256. fn kill_child(_pid: ChildId) {}
  257. #[tauri_macros::module_command_test(shell_open, "shell > open")]
  258. #[quickcheck_macros::quickcheck]
  259. fn open(_path: String, _with: Option<String>) {}
  260. }