next_napi_bindings/
lockfile.rs1use std::{
2 fs::{File, OpenOptions},
3 io::Write,
4 mem::ManuallyDrop,
5 sync::Mutex,
6};
7
8use anyhow::Context;
9use napi::bindgen_prelude::External;
10use napi_derive::napi;
11
12type JsLockfile = Mutex<ManuallyDrop<Option<LockfileInner>>>;
20
21pub struct LockfileInner {
22 file: File,
23 #[cfg(not(windows))]
24 path: std::path::PathBuf,
25}
26
27#[napi(ts_return_type = "{ __napiType: \"Lockfile\" } | null")]
28pub fn lockfile_try_acquire_sync(
29 path: String,
30 content: Option<String>,
31) -> napi::Result<Option<External<JsLockfile>>> {
32 #[cfg(windows)]
41 return {
42 use std::os::windows::fs::OpenOptionsExt;
43
44 use windows_sys::Win32::{Foundation, Storage::FileSystem};
45
46 let mut open_options = OpenOptions::new();
49 open_options.write(true).create(true).truncate(true);
50 open_options
51 .share_mode(FileSystem::FILE_SHARE_READ | FileSystem::FILE_SHARE_DELETE)
52 .custom_flags(FileSystem::FILE_FLAG_DELETE_ON_CLOSE);
53 match open_options.open(&path) {
54 Ok(mut file) => {
55 if let Some(ref data) = content {
57 file.write_all(data.as_bytes())?;
58 file.flush()?;
59 }
60 Ok(Some(External::new(Mutex::new(ManuallyDrop::new(Some(
61 LockfileInner { file },
62 ))))))
63 }
64 Err(err)
65 if err.raw_os_error()
66 == Some(Foundation::ERROR_SHARING_VIOLATION.try_into().unwrap()) =>
67 {
68 Ok(None)
69 }
70 Err(err) => Err(err.into()),
71 }
72 };
73
74 #[cfg(not(windows))]
75 return {
76 use std::{fs::TryLockError, io::Seek};
77
78 let mut open_options = OpenOptions::new();
82 open_options.write(true).create(true).read(true);
83
84 let file = open_options.open(&path)?;
85 match file.try_lock() {
86 Ok(_) => {
87 file.set_len(0)?;
89 (&file).seek(std::io::SeekFrom::Start(0))?;
90 if let Some(ref data) = content {
91 (&file).write_all(data.as_bytes())?;
92 (&file).flush()?;
93 }
94 Ok(Some(External::new(Mutex::new(ManuallyDrop::new(Some(
95 LockfileInner {
96 file,
97 path: path.into(),
98 },
99 ))))))
100 }
101 Err(TryLockError::WouldBlock) => Ok(None),
102 Err(TryLockError::Error(err)) => Err(err.into()),
103 }
104 };
105}
106
107#[napi(ts_return_type = "Promise<{ __napiType: \"Lockfile\" } | null>")]
108pub async fn lockfile_try_acquire(
109 path: String,
110 content: Option<String>,
111) -> napi::Result<Option<External<JsLockfile>>> {
112 tokio::task::spawn_blocking(move || lockfile_try_acquire_sync(path, content))
113 .await
114 .context("panicked while attempting to acquire lockfile")?
115}
116
117#[napi]
118pub fn lockfile_unlock_sync(
119 #[napi(ts_arg_type = "{ __napiType: \"Lockfile\" }")] lockfile: External<JsLockfile>,
120) {
121 let Some(inner): Option<LockfileInner> = lockfile
124 .lock()
125 .expect("poisoned: another thread panicked during `lockfile_unlock_sync`?")
126 .take()
127 else {
128 return;
129 };
130
131 #[cfg(not(windows))]
137 let _ = std::fs::remove_file(inner.path);
138
139 drop(inner.file);
140}
141
142#[napi]
143pub async fn lockfile_unlock(
144 #[napi(ts_arg_type = "{ __napiType: \"Lockfile\" }")] lockfile: External<JsLockfile>,
145) -> napi::Result<()> {
146 Ok(
147 tokio::task::spawn_blocking(move || lockfile_unlock_sync(lockfile))
148 .await
149 .context("panicked while attempting to unlock lockfile")?,
150 )
151}