use std::{
collections::{BTreeSet, HashMap},
io::Write,
path::Path,
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::{Duration, Instant},
};
use tokio::sync::OnceCell;
use super::Record;
use crate::{
auth::remote::{self, Failure, Principal, Remote, Share},
store::{Execution, Receipts, Request},
};
pub(crate) const REFRESH: Duration = Duration::from_secs(1);
pub struct Session {
remote: Remote,
principal: OnceCell<Option<Principal>>,
offline: AtomicBool,
looked_up: Mutex<HashMap<crate::types::ReuseKey, Instant>>,
}
fn warn(message: &str) {
let _ = writeln!(std::io::stderr().lock(), "{message}");
}
impl Session {
pub fn open(state: Option<&Path>, repo: Option<&Path>) -> Result<Option<Arc<Self>>, String> {
Ok(remote::remote(state, repo)?.map(|remote| {
Arc::new(Self {
remote,
principal: OnceCell::new(),
offline: AtomicBool::new(false),
looked_up: Mutex::default(),
})
}))
}
fn failed<T: Default>(&self, failure: Failure) -> Result<T, String> {
if !failure.unavailable {
return Err(failure.message);
}
if !self.offline.swap(true, Ordering::SeqCst) {
warn(&format!(
"{}; continuing without the remote review store.",
failure.message.trim_end_matches('.')
));
}
Ok(T::default())
}
async fn principal(&self) -> Result<Option<&Principal>, String> {
if self.offline.load(Ordering::SeqCst) {
return Ok(None);
}
let principal = self
.principal
.get_or_try_init(|| async {
match self.remote.whoami().await {
Ok(principal) => Ok(Some(principal)),
Err(failure) => self.failed(failure),
}
})
.await?;
Ok(principal.as_ref())
}
pub async fn lookup(&self, keys: &[crate::types::ReuseKey]) -> Result<Vec<Execution>, String> {
if keys.is_empty() {
return Ok(Vec::new());
}
let Some(principal) = self.principal().await? else {
return Ok(Vec::new());
};
if !has_scope(principal, &crate::auth::remote::Scope::Read) {
return Ok(Vec::new());
}
let entries = match self.remote.lookup(keys).await {
Ok(entries) => entries,
Err(failure) => return self.failed(failure),
};
let requested: BTreeSet<_> = keys.iter().map(crate::types::ReuseKey::as_str).collect();
let mut executions = Vec::new();
for entry in entries {
let mirrored = entry.and_then(|record| {
if !requested.contains(record.key.as_str()) {
return Err("the store returned an unrequested key".into());
}
record.mirror(self.remote.url.as_str())
});
match mirrored {
Ok(execution) => executions.push(execution),
Err(error) => warn(&format!(
"Ignoring an invalid remote review record: {error}"
)),
}
}
Ok(executions)
}
pub async fn refresh(
&self,
receipts: &Receipts,
keys: Vec<crate::types::ReuseKey>,
) -> Result<(), String> {
let now = Instant::now();
let keys: Vec<_> = {
let mut looked_up = self.looked_up.lock().expect("lookup times");
keys.into_iter()
.filter(|key| {
let due = looked_up
.get(key)
.is_none_or(|at| now.duration_since(*at) >= REFRESH);
if due {
looked_up.insert(key.clone(), now);
}
due
})
.collect()
};
if keys.is_empty() || self.offline.load(Ordering::SeqCst) {
return Ok(());
}
for execution in self.lookup(&keys).await? {
receipts.mirror_execution(&execution).await?;
}
Ok(())
}
pub async fn publish_request(
&self,
receipts: &Receipts,
request: &Request,
) -> Result<(), String> {
let (Some(_), Some(execution_id)) = (&request.key, &request.execution_id) else {
return Ok(());
};
match receipts.published_execution(execution_id).await? {
Some(execution) => self.publish(&execution).await,
None => Ok(()),
}
}
async fn publish(&self, execution: &Execution) -> Result<(), String> {
let Some(principal) = self.principal().await? else {
return Ok(());
};
if !has_scope(principal, &crate::auth::remote::Scope::Publish) {
return Ok(());
}
let record = match record(execution, principal, self.remote.share) {
Ok(record) => record,
Err(reason) => {
warn(&format!("This result stays local: {reason}."));
return Ok(());
}
};
match self.remote.publish(&record).await {
Ok(_) => Ok(()),
Err(failure) => self.failed(failure),
}
}
}
pub(super) fn has_scope(principal: &Principal, scope: &crate::auth::remote::Scope) -> bool {
principal.scopes.contains(scope)
}
pub(super) fn record(
execution: &Execution,
principal: &Principal,
share: Share,
) -> Result<Record, String> {
if execution.profile.kind() == crate::config::ProfileKind::Human
&& !has_scope(principal, &crate::auth::remote::Scope::Human)
{
return Err(format!(
"remote token {} lacks the human scope",
principal.principal
));
}
Record::new(execution, share == Share::Full)
}