Mountain/Vine/Server/
MountainVinegRPCService.rs

1//! # MountainVinegRPCService
2//!
3//! Defines the gRPC service implementation for Mountain. This struct handles
4//! incoming RPC calls from the `Cocoon` sidecar, dispatches them to the
5//! application's core logic via the `Track` module, and returns the results.
6//!
7//! ## Service Methods
8//!
9//! - **process_cocoon_request**: Handles request-response calls from Cocoon
10//! - **send_cocoon_notification**: Handles fire-and-forget notifications from
11//!   Cocoon
12//! - **cancel_operation**: Cancels long-running operations requested by Cocoon
13//!
14//! ## Request Processing
15//!
16//! 1. Deserialize JSON parameters from request
17//! 2. Validate method name and parameters
18//! 3. Dispatch request to Track::DispatchLogic
19//! 4. Serialize response or error
20//! 5. Return gRPC response with proper status codes
21//!
22//! ## Error Handling
23//!
24//! All errors are converted to JSON-RPC compliant Error objects:
25//! - Parse errors: code -32700
26//! - Server errors: code -32000
27//! - Method not found: code -32601
28//! - Invalid params: code -32602
29//!
30//! ## Security
31//!
32//! - Parameter validation before processing
33//! - Message size limits enforced
34//! - Method name sanitization
35//! - Safe error messages (no sensitive data)
36
37use std::{collections::HashMap, sync::Arc};
38
39use log::{debug, error, info, trace, warn};
40use serde_json::Value;
41use tauri::AppHandle;
42use tokio::sync::RwLock;
43use tonic::{Request, Response, Status};
44
45use crate::{
46	RunTime::ApplicationRunTime::ApplicationRunTime,
47	Track,
48	Vine::Generated::{
49		CancelOperationRequest,
50		Empty,
51		GenericNotification,
52		GenericRequest,
53		GenericResponse,
54		RpcError as RPCError,
55		mountain_service_server::MountainService,
56	},
57};
58
59/// Configuration for MountainService
60mod ServiceConfig {
61	/// Maximum number of concurrent operations
62	pub const MAX_CONCURRENT_OPERATIONS:usize = 50;
63
64	/// Default timeout for operation cancellation
65	pub const CANCELLATION_TIMEOUT_MS:u64 = 5000;
66
67	/// Maximum method name length
68	pub const MAX_METHOD_NAME_LENGTH:usize = 128;
69}
70
71/// The concrete implementation of the `MountainService` gRPC service.
72///
73/// This service handles all incoming RPC calls from the Cocoon sidecar,
74/// validating requests, dispatching to appropriate handlers, and returning
75/// responses in the expected gRPC format.
76pub struct MountainVinegRPCService {
77	/// Tauri application handle for VS Code integration
78	ApplicationHandle:AppHandle,
79
80	/// Application runtime containing core dependencies
81	RunTime:Arc<ApplicationRunTime>,
82
83	/// Registry of active operations with their cancellation tokens
84	/// Maps request ID to cancellation token for operation cancellation
85	ActiveOperations:Arc<RwLock<HashMap<u64, tokio_util::sync::CancellationToken>>>,
86}
87
88impl MountainVinegRPCService {
89	/// Creates a new instance of the Mountain gRPC service.
90	///
91	/// # Parameters
92	/// - `ApplicationHandle`: Tauri app handle for framework integration
93	/// - `RunTime`: Application runtime with core dependencies
94	///
95	/// # Returns
96	/// New MountainVinegRPCService instance
97	pub fn Create(ApplicationHandle:AppHandle, RunTime:Arc<ApplicationRunTime>) -> Self {
98		info!("[MountainVinegRPCService] New instance created");
99
100		Self {
101			ApplicationHandle,
102			RunTime,
103			ActiveOperations:Arc::new(RwLock::new(HashMap::new())),
104		}
105	}
106
107	/// Registers an operation for potential cancellation
108	///
109	/// # Parameters
110	/// - `request_id`: The request identifier for the operation
111	///
112	/// # Returns
113	/// A cancellation token that can be used to cancel the operation
114	pub async fn RegisterOperation(&self, request_id:u64) -> tokio_util::sync::CancellationToken {
115		let token = tokio_util::sync::CancellationToken::new();
116		self.ActiveOperations.write().await.insert(request_id, token.clone());
117		debug!("[MountainVinegRPCService] Registered operation {} for cancellation", request_id);
118		token
119	}
120
121	/// Unregisters an operation after completion
122	///
123	/// # Parameters
124	/// - `request_id`: The request identifier to unregister
125	pub async fn UnregisterOperation(&self, request_id:u64) {
126		self.ActiveOperations.write().await.remove(&request_id);
127		debug!("[MountainVinegRPCService] Unregistered operation {}", request_id);
128	}
129
130	/// Validates a generic request before processing.
131	///
132	/// # Parameters
133	/// - `request`: The request to validate
134	///
135	/// # Returns
136	/// - `Ok(())`: Request is valid
137	/// - `Err(Status)`: Validation failed with appropriate gRPC status
138	fn ValidateRequest(&self, request:&GenericRequest) -> Result<(), Status> {
139		// Validate method name
140		if request.method.is_empty() {
141			return Err(Status::invalid_argument("Method name cannot be empty"));
142		}
143
144		if request.method.len() > ServiceConfig::MAX_METHOD_NAME_LENGTH {
145			return Err(Status::invalid_argument(format!(
146				"Method name exceeds maximum length of {} characters",
147				ServiceConfig::MAX_METHOD_NAME_LENGTH
148			)));
149		}
150
151		// Validate parameter size (rough estimate using JSON bytes)
152		if request.parameter.len() > 4 * 1024 * 1024 {
153			return Err(Status::resource_exhausted("Request parameter size exceeds limit"));
154		}
155
156		// Check for potentially malicious method names
157		if request.method.contains("../") || request.method.contains("::") {
158			return Err(Status::permission_denied("Invalid method name format"));
159		}
160
161		Ok(())
162	}
163
164	/// Creates a JSON-RPC compliant error response.
165	///
166	/// # Parameters
167	/// - `RequestIdentifier`: The request ID to echo back
168	/// - `code`: JSON-RPC error code
169	/// - `message`: Error message
170	/// - `data`: Optional error data (serialized)
171	///
172	/// # Returns
173	/// GenericResponse with error populated
174	fn CreateErrorResponse(RequestIdentifier:u64, code:i32, message:String, data:Option<Vec<u8>>) -> GenericResponse {
175		GenericResponse {
176			request_identifier:RequestIdentifier,
177			result:vec![],
178			error:Some(RPCError { code, message, data:data.unwrap_or_default() }),
179		}
180	}
181
182	/// Creates a successful JSON-RPC response.
183	///
184	/// # Parameters
185	/// - `RequestIdentifier`: The request ID to echo back
186	/// - `result`: Result value to serialize
187	///
188	/// # Returns
189	/// GenericResponse with result populated, or error if serialization fails
190	fn CreateSuccessResponse(RequestIdentifier:u64, result:&Value) -> GenericResponse {
191		let result_bytes = match serde_json::to_vec(result) {
192			Ok(bytes) => bytes,
193			Err(e) => {
194				error!("[MountainVinegRPCService] Failed to serialize result: {}", e);
195
196				// Return error response instead
197				return Self::CreateErrorResponse(
198					RequestIdentifier,
199					-32603, // Internal error
200					"Failed to serialize response".to_string(),
201					None,
202				);
203			},
204		};
205
206		GenericResponse { request_identifier:RequestIdentifier, result:result_bytes, error:None }
207	}
208}
209
210#[tonic::async_trait]
211impl MountainService for MountainVinegRPCService {
212	/// Handles generic request-response RPCs from Cocoon.
213	///
214	/// This is the main entry point for Cocoon to request operations from
215	/// Mountain. It validates the request, deserializes parameters, dispatches
216	/// to the Track module, and returns the result or error in JSON-RPC
217	/// format.
218	///
219	/// # Parameters
220	/// - `request`: GenericRequest containing method name and serialized
221	///   parameters
222	///
223	/// # Returns
224	/// - `Ok(Response<GenericResponse>)`: Response with result or error
225	/// - `Err(Status)`: gRPC status error (only for critical failures)
226	async fn process_cocoon_request(
227		&self,
228		request:Request<GenericRequest>,
229	) -> Result<Response<GenericResponse>, Status> {
230		let RequestData = request.into_inner();
231
232		let MethodName = RequestData.method.clone();
233
234		let RequestIdentifier = RequestData.request_identifier;
235
236		info!(
237			"[MountainVinegRPCService] Received gRPC Request [ID: {}]: Method='{}'",
238			RequestIdentifier, MethodName
239		);
240
241		// Validate request before processing
242		if let Err(status) = self.ValidateRequest(&RequestData) {
243			warn!("[MountainVinegRPCService] Request validation failed: {}", status);
244
245			return Ok(Response::new(Self::CreateErrorResponse(
246				RequestIdentifier,
247				-32602, // Invalid params
248				status.message().to_string(),
249				None,
250			)));
251		}
252
253		// Deserialize JSON parameters
254		let ParametersValue:Value = match serde_json::from_slice(&RequestData.parameter) {
255			Ok(v) => {
256				trace!("[MountainVinegRPCService] Params for [ID: {}]: {:?}", RequestIdentifier, v);
257				v
258			},
259			Err(e) => {
260				let msg = format!("Failed to deserialize parameters for method '{}': {}", MethodName, e);
261
262				error!("{}", msg);
263
264				return Ok(Response::new(Self::CreateErrorResponse(
265					RequestIdentifier,
266					-32700, // Parse error
267					msg,
268					None,
269				)));
270			},
271		};
272
273		debug!(
274			"[MountainVinegRPCService] Dispatching request [ID: {}] to Track::DispatchLogic",
275			RequestIdentifier
276		);
277
278		// Dispatch request to Track module for processing
279		let DispatchResult = Track::SideCarRequest::DispatchSideCarRequest(
280			self.ApplicationHandle.clone(),
281			self.RunTime.clone(),
282			// In the future, this could come from connection metadata
283			"cocoon-main".to_string(),
284			MethodName.clone(),
285			ParametersValue,
286		)
287		.await;
288
289		match DispatchResult {
290			Ok(SuccessfulResult) => {
291				info!(
292					"[MountainVinegRPCService] Request [ID: {}] completed successfully",
293					RequestIdentifier
294				);
295
296				Ok(Response::new(Self::CreateSuccessResponse(RequestIdentifier, &SuccessfulResult)))
297			},
298
299			Err(ErrorString) => {
300				error!(
301					"[MountainVinegRPCService] Request [ID: {}] failed: {}",
302					RequestIdentifier, ErrorString
303				);
304
305				Ok(Response::new(Self::CreateErrorResponse(
306					RequestIdentifier,
307					-32000, // Server error
308					ErrorString,
309					None,
310				)))
311			},
312		}
313	}
314
315	/// Handles generic fire-and-forget notifications from Cocoon.
316	///
317	/// Notifications do not expect a response beyond acknowledgment.
318	/// They are used for status updates, events, and other asynchronous
319	/// notifications.
320	///
321	/// # Parameters
322	/// - `request`: GenericNotification with method name and parameters
323	///
324	/// # Returns
325	/// - `Ok(Response<Empty>)`: Notification was received and logged
326	/// - `Err(Status)`: Critical error during processing
327	///
328	/// # TODO
329	/// Future implementation should route notifications to dedicated handlers:
330	/// ```rust,ignore
331	/// let Parameter: Value = serde_json::from_slice(&notification.parameter)?;
332	/// NotificationHandler::Handle(MethodName, Parameter).await?;
333	/// ```
334	async fn send_cocoon_notification(&self, request:Request<GenericNotification>) -> Result<Response<Empty>, Status> {
335		let NotificationData = request.into_inner();
336
337		let MethodName = NotificationData.method;
338
339		info!("[MountainVinegRPCService] Received gRPC Notification: Method='{}'", MethodName);
340
341		// Validate notification method name
342		if MethodName.is_empty() {
343			warn!("[MountainVinegRPCService] Received notification with empty method name");
344			return Err(Status::invalid_argument("Method name cannot be empty"));
345		}
346
347		// Route notifications to appropriate handlers based on MethodName. Currently
348		// only logs known notification types and acknowledges all others. A complete
349		// implementation would maintain a registry of notification handlers per method,
350		// route notifications to registered handlers asynchronously, allow handlers
351		// to perform side effects (state updates, UI updates), support cancellation
352		// and timeouts for long-running handlers, and log unhandled notifications
353		// at debug level for diagnostics. Known notifications include:
354		// ExtensionActivated, ExtensionDeactivated, WebviewReady.
355
356		match MethodName.as_str() {
357			"ExtensionActivated" => {
358				debug!("[MountainVinegRPCService] Extension activated notification received");
359			},
360			"ExtensionDeactivated" => {
361				debug!("[MountainVinegRPCService] Extension deactivated notification received");
362			},
363			"WebviewReady" => {
364				debug!("[MountainVinegRPCService] Webview ready notification received");
365			},
366			_ => {
367				debug!("[MountainVinegRPCService] Unknown notification method: {}", MethodName);
368			},
369		}
370
371		Ok(Response::new(Empty {}))
372	}
373
374	/// Handles a request from Cocoon to cancel a long-running operation.
375	///
376	/// This method is called when Cocoon wants to cancel an operation that
377	/// was previously initiated via process_cocoon_request.
378	///
379	/// # Parameters
380	/// - `request`: CancelOperationRequest with the request ID to cancel
381	///
382	/// # Returns
383	/// - `Ok(Response<Empty>)`: Cancellation was initiated
384	/// - `Err(Status)`: Critical error during cancellation
385	async fn cancel_operation(&self, request:Request<CancelOperationRequest>) -> Result<Response<Empty>, Status> {
386		let cancel_request = request.into_inner();
387
388		let RequestIdentifierToCancel = cancel_request.request_identifier_to_cancel;
389
390		info!(
391			"[MountainVinegRPCService] Received CancelOperation request for RequestID: {}",
392			RequestIdentifierToCancel
393		);
394
395		// Look up the operation in the active operations registry
396		let cancel_token = {
397			let operations = self.ActiveOperations.read().await;
398			operations.get(&RequestIdentifierToCancel).cloned()
399		};
400
401		match cancel_token {
402			Some(token) => {
403				// Trigger cancellation token to signal the operation to abort
404				token.cancel();
405
406				info!(
407					"[MountainVinegRPCService] Successfully initiated cancellation for operation {}",
408					RequestIdentifierToCancel
409				);
410
411				// Note: We don't remove the token here - the operation itself should
412				// call UnregisterOperation when it completes. This allows the
413				// operation to detect the cancellation and clean up properly.
414
415				Ok(Response::new(Empty {}))
416			},
417			None => {
418				// Operation not found - it may have already completed
419				warn!(
420					"[MountainVinegRPCService] Cannot cancel operation {}: operation not found (may have already \
421					 completed)",
422					RequestIdentifierToCancel
423				);
424
425				// Return success anyway - the operation is not running
426				Ok(Response::new(Empty {}))
427			},
428		}
429	}
430}