1mod container_picker;
3use crate::{
4 remote::{Lifetime, RemoteServices},
5 worker::run,
6};
7use buntrail_core::contracts::{
8 async_types::{EventStream, ServiceFuture},
9 *,
10};
11pub use buntrail_services::connections::authentication::{
12 AuthenticationKind, AuthenticationRequest, AuthenticationResponse,
13};
14pub use buntrail_services::connections::bootstrap::BundledPackages;
15pub use buntrail_services::connections::container_identity::ContainerState;
16use buntrail_services::{
17 connections::{
18 authentication::AuthenticationBroker,
19 bootstrap::{HelperBootstrapper, HelperPackage, Platform},
20 container_engine::ContainerEngineClient,
21 container_transport::{ContainerConnector, EngineHost},
22 local::{LocalConnection, new_id},
23 ssh::{SshConnector, SshProfile},
24 },
25 contracts::*,
26};
27use container_picker::SelectedContainer;
28pub use container_picker::{ContainerInventory, ContainerQuery};
29use futures_core::Stream;
30use std::{
31 future::Future,
32 path::PathBuf,
33 pin::Pin,
34 sync::{
35 Arc, Mutex, Weak,
36 atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
37 },
38 task::{Context, Poll, Waker},
39 time::Duration,
40};
41
42const MAX_CONNECTIONS: usize = 100;
43fn unavailable(message: &str) -> CoreError {
44 CoreError::new(ErrorCode::Unavailable, message)
45}
46fn cancelled() -> CoreError {
47 CoreError::new(ErrorCode::Cancelled, "connection request cancelled")
48}
49fn io(error: impl std::fmt::Display) -> CoreError {
50 CoreError::new(ErrorCode::Io, &error.to_string())
51}
52
53struct State {
54 value: ConnectionState,
55 revision: u64,
56 waiters: Vec<Waker>,
57}
58impl State {
59 fn new(value: ConnectionState) -> Arc<Mutex<Self>> {
60 Arc::new(Mutex::new(Self {
61 value,
62 revision: 1,
63 waiters: vec![],
64 }))
65 }
66 fn publish(shared: &Arc<Mutex<Self>>, value: ConnectionState) {
67 let mut state = shared.lock().unwrap();
68 if state.value == value {
69 return;
70 }
71 state.value = value;
72 state.revision += 1;
73 for wake in state.waiters.drain(..) {
74 wake.wake();
75 }
76 }
77}
78struct States {
79 state: Arc<Mutex<State>>,
80 revision: u64,
81}
82impl Stream for States {
83 type Item = Result<ConnectionState, CoreError>;
84 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
85 let shared = self.state.clone();
86 let mut state = shared.lock().unwrap();
87 if state.revision != self.revision {
88 self.revision = state.revision;
89 return Poll::Ready(Some(Ok(state.value.clone())));
90 }
91 if matches!(
92 state.value,
93 ConnectionState::Disconnected
94 | ConnectionState::Failed(_)
95 | ConnectionState::TargetChanged { .. }
96 ) {
97 return Poll::Ready(None);
98 }
99 if !state.waiters.iter().any(|w| w.will_wake(cx.waker())) {
100 if state.waiters.len() >= MAX_CONNECTIONS {
101 return Poll::Ready(Some(Err(CoreError::new(
102 ErrorCode::Busy,
103 "connection state subscriber capacity reached",
104 ))));
105 }
106 state.waiters.push(cx.waker().clone());
107 }
108 Poll::Pending
109 }
110}
111#[derive(Default)]
112struct Done {
113 finished: AtomicBool,
114 waiters: Mutex<Vec<Waker>>,
115}
116impl Done {
117 fn notify(&self) {
118 for w in self.waiters.lock().unwrap().drain(..) {
119 w.wake();
120 }
121 }
122 fn finish(&self) {
123 self.finished.store(true, Ordering::SeqCst);
124 self.notify();
125 }
126 fn poll(&self, cx: &mut Context<'_>) -> Poll<()> {
127 let mut waiters = self.waiters.lock().unwrap();
128 if self.finished.load(Ordering::SeqCst) {
129 Poll::Ready(())
130 } else {
131 if !waiters.iter().any(|w| w.will_wake(cx.waker())) && waiters.len() < MAX_CONNECTIONS {
132 waiters.push(cx.waker().clone());
133 }
134 Poll::Pending
135 }
136 }
137}
138struct Unused(Arc<PhysicalOwner>);
139impl Future for Unused {
140 type Output = ();
141 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
142 let polled = self.0.done.poll(cx);
143 if self.0.lifetime.connected.load(Ordering::SeqCst)
144 && (self.0.reservations.load(Ordering::SeqCst) != 0
145 || self.0.lifetime.has_active_views())
146 {
147 Poll::Ready(())
148 } else {
149 polled
150 }
151 }
152}
153struct Retired(Arc<Done>);
154impl Future for Retired {
155 type Output = ();
156 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
157 self.0.poll(cx)
158 }
159}
160
161pub(crate) struct PhysicalOwner {
162 admission: Arc<Mutex<()>>,
163 lifetime: Arc<Lifetime>,
164 reservations: Arc<AtomicUsize>,
165 state: Arc<Mutex<State>>,
166 done: Arc<Done>,
167}
168impl PhysicalOwner {
169 pub(crate) fn start(
170 lifetime: Lifetime,
171 retire: Box<dyn FnOnce() -> ServiceFuture<'static, ()> + Send>,
172 ) -> Result<Arc<Self>, CoreError> {
173 let owner = Arc::new(Self {
174 admission: Arc::new(Mutex::new(())),
175 lifetime: Arc::new(lifetime),
176 reservations: Arc::new(AtomicUsize::new(1)),
177 state: State::new(ConnectionState::Negotiating),
178 done: Arc::new(Done::default()),
179 });
180 let lifetime = owner.lifetime.clone();
181 let reservations = owner.reservations.clone();
182 let state = owner.state.clone();
183 let done = owner.done.clone();
184 let admission = owner.admission.clone();
185 std::thread::Builder::new()
186 .name("buntrail-connection-retirement".into())
187 .spawn(move || {
188 loop {
189 {
190 let _admission = admission.lock().unwrap();
191 if !lifetime.connected.load(Ordering::SeqCst)
192 || (reservations.load(Ordering::SeqCst) == 0 && lifetime.idle())
193 {
194 lifetime.connected.store(false, Ordering::SeqCst);
195 break;
196 }
197 }
198 std::thread::sleep(Duration::from_millis(10));
199 }
200 State::publish(&state, ConnectionState::Disconnected);
201 run(Box::pin(lifetime.shutdown()));
202 run(retire());
203 done.finish();
204 })
205 .map_err(io)?;
206 Ok(owner)
207 }
208 fn reserve(&self) -> Result<(), CoreError> {
209 let _admission = self.admission.lock().unwrap();
210 if !self.lifetime.connected.load(Ordering::SeqCst) {
211 return Err(CoreError::new(
212 ErrorCode::Closed,
213 "physical connection is retiring; reconnect explicitly",
214 ));
215 }
216 self.reservations.fetch_add(1, Ordering::SeqCst);
217 self.done.notify();
218 Ok(())
219 }
220 #[cfg(test)]
221 pub(crate) fn finish_fixture_startup(&self) {
222 assert_eq!(self.reservations.fetch_sub(1, Ordering::SeqCst), 1);
223 self.done.notify();
224 }
225 pub(crate) fn wait_unused(self: &Arc<Self>) -> ServiceFuture<'static, ()> {
226 Box::pin(Unused(self.clone()))
227 }
228 fn ready(&self) {
229 State::publish(&self.state, ConnectionState::Ready);
230 }
231 pub(crate) fn stop(&self) {
232 self.lifetime.connected.store(false, Ordering::SeqCst);
233 }
234 pub(crate) fn shutdown(&self) -> ServiceFuture<'static, ()> {
235 self.stop();
236 Box::pin(Retired(self.done.clone()))
237 }
238 fn retired(&self) -> bool {
239 self.done.finished.load(Ordering::SeqCst)
240 }
241}
242impl Drop for PhysicalOwner {
243 fn drop(&mut self) {
244 self.stop();
245 }
246}
247impl ConnectionOwner for PhysicalOwner {
248 fn states(&self) -> EventStream<ConnectionState> {
249 Box::pin(States {
250 state: self.state.clone(),
251 revision: 0,
252 })
253 }
254 fn close(&self, reason: ConnectionCloseReason) -> ServiceFuture<'_, Result<(), CoreError>> {
255 Box::pin(async move {
256 if reason != ConnectionCloseReason::WindowDetachment {
257 self.shutdown().await;
258 }
259 Ok(())
260 })
261 }
262}
263
264#[derive(Clone)]
266pub struct SshConfiguration {
267 pub askpass: PathBuf,
268 pub packages: Vec<HelperPackage>,
269 pub bundle: Option<Arc<BundledPackages>>,
270 pub config: Option<PathBuf>,
271 pub spool_directory: PathBuf,
272}
273
274impl SshConfiguration {
275 fn selected_package(
277 &self,
278 platform: Platform,
279 cancel: &AtomicBool,
280 ) -> Result<HelperPackage, CoreError> {
281 if let Some(bundle) = &self.bundle {
282 return bundle.load(platform, cancel);
283 }
284 self.packages.iter().find(|package| package.platform == platform).cloned().ok_or_else(|| unavailable("trusted Linux helper/search artifacts for the verified architecture and libc are unavailable in this application"))
285 }
286}
287
288#[derive(Default)]
289struct ConnectionBudget {
290 normal: AtomicUsize,
291 transition: AtomicUsize,
292 resuming: Mutex<Vec<(ClientID, WorkspaceID, WorkspaceLeaseID)>>,
293}
294pub(crate) struct ConnectionPermit {
295 budget: Arc<ConnectionBudget>,
296 transition: bool,
297}
298impl Drop for ConnectionPermit {
299 fn drop(&mut self) {
300 let count = if self.transition {
301 &self.budget.transition
302 } else {
303 &self.budget.normal
304 };
305 count.fetch_sub(1, Ordering::SeqCst);
306 }
307}
308impl ConnectionBudget {
309 fn acquire(self: &Arc<Self>, transition: bool) -> Result<Arc<ConnectionPermit>, CoreError> {
310 let (count, limit) = if transition {
311 (&self.transition, 4)
312 } else {
313 (&self.normal, MAX_CONNECTIONS)
314 };
315 count.try_update(Ordering::SeqCst,Ordering::SeqCst,|n|(n<limit).then_some(n+1)).map_err(|_|CoreError::new(ErrorCode::Busy,"live connection admission is full; retire an existing handle or finish a transition"))?;
316 Ok(Arc::new(ConnectionPermit {
317 budget: self.clone(),
318 transition,
319 }))
320 }
321 fn resume(self: &Arc<Self>, owner: WorkspaceLease) -> Result<Arc<ResumeAdmission>, CoreError> {
322 self.owner_admission(owner.client, owner.workspace, owner.lease)
323 }
324 fn owner_admission(
325 self: &Arc<Self>,
326 client: ClientID,
327 workspace: WorkspaceID,
328 lease: WorkspaceLeaseID,
329 ) -> Result<Arc<ResumeAdmission>, CoreError> {
330 let owner = (client, workspace, lease);
331 let mut owners = self.resuming.lock().unwrap();
332 if owners.contains(&owner) {
333 return Err(CoreError::new(
334 ErrorCode::Busy,
335 "this owner already has an explicit resume in progress",
336 ));
337 }
338 owners.push(owner);
339 Ok(Arc::new(ResumeAdmission {
340 budget: self.clone(),
341 owner,
342 }))
343 }
344}
345struct ResumeAdmission {
346 budget: Arc<ConnectionBudget>,
347 owner: (ClientID, WorkspaceID, WorkspaceLeaseID),
348}
349impl Drop for ResumeAdmission {
350 fn drop(&mut self) {
351 self.budget
352 .resuming
353 .lock()
354 .unwrap()
355 .retain(|o| o != &self.owner);
356 }
357}
358struct PoolEntry {
359 locator: EnvironmentLocator,
360 attempt: Weak<Attempt>,
361 route: Weak<RemoteServices>,
362}
363struct PhysicalRecord {
364 target: TargetIdentity,
365 locator: EnvironmentLocator,
366 done: Arc<Done>,
367}
368struct TargetClaim {
369 target: TargetIdentity,
370 locator: EnvironmentLocator,
371 attempt: Weak<Attempt>,
372}
373struct ConnectorState {
374 entries: Vec<PoolEntry>,
375 retirements: Vec<PhysicalRecord>,
376 claims: Vec<TargetClaim>,
377 queries: Vec<Weak<container_picker::QueryState>>,
378 closed: bool,
379}
380#[derive(Clone)]
381pub struct EnvironmentConnector {
382 budget: Arc<ConnectionBudget>,
383 handles: Arc<()>,
384 local: Arc<LocalConnection>,
385 configuration: SshConfiguration,
386 state: Arc<Mutex<ConnectorState>>,
387 workers: Arc<AtomicUsize>,
388}
389struct Completion<T> {
390 value: Option<Result<T, CoreError>>,
391 waker: Option<Waker>,
392}
393impl<T> Completion<T> {
394 fn new() -> Self {
395 Self {
396 value: None,
397 waker: None,
398 }
399 }
400 fn complete(&mut self, result: Result<T, CoreError>) {
401 self.value = Some(result);
402 if let Some(w) = self.waker.take() {
403 w.wake();
404 }
405 }
406}
407struct PromptClaim {
408 request: AuthenticationRequest,
409 waiter: Option<u64>,
410}
411struct Attempt {
412 id: u64,
413 prompt: Mutex<Option<PromptClaim>>,
414 expected: Option<TargetIdentity>,
415 selected_container: Option<SelectedContainer>,
416 state: Arc<Mutex<State>>,
417 broker: Mutex<Option<Arc<AuthenticationBroker>>>,
418 cancel: Arc<AtomicBool>,
419 result: Mutex<Completion<Arc<RemoteServices>>>,
420 dependents: AtomicUsize,
421 reservation_released: AtomicBool,
422}
423impl Attempt {
424 fn cancel_startup(&self) {
425 if !matches!(self.result.lock().unwrap().value, Some(Ok(_))) {
427 self.cancel.store(true, Ordering::SeqCst);
428 if let Some(broker) = self.broker.lock().unwrap().as_ref() {
429 broker.cancel();
430 }
431 }
432 }
433 fn release_reservation(&self) {
434 if let Some(Ok(route)) = &self.result.lock().unwrap().value
435 && !self.reservation_released.swap(true, Ordering::SeqCst)
436 && let Some(owner) = route.physical()
437 {
438 owner.reservations.fetch_sub(1, Ordering::SeqCst);
439 }
440 }
441}
442impl Drop for Attempt {
443 fn drop(&mut self) {
444 self.release_reservation();
445 self.cancel_startup();
446 }
447}
448struct Worker(Arc<AtomicUsize>);
449impl Drop for Worker {
450 fn drop(&mut self) {
451 self.0.fetch_sub(1, Ordering::SeqCst);
452 }
453}
454fn spawn(
455 workers: &Arc<AtomicUsize>,
456 work: impl FnOnce() + Send + 'static,
457) -> Result<(), CoreError> {
458 workers.fetch_add(1, Ordering::SeqCst);
459 let guard = Worker(workers.clone());
460 std::thread::Builder::new()
461 .name("buntrail-environment".into())
462 .spawn(move || {
463 let _guard = guard;
464 work();
465 })
466 .map(|_| ())
467 .map_err(io)
468}
469
470pub struct ConnectionPrompt {
472 pub attempt: u64,
473 pub request: AuthenticationRequest,
474}
475pub struct ConnectRequest {
476 permit: Arc<ConnectionPermit>,
477 transition: Option<Arc<ConnectionPermit>>,
478 resume_admission: Option<Arc<ResumeAdmission>>,
479 attached: AtomicBool,
480 waiter: u64,
481 attempt: Arc<Attempt>,
482 cancel: Arc<AtomicBool>,
483 registration: Option<Arc<Mutex<Completion<Arc<RemoteServices>>>>>,
484 locator: EnvironmentLocator,
485 local_state: Arc<Mutex<State>>,
486 resume: Option<WorkspaceLease>,
487 saved: Option<(WorkspaceID, WorkspaceLeaseID)>,
488 workers: Arc<AtomicUsize>,
489 finished: bool,
490}
491#[derive(Clone)]
492pub struct RemoteEnvironment {
493 pub locator: EnvironmentLocator,
494 pub services: Arc<RemoteServices>,
495 _workers: Arc<AtomicUsize>,
496 _owner_admission: Option<Arc<ResumeAdmission>>,
497}
498impl RemoteEnvironment {
499 #[cfg(feature = "test-support")]
500 pub fn fixture(services: Arc<RemoteServices>, locator: EnvironmentLocator) -> Self {
501 Self {
502 locator,
503 services,
504 _workers: Arc::new(AtomicUsize::new(0)),
505 _owner_admission: None,
506 }
507 }
508
509 pub fn environment(&self) -> ConnectedEnvironment {
510 let service = self.services.clone();
511 ConnectedEnvironment {
512 identity: service.target().clone(),
513 runtime: service.runtime(),
514 capabilities: Capabilities(vec![
515 Capability::Files,
516 Capability::Watch,
517 Capability::Search,
518 Capability::Git,
519 Capability::Terminal,
520 Capability::Operations,
521 Capability::Leases,
522 ]),
523 owner: Arc::new(ViewOwner(service.clone())),
524 files: ServiceAvailability::Available(service.clone()),
525 search: ServiceAvailability::Available(service.clone()),
526 git: ServiceAvailability::Available(service.clone()),
527 terminal: ServiceAvailability::Available(service.clone()),
528 operations: ServiceAvailability::Available(service.clone()),
529 leases: ServiceAvailability::Available(service),
530 }
531 }
532}
533struct ViewOwner(Arc<RemoteServices>);
534impl ConnectionOwner for ViewOwner {
535 fn states(&self) -> EventStream<ConnectionState> {
536 self.0
537 .physical()
538 .expect("connected physical owner")
539 .states()
540 }
541 fn close(&self, reason: ConnectionCloseReason) -> ServiceFuture<'_, Result<(), CoreError>> {
542 Box::pin(async move {
543 self.0
544 .release_workspace(if reason == ConnectionCloseReason::WindowDetachment {
545 LeaseReleaseIntent::ViewDetachment
546 } else {
547 LeaseReleaseIntent::ResolvedWorkspaceClosure
548 })?;
549 let owner = self.0.physical().unwrap();
550 owner.wait_unused().await;
551 Ok(())
552 })
553 }
554}
555impl EnvironmentConnector {
556 pub fn new(
557 local: Arc<LocalConnection>,
558 configuration: SshConfiguration,
559 ) -> Result<Self, CoreError> {
560 if configuration.bundle.is_some() && !configuration.packages.is_empty() {
561 return Err(unavailable("select one trusted bundled package source"));
562 }
563 if configuration.packages.len() > 4 {
564 return Err(unavailable(
565 "at most one trusted package per supported platform is allowed",
566 ));
567 }
568 for (i, package) in configuration.packages.iter().enumerate() {
569 package.validate()?;
570 if configuration.packages[..i]
571 .iter()
572 .any(|p| p.platform == package.platform)
573 {
574 return Err(unavailable("duplicate trusted platform package"));
575 }
576 }
577 Ok(Self {
578 budget: Arc::new(ConnectionBudget::default()),
579 handles: Arc::new(()),
580 local,
581 configuration,
582 state: Arc::new(Mutex::new(ConnectorState {
583 entries: vec![],
584 retirements: vec![],
585 claims: vec![],
586 queries: vec![],
587 closed: false,
588 })),
589 workers: Arc::new(AtomicUsize::new(0)),
590 })
591 }
592 pub fn start(
593 &self,
594 locator: EnvironmentLocator,
595 expected: Option<TargetIdentity>,
596 resume: Option<WorkspaceLease>,
597 ) -> Result<ConnectRequest, CoreError> {
598 self.start_admitted(locator, expected, resume, None, false, None, None)
599 }
600 pub fn start_saved(
601 &self,
602 record: &buntrail_core::workspaces::WorkspaceRecord,
603 ) -> Result<ConnectRequest, CoreError> {
604 self.start_admitted(
605 record.locator.environment.clone(),
606 Some(record.target.clone()),
607 None,
608 None,
609 false,
610 Some((record.id, record.lease)),
611 None,
612 )
613 }
614 pub fn start_observer(
615 &self,
616 locator: EnvironmentLocator,
617 expected: TargetIdentity,
618 ) -> Result<ConnectRequest, CoreError> {
619 self.start_admitted(locator, Some(expected), None, None, true, None, None)
620 }
621 #[allow(clippy::too_many_arguments)]
623 fn start_admitted(
624 &self,
625 locator: EnvironmentLocator,
626 expected: Option<TargetIdentity>,
627 resume: Option<WorkspaceLease>,
628 existing: Option<Arc<ConnectionPermit>>,
629 observer: bool,
630 saved: Option<(WorkspaceID, WorkspaceLeaseID)>,
631 selected_container: Option<SelectedContainer>,
632 ) -> Result<ConnectRequest, CoreError> {
633 let (permit, transition) = if let Some(existing) = existing {
634 if !Arc::ptr_eq(&existing.budget, &self.budget) {
635 return Err(CoreError::new(
636 ErrorCode::InvalidArgument,
637 "owner belongs to another connector",
638 ));
639 }
640 (existing, Some(self.budget.acquire(true)?))
641 } else {
642 (
643 self.budget.acquire(observer).or_else(|error| {
644 if resume.is_some() {
645 self.budget.acquire(true)
646 } else {
647 Err(error)
648 }
649 })?,
650 None,
651 )
652 };
653 let resume_admission = if let Some((workspace, lease)) = saved {
654 Some(
655 self.budget
656 .owner_admission(self.local.client, workspace, lease)?,
657 )
658 } else {
659 resume
660 .clone()
661 .map(|owner| self.budget.resume(owner))
662 .transpose()?
663 };
664 static NEXT: AtomicU64 = AtomicU64::new(1);
665 static NEXT_WAITER: AtomicU64 = AtomicU64::new(1);
666 let valid = match locator.route {
667 RouteKind::Ssh => {
668 locator.ssh_profile.as_ref().is_some_and(|p| !p.is_empty())
669 && locator.engine.is_none()
670 && locator.engine_context.is_none()
671 && locator.container_selector.is_none()
672 && locator.effective_user.is_none()
673 }
674 RouteKind::LocalContainer => {
675 locator.ssh_profile.is_none()
676 && locator.engine.is_some()
677 && locator
678 .container_selector
679 .as_ref()
680 .is_some_and(|s| !s.is_empty())
681 }
682 RouteKind::SshContainer => {
683 locator.ssh_profile.as_ref().is_some_and(|p| !p.is_empty())
684 && locator.engine.is_some()
685 && locator
686 .container_selector
687 .as_ref()
688 .is_some_and(|s| !s.is_empty())
689 }
690 RouteKind::Local => false,
691 };
692 if !valid {
693 return Err(unavailable(
694 "Choose a saved SSH profile or an engine and existing container for this route",
695 ));
696 }
697 if resume
698 .as_ref()
699 .is_some_and(|o| o.client != self.local.client)
700 {
701 return Err(CoreError::new(
702 ErrorCode::TargetChanged,
703 "workspace client changed",
704 ));
705 }
706 let mut pool = self.state.lock().unwrap();
707 if pool.closed {
708 return Err(CoreError::new(
709 ErrorCode::Closed,
710 "environment connector is closed",
711 ));
712 }
713 pool.entries
714 .retain(|e| e.attempt.strong_count() > 0 || e.route.strong_count() > 0);
715 pool.retirements
716 .retain(|record| !record.done.finished.load(Ordering::SeqCst));
717 if pool.retirements.len() >= MAX_CONNECTIONS {
718 return Err(CoreError::new(
719 ErrorCode::Busy,
720 "wait for prior physical connection retirement",
721 ));
722 }
723 let index = pool.entries.iter().position(|e| e.locator == locator);
724 let existing = index
725 .and_then(|i| pool.entries[i].attempt.upgrade())
726 .filter(|a| !a.cancel.load(Ordering::SeqCst));
727 let attempt = if let Some(attempt) = existing {
728 if attempt.expected != expected || attempt.selected_container != selected_container {
729 return Err(CoreError::new(
730 ErrorCode::Busy,
731 "a connection with another target expectation is in progress",
732 ));
733 }
734 attempt
735 } else {
736 if pool.entries.len() >= MAX_CONNECTIONS && index.is_none() {
737 return Err(CoreError::new(
738 ErrorCode::Busy,
739 "connection capacity reached",
740 ));
741 }
742 let attempt = Arc::new(Attempt {
743 id: NEXT.fetch_add(1, Ordering::SeqCst),
744 prompt: Mutex::new(None),
745 expected: expected.clone(),
746 selected_container: selected_container.clone(),
747 state: State::new(ConnectionState::Resolving),
748 broker: Mutex::new(None),
749 cancel: Arc::new(AtomicBool::new(false)),
750 result: Mutex::new(Completion::new()),
751 dependents: AtomicUsize::new(0),
752 reservation_released: AtomicBool::new(false),
753 });
754 let reusable = index
755 .and_then(|i| pool.entries[i].route.upgrade())
756 .filter(|r| r.connection_state() == ConnectionState::Ready);
757 let entry = PoolEntry {
758 locator: locator.clone(),
759 attempt: Arc::downgrade(&attempt),
760 route: reusable.as_ref().map(Arc::downgrade).unwrap_or_default(),
761 };
762 if let Some(i) = index {
763 pool.entries[i] = entry;
764 } else {
765 pool.entries.push(entry);
766 }
767 if let Some(route) = reusable {
768 if let Some(selected) = selected_container.as_ref()
769 && !selected.matches_existing(route.target(), &route.runtime())
770 {
771 return Err(CoreError::new(
772 ErrorCode::TargetChanged,
773 "Selected container no longer matches the active physical target/runtime",
774 ));
775 }
776 route.validate_connection_ready()?;
777 route.physical().unwrap().reserve()?;
778 State::publish(&attempt.state, ConnectionState::Ready);
779 attempt.result.lock().unwrap().complete(Ok(route));
780 } else {
781 let worker = attempt.clone();
782 let config = self.configuration.clone();
783 let client = self.local.client;
784 let registry = self.state.clone();
785 let location = locator.clone();
786 spawn(&self.workers, move || {
787 let result = connect_ssh(&worker, config, client, &location, ®istry);
788 if let Ok(route) = &result {
789 let mut registry = registry.lock().unwrap();
790 registry.retirements.push(PhysicalRecord {
791 target: route.target().clone(),
792 locator: location.clone(),
793 done: route.physical().unwrap().done.clone(),
794 });
795 if let Some(entry) = registry
796 .entries
797 .iter_mut()
798 .find(|e| e.attempt.as_ptr() == Arc::as_ptr(&worker))
799 {
800 entry.route = Arc::downgrade(route);
801 }
802 }
803 if let Err(error) = &result
804 && !matches!(
805 worker.state.lock().unwrap().value,
806 ConnectionState::TargetChanged { .. }
807 )
808 {
809 State::publish(&worker.state, ConnectionState::Failed(error.clone()));
810 }
811 worker.result.lock().unwrap().complete(result);
812 if worker.dependents.load(Ordering::SeqCst) == 0 {
813 worker.release_reservation();
814 }
815 })?;
816 }
817 attempt
818 };
819 if attempt.dependents.fetch_add(1, Ordering::SeqCst) >= MAX_CONNECTIONS {
820 attempt.dependents.fetch_sub(1, Ordering::SeqCst);
821 return Err(CoreError::new(
822 ErrorCode::Busy,
823 "connection dependent capacity reached",
824 ));
825 }
826 drop(pool);
827 let mut request = ConnectRequest {
828 permit,
829 transition,
830 resume_admission,
831 attached: AtomicBool::new(true),
832 waiter: NEXT_WAITER.fetch_add(1, Ordering::SeqCst),
833 attempt,
834 cancel: Arc::new(AtomicBool::new(false)),
835 registration: None,
836 locator: locator.clone(),
837 local_state: State::new(if resume.is_some() {
838 ConnectionState::Reconnecting
839 } else {
840 ConnectionState::Resolving
841 }),
842 resume,
843 saved,
844 workers: self.workers.clone(),
845 finished: false,
846 };
847 request.begin_registration(locator, expected)?;
849 Ok(request)
850 }
851 pub fn shutdown(&self) {
852 let mut pool = self.state.lock().unwrap();
853 pool.closed = true;
854 for query in pool.queries.iter().filter_map(Weak::upgrade) {
855 query.cancel();
856 }
857 for entry in &pool.entries {
858 if let Some(attempt) = entry.attempt.upgrade() {
859 attempt.cancel.store(true, Ordering::SeqCst);
860 if let Some(broker) = attempt.broker.lock().unwrap().as_ref() {
861 broker.cancel();
862 }
863 }
864 if let Some(route) = entry.route.upgrade()
865 && let Some(owner) = route.physical()
866 {
867 owner.stop();
868 }
869 }
870 }
871 pub fn shutdown_ready(&self) -> bool {
872 self.workers.load(Ordering::SeqCst) == 0 && {
873 let state = self.state.lock().unwrap();
874 state
875 .retirements
876 .iter()
877 .all(|r| r.done.finished.load(Ordering::SeqCst))
878 && state
879 .entries
880 .iter()
881 .filter_map(|e| e.route.upgrade())
882 .all(|r| r.physical().is_none_or(|p| p.retired()))
883 }
884 }
885}
886impl ConnectRequest {
887 pub fn attempt_id(&self) -> u64 {
888 self.attempt.id
889 }
890 pub fn state(&self) -> ConnectionState {
891 self.local_state.lock().unwrap().value.clone()
892 }
893 pub fn states(&self) -> EventStream<ConnectionState> {
894 Box::pin(States {
895 state: self.local_state.clone(),
896 revision: 0,
897 })
898 }
899 pub fn take_authentication(&self) -> Option<ConnectionPrompt> {
900 let mut claim = self.attempt.prompt.lock().unwrap();
901 if self.cancel.load(Ordering::SeqCst) {
902 return None;
903 }
904 if claim.is_none() {
905 let request = self
906 .attempt
907 .broker
908 .lock()
909 .unwrap()
910 .as_ref()?
911 .take_request()?;
912 *claim = Some(PromptClaim {
913 request,
914 waiter: None,
915 });
916 }
917 let claim = claim.as_mut()?;
918 if claim.waiter.is_some() {
919 return None;
920 }
921 claim.waiter = Some(self.waiter);
922 State::publish(&self.attempt.state, ConnectionState::Authenticating);
923 State::publish(&self.local_state, ConnectionState::Authenticating);
924 Some(ConnectionPrompt {
925 attempt: self.attempt.id,
926 request: AuthenticationRequest {
927 id: claim.request.id,
928 kind: claim.request.kind.clone(),
929 prompt: claim.request.prompt.clone(),
930 },
931 })
932 }
933 pub fn respond(&self, attempt: u64, prompt: u64, response: AuthenticationResponse) -> bool {
934 let mut claim = self.attempt.prompt.lock().unwrap();
935 if attempt != self.attempt.id
936 || self.cancel.load(Ordering::SeqCst)
937 || !claim
938 .as_ref()
939 .is_some_and(|c| c.request.id == prompt && c.waiter == Some(self.waiter))
940 {
941 return false;
942 }
943 let accepted = self
944 .attempt
945 .broker
946 .lock()
947 .unwrap()
948 .as_ref()
949 .is_some_and(|b| b.respond(prompt, response));
950 if accepted {
951 *claim = None;
952 }
953 accepted
954 }
955 fn detach_attempt(&self) {
956 if self.attached.swap(false, Ordering::SeqCst)
957 && self.attempt.dependents.fetch_sub(1, Ordering::SeqCst) == 1
958 {
959 self.attempt.cancel_startup();
960 self.attempt.release_reservation();
961 }
962 }
963 pub fn cancel(&self) {
964 self.cancel.store(true, Ordering::SeqCst);
965 if let Some(claim) = self.attempt.prompt.lock().unwrap().as_mut()
966 && claim.waiter == Some(self.waiter)
967 {
968 claim.waiter = None;
969 }
970 if !self.finished {
971 State::publish(&self.local_state, ConnectionState::Failed(cancelled()));
972 }
973 self.detach_attempt();
974 }
975 fn begin_registration(
976 &mut self,
977 locator: EnvironmentLocator,
978 expected: Option<TargetIdentity>,
979 ) -> Result<(), CoreError> {
980 self.locator = locator;
981 let completion = Arc::new(Mutex::new(Completion::new()));
982 self.registration = Some(completion.clone());
983 let permit = self.permit.clone();
984 let transition = self.transition.clone();
985 let resume_admission = self.resume_admission.clone();
986 let attempt = self.attempt.clone();
987 let cancel = self.cancel.clone();
988 let resume = self.resume.clone();
989 let saved = self.saved;
990 let status = self.local_state.clone();
991 let release_intent = if resume.is_some() || saved.is_some() {
992 LeaseReleaseIntent::ViewDetachment
993 } else {
994 LeaseReleaseIntent::ResolvedWorkspaceClosure
995 };
996 spawn(&self.workers, move || {
997 let _transition = transition;
998 let _resume_admission = resume_admission;
999 let result = (|| {
1000 let anchor = loop {
1001 if cancel.load(Ordering::SeqCst) || attempt.cancel.load(Ordering::SeqCst) {
1002 return Err(cancelled());
1003 }
1004 let current = attempt.state.lock().unwrap().value.clone();
1005 State::publish(
1006 &status,
1007 if current == ConnectionState::Ready {
1008 ConnectionState::Negotiating
1009 } else if current == ConnectionState::Resolving && resume.is_some() {
1010 ConnectionState::Reconnecting
1011 } else {
1012 current
1013 },
1014 );
1015 if let Some(result) = attempt.result.lock().unwrap().value.clone() {
1016 break result?;
1017 }
1018 std::thread::sleep(Duration::from_millis(10));
1019 };
1020 if let Some(expected) = expected.filter(|e| e != anchor.target()) {
1021 State::publish(
1022 &status,
1023 ConnectionState::TargetChanged {
1024 expected,
1025 observed: anchor.target().clone(),
1026 },
1027 );
1028 return Err(CoreError::new(
1029 ErrorCode::TargetChanged,
1030 "verified SSH target changed; explicit target selection is required",
1031 ));
1032 }
1033 anchor.validate_connection_ready()?;
1034 let lease = if let Some(owner) = resume {
1035 let observer = anchor.anchored_workspace(WorkspaceLease {
1036 client: anchor.lease().client,
1037 workspace: WorkspaceID(new_id()?),
1038 lease: WorkspaceLeaseID(new_id()?),
1039 runtime: anchor.runtime(),
1040 })?;
1041 let observed = observer.observe_owner(owner.clone());
1042 observer.release_workspace(LeaseReleaseIntent::ResolvedWorkspaceClosure)?;
1043 let observed = observed?;
1044 if !matches!(
1045 observed.presence,
1046 LeasePresence::Active | LeasePresence::Detached { .. }
1047 ) {
1048 return Err(CoreError::new(
1049 ErrorCode::TargetChanged,
1050 "original owner/runtime is absent; terminal recreation and automatic resume are disabled",
1051 ));
1052 }
1053 owner
1054 } else if let Some((workspace, lease)) = saved {
1055 WorkspaceLease {
1056 client: anchor.lease().client,
1057 workspace,
1058 lease,
1059 runtime: anchor.runtime(),
1060 }
1061 } else {
1062 WorkspaceLease {
1063 client: anchor.lease().client,
1064 workspace: WorkspaceID(new_id()?),
1065 lease: WorkspaceLeaseID(new_id()?),
1066 runtime: anchor.runtime(),
1067 }
1068 };
1069 let mut view = anchor.anchored_workspace(lease)?;
1070 view.bind_connection_permit(permit);
1071 let view = Arc::new(view);
1072 let mut output = completion.lock().unwrap();
1073 if cancel.load(Ordering::SeqCst) || attempt.cancel.load(Ordering::SeqCst) {
1074 drop(output);
1075 view.release_workspace(release_intent)?;
1076 return Err(cancelled());
1077 }
1078 State::publish(&status, ConnectionState::Ready);
1079 output.complete(Ok(view));
1080 Ok(())
1081 })();
1082 if let Err(error) = result {
1083 if !matches!(
1084 status.lock().unwrap().value,
1085 ConnectionState::TargetChanged { .. }
1086 ) {
1087 State::publish(&status, ConnectionState::Failed(error.clone()));
1088 }
1089 completion.lock().unwrap().complete(Err(error));
1090 }
1091 })
1092 }
1093}
1094impl Future for ConnectRequest {
1095 type Output = Result<RemoteEnvironment, CoreError>;
1096 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1097 if self.finished {
1098 return Poll::Ready(Err(CoreError::new(
1099 ErrorCode::Closed,
1100 "connect request already consumed",
1101 )));
1102 }
1103 let registration = self.registration.as_ref().unwrap().clone();
1104 let mut output = registration.lock().unwrap();
1105 if let Some(result) = output.value.take() {
1106 if self.cancel.load(Ordering::SeqCst)
1107 && let Ok(view) = &result
1108 {
1109 let view = view.clone();
1110 let completion = registration.clone();
1111 let intent = if self.resume.is_some() || self.saved.is_some() {
1112 LeaseReleaseIntent::ViewDetachment
1113 } else {
1114 LeaseReleaseIntent::ResolvedWorkspaceClosure
1115 };
1116 if let Err(error) = spawn(&self.workers, move || {
1117 let result = view.release_workspace(intent);
1118 if result.is_ok()
1119 && let Some(owner) = view.physical()
1120 {
1121 run(owner.wait_unused());
1122 }
1123 completion
1124 .lock()
1125 .unwrap()
1126 .complete(Err(result.err().unwrap_or_else(cancelled)));
1127 }) {
1128 output.complete(Err(error));
1129 }
1130 output.waker = Some(cx.waker().clone());
1131 return Poll::Pending;
1132 }
1133 self.finished = true;
1134 self.detach_attempt();
1135 self.transition.take();
1136 let owner_admission = if self.saved.is_some() {
1137 self.resume_admission.take()
1138 } else {
1139 self.resume_admission.take();
1140 None
1141 };
1142 Poll::Ready(result.map(|services| RemoteEnvironment {
1143 locator: self.locator.clone(),
1144 services,
1145 _workers: self.workers.clone(),
1146 _owner_admission: owner_admission,
1147 }))
1148 } else {
1149 output.waker = Some(cx.waker().clone());
1150 Poll::Pending
1151 }
1152 }
1153}
1154impl Drop for ConnectRequest {
1155 fn drop(&mut self) {
1156 self.cancel();
1157 if let Some(output) = &self.registration
1158 && let Some(Ok(view)) = output.lock().unwrap().value.take()
1159 {
1160 let intent = if self.resume.is_some() || self.saved.is_some() {
1161 LeaseReleaseIntent::ViewDetachment
1162 } else {
1163 LeaseReleaseIntent::ResolvedWorkspaceClosure
1164 };
1165 let _ = spawn(&self.workers, move || {
1166 let _ = view.release_workspace(intent);
1167 });
1168 }
1169 }
1170}
1171fn claim_target(
1172 state: &mut ConnectorState,
1173 attempt: &Arc<Attempt>,
1174 target: &TargetIdentity,
1175 locator: &EnvironmentLocator,
1176) -> Result<(), CoreError> {
1177 if state.closed {
1178 return Err(CoreError::new(
1179 ErrorCode::Closed,
1180 "connector is shutting down",
1181 ));
1182 }
1183 state
1184 .retirements
1185 .retain(|record| !record.done.finished.load(Ordering::SeqCst));
1186 state.claims.retain(|claim| {
1187 claim.attempt.upgrade().is_some_and(|a| {
1188 !a.result
1189 .lock()
1190 .unwrap()
1191 .value
1192 .as_ref()
1193 .is_some_and(Result::is_err)
1194 })
1195 });
1196 let conflict = state
1197 .retirements
1198 .iter()
1199 .find(|r| &r.target == target)
1200 .map(|r| &r.locator)
1201 .or_else(|| {
1202 state
1203 .claims
1204 .iter()
1205 .find(|c| &c.target == target && c.attempt.as_ptr() != Arc::as_ptr(attempt))
1206 .map(|c| &c.locator)
1207 });
1208 if let Some(existing) = conflict {
1209 return Err(CoreError::new(
1210 ErrorCode::Busy,
1211 if existing != locator {
1212 "this verified target already uses another SSH profile; use its existing locator or wait for physical retirement before switching authentication policy"
1213 } else {
1214 "wait for the original physical connection to retire before reconnecting"
1215 },
1216 ));
1217 }
1218 if state.claims.len() >= MAX_CONNECTIONS {
1219 return Err(CoreError::new(
1220 ErrorCode::Busy,
1221 "verified target attempt capacity reached",
1222 ));
1223 }
1224 state.claims.push(TargetClaim {
1225 target: target.clone(),
1226 locator: locator.clone(),
1227 attempt: Arc::downgrade(attempt),
1228 });
1229 Ok(())
1230}
1231fn connect_ssh(
1232 attempt: &Arc<Attempt>,
1233 configuration: SshConfiguration,
1234 client: ClientID,
1235 locator: &EnvironmentLocator,
1236 registry: &Arc<Mutex<ConnectorState>>,
1237) -> Result<Arc<RemoteServices>, CoreError> {
1238 let broker = if locator.route == RouteKind::LocalContainer {
1239 None
1240 } else {
1241 Some(AuthenticationBroker::new(configuration.askpass.clone())?)
1242 };
1243 *attempt.broker.lock().unwrap() = broker.clone();
1244 let result = (|| {
1245 let ssh = if locator.route == RouteKind::LocalContainer {
1246 None
1247 } else {
1248 Some(SshConnector::new(
1249 SshProfile {
1250 name: locator.ssh_profile.clone().unwrap(),
1251 config: configuration.config.clone(),
1252 },
1253 broker.clone(),
1254 attempt.cancel.clone(),
1255 )?)
1256 };
1257 let container = if let Some(engine) = locator.engine.as_ref() {
1258 let host = if let Some(ssh) = ssh.as_ref() {
1259 EngineHost::ssh(ssh.clone())?
1260 } else {
1261 EngineHost::local(attempt.cancel.clone())?
1262 };
1263 let client = ContainerEngineClient::new(
1264 host,
1265 engine.clone(),
1266 locator.engine_context.clone(),
1267 locator.effective_user.clone(),
1268 )?;
1269 let selected = client.resolve(locator.container_selector.as_ref().unwrap())?;
1270 if let Some(expected) = attempt.selected_container.as_ref() {
1271 if expected.host_binding != client.host.binding()
1272 || expected.pinned != client.pinned
1273 {
1274 return Err(CoreError::new(
1275 ErrorCode::TargetChanged,
1276 "Selected container engine endpoint/host changed before connect",
1277 ));
1278 }
1279 expected.descriptor.revalidate(&selected)?;
1280 }
1281 let connector = ContainerConnector::new(client, selected)?;
1282 if let Some(expected) = attempt.selected_container.as_ref() {
1283 expected.descriptor.revalidate(&connector.selected)?;
1284 }
1285 Some(connector)
1286 } else {
1287 None
1288 };
1289 let probe = if let Some(container) = container.as_ref() {
1290 container.probe()?
1291 } else {
1292 ssh.as_ref().unwrap().probe()?
1293 };
1294 let container_start = container
1295 .as_ref()
1296 .and_then(|c| c.binding.start_generation.clone());
1297 let platform = Platform::from_probe(&probe)?;
1298 let target = probe.identity.ok_or_else(|| {
1299 unavailable("SSH probe did not establish a supported target identity")
1300 })?;
1301 if let Some(expected) = attempt.expected.as_ref().filter(|e| *e != &target) {
1302 State::publish(
1303 &attempt.state,
1304 ConnectionState::TargetChanged {
1305 expected: expected.clone(),
1306 observed: target,
1307 },
1308 );
1309 return Err(CoreError::new(
1310 ErrorCode::TargetChanged,
1311 "verified SSH target changed before bootstrap",
1312 ));
1313 }
1314 claim_target(&mut registry.lock().unwrap(), attempt, &target, locator)?;
1315 let package = configuration.selected_package(platform, &attempt.cancel)?;
1316 State::publish(&attempt.state, ConnectionState::PreparingHelper);
1317 let bootstrap = if let Some(container) = container {
1318 HelperBootstrapper::container(container, package)?
1319 } else {
1320 HelperBootstrapper::new(ssh.unwrap(), package)?
1321 };
1322 let descriptor = run(bootstrap.ensure_available(target.clone()))?;
1323 let bridge = bootstrap.bridge(&descriptor)?;
1324 State::publish(&attempt.state, ConnectionState::Negotiating);
1325 let lease = WorkspaceLease {
1326 client,
1327 workspace: WorkspaceID(new_id()?),
1328 lease: WorkspaceLeaseID(new_id()?),
1329 runtime: RuntimeGeneration {
1330 supervisor_boot: [0; 16],
1331 container_start,
1332 },
1333 };
1334 let mut remote = RemoteServices::connect(
1335 bridge.channel.stream()?,
1336 target,
1337 lease,
1338 configuration.spool_directory,
1339 )?;
1340 remote.bind_physical(bridge.channel)?;
1341 let owner = remote.physical().unwrap();
1342 if let Err(error) = remote
1343 .set_authentication(bridge.authentication)
1344 .and_then(|_| remote.validate_environment_ready())
1345 {
1346 run(owner.shutdown());
1347 return Err(error);
1348 }
1349 if let Err(error) = remote.release_workspace(LeaseReleaseIntent::ResolvedWorkspaceClosure) {
1351 run(owner.shutdown());
1352 return Err(error);
1353 }
1354 owner.ready();
1355 State::publish(&attempt.state, ConnectionState::Ready);
1356 Ok(Arc::new(remote))
1357 })();
1358 if let Some(broker) = broker {
1359 run(Box::pin(broker.close()));
1360 }
1361 *attempt.broker.lock().unwrap() = None;
1362 *attempt.prompt.lock().unwrap() = None;
1363 result
1364}
1365impl EnvironmentService for EnvironmentConnector {
1366 fn connect(
1367 &self,
1368 locator: EnvironmentLocator,
1369 expected: Option<TargetIdentity>,
1370 ) -> ServiceFuture<'_, Result<ConnectedEnvironment, CoreError>> {
1371 Box::pin(async move {
1372 if locator.route == RouteKind::Local {
1373 return self.local.connect(locator, expected).await;
1374 }
1375 Ok(self.start(locator, expected, None)?.await?.environment())
1376 })
1377 }
1378}
1379impl Drop for EnvironmentConnector {
1380 fn drop(&mut self) {
1381 if Arc::strong_count(&self.handles) == 1 {
1382 self.shutdown();
1383 }
1384 }
1385}
1386
1387#[cfg(test)]
1388#[path = "environment_tests.rs"]
1389mod tests;