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