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