solana_install/
config.rs

1use {
2    crate::update_manifest::UpdateManifest,
3    serde::{Deserialize, Serialize},
4    solana_sdk::pubkey::Pubkey,
5    std::{
6        fs::{create_dir_all, File},
7        io::{self, Write},
8        path::{Path, PathBuf},
9    },
10};
11
12#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
13pub enum ExplicitRelease {
14    Semver(String),
15    Channel(String),
16}
17
18#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq)]
19pub struct Config {
20    pub json_rpc_url: String,
21    pub update_manifest_pubkey: Pubkey,
22    pub current_update_manifest: Option<UpdateManifest>,
23    pub update_poll_secs: u64,
24    pub explicit_release: Option<ExplicitRelease>,
25    pub releases_dir: PathBuf,
26    active_release_dir: PathBuf,
27}
28
29const LEGACY_FMT_LOAD_ERR: &str =
30    "explicit_release: invalid type: map, expected a YAML tag starting with '!'";
31
32impl Config {
33    pub fn new(
34        data_dir: &str,
35        json_rpc_url: &str,
36        update_manifest_pubkey: &Pubkey,
37        explicit_release: Option<ExplicitRelease>,
38    ) -> Self {
39        Self {
40            json_rpc_url: json_rpc_url.to_string(),
41            update_manifest_pubkey: *update_manifest_pubkey,
42            current_update_manifest: None,
43            update_poll_secs: 60 * 60, // check for updates once an hour
44            explicit_release,
45            releases_dir: PathBuf::from(data_dir).join("releases"),
46            active_release_dir: PathBuf::from(data_dir).join("active_release"),
47        }
48    }
49
50    fn _load(config_file: &str) -> Result<Self, io::Error> {
51        let file = File::open(config_file)?;
52        serde_yaml::from_reader(file).or_else(|err| {
53            let err_string = format!("{err:?}");
54            if err_string.contains(LEGACY_FMT_LOAD_ERR) {
55                // looks like a config written by serde_yaml <0.9.0.
56                // let's try to upgrade it
57                Self::try_migrate_08(config_file)
58                    .map_err(|_| io::Error::new(io::ErrorKind::Other, err_string))
59            } else {
60                Err(io::Error::new(io::ErrorKind::Other, err_string))
61            }
62        })
63    }
64
65    fn try_migrate_08(config_file: &str) -> Result<Self, io::Error> {
66        eprintln!("attempting to upgrade legacy config file");
67        let bak_filename = config_file.to_string() + ".bak";
68        std::fs::copy(config_file, &bak_filename)?;
69        let result = File::open(config_file).and_then(|file| {
70            serde_yaml_08::from_reader(file)
71                .map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{err:?}")))
72                .and_then(|config_08: Self| {
73                    let save = config_08._save(config_file).map(|_| config_08);
74                    if save.is_ok() {
75                        let _ = std::fs::remove_file(&bak_filename);
76                    }
77                    save
78                })
79        });
80        if result.is_err() {
81            eprintln!("config upgrade failed! restoring orignal");
82            let restored = std::fs::copy(&bak_filename, config_file)
83                .and_then(|_| std::fs::remove_file(&bak_filename));
84            if restored.is_err() {
85                eprintln!("restoration failed! original: `{bak_filename}`");
86            } else {
87                eprintln!("restoration succeeded!");
88            }
89        } else {
90            eprintln!("config upgrade succeeded!");
91        }
92        result
93    }
94
95    pub fn load(config_file: &str) -> Result<Self, String> {
96        Self::_load(config_file).map_err(|err| format!("Unable to load {config_file}: {err:?}"))
97    }
98
99    fn _save(&self, config_file: &str) -> Result<(), io::Error> {
100        let serialized = serde_yaml::to_string(self)
101            .map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{err:?}")))?;
102
103        if let Some(outdir) = Path::new(&config_file).parent() {
104            create_dir_all(outdir)?;
105        }
106        let mut file = File::create(config_file)?;
107        file.write_all(b"---\n")?;
108        file.write_all(&serialized.into_bytes())?;
109
110        Ok(())
111    }
112
113    pub fn save(&self, config_file: &str) -> Result<(), String> {
114        self._save(config_file)
115            .map_err(|err| format!("Unable to save {config_file}: {err:?}"))
116    }
117
118    pub fn active_release_dir(&self) -> &PathBuf {
119        &self.active_release_dir
120    }
121
122    pub fn active_release_bin_dir(&self) -> PathBuf {
123        self.active_release_dir.join("bin")
124    }
125
126    pub fn release_dir(&self, release_id: &str) -> PathBuf {
127        self.releases_dir.join(release_id)
128    }
129}
130
131#[cfg(test)]
132mod test {
133    use {
134        super::*,
135        scopeguard::defer,
136        std::{
137            env,
138            fs::{read_to_string, remove_file},
139        },
140    };
141
142    #[test]
143    fn test_save() {
144        let root_dir = env::var("CARGO_MANIFEST_DIR").expect("$CARGO_MANIFEST_DIR");
145        let json_rpc_url = "https://api.mainnet-beta.solana.com";
146        let pubkey = Pubkey::default();
147        let config_name = "config.yaml";
148        let config_path = format!("{root_dir}/{config_name}");
149
150        let config = Config::new(&root_dir, json_rpc_url, &pubkey, None);
151
152        assert_eq!(config.save(config_name), Ok(()));
153        defer! {
154            remove_file(&config_path).unwrap();
155        }
156
157        assert_eq!(
158            read_to_string(&config_path).unwrap(),
159            format!(
160                "---
161json_rpc_url: https://api.mainnet-beta.solana.com
162update_manifest_pubkey:
163- 0
164- 0
165- 0
166- 0
167- 0
168- 0
169- 0
170- 0
171- 0
172- 0
173- 0
174- 0
175- 0
176- 0
177- 0
178- 0
179- 0
180- 0
181- 0
182- 0
183- 0
184- 0
185- 0
186- 0
187- 0
188- 0
189- 0
190- 0
191- 0
192- 0
193- 0
194- 0
195current_update_manifest: null
196update_poll_secs: 3600
197explicit_release: null
198releases_dir: {root_dir}/releases
199active_release_dir: {root_dir}/active_release
200"
201            ),
202        );
203    }
204
205    #[test]
206    fn test_load_serde_yaml_v_0_8_config() {
207        let file_name = "config.yml";
208        let mut file = File::create(file_name).unwrap();
209        defer! {
210            remove_file(file_name).unwrap();
211        }
212
213        let root_dir = "/home/sol/.local/share/solana/install";
214
215        writeln!(
216            file,
217            "---
218json_rpc_url: \"http://api.devnet.solana.com\"
219update_manifest_pubkey:
220  - 0
221  - 0
222  - 0
223  - 0
224  - 0
225  - 0
226  - 0
227  - 0
228  - 0
229  - 0
230  - 0
231  - 0
232  - 0
233  - 0
234  - 0
235  - 0
236  - 0
237  - 0
238  - 0
239  - 0
240  - 0
241  - 0
242  - 0
243  - 0
244  - 0
245  - 0
246  - 0
247  - 0
248  - 0
249  - 0
250  - 0
251  - 0
252current_update_manifest: ~
253update_poll_secs: 3600
254explicit_release:
255  Semver: 1.13.6
256releases_dir: {root_dir}/releases
257active_release_dir: {root_dir}/active_release
258"
259        )
260        .unwrap();
261        let config = Config::load(file_name).unwrap();
262        assert_eq!(
263            config,
264            Config {
265                json_rpc_url: String::from("http://api.devnet.solana.com"),
266                update_manifest_pubkey: Pubkey::default(),
267                current_update_manifest: None,
268                update_poll_secs: 3600,
269                explicit_release: Some(ExplicitRelease::Semver(String::from("1.13.6"))),
270                releases_dir: PathBuf::from(format!("{root_dir}/releases")),
271                active_release_dir: PathBuf::from(format!("{root_dir}/active_release")),
272            },
273        );
274    }
275}