1extern crate dirs;
7
8use std::{
9 fs::{self, create_dir_all, read_to_string, remove_dir_all},
10 io::Write,
11 path::{Path, PathBuf},
12 sync::{Arc, Mutex},
13};
14
15use serde::{de::DeserializeOwned, Serialize};
16
17use crate::{
18 constants::{DEFAULT_DATA_PARENT_DIR, VSCODE_CLI_QUALITY},
19 download_cache::DownloadCache,
20 util::errors::{wrap, AnyError, NoHomeForLauncherError, WrappedError},
21};
22
23const HOME_DIR_ALTS: [&str; 2] = ["$HOME", "~"];
24
25#[derive(Clone)]
26pub struct LauncherPaths {
27 pub server_cache: DownloadCache,
28 pub cli_cache: DownloadCache,
29 root: PathBuf,
30}
31
32struct PersistedStateContainer<T>
33where
34 T: Clone + Serialize + DeserializeOwned + Default,
35{
36 path: PathBuf,
37 state: Option<T>,
38 #[allow(dead_code)]
39 mode: u32,
40}
41
42impl<T> PersistedStateContainer<T>
43where
44 T: Clone + Serialize + DeserializeOwned + Default,
45{
46 fn load_or_get(&mut self) -> T {
47 if let Some(state) = &self.state {
48 return state.clone();
49 }
50
51 let state = if let Ok(s) = read_to_string(&self.path) {
52 serde_json::from_str::<T>(&s).unwrap_or_default()
53 } else {
54 T::default()
55 };
56
57 self.state = Some(state.clone());
58 state
59 }
60
61 fn save(&mut self, state: T) -> Result<(), WrappedError> {
62 let s = serde_json::to_string(&state).unwrap();
63 self.state = Some(state);
64 self.write_state(s).map_err(|e| {
65 wrap(
66 e,
67 format!("error saving launcher state into {}", self.path.display()),
68 )
69 })
70 }
71
72 fn write_state(&mut self, s: String) -> std::io::Result<()> {
73 #[cfg(not(windows))]
74 use std::os::unix::fs::OpenOptionsExt;
75
76 let mut f = fs::OpenOptions::new();
77 f.create(true);
78 f.write(true);
79 f.truncate(true);
80 #[cfg(not(windows))]
81 f.mode(self.mode);
82
83 let mut f = f.open(&self.path)?;
84 f.write_all(s.as_bytes())
85 }
86}
87
88#[derive(Clone)]
90pub struct PersistedState<T>
91where
92 T: Clone + Serialize + DeserializeOwned + Default,
93{
94 container: Arc<Mutex<PersistedStateContainer<T>>>,
95}
96
97impl<T> PersistedState<T>
98where
99 T: Clone + Serialize + DeserializeOwned + Default,
100{
101 pub fn new(path: PathBuf) -> PersistedState<T> {
103 Self::new_with_mode(path, 0o644)
104 }
105
106 pub fn new_with_mode(path: PathBuf, mode: u32) -> PersistedState<T> {
108 PersistedState {
109 container: Arc::new(Mutex::new(PersistedStateContainer {
110 path,
111 state: None,
112 mode,
113 })),
114 }
115 }
116
117 pub fn load(&self) -> T {
119 self.container.lock().unwrap().load_or_get()
120 }
121
122 pub fn save(&self, state: T) -> Result<(), WrappedError> {
124 self.container.lock().unwrap().save(state)
125 }
126
127 pub fn update<R>(&self, mutator: impl FnOnce(&mut T) -> R) -> Result<R, WrappedError> {
129 let mut container = self.container.lock().unwrap();
130 let mut state = container.load_or_get();
131 let r = mutator(&mut state);
132 container.save(state).map(|_| r)
133 }
134}
135
136impl LauncherPaths {
137 pub fn migrate(root: Option<String>) -> Result<LauncherPaths, AnyError> {
139 if root.is_some() {
140 return Self::new(root);
141 }
142
143 let home_dir = match dirs::home_dir() {
144 None => return Self::new(root),
145 Some(d) => d,
146 };
147
148 let old_dir = home_dir.join(".vscode-cli");
149 let mut new_dir = home_dir;
150 new_dir.push(DEFAULT_DATA_PARENT_DIR);
151 new_dir.push("cli");
152 if !old_dir.exists() || new_dir.exists() {
153 return Self::new_for_path(new_dir);
154 }
155
156 if let Err(e) = std::fs::rename(&old_dir, &new_dir) {
157 eprintln!(
159 "Failed to migrate old CLI data directory, will create a new one ({})",
160 e
161 );
162 }
163
164 Self::new_for_path(new_dir)
165 }
166
167 pub fn new(root: Option<String>) -> Result<LauncherPaths, AnyError> {
168 let root = root.unwrap_or_else(|| format!("~/{}/cli", DEFAULT_DATA_PARENT_DIR));
169 let mut replaced = root.to_owned();
170 for token in HOME_DIR_ALTS {
171 if root.contains(token) {
172 if let Some(home) = dirs::home_dir() {
173 replaced = root.replace(token, &home.to_string_lossy())
174 } else {
175 return Err(AnyError::from(NoHomeForLauncherError()));
176 }
177 }
178 }
179
180 Self::new_for_path(PathBuf::from(replaced))
181 }
182
183 fn new_for_path(root: PathBuf) -> Result<LauncherPaths, AnyError> {
184 if !root.exists() {
185 create_dir_all(&root)
186 .map_err(|e| wrap(e, format!("error creating directory {}", root.display())))?;
187 }
188
189 Ok(LauncherPaths::new_without_replacements(root))
190 }
191
192 pub fn new_without_replacements(root: PathBuf) -> LauncherPaths {
193 let _ = std::fs::remove_dir_all(root.join("server-insiders"));
195 let _ = std::fs::remove_dir_all(root.join("server-stable"));
196
197 LauncherPaths {
198 server_cache: DownloadCache::new(root.join("servers")),
199 cli_cache: DownloadCache::new(root.join("cli")),
200 root,
201 }
202 }
203
204 pub fn root(&self) -> &Path {
206 &self.root
207 }
208
209 pub fn tunnel_lockfile(&self) -> PathBuf {
211 self.root.join(format!(
212 "tunnel-{}.lock",
213 VSCODE_CLI_QUALITY.unwrap_or("oss")
214 ))
215 }
216
217 pub fn forwarding_lockfile(&self) -> PathBuf {
219 self.root.join(format!(
220 "forwarding-{}.lock",
221 VSCODE_CLI_QUALITY.unwrap_or("oss")
222 ))
223 }
224
225 pub fn service_log_file(&self) -> PathBuf {
227 self.root.join("tunnel-service.log")
228 }
229
230 pub fn remove(&self) -> Result<(), WrappedError> {
232 remove_dir_all(&self.root).map_err(|e| {
233 wrap(
234 e,
235 format!(
236 "error removing launcher data directory {}",
237 self.root.display()
238 ),
239 )
240 })
241 }
242
243 pub fn web_server_storage(&self) -> PathBuf {
245 self.root.join("serve-web")
246 }
247}