cli/
self_update.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::{fs, path::Path};
7use tempfile::tempdir;
8
9use crate::{
10	constants::{VSCODE_CLI_COMMIT, VSCODE_CLI_QUALITY},
11	options::Quality,
12	update_service::{unzip_downloaded_release, Platform, Release, TargetKind, UpdateService},
13	util::{
14		command::new_std_command,
15		errors::{wrap, AnyError, CodeError, CorruptDownload},
16		http,
17		io::{ReportCopyProgress, SilentCopyProgress},
18	},
19};
20
21pub struct SelfUpdate<'a> {
22	commit: &'static str,
23	quality: Quality,
24	platform: Platform,
25	update_service: &'a UpdateService,
26}
27
28static OLD_UPDATE_EXTENSION: &str = "Updating CLI";
29
30impl<'a> SelfUpdate<'a> {
31	pub fn new(update_service: &'a UpdateService) -> Result<Self, AnyError> {
32		let commit = VSCODE_CLI_COMMIT
33			.ok_or_else(|| CodeError::UpdatesNotConfigured("unknown build commit"))?;
34
35		let quality = VSCODE_CLI_QUALITY
36			.ok_or_else(|| CodeError::UpdatesNotConfigured("no configured quality"))
37			.and_then(|q| {
38				Quality::try_from(q).map_err(|_| CodeError::UpdatesNotConfigured("unknown quality"))
39			})?;
40
41		let platform = Platform::env_default().ok_or_else(|| {
42			CodeError::UpdatesNotConfigured("Unknown platform, please report this error")
43		})?;
44
45		Ok(Self {
46			commit,
47			quality,
48			platform,
49			update_service,
50		})
51	}
52
53	/// Gets the current release
54	pub async fn get_current_release(&self) -> Result<Release, AnyError> {
55		self.update_service
56			.get_latest_commit(self.platform, TargetKind::Cli, self.quality)
57			.await
58	}
59
60	/// Gets whether the given release is what this CLI is built against
61	pub fn is_up_to_date_with(&self, release: &Release) -> bool {
62		release.commit == self.commit
63	}
64
65	/// Cleans up old self-updated binaries. Should be called with regularity.
66	/// May fail if old versions are still running.
67	pub fn cleanup_old_update(&self) -> Result<(), std::io::Error> {
68		let current_path = std::env::current_exe()?;
69		let old_path = current_path.with_extension(OLD_UPDATE_EXTENSION);
70		if old_path.exists() {
71			fs::remove_file(old_path)?;
72		}
73
74		Ok(())
75	}
76
77	/// Updates the CLI to the given release.
78	pub async fn do_update(
79		&self,
80		release: &Release,
81		progress: impl ReportCopyProgress,
82	) -> Result<(), AnyError> {
83		// 1. Download the archive into a temporary directory
84		let tempdir = tempdir().map_err(|e| wrap(e, "Failed to create temp dir"))?;
85		let stream = self.update_service.get_download_stream(release).await?;
86		let archive_path = tempdir.path().join(stream.url_path_basename().unwrap());
87		http::download_into_file(&archive_path, progress, stream).await?;
88
89		// 2. Unzip the archive and get the binary
90		let target_path =
91			std::env::current_exe().map_err(|e| wrap(e, "could not get current exe"))?;
92		let staging_path = target_path.with_extension(".update");
93		let archive_contents_path = tempdir.path().join("content");
94		// unzipping the single binary is pretty small and fast--don't bother with passing progress
95		unzip_downloaded_release(&archive_path, &archive_contents_path, SilentCopyProgress())?;
96		copy_updated_cli_to_path(&archive_contents_path, &staging_path)?;
97
98		// 3. Copy file metadata, make sure the new binary is executable\
99		copy_file_metadata(&target_path, &staging_path)
100			.map_err(|e| wrap(e, "failed to set file permissions"))?;
101		validate_cli_is_good(&staging_path)?;
102
103		// Try to rename the old CLI to the tempdir, where it can get cleaned up by the
104		// OS later. However, this can fail if the tempdir is on a different drive
105		// than the installation dir. In this case just rename it to ".old".
106		if fs::rename(&target_path, tempdir.path().join("old-code-cli")).is_err() {
107			fs::rename(
108				&target_path,
109				target_path.with_extension(OLD_UPDATE_EXTENSION),
110			)
111			.map_err(|e| wrap(e, "failed to rename old CLI"))?;
112		}
113
114		fs::rename(&staging_path, &target_path)
115			.map_err(|e| wrap(e, "failed to rename newly installed CLI"))?;
116
117		Ok(())
118	}
119}
120
121fn validate_cli_is_good(exe_path: &Path) -> Result<(), AnyError> {
122	let o = new_std_command(exe_path)
123		.args(["--version"])
124		.output()
125		.map_err(|e| CorruptDownload(format!("could not execute new binary, aborting: {}", e)))?;
126
127	if !o.status.success() {
128		let msg = format!(
129			"could not execute new binary, aborting. Stdout:\n\n{}\n\nStderr:\n\n{}",
130			String::from_utf8_lossy(&o.stdout),
131			String::from_utf8_lossy(&o.stderr),
132		);
133
134		return Err(CorruptDownload(msg).into());
135	}
136
137	Ok(())
138}
139
140fn copy_updated_cli_to_path(unzipped_content: &Path, staging_path: &Path) -> Result<(), AnyError> {
141	let unzipped_files = fs::read_dir(unzipped_content)
142		.map_err(|e| wrap(e, "could not read update contents"))?
143		.collect::<Vec<_>>();
144	if unzipped_files.len() != 1 {
145		let msg = format!(
146			"expected exactly one file in update, got {}",
147			unzipped_files.len()
148		);
149		return Err(CorruptDownload(msg).into());
150	}
151
152	let archive_file = unzipped_files[0]
153		.as_ref()
154		.map_err(|e| wrap(e, "error listing update files"))?;
155	fs::copy(archive_file.path(), staging_path)
156		.map_err(|e| wrap(e, "error copying to staging file"))?;
157	Ok(())
158}
159
160#[cfg(target_os = "windows")]
161fn copy_file_metadata(from: &Path, to: &Path) -> Result<(), std::io::Error> {
162	let permissions = from.metadata()?.permissions();
163	fs::set_permissions(to, permissions)?;
164	Ok(())
165}
166
167#[cfg(not(target_os = "windows"))]
168fn copy_file_metadata(from: &Path, to: &Path) -> Result<(), std::io::Error> {
169	use std::os::unix::ffi::OsStrExt;
170	use std::os::unix::fs::MetadataExt;
171
172	let metadata = from.metadata()?;
173	fs::set_permissions(to, metadata.permissions())?;
174
175	// based on coreutils' chown https://github.com/uutils/coreutils/blob/72b4629916abe0852ad27286f4e307fbca546b6e/src/chown/chown.rs#L266-L281
176	let s = std::ffi::CString::new(to.as_os_str().as_bytes()).unwrap();
177	let ret = unsafe { libc::chown(s.as_ptr(), metadata.uid(), metadata.gid()) };
178	if ret != 0 {
179		return Err(std::io::Error::last_os_error());
180	}
181
182	Ok(())
183}