use std::collections::{BTreeMap, VecDeque};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use async_trait::async_trait;
use pointlock_ir::{
ActionOutcome, ActionResult, AssetRef, EventCursor, FeatureId, Hash, Observation,
ReconcileResult,
};
use pointlock_provider_devicerail::{DEFAULT_DAEMON_COMMAND, DEFAULT_SHUTDOWN_GRACE_MS, SpawnSpec};
use pointlock_provider_kit::lockfile::{
CapabilityAttestation, CapabilityLockfile, LockfileDevice, LockfileHello, LockfileProvider,
PeerInfo, ProtocolVersion,
};
use pointlock_provider_kit::manifest::{PlatformKind, ProviderManifest};
use pointlock_provider_kit::{
BoundActionCall, CancellationToken, EvidenceStream, FakeHandle, FakeProvider, ObserveRequest,
Provider, ProviderError, ProviderSession, ScriptedOutcome, SessionHealth, SessionOutcome,
UiSnapshotOutcome, VerdictWrite,
};
pub const PROVIDER_NAME: &str = "devicerail";
pub const FAKE_REGISTRATION: &str = "fake";
pub const DEVICERAIL_REGISTRATION: &str = "devicerail";
pub const DEFAULT_DEVICE_ID: &str = "fake-device-1";
pub const DEVICERAIL_DEFAULT_DEVICE_ID: &str = "mock-1";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Registration {
Fake,
DeviceRail,
}
pub fn registration(name: &str) -> Result<Registration, String> {
match name {
FAKE_REGISTRATION => Ok(Registration::Fake),
DEVICERAIL_REGISTRATION => Ok(Registration::DeviceRail),
other => Err(format!(
"provider registration '{other}' is unknown; v0.1 registers '{FAKE_REGISTRATION}' \
(the M0 mock) and '{DEVICERAIL_REGISTRATION}' (the real DeviceRail provider), both \
assembled under the v0.1 provider name '{PROVIDER_NAME}'"
)),
}
}
const MOCK_LOCK_ATTESTED_AT: &str = "1970-01-01T00:00:00Z";
const MOCK_SESSION_ATTESTED_AT: &str = "1970-01-01T00:00:01Z";
pub struct FakeAssembly {
provider: FakeProvider,
manifest: ProviderManifest,
lockfile: CapabilityLockfile,
}
pub fn assemble_fake() -> FakeAssembly {
let provider = FakeProvider::new(VecDeque::new());
let mut manifest = provider.manifest().clone();
manifest.name = PROVIDER_NAME.to_owned();
let lockfile = synthesize_lockfile(&manifest);
FakeAssembly {
provider,
manifest,
lockfile,
}
}
fn synthesize_lockfile(manifest: &ProviderManifest) -> CapabilityLockfile {
let mut lockfile = CapabilityLockfile {
provider: LockfileProvider {
name: manifest.name.clone(),
version: manifest.version.clone(),
},
attested_at: MOCK_LOCK_ATTESTED_AT.to_owned(),
hello: LockfileHello {
protocol_selected: ProtocolVersion { major: 1, minor: 5 },
features_enabled: manifest.features.guaranteed.clone(),
server: PeerInfo {
name: "pointlock-fake-daemon".to_owned(),
version: manifest.version.clone(),
},
},
device: LockfileDevice {
platform: PlatformKind::Android,
actions: manifest
.known_actions
.iter()
.filter(|action| !action.synthetic)
.cloned()
.collect(),
},
digest: Hash::new(format!("sha256:{}", "0".repeat(64)))
.expect("the all-zero placeholder digest is grammatical"),
};
lockfile.seal();
lockfile
}
impl FakeAssembly {
pub fn manifest(&self) -> &ProviderManifest {
&self.manifest
}
pub fn lockfile(&self) -> &CapabilityLockfile {
&self.lockfile
}
pub fn platform(&self) -> String {
serde_json::to_value(self.lockfile.device.platform)
.ok()
.and_then(|value| value.as_str().map(str::to_owned))
.expect("PlatformKind serializes to a string literal")
}
pub async fn open_session(
&self,
device_id: &str,
required_features: Vec<FeatureId>,
stop_after: Option<StopAfterPlan>,
) -> Result<Box<dyn ProviderSession>, ProviderError> {
let mut opts = self.provider.default_open_options();
opts.device_id = device_id.to_owned();
opts.required_features = required_features;
let inner = self.provider.open_session(opts).await?;
let attestation =
CapabilityAttestation::from_lockfile(&self.lockfile, MOCK_SESSION_ATTESTED_AT);
Ok(Box::new(EchoSession {
inner,
attestation,
handle: self.provider.handle(),
device_id: device_id.to_owned(),
stop_after,
}))
}
}
pub struct StopAfterPlan {
pub token: CancellationToken,
pub remaining: AtomicUsize,
}
struct EchoSession {
inner: Box<dyn ProviderSession>,
attestation: CapabilityAttestation,
handle: FakeHandle,
device_id: String,
stop_after: Option<StopAfterPlan>,
}
impl EchoSession {
fn echo_outcome(&self, output: serde_json::Value) -> ScriptedOutcome {
ScriptedOutcome::Terminal(ActionOutcome::Succeeded {
result: Box::new(ActionResult {
call_id: String::new(),
started_at_ms: 0,
finished_at_ms: 0,
output,
before: None,
after: Some(self.handle.make_observation_for(&self.device_id, None)),
evidence: Vec::new(),
execution: None,
}),
})
}
}
#[async_trait]
impl ProviderSession for EchoSession {
fn attestation(&self) -> &CapabilityAttestation {
&self.attestation
}
async fn execute(
&self,
call: BoundActionCall,
cancel: Option<CancellationToken>,
) -> Result<ActionOutcome, ProviderError> {
self.handle
.push_script(self.echo_outcome(call.arguments.clone()));
let outcome = self.inner.execute(call, cancel).await;
if let Some(plan) = &self.stop_after
&& plan.remaining.fetch_sub(1, Ordering::SeqCst) == 1
{
plan.token.cancel();
}
outcome
}
async fn observe(
&self,
req: ObserveRequest,
cancel: Option<CancellationToken>,
) -> Result<Observation, ProviderError> {
self.inner.observe(req, cancel).await
}
async fn ui_snapshot(&self, observation_id: &str) -> Result<UiSnapshotOutcome, ProviderError> {
self.inner.ui_snapshot(observation_id).await
}
async fn reconcile(
&self,
call_id: &str,
issuing: &EventCursor,
) -> Result<ReconcileResult, ProviderError> {
self.inner.reconcile(call_id, issuing).await
}
async fn fetch_evidence(&self, asset: &AssetRef) -> Result<EvidenceStream, ProviderError> {
self.inner.fetch_evidence(asset).await
}
async fn record_verdict(&self, verdict: VerdictWrite) -> Result<(), ProviderError> {
self.inner.record_verdict(verdict).await
}
async fn current_cursor(&self) -> Result<EventCursor, ProviderError> {
self.inner.current_cursor().await
}
async fn health(&self) -> Result<SessionHealth, ProviderError> {
self.inner.health().await
}
async fn end(
&self,
outcome: SessionOutcome,
reason: Option<String>,
) -> Result<(), ProviderError> {
self.inner.end(outcome, reason).await
}
}
static EVIDENCE_DIR_COUNTER: AtomicUsize = AtomicUsize::new(0);
pub struct DeviceRailAssembly {
spawn: SpawnSpec,
}
impl DeviceRailAssembly {
pub fn new(
daemon_cmd: Option<&Path>,
daemon_env: Vec<(String, String)>,
) -> Result<Self, String> {
let mut env: BTreeMap<String, String> =
BTreeMap::from([("DEVICERAIL_ANDROID".to_owned(), "off".to_owned())]);
for (key, value) in daemon_env {
env.insert(key, value);
}
let evidence_dir = match env.get("DEVICERAIL_EVIDENCE_DIR") {
Some(pinned) => PathBuf::from(pinned),
None => {
let dir = std::env::temp_dir().join(format!(
"pointlock-devicerail-evidence-{}-{}",
std::process::id(),
EVIDENCE_DIR_COUNTER.fetch_add(1, Ordering::Relaxed),
));
env.insert(
"DEVICERAIL_EVIDENCE_DIR".to_owned(),
dir.to_string_lossy().into_owned(),
);
dir
}
};
std::fs::create_dir_all(&evidence_dir).map_err(|err| {
format!(
"cannot create the daemon evidence directory {}: {err}",
evidence_dir.display()
)
})?;
Ok(DeviceRailAssembly {
spawn: SpawnSpec {
command: daemon_cmd
.map(|path| path.to_string_lossy().into_owned())
.unwrap_or_else(|| DEFAULT_DAEMON_COMMAND.to_owned()),
args: Vec::new(),
env,
cwd: Some(evidence_dir.to_string_lossy().into_owned()),
shutdown_grace_ms: DEFAULT_SHUTDOWN_GRACE_MS,
},
})
}
pub fn spawn_spec(&self) -> &SpawnSpec {
&self.spawn
}
pub fn endpoint(&self) -> serde_json::Value {
serde_json::json!({ "spawn": self.spawn })
}
}
pub struct StopAfterSession {
inner: Box<dyn ProviderSession>,
plan: StopAfterPlan,
}
impl StopAfterSession {
pub fn new(inner: Box<dyn ProviderSession>, plan: StopAfterPlan) -> Self {
StopAfterSession { inner, plan }
}
}
#[async_trait]
impl ProviderSession for StopAfterSession {
fn attestation(&self) -> &CapabilityAttestation {
self.inner.attestation()
}
async fn execute(
&self,
call: BoundActionCall,
cancel: Option<CancellationToken>,
) -> Result<ActionOutcome, ProviderError> {
let outcome = self.inner.execute(call, cancel).await;
if self.plan.remaining.fetch_sub(1, Ordering::SeqCst) == 1 {
self.plan.token.cancel();
}
outcome
}
async fn observe(
&self,
req: ObserveRequest,
cancel: Option<CancellationToken>,
) -> Result<Observation, ProviderError> {
self.inner.observe(req, cancel).await
}
async fn ui_snapshot(&self, observation_id: &str) -> Result<UiSnapshotOutcome, ProviderError> {
self.inner.ui_snapshot(observation_id).await
}
async fn reconcile(
&self,
call_id: &str,
issuing: &EventCursor,
) -> Result<ReconcileResult, ProviderError> {
self.inner.reconcile(call_id, issuing).await
}
async fn fetch_evidence(&self, asset: &AssetRef) -> Result<EvidenceStream, ProviderError> {
self.inner.fetch_evidence(asset).await
}
async fn record_verdict(&self, verdict: VerdictWrite) -> Result<(), ProviderError> {
self.inner.record_verdict(verdict).await
}
async fn current_cursor(&self) -> Result<EventCursor, ProviderError> {
self.inner.current_cursor().await
}
async fn health(&self) -> Result<SessionHealth, ProviderError> {
self.inner.health().await
}
async fn end(
&self,
outcome: SessionOutcome,
reason: Option<String>,
) -> Result<(), ProviderError> {
self.inner.end(outcome, reason).await
}
}