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(crate) 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 Fn() -> 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(crate) 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 let _ = crate::Vine::Client::SendNotification::Fn(
374 "cocoon-main".to_string(),
375 "$acceptTerminalClosed".to_string(),
376 serde_json::json!({ "id": TermIDForExit }),
377 )
378 .await;
379 });
380
381 self.ApplicationState
382 .Feature
383 .Terminals
384 .ActiveTerminals
385 .lock()
386 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
387 .insert(TerminalIdentifier, Arc::new(std::sync::Mutex::new(TerminalState.clone())));
388
389 let CreateAppHandle = self.ApplicationHandle.clone();
417
418 let CreateTermId = TerminalIdentifier;
419
420 let CreateName = Name.clone();
421
422 let CreatePid = TerminalState.OSProcessIdentifier;
423
424 tokio::spawn(async move {
425 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
430 let CreatePayload = json!({
431 "id": CreateTermId,
432 "name": CreateName.clone(),
433 "pid": CreatePid,
434 });
435 if let Err(Error) = LogSkyEmit(&CreateAppHandle, SkyEvent::TerminalCreate.AsStr(), CreatePayload.clone()) {
441 dev_log!(
442 "terminal",
443 "warn: [TerminalProvider] sky://terminal/create emit failed for ID {}: {}",
444 CreateTermId,
445 Error
446 );
447 }
448
449 if let Err(E) = crate::Vine::Client::SendNotification::Fn(
454 "cocoon-main".to_string(),
455 "$acceptTerminalOpened".to_string(),
456 serde_json::json!({ "id": CreateTermId, "name": CreateName, "pid": CreatePid }),
457 )
458 .await
459 {
460 dev_log!(
461 "terminal",
462 "warn: [TerminalProvider] $acceptTerminalOpened notify failed ID={}: {}",
463 CreateTermId,
464 E
465 );
466 }
467 });
468
469 dev_log!(
470 "terminal",
471 "[TerminalProvider] localPty:spawn OK id={} pid={:?}",
472 TerminalIdentifier,
473 TerminalState.OSProcessIdentifier
474 );
475
476 Ok(json!({ "id": TerminalIdentifier, "name": Name, "pid": TerminalState.OSProcessIdentifier }))
477 }
478
479 async fn SendTextToTerminal(&self, TerminalId:u64, Text:String) -> Result<(), CommonError> {
480 dev_log!("terminal", "[TerminalProvider] Sending text to terminal ID: {}", TerminalId);
481
482 let SenderOption = {
483 let TerminalsGuard = self
484 .ApplicationState
485 .Feature
486 .Terminals
487 .ActiveTerminals
488 .lock()
489 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
490
491 TerminalsGuard
492 .get(&TerminalId)
493 .and_then(|TerminalArc| TerminalArc.lock().ok())
494 .and_then(|TerminalStateGuard| TerminalStateGuard.PTYInputTransmitter.clone())
495 };
496
497 if let Some(Sender) = SenderOption {
498 Sender
499 .send(Text)
500 .await
501 .map_err(|Error| CommonError::IPCError { Description:Error.to_string() })
502 } else {
503 Err(CommonError::IPCError {
504 Description:format!("Terminal with ID {} not found or has no input channel.", TerminalId),
505 })
506 }
507 }
508
509 async fn DisposeTerminal(&self, TerminalId:u64) -> Result<(), CommonError> {
510 dev_log!("terminal", "[TerminalProvider] Disposing terminal ID: {}", TerminalId);
511
512 let TerminalArc = self
513 .ApplicationState
514 .Feature
515 .Terminals
516 .ActiveTerminals
517 .lock()
518 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
519 .remove(&TerminalId);
520
521 if let Some(TerminalArc) = TerminalArc {
522 drop(TerminalArc);
525 }
526
527 Ok(())
528 }
529
530 async fn ShowTerminal(&self, TerminalId:u64, PreserveFocus:bool) -> Result<(), CommonError> {
531 dev_log!("terminal", "[TerminalProvider] Showing terminal ID: {}", TerminalId);
532
533 self.ApplicationHandle
534 .emit(
535 SkyEvent::TerminalShow.AsStr(),
536 json!({ "id": TerminalId, "preserveFocus": PreserveFocus }),
537 )
538 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })
539 }
540
541 async fn HideTerminal(&self, TerminalId:u64) -> Result<(), CommonError> {
542 dev_log!("terminal", "[TerminalProvider] Hiding terminal ID: {}", TerminalId);
543
544 LogSkyEmit(
547 &self.ApplicationHandle,
548 SkyEvent::TerminalHide.AsStr(),
549 json!({ "id": TerminalId }),
550 )
551 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })
552 }
553
554 async fn GetTerminalProcessId(&self, TerminalId:u64) -> Result<Option<u32>, CommonError> {
555 let TerminalsGuard = self
556 .ApplicationState
557 .Feature
558 .Terminals
559 .ActiveTerminals
560 .lock()
561 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
562
563 Ok(TerminalsGuard
564 .get(&TerminalId)
565 .and_then(|t| t.lock().ok().and_then(|g| g.OSProcessIdentifier)))
566 }
567
568 async fn ResizeTerminal(&self, TerminalId:u64, Columns:u16, Rows:u16) -> Result<(), CommonError> {
569 if Columns == 0 || Rows == 0 {
570 return Err(CommonError::InvalidArgument {
571 ArgumentName:"Columns/Rows".to_string(),
572 Reason:format!("Columns and Rows must be ≥ 1 (got {}×{})", Columns, Rows),
573 });
574 }
575
576 let MasterOption = {
579 let TerminalsGuard = self
580 .ApplicationState
581 .Feature
582 .Terminals
583 .ActiveTerminals
584 .lock()
585 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
586
587 TerminalsGuard
588 .get(&TerminalId)
589 .and_then(|TerminalArc| TerminalArc.lock().ok())
590 .and_then(|TerminalStateGuard| TerminalStateGuard.PTYMaster.clone())
591 };
592
593 let Master = MasterOption.ok_or_else(|| {
594 CommonError::IPCError {
595 Description:format!("Terminal with ID {} not found or has no PTY master handle.", TerminalId),
596 }
597 })?;
598
599 let Size = PtySize { rows:Rows, cols:Columns, pixel_width:0, pixel_height:0 };
600
601 tokio::task::spawn_blocking(move || {
607 let Guard = Master.lock().map_err(|_| "PTY master mutex poisoned".to_string())?;
608 Guard.resize(Size).map_err(|Error| Error.to_string())
609 })
610 .await
611 .map_err(|Error| CommonError::IPCError { Description:format!("resize join error: {}", Error) })?
612 .map_err(|Error| CommonError::IPCError { Description:format!("PTY resize failed: {}", Error) })?;
613
614 dev_log!(
615 "terminal",
616 "[TerminalProvider] Resized terminal ID {} to {}×{}",
617 TerminalId,
618 Columns,
619 Rows
620 );
621
622 Ok(())
623 }
624}