1use 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 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 pub fn is_up_to_date_with(&self, release: &Release) -> bool {
62 release.commit == self.commit
63 }
64
65 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 pub async fn do_update(
79 &self,
80 release: &Release,
81 progress: impl ReportCopyProgress,
82 ) -> Result<(), AnyError> {
83 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 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 unzip_downloaded_release(&archive_path, &archive_contents_path, SilentCopyProgress())?;
96 copy_updated_cli_to_path(&archive_contents_path, &staging_path)?;
97
98 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 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 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}