Skip to content

Commit 89a9cd4

Browse files
authored
Add support for JSON RPC requests (#34)
1 parent 5d42aad commit 89a9cd4

File tree

16 files changed

+371
-78
lines changed

16 files changed

+371
-78
lines changed

crates/pet-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub mod manager;
1111
pub mod os_environment;
1212
pub mod python_environment;
1313
pub mod reporter;
14+
// pub mod telemetry;
1415

1516
#[derive(Debug, Clone)]
1617
pub struct LocatorResult {

crates/pet-core/src/reporter.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@
44
use crate::{manager::EnvManager, python_environment::PythonEnvironment};
55

66
pub trait Reporter: Send + Sync {
7-
// fn get_reported_managers() -> Arc<Mutex<HashSet<PathBuf>>>;
8-
// fn get_reported_environments() -> Arc<Mutex<HashSet<PathBuf>>>;
9-
107
fn report_manager(&self, manager: &EnvManager);
118
fn report_environment(&self, env: &PythonEnvironment);
12-
fn report_completion(&self, duration: std::time::Duration);
9+
// fn report_telemetry(&self, event: &TelemetryEvent);
1310
}

crates/pet-homebrew/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ impl Homebrew {
2828
}
2929
}
3030

31-
fn resolve(env: &PythonEnv) -> Option<PythonEnvironment> {
31+
fn from(env: &PythonEnv) -> Option<PythonEnvironment> {
3232
// Note: Sometimes if Python 3.10 was installed by other means (e.g. from python.org or other)
3333
// & then you install Python 3.10 via Homebrew, then some files will get installed via homebrew,
3434
// However everything (symlinks, Python executable `sys.executable`, `sys.prefix`) eventually point back to the existing installation.
@@ -83,7 +83,7 @@ fn resolve(env: &PythonEnv) -> Option<PythonEnvironment> {
8383

8484
impl Locator for Homebrew {
8585
fn from(&self, env: &PythonEnv) -> Option<PythonEnvironment> {
86-
resolve(env)
86+
from(env)
8787
}
8888

8989
fn find(&self, reporter: &dyn Reporter) {
@@ -112,7 +112,7 @@ impl Locator for Homebrew {
112112
// However this is a very generic location, and we might end up with other python installs here.
113113
// Hence call `resolve` to correctly identify homebrew python installs.
114114
let env_to_resolve = PythonEnv::new(file.clone(), None, None);
115-
if let Some(env) = resolve(&env_to_resolve) {
115+
if let Some(env) = from(&env_to_resolve) {
116116
reporter.report_environment(&env);
117117
}
118118
});

crates/pet-jsonrpc/src/core.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,32 @@ pub fn send_message<T: serde::Serialize>(method: &'static str, params: Option<T>
2828
);
2929
let _ = io::stdout().flush();
3030
}
31+
pub fn send_reply<T: serde::Serialize>(id: u32, payload: Option<T>) {
32+
let payload = serde_json::json!({
33+
"jsonrpc": "2.0",
34+
"result": payload,
35+
"id": id
36+
});
37+
let message = serde_json::to_string(&payload).unwrap();
38+
print!(
39+
"Content-Length: {}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{}",
40+
message.len(),
41+
message
42+
);
43+
let _ = io::stdout().flush();
44+
}
45+
46+
pub fn send_error(id: Option<u32>, code: i32, message: String) {
47+
let payload = serde_json::json!({
48+
"jsonrpc": "2.0",
49+
"error": { "code": code, "message": message },
50+
"id": id
51+
});
52+
let message = serde_json::to_string(&payload).unwrap();
53+
print!(
54+
"Content-Length: {}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{}",
55+
message.len(),
56+
message
57+
);
58+
let _ = io::stdout().flush();
59+
}

crates/pet-jsonrpc/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22
// Licensed under the MIT License.
33

44
mod core;
5+
pub mod server;
56

67
pub fn send_message<T: serde::Serialize>(method: &'static str, params: Option<T>) {
78
core::send_message(method, params)
89
}
10+
11+
pub fn send_reply<T: serde::Serialize>(id: u32, payload: Option<T>) {
12+
core::send_reply(id, payload)
13+
}
14+
15+
pub fn send_error(id: Option<u32>, code: i32, message: String) {
16+
core::send_error(id, code, message)
17+
}

crates/pet-jsonrpc/src/server.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use crate::core::send_error;
5+
use serde_json::{self, Value};
6+
use std::{
7+
collections::HashMap,
8+
io::{self, Read},
9+
sync::Arc,
10+
};
11+
12+
type RequestHandler<C> = Arc<dyn Fn(Arc<C>, u32, Value)>;
13+
type NotificationHandler<C> = Arc<dyn Fn(Arc<C>, Value)>;
14+
15+
pub struct HandlersKeyedByMethodName<C> {
16+
context: Arc<C>,
17+
requests: HashMap<&'static str, RequestHandler<C>>,
18+
notifications: HashMap<&'static str, NotificationHandler<C>>,
19+
}
20+
21+
impl<C> HandlersKeyedByMethodName<C> {
22+
pub fn new(context: Arc<C>) -> Self {
23+
HandlersKeyedByMethodName {
24+
context,
25+
requests: HashMap::new(),
26+
notifications: HashMap::new(),
27+
}
28+
}
29+
30+
pub fn add_request_handler<F>(&mut self, method: &'static str, handler: F)
31+
where
32+
F: Fn(Arc<C>, u32, Value) + Send + Sync + 'static,
33+
{
34+
self.requests.insert(
35+
method,
36+
Arc::new(move |context, id, params| {
37+
handler(context, id, params);
38+
}),
39+
);
40+
}
41+
42+
pub fn add_notification_handler<F>(&mut self, method: &'static str, handler: F)
43+
where
44+
F: Fn(Arc<C>, Value) + Send + Sync + 'static,
45+
{
46+
self.notifications.insert(
47+
method,
48+
Arc::new(move |context, params| {
49+
handler(context, params);
50+
}),
51+
);
52+
}
53+
54+
fn handle_request(&self, message: Value) {
55+
match message["method"].as_str() {
56+
Some(method) => {
57+
if let Some(id) = message["id"].as_u64() {
58+
if let Some(handler) = self.requests.get(method) {
59+
handler(self.context.clone(), id as u32, message["params"].clone());
60+
} else {
61+
eprint!("Failed to find handler for method: {}", method);
62+
send_error(
63+
Some(id as u32),
64+
-1,
65+
format!("Failed to find handler for request {}", method),
66+
);
67+
}
68+
} else {
69+
// No id, so this is a notification
70+
if let Some(handler) = self.notifications.get(method) {
71+
handler(self.context.clone(), message["params"].clone());
72+
} else {
73+
eprint!("Failed to find handler for method: {}", method);
74+
send_error(
75+
None,
76+
-2,
77+
format!("Failed to find handler for notification {}", method),
78+
);
79+
}
80+
}
81+
}
82+
None => {
83+
eprint!("Failed to get method from message: {}", message);
84+
send_error(
85+
None,
86+
-3,
87+
format!(
88+
"Failed to extract method from JSONRPC payload {:?}",
89+
message
90+
),
91+
);
92+
}
93+
};
94+
}
95+
}
96+
97+
/// Starts the jsonrpc server that listens for requests on stdin.
98+
/// This function will block forever.
99+
pub fn start_server<C>(handlers: &HandlersKeyedByMethodName<C>) -> ! {
100+
let mut stdin = io::stdin();
101+
loop {
102+
let mut input = String::new();
103+
match stdin.read_line(&mut input) {
104+
Ok(_) => {
105+
let mut empty_line = String::new();
106+
match get_content_length(&input) {
107+
Ok(content_length) => {
108+
let _ = stdin.read_line(&mut empty_line);
109+
let mut buffer = vec![0; content_length];
110+
111+
match stdin.read_exact(&mut buffer) {
112+
Ok(_) => {
113+
let request =
114+
String::from_utf8_lossy(&buffer[..content_length]).to_string();
115+
match serde_json::from_str(&request) {
116+
Ok(request) => handlers.handle_request(request),
117+
Err(err) => {
118+
eprint!("Failed to parse LINE: {}, {:?}", request, err)
119+
}
120+
}
121+
continue;
122+
}
123+
Err(err) => {
124+
eprint!(
125+
"Failed to read exactly {} bytes, {:?}",
126+
content_length, err
127+
)
128+
}
129+
}
130+
}
131+
Err(err) => eprint!("Failed to get content length from {}, {:?}", input, err),
132+
};
133+
}
134+
Err(error) => println!("Error in reading a line from stdin: {error}"),
135+
}
136+
}
137+
}
138+
139+
/// Parses the content length from the given line.
140+
fn get_content_length(line: &str) -> Result<usize, String> {
141+
let line = line.trim();
142+
if let Some(content_length) = line.find("Content-Length: ") {
143+
let start = content_length + "Content-Length: ".len();
144+
if let Ok(length) = line[start..].parse::<usize>() {
145+
Ok(length)
146+
} else {
147+
Err(format!(
148+
"Failed to parse content length from {} for {}",
149+
&line[start..],
150+
line
151+
))
152+
}
153+
} else {
154+
Err(format!(
155+
"String 'Content-Length' not found in input => {}",
156+
line
157+
))
158+
}
159+
}

crates/pet-python-utils/src/headers.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,25 @@ pub fn get_version(path: &Path) -> Option<String> {
4242
if let Ok(result) = fs::read_to_string(patchlevel_h) {
4343
contents = result;
4444
} else if fs::metadata(&headers_path).is_err() {
45+
// TODO: Remove this check, unnecessary, as we try to read the dir below.
4546
// Such a path does not exist, get out.
4647
continue;
4748
} else {
4849
// Try the other path
4950
// Sometimes we have it in a sub directory such as `python3.10` or `pypy3.9`
5051
if let Ok(readdir) = fs::read_dir(&headers_path) {
51-
for path in readdir.filter_map(Result::ok).map(|e| e.path()) {
52-
if let Ok(metadata) = fs::metadata(&path) {
53-
if metadata.is_dir() {
54-
let patchlevel_h = path.join("patchlevel.h");
55-
if let Ok(result) = fs::read_to_string(patchlevel_h) {
56-
contents = result;
57-
break;
58-
}
52+
for path in readdir.filter_map(Result::ok) {
53+
if let Ok(t) = path.file_type() {
54+
if !t.is_dir() {
55+
continue;
5956
}
6057
}
58+
let path = path.path();
59+
let patchlevel_h = path.join("patchlevel.h");
60+
if let Ok(result) = fs::read_to_string(patchlevel_h) {
61+
contents = result;
62+
break;
63+
}
6164
}
6265
}
6366
}

crates/pet-reporter/src/jsonrpc.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,6 @@ impl Reporter for JsonRpcReporter {
3939
}
4040
}
4141
}
42-
fn report_completion(&self, duration: std::time::Duration) {
43-
send_message("exit", duration.as_millis().into())
44-
}
4542
}
4643

4744
pub fn create_reporter() -> impl Reporter {

crates/pet-reporter/src/stdio.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ impl Reporter for StdioReporter {
4040
}
4141
}
4242
}
43-
fn report_completion(&self, duration: std::time::Duration) {
44-
println!("Refresh completed in {}ms", duration.as_millis())
45-
}
4643
}
4744

4845
pub fn create_reporter() -> impl Reporter {

crates/pet-reporter/src/test.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,6 @@ impl Reporter for TestReporter {
5050
}
5151
}
5252
}
53-
fn report_completion(&self, _duration: std::time::Duration) {
54-
//
55-
}
5653
}
5754

5855
pub fn create_reporter() -> TestReporter {

0 commit comments

Comments
 (0)