Skip to main content

Mountain/ApplicationState/DTO/
TerminalStateDTO.rs

1//! # TerminalStateDTO
2//!
3//! # RESPONSIBILITY
4//! - Data transfer object for integrated terminal state
5//! - Serializable format for gRPC/IPC transmission
6//! - Used by Mountain to track terminal lifecycle and configuration
7//! - Contains runtime handles for PTY I/O
8//!
9//! # FIELDS
10//! - Identifier: Unique terminal identifier
11//! - Name: Terminal display name
12//! - OSProcessIdentifier: OS process ID
13//! - ShellPath: Shell executable path
14//! - ShellArguments: Shell launch arguments
15//! - CurrentWorkingDirectory: Working directory path
16//! - EnvironmentVariables: Environment variable map
17//! - IsPTY: PTY mode flag
18//! - PTYInputTransmitter: PTY input channel sender
19//! - ReaderTaskHandle: Output reader task handle
20//! - ProcessWaitHandle: Process wait task handle
21use std::{
22	collections::HashMap,
23	path::PathBuf,
24	sync::{Arc, Mutex as StandardMutex},
25};
26
27use portable_pty::MasterPty;
28use serde::{Deserialize, Serialize};
29use serde_json::Value;
30use tokio::{
31	sync::{Mutex as TokioMutex, mpsc as TokioMPSC},
32	task::JoinHandle,
33};
34
35/// Thread-safe handle around a portable-pty master PTY. We keep the handle
36/// alive past CreateTerminal so Resize / drop-to-kill semantics work. Not
37/// Clone / Serialize; the surrounding struct marks it `#[serde(skip)]`.
38pub type PtyMasterHandle = Arc<StandardMutex<Box<dyn MasterPty + Send>>>;
39
40/// Maximum terminal name length
41const MAX_TERMINAL_NAME_LENGTH:usize = 128;
42
43/// Maximum shell path length
44const MAX_SHELL_PATH_LENGTH:usize = 1024;
45
46/// Maximum number of shell arguments
47const MAX_SHELL_ARGUMENTS:usize = 100;
48
49/// Maximum argument string length
50const MAX_ARGUMENT_LENGTH:usize = 4096;
51
52/// Maximum number of environment variables
53#[allow(dead_code)]
54const MAX_ENV_VARS:usize = 1000;
55
56/// Holds the complete state and runtime resources for a single pseudo-terminal
57/// (PTY) instance. This includes configuration, process identifiers, and
58/// handles for I/O tasks.
59///
60/// `Debug` is implemented manually at the bottom of this file because the
61/// `PTYMaster` field stores `dyn MasterPty + Send`, which does not itself
62/// implement `Debug`. The manual impl prints the master handle as an opaque
63/// placeholder so the surrounding struct remains `Debug`-printable.
64#[derive(Clone, Serialize, Deserialize)]
65pub struct TerminalStateDTO {
66	// --- Identifiers ---
67	/// Unique terminal identifier
68	pub Identifier:u64,
69
70	/// Terminal display name
71	#[serde(skip_serializing_if = "String::is_empty")]
72	pub Name:String,
73
74	/// OS process identifier (if running)
75	pub OSProcessIdentifier:Option<u32>,
76
77	// --- Configuration ---
78	/// Shell executable path
79	#[serde(skip_serializing_if = "String::is_empty")]
80	pub ShellPath:String,
81
82	/// Shell launch arguments
83	#[serde(skip_serializing_if = "Vec::is_empty")]
84	pub ShellArguments:Vec<String>,
85
86	/// Current working directory
87	pub CurrentWorkingDirectory:Option<PathBuf>,
88
89	/// Environment variables map
90	#[serde(skip_serializing_if = "Option::is_none")]
91	pub EnvironmentVariables:Option<HashMap<String, Option<String>>>,
92
93	/// Whether this is a PTY terminal
94	pub IsPTY:bool,
95
96	// --- Runtime Handles ---
97	/// Channel for sending input to PTY
98	#[serde(skip)]
99	pub PTYInputTransmitter:Option<TokioMPSC::Sender<String>>,
100
101	/// Handle for output reader task
102	#[serde(skip)]
103	pub ReaderTaskHandle:Option<Arc<TokioMutex<Option<JoinHandle<()>>>>>,
104
105	/// Handle for process wait task
106	#[serde(skip)]
107	pub ProcessWaitHandle:Option<Arc<TokioMutex<Option<JoinHandle<()>>>>>,
108
109	/// Master PTY handle kept alive for `Resize` and for ownership semantics
110	/// (dropping the master closes the slave, terminating the shell).
111	#[serde(skip)]
112	pub PTYMaster:Option<PtyMasterHandle>,
113}
114
115impl TerminalStateDTO {
116	/// Creates a new `TerminalStateDTO` by parsing terminal options from a
117	/// `serde_json::Value` with validation.
118	///
119	/// # Arguments
120	/// * `Identifier` - Unique terminal identifier
121	/// * `Name` - Terminal display name
122	/// * `OptionsValue` - Terminal options JSON
123	/// * `DefaultShellPath` - Default shell if not specified
124	///
125	/// # Returns
126	/// Result containing the DTO or validation error
127	pub fn Create(Identifier:u64, Name:String, OptionsValue:&Value, DefaultShellPath:String) -> Result<Self, String> {
128		// Validate name length
129		if Name.len() > MAX_TERMINAL_NAME_LENGTH {
130			return Err(format!(
131				"Terminal name exceeds maximum length of {} bytes",
132				MAX_TERMINAL_NAME_LENGTH
133			));
134		}
135
136		let ShellPath = OptionsValue
137			.get("shellPath")
138			.and_then(Value::as_str)
139			.unwrap_or(&DefaultShellPath)
140			.to_string();
141
142		// Validate shell path length
143		if ShellPath.len() > MAX_SHELL_PATH_LENGTH {
144			return Err(format!("Shell path exceeds maximum length of {} bytes", MAX_SHELL_PATH_LENGTH));
145		}
146
147		let ShellArguments = match OptionsValue.get("shellArgs") {
148			Some(Value::Array(Array)) => {
149				let Args:Vec<String> = Array.iter().filter_map(Value::as_str).map(String::from).collect();
150
151				// Validate argument count
152				if Args.len() > MAX_SHELL_ARGUMENTS {
153					return Err(format!("Shell arguments exceed maximum count of {}", MAX_SHELL_ARGUMENTS));
154				}
155
156				// Validate individual argument lengths
157				for Arg in &Args {
158					if Arg.len() > MAX_ARGUMENT_LENGTH {
159						return Err(format!(
160							"Shell argument exceeds maximum length of {} bytes",
161							MAX_ARGUMENT_LENGTH
162						));
163					}
164				}
165
166				Args
167			},
168
169			_ => Vec::new(),
170		};
171
172		let CWD = OptionsValue.get("cwd").and_then(Value::as_str).map(PathBuf::from);
173
174		// A more complete implementation would parse the `env` object.
175		let EnvVars = None;
176
177		Ok(Self {
178			Identifier,
179			Name,
180			ShellPath,
181			ShellArguments,
182			CurrentWorkingDirectory:CWD,
183			EnvironmentVariables:EnvVars,
184			OSProcessIdentifier:None,
185			IsPTY:true,
186			PTYInputTransmitter:None,
187			ReaderTaskHandle:None,
188			ProcessWaitHandle:None,
189			PTYMaster:None,
190		})
191	}
192
193	/// Checks if the terminal process is currently running.
194	pub fn IsRunning(&self) -> bool { self.OSProcessIdentifier.is_some() }
195
196	/// Checks if the terminal has an active PTY input channel.
197	pub fn HasInputChannel(&self) -> bool { self.PTYInputTransmitter.is_some() }
198
199	/// Returns the working directory as a string, or default if not set.
200	pub fn GetWorkingDirectory(&self) -> String {
201		self.CurrentWorkingDirectory
202			.as_ref()
203			.and_then(|Path| Path.to_str())
204			.unwrap_or("")
205			.to_string()
206	}
207
208	/// Clears the runtime handles (useful when terminating terminal).
209	pub fn ClearHandles(&mut self) {
210		self.PTYInputTransmitter = None;
211		self.ReaderTaskHandle = None;
212		self.ProcessWaitHandle = None;
213		self.PTYMaster = None;
214	}
215}
216
217impl std::fmt::Debug for TerminalStateDTO {
218	fn fmt(&self, Formatter:&mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219		Formatter
220			.debug_struct("TerminalStateDTO")
221			.field("Identifier", &self.Identifier)
222			.field("Name", &self.Name)
223			.field("OSProcessIdentifier", &self.OSProcessIdentifier)
224			.field("ShellPath", &self.ShellPath)
225			.field("ShellArguments", &self.ShellArguments)
226			.field("CurrentWorkingDirectory", &self.CurrentWorkingDirectory)
227			.field("EnvironmentVariables", &self.EnvironmentVariables)
228			.field("IsPTY", &self.IsPTY)
229			.field("PTYInputTransmitter", &self.PTYInputTransmitter.as_ref().map(|_| "<channel>"))
230			.field("ReaderTaskHandle", &self.ReaderTaskHandle.as_ref().map(|_| "<task>"))
231			.field("ProcessWaitHandle", &self.ProcessWaitHandle.as_ref().map(|_| "<task>"))
232			.field("PTYMaster", &self.PTYMaster.as_ref().map(|_| "<master-pty>"))
233			.finish()
234	}
235}