1use 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}