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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
use anyhow::{Result, anyhow};
use fs2::FileExt;
use std::fs::{self, File};
use std::path::PathBuf;
use std::time::{Duration, Instant};
use sysinfo::{Pid, ProcessesToUpdate, System};
use crate::output::output::OutputKind;
use crate::print_text;
use crate::utils::path::cache_dir;
/// Exit code used when this instance shuts down because a newer one took over.
/// 128 + SIGINT(2) by convention. CommandProvider maps it to coroutine
/// cancellation, so the UI reports "cancelled" instead of "failed".
const EXIT_SUPERSEDED: i32 = 130;
pub struct RunGuard {
pid_path: PathBuf,
}
impl RunGuard {
/// Launch the singleton guard: notifies other instances, waits for them to stop,
/// creates our PID file, starts a stop-file monitor, and returns the guard.
pub fn start() -> Result<RunGuard> {
let pid = std::process::id();
let cache_dir = cache_dir();
// Ensure directories exist before any operations
fs::create_dir_all(&cache_dir)?;
// Prevent concurrent startup by acquiring an exclusive lock file.
let _startup_lock = Self::acquire_startup_lock(&cache_dir)?;
// Create a single System instance to reuse across all process checks,
// avoiding repeated expensive initializations inside loops.
let mut sys = System::new();
// Remove stale files (process no longer alive) before doing anything else
Self::pre_clean(&cache_dir, &mut sys);
// Notify all running instances by renaming their *.pid → *.pid.stop
let has_notified = Self::notify_all_instances(&cache_dir, pid, &mut sys);
// Inform the user that we are waiting for the previous instance(s) to stop
if has_notified {
print_text!(
OutputKind::Warning,
"Waiting for running instances to exit..."
);
}
// Create our PID file to signal that we are the active instance.
let pid_path = cache_dir.join(format!("{}.pid", pid));
File::create(&pid_path)?;
// Start a background thread that watches for our *.pid.stop and exits when it appears.
Self::start_stop_monitor(cache_dir.clone(), pid);
if has_notified {
// Wait until all *.pid.stop files disappear (or timeout).
Self::wait_for_instances(&cache_dir, pid);
// Forcefully terminate any instances that ignored the shutdown notification and remove their stop files.
Self::kill_stale_instances(&cache_dir, pid, &mut sys);
}
// The guard will delete the pid file on drop (normal or exit)
Ok(RunGuard { pid_path })
}
/// Remove *.pid / *.pid.stop files belonging to dead processes.
fn pre_clean(cache_dir: &PathBuf, sys: &mut System) {
if let Ok(entries) = fs::read_dir(cache_dir) {
for entry in entries.flatten() {
let path = entry.path();
let is_pid = path.extension().map_or(false, |ext| ext == "pid");
let is_stop = !is_pid
&& path.extension().map_or(false, |ext| ext == "stop")
&& path
.file_stem()
.map(|s| {
std::path::Path::new(s)
.extension()
.map_or(false, |e| e == "pid")
})
.unwrap_or(false);
if !is_pid && !is_stop {
continue;
}
let pid = if is_pid {
path.file_stem()
.and_then(|s| s.to_str())
.and_then(|s| s.parse::<u32>().ok())
} else {
parse_stop_pid(&path)
};
if let Some(pid) = pid {
if !Self::pid_exists(pid, sys) {
let _ = fs::remove_file(&path);
}
} else {
// Malformed name – clean up
let _ = fs::remove_file(&path);
}
}
}
}
/// Check if a process with given PID exists.
fn pid_exists(current_pid: u32, sys: &mut System) -> bool {
let target = Pid::from(current_pid as usize);
// true ensures dead processes are removed from cache immediately
sys.refresh_processes(ProcessesToUpdate::Some(&[target]), true);
sys.process(target).is_some()
}
/// Rename all *.pid files to *.pid.stop to notify running instances to shut down.
/// Only notifies alive processes that belong to our executable.
/// Removes dead or foreign PID files to keep the cache directory clean.
fn notify_all_instances(cache_dir: &PathBuf, current_pid: u32, sys: &mut System) -> bool {
let mut has_notified = false;
if let Ok(entries) = std::fs::read_dir(cache_dir) {
for entry in entries.flatten() {
let path = entry.path();
// Interested only in files with ".pid" extension
if path.extension().map_or(false, |ext| ext == "pid") {
// Try to parse PID from filename (e.g., "123.pid")
if let Some(pid) = path
.file_stem()
.and_then(|s| s.to_str())
.and_then(|s| s.parse::<u32>().ok())
{
// Skip our own PID file (just created)
if pid == current_pid {
continue;
}
// Check if it's a living process of our application
if Self::is_our_process(pid, sys) {
// Alive and ours — notify by renaming to .pid.stop
let stop_path = path.with_extension("pid.stop");
if std::fs::rename(&path, &stop_path).is_ok() {
has_notified = true;
}
} else {
// Dead or foreign process — this file is junk, remove it
let _ = std::fs::remove_file(&path);
}
} else {
// Malformed file name — just clean up
let _ = std::fs::remove_file(&path);
}
}
}
}
has_notified
}
/// Wait until all *.pid.stop files disappear or timeout (3 seconds) expires.
fn wait_for_instances(cache_dir: &PathBuf, current_pid: u32) {
let timeout = Duration::from_secs(3);
let deadline = Instant::now() + timeout;
loop {
let any_stop = std::fs::read_dir(&cache_dir).ok().map_or(false, |entries| {
entries.flatten().any(|entry| {
let path = entry.path();
let matches_pattern = path.extension().map_or(false, |ext| ext == "stop")
&& path
.file_stem()
.map_or(false, |stem| stem.to_string_lossy().ends_with(".pid"));
if matches_pattern {
if let Some(pid) = parse_stop_pid(&path) {
return pid != current_pid;
}
}
false
})
});
if !any_stop {
break;
}
if Instant::now() > deadline {
break;
}
std::thread::sleep(Duration::from_millis(200));
}
}
/// Forcefully kill processes that ignored the shutdown notification and remove their stop files.
fn kill_stale_instances(cache_dir: &PathBuf, current_pid: u32, sys: &mut System) {
if let Ok(entries) = std::fs::read_dir(cache_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().map_or(false, |ext| ext == "stop")
&& path
.file_stem()
.map_or(false, |stem| stem.to_string_lossy().ends_with(".pid"))
{
if let Some(pid) = parse_stop_pid(&path) {
if pid == current_pid {
continue;
}
// is_our_process already refreshed info for this PID,
// so we can directly fetch the process from the cache.
if Self::is_our_process(pid, sys) {
let target_pid = Pid::from(pid as usize);
if let Some(process) = sys.process(target_pid) {
process.kill();
}
}
}
let _ = std::fs::remove_file(&path);
}
}
}
}
/// Spawn a background thread that watches for our stop file and exits the process when it appears.
fn start_stop_monitor(cache_dir: PathBuf, current_pid: u32) {
std::thread::spawn(move || {
let stop_path = cache_dir.join(format!("{}.pid.stop", current_pid));
let pid_path = cache_dir.join(format!("{}.pid", current_pid));
loop {
std::thread::sleep(Duration::from_millis(200));
if stop_path.exists() {
print_text!(
OutputKind::Warning,
"Another instance is starting; shutting down."
);
let _ = std::fs::remove_file(&stop_path);
let _ = std::fs::remove_file(&pid_path);
std::process::exit(EXIT_SUPERSEDED);
}
}
});
}
/// Check if a process with the given PID is our own executable by comparing base file names (case-insensitive).
fn is_our_process(current_pid: u32, sys: &mut System) -> bool {
let our_name = match std::env::current_exe()
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
{
Some(name) => name,
None => return false,
};
if our_name.is_empty() {
return false;
}
let target = Pid::from(current_pid as usize);
sys.refresh_processes(ProcessesToUpdate::Some(&[target]), true);
sys.process(target)
.map(|proc| {
let proc_name = proc.name().to_string_lossy();
!proc_name.is_empty() && proc_name.eq_ignore_ascii_case(&our_name)
})
.unwrap_or(false)
}
/// Acquire the startup lock.
///
/// The lock is held for the whole startup sequence, including waiting for
/// the previous instance to exit (up to ~3s). A single try_lock would
/// therefore fail in the common "cancel → immediately re-run" flow, so we
/// retry until the previous startup finishes.
fn acquire_startup_lock(cache_dir: &PathBuf) -> Result<File> {
let lock_path = cache_dir.join(".run_lock");
let file =
File::create(&lock_path).map_err(|e| anyhow!("Failed to open lock file: {}", e))?;
let deadline = Instant::now() + Duration::from_secs(15);
loop {
match file.try_lock_exclusive() {
Ok(()) => return Ok(file),
Err(e) => {
if Instant::now() >= deadline {
return Err(anyhow!(
"Another vibe-action instance is still starting: {}",
e
));
}
std::thread::sleep(Duration::from_millis(100));
}
}
}
}
}
/// Extract PID from a filename like "123.pid.stop".
fn parse_stop_pid(path: &PathBuf) -> Option<u32> {
path.file_stem()
.and_then(|s| s.to_str())
.and_then(|s| s.strip_suffix(".pid"))
.and_then(|s| s.parse::<u32>().ok())
}
impl Drop for RunGuard {
fn drop(&mut self) {
let _ = fs::remove_file(&self.pid_path);
}
}