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, SystemTime};
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, Clone)]
47pub(crate) struct Answer {
48    pub status: u16,
49    pub body: String,
50    pub trusted: bool,
51    /// `Retry-After` when the writer sent one, in whole seconds.
52    pub retry_after: Option<Duration>,
53}
54
55/// Answers a hook process already has for a search or a status read, by
56/// request, for [`ANSWER_MEMO`]. A prompt hook asks the same search
57/// twice and the status twice; the second asks are answered here. Only a
58/// hook keeps one, and any write through the client drops it.
59type KeptAnswer = (String, Instant, Answer);
60static KEPT_ANSWERS: Mutex<Vec<KeptAnswer>> = Mutex::new(Vec::new());
61const ANSWER_MEMO: Duration = Duration::from_secs(2);
62
63fn answer_memo_key(
64    method: &str,
65    path: &str,
66    query: &[(&str, &str)],
67    bearer: bool,
68) -> Option<String> {
69    if method != "GET" || !matches!(path, "/v1/search" | "/v1/status") {
70        return None;
71    }
72    std::env::var_os("LJOS_IN_HOOK")?;
73    let mut key = format!("{path}|{bearer}");
74    for (k, v) in query {
75        key.push('|');
76        key.push_str(k);
77        key.push('=');
78        key.push_str(v);
79    }
80    Some(key)
81}
82
83fn kept_answer(key: &str) -> Option<Answer> {
84    let kept = KEPT_ANSWERS.lock().unwrap_or_else(|p| p.into_inner());
85    kept.iter()
86        .find(|(k, at, _)| k == key && at.elapsed() < ANSWER_MEMO)
87        .map(|(_, _, answer)| answer.clone())
88}
89
90fn keep_answer(key: String, answer: &Answer) {
91    let mut kept = KEPT_ANSWERS.lock().unwrap_or_else(|p| p.into_inner());
92    kept.retain(|(k, _, _)| k != &key);
93    kept.push((key, Instant::now(), answer.clone()));
94}
95
96/// Drop every kept answer. Called on any write through the client.
97pub(crate) fn forget_kept_answers() {
98    KEPT_ANSWERS
99        .lock()
100        .unwrap_or_else(|p| p.into_inner())
101        .clear();
102}
103
104struct Slot {
105    stream: Option<TcpStream>,
106    /// The proof held on `stream` and the stream is still the one it held on.
107    proven: bool,
108    token_tag: u64,
109    /// The last answer on this socket carried the token.
110    carried: bool,
111}
112
113fn slots() -> &'static Mutex<HashMap<SocketAddr, Arc<Mutex<Slot>>>> {
114    static SLOTS: OnceLock<Mutex<HashMap<SocketAddr, Arc<Mutex<Slot>>>>> = OnceLock::new();
115    SLOTS.get_or_init(|| Mutex::new(HashMap::new()))
116}
117
118fn slot_for(addr: SocketAddr) -> Arc<Mutex<Slot>> {
119    let mut map = slots().lock().unwrap_or_else(|p| p.into_inner());
120    map.entry(addr)
121        .or_insert_with(|| {
122            Arc::new(Mutex::new(Slot {
123                stream: None,
124                proven: false,
125                token_tag: 0,
126                carried: false,
127            }))
128        })
129        .clone()
130}
131
132fn load_token() -> Option<String> {
133    packset_client::token()
134}
135
136fn token_tag(token: Option<&str>) -> u64 {
137    let Some(token) = token else {
138        return 0;
139    };
140    let mut hash = 0xcbf29ce484222325u64;
141    for byte in token.as_bytes() {
142        hash ^= u64::from(*byte);
143        hash = hash.wrapping_mul(0x100000001b3);
144    }
145    hash
146}
147
148fn nonce() -> Option<String> {
149    use std::io::Read as _;
150    let mut raw = [0u8; 32];
151    std::fs::File::open("/dev/urandom")
152        .and_then(|mut file| file.read_exact(&mut raw))
153        .ok()
154        .map(|()| hex::encode(raw))
155}
156
157fn pin(url: &Url) -> Option<SocketAddr> {
158    let host = url.host_str()?;
159    let port = url.port_or_known_default()?;
160    if !crate::tool_bound::armed() {
161        return (host, port).to_socket_addrs().ok()?.next();
162    }
163    let left = crate::wall_left().unwrap_or(Duration::from_millis(1));
164    if left.is_zero() {
165        crate::tool_bound::note("packsetd");
166        crate::tool_bound::mark_failed("packsetd timed out");
167        return None;
168    }
169    let host = host.to_string();
170    let (tx, rx) = std::sync::mpsc::channel();
171    std::thread::spawn(move || {
172        let found = (host.as_str(), port)
173            .to_socket_addrs()
174            .ok()
175            .and_then(|mut addrs| addrs.next());
176        let _ = tx.send(found);
177    });
178    match rx.recv_timeout(left) {
179        Ok(addr) => addr,
180        Err(_) => {
181            crate::tool_bound::note("packsetd");
182            crate::tool_bound::mark_failed("packsetd timed out");
183            None
184        }
185    }
186}
187
188fn host_header(url: &Url) -> String {
189    match (url.host_str(), url.port()) {
190        (Some(host), Some(port)) if host.contains(':') => format!("[{host}]:{port}"),
191        (Some(host), Some(port)) => format!("{host}:{port}"),
192        (Some(host), None) => host.to_string(),
193        _ => "localhost".to_string(),
194    }
195}
196
197fn budget() -> Duration {
198    let base = crate::pack_budget_ms()
199        .or_else(|| {
200            std::env::var("PACKSET_TIMEOUT_MS")
201                .ok()
202                .and_then(|v| v.trim().parse::<u64>().ok())
203                .filter(|ms| *ms > 0)
204        })
205        .map_or(Duration::from_secs(30), Duration::from_millis);
206    match crate::wall_left() {
207        Some(left) => base.min(left).max(Duration::from_millis(1)),
208        None => base,
209    }
210}
211
212struct Clock {
213    deadline: Instant,
214    depth: u32,
215}
216
217thread_local! {
218    /// This thread's pack budget. A nested call keeps the earlier deadline.
219    /// Another thread has its own: one search must not spend a read that
220    /// is running beside it.
221    static CLOCK: std::cell::RefCell<Option<Clock>> = const { std::cell::RefCell::new(None) };
222}
223
224/// Bound every pack socket call on this thread until this guard drops.
225/// A nested guard on the same thread keeps the earlier deadline, so a
226/// hook stays under 2s and an interactive command under 5s across every
227/// socket of the call. A call on another thread keeps the budget it armed.
228pub struct PackClock {
229    /// Drop has to run on the thread that entered, where the clock lives.
230    _thread: std::marker::PhantomData<*const ()>,
231}
232
233impl PackClock {
234    #[must_use]
235    pub fn enter(total: Duration) -> Self {
236        CLOCK.with(|slot| {
237            let mut slot = slot.borrow_mut();
238            match slot.as_mut() {
239                Some(clock) => clock.depth = clock.depth.saturating_add(1),
240                None => {
241                    *slot = Some(Clock {
242                        deadline: Instant::now() + total,
243                        depth: 1,
244                    });
245                }
246            }
247        });
248        Self {
249            _thread: std::marker::PhantomData,
250        }
251    }
252}
253
254impl Drop for PackClock {
255    fn drop(&mut self) {
256        CLOCK.with(|slot| {
257            let mut slot = slot.borrow_mut();
258            if let Some(clock) = slot.as_mut() {
259                clock.depth = clock.depth.saturating_sub(1);
260                if clock.depth == 0 {
261                    *slot = None;
262                }
263            }
264        });
265    }
266}
267
268fn clock_deadline() -> Option<Instant> {
269    CLOCK.with(|slot| slot.borrow().as_ref().map(|clock| clock.deadline))
270}
271
272pub(crate) fn clock_armed() -> bool {
273    clock_deadline().is_some()
274}
275
276/// Time left on the armed pack clock. `None` when no clock is armed.
277pub(crate) fn clock_left() -> Option<Duration> {
278    clock_deadline().map(|deadline| deadline.saturating_duration_since(Instant::now()))
279}
280
281/// A search or island read in this clock already went quiet, or the clock
282/// is spent. Later reads must not wait it out again.
283pub(crate) fn search_stalled() -> bool {
284    pack_quiet() || clock_deadline().is_some_and(|deadline| Instant::now() >= deadline)
285}
286
287pub(crate) fn error_is_timeout(err: &Error) -> bool {
288    match err {
289        Error::Io(io) => {
290            io_is_quiet(io)
291                || io.to_string().contains("the pack did not answer")
292                || io.to_string().contains("packsetd timed out")
293        }
294        _ => false,
295    }
296}
297
298/// Whether every socket listening on the loopback port of `url` belongs to
299/// another user. Unknown is not foreign.
300///
301/// A hook keeps its first answer for each url. The socket tables are read
302/// in full each time, a hook connects more than once, and it ends within
303/// the runner's cut-off. Elsewhere a search keeps its first answer until
304/// it ends ([`OwnerOnce`]), and any other call reads them again. A writer
305/// that stops leaves its port to whoever binds it next. A long-lived
306/// process that kept its first answer would go on carrying the token to
307/// another user's listener, or never to its own.
308fn foreign(url: &str) -> bool {
309    if std::env::var_os("LJOS_IN_HOOK").is_none() {
310        return SEARCH_OWNERS.with(|kept| match kept.borrow_mut().as_mut() {
311            Some(kept) => match kept.iter().find(|(u, _)| u == url) {
312                Some((_, known)) => *known,
313                None => {
314                    let answer = foreign_read(url);
315                    kept.push((url.to_string(), answer));
316                    answer
317                }
318            },
319            None => foreign_read(url),
320        });
321    }
322    static KNOWN: std::sync::LazyLock<Mutex<Vec<(String, bool)>>> =
323        std::sync::LazyLock::new(|| Mutex::new(Vec::new()));
324    if let Some((_, known)) = KNOWN
325        .lock()
326        .unwrap_or_else(|p| p.into_inner())
327        .iter()
328        .find(|(u, _)| u == url)
329    {
330        return *known;
331    }
332    let answer = foreign_read(url);
333    KNOWN
334        .lock()
335        .unwrap_or_else(|p| p.into_inner())
336        .push((url.to_string(), answer));
337    answer
338}
339
340thread_local! {
341    /// [`foreign`]'s answers for the search on this thread, by url, for
342    /// as long as an [`OwnerOnce`] lasts.
343    static SEARCH_OWNERS: std::cell::RefCell<Option<Vec<(String, bool)>>> =
344        const { std::cell::RefCell::new(None) };
345}
346
347/// Read the pack port's owner once for the requests of one search on this
348/// thread, until the guard drops. Without it, each request of the search
349/// that carries the token reads both tables. A guard inside another keeps
350/// the outer one's answers.
351pub(crate) struct OwnerOnce {
352    outer: bool,
353}
354
355impl OwnerOnce {
356    #[must_use]
357    pub(crate) fn enter() -> Self {
358        let outer = SEARCH_OWNERS.with(|kept| {
359            let mut kept = kept.borrow_mut();
360            let outer = kept.is_none();
361            if outer {
362                *kept = Some(Vec::new());
363            }
364            outer
365        });
366        Self { outer }
367    }
368}
369
370impl Drop for OwnerOnce {
371    fn drop(&mut self) {
372        if self.outer {
373            SEARCH_OWNERS.with(|kept| *kept.borrow_mut() = None);
374        }
375    }
376}
377
378#[cfg(test)]
379thread_local! {
380    /// How many times this thread read the socket tables.
381    static TABLE_READS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
382}
383
384fn foreign_read(url: &str) -> bool {
385    #[cfg(test)]
386    TABLE_READS.with(|reads| reads.set(reads.get() + 1));
387    #[cfg(target_os = "linux")]
388    {
389        use std::os::unix::fs::MetadataExt;
390        let Some(port) = loopback_port(url) else {
391            return false;
392        };
393        let Ok(me) = std::fs::metadata("/proc/self").map(|m| m.uid()) else {
394            return false;
395        };
396        let owners: Vec<u32> = ["/proc/net/tcp", "/proc/net/tcp6"]
397            .iter()
398            .filter_map(|table| std::fs::read_to_string(table).ok())
399            .flat_map(|text| listen_uids(&text, port))
400            .collect();
401        !owners.is_empty() && owners.iter().all(|uid| *uid != me)
402    }
403    #[cfg(not(target_os = "linux"))]
404    {
405        let _ = url;
406        false
407    }
408}
409
410#[cfg(target_os = "linux")]
411fn loopback_port(url: &str) -> Option<u16> {
412    let rest = url
413        .strip_prefix("http://")
414        .or_else(|| url.strip_prefix("https://"))?;
415    let authority = rest.split(['/', '?', '#']).next()?;
416    let (host, port) = match authority.rsplit_once(':') {
417        Some((host, port)) if !port.contains(']') => (host, port.parse().ok()?),
418        _ => (authority, 80),
419    };
420    matches!(host, "127.0.0.1" | "localhost" | "[::1]").then_some(port)
421}
422
423#[cfg(target_os = "linux")]
424fn listen_uids(table: &str, port: u16) -> Vec<u32> {
425    table
426        .lines()
427        .skip(1)
428        .filter_map(|line| {
429            let fields: Vec<&str> = line.split_whitespace().collect();
430            if fields.get(3) != Some(&"0A") {
431                return None;
432            }
433            let hex = fields.get(1)?.rsplit(':').next()?;
434            if u16::from_str_radix(hex, 16).ok()? != port {
435                return None;
436            }
437            fields.get(7)?.parse().ok()
438        })
439        .collect()
440}
441
442fn target(path: &str, query: &[(&str, &str)]) -> String {
443    if query.is_empty() {
444        return path.to_string();
445    }
446    let mut ser = url::form_urlencoded::Serializer::new(String::new());
447    for (key, value) in query {
448        ser.append_pair(key, value);
449    }
450    format!("{path}?{}", ser.finish())
451}
452
453struct Raw {
454    status: u16,
455    body: String,
456    keep: bool,
457    retry_after: Option<Duration>,
458}
459
460/// Each later wait is this many times the last, and never past five seconds.
461const BUSY_GROWTH: u32 = 3;
462const BUSY_WAIT_CAP: Duration = Duration::from_secs(5);
463
464thread_local! {
465    /// The command's admission deadline, as a wall time. A nested guard
466    /// keeps the sooner of the two, so a search inside a longer command
467    /// stays short. The socket clock is a separate bound.
468    static COMMAND_DEADLINE: std::cell::RefCell<Option<SystemTime>> = const { std::cell::RefCell::new(None) };
469    /// The `X-Packset-Deadline` for the request in flight, frozen when the
470    /// call starts so a retry does not push the deadline out.
471    static HELD_DEADLINE: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
472}
473
474/// How long this command is willing to wait for the pack. Search and
475/// recall pass a few seconds. Remember passes twenty. Import passes a minute.
476pub struct CommandDeadline {
477    prev: Option<SystemTime>,
478}
479
480impl CommandDeadline {
481    #[must_use]
482    pub fn enter(budget: Duration) -> Self {
483        let prev = COMMAND_DEADLINE.with(|cell| *cell.borrow());
484        let proposed = SystemTime::now() + budget;
485        let next = match prev {
486            Some(existing) if existing <= proposed => existing,
487            _ => proposed,
488        };
489        COMMAND_DEADLINE.with(|cell| *cell.borrow_mut() = Some(next));
490        Self { prev }
491    }
492}
493
494impl Drop for CommandDeadline {
495    fn drop(&mut self) {
496        let prev = self.prev.take();
497        COMMAND_DEADLINE.with(|cell| *cell.borrow_mut() = prev);
498    }
499}
500
501struct HeldDeadline {
502    prev: Option<String>,
503    active: bool,
504}
505
506impl HeldDeadline {
507    fn enter(budget: Duration) -> Self {
508        let prev = HELD_DEADLINE.with(|cell| cell.borrow().clone());
509        if prev.is_some() {
510            return Self {
511                prev,
512                active: false,
513            };
514        }
515        let stamp = deadline_stamp(budget);
516        HELD_DEADLINE.with(|cell| *cell.borrow_mut() = Some(stamp));
517        Self { prev, active: true }
518    }
519}
520
521impl Drop for HeldDeadline {
522    fn drop(&mut self) {
523        if self.active {
524            let prev = self.prev.clone();
525            HELD_DEADLINE.with(|cell| *cell.borrow_mut() = prev);
526        }
527    }
528}
529
530fn is_write(method: &str) -> bool {
531    !matches!(method, "GET" | "HEAD")
532}
533
534fn command_remaining() -> Option<Duration> {
535    let when = COMMAND_DEADLINE.with(|cell| *cell.borrow())?;
536    Some(
537        when.duration_since(SystemTime::now())
538            .unwrap_or(Duration::ZERO),
539    )
540}
541
542/// How long the retries of one call may take. A read stops inside the
543/// short search budget. A write may use the command's remaining time,
544/// which is longer for remember and import.
545pub(crate) fn admission_retry_budget(method: &str, per_try: Duration) -> Duration {
546    let fallback = if is_write(method) {
547        super::REMEMBER_BUDGET
548    } else {
549        super::SEARCH_PACK_BUDGET
550    };
551    let room = match (command_remaining(), clock_left()) {
552        (Some(cmd), Some(clock)) => cmd.min(clock),
553        (Some(cmd), None) => cmd,
554        (None, Some(clock)) => clock,
555        (None, None) => fallback,
556    };
557    let room = if is_write(method) {
558        room
559    } else {
560        room.min(super::SEARCH_PACK_BUDGET)
561    };
562    per_try.min(room).max(Duration::from_millis(1))
563}
564
565fn deadline_stamp(budget: Duration) -> String {
566    let proposed = SystemTime::now() + budget;
567    let when = match COMMAND_DEADLINE.with(|cell| *cell.borrow()) {
568        Some(command) if command < proposed => command,
569        _ => proposed,
570    };
571    unix_ms(when).to_string()
572}
573
574fn unix_ms(when: SystemTime) -> u64 {
575    when.duration_since(std::time::UNIX_EPOCH)
576        .map(|elapsed| u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX))
577        .unwrap_or(0)
578}
579
580fn current_deadline(method: &str) -> String {
581    HELD_DEADLINE
582        .with(|cell| cell.borrow().clone())
583        .unwrap_or_else(|| deadline_stamp(admission_retry_budget(method, budget())))
584}
585
586struct Pause {
587    sleep: Duration,
588    charge: Duration,
589}
590
591fn planned_wait(backoff: Duration, retry_after: Option<Duration>) -> Duration {
592    match retry_after {
593        Some(after) if after > backoff => after,
594        _ => backoff,
595    }
596}
597
598/// Half of `wait`, plus up to the other half. `salt` picks the fraction,
599/// so a herd waiting on the same `Retry-After` does not wake together.
600fn jittered(wait: Duration, salt: u64) -> Duration {
601    let ms = u64::try_from(wait.as_millis()).unwrap_or(u64::MAX).max(1);
602    let half = ms / 2;
603    let extra = if half == 0 { 0 } else { salt % (half + 1) };
604    Duration::from_millis(half + extra)
605}
606
607/// The next wait, when charging it still ends inside `budget`. The sleep
608/// is the jittered wait. The charge is the full wait, so every host stops
609/// on the same attempt.
610fn pause_for(
611    backoff: Duration,
612    retry_after: Option<Duration>,
613    waited: Duration,
614    budget: Duration,
615    salt: u64,
616) -> Option<Pause> {
617    let charge = planned_wait(backoff, retry_after);
618    if waited.saturating_add(charge) >= budget {
619        return None;
620    }
621    let room = budget.saturating_sub(waited).max(Duration::from_millis(1));
622    Some(Pause {
623        sleep: jittered(charge, salt)
624            .min(room)
625            .max(Duration::from_millis(1)),
626        charge,
627    })
628}
629
630fn retry_after_of(header: Option<&str>) -> Option<Duration> {
631    let raw = header?.trim();
632    if raw.is_empty() {
633        return None;
634    }
635    raw.parse::<u64>().ok().map(Duration::from_secs)
636}
637
638fn jitter_salt(asked: u32) -> u64 {
639    let nanos = SystemTime::now()
640        .duration_since(std::time::UNIX_EPOCH)
641        .map(|elapsed| u64::from(elapsed.subsec_nanos()))
642        .unwrap_or(0);
643    nanos ^ u64::from(asked)
644}
645
646fn shed_error(method: &str) -> Error {
647    let line = if is_write(method) {
648        super::PACK_SHED_LINE
649    } else {
650        super::PACK_READ_BUSY_LINE
651    };
652    Error::Unavailable(line.to_string())
653}
654
655fn deadline_error() -> Error {
656    Error::Unavailable(super::PACK_DEADLINE_LINE.to_string())
657}
658
659#[cfg(test)]
660pub(crate) fn busy_attempts(budget: Duration) -> u32 {
661    let mut waited = Duration::ZERO;
662    let mut wait = super::BUSY_WAIT;
663    let mut attempts = 1u32;
664    while pause_for(wait, None, waited, budget, 0).is_some() {
665        waited = waited.saturating_add(wait);
666        wait = (wait * BUSY_GROWTH).min(BUSY_WAIT_CAP);
667        attempts = attempts.saturating_add(1);
668    }
669    attempts
670}
671
672/// The pack accepted a connection and then stayed silent, or the connect
673/// itself never finished. While a [`QuietPack`] guard is alive, the first
674/// such read still waits out its budget and a later read returns before it
675/// opens another socket. A consensus reading asks several times. Without
676/// the stop, each ask waited the whole budget and the command sat for the
677/// sum. Dropping the guard clears it, so a later call can try the pack again.
678static PACK_QUIET: AtomicBool = AtomicBool::new(false);
679static QUIET_HELD: AtomicUsize = AtomicUsize::new(0);
680
681/// Remember one quiet pack for the reads this guard covers.
682pub struct QuietPack;
683
684impl QuietPack {
685    /// Start remembering a silent pack. The first guard of a pile drops a
686    /// quiet read already seen, so the pile starts with a fresh try.
687    #[must_use]
688    pub fn enter() -> Self {
689        if QUIET_HELD.fetch_add(1, Ordering::AcqRel) == 0 {
690            PACK_QUIET.store(false, Ordering::Relaxed);
691        }
692        Self
693    }
694}
695
696impl Drop for QuietPack {
697    fn drop(&mut self) {
698        if QUIET_HELD.fetch_sub(1, Ordering::AcqRel) == 1 {
699            PACK_QUIET.store(false, Ordering::Relaxed);
700        }
701    }
702}
703
704fn holding_quiet() -> bool {
705    QUIET_HELD.load(Ordering::Relaxed) > 0
706}
707
708fn pack_quiet() -> bool {
709    holding_quiet() && PACK_QUIET.load(Ordering::Relaxed)
710}
711
712fn mark_pack_quiet() {
713    if holding_quiet() {
714        PACK_QUIET.store(true, Ordering::Relaxed);
715    }
716}
717
718/// The tool call's own bound is spent. A shorter read, one atom inside
719/// its own budget, times out while the call still has time and the caller
720/// carries on. That read is not the call failing.
721fn tool_bound_spent() -> bool {
722    crate::tool_bound::remaining().is_some_and(|left| left <= Duration::from_millis(50))
723}
724
725fn pack_io(err: std::io::Error) -> std::io::Error {
726    let quiet = io_is_quiet(&err) || err.to_string().contains("the pack did not answer");
727    if quiet && tool_bound_spent() {
728        crate::tool_bound::note("packsetd");
729        crate::tool_bound::mark_failed("packsetd timed out");
730        return std::io::Error::new(std::io::ErrorKind::TimedOut, "packsetd timed out");
731    }
732    err
733}
734
735fn quiet_io() -> std::io::Error {
736    pack_io(std::io::Error::new(
737        std::io::ErrorKind::TimedOut,
738        "the pack did not answer",
739    ))
740}
741
742/// A read or a connect that ended because the socket stayed silent.
743/// A refused connect is not this: the writer may still be starting.
744fn io_is_quiet(err: &std::io::Error) -> bool {
745    matches!(
746        err.kind(),
747        std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock
748    )
749}
750
751fn connect(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
752    if pack_quiet() {
753        return Err(quiet_io());
754    }
755    if side_budget_spent() {
756        return Err(budget_spent());
757    }
758    let started = Instant::now();
759    let result = connect_once(addr, timeout);
760    // A connect that returns a socket is the pack accepting. Time spent
761    // there is not a quiet wait. A connect that fails spends the budget.
762    // A connect that never finishes is remembered, so the next read does
763    // not wait the budget out again.
764    if let Err(err) = &result {
765        if io_is_quiet(err) {
766            mark_pack_quiet();
767        }
768        super::charge_side_wait(started.elapsed());
769    }
770    result
771}
772
773/// A side event's pack budget is gone. Opening a socket for a one
774/// millisecond timer still blocks, and a prompt makes several of those
775/// reads, so the call returns before the connect.
776fn side_budget_spent() -> bool {
777    super::side_remaining_ms() == Some(0)
778}
779
780fn budget_spent() -> std::io::Error {
781    std::io::Error::new(
782        std::io::ErrorKind::TimedOut,
783        "the side event's pack budget is spent",
784    )
785}
786
787fn connect_once(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
788    let wait = bounded_wait(timeout, true)?;
789    let stream = TcpStream::connect_timeout(&addr, wait)?;
790    let io_wait = bounded_wait(timeout, false)?;
791    stream.set_read_timeout(Some(io_wait))?;
792    stream.set_write_timeout(Some(io_wait))?;
793    let _ = stream.set_nodelay(true);
794    Ok(stream)
795}
796
797/// The wait a socket call may use. With a [`PackClock`], a connect is
798/// capped by [`super::pack_connect_budget`] and every call stops at the
799/// clock. Past the clock the call returns before it opens a socket.
800fn bounded_wait(requested: Duration, connect: bool) -> std::io::Result<Duration> {
801    let mut wait = capped_wait(requested);
802    if let Some(deadline) = clock_deadline() {
803        let left = deadline.saturating_duration_since(Instant::now());
804        if left.is_zero() {
805            return Err(pack_io(std::io::Error::new(
806                std::io::ErrorKind::TimedOut,
807                "the pack did not answer",
808            )));
809        }
810        wait = wait.min(left);
811        if connect {
812            wait = wait.min(super::pack_connect_budget());
813        }
814        if wait.is_zero() {
815            return Err(pack_io(std::io::Error::new(
816                std::io::ErrorKind::TimedOut,
817                "the pack did not answer",
818            )));
819        }
820    }
821    Ok(wait)
822}
823
824/// A side-event bound shrinks this wait. A zero bound is one millisecond,
825/// so a later read fails at once instead of taking the default.
826fn capped_wait(timeout: Duration) -> Duration {
827    let wait = super::cap_pack_wait(timeout);
828    if wait.is_zero() {
829        Duration::from_millis(1)
830    } else {
831        wait
832    }
833}
834
835fn write_all(stream: &mut TcpStream, bytes: &[u8]) -> std::io::Result<()> {
836    let mut sent = 0;
837    while sent < bytes.len() {
838        match stream.write(&bytes[sent..]) {
839            Ok(0) => {
840                return Err(std::io::Error::new(
841                    std::io::ErrorKind::WriteZero,
842                    "the pack closed the socket",
843                ))
844            }
845            Ok(n) => sent += n,
846            Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
847            Err(err) => return Err(pack_io(err)),
848        }
849    }
850    stream.flush()
851}
852
853fn read_response(stream: &mut TcpStream, deadline: Instant) -> std::io::Result<Raw> {
854    let mut buf = Vec::new();
855    let mut tmp = [0u8; 4096];
856    let head_end = loop {
857        if buf.len() > 64 * 1024 {
858            return Err(std::io::Error::new(
859                std::io::ErrorKind::InvalidData,
860                "the pack's headers are too long",
861            ));
862        }
863        arm_read(stream, deadline)?;
864        let n = read_some(stream, &mut tmp)?;
865        buf.extend_from_slice(&tmp[..n]);
866        if let Some(at) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
867            break at;
868        }
869    };
870    let header = String::from_utf8_lossy(&buf[..head_end]).into_owned();
871    let status = header
872        .lines()
873        .next()
874        .and_then(|line| line.split_whitespace().nth(1))
875        .and_then(|code| code.parse().ok())
876        .ok_or_else(|| {
877            std::io::Error::new(
878                std::io::ErrorKind::InvalidData,
879                "the pack's status line is not HTTP",
880            )
881        })?;
882    let mut length = None;
883    let mut close = header.starts_with("HTTP/1.0");
884    let mut retry_after = None;
885    for line in header.lines().skip(1) {
886        let Some((name, value)) = line.split_once(':') else {
887            continue;
888        };
889        if name.eq_ignore_ascii_case("content-length") {
890            length = value.trim().parse().ok();
891        } else if name.eq_ignore_ascii_case("retry-after") {
892            retry_after = retry_after_of(Some(value));
893        } else if name.eq_ignore_ascii_case("connection")
894            && value.trim().eq_ignore_ascii_case("close")
895        {
896            close = true;
897        }
898    }
899    let mut body = buf.split_off(head_end + 4);
900    if let Some(length) = length {
901        while body.len() < length {
902            arm_read(stream, deadline)?;
903            let n = read_some(stream, &mut tmp)?;
904            body.extend_from_slice(&tmp[..n]);
905            if body.len() > length.saturating_add(64 * 1024) {
906                break;
907            }
908        }
909        body.truncate(length);
910    } else {
911        loop {
912            if deadline.saturating_duration_since(Instant::now()).is_zero() {
913                break;
914            }
915            let _ = arm_read(stream, deadline);
916            match stream.read(&mut tmp) {
917                Ok(0) => break,
918                Ok(n) => body.extend_from_slice(&tmp[..n]),
919                Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
920                Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => break,
921                Err(err) if err.kind() == std::io::ErrorKind::TimedOut => break,
922                Err(_) => break,
923            }
924        }
925        close = true;
926    }
927    // A redirect is the answer. The token must not follow it.
928    Ok(Raw {
929        status,
930        body: String::from_utf8_lossy(&body).into_owned(),
931        keep: !close && !(300..400).contains(&status),
932        retry_after,
933    })
934}
935
936fn arm_read(stream: &TcpStream, deadline: Instant) -> std::io::Result<()> {
937    let remain = deadline.saturating_duration_since(Instant::now());
938    if remain.is_zero() {
939        return Err(pack_io(std::io::Error::new(
940            std::io::ErrorKind::TimedOut,
941            "the pack did not answer",
942        )));
943    }
944    stream.set_read_timeout(Some(remain))?;
945    Ok(())
946}
947
948fn read_some(stream: &mut TcpStream, tmp: &mut [u8]) -> std::io::Result<usize> {
949    loop {
950        match stream.read(tmp) {
951            Ok(0) => {
952                return Err(std::io::Error::new(
953                    std::io::ErrorKind::UnexpectedEof,
954                    "the pack closed the socket",
955                ))
956            }
957            Ok(n) => return Ok(n),
958            Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
959            Err(err) => return Err(pack_io(err)),
960        }
961    }
962}
963
964fn render(
965    method: &str,
966    host: &str,
967    path: &str,
968    token: Option<&str>,
969    body: Option<&[u8]>,
970) -> Vec<u8> {
971    let deadline = current_deadline(method);
972    let mut head = format!(
973        "{method} {path} HTTP/1.1\r\nHost: {host}\r\nConnection: keep-alive\r\nX-Packset-Deadline: {deadline}\r\n"
974    );
975    if let Some(token) = token {
976        head.push_str("Authorization: Bearer ");
977        head.push_str(token);
978        head.push_str("\r\n");
979    }
980    let mut out = Vec::new();
981    if let Some(body) = body {
982        head.push_str("Content-Type: application/json\r\n");
983        head.push_str(&format!("Content-Length: {}\r\n\r\n", body.len()));
984        out.extend(head.into_bytes());
985        out.extend_from_slice(body);
986    } else {
987        head.push_str("\r\n");
988        out.extend(head.into_bytes());
989    }
990    out
991}
992
993struct Wire<'a> {
994    method: &'a str,
995    path: &'a str,
996    query: &'a [(&'a str, &'a str)],
997    body: Option<&'a [u8]>,
998    token: Option<&'a str>,
999}
1000
1001enum Gate {
1002    /// The proof held. The call may carry the token.
1003    Send,
1004    /// The listener did not ask for the token.
1005    Bare,
1006    /// The writer is busy. The call is not sent.
1007    Busy(Raw),
1008}
1009
1010impl PacksetClient {
1011    pub fn new(base: impl Into<String>) -> Self {
1012        let mut base = base.into();
1013        while base.ends_with('/') {
1014            base.pop();
1015        }
1016        let origin = Url::parse(&base).unwrap_or_else(|_| Url::parse("http://127.0.0.1/").unwrap());
1017        let addr = Url::parse(&base).ok().as_ref().and_then(pin);
1018        Self {
1019            base,
1020            origin,
1021            addr,
1022            workspace: None,
1023            token: Mutex::new(load_token()),
1024        }
1025    }
1026
1027    /// The writer the seat talks to. `PACKSET_URL=off` is no pack.
1028    pub fn from_env() -> Result<Self, Error> {
1029        packset_client::load_seat_env();
1030        let url = std::env::var("PACKSET_URL")
1031            .or_else(|_| std::env::var("INSIDE_MEMORY_URL"))
1032            .ok()
1033            .filter(|url| !url.is_empty());
1034        match url {
1035            Some(url) if url == "off" => Err(Error::NoUrl),
1036            Some(url) => Ok(Self::new(url)),
1037            None => Ok(Self::new(format!(
1038                "http://127.0.0.1:{}",
1039                packset_client::default_port()
1040            ))),
1041        }
1042    }
1043
1044    #[must_use]
1045    pub fn with_workspace(mut self, workspace: impl Into<String>) -> Self {
1046        let workspace = workspace.into();
1047        self.workspace = (!workspace.is_empty()).then_some(workspace);
1048        self
1049    }
1050
1051    pub fn base(&self) -> &str {
1052        &self.base
1053    }
1054
1055    /// Whether this seat has a token to show. The token itself stays here.
1056    #[must_use]
1057    pub(crate) fn has_token(&self) -> bool {
1058        load_token().is_some()
1059    }
1060
1061    /// Whether a call may carry the token: the seat has one, and another
1062    /// user does not hold the loopback port.
1063    #[must_use]
1064    pub(crate) fn may_carry(&self) -> bool {
1065        load_token().is_some() && !foreign(&self.base)
1066    }
1067
1068    /// Whether the last answer on this socket carried the token. A closed
1069    /// socket does not keep this for the next call: that call proves again.
1070    #[must_use]
1071    pub(crate) fn carried(&self) -> bool {
1072        let Some(addr) = self.addr else {
1073            return false;
1074        };
1075        let map = slots().lock().unwrap_or_else(|p| p.into_inner());
1076        map.get(&addr)
1077            .is_some_and(|slot| slot.lock().unwrap_or_else(|p| p.into_inner()).carried)
1078    }
1079
1080    pub fn workspace(&self) -> String {
1081        if let Some(workspace) = &self.workspace {
1082            return workspace.clone();
1083        }
1084        if let Ok(workspace) = std::env::var("PACKSET_WORKSPACE") {
1085            if !workspace.is_empty() {
1086                return workspace;
1087            }
1088        }
1089        let cwd = std::env::var("HOSTPACK_WORKSPACE")
1090            .ok()
1091            .map(|s| s.trim().to_string())
1092            .filter(|s| !s.is_empty())
1093            .map(std::path::PathBuf::from)
1094            .or_else(|| std::env::current_dir().ok())
1095            .unwrap_or_else(|| std::path::PathBuf::from("."));
1096        let abs = cwd.canonicalize().unwrap_or(cwd);
1097        let shown = abs.to_string_lossy().into_owned();
1098        if let Ok(answer) = self.roundtrip(
1099            "GET",
1100            "/v1/identity",
1101            &[("cwd", shown.as_str())],
1102            None,
1103            true,
1104            budget(),
1105        ) {
1106            if answer.status == 200 {
1107                if let Ok(val) = serde_json::from_str::<Value>(&answer.body) {
1108                    if let Some(ws) = val.get("workspace").and_then(|v| v.as_str()) {
1109                        if !ws.is_empty() {
1110                            return ws.to_string();
1111                        }
1112                    }
1113                }
1114            }
1115        }
1116        format!("dir:{}", abs.display())
1117    }
1118
1119    /// One call. `bearer` asks to carry the token; it is left off when
1120    /// the proof does not hold or another user owns the port. A 3xx is
1121    /// returned as the answer and not followed. A 503 is asked again
1122    /// after `Retry-After`, with jitter, while that wait still fits in
1123    /// the call's budget. A 408 is the deadline passing: it is not asked
1124    /// again.
1125    pub(crate) fn roundtrip(
1126        &self,
1127        method: &str,
1128        path: &str,
1129        query: &[(&str, &str)],
1130        body: Option<&[u8]>,
1131        bearer: bool,
1132        timeout: Duration,
1133    ) -> Result<Answer, Error> {
1134        // A write changes what a listing would say: the memo this process
1135        // keeps and the gate's freshness mark both go.
1136        if method != "GET" {
1137            crate::forget_atom_listings();
1138            crate::forget_gate_fresh();
1139            forget_kept_answers();
1140        }
1141        let memo_key = answer_memo_key(method, path, query, bearer);
1142        if let Some(answer) = memo_key.as_deref().and_then(kept_answer) {
1143            return Ok(answer);
1144        }
1145        let Some(addr) = self.addr else {
1146            return Err(Error::Bad(format!(
1147                "{} is not a socket this seat can pin",
1148                self.base
1149            )));
1150        };
1151        let token = load_token();
1152        *self.token.lock().unwrap_or_else(|p| p.into_inner()) = token.clone();
1153        let allow = bearer && token.is_some() && !foreign(&self.base);
1154        if crate::tool_bound::armed() {
1155            crate::tool_bound::note("packsetd");
1156        }
1157        let budget = admission_retry_budget(method, timeout);
1158        let _held = HeldDeadline::enter(budget);
1159        let mut waited = Duration::ZERO;
1160        let mut wait = super::BUSY_WAIT;
1161        let mut asked = 0u32;
1162        loop {
1163            let answer = self.exchange(
1164                addr,
1165                &Wire {
1166                    method,
1167                    path,
1168                    query,
1169                    body,
1170                    token: token.as_deref(),
1171                },
1172                allow,
1173                timeout,
1174            )?;
1175            if answer.status == 408 {
1176                return Err(deadline_error());
1177            }
1178            if answer.status != 503 {
1179                if let Some(key) = &memo_key {
1180                    if (200..300).contains(&answer.status) {
1181                        keep_answer(key.clone(), &answer);
1182                    }
1183                }
1184                return Ok(answer);
1185            }
1186            asked = asked.saturating_add(1);
1187            match pause_for(wait, answer.retry_after, waited, budget, jitter_salt(asked)) {
1188                Some(pause) => {
1189                    std::thread::sleep(pause.sleep);
1190                    waited = waited.saturating_add(pause.charge);
1191                    wait = (wait * BUSY_GROWTH).min(BUSY_WAIT_CAP);
1192                }
1193                None => return Err(shed_error(method)),
1194            }
1195        }
1196    }
1197
1198    fn exchange(
1199        &self,
1200        addr: SocketAddr,
1201        call: &Wire<'_>,
1202        allow: bool,
1203        timeout: Duration,
1204    ) -> Result<Answer, Error> {
1205        // The proof and the call share this budget. A nested guard keeps
1206        // the command's earlier deadline, so a hook stays under 2s and an
1207        // interactive command under 5s across every socket of the call.
1208        // A restart does not start the budget over.
1209        let _budget = PackClock::enter(timeout);
1210        let slot = slot_for(addr);
1211        let mut live = slot.lock().unwrap_or_else(|p| p.into_inner());
1212        let tag = token_tag(call.token);
1213        if live.token_tag != tag {
1214            live.stream = None;
1215            live.proven = false;
1216            live.token_tag = tag;
1217        }
1218        let carried = if allow {
1219            match self.gate(&mut live, addr, timeout)? {
1220                Gate::Bare => None,
1221                Gate::Send => call.token,
1222                Gate::Busy(raw) => {
1223                    live.carried = false;
1224                    live.proven = false;
1225                    return Ok(Answer {
1226                        status: raw.status,
1227                        body: raw.body,
1228                        trusted: false,
1229                        retry_after: raw.retry_after,
1230                    });
1231                }
1232            }
1233        } else {
1234            None
1235        };
1236        if live.stream.is_none() {
1237            match connect(addr, timeout) {
1238                Ok(stream) => live.stream = Some(stream),
1239                Err(err) => {
1240                    live.proven = false;
1241                    live.carried = false;
1242                    return Err(Error::Io(err));
1243                }
1244            }
1245            // The probes ran on sockets the writer closed. This socket is
1246            // the one the token rides, the third step of the same proof,
1247            // not a later call.
1248            live.proven = false;
1249        }
1250        let raw = match send(
1251            &mut live,
1252            &self.origin,
1253            &Wire {
1254                token: carried,
1255                ..*call
1256            },
1257            timeout,
1258        ) {
1259            Ok(raw) => raw,
1260            Err(err) => {
1261                live.stream = None;
1262                live.proven = false;
1263                live.carried = false;
1264                return Err(Error::Io(err));
1265            }
1266        };
1267        let trusted = carried.is_some();
1268        live.carried = trusted;
1269        if trusted && live.stream.is_some() {
1270            live.proven = true;
1271        } else if live.stream.is_none() {
1272            live.proven = false;
1273        }
1274        Ok(Answer {
1275            status: raw.status,
1276            body: raw.body,
1277            trusted,
1278            retry_after: raw.retry_after,
1279        })
1280    }
1281
1282    /// Two refusals on this socket, then the call may carry the token.
1283    /// A busy writer (503) is returned as that answer; the call is not sent.
1284    fn gate(&self, live: &mut Slot, addr: SocketAddr, timeout: Duration) -> Result<Gate, Error> {
1285        if live.proven && live.stream.is_some() {
1286            return Ok(Gate::Send);
1287        }
1288        live.proven = false;
1289        let (Some(id), Some(bogus)) = (nonce(), nonce()) else {
1290            return Err(Error::Io(std::io::Error::other(
1291                "no randomness for the pack proof",
1292            )));
1293        };
1294        let workspace = self.proof_workspace();
1295        let path = format!("/v1/atoms/{}", &id[..32]);
1296        let query = [("workspace", workspace.as_str())];
1297        let mut stage = 0u8;
1298        let mut restarts = 0u8;
1299        loop {
1300            if live.stream.is_none() {
1301                live.stream = Some(connect(addr, timeout).map_err(Error::Io)?);
1302                live.proven = false;
1303            }
1304            let shown = if stage == 0 {
1305                None
1306            } else {
1307                Some(bogus.as_str())
1308            };
1309            let raw = match send(
1310                live,
1311                &self.origin,
1312                &Wire {
1313                    method: "GET",
1314                    path: &path,
1315                    query: &query,
1316                    body: None,
1317                    token: shown,
1318                },
1319                timeout,
1320            ) {
1321                Ok(raw) => raw,
1322                Err(err) => {
1323                    live.stream = None;
1324                    live.proven = false;
1325                    // A silent writer is the answer. Another socket would
1326                    // wait the budget out again, and three of those is a
1327                    // minute and a half. A closed socket may be tried
1328                    // again, inside the same deadline.
1329                    if io_is_quiet(&err) {
1330                        return Err(Error::Io(err));
1331                    }
1332                    restarts += 1;
1333                    let spent = clock_deadline().is_some_and(|deadline| Instant::now() >= deadline);
1334                    if spent || restarts > 2 {
1335                        return Err(Error::Io(err));
1336                    }
1337                    stage = 0;
1338                    continue;
1339                }
1340            };
1341            // A shed or a passed deadline is the answer. The call is not sent.
1342            if raw.status == 503 || raw.status == 408 {
1343                return Ok(Gate::Busy(raw));
1344            }
1345            if raw.status != 401 {
1346                return Ok(Gate::Bare);
1347            }
1348            if stage == 0 {
1349                stage = 1;
1350                continue;
1351            }
1352            return Ok(Gate::Send);
1353        }
1354    }
1355
1356    fn proof_workspace(&self) -> String {
1357        if let Some(workspace) = &self.workspace {
1358            return workspace.clone();
1359        }
1360        std::env::var("PACKSET_WORKSPACE")
1361            .ok()
1362            .filter(|w| !w.is_empty())
1363            .unwrap_or_else(|| "seat".to_string())
1364    }
1365
1366    /// [`Self::roundtrip`] with the token when the proof holds.
1367    fn tried(
1368        &self,
1369        method: &str,
1370        path: &str,
1371        query: &[(&str, &str)],
1372        body: Option<&[u8]>,
1373        timeout: Duration,
1374    ) -> Result<Answer, Error> {
1375        self.roundtrip(method, path, query, body, true, timeout)
1376    }
1377
1378    fn json_ok<T: DeserializeOwned>(&self, path: &str, answer: Answer) -> Result<T, Error> {
1379        if !(200..300).contains(&answer.status) {
1380            return Err(refused(
1381                &format!("{}{path}", self.base),
1382                answer.status,
1383                &answer.body,
1384            ));
1385        }
1386        serde_json::from_str(&answer.body).map_err(Error::from)
1387    }
1388
1389    /// The `line` field of `GET /stats`: `ok`, or `busy, retrying` when
1390    /// the writer is shedding. One try, so doctor does not spend the
1391    /// read budget on a writer that has no `/stats`. `None` when the
1392    /// writer does not answer, or the body has no line.
1393    #[must_use]
1394    pub fn admission_line(&self) -> Option<String> {
1395        let timeout =
1396            Duration::from_millis(300).min(clock_left().unwrap_or(Duration::from_secs(1)));
1397        let answer = self
1398            .exchange_direct("GET", "/stats", &[], None, true, timeout)
1399            .ok()?;
1400        if answer.status == 503 {
1401            return Some("busy, retrying".to_string());
1402        }
1403        if !(200..300).contains(&answer.status) {
1404            return None;
1405        }
1406        let value: Value = serde_json::from_str(&answer.body).ok()?;
1407        value
1408            .get("line")
1409            .and_then(Value::as_str)
1410            .filter(|line| !line.is_empty())
1411            .map(str::to_string)
1412    }
1413
1414    /// One exchange, with the deadline header and no retry. `/stats` uses
1415    /// this so a busy writer is one answer, not a wait.
1416    fn exchange_direct(
1417        &self,
1418        method: &str,
1419        path: &str,
1420        query: &[(&str, &str)],
1421        body: Option<&[u8]>,
1422        bearer: bool,
1423        timeout: Duration,
1424    ) -> Result<Answer, Error> {
1425        let Some(addr) = self.addr else {
1426            return Err(Error::Bad(format!(
1427                "{} is not a socket this seat can pin",
1428                self.base
1429            )));
1430        };
1431        let token = load_token();
1432        let allow = bearer && token.is_some() && !foreign(&self.base);
1433        if crate::tool_bound::armed() {
1434            crate::tool_bound::note("packsetd");
1435        }
1436        let budget = admission_retry_budget(method, timeout);
1437        let _held = HeldDeadline::enter(budget);
1438        self.exchange(
1439            addr,
1440            &Wire {
1441                method,
1442                path,
1443                query,
1444                body,
1445                token: token.as_deref(),
1446            },
1447            allow,
1448            timeout,
1449        )
1450    }
1451
1452    pub fn health(&self) -> Result<String, Error> {
1453        let answer = self.tried("GET", "/health", &[], None, budget())?;
1454        if !(200..300).contains(&answer.status) {
1455            return Err(refused(
1456                &format!("{}/health", self.base),
1457                answer.status,
1458                &answer.body,
1459            ));
1460        }
1461        Ok(answer.body)
1462    }
1463
1464    pub fn status(&self, workspace: Option<&str>) -> Result<Value, Error> {
1465        let query: Vec<(&str, &str)> = workspace
1466            .map(|workspace| vec![("workspace", workspace)])
1467            .unwrap_or_default();
1468        let answer = self.tried("GET", "/v1/status", &query, None, budget())?;
1469        self.json_ok("/v1/status", answer)
1470    }
1471
1472    pub fn post_atom(&self, atom: &Value) -> Result<Value, Error> {
1473        let bytes = serde_json::to_vec(atom)?;
1474        let answer = self.tried("POST", "/v1/atoms", &[], Some(&bytes), budget())?;
1475        self.json_ok("/v1/atoms", answer)
1476    }
1477
1478    pub fn delete_atom(
1479        &self,
1480        workspace: &str,
1481        id: &str,
1482        why: Option<&str>,
1483    ) -> Result<Value, Error> {
1484        let mut body = serde_json::json!({"workspace": workspace, "id": id});
1485        if let Some(accession) = why {
1486            body["why"] = Value::String(accession.to_string());
1487        }
1488        let bytes = serde_json::to_vec(&body)?;
1489        let answer = self.tried("POST", "/v1/atoms/delete", &[], Some(&bytes), budget())?;
1490        if answer.status == 404 {
1491            return Err(Error::Bad(format!("no atom {id}")));
1492        }
1493        self.json_ok("/v1/atoms/delete", answer)
1494    }
1495
1496    pub fn atoms_of_kind(&self, workspace: &str, kind: &str) -> Result<Vec<Value>, Error> {
1497        let answer = self.tried(
1498            "GET",
1499            "/v1/atoms",
1500            &[("workspace", workspace), ("kind", kind)],
1501            None,
1502            budget(),
1503        )?;
1504        let body: Value = self.json_ok("/v1/atoms", answer)?;
1505        Ok(body
1506            .get("atoms")
1507            .and_then(Value::as_array)
1508            .cloned()
1509            .unwrap_or_default())
1510    }
1511
1512    pub fn atoms_in_set(&self, workspace: &str, set: &str) -> Result<Vec<Value>, Error> {
1513        let answer = self.tried(
1514            "GET",
1515            "/v1/pack",
1516            &[("workspace", workspace), ("set", set)],
1517            None,
1518            budget(),
1519        )?;
1520        let body: Value = self.json_ok("/v1/pack", answer)?;
1521        Ok(body
1522            .get("atoms")
1523            .and_then(Value::as_array)
1524            .cloned()
1525            .unwrap_or_default())
1526    }
1527
1528    pub fn sweep(&self, workspace: &str) -> Result<Value, Error> {
1529        let bytes = serde_json::to_vec(&serde_json::json!({"workspace": workspace}))?;
1530        let answer = self.tried("POST", "/v1/sweep", &[], Some(&bytes), budget())?;
1531        self.json_ok("/v1/sweep", answer)
1532    }
1533
1534    pub fn grade(&self, workspace: &str, id: &str, recalled: bool) -> Result<Value, Error> {
1535        let bytes = serde_json::to_vec(&serde_json::json!({
1536            "workspace": workspace,
1537            "id": id,
1538            "recalled": recalled,
1539        }))?;
1540        let answer = self.tried("POST", "/v1/grade", &[], Some(&bytes), budget())?;
1541        self.json_ok("/v1/grade", answer)
1542    }
1543
1544    /// Merge `fields` into one current atom, under its id.
1545    pub fn update_atom(&self, workspace: &str, id: &str, fields: &Value) -> Result<Value, Error> {
1546        let bytes = serde_json::to_vec(&serde_json::json!({
1547            "workspace": workspace,
1548            "id": id,
1549            "fields": fields,
1550        }))?;
1551        let answer = self.tried("POST", "/v1/atoms/update", &[], Some(&bytes), budget())?;
1552        self.json_ok("/v1/atoms/update", answer)
1553    }
1554
1555    pub fn activate_as(
1556        &self,
1557        workspace: &str,
1558        q: &str,
1559        limit: u32,
1560        fire: bool,
1561        lens: Option<&str>,
1562    ) -> Result<Value, Error> {
1563        let limit = limit.to_string();
1564        let fire = if fire { "1" } else { "0" };
1565        let mut query = vec![
1566            ("workspace", workspace),
1567            ("q", q),
1568            ("limit", limit.as_str()),
1569            ("fire", fire),
1570        ];
1571        if let Some(name) = lens.filter(|n| !n.trim().is_empty()) {
1572            query.push(("as", name));
1573        }
1574        let answer = self.tried("GET", "/v1/activate", &query, None, budget())?;
1575        self.json_ok("/v1/activate", answer)
1576    }
1577
1578    pub fn hubs(&self, workspace: &str, limit: usize) -> Result<Value, Error> {
1579        let limit = limit.to_string();
1580        let answer = self.tried(
1581            "GET",
1582            "/v1/hubs",
1583            &[("workspace", workspace), ("limit", limit.as_str())],
1584            None,
1585            budget(),
1586        )?;
1587        self.json_ok("/v1/hubs", answer)
1588    }
1589
1590    pub fn consolidate(&self, workspace: &str, apply: bool) -> Result<Value, Error> {
1591        let bytes =
1592            serde_json::to_vec(&serde_json::json!({"workspace": workspace, "apply": apply}))?;
1593        let answer = self.tried("POST", "/v1/consolidate", &[], Some(&bytes), budget())?;
1594        self.json_ok("/v1/consolidate", answer)
1595    }
1596
1597    pub fn search_opts(
1598        &self,
1599        workspace: &str,
1600        q: &str,
1601        limit: u32,
1602        as_of: Option<&str>,
1603        rerank: bool,
1604    ) -> Result<Vec<Hit>, Error> {
1605        Ok(self.search_fetched(workspace, q, limit, as_of, rerank)?.0)
1606    }
1607
1608    pub(crate) fn search_fetched(
1609        &self,
1610        workspace: &str,
1611        q: &str,
1612        limit: u32,
1613        as_of: Option<&str>,
1614        rerank: bool,
1615    ) -> Result<(Vec<Hit>, bool), Error> {
1616        let limit_s = limit.to_string();
1617        let mut query = vec![
1618            ("workspace", workspace),
1619            ("q", q),
1620            ("limit", limit_s.as_str()),
1621        ];
1622        if let Some(at) = as_of {
1623            query.push(("as_of", at));
1624        }
1625        if rerank {
1626            query.push(("rerank", "1"));
1627        }
1628        // A rerank used to wait at least a minute, past any shorter
1629        // budget. A clocked call, and a hook, keep the short budget.
1630        let timeout = if rerank && !clock_armed() && std::env::var_os("LJOS_IN_HOOK").is_none() {
1631            Duration::from_secs(60).max(budget())
1632        } else {
1633            budget()
1634        };
1635        let answer = self.tried("GET", "/v1/search", &query, None, timeout)?;
1636        let trusted = answer.trusted;
1637        let body: Value = self.json_ok("/v1/search", answer)?;
1638        let hits = body.get("hits").cloned().unwrap_or(Value::Array(vec![]));
1639        Ok((serde_json::from_value(hits)?, trusted))
1640    }
1641
1642    pub fn fire(&self, workspace: &str, ids: &[String]) -> Result<Value, Error> {
1643        let bytes = serde_json::to_vec(&serde_json::json!({
1644            "workspace": workspace,
1645            "ids": ids,
1646            "as": "",
1647        }))?;
1648        let answer = self.tried("POST", "/v1/fire", &[], Some(&bytes), budget())?;
1649        self.json_ok("/v1/fire", answer)
1650    }
1651}
1652
1653fn refused(url: &str, code: u16, body: &str) -> Error {
1654    let reason = serde_json::from_str::<Value>(body)
1655        .ok()
1656        .and_then(|v| v.get("error").and_then(|r| r.as_str()).map(str::to_string))
1657        .unwrap_or_else(|| body.to_string());
1658    let reason = reason.trim();
1659    if code == 401 {
1660        return Error::Bad(format!(
1661            "{url}: 401: {reason}; this seat's token does not match the writer's, \
1662             so it is another user's writer or PACKSET_HOME names another pack"
1663        ));
1664    }
1665    if reason.is_empty() {
1666        Error::Bad(format!("{url}: status code {code}"))
1667    } else {
1668        Error::Bad(format!("{url}: {code}: {reason}"))
1669    }
1670}
1671
1672fn send(live: &mut Slot, origin: &Url, call: &Wire<'_>, timeout: Duration) -> std::io::Result<Raw> {
1673    if pack_quiet() {
1674        return Err(quiet_io());
1675    }
1676    if side_budget_spent() {
1677        return Err(budget_spent());
1678    }
1679    let started = Instant::now();
1680    let result = send_once(live, origin, call, timeout);
1681    // An answer is not charged. A read that ends without one is a quiet
1682    // socket and spends the budget, so the next read does not open another.
1683    // Outside a hook the side budget is not armed. The quiet flag is that
1684    // same stop for a command, which asks the pack more than once.
1685    if let Err(err) = &result {
1686        if io_is_quiet(err) {
1687            mark_pack_quiet();
1688        }
1689        super::charge_side_wait(started.elapsed());
1690    }
1691    result
1692}
1693
1694fn send_once(
1695    live: &mut Slot,
1696    origin: &Url,
1697    call: &Wire<'_>,
1698    timeout: Duration,
1699) -> std::io::Result<Raw> {
1700    let stream = live
1701        .stream
1702        .as_mut()
1703        .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotConnected, "no pack socket"))?;
1704    let wait = bounded_wait(timeout, false)?;
1705    let deadline = Instant::now() + wait;
1706    stream.set_read_timeout(Some(wait))?;
1707    stream.set_write_timeout(Some(wait))?;
1708    let bytes = render(
1709        call.method,
1710        &host_header(origin),
1711        &target(call.path, call.query),
1712        call.token,
1713        call.body,
1714    );
1715    if let Err(err) = write_all(stream, &bytes) {
1716        live.stream = None;
1717        live.proven = false;
1718        return Err(err);
1719    }
1720    match read_response(stream, deadline) {
1721        Ok(raw) => {
1722            if !raw.keep {
1723                live.stream = None;
1724                live.proven = false;
1725            }
1726            Ok(raw)
1727        }
1728        Err(err) => {
1729            live.stream = None;
1730            live.proven = false;
1731            Err(err)
1732        }
1733    }
1734}
1735
1736#[cfg(test)]
1737mod tests {
1738    use super::{foreign, OwnerOnce, TABLE_READS};
1739
1740    fn reads() -> usize {
1741        TABLE_READS.with(std::cell::Cell::get)
1742    }
1743
1744    /// The requests of one search share one read of the socket tables for
1745    /// each url. A guard inside it keeps that read, and a call after the
1746    /// search reads them again.
1747    #[test]
1748    fn a_search_reads_the_port_owner_once_for_each_url() {
1749        let _g = crate::tests::env_guard();
1750        // SAFETY: under the lock every environment-reading test takes.
1751        unsafe { std::env::remove_var("LJOS_IN_HOOK") };
1752        let (url, other) = ("http://127.0.0.1:9", "http://127.0.0.1:10");
1753        let start = reads();
1754        foreign(url);
1755        foreign(url);
1756        assert_eq!(reads() - start, 2, "outside a search each call reads");
1757        {
1758            let _search = OwnerOnce::enter();
1759            let first = foreign(url);
1760            {
1761                let _inner = OwnerOnce::enter();
1762                assert_eq!(foreign(url), first);
1763            }
1764            assert_eq!(foreign(url), first);
1765            foreign(other);
1766            foreign(other);
1767            assert_eq!(reads() - start, 4, "one search reads once a url");
1768        }
1769        foreign(url);
1770        assert_eq!(reads() - start, 5, "a call after the search reads again");
1771    }
1772
1773    fn quiet_pack() -> tempfile::TempDir {
1774        let home = tempfile::tempdir().unwrap();
1775        unsafe {
1776            std::env::remove_var("PACKSET_TOKEN");
1777            std::env::remove_var("PACKSET_TOKEN_FILE");
1778            std::env::remove_var("PACKSET_TIMEOUT_MS");
1779            std::env::remove_var("LJOS_IN_HOOK");
1780            std::env::set_var("PACKSET_HOME", home.path());
1781        }
1782        home
1783    }
1784
1785    /// `on` sees the request text and how many requests came before this one.
1786    /// It answers with a status, a body, and `Retry-After` seconds when set.
1787    fn serve(
1788        on: impl Fn(usize, &str) -> (u16, String, Option<u64>) + Send + 'static,
1789    ) -> (
1790        super::PacksetClient,
1791        std::sync::Arc<std::sync::Mutex<Vec<String>>>,
1792    ) {
1793        use std::io::{Read, Write};
1794        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1795        let addr = listener.local_addr().unwrap();
1796        let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1797        let log = std::sync::Arc::clone(&seen);
1798        std::thread::spawn(move || {
1799            for stream in listener.incoming() {
1800                let Ok(mut sock) = stream else { break };
1801                let _ = sock.set_read_timeout(Some(std::time::Duration::from_secs(2)));
1802                let mut buf = Vec::new();
1803                let mut tmp = [0u8; 2048];
1804                loop {
1805                    match sock.read(&mut tmp) {
1806                        Ok(0) | Err(_) => break,
1807                        Ok(n) => {
1808                            buf.extend_from_slice(&tmp[..n]);
1809                            if buf.windows(4).any(|w| w == b"\r\n\r\n") {
1810                                break;
1811                            }
1812                        }
1813                    }
1814                }
1815                let req = String::from_utf8_lossy(&buf).into_owned();
1816                let n = {
1817                    let mut log = log.lock().unwrap();
1818                    log.push(req.clone());
1819                    log.len() - 1
1820                };
1821                let (code, body, retry) = on(n, &req);
1822                let reason = match code {
1823                    200 => "OK",
1824                    408 => "Request Timeout",
1825                    503 => "Service Unavailable",
1826                    _ => "Error",
1827                };
1828                let retry = retry
1829                    .map(|secs| format!("Retry-After: {secs}\r\n"))
1830                    .unwrap_or_default();
1831                let resp = format!(
1832                    "HTTP/1.1 {code} {reason}\r\n{retry}Content-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
1833                    body.len()
1834                );
1835                let _ = sock.write_all(resp.as_bytes());
1836            }
1837        });
1838        (super::PacksetClient::new(format!("http://{addr}")), seen)
1839    }
1840
1841    fn deadline_of(req: &str) -> u64 {
1842        req.lines()
1843            .find_map(|line| {
1844                let (name, value) = line.split_once(':')?;
1845                name.eq_ignore_ascii_case("x-packset-deadline")
1846                    .then(|| value.trim().parse().ok())
1847                    .flatten()
1848            })
1849            .unwrap_or(0)
1850    }
1851
1852    #[test]
1853    fn a_read_waits_out_retry_after_and_then_reads() {
1854        let _g = crate::tests::env_guard();
1855        let _home = quiet_pack();
1856        let (client, seen) = serve(|n, req| {
1857            assert!(deadline_of(req) > 0, "{req}");
1858            if n == 0 {
1859                (
1860                    503,
1861                    r#"{"error":"packsetd is busy: the read queue is full"}"#.into(),
1862                    Some(1),
1863                )
1864            } else {
1865                (200, "ok".into(), None)
1866            }
1867        });
1868        let started = std::time::Instant::now();
1869        let body = client.health().unwrap();
1870        let took = started.elapsed();
1871        assert_eq!(body, "ok");
1872        assert_eq!(seen.lock().unwrap().len(), 2);
1873        assert!(
1874            took >= std::time::Duration::from_millis(400),
1875            "Retry-After was not waited: {took:?}"
1876        );
1877        assert!(took < std::time::Duration::from_secs(3), "{took:?}");
1878        let first = &seen.lock().unwrap()[0];
1879        let now = super::unix_ms(std::time::SystemTime::now());
1880        let stamp = deadline_of(first);
1881        assert!(stamp > now, "deadline {stamp} is not after {now}");
1882        assert!(stamp < now + 5_000, "deadline {stamp} is not a short read");
1883    }
1884
1885    #[test]
1886    fn a_retry_after_past_the_read_budget_fails_in_one_line() {
1887        let _g = crate::tests::env_guard();
1888        let _home = quiet_pack();
1889        let (client, seen) = serve(|_, _| (503, r#"{"error":"busy"}"#.into(), Some(30)));
1890        let started = std::time::Instant::now();
1891        let err = client.health().unwrap_err();
1892        let took = started.elapsed();
1893        assert_eq!(err.to_string(), crate::PACK_READ_BUSY_LINE, "{err}");
1894        assert_eq!(seen.lock().unwrap().len(), 1, "the long wait was taken");
1895        assert!(took < std::time::Duration::from_millis(500), "{took:?}");
1896    }
1897
1898    #[test]
1899    fn a_passed_deadline_is_one_line_and_is_not_retried() {
1900        let _g = crate::tests::env_guard();
1901        let _home = quiet_pack();
1902        let (client, seen) = serve(|_, req| {
1903            assert!(deadline_of(req) > 0, "{req}");
1904            (
1905                408,
1906                r#"{"error":"the deadline passed before this request ran"}"#.into(),
1907                None,
1908            )
1909        });
1910        let err = client.health().unwrap_err();
1911        assert_eq!(err.to_string(), crate::PACK_DEADLINE_LINE, "{err}");
1912        assert_eq!(seen.lock().unwrap().len(), 1);
1913    }
1914
1915    #[test]
1916    fn a_shed_write_names_the_later_retry() {
1917        let _g = crate::tests::env_guard();
1918        let _home = quiet_pack();
1919        let (client, seen) = serve(|_, _| (503, r#"{"error":"busy"}"#.into(), Some(1)));
1920        let started = std::time::Instant::now();
1921        let err = {
1922            let _deadline = super::CommandDeadline::enter(std::time::Duration::from_millis(200));
1923            client
1924                .post_atom(&serde_json::json!({"text": "one write", "workspace": "seat"}))
1925                .unwrap_err()
1926        };
1927        let took = started.elapsed();
1928        assert_eq!(err.to_string(), crate::PACK_SHED_LINE, "{err}");
1929        assert_eq!(seen.lock().unwrap().len(), 1);
1930        assert!(took < std::time::Duration::from_millis(500), "{took:?}");
1931    }
1932
1933    #[test]
1934    fn stats_line_is_the_admission_line() {
1935        let _g = crate::tests::env_guard();
1936        let _home = quiet_pack();
1937        let (client, seen) = serve(|_, req| {
1938            let line = req.lines().next().unwrap_or("");
1939            if line.contains("GET /stats") {
1940                (200, r#"{"ok":true,"line":"busy, retrying"}"#.into(), None)
1941            } else {
1942                (200, r#"{"line":"ok"}"#.into(), None)
1943            }
1944        });
1945        assert_eq!(client.admission_line().as_deref(), Some("busy, retrying"));
1946        assert_eq!(seen.lock().unwrap().len(), 1);
1947        let (client, seen) = serve(|_, _| (503, r#"{"error":"busy"}"#.into(), Some(1)));
1948        assert_eq!(client.admission_line().as_deref(), Some("busy, retrying"));
1949        assert_eq!(
1950            seen.lock().unwrap().len(),
1951            1,
1952            "stats does not wait out Retry-After"
1953        );
1954    }
1955}