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