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