]> git.proxmox.com Git - proxmox.git/blob - proxmox-schema/src/upid.rs
clippy fix: needless borrow
[proxmox.git] / proxmox-schema / src / upid.rs
1 use anyhow::{bail, Error};
2
3 use crate::{const_regex, ApiStringFormat, ApiType, Schema, StringSchema};
4
5 /// Unique Process/Task Identifier
6 ///
7 /// We use this to uniquely identify worker task. UPIDs have a short
8 /// string repesentaion, which gives additional information about the
9 /// type of the task. for example:
10 /// ```text
11 /// UPID:{node}:{pid}:{pstart}:{task_id}:{starttime}:{worker_type}:{worker_id}:{userid}:
12 /// UPID:elsa:00004F37:0039E469:00000000:5CA78B83:garbage_collection::root@pam:
13 /// ```
14 /// Please note that we use tokio, so a single thread can run multiple
15 /// tasks.
16 // #[api] - manually implemented API type
17 #[derive(Debug, Clone)]
18 pub struct UPID {
19 /// The Unix PID
20 pub pid: i32, // really libc::pid_t, but we don't want this as a dependency for proxmox-schema
21 /// The Unix process start time from `/proc/pid/stat`
22 pub pstart: u64,
23 /// The task start time (Epoch)
24 pub starttime: i64,
25 /// The task ID (inside the process/thread)
26 pub task_id: usize,
27 /// Worker type (arbitrary ASCII string)
28 pub worker_type: String,
29 /// Worker ID (arbitrary ASCII string)
30 pub worker_id: Option<String>,
31 /// The authenticated entity who started the task
32 pub auth_id: String,
33 /// The node name.
34 pub node: String,
35 }
36
37 const_regex! {
38 pub PROXMOX_UPID_REGEX = concat!(
39 r"^UPID:(?P<node>[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?):(?P<pid>[0-9A-Fa-f]{8}):",
40 r"(?P<pstart>[0-9A-Fa-f]{8,9}):(?P<task_id>[0-9A-Fa-f]{8,16}):(?P<starttime>[0-9A-Fa-f]{8}):",
41 r"(?P<wtype>[^:\s]+):(?P<wid>[^:\s]*):(?P<authid>[^:\s]+):$"
42 );
43 }
44
45 pub const PROXMOX_UPID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&PROXMOX_UPID_REGEX);
46
47 pub const UPID_SCHEMA: Schema = StringSchema::new("Unique Process/Task Identifier")
48 .min_length("UPID:N:12345678:12345678:12345678:::".len())
49 .format(&PROXMOX_UPID_FORMAT)
50 .schema();
51
52 impl ApiType for UPID {
53 const API_SCHEMA: Schema = UPID_SCHEMA;
54 }
55
56 impl std::str::FromStr for UPID {
57 type Err = Error;
58
59 fn from_str(s: &str) -> Result<Self, Self::Err> {
60 if let Some(cap) = PROXMOX_UPID_REGEX.captures(s) {
61 let worker_id = if cap["wid"].is_empty() {
62 None
63 } else {
64 let wid = unescape_id(&cap["wid"])?;
65 Some(wid)
66 };
67
68 Ok(UPID {
69 pid: i32::from_str_radix(&cap["pid"], 16).unwrap(),
70 pstart: u64::from_str_radix(&cap["pstart"], 16).unwrap(),
71 starttime: i64::from_str_radix(&cap["starttime"], 16).unwrap(),
72 task_id: usize::from_str_radix(&cap["task_id"], 16).unwrap(),
73 worker_type: cap["wtype"].to_string(),
74 worker_id,
75 auth_id: cap["authid"].to_string(),
76 node: cap["node"].to_string(),
77 })
78 } else {
79 bail!("unable to parse UPID '{}'", s);
80 }
81 }
82 }
83
84 impl std::fmt::Display for UPID {
85 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
86 let wid = if let Some(ref id) = self.worker_id {
87 escape_id(id)
88 } else {
89 String::new()
90 };
91
92 // Note: pstart can be > 32bit if uptime > 497 days, so this can result in
93 // more that 8 characters for pstart
94
95 write!(
96 f,
97 "UPID:{}:{:08X}:{:08X}:{:08X}:{:08X}:{}:{}:{}:",
98 self.node,
99 self.pid,
100 self.pstart,
101 self.task_id,
102 self.starttime,
103 self.worker_type,
104 wid,
105 self.auth_id
106 )
107 }
108 }
109
110 impl serde::Serialize for UPID {
111 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
112 where
113 S: serde::ser::Serializer,
114 {
115 serializer.serialize_str(&ToString::to_string(self))
116 }
117 }
118
119 impl<'de> serde::Deserialize<'de> for UPID {
120 fn deserialize<D>(deserializer: D) -> Result<UPID, D::Error>
121 where
122 D: serde::Deserializer<'de>,
123 {
124 struct ForwardToStrVisitor;
125
126 impl<'a> serde::de::Visitor<'a> for ForwardToStrVisitor {
127 type Value = UPID;
128
129 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
130 formatter.write_str("a valid UPID")
131 }
132
133 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<UPID, E> {
134 v.parse::<UPID>().map_err(|_| {
135 serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self)
136 })
137 }
138 }
139
140 deserializer.deserialize_str(ForwardToStrVisitor)
141 }
142 }
143
144 // the following two are copied as they're the only `proxmox-systemd` dependencies in this crate,
145 // and this crate has MUCH fewer dependencies without it
146 /// non-path systemd-unit compatible escaping
147 fn escape_id(unit: &str) -> String {
148 use std::fmt::Write;
149
150 let mut escaped = String::new();
151
152 for (i, &c) in unit.as_bytes().iter().enumerate() {
153 if c == b'/' {
154 escaped.push('-');
155 } else if (i == 0 && c == b'.')
156 || !matches!(c, b'_' | b'.' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z')
157 {
158 // unwrap: writing to a String
159 write!(escaped, "\\x{:02x}", c).unwrap();
160 } else {
161 escaped.push(char::from(c));
162 }
163 }
164
165 escaped
166 }
167
168 fn hex_digit(d: u8) -> Result<u8, Error> {
169 match d {
170 b'0'..=b'9' => Ok(d - b'0'),
171 b'A'..=b'F' => Ok(d - b'A' + 10),
172 b'a'..=b'f' => Ok(d - b'a' + 10),
173 _ => bail!("got invalid hex digit"),
174 }
175 }
176
177 /// systemd-unit compatible escaping
178 fn unescape_id(text: &str) -> Result<String, Error> {
179 let mut i = text.as_bytes();
180
181 let mut data: Vec<u8> = Vec::new();
182
183 loop {
184 if i.is_empty() {
185 break;
186 }
187 let next = i[0];
188 if next == b'\\' {
189 if i.len() < 4 || i[1] != b'x' {
190 bail!("error in escape sequence");
191 }
192 let h1 = hex_digit(i[2])?;
193 let h0 = hex_digit(i[3])?;
194 data.push(h1 << 4 | h0);
195 i = &i[4..]
196 } else if next == b'-' {
197 data.push(b'/');
198 i = &i[1..]
199 } else {
200 data.push(next);
201 i = &i[1..]
202 }
203 }
204
205 let text = String::from_utf8(data)?;
206
207 Ok(text)
208 }
209
210 #[cfg(feature = "upid-api-impl")]
211 mod upid_impl {
212 use std::os::unix::ffi::OsStrExt;
213 use std::sync::atomic::{AtomicUsize, Ordering};
214
215 use anyhow::{bail, format_err, Error};
216
217 use super::UPID;
218
219 impl UPID {
220 /// Create a new UPID
221 pub fn new(
222 worker_type: &str,
223 worker_id: Option<String>,
224 auth_id: String,
225 ) -> Result<Self, Error> {
226 let pid = unsafe { libc::getpid() };
227
228 let bad: &[_] = &['/', ':', ' '];
229
230 if worker_type.contains(bad) {
231 bail!("illegal characters in worker type '{}'", worker_type);
232 }
233
234 if auth_id.contains(bad) {
235 bail!("illegal characters in auth_id '{}'", auth_id);
236 }
237
238 static WORKER_TASK_NEXT_ID: AtomicUsize = AtomicUsize::new(0);
239
240 let task_id = WORKER_TASK_NEXT_ID.fetch_add(1, Ordering::SeqCst);
241
242 Ok(UPID {
243 pid,
244 pstart: get_pid_start(pid)?,
245 starttime: epoch_i64(),
246 task_id,
247 worker_type: worker_type.to_owned(),
248 worker_id,
249 auth_id,
250 node: std::str::from_utf8(nix::sys::utsname::uname()?.nodename().as_bytes())
251 .map_err(|_| format_err!("non-utf8 nodename not supported"))?
252 .split('.')
253 .next()
254 .ok_or_else(|| format_err!("failed to get nodename from uname()"))?
255 .to_owned(),
256 })
257 }
258 }
259
260 fn get_pid_start(pid: libc::pid_t) -> Result<u64, Error> {
261 let statstr = String::from_utf8(std::fs::read(format!("/proc/{}/stat", pid))?)?;
262 let cmdend = statstr
263 .rfind(')')
264 .ok_or_else(|| format_err!("missing ')' in /proc/PID/stat"))?;
265 let starttime = statstr[cmdend + 1..]
266 .trim_start()
267 .split_ascii_whitespace()
268 .nth(19)
269 .ok_or_else(|| format_err!("failed to find starttime in /proc/{}/stat", pid))?;
270 starttime.parse().map_err(|err| {
271 format_err!(
272 "failed to parse starttime from /proc/{}/stat ({:?}): {}",
273 pid,
274 starttime,
275 err,
276 )
277 })
278 }
279
280 // Copied as this is the only `proxmox-time` dependency in this crate
281 // and this crate has MUCH fewer dependencies without it
282 fn epoch_i64() -> i64 {
283 use std::time::{SystemTime, UNIX_EPOCH};
284
285 let now = SystemTime::now();
286
287 if now > UNIX_EPOCH {
288 i64::try_from(now.duration_since(UNIX_EPOCH).unwrap().as_secs())
289 .expect("epoch_i64: now is too large")
290 } else {
291 -i64::try_from(UNIX_EPOCH.duration_since(now).unwrap().as_secs())
292 .expect("epoch_i64: now is too small")
293 }
294 }
295 }