1use super::{Identity, Template};
9use anyhow::Result;
10use serde::{Deserialize, Serialize};
11use std::path::PathBuf;
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
15pub enum IsolationLevel {
16 Podman,
18 Namespace,
20 #[default]
22 None,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SpaceConfig {
28 pub isolation: IsolationLevel,
30
31 pub template: Option<String>,
33
34 pub workdir: PathBuf,
36
37 pub env: Vec<(String, String)>,
39
40 pub mounts: Vec<(PathBuf, PathBuf)>,
42
43 pub memory_limit: u64,
45
46 pub cpu_limit: f32,
48
49 pub network: NetworkConfig,
51}
52
53impl Default for SpaceConfig {
54 fn default() -> Self {
55 SpaceConfig {
56 isolation: IsolationLevel::None,
57 template: None,
58 workdir: PathBuf::from("."),
59 env: Vec::new(),
60 mounts: Vec::new(),
61 memory_limit: 0,
62 cpu_limit: 0.0,
63 network: NetworkConfig::default(),
64 }
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, Default)]
70pub struct NetworkConfig {
71 pub internet: bool,
73 pub host_access: bool,
75 pub exposed_ports: Vec<u16>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct UserSpace {
82 pub identity: Identity,
84
85 pub config: SpaceConfig,
87
88 pub container_id: Option<String>,
90
91 pub socket_path: Option<PathBuf>,
93
94 pub shell_pid: Option<u32>,
96
97 pub created_at: u64,
99
100 pub last_active: u64,
102}
103
104impl UserSpace {
105 pub fn new(identity: Identity, config: SpaceConfig) -> Self {
107 let now = std::time::SystemTime::now()
108 .duration_since(std::time::UNIX_EPOCH)
109 .unwrap_or_default()
110 .as_secs();
111
112 UserSpace {
113 identity,
114 config,
115 container_id: None,
116 socket_path: None,
117 shell_pid: None,
118 created_at: now,
119 last_active: now,
120 }
121 }
122
123 pub fn with_template(identity: Identity, template: &Template) -> Self {
125 let config = SpaceConfig {
126 template: Some(template.name.clone()),
127 isolation: template.default_isolation.clone(),
128 env: template.env.clone(),
129 ..Default::default()
130 };
131 Self::new(identity, config)
132 }
133
134 pub async fn start(&mut self) -> Result<()> {
136 match self.config.isolation {
137 IsolationLevel::Podman => self.start_podman().await,
138 IsolationLevel::Namespace => self.start_namespace().await,
139 IsolationLevel::None => self.start_direct().await,
140 }
141 }
142
143 async fn start_podman(&mut self) -> Result<()> {
145 tracing::info!("Starting podman container for {}", self.identity);
148 Ok(())
149 }
150
151 async fn start_namespace(&mut self) -> Result<()> {
153 tracing::info!("Starting namespace for {}", self.identity);
156 Ok(())
157 }
158
159 async fn start_direct(&mut self) -> Result<()> {
161 tracing::info!("Starting direct space for {}", self.identity);
162 Ok(())
164 }
165
166 pub async fn stop(&mut self) -> Result<()> {
168 match self.config.isolation {
169 IsolationLevel::Podman => {
170 if let Some(ref id) = self.container_id {
171 tracing::info!("Stopping podman container {}", id);
173 }
174 }
175 IsolationLevel::Namespace => {
176 if let Some(pid) = self.shell_pid {
177 tracing::info!("Stopping namespace pid {}", pid);
179 }
180 }
181 IsolationLevel::None => {
182 }
184 }
185 self.container_id = None;
186 self.shell_pid = None;
187 Ok(())
188 }
189
190 pub async fn exec(&self, command: &[&str]) -> Result<String> {
192 match self.config.isolation {
193 IsolationLevel::Podman => {
194 if let Some(ref id) = self.container_id {
195 tracing::debug!("Exec in podman {}: {:?}", id, command);
197 }
198 }
199 IsolationLevel::Namespace => {
200 if let Some(pid) = self.shell_pid {
201 tracing::debug!("Exec in namespace {}: {:?}", pid, command);
203 }
204 }
205 IsolationLevel::None => {
206 tracing::debug!("Direct exec: {:?}", command);
208 }
209 }
210 Ok(String::new())
212 }
213
214 pub async fn share_terminal(&self, with: &Identity) -> Result<String> {
216 let session_id = format!(
218 "term-{}-{}",
219 self.identity.username,
220 std::time::SystemTime::now()
221 .duration_since(std::time::UNIX_EPOCH)
222 .unwrap()
223 .as_millis()
224 );
225 tracing::info!(
226 "Sharing terminal {} with {}",
227 session_id,
228 with.canonical()
229 );
230 Ok(session_id)
231 }
232
233 pub fn touch(&mut self) {
235 self.last_active = std::time::SystemTime::now()
236 .duration_since(std::time::UNIX_EPOCH)
237 .unwrap_or_default()
238 .as_secs();
239 }
240
241 pub fn is_idle(&self, timeout_secs: u64) -> bool {
243 let now = std::time::SystemTime::now()
244 .duration_since(std::time::UNIX_EPOCH)
245 .unwrap_or_default()
246 .as_secs();
247 now - self.last_active > timeout_secs
248 }
249}
250
251#[derive(Debug, Default)]
253pub struct SpaceManager {
254 spaces: std::collections::HashMap<String, UserSpace>,
256}
257
258impl SpaceManager {
259 pub fn new() -> Self {
260 SpaceManager {
261 spaces: std::collections::HashMap::new(),
262 }
263 }
264
265 pub async fn get_or_create(
267 &mut self,
268 identity: Identity,
269 config: SpaceConfig,
270 ) -> Result<&mut UserSpace> {
271 let key = identity.canonical();
272 if !self.spaces.contains_key(&key) {
273 let mut space = UserSpace::new(identity, config);
274 space.start().await?;
275 self.spaces.insert(key.clone(), space);
276 }
277 Ok(self.spaces.get_mut(&key).unwrap())
278 }
279
280 pub fn get(&self, identity: &Identity) -> Option<&UserSpace> {
282 self.spaces.get(&identity.canonical())
283 }
284
285 pub async fn remove(&mut self, identity: &Identity) -> Result<()> {
287 let key = identity.canonical();
288 if let Some(mut space) = self.spaces.remove(&key) {
289 space.stop().await?;
290 }
291 Ok(())
292 }
293
294 pub fn list(&self) -> Vec<&UserSpace> {
296 self.spaces.values().collect()
297 }
298
299 pub async fn cleanup_idle(&mut self, timeout_secs: u64) -> Result<usize> {
301 let idle: Vec<String> = self
302 .spaces
303 .iter()
304 .filter(|(_, s)| s.is_idle(timeout_secs))
305 .map(|(k, _)| k.clone())
306 .collect();
307
308 let count = idle.len();
309 for key in idle {
310 if let Some(mut space) = self.spaces.remove(&key) {
311 space.stop().await?;
312 }
313 }
314 Ok(count)
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn test_space_config_default() {
324 let config = SpaceConfig::default();
325 assert_eq!(config.isolation, IsolationLevel::None);
326 assert!(config.template.is_none());
327 }
328
329 #[test]
330 fn test_user_space_creation() {
331 let identity = Identity::local("test");
332 let space = UserSpace::new(identity.clone(), SpaceConfig::default());
333 assert_eq!(space.identity, identity);
334 assert!(space.container_id.is_none());
335 }
336}