csharp_language_server/
server.rs1use 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}