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 clock_left() -> Option<Duration> {
240 clock_deadline().map(|deadline| deadline.saturating_duration_since(Instant::now()))
241}
242
243pub(crate) fn search_stalled() -> bool {
246 pack_quiet() || clock_deadline().is_some_and(|deadline| Instant::now() >= deadline)
247}
248
249pub(crate) fn error_is_timeout(err: &Error) -> bool {
250 match err {
251 Error::Io(io) => io_is_quiet(io) || io.to_string().contains("the pack did not answer"),
252 _ => false,
253 }
254}
255
256fn foreign(url: &str) -> bool {
259 static KNOWN: std::sync::LazyLock<Mutex<Vec<(String, bool)>>> =
262 std::sync::LazyLock::new(|| Mutex::new(Vec::new()));
263 if let Some((_, known)) = KNOWN
264 .lock()
265 .unwrap_or_else(|p| p.into_inner())
266 .iter()
267 .find(|(u, _)| u == url)
268 {
269 return *known;
270 }
271 let answer = foreign_read(url);
272 KNOWN
273 .lock()
274 .unwrap_or_else(|p| p.into_inner())
275 .push((url.to_string(), answer));
276 answer
277}
278
279fn foreign_read(url: &str) -> bool {
280 #[cfg(target_os = "linux")]
281 {
282 use std::os::unix::fs::MetadataExt;
283 let Some(port) = loopback_port(url) else {
284 return false;
285 };
286 let Ok(me) = std::fs::metadata("/proc/self").map(|m| m.uid()) else {
287 return false;
288 };
289 let owners: Vec<u32> = ["/proc/net/tcp", "/proc/net/tcp6"]
290 .iter()
291 .filter_map(|table| std::fs::read_to_string(table).ok())
292 .flat_map(|text| listen_uids(&text, port))
293 .collect();
294 !owners.is_empty() && owners.iter().all(|uid| *uid != me)
295 }
296 #[cfg(not(target_os = "linux"))]
297 {
298 let _ = url;
299 false
300 }
301}
302
303#[cfg(target_os = "linux")]
304fn loopback_port(url: &str) -> Option<u16> {
305 let rest = url
306 .strip_prefix("http://")
307 .or_else(|| url.strip_prefix("https://"))?;
308 let authority = rest.split(['/', '?', '#']).next()?;
309 let (host, port) = match authority.rsplit_once(':') {
310 Some((host, port)) if !port.contains(']') => (host, port.parse().ok()?),
311 _ => (authority, 80),
312 };
313 matches!(host, "127.0.0.1" | "localhost" | "[::1]").then_some(port)
314}
315
316#[cfg(target_os = "linux")]
317fn listen_uids(table: &str, port: u16) -> Vec<u32> {
318 table
319 .lines()
320 .skip(1)
321 .filter_map(|line| {
322 let fields: Vec<&str> = line.split_whitespace().collect();
323 if fields.get(3) != Some(&"0A") {
324 return None;
325 }
326 let hex = fields.get(1)?.rsplit(':').next()?;
327 if u16::from_str_radix(hex, 16).ok()? != port {
328 return None;
329 }
330 fields.get(7)?.parse().ok()
331 })
332 .collect()
333}
334
335fn target(path: &str, query: &[(&str, &str)]) -> String {
336 if query.is_empty() {
337 return path.to_string();
338 }
339 let mut ser = url::form_urlencoded::Serializer::new(String::new());
340 for (key, value) in query {
341 ser.append_pair(key, value);
342 }
343 format!("{path}?{}", ser.finish())
344}
345
346struct Raw {
347 status: u16,
348 body: String,
349 keep: bool,
350}
351
352static PACK_QUIET: AtomicBool = AtomicBool::new(false);
359static QUIET_HELD: AtomicUsize = AtomicUsize::new(0);
360
361pub struct QuietPack;
363
364impl QuietPack {
365 #[must_use]
368 pub fn enter() -> Self {
369 if QUIET_HELD.fetch_add(1, Ordering::AcqRel) == 0 {
370 PACK_QUIET.store(false, Ordering::Relaxed);
371 }
372 Self
373 }
374}
375
376impl Drop for QuietPack {
377 fn drop(&mut self) {
378 if QUIET_HELD.fetch_sub(1, Ordering::AcqRel) == 1 {
379 PACK_QUIET.store(false, Ordering::Relaxed);
380 }
381 }
382}
383
384fn holding_quiet() -> bool {
385 QUIET_HELD.load(Ordering::Relaxed) > 0
386}
387
388fn pack_quiet() -> bool {
389 holding_quiet() && PACK_QUIET.load(Ordering::Relaxed)
390}
391
392fn mark_pack_quiet() {
393 if holding_quiet() {
394 PACK_QUIET.store(true, Ordering::Relaxed);
395 }
396}
397
398fn quiet_io() -> std::io::Error {
399 std::io::Error::new(std::io::ErrorKind::TimedOut, "the pack did not answer")
400}
401
402fn io_is_quiet(err: &std::io::Error) -> bool {
405 matches!(
406 err.kind(),
407 std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock
408 )
409}
410
411fn connect(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
412 if pack_quiet() {
413 return Err(quiet_io());
414 }
415 if side_budget_spent() {
416 return Err(budget_spent());
417 }
418 let started = Instant::now();
419 let result = connect_once(addr, timeout);
420 if let Err(err) = &result {
425 if io_is_quiet(err) {
426 mark_pack_quiet();
427 }
428 super::charge_side_wait(started.elapsed());
429 }
430 result
431}
432
433fn side_budget_spent() -> bool {
437 super::side_remaining_ms() == Some(0)
438}
439
440fn budget_spent() -> std::io::Error {
441 std::io::Error::new(
442 std::io::ErrorKind::TimedOut,
443 "the side event's pack budget is spent",
444 )
445}
446
447fn connect_once(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
448 let wait = bounded_wait(timeout, true)?;
449 let stream = TcpStream::connect_timeout(&addr, wait)?;
450 let io_wait = bounded_wait(timeout, false)?;
451 stream.set_read_timeout(Some(io_wait))?;
452 stream.set_write_timeout(Some(io_wait))?;
453 let _ = stream.set_nodelay(true);
454 Ok(stream)
455}
456
457fn bounded_wait(requested: Duration, connect: bool) -> std::io::Result<Duration> {
461 let mut wait = capped_wait(requested);
462 if let Some(deadline) = clock_deadline() {
463 let left = deadline.saturating_duration_since(Instant::now());
464 if left.is_zero() {
465 return Err(std::io::Error::new(
466 std::io::ErrorKind::TimedOut,
467 "the pack did not answer",
468 ));
469 }
470 wait = wait.min(left);
471 if connect {
472 wait = wait.min(super::pack_connect_budget());
473 }
474 if wait.is_zero() {
475 return Err(std::io::Error::new(
476 std::io::ErrorKind::TimedOut,
477 "the pack did not answer",
478 ));
479 }
480 }
481 Ok(wait)
482}
483
484fn capped_wait(timeout: Duration) -> Duration {
487 let wait = super::cap_pack_wait(timeout);
488 if wait.is_zero() {
489 Duration::from_millis(1)
490 } else {
491 wait
492 }
493}
494
495fn write_all(stream: &mut TcpStream, bytes: &[u8]) -> std::io::Result<()> {
496 let mut sent = 0;
497 while sent < bytes.len() {
498 match stream.write(&bytes[sent..]) {
499 Ok(0) => {
500 return Err(std::io::Error::new(
501 std::io::ErrorKind::WriteZero,
502 "the pack closed the socket",
503 ))
504 }
505 Ok(n) => sent += n,
506 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
507 Err(err) => return Err(err),
508 }
509 }
510 stream.flush()
511}
512
513fn read_response(stream: &mut TcpStream, deadline: Instant) -> std::io::Result<Raw> {
514 let mut buf = Vec::new();
515 let mut tmp = [0u8; 4096];
516 let head_end = loop {
517 if buf.len() > 64 * 1024 {
518 return Err(std::io::Error::new(
519 std::io::ErrorKind::InvalidData,
520 "the pack's headers are too long",
521 ));
522 }
523 arm_read(stream, deadline)?;
524 let n = read_some(stream, &mut tmp)?;
525 buf.extend_from_slice(&tmp[..n]);
526 if let Some(at) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
527 break at;
528 }
529 };
530 let header = String::from_utf8_lossy(&buf[..head_end]).into_owned();
531 let status = header
532 .lines()
533 .next()
534 .and_then(|line| line.split_whitespace().nth(1))
535 .and_then(|code| code.parse().ok())
536 .ok_or_else(|| {
537 std::io::Error::new(
538 std::io::ErrorKind::InvalidData,
539 "the pack's status line is not HTTP",
540 )
541 })?;
542 let mut length = None;
543 let mut close = header.starts_with("HTTP/1.0");
544 for line in header.lines().skip(1) {
545 let Some((name, value)) = line.split_once(':') else {
546 continue;
547 };
548 if name.eq_ignore_ascii_case("content-length") {
549 length = value.trim().parse().ok();
550 } else if name.eq_ignore_ascii_case("connection")
551 && value.trim().eq_ignore_ascii_case("close")
552 {
553 close = true;
554 }
555 }
556 let mut body = buf.split_off(head_end + 4);
557 if let Some(length) = length {
558 while body.len() < length {
559 arm_read(stream, deadline)?;
560 let n = read_some(stream, &mut tmp)?;
561 body.extend_from_slice(&tmp[..n]);
562 if body.len() > length.saturating_add(64 * 1024) {
563 break;
564 }
565 }
566 body.truncate(length);
567 } else {
568 loop {
569 if deadline.saturating_duration_since(Instant::now()).is_zero() {
570 break;
571 }
572 let _ = arm_read(stream, deadline);
573 match stream.read(&mut tmp) {
574 Ok(0) => break,
575 Ok(n) => body.extend_from_slice(&tmp[..n]),
576 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
577 Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => break,
578 Err(err) if err.kind() == std::io::ErrorKind::TimedOut => break,
579 Err(_) => break,
580 }
581 }
582 close = true;
583 }
584 Ok(Raw {
586 status,
587 body: String::from_utf8_lossy(&body).into_owned(),
588 keep: !close && !(300..400).contains(&status),
589 })
590}
591
592fn arm_read(stream: &TcpStream, deadline: Instant) -> std::io::Result<()> {
593 let remain = deadline.saturating_duration_since(Instant::now());
594 if remain.is_zero() {
595 return Err(std::io::Error::new(
596 std::io::ErrorKind::TimedOut,
597 "the pack did not answer",
598 ));
599 }
600 stream.set_read_timeout(Some(remain))?;
601 Ok(())
602}
603
604fn read_some(stream: &mut TcpStream, tmp: &mut [u8]) -> std::io::Result<usize> {
605 loop {
606 match stream.read(tmp) {
607 Ok(0) => {
608 return Err(std::io::Error::new(
609 std::io::ErrorKind::UnexpectedEof,
610 "the pack closed the socket",
611 ))
612 }
613 Ok(n) => return Ok(n),
614 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
615 Err(err) => return Err(err),
616 }
617 }
618}
619
620fn render(
621 method: &str,
622 host: &str,
623 path: &str,
624 token: Option<&str>,
625 body: Option<&[u8]>,
626) -> Vec<u8> {
627 let mut head =
628 format!("{method} {path} HTTP/1.1\r\nHost: {host}\r\nConnection: keep-alive\r\n");
629 if let Some(token) = token {
630 head.push_str("Authorization: Bearer ");
631 head.push_str(token);
632 head.push_str("\r\n");
633 }
634 let mut out = Vec::new();
635 if let Some(body) = body {
636 head.push_str("Content-Type: application/json\r\n");
637 head.push_str(&format!("Content-Length: {}\r\n\r\n", body.len()));
638 out.extend(head.into_bytes());
639 out.extend_from_slice(body);
640 } else {
641 head.push_str("\r\n");
642 out.extend(head.into_bytes());
643 }
644 out
645}
646
647struct Wire<'a> {
648 method: &'a str,
649 path: &'a str,
650 query: &'a [(&'a str, &'a str)],
651 body: Option<&'a [u8]>,
652 token: Option<&'a str>,
653}
654
655enum Gate {
656 Send,
658 Bare,
660 Busy(Raw),
662}
663
664impl PacksetClient {
665 pub fn new(base: impl Into<String>) -> Self {
666 let mut base = base.into();
667 while base.ends_with('/') {
668 base.pop();
669 }
670 let origin = Url::parse(&base).unwrap_or_else(|_| Url::parse("http://127.0.0.1/").unwrap());
671 let addr = Url::parse(&base).ok().as_ref().and_then(pin);
672 Self {
673 base,
674 origin,
675 addr,
676 workspace: None,
677 token: Mutex::new(load_token()),
678 }
679 }
680
681 pub fn from_env() -> Result<Self, Error> {
683 packset_client::load_seat_env();
684 let url = std::env::var("PACKSET_URL")
685 .or_else(|_| std::env::var("INSIDE_MEMORY_URL"))
686 .ok()
687 .filter(|url| !url.is_empty());
688 match url {
689 Some(url) if url == "off" => Err(Error::NoUrl),
690 Some(url) => Ok(Self::new(url)),
691 None => Ok(Self::new(format!(
692 "http://127.0.0.1:{}",
693 packset_client::default_port()
694 ))),
695 }
696 }
697
698 #[must_use]
699 pub fn with_workspace(mut self, workspace: impl Into<String>) -> Self {
700 let workspace = workspace.into();
701 self.workspace = (!workspace.is_empty()).then_some(workspace);
702 self
703 }
704
705 pub fn base(&self) -> &str {
706 &self.base
707 }
708
709 #[must_use]
711 pub(crate) fn has_token(&self) -> bool {
712 load_token().is_some()
713 }
714
715 #[must_use]
718 pub(crate) fn may_carry(&self) -> bool {
719 load_token().is_some() && !foreign(&self.base)
720 }
721
722 #[must_use]
725 pub(crate) fn carried(&self) -> bool {
726 let Some(addr) = self.addr else {
727 return false;
728 };
729 let map = slots().lock().unwrap_or_else(|p| p.into_inner());
730 map.get(&addr)
731 .is_some_and(|slot| slot.lock().unwrap_or_else(|p| p.into_inner()).carried)
732 }
733
734 pub fn workspace(&self) -> String {
735 if let Some(workspace) = &self.workspace {
736 return workspace.clone();
737 }
738 if let Ok(workspace) = std::env::var("PACKSET_WORKSPACE") {
739 if !workspace.is_empty() {
740 return workspace;
741 }
742 }
743 let cwd = std::env::var("HOSTPACK_WORKSPACE")
744 .ok()
745 .map(|s| s.trim().to_string())
746 .filter(|s| !s.is_empty())
747 .map(std::path::PathBuf::from)
748 .or_else(|| std::env::current_dir().ok())
749 .unwrap_or_else(|| std::path::PathBuf::from("."));
750 let abs = cwd.canonicalize().unwrap_or(cwd);
751 let shown = abs.to_string_lossy().into_owned();
752 if let Ok(answer) = self.roundtrip(
753 "GET",
754 "/v1/identity",
755 &[("cwd", shown.as_str())],
756 None,
757 true,
758 budget(),
759 ) {
760 if answer.status == 200 {
761 if let Ok(val) = serde_json::from_str::<Value>(&answer.body) {
762 if let Some(ws) = val.get("workspace").and_then(|v| v.as_str()) {
763 if !ws.is_empty() {
764 return ws.to_string();
765 }
766 }
767 }
768 }
769 }
770 format!("dir:{}", abs.display())
771 }
772
773 pub(crate) fn roundtrip(
778 &self,
779 method: &str,
780 path: &str,
781 query: &[(&str, &str)],
782 body: Option<&[u8]>,
783 bearer: bool,
784 timeout: Duration,
785 ) -> Result<Answer, Error> {
786 if method != "GET" {
789 crate::forget_atom_listings();
790 crate::forget_gate_fresh();
791 forget_kept_answers();
792 }
793 let memo_key = answer_memo_key(method, path, query, bearer);
794 if let Some(answer) = memo_key.as_deref().and_then(kept_answer) {
795 return Ok(answer);
796 }
797 let Some(addr) = self.addr else {
798 return Err(Error::Bad(format!(
799 "{} is not a socket this seat can pin",
800 self.base
801 )));
802 };
803 let token = load_token();
804 *self.token.lock().unwrap_or_else(|p| p.into_inner()) = token.clone();
805 let allow = bearer && token.is_some() && !foreign(&self.base);
806 let answer = self.exchange(
807 addr,
808 &Wire {
809 method,
810 path,
811 query,
812 body,
813 token: token.as_deref(),
814 },
815 allow,
816 timeout,
817 )?;
818 if let Some(key) = memo_key {
819 if (200..300).contains(&answer.status) {
820 keep_answer(key, &answer);
821 }
822 }
823 Ok(answer)
824 }
825
826 fn exchange(
827 &self,
828 addr: SocketAddr,
829 call: &Wire<'_>,
830 allow: bool,
831 timeout: Duration,
832 ) -> Result<Answer, Error> {
833 let _budget = PackClock::enter(timeout);
838 let slot = slot_for(addr);
839 let mut live = slot.lock().unwrap_or_else(|p| p.into_inner());
840 let tag = token_tag(call.token);
841 if live.token_tag != tag {
842 live.stream = None;
843 live.proven = false;
844 live.token_tag = tag;
845 }
846 let carried = if allow {
847 match self.gate(&mut live, addr, timeout)? {
848 Gate::Bare => None,
849 Gate::Send => call.token,
850 Gate::Busy(raw) => {
851 live.carried = false;
852 live.proven = false;
853 return Ok(Answer {
854 status: raw.status,
855 body: raw.body,
856 trusted: false,
857 });
858 }
859 }
860 } else {
861 None
862 };
863 if live.stream.is_none() {
864 match connect(addr, timeout) {
865 Ok(stream) => live.stream = Some(stream),
866 Err(err) => {
867 live.proven = false;
868 live.carried = false;
869 return Err(Error::Io(err));
870 }
871 }
872 live.proven = false;
876 }
877 let raw = match send(
878 &mut live,
879 &self.origin,
880 &Wire {
881 token: carried,
882 ..*call
883 },
884 timeout,
885 ) {
886 Ok(raw) => raw,
887 Err(err) => {
888 live.stream = None;
889 live.proven = false;
890 live.carried = false;
891 return Err(Error::Io(err));
892 }
893 };
894 let trusted = carried.is_some();
895 live.carried = trusted;
896 if trusted && live.stream.is_some() {
897 live.proven = true;
898 } else if live.stream.is_none() {
899 live.proven = false;
900 }
901 Ok(Answer {
902 status: raw.status,
903 body: raw.body,
904 trusted,
905 })
906 }
907
908 fn gate(&self, live: &mut Slot, addr: SocketAddr, timeout: Duration) -> Result<Gate, Error> {
911 if live.proven && live.stream.is_some() {
912 return Ok(Gate::Send);
913 }
914 live.proven = false;
915 let (Some(id), Some(bogus)) = (nonce(), nonce()) else {
916 return Err(Error::Io(std::io::Error::other(
917 "no randomness for the pack proof",
918 )));
919 };
920 let workspace = self.proof_workspace();
921 let path = format!("/v1/atoms/{}", &id[..32]);
922 let query = [("workspace", workspace.as_str())];
923 let mut stage = 0u8;
924 let mut restarts = 0u8;
925 loop {
926 if live.stream.is_none() {
927 live.stream = Some(connect(addr, timeout).map_err(Error::Io)?);
928 live.proven = false;
929 }
930 let shown = if stage == 0 {
931 None
932 } else {
933 Some(bogus.as_str())
934 };
935 let raw = match send(
936 live,
937 &self.origin,
938 &Wire {
939 method: "GET",
940 path: &path,
941 query: &query,
942 body: None,
943 token: shown,
944 },
945 timeout,
946 ) {
947 Ok(raw) => raw,
948 Err(err) => {
949 live.stream = None;
950 live.proven = false;
951 if io_is_quiet(&err) {
956 return Err(Error::Io(err));
957 }
958 restarts += 1;
959 let spent = clock_deadline().is_some_and(|deadline| Instant::now() >= deadline);
960 if spent || restarts > 2 {
961 return Err(Error::Io(err));
962 }
963 stage = 0;
964 continue;
965 }
966 };
967 if raw.status == 503 {
968 return Ok(Gate::Busy(raw));
969 }
970 if raw.status != 401 {
971 return Ok(Gate::Bare);
972 }
973 if stage == 0 {
974 stage = 1;
975 continue;
976 }
977 return Ok(Gate::Send);
978 }
979 }
980
981 fn proof_workspace(&self) -> String {
982 if let Some(workspace) = &self.workspace {
983 return workspace.clone();
984 }
985 std::env::var("PACKSET_WORKSPACE")
986 .ok()
987 .filter(|w| !w.is_empty())
988 .unwrap_or_else(|| "seat".to_string())
989 }
990
991 fn tried(
993 &self,
994 method: &str,
995 path: &str,
996 query: &[(&str, &str)],
997 body: Option<&[u8]>,
998 timeout: Duration,
999 ) -> Result<Answer, Error> {
1000 if clock_armed() {
1003 return self.roundtrip(method, path, query, body, true, timeout);
1004 }
1005 let started = Instant::now();
1006 let mut wait = Duration::from_millis(50);
1007 for n in 0..4 {
1008 let answer = self.roundtrip(method, path, query, body, true, timeout)?;
1009 if answer.status == 503 && n < 3 && started.elapsed() + wait < timeout {
1010 std::thread::sleep(wait);
1011 wait *= 3;
1012 continue;
1013 }
1014 return Ok(answer);
1015 }
1016 Err(Error::Bad(format!(
1017 "{}{path}: 503: packsetd is busy",
1018 self.base
1019 )))
1020 }
1021
1022 fn json_ok<T: DeserializeOwned>(&self, path: &str, answer: Answer) -> Result<T, Error> {
1023 if !(200..300).contains(&answer.status) {
1024 return Err(refused(
1025 &format!("{}{path}", self.base),
1026 answer.status,
1027 &answer.body,
1028 ));
1029 }
1030 serde_json::from_str(&answer.body).map_err(Error::from)
1031 }
1032
1033 pub fn health(&self) -> Result<String, Error> {
1034 let answer = self.tried("GET", "/health", &[], None, budget())?;
1035 if !(200..300).contains(&answer.status) {
1036 return Err(refused(
1037 &format!("{}/health", self.base),
1038 answer.status,
1039 &answer.body,
1040 ));
1041 }
1042 Ok(answer.body)
1043 }
1044
1045 pub fn status(&self, workspace: Option<&str>) -> Result<Value, Error> {
1046 let query: Vec<(&str, &str)> = workspace
1047 .map(|workspace| vec![("workspace", workspace)])
1048 .unwrap_or_default();
1049 let answer = self.tried("GET", "/v1/status", &query, None, budget())?;
1050 self.json_ok("/v1/status", answer)
1051 }
1052
1053 pub fn post_atom(&self, atom: &Value) -> Result<Value, Error> {
1054 let bytes = serde_json::to_vec(atom)?;
1055 let answer = self.tried("POST", "/v1/atoms", &[], Some(&bytes), budget())?;
1056 self.json_ok("/v1/atoms", answer)
1057 }
1058
1059 pub fn delete_atom(
1060 &self,
1061 workspace: &str,
1062 id: &str,
1063 why: Option<&str>,
1064 ) -> Result<Value, Error> {
1065 let mut body = serde_json::json!({"workspace": workspace, "id": id});
1066 if let Some(accession) = why {
1067 body["why"] = Value::String(accession.to_string());
1068 }
1069 let bytes = serde_json::to_vec(&body)?;
1070 let answer = self.tried("POST", "/v1/atoms/delete", &[], Some(&bytes), budget())?;
1071 if answer.status == 404 {
1072 return Err(Error::Bad(format!("no atom {id}")));
1073 }
1074 self.json_ok("/v1/atoms/delete", answer)
1075 }
1076
1077 pub fn atoms_of_kind(&self, workspace: &str, kind: &str) -> Result<Vec<Value>, Error> {
1078 let answer = self.tried(
1079 "GET",
1080 "/v1/atoms",
1081 &[("workspace", workspace), ("kind", kind)],
1082 None,
1083 budget(),
1084 )?;
1085 let body: Value = self.json_ok("/v1/atoms", answer)?;
1086 Ok(body
1087 .get("atoms")
1088 .and_then(Value::as_array)
1089 .cloned()
1090 .unwrap_or_default())
1091 }
1092
1093 pub fn atoms_in_set(&self, workspace: &str, set: &str) -> Result<Vec<Value>, Error> {
1094 let answer = self.tried(
1095 "GET",
1096 "/v1/pack",
1097 &[("workspace", workspace), ("set", set)],
1098 None,
1099 budget(),
1100 )?;
1101 let body: Value = self.json_ok("/v1/pack", answer)?;
1102 Ok(body
1103 .get("atoms")
1104 .and_then(Value::as_array)
1105 .cloned()
1106 .unwrap_or_default())
1107 }
1108
1109 pub fn sweep(&self, workspace: &str) -> Result<Value, Error> {
1110 let bytes = serde_json::to_vec(&serde_json::json!({"workspace": workspace}))?;
1111 let answer = self.tried("POST", "/v1/sweep", &[], Some(&bytes), budget())?;
1112 self.json_ok("/v1/sweep", answer)
1113 }
1114
1115 pub fn grade(&self, workspace: &str, id: &str, recalled: bool) -> Result<Value, Error> {
1116 let bytes = serde_json::to_vec(&serde_json::json!({
1117 "workspace": workspace,
1118 "id": id,
1119 "recalled": recalled,
1120 }))?;
1121 let answer = self.tried("POST", "/v1/grade", &[], Some(&bytes), budget())?;
1122 self.json_ok("/v1/grade", answer)
1123 }
1124
1125 pub fn update_atom(&self, workspace: &str, id: &str, fields: &Value) -> Result<Value, Error> {
1127 let bytes = serde_json::to_vec(&serde_json::json!({
1128 "workspace": workspace,
1129 "id": id,
1130 "fields": fields,
1131 }))?;
1132 let answer = self.tried("POST", "/v1/atoms/update", &[], Some(&bytes), budget())?;
1133 self.json_ok("/v1/atoms/update", answer)
1134 }
1135
1136 pub fn activate_as(
1137 &self,
1138 workspace: &str,
1139 q: &str,
1140 limit: u32,
1141 fire: bool,
1142 lens: Option<&str>,
1143 ) -> Result<Value, Error> {
1144 let limit = limit.to_string();
1145 let fire = if fire { "1" } else { "0" };
1146 let mut query = vec![
1147 ("workspace", workspace),
1148 ("q", q),
1149 ("limit", limit.as_str()),
1150 ("fire", fire),
1151 ];
1152 if let Some(name) = lens.filter(|n| !n.trim().is_empty()) {
1153 query.push(("as", name));
1154 }
1155 let answer = self.tried("GET", "/v1/activate", &query, None, budget())?;
1156 self.json_ok("/v1/activate", answer)
1157 }
1158
1159 pub fn hubs(&self, workspace: &str, limit: usize) -> Result<Value, Error> {
1160 let limit = limit.to_string();
1161 let answer = self.tried(
1162 "GET",
1163 "/v1/hubs",
1164 &[("workspace", workspace), ("limit", limit.as_str())],
1165 None,
1166 budget(),
1167 )?;
1168 self.json_ok("/v1/hubs", answer)
1169 }
1170
1171 pub fn consolidate(&self, workspace: &str, apply: bool) -> Result<Value, Error> {
1172 let bytes =
1173 serde_json::to_vec(&serde_json::json!({"workspace": workspace, "apply": apply}))?;
1174 let answer = self.tried("POST", "/v1/consolidate", &[], Some(&bytes), budget())?;
1175 self.json_ok("/v1/consolidate", answer)
1176 }
1177
1178 pub fn search_opts(
1179 &self,
1180 workspace: &str,
1181 q: &str,
1182 limit: u32,
1183 as_of: Option<&str>,
1184 rerank: bool,
1185 ) -> Result<Vec<Hit>, Error> {
1186 Ok(self.search_fetched(workspace, q, limit, as_of, rerank)?.0)
1187 }
1188
1189 pub(crate) fn search_fetched(
1190 &self,
1191 workspace: &str,
1192 q: &str,
1193 limit: u32,
1194 as_of: Option<&str>,
1195 rerank: bool,
1196 ) -> Result<(Vec<Hit>, bool), Error> {
1197 let limit_s = limit.to_string();
1198 let mut query = vec![
1199 ("workspace", workspace),
1200 ("q", q),
1201 ("limit", limit_s.as_str()),
1202 ];
1203 if let Some(at) = as_of {
1204 query.push(("as_of", at));
1205 }
1206 if rerank {
1207 query.push(("rerank", "1"));
1208 }
1209 let timeout = if rerank && !clock_armed() && std::env::var_os("LJOS_IN_HOOK").is_none() {
1212 Duration::from_secs(60).max(budget())
1213 } else {
1214 budget()
1215 };
1216 let answer = self.tried("GET", "/v1/search", &query, None, timeout)?;
1217 let trusted = answer.trusted;
1218 let body: Value = self.json_ok("/v1/search", answer)?;
1219 let hits = body.get("hits").cloned().unwrap_or(Value::Array(vec![]));
1220 Ok((serde_json::from_value(hits)?, trusted))
1221 }
1222
1223 pub fn fire(&self, workspace: &str, ids: &[String]) -> Result<Value, Error> {
1224 let bytes = serde_json::to_vec(&serde_json::json!({
1225 "workspace": workspace,
1226 "ids": ids,
1227 "as": "",
1228 }))?;
1229 let answer = self.tried("POST", "/v1/fire", &[], Some(&bytes), budget())?;
1230 self.json_ok("/v1/fire", answer)
1231 }
1232}
1233
1234fn refused(url: &str, code: u16, body: &str) -> Error {
1235 let reason = serde_json::from_str::<Value>(body)
1236 .ok()
1237 .and_then(|v| v.get("error").and_then(|r| r.as_str()).map(str::to_string))
1238 .unwrap_or_else(|| body.to_string());
1239 let reason = reason.trim();
1240 if code == 401 {
1241 return Error::Bad(format!(
1242 "{url}: 401: {reason}; this seat's token does not match the writer's, \
1243 so it is another user's writer or PACKSET_HOME names another pack"
1244 ));
1245 }
1246 if reason.is_empty() {
1247 Error::Bad(format!("{url}: status code {code}"))
1248 } else {
1249 Error::Bad(format!("{url}: {code}: {reason}"))
1250 }
1251}
1252
1253fn send(live: &mut Slot, origin: &Url, call: &Wire<'_>, timeout: Duration) -> std::io::Result<Raw> {
1254 if pack_quiet() {
1255 return Err(quiet_io());
1256 }
1257 if side_budget_spent() {
1258 return Err(budget_spent());
1259 }
1260 let started = Instant::now();
1261 let result = send_once(live, origin, call, timeout);
1262 if let Err(err) = &result {
1267 if io_is_quiet(err) {
1268 mark_pack_quiet();
1269 }
1270 super::charge_side_wait(started.elapsed());
1271 }
1272 result
1273}
1274
1275fn send_once(
1276 live: &mut Slot,
1277 origin: &Url,
1278 call: &Wire<'_>,
1279 timeout: Duration,
1280) -> std::io::Result<Raw> {
1281 let stream = live
1282 .stream
1283 .as_mut()
1284 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotConnected, "no pack socket"))?;
1285 let wait = bounded_wait(timeout, false)?;
1286 let deadline = Instant::now() + wait;
1287 stream.set_read_timeout(Some(wait))?;
1288 stream.set_write_timeout(Some(wait))?;
1289 let bytes = render(
1290 call.method,
1291 &host_header(origin),
1292 &target(call.path, call.query),
1293 call.token,
1294 call.body,
1295 );
1296 if let Err(err) = write_all(stream, &bytes) {
1297 live.stream = None;
1298 live.proven = false;
1299 return Err(err);
1300 }
1301 match read_response(stream, deadline) {
1302 Ok(raw) => {
1303 if !raw.keep {
1304 live.stream = None;
1305 live.proven = false;
1306 }
1307 Ok(raw)
1308 }
1309 Err(err) => {
1310 live.stream = None;
1311 live.proven = false;
1312 Err(err)
1313 }
1314 }
1315}