xtask_todo_lib/devshell/
session_store.rs1use std::io;
15use std::path::{Path, PathBuf};
16use std::time::{SystemTime, UNIX_EPOCH};
17
18use serde::{Deserialize, Serialize};
19
20pub const ENV_DEVSHELL_WORKSPACE_ROOT: &str = "DEVSHELL_WORKSPACE_ROOT";
22
23pub const FORMAT_DEVSHELL_SESSION_V1: &str = "devshell_session_v1";
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct GuestPrimarySessionV1 {
29 pub format: String,
30 pub logical_cwd: String,
32 pub saved_at_unix_ms: u64,
34}
35
36impl GuestPrimarySessionV1 {
37 fn new(logical_cwd: String) -> Self {
38 let saved_at_unix_ms = SystemTime::now()
39 .duration_since(UNIX_EPOCH)
40 .map_or(0, |d| u64::try_from(d.as_millis()).unwrap_or(0));
41 Self {
42 format: FORMAT_DEVSHELL_SESSION_V1.to_string(),
43 logical_cwd,
44 saved_at_unix_ms,
45 }
46 }
47}
48
49#[must_use]
53pub fn session_path_for_bin(bin_path: &Path) -> PathBuf {
54 bin_path.with_extension("session.json")
55}
56
57#[must_use]
59pub fn workspace_session_metadata_path() -> Option<PathBuf> {
60 std::env::var(ENV_DEVSHELL_WORKSPACE_ROOT)
61 .ok()
62 .and_then(|s| {
63 let t = s.trim();
64 if t.is_empty() {
65 None
66 } else {
67 Some(
68 PathBuf::from(t)
69 .join(".cargo-devshell")
70 .join("session.json"),
71 )
72 }
73 })
74}
75
76#[must_use]
80pub fn cwd_session_metadata_path() -> Option<PathBuf> {
81 std::env::current_dir()
82 .ok()
83 .map(|p| p.join(".cargo-devshell").join("session.json"))
84}
85
86#[must_use]
88pub fn session_metadata_path(bin_path: &Path) -> PathBuf {
89 workspace_session_metadata_path()
90 .or_else(cwd_session_metadata_path)
91 .unwrap_or_else(|| session_path_for_bin(bin_path))
92}
93
94fn load_one_guest_primary(p: &Path) -> io::Result<Option<GuestPrimarySessionV1>> {
95 if !p.is_file() {
96 return Ok(None);
97 }
98 let text = std::fs::read_to_string(p)?;
99 let v: GuestPrimarySessionV1 = serde_json::from_str(&text).map_err(|e| {
100 io::Error::new(
101 io::ErrorKind::InvalidData,
102 format!("invalid guest-primary session JSON {}: {e}", p.display()),
103 )
104 })?;
105 if v.format != FORMAT_DEVSHELL_SESSION_V1 {
106 return Ok(None);
107 }
108 Ok(Some(v))
109}
110
111pub fn save_guest_primary(bin_path: &Path, logical_cwd: &str) -> io::Result<()> {
116 let meta = GuestPrimarySessionV1::new(logical_cwd.to_string());
117 let text = serde_json::to_string_pretty(&meta).map_err(|e| io::Error::other(e.to_string()))?;
118 let path = session_metadata_path(bin_path);
119 if let Some(parent) = path.parent() {
120 std::fs::create_dir_all(parent)?;
121 }
122 std::fs::write(path, text)
123}
124
125pub fn load_guest_primary(bin_path: &Path) -> io::Result<Option<GuestPrimarySessionV1>> {
132 if let Some(ref ws) = workspace_session_metadata_path() {
133 match load_one_guest_primary(ws) {
134 Ok(Some(m)) => return Ok(Some(m)),
135 Ok(None) => {}
136 Err(e) => return Err(e),
137 }
138 }
139 if let Some(ref cwd_meta) = cwd_session_metadata_path() {
140 match load_one_guest_primary(cwd_meta) {
141 Ok(Some(m)) => return Ok(Some(m)),
142 Ok(None) => {}
143 Err(e) => return Err(e),
144 }
145 }
146 load_one_guest_primary(&session_path_for_bin(bin_path))
147}
148
149pub fn apply_guest_primary_startup(
157 vfs: &mut crate::devshell::vfs::Vfs,
158 bin_path: &Path,
159) -> io::Result<()> {
160 let Some(meta) = load_guest_primary(bin_path)? else {
161 return Ok(());
162 };
163 *vfs = crate::devshell::vfs::Vfs::new();
164 let cwd = meta.logical_cwd.trim();
165 if cwd.is_empty() {
166 return Ok(());
167 }
168 if let Err(e) = vfs.mkdir(cwd) {
169 return Err(io::Error::new(
170 io::ErrorKind::InvalidData,
171 format!("session logical_cwd mkdir {cwd}: {e}"),
172 ));
173 }
174 vfs.set_cwd(cwd).map_err(|e| {
175 io::Error::new(
176 io::ErrorKind::InvalidData,
177 format!("session logical_cwd set_cwd {cwd}: {e}"),
178 )
179 })?;
180 Ok(())
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::test_support::{cwd_mutex, devshell_workspace_env_mutex};
187 use std::fs;
188 use std::io;
189
190 fn tmp_dir(name: &str) -> PathBuf {
191 std::env::temp_dir().join(format!(
192 "xtask_devshell_{name}_{}_{}",
193 std::process::id(),
194 std::thread::current().name().unwrap_or("test")
195 ))
196 }
197
198 struct EnvRestore {
199 old: Option<String>,
200 }
201
202 impl EnvRestore {
203 fn set_workspace_root(value: impl AsRef<std::ffi::OsStr>) -> Self {
204 let old = std::env::var(ENV_DEVSHELL_WORKSPACE_ROOT).ok();
205 std::env::set_var(ENV_DEVSHELL_WORKSPACE_ROOT, value);
206 Self { old }
207 }
208
209 fn clear_workspace_root() -> Self {
210 let old = std::env::var(ENV_DEVSHELL_WORKSPACE_ROOT).ok();
211 std::env::remove_var(ENV_DEVSHELL_WORKSPACE_ROOT);
212 Self { old }
213 }
214 }
215
216 impl Drop for EnvRestore {
217 fn drop(&mut self) {
218 match &self.old {
219 Some(s) => std::env::set_var(ENV_DEVSHELL_WORKSPACE_ROOT, s),
220 None => std::env::remove_var(ENV_DEVSHELL_WORKSPACE_ROOT),
221 }
222 }
223 }
224
225 struct CurrentDirRestore {
226 previous: PathBuf,
227 }
228
229 impl CurrentDirRestore {
230 fn chdir(dir: &Path) -> io::Result<Self> {
231 let previous = std::env::current_dir()?;
232 std::env::set_current_dir(dir)?;
233 Ok(Self { previous })
234 }
235 }
236
237 impl Drop for CurrentDirRestore {
238 fn drop(&mut self) {
239 let _ = std::env::set_current_dir(&self.previous);
240 }
241 }
242
243 #[test]
244 fn session_path_for_bin_replaces_extension() {
245 let p = Path::new(".dev_shell.bin");
246 assert_eq!(
247 session_path_for_bin(p),
248 PathBuf::from(".dev_shell.session.json")
249 );
250 }
251
252 #[test]
253 fn roundtrip_guest_primary_uses_cwd_cargo_devshell_when_no_workspace_env() {
254 let _cwd_lock = cwd_mutex();
255 let _workspace_env = devshell_workspace_env_mutex();
256 let _env = EnvRestore::clear_workspace_root();
257 let dir = tmp_dir("roundtrip");
258 let _ = fs::remove_dir_all(&dir);
259 fs::create_dir_all(&dir).unwrap();
260 let dir = dir.canonicalize().expect("canonicalize tmp");
261 let _restore_dir = CurrentDirRestore::chdir(&dir).expect("chdir tmp");
262 let bin_path = dir.join("state.bin");
263 save_guest_primary(&bin_path, "/proj/foo").unwrap();
264 let expected = dir.join(".cargo-devshell").join("session.json");
265 assert!(expected.is_file(), "expected {}", expected.display());
266 let meta = load_guest_primary(&bin_path).unwrap().expect("some");
267 assert_eq!(meta.logical_cwd, "/proj/foo");
268 assert_eq!(meta.format, FORMAT_DEVSHELL_SESSION_V1);
269 let _ = fs::remove_dir_all(&dir);
270 }
271
272 #[test]
273 fn save_prefers_workspace_env_path() {
274 let _cwd_lock = cwd_mutex();
275 let _workspace_env = devshell_workspace_env_mutex();
276 let dir = tmp_dir("ws_sess");
277 let _ = fs::remove_dir_all(&dir);
278 fs::create_dir_all(&dir).unwrap();
279 let dir = dir.canonicalize().expect("canonicalize tmp");
280 let _env = EnvRestore::set_workspace_root(&dir);
281 let bin_path = dir.join("ignored.bin");
282 save_guest_primary(&bin_path, "/x").unwrap();
283 let expected = dir.join(".cargo-devshell").join("session.json");
284 assert!(expected.is_file(), "expected {}", expected.display());
285 let loaded = load_guest_primary(&bin_path).unwrap().expect("meta");
286 assert_eq!(loaded.logical_cwd, "/x");
287 let _ = fs::remove_dir_all(&dir);
288 }
289
290 #[test]
291 fn apply_startup_sets_cwd() {
292 let _cwd_lock = cwd_mutex();
293 let _workspace_env = devshell_workspace_env_mutex();
294 let _env = EnvRestore::clear_workspace_root();
295 let dir = tmp_dir("apply");
296 let _ = fs::remove_dir_all(&dir);
297 fs::create_dir_all(&dir).unwrap();
298 let dir = dir.canonicalize().expect("canonicalize tmp");
299 let _restore_dir = CurrentDirRestore::chdir(&dir).expect("chdir tmp");
300 let bin_path = dir.join("x.bin");
301 let session_path = session_path_for_bin(&bin_path);
302 fs::write(
303 &session_path,
304 r#"{
305 "format": "devshell_session_v1",
306 "logical_cwd": "/a/b",
307 "saved_at_unix_ms": 0
308 }"#,
309 )
310 .unwrap();
311 let mut vfs = crate::devshell::vfs::Vfs::new();
312 vfs.mkdir("/a/b").unwrap();
313 vfs.write_file("/a/b/f", b"x").unwrap();
314 apply_guest_primary_startup(&mut vfs, &bin_path).unwrap();
315 assert_eq!(vfs.cwd(), "/a/b");
316 assert!(vfs.read_file("/a/b/f").is_err());
317 let _ = fs::remove_dir_all(&dir);
318 }
319}