1use super::client::{DesktopLink, LinkPoll, Me, PlmClient, PlmError};
21use super::transport::EhttpTransport;
22use super::PlmTransport;
23use std::cell::RefCell;
24use std::rc::Rc;
25
26#[derive(Clone, Debug, PartialEq)]
28pub enum Status {
29 Connected { url: String, username: String },
31 NotConnected { url: String, reason: Option<String> },
35}
36
37#[derive(Clone, Debug, PartialEq)]
39pub enum Outcome {
40 Done(String),
42 Failed(String),
44}
45
46#[derive(Clone, Debug, PartialEq)]
49pub struct Session {
50 pub status: Status,
51 pub keep: Keep,
52}
53
54#[derive(Clone, Debug, PartialEq)]
57pub struct UrlSetting {
58 pub dir: std::path::PathBuf,
60 pub url: Option<String>,
63 pub source: Option<super::config::Source>,
65}
66
67#[derive(Clone, Debug, PartialEq)]
70pub struct UrlSaved {
71 pub url: String,
72 pub token_dropped: bool,
73}
74
75pub fn parse_label(label: &str) -> Option<(String, String)> {
78 let rest = label.strip_prefix("PLM ")?;
79 let (url, user) = rest.rsplit_once(" as ")?;
80 Some((url.to_string(), user.to_string()))
81}
82
83#[derive(Clone, Debug, PartialEq)]
86pub enum Keep {
87 Files(std::path::PathBuf),
88 BrowserSession,
89}
90
91pub struct PlmConnection {
92 pub status: Status,
93 keep: Keep,
94 pub token_input: String,
95 pub username: String,
96 pub password: String,
97 pending: Option<Rc<RefCell<Option<Outcome>>>>,
98 pub last: Option<Outcome>,
99 reload: bool,
101 connectable: bool,
104 transport: Rc<dyn Fn(&str) -> Rc<dyn PlmTransport>>,
107 registering: Option<Slot<Result<DesktopLink, PlmError>>>,
109 pub link: Option<BrowserLink>,
111 open_request: Option<String>,
115 clock: Rc<dyn Fn() -> f64>,
117 fresh: Slot<(String, String)>,
120 signed_out_here: Slot<()>,
123}
124
125type Slot<T> = Rc<RefCell<Option<T>>>;
126
127const LINK_EXPIRED: &str = "the browser sign-in expired before it was approved — start it again";
129
130const MAX_BACKOFF: f64 = 30.0;
132
133pub struct BrowserLink {
136 pub code: String,
137 pub url: String,
138 pub note: Option<String>,
140 secret: String,
141 poller: LinkPoller,
142 in_flight: Option<Slot<Result<LinkPoll, PlmError>>>,
143}
144
145#[derive(Clone, Debug, PartialEq)]
148pub struct LinkPoller {
149 expires_at: f64,
150 interval: f64,
151 next_at: f64,
152 failures: u32,
153}
154
155#[derive(Clone, Debug, PartialEq)]
157pub enum LinkStep {
158 Wait { note: Option<String> },
160 Approved(String),
162 Ended(String),
164}
165
166impl LinkPoller {
167 pub fn new(now: f64, expires_in: u64, interval: u64) -> Self {
168 let interval = interval.max(1) as f64;
169 Self { expires_at: now + expires_in as f64, interval, next_at: now + interval, failures: 0 }
170 }
171
172 pub fn expired(&self, now: f64) -> bool {
173 now >= self.expires_at
174 }
175
176 pub fn due(&self, now: f64) -> bool {
178 !self.expired(now) && now >= self.next_at
179 }
180
181 pub fn wait(&self, now: f64) -> f64 {
183 (self.next_at.min(self.expires_at) - now).max(0.0)
184 }
185
186 pub fn answer(&mut self, now: f64, answer: Result<LinkPoll, PlmError>) -> LinkStep {
191 match answer {
192 Ok(LinkPoll::Approved(token)) => LinkStep::Approved(token),
193 Ok(LinkPoll::Denied) => LinkStep::Ended("the sign-in was denied in the browser".into()),
194 Ok(LinkPoll::Gone) => LinkStep::Ended(LINK_EXPIRED.into()),
195 Ok(LinkPoll::Pending) => {
196 self.failures = 0;
197 self.next_at = now + self.interval;
198 self.wait_or_end(now, None)
199 }
200 Err(error) if passing(&error) => {
201 self.failures += 1;
202 let wait = (self.interval * 2f64.powi(self.failures as i32)).min(MAX_BACKOFF);
203 self.next_at = now + wait;
204 self.wait_or_end(now, Some(format!("{error} — trying again")))
205 }
206 Err(error) => LinkStep::Ended(error.to_string()),
207 }
208 }
209
210 fn wait_or_end(&self, now: f64, note: Option<String>) -> LinkStep {
211 if self.expired(now) {
212 LinkStep::Ended(LINK_EXPIRED.into())
213 } else {
214 LinkStep::Wait { note }
215 }
216 }
217}
218
219fn passing(error: &PlmError) -> bool {
222 match error {
223 PlmError::Unreachable(_) | PlmError::Throttled(_) => true,
224 PlmError::Refused { status, .. } => *status >= 500,
225 _ => false,
226 }
227}
228
229impl PlmConnection {
230 pub fn new(status: Status, keep: Keep) -> Self {
231 Self {
232 status,
233 keep,
234 token_input: String::new(),
235 username: String::new(),
236 password: String::new(),
237 pending: None,
238 last: None,
239 reload: false,
240 connectable: false,
241 transport: Rc::new(|url: &str| Rc::new(EhttpTransport::new(url.to_string())) as Rc<dyn PlmTransport>),
242 registering: None,
243 link: None,
244 open_request: None,
245 clock: Rc::new(monotonic_seconds),
246 fresh: Rc::default(),
247 signed_out_here: Rc::default(),
248 }
249 }
250
251 pub fn url(&self) -> &str {
252 match &self.status {
253 Status::Connected { url, .. } | Status::NotConnected { url, .. } => url,
254 }
255 }
256
257 pub fn keep(&self) -> &Keep {
258 &self.keep
259 }
260
261 pub fn busy(&self) -> bool {
263 self.pending.is_some()
264 }
265
266 pub fn poll(&mut self) -> bool {
270 let polling = self.link.as_ref().is_some_and(|l| l.in_flight.is_some());
271 if self.pending.is_none() && self.registering.is_none() && !polling && self.link.is_none() {
272 return false;
273 }
274 #[cfg(not(target_arch = "wasm32"))]
275 if self.pending.is_some() || self.registering.is_some() || polling {
276 super::native::run_pending();
277 }
278 let mut changed = self.poll_link();
279 let Some(slot) = &self.pending else { return changed };
280 let Some(outcome) = slot.borrow_mut().take() else { return changed };
281 changed = true;
282 self.pending = None;
283 if let (Keep::BrowserSession, Outcome::Done(_)) = (&self.keep, &outcome) {
284 self.reload = true;
285 }
286 self.connectable = matches!(self.keep, Keep::Files(_)) && matches!(outcome, Outcome::Done(_));
289 if let (Status::NotConnected { url, .. }, Outcome::Failed(why)) = (&self.status, &outcome) {
290 self.status = Status::NotConnected { url: url.clone(), reason: Some(why.clone()) };
291 }
292 self.last = Some(outcome);
293 changed
294 }
295
296 fn poll_link(&mut self) -> bool {
301 let mut changed = false;
302 if let Some(answer) = self.registering.as_ref().and_then(|slot| slot.borrow_mut().take()) {
303 self.registering = None;
304 changed = true;
305 match answer {
306 Ok(link) => self.open_link(link),
307 Err(error) => self.last = Some(Outcome::Failed(format!("could not start the browser sign-in: {error}"))),
308 }
309 }
310 let now = (self.clock)();
311 let Some(link) = self.link.as_mut() else { return changed };
312 let answered = link.in_flight.as_ref().and_then(|slot| slot.borrow_mut().take());
313 let step = match answered {
314 Some(answer) => {
315 link.in_flight = None;
316 Some(link.poller.answer(now, answer))
317 }
318 None if link.in_flight.is_none() && link.poller.expired(now) => Some(LinkStep::Ended(LINK_EXPIRED.into())),
319 None => None,
320 };
321 match step {
322 Some(LinkStep::Wait { note }) => {
323 link.note = note;
324 changed = true;
325 }
326 Some(LinkStep::Approved(token)) => {
327 self.link = None;
328 self.token_input = token;
329 self.use_token();
330 return true;
331 }
332 Some(LinkStep::Ended(why)) => {
333 self.link = None;
334 self.connectable = false;
335 self.last = Some(Outcome::Failed(why));
336 return true;
337 }
338 None => {}
339 }
340 if link.in_flight.is_none() && link.poller.due(now) {
341 let slot: Slot<Result<LinkPoll, PlmError>> = Rc::default();
342 link.in_flight = Some(slot.clone());
343 let secret = link.secret.clone();
344 let client = self.client();
345 spawn(async move {
346 let answer = client.poll_desktop_link(&secret).await;
347 *slot.borrow_mut() = Some(answer);
348 });
349 changed = true;
350 }
351 changed
352 }
353
354 fn open_link(&mut self, link: DesktopLink) {
355 let url = format!("{}{}", self.url().trim_end_matches('/'), link.path);
356 self.open_request = Some(url.clone());
357 let poller = LinkPoller::new((self.clock)(), link.expires_in, link.interval);
358 self.link = Some(BrowserLink { code: link.code, url, note: None, secret: link.poll_secret, poller, in_flight: None });
359 }
360
361 pub fn take_signed_out_here(&mut self) -> bool {
363 self.signed_out_here.borrow_mut().take().is_some()
364 }
365
366 pub fn take_fresh_token(&mut self) -> Option<(String, String)> {
368 self.fresh.borrow_mut().take()
369 }
370
371 pub fn take_open_request(&mut self) -> Option<String> {
373 self.open_request.take()
374 }
375
376 pub fn link_wait(&self) -> Option<f64> {
379 let link = self.link.as_ref()?;
380 Some(if link.in_flight.is_some() { 0.25 } else { link.poller.wait((self.clock)()) })
381 }
382
383 pub fn sign_in_with_browser(&mut self) {
388 let client = self.client();
389 let slot: Slot<Result<DesktopLink, PlmError>> = Rc::default();
390 self.registering = Some(slot.clone());
391 self.link = None;
392 self.last = None;
393 let (host, os) = machine();
394 spawn(async move {
395 let answer = client.register_desktop_link(&host, &os).await;
396 *slot.borrow_mut() = Some(answer);
397 });
398 }
399
400 pub fn cancel_link(&mut self) {
403 self.registering = None;
404 self.link = None;
405 self.open_request = None;
406 }
407
408 pub fn linking(&self) -> bool {
410 self.registering.is_some() || self.link.is_some()
411 }
412
413 pub fn connectable(&self) -> bool {
415 self.connectable
416 }
417
418 pub fn reconnected(&mut self, outcome: Result<String, String>) {
421 self.connectable = outcome.is_err() && self.connectable;
422 self.last = Some(match outcome {
423 Ok(text) => Outcome::Done(text),
424 Err(why) => Outcome::Failed(why),
425 });
426 }
427
428 pub fn set_status(&mut self, status: Status) {
430 if std::mem::discriminant(&status) != std::mem::discriminant(&self.status) {
431 self.status = status;
432 self.connectable = false;
433 }
434 }
435
436 pub fn take_reload(&mut self) -> bool {
438 std::mem::take(&mut self.reload)
439 }
440
441 fn client(&self) -> Rc<PlmClient> {
442 Rc::new(PlmClient::new((self.transport)(self.url())))
443 }
444
445 fn start(&mut self, work: impl std::future::Future<Output = Outcome> + 'static) {
446 let slot: Rc<RefCell<Option<Outcome>>> = Rc::default();
447 let into = slot.clone();
448 self.pending = Some(slot);
449 self.last = None;
450 spawn(async move {
451 let outcome = work.await;
452 *into.borrow_mut() = Some(outcome);
453 });
454 }
455
456 pub fn use_token(&mut self) {
459 self.cancel_link();
460 let token = self.token_input.trim().to_string();
461 let (client, keep, fresh) = (self.client(), self.keep.clone(), self.fresh.clone());
462 self.token_input.clear();
463 self.start(async move {
464 match client.sign_in_with_token(&token).await {
465 Ok(me) => keep_token(&keep, &token, &me, &fresh),
466 Err(e) => Outcome::Failed(e.to_string()),
467 }
468 });
469 }
470
471 pub fn use_password(&mut self) {
474 self.cancel_link();
475 let (username, password) = (self.username.trim().to_string(), std::mem::take(&mut self.password));
476 let (client, keep, fresh) = (self.client(), self.keep.clone(), self.fresh.clone());
477 self.start(async move {
478 match keep {
479 Keep::Files(_) => match client.sign_in_for_token(&username, &password, &token_name()).await {
480 Ok((me, token)) => keep_token(&keep, &token, &me, &fresh),
481 Err(e) => Outcome::Failed(e.to_string()),
482 },
483 Keep::BrowserSession => match client.sign_in_with_password(&username, &password).await {
484 Ok(me) => Outcome::Done(format!("signed in as {}; reloading onto the PLM", me.username)),
485 Err(e) => Outcome::Failed(e.to_string()),
486 },
487 }
488 });
489 }
490
491 pub fn check(&mut self) {
494 let (client, keep) = (self.client(), self.keep.clone());
495 self.start(async move {
496 let signed_in = match &keep {
497 Keep::Files(dir) => match held_token(dir) {
498 Ok(Some(token)) => client.sign_in_with_token(&token).await,
499 Ok(None) => return Outcome::Failed("no token on this machine: paste one, or sign in with a password".into()),
500 Err(e) => return Outcome::Failed(e),
501 },
502 Keep::BrowserSession => client.sign_in_with_browser_session().await,
503 };
504 match signed_in {
505 Ok(me) => Outcome::Done(format!("the server accepts this machine as {} and serves this app", me.username)),
506 Err(e) => Outcome::Failed(e.to_string()),
507 }
508 });
509 }
510
511 pub fn forget(&mut self) {
515 match self.keep.clone() {
516 Keep::Files(dir) => {
517 self.connectable = false;
518 self.cancel_link();
519 let held = held_token(&dir).ok().flatten();
524 let client = self.client();
525 let signed_out_here = self.signed_out_here.clone();
526 self.start(async move {
527 let told: Result<bool, String> = match held {
531 Some(token) => {
532 client.replace_token(&token);
533 match client.raw("POST", "/api/desktop/sign-out", Some(b"{}".to_vec())).await {
534 Ok(answer) if answer.status == 200 => Ok(serde_json::from_slice::<serde_json::Value>(&answer.body)
535 .is_ok_and(|v| v["revoked"] == true)),
536 Ok(answer) if answer.status == 401 => Ok(true),
537 Ok(answer) => Err(format!("the server answered {}", answer.status)),
538 Err(e) => Err(e.to_string()),
539 }
540 }
541 None => Ok(false),
542 };
543 if told == Ok(true) {
544 *signed_out_here.borrow_mut() = Some(());
545 }
546 match (forget_files(&dir), told) {
547 (Err(e), _) => Outcome::Failed(e),
548 (Ok(()), Ok(true)) => Outcome::Done("signed out: this machine's PLM sign-in is forgotten and ended on the server; the next start is the file app".into()),
549 (Ok(()), Ok(false)) => Outcome::Done("this machine's PLM sign-in is forgotten; the next start is the file app".into()),
550 (Ok(()), Err(why)) => Outcome::Done(format!(
551 "this machine's PLM sign-in is forgotten, but the server could not be told ({why}): revoke it under Linked desktops on your PLM account page"
552 )),
553 }
554 });
555 }
556 Keep::BrowserSession => {
557 let client = self.client();
558 self.start(async move {
559 match client.raw("POST", "/api/logout", Some(b"{}".to_vec())).await {
560 Ok(_) => Outcome::Done("signed out; reloading".into()),
561 Err(e) => Outcome::Failed(e.to_string()),
562 }
563 });
564 }
565 }
566 }
567}
568
569fn token_name() -> String {
571 #[cfg(not(target_arch = "wasm32"))]
572 {
573 format!("brep-app {}", machine().0)
574 }
575 #[cfg(target_arch = "wasm32")]
576 {
577 "brep-app web".into()
578 }
579}
580
581fn machine() -> (String, String) {
584 let named = |text: String| Some(text.trim().to_string()).filter(|t| !t.is_empty());
585 let host = std::env::var("HOSTNAME")
586 .ok()
587 .and_then(named)
588 .or_else(|| std::env::var("COMPUTERNAME").ok().and_then(named));
589 #[cfg(not(target_arch = "wasm32"))]
590 let host = host.or_else(|| std::fs::read_to_string("/etc/hostname").ok().and_then(named));
591 (host.unwrap_or_else(|| "desktop".into()), os_name(std::env::consts::OS).to_string())
592}
593
594fn os_name(os: &str) -> &str {
596 match os {
597 "windows" => "Windows",
598 "linux" => "Linux",
599 "macos" => "macOS",
600 other => other,
601 }
602}
603
604fn monotonic_seconds() -> f64 {
606 thread_local! {
607 static START: web_time::Instant = web_time::Instant::now();
608 }
609 START.with(|start| start.elapsed().as_secs_f64())
610}
611
612fn keep_token(keep: &Keep, token: &str, me: &Me, fresh: &Slot<(String, String)>) -> Outcome {
613 match keep {
614 Keep::Files(dir) => match write_token(dir, token) {
615 Ok(()) => Outcome::Done({
616 *fresh.borrow_mut() = Some((me.username.clone(), token.to_string()));
617 format!(
618 "signed in as {}; the token is kept on this machine. Connect now switches this session to the PLM; otherwise the next start does",
619 me.username
620 )
621 }),
622 Err(e) => Outcome::Failed(format!("signed in, but the token could not be kept: {e}")),
623 },
624 Keep::BrowserSession => Outcome::Done(format!("signed in as {}; reloading onto the PLM", me.username)),
625 }
626}
627
628#[cfg(not(target_arch = "wasm32"))]
629fn write_token(dir: &std::path::Path, token: &str) -> Result<(), String> {
630 super::config::write_token(dir, token)
631}
632
633#[cfg(target_arch = "wasm32")]
634fn write_token(_: &std::path::Path, _: &str) -> Result<(), String> {
635 Err("a browser keeps no token file".into())
636}
637
638#[cfg(not(target_arch = "wasm32"))]
639fn held_token(dir: &std::path::Path) -> Result<Option<String>, String> {
640 Ok(super::config::resolve(dir, &|_| None, &Default::default())?.and_then(|c| c.token))
641}
642
643#[cfg(target_arch = "wasm32")]
644fn held_token(_: &std::path::Path) -> Result<Option<String>, String> {
645 Ok(None)
646}
647
648#[cfg(not(target_arch = "wasm32"))]
649fn forget_files(dir: &std::path::Path) -> Result<(), String> {
650 super::config::forget(dir).map(|_| ())
651}
652
653#[cfg(target_arch = "wasm32")]
654fn forget_files(_: &std::path::Path) -> Result<(), String> {
655 Err("a browser keeps no PLM files".into())
656}
657
658fn spawn(task: impl std::future::Future<Output = ()> + 'static) {
659 crate::store::spawn_waking(task);
660}
661