1use std::collections::HashSet;
17use std::path::PathBuf;
18use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
19use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
20use std::thread;
21use std::time::{Duration, Instant, SystemTime};
22
23use moonbase::version::update_available;
24use moonbase::{
25 ActivationMethod, ActivationRequest, DownloadTarget, Error as SdkError, FileLicenseStore, License, LicenseStore,
26 Licensing, Platform, ReleaseAccess, ReleaseNotCovered, can_download,
27};
28use tauri::{AppHandle, Emitter, Manager, Runtime};
29
30use crate::error::{Error, Result};
31use crate::models::{
32 AddOnsPhase, AddOnsState, DeviceInfo, Failure, LicenseState, LicenseView, LockReason, NotCovered,
33 PendingActivation, Status, UpdateHint, UpdateInfo,
34};
35use crate::ui::Ui;
36
37pub const STATE_CHANGED_EVENT: &str = "moonbase://state-changed";
39
40const POLL_INTERVAL: Duration = Duration::from_secs(2);
43const WATCH_INTERVAL: Duration = Duration::from_secs(2);
44
45const PURCHASE_WINDOW: Duration = Duration::from_secs(60 * 60);
48
49pub(crate) struct Settings {
50 pub grace_period: Duration,
51 pub check_interval: Duration,
52 pub application_version: Option<String>,
53 pub open_browser: bool,
54 pub auto_activate_sub_products: bool,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59struct FileStamp {
60 exists: bool,
61 len: u64,
62 modified: Option<SystemTime>,
63}
64
65#[derive(Debug, Clone)]
67struct NotCoveredLicense {
68 license: License,
69 app_version: String,
70 release_access: Option<ReleaseAccess>,
71 upgrade_url: Option<String>,
72}
73
74impl NotCoveredLicense {
75 fn from_refusal(refusal: &ReleaseNotCovered, fallback: &License) -> Self {
76 let license = refusal.license().cloned().unwrap_or_else(|| fallback.clone());
77 Self {
78 app_version: refusal.app_version().to_owned(),
79 release_access: refusal.release_access().cloned().or_else(|| license.release_access.clone()),
80 upgrade_url: refusal.upgrade_url().map(str::to_owned),
81 license,
82 }
83 }
84
85 fn view(&self) -> NotCovered {
86 NotCovered {
87 license: LicenseView::from(&self.license),
88 app_version: self.app_version.clone(),
89 release_access: self.release_access.clone(),
90 upgrade_url: self.upgrade_url.clone(),
91 }
92 }
93}
94
95#[derive(Debug, Clone)]
98enum Standing {
99 Unlicensed(Option<LockReason>),
101 Licensed(License),
103 TrialExpired(License),
105 NotCovered(NotCoveredLicense),
107}
108
109impl Default for Standing {
110 fn default() -> Self {
111 Self::Unlicensed(None)
112 }
113}
114
115impl Standing {
116 fn license(&self) -> Option<&License> {
118 match self {
119 Self::Licensed(license) => Some(license),
120 _ => None,
121 }
122 }
123
124 fn held(&self) -> Option<&License> {
127 match self {
128 Self::Licensed(license) => Some(license),
129 Self::NotCovered(not_covered) => Some(¬_covered.license),
130 Self::Unlicensed(_) | Self::TrialExpired(_) => None,
131 }
132 }
133
134 fn not_covered(&self) -> Option<&NotCoveredLicense> {
135 match self {
136 Self::NotCovered(not_covered) => Some(not_covered),
137 _ => None,
138 }
139 }
140
141 fn lock_reason(&self) -> Option<LockReason> {
142 match self {
143 Self::Unlicensed(reason) => *reason,
144 Self::Licensed(_) => None,
145 Self::TrialExpired(_) => Some(LockReason::Expired),
146 Self::NotCovered(_) => Some(LockReason::NotCovered),
147 }
148 }
149}
150
151#[derive(Default)]
152struct Inner {
153 loading: bool,
154 standing: Standing,
155 activation: Option<PendingActivation>,
157 pending: Option<ActivationRequest>,
158 deactivating: Option<u64>,
160 error: Option<Failure>,
161 refreshes_in_flight: usize,
162 last_grace_check: Option<Instant>,
163 file_stamp: Option<FileStamp>,
164 update_upgrade_url: Option<String>,
166
167 add_ons: AddOnsState,
168 add_ons_generation: u64,
171 add_on_calls: u64,
173 add_on_call: u64,
174 lapses_checked: HashSet<String>,
176 purchase_opened_at: Option<Instant>,
178 left_for_purchase: Option<Instant>,
180 countdown: (Vec<SystemTime>, Vec<u64>),
183}
184
185struct Shared<R: Runtime> {
186 app: AppHandle<R>,
187 licensing: Arc<Licensing>,
188 store: FileLicenseStore,
189 settings: Settings,
190 inner: Mutex<Inner>,
191 generation: AtomicU64,
192 licensed: AtomicBool,
193 shutdown: AtomicBool,
194 emit_lock: Mutex<()>,
197 online_calls: Mutex<()>,
202 emitted_sub_products: Mutex<Vec<String>>,
204 ui: Option<Ui>,
206}
207
208pub struct Moonbase<R: Runtime>(Arc<Shared<R>>);
217
218impl<R: Runtime> Clone for Moonbase<R> {
219 fn clone(&self) -> Self {
220 Self(self.0.clone())
221 }
222}
223
224enum Activity {
225 StartActivation,
226 Activation,
227 Deactivation,
228 OfflineActivation,
229}
230
231fn busy_status(status: u16) -> bool {
232 status == 408 || status == 429 || status >= 500
233}
234
235fn failure(activity: Activity, error: &SdkError) -> Failure {
236 let message = match (activity, error) {
237 (_, SdkError::InsufficientDeviceIdentity { .. }) => {
238 "This computer can't be identified, so it can't be activated. Contact the developer for help.".into()
239 }
240 (Activity::Deactivation, SdkError::StoreClosed(_)) => {
241 "This store has closed, so this computer can't be deactivated.".into()
242 }
243 (_, SdkError::StoreClosed(_)) => "This store has closed, so activation isn't available.".into(),
244
245 (Activity::StartActivation, SdkError::Api(api)) if busy_status(api.status_code()) => {
246 "Moonbase couldn't start activation right now. Try again in a minute.".into()
247 }
248 (Activity::StartActivation, SdkError::Api(_)) => {
249 "Couldn't reach Moonbase to start activation. Check your internet connection and try again.".into()
250 }
251 (Activity::StartActivation, error) if error.is_license_rejection() => {
252 "Activation isn't available for this product right now. If this keeps happening, contact the developer."
253 .into()
254 }
255
256 (Activity::Activation, SdkError::ActivationRequestEnded(_)) => {
257 "This activation expired or was cancelled. Activate again to continue.".into()
258 }
259 (Activity::Activation, SdkError::LicenseExpired(_)) => {
260 "This license has expired, so it can't be activated.".into()
261 }
262 (Activity::Activation, error) if error.is_license_invalid() => {
263 "Activation was rejected. If this keeps happening, contact the developer.".into()
264 }
265
266 (Activity::Deactivation, SdkError::Api(api)) if busy_status(api.status_code()) => {
267 "Moonbase couldn't deactivate right now. Try again in a minute.".into()
268 }
269 (Activity::Deactivation, SdkError::Api(_)) => {
270 "Couldn't reach Moonbase to deactivate. Try again when you're online.".into()
271 }
272
273 (Activity::OfflineActivation, SdkError::ReleaseNotCovered(_)) => {
274 "That license doesn't cover this version.".into()
275 }
276 (Activity::OfflineActivation, _) => "That license file isn't valid for this device.".into(),
277
278 (Activity::StartActivation, error) => format!("Activation couldn't start: {error}"),
280 (Activity::Activation, error) => format!("Activation failed: {error}"),
281 (Activity::Deactivation, error) => format!("Couldn't deactivate: {error}"),
282 };
283 Failure { message, detail: error.to_string(), kind: Some(error.error_type()) }
284}
285
286fn add_on_failure(error: &SdkError, lead: &str, not_found: &str) -> String {
290 match error {
291 SdkError::Api(api) if api.status_code() == 404 => not_found.into(),
292 SdkError::Api(api) if busy_status(api.status_code()) => {
293 "Moonbase couldn't do that right now. Try again in a minute.".into()
294 }
295 SdkError::Api(_) => "Couldn't reach Moonbase. Check your internet connection and try again.".into(),
296 SdkError::StoreClosed(_) => "This store has closed, so add-ons aren't available.".into(),
297 SdkError::InsufficientDeviceIdentity { .. } => {
298 "This computer can't be identified. Contact the developer for help.".into()
299 }
300 error if error.is_license_rejection() || matches!(error, SdkError::Configuration(_)) => {
301 format!("{lead}. If this keeps happening, contact the developer.")
302 }
303 error => format!("{lead}: {error}"),
304 }
305}
306
307fn ends_activation(error: &SdkError) -> bool {
309 error.is_license_rejection()
310 || matches!(
311 error,
312 SdkError::ActivationRequestEnded(_)
313 | SdkError::StoreClosed(_)
314 | SdkError::InsufficientDeviceIdentity { .. }
315 | SdkError::Configuration(_)
316 )
317}
318
319fn age(time: SystemTime) -> Duration {
320 SystemTime::now().duration_since(time).unwrap_or(Duration::ZERO)
321}
322
323fn days_until(when: SystemTime) -> u64 {
325 when.duration_since(SystemTime::now()).map_or(0, |left| left.as_secs().div_ceil(86_400))
326}
327
328fn active_sub_products(license: &License, now: SystemTime) -> Vec<String> {
331 let mut active: Vec<String> = Vec::new();
332 for entry in &license.sub_products {
333 if license.has_sub_product_at(&entry.id, now) && !active.contains(&entry.id) {
334 active.push(entry.id.clone());
335 }
336 }
337 active
338}
339
340#[derive(Default)]
342struct StartupOutcome {
343 license: Option<License>,
344 expired_trial: Option<License>,
345 not_covered: Option<NotCoveredLicense>,
346 delete_expired_offline: bool,
347 reason: Option<LockReason>,
348}
349
350fn accept_locally(licensing: &Licensing, license: License, outcome: &mut StartupOutcome) {
354 match licensing.validator().ensure_covers_application_version(&license) {
355 Ok(()) => outcome.license = Some(license),
356 Err(SdkError::ReleaseNotCovered(refusal)) => {
357 log::info!("moonbase: the stored license doesn't cover this version: {refusal}");
359 outcome.not_covered = Some(NotCoveredLicense::from_refusal(&refusal, &license));
360 }
361 Err(error) => {
362 log::warn!("moonbase: stored license rejected: {error}");
363 outcome.reason = Some(LockReason::Invalid);
364 }
365 }
366}
367
368fn check_stored_license(
369 licensing: &Licensing,
370 grace_period: Duration,
371 still_current: impl Fn() -> bool,
372) -> StartupOutcome {
373 let mut outcome = StartupOutcome::default();
374 let stored = match licensing.store().load_local_license() {
375 Ok(Some(stored)) => stored,
376 Ok(None) => return outcome,
377 Err(error) => {
378 log::warn!("moonbase: loading the stored license failed: {error}");
379 return outcome;
380 }
381 };
382
383 let peek = match licensing.validate_token_local_allow_expired(&stored.token) {
387 Ok(peek) => peek,
388 Err(SdkError::InsufficientDeviceIdentity { .. }) => {
389 log::warn!("moonbase: could not identify this device, so the stored license could not be checked");
391 return outcome;
392 }
393 Err(error) => {
394 log::warn!("moonbase: stored license rejected: {error}");
396 outcome.reason = Some(LockReason::Invalid);
397 return outcome;
398 }
399 };
400
401 if peek.method == ActivationMethod::Offline {
402 if peek.is_expired() {
403 log::info!("moonbase: the stored offline license has expired; removing it");
404 outcome.delete_expired_offline = true;
405 outcome.reason = Some(LockReason::Expired);
406 } else {
407 accept_locally(licensing, peek, &mut outcome);
408 }
409 return outcome;
410 }
411
412 match licensing.validate_token_online_with(&stored.token, still_current) {
415 Ok(license) => outcome.license = Some(license),
416 Err(SdkError::ReleaseNotCovered(refusal)) => {
417 log::info!("moonbase: the stored license doesn't cover this version: {refusal}");
420 outcome.not_covered = Some(NotCoveredLicense::from_refusal(&refusal, &peek));
421 }
422 Err(SdkError::LicenseExpired(_)) => {
423 outcome.reason = Some(LockReason::Expired);
424 if peek.trial {
425 outcome.expired_trial = Some(peek);
426 }
427 }
428 Err(error) if error.is_license_invalid() => {
429 log::warn!("moonbase: re-validating the stored license failed: {error}");
430 outcome.reason = Some(LockReason::Invalid);
431 }
432 Err(SdkError::Storage(error)) => {
433 log::warn!("moonbase: re-validating the stored license failed: {error}");
438 if age(peek.validated_at) <= grace_period {
439 accept_locally(licensing, peek, &mut outcome);
440 } else {
441 outcome.reason = Some(LockReason::Unverified);
442 }
443 }
444 Err(error) => {
445 log::warn!("moonbase: re-validating the stored license failed: {error}");
447 outcome.reason = Some(LockReason::Unverified);
448 }
449 }
450 outcome
451}
452
453enum AddOnOutcome {
455 Applied {
456 relist: bool,
457 },
458 Changed,
460 Failed {
461 error: SdkError,
462 recheck: bool,
463 },
464}
465
466const LICENSE_CHANGED: &str = "Your license changed while this was in progress.";
467
468impl<R: Runtime> Moonbase<R> {
469 pub(crate) fn new(
470 app: AppHandle<R>,
471 licensing: Licensing,
472 store: FileLicenseStore,
473 settings: Settings,
474 ui: Option<Ui>,
475 ) -> Self {
476 Self(Arc::new(Shared {
477 app,
478 licensing: Arc::new(licensing),
479 store,
480 settings,
481 inner: Mutex::new(Inner::default()),
482 generation: AtomicU64::new(0),
483 licensed: AtomicBool::new(false),
484 shutdown: AtomicBool::new(false),
485 emit_lock: Mutex::new(()),
486 online_calls: Mutex::new(()),
487 emitted_sub_products: Mutex::default(),
488 ui,
489 }))
490 }
491
492 fn inner(&self) -> MutexGuard<'_, Inner> {
493 self.0.inner.lock().unwrap_or_else(PoisonError::into_inner)
494 }
495
496 fn online_turn(&self) -> MutexGuard<'_, ()> {
497 self.0.online_calls.lock().unwrap_or_else(PoisonError::into_inner)
498 }
499
500 fn bump(&self) -> u64 {
501 self.0.generation.fetch_add(1, Ordering::SeqCst) + 1
502 }
503
504 fn is_current(&self, generation: u64) -> bool {
505 self.0.generation.load(Ordering::SeqCst) == generation
506 }
507
508 fn emit(&self) {
509 let _ordered = self.0.emit_lock.lock().unwrap_or_else(PoisonError::into_inner);
510 let state = self.state();
511 state
512 .active_sub_products
513 .clone_into(&mut self.0.emitted_sub_products.lock().unwrap_or_else(PoisonError::into_inner));
514 if let Err(error) = self.0.app.emit(STATE_CHANGED_EVENT, &state) {
515 log::warn!("moonbase: could not emit the license state: {error}");
516 }
517 self.after_state_change(&state);
518 }
519
520 pub(crate) fn app(&self) -> &AppHandle<R> {
521 &self.0.app
522 }
523
524 pub(crate) fn ui(&self) -> Option<&Ui> {
525 self.0.ui.as_ref()
526 }
527
528 pub fn licensing(&self) -> &Licensing {
530 &self.0.licensing
531 }
532
533 pub fn is_licensed(&self) -> bool {
536 self.0.licensed.load(Ordering::Acquire)
537 }
538
539 pub fn license(&self) -> Option<License> {
541 self.inner().standing.license().cloned()
542 }
543
544 pub fn has_sub_product(&self, sub_product_id: &str) -> bool {
549 self.inner().standing.license().is_some_and(|license| license.has_sub_product(sub_product_id))
550 }
551
552 pub fn active_sub_products(&self) -> Vec<String> {
555 self.inner()
556 .standing
557 .license()
558 .map(|license| active_sub_products(license, SystemTime::now()))
559 .unwrap_or_default()
560 }
561
562 pub fn license_file(&self) -> PathBuf {
564 self.0.store.path().to_owned()
565 }
566
567 pub fn state(&self) -> LicenseState {
569 let inner = self.inner();
570 let status = if inner.loading {
571 Status::Loading
572 } else if inner.activation.is_some() {
573 Status::Activating
574 } else {
575 match &inner.standing {
576 Standing::Licensed(license) if license.trial => Status::Trial,
577 Standing::Licensed(_) => Status::Licensed,
578 Standing::TrialExpired(_) => Status::TrialExpired,
579 Standing::NotCovered(_) => Status::NotCovered,
580 Standing::Unlicensed(_) => Status::Unlicensed,
581 }
582 };
583 let license = inner.standing.license();
584 let update = match (license, &self.0.settings.application_version) {
585 (Some(license), Some(current)) => license
586 .licensed_product
587 .current_release_version
588 .as_ref()
589 .filter(|latest| update_available(current, latest))
590 .map(|latest| UpdateHint { current_version: current.clone(), latest_version: latest.clone() }),
591 _ => None,
592 };
593 LicenseState {
594 status,
595 licensed: license.is_some(),
596 license: license.map(LicenseView::from),
597 expired_trial: match &inner.standing {
598 Standing::TrialExpired(trial) => Some(LicenseView::from(trial)),
599 _ => None,
600 },
601 not_covered: inner.standing.not_covered().map(NotCoveredLicense::view),
602 lock_reason: inner.standing.lock_reason(),
603 activation: inner.activation.clone(),
604 busy: inner.deactivating.is_some(),
605 refreshing: inner.refreshes_in_flight > 0,
606 error: inner.error.clone(),
607 update,
608 active_sub_products: license
609 .map(|license| active_sub_products(license, SystemTime::now()))
610 .unwrap_or_default(),
611 add_ons: inner.add_ons.clone(),
612 }
613 }
614
615 fn is_older_than_held(inner: &Inner, copy: &License) -> bool {
618 inner
619 .standing
620 .held()
621 .is_some_and(|held| held.activation_id == copy.activation_id && copy.validated_at < held.validated_at)
622 }
623
624 fn reset_add_ons(inner: &mut Inner) {
625 inner.add_ons_generation += 1;
626 inner.add_ons = AddOnsState::default();
627 inner.add_on_call = 0;
628 inner.lapses_checked.clear();
629 inner.purchase_opened_at = None;
630 inner.left_for_purchase = None;
631 }
632
633 fn set_standing(&self, inner: &mut Inner, standing: Standing) {
634 let same_activation = matches!(
635 (inner.standing.license(), standing.license()),
636 (Some(current), Some(next)) if current.activation_id == next.activation_id
637 );
638 self.0.licensed.store(standing.license().is_some(), Ordering::Release);
639 if !same_activation {
642 Self::reset_add_ons(inner);
643 }
644 inner.standing = standing;
645 }
646
647 fn set_license(&self, inner: &mut Inner, license: License) {
648 self.set_standing(inner, Standing::Licensed(license));
649 }
650
651 fn lock(&self, inner: &mut Inner, reason: Option<LockReason>) {
654 self.set_standing(inner, Standing::Unlicensed(reason));
655 }
656
657 fn show_trial_expired(&self, inner: &mut Inner, trial: License) {
658 self.set_standing(inner, Standing::TrialExpired(trial));
659 }
660
661 fn show_not_covered(&self, inner: &mut Inner, not_covered: NotCoveredLicense) {
664 self.set_standing(inner, Standing::NotCovered(not_covered));
665 inner.error = None;
666 }
667
668 fn end_activation_flows(&self, inner: &mut Inner) {
670 inner.activation = None;
671 inner.pending = None;
672 inner.deactivating = None;
673 Self::reset_add_ons(inner);
674 self.bump();
675 }
676
677 fn stamp_file(&self) -> FileStamp {
678 match std::fs::metadata(self.0.store.path()) {
679 Ok(metadata) if metadata.is_file() => {
680 FileStamp { exists: true, len: metadata.len(), modified: metadata.modified().ok() }
681 }
682 _ => FileStamp { exists: false, len: 0, modified: None },
683 }
684 }
685
686 fn stores_activation(store: &dyn LicenseStore, activation_id: &str) -> bool {
690 store.load_local_license().ok().flatten().is_some_and(|stored| stored.activation_id == activation_id)
691 }
692
693 fn delete_stored(&self, inner: &mut Inner, only_activation: Option<&str>) {
694 let store = self.0.licensing.store();
695 let result = store.lock_for_update().and_then(|_guard| match only_activation {
696 Some(activation_id) => match store.load_local_license()? {
697 Some(stored) if stored.activation_id == activation_id => store.delete_local_license(),
698 _ => Ok(()),
699 },
700 None => store.delete_local_license(),
701 });
702 if let Err(error) = result {
703 log::warn!("moonbase: couldn't delete the stored license: {error}");
704 }
705 inner.file_stamp = Some(self.stamp_file());
706 }
707
708 pub(crate) fn start(&self) {
709 {
710 let mut inner = self.inner();
711 inner.loading = true;
712 inner.file_stamp = Some(self.stamp_file());
715 }
716 let generation = self.bump();
717
718 let this = self.clone();
719 thread::spawn(move || {
720 let outcome =
721 check_stored_license(&this.0.licensing, this.0.settings.grace_period, || this.is_current(generation));
722 let mut inner = this.inner();
723 inner.loading = false;
724 if this.is_current(generation) {
726 if outcome.delete_expired_offline {
727 this.delete_stored(&mut inner, None);
728 }
729 if let Some(trial) = outcome.expired_trial {
730 this.show_trial_expired(&mut inner, trial);
731 } else if let Some(not_covered) = outcome.not_covered {
732 this.show_not_covered(&mut inner, not_covered);
733 } else if let Some(license) = outcome.license {
734 this.set_license(&mut inner, license);
735 } else {
736 this.lock(&mut inner, outcome.reason);
737 }
738 }
739 drop(inner);
740 this.emit();
741 });
742
743 let this = self.clone();
744 thread::spawn(move || {
745 while !this.0.shutdown.load(Ordering::Acquire) {
746 thread::sleep(WATCH_INTERVAL);
747 this.watch();
748 }
749 });
750 }
751
752 pub(crate) fn shut_down(&self) {
753 self.0.shutdown.store(true, Ordering::Release);
754 self.bump();
755 self.0.store.cancel_lock_waits();
758 }
759
760 pub fn activate(&self, method: ActivationMethod) -> Result<LicenseState> {
767 let generation = {
768 let mut inner = self.inner();
769 if inner.deactivating.is_some() {
770 return Err(Error::Busy("A deactivation is in progress".into()));
771 }
772 inner.activation = Some(PendingActivation { browser_url: None, method, browser_open_failed: false });
773 inner.pending = None;
774 inner.error = None;
775 self.bump()
776 };
777 self.emit();
778
779 let result = self.0.licensing.request_activation(method);
780
781 let mut inner = self.inner();
782 if !self.is_current(generation) {
783 drop(inner);
784 return Ok(self.state());
785 }
786 let request = match result {
787 Ok(request) => request,
788 Err(error) => {
789 log::warn!("moonbase: request_activation failed: {error}");
790 inner.activation = None;
791 inner.error = Some(failure(Activity::StartActivation, &error));
792 drop(inner);
793 self.emit();
794 return Err(error.into());
795 }
796 };
797 inner.pending = Some(request.clone());
798 inner.activation = Some(PendingActivation {
799 browser_url: Some(request.browser_url.clone()),
800 method,
801 browser_open_failed: false,
802 });
803 drop(inner);
804
805 if self.0.settings.open_browser
806 && let Err(error) = self.open_url(&request.browser_url)
807 {
808 log::warn!("moonbase: couldn't open the browser for activation ({error}); the state carries the link");
811 let mut inner = self.inner();
812 if self.is_current(generation)
813 && let Some(activation) = &mut inner.activation
814 {
815 activation.browser_open_failed = true;
816 }
817 }
818 self.spawn_poll(generation, request);
819 self.emit();
820 Ok(self.state())
821 }
822
823 pub(crate) fn open_url(&self, url: &str) -> std::result::Result<(), String> {
827 let opened = match self.0.app.try_state::<tauri_plugin_opener::Opener<R>>() {
828 Some(opener) => opener.open_url(url, None::<String>),
829 None => tauri_plugin_opener::open_url(url, None::<&str>),
830 };
831 opened.map_err(|error| error.to_string())
832 }
833
834 fn spawn_poll(&self, generation: u64, request: ActivationRequest) {
835 let this = self.clone();
836 thread::spawn(move || {
837 loop {
838 thread::sleep(POLL_INTERVAL);
839 if this.0.shutdown.load(Ordering::Acquire) || !this.is_current(generation) {
840 return;
841 }
842 match this.0.licensing.complete_activation_with(&request, || this.is_current(generation)) {
844 Ok(None) => {}
845 Ok(Some(license)) => return this.on_activated(generation, license),
846 Err(error) if ends_activation(&error) => {
847 log::warn!("moonbase: activation rejected during polling: {error}");
848 let mut inner = this.inner();
849 if this.is_current(generation) {
850 inner.activation = None;
851 inner.pending = None;
852 inner.error = Some(failure(Activity::Activation, &error));
853 }
854 drop(inner);
855 return this.emit();
856 }
857 Err(error) => log::debug!("moonbase: activation poll failed, still waiting: {error}"),
859 }
860 }
861 });
862 }
863
864 fn on_activated(&self, generation: u64, license: License) {
865 let mut inner = self.inner();
866 if !self.is_current(generation) {
867 return;
868 }
869 self.end_activation_flows(&mut inner);
870 inner.file_stamp = Some(self.stamp_file());
871 self.set_license(&mut inner, license);
872 inner.error = None;
873 drop(inner);
874 self.emit();
875 }
876
877 pub fn cancel_activation(&self) -> LicenseState {
879 let mut inner = self.inner();
880 if inner.activation.is_none() {
882 return self.state_with(inner);
883 }
884 inner.activation = None;
885 inner.pending = None;
886 inner.error = None;
887 self.bump();
888 drop(inner);
889 self.emit();
890 self.state()
891 }
892
893 pub fn refresh(&self, force: bool) -> Result<LicenseState> {
905 let (generation, current, not_covered_before) = {
906 let mut inner = self.inner();
907 if inner.activation.is_some() {
908 return Ok(self.state_with(inner));
909 }
910 if inner.deactivating.is_some() {
911 return Err(Error::Busy("A deactivation is in progress".into()));
912 }
913 let Some(current) = inner.standing.held().cloned() else {
914 return Err(Error::NotLicensed);
915 };
916 if current.method == ActivationMethod::Offline {
917 return Ok(self.state_with(inner));
918 }
919 inner.refreshes_in_flight += 1;
920 (self.bump(), current, inner.standing.license().is_none())
921 };
922 if not_covered_before {
923 self.emit(); }
925
926 let _turn = self.online_turn();
927 let store = self.0.licensing.store();
928 let still_current = || self.is_current(generation) && Self::stores_activation(store, ¤t.activation_id);
933
934 let result = if force {
935 store.lock_for_update().and_then(|_guard| {
940 let result = self.0.licensing.client().validate_token_online(¤t.token);
941 let answer = match &result {
942 Ok(refreshed) => Some(refreshed),
943 Err(SdkError::ReleaseNotCovered(refusal)) => refusal.license(),
946 Err(_) => None,
947 };
948 if let Some(answer) = answer
949 && still_current()
950 && let Err(error) = store.store_local_license(answer)
951 {
952 log::warn!("moonbase: couldn't persist the refreshed license: {error}");
953 }
954 result
955 })
956 } else {
957 self.0.licensing.validate_token_online_with(¤t.token, still_current)
959 };
960
961 let mut inner = self.inner();
962 inner.refreshes_in_flight -= 1;
963 if !self.is_current(generation) {
964 drop(inner);
965 if not_covered_before {
966 self.emit(); }
968 return Ok(self.state());
969 }
970 if let Ok(refreshed) = &result
976 && !force
977 && refreshed.token == current.token
978 && inner
979 .standing
980 .license()
981 .is_some_and(|held| held.activation_id == refreshed.activation_id && held.token != current.token)
982 {
983 return Ok(self.state_with(inner));
984 }
985 let outcome = match result {
986 Ok(refreshed) => {
987 inner.file_stamp = Some(self.stamp_file());
988 if not_covered_before {
989 inner.error = None;
990 }
991 self.set_license(&mut inner, refreshed);
992 Ok(())
993 }
994 Err(SdkError::ReleaseNotCovered(refusal)) => {
995 let not_covered = NotCoveredLicense::from_refusal(&refusal, ¤t);
996 if Self::is_older_than_held(&inner, ¬_covered.license) {
997 log::debug!("moonbase: dropped a re-validation answer older than the license held: {refusal}");
1000 } else {
1001 log::info!("moonbase: the license doesn't cover this version on re-validation: {refusal}");
1002 self.show_not_covered(&mut inner, not_covered);
1003 }
1004 Ok(())
1005 }
1006 Err(SdkError::LicenseExpired(message)) if current.trial => {
1007 log::info!("moonbase: the trial ended on re-validation: {message}");
1008 self.show_trial_expired(&mut inner, current);
1009 Ok(())
1010 }
1011 Err(error) if !error.is_license_rejection() && inner.standing.not_covered().is_some() => {
1012 log::warn!("moonbase: couldn't re-check the license that doesn't cover this version: {error}");
1015 inner.error = Some(Failure {
1016 message: "Couldn't reach Moonbase to check your license. Try again when you're online.".into(),
1017 detail: error.to_string(),
1018 kind: Some(error.error_type()),
1019 });
1020 Err(error)
1021 }
1022 Err(error) if error.is_license_rejection() => {
1023 log::warn!("moonbase: license rejected on re-validation: {error}");
1024 let reason = if matches!(error, SdkError::LicenseExpired(_)) {
1025 LockReason::Expired
1026 } else {
1027 LockReason::Invalid
1028 };
1029 self.lock(&mut inner, Some(reason));
1030 Ok(())
1031 }
1032 Err(error) if age(current.validated_at) > self.0.settings.grace_period => {
1033 log::warn!("moonbase: couldn't re-validate within the offline grace period: {error}");
1034 self.lock(&mut inner, Some(LockReason::Unverified));
1035 Ok(())
1036 }
1037 Err(error) => Err(error),
1039 };
1040 drop(inner);
1041 self.emit();
1042 outcome.map(|()| self.state()).map_err(Error::from)
1043 }
1044
1045 fn state_with(&self, inner: MutexGuard<'_, Inner>) -> LicenseState {
1046 drop(inner);
1047 self.state()
1048 }
1049
1050 pub fn deactivate(&self) -> Result<LicenseState> {
1055 let (generation, license) = {
1056 let mut inner = self.inner();
1057 let Some(license) = inner.standing.held().cloned() else {
1058 return Err(Error::NotLicensed);
1059 };
1060 if license.method == ActivationMethod::Offline || license.trial {
1061 drop(inner);
1062 return Ok(self.forget());
1063 }
1064 if inner.deactivating.is_some() {
1065 return Err(Error::Busy("A deactivation is already in progress".into()));
1066 }
1067 if inner.activation.is_some() {
1069 return Err(Error::Busy("Finish or cancel the activation first".into()));
1070 }
1071 inner.error = None;
1072 let generation = self.bump();
1073 inner.deactivating = Some(generation);
1074 (generation, license)
1075 };
1076 self.emit();
1077
1078 let result = self.0.licensing.revoke_activation(&license.token);
1079
1080 let mut inner = self.inner();
1081 if inner.deactivating == Some(generation) {
1082 inner.deactivating = None;
1083 }
1084 if !self.is_current(generation) {
1085 drop(inner);
1086 self.emit();
1087 return Ok(self.state());
1088 }
1089 let outcome = match result {
1090 Ok(()) | Err(SdkError::OperationNotSupported(_)) => Ok(()),
1093 Err(error) if error.is_license_rejection() => Ok(()),
1094 Err(error) => {
1095 log::warn!("moonbase: revoke_activation failed, license kept: {error}");
1096 inner.error = Some(failure(Activity::Deactivation, &error));
1097 Err(error)
1098 }
1099 };
1100 if outcome.is_ok() {
1101 self.delete_stored(&mut inner, Some(&license.activation_id));
1102 self.lock(&mut inner, Some(LockReason::Deactivated));
1103 }
1104 drop(inner);
1105 self.emit();
1106 outcome.map(|()| self.state()).map_err(Error::from)
1107 }
1108
1109 pub fn forget(&self) -> LicenseState {
1111 let mut inner = self.inner();
1112 self.end_activation_flows(&mut inner);
1113 self.delete_stored(&mut inner, None);
1114 self.lock(&mut inner, Some(LockReason::Deactivated));
1115 inner.error = None;
1116 drop(inner);
1117 self.emit();
1118 self.state()
1119 }
1120
1121 pub fn device_token(&self) -> Result<String> {
1126 self.0.licensing.generate_device_token().map_err(Error::from)
1127 }
1128
1129 pub fn activate_offline(&self, license_file_contents: &str) -> Result<LicenseState> {
1135 match self.0.licensing.import_offline_license(license_file_contents) {
1137 Ok(license) => {
1138 let mut inner = self.inner();
1139 self.end_activation_flows(&mut inner);
1140 inner.file_stamp = Some(self.stamp_file());
1141 self.set_license(&mut inner, license);
1142 inner.error = None;
1143 drop(inner);
1144 self.emit();
1145 Ok(self.state())
1146 }
1147 Err(SdkError::ReleaseNotCovered(refusal)) if refusal.license().is_some() => {
1148 log::info!("moonbase: the offline license doesn't cover this version: {refusal}");
1150 let license = refusal.license().cloned().expect("checked above");
1151 let mut inner = self.inner();
1152 self.end_activation_flows(&mut inner);
1153 inner.file_stamp = Some(self.stamp_file());
1154 self.show_not_covered(&mut inner, NotCoveredLicense::from_refusal(&refusal, &license));
1155 drop(inner);
1156 self.emit();
1157 Err(SdkError::ReleaseNotCovered(refusal).into())
1158 }
1159 Err(error) => {
1160 log::warn!("moonbase: offline license file rejected: {error}");
1161 self.inner().error = Some(failure(Activity::OfflineActivation, &error));
1162 self.emit();
1163 Err(error.into())
1164 }
1165 }
1166 }
1167
1168 pub fn upgrade_license(&self) -> Result<LicenseState> {
1176 let (url, not_covered) = {
1177 let inner = self.inner();
1178 match inner.standing.not_covered() {
1179 Some(not_covered) => (not_covered.upgrade_url.clone(), true),
1180 None => (inner.update_upgrade_url.clone(), false),
1181 }
1182 };
1183 let Some(url) = url else {
1184 if not_covered {
1185 return self.activate(ActivationMethod::Online);
1188 }
1189 return Err(Error::Internal("There is no upgrade to offer".into()));
1190 };
1191 if let Err(error) = self.open_url(&url) {
1192 log::warn!("moonbase: couldn't open the browser for the upgrade: {error}");
1193 if not_covered {
1194 self.inner().error = Some(Failure {
1195 message: format!("Couldn't open your browser. Upgrade at {url}"),
1196 detail: error.clone(),
1197 kind: None,
1198 });
1199 self.emit();
1200 }
1201 return Err(Error::Internal(format!("Couldn't open the browser: {error}")));
1202 }
1203 Ok(self.state())
1204 }
1205
1206 pub fn dismiss_error(&self) -> LicenseState {
1208 self.inner().error = None;
1209 self.emit();
1210 self.state()
1211 }
1212
1213 pub fn device(&self) -> DeviceInfo {
1215 let resolver = self.0.licensing.device_resolver();
1216 let (description, error) = match resolver.describe_device() {
1217 Ok(description) => (description, None),
1218 Err(error) => (None, Some(error.to_string())),
1219 };
1220 DeviceInfo { name: resolver.device_name(), platform: Platform::current().as_str().into(), description, error }
1221 }
1222
1223 pub fn check_update(&self) -> Result<UpdateInfo> {
1227 let license = self.license().ok_or(Error::NotLicensed)?;
1228 let current = self.0.settings.application_version.clone().unwrap_or_default();
1229 let latest = license.licensed_product.current_release_version.clone();
1230 let mut info = UpdateInfo {
1231 available: latest.as_deref().is_some_and(|latest| update_available(¤t, latest)),
1232 current_version: current,
1233 latest_version: latest.clone(),
1234 release_notes: String::new(),
1235 has_downloads: false,
1236 can_download: false,
1237 covered: true,
1238 release_access: None,
1239 upgrade_url: None,
1240 };
1241 if let (true, Some(latest)) = (info.available, latest) {
1242 let release = self.0.licensing.inventory().get_release(&latest, &license.token)?;
1243 info.can_download = release.has_downloads && can_download(&license, &release);
1246 info.has_downloads = release.has_downloads;
1247 info.release_notes = release.description;
1248 info.covered = release.release_covered;
1249 info.release_access = release.release_access;
1250 info.upgrade_url.clone_from(&release.upgrade_url);
1251 self.inner().update_upgrade_url = release.upgrade_url;
1252 }
1253 Ok(info)
1254 }
1255
1256 pub fn update_download_url(&self) -> Result<DownloadTarget> {
1261 let license = self.license().ok_or(Error::NotLicensed)?;
1262 match self.0.licensing.inventory().get_download_url(Platform::current().as_str(), &license.token) {
1263 Ok(target) => Ok(target),
1264 Err(SdkError::ReleaseNotCovered(refusal)) => {
1265 if let Some(url) = refusal.upgrade_url() {
1266 self.inner().update_upgrade_url = Some(url.to_owned());
1267 }
1268 Err(SdkError::ReleaseNotCovered(refusal).into())
1269 }
1270 Err(error) => Err(error.into()),
1271 }
1272 }
1273
1274 pub fn load_add_ons(&self) -> Result<LicenseState> {
1289 self.list_add_ons(true)
1290 }
1291
1292 fn list_add_ons(&self, pick_up_seats: bool) -> Result<LicenseState> {
1293 let (generation, token) = {
1294 let mut inner = self.inner();
1295 let Some(token) = inner.standing.license().map(|license| license.token.clone()) else {
1296 return Err(Error::NotLicensed);
1297 };
1298 inner.add_ons_generation += 1;
1299 inner.add_ons.phase = AddOnsPhase::Loading;
1300 inner.add_ons.error = None;
1301 (inner.add_ons_generation, token)
1302 };
1303 self.emit();
1304
1305 let result = self.0.licensing.list_sub_products(&token);
1306
1307 let (recheck, take_seat) = {
1308 let mut inner = self.inner();
1309 if inner.add_ons_generation != generation {
1310 return Ok(self.state_with(inner));
1312 }
1313 inner.add_ons.phase = AddOnsPhase::Ready;
1314 let refused = match &result {
1315 Ok(catalog) => {
1316 inner.add_ons.currency = Some(catalog.currency.clone());
1317 inner.add_ons.add_ons.clone_from(&catalog.sub_products);
1318 inner.add_ons.error = None;
1319 false
1320 }
1321 Err(error) => {
1322 log::warn!("moonbase: listing the add-ons failed: {error}");
1323 inner.add_ons.error = Some(match error {
1324 SdkError::LicenseExpired(_) => "Your license has expired.".into(),
1325 error if error.is_license_invalid() => {
1326 "Your license is no longer valid on this computer.".into()
1327 }
1328 error => add_on_failure(
1329 error,
1330 "Couldn't load the add-ons",
1331 "Moonbase couldn't find the add-ons for this license.",
1332 ),
1333 });
1334 error.is_license_rejection()
1335 }
1336 };
1337 let take_seat = result.is_ok() && pick_up_seats && self.lists_seat_to_take(&inner);
1341 (refused && inner.deactivating.is_none(), take_seat)
1342 };
1343 self.emit();
1344
1345 if recheck && let Err(error) = self.refresh(true) {
1348 log::debug!("moonbase: the re-check after a refused add-on list failed: {error}");
1349 }
1350 if take_seat {
1351 log::info!("moonbase: an add-on the customer owns has a seat free; re-checking the license to take it");
1352 return self.refresh_and_list();
1353 }
1354 result.map(|_| self.state()).map_err(Error::from)
1355 }
1356
1357 fn lists_seat_to_take(&self, inner: &Inner) -> bool {
1358 let Some(license) = inner.standing.license() else {
1359 return false;
1360 };
1361 if !self.0.settings.auto_activate_sub_products || license.trial || license.method != ActivationMethod::Online {
1362 return false;
1363 }
1364 let now = SystemTime::now();
1365 inner.add_ons.add_ons.iter().any(|entry| {
1366 let owned = entry.product.owned || entry.product.subscribed;
1367 if !entry.included_in_license || !owned || entry.revoked {
1368 return false;
1369 }
1370 let held = license.find_sub_product(&entry.id);
1373 if held.is_some_and(|held| !held.trial) && license.has_sub_product_at(&entry.id, now) {
1374 return false;
1375 }
1376 entry.active || entry.seats.is_some_and(|seats| seats.remaining > 0)
1379 })
1380 }
1381
1382 pub fn refresh_add_ons(&self) -> Result<LicenseState> {
1386 self.inner().add_ons.action_error = None;
1387 self.refresh_and_list()
1388 }
1389
1390 fn refresh_and_list(&self) -> Result<LicenseState> {
1391 let offline = {
1392 let mut inner = self.inner();
1393 let Some(license) = inner.standing.license() else {
1395 return Ok(self.state_with(inner));
1396 };
1397 if inner.deactivating.is_some() {
1398 return Ok(self.state_with(inner));
1399 }
1400 let offline = license.method == ActivationMethod::Offline;
1401 if !offline {
1402 inner.add_ons.phase = AddOnsPhase::Loading;
1403 inner.add_ons.error = None;
1404 }
1405 offline
1406 };
1407 if !offline {
1409 self.emit();
1410 if let Err(error) = self.refresh(true) {
1413 log::debug!("moonbase: the re-check before listing the add-ons failed: {error}");
1414 }
1415 if self.inner().standing.license().is_none() {
1416 return Ok(self.state());
1417 }
1418 }
1419 self.list_add_ons(false)
1420 }
1421
1422 pub fn activate_sub_product(&self, sub_product_id: &str) -> Result<LicenseState> {
1433 self.run_sub_product_action(sub_product_id, false)
1434 }
1435
1436 pub fn request_sub_product_trial(&self, sub_product_id: &str) -> Result<LicenseState> {
1440 self.run_sub_product_action(sub_product_id, true)
1441 }
1442
1443 fn add_on_name(inner: &Inner, sub_product_id: &str) -> Option<String> {
1444 inner
1445 .add_ons
1446 .add_ons
1447 .iter()
1448 .find(|entry| entry.id == sub_product_id || entry.product.id == sub_product_id)
1449 .map(|entry| entry.product.name.clone())
1450 }
1451
1452 fn run_sub_product_action(&self, sub_product_id: &str, trial: bool) -> Result<LicenseState> {
1453 let (call, token, activation_id, name) = {
1454 let mut inner = self.inner();
1455 let refusal = match inner.standing.license() {
1456 None => Some((format!("Activate {} first.", self.product_name()), Error::NotLicensed)),
1457 Some(license) if license.trial => {
1458 let message = "Add-ons are activated from a full license, not a trial.";
1459 Some((message.to_owned(), SdkError::OperationNotSupported(message.into()).into()))
1460 }
1461 Some(_) => None,
1462 };
1463 if let Some((message, error)) = refusal {
1464 inner.add_ons.action_error = Some(message);
1465 drop(inner);
1466 self.emit();
1467 return Err(error);
1468 }
1469 if inner.add_ons.busy_id.is_some() {
1470 return Err(Error::Busy("Another add-on is still being set up. Try again in a moment.".into()));
1471 }
1472 let license = inner.standing.license().expect("checked above");
1473 let (token, activation_id) = (license.token.clone(), license.activation_id.clone());
1474 let name = Self::add_on_name(&inner, sub_product_id);
1475 inner.add_on_calls += 1;
1476 inner.add_on_call = inner.add_on_calls;
1477 inner.add_ons.busy_id = Some(sub_product_id.to_owned());
1478 inner.add_ons.action_error = None;
1479 (inner.add_on_call, token, activation_id, name)
1480 };
1481 self.emit();
1482
1483 let outcome = {
1484 let _turn = self.online_turn();
1485 let store = self.0.licensing.store();
1486 let still_this_license = || Self::stores_activation(store, &activation_id);
1491 let result = if trial {
1492 self.0.licensing.request_sub_product_trial_with(&token, sub_product_id, still_this_license)
1493 } else {
1494 self.0.licensing.activate_sub_product_with(&token, sub_product_id, still_this_license)
1495 };
1496
1497 let mut inner = self.inner();
1498 if inner.add_on_call == call {
1500 inner.add_on_call = 0;
1501 inner.add_ons.busy_id = None;
1502 }
1503 let same_license = inner.standing.license().is_some_and(|license| license.activation_id == activation_id);
1506 match result {
1507 Ok(license) if same_license && license.activation_id == activation_id => {
1508 inner.file_stamp = Some(self.stamp_file());
1509 self.set_license(&mut inner, license);
1510 AddOnOutcome::Applied { relist: inner.add_ons.phase != AddOnsPhase::Idle }
1511 }
1512 Ok(_) => AddOnOutcome::Changed,
1513 Err(error) => {
1514 log::warn!(
1515 "moonbase: {} failed: {error}",
1516 if trial { "request_sub_product_trial" } else { "activate_sub_product" }
1517 );
1518 if same_license {
1519 let subject = name.clone().unwrap_or_else(|| "This add-on".into());
1520 let object = name.clone().unwrap_or_else(|| "this add-on".into());
1521 inner.add_ons.action_error = Some(match &error {
1522 SdkError::NoEligibleLicense(_) => format!(
1523 "No seat of {object} is free for this computer. Free one in your {} account, or buy one.",
1524 self.account_name()
1525 ),
1526 SdkError::OperationNotSupported(_) if trial => {
1527 format!("{subject} can't be tried on this computer.")
1528 }
1529 SdkError::OperationNotSupported(_) => {
1530 format!("{subject} can't be activated on this computer.")
1531 }
1532 SdkError::LicenseExpired(_) => "Your license has expired.".into(),
1533 error if error.is_license_invalid() => {
1534 "Your license is no longer valid on this computer.".into()
1535 }
1536 error => add_on_failure(
1537 error,
1538 &if trial {
1539 format!("Couldn't start the trial of {object}")
1540 } else {
1541 format!("Couldn't activate {object}")
1542 },
1543 &format!("Moonbase couldn't find {object}."),
1544 ),
1545 });
1546 let recheck = error.is_license_rejection() && inner.deactivating.is_none();
1547 AddOnOutcome::Failed { error, recheck }
1548 } else {
1549 AddOnOutcome::Changed
1550 }
1551 }
1552 }
1553 };
1554 self.emit();
1555
1556 match outcome {
1557 AddOnOutcome::Applied { relist } => {
1558 if relist && let Err(error) = self.list_add_ons(false) {
1560 log::debug!("moonbase: listing the add-ons again failed: {error}");
1561 }
1562 Ok(self.state())
1563 }
1564 AddOnOutcome::Changed => Err(Error::Busy(LICENSE_CHANGED.into())),
1565 AddOnOutcome::Failed { error, recheck } => {
1566 if recheck && let Err(error) = self.refresh(true) {
1569 log::debug!("moonbase: the re-check after a refused add-on failed: {error}");
1570 }
1571 Err(error.into())
1572 }
1573 }
1574 }
1575
1576 pub fn buy_sub_product(&self, sub_product_id: &str) -> Result<()> {
1582 let (url, name) = {
1583 let inner = self.inner();
1584 inner
1585 .add_ons
1586 .add_ons
1587 .iter()
1588 .find(|entry| entry.id == sub_product_id || entry.product.id == sub_product_id)
1589 .and_then(|entry| entry.buy_url.clone().map(|url| (url, entry.product.name.clone())))
1590 .ok_or_else(|| Error::Internal("That add-on isn't for sale here".into()))?
1591 };
1592 if let Err(error) = self.open_url(&url) {
1593 log::warn!("moonbase: couldn't open the browser to buy an add-on: {error}");
1594 self.inner().add_ons.action_error = Some(format!("Couldn't open your browser to buy {name}."));
1595 self.emit();
1596 return Err(Error::Internal(format!("Couldn't open the browser: {error}")));
1597 }
1598 let mut inner = self.inner();
1599 inner.purchase_opened_at = Some(Instant::now());
1600 inner.left_for_purchase = None;
1601 Ok(())
1602 }
1603
1604 pub(crate) fn on_focus_changed(&self, focused: bool) {
1607 let mut inner = self.inner();
1608 let Some(opened) = inner.purchase_opened_at else {
1609 return;
1610 };
1611 if opened.elapsed() > PURCHASE_WINDOW {
1612 inner.purchase_opened_at = None;
1614 inner.left_for_purchase = None;
1615 return;
1616 }
1617 if !focused {
1618 inner.left_for_purchase = Some(Instant::now());
1619 return;
1620 }
1621 let away = inner.left_for_purchase.is_some_and(|left| left.elapsed() >= Duration::from_secs(1));
1624 if !away || inner.standing.license().is_none() || inner.deactivating.is_some() {
1625 return;
1626 }
1627 inner.purchase_opened_at = None;
1628 inner.left_for_purchase = None;
1629 drop(inner);
1630 log::info!("moonbase: back from the browser after opening an add-on's buy link; refreshing the add-ons");
1631 let this = self.clone();
1632 thread::spawn(move || {
1633 if let Err(error) = this.refresh_add_ons() {
1634 log::debug!("moonbase: refreshing the add-ons failed: {error}");
1635 }
1636 });
1637 }
1638
1639 fn product_name(&self) -> String {
1640 self.ui().map_or_else(|| self.0.app.package_info().name.clone(), |ui| ui.product_name(&self.0.app))
1641 }
1642
1643 fn account_name(&self) -> String {
1644 self.ui().and_then(Ui::manufacturer_name).unwrap_or("Moonbase").to_owned()
1645 }
1646
1647 fn watch(&self) {
1652 if self.inner().loading {
1653 return;
1654 }
1655 self.check_license_file();
1656 if !self.check_sub_products() {
1657 self.check_deadlines();
1658 }
1659 self.check_countdowns();
1660 }
1661
1662 fn check_license_file(&self) {
1663 let stamp = self.stamp_file();
1664 let generation = self.0.generation.load(Ordering::SeqCst);
1667 {
1668 let mut inner = self.inner();
1669 if inner.file_stamp == Some(stamp) {
1670 return;
1671 }
1672 let existed = inner.file_stamp.is_some_and(|previous| previous.exists);
1675 inner.file_stamp = Some(stamp);
1676 if !stamp.exists {
1677 if existed {
1678 self.on_license_file_removed(inner);
1679 }
1680 return;
1681 }
1682 }
1683
1684 let Ok(on_disk) = std::fs::read(self.0.store.path())
1689 .map_err(|_| ())
1690 .and_then(|bytes| serde_json::from_slice::<License>(&bytes).map_err(|_| ()))
1691 else {
1692 return;
1693 };
1694 if self.inner().standing.held().is_some_and(|held| held.token == on_disk.token) {
1696 return;
1697 }
1698 let (stored, not_covered) = match self.0.licensing.validate_token_local(&on_disk.token) {
1699 Ok(stored) => (stored, None),
1700 Err(SdkError::ReleaseNotCovered(refusal)) => match refusal.license() {
1704 Some(license) => (license.clone(), Some(NotCoveredLicense::from_refusal(&refusal, license))),
1705 None => return,
1706 },
1707 Err(_) => return,
1709 };
1710 if stored.method != ActivationMethod::Offline && age(stored.validated_at) > self.0.settings.grace_period {
1714 return;
1715 }
1716 match not_covered {
1717 Some(not_covered) => self.adopt_not_covered(not_covered, generation),
1718 None => self.adopt_stored_license(stored, generation),
1719 }
1720 }
1721
1722 fn on_license_file_removed(&self, mut inner: MutexGuard<'_, Inner>) {
1723 if inner.standing.license().is_none() {
1724 if inner.standing.not_covered().is_none() {
1726 return;
1727 }
1728 log::info!("moonbase: the license file was removed by another instance or process");
1729 if inner.activation.is_some() {
1730 self.lock(&mut inner, Some(LockReason::Deactivated));
1732 } else {
1733 self.end_activation_flows(&mut inner);
1735 self.lock(&mut inner, Some(LockReason::Deactivated));
1736 }
1737 drop(inner);
1738 return self.emit();
1739 }
1740 log::info!("moonbase: the license file was removed by another instance or process; locking");
1741 if inner.activation.is_some() {
1742 self.lock(&mut inner, Some(LockReason::Deactivated));
1745 } else {
1746 self.end_activation_flows(&mut inner);
1747 self.lock(&mut inner, Some(LockReason::Deactivated));
1748 }
1749 drop(inner);
1750 self.emit();
1751 }
1752
1753 fn adopt_stored_license(&self, stored: License, generation: u64) {
1754 let mut inner = self.inner();
1755 if !self.is_current(generation) {
1756 inner.file_stamp = None;
1758 return;
1759 }
1760 if Self::is_older_than_held(&inner, &stored) {
1764 return;
1765 }
1766 if let Some(current) = inner.standing.license()
1767 && current.activation_id == stored.activation_id
1768 {
1769 self.set_license(&mut inner, stored);
1771 drop(inner);
1772 return self.emit();
1773 }
1774 log::info!("moonbase: picked up a license stored by another instance or process");
1778 self.end_activation_flows(&mut inner);
1779 self.set_license(&mut inner, stored);
1780 drop(inner);
1781 self.emit();
1782 }
1783
1784 fn adopt_not_covered(&self, mut not_covered: NotCoveredLicense, generation: u64) {
1785 let mut inner = self.inner();
1786 if !self.is_current(generation) {
1787 inner.file_stamp = None;
1788 return;
1789 }
1790 if Self::is_older_than_held(&inner, ¬_covered.license) || inner.activation.is_some() {
1793 return;
1794 }
1795 let same_activation =
1796 inner.standing.held().is_some_and(|held| held.activation_id == not_covered.license.activation_id);
1797 log::info!("moonbase: another instance or process found that the license doesn't cover this version; locking");
1798 if same_activation
1800 && not_covered.upgrade_url.is_none()
1801 && let Some(held) = inner.standing.not_covered()
1802 {
1803 not_covered.upgrade_url.clone_from(&held.upgrade_url);
1804 }
1805 if !same_activation {
1807 self.end_activation_flows(&mut inner);
1808 }
1809 self.show_not_covered(&mut inner, not_covered);
1810 drop(inner);
1811 self.emit();
1812 }
1813
1814 fn check_sub_products(&self) -> bool {
1818 let now = SystemTime::now();
1819 let (changed, ran_out) = {
1820 let mut inner = self.inner();
1821 let Some(license) = inner.standing.license().cloned() else {
1822 return false;
1823 };
1824 let active = active_sub_products(&license, now);
1825 let changed = *self.0.emitted_sub_products.lock().unwrap_or_else(PoisonError::into_inner) != active;
1826 for id in &active {
1827 inner.lapses_checked.remove(id);
1829 }
1830 let mut ran_out = Vec::new();
1834 if license.method == ActivationMethod::Online
1835 && inner.deactivating.is_none()
1836 && inner.refreshes_in_flight == 0
1837 && inner.activation.is_none()
1838 {
1839 for entry in &license.sub_products {
1840 if entry.active
1841 && entry.expires_at.is_some_and(|expires| expires <= now)
1842 && !inner.lapses_checked.contains(&entry.id)
1843 {
1844 ran_out.push(entry.id.clone());
1845 }
1846 }
1847 inner.lapses_checked.extend(ran_out.iter().cloned());
1848 }
1849 (changed, !ran_out.is_empty())
1850 };
1851 if changed {
1852 self.emit();
1853 }
1854 if ran_out {
1855 log::info!("moonbase: an add-on ran out; re-checking the license in case it was bought or renewed");
1856 if let Err(error) = self.refresh(true) {
1857 log::debug!("moonbase: the add-on re-check failed: {error}");
1858 }
1859 }
1860 ran_out
1861 }
1862
1863 fn check_deadlines(&self) {
1864 let mut inner = self.inner();
1865 let Some(license) = inner.standing.license().cloned() else {
1867 return;
1868 };
1869 if inner.deactivating.is_some() || inner.refreshes_in_flight > 0 {
1870 return;
1871 }
1872 let expired = license.expires_at.is_some_and(|expires| expires <= SystemTime::now());
1873 let offline = license.method == ActivationMethod::Offline;
1874 let past_grace = !offline && age(license.validated_at) > self.0.settings.grace_period;
1875 if !expired && !past_grace {
1876 return;
1877 }
1878
1879 if inner.activation.is_some() {
1880 log::info!("moonbase: the license ran out while an activation was in progress; locking");
1884 if expired && license.trial {
1885 self.show_trial_expired(&mut inner, license);
1886 } else {
1887 self.lock(&mut inner, Some(if expired { LockReason::Expired } else { LockReason::Unverified }));
1888 }
1889 drop(inner);
1890 return self.emit();
1891 }
1892
1893 if offline {
1894 log::info!("moonbase: the offline license has expired; removing it");
1896 self.bump();
1897 self.delete_stored(&mut inner, None);
1898 self.lock(&mut inner, Some(LockReason::Expired));
1899 drop(inner);
1900 return self.emit();
1901 }
1902
1903 if !expired {
1904 let now = Instant::now();
1907 if inner.last_grace_check.is_some_and(|last| now - last < self.0.settings.check_interval) {
1908 return;
1909 }
1910 inner.last_grace_check = Some(now);
1911 }
1912 drop(inner);
1913
1914 if let Err(error) = self.refresh(false) {
1917 log::debug!("moonbase: the deadline re-check failed: {error}");
1918 }
1919 }
1920
1921 fn check_countdowns(&self) {
1926 let mut inner = self.inner();
1927 let mut ends: Vec<SystemTime> = Vec::new();
1928 if let Some(license) = inner.standing.license() {
1929 ends.extend(license.expires_at);
1930 ends.extend(license.sub_products.iter().filter_map(|entry| entry.expires_at));
1931 }
1932 for entry in &inner.add_ons.add_ons {
1933 ends.extend(entry.expires_at);
1934 ends.extend(entry.trial.as_ref().and_then(|trial| trial.expires_at));
1935 }
1936 let days: Vec<u64> = ends.iter().copied().map(days_until).collect();
1937 let day_went_by = ends == inner.countdown.0 && days != inner.countdown.1;
1938 inner.countdown = (ends, days);
1939 drop(inner);
1940 if day_went_by {
1941 self.emit();
1942 }
1943 }
1944}
1945
1946impl<R: Runtime> std::fmt::Debug for Moonbase<R> {
1947 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1948 f.debug_struct("Moonbase")
1949 .field("license_file", &self.0.store.path())
1950 .field("licensed", &self.is_licensed())
1951 .finish_non_exhaustive()
1952 }
1953}