use super::{FileDigestResolution, FileDigestScope, FileIdentity, RecordedFileDigest};
use crate::{ActionPrediction, CacheDigest, RemoteActionResult};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::PathBuf;
pub const AGENT_PROTOCOL_VERSION: u8 = 11;
pub(super) const MAX_REQUEST_BYTES: usize = 16 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PinnedFile {
pub path: PathBuf,
pub state: Option<PinnedState>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PinnedState {
pub len: u64,
pub modified_secs: u64,
pub modified_nanos: u32,
pub changed_secs: u64,
pub changed_nanos: u32,
pub inode: u64,
pub mode: u32,
}
impl PinnedFile {
pub fn describe(path: impl Into<PathBuf>) -> Option<Self> {
let path = path.into();
let state = match std::fs::metadata(&path) {
Ok(metadata) => Some(PinnedState::of(&metadata)?),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
Err(_) => return None,
};
Some(Self { path, state })
}
pub fn holds(&self) -> bool {
PinnedFile::describe(self.path.clone()).as_ref() == Some(self)
}
}
impl PinnedState {
#[cfg(unix)]
fn of(metadata: &std::fs::Metadata) -> Option<Self> {
use std::os::unix::fs::MetadataExt as _;
let modified = metadata
.modified()
.ok()?
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.ok()?;
Some(Self {
len: metadata.len(),
modified_secs: modified.as_secs(),
modified_nanos: modified.subsec_nanos(),
changed_secs: u64::try_from(metadata.ctime()).ok()?,
changed_nanos: u32::try_from(metadata.ctime_nsec()).ok()?,
inode: metadata.ino(),
mode: metadata.mode(),
})
}
#[cfg(windows)]
fn of(metadata: &std::fs::Metadata) -> Option<Self> {
use std::os::windows::fs::MetadataExt as _;
let modified = metadata
.modified()
.ok()?
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.ok()?;
let created = metadata
.created()
.ok()?
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.ok()?;
Some(Self {
len: metadata.len(),
modified_secs: modified.as_secs(),
modified_nanos: modified.subsec_nanos(),
changed_secs: created.as_secs(),
changed_nanos: created.subsec_nanos(),
inode: 0,
mode: metadata.file_attributes(),
})
}
#[cfg(not(any(unix, windows)))]
fn of(_metadata: &std::fs::Metadata) -> Option<Self> {
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AgentRequest {
Hello {
protocol: u8,
client_version: String,
},
BeginTask {
task: String,
},
CommitTask {
run: String,
},
FindBlob {
digest: CacheDigest,
},
FindBlobs {
digests: Vec<CacheDigest>,
},
StoreBlob {
digest: CacheDigest,
source: PathBuf,
},
FindActionResult {
action: CacheDigest,
},
RecordActionHit {
action: CacheDigest,
restore: RestoreStats,
crate_name: Option<String>,
},
RecordBypass {
kind: String,
},
RecordUnconsulted,
RecordCompilerInvocation {
outcome: String,
crate_name: Option<String>,
duration_ns: u64,
},
RecordActionVerification {
matched: bool,
restore: RestoreStats,
},
StoreActionResult {
result: RemoteActionResult,
},
FindActionPrediction {
task: String,
invocation: CacheDigest,
},
RecordActionPrediction {
task: String,
prediction: ActionPrediction,
},
FindExecutableIdentity {
executable: PathBuf,
environment: BTreeMap<String, Option<String>>,
},
StoreExecutableIdentity {
executable: PathBuf,
environment: BTreeMap<String, Option<String>>,
stdout: Vec<u8>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pins: Vec<PinnedFile>,
},
RecordWarning {
message: String,
},
FindFileDigests {
scope: FileDigestScope,
files: Vec<FileIdentity>,
},
RecordFileDigests {
scope: FileDigestScope,
entries: Vec<RecordedFileDigest>,
},
JoinActionPromise {
adapter: String,
invocation: CacheDigest,
},
CompleteActionPromise {
claim: String,
prediction: ActionPrediction,
},
ResolveFileDigests {
scope: FileDigestScope,
files: Vec<FileIdentity>,
},
RecordWrapperTiming {
timing: WrapperTiming,
},
RecordError {
message: String,
},
RecordDebug {
target: String,
message: String,
},
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct WrapperTiming {
pub adapter: String,
pub unit: Option<String>,
pub pid: u32,
pub start_us: u64,
pub duration_ns: u64,
pub phases_ns: std::collections::BTreeMap<String, u64>,
pub spans: Vec<WrapperSpan>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unit_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dependencies: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct WrapperSpan {
pub name: String,
pub start_ns: u64,
pub duration_ns: u64,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RestoreStats {
pub duration_ns: u64,
pub avoided_compiler_duration_ns: u64,
pub output_files: u64,
pub output_bytes: u64,
pub reflinked_output_files: u64,
pub reflinked_output_bytes: u64,
pub hardlinked_output_files: u64,
pub hardlinked_output_bytes: u64,
pub copied_output_files: u64,
pub copied_output_bytes: u64,
pub reused_output_files: u64,
pub reused_output_bytes: u64,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum AgentEvent {
WrapperTiming {
timing: WrapperTiming,
},
ActionHit {
crate_name: Option<String>,
restore: RestoreStats,
},
Bypass {
kind: String,
},
Unconsulted,
BypassedCompilation {
kind: String,
crate_name: Option<String>,
duration_ns: u64,
},
CompilerInvocation {
outcome: String,
crate_name: Option<String>,
duration_ns: u64,
},
Verification {
matched: bool,
restore: RestoreStats,
},
Warning {
message: String,
},
Error {
message: String,
},
ActionDiagnostic {
outcome: String,
crate_name: Option<String>,
diagnostic: ActionDiagnostic,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActionDiagnostic {
pub action: CacheDigest,
pub components: BTreeMap<String, CacheDigest>,
pub inputs: BTreeMap<String, CacheDigest>,
}
pub trait AgentEventObserver: Send + Sync {
fn event(&self, event: AgentEvent);
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AgentResponse {
Hello {
protocol: u8,
agent_version: String,
},
TaskBegun {
run: String,
},
TaskCommitted,
Blob {
path: Option<PathBuf>,
},
Blobs {
paths: Vec<Option<PathBuf>>,
},
Stored {
path: PathBuf,
},
ActionResult {
result: Option<RemoteActionResult>,
},
ActionHitRecorded,
ActionVerificationRecorded,
BypassRecorded,
UnconsultedRecorded,
CompilerInvocationRecorded,
ActionStored {
path: PathBuf,
},
ActionPrediction {
prediction: Option<ActionPrediction>,
},
ActionPredictionRecorded,
ExecutableIdentity {
stdout: Option<Vec<u8>>,
},
Error {
message: String,
},
WarningRecorded,
FileDigests {
digests: Vec<Option<CacheDigest>>,
},
FileDigestsRecorded,
ActionPromise {
claim: Option<String>,
prediction: Option<ActionPrediction>,
},
ActionPromiseCompleted,
FileDigestsResolved {
resolutions: Vec<FileDigestResolution>,
},
WrapperTimingRecorded,
ErrorRecorded,
DebugRecorded,
}