Skip to main content

buntrail_engine/
environment.rs

1//! Remote composition belongs in Engine; transports and process ownership remain in Services.
2mod 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/// Immutable application-owned artifacts. Missing platform packages are unavailable, never substituted.
265#[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    /// Called only after verified platform selection on the existing connection worker.
276    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        // Once physical ownership is published, request retirement must not cancel its live channel.
426        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
470/// Native consumers retain this handle; prompt IDs are meaningful only within `attempt`.
471pub 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    // Independent saved/resume/registration/container identities must remain explicit at admission.
622    #[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, &registry);
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        // Metadata is retained by the result mailbox, never encoded into a route or global selection.
848        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        // This anchor cannot launch work; each dependent gets its own registered lease.
1350        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;