Skip to main content

strop_remote/
client.rs

1//! Worker-only requests to the per-endpoint SFTP actor. Cloning a client or
2//! lease never connects; every accepted request retains its own lease.
3use crate::address::{RemoteEndpoint, RemoteFile, RemoteLocation};
4use crate::pool::{EndpointHandle, Job, Registry, Reply, Request, SessionState};
5use crate::{ReadFailureKind, ReadSelection, ReadStage, RemoteReadError, RemoteWindow};
6use std::sync::{mpsc, Arc};
7use std::time::Duration;
8use strop_core::worker::CancelToken;
9
10const CANCEL_POLL: Duration = Duration::from_millis(50);
11
12#[derive(Debug, Clone)]
13pub struct ConnectionLease {
14    pub(crate) inner: Arc<EndpointHandle>,
15}
16impl ConnectionLease {
17    pub fn endpoint(&self) -> &RemoteEndpoint {
18        self.inner.endpoint()
19    }
20    pub fn is_connected(&self) -> bool {
21        !self.inner.stop_signal().signalled() && self.inner.status() == SessionState::Connected
22    }
23}
24
25pub struct RemoteSnapshot {
26    pub file: RemoteFile,
27    pub buffer: strop_core::Buffer,
28    pub window: RemoteWindow,
29    pub connection: ConnectionLease,
30}
31pub struct RemoteDirectorySnapshot {
32    pub directory: RemoteFile,
33    pub entries: Vec<RemoteEntry>,
34    pub connection: ConnectionLease,
35}
36pub enum RemoteResource {
37    File(Box<RemoteSnapshot>),
38    Directory(RemoteDirectorySnapshot),
39}
40#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
41pub struct RemoteEntry {
42    pub file: RemoteFile,
43    pub kind: RemoteEntryKind,
44}
45#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
46pub enum RemoteEntryKind {
47    File,
48    Directory,
49    Other,
50}
51
52#[derive(Clone, Default)]
53pub struct RemoteClient {
54    registry: Arc<Registry>,
55}
56impl RemoteClient {
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    pub fn read(
62        &self,
63        location: &RemoteLocation,
64        selection: ReadSelection,
65        token: &CancelToken,
66    ) -> Result<RemoteSnapshot, RemoteReadError> {
67        match self.request(
68            location.endpoint(),
69            Request::Read {
70                location: location.clone(),
71                selection,
72            },
73            token,
74            false,
75        )? {
76            Reply::Snapshot(snapshot) => Ok(*snapshot),
77            _ => Err(wrong_reply()),
78        }
79    }
80    pub fn open(
81        &self,
82        location: &RemoteLocation,
83        selection: ReadSelection,
84        token: &CancelToken,
85    ) -> Result<RemoteResource, RemoteReadError> {
86        match self.request(
87            location.endpoint(),
88            Request::Open {
89                location: location.clone(),
90                selection,
91            },
92            token,
93            false,
94        )? {
95            Reply::Snapshot(snapshot) => Ok(RemoteResource::File(snapshot)),
96            Reply::Directory(snapshot) => Ok(RemoteResource::Directory(snapshot)),
97            _ => Err(wrong_reply()),
98        }
99    }
100    pub fn list(
101        &self,
102        location: &RemoteLocation,
103        token: &CancelToken,
104    ) -> Result<RemoteDirectorySnapshot, RemoteReadError> {
105        match self.request(
106            location.endpoint(),
107            Request::List {
108                location: location.clone(),
109            },
110            token,
111            false,
112        )? {
113            Reply::Directory(snapshot) => Ok(snapshot),
114            _ => Err(wrong_reply()),
115        }
116    }
117    /// Completion never creates an actor or authenticates. The actor checks the
118    /// same policy again when dequeuing, since the observed connection may die.
119    pub fn list_connected(
120        &self,
121        directory: &RemoteFile,
122        token: &CancelToken,
123    ) -> Result<Vec<RemoteEntry>, RemoteReadError> {
124        match self.request(
125            directory.endpoint(),
126            Request::ListConnected {
127                directory: directory.clone(),
128            },
129            token,
130            true,
131        )? {
132            Reply::Entries(entries) => Ok(entries),
133            _ => Err(wrong_reply()),
134        }
135    }
136    pub fn connect(
137        &self,
138        endpoint: &RemoteEndpoint,
139        token: &CancelToken,
140    ) -> Result<ConnectionLease, RemoteReadError> {
141        match self.request(endpoint, Request::Connect, token, false)? {
142            Reply::Lease(lease) => Ok(lease),
143            _ => Err(wrong_reply()),
144        }
145    }
146    /// Worker-only: revoke admission, then wait for the actor's cleanup result.
147    pub fn disconnect(&self, endpoint: &RemoteEndpoint) -> Result<(), RemoteReadError> {
148        if let Some(handle) = self.registry.remove(endpoint) {
149            handle.wait_stopped()?;
150        }
151        Ok(())
152    }
153    pub fn disconnect_all(&self) -> Result<(), RemoteReadError> {
154        let handles = self.registry.clear();
155        let mut failure = None;
156        for handle in handles {
157            if let Err(error) = handle.wait_stopped() {
158                failure.get_or_insert(error);
159            }
160        }
161        failure.map_or(Ok(()), Err)
162    }
163    /// Observational metadata, not a promise that the next request will succeed.
164    pub fn connections(&self) -> Vec<RemoteEndpoint> {
165        self.registry.connected()
166    }
167
168    fn request(
169        &self,
170        endpoint: &RemoteEndpoint,
171        request: Request,
172        cancel: &CancelToken,
173        connected_only: bool,
174    ) -> Result<Reply, RemoteReadError> {
175        if cancel.is_cancelled() {
176            return Err(cancelled());
177        }
178        let inner = if connected_only {
179            self.registry.connected_handle(endpoint).ok_or_else(|| {
180                RemoteReadError::bare(
181                    ReadStage::Session,
182                    ReadFailureKind::NotConnected,
183                    "completion requires an existing authenticated connection",
184                )
185            })?
186        } else {
187            self.registry.acquire(endpoint)?
188        };
189        let (reply, receiver) = mpsc::channel();
190        let job = Job {
191            lease: ConnectionLease {
192                inner: inner.clone(),
193            },
194            request,
195            cancel: cancel.clone(),
196            reply,
197        };
198        inner.jobs().try_send(job).map_err(|error| match error {
199            mpsc::TrySendError::Full(_) => RemoteReadError::bare(
200                ReadStage::Session,
201                ReadFailureKind::QueueFull,
202                "remote session queue is full",
203            ),
204            mpsc::TrySendError::Disconnected(_) => stopped(),
205        })?;
206        loop {
207            if cancel.is_cancelled() {
208                return Err(cancelled());
209            }
210            if inner.stop_signal().signalled() {
211                return Err(stopped());
212            }
213            match receiver.recv_timeout(CANCEL_POLL) {
214                Ok(result) => return result,
215                Err(mpsc::RecvTimeoutError::Timeout) => {}
216                Err(mpsc::RecvTimeoutError::Disconnected) => return Err(stopped()),
217            }
218        }
219    }
220}
221fn cancelled() -> RemoteReadError {
222    RemoteReadError::bare(
223        ReadStage::Session,
224        ReadFailureKind::Cancelled,
225        "remote request cancelled",
226    )
227}
228fn stopped() -> RemoteReadError {
229    RemoteReadError::bare(
230        ReadStage::Session,
231        ReadFailureKind::Stopped,
232        "remote session stopped before delivering a result",
233    )
234}
235fn wrong_reply() -> RemoteReadError {
236    RemoteReadError::bare(
237        ReadStage::Session,
238        ReadFailureKind::Protocol,
239        "session reply does not match the accepted request",
240    )
241}