Skip to main content

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