OXYGENHUB
# 94cdbc576ce1 oxygen / crates / oxy-core / src / repo / workspace.rs
10.0 KiB · Rust · last changed Hold the working-copy lock through every working-copy write 1 month ago source blame history raw
1
//! Multiple workspaces (spec §12.2): extra checkout directories sharing one
2
//! `.oxy`. A secondary workspace holds a `.oxy` *file* of the form
3
//! `oxydir: <path>\nworkspace: <id>`; the repository lists workspaces under
4
//! `.oxy/workspaces/<id>/path`. Each workspace has its own working-copy
5
//! change, manifest, and short-lived snapshot lock.
6
7
use super::*;
8
9
#[derive(Debug, Clone, PartialEq, Eq)]
10
pub struct WorkspaceInfo {
11
    pub id: WorkspaceId,
12
    pub path: PathBuf,
13
    /// Working-copy change hash from the current view, if the workspace is registered there.
14
    pub working_copy: Option<ChangeHash>,
15
    pub current: bool,
16
}
17
18
/// A lock file, taken by creating it exclusively and released on drop: a
19
/// workspace's working-copy lock, the operation head's lock. A stale one,
20
/// whose process is gone (or, where that cannot be told, older than a
21
/// minute), is taken over.
22
pub struct FileLock {
23
    path: PathBuf,
24
}
25
26
/// A workspace's working-copy lock (spec §12.1), held by everything that
27
/// reads or writes the working copy and records its working-copy change:
28
/// snapshot, checkout, bookmark switch, commit, discard, pull. It is
29
/// re-entrant within a thread, so a checkout that snapshots first holds it
30
/// once.
31
pub struct WorkingCopyLock {
32
    path: PathBuf,
33
    _file: Option<FileLock>,
34
}
35
36
/// The working-copy lock; its old name.
37
pub type SnapshotLock = WorkingCopyLock;
38
39
thread_local! {
40
    /// Working-copy locks this thread holds, with their depth.
41
    static HELD: std::cell::RefCell<HashMap<PathBuf, usize>> = Default::default();
42
}
43
44
impl Drop for WorkingCopyLock {
45
    fn drop(&mut self) {
46
        HELD.with(|h| {
47
            let mut h = h.borrow_mut();
48
            if let Some(n) = h.get_mut(&self.path) {
49
                *n -= 1;
50
                if *n == 0 {
51
                    h.remove(&self.path);
52
                }
53
            }
54
        });
55
    }
56
}
57
58
/// Whether the process that wrote a lock file is gone. Where that cannot
59
/// be told (no `/proc`), a lock older than a minute counts as stale.
60
pub(crate) fn lock_is_stale(path: &Path) -> bool {
61
    let old = fs::metadata(path)
62
        .and_then(|m| m.modified())
63
        .map(|t| t.elapsed().map(|d| d.as_secs() > 60).unwrap_or(false))
64
        .unwrap_or(true);
65
    let proc_fs = Path::new("/proc/self").exists();
66
    match fs::read_to_string(path)
67
        .ok()
68
        .and_then(|s| s.trim().parse::<u32>().ok())
69
    {
70
        Some(pid) if proc_fs => !Path::new("/proc").join(pid.to_string()).exists(),
71
        // Being written this instant, or by something that is not oxy.
72
        _ => old,
73
    }
74
}
75
76
impl Drop for FileLock {
77
    fn drop(&mut self) {
78
        let _ = fs::remove_file(&self.path);
79
    }
80
}
81
82
impl FileLock {
83
    /// Wait up to about five seconds for `path`; `busy` is the error after that.
84
    pub(crate) fn acquire(path: PathBuf, busy: &str) -> Result<FileLock> {
85
        Self::acquire_within(path, busy, std::time::Duration::from_secs(5))
86
    }
87
88
    /// Wait up to `wait` for `path`; `busy` is the error after that.
89
    pub(crate) fn acquire_within(
90
        path: PathBuf,
91
        busy: &str,
92
        wait: std::time::Duration,
93
    ) -> Result<FileLock> {
94
        let until = std::time::Instant::now() + wait;
95
        loop {
96
            match fs::OpenOptions::new()
97
                .write(true)
98
                .create_new(true)
99
                .open(&path)
100
            {
101
                Ok(mut f) => {
102
                    let _ = write!(f, "{}", std::process::id());
103
                    return Ok(FileLock { path });
104
                }
105
                Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
106
                    if lock_is_stale(&path) {
107
                        let _ = fs::remove_file(&path);
108
                        continue;
109
                    }
110
                    if std::time::Instant::now() >= until {
111
                        return fail(busy);
112
                    }
113
                    std::thread::sleep(std::time::Duration::from_millis(10));
114
                }
115
                Err(e) => return Err(e.into()),
116
            }
117
        }
118
    }
