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,
16 log: PathBuf,
17}
18
19#[derive(Debug)]
20pub enum UpdateError {
21 Io(io::Error),
22 Json(JsonError),
23 InvalidChannel,
24 CouldNotTerminate(String),
25}
26
27impl JetBrainsToolboxInstallation {
28 fn update_all_channels<F>(&self, mut operation: F) -> Result<(), UpdateError>
29 where
30 F: FnMut(&PathBuf, &mut JsonValue) -> Result<(), UpdateError>,
31 {
32 for file in fs::read_dir(&self.channels).map_err(UpdateError::Io)? {
33 let file = file.map_err(UpdateError::Io)?;
34 self.update_channel(file.path(), &mut operation)?;
35 }
36 Ok(())
37 }
38
39 fn update_channel<F>(&self, path: PathBuf, operation: &mut F) -> Result<(), UpdateError>
40 where
41 F: FnMut(&PathBuf, &mut JsonValue) -> Result<(), UpdateError>,
42 {
43 let mut file = File::options()
44 .read(true)
45 .write(true)
46 .open(&path)
47 .map_err(UpdateError::Io)?;
48 let mut buf = String::new();
49 file.read_to_string(&mut buf).map_err(UpdateError::Io)?;
50 let mut data = json::parse(&buf).map_err(UpdateError::Json)?;
51 operation(&path, &mut data)?;
52 file.seek(SeekFrom::Start(0)).map_err(UpdateError::Io)?;
54 buf = data.dump();
55 file.write_all(buf.as_bytes()).map_err(UpdateError::Io)?;
56 let current_position = file.stream_position().map_err(UpdateError::Io)?;
57 file.set_len(current_position).map_err(UpdateError::Io)?;
58
59 Ok(())
60 }
61
62 fn start_minimized(&self) -> io::Result<Child> {
63 Command::new(&self.binary).arg("--minimize").spawn()
64 }
65}
66
67#[derive(Debug, Clone)]
68pub enum FindError {
69 NotFound,
70 InvalidInstallation,
71 NoHomeDir,
72 UnsupportedOS,
73}
74
75#[cfg(target_os = "linux")]
76pub fn find_jetbrains_toolbox() -> Result<JetBrainsToolboxInstallation, FindError> {
77 let home_dir = home_dir().ok_or(FindError::NoHomeDir)?;
78 let dir = home_dir.join(".local/share/JetBrains/Toolbox");
79 if !dir.exists() {
80 return Err(FindError::NotFound);
81 } else if !dir.is_dir() {
82 return Err(FindError::InvalidInstallation);
84 }
85 let binary = dir.join("bin/jetbrains-toolbox");
86 if !binary.exists() {
87 return Err(FindError::InvalidInstallation);
88 }
89 let channels = dir.join("channels");
90 if !channels.is_dir() {
91 return Err(FindError::InvalidInstallation);
92 }
93 let logs_dir = dir.join("logs");
94 if !logs_dir.is_dir() {
95 return Err(FindError::InvalidInstallation);
96 }
97 let log = logs_dir.join("toolbox.log"); Ok(JetBrainsToolboxInstallation {
100 binary,
101 channels,
102 log,
103 })
104}
105
106#[cfg(target_os = "windows")]
107fn find_jetbrains_toolbox() -> Result<JetBrainsToolboxInstallation, FindError> {
108 FindError::UnsupportedOS }
110
111#[cfg(target_os = "macos")]
112fn find_jetbrains_toolbox() -> Result<JetBrainsToolboxInstallation, FindError> {
113 FindError::UnsupportedOS }
115
116#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
117fn find_jetbrains_toolbox() -> Result<Installation, FindError> {
118 FindError::UnsupportedOS
120}
121
122fn kill_all() -> Result<bool, UpdateError> {
123 let mut sys = System::new_all();
124 sys.refresh_all();
125 let processes = sys
127 .processes()
128 .values()
129 .filter_map(|p| {
130 let exe = p.exe()?; let name = p.name();
132 match exe.file_name().ok_or(UpdateError::CouldNotTerminate(
133 "Error getting file_name".to_string(),
134 )) {
135 Ok(file_name)
136 if file_name == "jetbrains-toolbox"
137 && name.to_str()?.starts_with("jetbrains") =>
138 {
139 Some(Ok(p))
140 }
141 Ok(_) => None, Err(e) => Some(Err(e)), }
144 })
145 .collect::<Result<Vec<&Process>, UpdateError>>()?;
146 Ok(match processes.len() {
147 0 => false,
148 _ => {
149 println!("Found {} processes", processes.len());
150 for process in processes {
151 process.kill();
152 process.wait();
153 }
154 true
155 }
156 })
157}
158
159pub fn update_jetbrains_toolbox(
160 installation: JetBrainsToolboxInstallation,
161) -> Result<(), UpdateError> {
162 let toolbox_was_open = kill_all()?;
164
165 let mut skipped_channels = vec![];
167 installation.update_all_channels(|channel, d| {
168 if !d.has_key("channel") {
169 return Err(UpdateError::InvalidChannel);
170 }
171 if d["channel"].has_key("autoUpdate") {
172 if d["channel"]["autoUpdate"] == true {
173 skipped_channels.push(channel.clone());
175 return Ok(());
176 } else {
177 return Err(UpdateError::InvalidChannel);
178 }
179 }
180
181 d["channel"]["autoUpdate"] = true.into();
182 Ok(())
183 })?;
184
185 installation.start_minimized().map_err(UpdateError::Io)?;
187
188 let mut updates: u32 = 0;
190 let mut correct_checksums_expected: u32 = 0;
191 let start_time = Instant::now();
192
193 let file = File::open(&installation.log).map_err(UpdateError::Io)?;
194 let mut file = BufReader::new(file);
195 file.seek(SeekFrom::End(0)).map_err(UpdateError::Io)?;
196 loop {
197 if updates == 0 && start_time + Duration::from_secs(10) < Instant::now() {
200 println!("No updates found.");
201 break;
202 }
203
204 let curr_position = file.stream_position().map_err(UpdateError::Io)?;
205
206 let mut line = String::new();
207 file.read_line(&mut line).map_err(UpdateError::Io)?;
208
209 if line.is_empty() {
210 file.seek(SeekFrom::Start(curr_position))
211 .map_err(UpdateError::Io)?;
212 sleep(Duration::from_millis(100));
213 } else {
214 if line.contains("Correct checksum for") || line.contains("Downloading from") {
216 if line.contains("Correct checksum for") && correct_checksums_expected > 0 {
217 correct_checksums_expected -= 1;
218 continue;
219 }
220 println!("Found an update, waiting until it finishes...");
222 updates += 1;
223 if line.contains("Downloading from") {
224 correct_checksums_expected += 1;
226 }
227 } else if line.contains("Show notification") {
228 updates -= 1;
230 if updates == 0 {
231 println!("Update finished, exiting...");
232 sleep(Duration::from_secs(2)); break;
234 } else {
235 println!("Update finished, waiting for other update(s) to finish")
236 }
237 }
238 }
239 }
240
241 assert!(kill_all()?);
243
244 installation.update_all_channels(|channel, d| {
246 if !d.has_key("channel") {
247 return Err(UpdateError::InvalidChannel);
248 }
249 if skipped_channels.contains(channel) {
250 return Ok(());
251 }
252 d["channel"].remove("autoUpdate");
253 Ok(())
254 })?;
255
256 if toolbox_was_open {
258 installation.start_minimized().map_err(UpdateError::Io)?;
259 }
260
261 Ok(())
262}