⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/browser/
tab_isolation.rs

1//! Tab Process Isolation
2//!
3//! Provides per-tab sandboxing with separate DOM trees, JS virtual machines,
4//! and GC heaps. Each tab runs as an isolated "process" with restricted
5//! capabilities. Includes crash recovery, IPC between tabs, and resource
6//! limits to prevent any single tab from monopolizing the system.
7
8#![allow(dead_code)]
9
10use alloc::{
11    collections::BTreeMap,
12    format,
13    string::{String, ToString},
14    vec::Vec,
15};
16
17use super::{dom_bindings::DomApi, js_gc::GcHeap, js_vm::JsVm, tabs::TabId};
18
19// ---------------------------------------------------------------------------
20// Tab Error Type
21// ---------------------------------------------------------------------------
22
23/// Errors from tab process isolation
24#[derive(Debug, Clone)]
25pub enum TabError {
26    /// Maximum process limit reached
27    ProcessLimitReached,
28    /// Process already exists for this tab
29    ProcessAlreadyExists,
30    /// No process found for the given tab
31    ProcessNotFound,
32    /// Tab is not in the expected state
33    InvalidState { expected: &'static str },
34    /// A capability is not permitted
35    CapabilityDenied { capability: &'static str },
36    /// A resource limit was violated
37    ResourceLimitViolation { message: String },
38    /// JavaScript execution failed
39    ScriptError { message: String },
40}
41
42impl core::fmt::Display for TabError {
43    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
44        match self {
45            Self::ProcessLimitReached => write!(f, "Maximum process limit reached"),
46            Self::ProcessAlreadyExists => write!(f, "Process already exists for this tab"),
47            Self::ProcessNotFound => write!(f, "No process for tab"),
48            Self::InvalidState { expected } => {
49                write!(f, "Tab is not in {} state", expected)
50            }
51            Self::CapabilityDenied { capability } => {
52                write!(f, "{} not permitted", capability)
53            }
54            Self::ResourceLimitViolation { message } => write!(f, "{}", message),
55            Self::ScriptError { message } => write!(f, "{}", message),
56        }
57    }
58}
59
60// ---------------------------------------------------------------------------
61// Tab process state
62// ---------------------------------------------------------------------------
63
64/// State of a tab process
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
66pub enum TabProcessState {
67    /// Not yet started
68    #[default]
69    Created,
70    /// Running normally
71    Running,
72    /// Suspended (e.g., background tab)
73    Suspended,
74    /// Crashed and awaiting recovery
75    Crashed,
76    /// Terminated / cleaned up
77    Terminated,
78}
79
80// ---------------------------------------------------------------------------
81// Resource limits
82// ---------------------------------------------------------------------------
83
84/// Resource limits for a tab process
85#[derive(Debug, Clone)]
86pub struct ResourceLimits {
87    /// Maximum JS heap size in bytes
88    pub max_heap_bytes: usize,
89    /// Maximum DOM nodes
90    pub max_dom_nodes: usize,
91    /// Maximum timers
92    pub max_timers: usize,
93    /// Maximum execution steps per tick
94    pub max_steps_per_tick: usize,
95    /// Maximum network connections (future)
96    pub max_connections: usize,
97    /// CPU time budget per tick (in microseconds)
98    pub cpu_budget_us: u64,
99}
100
101impl Default for ResourceLimits {
102    fn default() -> Self {
103        Self {
104            max_heap_bytes: 64 * 1024 * 1024, // 64 MB
105            max_dom_nodes: 100_000,
106            max_timers: 1000,
107            max_steps_per_tick: 1_000_000,
108            max_connections: 16,
109            cpu_budget_us: 50_000, // 50ms
110        }
111    }
112}
113
114// ---------------------------------------------------------------------------
115// Resource usage tracking
116// ---------------------------------------------------------------------------
117
118/// Current resource usage for a tab
119#[derive(Debug, Clone, Default)]
120pub struct ResourceUsage {
121    /// Current heap bytes
122    pub heap_bytes: usize,
123    /// Current DOM node count
124    pub dom_node_count: usize,
125    /// Current timer count
126    pub timer_count: usize,
127    /// Total JS execution steps
128    pub total_steps: u64,
129    /// Steps this tick
130    pub steps_this_tick: usize,
131    /// Total ticks processed
132    pub ticks_processed: u64,
133    /// Number of GC collections
134    pub gc_collections: usize,
135    /// Number of crashes
136    pub crash_count: usize,
137}
138
139// ---------------------------------------------------------------------------
140// IPC message between tabs
141// ---------------------------------------------------------------------------
142
143/// Message types for inter-tab communication
144#[derive(Debug, Clone)]
145pub enum IpcMessage {
146    /// postMessage-style string message
147    PostMessage {
148        source_tab: TabId,
149        target_tab: TabId,
150        origin: String,
151        data: String,
152    },
153    /// Broadcast channel message
154    BroadcastMessage {
155        source_tab: TabId,
156        channel: String,
157        data: String,
158    },
159    /// Storage event (localStorage change)
160    StorageEvent {
161        key: String,
162        old_value: Option<String>,
163        new_value: Option<String>,
164    },
165    /// Tab lifecycle notification
166    TabEvent {
167        tab_id: TabId,
168        event: TabLifecycleEvent,
169    },
170}
171
172/// Tab lifecycle events for IPC
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum TabLifecycleEvent {
175    /// Tab was created
176    Created,
177    /// Tab became active
178    Activated,
179    /// Tab was deactivated
180    Deactivated,
181    /// Tab is about to close
182    BeforeUnload,
183    /// Tab was closed
184    Closed,
185    /// Tab crashed
186    Crashed,
187    /// Tab recovered from crash
188    Recovered,
189}
190
191// ---------------------------------------------------------------------------
192// Tab process
193// ---------------------------------------------------------------------------
194
195/// An isolated tab process with its own JS VM, DOM, and GC
196pub struct TabProcess {
197    /// Associated tab ID
198    pub tab_id: TabId,
199    /// Process state
200    pub state: TabProcessState,
201    /// JavaScript virtual machine (isolated per tab)
202    pub vm: JsVm,
203    /// Garbage collector (isolated per tab)
204    pub gc: GcHeap,
205    /// DOM API (isolated per tab)
206    pub dom_api: DomApi,
207    /// Resource limits
208    pub limits: ResourceLimits,
209    /// Resource usage
210    pub usage: ResourceUsage,
211    /// Pending IPC messages to deliver
212    pub inbox: Vec<IpcMessage>,
213    /// Capabilities bitmap (what this tab is allowed to do)
214    pub capabilities: TabCapabilities,
215    /// Last error message
216    pub last_error: Option<String>,
217    /// Origin (scheme + host + port) for same-origin policy
218    pub origin: String,
219}
220
221impl TabProcess {
222    pub fn new(tab_id: TabId) -> Self {
223        Self {
224            tab_id,
225            state: TabProcessState::Created,
226            vm: JsVm::new(),
227            gc: GcHeap::new(),
228            dom_api: DomApi::new(),
229            limits: ResourceLimits::default(),
230            usage: ResourceUsage::default(),
231            inbox: Vec::new(),
232            capabilities: TabCapabilities::default_web(),
233            last_error: None,
234            origin: String::new(),
235        }
236    }
237
238    /// Start the process
239    pub fn start(&mut self) {
240        self.state = TabProcessState::Running;
241    }
242
243    /// Suspend the process (background tab)
244    pub fn suspend(&mut self) {
245        if self.state == TabProcessState::Running {
246            self.state = TabProcessState::Suspended;
247        }
248    }
249
250    /// Resume a suspended process
251    pub fn resume(&mut self) {
252        if self.state == TabProcessState::Suspended {
253            self.state = TabProcessState::Running;
254        }
255    }
256
257    /// Mark as crashed
258    pub fn crash(&mut self, error: &str) {
259        self.state = TabProcessState::Crashed;
260        self.last_error = Some(error.to_string());
261        self.usage.crash_count += 1;
262    }
263
264    /// Terminate and clean up
265    pub fn terminate(&mut self) {
266        self.state = TabProcessState::Terminated;
267        self.vm = JsVm::new(); // Reset VM
268        self.gc = GcHeap::new(); // Reset GC
269        self.dom_api = DomApi::new(); // Reset DOM
270        self.inbox.clear();
271    }
272
273    /// Check if a resource limit would be exceeded
274    pub fn check_limits(&self) -> Option<TabError> {
275        if self.usage.heap_bytes > self.limits.max_heap_bytes {
276            return Some(TabError::ResourceLimitViolation {
277                message: String::from("Heap size limit exceeded"),
278            });
279        }
280        if self.usage.dom_node_count > self.limits.max_dom_nodes {
281            return Some(TabError::ResourceLimitViolation {
282                message: String::from("DOM node limit exceeded"),
283            });
284        }
285        if self.usage.timer_count > self.limits.max_timers {
286            return Some(TabError::ResourceLimitViolation {
287                message: String::from("Timer limit exceeded"),
288            });
289        }
290        if self.usage.steps_this_tick > self.limits.max_steps_per_tick {
291            return Some(TabError::ResourceLimitViolation {
292                message: String::from("CPU budget exceeded for this tick"),
293            });
294        }
295        None
296    }
297
298    /// Update resource usage from current state
299    pub fn update_usage(&mut self) {
300        self.usage.heap_bytes = self.gc.arena.bytes_allocated();
301        self.usage.dom_node_count = self.dom_api.node_count();
302        self.usage.timer_count = self.dom_api.timer_queue.pending_count();
303    }
304
305    /// Process one tick of execution
306    pub fn tick(&mut self) -> Result<(), TabError> {
307        if self.state != TabProcessState::Running {
308            return Ok(());
309        }
310
311        self.usage.steps_this_tick = 0;
312        self.usage.ticks_processed += 1;
313
314        // Process timers
315        let expired = self.dom_api.timer_queue.tick();
316        for _callback_id in expired {
317            // Invoke callback (stub -- connected when VM has function
318            // call-by-id)
319        }
320
321        // Process IPC inbox
322        let messages = core::mem::take(&mut self.inbox);
323        for _msg in messages {
324            // Deliver message to JS context (stub)
325        }
326
327        // GC check
328        if self.gc.should_collect() {
329            self.gc.collect(&self.vm);
330            self.usage.gc_collections += 1;
331        }
332
333        // Update usage
334        self.update_usage();
335
336        // Check limits
337        if let Some(violation) = self.check_limits() {
338            let msg = format!("{}", violation);
339            self.crash(&msg);
340            return Err(violation);
341        }
342
343        Ok(())
344    }
345
346    /// Execute JavaScript in this tab's context
347    pub fn execute_script(&mut self, source: &str) -> Result<(), TabError> {
348        if self.state != TabProcessState::Running {
349            return Err(TabError::InvalidState {
350                expected: "running",
351            });
352        }
353
354        // Quick check: can we handle this?
355        if !self.capabilities.can_execute_js {
356            return Err(TabError::CapabilityDenied {
357                capability: "JavaScript execution",
358            });
359        }
360
361        let mut parser = super::js_parser::JsParser::from_source(source);
362        let root = parser.parse();
363
364        if !parser.errors.is_empty() {
365            let err = parser.errors.join("; ");
366            self.last_error = Some(err.clone());
367            return Err(TabError::ScriptError { message: err });
368        }
369
370        let mut compiler = super::js_compiler::Compiler::new();
371        let chunk = compiler.compile(&parser.arena, root);
372
373        match self.vm.run_chunk(&chunk) {
374            Ok(_) => {
375                self.update_usage();
376                if let Some(violation) = self.check_limits() {
377                    let msg = format!("{}", violation);
378                    self.crash(&msg);
379                    return Err(violation);
380                }
381                Ok(())
382            }
383            Err(e) => {
384                let msg = format!("{}", e);
385                self.last_error = Some(msg.clone());
386                Err(TabError::ScriptError { message: msg })
387            }
388        }
389    }
390
391    /// Set the origin for same-origin checks
392    pub fn set_origin(&mut self, origin: &str) {
393        self.origin = origin.to_string();
394    }
395
396    /// Check if a target origin matches this tab's origin
397    pub fn is_same_origin(&self, target_origin: &str) -> bool {
398        self.origin == target_origin
399    }
400}
401
402// ---------------------------------------------------------------------------
403// Tab capabilities
404// ---------------------------------------------------------------------------
405
406/// Capabilities bitmap for tab sandboxing
407#[derive(Debug, Clone)]
408pub struct TabCapabilities {
409    /// Can execute JavaScript
410    pub can_execute_js: bool,
411    /// Can access localStorage
412    pub can_local_storage: bool,
413    /// Can use timers (setTimeout/setInterval)
414    pub can_timers: bool,
415    /// Can make network requests
416    pub can_network: bool,
417    /// Can use postMessage to other tabs
418    pub can_post_message: bool,
419    /// Can use geolocation (future)
420    pub can_geolocation: bool,
421    /// Can use clipboard
422    pub can_clipboard: bool,
423    /// Can use notifications
424    pub can_notifications: bool,
425    /// Can access camera/microphone (future)
426    pub can_media_devices: bool,
427    /// Can create popups / new windows
428    pub can_popups: bool,
429}
430
431impl TabCapabilities {
432    /// Default capabilities for a web page
433    pub fn default_web() -> Self {
434        Self {
435            can_execute_js: true,
436            can_local_storage: true,
437            can_timers: true,
438            can_network: true,
439            can_post_message: true,
440            can_geolocation: false,
441            can_clipboard: false,
442            can_notifications: false,
443            can_media_devices: false,
444            can_popups: false,
445        }
446    }
447
448    /// Restricted capabilities (e.g., sandboxed iframe)
449    pub fn sandboxed() -> Self {
450        Self {
451            can_execute_js: false,
452            can_local_storage: false,
453            can_timers: false,
454            can_network: false,
455            can_post_message: false,
456            can_geolocation: false,
457            can_clipboard: false,
458            can_notifications: false,
459            can_media_devices: false,
460            can_popups: false,
461        }
462    }
463
464    /// Full capabilities (trusted content like veridian:// pages)
465    pub fn trusted() -> Self {
466        Self {
467            can_execute_js: true,
468            can_local_storage: true,
469            can_timers: true,
470            can_network: true,
471            can_post_message: true,
472            can_geolocation: true,
473            can_clipboard: true,
474            can_notifications: true,
475            can_media_devices: true,
476            can_popups: true,
477        }
478    }
479
480    /// Apply sandbox flags (like HTML sandbox attribute)
481    pub fn apply_sandbox_flags(&mut self, flags: &str) {
482        // Reset all to sandboxed first
483        *self = Self::sandboxed();
484
485        // Then allow specific things based on flags
486        for flag in flags.split_whitespace() {
487            match flag {
488                "allow-scripts" => self.can_execute_js = true,
489                "allow-same-origin" => self.can_local_storage = true,
490                "allow-popups" => self.can_popups = true,
491                "allow-forms" => {}          // forms always allowed in our model
492                "allow-top-navigation" => {} // handled elsewhere
493                _ => {}                      // unknown flag, ignore
494            }
495        }
496    }
497
498    /// Count of enabled capabilities
499    pub fn enabled_count(&self) -> usize {
500        let bools = [
501            self.can_execute_js,
502            self.can_local_storage,
503            self.can_timers,
504            self.can_network,
505            self.can_post_message,
506            self.can_geolocation,
507            self.can_clipboard,
508            self.can_notifications,
509            self.can_media_devices,
510            self.can_popups,
511        ];
512        bools.iter().filter(|&&b| b).count()
513    }
514}
515
516// ---------------------------------------------------------------------------
517// Process isolation manager
518// ---------------------------------------------------------------------------
519
520/// Manages all tab processes and their isolation
521pub struct ProcessIsolation {
522    /// Tab processes keyed by TabId
523    processes: BTreeMap<TabId, TabProcess>,
524    /// IPC message queue (pending delivery)
525    message_queue: Vec<IpcMessage>,
526    /// Shared storage (simulates localStorage shared by same-origin tabs)
527    shared_storage: BTreeMap<String, BTreeMap<String, String>>,
528    /// Maximum concurrent processes
529    max_processes: usize,
530    /// Broadcast channel subscriptions: channel_name -> [tab_ids]
531    broadcast_channels: BTreeMap<String, Vec<TabId>>,
532    /// Total crash count across all tabs
533    total_crashes: usize,
534}
535
536impl Default for ProcessIsolation {
537    fn default() -> Self {
538        Self::new()
539    }
540}
541
542impl ProcessIsolation {
543    pub fn new() -> Self {
544        Self {
545            processes: BTreeMap::new(),
546            message_queue: Vec::new(),
547            shared_storage: BTreeMap::new(),
548            max_processes: 32,
549            broadcast_channels: BTreeMap::new(),
550            total_crashes: 0,
551        }
552    }
553
554    /// Spawn a new tab process
555    pub fn spawn_tab_process(&mut self, tab_id: TabId) -> Result<(), TabError> {
556        if self.processes.len() >= self.max_processes {
557            return Err(TabError::ProcessLimitReached);
558        }
559        if self.processes.contains_key(&tab_id) {
560            return Err(TabError::ProcessAlreadyExists);
561        }
562
563        let mut process = TabProcess::new(tab_id);
564        process.start();
565        self.processes.insert(tab_id, process);
566        Ok(())
567    }
568
569    /// Spawn with custom capabilities
570    pub fn spawn_with_capabilities(
571        &mut self,
572        tab_id: TabId,
573        capabilities: TabCapabilities,
574    ) -> Result<(), TabError> {
575        self.spawn_tab_process(tab_id)?;
576        if let Some(proc) = self.processes.get_mut(&tab_id) {
577            proc.capabilities = capabilities;
578        }
579        Ok(())
580    }
581
582    /// Kill a tab process
583    pub fn kill_tab_process(&mut self, tab_id: TabId) -> bool {
584        if let Some(mut proc) = self.processes.remove(&tab_id) {
585            proc.terminate();
586
587            // Remove from broadcast channels
588            for subscribers in self.broadcast_channels.values_mut() {
589                subscribers.retain(|&id| id != tab_id);
590            }
591
592            // Queue lifecycle event
593            self.message_queue.push(IpcMessage::TabEvent {
594                tab_id,
595                event: TabLifecycleEvent::Closed,
596            });
597
598            true
599        } else {
600            false
601        }
602    }
603
604    /// Recover a crashed tab process (recreate its context)
605    pub fn recover_from_crash(&mut self, tab_id: TabId) -> Result<(), TabError> {
606        let proc = self
607            .processes
608            .get_mut(&tab_id)
609            .ok_or(TabError::ProcessNotFound)?;
610
611        if proc.state != TabProcessState::Crashed {
612            return Err(TabError::InvalidState {
613                expected: "crashed",
614            });
615        }
616
617        // Reset VM and GC, keep limits and capabilities
618        let limits = proc.limits.clone();
619        let capabilities = proc.capabilities.clone();
620        let origin = proc.origin.clone();
621        let crash_count = proc.usage.crash_count;
622
623        proc.vm = JsVm::new();
624        proc.gc = GcHeap::new();
625        proc.dom_api = DomApi::new();
626        proc.inbox.clear();
627        proc.state = TabProcessState::Running;
628        proc.limits = limits;
629        proc.capabilities = capabilities;
630        proc.origin = origin;
631        proc.usage = ResourceUsage::default();
632        proc.usage.crash_count = crash_count;
633        proc.last_error = None;
634
635        self.total_crashes += 1;
636
637        // Notify other tabs
638        self.message_queue.push(IpcMessage::TabEvent {
639            tab_id,
640            event: TabLifecycleEvent::Recovered,
641        });
642
643        Ok(())
644    }
645
646    /// Restrict capabilities for a tab
647    pub fn restrict_capabilities(&mut self, tab_id: TabId, capabilities: TabCapabilities) -> bool {
648        if let Some(proc) = self.processes.get_mut(&tab_id) {
649            proc.capabilities = capabilities;
650            true
651        } else {
652            false
653        }
654    }
655
656    /// Get a tab process
657    pub fn get_process(&self, tab_id: TabId) -> Option<&TabProcess> {
658        self.processes.get(&tab_id)
659    }
660
661    /// Get a tab process mutably
662    pub fn get_process_mut(&mut self, tab_id: TabId) -> Option<&mut TabProcess> {
663        self.processes.get_mut(&tab_id)
664    }
665
666    /// Send postMessage from one tab to another
667    pub fn post_message(
668        &mut self,
669        source: TabId,
670        target: TabId,
671        data: &str,
672    ) -> Result<(), TabError> {
673        // Check source can send
674        let source_proc = self
675            .processes
676            .get(&source)
677            .ok_or(TabError::ProcessNotFound)?;
678        if !source_proc.capabilities.can_post_message {
679            return Err(TabError::CapabilityDenied {
680                capability: "postMessage",
681            });
682        }
683        let origin = source_proc.origin.clone();
684
685        // Deliver to target
686        let target_proc = self
687            .processes
688            .get_mut(&target)
689            .ok_or(TabError::ProcessNotFound)?;
690
691        target_proc.inbox.push(IpcMessage::PostMessage {
692            source_tab: source,
693            target_tab: target,
694            origin,
695            data: data.to_string(),
696        });
697
698        Ok(())
699    }
700
701    /// Subscribe a tab to a broadcast channel
702    pub fn subscribe_broadcast(&mut self, tab_id: TabId, channel: &str) -> bool {
703        if !self.processes.contains_key(&tab_id) {
704            return false;
705        }
706        let subscribers = self
707            .broadcast_channels
708            .entry(channel.to_string())
709            .or_default();
710        if !subscribers.contains(&tab_id) {
711            subscribers.push(tab_id);
712        }
713        true
714    }
715
716    /// Unsubscribe a tab from a broadcast channel
717    pub fn unsubscribe_broadcast(&mut self, tab_id: TabId, channel: &str) {
718        if let Some(subscribers) = self.broadcast_channels.get_mut(channel) {
719            subscribers.retain(|&id| id != tab_id);
720        }
721    }
722
723    /// Send a broadcast message to all subscribers of a channel
724    pub fn broadcast_message(&mut self, source: TabId, channel: &str, data: &str) -> usize {
725        let subscribers: Vec<TabId> = self
726            .broadcast_channels
727            .get(channel)
728            .cloned()
729            .unwrap_or_default();
730
731        let mut delivered = 0;
732        for &sub_id in &subscribers {
733            if sub_id == source {
734                continue; // Don't deliver to sender
735            }
736            if let Some(proc) = self.processes.get_mut(&sub_id) {
737                proc.inbox.push(IpcMessage::BroadcastMessage {
738                    source_tab: source,
739                    channel: channel.to_string(),
740                    data: data.to_string(),
741                });
742                delivered += 1;
743            }
744        }
745        delivered
746    }
747
748    /// Set a value in shared storage for an origin
749    pub fn storage_set(&mut self, origin: &str, key: &str, value: &str) {
750        let old_value = self
751            .shared_storage
752            .entry(origin.to_string())
753            .or_default()
754            .insert(key.to_string(), value.to_string());
755
756        // Generate storage events for all same-origin tabs
757        let msg = IpcMessage::StorageEvent {
758            key: key.to_string(),
759            old_value,
760            new_value: Some(value.to_string()),
761        };
762
763        let tab_ids: Vec<TabId> = self
764            .processes
765            .iter()
766            .filter(|(_, proc)| proc.origin == origin)
767            .map(|(&id, _)| id)
768            .collect();
769
770        for tab_id in tab_ids {
771            if let Some(proc) = self.processes.get_mut(&tab_id) {
772                proc.inbox.push(msg.clone());
773            }
774        }
775    }
776
777    /// Get a value from shared storage
778    pub fn storage_get(&self, origin: &str, key: &str) -> Option<&String> {
779        self.shared_storage.get(origin)?.get(key)
780    }
781
782    /// Remove a value from shared storage
783    pub fn storage_remove(&mut self, origin: &str, key: &str) -> Option<String> {
784        self.shared_storage.get_mut(origin)?.remove(key)
785    }
786
787    /// Tick all running processes
788    pub fn tick_all(&mut self) {
789        let tab_ids: Vec<TabId> = self.processes.keys().copied().collect();
790        for tab_id in tab_ids {
791            if let Some(proc) = self.processes.get_mut(&tab_id) {
792                if proc.state == TabProcessState::Running {
793                    if let Err(_e) = proc.tick() {
794                        // Process crashed during tick, already marked
795                    }
796                }
797            }
798        }
799    }
800
801    /// Suspend all background tab processes
802    pub fn suspend_background_tabs(&mut self, active_tab: TabId) {
803        for (&id, proc) in self.processes.iter_mut() {
804            if id != active_tab && proc.state == TabProcessState::Running {
805                proc.suspend();
806            }
807        }
808    }
809
810    /// Resume a specific tab process
811    pub fn resume_tab(&mut self, tab_id: TabId) -> bool {
812        if let Some(proc) = self.processes.get_mut(&tab_id) {
813            proc.resume();
814            true
815        } else {
816            false
817        }
818    }
819
820    /// Number of active processes
821    pub fn active_count(&self) -> usize {
822        self.processes
823            .values()
824            .filter(|p| p.state == TabProcessState::Running)
825            .count()
826    }
827
828    /// Total number of processes
829    pub fn total_count(&self) -> usize {
830        self.processes.len()
831    }
832
833    /// Total crashes across all tabs
834    pub fn total_crashes(&self) -> usize {
835        self.total_crashes
836    }
837
838    /// Get resource usage for a tab
839    pub fn resource_usage(&self, tab_id: TabId) -> Option<&ResourceUsage> {
840        self.processes.get(&tab_id).map(|p| &p.usage)
841    }
842
843    /// Get aggregate resource usage across all tabs
844    pub fn aggregate_usage(&self) -> ResourceUsage {
845        let mut total = ResourceUsage::default();
846        for proc in self.processes.values() {
847            total.heap_bytes += proc.usage.heap_bytes;
848            total.dom_node_count += proc.usage.dom_node_count;
849            total.timer_count += proc.usage.timer_count;
850            total.total_steps += proc.usage.total_steps;
851            total.gc_collections += proc.usage.gc_collections;
852            total.crash_count += proc.usage.crash_count;
853        }
854        total
855    }
856}
857
858// ---------------------------------------------------------------------------
859// Tests
860// ---------------------------------------------------------------------------
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865
866    #[test]
867    fn test_tab_process_new() {
868        let proc = TabProcess::new(1);
869        assert_eq!(proc.tab_id, 1);
870        assert_eq!(proc.state, TabProcessState::Created);
871    }
872
873    #[test]
874    fn test_tab_process_lifecycle() {
875        let mut proc = TabProcess::new(1);
876        proc.start();
877        assert_eq!(proc.state, TabProcessState::Running);
878        proc.suspend();
879        assert_eq!(proc.state, TabProcessState::Suspended);
880        proc.resume();
881        assert_eq!(proc.state, TabProcessState::Running);
882        proc.terminate();
883        assert_eq!(proc.state, TabProcessState::Terminated);
884    }
885
886    #[test]
887    fn test_tab_process_crash() {
888        let mut proc = TabProcess::new(1);
889        proc.start();
890        proc.crash("out of memory");
891        assert_eq!(proc.state, TabProcessState::Crashed);
892        assert_eq!(proc.last_error.as_deref(), Some("out of memory"));
893        assert_eq!(proc.usage.crash_count, 1);
894    }
895
896    #[test]
897    fn test_tab_process_tick() {
898        let mut proc = TabProcess::new(1);
899        proc.start();
900        assert!(proc.tick().is_ok());
901        assert_eq!(proc.usage.ticks_processed, 1);
902    }
903
904    #[test]
905    fn test_tab_process_tick_not_running() {
906        let mut proc = TabProcess::new(1);
907        // Still in Created state
908        assert!(proc.tick().is_ok());
909        assert_eq!(proc.usage.ticks_processed, 0);
910    }
911
912    #[test]
913    fn test_tab_process_execute_script() {
914        let mut proc = TabProcess::new(1);
915        proc.start();
916        assert!(proc.execute_script("let x = 42;").is_ok());
917    }
918
919    #[test]
920    fn test_tab_process_execute_script_not_running() {
921        let mut proc = TabProcess::new(1);
922        assert!(proc.execute_script("let x = 1;").is_err());
923    }
924
925    #[test]
926    fn test_tab_process_execute_js_disabled() {
927        let mut proc = TabProcess::new(1);
928        proc.start();
929        proc.capabilities.can_execute_js = false;
930        assert!(proc.execute_script("let x = 1;").is_err());
931    }
932
933    #[test]
934    fn test_capabilities_default_web() {
935        let caps = TabCapabilities::default_web();
936        assert!(caps.can_execute_js);
937        assert!(caps.can_local_storage);
938        assert!(caps.can_timers);
939        assert!(caps.can_network);
940        assert!(!caps.can_geolocation);
941        assert!(!caps.can_clipboard);
942    }
943
944    #[test]
945    fn test_capabilities_sandboxed() {
946        let caps = TabCapabilities::sandboxed();
947        assert_eq!(caps.enabled_count(), 0);
948    }
949
950    #[test]
951    fn test_capabilities_trusted() {
952        let caps = TabCapabilities::trusted();
953        assert_eq!(caps.enabled_count(), 10);
954    }
955
956    #[test]
957    fn test_capabilities_sandbox_flags() {
958        let mut caps = TabCapabilities::default_web();
959        caps.apply_sandbox_flags("allow-scripts allow-popups");
960        assert!(caps.can_execute_js);
961        assert!(caps.can_popups);
962        assert!(!caps.can_network);
963        assert!(!caps.can_local_storage);
964    }
965
966    #[test]
967    fn test_process_isolation_spawn() {
968        let mut iso = ProcessIsolation::new();
969        assert!(iso.spawn_tab_process(1).is_ok());
970        assert_eq!(iso.total_count(), 1);
971        assert_eq!(iso.active_count(), 1);
972    }
973
974    #[test]
975    fn test_process_isolation_duplicate_spawn() {
976        let mut iso = ProcessIsolation::new();
977        iso.spawn_tab_process(1).unwrap();
978        assert!(iso.spawn_tab_process(1).is_err());
979    }
980
981    #[test]
982    fn test_process_isolation_kill() {
983        let mut iso = ProcessIsolation::new();
984        iso.spawn_tab_process(1).unwrap();
985        assert!(iso.kill_tab_process(1));
986        assert_eq!(iso.total_count(), 0);
987        assert!(!iso.kill_tab_process(1)); // already gone
988    }
989
990    #[test]
991    fn test_process_isolation_recover() {
992        let mut iso = ProcessIsolation::new();
993        iso.spawn_tab_process(1).unwrap();
994        iso.get_process_mut(1).unwrap().crash("boom");
995        assert!(iso.recover_from_crash(1).is_ok());
996        assert_eq!(iso.get_process(1).unwrap().state, TabProcessState::Running);
997        assert_eq!(iso.total_crashes(), 1);
998    }
999
1000    #[test]
1001    fn test_process_isolation_recover_not_crashed() {
1002        let mut iso = ProcessIsolation::new();
1003        iso.spawn_tab_process(1).unwrap();
1004        assert!(iso.recover_from_crash(1).is_err());
1005    }
1006
1007    #[test]
1008    fn test_post_message() {
1009        let mut iso = ProcessIsolation::new();
1010        iso.spawn_tab_process(1).unwrap();
1011        iso.spawn_tab_process(2).unwrap();
1012        iso.get_process_mut(1)
1013            .unwrap()
1014            .set_origin("https://example.com");
1015        assert!(iso.post_message(1, 2, "hello").is_ok());
1016        assert_eq!(iso.get_process(2).unwrap().inbox.len(), 1);
1017    }
1018
1019    #[test]
1020    fn test_broadcast_channel() {
1021        let mut iso = ProcessIsolation::new();
1022        iso.spawn_tab_process(1).unwrap();
1023        iso.spawn_tab_process(2).unwrap();
1024        iso.spawn_tab_process(3).unwrap();
1025        iso.subscribe_broadcast(1, "updates");
1026        iso.subscribe_broadcast(2, "updates");
1027        iso.subscribe_broadcast(3, "updates");
1028
1029        let delivered = iso.broadcast_message(1, "updates", "data");
1030        assert_eq!(delivered, 2); // 2 and 3, not 1 (sender excluded)
1031    }
1032
1033    #[test]
1034    fn test_shared_storage() {
1035        let mut iso = ProcessIsolation::new();
1036        iso.spawn_tab_process(1).unwrap();
1037        iso.get_process_mut(1)
1038            .unwrap()
1039            .set_origin("https://example.com");
1040
1041        iso.storage_set("https://example.com", "key1", "value1");
1042        assert_eq!(
1043            iso.storage_get("https://example.com", "key1"),
1044            Some(&"value1".to_string())
1045        );
1046
1047        iso.storage_remove("https://example.com", "key1");
1048        assert!(iso.storage_get("https://example.com", "key1").is_none());
1049    }
1050
1051    #[test]
1052    fn test_suspend_background_tabs() {
1053        let mut iso = ProcessIsolation::new();
1054        iso.spawn_tab_process(1).unwrap();
1055        iso.spawn_tab_process(2).unwrap();
1056        iso.spawn_tab_process(3).unwrap();
1057
1058        iso.suspend_background_tabs(2);
1059        assert_eq!(
1060            iso.get_process(1).unwrap().state,
1061            TabProcessState::Suspended
1062        );
1063        assert_eq!(iso.get_process(2).unwrap().state, TabProcessState::Running);
1064        assert_eq!(
1065            iso.get_process(3).unwrap().state,
1066            TabProcessState::Suspended
1067        );
1068    }
1069
1070    #[test]
1071    fn test_resume_tab() {
1072        let mut iso = ProcessIsolation::new();
1073        iso.spawn_tab_process(1).unwrap();
1074        iso.get_process_mut(1).unwrap().suspend();
1075        assert!(iso.resume_tab(1));
1076        assert_eq!(iso.get_process(1).unwrap().state, TabProcessState::Running);
1077    }
1078
1079    #[test]
1080    fn test_restrict_capabilities() {
1081        let mut iso = ProcessIsolation::new();
1082        iso.spawn_tab_process(1).unwrap();
1083        let caps = TabCapabilities::sandboxed();
1084        assert!(iso.restrict_capabilities(1, caps));
1085        assert_eq!(iso.get_process(1).unwrap().capabilities.enabled_count(), 0);
1086    }
1087
1088    #[test]
1089    fn test_aggregate_usage() {
1090        let mut iso = ProcessIsolation::new();
1091        iso.spawn_tab_process(1).unwrap();
1092        iso.spawn_tab_process(2).unwrap();
1093        let agg = iso.aggregate_usage();
1094        assert_eq!(agg.crash_count, 0);
1095    }
1096
1097    #[test]
1098    fn test_resource_limits_default() {
1099        let limits = ResourceLimits::default();
1100        assert_eq!(limits.max_heap_bytes, 64 * 1024 * 1024);
1101        assert_eq!(limits.max_dom_nodes, 100_000);
1102    }
1103
1104    #[test]
1105    fn test_same_origin() {
1106        let mut proc = TabProcess::new(1);
1107        proc.set_origin("https://example.com");
1108        assert!(proc.is_same_origin("https://example.com"));
1109        assert!(!proc.is_same_origin("https://other.com"));
1110    }
1111
1112    #[test]
1113    fn test_tick_all() {
1114        let mut iso = ProcessIsolation::new();
1115        iso.spawn_tab_process(1).unwrap();
1116        iso.spawn_tab_process(2).unwrap();
1117        iso.tick_all();
1118        assert_eq!(iso.get_process(1).unwrap().usage.ticks_processed, 1);
1119        assert_eq!(iso.get_process(2).unwrap().usage.ticks_processed, 1);
1120    }
1121
1122    #[test]
1123    fn test_max_processes() {
1124        let mut iso = ProcessIsolation::new();
1125        iso.max_processes = 2;
1126        iso.spawn_tab_process(1).unwrap();
1127        iso.spawn_tab_process(2).unwrap();
1128        assert!(iso.spawn_tab_process(3).is_err());
1129    }
1130
1131    #[test]
1132    fn test_spawn_with_capabilities() {
1133        let mut iso = ProcessIsolation::new();
1134        let caps = TabCapabilities::trusted();
1135        iso.spawn_with_capabilities(1, caps).unwrap();
1136        assert_eq!(iso.get_process(1).unwrap().capabilities.enabled_count(), 10);
1137    }
1138
1139    #[test]
1140    fn test_unsubscribe_broadcast() {
1141        let mut iso = ProcessIsolation::new();
1142        iso.spawn_tab_process(1).unwrap();
1143        iso.subscribe_broadcast(1, "ch");
1144        iso.unsubscribe_broadcast(1, "ch");
1145        let delivered = iso.broadcast_message(2, "ch", "data");
1146        assert_eq!(delivered, 0);
1147    }
1148}