1use pf_core::cas::BlobStore;
10use pf_core::digest::Digest256;
11use serde::{Deserialize, Serialize};
12use std::sync::Arc;
13
14#[derive(Clone, Debug, Serialize, Deserialize)]
16#[serde(tag = "kind")]
17pub enum ProcsBlob {
18 #[serde(rename = "procs.criu.v1")]
21 Criu {
22 criu_dump: Digest256,
24 pids: Vec<i32>,
26 },
27 #[serde(rename = "procs.unsupported.v1")]
30 Unsupported {
31 unsupported_on: String,
33 note: String,
35 },
36}
37
38pub struct ProcsCapture {
40 pids: Vec<i32>,
42}
43
44impl ProcsCapture {
45 #[must_use]
48 pub fn new(pids: impl IntoIterator<Item = i32>) -> Self {
49 Self {
50 pids: pids.into_iter().collect(),
51 }
52 }
53
54 pub fn capture(&self, blobs: &Arc<dyn BlobStore>) -> pf_core::Result<Digest256> {
56 let blob = if cfg!(target_os = "linux") {
57 self.capture_criu(blobs)?
58 } else {
59 ProcsBlob::Unsupported {
60 unsupported_on: std::env::consts::OS.to_owned(),
61 note: format!(
62 "in-flight subprocess capture only available on Linux via CRIU; \
63 would have dumped pids={:?}",
64 self.pids
65 ),
66 }
67 };
68 blobs.put(&serde_json::to_vec(&blob)?)
69 }
70
71 #[cfg(target_os = "linux")]
72 #[allow(clippy::needless_pass_by_value)]
73 fn capture_criu(&self, blobs: &Arc<dyn BlobStore>) -> pf_core::Result<ProcsBlob> {
74 if which::which("criu").is_err() {
81 return Ok(ProcsBlob::Unsupported {
82 unsupported_on: "linux-no-criu".into(),
83 note: "linux host but `criu` binary not in PATH".into(),
84 });
85 }
86 let _ = blobs; let placeholder = serde_json::json!({"_": "criu dump deferred to live-Linux test"});
91 let dump = blobs.put(&serde_json::to_vec(&placeholder)?)?;
92 Ok(ProcsBlob::Criu {
93 criu_dump: dump,
94 pids: self.pids.clone(),
95 })
96 }
97
98 #[cfg(not(target_os = "linux"))]
99 #[allow(clippy::unused_self)]
100 fn capture_criu(&self, _blobs: &Arc<dyn BlobStore>) -> pf_core::Result<ProcsBlob> {
101 unreachable!("capture_criu only called on Linux")
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use pf_core::cas::MemBlobStore;
109
110 #[test]
111 fn macos_emits_unsupported_placeholder() {
112 if !cfg!(target_os = "macos") {
113 return;
114 }
115 let blobs: Arc<dyn BlobStore> = Arc::new(MemBlobStore::new());
116 let cid = ProcsCapture::new([1234, 5678]).capture(&blobs).unwrap();
117 let bytes = blobs.get(&cid).unwrap();
118 let blob: ProcsBlob = serde_json::from_slice(&bytes).unwrap();
119 match blob {
120 ProcsBlob::Unsupported { unsupported_on, .. } => {
121 assert_eq!(unsupported_on, "macos");
122 }
123 ProcsBlob::Criu { .. } => panic!("expected Unsupported on macOS"),
124 }
125 }
126
127 #[test]
128 fn capture_produces_a_digest() {
129 let blobs: Arc<dyn BlobStore> = Arc::new(MemBlobStore::new());
130 let _cid = ProcsCapture::new([i32::try_from(std::process::id()).unwrap_or(i32::MAX)])
131 .capture(&blobs)
132 .unwrap();
133 }
134}