1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
use std::{
    cell::Cell,
    ffi::OsStr,
    ffi::OsString,
    mem::MaybeUninit,
    path::{Path, PathBuf},
    ptr,
    sync::{Mutex, MutexGuard, Once},
};

use crate::{cwd, error::fs_err, Result};

pub fn pushd(dir: impl AsRef<Path>) -> Result<Pushd> {
    Pushd::new(dir.as_ref())
}

#[must_use]
pub struct Pushd {
    _guard: GlobalShellLock,
    prev_dir: PathBuf,
    dir: PathBuf,
}

pub fn pushenv(k: impl AsRef<OsStr>, v: impl AsRef<OsStr>) -> Pushenv {
    Pushenv::new(k.as_ref(), v.as_ref())
}

#[must_use]
pub struct Pushenv {
    _guard: GlobalShellLock,
    key: OsString,
    prev_value: Option<OsString>,
    value: OsString,
}

impl Pushd {
    fn new(dir: &Path) -> Result<Pushd> {
        let guard = GlobalShellLock::lock();
        let prev_dir = cwd()?;
        set_current_dir(&dir)?;
        let dir = cwd()?;
        Ok(Pushd { _guard: guard, prev_dir, dir })
    }
}

impl Drop for Pushd {
    fn drop(&mut self) {
        let dir = cwd().unwrap();
        assert_eq!(
            dir,
            self.dir,
            "current directory was changed concurrently.
expected {}
got      {}",
            self.dir.display(),
            dir.display()
        );
        set_current_dir(&self.prev_dir).unwrap()
    }
}

fn set_current_dir(path: &Path) -> Result<()> {
    std::env::set_current_dir(path).map_err(|err| fs_err(path.to_path_buf(), err))
}

impl Pushenv {
    fn new(key: &OsStr, value: &OsStr) -> Pushenv {
        let guard = GlobalShellLock::lock();
        let prev_value = std::env::var_os(key);
        std::env::set_var(key, value);
        Pushenv { _guard: guard, key: key.to_os_string(), prev_value, value: value.to_os_string() }
    }
}

impl Drop for Pushenv {
    fn drop(&mut self) {
        let value = std::env::var_os(&self.key);
        assert_eq!(
            value.as_ref(),
            Some(&self.value),
            "environmental variable was changed concurrently.
var      {:?}
expected {:?}
got      {:?}",
            self.key,
            self.value,
            value
        );
        match &self.prev_value {
            Some(it) => std::env::set_var(&self.key, &it),
            None => std::env::remove_var(&self.key),
        }
    }
}

struct GlobalShellLock {
    guard: Option<MutexGuard<'static, ()>>,
}

static mut MUTEX: MaybeUninit<Mutex<()>> = MaybeUninit::uninit();
static MUTEX_INIT: Once = Once::new();
thread_local! {
    pub static LOCKED: Cell<bool> = Cell::new(false);
}

impl GlobalShellLock {
    fn lock() -> GlobalShellLock {
        if LOCKED.with(|it| it.get()) {
            return GlobalShellLock { guard: None };
        }

        let guard = unsafe {
            MUTEX_INIT.call_once(|| ptr::write(MUTEX.as_mut_ptr(), Mutex::new(())));
            (*MUTEX.as_ptr()).lock().unwrap()
        };
        LOCKED.with(|it| it.set(true));
        GlobalShellLock { guard: Some(guard) }
    }
}

impl Drop for GlobalShellLock {
    fn drop(&mut self) {
        if self.guard.is_some() {
            LOCKED.with(|it| it.set(false))
        }
    }
}