1use serde::{Deserialize, Serialize};
2use sim_platform_core::{Lifecycle, OpenSymbol, RefusalKind, ResolutionRefusal};
3use std::{
4 collections::{BTreeMap, BTreeSet, VecDeque},
5 path::PathBuf,
6 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
7};
8
9#[derive(Clone, Copy, Debug, Default)]
11pub struct LinuxWallClock;
12
13impl sim_lib_server::WallClock for LinuxWallClock {
14 fn now(&self) -> sim_kernel::Result<sim_lib_server::WallTimestamp> {
15 let elapsed = SystemTime::now()
16 .duration_since(UNIX_EPOCH)
17 .map_err(|error| {
18 sim_kernel::Error::HostError(format!(
19 "Linux wall clock precedes Unix epoch: {error}"
20 ))
21 })?;
22 let unix_millis = u64::try_from(elapsed.as_millis()).map_err(|_| {
23 sim_kernel::Error::HostError("Linux wall clock exceeds u64 milliseconds".to_owned())
24 })?;
25 Ok(sim_lib_server::WallTimestamp::from_unix_millis(unix_millis))
26 }
27}
28
29#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
31pub struct HostFacts {
32 pub locale: String,
33 pub timezone: String,
34 pub memory_bytes: u64,
35 pub storage_bytes: u64,
36 pub parallelism: u16,
37 pub pressure: u8,
38}
39
40#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct XdgMounts {
43 roots: BTreeMap<OpenSymbol, PathBuf>,
44}
45impl XdgMounts {
46 #[must_use]
48 pub fn new(
49 config: PathBuf,
50 cache: PathBuf,
51 data: PathBuf,
52 state: PathBuf,
53 temp: PathBuf,
54 ) -> Self {
55 Self {
56 roots: BTreeMap::from([
57 (OpenSymbol("mount/xdg-config".into()), config),
58 (OpenSymbol("mount/xdg-cache".into()), cache),
59 (OpenSymbol("mount/xdg-data".into()), data),
60 (OpenSymbol("mount/xdg-state".into()), state),
61 (OpenSymbol("mount/temp".into()), temp),
62 ]),
63 }
64 }
65 #[must_use]
66 pub fn contains(&self, mount: &OpenSymbol) -> bool {
67 self.roots.contains_key(mount)
68 }
69 #[must_use]
70 pub fn names(&self) -> Vec<OpenSymbol> {
71 self.roots.keys().cloned().collect()
72 }
73}
74
75pub trait Portal: Send {
77 fn call(&mut self, operation: DesktopOperation) -> Result<PortalReply, PortalError>;
80 fn cancel(&mut self, token: u64);
81 fn cleanup(&mut self);
82}
83#[derive(Clone, Debug, Eq, PartialEq)]
84pub enum DesktopOperation {
85 Open(String),
86 Share(String),
87 Notify(String),
88 ClipboardRead,
89 ClipboardWrite(String),
90 PermissionStatus(String),
91 PermissionRequest(String),
92 KeepAwake(bool),
93 Activate(String),
94}
95#[derive(Clone, Debug, Eq, PartialEq)]
96pub enum PortalReply {
97 Accepted,
98 Text(String),
99 Permission(Permission),
100}
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
102pub enum Permission {
103 Granted,
104 Denied,
105 Revoked,
106}
107#[derive(Clone, Debug, Eq, PartialEq)]
108pub enum PortalError {
109 Unsupported,
110 Denied,
111 Revoked,
112 Cancelled,
113}
114
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117pub struct Budget {
118 pub requests: u32,
119 pub queue: usize,
120 pub entropy_bytes: usize,
121 pub timer_ns: u64,
122}
123
124#[derive(Clone, Debug, Eq, PartialEq)]
126pub enum Request {
127 WallClock,
128 MonotonicClock,
129 Timer(u64),
130 Entropy(usize),
131 Locale,
132 Timezone,
133 Pressure,
134 Limits,
135 Mount(OpenSymbol),
136 Desktop(DesktopOperation),
137 Lifecycle,
138 Cancel(u64),
139}
140#[derive(Clone, Debug, Eq, PartialEq)]
141pub enum Reply {
142 Integer(i128),
143 Bytes(Vec<u8>),
144 Text(String),
145 Limits {
146 memory_bytes: u64,
147 storage_bytes: u64,
148 parallelism: u16,
149 },
150 Mount(OpenSymbol),
151 Portal(PortalReply),
152 Lifecycle(Lifecycle),
153 Cancelled,
154}
155
156pub struct Capsule<P: Portal> {
158 facts: HostFacts,
159 mounts: XdgMounts,
160 portal: Option<P>,
161 budget: Budget,
162 used: u32,
163 entropy_used: usize,
164 seed: u64,
165 start: Instant,
166 lifecycle: Lifecycle,
167 queue: VecDeque<u64>,
168 cancelled: BTreeSet<u64>,
169}
170impl<P: Portal> Capsule<P> {
171 #[must_use]
172 pub fn new(
173 facts: HostFacts,
174 mounts: XdgMounts,
175 portal: Option<P>,
176 budget: Budget,
177 seed: u64,
178 ) -> Self {
179 Self {
180 facts,
181 mounts,
182 portal,
183 budget,
184 used: 0,
185 entropy_used: 0,
186 seed: seed.max(1),
187 start: Instant::now(),
188 lifecycle: Lifecycle::Ready,
189 queue: VecDeque::new(),
190 cancelled: BTreeSet::new(),
191 }
192 }
193 pub fn suspend(&mut self) {
194 self.lifecycle = Lifecycle::Suspended;
195 }
196 pub fn resume(&mut self) {
197 self.lifecycle = Lifecycle::Ready;
198 }
199 pub fn revoke(&mut self, token: u64) {
200 self.cancelled.insert(token);
201 }
202 pub fn apply(&mut self, token: u64, request: Request) -> Result<Reply, ResolutionRefusal> {
208 if self.lifecycle == Lifecycle::Suspended {
209 return Err(refusal(RefusalKind::Suspended, "activation is suspended"));
210 }
211 if self.cancelled.contains(&token) {
212 return Err(refusal(
213 RefusalKind::Cancelled,
214 "request is cancelled or revoked",
215 ));
216 }
217 if self.used >= self.budget.requests || self.queue.len() >= self.budget.queue {
218 return Err(refusal(
219 RefusalKind::BudgetExhausted,
220 "activation budget exhausted",
221 ));
222 }
223 self.used += 1;
224 self.queue.push_back(token);
225 let result = self.execute(token, request);
226 self.queue.pop_front();
227 result
228 }
229 fn execute(&mut self, _token: u64, request: Request) -> Result<Reply, ResolutionRefusal> {
230 match request {
231 Request::WallClock => Ok(Reply::Integer(
232 SystemTime::now()
233 .duration_since(UNIX_EPOCH)
234 .map_err(|_| refusal(RefusalKind::ProviderFault, "wall clock before epoch"))?
235 .as_nanos()
236 .cast_signed(),
237 )),
238 Request::MonotonicClock => Ok(Reply::Integer(
239 self.start.elapsed().as_nanos().cast_signed(),
240 )),
241 Request::Timer(ns) if ns <= self.budget.timer_ns => {
242 std::thread::sleep(Duration::from_nanos(ns));
243 Ok(Reply::Integer(
244 self.start.elapsed().as_nanos().cast_signed(),
245 ))
246 }
247 Request::Timer(_) => Err(refusal(
248 RefusalKind::BudgetExhausted,
249 "timer budget exceeded",
250 )),
251 Request::Entropy(bytes)
252 if self.entropy_used.saturating_add(bytes) <= self.budget.entropy_bytes =>
253 {
254 self.entropy_used += bytes;
255 Ok(Reply::Bytes(self.entropy(bytes)))
256 }
257 Request::Entropy(_) => Err(refusal(
258 RefusalKind::BudgetExhausted,
259 "entropy budget exceeded",
260 )),
261 Request::Locale => Ok(Reply::Text(self.facts.locale.clone())),
262 Request::Timezone => Ok(Reply::Text(self.facts.timezone.clone())),
263 Request::Pressure => Ok(Reply::Integer(i128::from(self.facts.pressure))),
264 Request::Limits => Ok(Reply::Limits {
265 memory_bytes: self.facts.memory_bytes,
266 storage_bytes: self.facts.storage_bytes,
267 parallelism: self.facts.parallelism,
268 }),
269 Request::Mount(name) if self.mounts.contains(&name) => Ok(Reply::Mount(name)),
270 Request::Mount(_) => Err(refusal(RefusalKind::Unsupported, "mount was not preopened")),
271 Request::Desktop(op) => self
272 .portal
273 .as_mut()
274 .ok_or_else(|| refusal(RefusalKind::Unsupported, "desktop service is absent"))?
275 .call(op)
276 .map(Reply::Portal)
277 .map_err(|e| {
278 refusal(
279 match e {
280 PortalError::Unsupported => RefusalKind::Unsupported,
281 PortalError::Denied | PortalError::Revoked => RefusalKind::Denied,
282 PortalError::Cancelled => RefusalKind::Cancelled,
283 },
284 "portal refused request",
285 )
286 }),
287 Request::Lifecycle => Ok(Reply::Lifecycle(self.lifecycle.clone())),
288 Request::Cancel(cancelled) => {
289 self.cancelled.insert(cancelled);
290 if let Some(portal) = &mut self.portal {
291 portal.cancel(cancelled);
292 }
293 Ok(Reply::Cancelled)
294 }
295 }
296 }
297 fn entropy(&mut self, count: usize) -> Vec<u8> {
298 (0..count)
299 .map(|_| {
300 self.seed ^= self.seed << 13;
301 self.seed ^= self.seed >> 7;
302 self.seed ^= self.seed << 17;
303 self.seed.to_le_bytes()[0]
304 })
305 .collect()
306 }
307}
308impl<P: Portal> Drop for Capsule<P> {
309 fn drop(&mut self) {
310 self.lifecycle = Lifecycle::Stopped;
311 self.queue.clear();
312 if let Some(portal) = &mut self.portal {
313 portal.cleanup();
314 }
315 }
316}
317fn refusal(kind: RefusalKind, detail: &str) -> ResolutionRefusal {
318 ResolutionRefusal {
319 request: OpenSymbol("request/ubuntu-pc".into()),
320 service: OpenSymbol("service/ubuntu-pc".into()),
321 kind,
322 detail: detail.into(),
323 }
324}
325
326pub struct HeadlessPortal;
328impl Portal for HeadlessPortal {
329 fn call(&mut self, _: DesktopOperation) -> Result<PortalReply, PortalError> {
330 Err(PortalError::Unsupported)
331 }
332 fn cancel(&mut self, _: u64) {}
333 fn cleanup(&mut self) {}
334}