nucleus/resources/
cgroup.rs1use crate::error::{NucleusError, Result, StateTransition};
2use crate::resources::{CgroupState, ResourceLimits};
3use std::fs;
4use std::path::{Path, PathBuf};
5use tracing::{debug, info};
6
7const CGROUP_V2_ROOT: &str = "/sys/fs/cgroup";
8
9pub struct Cgroup {
14 path: PathBuf,
15 state: CgroupState,
16}
17
18impl Cgroup {
19 pub fn create(name: &str) -> Result<Self> {
23 let state = CgroupState::Nonexistent.transition(CgroupState::Created)?;
24 let path = PathBuf::from(CGROUP_V2_ROOT).join(name);
25
26 info!("Creating cgroup at {:?}", path);
27
28 fs::create_dir_all(&path).map_err(|e| {
30 NucleusError::CgroupError(format!("Failed to create cgroup directory: {}", e))
31 })?;
32
33 Ok(Self { path, state })
34 }
35
36 pub fn set_limits(&mut self, limits: &ResourceLimits) -> Result<()> {
40 self.state = self.state.transition(CgroupState::Configured)?;
41
42 info!("Configuring cgroup limits: {:?}", limits);
43
44 if let Some(memory_bytes) = limits.memory_bytes {
46 self.write_value("memory.max", &memory_bytes.to_string())?;
47 debug!("Set memory.max = {}", memory_bytes);
48 }
49
50 if let Some(memory_high) = limits.memory_high {
52 self.write_value("memory.high", &memory_high.to_string())?;
53 debug!("Set memory.high = {}", memory_high);
54 }
55
56 if let Some(swap_max) = limits.memory_swap_max {
58 self.write_value("memory.swap.max", &swap_max.to_string())?;
59 debug!("Set memory.swap.max = {}", swap_max);
60 }
61
62 if let Some(cpu_quota_us) = limits.cpu_quota_us {
64 let cpu_max = format!("{} {}", cpu_quota_us, limits.cpu_period_us);
65 self.write_value("cpu.max", &cpu_max)?;
66 debug!("Set cpu.max = {}", cpu_max);
67 }
68
69 if let Some(cpu_weight) = limits.cpu_weight {
71 self.write_value("cpu.weight", &cpu_weight.to_string())?;
72 debug!("Set cpu.weight = {}", cpu_weight);
73 }
74
75 if let Some(pids_max) = limits.pids_max {
77 self.write_value("pids.max", &pids_max.to_string())?;
78 debug!("Set pids.max = {}", pids_max);
79 }
80
81 for io_limit in &limits.io_limits {
83 let line = io_limit.to_io_max_line();
84 self.write_value("io.max", &line)?;
85 debug!("Set io.max: {}", line);
86 }
87
88 info!("Successfully configured cgroup limits");
89
90 Ok(())
91 }
92
93 pub fn attach_process(&mut self, pid: u32) -> Result<()> {
97 self.state = self.state.transition(CgroupState::Attached)?;
98
99 info!("Attaching process {} to cgroup", pid);
100
101 self.write_value("cgroup.procs", &pid.to_string())?;
102
103 info!("Successfully attached process to cgroup");
104
105 Ok(())
106 }
107
108 fn write_value(&self, file: &str, value: &str) -> Result<()> {
110 let file_path = self.path.join(file);
111 fs::write(&file_path, value).map_err(|e| {
112 NucleusError::CgroupError(format!(
113 "Failed to write {} to {:?}: {}",
114 value, file_path, e
115 ))
116 })?;
117 Ok(())
118 }
119
120 fn read_value(&self, file: &str) -> Result<String> {
122 let file_path = self.path.join(file);
123 fs::read_to_string(&file_path).map_err(|e| {
124 NucleusError::CgroupError(format!("Failed to read {:?}: {}", file_path, e))
125 })
126 }
127
128 pub fn memory_current(&self) -> Result<u64> {
130 let value = self.read_value("memory.current")?;
131 value.trim().parse().map_err(|e| {
132 NucleusError::CgroupError(format!("Failed to parse memory.current: {}", e))
133 })
134 }
135
136 pub fn path(&self) -> &Path {
138 &self.path
139 }
140
141 pub fn state(&self) -> CgroupState {
143 self.state
144 }
145
146 pub fn cleanup(mut self) -> Result<()> {
150 info!("Cleaning up cgroup {:?}", self.path);
151
152 if self.path.exists() {
155 fs::remove_dir(&self.path).map_err(|e| {
156 NucleusError::CgroupError(format!("Failed to remove cgroup: {}", e))
159 })?;
160 }
161
162 self.state = CgroupState::Removed;
164 info!("Successfully cleaned up cgroup");
165
166 Ok(())
167 }
168}
169
170impl Drop for Cgroup {
171 fn drop(&mut self) {
172 if !self.state.is_terminal() && self.path.exists() {
173 let _ = fs::remove_dir(&self.path);
174 }
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn test_resource_limits_unlimited() {
184 let limits = ResourceLimits::unlimited();
185 assert!(limits.memory_bytes.is_none());
186 assert!(limits.memory_high.is_none());
187 assert!(limits.memory_swap_max.is_none());
188 assert!(limits.cpu_quota_us.is_none());
189 assert!(limits.cpu_weight.is_none());
190 assert!(limits.pids_max.is_none());
191 assert!(limits.io_limits.is_empty());
192 }
193
194 #[test]
198 fn test_cleanup_sets_removed_only_after_success() {
199 let source = include_str!("cgroup.rs");
203 let fn_start = source.find("pub fn cleanup").unwrap();
204 let after = &source[fn_start..];
205 let open = after.find('{').unwrap();
206 let mut depth = 0u32;
207 let mut fn_end = open;
208 for (i, ch) in after[open..].char_indices() {
209 match ch {
210 '{' => depth += 1,
211 '}' => {
212 depth -= 1;
213 if depth == 0 {
214 fn_end = open + i + 1;
215 break;
216 }
217 }
218 _ => {}
219 }
220 }
221 let cleanup_body = &after[..fn_end];
222 let removed_pos = cleanup_body
223 .find("Removed")
224 .expect("must reference Removed state");
225 let remove_dir_pos = cleanup_body
226 .find("remove_dir")
227 .expect("must call remove_dir");
228 assert!(
229 removed_pos > remove_dir_pos,
230 "CgroupState::Removed must be set AFTER remove_dir succeeds, not before"
231 );
232 }
233}