1use crate::pool::{EndpointHandle, Job, Registry, Reply, Request, SessionState};
4use crate::{ReadFailureKind, ReadSelection, ReadStage, RemoteReadError, RemoteWindow};
5use std::sync::{mpsc, Arc};
6use std::time::Duration;
7use strop_core::worker::CancelToken;
8use strop_workspace::{RemoteEndpoint, RemoteFile, RemoteLocation};
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 pub permissions: Option<RemotePermissions>,
45 pub size: Option<crate::RemoteSize>,
47}
48#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
49pub enum RemoteEntryKind {
50 File,
51 Directory,
52 SymbolicLink,
53 Fifo,
54 Socket,
55 BlockDevice,
56 CharacterDevice,
57 Unknown,
58}
59
60impl RemoteEntryKind {
61 pub const fn marker(self) -> char {
62 match self {
63 Self::File => '-',
64 Self::Directory => 'd',
65 Self::SymbolicLink => 'l',
66 Self::Fifo => 'p',
67 Self::Socket => 's',
68 Self::BlockDevice => 'b',
69 Self::CharacterDevice => 'c',
70 Self::Unknown => '?',
71 }
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
77#[serde(try_from = "u16", into = "u16")]
78pub struct RemotePermissions(u16);
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
81#[error("permission bits exceed POSIX access and special bits")]
82pub struct PermissionBitsError;
83
84impl RemotePermissions {
85 pub const fn new(bits: u16) -> Result<Self, PermissionBitsError> {
86 if bits & !0o7777 != 0 {
87 Err(PermissionBitsError)
88 } else {
89 Ok(Self(bits))
90 }
91 }
92 pub(crate) const fn from_mode(mode: u32) -> Self {
93 Self((mode & 0o7777) as u16)
94 }
95 pub const fn bits(self) -> u16 {
96 self.0
97 }
98}
99impl TryFrom<u16> for RemotePermissions {
100 type Error = PermissionBitsError;
101 fn try_from(bits: u16) -> Result<Self, Self::Error> {
102 Self::new(bits)
103 }
104}
105impl From<RemotePermissions> for u16 {
106 fn from(value: RemotePermissions) -> Self {
107 value.bits()
108 }
109}
110impl std::fmt::Display for RemotePermissions {
111 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 for (read, write, execute, special, lower, upper) in [
113 (0o400, 0o200, 0o100, 0o4000, 's', 'S'),
114 (0o040, 0o020, 0o010, 0o2000, 's', 'S'),
115 (0o004, 0o002, 0o001, 0o1000, 't', 'T'),
116 ] {
117 let r = if self.0 & read != 0 { 'r' } else { '-' };
118 let w = if self.0 & write != 0 { 'w' } else { '-' };
119 let x = match (self.0 & execute != 0, self.0 & special != 0) {
120 (true, true) => lower,
121 (false, true) => upper,
122 (true, false) => 'x',
123 (false, false) => '-',
124 };
125 write!(formatter, "{r}{w}{x}")?;
126 }
127 Ok(())
128 }
129}
130
131#[derive(Clone, Default)]
132pub struct RemoteClient {
133 registry: Arc<Registry>,
134}
135impl RemoteClient {
136 pub fn new() -> Self {
137 Self::default()
138 }
139
140 pub fn read(
141 &self,
142 location: &RemoteLocation,
143 selection: ReadSelection,
144 token: &CancelToken,
145 ) -> Result<RemoteSnapshot, RemoteReadError> {
146 match self.request(
147 location.endpoint(),
148 Request::Read {
149 location: location.clone(),
150 selection,
151 },
152 token,
153 false,
154 )? {
155 Reply::Snapshot(snapshot) => Ok(*snapshot),
156 _ => Err(wrong_reply()),
157 }
158 }
159 pub fn open(
160 &self,
161 location: &RemoteLocation,
162 selection: ReadSelection,
163 token: &CancelToken,
164 ) -> Result<RemoteResource, RemoteReadError> {
165 match self.request(
166 location.endpoint(),
167 Request::Open {
168 location: location.clone(),
169 selection,
170 },
171 token,
172 false,
173 )? {
174 Reply::Snapshot(snapshot) => Ok(RemoteResource::File(snapshot)),
175 Reply::Directory(snapshot) => Ok(RemoteResource::Directory(snapshot)),
176 _ => Err(wrong_reply()),
177 }
178 }
179 pub fn list(
180 &self,
181 location: &RemoteLocation,
182 token: &CancelToken,
183 ) -> Result<RemoteDirectorySnapshot, RemoteReadError> {
184 match self.request(
185 location.endpoint(),
186 Request::List {
187 location: location.clone(),
188 },
189 token,
190 false,
191 )? {
192 Reply::Directory(snapshot) => Ok(snapshot),
193 _ => Err(wrong_reply()),
194 }
195 }
196 pub fn list_connected(
199 &self,
200 directory: &RemoteFile,
201 token: &CancelToken,
202 ) -> Result<Vec<RemoteEntry>, RemoteReadError> {
203 match self.request(
204 directory.endpoint(),
205 Request::ListConnected {
206 directory: directory.clone(),
207 },
208 token,
209 true,
210 )? {
211 Reply::Entries(entries) => Ok(entries),
212 _ => Err(wrong_reply()),
213 }
214 }
215 pub fn connect(
216 &self,
217 endpoint: &RemoteEndpoint,
218 token: &CancelToken,
219 ) -> Result<ConnectionLease, RemoteReadError> {
220 match self.request(endpoint, Request::Connect, token, false)? {
221 Reply::Lease(lease) => Ok(lease),
222 _ => Err(wrong_reply()),
223 }
224 }
225 pub fn disconnect(&self, endpoint: &RemoteEndpoint) -> Result<(), RemoteReadError> {
227 if let Some(handle) = self.registry.remove(endpoint) {
228 handle.wait_stopped()?;
229 }
230 Ok(())
231 }
232 pub fn disconnect_all(&self) -> Result<(), RemoteReadError> {
233 let handles = self.registry.clear();
234 let mut failure = None;
235 for handle in handles {
236 if let Err(error) = handle.wait_stopped() {
237 failure.get_or_insert(error);
238 }
239 }
240 failure.map_or(Ok(()), Err)
241 }
242 pub fn connections(&self) -> Vec<RemoteEndpoint> {
244 self.registry.connected()
245 }
246
247 fn request(
248 &self,
249 endpoint: &RemoteEndpoint,
250 request: Request,
251 cancel: &CancelToken,
252 connected_only: bool,
253 ) -> Result<Reply, RemoteReadError> {
254 if cancel.is_cancelled() {
255 return Err(cancelled());
256 }
257 let inner = if connected_only {
258 self.registry.connected_handle(endpoint).ok_or_else(|| {
259 RemoteReadError::bare(
260 ReadStage::Session,
261 ReadFailureKind::NotConnected,
262 "completion requires an existing authenticated connection",
263 )
264 })?
265 } else {
266 self.registry.acquire(endpoint)?
267 };
268 let (reply, receiver) = mpsc::channel();
269 let job = Job {
270 lease: ConnectionLease {
271 inner: inner.clone(),
272 },
273 request,
274 cancel: cancel.clone(),
275 reply,
276 };
277 inner.jobs().try_send(job).map_err(|error| match error {
278 mpsc::TrySendError::Full(_) => RemoteReadError::bare(
279 ReadStage::Session,
280 ReadFailureKind::QueueFull,
281 "remote session queue is full",
282 ),
283 mpsc::TrySendError::Disconnected(_) => stopped(),
284 })?;
285 loop {
286 if cancel.is_cancelled() {
287 return Err(cancelled());
288 }
289 if inner.stop_signal().signalled() {
290 return Err(stopped());
291 }
292 match receiver.recv_timeout(CANCEL_POLL) {
293 Ok(result) => return result,
294 Err(mpsc::RecvTimeoutError::Timeout) => {}
295 Err(mpsc::RecvTimeoutError::Disconnected) => return Err(stopped()),
296 }
297 }
298 }
299}
300fn cancelled() -> RemoteReadError {
301 RemoteReadError::bare(
302 ReadStage::Session,
303 ReadFailureKind::Cancelled,
304 "remote request cancelled",
305 )
306}
307fn stopped() -> RemoteReadError {
308 RemoteReadError::bare(
309 ReadStage::Session,
310 ReadFailureKind::Stopped,
311 "remote session stopped before delivering a result",
312 )
313}
314fn wrong_reply() -> RemoteReadError {
315 RemoteReadError::bare(
316 ReadStage::Session,
317 ReadFailureKind::Protocol,
318 "session reply does not match the accepted request",
319 )
320}