cli/
update_service.rs

1/*---------------------------------------------------------------------------------------------
2 *  Copyright (c) Microsoft Corporation. All rights reserved.
3 *  Licensed under the MIT License. See License.txt in the project root for license information.
4 *--------------------------------------------------------------------------------------------*/
5
6use std::{ffi::OsStr, fmt, path::Path};
7
8use serde::{Deserialize, Serialize};
9
10use crate::{
11	constants::VSCODE_CLI_UPDATE_ENDPOINT,
12	debug, log, options, spanf,
13	util::{
14		errors::{AnyError, CodeError, WrappedError},
15		http::{BoxedHttp, SimpleResponse},
16		io::ReportCopyProgress,
17		tar, zipper,
18	},
19};
20
21/// Implementation of the VS Code Update service for use in the CLI.
22#[derive(Clone)]
23pub struct UpdateService {
24	client: BoxedHttp,
25	log: log::Logger,
26}
27
28/// Describes a specific release, can be created manually or returned from the update service.
29#[derive(Clone, Eq, PartialEq)]
30pub struct Release {
31	pub name: String,
32	pub platform: Platform,
33	pub target: TargetKind,
34	pub quality: options::Quality,
35	pub commit: String,
36}
37
38impl std::fmt::Display for Release {
39	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40		write!(f, "{} (commit {})", self.name, self.commit)
41	}
42}
43
44#[derive(Deserialize)]
45struct UpdateServerVersion {
46	pub version: String,
47	pub name: String,
48}
49
50fn quality_download_segment(quality: options::Quality) -> &'static str {
51	match quality {
52		options::Quality::Stable => "stable",
53		options::Quality::Insiders => "insider",
54		options::Quality::Exploration => "exploration",
55	}
56}
57
58fn get_update_endpoint() -> Result<&'static str, CodeError> {
59	VSCODE_CLI_UPDATE_ENDPOINT.ok_or_else(|| CodeError::UpdatesNotConfigured("no service url"))
60}
61
62impl UpdateService {
63	pub fn new(log: log::Logger, http: BoxedHttp) -> Self {
64		UpdateService { client: http, log }
65	}
66
67	pub async fn get_release_by_semver_version(
68		&self,
69		platform: Platform,
70		target: TargetKind,
71		quality: options::Quality,
72		version: &str,
73	) -> Result<Release, AnyError> {
74		let update_endpoint = get_update_endpoint()?;
75		let download_segment = target
76			.download_segment(platform)
77			.ok_or_else(|| CodeError::UnsupportedPlatform(platform.to_string()))?;
78		let download_url = format!(
79			"{}/api/versions/{}/{}/{}",
80			update_endpoint,
81			version,
82			download_segment,
83			quality_download_segment(quality),
84		);
85
86		let mut response = spanf!(
87			self.log,
88			self.log.span("server.version.resolve"),
89			self.client.make_request("GET", download_url)
90		)?;
91
92		if !response.status_code.is_success() {
93			return Err(response.into_err().await.into());
94		}
95
96		let res = response.json::<UpdateServerVersion>().await?;
97		debug!(self.log, "Resolved version {} to {}", version, res.version);
98
99		Ok(Release {
100			target,
101			platform,
102			quality,
103			name: res.name,
104			commit: res.version,
105		})
106	}
107
108	/// Gets the latest commit for the target of the given quality.
109	pub async fn get_latest_commit(
110		&self,
111		platform: Platform,
112		target: TargetKind,
113		quality: options::Quality,
114	) -> Result<Release, AnyError> {
115		let update_endpoint = get_update_endpoint()?;
116		let download_segment = target
117			.download_segment(platform)
118			.ok_or_else(|| CodeError::UnsupportedPlatform(platform.to_string()))?;
119		let download_url = format!(
120			"{}/api/latest/{}/{}",
121			update_endpoint,
122			download_segment,
123			quality_download_segment(quality),
124		);
125
126		let mut response = spanf!(
127			self.log,
128			self.log.span("server.version.resolve"),
129			self.client.make_request("GET", download_url)
130		)?;
131
132		if !response.status_code.is_success() {
133			return Err(response.into_err().await.into());
134		}
135
136		let res = response.json::<UpdateServerVersion>().await?;
137		debug!(self.log, "Resolved quality {} to {}", quality, res.version);
138
139		Ok(Release {
140			target,
141			platform,
142			quality,
143			name: res.name,
144			commit: res.version,
145		})
146	}
147
148	/// Gets the download stream for the release.
149	pub async fn get_download_stream(&self, release: &Release) -> Result<SimpleResponse, AnyError> {
150		let update_endpoint = get_update_endpoint()?;
151		let download_segment = release
152			.target
153			.download_segment(release.platform)
154			.ok_or_else(|| CodeError::UnsupportedPlatform(release.platform.to_string()))?;
155
156		let download_url = format!(
157			"{}/commit:{}/{}/{}",
158			update_endpoint,
159			release.commit,
160			download_segment,
161			quality_download_segment(release.quality),
162		);
163
164		let response = self.client.make_request("GET", download_url).await?;
165		if !response.status_code.is_success() {
166			return Err(response.into_err().await.into());
167		}
168
169		Ok(response)
170	}
171}
172
173pub fn unzip_downloaded_release<T>(
174	compressed_file: &Path,
175	target_dir: &Path,
176	reporter: T,
177) -> Result<(), WrappedError>
178where
179	T: ReportCopyProgress,
180{
181	if compressed_file.extension() == Some(OsStr::new("zip")) {
182		zipper::unzip_file(compressed_file, target_dir, reporter)
183	} else {
184		tar::decompress_tarball(compressed_file, target_dir, reporter)
185	}
186}
187
188#[derive(Eq, PartialEq, Copy, Clone)]
189pub enum TargetKind {
190	Server,
191	Archive,
192	Web,
193	Cli,
194}
195
196impl TargetKind {
197	fn download_segment(&self, platform: Platform) -> Option<String> {
198		match *self {
199			TargetKind::Server => Some(platform.headless()),
200			TargetKind::Archive => platform.archive(),
201			TargetKind::Web => Some(platform.web()),
202			TargetKind::Cli => Some(platform.cli()),
203		}
204	}
205}
206
207#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
208pub enum Platform {
209	LinuxAlpineX64,
210	LinuxAlpineARM64,
211	LinuxX64,
212	LinuxARM64,
213	LinuxARM32,
214	DarwinX64,
215	DarwinARM64,
216	WindowsX64,
217	WindowsX86,
218	WindowsARM64,
219}
220
221impl Platform {
222	pub fn archive(&self) -> Option<String> {
223		match self {
224			Platform::LinuxX64 => Some("linux-x64".to_owned()),
225			Platform::LinuxARM64 => Some("linux-arm64".to_owned()),
226			Platform::LinuxARM32 => Some("linux-armhf".to_owned()),
227			Platform::DarwinX64 => Some("darwin".to_owned()),
228			Platform::DarwinARM64 => Some("darwin-arm64".to_owned()),
229			Platform::WindowsX64 => Some("win32-x64-archive".to_owned()),
230			Platform::WindowsX86 => Some("win32-archive".to_owned()),
231			Platform::WindowsARM64 => Some("win32-arm64-archive".to_owned()),
232			_ => None,
233		}
234	}
235	pub fn headless(&self) -> String {
236		match self {
237			Platform::LinuxAlpineARM64 => "server-alpine-arm64",
238			Platform::LinuxAlpineX64 => "server-linux-alpine",
239			Platform::LinuxX64 => "server-linux-x64",
240			Platform::LinuxARM64 => "server-linux-arm64",
241			Platform::LinuxARM32 => "server-linux-armhf",
242			Platform::DarwinX64 => "server-darwin",
243			Platform::DarwinARM64 => "server-darwin-arm64",
244			Platform::WindowsX64 => "server-win32-x64",
245			Platform::WindowsX86 => "server-win32",
246			Platform::WindowsARM64 => "server-win32-x64", // we don't publish an arm64 server build yet
247		}
248		.to_owned()
249	}
250
251	pub fn cli(&self) -> String {
252		match self {
253			Platform::LinuxAlpineARM64 => "cli-alpine-arm64",
254			Platform::LinuxAlpineX64 => "cli-alpine-x64",
255			Platform::LinuxX64 => "cli-linux-x64",
256			Platform::LinuxARM64 => "cli-linux-arm64",
257			Platform::LinuxARM32 => "cli-linux-armhf",
258			Platform::DarwinX64 => "cli-darwin-x64",
259			Platform::DarwinARM64 => "cli-darwin-arm64",
260			Platform::WindowsARM64 => "cli-win32-arm64",
261			Platform::WindowsX64 => "cli-win32-x64",
262			Platform::WindowsX86 => "cli-win32",
263		}
264		.to_owned()
265	}
266
267	pub fn web(&self) -> String {
268		format!("{}-web", self.headless())
269	}
270
271	pub fn env_default() -> Option<Platform> {
272		if cfg!(all(
273			target_os = "linux",
274			target_arch = "x86_64",
275			target_env = "musl"
276		)) {
277			Some(Platform::LinuxAlpineX64)
278		} else if cfg!(all(
279			target_os = "linux",
280			target_arch = "aarch64",
281			target_env = "musl"
282		)) {
283			Some(Platform::LinuxAlpineARM64)
284		} else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
285			Some(Platform::LinuxX64)
286		} else if cfg!(all(target_os = "linux", target_arch = "arm")) {
287			Some(Platform::LinuxARM32)
288		} else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
289			Some(Platform::LinuxARM64)
290		} else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
291			Some(Platform::DarwinX64)
292		} else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
293			Some(Platform::DarwinARM64)
294		} else if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
295			Some(Platform::WindowsX64)
296		} else if cfg!(all(target_os = "windows", target_arch = "x86")) {
297			Some(Platform::WindowsX86)
298		} else if cfg!(all(target_os = "windows", target_arch = "aarch64")) {
299			Some(Platform::WindowsARM64)
300		} else {
301			None
302		}
303	}
304}
305
306impl fmt::Display for Platform {
307	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
308		f.write_str(match self {
309			Platform::LinuxAlpineARM64 => "LinuxAlpineARM64",
310			Platform::LinuxAlpineX64 => "LinuxAlpineX64",
311			Platform::LinuxX64 => "LinuxX64",
312			Platform::LinuxARM64 => "LinuxARM64",
313			Platform::LinuxARM32 => "LinuxARM32",
314			Platform::DarwinX64 => "DarwinX64",
315			Platform::DarwinARM64 => "DarwinARM64",
316			Platform::WindowsX64 => "WindowsX64",
317			Platform::WindowsX86 => "WindowsX86",
318			Platform::WindowsARM64 => "WindowsARM64",
319		})
320	}
321}