1mod selection;
4mod session_store;
5
6use std::future::Future;
7use std::pin::Pin;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::Arc;
10
11use serde::{Deserialize, Serialize};
12use tokio::sync::Mutex;
13
14use crate::errors::Result;
15use crate::state::bash_state::BashState;
16use crate::tools::bash_command::BashCommandResult;
17use crate::types::BashCommand;
18
19#[cfg(unix)]
20pub use selection::{
21 configured_daemon_binary, ensure_control_daemon_at, ensure_daemon_at, restart_control_daemon_at,
22};
23pub use selection::{
24 configured_runtime_mode, configured_shell_runtime, select_runtime_mode, RuntimeMode,
25};
26pub(crate) use session_store::lock_session_store;
27pub use session_store::{SessionStore, ShellTarget};
28
29pub type ShellRuntimeFuture<'a> =
31 Pin<Box<dyn Future<Output = Result<BashCommandResult>> + Send + 'a>>;
32pub type ShellRuntimeUnitFuture<'a> = Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
33pub type ShellRuntimeBoolFuture<'a> = Pin<Box<dyn Future<Output = Result<bool>> + Send + 'a>>;
34pub type ShellRuntimeDetailedFuture<'a> =
35 Pin<Box<dyn Future<Output = Result<BashCommandRuntimeResult>> + Send + 'a>>;
36
37#[derive(Clone, Debug, Eq, PartialEq)]
41pub struct BashCommandRuntimeResult {
42 pub result: BashCommandResult,
43 pub compact_output: Option<String>,
44 pub command_generation: Option<u64>,
45 pub execution_token: Option<ShellExecutionToken>,
46 pub generation_bound_actions: bool,
47 pub output_truncated: bool,
50}
51
52impl BashCommandRuntimeResult {
53 pub fn legacy(result: BashCommandResult) -> Self {
54 Self {
55 result,
56 compact_output: None,
57 command_generation: None,
58 execution_token: None,
59 generation_bound_actions: false,
60 output_truncated: false,
61 }
62 }
63}
64
65#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
66pub struct ShellExecutionToken {
67 pub guardian_epoch: String,
68 pub session_epoch: String,
69 pub generation: u64,
70}
71
72#[derive(Clone, Debug, Default, Serialize, Deserialize)]
73pub struct ShellActionOptions {
74 pub compact_output: bool,
75 pub expected_generation: Option<u64>,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub expected_execution: Option<ShellExecutionToken>,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub expected_guardian_epoch: Option<String>,
84 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
87 pub require_generation_binding: bool,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub cancellation_key: Option<String>,
92 #[serde(skip)]
95 pub launch_cancelled: Option<Arc<AtomicBool>>,
96 #[cfg(test)]
99 #[serde(skip)]
100 pub(crate) force_clear_to_run_failure: bool,
101}
102
103impl ShellActionOptions {
104 pub(crate) fn is_default(&self) -> bool {
105 let is_default = !self.compact_output
106 && self.expected_generation.is_none()
107 && self.expected_execution.is_none()
108 && self.expected_guardian_epoch.is_none()
109 && !self.require_generation_binding
110 && self.cancellation_key.is_none();
111 #[cfg(test)]
112 {
113 is_default && !self.force_clear_to_run_failure
114 }
115 #[cfg(not(test))]
116 {
117 is_default
118 }
119 }
120
121 pub(crate) fn is_launch_cancelled(&self) -> bool {
122 self.launch_cancelled.as_ref().is_some_and(|flag| flag.load(Ordering::SeqCst))
123 }
124}
125
126impl PartialEq for ShellActionOptions {
127 fn eq(&self, other: &Self) -> bool {
128 let equal = self.compact_output == other.compact_output
129 && self.expected_generation == other.expected_generation
130 && self.expected_execution == other.expected_execution
131 && self.expected_guardian_epoch == other.expected_guardian_epoch
132 && self.require_generation_binding == other.require_generation_binding
133 && self.cancellation_key == other.cancellation_key;
134 #[cfg(test)]
135 {
136 equal && self.force_clear_to_run_failure == other.force_clear_to_run_failure
137 }
138 #[cfg(not(test))]
139 {
140 equal
141 }
142 }
143}
144
145impl Eq for ShellActionOptions {}
146
147#[derive(Clone, Debug, Default, Eq, PartialEq)]
148pub struct ShellSessionConfiguration {
149 pub attach_hint: Option<String>,
150 pub attached_existing: bool,
151}
152
153pub type ShellRuntimeConfigureFuture<'a> =
154 Pin<Box<dyn Future<Output = Result<ShellSessionConfiguration>> + Send + 'a>>;
155
156#[derive(Clone, Copy, Debug, Eq, PartialEq)]
158pub enum ShellSessionTransition {
159 FirstCall,
160 ModeChange,
161 Reset,
162 WorkspaceChange,
163}
164
165pub trait ShellRuntime: Send + Sync {
167 fn configure_session<'a>(
168 &'a self,
169 bash_state: &'a mut BashState,
170 transition: ShellSessionTransition,
171 ) -> ShellRuntimeConfigureFuture<'a>;
172
173 fn run_action<'a>(
174 &'a self,
175 bash_state: &'a Arc<Mutex<Option<BashState>>>,
176 command: BashCommand,
177 ) -> ShellRuntimeFuture<'a>;
178
179 fn run_action_detailed<'a>(
182 &'a self,
183 bash_state: &'a Arc<Mutex<Option<BashState>>>,
184 command: BashCommand,
185 _options: ShellActionOptions,
186 ) -> ShellRuntimeDetailedFuture<'a> {
187 Box::pin(async move {
188 self.run_action(bash_state, command).await.map(BashCommandRuntimeResult::legacy)
189 })
190 }
191
192 fn supports_generation_bound_actions(&self) -> ShellRuntimeBoolFuture<'_> {
193 Box::pin(async { Ok(false) })
194 }
195
196 fn supports_generation_bound_actions_for<'a>(
200 &'a self,
201 _bash_state: &'a Arc<Mutex<Option<BashState>>>,
202 ) -> ShellRuntimeBoolFuture<'a> {
203 self.supports_generation_bound_actions()
204 }
205
206 fn interrupt<'a>(
207 &'a self,
208 bash_state: &'a Arc<Mutex<Option<BashState>>>,
209 ) -> ShellRuntimeUnitFuture<'a>;
210
211 fn interrupt_generation<'a>(
214 &'a self,
215 bash_state: &'a Arc<Mutex<Option<BashState>>>,
216 expected_generation: Option<u64>,
217 ) -> ShellRuntimeBoolFuture<'a> {
218 Box::pin(async move {
219 if expected_generation.is_some() {
220 return Ok(false);
221 }
222 self.interrupt(bash_state).await?;
223 Ok(true)
224 })
225 }
226
227 fn interrupt_execution<'a>(
230 &'a self,
231 bash_state: &'a Arc<Mutex<Option<BashState>>>,
232 expected: Option<ShellExecutionToken>,
233 ) -> ShellRuntimeBoolFuture<'a> {
234 self.interrupt_generation(bash_state, expected.map(|token| token.generation))
235 }
236
237 fn cancel_pending_action<'a>(
241 &'a self,
242 _bash_state: &'a Arc<Mutex<Option<BashState>>>,
243 _cancellation_key: &'a str,
244 ) -> ShellRuntimeBoolFuture<'a> {
245 Box::pin(async { Ok(false) })
246 }
247
248 fn terminate_session<'a>(&'a self, thread_id: &'a str) -> ShellRuntimeUnitFuture<'a>;
250}
251
252#[derive(Clone, Copy, Debug, Default)]
255pub struct EmbeddedShellRuntime;
256
257impl ShellRuntime for EmbeddedShellRuntime {
258 fn configure_session<'a>(
259 &'a self,
260 bash_state: &'a mut BashState,
261 transition: ShellSessionTransition,
262 ) -> ShellRuntimeConfigureFuture<'a> {
263 Box::pin(async move {
264 let operation_barrier =
265 lock_session_store().operation_barrier(&bash_state.current_thread_id);
266 let _operation = operation_barrier.write().await;
267 if matches!(
268 transition,
269 ShellSessionTransition::FirstCall | ShellSessionTransition::Reset
270 ) && (transition == ShellSessionTransition::Reset || bash_state.cwd.exists())
271 {
272 bash_state.init_pty_shell().await?;
273 }
274 let attach_hint = {
275 let shell = bash_state.pty_shell.lock().await;
276 shell.as_ref().and_then(|shell| shell.attach_hint.clone())
277 };
278 Ok(ShellSessionConfiguration { attach_hint, attached_existing: false })
279 })
280 }
281
282 fn run_action<'a>(
283 &'a self,
284 bash_state: &'a Arc<Mutex<Option<BashState>>>,
285 command: BashCommand,
286 ) -> ShellRuntimeFuture<'a> {
287 Box::pin(crate::tools::bash_command::handle_embedded_tool_call(bash_state, command))
288 }
289
290 fn run_action_detailed<'a>(
291 &'a self,
292 bash_state: &'a Arc<Mutex<Option<BashState>>>,
293 command: BashCommand,
294 options: ShellActionOptions,
295 ) -> ShellRuntimeDetailedFuture<'a> {
296 Box::pin(crate::tools::bash_command::handle_embedded_tool_call_detailed(
297 bash_state, command, options,
298 ))
299 }
300
301 fn supports_generation_bound_actions(&self) -> ShellRuntimeBoolFuture<'_> {
302 Box::pin(async { Ok(true) })
303 }
304
305 fn interrupt<'a>(
306 &'a self,
307 bash_state: &'a Arc<Mutex<Option<BashState>>>,
308 ) -> ShellRuntimeUnitFuture<'a> {
309 Box::pin(interrupt_embedded(bash_state))
310 }
311
312 fn interrupt_generation<'a>(
313 &'a self,
314 bash_state: &'a Arc<Mutex<Option<BashState>>>,
315 expected_generation: Option<u64>,
316 ) -> ShellRuntimeBoolFuture<'a> {
317 Box::pin(interrupt_embedded_generation(bash_state, expected_generation))
318 }
319
320 fn interrupt_execution<'a>(
321 &'a self,
322 bash_state: &'a Arc<Mutex<Option<BashState>>>,
323 expected: Option<ShellExecutionToken>,
324 ) -> ShellRuntimeBoolFuture<'a> {
325 Box::pin(interrupt_embedded_execution(bash_state, expected))
326 }
327
328 fn terminate_session<'a>(&'a self, thread_id: &'a str) -> ShellRuntimeUnitFuture<'a> {
329 Box::pin(async move {
330 let operation_barrier = lock_session_store().operation_barrier(thread_id);
331 let _operation = operation_barrier.write().await;
332 let shell = lock_session_store().resolve(thread_id, &ShellTarget::Main);
333 if let Some(shell) = shell {
334 *shell.lock().await = None;
335 }
336 Ok(())
337 })
338 }
339}
340
341async fn interrupt_embedded(bash_state: &Arc<Mutex<Option<BashState>>>) -> Result<()> {
342 let _ = interrupt_embedded_generation(bash_state, None).await?;
343 Ok(())
344}
345
346async fn interrupt_embedded_generation(
347 bash_state: &Arc<Mutex<Option<BashState>>>,
348 expected_generation: Option<u64>,
349) -> Result<bool> {
350 interrupt_embedded_guarded(bash_state, expected_generation, None).await
351}
352
353async fn interrupt_embedded_execution(
354 bash_state: &Arc<Mutex<Option<BashState>>>,
355 expected: Option<ShellExecutionToken>,
356) -> Result<bool> {
357 interrupt_embedded_guarded(
358 bash_state,
359 expected.as_ref().map(|token| token.generation),
360 expected.as_ref(),
361 )
362 .await
363}
364
365async fn interrupt_embedded_guarded(
366 bash_state: &Arc<Mutex<Option<BashState>>>,
367 expected_generation: Option<u64>,
368 expected_execution: Option<&ShellExecutionToken>,
369) -> Result<bool> {
370 let (shell, _operation) = {
373 let state = bash_state.lock().await;
374 let Some(state) = state.as_ref() else { return Ok(false) };
375 let operation_barrier = lock_session_store().operation_barrier(&state.current_thread_id);
376 let operation = operation_barrier.read_owned().await;
377 (state.pty_shell.clone(), operation)
378 };
379 {
380 let mut guard = shell.lock().await;
381 if let Some(pty) = guard.as_mut() {
382 if expected_execution.is_some_and(|expected| {
383 expected.guardian_epoch != "embedded"
384 || expected.session_epoch != format!("{:016x}", pty.incarnation())
385 }) {
386 return Ok(false);
387 }
388 if expected_generation.is_some_and(|expected| {
389 pty.command_generation() != expected || !pty.command_running
390 }) {
391 return Ok(false);
392 }
393 pty.send_interrupt().map_err(|error| {
394 crate::errors::WinxError::CommandExecutionError(format!(
395 "failed to interrupt shell: {error}"
396 ))
397 })?;
398 }
399 }
400
401 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
402 while std::time::Instant::now() < deadline {
403 let recovered = {
404 let mut guard = shell.lock().await;
405 match guard.as_mut() {
406 Some(pty) => pty.poll_output_nonblocking() || !pty.command_running,
407 None => true,
408 }
409 };
410 if recovered {
411 return Ok(true);
412 }
413 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
414 }
415 Err(crate::errors::WinxError::CommandExecutionError(
416 "interrupted shell did not return to a prompt within 3 seconds".to_string(),
417 ))
418}