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)]
20pub enum UpdateError {
21 Io(io::Error),
22 Json(JsonError),
23 InvalidChannel,
24 CouldNotTerminate(String),
25 PrematureExit,
26 DoubleToolboxSelfUpdate,
27}
28
29impl From<io::Error> for UpdateError {
30 fn from(err: io::Error) -> UpdateError {
31 UpdateError::Io(err)
32 }
33}
34
35impl From<JsonError> for UpdateError {
36 fn from(err: JsonError) -> UpdateError {
37 UpdateError::Json(err)
38 }
39}
40
41impl JetBrainsToolboxInstallation {
42 fn update_all_channels<F>(&self, mut operation: F) -> Result<(), UpdateError>
43 where
44 F: FnMut(&PathBuf, &mut JsonValue) -> Result<(), UpdateError>,
45 {
46 for file in fs::read_dir(&self.channels)? {
47 let file = file?;
48 self.update_channel(file.path(), &mut operation)?;
49 }
50 Ok(())
51 }
52
53 fn update_channel<F>(&self, path: PathBuf, operation: &mut F) -> Result<(), UpdateError>
54 where
55 F: FnMut(&PathBuf, &mut JsonValue) -> Result<(), UpdateError>,
56 {
57 let mut file = File::options().read(true).write(true).open(&path)?;
58 let mut buf = String::new();
59 file.read_to_string(&mut buf)?;
60 let mut data = json::parse(&buf)?;
61 operation(&path, &mut data)?;
62 file.seek(SeekFrom::Start(0))?; buf = data.dump();
65 file.write_all(buf.as_bytes())?; let current_position = file.stream_position()?;
67 file.set_len(current_position)?; Ok(())
70 }
71
72 fn start_minimized(&self) -> io::Result<Child> {
73 Command::new(&self.binary).arg("--minimize").spawn()
74 }
75}
76
77#[derive(Debug, Clone)]
78pub enum FindError {
79 NotFound,
80 InvalidInstallation,
81 NoHomeDir,
82 UnsupportedOS(String),
83}
84
85#[cfg(target_os = "linux")]
86pub fn find_jetbrains_toolbox() -> Result<JetBrainsToolboxInstallation, FindError> {
87 let home_dir = home_dir().ok_or(FindError::NoHomeDir)?;
88 let dir = home_dir.join(".local/share/JetBrains/Toolbox");
89 if !dir.exists() {
90 return Err(FindError::NotFound);
91 } else if !dir.is_dir() {
92 return Err(FindError::InvalidInstallation);
94 }
95 let binary = dir.join("bin/jetbrains-toolbox");
96 if !binary.exists() {
97 return Err(FindError::InvalidInstallation);
98 }
99 let channels = dir.join("channels");
100 if !channels.is_dir() {
101 return Err(FindError::InvalidInstallation);
102 }
103 let logs_dir = dir.join("logs");
104 if !logs_dir.is_dir() {
105 return Err(FindError::InvalidInstallation);
106 }
107 let log = logs_dir.join("toolbox.log"); Ok(JetBrainsToolboxInstallation {
110 binary,
111 channels,
112 log,
113 })
114}
115
116#[cfg(target_os = "windows")]
117pub fn find_jetbrains_toolbox() -> Result<JetBrainsToolboxInstallation, FindError> {
118 Err(FindError::UnsupportedOS("Windows".to_string())) }
120
121#[cfg(target_os = "macos")]
122pub fn find_jetbrains_toolbox() -> Result<JetBrainsToolboxInstallation, FindError> {
123 Err(FindError::UnsupportedOS("MacOS".to_string())) }
125
126#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
127pub fn find_jetbrains_toolbox() -> Result<JetBrainsToolboxInstallation, FindError> {
128 Err(FindError::UnsupportedOS(std::env::consts::OS.to_string()))
130}
131
132fn kill_all() -> Result<bool, UpdateError> {
134 let mut sys = System::new_all();
135 sys.refresh_all();
136 let processes = sys
138 .processes()
139 .values()
140 .filter_map(|p| {
141 let exe = p.exe()?; let name = p.name();
143 match exe.file_name().ok_or(UpdateError::CouldNotTerminate(
144 "Error getting file_name".to_string(),
145 )) {
146 Ok(file_name)
154 if file_name == "jetbrains-toolbox"
155 && name.to_str()?.starts_with("jetbrains") =>
156 {
157 Some(Ok(p))
158 }
159 Ok(_) => None, Err(e) => Some(Err(e)), }
162 })
163 .collect::<Result<Vec<&Process>, UpdateError>>()?;
164 Ok(match processes.len() {
165 0 => false, _ => {
167 for process in processes {
168 process.kill();
169 process.wait();
170 }
171 true }
173 })
174}
175
176pub fn update_jetbrains_toolbox(installation: JetBrainsToolboxInstallation) -> Result<(), UpdateError> {
177 _update_jetbrains_toolbox::<false>(installation)
178}
179
180fn _update_jetbrains_toolbox<const IS_RECURSIVE: bool>(
181 installation: JetBrainsToolboxInstallation,
182) -> Result<(), UpdateError> {
183 let toolbox_was_open = kill_all()?;
185
186 let skipped_channels = change_config(&installation)?;
188
189 let redo = match actual_update(&installation) {
190 Err(e) => {
191 println!("Unexpected error encountered, resetting configuration to previous state");
192 reset_config(&installation, skipped_channels)?;
193 return Err(e);
194 }
195 Ok(redo) => redo,
196 };
197
198 reset_config(&installation, skipped_channels)?;
200
201 if toolbox_was_open {
203 installation.start_minimized()?;
204 }
205
206 if redo {
207 if IS_RECURSIVE {
213 return Err(UpdateError::DoubleToolboxSelfUpdate);
215 }
216
217 _update_jetbrains_toolbox::<true>(installation)
218 } else {
219 Ok(())
220 }
221}
222
223fn actual_update(installation: &JetBrainsToolboxInstallation) -> Result<bool, UpdateError> {
225 let mut redo = false;
226
227 installation.start_minimized()?;
229
230 let mut updates: u32 = 0;
232 let mut correct_checksums_expected: u32 = 0;
233 let start_time = Instant::now();
234
235 let file = File::open(&installation.log)?;
236 let mut file = BufReader::new(file);
237 file.seek(SeekFrom::End(0))?;
238 loop {
239 if updates == 0 && start_time + Duration::from_secs(10) < Instant::now() {
242 println!("No updates found.");
243 break;
244 }
245
246 let curr_position = file.stream_position()?;
247
248 let mut line = String::new();
250 file.read_line(&mut line)?;
251
252 if line.is_empty() {
253 file.seek(SeekFrom::Start(curr_position))?;
256 sleep(Duration::from_millis(100));
257 } else {
258 if line.contains("Correct checksum for") || line.contains("Downloading from") {
264 if line.contains("Correct checksum for") && correct_checksums_expected > 0 {
265 correct_checksums_expected -= 1;
266 continue;
267 }
268 println!("Found an update, waiting until it finishes...");
270 updates += 1;
271 if line.contains("Downloading from") {
272 correct_checksums_expected += 1;
274 }
275 } else if line.contains("Show notification") {
276 updates -= 1;
278 if updates == 0 {
279 println!("Update finished, exiting...");
280 sleep(Duration::from_secs(2)); break;
282 } else {
283 println!("Update finished, waiting for other update(s) to finish")
284 }
285 } else if line.contains(
286 "Shutting down. Reason: The updated app is starting, closing the current process",
287 ) {
288 redo = true;
291 println!("Toolbox (self-)update finished.");
292 sleep(Duration::from_secs(2)); break;
294 }
295 }
296 }
297
298 if !kill_all()? {
300 return Err(UpdateError::PrematureExit);
302 }
303
304 Ok(redo)
305}
306
307fn change_config(installation: &JetBrainsToolboxInstallation) -> Result<Vec<PathBuf>, UpdateError> {
308 let mut skipped_channels = vec![];
309 installation.update_all_channels(|channel, d| {
310 if !d.has_key("channel") {
311 return Err(UpdateError::InvalidChannel);
312 }
313 if d["channel"].has_key("autoUpdate") {
314 if d["channel"]["autoUpdate"] == true {
315 skipped_channels.push(channel.clone());
317 return Ok(());
318 } else {
319 return Err(UpdateError::InvalidChannel);
321 }
322 }
323
324 d["channel"]["autoUpdate"] = true.into();
325 Ok(())
326 })?;
327 Ok(skipped_channels)
328}
329
330fn reset_config(
331 installation: &JetBrainsToolboxInstallation,
332 skipped_channels: Vec<PathBuf>,
333) -> Result<(), UpdateError> {
334 installation.update_all_channels(|channel, d| {
335 if !d.has_key("channel") {
336 return Err(UpdateError::InvalidChannel);
337 }
338 if skipped_channels.contains(channel) {
339 return Ok(());
341 }
342 d["channel"].remove("autoUpdate");
343 Ok(())
344 })?;
345 Ok(())
346}