Skip to main content

canic_host/install_root/
state.rs

1use crate::release_set::dfx_root;
2use serde::{Deserialize, Serialize};
3use std::{fs, path::Path, path::PathBuf};
4
5pub(super) const INSTALL_STATE_SCHEMA_VERSION: u32 = 1;
6const CURRENT_NETWORK_FILE: &str = "current-network";
7const CURRENT_FLEET_FILE: &str = "current-fleet";
8
9///
10/// InstallState
11///
12
13#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
14pub struct InstallState {
15    pub schema_version: u32,
16    pub fleet: String,
17    pub installed_at_unix_secs: u64,
18    pub network: String,
19    pub root_target: String,
20    pub root_canister_id: String,
21    pub root_build_target: String,
22    pub workspace_root: String,
23    pub dfx_root: String,
24    pub config_path: String,
25    pub release_set_manifest_path: String,
26}
27
28///
29/// FleetSummary
30///
31
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct FleetSummary {
34    pub name: String,
35    pub current: bool,
36    pub state: InstallState,
37}
38
39/// Read the persisted install state for one project/network when present.
40pub(super) fn read_install_state(
41    dfx_root: &Path,
42    network: &str,
43) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
44    if let Some(fleet) = read_selected_fleet_name(dfx_root, network)? {
45        return read_fleet_install_state(dfx_root, network, &fleet);
46    }
47
48    Ok(None)
49}
50
51/// Read a named fleet install state for one project/network when present.
52pub(super) fn read_fleet_install_state(
53    dfx_root: &Path,
54    network: &str,
55    fleet: &str,
56) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
57    validate_fleet_name(fleet)?;
58    let path = fleet_install_state_path(dfx_root, network, fleet);
59    if !path.is_file() {
60        return Ok(None);
61    }
62
63    let bytes = fs::read(&path)?;
64    let state: InstallState = serde_json::from_slice(&bytes)?;
65    Ok(Some(state))
66}
67
68/// Read the install state for the discovered current project/network.
69pub fn read_current_install_state(
70    network: &str,
71) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
72    let dfx_root = dfx_root()?;
73    read_install_state(&dfx_root, network)
74}
75
76/// Read the selected default network for the discovered current project.
77pub fn read_current_network_name() -> Result<Option<String>, Box<dyn std::error::Error>> {
78    let dfx_root = dfx_root()?;
79    read_selected_network_name(&dfx_root)
80}
81
82/// Select the current default network for the discovered current project.
83pub fn select_current_network_name(network: &str) -> Result<(), Box<dyn std::error::Error>> {
84    let dfx_root = dfx_root()?;
85    write_current_network_name(&dfx_root, network)
86}
87
88/// Read the selected fleet name for the discovered current project/network.
89pub fn read_current_fleet_name(
90    network: &str,
91) -> Result<Option<String>, Box<dyn std::error::Error>> {
92    let dfx_root = dfx_root()?;
93    read_selected_fleet_name(&dfx_root, network)
94}
95
96/// Read either a named fleet state or the selected current fleet state.
97pub fn read_current_or_fleet_install_state(
98    network: &str,
99    fleet: Option<&str>,
100) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
101    let dfx_root = dfx_root()?;
102    match fleet {
103        Some(fleet) => read_fleet_install_state(&dfx_root, network, fleet),
104        None => read_install_state(&dfx_root, network),
105    }
106}
107
108/// List installed fleets for the current project/network.
109pub fn list_current_fleets(network: &str) -> Result<Vec<FleetSummary>, Box<dyn std::error::Error>> {
110    let dfx_root = dfx_root()?;
111    list_fleets(&dfx_root, network)
112}
113
114/// List installed fleets for one project/network.
115pub(super) fn list_fleets(
116    dfx_root: &Path,
117    network: &str,
118) -> Result<Vec<FleetSummary>, Box<dyn std::error::Error>> {
119    let current = read_selected_fleet_name(dfx_root, network)?;
120    let mut fleets = Vec::new();
121    let dir = fleets_dir(dfx_root, network);
122    if dir.is_dir() {
123        for entry in fs::read_dir(&dir)? {
124            let entry = entry?;
125            let path = entry.path();
126            if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
127                continue;
128            }
129            let Some(name) = path.file_stem().and_then(|stem| stem.to_str()) else {
130                continue;
131            };
132            if let Some(state) = read_fleet_install_state(dfx_root, network, name)? {
133                fleets.push(FleetSummary {
134                    name: name.to_string(),
135                    current: current.as_deref() == Some(name),
136                    state,
137                });
138            }
139        }
140    }
141
142    fleets.sort_by(|left, right| left.name.cmp(&right.name));
143    Ok(fleets)
144}
145
146/// Select one installed fleet as the current project/network default.
147pub fn select_current_fleet(
148    network: &str,
149    fleet: &str,
150) -> Result<InstallState, Box<dyn std::error::Error>> {
151    let dfx_root = dfx_root()?;
152    select_fleet(&dfx_root, network, fleet)
153}
154
155/// Select one fleet name as the current project/network default before install.
156pub fn select_current_fleet_name(
157    network: &str,
158    fleet: &str,
159) -> Result<(), Box<dyn std::error::Error>> {
160    let dfx_root = dfx_root()?;
161    write_current_fleet_name(&dfx_root, network, fleet)
162}
163
164/// Clear current-fleet markers that point at one deleted fleet.
165pub fn clear_current_fleet_name_if_matches(
166    fleet: &str,
167) -> Result<Vec<String>, Box<dyn std::error::Error>> {
168    let dfx_root = dfx_root()?;
169    clear_selected_fleet_name_if_matches(&dfx_root, fleet)
170}
171
172/// Select one installed fleet for one project/network.
173fn select_fleet(
174    dfx_root: &Path,
175    network: &str,
176    fleet: &str,
177) -> Result<InstallState, Box<dyn std::error::Error>> {
178    let Some(state) = read_fleet_install_state(dfx_root, network, fleet)? else {
179        return Err(format!("unknown fleet {fleet} on network {network}").into());
180    };
181    write_current_fleet_name(dfx_root, network, fleet)?;
182    Ok(state)
183}
184
185/// Return the project-local current-network pointer path.
186#[must_use]
187pub(super) fn current_network_path(dfx_root: &Path) -> PathBuf {
188    dfx_root.join(".canic").join(CURRENT_NETWORK_FILE)
189}
190
191/// Return the project-local state path for one named fleet.
192#[must_use]
193pub(super) fn fleet_install_state_path(dfx_root: &Path, network: &str, fleet: &str) -> PathBuf {
194    fleets_dir(dfx_root, network).join(format!("{fleet}.json"))
195}
196
197/// Return the project-local current-fleet pointer path for one network.
198#[must_use]
199pub(super) fn current_fleet_path(dfx_root: &Path, network: &str) -> PathBuf {
200    dfx_root
201        .join(".canic")
202        .join(network)
203        .join(CURRENT_FLEET_FILE)
204}
205
206// Return the directory that owns named fleet state files.
207fn fleets_dir(dfx_root: &Path, network: &str) -> PathBuf {
208    dfx_root.join(".canic").join(network).join("fleets")
209}
210
211// Persist the completed install state under the project-local `.canic` directory.
212pub(super) fn write_install_state(
213    dfx_root: &Path,
214    network: &str,
215    state: &InstallState,
216) -> Result<PathBuf, Box<dyn std::error::Error>> {
217    validate_fleet_name(&state.fleet)?;
218    let path = fleet_install_state_path(dfx_root, network, &state.fleet);
219    if let Some(parent) = path.parent() {
220        fs::create_dir_all(parent)?;
221    }
222    fs::write(&path, serde_json::to_vec_pretty(state)?)?;
223    write_current_fleet_name(dfx_root, network, &state.fleet)?;
224    Ok(path)
225}
226
227// Read the selected default network for one project.
228fn read_selected_network_name(
229    dfx_root: &Path,
230) -> Result<Option<String>, Box<dyn std::error::Error>> {
231    let path = current_network_path(dfx_root);
232    if !path.is_file() {
233        return Ok(None);
234    }
235
236    let name = fs::read_to_string(path)?.trim().to_string();
237    validate_network_name(&name)?;
238    Ok(Some(name))
239}
240
241// Write the selected default network for one project.
242fn write_current_network_name(
243    dfx_root: &Path,
244    network: &str,
245) -> Result<(), Box<dyn std::error::Error>> {
246    validate_network_name(network)?;
247    let path = current_network_path(dfx_root);
248    if let Some(parent) = path.parent() {
249        fs::create_dir_all(parent)?;
250    }
251    fs::write(path, format!("{network}\n"))?;
252    Ok(())
253}
254
255// Read the selected fleet name for one project/network.
256pub(super) fn read_selected_fleet_name(
257    dfx_root: &Path,
258    network: &str,
259) -> Result<Option<String>, Box<dyn std::error::Error>> {
260    let path = current_fleet_path(dfx_root, network);
261    if !path.is_file() {
262        return Ok(None);
263    }
264
265    let name = fs::read_to_string(path)?.trim().to_string();
266    validate_fleet_name(&name)?;
267    Ok(Some(name))
268}
269
270// Write the selected fleet name for one project/network.
271fn write_current_fleet_name(
272    dfx_root: &Path,
273    network: &str,
274    fleet: &str,
275) -> Result<(), Box<dyn std::error::Error>> {
276    validate_fleet_name(fleet)?;
277    let path = current_fleet_path(dfx_root, network);
278    if let Some(parent) = path.parent() {
279        fs::create_dir_all(parent)?;
280    }
281    fs::write(path, format!("{fleet}\n"))?;
282    Ok(())
283}
284
285// Remove selected-fleet marker files that point at a deleted fleet.
286pub(super) fn clear_selected_fleet_name_if_matches(
287    dfx_root: &Path,
288    fleet: &str,
289) -> Result<Vec<String>, Box<dyn std::error::Error>> {
290    let canic_dir = dfx_root.join(".canic");
291    if !canic_dir.is_dir() {
292        return Ok(Vec::new());
293    }
294
295    let mut cleared = Vec::new();
296    for entry in fs::read_dir(canic_dir)? {
297        let entry = entry?;
298        if !entry.file_type()?.is_dir() {
299            continue;
300        }
301        let Some(network) = entry.file_name().to_str().map(str::to_string) else {
302            continue;
303        };
304        if validate_network_name(&network).is_err() {
305            continue;
306        }
307        let marker = current_fleet_path(dfx_root, &network);
308        if marker.is_file() && fs::read_to_string(&marker)?.trim() == fleet {
309            fs::remove_file(marker)?;
310            cleared.push(network);
311        }
312    }
313
314    cleared.sort();
315    Ok(cleared)
316}
317
318// Keep fleet names filesystem-safe and easy to type in commands.
319pub(super) fn validate_fleet_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
320    let valid = !name.is_empty()
321        && name
322            .bytes()
323            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
324    if valid {
325        Ok(())
326    } else {
327        Err(format!("invalid fleet name {name:?}; use letters, numbers, '-' or '_'").into())
328    }
329}
330
331// Keep network names safe for `.canic/<network>` state paths.
332fn validate_network_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
333    let valid = !name.is_empty()
334        && name
335            .bytes()
336            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
337    if valid {
338        Ok(())
339    } else {
340        Err(format!("invalid network name {name:?}; use letters, numbers, '-' or '_'").into())
341    }
342}