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