1use 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
23pub 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#[derive(Debug, Clone)]
47pub(crate) struct Answer {
48 pub status: u16,
49 pub body: String,
50 pub trusted: bool,
51}
52
53type KeptAnswer = (String, Instant, Answer);
58static KEPT_ANSWERS: Mutex<Vec<KeptAnswer>> = Mutex::new(Vec::new());
59const ANSWER_MEMO: Duration = Duration::from_secs(2);
60
61fn answer_memo_key(
62 method: &str,
63 path: &str,
64 query: &[(&str, &str)],
65 bearer: bool,
66) -> Option<String> {
67 if method != "GET" || !matches!(path, "/v1/search" | "/v1/status") {
68 return None;
69 }
70 std::env::var_os("LJOS_IN_HOOK")?;
71 let mut key = format!("{path}|{bearer}");
72 for (k, v) in query {
73 key.push('|');
74 key.push_str(k);
75 key.push('=');
76 key.push_str(v);
77 }
78 Some(key)
79}
80
81fn kept_answer(key: &str) -> Option<Answer> {
82 let kept = KEPT_ANSWERS.lock().unwrap_or_else(|p| p.into_inner());
83 kept.iter()
84 .find(|(k, at, _)| k == key && at.elapsed() < ANSWER_MEMO)
85 .map(|(_, _, answer)| answer.clone())
86}
87
88fn keep_answer(key: String, answer: &Answer) {
89 let mut kept = KEPT_ANSWERS.lock().unwrap_or_else(|p| p.into_inner());
90 kept.retain(|(k, _, _)| k != &key);
91 kept.push((key, Instant::now(), answer.clone()));
92}
93
94pub(crate) fn forget_kept_answers() {
96 KEPT_ANSWERS
97 .lock()
98 .unwrap_or_else(|p| p.into_inner())
99 .clear();
100}
101
102struct Slot {
103 stream: Option<TcpStream>,
104 proven: bool,
106 token_tag: u64,
107 carried: bool,
109}
110
111fn slots() -> &'static Mutex<HashMap<SocketAddr, Arc<Mutex<Slot>>>> {
112 static SLOTS: OnceLock<Mutex<HashMap<SocketAddr, Arc<Mutex<Slot>>>>> = OnceLock::new();
113 SLOTS.get_or_init(|| Mutex::new(HashMap::new()))
114}
115
116fn slot_for(addr: SocketAddr) -> Arc<Mutex<Slot>> {
117 let mut map = slots().lock().unwrap_or_else(|p| p.into_inner());
118 map.entry(addr)
119 .or_insert_with(|| {
120 Arc::new(Mutex::new(Slot {
121 stream: None,
122 proven: false,
123 token_tag: 0,
124 carried: false,
125 }))
126 })
127 .clone()
128}
129
130fn load_token() -> Option<String> {
131 packset_client::token()
132}
133
134fn token_tag(token: Option<&str>) -> u64 {
135 let Some(token) = token else {
136 return 0;
137 };
138 let mut hash = 0xcbf29ce484222325u64;
139 for byte in token.as_bytes() {
140 hash ^= u64::from(*byte);
141 hash = hash.wrapping_mul(0x100000001b3);
142 }
143 hash
144}
145
146fn nonce() -> Option<String> {
147 use std::io::Read as _;
148 let mut raw = [0u8; 32];
149 std::fs::File::open("/dev/urandom")
150 .and_then(|mut file| file.read_exact(&mut raw))
151 .ok()
152 .map(|()| hex::encode(raw))
153}
154
155fn pin(url: &Url) -> Option<SocketAddr> {
156 let host = url.host_str()?;
157 let port = url.port_or_known_default()?;
158 (host, port).to_socket_addrs().ok()?.next()
159}
160
161fn host_header(url: &Url) -> String {
162 match (url.host_str(), url.port()) {
163 (Some(host), Some(port)) if host.contains(':') => format!("[{host}]:{port}"),
164 (Some(host), Some(port)) => format!("{host}:{port}"),
165 (Some(host), None) => host.to_string(),
166 _ => "localhost".to_string(),
167 }
168}
169
170fn budget() -> Duration {
171 let base = crate::pack_budget_ms()
172 .or_else(|| {
173 std::env::var("PACKSET_TIMEOUT_MS")
174 .ok()
175 .and_then(|v| v.trim().parse::<u64>().ok())
176 .filter(|ms| *ms > 0)
177 })
178 .map_or(Duration::from_secs(30), Duration::from_millis);
179 match crate::wall_left() {
180 Some(left) => base.min(left).max(Duration::from_millis(1)),
181 None => base,
182 }
183}
184
185struct Clock {
186 deadline: Instant,
187 depth: u32,
188}
189
190static CLOCK: Mutex<Option<Clock>> = Mutex::new(None);
191
192pub struct PackClock;
196
197impl PackClock {
198 #[must_use]
199 pub fn enter(total: Duration) -> Self {
200 let mut slot = CLOCK.lock().unwrap_or_else(|p| p.into_inner());
201 match slot.as_mut() {
202 Some(clock) => clock.depth = clock.depth.saturating_add(1),
203 None => {
204 *slot = Some(Clock {
205 deadline: Instant::now() + total,
206 depth: 1,
207 });
208 }
209 }
210 Self
211 }
212}
213
214impl Drop for PackClock {
215 fn drop(&mut self) {
216 let mut slot = CLOCK.lock().unwrap_or_else(|p| p.into_inner());
217 if let Some(clock) = slot.as_mut() {
218 clock.depth = clock.depth.saturating_sub(1);
219 if clock.depth == 0 {
220 *slot = None;
221 }
222 }
223 }
224}
225
226fn clock_deadline() -> Option<Instant> {
227 CLOCK
228 .lock()
229 .unwrap_or_else(|p| p.into_inner())
230 .as_ref()
231 .map(|clock| clock.deadline)
232}
233
234pub(crate) fn clock_armed() -> bool {
235 clock_deadline().is_some()
236}
237
238pub(crate) fn error_is_timeout(err: &Error) -> bool {
239 match err {
240 Error::Io(io) => io_is_quiet(io) || io.to_string().contains("the pack did not answer"),
241 _ => false,
242 }
243}
244
245fn foreign(url: &str) -> bool {
248 static KNOWN: std::sync::LazyLock<Mutex<Vec<(String, bool)>>> =
251 std::sync::LazyLock::new(|| Mutex::new(Vec::new()));
252 if let Some((_, known)) = KNOWN
253 .lock()
254 .unwrap_or_else(|p| p.into_inner())
255 .iter()
256 .find(|(u, _)| u == url)
257 {
258 return *known;
259 }
260 let answer = foreign_read(url);
261 KNOWN
262 .lock()
263 .unwrap_or_else(|p| p.into_inner())
264 .push((url.to_string(), answer));
265 answer
266}
267
268fn foreign_read(url: &str) -> bool {
269 #[cfg(target_os = "linux")]
270 {
271 use std::os::unix::fs::MetadataExt;
272 let Some(port) = loopback_port(url) else {
273 return false;
274 };
275 let Ok(me) = std::fs::metadata("/proc/self").map(|m| m.uid()) else {
276 return false;
277 };
278 let owners: Vec<u32> = ["/proc/net/tcp", "/proc/net/tcp6"]
279 .iter()
280 .filter_map(|table| std::fs::read_to_string(table).ok())
281 .flat_map(|text| listen_uids(&text, port))
282 .collect();
283 !owners.is_empty() && owners.iter().all(|uid| *uid != me)
284 }
285 #[cfg(not(target_os = "linux"))]
286 {
287 let _ = url;
288 false
289 }
290}
291
292#[cfg(target_os = "linux")]
293fn loopback_port(url: &str) -> Option<u16> {
294 let rest = url
295 .strip_prefix("http://")
296 .or_else(|| url.strip_prefix("https://"))?;
297 let authority = rest.split(['/', '?', '#']).next()?;
298 let (host, port) = match authority.rsplit_once(':') {
299 Some((host, port)) if !port.contains(']') => (host, port.parse().ok()?),
300 _ => (authority, 80),
301 };
302 matches!(host, "127.0.0.1" | "localhost" | "[::1]").then_some(port)
303}
304
305#[cfg(target_os = "linux")]
306fn listen_uids(table: &str, port: u16) -> Vec<u32> {
307 table
308 .lines()
309 .skip(1)
310 .filter_map(|line| {
311 let fields: Vec<&str> = line.split_whitespace().collect();
312 if fields.get(3) != Some(&"0A") {
313 return None;
314 }
315 let hex = fields.get(1)?.rsplit(':').next()?;
316 if u16::from_str_radix(hex, 16).ok()? != port {
317 return None;
318 }
319 fields.get(7)?.parse().ok()
320 })
321 .collect()
322}
323
324fn target(path: &str, query: &[(&str, &str)]) -> String {
325 if query.is_empty() {
326 return path.to_string();
327 }
328 let mut ser = url::form_urlencoded::Serializer::new(String::new());
329 for (key, value) in query {
330 ser.append_pair(key, value);
331 }
332 format!("{path}?{}", ser.finish())
333}
334
335struct Raw {
336 status: u16,
337 body: String,
338 keep: bool,
339}
340
341static PACK_QUIET: AtomicBool = AtomicBool::new(false);
348static QUIET_HELD: AtomicUsize = AtomicUsize::new(0);
349
350pub struct QuietPack;
352
353impl QuietPack {
354 #[must_use]
357 pub fn enter() -> Self {
358 if QUIET_HELD.fetch_add(1, Ordering::AcqRel) == 0 {
359 PACK_QUIET.store(false, Ordering::Relaxed);
360 }
361 Self
362 }
363}
364
365impl Drop for QuietPack {
366 fn drop(&mut self) {
367 if QUIET_HELD.fetch_sub(1, Ordering::AcqRel) == 1 {
368 PACK_QUIET.store(false, Ordering::Relaxed);
369 }
370 }
371}
372
373fn holding_quiet() -> bool {
374 QUIET_HELD.load(Ordering::Relaxed) > 0
375}
376
377fn pack_quiet() -> bool {
378 holding_quiet() && PACK_QUIET.load(Ordering::Relaxed)
379}
380
381fn mark_pack_quiet() {
382 if holding_quiet() {
383 PACK_QUIET.store(true, Ordering::Relaxed);
384 }
385}
386
387fn quiet_io() -> std::io::Error {
388 std::io::Error::new(std::io::ErrorKind::TimedOut, "the pack did not answer")
389}
390
391fn io_is_quiet(err: &std::io::Error) -> bool {
394 matches!(
395 err.kind(),
396 std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock
397 )
398}
399
400fn connect(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
401 if pack_quiet() {
402 return Err(quiet_io());
403 }
404 if side_budget_spent() {
405 return Err(budget_spent());
406 }
407 let started = Instant::now();
408 let result = connect_once(addr, timeout);
409 if let Err(err) = &result {
414 if io_is_quiet(err) {
415 mark_pack_quiet();
416 }
417 super::charge_side_wait(started.elapsed());
418 }
419 result
420}
421
422fn side_budget_spent() -> bool {
426 super::side_remaining_ms() == Some(0)
427}
428
429fn budget_spent() -> std::io::Error {
430 std::io::Error::new(
431 std::io::ErrorKind::TimedOut,
432 "the side event's pack budget is spent",
433 )
434}
435
436fn connect_once(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
437 let wait = bounded_wait(timeout, true)?;
438 let stream = TcpStream::connect_timeout(&addr, wait)?;
439 let io_wait = bounded_wait(timeout, false)?;
440 stream.set_read_timeout(Some(io_wait))?;
441 stream.set_write_timeout(Some(io_wait))?;
442 let _ = stream.set_nodelay(true);
443 Ok(stream)
444}
445
446fn bounded_wait(requested: Duration, connect: bool) -> std::io::Result<Duration> {
450 let mut wait = capped_wait(requested);
451 if let Some(deadline) = clock_deadline() {
452 let left = deadline.saturating_duration_since(Instant::now());
453 if left.is_zero() {
454 return Err(std::io::Error::new(
455 std::io::ErrorKind::TimedOut,
456 "the pack did not answer",
457 ));
458 }
459 wait = wait.min(left);
460 if connect {
461 wait = wait.min(super::pack_connect_budget());
462 }
463 if wait.is_zero() {
464 return Err(std::io::Error::new(
465 std::io::ErrorKind::TimedOut,
466 "the pack did not answer",
467 ));
468 }
469 }
470 Ok(wait)
471}
472
473fn capped_wait(timeout: Duration) -> Duration {
476 let wait = super::cap_pack_wait(timeout);
477 if wait.is_zero() {
478 Duration::from_millis(1)
479 } else {
480 wait
481 }
482}
483
484fn write_all(stream: &mut TcpStream, bytes: &[u8]) -> std::io::Result<()> {
485 let mut sent = 0;
486 while sent < bytes.len() {
487 match stream.write(&bytes[sent..]) {
488 Ok(0) => {
489 return Err(std::io::Error::new(
490 std::io::ErrorKind::WriteZero,
491 "the pack closed the socket",
492 ))
493 }
494 Ok(n) => sent += n,
495 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
496 Err(err) => return Err(err),
497 }
498 }
499 stream.flush()
500}
501
502fn read_response(stream: &mut TcpStream, deadline: Instant) -> std::io::Result<Raw> {
503 let mut buf = Vec::new();
504 let mut tmp = [0u8; 4096];
505 let head_end = loop {
506 if buf.len() > 64 * 1024 {
507 return Err(std::io::Error::new(
508 std::io::ErrorKind::InvalidData,
509 "the pack's headers are too long",
510 ));
511 }
512 arm_read(stream, deadline)?;
513 let n = read_some(stream, &mut tmp)?;
514 buf.extend_from_slice(&tmp[..n]);
515 if let Some(at) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
516 break at;
517 }
518 };
519 let header = String::from_utf8_lossy(&buf[..head_end]).into_owned();
520 let status = header
521 .lines()
522 .next()
523 .and_then(|line| line.split_whitespace().nth(1))
524 .and_then(|code| code.parse().ok())
525 .ok_or_else(|| {
526 std::io::Error::new(
527 std::io::ErrorKind::InvalidData,
528 "the pack's status line is not HTTP",
529 )
530 })?;
531 let mut length = None;
532 let mut close = header.starts_with("HTTP/1.0");
533 for line in header.lines().skip(1) {
534 let Some((name, value)) = line.split_once(':') else {
535 continue;
536 };
537 if name.eq_ignore_ascii_case("content-length") {
538 length = value.trim().parse().ok();
539 } else if name.eq_ignore_ascii_case("connection")
540 && value.trim().eq_ignore_ascii_case("close")
541 {
542 close = true;
543 }
544 }
545 let mut body = buf.split_off(head_end + 4);
546 if let Some(length) = length {
547 while body.len() < length {
548 arm_read(stream, deadline)?;
549 let n = read_some(stream, &mut tmp)?;
550 body.extend_from_slice(&tmp[..n]);
551 if body.len() > length.saturating_add(64 * 1024) {
552 break;
553 }
554 }
555 body.truncate(length);
556 } else {
557 loop {
558 if deadline.saturating_duration_since(Instant::now()).is_zero() {
559 break;
560 }
561 let _ = arm_read(stream, deadline);
562 match stream.read(&mut tmp) {
563 Ok(0) => break,
564 Ok(n) => body.extend_from_slice(&tmp[..n]),
565 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
566 Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => break,
567 Err(err) if err.kind() == std::io::ErrorKind::TimedOut => break,
568 Err(_) => break,
569 }
570 }
571 close = true;
572 }
573 Ok(Raw {
575 status,
576 body: String::from_utf8_lossy(&body).into_owned(),
577 keep: !close && !(300..400).contains(&status),
578 })
579}
580
581fn arm_read(stream: &TcpStream, deadline: Instant) -> std::io::Result<()> {
582 let remain = deadline.saturating_duration_since(Instant::now());
583 if remain.is_zero() {
584 return Err(std::io::Error::new(
585 std::io::ErrorKind::TimedOut,
586 "the pack did not answer",
587 ));
588 }
589 stream.set_read_timeout(Some(remain))?;
590 Ok(())
591}
592
593fn read_some(stream: &mut TcpStream, tmp: &mut [u8]) -> std::io::Result<usize> {
594 loop {
595 match stream.read(tmp) {
596 Ok(0) => {
597 return Err(std::io::Error::new(
598 std::io::ErrorKind::UnexpectedEof,
599 "the pack closed the socket",
600 ))
601 }
602 Ok(n) => return Ok(n),
603 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
604 Err(err) => return Err(err),
605 }
606 }
607}
608
609fn render(
610 method: &str,
611 host: &str,
612 path: &str,
613 token: Option<&str>,
614 body: Option<&[u8]>,
615) -> Vec<u8> {
616 let mut head =
617 format!("{method} {path} HTTP/1.1\r\nHost: {host}\r\nConnection: keep-alive\r\n");
618 if let Some(token) = token {
619 head.push_str("Authorization: Bearer ");
620 head.push_str(token);
621 head.push_str("\r\n");
622 }
623 let mut out = Vec::new();
624 if let Some(body) = body {
625 head.push_str("Content-Type: application/json\r\n");
626 head.push_str(&format!("Content-Length: {}\r\n\r\n", body.len()));
627 out.extend(head.into_bytes());
628 out.extend_from_slice(body);
629 } else {
630 head.push_str("\r\n");
631 out.extend(head.into_bytes());
632 }
633 out
634}
635
636struct Wire<'a> {
637 method: &'a str,
638 path: &'a str,
639 query: &'a [(&'a str, &'a str)],
640 body: Option<&'a [u8]>,
641 token: Option<&'a str>,
642}
643
644enum Gate {
645 Send,
647 Bare,
649 Busy(Raw),
651}
652
653impl PacksetClient {
654 pub fn new(base: impl Into<String>) -> Self {
655 let mut base = base.into();
656 while base.ends_with('/') {
657 base.pop();
658 }
659 let origin = Url::parse(&base).unwrap_or_else(|_| Url::parse("http://127.0.0.1/").unwrap());
660 let addr = Url::parse(&base).ok().as_ref().and_then(pin);
661 Self {
662 base,
663 origin,
664 addr,
665 workspace: None,
666 token: Mutex::new(load_token()),
667 }
668 }
669
670 pub fn from_env() -> Result<Self, Error> {
672 packset_client::load_seat_env();
673 let url = std::env::var("PACKSET_URL")
674 .or_else(|_| std::env::var("INSIDE_MEMORY_URL"))
675 .ok()
676 .filter(|url| !url.is_empty());
677 match url {
678 Some(url) if url == "off" => Err(Error::NoUrl),
679 Some(url) => Ok(Self::new(url)),
680 None => Ok(Self::new(format!(
681 "http://127.0.0.1:{}",
682 packset_client::default_port()
683 ))),
684 }
685 }
686
687 #[must_use]
688 pub fn with_workspace(mut self, workspace: impl Into<String>) -> Self {
689 let workspace = workspace.into();
690 self.workspace = (!workspace.is_empty()).then_some(workspace);
691 self
692 }
693
694 pub fn base(&self) -> &str {
695 &self.base
696 }
697
698 #[must_use]
700 pub(crate) fn has_token(&self) -> bool {
701 load_token().is_some()
702 }
703
704 #[must_use]
707 pub(crate) fn may_carry(&self) -> bool {
708 load_token().is_some() && !foreign(&self.base)
709 }
710
711 #[must_use]
714 pub(crate) fn carried(&self) -> bool {
715 let Some(addr) = self.addr else {
716 return false;
717 };
718 let map = slots().lock().unwrap_or_else(|p| p.into_inner());
719 map.get(&addr)
720 .is_some_and(|slot| slot.lock().unwrap_or_else(|p| p.into_inner()).carried)
721 }
722
723 pub fn workspace(&self) -> String {
724 if let Some(workspace) = &self.workspace {
725 return workspace.clone();
726 }
727 if let Ok(workspace) = std::env::var("PACKSET_WORKSPACE") {
728 if !workspace.is_empty() {
729 return workspace;
730 }
731 }
732 let cwd = std::env::var("HOSTPACK_WORKSPACE")
733 .ok()
734 .map(|s| s.trim().to_string())
735 .filter(|s| !s.is_empty())
736 .map(std::path::PathBuf::from)
737 .or_else(|| std::env::current_dir().ok())
738 .unwrap_or_else(|| std::path::PathBuf::from("."));
739 let abs = cwd.canonicalize().unwrap_or(cwd);
740 let shown = abs.to_string_lossy().into_owned();
741 if let Ok(answer) = self.roundtrip(
742 "GET",
743 "/v1/identity",
744 &[("cwd", shown.as_str())],
745 None,
746 true,
747 budget(),
748 ) {
749 if answer.status == 200 {
750 if let Ok(val) = serde_json::from_str::<Value>(&answer.body) {
751 if let Some(ws) = val.get("workspace").and_then(|v| v.as_str()) {
752 if !ws.is_empty() {
753 return ws.to_string();
754 }
755 }
756 }
757 }
758 }
759 format!("dir:{}", abs.display())
760 }
761
762 pub(crate) fn roundtrip(
767 &self,
768 method: &str,
769 path: &str,
770 query: &[(&str, &str)],
771 body: Option<&[u8]>,
772 bearer: bool,
773 timeout: Duration,
774 ) -> Result<Answer, Error> {
775 if method != "GET" {
778 crate::forget_atom_listings();
779 crate::forget_gate_fresh();
780 forget_kept_answers();
781 }
782 let memo_key = answer_memo_key(method, path, query, bearer);
783 if let Some(answer) = memo_key.as_deref().and_then(kept_answer) {
784 return Ok(answer);
785 }
786 let Some(addr) = self.addr else {
787 return Err(Error::Bad(format!(
788 "{} is not a socket this seat can pin",
789 self.base
790 )));
791 };
792 let token = load_token();
793 *self.token.lock().unwrap_or_else(|p| p.into_inner()) = token.clone();
794 let allow = bearer && token.is_some() && !foreign(&self.base);
795 let answer = self.exchange(
796 addr,
797 &Wire {
798 method,
799 path,
800 query,
801 body,
802 token: token.as_deref(),
803 },
804 allow,
805 timeout,
806 )?;
807 if let Some(key) = memo_key {
808 if (200..300).contains(&answer.status) {
809 keep_answer(key, &answer);
810 }
811 }
812 Ok(answer)
813 }
814
815 fn exchange(
816 &self,
817 addr: SocketAddr,
818 call: &Wire<'_>,
819 allow: bool,
820 timeout: Duration,
821 ) -> Result<Answer, Error> {
822 let _budget = PackClock::enter(timeout);
827 let slot = slot_for(addr);
828 let mut live = slot.lock().unwrap_or_else(|p| p.into_inner());
829 let tag = token_tag(call.token);
830 if live.token_tag != tag {
831 live.stream = None;
832 live.proven = false;
833 live.token_tag = tag;
834 }
835 let carried = if allow {
836 match self.gate(&mut live, addr, timeout)? {
837 Gate::Bare => None,
838 Gate::Send => call.token,
839 Gate::Busy(raw) => {
840 live.carried = false;
841 live.proven = false;
842 return Ok(Answer {
843 status: raw.status,
844 body: raw.body,
845 trusted: false,
846 });
847 }
848 }
849 } else {
850 None
851 };
852 if live.stream.is_none() {
853 match connect(addr, timeout) {
854 Ok(stream) => live.stream = Some(stream),
855 Err(err) => {
856 live.proven = false;
857 live.carried = false;
858 return Err(Error::Io(err));
859 }
860 }
861 live.proven = false;
865 }
866 let raw = match send(
867 &mut live,
868 &self.origin,
869 &Wire {
870 token: carried,
871 ..*call
872 },
873 timeout,
874 ) {
875 Ok(raw) => raw,
876 Err(err) => {
877 live.stream = None;
878 live.proven = false;
879 live.carried = false;
880 return Err(Error::Io(err));
881 }
882 };
883 let trusted = carried.is_some();
884 live.carried = trusted;
885 if trusted && live.stream.is_some() {
886 live.proven = true;
887 } else if live.stream.is_none() {
888 live.proven = false;
889 }
890 Ok(Answer {
891 status: raw.status,
892 body: raw.body,
893 trusted,
894 })
895 }
896
897 fn gate(&self, live: &mut Slot, addr: SocketAddr, timeout: Duration) -> Result<Gate, Error> {
900 if live.proven && live.stream.is_some() {
901 return Ok(Gate::Send);
902 }
903 live.proven = false;
904 let (Some(id), Some(bogus)) = (nonce(), nonce()) else {
905 return Err(Error::Io(std::io::Error::other(
906 "no randomness for the pack proof",
907 )));
908 };
909 let workspace = self.proof_workspace();
910 let path = format!("/v1/atoms/{}", &id[..32]);
911 let query = [("workspace", workspace.as_str())];
912 let mut stage = 0u8;
913 let mut restarts = 0u8;
914 loop {
915 if live.stream.is_none() {
916 live.stream = Some(connect(addr, timeout).map_err(Error::Io)?);
917 live.proven = false;
918 }
919 let shown = if stage == 0 {
920 None
921 } else {
922 Some(bogus.as_str())
923 };
924 let raw = match send(
925 live,
926 &self.origin,
927 &Wire {
928 method: "GET",
929 path: &path,
930 query: &query,
931 body: None,
932 token: shown,
933 },
934 timeout,
935 ) {
936 Ok(raw) => raw,
937 Err(err) => {
938 live.stream = None;
939 live.proven = false;
940 if io_is_quiet(&err) {
945 return Err(Error::Io(err));
946 }
947 restarts += 1;
948 let spent = clock_deadline().is_some_and(|deadline| Instant::now() >= deadline);
949 if spent || restarts > 2 {
950 return Err(Error::Io(err));
951 }
952 stage = 0;
953 continue;
954 }
955 };
956 if raw.status == 503 {
957 return Ok(Gate::Busy(raw));
958 }
959 if raw.status != 401 {
960 return Ok(Gate::Bare);
961 }
962 if stage == 0 {
963 stage = 1;
964 continue;
965 }
966 return Ok(Gate::Send);
967 }
968 }
969
970 fn proof_workspace(&self) -> String {
971 if let Some(workspace) = &self.workspace {
972 return workspace.clone();
973 }
974 std::env::var("PACKSET_WORKSPACE")
975 .ok()
976 .filter(|w| !w.is_empty())
977 .unwrap_or_else(|| "seat".to_string())
978 }
979
980 fn tried(
982 &self,
983 method: &str,
984 path: &str,
985 query: &[(&str, &str)],
986 body: Option<&[u8]>,
987 timeout: Duration,
988 ) -> Result<Answer, Error> {
989 if clock_armed() {
992 return self.roundtrip(method, path, query, body, true, timeout);
993 }
994 let started = Instant::now();
995 let mut wait = Duration::from_millis(50);
996 for n in 0..4 {
997 let answer = self.roundtrip(method, path, query, body, true, timeout)?;
998 if answer.status == 503 && n < 3 && started.elapsed() + wait < timeout {
999 std::thread::sleep(wait);
1000 wait *= 3;
1001 continue;
1002 }
1003 return Ok(answer);
1004 }
1005 Err(Error::Bad(format!(
1006 "{}{path}: 503: packsetd is busy",
1007 self.base
1008 )))
1009 }
1010
1011 fn json_ok<T: DeserializeOwned>(&self, path: &str, answer: Answer) -> Result<T, Error> {
1012 if !(200..300).contains(&answer.status) {
1013 return Err(refused(
1014 &format!("{}{path}", self.base),
1015 answer.status,
1016 &answer.body,
1017 ));
1018 }
1019 serde_json::from_str(&answer.body).map_err(Error::from)
1020 }
1021
1022 pub fn health(&self) -> Result<String, Error> {
1023 let answer = self.tried("GET", "/health", &[], None, budget())?;
1024 if !(200..300).contains(&answer.status) {
1025 return Err(refused(
1026 &format!("{}/health", self.base),
1027 answer.status,
1028 &answer.body,
1029 ));
1030 }
1031 Ok(answer.body)
1032 }
1033
1034 pub fn status(&self, workspace: Option<&str>) -> Result<Value, Error> {
1035 let query: Vec<(&str, &str)> = workspace
1036 .map(|workspace| vec![("workspace", workspace)])
1037 .unwrap_or_default();
1038 let answer = self.tried("GET", "/v1/status", &query, None, budget())?;
1039 self.json_ok("/v1/status", answer)
1040 }
1041
1042 pub fn post_atom(&self, atom: &Value) -> Result<Value, Error> {
1043 let bytes = serde_json::to_vec(atom)?;
1044 let answer = self.tried("POST", "/v1/atoms", &[], Some(&bytes), budget())?;
1045 self.json_ok("/v1/atoms", answer)
1046 }
1047
1048 pub fn delete_atom(
1049 &self,
1050 workspace: &str,
1051 id: &str,
1052 why: Option<&str>,
1053 ) -> Result<Value, Error> {
1054 let mut body = serde_json::json!({"workspace": workspace, "id": id});
1055 if let Some(accession) = why {
1056 body["why"] = Value::String(accession.to_string());
1057 }
1058 let bytes = serde_json::to_vec(&body)?;
1059 let answer = self.tried("POST", "/v1/atoms/delete", &[], Some(&bytes), budget())?;
1060 if answer.status == 404 {
1061 return Err(Error::Bad(format!("no atom {id}")));
1062 }
1063 self.json_ok("/v1/atoms/delete", answer)
1064 }
1065
1066 pub fn atoms_of_kind(&self, workspace: &str, kind: &str) -> Result<Vec<Value>, Error> {
1067 let answer = self.tried(
1068 "GET",
1069 "/v1/atoms",
1070 &[("workspace", workspace), ("kind", kind)],
1071 None,
1072 budget(),
1073 )?;
1074 let body: Value = self.json_ok("/v1/atoms", answer)?;
1075 Ok(body
1076 .get("atoms")
1077 .and_then(Value::as_array)
1078 .cloned()
1079 .unwrap_or_default())
1080 }
1081
1082 pub fn atoms_in_set(&self, workspace: &str, set: &str) -> Result<Vec<Value>, Error> {
1083 let answer = self.tried(
1084 "GET",
1085 "/v1/pack",
1086 &[("workspace", workspace), ("set", set)],
1087 None,
1088 budget(),
1089 )?;
1090 let body: Value = self.json_ok("/v1/pack", answer)?;
1091 Ok(body
1092 .get("atoms")
1093 .and_then(Value::as_array)
1094 .cloned()
1095 .unwrap_or_default())
1096 }
1097
1098 pub fn sweep(&self, workspace: &str) -> Result<Value, Error> {
1099 let bytes = serde_json::to_vec(&serde_json::json!({"workspace": workspace}))?;
1100 let answer = self.tried("POST", "/v1/sweep", &[], Some(&bytes), budget())?;
1101 self.json_ok("/v1/sweep", answer)
1102 }
1103
1104 pub fn grade(&self, workspace: &str, id: &str, recalled: bool) -> Result<Value, Error> {
1105 let bytes = serde_json::to_vec(&serde_json::json!({
1106 "workspace": workspace,
1107 "id": id,
1108 "recalled": recalled,
1109 }))?;
1110 let answer = self.tried("POST", "/v1/grade", &[], Some(&bytes), budget())?;
1111 self.json_ok("/v1/grade", answer)
1112 }
1113
1114 pub fn activate_as(
1115 &self,
1116 workspace: &str,
1117 q: &str,
1118 limit: u32,
1119 fire: bool,
1120 lens: Option<&str>,
1121 ) -> Result<Value, Error> {
1122 let limit = limit.to_string();
1123 let fire = if fire { "1" } else { "0" };
1124 let mut query = vec![
1125 ("workspace", workspace),
1126 ("q", q),
1127 ("limit", limit.as_str()),
1128 ("fire", fire),
1129 ];
1130 if let Some(name) = lens.filter(|n| !n.trim().is_empty()) {
1131 query.push(("as", name));
1132 }
1133 let answer = self.tried("GET", "/v1/activate", &query, None, budget())?;
1134 self.json_ok("/v1/activate", answer)
1135 }
1136
1137 pub fn hubs(&self, workspace: &str, limit: usize) -> Result<Value, Error> {
1138 let limit = limit.to_string();
1139 let answer = self.tried(
1140 "GET",
1141 "/v1/hubs",
1142 &[("workspace", workspace), ("limit", limit.as_str())],
1143 None,
1144 budget(),
1145 )?;
1146 self.json_ok("/v1/hubs", answer)
1147 }
1148
1149 pub fn consolidate(&self, workspace: &str, apply: bool) -> Result<Value, Error> {
1150 let bytes =
1151 serde_json::to_vec(&serde_json::json!({"workspace": workspace, "apply": apply}))?;
1152 let answer = self.tried("POST", "/v1/consolidate", &[], Some(&bytes), budget())?;
1153 self.json_ok("/v1/consolidate", answer)
1154 }
1155
1156 pub fn search_opts(
1157 &self,
1158 workspace: &str,
1159 q: &str,
1160 limit: u32,
1161 as_of: Option<&str>,
1162 rerank: bool,
1163 ) -> Result<Vec<Hit>, Error> {
1164 Ok(self.search_fetched(workspace, q, limit, as_of, rerank)?.0)
1165 }
1166
1167 pub(crate) fn search_fetched(
1168 &self,
1169 workspace: &str,
1170 q: &str,
1171 limit: u32,
1172 as_of: Option<&str>,
1173 rerank: bool,
1174 ) -> Result<(Vec<Hit>, bool), Error> {
1175 let limit_s = limit.to_string();
1176 let mut query = vec![
1177 ("workspace", workspace),
1178 ("q", q),
1179 ("limit", limit_s.as_str()),
1180 ];
1181 if let Some(at) = as_of {
1182 query.push(("as_of", at));
1183 }
1184 if rerank {
1185 query.push(("rerank", "1"));
1186 }
1187 let timeout = if rerank && !clock_armed() && std::env::var_os("LJOS_IN_HOOK").is_none() {
1190 Duration::from_secs(60).max(budget())
1191 } else {
1192 budget()
1193 };
1194 let answer = self.tried("GET", "/v1/search", &query, None, timeout)?;
1195 let trusted = answer.trusted;
1196 let body: Value = self.json_ok("/v1/search", answer)?;
1197 let hits = body.get("hits").cloned().unwrap_or(Value::Array(vec![]));
1198 Ok((serde_json::from_value(hits)?, trusted))
1199 }
1200
1201 pub fn fire(&self, workspace: &str, ids: &[String]) -> Result<Value, Error> {
1202 let bytes = serde_json::to_vec(&serde_json::json!({
1203 "workspace": workspace,
1204 "ids": ids,
1205 "as": "",
1206 }))?;
1207 let answer = self.tried("POST", "/v1/fire", &[], Some(&bytes), budget())?;
1208 self.json_ok("/v1/fire", answer)
1209 }
1210}
1211
1212fn refused(url: &str, code: u16, body: &str) -> Error {
1213 let reason = serde_json::from_str::<Value>(body)
1214 .ok()
1215 .and_then(|v| v.get("error").and_then(|r| r.as_str()).map(str::to_string))
1216 .unwrap_or_else(|| body.to_string());
1217 let reason = reason.trim();
1218 if code == 401 {
1219 return Error::Bad(format!(
1220 "{url}: 401: {reason}; this seat's token does not match the writer's, \
1221 so it is another user's writer or PACKSET_HOME names another pack"
1222 ));
1223 }
1224 if reason.is_empty() {
1225 Error::Bad(format!("{url}: status code {code}"))
1226 } else {
1227 Error::Bad(format!("{url}: {code}: {reason}"))
1228 }
1229}
1230
1231fn send(live: &mut Slot, origin: &Url, call: &Wire<'_>, timeout: Duration) -> std::io::Result<Raw> {
1232 if pack_quiet() {
1233 return Err(quiet_io());
1234 }
1235 if side_budget_spent() {
1236 return Err(budget_spent());
1237 }
1238 let started = Instant::now();
1239 let result = send_once(live, origin, call, timeout);
1240 if let Err(err) = &result {
1245 if io_is_quiet(err) {
1246 mark_pack_quiet();
1247 }
1248 super::charge_side_wait(started.elapsed());
1249 }
1250 result
1251}
1252
1253fn send_once(
1254 live: &mut Slot,
1255 origin: &Url,
1256 call: &Wire<'_>,
1257 timeout: Duration,
1258) -> std::io::Result<Raw> {
1259 let stream = live
1260 .stream
1261 .as_mut()
1262 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotConnected, "no pack socket"))?;
1263 let wait = bounded_wait(timeout, false)?;
1264 let deadline = Instant::now() + wait;
1265 stream.set_read_timeout(Some(wait))?;
1266 stream.set_write_timeout(Some(wait))?;
1267 let bytes = render(
1268 call.method,
1269 &host_header(origin),
1270 &target(call.path, call.query),
1271 call.token,
1272 call.body,
1273 );
1274 if let Err(err) = write_all(stream, &bytes) {
1275 live.stream = None;
1276 live.proven = false;
1277 return Err(err);
1278 }
1279 match read_response(stream, deadline) {
1280 Ok(raw) => {
1281 if !raw.keep {
1282 live.stream = None;
1283 live.proven = false;
1284 }
1285 Ok(raw)
1286 }
1287 Err(err) => {
1288 live.stream = None;
1289 live.proven = false;
1290 Err(err)
1291 }
1292 }
1293}