1use std::sync::Arc;
30use std::time::Duration;
31use time::OffsetDateTime;
32
33use crate::app::{self, App};
34use crate::config::Config;
35use crate::credential::api::CredentialApi;
36use crate::credential::types::{
37 CredentialInjectionPolicyInfo, CredentialInjectionPolicyPage, InjectionRule,
38 ListCredentialInjectionPoliciesQuery, SecretInfo,
39};
40use crate::error::{RpcStatus, SailError};
41use crate::exec::{ExecOptions, ExecParams, ExecProcess, ExecResult, OutputStream};
42use crate::http::HttpCore;
43use crate::imagebuilder::ImageBuilder;
44use crate::sailbox::api::{SailboxApi, UpgradeResult};
45use crate::sailbox::fs::{DirEntry, EntryType};
46use crate::sailbox::object::Sailbox;
47use crate::sailbox::types::{
48 CreateSailboxRequest, ListSailboxesQuery, SailboxCheckpoint, SailboxHandle, SailboxInfo,
49 SailboxMetricsQuery, SailboxMetricsResponse, SailboxPage, SailboxSpendQuery,
50 SailboxSpendResponse, VolumeInfo, WhoAmI,
51};
52use crate::worker::{
53 is_transient_transport_message, FileReader, FileWriter, Listener, WorkerProxy, WriteOptions,
54};
55
56#[derive(Clone)]
58pub struct Client {
59 inner: Arc<Inner>,
60}
61
62impl std::fmt::Debug for Client {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.debug_struct("Client")
65 .field("config", &self.inner.config)
66 .finish_non_exhaustive()
67 }
68}
69
70struct Inner {
71 config: Config,
72 sailbox_http: HttpCore,
74 api_http: HttpCore,
76 worker: Arc<WorkerProxy>,
79 imagebuilder: ImageBuilder,
80 image_ready: crate::imagecache::ImageReadyCache,
83}
84
85const HINTED_EXEC_ENDPOINT_PROBE_TIMEOUT: Duration = Duration::from_secs(1);
88
89#[derive(Default, Clone)]
95pub struct ClientBuilder {
96 mode: Option<String>,
97 api_key: Option<String>,
98 api_url: Option<String>,
99 sailbox_api_url: Option<String>,
100 imagebuilder_url: Option<String>,
101 ingress_url: Option<String>,
102 client_label: Option<String>,
103}
104
105impl std::fmt::Debug for ClientBuilder {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 f.debug_struct("ClientBuilder")
108 .field(
109 "api_key",
110 &crate::config::redact_key(self.api_key.as_deref().unwrap_or("")),
111 )
112 .field("mode", &self.mode)
113 .field("api_url", &self.api_url)
114 .field("sailbox_api_url", &self.sailbox_api_url)
115 .field("imagebuilder_url", &self.imagebuilder_url)
116 .field("ingress_url", &self.ingress_url)
117 .field("client_label", &self.client_label)
118 .finish()
119 }
120}
121
122impl ClientBuilder {
123 pub fn new(api_key: impl Into<String>) -> ClientBuilder {
126 ClientBuilder {
127 api_key: Some(api_key.into()),
128 ..ClientBuilder::default()
129 }
130 }
131
132 #[doc(hidden)]
135 pub fn mode(mut self, mode: impl Into<String>) -> ClientBuilder {
136 self.mode = Some(mode.into());
137 self
138 }
139
140 pub fn api_url(mut self, api_url: impl Into<String>) -> ClientBuilder {
142 self.api_url = Some(api_url.into());
143 self
144 }
145
146 pub fn sailbox_api_url(mut self, url: impl Into<String>) -> ClientBuilder {
148 self.sailbox_api_url = Some(url.into());
149 self
150 }
151
152 pub fn imagebuilder_url(mut self, url: impl Into<String>) -> ClientBuilder {
154 self.imagebuilder_url = Some(url.into());
155 self
156 }
157
158 pub fn ingress_url(mut self, url: impl Into<String>) -> ClientBuilder {
161 self.ingress_url = Some(url.into());
162 self
163 }
164
165 #[doc(hidden)]
167 pub fn client_label(mut self, label: impl Into<String>) -> ClientBuilder {
168 self.client_label = Some(label.into());
169 self
170 }
171
172 pub fn build(self) -> Result<Client, SailError> {
174 let api_key = self.api_key.unwrap_or_default();
175 let config = Config::resolve(
176 self.mode.as_deref(),
177 api_key,
178 self.api_url,
179 self.sailbox_api_url,
180 self.imagebuilder_url,
181 self.ingress_url,
182 )?;
183 Client::from_config_with_label(
184 config,
185 self.client_label
186 .as_deref()
187 .unwrap_or(crate::http::DEFAULT_CLIENT_LABEL),
188 )
189 }
190}
191
192const STALE_IMAGE_REBUILD_TIMEOUT: Duration = Duration::from_mins(30);
196
197fn image_not_ready_conflict(result: &Result<SailboxHandle, SailError>) -> bool {
201 matches!(
202 result,
203 Err(SailError::Creation {
204 status: 409,
205 message,
206 ..
207 }) if message.starts_with("resolve image:")
208 )
209}
210
211impl Client {
212 pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
214 ClientBuilder::new(api_key)
215 }
216
217 pub fn from_env() -> Result<Client, SailError> {
219 Client::from_config(Config::from_env()?)
220 }
221
222 #[doc(hidden)]
224 pub fn from_env_with_label(label: &str) -> Result<Client, SailError> {
225 Client::from_config_with_label(Config::from_env()?, label)
226 }
227
228 pub fn from_config(config: Config) -> Result<Client, SailError> {
230 Client::from_config_with_label(config, crate::http::DEFAULT_CLIENT_LABEL)
231 }
232
233 fn from_config_with_label(config: Config, client_label: &str) -> Result<Client, SailError> {
234 let sailbox_http = HttpCore::new(&config.sailbox_api_url, &config.api_key)?
235 .with_client_label(client_label);
236 let api_http =
237 HttpCore::new(&config.api_url, &config.api_key)?.with_client_label(client_label);
238 let worker = Arc::new(WorkerProxy::new(&config.api_key)?);
239 let imagebuilder = ImageBuilder::new(&config.imagebuilder_url, &config.api_key)?;
240 Ok(Client {
241 inner: Arc::new(Inner {
242 config,
243 sailbox_http,
244 api_http,
245 worker,
246 imagebuilder,
247 image_ready: crate::imagecache::ImageReadyCache::new(),
248 }),
249 })
250 }
251
252 pub(crate) fn image_ready_cache(&self) -> &crate::imagecache::ImageReadyCache {
253 &self.inner.image_ready
254 }
255
256 #[cfg(any(test, feature = "test-fakes"))]
261 pub fn set_image_ready_refresh_window(&self, window: std::time::Duration) {
262 self.inner.image_ready.set_refresh_window(window);
263 }
264
265 pub fn config(&self) -> &Config {
267 &self.inner.config
268 }
269
270 #[doc(hidden)]
272 pub fn worker(&self) -> Arc<WorkerProxy> {
273 Arc::clone(&self.inner.worker)
274 }
275
276 #[doc(hidden)]
278 pub fn imagebuilder(&self) -> &ImageBuilder {
279 &self.inner.imagebuilder
280 }
281
282 #[doc(hidden)]
284 pub fn sailbox_http(&self) -> &HttpCore {
285 &self.inner.sailbox_http
286 }
287
288 #[doc(hidden)]
290 pub fn api_http(&self) -> &HttpCore {
291 &self.inner.api_http
292 }
293
294 fn sailbox_api(&self) -> SailboxApi<'_> {
295 SailboxApi::new(&self.inner.sailbox_http)
296 }
297
298 async fn create_with_image_revalidation(
306 &self,
307 req: &CreateSailboxRequest,
308 timeout: Option<Duration>,
309 ) -> Result<SailboxHandle, SailError> {
310 let create_started = std::time::Instant::now();
311 let result = self.sailbox_api().create(req, timeout).await;
312 let custom_image = req.image != crate::image::ImageSpec::default()
313 && !crate::imagebuild::is_builtin_base_spec(&req.image);
314 if !custom_image || !image_not_ready_conflict(&result) {
315 return result;
316 }
317 if let Ok(spec_hash) = crate::imagebuild::canonical_spec_key(&req.image) {
318 self.image_ready_cache()
323 .invalidate_spec_started_before(&spec_hash, create_started);
324 }
325 let rebuild_timeout = req
329 .image_build_timeout
330 .unwrap_or(STALE_IMAGE_REBUILD_TIMEOUT);
331 let rebuild =
332 self.build_spec_ready_cached(&req.image, rebuild_timeout, true);
333 tokio::time::timeout(rebuild_timeout, rebuild)
334 .await
335 .unwrap_or_else(|_| {
336 Err(SailError::Transport {
337 kind: crate::error::TransportKind::Timeout,
338 message: "timed out building the image".to_string(),
339 source: None,
340 })
341 })?;
342 self.sailbox_api().create(req, timeout).await
343 }
344
345 pub async fn create_sailbox(
357 &self,
358 req: &CreateSailboxRequest,
359 timeout: Option<Duration>,
360 ) -> Result<Sailbox, SailError> {
361 let bind = |handle: SailboxHandle| Sailbox::bind(self.clone(), handle);
362 if !req.ssh {
363 return self
364 .create_with_image_revalidation(req, timeout)
365 .await
366 .map(bind);
367 }
368 crate::sailbox::api::validate_ingress_ports(&req.ingress_ports)?;
373 self.org_ssh_ca_public_key().await?;
376 let mut req = req.clone();
381 let ssh_allowlist = req
382 .ingress_ports
383 .iter()
384 .find(|port| port.guest_port == 22)
385 .map(|port| port.allowlist.clone())
386 .unwrap_or_default();
387 req.ingress_ports.retain(|port| port.guest_port != 22);
388 let handle = self.create_with_image_revalidation(&req, timeout).await?;
389 let handle_id = handle.sailbox_id.clone();
390 if let Err(err) = self
392 .enable_ssh(
393 &handle_id,
394 &ssh_allowlist,
395 false,
396 Duration::ZERO,
397 )
398 .await
399 {
400 return Err(SailError::Creation {
403 message: format!(
404 "sailbox {handle_id} was created, but SSH setup failed: {err}. Fetch it by \
405 id to retry enable_ssh or terminate it."
406 ),
407 status: 0,
408 body: serde_json::Value::Null,
409 });
410 }
411 Ok(bind(handle))
412 }
413
414 #[doc(hidden)]
416 pub async fn get_sailbox(&self, sailbox_id: &str) -> Result<SailboxInfo, SailError> {
417 self.sailbox_api().get(sailbox_id).await
418 }
419
420 #[doc(hidden)]
422 pub async fn whoami(&self) -> Result<WhoAmI, SailError> {
423 self.sailbox_api().whoami().await
424 }
425
426 pub async fn list_sailboxes(
428 &self,
429 query: &ListSailboxesQuery,
430 ) -> Result<SailboxPage, SailError> {
431 self.sailbox_api().list(query).await
432 }
433
434 pub async fn sailbox_spend(
436 &self,
437 query: &SailboxSpendQuery,
438 ) -> Result<SailboxSpendResponse, SailError> {
439 self.sailbox_api().spend(query).await
440 }
441
442 pub async fn sailbox_metrics(
444 &self,
445 sailbox_id: &str,
446 query: &SailboxMetricsQuery,
447 ) -> Result<SailboxMetricsResponse, SailError> {
448 self.sailbox_api().metrics(sailbox_id, query).await
449 }
450
451 #[doc(hidden)]
453 pub async fn terminate_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
454 self.sailbox_api().terminate(sailbox_id).await
455 }
456
457 #[doc(hidden)]
459 pub async fn pause_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
460 self.sailbox_api().pause(sailbox_id).await
461 }
462
463 #[doc(hidden)]
465 pub async fn sleep_sailbox(
466 &self,
467 sailbox_id: &str,
468 wake_at: Option<OffsetDateTime>,
469 ) -> Result<Option<OffsetDateTime>, SailError> {
470 self.sailbox_api().sleep(sailbox_id, wake_at).await
471 }
472
473 #[doc(hidden)]
475 pub async fn resume_sailbox(&self, sailbox_id: &str) -> Result<SailboxHandle, SailError> {
476 self.sailbox_api().resume(sailbox_id).await
477 }
478
479 #[doc(hidden)]
481 pub async fn checkpoint_sailbox(
482 &self,
483 sailbox_id: &str,
484 name: Option<&str>,
485 ttl_seconds: Option<i64>,
486 ) -> Result<SailboxCheckpoint, SailError> {
487 self.sailbox_api()
488 .checkpoint(sailbox_id, name, ttl_seconds)
489 .await
490 }
491
492 #[doc(hidden)]
495 pub async fn fork_sailbox(
496 &self,
497 sailbox_id: &str,
498 name: Option<&str>,
499 timeout: Option<Duration>,
500 ) -> Result<Sailbox, SailError> {
501 self.sailbox_api()
502 .fork(sailbox_id, name, timeout.map(duration_to_whole_seconds))
503 .await
504 .map(|handle| Sailbox::bind(self.clone(), handle))
505 }
506
507 pub async fn create_from_checkpoint(
524 &self,
525 checkpoint_id: &str,
526 name: Option<&str>,
527 timeout: Option<Duration>,
528 ) -> Result<Sailbox, SailError> {
529 self.sailbox_api()
530 .from_checkpoint(checkpoint_id, name, timeout.map(duration_to_whole_seconds))
531 .await
532 .map(|handle| Sailbox::bind(self.clone(), handle))
533 }
534
535 #[doc(hidden)]
537 pub async fn upgrade_sailbox(&self, sailbox_id: &str) -> Result<UpgradeResult, SailError> {
538 self.sailbox_api().upgrade(sailbox_id).await
539 }
540
541 #[doc(hidden)]
546 pub async fn expose_listener(
547 &self,
548 sailbox_id: &str,
549 guest_port: u32,
550 protocol: crate::sailbox::types::IngressProtocol,
551 allowlist: &[String],
552 ) -> Result<Listener, SailError> {
553 let mut response = self
554 .sailbox_api()
555 .expose(sailbox_id, guest_port, protocol, allowlist)
556 .await?;
557 self.fill_listener_url(sailbox_id, &mut response);
558 Ok(response)
559 }
560
561 #[doc(hidden)]
563 pub async fn unexpose_listener(
564 &self,
565 sailbox_id: &str,
566 guest_port: u32,
567 ) -> Result<(), SailError> {
568 self.sailbox_api().unexpose(sailbox_id, guest_port).await
569 }
570
571 #[doc(hidden)]
573 pub async fn list_listeners(
574 &self,
575 sailbox_id: &str,
576 ) -> Result<Vec<crate::worker::Listener>, SailError> {
577 let mut listeners = self.sailbox_api().list_listeners(sailbox_id).await?;
578 for listener in &mut listeners {
579 self.fill_listener_url(sailbox_id, listener);
580 }
581 Ok(listeners)
582 }
583
584 #[doc(hidden)]
587 pub async fn get_listener(
588 &self,
589 sailbox_id: &str,
590 guest_port: u32,
591 ) -> Result<crate::worker::Listener, SailError> {
592 let mut listener = self
593 .sailbox_api()
594 .get_listener(sailbox_id, guest_port)
595 .await?;
596 self.fill_listener_url(sailbox_id, &mut listener);
597 Ok(listener)
598 }
599
600 #[doc(hidden)]
602 pub async fn custom_domain_dns_target(&self) -> Result<String, SailError> {
603 self.sailbox_api().custom_domain_dns_target().await
604 }
605
606 #[doc(hidden)]
608 pub async fn attach_custom_domain(
609 &self,
610 sailbox_id: &str,
611 domain: &str,
612 guest_port: u32,
613 ) -> Result<crate::sailbox::types::CustomDomainInfo, SailError> {
614 self.sailbox_api()
615 .attach_custom_domain(sailbox_id, domain, guest_port)
616 .await
617 }
618
619 #[doc(hidden)]
621 pub async fn list_custom_domains(
622 &self,
623 sailbox_id: &str,
624 ) -> Result<Vec<crate::sailbox::types::CustomDomainInfo>, SailError> {
625 self.sailbox_api().list_custom_domains(sailbox_id).await
626 }
627
628 #[doc(hidden)]
630 pub async fn detach_custom_domain(
631 &self,
632 sailbox_id: &str,
633 domain: &str,
634 ) -> Result<(), SailError> {
635 self.sailbox_api()
636 .detach_custom_domain(sailbox_id, domain)
637 .await
638 }
639
640 fn fill_listener_url(&self, sailbox_id: &str, listener: &mut crate::worker::Listener) {
644 if listener.public_url.is_empty()
645 && listener.protocol != crate::sailbox::types::ListenerProtocol::Tcp
646 {
647 listener.public_url = crate::sailbox::listeners::synthesized_public_url(
648 self.config(),
649 sailbox_id,
650 listener.guest_port,
651 );
652 }
653 }
654
655 #[doc(hidden)]
657 pub async fn ingress_auth_headers(
658 &self,
659 sailbox_id: &str,
660 ) -> Result<Vec<(String, String)>, SailError> {
661 self.sailbox_api().ingress_auth_headers(sailbox_id).await
662 }
663
664 pub async fn org_ssh_ca_public_key(&self) -> Result<String, SailError> {
666 self.sailbox_api().org_ssh_ca_public_key().await
667 }
668
669 pub async fn issue_user_cert(
673 &self,
674 public_key: &str,
675 timeout: Option<Duration>,
676 ) -> Result<crate::sailbox::types::IssuedUserCert, SailError> {
677 self.sailbox_api()
678 .issue_user_cert(public_key, timeout.map(|t| t.as_secs_f64()))
679 .await
680 }
681
682 pub async fn get_volume(
686 &self,
687 name: &str,
688 mint_if_missing: bool,
689 ) -> Result<VolumeInfo, SailError> {
690 self.sailbox_api().get_volume(name, mint_if_missing).await
691 }
692
693 pub async fn list_volumes(
695 &self,
696 max_objects: Option<i64>,
697 ) -> Result<Vec<VolumeInfo>, SailError> {
698 self.sailbox_api().list_volumes(max_objects).await
699 }
700
701 pub async fn delete_volume(
703 &self,
704 volume_id: &str,
705 allow_missing: bool,
706 ) -> Result<Option<VolumeInfo>, SailError> {
707 self.sailbox_api()
708 .delete_volume(volume_id, allow_missing)
709 .await
710 }
711
712 fn credential_api(&self) -> CredentialApi<'_> {
719 CredentialApi::new(&self.inner.sailbox_http)
720 }
721
722 #[doc(hidden)]
724 pub async fn set_secret(&self, name: &str, value: &str) -> Result<SecretInfo, SailError> {
725 self.credential_api().set_secret(name, value).await
726 }
727
728 #[doc(hidden)]
730 pub async fn get_secret(&self, name: &str) -> Result<SecretInfo, SailError> {
731 self.credential_api().get_secret(name).await
732 }
733
734 #[doc(hidden)]
736 pub async fn list_secrets(&self) -> Result<Vec<SecretInfo>, SailError> {
737 self.credential_api().list_secrets().await
738 }
739
740 #[doc(hidden)]
742 pub async fn delete_secret(&self, name: &str) -> Result<(), SailError> {
743 self.credential_api().delete_secret(name).await
744 }
745
746 #[doc(hidden)]
748 pub async fn create_credential_policy(
749 &self,
750 name: &str,
751 rules: &[InjectionRule],
752 ) -> Result<CredentialInjectionPolicyInfo, SailError> {
753 self.credential_api().create_policy(name, rules).await
754 }
755
756 #[doc(hidden)]
758 pub async fn get_credential_policy(
759 &self,
760 policy_id: &str,
761 ) -> Result<CredentialInjectionPolicyInfo, SailError> {
762 self.credential_api().get_policy(policy_id).await
763 }
764
765 #[doc(hidden)]
767 pub async fn list_credential_policies(
768 &self,
769 query: &ListCredentialInjectionPoliciesQuery,
770 ) -> Result<CredentialInjectionPolicyPage, SailError> {
771 self.credential_api().list_policies(query).await
772 }
773
774 #[doc(hidden)]
776 pub async fn rename_credential_policy(
777 &self,
778 policy_id: &str,
779 name: &str,
780 ) -> Result<CredentialInjectionPolicyInfo, SailError> {
781 self.credential_api().rename_policy(policy_id, name).await
782 }
783
784 #[doc(hidden)]
786 pub async fn delete_credential_policy(&self, policy_id: &str) -> Result<(), SailError> {
787 self.credential_api().delete_policy(policy_id).await
788 }
789
790 #[doc(hidden)]
792 pub async fn sailbox_credential_policy(
793 &self,
794 sailbox_id: &str,
795 ) -> Result<Option<CredentialInjectionPolicyInfo>, SailError> {
796 self.credential_api().sailbox_policy(sailbox_id).await
797 }
798
799 #[doc(hidden)]
801 pub async fn set_sailbox_credential_policy(
802 &self,
803 sailbox_id: &str,
804 policy_id: &str,
805 ) -> Result<(), SailError> {
806 self.credential_api()
807 .attach_sailbox_policy(sailbox_id, policy_id)
808 .await
809 }
810
811 #[doc(hidden)]
813 pub async fn clear_sailbox_credential_policy(&self, sailbox_id: &str) -> Result<(), SailError> {
814 self.credential_api()
815 .detach_sailbox_policy(sailbox_id)
816 .await
817 }
818
819 pub async fn find_app(&self, name: &str, mint_if_missing: bool) -> Result<App, SailError> {
823 app::find_app(&self.inner.api_http, name, mint_if_missing).await
824 }
825
826 pub async fn list_apps(&self) -> Result<Vec<App>, SailError> {
828 app::list_apps(&self.inner.api_http).await
829 }
830
831 #[doc(hidden)]
841 pub async fn exec_endpoint(&self, sailbox_id: &str) -> Result<String, SailError> {
842 let handle = self.resume_sailbox(sailbox_id).await?;
843 if handle.exec_endpoint.is_empty() {
844 return Err(SailError::Internal {
845 message: format!("sailbox {sailbox_id} resumed without an exec endpoint"),
846 });
847 }
848 Ok(handle.exec_endpoint)
849 }
850
851 #[doc(hidden)]
854 pub async fn exec(
855 &self,
856 sailbox_id: &str,
857 argv: Vec<String>,
858 options: ExecOptions,
859 ) -> Result<ExecProcess, SailError> {
860 self.exec_at_endpoint(sailbox_id, None, argv, options).await
861 }
862
863 #[doc(hidden)]
867 pub async fn exec_at_endpoint(
868 &self,
869 sailbox_id: &str,
870 exec_endpoint: Option<&str>,
871 argv: Vec<String>,
872 options: ExecOptions,
873 ) -> Result<ExecProcess, SailError> {
874 if argv.is_empty() {
875 return Err(SailError::InvalidArgument {
876 message: "command must be non-empty".to_string(),
877 });
878 }
879 if options.cwd.is_some() || options.background {
880 return Err(SailError::InvalidArgument {
881 message: "cwd and background require a shell command; use exec_shell or run_shell"
882 .to_string(),
883 });
884 }
885 let env = crate::exec::encode_env(&options.env)?;
888 let hinted_endpoint = exec_endpoint.filter(|endpoint| !endpoint.is_empty());
889 let exec_endpoint = match hinted_endpoint {
890 Some(endpoint) => endpoint.to_string(),
891 None => self.exec_endpoint(sailbox_id).await?,
892 };
893 let params = ExecParams {
894 sailbox_id: sailbox_id.to_string(),
895 exec_endpoint,
896 argv,
897 timeout_seconds: options
900 .timeout
901 .map_or(0, |d| d.as_secs_f64().ceil().max(1.0) as u32),
902 idempotency_key: options.idempotency_key,
903 open_stdin: options.open_stdin || options.pty,
906 pty: options.pty,
907 term: options.term,
908 cols: options.cols,
909 rows: options.rows,
910 env,
911 retry_timeout: options.retry_timeout.as_secs_f64(),
912 forward_ports: options.forward_ports,
913 forward_browser: options.forward_browser,
914 extra_metadata: Vec::new(),
915 forward_clipboard: options.forward_clipboard && options.pty,
918 };
919 self.start_exec_params_at_endpoint(params, hinted_endpoint.is_some())
920 .await
921 }
922
923 #[doc(hidden)]
927 pub async fn start_exec_params_at_endpoint(
928 &self,
929 mut params: ExecParams,
930 endpoint_was_hint: bool,
931 ) -> Result<ExecProcess, SailError> {
932 if !endpoint_was_hint {
933 return ExecProcess::start(self.worker(), params).await;
934 }
935
936 params.ensure_idempotency_key();
943 let hinted_start =
944 ExecProcess::start_with_initial_retry_timeout(self.worker(), params.clone(), Some(0.0));
945 match tokio::time::timeout(HINTED_EXEC_ENDPOINT_PROBE_TIMEOUT, hinted_start).await {
946 Ok(Ok(process)) => return Ok(process),
947 Ok(Err(err)) if !should_reresolve_hinted_exec_endpoint(&err) => return Err(err),
948 Ok(Err(_)) | Err(_) => {}
949 }
950
951 self.worker().channels().invalidate(¶ms.exec_endpoint);
957 let endpoint = self.exec_endpoint(¶ms.sailbox_id).await?;
958 params.exec_endpoint = endpoint;
959 ExecProcess::start(self.worker(), params).await
960 }
961
962 #[doc(hidden)]
966 pub async fn exec_shell(
967 &self,
968 sailbox_id: &str,
969 command: &str,
970 options: ExecOptions,
971 ) -> Result<ExecProcess, SailError> {
972 self.exec_shell_at_endpoint(sailbox_id, None, command, options)
973 .await
974 }
975
976 #[doc(hidden)]
978 pub async fn exec_shell_at_endpoint(
979 &self,
980 sailbox_id: &str,
981 exec_endpoint: Option<&str>,
982 command: &str,
983 mut options: ExecOptions,
984 ) -> Result<ExecProcess, SailError> {
985 let argv = crate::exec::shell_argv(command, &options)?;
986 options.cwd = None;
989 options.background = false;
990 self.exec_at_endpoint(sailbox_id, exec_endpoint, argv, options)
991 .await
992 }
993
994 #[doc(hidden)]
1002 pub async fn read_stream(
1003 &self,
1004 sailbox_id: &str,
1005 remote_path: &str,
1006 ) -> Result<FileReader, SailError> {
1007 let endpoint = self.exec_endpoint(sailbox_id).await?;
1008 Ok(self
1009 .inner
1010 .worker
1011 .read_file(&endpoint, sailbox_id, remote_path))
1012 }
1013
1014 #[doc(hidden)]
1018 pub async fn read_file(
1019 &self,
1020 sailbox_id: &str,
1021 remote_path: &str,
1022 ) -> Result<Vec<u8>, SailError> {
1023 let reader = self.read_stream(sailbox_id, remote_path).await?;
1024 let mut contents = Vec::new();
1025 while let Some(chunk) = reader.next().await {
1026 contents.extend_from_slice(&chunk?);
1027 }
1028 Ok(contents)
1029 }
1030
1031 #[doc(hidden)]
1040 pub async fn write_stream(
1041 &self,
1042 sailbox_id: &str,
1043 remote_path: &str,
1044 options: WriteOptions,
1045 ) -> Result<FileWriter, SailError> {
1046 let endpoint = self.exec_endpoint(sailbox_id).await?;
1047 Ok(self.inner.worker.write_file(
1048 &endpoint,
1049 sailbox_id,
1050 remote_path,
1051 options.create_parents,
1052 options.mode,
1053 ))
1054 }
1055
1056 #[doc(hidden)]
1060 pub async fn write_file(
1061 &self,
1062 sailbox_id: &str,
1063 remote_path: &str,
1064 data: &[u8],
1065 options: WriteOptions,
1066 ) -> Result<(), SailError> {
1067 let mut writer = self.write_stream(sailbox_id, remote_path, options).await?;
1068 writer.write(data).await?;
1069 writer.finish().await
1070 }
1071
1072 async fn run_argv(&self, sailbox_id: &str, argv: Vec<String>) -> Result<ExecResult, SailError> {
1081 self.exec(sailbox_id, argv, ExecOptions::default())
1082 .await?
1083 .wait()
1084 .await
1085 }
1086
1087 #[doc(hidden)]
1090 pub async fn make_dir(&self, sailbox_id: &str, path: &str) -> Result<(), SailError> {
1091 crate::sailbox::fs::require_path(path)?;
1092 let result = self
1093 .run_argv(
1094 sailbox_id,
1095 vec![
1096 "mkdir".to_string(),
1097 "-p".to_string(),
1098 "--".to_string(),
1099 path.to_string(),
1100 ],
1101 )
1102 .await?;
1103 fs_command_ok(&result, &format!("create directory {path}"))
1104 }
1105
1106 #[doc(hidden)]
1109 pub async fn remove_path(&self, sailbox_id: &str, path: &str) -> Result<(), SailError> {
1110 crate::sailbox::fs::require_path(path)?;
1111 let result = self
1112 .run_argv(
1113 sailbox_id,
1114 vec![
1115 "rm".to_string(),
1116 "-rf".to_string(),
1117 "--".to_string(),
1118 path.to_string(),
1119 ],
1120 )
1121 .await?;
1122 fs_command_ok(&result, &format!("remove {path}"))
1123 }
1124
1125 #[doc(hidden)]
1128 pub async fn path_exists(&self, sailbox_id: &str, path: &str) -> Result<bool, SailError> {
1129 crate::sailbox::fs::require_path(path)?;
1130 let result = self
1131 .run_argv(
1132 sailbox_id,
1133 vec!["test".to_string(), "-e".to_string(), path.to_string()],
1134 )
1135 .await?;
1136 match result.exit_code {
1140 0 => Ok(true),
1141 1 => Ok(false),
1142 _ => Err(fs_command_error(
1143 &result,
1144 &format!("check whether {path} exists"),
1145 )),
1146 }
1147 }
1148
1149 #[doc(hidden)]
1153 pub async fn list_dir(&self, sailbox_id: &str, path: &str) -> Result<Vec<DirEntry>, SailError> {
1154 crate::sailbox::fs::require_path(path)?;
1155 let process = self
1156 .exec(
1157 sailbox_id,
1158 crate::sailbox::fs::list_dir_argv(path),
1159 ExecOptions::default(),
1160 )
1161 .await?;
1162 let result = process.wait().await?;
1163 fs_command_ok(&result, &format!("list directory {path}"))?;
1164 if result.stdout_truncated {
1167 return Err(SailError::Execution {
1168 code: RpcStatus::FailedPrecondition,
1169 detail: format!(
1170 "directory listing for {path} was truncated because it has \
1171 too many entries; list a smaller subtree"
1172 ),
1173 });
1174 }
1175 if !result.stdout_complete {
1181 return Err(SailError::Execution {
1182 code: RpcStatus::FailedPrecondition,
1183 detail: format!(
1184 "directory listing for {path} was interrupted before it \
1185 finished streaming; retry the call"
1186 ),
1187 });
1188 }
1189 let mut entries =
1190 crate::sailbox::fs::parse_dir_entries(&process.buffered_output(OutputStream::Stdout))
1191 .map_err(|detail| SailError::Execution {
1192 code: RpcStatus::FailedPrecondition,
1193 detail: format!("directory listing for {path} could not be used: {detail}"),
1194 })?;
1195 if entries.is_empty() {
1198 return Err(SailError::Execution {
1199 code: RpcStatus::FailedPrecondition,
1200 detail: format!(
1201 "directory listing for {path} produced no records; \
1202 listing requires GNU find in the guest"
1203 ),
1204 });
1205 }
1206 let start = entries.remove(0);
1207 if start.entry_type != EntryType::Directory {
1208 return Err(SailError::Execution {
1209 code: RpcStatus::FailedPrecondition,
1210 detail: format!(
1211 "{path} is not a directory (it is a {})",
1212 start.entry_type.as_str()
1213 ),
1214 });
1215 }
1216 Ok(entries)
1217 }
1218}
1219
1220pub(crate) fn duration_to_whole_seconds(duration: Duration) -> i64 {
1224 duration.as_secs_f64().ceil() as i64
1225}
1226
1227fn fs_command_ok(result: &ExecResult, action: &str) -> Result<(), SailError> {
1229 if result.exit_code != 0 {
1230 return Err(fs_command_error(result, action));
1231 }
1232 Ok(())
1233}
1234
1235fn fs_command_error(result: &ExecResult, action: &str) -> SailError {
1238 let stderr = result.stderr.trim();
1239 let suffix = if stderr.is_empty() {
1240 String::new()
1241 } else {
1242 format!(": {stderr}")
1243 };
1244 SailError::Execution {
1245 code: RpcStatus::FailedPrecondition,
1246 detail: format!(
1247 "failed to {action} (exit code {}){suffix}",
1248 result.exit_code
1249 ),
1250 }
1251}
1252
1253fn should_reresolve_hinted_exec_endpoint(err: &SailError) -> bool {
1257 err.retryable()
1258 || matches!(
1259 err,
1260 SailError::Terminated { .. } | SailError::HostLost { .. }
1261 )
1262 || matches!(
1263 err,
1264 SailError::Execution {
1265 code: RpcStatus::Unknown | RpcStatus::Internal,
1266 detail,
1267 } if is_transient_transport_message(detail)
1268 )
1269}
1270
1271#[cfg(test)]
1272mod timeout_tests {
1273 use super::*;
1274
1275 #[test]
1276 fn durations_round_up_to_whole_seconds() {
1277 assert_eq!(duration_to_whole_seconds(Duration::ZERO), 0);
1278 assert_eq!(duration_to_whole_seconds(Duration::from_millis(500)), 1);
1279 assert_eq!(duration_to_whole_seconds(Duration::from_millis(1500)), 2);
1280 assert_eq!(duration_to_whole_seconds(Duration::from_mins(1)), 60);
1281 }
1282
1283 #[test]
1284 fn hinted_exec_reresolves_source_less_transport_statuses() {
1285 let relayed_transport = SailError::Execution {
1286 code: RpcStatus::Unknown,
1287 detail: "error reading server preface: EOF".to_string(),
1288 };
1289 assert!(should_reresolve_hinted_exec_endpoint(&relayed_transport));
1290
1291 let server_verdict = SailError::Execution {
1292 code: RpcStatus::Unknown,
1293 detail: "application rejected exec".to_string(),
1294 };
1295 assert!(!should_reresolve_hinted_exec_endpoint(&server_verdict));
1296 }
1297}