mod protocol;
use clap::Subcommand;
use openssh::{Session, SessionBuilder, Stdio};
use std::path::{Path, PathBuf};
use tokio::io::BufReader;
use tracing::{debug, info};
use crate::{Host, Port, Workspace};
#[derive(Subcommand)]
pub enum LspServerCommand {
RustAnalyzer {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Clangd {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Pyright {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
}
#[derive(Debug, Clone)]
pub enum LspServer {
RustAnalyzer { args: Vec<String> },
Clangd { args: Vec<String> },
Pyright { args: Vec<String> },
}
impl LspServer {
pub fn name(&self) -> &str {
match self {
LspServer::RustAnalyzer { .. } => "rust-analyzer",
LspServer::Clangd { .. } => "clangd",
LspServer::Pyright { .. } => "pyright",
}
}
pub fn args(&self) -> &[String] {
match self {
LspServer::RustAnalyzer { args } => args,
LspServer::Clangd { args } => args,
LspServer::Pyright { args } => args,
}
}
}
impl From<&LspServerCommand> for LspServer {
fn from(cmd: &LspServerCommand) -> Self {
match cmd {
LspServerCommand::RustAnalyzer { args } => {
LspServer::RustAnalyzer { args: args.clone() }
}
LspServerCommand::Clangd { args } => LspServer::Clangd { args: args.clone() },
LspServerCommand::Pyright { args } => LspServer::Pyright { args: args.clone() },
}
}
}
async fn sync_workspace(
session: &Session,
local_path: &Path,
exclude_patterns: &[String],
) -> Result<String, Box<dyn std::error::Error>> {
if !local_path.exists() {
return Err(format!("Local path does not exist: {}", local_path.display()).into());
}
let folder_name = local_path
.file_name()
.ok_or("Invalid path")?
.to_string_lossy();
let remote_path = format!("/tmp/{}", folder_name);
info!(
local = %local_path.display(),
remote = %remote_path,
"Copying folder to remote system"
);
let output = session
.command("mkdir")
.arg("-p")
.arg(&remote_path)
.output()
.await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Failed to create remote directory: {}", stderr).into());
}
debug!("Created remote directory: {}", remote_path);
let mut rsync_cmd = "rsync -avz".to_string();
for pattern in exclude_patterns {
rsync_cmd.push_str(&format!(" --exclude='{}'", pattern));
debug!("Excluding pattern: {}", pattern);
}
rsync_cmd.push_str(&format!(" {}/ {}/", local_path.display(), remote_path));
info!("Running: {}", rsync_cmd);
let output = session
.command("bash")
.arg("-c")
.arg(&rsync_cmd)
.output()
.await?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
debug!("Rsync output: {}", stdout);
info!("Successfully copied folder to {}", remote_path);
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Failed to copy folder: {}", stderr).into());
}
let output = session
.command("ls")
.arg("-la")
.arg(&remote_path)
.output()
.await?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
debug!("Remote directory contents:\n{}", stdout);
}
Ok(remote_path)
}
async fn run_lsp_server(
session: &Session,
remote_path: &str,
server_name: &str,
server_args: &[String],
) -> Result<(), Box<dyn std::error::Error>> {
info!("Starting {} on remote machine", server_name);
let output = session
.command("bash")
.arg("-c")
.arg(format!("cd {} && {} --version", remote_path, server_name))
.output()
.await?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
info!("{} version: {}", server_name, stdout.trim());
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("{} not found: {}", server_name, stderr).into());
}
let mut cmd = format!("cd {} && {}", remote_path, server_name);
for arg in server_args {
cmd.push_str(&format!(" {}", arg));
}
info!("Running: {}", cmd);
info!("Starting LSP proxy (bidirectional JSON-RPC over stdin/stdout)");
let mut child = session
.command("bash")
.arg("-c")
.arg(&cmd)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.await?;
let remote_stdin = child.stdin().take().ok_or("Failed to get remote stdin")?;
let remote_stdout = child.stdout().take().ok_or("Failed to get remote stdout")?;
let stdin_to_remote = tokio::spawn(async move {
let local_stdin = tokio::io::stdin();
let mut local_stdin_reader = BufReader::new(local_stdin);
let mut remote_stdin = remote_stdin;
loop {
match protocol::read_lsp_message(&mut local_stdin_reader).await {
Ok(Some(message)) => {
debug!("→ Forwarding message to remote: {} bytes", message.len());
if let Err(e) = protocol::write_lsp_message(&mut remote_stdin, &message).await {
debug!("Failed to write to remote stdin: {}", e);
break;
}
}
Ok(None) => {
info!("Local stdin closed");
break;
}
Err(e) => {
debug!("Failed to read from local stdin: {}", e);
break;
}
}
}
});
let remote_to_stdout = tokio::spawn(async move {
let mut remote_stdout_reader = BufReader::new(remote_stdout);
let mut local_stdout = tokio::io::stdout();
loop {
match protocol::read_lsp_message(&mut remote_stdout_reader).await {
Ok(Some(message)) => {
debug!("← Forwarding message from remote: {} bytes", message.len());
if let Err(e) = protocol::write_lsp_message(&mut local_stdout, &message).await {
debug!("Failed to write to local stdout: {}", e);
break;
}
}
Ok(None) => {
info!("Remote stdout closed");
break;
}
Err(e) => {
debug!("Failed to read from remote stdout: {}", e);
break;
}
}
}
});
tokio::select! {
_ = stdin_to_remote => {
info!("stdin→remote task completed");
}
_ = remote_to_stdout => {
info!("remote→stdout task completed");
}
status = child.wait() => {
match status {
Ok(s) => info!("{} exited with status: {:?}", server_name, s),
Err(e) => info!("Failed to wait for child: {}", e),
}
}
}
Ok(())
}
pub async fn start(
host: Host,
port: Port,
workspace: Workspace,
exclude_patterns: &[String],
server: &LspServer,
) -> Result<(), Box<dyn std::error::Error>> {
let address: String = host.into();
let port = port.into();
let local_path: PathBuf = workspace.into();
info!(
address = %address,
port = port,
path = %local_path.display(),
server = %server.name(),
"Connecting to remote host"
);
let session = SessionBuilder::default()
.port(port)
.connect(address)
.await?;
info!("SSH connection established");
let remote_path = sync_workspace(&session, &local_path, exclude_patterns).await?;
run_lsp_server(&session, &remote_path, server.name(), server.args()).await?;
session.close().await?;
info!("SSH session closed");
Ok(())
}