1use std::{env, io::Write, sync::Arc};
102
103use CommonLibrary::{
104 Environment::Requires::Requires,
105 Error::CommonError::CommonError,
106 IPC::{IPCProvider::IPCProvider, SkyEvent::SkyEvent},
107 Terminal::TerminalProvider::TerminalProvider,
108};
109use async_trait::async_trait;
110use portable_pty::{CommandBuilder, MasterPty, NativePtySystem, PtySize, PtySystem};
111use serde_json::{Value, json};
112use tauri::Emitter;
113use tokio::sync::mpsc as TokioMPSC;
114
115use super::{MountainEnvironment::MountainEnvironment, Utility};
116use crate::{ApplicationState::DTO::TerminalStateDTO::TerminalStateDTO, IPC::SkyEmit::LogSkyEmit, dev_log};
117
118const MAX_BUFFERED_BYTES:usize = 64 * 1024;
132
133static TERMINAL_OUTPUT_BUFFER:std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<u64, Vec<u8>>>> =
134 std::sync::OnceLock::new();
135
136fn TerminalOutputBuffer() -> &'static std::sync::Mutex<std::collections::HashMap<u64, Vec<u8>>> {
137 TERMINAL_OUTPUT_BUFFER.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
138}
139
140pub fn AppendTerminalOutput(TerminalId:u64, Bytes:&[u8]) {
141 if let Ok(mut Map) = TerminalOutputBuffer().lock() {
142 let Entry = Map.entry(TerminalId).or_insert_with(Vec::new);
143 Entry.extend_from_slice(Bytes);
144 if Entry.len() > MAX_BUFFERED_BYTES {
147 let DropCount = Entry.len() - MAX_BUFFERED_BYTES;
148 Entry.drain(..DropCount);
149 }
150 }
151}
152
153pub fn DrainTerminalOutputBuffer() -> Vec<(u64, Vec<u8>)> {
154 if let Ok(Map) = TerminalOutputBuffer().lock() {
155 Map.iter().map(|(K, V)| (*K, V.clone())).collect()
156 } else {
157 Vec::new()
158 }
159}
160
161pub fn RemoveTerminalOutputBuffer(TerminalId:u64) {
162 if let Ok(mut Map) = TerminalOutputBuffer().lock() {
163 Map.remove(&TerminalId);
164 }
165}
166
167#[async_trait]
168impl TerminalProvider for MountainEnvironment {
169 async fn CreateTerminal(&self, OptionsValue:Value) -> Result<Value, CommonError> {
171 let TerminalIdentifier = self.ApplicationState.GetNextTerminalIdentifier();
172
173 let DefaultShell = if cfg!(windows) {
174 "powershell.exe".to_string()
175 } else {
176 env::var("SHELL").unwrap_or_else(|_| "sh".to_string())
177 };
178
179 let Name = OptionsValue
180 .get("name")
181 .and_then(Value::as_str)
182 .unwrap_or("terminal")
183 .to_string();
184
185 dev_log!(
186 "terminal",
187 "[TerminalProvider] Creating terminal ID: {}, Name: '{}'",
188 TerminalIdentifier,
189 Name
190 );
191
192 let mut TerminalState = TerminalStateDTO::Create(TerminalIdentifier, Name.clone(), &OptionsValue, DefaultShell)
193 .map_err(|e| {
194 CommonError::ConfigurationLoad { Description:format!("Failed to create terminal state: {}", e) }
195 })?;
196
197 let PtySystem = NativePtySystem::default();
198
199 let PtyPair = PtySystem
200 .openpty(PtySize::default())
201 .map_err(|Error| CommonError::IPCError { Description:format!("Failed to open PTY: {}", Error) })?;
202
203 let mut Command = CommandBuilder::new(&TerminalState.ShellPath);
204
205 Command.args(&TerminalState.ShellArguments);
206
207 if let Some(CWD) = &TerminalState.CurrentWorkingDirectory {
208 Command.cwd(CWD);
209 }
210
211 let mut ChildProcess = PtyPair.slave.spawn_command(Command).map_err(|Error| {
212 CommonError::IPCError { Description:format!("Failed to spawn shell process: {}", Error) }
213 })?;
214
215 TerminalState.OSProcessIdentifier = ChildProcess.process_id();
216
217 let mut PTYWriter = PtyPair.master.take_writer().map_err(|Error| {
218 CommonError::FileSystemIO {
219 Path:"pty master".into(),
220
221 Description:format!("Failed to take PTY writer: {}", Error),
222 }
223 })?;
224
225 let (InputTransmitter, mut InputReceiver) = TokioMPSC::channel::<String>(32);
226
227 TerminalState.PTYInputTransmitter = Some(InputTransmitter);
228
229 let TermIDForInput = TerminalIdentifier;
230
231 tokio::spawn(async move {
232 while let Some(Data) = InputReceiver.recv().await {
233 if let Err(Error) = PTYWriter.write_all(Data.as_bytes()) {
234 dev_log!(
235 "terminal",
236 "error: [TerminalProvider] PTY write failed for ID {}: {}",
237 TermIDForInput,
238 Error
239 );
240
241 break;
242 }
243 }
244 });
245
246 let mut PTYReader = PtyPair.master.try_clone_reader().map_err(|Error| {
247 CommonError::FileSystemIO {
248 Path:"pty master".into(),
249
250 Description:format!("Failed to clone PTY reader: {}", Error),
251 }
252 })?;
253
254 let PTYMasterHandle:crate::ApplicationState::DTO::TerminalStateDTO::PtyMasterHandle =
258 Arc::new(std::sync::Mutex::new(PtyPair.master));
259 TerminalState.PTYMaster = Some(PTYMasterHandle);
260
261 let IPCProvider:Arc<dyn IPCProvider> = self.Require();
262
263 let TermIDForOutput = TerminalIdentifier;
264 let AppHandleForOutput = self.ApplicationHandle.clone();
265
266 tokio::spawn(async move {
267 let mut Buffer = [0u8; 8192];
268
269 loop {
270 match PTYReader.read(&mut Buffer) {
271 Ok(count) if count > 0 => {
272 AppendTerminalOutput(TermIDForOutput, &Buffer[..count]);
279
280 let DataString = String::from_utf8_lossy(&Buffer[..count]).to_string();
281
282 let Payload = json!([TermIDForOutput, DataString.clone()]);
295 if let Err(Error) = IPCProvider
296 .SendNotificationToSideCar(
297 "cocoon-main".into(),
298 "$acceptTerminalProcessData".into(),
299 Payload,
300 )
301 .await
302 {
303 dev_log!(
304 "terminal",
305 "warn: [TerminalProvider] Failed to send process data for ID {}: {}",
306 TermIDForOutput,
307 Error
308 );
309 }
310
311 if let Err(Error) = AppHandleForOutput.emit(
312 SkyEvent::TerminalData.AsStr(),
313 json!({
314 "id": TermIDForOutput,
315 "data": DataString,
316 }),
317 ) {
318 dev_log!(
319 "terminal",
320 "warn: [TerminalProvider] sky://terminal/data emit failed for ID {}: {}",
321 TermIDForOutput,
322 Error
323 );
324 }
325 },
326
327 _ => break,
329 }
330 }
331 });
332
333 let TermIDForExit = TerminalIdentifier;
334
335 let PidForExit = ChildProcess.process_id();
344
345 let EnvironmentClone = self.clone();
346
347 tokio::spawn(async move {
348 let ExitStatus = ChildProcess.wait();
349
350 let StatusSummary = match &ExitStatus {
355 Ok(Code) => format!("exited {:?}", Code),
356 Err(Error) => format!("wait failed: {}", Error),
357 };
358
359 dev_log!(
360 "terminal",
361 "[TerminalProvider] Process for terminal ID {} pid={:?} {}",
362 TermIDForExit,
363 PidForExit,
364 StatusSummary
365 );
366
367 let IPCProvider:Arc<dyn IPCProvider> = EnvironmentClone.Require();
368
369 if let Err(Error) = IPCProvider
370 .SendNotificationToSideCar(
371 "cocoon-main".into(),
372 "$acceptTerminalProcessExit".into(),
373 json!([TermIDForExit]),
374 )
375 .await
376 {
377 dev_log!(
378 "terminal",
379 "warn: [TerminalProvider] Failed to send process exit notification for ID {}: {}",
380 TermIDForExit,
381 Error
382 );
383 }
384
385 if let Ok(mut Guard) = EnvironmentClone.ApplicationState.Feature.Terminals.ActiveTerminals.lock() {
387 Guard.remove(&TermIDForExit);
388 }
389 RemoveTerminalOutputBuffer(TermIDForExit);
392
393 if let Err(Error) = LogSkyEmit(
398 &EnvironmentClone.ApplicationHandle,
399 SkyEvent::TerminalExit.AsStr(),
400 json!({ "id": TermIDForExit }),
401 ) {
402 dev_log!(
403 "terminal",
404 "warn: [TerminalProvider] sky://terminal/exit emit failed for ID {}: {}",
405 TermIDForExit,
406 Error
407 );
408 }
409 });
410
411 self.ApplicationState
412 .Feature
413 .Terminals
414 .ActiveTerminals
415 .lock()
416 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
417 .insert(TerminalIdentifier, Arc::new(std::sync::Mutex::new(TerminalState.clone())));
418
419 let CreateAppHandle = self.ApplicationHandle.clone();
447 let CreateTermId = TerminalIdentifier;
448 let CreateName = Name.clone();
449 let CreatePid = TerminalState.OSProcessIdentifier;
450 tokio::spawn(async move {
451 tokio::time::sleep(std::time::Duration::from_millis(120)).await;
452 let CreatePayload = json!({
453 "id": CreateTermId,
454 "name": CreateName,
455 "pid": CreatePid,
456 });
457 if let Err(Error) = LogSkyEmit(&CreateAppHandle, SkyEvent::TerminalCreate.AsStr(), CreatePayload) {
463 dev_log!(
464 "terminal",
465 "warn: [TerminalProvider] sky://terminal/create emit failed for ID {}: {}",
466 CreateTermId,
467 Error
468 );
469 }
470 });
471
472 dev_log!(
473 "terminal",
474 "[TerminalProvider] localPty:spawn OK id={} pid={:?}",
475 TerminalIdentifier,
476 TerminalState.OSProcessIdentifier
477 );
478
479 Ok(json!({ "id": TerminalIdentifier, "name": Name, "pid": TerminalState.OSProcessIdentifier }))
480 }
481
482 async fn SendTextToTerminal(&self, TerminalId:u64, Text:String) -> Result<(), CommonError> {
483 dev_log!("terminal", "[TerminalProvider] Sending text to terminal ID: {}", TerminalId);
484
485 let SenderOption = {
486 let TerminalsGuard = self
487 .ApplicationState
488 .Feature
489 .Terminals
490 .ActiveTerminals
491 .lock()
492 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
493
494 TerminalsGuard
495 .get(&TerminalId)
496 .and_then(|TerminalArc| TerminalArc.lock().ok())
497 .and_then(|TerminalStateGuard| TerminalStateGuard.PTYInputTransmitter.clone())
498 };
499
500 if let Some(Sender) = SenderOption {
501 Sender
502 .send(Text)
503 .await
504 .map_err(|Error| CommonError::IPCError { Description:Error.to_string() })
505 } else {
506 Err(CommonError::IPCError {
507 Description:format!("Terminal with ID {} not found or has no input channel.", TerminalId),
508 })
509 }
510 }
511
512 async fn DisposeTerminal(&self, TerminalId:u64) -> Result<(), CommonError> {
513 dev_log!("terminal", "[TerminalProvider] Disposing terminal ID: {}", TerminalId);
514
515 let TerminalArc = self
516 .ApplicationState
517 .Feature
518 .Terminals
519 .ActiveTerminals
520 .lock()
521 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
522 .remove(&TerminalId);
523
524 if let Some(TerminalArc) = TerminalArc {
525 drop(TerminalArc);
528 }
529
530 Ok(())
531 }
532
533 async fn ShowTerminal(&self, TerminalId:u64, PreserveFocus:bool) -> Result<(), CommonError> {
534 dev_log!("terminal", "[TerminalProvider] Showing terminal ID: {}", TerminalId);
535
536 self.ApplicationHandle
537 .emit(
538 SkyEvent::TerminalShow.AsStr(),
539 json!({ "id": TerminalId, "preserveFocus": PreserveFocus }),
540 )
541 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })
542 }
543
544 async fn HideTerminal(&self, TerminalId:u64) -> Result<(), CommonError> {
545 dev_log!("terminal", "[TerminalProvider] Hiding terminal ID: {}", TerminalId);
546
547 LogSkyEmit(
550 &self.ApplicationHandle,
551 SkyEvent::TerminalHide.AsStr(),
552 json!({ "id": TerminalId }),
553 )
554 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })
555 }
556
557 async fn GetTerminalProcessId(&self, TerminalId:u64) -> Result<Option<u32>, CommonError> {
558 let TerminalsGuard = self
559 .ApplicationState
560 .Feature
561 .Terminals
562 .ActiveTerminals
563 .lock()
564 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
565
566 Ok(TerminalsGuard
567 .get(&TerminalId)
568 .and_then(|t| t.lock().ok().and_then(|g| g.OSProcessIdentifier)))
569 }
570
571 async fn ResizeTerminal(&self, TerminalId:u64, Columns:u16, Rows:u16) -> Result<(), CommonError> {
572 if Columns == 0 || Rows == 0 {
573 return Err(CommonError::InvalidArgument {
574 ArgumentName:"Columns/Rows".to_string(),
575 Reason:format!("Columns and Rows must be ≥ 1 (got {}×{})", Columns, Rows),
576 });
577 }
578
579 let MasterOption = {
582 let TerminalsGuard = self
583 .ApplicationState
584 .Feature
585 .Terminals
586 .ActiveTerminals
587 .lock()
588 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
589 TerminalsGuard
590 .get(&TerminalId)
591 .and_then(|TerminalArc| TerminalArc.lock().ok())
592 .and_then(|TerminalStateGuard| TerminalStateGuard.PTYMaster.clone())
593 };
594
595 let Master = MasterOption.ok_or_else(|| {
596 CommonError::IPCError {
597 Description:format!("Terminal with ID {} not found or has no PTY master handle.", TerminalId),
598 }
599 })?;
600
601 let Size = PtySize { rows:Rows, cols:Columns, pixel_width:0, pixel_height:0 };
602
603 tokio::task::spawn_blocking(move || {
609 let Guard = Master.lock().map_err(|_| "PTY master mutex poisoned".to_string())?;
610 Guard.resize(Size).map_err(|Error| Error.to_string())
611 })
612 .await
613 .map_err(|Error| CommonError::IPCError { Description:format!("resize join error: {}", Error) })?
614 .map_err(|Error| CommonError::IPCError { Description:format!("PTY resize failed: {}", Error) })?;
615
616 dev_log!(
617 "terminal",
618 "[TerminalProvider] Resized terminal ID {} to {}×{}",
619 TerminalId,
620 Columns,
621 Rows
622 );
623
624 Ok(())
625 }
626}