1use std::fs::File;
2use std::os::unix::io::AsRawFd;
3use std::path::PathBuf;
4
5pub fn get_config_dir() -> PathBuf {
6 let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
7 PathBuf::from(home).join(".config").join("leta")
8}
9
10pub fn get_config_path() -> PathBuf {
11 get_config_dir().join("config.toml")
12}
13
14pub fn get_cache_dir() -> PathBuf {
15 let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
16 PathBuf::from(home).join(".cache").join("leta")
17}
18
19pub fn get_log_dir() -> PathBuf {
20 get_cache_dir().join("log")
21}
22
23pub fn get_socket_path() -> PathBuf {
24 get_cache_dir().join("daemon.sock")
25}
26
27pub fn get_pid_path() -> PathBuf {
28 get_cache_dir().join("daemon.pid")
29}
30
31pub fn write_pid(path: &std::path::Path, pid: u32) -> std::io::Result<()> {
32 if let Some(parent) = path.parent() {
33 std::fs::create_dir_all(parent)?;
34 }
35 std::fs::write(path, pid.to_string())
36}
37
38pub fn remove_pid(path: &std::path::Path) {
39 let _ = std::fs::remove_file(path);
40}
41
42pub fn read_pid(path: &std::path::Path) -> Option<u32> {
43 std::fs::read_to_string(path)
44 .ok()
45 .and_then(|s| s.trim().parse().ok())
46}
47
48pub fn is_daemon_running() -> bool {
49 let pid_path = get_pid_path();
50 if let Some(pid) = read_pid(&pid_path) {
51 is_process_running(pid)
52 } else {
53 false
54 }
55}
56
57#[cfg(unix)]
58fn is_process_running(pid: u32) -> bool {
59 unsafe { libc::kill(pid as i32, 0) == 0 }
60}
61
62#[cfg(not(unix))]
63fn is_process_running(_pid: u32) -> bool {
64 false
65}
66
67pub fn detect_workspace_root(path: &std::path::Path) -> Option<PathBuf> {
68 let markers = [
69 ".git",
70 "pyproject.toml",
71 "setup.py",
72 "package.json",
73 "Cargo.toml",
74 "go.mod",
75 "pom.xml",
76 "build.gradle",
77 "Gemfile",
78 "composer.json",
79 "mix.exs",
80 "dune-project",
81 ];
82
83 let mut current = path.to_path_buf();
84 loop {
85 for marker in &markers {
86 if current.join(marker).exists() {
87 return Some(current);
88 }
89 }
90 if !current.pop() {
91 break;
92 }
93 }
94 None
95}
96
97pub struct DaemonLock {
98 _file: File,
99}
100
101impl DaemonLock {
102 pub fn acquire() -> Option<DaemonLock> {
103 let lock_path = get_pid_path().with_extension("lock");
104 if let Some(parent) = lock_path.parent() {
105 let _ = std::fs::create_dir_all(parent);
106 }
107
108 let file = match File::options()
109 .write(true)
110 .create(true)
111 .truncate(true)
112 .open(&lock_path)
113 {
114 Ok(f) => f,
115 Err(_) => return None,
116 };
117
118 let fd = file.as_raw_fd();
119 let result = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
120 if result == 0 {
121 Some(DaemonLock { _file: file })
122 } else {
123 None
124 }
125 }
126}