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