cli/
state.rs

1/*---------------------------------------------------------------------------------------------
2 *  Copyright (c) Microsoft Corporation. All rights reserved.
3 *  Licensed under the MIT License. See License.txt in the project root for license information.
4 *--------------------------------------------------------------------------------------------*/
5
6extern 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/// Container that holds some state value that is persisted to disk.
89#[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	/// Creates a new state container that persists to the given path.
102	pub fn new(path: PathBuf) -> PersistedState<T> {
103		Self::new_with_mode(path, 0o644)
104	}
105
106	/// Creates a new state container that persists to the given path.
107	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	/// Loads persisted state.
118	pub fn load(&self) -> T {
119		self.container.lock().unwrap().load_or_get()
120	}
121
122	/// Saves persisted state.
123	pub fn save(&self, state: T) -> Result<(), WrappedError> {
124		self.container.lock().unwrap().save(state)
125	}
126
127	/// Mutates persisted state.
128	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	/// todo@conno4312: temporary migration from the old CLI data directory
138	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			// no logger exists at this point in the lifecycle, so just log to stderr
158			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		// cleanup folders that existed before the new LRU strategy:
194		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	/// Root directory for the server launcher
205	pub fn root(&self) -> &Path {
206		&self.root
207	}
208
209	/// Lockfile for the running tunnel
210	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	/// Lockfile for port forwarding
218	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	/// Suggested path for tunnel service logs, when using file logs
226	pub fn service_log_file(&self) -> PathBuf {
227		self.root.join("tunnel-service.log")
228	}
229
230	/// Removes the launcher data directory.
231	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	/// Suggested path for web server storage
244	pub fn web_server_storage(&self) -> PathBuf {
245		self.root.join("serve-web")
246	}
247}