119
}
120
121
/// Parse a secondary workspace's `.oxy` file.
122
pub(crate) fn parse_workspace_file(text: &str) -> Option<(PathBuf, WorkspaceId)> {
123
    let mut dir = None;
124
    let mut id = None;
125
    for line in text.lines() {
126
        if let Some(v) = line.strip_prefix("oxydir:") {
127
            dir = Some(PathBuf::from(v.trim()));
128
        } else if let Some(v) = line.strip_prefix("workspace:") {
129
            id = WorkspaceId::from_hex(v.trim());
130
        }
131
    }
132
    Some((dir?, id?))
133
}
134
135
impl Repo {
136
    fn workspace_dir(&self, id: &WorkspaceId) -> PathBuf {
137
        self.oxy.join("workspaces").join(id.to_hex())
138
    }
139
140
    /// Record where a workspace lives.
141
    pub(crate) fn register_workspace(&self, id: &WorkspaceId, path: &Path) -> Result<()> {
142
        let dir = self.workspace_dir(id);
143
        fs::create_dir_all(&dir)?;
144
        write_atomic(&dir.join("path"), path.to_string_lossy().as_bytes())
145
    }
146
147
    /// The directory of a workspace, if registered.
148
    pub fn workspace_path(&self, id: &WorkspaceId) -> Option<PathBuf> {
149
        fs::read_to_string(self.workspace_dir(id).join("path"))
150
            .ok()
151
            .map(|s| PathBuf::from(s.trim()))
152
    }
153
154
    /// Every workspace this repository knows about.
155
    pub fn workspaces(&self) -> Result<Vec<WorkspaceInfo>> {
156
        let view = self.view()?;
157
        let mut ids: BTreeSet<WorkspaceId> = view.workspaces.keys().copied().collect();
158
        if let Ok(rd) = fs::read_dir(self.oxy.join("workspaces")) {
159
            for e in rd.flatten() {
160
                if let Some(id) = WorkspaceId::from_hex(&e.file_name().to_string_lossy()) {
161
                    if e.path().join("path").exists() {
162
                        ids.insert(id);
163
                    }
164
                }
165
            }
166
        }
167
        let mut out = Vec::new();
168
        for id in ids {
169
            let path = if id == self.workspace {
170
                self.root.clone()
171
            } else {
172
                self.workspace_path(&id)
173
                    .unwrap_or_else(|| PathBuf::from("(unknown)"))
174
            };
175
            out.push(WorkspaceInfo {
176
                id,
177
                path,
178
                working_copy: view.workspaces.get(&id).copied(),
179
                current: id == self.workspace,
180
            });
181
        }
182
        Ok(out)
183
    }
184
185
    /// Create a workspace at `dir` checked out on `heads` (default the
186
    /// bookmark's), and return a `Repo` rooted there.
187
    pub fn workspace_add(&self, dir: &Path, heads: Option<&[ChangeHash]>) -> Result<Repo> {
188
        if dir.exists() && fs::read_dir(dir)?.next().is_some() {
189
            return fail(format!("{} exists and is not empty", dir.display()));
190
        }
191
        fs::create_dir_all(dir)?;
192
        let dir = dir.canonicalize()?;
193
        if dir == self.root || dir.starts_with(&self.root) || self.root.starts_with(&dir) {
194
            return fail("a workspace cannot be inside another workspace of the same repository");
195
        }
196
        let heads = match heads {
197
            Some(h) => h.to_vec(),
198
            None => self.bookmark_heads()?,
199
        };
200
        let id = WorkspaceId::random();
201
        let wc = self.new_working_copy(heads.clone(), "");
202
        let wc_hash = self.store.put_change(&wc)?;
203
        let (op_ref, op) = self.head_op()?;
204
        let mut view = op.view.clone();
205
        view.workspaces.insert(id, wc_hash);
206
        self.record_op(
207
            &[op_ref],
208
            &format!("oxy workspace add {}", dir.display()),
209
            view,
210
        )?;
211
        self.register_workspace(&id, &dir)?;
212
        self.set_workspace_bookmark(&id, &self.config.bookmark)?;
213
        write_atomic(
214
            &dir.join(OXY_DIR),
215
            format!(
216
                "oxydir: {}\nworkspace: {}\n",
217
                self.oxy.display(),
218
                id.to_hex()
219
            )
220
            .as_bytes(),
221
        )?;
222
        let repo = Repo::open(&dir)?.with_home(self.keys_dir.parent().unwrap_or(&self.keys_dir));
223
        repo.checkout(&heads, true, "oxy workspace add (checkout)")?;
224
        Ok(repo)
225
    }
226
227
    /// Drop a workspace from the view and registry; its files stay.
228
    pub fn workspace_forget(&self, id: &WorkspaceId) -> Result<()> {
229
        if *id == self.workspace {
230
            return fail("cannot forget the workspace you are in");
231
        }
232
        let (op_ref, op) = self.head_op()?;
233
        let mut view = op.view.clone();
234
        if view.workspaces.remove(id).is_none() && self.workspace_path(id).is_none() {
235
            return fail(format!("no workspace {}", id.short(8)));
236
        }
237
        self.record_op(
238
            &[op_ref],
239
            &format!("oxy workspace forget {}", id.short(8)),
240
            view,
241
        )?;
242
        let _ = fs::remove_dir_all(self.workspace_dir(id));
243
        Ok(())
244
    }
245
246
    /// Take this workspace's snapshot lock (spec §12.1).
247
    pub(crate) fn lock_snapshot(&self) -> Result<SnapshotLock> {
248
        self.lock_working_copy()
249
    }
250
251
    /// Take this workspace's working-copy lock (spec §12.1), waiting up to
252
    /// a minute for another process's checkout or snapshot to finish.
253
    pub fn lock_working_copy(&self) -> Result<WorkingCopyLock> {
254
        let dir = self.workspace_dir(&self.workspace);
255
        if self.bare {
256
            // No working copy to guard.
257
            return Ok(WorkingCopyLock {
258
                path: dir,
259
                _file: None,
260
            });
261
        }
262
        fs::create_dir_all(&dir)?;
263
        let path = dir.join("lock");
264
        let held = HELD.with(|h| {
265
            let mut h = h.borrow_mut();
266
            match h.get_mut(&path) {
267
                Some(n) => {
268
                    *n += 1;
269
                    true
270
                }
271
                None => false,
272
            }
273
        });
274
        if held {
275
            return Ok(WorkingCopyLock { path, _file: None });
276
        }
277
        let file = FileLock::acquire_within(
278
            path.clone(),
279
            "another oxy process is using this working copy (a checkout or snapshot); try again when it finishes",
280
            std::time::Duration::from_secs(60),
281
        )?;
282
        HELD.with(|h| h.borrow_mut().insert(path.clone(), 1));
283
        Ok(WorkingCopyLock {
284
            path,
285
            _file: Some(file),
286
        })
287
    }
288
}