Skip to content

Commit 49e1c12

Browse files
committed
Merge PR #283
2 parents 694bfb7 + 1221f67 commit 49e1c12

11 files changed

Lines changed: 2480 additions & 0 deletions

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cortex-common/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ workspace = true
1515
[dependencies]
1616
cortex-protocol = { workspace = true }
1717
serde = { workspace = true }
18+
serde_json = { workspace = true }
1819
toml = { workspace = true }
1920
once_cell = { workspace = true }
2021
clap = { workspace = true, optional = true }

cortex-common/src/cwd_guard.rs

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
//! Working directory change guard.
2+
//!
3+
//! Provides RAII-style guard for changing working directory that ensures
4+
//! the original directory is restored on exit or error.
5+
//!
6+
//! # Issue Addressed
7+
//! - #2796: Working directory not restored after error when using --cwd option
8+
9+
use std::env;
10+
use std::io;
11+
use std::path::{Path, PathBuf};
12+
13+
/// A guard that restores the working directory when dropped.
14+
///
15+
/// This ensures that if an operation changes the working directory and then
16+
/// fails, the original directory is restored, preventing user confusion.
17+
///
18+
/// # Examples
19+
/// ```ignore
20+
/// use cortex_common::cwd_guard::CwdGuard;
21+
///
22+
/// fn process_in_directory(dir: &Path) -> Result<(), Error> {
23+
/// let _guard = CwdGuard::new(dir)?;
24+
/// // Working directory is now `dir`
25+
///
26+
/// do_something_that_might_fail()?;
27+
///
28+
/// // Guard is dropped here, restoring original directory
29+
/// // even if do_something_that_might_fail() returned an error
30+
/// Ok(())
31+
/// }
32+
/// ```
33+
#[derive(Debug)]
34+
pub struct CwdGuard {
35+
/// The original working directory to restore
36+
original: PathBuf,
37+
/// Whether restoration has been disabled
38+
disabled: bool,
39+
}
40+
41+
impl CwdGuard {
42+
/// Create a new guard, changing to the specified directory.
43+
///
44+
/// # Arguments
45+
/// * `new_dir` - The directory to change to
46+
///
47+
/// # Returns
48+
/// A `Result` containing the guard, or an IO error if the directory
49+
/// change failed.
50+
///
51+
/// # Examples
52+
/// ```ignore
53+
/// let guard = CwdGuard::new("/tmp/workspace")?;
54+
/// // Now in /tmp/workspace
55+
/// // Original directory will be restored when guard is dropped
56+
/// ```
57+
pub fn new(new_dir: impl AsRef<Path>) -> io::Result<Self> {
58+
let original = env::current_dir()?;
59+
env::set_current_dir(new_dir.as_ref())?;
60+
61+
Ok(Self {
62+
original,
63+
disabled: false,
64+
})
65+
}
66+
67+
/// Create a guard that saves the current directory without changing it.
68+
///
69+
/// This is useful when you might change directories later but want to
70+
/// ensure restoration regardless.
71+
///
72+
/// # Returns
73+
/// A `Result` containing the guard.
74+
pub fn save_current() -> io::Result<Self> {
75+
let original = env::current_dir()?;
76+
77+
Ok(Self {
78+
original,
79+
disabled: false,
80+
})
81+
}
82+
83+
/// Get the original directory that will be restored.
84+
pub fn original(&self) -> &Path {
85+
&self.original
86+
}
87+
88+
/// Get the current working directory.
89+
pub fn current() -> io::Result<PathBuf> {
90+
env::current_dir()
91+
}
92+
93+
/// Disable restoration when the guard is dropped.
94+
///
95+
/// Use this if you want to keep the new working directory after
96+
/// successful completion.
97+
pub fn disable_restore(&mut self) {
98+
self.disabled = true;
99+
}
100+
101+
/// Enable restoration (undo `disable_restore`).
102+
pub fn enable_restore(&mut self) {
103+
self.disabled = false;
104+
}
105+
106+
/// Manually restore the original directory early.
107+
///
108+
/// This is called automatically on drop, but can be called manually
109+
/// if you need to restore earlier.
110+
///
111+
/// # Returns
112+
/// An `io::Result` indicating success or failure of the directory change.
113+
pub fn restore_now(&mut self) -> io::Result<()> {
114+
if !self.disabled {
115+
env::set_current_dir(&self.original)?;
116+
self.disabled = true; // Prevent double-restore on drop
117+
}
118+
Ok(())
119+
}
120+
121+
/// Change to a new directory while keeping the same restoration point.
122+
///
123+
/// # Arguments
124+
/// * `new_dir` - The new directory to change to
125+
///
126+
/// # Returns
127+
/// An `io::Result` indicating success or failure.
128+
pub fn change_to(&self, new_dir: impl AsRef<Path>) -> io::Result<()> {
129+
env::set_current_dir(new_dir)
130+
}
131+
}
132+
133+
impl Drop for CwdGuard {
134+
fn drop(&mut self) {
135+
if !self.disabled {
136+
// Ignore errors on drop - we can't do much about them
137+
// and panicking in drop is bad
138+
let _ = env::set_current_dir(&self.original);
139+
}
140+
}
141+
}
142+
143+
/// Execute a closure in a specific directory, restoring on completion.
144+
///
145+
/// This is a convenience function for simple use cases where you don't
146+
/// need access to the guard.
147+
///
148+
/// # Arguments
149+
/// * `dir` - The directory to execute in
150+
/// * `f` - The closure to execute
151+
///
152+
/// # Returns
153+
/// The result of the closure, or an IO error if directory change failed.
154+
///
155+
/// # Examples
156+
/// ```ignore
157+
/// use cortex_common::cwd_guard::in_directory;
158+
///
159+
/// let result = in_directory("/tmp/workspace", || {
160+
/// // Do work in /tmp/workspace
161+
/// Ok(compute_something())
162+
/// })?;
163+
/// // Back in original directory
164+
/// ```
165+
pub fn in_directory<T, F>(dir: impl AsRef<Path>, f: F) -> io::Result<T>
166+
where
167+
F: FnOnce() -> T,
168+
{
169+
let _guard = CwdGuard::new(dir)?;
170+
Ok(f())
171+
}
172+
173+
/// Execute a fallible closure in a specific directory, restoring on completion.
174+
///
175+
/// # Arguments
176+
/// * `dir` - The directory to execute in
177+
/// * `f` - The closure to execute
178+
///
179+
/// # Returns
180+
/// The result of the closure, or an error.
181+
pub fn in_directory_result<T, E, F>(dir: impl AsRef<Path>, f: F) -> Result<T, E>
182+
where
183+
E: From<io::Error>,
184+
F: FnOnce() -> Result<T, E>,
185+
{
186+
let _guard = CwdGuard::new(dir)?;
187+
f()
188+
}
189+
190+
#[cfg(test)]
191+
mod tests {
192+
use super::*;
193+
use tempfile::TempDir;
194+
195+
#[test]
196+
fn test_cwd_guard_restores() {
197+
let original = env::current_dir().unwrap();
198+
let temp_dir = TempDir::new().unwrap();
199+
200+
{
201+
let _guard = CwdGuard::new(temp_dir.path()).unwrap();
202+
assert_eq!(
203+
env::current_dir().unwrap(),
204+
temp_dir.path().canonicalize().unwrap()
205+
);
206+
}
207+
208+
// Should be back to original after guard dropped
209+
assert_eq!(env::current_dir().unwrap(), original);
210+
}
211+
212+
#[test]
213+
fn test_cwd_guard_restores_on_panic() {
214+
let original = env::current_dir().unwrap();
215+
let temp_dir = TempDir::new().unwrap();
216+
217+
let result = std::panic::catch_unwind(|| {
218+
let _guard = CwdGuard::new(temp_dir.path()).unwrap();
219+
panic!("test panic");
220+
});
221+
222+
assert!(result.is_err());
223+
// Should still be back to original
224+
assert_eq!(env::current_dir().unwrap(), original);
225+
}
226+
227+
#[test]
228+
fn test_cwd_guard_disabled() {
229+
let original = env::current_dir().unwrap();
230+
let temp_dir = TempDir::new().unwrap();
231+
let temp_path = temp_dir.path().canonicalize().unwrap();
232+
233+
{
234+
let mut guard = CwdGuard::new(temp_dir.path()).unwrap();
235+
guard.disable_restore();
236+
}
237+
238+
// Should NOT be back to original
239+
assert_eq!(env::current_dir().unwrap(), temp_path);
240+
241+
// Restore manually
242+
env::set_current_dir(&original).unwrap();
243+
}
244+
245+
#[test]
246+
fn test_cwd_guard_restore_now() {
247+
let original = env::current_dir().unwrap();
248+
let temp_dir = TempDir::new().unwrap();
249+
250+
let mut guard = CwdGuard::new(temp_dir.path()).unwrap();
251+
252+
// Manually restore
253+
guard.restore_now().unwrap();
254+
assert_eq!(env::current_dir().unwrap(), original);
255+
256+
// Guard drop should not panic (it's disabled after restore_now)
257+
}
258+
259+
#[test]
260+
fn test_in_directory() {
261+
let original = env::current_dir().unwrap();
262+
let temp_dir = TempDir::new().unwrap();
263+
let temp_path = temp_dir.path().canonicalize().unwrap();
264+
265+
let result = in_directory(temp_dir.path(), || env::current_dir().unwrap()).unwrap();
266+
267+
assert_eq!(result, temp_path);
268+
assert_eq!(env::current_dir().unwrap(), original);
269+
}
270+
271+
#[test]
272+
fn test_in_directory_result() {
273+
let original = env::current_dir().unwrap();
274+
let temp_dir = TempDir::new().unwrap();
275+
276+
let result: Result<(), io::Error> = in_directory_result(temp_dir.path(), || {
277+
Err(io::Error::new(io::ErrorKind::Other, "test error"))
278+
});
279+
280+
assert!(result.is_err());
281+
// Should still be back to original
282+
assert_eq!(env::current_dir().unwrap(), original);
283+
}
284+
285+
#[test]
286+
fn test_save_current() {
287+
let original = env::current_dir().unwrap();
288+
let temp_dir = TempDir::new().unwrap();
289+
290+
{
291+
let guard = CwdGuard::save_current().unwrap();
292+
assert_eq!(guard.original(), original);
293+
294+
// Manually change directory
295+
env::set_current_dir(temp_dir.path()).unwrap();
296+
}
297+
298+
// Should be back to original after guard dropped
299+
assert_eq!(env::current_dir().unwrap(), original);
300+
}
301+
}

0 commit comments

Comments
 (0)