1use std::path::{Path, PathBuf};
8use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
9use std::sync::{Arc, Mutex, mpsc};
10
11use serde_json::{Value, json};
12use speccoder_core::actions::{Runnable, Via};
13use speccoder_core::{CommandResult, CommandStatus};
14use speccoder_runtime::service::approvals::{Answer, Approver, Presentation, Who};
15use speccoder_runtime::service::progress::{Event, Progress};
16use tokio::sync::broadcast;
17
18#[derive(Debug)]
20pub enum Reply {
21 Answer(Answer),
23 Text(Option<String>),
25}
26
27#[derive(Debug)]
28struct Waiting {
29 id: u64,
30 reply: mpsc::Sender<Reply>,
31 event: Value,
32}
33
34#[derive(Debug, Clone, serde::Serialize)]
36pub struct JobView {
37 pub id: u64,
39 pub command: String,
41 pub running: bool,
43 pub result: Option<Value>,
45}
46
47#[derive(Debug)]
49pub struct Hub {
50 root: crate::Root,
51 pub issuer: Arc<crate::fixes::Issuer>,
53 events: broadcast::Sender<String>,
54 next: AtomicU64,
55 job: Mutex<Option<JobView>>,
56 waiting: Mutex<Option<Waiting>>,
57 cancel: Mutex<Option<Arc<AtomicBool>>>,
58}
59
60impl Hub {
61 #[must_use]
64 pub fn new(root: crate::Root, issuer: Arc<crate::fixes::Issuer>) -> Arc<Self> {
65 let (events, _) = broadcast::channel(1024);
66 Arc::new(Self {
67 root,
68 issuer,
69 events,
70 next: AtomicU64::new(1),
71 job: Mutex::new(None),
72 waiting: Mutex::new(None),
73 cancel: Mutex::new(None),
74 })
75 }
76
77 #[must_use]
79 pub fn subscribe(&self) -> broadcast::Receiver<String> {
80 self.events.subscribe()
81 }
82
83 fn send(&self, v: &Value) {
84 let _ = self.events.send(v.to_string());
85 }
86
87 #[must_use]
89 pub fn job(&self) -> Option<JobView> {
90 self.job.lock().ok().and_then(|j| j.clone())
91 }
92
93 #[must_use]
95 pub fn waiting(&self) -> Option<Value> {
96 self.waiting
97 .lock()
98 .ok()
99 .and_then(|w| w.as_ref().map(|w| w.event.clone()))
100 }
101
102 pub fn answer(&self, id: u64, reply: Reply) -> Result<(), String> {
104 let w = self
105 .waiting
106 .lock()
107 .map_err(|_| "the host is broken".to_owned())?
108 .take();
109 match w {
110 Some(w) if w.id == id => {
111 let _ = w.reply.send(reply);
112 self.send(&json!({ "type": "answered", "id": id }));
113 Ok(())
114 }
115 Some(w) => {
116 let other = w.id;
117 if let Ok(mut slot) = self.waiting.lock() {
118 *slot = Some(w);
119 }
120 Err(format!("{other} waits, not {id}"))
121 }
122 None => Err("nothing waits for an answer".to_owned()),
123 }
124 }
125
126 pub fn stop(&self) {
129 if let Some(c) = self.cancel.lock().ok().and_then(|c| c.clone()) {
130 c.store(true, Ordering::SeqCst);
131 }
132 let who = Who::local_in(&self.root.get(), Via::Web);
133 if let Some(w) = self.waiting.lock().ok().and_then(|mut w| w.take()) {
134 let _ = w.reply.send(match w.event["type"].as_str() {
135 Some("question") => Reply::Text(None),
136 _ => Reply::Answer(Answer::stop(Some(who))),
137 });
138 }
139 self.send(&json!({ "type": "stopping" }));
140 }
141
142 fn wait_for(&self, mut event: Value) -> Option<Reply> {
143 let id = self.next.fetch_add(1, Ordering::SeqCst);
144 event["id"] = json!(id);
145 let (tx, rx) = mpsc::channel();
146 if let Ok(mut w) = self.waiting.lock() {
147 *w = Some(Waiting {
148 id,
149 reply: tx,
150 event: event.clone(),
151 });
152 }
153 self.send(&event);
154 rx.recv().ok()
155 }
156
157 pub fn exclusive<T>(&self, f: impl FnOnce() -> T) -> Result<T, String> {
164 let job = self
165 .job
166 .lock()
167 .map_err(|_| "the host is broken".to_owned())?;
168 if job.as_ref().is_some_and(|j| j.running) {
169 return Err("a job is working; do this when it ends".to_owned());
170 }
171 let out = f();
172 drop(job);
173 Ok(out)
174 }
175
176 fn begin(&self, command: &str) -> Result<(u64, Arc<AtomicBool>), String> {
179 let id = self.next.fetch_add(1, Ordering::SeqCst);
180 {
181 let mut job = self
182 .job
183 .lock()
184 .map_err(|_| "the host is broken".to_owned())?;
185 if job.as_ref().is_some_and(|j| j.running) {
186 return Err("a job is working; stop it or wait for it".to_owned());
187 }
188 *job = Some(JobView {
189 id,
190 command: command.to_owned(),
191 running: true,
192 result: None,
193 });
194 }
195 let cancel = Arc::new(AtomicBool::new(false));
196 if let Ok(mut c) = self.cancel.lock() {
197 *c = Some(Arc::clone(&cancel));
198 }
199 self.send(&json!({ "type": "started", "id": id, "command": command }));
200 Ok((id, cancel))
201 }
202
203 fn finish(&self, id: u64, root: &Path, result: &CommandResult) {
205 let value = self.issuer.signing(root.to_path_buf(), || {
206 serde_json::to_value(result).unwrap_or(Value::Null)
207 });
208 self.finish_value(id, value);
209 }
210
211 fn finish_value(&self, id: u64, value: Value) {
213 if let Ok(mut job) = self.job.lock()
214 && let Some(j) = job.as_mut()
215 {
216 j.running = false;
217 j.result = Some(value.clone());
218 }
219 if let Ok(mut c) = self.cancel.lock() {
220 *c = None;
221 }
222 self.send(&json!({ "type": "done", "id": id, "result": value }));
223 }
224
225 pub fn start(
229 self: &Arc<Self>,
230 run: Runnable,
231 input: Option<String>,
232 feature: Option<String>,
233 command: String,
234 root: PathBuf,
235 ) -> Result<u64, String> {
236 let (id, cancel) = self.begin(&command)?;
237 let hub = Arc::clone(self);
238 std::thread::spawn(move || {
239 let peer: Arc<Peer> = Arc::new(Peer {
240 hub: Arc::clone(&hub),
241 who: Who::local_in(&root, Via::Web),
242 });
243 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
246 hub.issuer.signing(hub.root.get(), || {
247 execute(
248 &root,
249 feature.as_deref(),
250 &run,
251 input.as_deref(),
252 &peer,
253 cancel,
254 )
255 })
256 }))
257 .unwrap_or_else(|_| {
258 CommandResult::new("job", CommandStatus::Error, "the job stopped unexpectedly")
259 });
260 let result = speccoder_runtime::service::next::attach(result, feature.as_deref());
261 if let Some(bound) = crate::welcome::bind_after(&hub.root, &run, &result) {
264 hub.send(&json!({ "type": "bound", "root": bound.display().to_string() }));
265 }
266 hub.finish(id, &hub.root.get(), &result);
267 });
268 Ok(id)
269 }
270
271 pub fn start_fix(
275 self: &Arc<Self>,
276 argvs: Vec<Vec<String>>,
277 root: PathBuf,
278 ) -> Result<u64, String> {
279 use speccoder_runtime::service::person;
280 let (id, cancel) = self.begin(&person::shown(&argvs))?;
281 let hub = Arc::clone(self);
282 std::thread::spawn(move || {
283 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
284 person::run(&root, &argvs, person::TIMEOUT, &cancel, &|stream, line| {
285 hub.send(&json!({ "type": "output", "id": id, "stream": stream.as_str(), "line": line }));
286 })
287 }))
288 .unwrap_or_else(|_| {
289 CommandResult::new("fix", CommandStatus::Error, "the command stopped unexpectedly")
290 });
291 hub.finish(id, &root, &result);
292 });
293 Ok(id)
294 }
295
296 pub fn start_speccoder(
302 self: &Arc<Self>,
303 args: Vec<String>,
304 root: PathBuf,
305 ) -> Result<u64, String> {
306 use speccoder_runtime::service::person;
307 let mut shown = vec!["speccoder".to_owned()];
308 shown.extend(args.iter().cloned());
309 let (id, cancel) = self.begin(&person::shown(&[shown]))?;
310 let hub = Arc::clone(self);
311 std::thread::spawn(move || {
312 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
313 person::speccoder(&root, &args, &cancel, &|stream, line| {
314 hub.send(&json!({ "type": "output", "id": id, "stream": stream.as_str(), "line": line }));
315 })
316 }))
317 .unwrap_or_else(|_| Err("it stopped unexpectedly".to_owned()));
318 match result {
319 Ok(v) => hub.finish_value(id, v),
321 Err(e) => hub.finish(
322 id,
323 &root,
324 &CommandResult::new(
325 "job",
326 CommandStatus::Error,
327 &format!("the command ended without a result: {e}"),
328 ),
329 ),
330 }
331 });
332 Ok(id)
333 }
334}
335
336#[derive(Debug)]
338struct Peer {
339 hub: Arc<Hub>,
340 who: Who,
341}
342
343impl Progress for Peer {
344 fn event(&self, event: &Event) {
345 self.hub
346 .send(&json!({ "type": "progress", "event": event }));
347 }
348}
349
350impl Approver for Peer {
351 fn ask(&self, p: &Presentation) -> Answer {
352 match self
353 .hub
354 .wait_for(json!({ "type": "ask", "presentation": p }))
355 {
356 Some(Reply::Answer(mut a)) => {
357 a.by = Some(self.who.clone());
358 a
359 }
360 _ => Answer::stop(Some(self.who.clone())),
361 }
362 }
363
364 fn answer(&self, question: &str) -> Option<String> {
365 match self
366 .hub
367 .wait_for(json!({ "type": "question", "question": question }))
368 {
369 Some(Reply::Text(t)) => t,
370 _ => None,
371 }
372 }
373
374 fn via(&self) -> Via {
375 Via::Web
376 }
377}
378
379fn execute(
380 root: &Path,
381 feature: Option<&str>,
382 run: &Runnable,
383 input: Option<&str>,
384 peer: &Arc<Peer>,
385 cancel: Arc<AtomicBool>,
386) -> CommandResult {
387 use speccoder_runtime::service::actions::{Dispatch, Interface, Offer, offered, perform};
388 if offered(Interface::WebJobs, run.kind()) == Offer::Refuse {
389 return Interface::WebJobs.refusal();
390 }
391 let options = speccoder_runtime::service::RunOptions {
392 approver: Some(Arc::clone(peer) as Arc<dyn Approver>),
393 progress: Some(Arc::clone(peer) as Arc<dyn Progress>),
394 cancel: Some(cancel),
395 ..speccoder_runtime::service::RunOptions::default()
396 };
397 perform(
398 root,
399 feature,
400 Dispatch {
401 run,
402 input,
403 via: Via::Web,
404 options,
405 },
406 )
407}