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