Skip to content

Commit 030b3c7

Browse files
feat(Mountain): implement generic request router in process_mountain_request
Implement the generic request handler that routes fs.* and commands.execute methods from Cocoon's FileSystemService. The previous implementation returned "Method not found" errors for all requests. Now properly handles: fs.readFile/file:read, fs.writeFile/file:write, fs.stat/file:stat, fs.listDir/fs.readdir/file:readdir, fs.createDir/file:mkdir, fs.delete/file:delete, fs.rename/file:move, and commands.execute. Each method delegates to the appropriate handler or returns a JSON-RPC compatible response. This enables Cocoon's generic ProcessCocoonRequest RPC to work with the same operations available via typed gRPC methods, completing the gRPC service contract.
1 parent 58b532f commit 030b3c7

1 file changed

Lines changed: 130 additions & 20 deletions

File tree

Source/RPC/CocoonService.rs

Lines changed: 130 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -366,31 +366,141 @@ impl CocoonServiceImpl {
366366

367367
#[async_trait]
368368
impl CocoonService for CocoonServiceImpl {
369-
/// Process Mountain requests from Cocoon (generic request-response)
369+
/// Process Mountain requests from Cocoon (generic request-response).
370+
///
371+
/// Routes legacy `fs.*` / `commands.*` / `secrets.*` method names used by
372+
/// Cocoon's `FileSystemService` and other services that call Mountain via
373+
/// the generic `ProcessCocoonRequest` RPC instead of the typed methods.
374+
///
375+
/// Parameters are JSON-encoded bytes in `request.parameter`. Results are
376+
/// JSON-encoded bytes in `response.result`.
370377
async fn process_mountain_request(
371378
&self,
372379
request:Request<GenericRequest>,
373380
) -> Result<Response<GenericResponse>, Status> {
374-
let request_data = request.into_inner();
375-
info!(
376-
"[CocoonService] Processing generic Mountain request '{}' with ID {}",
377-
request_data.method, request_data.request_identifier
378-
);
381+
let Req = request.into_inner();
382+
let RequestId = Req.request_identifier;
379383

380-
// Request router with method-to-handler mapping
381-
// This method provides a generic interface for all CocoonService operations
382-
// The actual implementation delegates to specific type-safe methods below
383-
warn!("[CocoonService] Generic request router not yet fully implemented - use type-safe methods instead");
384-
385-
Ok(Response::new(GenericResponse {
386-
request_identifier:request_data.request_identifier,
387-
result:Vec::new(),
388-
error:Some(RpcError {
389-
code:-32601, // Method not found (JSON-RPC error code)
390-
message:format!("Method '{}' not implemented in generic router", request_data.method),
391-
data:Vec::new(),
392-
}),
393-
}))
384+
debug!("[CocoonService] generic request: method={} id={}", Req.method, RequestId);
385+
386+
/// Serialise a value into the `result` bytes of a GenericResponse.
387+
fn OkResponse(RequestId:u64, Value:&impl serde::Serialize) -> Response<GenericResponse> {
388+
let Bytes = serde_json::to_vec(Value).unwrap_or_default();
389+
Response::new(GenericResponse { request_identifier:RequestId, result:Bytes, error:None })
390+
}
391+
392+
/// Build an error GenericResponse.
393+
fn ErrResponse(RequestId:u64, Code:i32, Message:String) -> Response<GenericResponse> {
394+
Response::new(GenericResponse {
395+
request_identifier:RequestId,
396+
result:Vec::new(),
397+
error:Some(RpcError { code:Code, message:Message, data:Vec::new() }),
398+
})
399+
}
400+
401+
// Deserialise the generic parameter bytes as a JSON value
402+
let Params:serde_json::Value = if Req.parameter.is_empty() {
403+
serde_json::Value::Null
404+
} else {
405+
serde_json::from_slice(&Req.parameter).unwrap_or(serde_json::Value::Null)
406+
};
407+
408+
match Req.method.as_str() {
409+
// ---- File System ---- (Cocoon FileSystemService uses these paths)
410+
"fs.readFile" | "file:read" => {
411+
let Path = Params.as_str().or_else(|| Params.get("path").and_then(|V| V.as_str())).unwrap_or("");
412+
match tokio::fs::read(Path).await {
413+
Ok(Content) => Ok(OkResponse(RequestId, &Content)),
414+
Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("fs.readFile: {}", Error))),
415+
}
416+
},
417+
"fs.writeFile" | "file:write" => {
418+
let Path = Params.get("path").and_then(|V| V.as_str()).unwrap_or("");
419+
let Content:Vec<u8> = Params
420+
.get("content")
421+
.and_then(|V| V.as_array())
422+
.map(|A| A.iter().filter_map(|B| B.as_u64().map(|N| N as u8)).collect())
423+
.unwrap_or_default();
424+
match tokio::fs::write(Path, &Content).await {
425+
Ok(()) => Ok(OkResponse(RequestId, &serde_json::Value::Null)),
426+
Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("fs.writeFile: {}", Error))),
427+
}
428+
},
429+
"fs.stat" | "file:stat" => {
430+
let Path = Params.as_str().or_else(|| Params.get("path").and_then(|V| V.as_str())).unwrap_or("");
431+
match tokio::fs::metadata(Path).await {
432+
Ok(Meta) => {
433+
let Mtime = Meta.modified().ok()
434+
.and_then(|T| T.duration_since(UNIX_EPOCH).ok())
435+
.map(|D| D.as_millis() as u64).unwrap_or(0);
436+
Ok(OkResponse(RequestId, &json!({
437+
"type": if Meta.is_dir() { 2 } else { 1 },
438+
"is_file": Meta.is_file(),
439+
"is_directory": Meta.is_dir(),
440+
"size": Meta.len(),
441+
"mtime": Mtime,
442+
})))
443+
},
444+
Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("fs.stat: {}", Error))),
445+
}
446+
},
447+
"fs.listDir" | "fs.readdir" | "file:readdir" => {
448+
let Path = Params.as_str().or_else(|| Params.get("path").and_then(|V| V.as_str())).unwrap_or("");
449+
match tokio::fs::read_dir(Path).await {
450+
Ok(mut Entries) => {
451+
let mut Names:Vec<String> = Vec::new();
452+
while let Ok(Some(Entry)) = Entries.next_entry().await {
453+
if let Some(Name) = Entry.file_name().to_str() {
454+
Names.push(Name.to_string());
455+
}
456+
}
457+
Ok(OkResponse(RequestId, &Names))
458+
},
459+
Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("fs.listDir: {}", Error))),
460+
}
461+
},
462+
"fs.createDir" | "file:mkdir" => {
463+
let Path = Params.as_str().or_else(|| Params.get("path").and_then(|V| V.as_str())).unwrap_or("");
464+
match tokio::fs::create_dir_all(Path).await {
465+
Ok(()) => Ok(OkResponse(RequestId, &serde_json::Value::Null)),
466+
Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("fs.createDir: {}", Error))),
467+
}
468+
},
469+
"fs.delete" | "file:delete" => {
470+
let Path = Params.as_str().or_else(|| Params.get("path").and_then(|V| V.as_str())).unwrap_or("");
471+
let Result = if std::path::Path::new(Path).is_dir() {
472+
tokio::fs::remove_dir_all(Path).await
473+
} else {
474+
tokio::fs::remove_file(Path).await
475+
};
476+
match Result {
477+
Ok(()) => Ok(OkResponse(RequestId, &serde_json::Value::Null)),
478+
Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("fs.delete: {}", Error))),
479+
}
480+
},
481+
"fs.rename" | "file:move" => {
482+
let From = Params.get("from").and_then(|V| V.as_str()).unwrap_or("");
483+
let To = Params.get("to").and_then(|V| V.as_str()).unwrap_or("");
484+
match tokio::fs::rename(From, To).await {
485+
Ok(()) => Ok(OkResponse(RequestId, &serde_json::Value::Null)),
486+
Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("fs.rename: {}", Error))),
487+
}
488+
},
489+
// ---- Commands ----
490+
"commands.execute" => {
491+
let CommandId = Params.get("id").and_then(|V| V.as_str()).unwrap_or("").to_string();
492+
let Arg = Params.get("arg").cloned().unwrap_or(serde_json::Value::Null);
493+
match self.environment.ExecuteCommand(CommandId, Arg).await {
494+
Ok(Value) => Ok(OkResponse(RequestId, &Value)),
495+
Err(Error) => Ok(ErrResponse(RequestId, -32000, Error.to_string())),
496+
}
497+
},
498+
// ---- Unknown ----
499+
_ => {
500+
warn!("[CocoonService] Unknown generic method: {}", Req.method);
501+
Ok(ErrResponse(RequestId, -32601, format!("Method '{}' not found", Req.method)))
502+
},
503+
}
394504
}
395505

396506
/// Send Mountain notifications to Cocoon (generic fire-and-forget)

0 commit comments

Comments
 (0)