csharp_language_server/
server.rs

1use anyhow::Result;
2use directories::ProjectDirs;
3use std::process::Stdio;
4use std::{
5    env::temp_dir,
6    fs,
7    io::Write,
8    path::{Path, PathBuf},
9};
10use tokio::process::Command;
11
12pub async fn start_server(
13    version: &str,
14    remove_old_server_versions: bool,
15    override_directory: Option<PathBuf>,
16) -> (tokio::process::ChildStdin, tokio::process::ChildStdout) {
17    let dir = override_directory.unwrap_or(cache_dir());
18    let log_dir = cache_dir().join("log");
19
20    let server = ensure_server_is_installed(version, remove_old_server_versions, &dir)
21        .await
22        .expect("Unable to install server");
23
24    let mut command = match server {
25        ServerPath::Exe(path) => Command::new(path),
26        ServerPath::Dll(path) => {
27            let mut cmd = Command::new("dotnet");
28            cmd.arg("exec");
29            cmd.arg(path);
30            cmd
31        }
32    };
33
34    let command = command
35        .arg("--logLevel=Information")
36        .arg("--extensionLogDirectory")
37        .arg(log_dir)
38        .arg("--stdio")
39        .stdout(Stdio::piped())
40        .stdin(Stdio::piped())
41        .spawn()
42        .expect("Failed to execute command");
43
44    (command.stdin.unwrap(), command.stdout.unwrap())
45}
46
47pub async fn download_server(
48    version: &str,
49    remove_old_server_versions: bool,
50    override_directory: Option<PathBuf>,
51) -> PathBuf {
52    let dir = override_directory.unwrap_or(cache_dir());
53
54    let server_path = ensure_server_is_installed(version, remove_old_server_versions, &dir)
55        .await
56        .expect("Unable to install server");
57
58    match server_path {
59        ServerPath::Exe(path_buf) => path_buf,
60        ServerPath::Dll(path_buf) => path_buf,
61    }
62}
63
64fn cache_dir() -> PathBuf {
65    let cache_dir = ProjectDirs::from("com", "github", "csharp-language-server")
66        .expect("Unable to find cache directory")
67        .cache_dir()
68        .to_path_buf();
69
70    cache_dir.join("server")
71}
72
73enum ServerPath {
74    Exe(PathBuf),
75    Dll(PathBuf),
76}
77
78async fn ensure_server_is_installed(
79    version: &str,
80    remove_old_server_versions: bool,
81    server_root_dir: &Path,
82) -> Result<ServerPath> {
83    let server_version_dir = server_root_dir.join(version);
84
85    let rid = current_rid();
86    if std::path::Path::new(&server_version_dir.join(rid)).exists() {
87        return Ok(get_server_path(&server_version_dir, rid));
88    }
89
90    create_all(server_root_dir, remove_old_server_versions)?;
91    create_all(&server_version_dir, true)?;
92
93    let temp_build_root = temp_dir().join("csharp-language-server");
94    create(&temp_build_root, true)?;
95
96    create_csharp_project(&temp_build_root)?;
97
98    let res = Command::new("dotnet")
99        .arg("restore")
100        .arg(format!(
101            "-p:LanguageServerPackage=Microsoft.CodeAnalysis.LanguageServer.{rid}"
102        ))
103        .arg(format!("-p:LanguageServerVersion={version}"))
104        .current_dir(fs::canonicalize(temp_build_root.clone())?)
105        .output()
106        .await?;
107
108    anyhow::ensure!(
109        res.status.success(),
110        "dotnet restore failed with exit code: {:?}\nstdout: {}\nstderr: {}",
111        res.status.code(),
112        String::from_utf8_lossy(&res.stdout),
113        String::from_utf8_lossy(&res.stderr)
114    );
115
116    let temp_build_dir = temp_build_root
117        .join("out")
118        .join(format!("microsoft.codeanalysis.languageserver.{rid}"))
119        .join(version)
120        .join("content")
121        .join("LanguageServer");
122
123    move_dir(&temp_build_dir, &server_version_dir)?;
124    remove(temp_build_root)?;
125
126    Ok(get_server_path(&server_version_dir, rid))
127}
128
129fn create_csharp_project(temp_build_root: &Path) -> Result<()> {
130    let mut csproj_file = std::fs::File::create(temp_build_root.join("ServerDownload.csproj"))?;
131    csproj_file.write_all(CSPROJ.as_bytes())?;
132    Ok(())
133}
134
135fn get_server_path(server_version_dir: &Path, rid: &str) -> ServerPath {
136    let server_dir = server_version_dir.join(rid);
137    if rid == "neutral" || rid.starts_with("osx-") {
138        ServerPath::Dll(server_dir.join("Microsoft.CodeAnalysis.LanguageServer.dll"))
139    } else if rid.starts_with("win-") {
140        ServerPath::Exe(server_dir.join("Microsoft.CodeAnalysis.LanguageServer.exe"))
141    } else {
142        ServerPath::Exe(server_dir.join("Microsoft.CodeAnalysis.LanguageServer"))
143    }
144}
145
146const CSPROJ: &str = r#"
147<Project Sdk="Microsoft.NET.Sdk">
148    <PropertyGroup>
149        <RestoreSources>https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json</RestoreSources>
150        <RestorePackagesPath>out</RestorePackagesPath>
151        <TargetFramework>netstandard2.0</TargetFramework>
152        <DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
153        <DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences>
154    </PropertyGroup>
155
156    <ItemGroup>
157        <PackageDownload Include="$(LanguageServerPackage)" Version="[$(LanguageServerVersion)]" />
158    </ItemGroup>
159</Project>"#;
160
161#[allow(unreachable_code)]
162const fn current_rid() -> &'static str {
163    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
164    return "win-x64";
165
166    #[cfg(all(target_os = "windows", target_arch = "aarch64"))]
167    return "win-arm64";
168
169    #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))]
170    return "linux-x64";
171
172    #[cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))]
173    return "linux-arm64";
174
175    #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "musl"))]
176    return "linux-musl-x64";
177
178    #[cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "musl"))]
179    return "linux-musl-arm64";
180
181    #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
182    return "osx-x64";
183
184    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
185    return "osx-arm64";
186
187    "neutral"
188}
189
190fn create_all<P: AsRef<Path>>(path: P, erase: bool) -> Result<()> {
191    if erase && path.as_ref().exists() {
192        remove(&path)?;
193    }
194    Ok(fs::create_dir_all(&path)?)
195}
196
197fn remove<P: AsRef<Path>>(path: P) -> Result<()> {
198    if path.as_ref().exists() {
199        Ok(fs::remove_dir_all(path)?)
200    } else {
201        Ok(())
202    }
203}
204
205fn create<P: AsRef<Path>>(path: P, erase: bool) -> Result<()> {
206    if erase && path.as_ref().exists() {
207        remove(&path)?;
208    }
209    Ok(fs::create_dir(&path)?)
210}
211
212fn move_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
213    match fs::rename(src, dst) {
214        Ok(()) => Ok(()),
215        Err(e) if e.kind() == std::io::ErrorKind::CrossesDevices => {
216            copy_tree(src, dst)?;
217            fs::remove_dir_all(src)
218        }
219        Err(e) => Err(e),
220    }
221}
222
223fn copy_tree(src: &Path, dst: &Path) -> std::io::Result<()> {
224    fs::create_dir_all(dst)?;
225    for entry in fs::read_dir(src)? {
226        let entry = entry?;
227        let from = entry.path();
228        let to = dst.join(entry.file_name());
229
230        if from.is_dir() {
231            copy_tree(&from, &to)?;
232        } else {
233            fs::copy(&from, &to)?;
234        }
235    }
236    Ok(())
237}