1use crate::fsutil::sync_dir_best_effort;
17use anyhow::{Context, Result, anyhow};
18use std::fs;
19use std::io::Write;
20use std::path::Path;
21
22pub(crate) const OWNER_FILE_NAME: &str = "owner";
23pub const TASK_OWNER_PREFIX: &str = "owner_task_";
24
25#[derive(Debug, Clone)]
27pub struct LockOwner {
28 pub pid: u32,
29 pub started_at: String,
30 pub command: String,
31 pub label: String,
32}
33
34impl LockOwner {
35 pub(crate) fn render(&self) -> String {
36 format!(
37 "pid: {}\nstarted_at: {}\ncommand: {}\nlabel: {}\n",
38 self.pid, self.started_at, self.command, self.label
39 )
40 }
41}
42
43pub fn read_lock_owner(lock_dir: &Path) -> Result<Option<LockOwner>> {
44 let owner_path = lock_dir.join(OWNER_FILE_NAME);
45 let raw = match fs::read_to_string(&owner_path) {
46 Ok(raw) => raw,
47 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
48 Err(err) => {
49 return Err(anyhow!(err))
50 .with_context(|| format!("read lock owner {}", owner_path.display()));
51 }
52 };
53 Ok(parse_lock_owner(&raw))
54}
55
56pub(crate) fn write_lock_owner(owner_path: &Path, owner: &LockOwner) -> Result<()> {
57 log::debug!("writing lock owner: {}", owner_path.display());
58 let mut file = fs::File::create(owner_path)
59 .with_context(|| format!("create lock owner {}", owner_path.display()))?;
60 file.write_all(owner.render().as_bytes())
61 .context("write lock owner")?;
62 file.flush().context("flush lock owner")?;
63 file.sync_all().context("sync lock owner")?;
64 if let Some(parent) = owner_path.parent() {
65 sync_dir_best_effort(parent);
66 }
67 Ok(())
68}
69
70pub(crate) fn parse_lock_owner(raw: &str) -> Option<LockOwner> {
71 let mut pid = None;
72 let mut started_at = None;
73 let mut command = None;
74 let mut label = None;
75
76 for line in raw.lines() {
77 let trimmed = line.trim();
78 if trimmed.is_empty() {
79 continue;
80 }
81 if let Some((key, value)) = trimmed.split_once(':') {
82 let value = value.trim().to_string();
83 match key.trim() {
84 "pid" => {
85 pid = value
86 .parse::<u32>()
87 .inspect_err(|error| {
88 log::debug!("Lock file has invalid pid '{}': {}", value, error)
89 })
90 .ok()
91 }
92 "started_at" => started_at = Some(value),
93 "command" => command = Some(value),
94 "label" => label = Some(value),
95 _ => {}
96 }
97 }
98 }
99
100 let pid = pid?;
101 Some(LockOwner {
102 pid,
103 started_at: started_at.unwrap_or_else(|| "unknown".to_string()),
104 command: command.unwrap_or_else(|| "unknown".to_string()),
105 label: label.unwrap_or_else(|| "unknown".to_string()),
106 })
107}
108
109pub(crate) fn command_line() -> String {
110 let joined = std::env::args().collect::<Vec<_>>().join(" ");
111 let trimmed = joined.trim();
112 if trimmed.is_empty() {
113 "unknown".to_string()
114 } else {
115 trimmed.to_string()
116 }
117}
118
119pub(crate) fn is_supervising_label(label: &str) -> bool {
120 matches!(label, "run one" | "run loop")
121}
122
123pub fn is_task_owner_file(name: &str) -> bool {
124 name.starts_with(TASK_OWNER_PREFIX)
125}
126
127pub(crate) fn is_task_sidecar_owner(owner_path: &Path) -> bool {
128 owner_path
129 .file_name()
130 .and_then(|name| name.to_str())
131 .map(is_task_owner_file)
132 .unwrap_or(false)
133}