1use std::path::PathBuf;
2use std::thread;
3use std::time::Duration;
4use std::time::Instant;
5
6use anyhow::{Context, Result, anyhow, bail};
7use capulus::managed::{
8 AgentInfo, AgentServiceOptions, ManagedProduct, ManagedProductOptions, ManagedProgramOptions,
9 ManagedRedeployOptions, ManagementClient, ManagementClientOptions, ManagementRequest,
10 ManagementResponse, ServiceHardening, SocketOptions,
11};
12use semver::Version;
13
14pub const APPLICATION_SOCKET_PATH: &str = "/run/auc/agent.sock";
15pub const MANAGEMENT_SOCKET_PATH: &str = "/run/auc/capulus.sock";
16pub const ACCESS_GROUP: &str = "auc";
17const READINESS_RETRY: Duration = Duration::from_secs(1);
18
19pub fn managed_product() -> Result<ManagedProduct> {
20 ManagedProductOptions {
21 product: "auc".to_string(),
22 package: "auc-tool".to_string(),
23 version: Version::parse(env!("CARGO_PKG_VERSION"))
24 .context("auc package version is not semantic")?,
25 program: ManagedProgramOptions {
26 cargo_binary: "auc".to_string(),
27 installed_path: PathBuf::from("/usr/local/bin/auc"),
28 command_prefix: vec!["agent".to_string()],
29 },
30 service: AgentServiceOptions {
31 description: "auc machine-local passkey authenticator".to_string(),
32 command: vec!["serve".to_string()],
33 restart_delay: Duration::from_secs(2),
34 network_required: false,
35 state_directory_mode: 0o700,
36 hardening: ServiceHardening::Strict {
37 read_write_paths: vec![PathBuf::from("/var/lib/auc")],
38 device_allow: vec![PathBuf::from("/dev/uhid")],
39 },
40 },
41 application_socket: SocketOptions {
42 path: PathBuf::from(APPLICATION_SOCKET_PATH),
43 mode: 0o660,
44 group: Some(ACCESS_GROUP.to_string()),
45 },
46 management_socket: SocketOptions {
47 path: PathBuf::from(MANAGEMENT_SOCKET_PATH),
48 mode: 0o660,
49 group: Some(ACCESS_GROUP.to_string()),
50 },
51 redeploy: ManagedRedeployOptions::default(),
52 }
53 .validate()
54 .context("auc managed-product declaration is invalid")
55}
56
57pub fn application_agent_info() -> Result<AgentInfo> {
58 use crate::application::{ApplicationClient, ApplicationRequest, ApplicationResponse};
59
60 match ApplicationClient::new(APPLICATION_SOCKET_PATH).request(ApplicationRequest::Status)? {
61 ApplicationResponse::Status(status) => {
62 if !status.device_present {
63 bail!("auc UHID device is not present");
64 }
65 Ok(AgentInfo {
66 product: status.product,
67 package: status.package,
68 version: status.version,
69 protocol_major: status.protocol_major,
70 })
71 }
72 _ => Err(anyhow!(
73 "auc application health request returned the wrong response"
74 )),
75 }
76}
77
78pub fn wait_until_healthy(expected_version: &Version, timeout: Duration) -> Result<()> {
79 let product = managed_product()?;
80 let mut options = ManagementClientOptions::new(MANAGEMENT_SOCKET_PATH);
81 options.timeout = Duration::from_secs(2);
82 let management = ManagementClient::new(options);
83 let started = Instant::now();
84 let mut last_error = None;
85 while started.elapsed() < timeout {
86 let result = management
87 .request(ManagementRequest::Info)
88 .map_err(anyhow::Error::from)
89 .and_then(|response| match response {
90 ManagementResponse::Info(info) => Ok(info),
91 _ => Err(anyhow!(
92 "auc management health request returned the wrong response"
93 )),
94 })
95 .and_then(|management| {
96 let application = application_agent_info()?;
97 for info in [&management, &application] {
98 if info.product != product.name()
99 || info.package != product.package()
100 || info.version != expected_version.to_string()
101 || info.protocol_major != capulus::managed::PROTOCOL_MAJOR
102 {
103 bail!("auc agent health identity or version does not match");
104 }
105 }
106 Ok(())
107 });
108 match result {
109 Ok(()) => return Ok(()),
110 Err(error) => last_error = Some(error),
111 }
112 thread::sleep(READINESS_RETRY);
113 }
114 Err(last_error
115 .unwrap_or_else(|| anyhow!("auc readiness deadline elapsed without a response"))
116 .context("auc did not become healthy on both sockets"))
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn manifest_has_one_program_and_two_distinct_sockets() {
125 let product = managed_product().unwrap();
126 assert_eq!(product.service_name(), "auc-agent.service");
127 assert_eq!(product.application_socket_name(), "auc-agent.socket");
128 assert_eq!(product.management_socket_name(), "auc-capulus.socket");
129 assert_ne!(
130 product.application_socket_path(),
131 product.management_socket_path()
132 );
133 product
134 .installation_manifest()
135 .validate(product.name())
136 .unwrap();
137 let binaries = product
138 .installation_manifest()
139 .files
140 .into_iter()
141 .filter(|file| matches!(file, capulus::managed::ManagedFile::Binary { .. }))
142 .count();
143 assert_eq!(binaries, 1);
144 assert_eq!(product.program().cargo_binary(), "auc");
145 assert_eq!(
146 product.program().installed_path(),
147 std::path::Path::new("/usr/local/bin/auc")
148 );
149 }
150}