1use std::{env, io::Write, sync::Arc};
51
52use CommonLibrary::{
53 Environment::Requires::Requires,
54 Error::CommonError::CommonError,
55 IPC::{IPCProvider::IPCProvider, SkyEvent::SkyEvent},
56 Terminal::TerminalProvider::TerminalProvider,
57};
58use async_trait::async_trait;
59use portable_pty::{CommandBuilder, MasterPty, NativePtySystem, PtySize, PtySystem};
60use serde_json::{Value, json};
61use tauri::Emitter;
62use tokio::sync::mpsc as TokioMPSC;
63
64use super::{MountainEnvironment::MountainEnvironment, Utility};
65use crate::{ApplicationState::DTO::TerminalStateDTO::TerminalStateDTO, IPC::SkyEmit::LogSkyEmit, dev_log};
66
67const MAX_BUFFERED_BYTES:usize = 64 * 1024;
81
82static TERMINAL_OUTPUT_BUFFER:std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<u64, Vec<u8>>>> =
83 std::sync::OnceLock::new();
84
85fn TerminalOutputBuffer() -> &'static std::sync::Mutex<std::collections::HashMap<u64, Vec<u8>>> {
86 TERMINAL_OUTPUT_BUFFER.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
87}
88
89pub fn AppendTerminalOutput(TerminalId:u64, Bytes:&[u8]) {
90 if let Ok(mut Map) = TerminalOutputBuffer().lock() {
91 let Entry = Map.entry(TerminalId).or_insert_with(Vec::new);
92
93 Entry.extend_from_slice(Bytes);
94
95 if Entry.len() > MAX_BUFFERED_BYTES {
98 let DropCount = Entry.len() - MAX_BUFFERED_BYTES;
99
100 Entry.drain(..DropCount);
101 }
102 }
103}
104
105pub fn DrainTerminalOutputBuffer() -> Vec<(u64, Vec<u8>)> {
106 if let Ok(Map) = TerminalOutputBuffer().lock() {
107 Map.iter().map(|(K, V)| (*K, V.clone())).collect()
108 } else {
109 Vec::new()
110 }
111}
112
113pub fn RemoveTerminalOutputBuffer(TerminalId:u64) {
114 if let Ok(mut Map) = TerminalOutputBuffer().lock() {
115 Map.remove(&TerminalId);
116 }
117}
118
119#[async_trait]
126impl TerminalProvider for MountainEnvironment {
127 async fn CreateTerminal(&self, OptionsValue:Value) -> Result<Value, CommonError> {
129 let TerminalIdentifier = self.ApplicationState.GetNextTerminalIdentifier();
130
131 let DefaultShell = if cfg!(windows) {
132 "powershell.exe".to_string()
133 } else {
134 env::var("SHELL").unwrap_or_else(|_| "sh".to_string())
135 };
136
137 let Name = OptionsValue
138 .get("name")
139 .and_then(Value::as_str)
140 .unwrap_or("terminal")
141 .to_string();
142
143 dev_log!(
144 "terminal",
145 "[TerminalProvider] Creating terminal ID: {}, Name: '{}'",
146 TerminalIdentifier,
147 Name
148 );
149
150 let mut TerminalState = TerminalStateDTO::Create(TerminalIdentifier, Name.clone(), &OptionsValue, DefaultShell)
151 .map_err(|e| {
152 CommonError::ConfigurationLoad { Description:format!("Failed to create terminal state: {}", e) }
153 })?;
154
155 let PtySystem = NativePtySystem::default();
156
157 let PtyPair = PtySystem
158 .openpty(PtySize::default())
159 .map_err(|Error| CommonError::IPCError { Description:format!("Failed to open PTY: {}", Error) })?;
160
161 let mut Command = CommandBuilder::new(&TerminalState.ShellPath);
162
163 Command.args(&TerminalState.ShellArguments);
164
165 if let Some(CWD) = &TerminalState.CurrentWorkingDirectory {
166 Command.cwd(CWD);
167 }
168
169 let mut ChildProcess = PtyPair.slave.spawn_command(Command).map_err(|Error| {
170 CommonError::IPCError { Description:format!("Failed to spawn shell process: {}", Error) }
171 })?;
172
173 TerminalState.OSProcessIdentifier = ChildProcess.process_id();
174
175 let mut PTYWriter = PtyPair.master.take_writer().map_err(|Error| {
176 CommonError::FileSystemIO {
177 Path:"pty master".into(),
178
179 Description:format!("Failed to take PTY writer: {}", Error),
180 }
181 })?;
182
183 let (InputTransmitter, mut InputReceiver) = TokioMPSC::channel::<String>(32);
184
185 TerminalState.PTYInputTransmitter = Some(InputTransmitter);
186
187 let TermIDForInput = TerminalIdentifier;
188
189 tokio::spawn(async move {
190 while let Some(Data) = InputReceiver.recv().await {
191 if let Err(Error) = PTYWriter.write_all(Data.as_bytes()) {
192 dev_log!(
193 "terminal",
194 "error: [TerminalProvider] PTY write failed for ID {}: {}",
195 TermIDForInput,
196 Error
197 );
198
199 break;
200 }
201 }
202 });
203
204 let mut PTYReader = PtyPair.master.try_clone_reader().map_err(|Error| {
205 CommonError::FileSystemIO {
206 Path:"pty master".into(),
207
208 Description:format!("Failed to clone PTY reader: {}", Error),
209 }
210 })?;
211
212 let PTYMasterHandle:crate::ApplicationState::DTO::TerminalStateDTO::PtyMasterHandle =
216 Arc::new(std::sync::Mutex::new(PtyPair.master));
217
218 TerminalState.PTYMaster = Some(PTYMasterHandle);
219
220 let IPCProvider:Arc<dyn IPCProvider> = self.Require();
221
222 let TermIDForOutput = TerminalIdentifier;
223
224 let AppHandleForOutput = self.ApplicationHandle.clone();
225
226 tokio::spawn(async move {
227 let mut Buffer = [0u8; 8192];
228
229 loop {
230 match PTYReader.read(&mut Buffer) {
231 Ok(count) if count > 0 => {
232 AppendTerminalOutput(TermIDForOutput, &Buffer[..count]);
239
240 let DataString = String::from_utf8_lossy(&Buffer[..count]).to_string();
241
242 let Payload = json!([TermIDForOutput, DataString.clone()]);
255 if let Err(Error) = IPCProvider
256 .SendNotificationToSideCar(
257 "cocoon-main".into(),
258 "$acceptTerminalProcessData".into(),
259 Payload,
260 )
261 .await
262 {
263 dev_log!(
264 "terminal",
265 "warn: [TerminalProvider] Failed to send process data for ID {}: {}",
266 TermIDForOutput,
267 Error
268 );
269 }
270
271 if let Err(Error) = AppHandleForOutput.emit(
272 SkyEvent::TerminalData.AsStr(),
273 json!({
274 "id": TermIDForOutput,
275 "data": DataString,
276 }),
277 ) {
278 dev_log!(
279 "terminal",
280 "warn: [TerminalProvider] sky://terminal/data emit failed for ID {}: {}",
281 TermIDForOutput,
282 Error
283 );
284 }
285 },
286
287 _ => break,
289 }
290 }
291 });
292
293 let TermIDForExit = TerminalIdentifier;
294
295 let PidForExit = ChildProcess.process_id();
304
305 let EnvironmentClone = self.clone();
306
307 tokio::spawn(async move {
308 let ExitStatus = ChildProcess.wait();
309
310 let StatusSummary = match &ExitStatus {
315 Ok(Code) => format!("exited {:?}", Code),
316 Err(Error) => format!("wait failed: {}", Error),
317 };
318
319 dev_log!(
320 "terminal",
321 "[TerminalProvider] Process for terminal ID {} pid={:?} {}",
322 TermIDForExit,
323 PidForExit,
324 StatusSummary
325 );
326
327 let IPCProvider:Arc<dyn IPCProvider> = EnvironmentClone.Require();
328
329 if let Err(Error) = IPCProvider
330 .SendNotificationToSideCar(
331 "cocoon-main".into(),
332 "$acceptTerminalProcessExit".into(),
333 json!([TermIDForExit]),
334 )
335 .await
336 {
337 dev_log!(
338 "terminal",
339 "warn: [TerminalProvider] Failed to send process exit notification for ID {}: {}",
340 TermIDForExit,
341 Error
342 );
343 }
344
345 if let Ok(mut Guard) = EnvironmentClone.ApplicationState.Feature.Terminals.ActiveTerminals.lock() {
347 Guard.remove(&TermIDForExit);
348 }
349 RemoveTerminalOutputBuffer(TermIDForExit);
352
353 if let Err(Error) = LogSkyEmit(
358 &EnvironmentClone.ApplicationHandle,
359 SkyEvent::TerminalExit.AsStr(),
360 json!({ "id": TermIDForExit }),
361 ) {
362 dev_log!(
363 "terminal",
364 "warn: [TerminalProvider] sky://terminal/exit emit failed for ID {}: {}",
365 TermIDForExit,
366 Error
367 );
368 }
369 });
370
371 self.ApplicationState
372 .Feature
373 .Terminals
374 .ActiveTerminals
375 .lock()
376 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
377 .insert(TerminalIdentifier, Arc::new(std::sync::Mutex::new(TerminalState.clone())));
378
379 let CreateAppHandle = self.ApplicationHandle.clone();
407
408 let CreateTermId = TerminalIdentifier;
409
410 let CreateName = Name.clone();
411
412 let CreatePid = TerminalState.OSProcessIdentifier;
413
414 tokio::spawn(async move {
415 tokio::time::sleep(std::time::Duration::from_millis(120)).await;
416 let CreatePayload = json!({
417 "id": CreateTermId,
418 "name": CreateName,
419 "pid": CreatePid,
420 });
421 if let Err(Error) = LogSkyEmit(&CreateAppHandle, SkyEvent::TerminalCreate.AsStr(), CreatePayload) {
427 dev_log!(
428 "terminal",
429 "warn: [TerminalProvider] sky://terminal/create emit failed for ID {}: {}",
430 CreateTermId,
431 Error
432 );
433 }
434 });
435
436 dev_log!(
437 "terminal",
438 "[TerminalProvider] localPty:spawn OK id={} pid={:?}",
439 TerminalIdentifier,
440 TerminalState.OSProcessIdentifier
441 );
442
443 Ok(json!({ "id": TerminalIdentifier, "name": Name, "pid": TerminalState.OSProcessIdentifier }))
444 }
445
446 async fn SendTextToTerminal(&self, TerminalId:u64, Text:String) -> Result<(), CommonError> {
447 dev_log!("terminal", "[TerminalProvider] Sending text to terminal ID: {}", TerminalId);
448
449 let SenderOption = {
450 let TerminalsGuard = self
451 .ApplicationState
452 .Feature
453 .Terminals
454 .ActiveTerminals
455 .lock()
456 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
457
458 TerminalsGuard
459 .get(&TerminalId)
460 .and_then(|TerminalArc| TerminalArc.lock().ok())
461 .and_then(|TerminalStateGuard| TerminalStateGuard.PTYInputTransmitter.clone())
462 };
463
464 if let Some(Sender) = SenderOption {
465 Sender
466 .send(Text)
467 .await
468 .map_err(|Error| CommonError::IPCError { Description:Error.to_string() })
469 } else {
470 Err(CommonError::IPCError {
471 Description:format!("Terminal with ID {} not found or has no input channel.", TerminalId),
472 })
473 }
474 }
475
476 async fn DisposeTerminal(&self, TerminalId:u64) -> Result<(), CommonError> {
477 dev_log!("terminal", "[TerminalProvider] Disposing terminal ID: {}", TerminalId);
478
479 let TerminalArc = self
480 .ApplicationState
481 .Feature
482 .Terminals
483 .ActiveTerminals
484 .lock()
485 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
486 .remove(&TerminalId);
487
488 if let Some(TerminalArc) = TerminalArc {
489 drop(TerminalArc);
492 }
493
494 Ok(())
495 }
496
497 async fn ShowTerminal(&self, TerminalId:u64, PreserveFocus:bool) -> Result<(), CommonError> {
498 dev_log!("terminal", "[TerminalProvider] Showing terminal ID: {}", TerminalId);
499
500 self.ApplicationHandle
501 .emit(
502 SkyEvent::TerminalShow.AsStr(),
503 json!({ "id": TerminalId, "preserveFocus": PreserveFocus }),
504 )
505 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })
506 }
507
508 async fn HideTerminal(&self, TerminalId:u64) -> Result<(), CommonError> {
509 dev_log!("terminal", "[TerminalProvider] Hiding terminal ID: {}", TerminalId);
510
511 LogSkyEmit(
514 &self.ApplicationHandle,
515 SkyEvent::TerminalHide.AsStr(),
516 json!({ "id": TerminalId }),
517 )
518 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })
519 }
520
521 async fn GetTerminalProcessId(&self, TerminalId:u64) -> Result<Option<u32>, CommonError> {
522 let TerminalsGuard = self
523 .ApplicationState
524 .Feature
525 .Terminals
526 .ActiveTerminals
527 .lock()
528 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
529
530 Ok(TerminalsGuard
531 .get(&TerminalId)
532 .and_then(|t| t.lock().ok().and_then(|g| g.OSProcessIdentifier)))
533 }
534
535 async fn ResizeTerminal(&self, TerminalId:u64, Columns:u16, Rows:u16) -> Result<(), CommonError> {
536 if Columns == 0 || Rows == 0 {
537 return Err(CommonError::InvalidArgument {
538 ArgumentName:"Columns/Rows".to_string(),
539 Reason:format!("Columns and Rows must be ≥ 1 (got {}×{})", Columns, Rows),
540 });
541 }
542
543 let MasterOption = {
546 let TerminalsGuard = self
547 .ApplicationState
548 .Feature
549 .Terminals
550 .ActiveTerminals
551 .lock()
552 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
553
554 TerminalsGuard
555 .get(&TerminalId)
556 .and_then(|TerminalArc| TerminalArc.lock().ok())
557 .and_then(|TerminalStateGuard| TerminalStateGuard.PTYMaster.clone())
558 };
559
560 let Master = MasterOption.ok_or_else(|| {
561 CommonError::IPCError {
562 Description:format!("Terminal with ID {} not found or has no PTY master handle.", TerminalId),
563 }
564 })?;
565
566 let Size = PtySize { rows:Rows, cols:Columns, pixel_width:0, pixel_height:0 };
567
568 tokio::task::spawn_blocking(move || {
574 let Guard = Master.lock().map_err(|_| "PTY master mutex poisoned".to_string())?;
575 Guard.resize(Size).map_err(|Error| Error.to_string())
576 })
577 .await
578 .map_err(|Error| CommonError::IPCError { Description:format!("resize join error: {}", Error) })?
579 .map_err(|Error| CommonError::IPCError { Description:format!("PTY resize failed: {}", Error) })?;
580
581 dev_log!(
582 "terminal",
583 "[TerminalProvider] Resized terminal ID {} to {}×{}",
584 TerminalId,
585 Columns,
586 Rows
587 );
588
589 Ok(())
590 }
591}