jetbrains_toolbox_updater/
lib.rs1use dirs::home_dir;
2use json::{JsonError, JsonValue};
3use std::fs::File;
4use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
5use std::path::PathBuf;
6use std::process::{Child, Command};
7use std::thread::sleep;
8use std::time::{Duration, Instant};
9use std::{fs, io};
10use sysinfo::{Process, System};
11
12#[derive(Debug, Clone)]
13pub struct JetBrainsToolboxInstallation {
14 binary: PathBuf,
15 channels: PathBuf, log: PathBuf,
17}
18
19#[derive(Debug)]
20#[non_exhaustive]
21pub enum UpdateError {
22 Io(io::Error),
23 Json(JsonError),
24 InvalidChannel,
25 CouldNotTerminate(String),
26 PrematureExit,
27 DoubleToolboxSelfUpdate,
28}
29
30impl From<io::Error> for UpdateError {
31 fn from(err: io::Error) -> UpdateError {
32 UpdateError::Io(err)
33 }
34}
35
36impl From<JsonError> for UpdateError {
37 fn from(err: JsonError) -> UpdateError {
38 UpdateError::Json(err)
39 }
40}
41
42impl JetBrainsToolboxInstallation {
43 fn update_all_channels<F>(&self, mut operation: F) -> Result<(), UpdateError>
44 where
45 F: FnMut(&PathBuf, &mut JsonValue) -> Result<(), UpdateError>,
46 {
47 for file in fs::read_dir(&self.channels)? {
48 let file = file?;
49 self.update_channel(file.path(), &mut operation)?;
50 }
51 Ok(())
52 }
53
54 fn update_channel<F>(&self, path: PathBuf, operation: &mut F) -> Result<(), UpdateError>
55 where
56 F: FnMut(&PathBuf, &mut JsonValue) -> Result<(), UpdateError>,
57 {
58 let mut file = File::options().read(true).write(true).open(&path)?;
59 let mut buf = String::new();
60 file.read_to_string(&mut buf)?;
61 let mut data = json::parse(&buf)?;
62 operation(&path, &mut data)?;
63 file.seek(SeekFrom::Start(0))?; buf = data.dump();
66 file.write_all(buf.as_bytes())?; let current_position = file.stream_position()?;
68 file.set_len(current_position)?; Ok(())
71 }
72
73 fn start_minimized(&self) -> io::Result<Child> {
74 Command::new(&self.binary).arg("--minimize").spawn()
75 }
76}
77
78#[derive(Debug, Clone)]
79#[non_exhaustive]
80pub enum FindError {
81 NotFound,
82 InvalidInstallation,
83 NoHomeDir,
84 UnsupportedOS(String),
85}
86
87#[cfg(target_os = "linux")]
88pub fn find_jetbrains_toolbox() -> Result<JetBrainsToolboxInstallation, FindError> {
89 let home_dir = home_dir().ok_or(FindError::NoHomeDir)?;
90 let dir = home_dir.join(".local/share/JetBrains/Toolbox");
91 if !dir.exists() {
92 return Err(FindError::NotFound);
93 } else if !dir.is_dir() {
94 return Err(FindError::InvalidInstallation);
96 }
97 let binary = dir.join("bin/jetbrains-toolbox");
98 if !binary.exists() {
99 return Err(FindError::InvalidInstallation);
100 }
101 let channels = dir.join("channels");
102 if !channels.is_dir() {
103 return Err(FindError::InvalidInstallation);
104 }
105 let logs_dir = dir.join("logs");
106 if !logs_dir.is_dir() {
107 return Err(FindError::InvalidInstallation);
108 }
109 let log = logs_dir.join("toolbox.log"); Ok(JetBrainsToolboxInstallation {
112 binary,
113 channels,
114 log,
115 })
116}
117
118#[cfg(target_os = "windows")]
119pub fn find_jetbrains_toolbox() -> Result<JetBrainsToolboxInstallation, FindError> {
120 Err(FindError::UnsupportedOS("Windows".to_string())) }
122
123#[cfg(target_os = "macos")]
124pub fn find_jetbrains_toolbox() -> Result<JetBrainsToolboxInstallation, FindError> {
125 Err(FindError::UnsupportedOS("MacOS".to_string())) }
127
128#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
129pub fn find_jetbrains_toolbox() -> Result<JetBrainsToolboxInstallation, FindError> {
130 Err(FindError::UnsupportedOS(std::env::consts::OS.to_string()))
132}
133
134fn kill_all() -> Result<bool, UpdateError> {
136 let mut sys = System::new_all();
137 sys.refresh_all();
138 let processes = sys
140 .processes()
141 .values()
142 .filter_map(|p| {
143 let exe = p.exe()?; let name = p.name();
145 match exe.file_name().ok_or(UpdateError::CouldNotTerminate(
146 "Error getting file_name".to_string(),
147 )) {
148 Ok(file_name)
156 if file_name == "jetbrains-toolbox"
157 && name.to_str()?.starts_with("jetbrains") =>
158 {
159 Some(Ok(p))
160 }
161 Ok(_) => None, Err(e) => Some(Err(e)), }
164 })
165 .collect::<Result<Vec<&Process>, UpdateError>>()?;
166 Ok(match processes.len() {
167 0 => false, _ => {
169 for process in processes {
170 process.kill();
171 process.wait();
172 }
173 true }
175 })
176}
177
178pub fn update_jetbrains_toolbox(
179 installation: JetBrainsToolboxInstallation,
180) -> Result<(), UpdateError> {
181 _update_jetbrains_toolbox::<false>(installation)
182}
183
184fn _update_jetbrains_toolbox<const IS_RECURSIVE: bool>(
185 installation: JetBrainsToolboxInstallation,
186) -> Result<(), UpdateError> {
187 let toolbox_was_open = kill_all()?;
189
190 let skipped_channels = change_config(&installation)?;
192
193 let redo = match actual_update(&installation) {
194 Err(e) => {
195 println!("Unexpected error encountered, resetting configuration to previous state");
196 reset_config(&installation, skipped_channels)?;
197 return Err(e);
198 }
199 Ok(redo) => redo,
200 };
201
202 reset_config(&installation, skipped_channels)?;
204
205 if toolbox_was_open {
207 installation.start_minimized()?;
208 }
209
210 if redo {
211 if IS_RECURSIVE {
217 return Err(UpdateError::DoubleToolboxSelfUpdate);
219 }
220
221 _update_jetbrains_toolbox::<true>(installation)
222 } else {
223 Ok(())
224 }
225}
226
227fn actual_update(installation: &JetBrainsToolboxInstallation) -> Result<bool, UpdateError> {
229 let mut redo = false;
230
231 installation.start_minimized()?;
233
234 let mut updates: u32 = 0;
236 let mut correct_checksums_expected: u32 = 0;
237 let start_time = Instant::now();
238
239 let file = File::open(&installation.log)?;
240 let mut file = BufReader::new(file);
241 file.seek(SeekFrom::End(0))?;
242 loop {
243 if updates == 0 && start_time + Duration::from_secs(10) < Instant::now() {
246 println!("No updates found.");
247 break;
248 }
249
250 let curr_position = file.stream_position()?;
251
252 let mut line = String::new();
254 file.read_line(&mut line)?;
255
256 if line.is_empty() {
257 file.seek(SeekFrom::Start(curr_position))?;
260 sleep(Duration::from_millis(100));
261 } else {
262 if line.contains("Correct checksum for") || line.contains("Downloading from") {
268 if line.contains("Correct checksum for") && correct_checksums_expected > 0 {
269 correct_checksums_expected -= 1;
270 continue;
271 }
272 println!("Found an update, waiting until it finishes...");
274 updates += 1;
275 if line.contains("Downloading from") {
276 correct_checksums_expected += 1;
278 }
279 } else if line.contains("Show notification") {
280 updates -= 1;
282 if updates == 0 {
283 println!("Update finished, exiting...");
284 sleep(Duration::from_secs(2)); break;
286 } else {
287 println!("Update finished, waiting for other update(s) to finish")
288 }
289 } else if line.contains(
290 "Shutting down. Reason: The updated app is starting, closing the current process",
291 ) {
292 redo = true;
295 println!("Toolbox (self-)update finished.");
296 sleep(Duration::from_secs(2)); break;
298 }
299 }
300 }
301
302 if !kill_all()? {
304 return Err(UpdateError::PrematureExit);
306 }
307
308 Ok(redo)
309}
310
311fn change_config(installation: &JetBrainsToolboxInstallation) -> Result<Vec<PathBuf>, UpdateError> {
312 let mut skipped_channels = vec![];
313 installation.update_all_channels(|channel, d| {
314 if !d.has_key("channel") {
315 return Err(UpdateError::InvalidChannel);
316 }
317 if d["channel"].has_key("autoUpdate") {
318 if d["channel"]["autoUpdate"] == true {
319 skipped_channels.push(channel.clone());
321 return Ok(());
322 } else {
323 return Err(UpdateError::InvalidChannel);
325 }
326 }
327
328 d["channel"]["autoUpdate"] = true.into();
329 Ok(())
330 })?;
331 Ok(skipped_channels)
332}
333
334fn reset_config(
335 installation: &JetBrainsToolboxInstallation,
336 skipped_channels: Vec<PathBuf>,
337) -> Result<(), UpdateError> {
338 installation.update_all_channels(|channel, d| {
339 if !d.has_key("channel") {
340 return Err(UpdateError::InvalidChannel);
341 }
342 if skipped_channels.contains(channel) {
343 return Ok(());
345 }
346 d["channel"].remove("autoUpdate");
347 Ok(())
348 })?;
349 Ok(())
350}