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 StartupFusAssistantTimeout,
29 DoubleStartupFusAssistant,
30}
31
32impl From<io::Error> for UpdateError {
33 fn from(err: io::Error) -> UpdateError {
34 UpdateError::Io(err)
35 }
36}
37
38impl From<JsonError> for UpdateError {
39 fn from(err: JsonError) -> UpdateError {
40 UpdateError::Json(err)
41 }
42}
43
44impl JetBrainsToolboxInstallation {
45 fn update_all_channels<F>(&self, mut operation: F) -> Result<(), UpdateError>
46 where
47 F: FnMut(&PathBuf, &mut JsonValue) -> Result<(), UpdateError>,
48 {
49 for file in fs::read_dir(&self.channels)? {
50 let file = file?;
51 self.update_channel(file.path(), &mut operation)?;
52 }
53 Ok(())
54 }
55
56 fn update_channel<F>(&self, path: PathBuf, operation: &mut F) -> Result<(), UpdateError>
57 where
58 F: FnMut(&PathBuf, &mut JsonValue) -> Result<(), UpdateError>,
59 {
60 let mut file = File::options().read(true).write(true).open(&path)?;
61 let mut buf = String::new();
62 file.read_to_string(&mut buf)?;
63 let mut data = json::parse(&buf)?;
64 operation(&path, &mut data)?;
65 file.seek(SeekFrom::Start(0))?; buf = data.dump();
68 file.write_all(buf.as_bytes())?; let current_position = file.stream_position()?;
70 file.set_len(current_position)?; Ok(())
73 }
74
75 fn start_minimized(&self) -> io::Result<Child> {
76 Command::new(&self.binary).arg("--minimize").spawn()
77 }
78}
79
80#[derive(Debug, Clone)]
81#[non_exhaustive]
82pub enum FindError {
83 NotFound,
84 InvalidInstallation,
85 NoHomeDir,
86 UnsupportedOS(String),
87}
88
89#[cfg(target_os = "linux")]
90pub fn find_jetbrains_toolbox() -> Result<JetBrainsToolboxInstallation, FindError> {
91 let home_dir = home_dir().ok_or(FindError::NoHomeDir)?;
92 let dir = home_dir.join(".local/share/JetBrains/Toolbox");
94 if !dir.exists() {
95 return Err(FindError::NotFound);
96 } else if !dir.is_dir() {
97 return Err(FindError::InvalidInstallation);
99 }
100 let binary = dir.join("bin/jetbrains-toolbox");
101 if !binary.exists() {
102 return Err(FindError::InvalidInstallation);
103 }
104 let channels = dir.join("channels");
105 if !channels.is_dir() {
106 return Err(FindError::InvalidInstallation);
107 }
108 let logs_dir = dir.join("logs");
109 if !logs_dir.is_dir() {
110 return Err(FindError::InvalidInstallation);
111 }
112 let log = logs_dir.join("toolbox.latest.log");
115
116 Ok(JetBrainsToolboxInstallation {
117 binary,
118 channels,
119 log,
120 })
121}
122
123#[cfg(target_os = "windows")]
124pub fn find_jetbrains_toolbox() -> Result<JetBrainsToolboxInstallation, FindError> {
125 Err(FindError::UnsupportedOS("Windows".to_string())) }
127
128#[cfg(target_os = "macos")]
129pub fn find_jetbrains_toolbox() -> Result<JetBrainsToolboxInstallation, FindError> {
130 Err(FindError::UnsupportedOS("MacOS".to_string())) }
132
133#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
134pub fn find_jetbrains_toolbox() -> Result<JetBrainsToolboxInstallation, FindError> {
135 Err(FindError::UnsupportedOS(std::env::consts::OS.to_string()))
137}
138
139fn kill_all() -> Result<bool, UpdateError> {
141 let mut sys = System::new_all();
142 sys.refresh_all();
143 let processes = sys
145 .processes()
146 .values()
147 .filter_map(|p| {
148 let exe = p.exe()?; let name = p.name();
150 match exe.file_name().ok_or(UpdateError::CouldNotTerminate(
151 "Error getting file_name".to_string(),
152 )) {
153 Ok(file_name)
161 if file_name == "jetbrains-toolbox"
162 && name.to_str()?.starts_with("jetbrains") =>
163 {
164 Some(Ok(p))
165 }
166 Ok(_) => None, Err(e) => Some(Err(e)), }
169 })
170 .collect::<Result<Vec<&Process>, UpdateError>>()?;
171 Ok(match processes.len() {
172 0 => false, _ => {
174 for process in processes {
175 process.kill();
176 process.wait();
177 }
178 true }
180 })
181}
182
183pub fn update_jetbrains_toolbox(
184 installation: JetBrainsToolboxInstallation,
185) -> Result<(), UpdateError> {
186 _update_jetbrains_toolbox::<false>(installation)
187}
188
189fn _update_jetbrains_toolbox<const IS_RECURSIVE: bool>(
190 installation: JetBrainsToolboxInstallation,
191) -> Result<(), UpdateError> {
192 println!("Killing Toolbox");
194 let toolbox_was_open = kill_all()?;
195
196 let skipped_channels = change_config(&installation)?;
198
199 let redo = match actual_update(&installation) {
200 Err(e) => {
201 println!("Unexpected error encountered, resetting configuration to previous state");
202 reset_config(&installation, skipped_channels)?;
203 return Err(e);
204 }
205 Ok(redo) => redo,
206 };
207
208 reset_config(&installation, skipped_channels)?;
210
211 if toolbox_was_open {
213 println!("Re-opening Toolbox");
214 installation.start_minimized()?;
215 }
216
217 if redo {
218 if IS_RECURSIVE {
224 return Err(UpdateError::DoubleToolboxSelfUpdate);
226 }
227
228 _update_jetbrains_toolbox::<true>(installation)
229 } else {
230 Ok(())
231 }
232}
233
234fn actual_update(installation: &JetBrainsToolboxInstallation) -> Result<bool, UpdateError> {
236 let mut redo = false;
237
238 println!("Starting Toolbox");
240 installation.start_minimized()?;
241
242 let mut updates: u32 = 0;
244 let mut correct_checksums_expected: u32 = 0;
245 let start_time = Instant::now();
246 let mut startup_time = None;
247
248 let file = File::open(&installation.log)?;
249 let mut file = BufReader::new(file);
250 file.seek(SeekFrom::End(0))?;
251 loop {
252 if startup_time.is_none() && start_time + Duration::from_secs(60) < Instant::now() {
255 return Err(UpdateError::StartupFusAssistantTimeout);
256 }
257
258 if let Some(startup_time) = startup_time {
259 if updates == 0 && startup_time + Duration::from_secs(10) < Instant::now() {
261 println!("No updates found");
262 break;
263 }
264 }
265
266 let curr_position = file.stream_position()?;
267
268 let mut line = String::new();
270 file.read_line(&mut line)?;
271
272 if line.is_empty() {
273 file.seek(SeekFrom::Start(curr_position))?;
276 sleep(Duration::from_millis(100));
277 } else {
278 if line.contains("Correct checksum for") || line.contains("Downloading from") {
286 if line.contains("Correct checksum for") && correct_checksums_expected > 0 {
287 println!("Verified a checksum for an update that was started earlier");
288 correct_checksums_expected -= 1;
289 continue;
290 }
291 println!("Found an update, waiting until it finishes");
293 updates += 1;
294 if line.contains("Downloading from") {
295 correct_checksums_expected += 1;
297 }
298 } else if line.contains("Show notification") {
299 updates -= 1;
301 if updates == 0 {
302 println!("All updates finished, exiting in 2 seconds");
303 sleep(Duration::from_secs(2)); break;
305 } else {
306 println!("Update finished, waiting for other update(s) to finish")
307 }
308 } else if line.contains(
309 "Shutting down. Reason: The updated app is starting, closing the current process",
310 ) {
311 redo = true;
314 println!(
315 "Toolbox (self-)update finished, update process will restart in 10 seconds"
316 );
317 sleep(Duration::from_secs(10));
323 break;
324 } else if line.contains("Downloaded fus-assistant.xml") {
325 if startup_time.is_some() {
326 return Err(UpdateError::DoubleStartupFusAssistant);
328 }
329 println!("Toolbox started");
330 startup_time = Some(Instant::now())
331 }
332 }
333 }
334
335 println!("Killing Toolbox");
337 if !kill_all()? {
338 return Err(UpdateError::PrematureExit);
340 }
341
342 Ok(redo)
343}
344
345fn change_config(installation: &JetBrainsToolboxInstallation) -> Result<Vec<PathBuf>, UpdateError> {
346 let mut skipped_channels = vec![];
347 installation.update_all_channels(|channel, d| {
348 if !d.has_key("channel") {
349 return Err(UpdateError::InvalidChannel);
350 }
351 if d["channel"].has_key("autoUpdate") {
352 if d["channel"]["autoUpdate"] == true {
353 skipped_channels.push(channel.clone());
355 return Ok(());
356 } else {
357 return Err(UpdateError::InvalidChannel);
359 }
360 }
361
362 d["channel"]["autoUpdate"] = true.into();
363 Ok(())
364 })?;
365 Ok(skipped_channels)
366}
367
368fn reset_config(
369 installation: &JetBrainsToolboxInstallation,
370 skipped_channels: Vec<PathBuf>,
371) -> Result<(), UpdateError> {
372 installation.update_all_channels(|channel, d| {
373 if !d.has_key("channel") {
374 return Err(UpdateError::InvalidChannel);
375 }
376 if skipped_channels.contains(channel) {
377 return Ok(());
379 }
380 d["channel"].remove("autoUpdate");
381 Ok(())
382 })?;
383 Ok(())
384}