DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_Mountain/Environment/
TerminalProvider.rs1use 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 let mut MergedEnv:std::collections::HashMap<String, String> = std::env::vars().collect();
170
171 super::TerminalEnvCollection::ApplyToEnv(&mut MergedEnv);
176
177 if let Some(Injection) =
181 super::Terminal::ShellIntegration::Compute(&self.ApplicationHandle, &TerminalState.ShellPath)
182 {
183 for (Key, Val) in Injection.EnvVars {
184 MergedEnv.insert(Key, Val);
185 }
186 let mut AllArgs = Injection.PrependArgs;
189 AllArgs.extend(TerminalState.ShellArguments.iter().cloned());
190 AllArgs.extend(Injection.AppendArgs);
191 Command.args(&AllArgs);
192 } else {
193 Command.args(&TerminalState.ShellArguments);
194 }
195
196 for (Key, Val) in &MergedEnv {
199 Command.env(Key, Val);
200 }
201
202 if let Some(CWD) = &TerminalState.CurrentWorkingDirectory {
203 Command.cwd(CWD);
204 }
205
206 let mut ChildProcess = PtyPair.slave.spawn_command(Command).map_err(|Error| {
207 CommonError::IPCError { Description:format!("Failed to spawn shell process: {}", Error) }
208 })?;
209
210 TerminalState.OSProcessIdentifier = ChildProcess.process_id();
211
212 let mut PTYWriter = PtyPair.master.take_writer().map_err(|Error| {
213 CommonError::FileSystemIO {
214 Path:"pty master".into(),
215
216 Description:format!("Failed to take PTY writer: {}", Error),
217 }
218 })?;
219
220 let (InputTransmitter, mut InputReceiver) = TokioMPSC::channel::<String>(32);
221
222 TerminalState.PTYInputTransmitter = Some(InputTransmitter);
223
224 let TermIDForInput = TerminalIdentifier;
225
226 tokio::spawn(async move {
227 while let Some(Data) = InputReceiver.recv().await {
228 if let Err(Error) = PTYWriter.write_all(Data.as_bytes()) {
229 dev_log!(
230 "terminal",
231 "error: [TerminalProvider] PTY write failed for ID {}: {}",
232 TermIDForInput,
233 Error
234 );
235
236 break;
237 }
238 }
239 });
240
241 let mut PTYReader = PtyPair.master.try_clone_reader().map_err(|Error| {
242 CommonError::FileSystemIO {
243 Path:"pty master".into(),
244
245 Description:format!("Failed to clone PTY reader: {}", Error),
246 }
247 })?;
248
249 let PTYMasterHandle:crate::ApplicationState::DTO::TerminalStateDTO::PtyMasterHandle =
253 Arc::new(std::sync::Mutex::new(PtyPair.master));
254
255 TerminalState.PTYMaster = Some(PTYMasterHandle);
256
257 let IPCProvider:Arc<dyn IPCProvider> = self.Require();
258
259 let TermIDForOutput = TerminalIdentifier;
260
261 let AppHandleForOutput = self.ApplicationHandle.clone();
262
263 tokio::spawn(async move {
264 let mut Buffer = [0u8; 8192];
265
266 loop {
267 match PTYReader.read(&mut Buffer) {
268 Ok(count) if count > 0 => {
269 AppendTerminalOutput(TermIDForOutput, &Buffer[..count]);
276
277 let DataString = String::from_utf8_lossy(&Buffer[..count]).to_string();
278
279 let Payload = json!([TermIDForOutput, DataString.clone()]);
292 if let Err(Error) = IPCProvider
293 .SendNotificationToSideCar(
294 "cocoon-main".into(),
295 "$acceptTerminalProcessData".into(),
296 Payload,
297 )
298 .await
299 {
300 dev_log!(
301 "terminal",
302 "warn: [TerminalProvider] Failed to send process data for ID {}: {}",
303 TermIDForOutput,
304 Error
305 );
306 }
307
308 if let Err(Error) = AppHandleForOutput.emit(
309 SkyEvent::TerminalData.AsStr(),
310 json!({
311 "id": TermIDForOutput,
312 "data": DataString,
313 }),
314 ) {
315 dev_log!(
316 "terminal",
317 "warn: [TerminalProvider] sky://terminal/data emit failed for ID {}: {}",
318 TermIDForOutput,
319 Error
320 );
321 }
322 },
323
324 _ => break,
326 }
327 }
328 });
329
330 let TermIDForExit = TerminalIdentifier;
331
332 let PidForExit = ChildProcess.process_id();
341
342 let EnvironmentClone = self.clone();
343
344 tokio::spawn(async move {
345 let ExitStatus = ChildProcess.wait();
346
347 let StatusSummary = match &ExitStatus {
352 Ok(Code) => format!("exited {:?}", Code),
353 Err(Error) => format!("wait failed: {}", Error),
354 };
355
356 dev_log!(
357 "terminal",
358 "[TerminalProvider] Process for terminal ID {} pid={:?} {}",
359 TermIDForExit,
360 PidForExit,
361 StatusSummary
362 );
363
364 let IPCProvider:Arc<dyn IPCProvider> = EnvironmentClone.Require();
365
366 if let Err(Error) = IPCProvider
367 .SendNotificationToSideCar(
368 "cocoon-main".into(),
369 "$acceptTerminalProcessExit".into(),
370 json!([TermIDForExit]),
371 )
372 .await
373 {
374 dev_log!(
375 "terminal",
376 "warn: [TerminalProvider] Failed to send process exit notification for ID {}: {}",
377 TermIDForExit,
378 Error
379 );
380 }
381
382 if let Ok(mut Guard) = EnvironmentClone.ApplicationState.Feature.Terminals.ActiveTerminals.lock() {
384 Guard.remove(&TermIDForExit);
385 }
386 RemoveTerminalOutputBuffer(TermIDForExit);
389
390 if let Err(Error) = LogSkyEmit(
395 &EnvironmentClone.ApplicationHandle,
396 SkyEvent::TerminalExit.AsStr(),
397 json!({ "id": TermIDForExit }),
398 ) {
399 dev_log!(
400 "terminal",
401 "warn: [TerminalProvider] sky://terminal/exit emit failed for ID {}: {}",
402 TermIDForExit,
403 Error
404 );
405 }
406
407 let _ = crate::Vine::Client::SendNotification::Fn(
411 "cocoon-main".to_string(),
412 "$acceptTerminalClosed".to_string(),
413 serde_json::json!({ "id": TermIDForExit }),
414 )
415 .await;
416 });
417
418 self.ApplicationState
419 .Feature
420 .Terminals
421 .ActiveTerminals
422 .lock()
423 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
424 .insert(TerminalIdentifier, Arc::new(std::sync::Mutex::new(TerminalState.clone())));
425
426 let CreateAppHandle = self.ApplicationHandle.clone();
454
455 let CreateTermId = TerminalIdentifier;
456
457 let CreateName = Name.clone();
458
459 let CreatePid = TerminalState.OSProcessIdentifier;
460
461 tokio::spawn(async move {
462 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
467 let CreatePayload = json!({
468 "id": CreateTermId,
469 "name": CreateName.clone(),
470 "pid": CreatePid,
471 });
472 if let Err(Error) = LogSkyEmit(&CreateAppHandle, SkyEvent::TerminalCreate.AsStr(), CreatePayload.clone()) {
478 dev_log!(
479 "terminal",
480 "warn: [TerminalProvider] sky://terminal/create emit failed for ID {}: {}",
481 CreateTermId,
482 Error
483 );
484 }
485
486 if let Err(E) = crate::Vine::Client::SendNotification::Fn(
491 "cocoon-main".to_string(),
492 "$acceptTerminalOpened".to_string(),
493 serde_json::json!({ "id": CreateTermId, "name": CreateName, "pid": CreatePid }),
494 )
495 .await
496 {
497 dev_log!(
498 "terminal",
499 "warn: [TerminalProvider] $acceptTerminalOpened notify failed ID={}: {}",
500 CreateTermId,
501 E
502 );
503 }
504 });
505
506 dev_log!(
507 "terminal",
508 "[TerminalProvider] localPty:spawn OK id={} pid={:?}",
509 TerminalIdentifier,
510 TerminalState.OSProcessIdentifier
511 );
512
513 Ok(json!({ "id": TerminalIdentifier, "name": Name, "pid": TerminalState.OSProcessIdentifier }))
514 }
515
516 async fn SendTextToTerminal(&self, TerminalId:u64, Text:String) -> Result<(), CommonError> {
517 dev_log!("terminal", "[TerminalProvider] Sending text to terminal ID: {}", TerminalId);
518
519 let SenderOption = {
520 let TerminalsGuard = self
521 .ApplicationState
522 .Feature
523 .Terminals
524 .ActiveTerminals
525 .lock()
526 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
527
528 TerminalsGuard
529 .get(&TerminalId)
530 .and_then(|TerminalArc| TerminalArc.lock().ok())
531 .and_then(|TerminalStateGuard| TerminalStateGuard.PTYInputTransmitter.clone())
532 };
533
534 if let Some(Sender) = SenderOption {
535 Sender
536 .send(Text)
537 .await
538 .map_err(|Error| CommonError::IPCError { Description:Error.to_string() })
539 } else {
540 Err(CommonError::IPCError {
541 Description:format!("Terminal with ID {} not found or has no input channel.", TerminalId),
542 })
543 }
544 }
545
546 async fn DisposeTerminal(&self, TerminalId:u64) -> Result<(), CommonError> {
547 dev_log!("terminal", "[TerminalProvider] Disposing terminal ID: {}", TerminalId);
548
549 let TerminalArc = self
550 .ApplicationState
551 .Feature
552 .Terminals
553 .ActiveTerminals
554 .lock()
555 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
556 .remove(&TerminalId);
557
558 if let Some(TerminalArc) = TerminalArc {
559 drop(TerminalArc);
562 }
563
564 Ok(())
565 }
566
567 async fn ShowTerminal(&self, TerminalId:u64, PreserveFocus:bool) -> Result<(), CommonError> {
568 dev_log!("terminal", "[TerminalProvider] Showing terminal ID: {}", TerminalId);
569
570 self.ApplicationHandle
571 .emit(
572 SkyEvent::TerminalShow.AsStr(),
573 json!({ "id": TerminalId, "preserveFocus": PreserveFocus }),
574 )
575 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })
576 }
577
578 async fn HideTerminal(&self, TerminalId:u64) -> Result<(), CommonError> {
579 dev_log!("terminal", "[TerminalProvider] Hiding terminal ID: {}", TerminalId);
580
581 LogSkyEmit(
584 &self.ApplicationHandle,
585 SkyEvent::TerminalHide.AsStr(),
586 json!({ "id": TerminalId }),
587 )
588 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })
589 }
590
591 async fn GetTerminalProcessId(&self, TerminalId:u64) -> Result<Option<u32>, CommonError> {
592 let TerminalsGuard = self
593 .ApplicationState
594 .Feature
595 .Terminals
596 .ActiveTerminals
597 .lock()
598 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
599
600 Ok(TerminalsGuard
601 .get(&TerminalId)
602 .and_then(|t| t.lock().ok().and_then(|g| g.OSProcessIdentifier)))
603 }
604
605 async fn ResizeTerminal(&self, TerminalId:u64, Columns:u16, Rows:u16) -> Result<(), CommonError> {
606 if Columns == 0 || Rows == 0 {
607 return Err(CommonError::InvalidArgument {
608 ArgumentName:"Columns/Rows".to_string(),
609 Reason:format!("Columns and Rows must be ≥ 1 (got {}×{})", Columns, Rows),
610 });
611 }
612
613 let MasterOption = {
616 let TerminalsGuard = self
617 .ApplicationState
618 .Feature
619 .Terminals
620 .ActiveTerminals
621 .lock()
622 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
623
624 TerminalsGuard
625 .get(&TerminalId)
626 .and_then(|TerminalArc| TerminalArc.lock().ok())
627 .and_then(|TerminalStateGuard| TerminalStateGuard.PTYMaster.clone())
628 };
629
630 let Master = MasterOption.ok_or_else(|| {
631 CommonError::IPCError {
632 Description:format!("Terminal with ID {} not found or has no PTY master handle.", TerminalId),
633 }
634 })?;
635
636 let Size = PtySize { rows:Rows, cols:Columns, pixel_width:0, pixel_height:0 };
637
638 tokio::task::spawn_blocking(move || {
644 let Guard = Master.lock().map_err(|_| "PTY master mutex poisoned".to_string())?;
645 Guard.resize(Size).map_err(|Error| Error.to_string())
646 })
647 .await
648 .map_err(|Error| CommonError::IPCError { Description:format!("resize join error: {}", Error) })?
649 .map_err(|Error| CommonError::IPCError { Description:format!("PTY resize failed: {}", Error) })?;
650
651 dev_log!(
652 "terminal",
653 "[TerminalProvider] Resized terminal ID {} to {}×{}",
654 TerminalId,
655 Columns,
656 Rows
657 );
658
659 Ok(())
660 }
661}