Skip to main content

strop_engine/editor/
containers.rs

1//! Explicit container discovery/inspection. Every subsequent resource read uses
2//! the common opener and Directory model, with an incarnation-pinned identity.
3use std::collections::HashMap;
4use std::sync::mpsc::{channel, Receiver, Sender};
5#[cfg(test)]
6mod tests;
7use super::io::OpenIntent;
8use super::Editor;
9use strop_core::id::{BufferRevision, DocumentId};
10use strop_core::worker::{self, Completion, FailureKind, Outcome, Ticket, WorkerId};
11
12#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
13pub enum ContainerJob {
14    Discover,
15    Attach {
16        id: String,
17        path: std::path::PathBuf,
18        intent: OpenIntent,
19    },
20}
21#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
22pub struct ContainerKey {
23    pub request: WorkerId,
24    pub document: DocumentId,
25    pub revision: BufferRevision,
26    pub focus: u64,
27    pub job: ContainerJob,
28}
29#[derive(serde::Serialize, serde::Deserialize)]
30pub enum ContainerResult {
31    Containers(Vec<strop_containers::ContainerIdentity>),
32    Attached(strop_containers::ContainerIdentity),
33}
34pub type ContainerEvent = Completion<ContainerKey, ContainerResult>;
35pub(crate) struct ContainerState {
36    pub tx: Sender<ContainerEvent>,
37    pub rx: Option<Receiver<ContainerEvent>>,
38    pub pending: Option<Ticket<ContainerKey>>,
39    pub attached: HashMap<String, strop_containers::ContainerIdentity>,
40}
41impl Default for ContainerState {
42    fn default() -> Self {
43        let (tx, rx) = channel();
44        Self {
45            tx,
46            rx: Some(rx),
47            pending: None,
48            attached: HashMap::new(),
49        }
50    }
51}
52impl ContainerState {
53    pub fn take_rx(&mut self) -> Option<Receiver<ContainerEvent>> {
54        self.rx.take()
55    }
56}
57impl Editor {
58    pub(crate) fn request_containers(&mut self) {
59        self.start_container_job(ContainerJob::Discover);
60    }
61    fn start_container_job(&mut self, job: ContainerJob) {
62        if self.docs.is_empty() {
63            return;
64        }
65        if let Some(old) = self.containers.pending.take() {
66            if let Some(handle) = self.worker_handles.remove(&old.request) {
67                handle.cancel(worker::CancelReason::Superseded);
68            }
69        }
70        let request = match self.worker_ids.allocate() {
71            Ok(request) => request,
72            Err(error) => {
73                self.message = error.message;
74                return;
75            }
76        };
77        let ticket = Ticket {
78            request,
79            key: ContainerKey {
80                request,
81                document: self.current(),
82                revision: self.buf().revision(),
83                focus: self.focus_epoch,
84                job: job.clone(),
85            },
86        };
87        self.containers.pending = Some(ticket.clone());
88        self.message = "inspecting container context".into();
89        match self.tape.request("container.job", &ticket) {
90            Ok(false) => return,
91            Ok(true) => {}
92            Err(error) => {
93                self.handle_container_event(Completion {
94                    ticket,
95                    outcome: Outcome::failed(FailureKind::Protocol, error.to_string()),
96                });
97                return;
98            }
99        }
100        let tx = self.containers.tx.clone();
101        let handle = worker::spawn(
102            "container-job",
103            move |outcome| {
104                let _ = tx.send(Completion { ticket, outcome });
105            },
106            move |token| match run_container_job(&job, &token) {
107                Ok(result) => Outcome::Success(result),
108                Err(strop_containers::ContainerError::Cancelled) => {
109                    Outcome::Cancelled(worker::CancelReason::Dismissed)
110                }
111                Err(error) => Outcome::failed(FailureKind::Io, error.to_string()),
112            },
113        );
114        self.worker_handles.insert(request, handle);
115    }
116    pub(crate) fn attach_container(&mut self, id: String) {
117        self.start_container_job(ContainerJob::Attach {
118            id,
119            path: "/".into(),
120            intent: OpenIntent::Browse,
121        });
122    }
123    pub(crate) fn attach_container_target(
124        &mut self,
125        container: strop_workspace::ContainerId,
126        path: std::path::PathBuf,
127        intent: OpenIntent,
128    ) {
129        self.start_container_job(ContainerJob::Attach {
130            id: container.to_string(),
131            path,
132            intent,
133        });
134    }
135    pub(crate) fn handle_container_event(&mut self, event: ContainerEvent) {
136        if self.containers.pending.as_ref() != Some(&event.ticket) {
137            return;
138        }
139        self.containers.pending = None;
140        self.worker_handles.remove(&event.ticket.request);
141        let key = event.ticket.key;
142        if self.finishing
143            || self.docs.is_empty()
144            || key.focus != self.focus_epoch
145            || self.current() != key.document
146            || self.buf().revision() != key.revision
147        {
148            return;
149        }
150        match event.outcome {
151            Outcome::Success(ContainerResult::Containers(list)) => {
152                if list.is_empty() {
153                    self.message = "no running containers on the local engine".into();
154                    return;
155                }
156                let mut items = Vec::with_capacity(list.len());
157                for identity in list {
158                    let reference = match strop_containers::ContainerRef::of(&identity) {
159                        Ok(reference) => reference,
160                        Err(error) => {
161                            self.message = error.to_string();
162                            return;
163                        }
164                    };
165                    items.push(strop_picker::Item {
166                        text: format!(
167                            "{}  {}  {}",
168                            identity.name,
169                            &reference.id().as_str()[..12],
170                            identity.image
171                        ),
172                        badge: None,
173                        payload: strop_picker::Payload::Container(identity.id),
174                    });
175                }
176                self.set_picker(super::picker::PickerGlue::diagnostics(
177                    strop_picker::Picker::new(strop_picker::Kind::Containers, items, false),
178                ));
179            }
180            Outcome::Success(ContainerResult::Attached(identity)) => {
181                let reference = match strop_containers::ContainerRef::of(&identity) {
182                    Ok(reference) => reference,
183                    Err(error) => {
184                        self.message = error.to_string();
185                        return;
186                    }
187                };
188                let ContainerJob::Attach { path, intent, .. } = key.job else {
189                    self.message = "unexpected container attachment result".into();
190                    return;
191                };
192                self.workspaces.bind(
193                    strop_workspace::Filesystem::Container(reference.id().clone()),
194                    None,
195                );
196                self.containers
197                    .attached
198                    .insert(identity.id.clone(), identity);
199                self.request_target(
200                    crate::files::FileTarget::Container {
201                        container: reference.id().clone(),
202                        path,
203                    },
204                    intent,
205                );
206            }
207            Outcome::Failed { failure, .. } => self.message = failure.message,
208            Outcome::Cancelled(_) => {}
209        }
210    }
211}
212fn run_container_job(
213    job: &ContainerJob,
214    token: &strop_core::worker::CancelToken,
215) -> Result<ContainerResult, strop_containers::ContainerError> {
216    let engine = strop_containers::engine(token)?;
217    match job {
218        ContainerJob::Discover => Ok(ContainerResult::Containers(strop_containers::list_running(
219            &engine, token,
220        )?)),
221        ContainerJob::Attach { id, .. } => Ok(ContainerResult::Attached(
222            strop_containers::inspect(&engine, id, token)?,
223        )),
224    }
225}