Skip to content

Commit

Permalink
parse URIs properly
Browse files Browse the repository at this point in the history
- parse file paths which are URIs using the uriparse crate
- use deprecated fields as fallback for current fields
- put back changedir before spawning child
- pass proxy-client cwd as a fallback
  • Loading branch information
pr2502 committed Oct 19, 2023
1 parent 71703ca commit 849e571
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 33 deletions.
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ tokio = { version = "1.15.0", features = ["fs", "io-std", "io-util", "macros", "
toml = "0.5.8"
tracing = "0.1.39"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
uriparse = "0.6.4"
126 changes: 99 additions & 27 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use std::io::ErrorKind;
use std::sync::Arc;

use anyhow::{bail, ensure, Context, Result};
use anyhow::{bail, Context, Result};
use serde_json::Value;
use tokio::io::BufReader;
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
use tokio::net::TcpStream;
use tokio::sync::{mpsc, Mutex};
use tokio::{select, task};
use tracing::{debug, error, info, Instrument};
use tracing::{debug, error, info, warn, Instrument};
use uriparse::URI;

use crate::instance::{self, Instance, InstanceKey, InstanceMap};
use crate::lsp::jsonrpc::{Message, RequestId, ResponseSuccess, Version};
Expand Down Expand Up @@ -50,31 +51,29 @@ pub async fn process(
// TODO verify protocol version

// Select the workspace root directory.
//
// Ideally we'd be looking up any server which has a superset of workspace
// folders active possibly adding transforming the `initialize` request into
// a few requests for adding workspace folders if the server supports it.
// Buuut let's just run with supporting single-folder workspaces only at
// first, it's probably the most common use-case anyway.
let folders = &init_params.workspace_folders;
ensure!(
folders.len() == 1,
"workspace must have exactly 1 folder, has {n}.\n{folders:#?}",
n = folders.len(),
);
let folder = init_params.workspace_folders[0].clone();
let workspace_root = {
let (scheme, _, mut path, _, _) = select_workspace_root(&init_params)
.context("could not get any workspace_root")?
.into_parts();
if scheme != uriparse::Scheme::File {
bail!("only `file://` URIs are supported");
}
path.normalize(false);
path.to_string()
};

// Get an language server instance for this client.
let key = InstanceKey {
server: options.server,
args: options.args,
workspace_root: folder.uri,
workspace_root,
};
let instance = instance::get_or_spawn(instance_map, key, init_params).await?;

// Respond to client's `initialize` request using a response result from
// the first time this server instance was initialized, it might not be a
// response directly to our previous request but it should be similar since
// the first time this server instance was initialized, it might not be
// a response directly to our previous request but it should be hopefully
// similar if it comes from another instance of the same client.
let res = ResponseSuccess {
jsonrpc: Version,
result: serde_json::to_value(instance.initialize_result()).unwrap(),
Expand Down Expand Up @@ -111,22 +110,95 @@ pub async fn process(
Ok(())
}

fn select_workspace_root(init_params: &InitializeParams) -> Result<URI> {
if init_params.workspace_folders.len() > 1 {
// TODO Ideally we'd be looking up any server which has a superset of
// workspace folders active possibly adding transforming the `initialize`
// request into a few requests for adding workspace folders if the
// server supports it. Buuut let's just run with supporting single-folder
// workspaces only at first, it's probably the most common use-case anyway.
warn!("initialize request with multiple workspace folders isn't supported");
debug!(workspace_folders = ?init_params.workspace_folders);
}

if init_params.workspace_folders.len() == 1 {
match URI::try_from(init_params.workspace_folders[0].uri.as_str())
.context("parse initParams.workspaceFolders[0].uri")
{
Ok(root) => return Ok(root),
Err(err) => warn!(?err, "failed to parse URI"),
}
}

assert!(init_params.workspace_folders.is_empty());

// Using the deprecated fields `rootPath` or `rootUri` as fallback
if let Some(root_uri) = &init_params.root_uri {
match URI::try_from(root_uri.as_str()).context("parse initParams.rootUri") {
Ok(root) => return Ok(root),
Err(err) => warn!(?err, "failed to parse URI"),
}
}
if let Some(root_path) = &init_params.root_path {
// `rootPath` doesn't have a schema but `Url` requires it to parse
match uriparse::Path::try_from(root_path.as_str())
.map_err(uriparse::URIError::from)
.and_then(|path| {
URI::builder()
.with_scheme(uriparse::Scheme::File)
.with_path(path)
.build()
})
.context("parse initParams.rootPath")
{
Ok(root) => return Ok(root),
Err(err) => warn!(?err, "failed to parse URI"),
}
}

// Using the proxy `cwd` as fallback
if let Some(proxy_cwd) = init_params
.initialization_options
.as_ref()
.and_then(|opts| opts.lsp_mux.as_ref())
.and_then(|lsp_mux| lsp_mux.cwd.as_ref())
{
match uriparse::Path::try_from(proxy_cwd.as_str())
.map_err(uriparse::URIError::from)
.and_then(|path| {
URI::builder()
.with_scheme(uriparse::Scheme::File)
.with_path(path)
.build()
})
.context("parse initParams.initializationOptions.lspMux.cwd")
{
Ok(root) => return Ok(root),
Err(err) => warn!(?err, "failed to parse URI"),
}
}

bail!("could not determine a suitable workspace_root");
}

/// Receives messages from a channel and writes tem to the client input socket
async fn input_task(
mut rx: mpsc::Receiver<Message>,
mut close_rx: mpsc::Receiver<Message>,
mut writer: LspWriter<OwnedWriteHalf>,
) {
// Unlike the output task, here we first wait on the channel which is going to
// block until the language server sends a notification, however if we're the last
// client and have just closed the server is unlikely to send any. This results in the
// last client often falsely hanging while the gc task depends on the input channels being
// closed to detect a disconnected client.
// Unlike the output task, here we first wait on the channel which is going
// to block until the language server sends a notification, however if
// we're the last client and have just closed the server is unlikely to send
// any. This results in the last client often falsely hanging while the gc
// task depends on the input channels being closed to detect a disconnected
// client.
//
// When a client sends a shutdown request we receive a message on the `close_rx`, send
// the reply and close the connection. If no shutdown request was received but the
// client closed `close_rx` channel will be dropped (unlike the normal rx channel which
// is shared) and the connection will close without sending any response.
// When a client sends a shutdown request we receive a message on the
// `close_rx`, send the reply and close the connection. If no shutdown
// request was received but the client closed `close_rx` channel will be
// dropped (unlike the normal rx channel which is shared) and the connection
// will close without sending any response.
while let Some(message) = select! {
message = close_rx.recv() => message,
message = rx.recv() => message,
Expand Down
3 changes: 1 addition & 2 deletions src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ use crate::lsp::{InitializeParams, InitializeResult};
pub struct InstanceKey {
pub server: String,
pub args: Vec<String>,
/// `initialize.params.workspace_folders[0].uri`
pub workspace_root: String,
}

Expand Down Expand Up @@ -184,7 +183,7 @@ async fn spawn(
) -> Result<Arc<Instance>> {
let mut child = Command::new(&key.server)
.args(&key.args)
// .current_dir(&key.workspace_root)
.current_dir(&key.workspace_root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
Expand Down
14 changes: 10 additions & 4 deletions src/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ pub struct LspMuxOptions {
/// empty list if omited.
#[serde(default = "Vec::new")]
pub args: Vec<String>,

/// Current working directory of the proxy command. This is only used as
/// fallback if the client doesn't provide any workspace root.
#[serde(skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
}

#[derive(Serialize, Deserialize, Clone)]
Expand Down Expand Up @@ -161,7 +166,8 @@ mod tests {
"lspMux": {
"version": "1",
"server": "some-language-server",
"args": ["a", "b", "c"]
"args": ["a", "b", "c"],
"cwd": "/home/user",
}
}))
}
Expand All @@ -172,7 +178,7 @@ mod tests {
"lspMux": {
"version": "1",
"server": "some-language-server",
"args": ["a", "b", "c"]
"args": ["a", "b", "c"],
},
"lsp_mux": "not the right key",
"lspmux": "also not it",
Expand All @@ -190,7 +196,7 @@ mod tests {
test::<InitializationOptions>(json!({
"lspMux": {
"server": "some-language-server",
"args": ["a", "b", "c"]
"args": ["a", "b", "c"],
},
}))
}
Expand All @@ -201,7 +207,7 @@ mod tests {
test::<InitializationOptions>(json!({
"lspMux": {
"version": "1",
"args": ["a", "b", "c"]
"args": ["a", "b", "c"],
},
}))
}
Expand Down
5 changes: 5 additions & 0 deletions src/proxy.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::env;
use std::pin::Pin;
use std::task::{Context, Poll};

Expand Down Expand Up @@ -53,6 +54,9 @@ impl AsyncWrite for Stdio {

pub async fn run(server: String, args: Vec<String>) -> Result<()> {
let config = Config::load_or_default().await;
let cwd = env::current_dir()
.ok()
.and_then(|path| path.to_str().map(String::from));
let version = String::from("test"); // TODO use a real protocol version number

let mut stream = TcpStream::connect(config.connect)
Expand All @@ -78,6 +82,7 @@ pub async fn run(server: String, args: Vec<String>) -> Result<()> {
version,
server,
args,
cwd,
});
req.params = serde_json::to_value(params).expect("BUG: invalid data");

Expand Down

0 comments on commit 849e571

Please sign in to comment.