Skip to main content

ljos_cli/
pack_link.rs

1//! The only speaker that may carry this seat's packsetd token.
2//!
3//! The token stays in this module. Each request is one byte stream to the
4//! socket address resolved for the writer's URL, with no proxy and no
5//! redirect. The proof runs before the token is written on that stream. A
6//! stream that closes is not remembered: the next call proves again before
7//! the token goes out. packsetd answers one request and closes, so the two
8//! refusals and the call that carries the token are three sockets in a row;
9//! the proof is not kept for a later call.
10
11use std::collections::HashMap;
12use std::io::{Read, Write};
13use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
14use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
15use std::sync::{Arc, Mutex, OnceLock};
16use std::time::{Duration, Instant};
17
18use packset_client::{Error, Hit};
19use serde::de::DeserializeOwned;
20use serde_json::Value;
21use url::Url;
22
23/// The seat's pack writer. The token field is private to this module.
24pub struct PacksetClient {
25    base: String,
26    origin: Url,
27    addr: Option<SocketAddr>,
28    workspace: Option<String>,
29    token: Mutex<Option<String>>,
30}
31
32impl Clone for PacksetClient {
33    fn clone(&self) -> Self {
34        Self {
35            base: self.base.clone(),
36            origin: self.origin.clone(),
37            addr: self.addr,
38            workspace: self.workspace.clone(),
39            token: Mutex::new(self.token.lock().unwrap_or_else(|p| p.into_inner()).clone()),
40        }
41    }
42}
43
44/// One answer. `trusted` means the token was on this answer, after the
45/// proof held.
46#[derive(Debug)]
47pub(crate) struct Answer {
48    pub status: u16,
49    pub body: String,
50    pub trusted: bool,
51}
52
53struct Slot {
54    stream: Option<TcpStream>,
55    /// The proof held on `stream` and the stream is still the one it held on.
56    proven: bool,
57    token_tag: u64,
58    /// The last answer on this socket carried the token.
59    carried: bool,
60}
61
62fn slots() -> &'static Mutex<HashMap<SocketAddr, Arc<Mutex<Slot>>>> {
63    static SLOTS: OnceLock<Mutex<HashMap<SocketAddr, Arc<Mutex<Slot>>>>> = OnceLock::new();
64    SLOTS.get_or_init(|| Mutex::new(HashMap::new()))
65}
66
67fn slot_for(addr: SocketAddr) -> Arc<Mutex<Slot>> {
68    let mut map = slots().lock().unwrap_or_else(|p| p.into_inner());
69    map.entry(addr)
70        .or_insert_with(|| {
71            Arc::new(Mutex::new(Slot {
72                stream: None,
73                proven: false,
74                token_tag: 0,
75                carried: false,
76            }))
77        })
78        .clone()
79}
80
81fn load_token() -> Option<String> {
82    packset_client::token()
83}
84
85fn token_tag(token: Option<&str>) -> u64 {
86    let Some(token) = token else {
87        return 0;
88    };
89    let mut hash = 0xcbf29ce484222325u64;
90    for byte in token.as_bytes() {
91        hash ^= u64::from(*byte);
92        hash = hash.wrapping_mul(0x100000001b3);
93    }
94    hash
95}
96
97fn nonce() -> Option<String> {
98    use std::io::Read as _;
99    let mut raw = [0u8; 32];
100    std::fs::File::open("/dev/urandom")
101        .and_then(|mut file| file.read_exact(&mut raw))
102        .ok()
103        .map(|()| hex::encode(raw))
104}
105
106fn pin(url: &Url) -> Option<SocketAddr> {
107    let host = url.host_str()?;
108    let port = url.port_or_known_default()?;
109    (host, port).to_socket_addrs().ok()?.next()
110}
111
112fn host_header(url: &Url) -> String {
113    match (url.host_str(), url.port()) {
114        (Some(host), Some(port)) if host.contains(':') => format!("[{host}]:{port}"),
115        (Some(host), Some(port)) => format!("{host}:{port}"),
116        (Some(host), None) => host.to_string(),
117        _ => "localhost".to_string(),
118    }
119}
120
121fn budget() -> Duration {
122    std::env::var("PACKSET_TIMEOUT_MS")
123        .ok()
124        .and_then(|v| v.trim().parse::<u64>().ok())
125        .filter(|ms| *ms > 0)
126        .map_or(Duration::from_secs(30), Duration::from_millis)
127}
128
129/// Whether every socket listening on the loopback port of `url` belongs to
130/// another user. Unknown is not foreign.
131fn foreign(url: &str) -> bool {
132    #[cfg(target_os = "linux")]
133    {
134        use std::os::unix::fs::MetadataExt;
135        let Some(port) = loopback_port(url) else {
136            return false;
137        };
138        let Ok(me) = std::fs::metadata("/proc/self").map(|m| m.uid()) else {
139            return false;
140        };
141        let owners: Vec<u32> = ["/proc/net/tcp", "/proc/net/tcp6"]
142            .iter()
143            .filter_map(|table| std::fs::read_to_string(table).ok())
144            .flat_map(|text| listen_uids(&text, port))
145            .collect();
146        !owners.is_empty() && owners.iter().all(|uid| *uid != me)
147    }
148    #[cfg(not(target_os = "linux"))]
149    {
150        let _ = url;
151        false
152    }
153}
154
155#[cfg(target_os = "linux")]
156fn loopback_port(url: &str) -> Option<u16> {
157    let rest = url
158        .strip_prefix("http://")
159        .or_else(|| url.strip_prefix("https://"))?;
160    let authority = rest.split(['/', '?', '#']).next()?;
161    let (host, port) = match authority.rsplit_once(':') {
162        Some((host, port)) if !port.contains(']') => (host, port.parse().ok()?),
163        _ => (authority, 80),
164    };
165    matches!(host, "127.0.0.1" | "localhost" | "[::1]").then_some(port)
166}
167
168#[cfg(target_os = "linux")]
169fn listen_uids(table: &str, port: u16) -> Vec<u32> {
170    table
171        .lines()
172        .skip(1)
173        .filter_map(|line| {
174            let fields: Vec<&str> = line.split_whitespace().collect();
175            if fields.get(3) != Some(&"0A") {
176                return None;
177            }
178            let hex = fields.get(1)?.rsplit(':').next()?;
179            if u16::from_str_radix(hex, 16).ok()? != port {
180                return None;
181            }
182            fields.get(7)?.parse().ok()
183        })
184        .collect()
185}
186
187fn target(path: &str, query: &[(&str, &str)]) -> String {
188    if query.is_empty() {
189        return path.to_string();
190    }
191    let mut ser = url::form_urlencoded::Serializer::new(String::new());
192    for (key, value) in query {
193        ser.append_pair(key, value);
194    }
195    format!("{path}?{}", ser.finish())
196}
197
198struct Raw {
199    status: u16,
200    body: String,
201    keep: bool,
202}
203
204/// The pack accepted a connection and then stayed silent, or the connect
205/// itself never finished. While a [`QuietPack`] guard is alive, the first
206/// such read still waits out its budget and a later read returns before it
207/// opens another socket. A consensus reading asks several times. Without
208/// the stop, each ask waited the whole budget and the command sat for the
209/// sum. Dropping the guard clears it, so a later call can try the pack again.
210static PACK_QUIET: AtomicBool = AtomicBool::new(false);
211static QUIET_HELD: AtomicUsize = AtomicUsize::new(0);
212
213/// Remember one quiet pack for the reads this guard covers.
214pub struct QuietPack;
215
216impl QuietPack {
217    /// Start remembering a silent pack. The first guard of a pile drops a
218    /// quiet read already seen, so the pile starts with a fresh try.
219    #[must_use]
220    pub fn enter() -> Self {
221        if QUIET_HELD.fetch_add(1, Ordering::AcqRel) == 0 {
222            PACK_QUIET.store(false, Ordering::Relaxed);
223        }
224        Self
225    }
226}
227
228impl Drop for QuietPack {
229    fn drop(&mut self) {
230        if QUIET_HELD.fetch_sub(1, Ordering::AcqRel) == 1 {
231            PACK_QUIET.store(false, Ordering::Relaxed);
232        }
233    }
234}
235
236fn holding_quiet() -> bool {
237    QUIET_HELD.load(Ordering::Relaxed) > 0
238}
239
240fn pack_quiet() -> bool {
241    holding_quiet() && PACK_QUIET.load(Ordering::Relaxed)
242}
243
244fn mark_pack_quiet() {
245    if holding_quiet() {
246        PACK_QUIET.store(true, Ordering::Relaxed);
247    }
248}
249
250fn quiet_io() -> std::io::Error {
251    std::io::Error::new(std::io::ErrorKind::TimedOut, "the pack did not answer")
252}
253
254/// A read or a connect that ended because the socket stayed silent.
255/// A refused connect is not this: the writer may still be starting.
256fn io_is_quiet(err: &std::io::Error) -> bool {
257    matches!(
258        err.kind(),
259        std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock
260    )
261}
262
263fn connect(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
264    if pack_quiet() {
265        return Err(quiet_io());
266    }
267    if side_budget_spent() {
268        return Err(budget_spent());
269    }
270    let started = Instant::now();
271    let result = connect_once(addr, timeout);
272    // A connect that returns a socket is the pack accepting. Time spent
273    // there is not a quiet wait. A connect that fails spends the budget.
274    // A connect that never finishes is remembered, so the next read does
275    // not wait the budget out again.
276    if let Err(err) = &result {
277        if io_is_quiet(err) {
278            mark_pack_quiet();
279        }
280        super::charge_side_wait(started.elapsed());
281    }
282    result
283}
284
285/// A side event's pack budget is gone. Opening a socket for a one
286/// millisecond timer still blocks, and a prompt makes several of those
287/// reads, so the call returns before the connect.
288fn side_budget_spent() -> bool {
289    super::side_remaining_ms() == Some(0)
290}
291
292fn budget_spent() -> std::io::Error {
293    std::io::Error::new(
294        std::io::ErrorKind::TimedOut,
295        "the side event's pack budget is spent",
296    )
297}
298
299fn connect_once(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
300    let wait = capped_wait(timeout);
301    let stream = TcpStream::connect_timeout(&addr, wait)?;
302    stream.set_read_timeout(Some(wait))?;
303    stream.set_write_timeout(Some(wait))?;
304    let _ = stream.set_nodelay(true);
305    Ok(stream)
306}
307
308/// A side-event bound shrinks this wait. A zero bound is one millisecond,
309/// so a later read fails at once instead of taking the default.
310fn capped_wait(timeout: Duration) -> Duration {
311    let wait = super::cap_pack_wait(timeout);
312    if wait.is_zero() {
313        Duration::from_millis(1)
314    } else {
315        wait
316    }
317}
318
319fn write_all(stream: &mut TcpStream, bytes: &[u8]) -> std::io::Result<()> {
320    let mut sent = 0;
321    while sent < bytes.len() {
322        match stream.write(&bytes[sent..]) {
323            Ok(0) => {
324                return Err(std::io::Error::new(
325                    std::io::ErrorKind::WriteZero,
326                    "the pack closed the socket",
327                ))
328            }
329            Ok(n) => sent += n,
330            Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
331            Err(err) => return Err(err),
332        }
333    }
334    stream.flush()
335}
336
337fn read_response(stream: &mut TcpStream, deadline: Instant) -> std::io::Result<Raw> {
338    let mut buf = Vec::new();
339    let mut tmp = [0u8; 4096];
340    let head_end = loop {
341        if buf.len() > 64 * 1024 {
342            return Err(std::io::Error::new(
343                std::io::ErrorKind::InvalidData,
344                "the pack's headers are too long",
345            ));
346        }
347        arm_read(stream, deadline)?;
348        let n = read_some(stream, &mut tmp)?;
349        buf.extend_from_slice(&tmp[..n]);
350        if let Some(at) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
351            break at;
352        }
353    };
354    let header = String::from_utf8_lossy(&buf[..head_end]).into_owned();
355    let status = header
356        .lines()
357        .next()
358        .and_then(|line| line.split_whitespace().nth(1))
359        .and_then(|code| code.parse().ok())
360        .ok_or_else(|| {
361            std::io::Error::new(
362                std::io::ErrorKind::InvalidData,
363                "the pack's status line is not HTTP",
364            )
365        })?;
366    let mut length = None;
367    let mut close = header.starts_with("HTTP/1.0");
368    for line in header.lines().skip(1) {
369        let Some((name, value)) = line.split_once(':') else {
370            continue;
371        };
372        if name.eq_ignore_ascii_case("content-length") {
373            length = value.trim().parse().ok();
374        } else if name.eq_ignore_ascii_case("connection")
375            && value.trim().eq_ignore_ascii_case("close")
376        {
377            close = true;
378        }
379    }
380    let mut body = buf.split_off(head_end + 4);
381    if let Some(length) = length {
382        while body.len() < length {
383            arm_read(stream, deadline)?;
384            let n = read_some(stream, &mut tmp)?;
385            body.extend_from_slice(&tmp[..n]);
386            if body.len() > length.saturating_add(64 * 1024) {
387                break;
388            }
389        }
390        body.truncate(length);
391    } else {
392        loop {
393            if deadline.saturating_duration_since(Instant::now()).is_zero() {
394                break;
395            }
396            let _ = arm_read(stream, deadline);
397            match stream.read(&mut tmp) {
398                Ok(0) => break,
399                Ok(n) => body.extend_from_slice(&tmp[..n]),
400                Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
401                Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => break,
402                Err(err) if err.kind() == std::io::ErrorKind::TimedOut => break,
403                Err(_) => break,
404            }
405        }
406        close = true;
407    }
408    // A redirect is the answer. The token must not follow it.
409    Ok(Raw {
410        status,
411        body: String::from_utf8_lossy(&body).into_owned(),
412        keep: !close && !(300..400).contains(&status),
413    })
414}
415
416fn arm_read(stream: &TcpStream, deadline: Instant) -> std::io::Result<()> {
417    let remain = deadline.saturating_duration_since(Instant::now());
418    if remain.is_zero() {
419        return Err(std::io::Error::new(
420            std::io::ErrorKind::TimedOut,
421            "the pack did not answer",
422        ));
423    }
424    stream.set_read_timeout(Some(remain))?;
425    Ok(())
426}
427
428fn read_some(stream: &mut TcpStream, tmp: &mut [u8]) -> std::io::Result<usize> {
429    loop {
430        match stream.read(tmp) {
431            Ok(0) => {
432                return Err(std::io::Error::new(
433                    std::io::ErrorKind::UnexpectedEof,
434                    "the pack closed the socket",
435                ))
436            }
437            Ok(n) => return Ok(n),
438            Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
439            Err(err) => return Err(err),
440        }
441    }
442}
443
444fn render(
445    method: &str,
446    host: &str,
447    path: &str,
448    token: Option<&str>,
449    body: Option<&[u8]>,
450) -> Vec<u8> {
451    let mut head =
452        format!("{method} {path} HTTP/1.1\r\nHost: {host}\r\nConnection: keep-alive\r\n");
453    if let Some(token) = token {
454        head.push_str("Authorization: Bearer ");
455        head.push_str(token);
456        head.push_str("\r\n");
457    }
458    let mut out = Vec::new();
459    if let Some(body) = body {
460        head.push_str("Content-Type: application/json\r\n");
461        head.push_str(&format!("Content-Length: {}\r\n\r\n", body.len()));
462        out.extend(head.into_bytes());
463        out.extend_from_slice(body);
464    } else {
465        head.push_str("\r\n");
466        out.extend(head.into_bytes());
467    }
468    out
469}
470
471struct Wire<'a> {
472    method: &'a str,
473    path: &'a str,
474    query: &'a [(&'a str, &'a str)],
475    body: Option<&'a [u8]>,
476    token: Option<&'a str>,
477}
478
479enum Gate {
480    /// The proof held. The call may carry the token.
481    Send,
482    /// The listener did not ask for the token.
483    Bare,
484    /// The writer is busy. The call is not sent.
485    Busy(Raw),
486}
487
488impl PacksetClient {
489    pub fn new(base: impl Into<String>) -> Self {
490        let mut base = base.into();
491        while base.ends_with('/') {
492            base.pop();
493        }
494        let origin = Url::parse(&base).unwrap_or_else(|_| Url::parse("http://127.0.0.1/").unwrap());
495        let addr = Url::parse(&base).ok().as_ref().and_then(pin);
496        Self {
497            base,
498            origin,
499            addr,
500            workspace: None,
501            token: Mutex::new(load_token()),
502        }
503    }
504
505    /// The writer the seat talks to. `PACKSET_URL=off` is no pack.
506    pub fn from_env() -> Result<Self, Error> {
507        packset_client::load_seat_env();
508        let url = std::env::var("PACKSET_URL")
509            .or_else(|_| std::env::var("INSIDE_MEMORY_URL"))
510            .ok()
511            .filter(|url| !url.is_empty());
512        match url {
513            Some(url) if url == "off" => Err(Error::NoUrl),
514            Some(url) => Ok(Self::new(url)),
515            None => Ok(Self::new(format!(
516                "http://127.0.0.1:{}",
517                packset_client::default_port()
518            ))),
519        }
520    }
521
522    #[must_use]
523    pub fn with_workspace(mut self, workspace: impl Into<String>) -> Self {
524        let workspace = workspace.into();
525        self.workspace = (!workspace.is_empty()).then_some(workspace);
526        self
527    }
528
529    pub fn base(&self) -> &str {
530        &self.base
531    }
532
533    /// Whether this seat has a token to show. The token itself stays here.
534    #[must_use]
535    pub(crate) fn has_token(&self) -> bool {
536        load_token().is_some()
537    }
538
539    /// Whether a call may carry the token: the seat has one, and another
540    /// user does not hold the loopback port.
541    #[must_use]
542    pub(crate) fn may_carry(&self) -> bool {
543        load_token().is_some() && !foreign(&self.base)
544    }
545
546    /// Whether the last answer on this socket carried the token. A closed
547    /// socket does not keep this for the next call: that call proves again.
548    #[must_use]
549    pub(crate) fn carried(&self) -> bool {
550        let Some(addr) = self.addr else {
551            return false;
552        };
553        let map = slots().lock().unwrap_or_else(|p| p.into_inner());
554        map.get(&addr)
555            .is_some_and(|slot| slot.lock().unwrap_or_else(|p| p.into_inner()).carried)
556    }
557
558    pub fn workspace(&self) -> String {
559        if let Some(workspace) = &self.workspace {
560            return workspace.clone();
561        }
562        if let Ok(workspace) = std::env::var("PACKSET_WORKSPACE") {
563            if !workspace.is_empty() {
564                return workspace;
565            }
566        }
567        let cwd = std::env::var("GROKOS_WORKSPACE")
568            .ok()
569            .map(|s| s.trim().to_string())
570            .filter(|s| !s.is_empty())
571            .map(std::path::PathBuf::from)
572            .or_else(|| std::env::current_dir().ok())
573            .unwrap_or_else(|| std::path::PathBuf::from("."));
574        let abs = cwd.canonicalize().unwrap_or(cwd);
575        let shown = abs.to_string_lossy().into_owned();
576        if let Ok(answer) = self.roundtrip(
577            "GET",
578            "/v1/identity",
579            &[("cwd", shown.as_str())],
580            None,
581            true,
582            budget(),
583        ) {
584            if answer.status == 200 {
585                if let Ok(val) = serde_json::from_str::<Value>(&answer.body) {
586                    if let Some(ws) = val.get("workspace").and_then(|v| v.as_str()) {
587                        if !ws.is_empty() {
588                            return ws.to_string();
589                        }
590                    }
591                }
592            }
593        }
594        format!("dir:{}", abs.display())
595    }
596
597    /// One request. `bearer` asks to carry the token; it is left off when
598    /// the proof does not hold or another user owns the port. A 3xx is
599    /// returned as the answer and not followed. HTTP 503 is one answer:
600    /// the caller decides whether to ask again.
601    pub(crate) fn roundtrip(
602        &self,
603        method: &str,
604        path: &str,
605        query: &[(&str, &str)],
606        body: Option<&[u8]>,
607        bearer: bool,
608        timeout: Duration,
609    ) -> Result<Answer, Error> {
610        let Some(addr) = self.addr else {
611            return Err(Error::Bad(format!(
612                "{} is not a socket this seat can pin",
613                self.base
614            )));
615        };
616        let token = load_token();
617        *self.token.lock().unwrap_or_else(|p| p.into_inner()) = token.clone();
618        let allow = bearer && token.is_some() && !foreign(&self.base);
619        self.exchange(
620            addr,
621            &Wire {
622                method,
623                path,
624                query,
625                body,
626                token: token.as_deref(),
627            },
628            allow,
629            timeout,
630        )
631    }
632
633    fn exchange(
634        &self,
635        addr: SocketAddr,
636        call: &Wire<'_>,
637        allow: bool,
638        timeout: Duration,
639    ) -> Result<Answer, Error> {
640        let slot = slot_for(addr);
641        let mut live = slot.lock().unwrap_or_else(|p| p.into_inner());
642        let tag = token_tag(call.token);
643        if live.token_tag != tag {
644            live.stream = None;
645            live.proven = false;
646            live.token_tag = tag;
647        }
648        let carried = if allow {
649            match self.gate(&mut live, addr, timeout)? {
650                Gate::Bare => None,
651                Gate::Send => call.token,
652                Gate::Busy(raw) => {
653                    live.carried = false;
654                    live.proven = false;
655                    return Ok(Answer {
656                        status: raw.status,
657                        body: raw.body,
658                        trusted: false,
659                    });
660                }
661            }
662        } else {
663            None
664        };
665        if live.stream.is_none() {
666            match connect(addr, timeout) {
667                Ok(stream) => live.stream = Some(stream),
668                Err(err) => {
669                    live.proven = false;
670                    live.carried = false;
671                    return Err(Error::Io(err));
672                }
673            }
674            // The probes ran on sockets the writer closed. This socket is
675            // the one the token rides, the third step of the same proof,
676            // not a later call.
677            live.proven = false;
678        }
679        let raw = match send(
680            &mut live,
681            &self.origin,
682            &Wire {
683                token: carried,
684                ..*call
685            },
686            timeout,
687        ) {
688            Ok(raw) => raw,
689            Err(err) => {
690                live.stream = None;
691                live.proven = false;
692                live.carried = false;
693                return Err(Error::Io(err));
694            }
695        };
696        let trusted = carried.is_some();
697        live.carried = trusted;
698        if trusted && live.stream.is_some() {
699            live.proven = true;
700        } else if live.stream.is_none() {
701            live.proven = false;
702        }
703        Ok(Answer {
704            status: raw.status,
705            body: raw.body,
706            trusted,
707        })
708    }
709
710    /// Two refusals on this socket, then the call may carry the token.
711    /// A busy writer (503) is returned as that answer; the call is not sent.
712    fn gate(&self, live: &mut Slot, addr: SocketAddr, timeout: Duration) -> Result<Gate, Error> {
713        if live.proven && live.stream.is_some() {
714            return Ok(Gate::Send);
715        }
716        live.proven = false;
717        let (Some(id), Some(bogus)) = (nonce(), nonce()) else {
718            return Err(Error::Io(std::io::Error::other(
719                "no randomness for the pack proof",
720            )));
721        };
722        let workspace = self.proof_workspace();
723        let path = format!("/v1/atoms/{}", &id[..32]);
724        let query = [("workspace", workspace.as_str())];
725        let mut stage = 0u8;
726        let mut restarts = 0u8;
727        loop {
728            if live.stream.is_none() {
729                live.stream = Some(connect(addr, timeout).map_err(Error::Io)?);
730                live.proven = false;
731            }
732            let shown = if stage == 0 {
733                None
734            } else {
735                Some(bogus.as_str())
736            };
737            let raw = match send(
738                live,
739                &self.origin,
740                &Wire {
741                    method: "GET",
742                    path: &path,
743                    query: &query,
744                    body: None,
745                    token: shown,
746                },
747                timeout,
748            ) {
749                Ok(raw) => raw,
750                Err(err) => {
751                    live.stream = None;
752                    live.proven = false;
753                    restarts += 1;
754                    if restarts > 2 {
755                        return Err(Error::Io(err));
756                    }
757                    stage = 0;
758                    continue;
759                }
760            };
761            if raw.status == 503 {
762                return Ok(Gate::Busy(raw));
763            }
764            if raw.status != 401 {
765                return Ok(Gate::Bare);
766            }
767            if stage == 0 {
768                stage = 1;
769                continue;
770            }
771            return Ok(Gate::Send);
772        }
773    }
774
775    fn proof_workspace(&self) -> String {
776        if let Some(workspace) = &self.workspace {
777            return workspace.clone();
778        }
779        std::env::var("PACKSET_WORKSPACE")
780            .ok()
781            .filter(|w| !w.is_empty())
782            .unwrap_or_else(|| "seat".to_string())
783    }
784
785    /// [`Self::roundtrip`] again while the writer answers busy, inside `timeout`.
786    fn tried(
787        &self,
788        method: &str,
789        path: &str,
790        query: &[(&str, &str)],
791        body: Option<&[u8]>,
792        timeout: Duration,
793    ) -> Result<Answer, Error> {
794        let started = Instant::now();
795        let mut wait = Duration::from_millis(50);
796        for n in 0..4 {
797            let answer = self.roundtrip(method, path, query, body, true, timeout)?;
798            if answer.status == 503 && n < 3 && started.elapsed() + wait < timeout {
799                std::thread::sleep(wait);
800                wait *= 3;
801                continue;
802            }
803            return Ok(answer);
804        }
805        Err(Error::Bad(format!(
806            "{}{path}: 503: packsetd is busy",
807            self.base
808        )))
809    }
810
811    fn json_ok<T: DeserializeOwned>(&self, path: &str, answer: Answer) -> Result<T, Error> {
812        if !(200..300).contains(&answer.status) {
813            return Err(refused(
814                &format!("{}{path}", self.base),
815                answer.status,
816                &answer.body,
817            ));
818        }
819        serde_json::from_str(&answer.body).map_err(Error::from)
820    }
821
822    pub fn health(&self) -> Result<String, Error> {
823        let answer = self.tried("GET", "/health", &[], None, budget())?;
824        if !(200..300).contains(&answer.status) {
825            return Err(refused(
826                &format!("{}/health", self.base),
827                answer.status,
828                &answer.body,
829            ));
830        }
831        Ok(answer.body)
832    }
833
834    pub fn status(&self, workspace: Option<&str>) -> Result<Value, Error> {
835        let query: Vec<(&str, &str)> = workspace
836            .map(|workspace| vec![("workspace", workspace)])
837            .unwrap_or_default();
838        let answer = self.tried("GET", "/v1/status", &query, None, budget())?;
839        self.json_ok("/v1/status", answer)
840    }
841
842    pub fn post_atom(&self, atom: &Value) -> Result<Value, Error> {
843        let bytes = serde_json::to_vec(atom)?;
844        let answer = self.tried("POST", "/v1/atoms", &[], Some(&bytes), budget())?;
845        self.json_ok("/v1/atoms", answer)
846    }
847
848    pub fn delete_atom(
849        &self,
850        workspace: &str,
851        id: &str,
852        why: Option<&str>,
853    ) -> Result<Value, Error> {
854        let mut body = serde_json::json!({"workspace": workspace, "id": id});
855        if let Some(accession) = why {
856            body["why"] = Value::String(accession.to_string());
857        }
858        let bytes = serde_json::to_vec(&body)?;
859        let answer = self.tried("POST", "/v1/atoms/delete", &[], Some(&bytes), budget())?;
860        if answer.status == 404 {
861            return Err(Error::Bad(format!("no atom {id}")));
862        }
863        self.json_ok("/v1/atoms/delete", answer)
864    }
865
866    pub fn atoms_of_kind(&self, workspace: &str, kind: &str) -> Result<Vec<Value>, Error> {
867        let answer = self.tried(
868            "GET",
869            "/v1/atoms",
870            &[("workspace", workspace), ("kind", kind)],
871            None,
872            budget(),
873        )?;
874        let body: Value = self.json_ok("/v1/atoms", answer)?;
875        Ok(body
876            .get("atoms")
877            .and_then(Value::as_array)
878            .cloned()
879            .unwrap_or_default())
880    }
881
882    pub fn atoms_in_set(&self, workspace: &str, set: &str) -> Result<Vec<Value>, Error> {
883        let answer = self.tried(
884            "GET",
885            "/v1/pack",
886            &[("workspace", workspace), ("set", set)],
887            None,
888            budget(),
889        )?;
890        let body: Value = self.json_ok("/v1/pack", answer)?;
891        Ok(body
892            .get("atoms")
893            .and_then(Value::as_array)
894            .cloned()
895            .unwrap_or_default())
896    }
897
898    pub fn sweep(&self, workspace: &str) -> Result<Value, Error> {
899        let bytes = serde_json::to_vec(&serde_json::json!({"workspace": workspace}))?;
900        let answer = self.tried("POST", "/v1/sweep", &[], Some(&bytes), budget())?;
901        self.json_ok("/v1/sweep", answer)
902    }
903
904    pub fn grade(&self, workspace: &str, id: &str, recalled: bool) -> Result<Value, Error> {
905        let bytes = serde_json::to_vec(&serde_json::json!({
906            "workspace": workspace,
907            "id": id,
908            "recalled": recalled,
909        }))?;
910        let answer = self.tried("POST", "/v1/grade", &[], Some(&bytes), budget())?;
911        self.json_ok("/v1/grade", answer)
912    }
913
914    pub fn activate_as(
915        &self,
916        workspace: &str,
917        q: &str,
918        limit: u32,
919        fire: bool,
920        lens: Option<&str>,
921    ) -> Result<Value, Error> {
922        let limit = limit.to_string();
923        let fire = if fire { "1" } else { "0" };
924        let mut query = vec![
925            ("workspace", workspace),
926            ("q", q),
927            ("limit", limit.as_str()),
928            ("fire", fire),
929        ];
930        if let Some(name) = lens.filter(|n| !n.trim().is_empty()) {
931            query.push(("as", name));
932        }
933        let answer = self.tried("GET", "/v1/activate", &query, None, budget())?;
934        self.json_ok("/v1/activate", answer)
935    }
936
937    pub fn hubs(&self, workspace: &str, limit: usize) -> Result<Value, Error> {
938        let limit = limit.to_string();
939        let answer = self.tried(
940            "GET",
941            "/v1/hubs",
942            &[("workspace", workspace), ("limit", limit.as_str())],
943            None,
944            budget(),
945        )?;
946        self.json_ok("/v1/hubs", answer)
947    }
948
949    pub fn consolidate(&self, workspace: &str, apply: bool) -> Result<Value, Error> {
950        let bytes =
951            serde_json::to_vec(&serde_json::json!({"workspace": workspace, "apply": apply}))?;
952        let answer = self.tried("POST", "/v1/consolidate", &[], Some(&bytes), budget())?;
953        self.json_ok("/v1/consolidate", answer)
954    }
955
956    pub fn search_opts(
957        &self,
958        workspace: &str,
959        q: &str,
960        limit: u32,
961        as_of: Option<&str>,
962        rerank: bool,
963    ) -> Result<Vec<Hit>, Error> {
964        Ok(self.search_fetched(workspace, q, limit, as_of, rerank)?.0)
965    }
966
967    pub(crate) fn search_fetched(
968        &self,
969        workspace: &str,
970        q: &str,
971        limit: u32,
972        as_of: Option<&str>,
973        rerank: bool,
974    ) -> Result<(Vec<Hit>, bool), Error> {
975        let limit_s = limit.to_string();
976        let mut query = vec![
977            ("workspace", workspace),
978            ("q", q),
979            ("limit", limit_s.as_str()),
980        ];
981        if let Some(at) = as_of {
982            query.push(("as_of", at));
983        }
984        if rerank {
985            query.push(("rerank", "1"));
986        }
987        let timeout = if rerank {
988            Duration::from_secs(60).max(budget())
989        } else {
990            budget()
991        };
992        let answer = self.tried("GET", "/v1/search", &query, None, timeout)?;
993        let trusted = answer.trusted;
994        let body: Value = self.json_ok("/v1/search", answer)?;
995        let hits = body.get("hits").cloned().unwrap_or(Value::Array(vec![]));
996        Ok((serde_json::from_value(hits)?, trusted))
997    }
998
999    pub fn fire(&self, workspace: &str, ids: &[String]) -> Result<Value, Error> {
1000        let bytes = serde_json::to_vec(&serde_json::json!({
1001            "workspace": workspace,
1002            "ids": ids,
1003            "as": "",
1004        }))?;
1005        let answer = self.tried("POST", "/v1/fire", &[], Some(&bytes), budget())?;
1006        self.json_ok("/v1/fire", answer)
1007    }
1008}
1009
1010fn refused(url: &str, code: u16, body: &str) -> Error {
1011    let reason = serde_json::from_str::<Value>(body)
1012        .ok()
1013        .and_then(|v| v.get("error").and_then(|r| r.as_str()).map(str::to_string))
1014        .unwrap_or_else(|| body.to_string());
1015    let reason = reason.trim();
1016    if code == 401 {
1017        return Error::Bad(format!(
1018            "{url}: 401: {reason}; this seat's token does not match the writer's, \
1019             so it is another user's writer or PACKSET_HOME names another pack"
1020        ));
1021    }
1022    if reason.is_empty() {
1023        Error::Bad(format!("{url}: status code {code}"))
1024    } else {
1025        Error::Bad(format!("{url}: {code}: {reason}"))
1026    }
1027}
1028
1029fn send(live: &mut Slot, origin: &Url, call: &Wire<'_>, timeout: Duration) -> std::io::Result<Raw> {
1030    if pack_quiet() {
1031        return Err(quiet_io());
1032    }
1033    if side_budget_spent() {
1034        return Err(budget_spent());
1035    }
1036    let started = Instant::now();
1037    let result = send_once(live, origin, call, timeout);
1038    // An answer is not charged. A read that ends without one is a quiet
1039    // socket and spends the budget, so the next read does not open another.
1040    // Outside a hook the side budget is not armed. The quiet flag is that
1041    // same stop for a command, which asks the pack more than once.
1042    if let Err(err) = &result {
1043        if io_is_quiet(err) {
1044            mark_pack_quiet();
1045        }
1046        super::charge_side_wait(started.elapsed());
1047    }
1048    result
1049}
1050
1051fn send_once(
1052    live: &mut Slot,
1053    origin: &Url,
1054    call: &Wire<'_>,
1055    timeout: Duration,
1056) -> std::io::Result<Raw> {
1057    let stream = live
1058        .stream
1059        .as_mut()
1060        .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotConnected, "no pack socket"))?;
1061    let wait = capped_wait(timeout);
1062    let deadline = Instant::now() + wait;
1063    stream.set_read_timeout(Some(wait))?;
1064    stream.set_write_timeout(Some(wait))?;
1065    let bytes = render(
1066        call.method,
1067        &host_header(origin),
1068        &target(call.path, call.query),
1069        call.token,
1070        call.body,
1071    );
1072    if let Err(err) = write_all(stream, &bytes) {
1073        live.stream = None;
1074        live.proven = false;
1075        return Err(err);
1076    }
1077    match read_response(stream, deadline) {
1078        Ok(raw) => {
1079            if !raw.keep {
1080                live.stream = None;
1081                live.proven = false;
1082            }
1083            Ok(raw)
1084        }
1085        Err(err) => {
1086            live.stream = None;
1087            live.proven = false;
1088            Err(err)
1089        }
1090    }
1091}