initial run loop

This commit is contained in:
Jędrzej Stuczyński
2023-11-08 12:07:23 +00:00
parent 2e077ca946
commit 22da01ccd4
10 changed files with 347 additions and 333 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ use crate::config::{default_config_filepath, Config, BIN_DIR, GENESIS_DIR};
use crate::daemon::Daemon;
use crate::env::Env;
use crate::error::NymvisorError;
use crate::upgrades::{UpgradeInfo, UpgradePlan};
use crate::upgrades::types::{UpgradeInfo, UpgradePlan};
use nym_bin_common::build_information::BinaryBuildInformationOwned;
use nym_bin_common::logging::{setup_logging, setup_tracing_logger};
use nym_bin_common::output_format::OutputFormat;
+1 -1
View File
@@ -45,7 +45,7 @@ pub(crate) fn execute(args: Args) -> Result<(), NymvisorError> {
// - one for managing the daemon launcher
// - the other one for watching the upgrade plan file
// - the last one for polling upstream source for upgrade info
// so once the daemon has finished, for whatever reason, abort the file watcher and upstream poller and terminate the nymvisor
// so once the daemon has finished, for whatever reason, abort the file watcher and upstream poller to terminate the nymvisor
todo!()
// // spawn the root task
+1
View File
@@ -115,6 +115,7 @@ pub(crate) struct ExecutingDaemon {
interrupt_sent: bool,
interrupt_handle: Option<Arc<Notify>>,
// TODO: can we maybe get rid of that dynamic dispatch here in favour of concrete types?
// interrupted: Option<Pin<Box<Notified<'static>>>>,
interrupted: Pin<Box<dyn Future<Output = ()> + Send + Sync>>,
kill_timeout: Option<Pin<Box<Sleep>>>,
-35
View File
@@ -1,37 +1,2 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use futures::future::Aborted;
use futures::stream::AbortHandle;
use std::future::Future;
use tokio::task::{JoinError, JoinHandle};
pub(crate) struct TaskHandle<T>
where
T: Send + 'static,
{
abort_handle: AbortHandle,
// join_handle: JoinHandle<F::Output>,
join_handle: JoinHandle<Result<T, Aborted>>,
}
impl<T> TaskHandle<T>
where
T: Send + 'static,
{
pub(crate) fn new(
abort_handle: AbortHandle,
join_handle: JoinHandle<Result<T, Aborted>>,
) -> Self {
TaskHandle {
abort_handle,
join_handle,
}
}
// TODO: change return type
pub(crate) async fn abort_and_finalize(self) -> Result<Result<T, Aborted>, JoinError> {
self.abort_handle.abort();
self.join_handle.await
}
}
+77 -35
View File
@@ -4,16 +4,21 @@
use crate::config::Config;
use crate::daemon::Daemon;
use crate::error::NymvisorError;
use crate::upgrades::{UpgradeInfo, UpgradePlan};
use crate::upgrades::{
types::{UpgradeInfo, UpgradePlan},
upgrade_binary,
};
use async_file_watcher::FileWatcherEventReceiver;
use futures::future::{FusedFuture, OptionFuture};
use futures::{FutureExt, StreamExt};
use nym_task::signal::wait_for_signal;
use std::sync::Arc;
use std::time::Duration;
use time::OffsetDateTime;
use tokio::pin;
use tokio::sync::Notify;
use tokio::time::Sleep;
use tracing::{error, info, warn};
use tokio::time::{sleep, Sleep};
use tracing::{debug, error, info, warn};
pub(crate) struct DaemonLauncher {
config: Config,
@@ -25,15 +30,43 @@ impl DaemonLauncher {
todo!()
}
// responsible for running until exit or until update is detected
pub(crate) async fn run(&mut self, args: Vec<String>) -> Result<(), NymvisorError> {
// the full upgrade process process, i.e. run until upgrade, do backup and perform the upgrade.
// returns a boolean indicating whether the process should get restarted
pub(crate) async fn run(&mut self, args: Vec<String>) -> Result<bool, NymvisorError> {
let upgrade_available = self.wait_for_upgrade_or_termination(args.clone()).await?;
if !upgrade_available {
return Ok(false);
}
self.perform_backup()?;
upgrade_binary()?;
todo!()
}
fn check_upgrade_plan_changes(&self) {
//
/// this function gets called whenever the file watcher detects changes in the upgrade plan file
/// it returns an option indicating when the next upgrade should be performed
fn check_upgrade_plan_changes(&self) -> Option<Duration> {
info!("checking changes in the upgrade plan file...");
let current_upgrade_plan = match UpgradePlan::try_load(self.config.upgrade_plan_filepath())
{
Ok(upgrade_plan) => upgrade_plan,
Err(err) => {
error!("failed to read the current upgrade plan: {err}");
return None;
}
};
if let Some(next) = current_upgrade_plan.next_upgrade() {
let now = OffsetDateTime::now_utc();
Some((next.upgrade_time - now).try_into().unwrap_or_default())
} else {
None
}
}
// responsible for running until exit or until update is detected
async fn wait_for_upgrade_or_termination(
&mut self,
args: Vec<String>,
@@ -45,6 +78,8 @@ impl DaemonLauncher {
let current_upgrade_plan = UpgradePlan::try_load(self.config.upgrade_plan_filepath())?;
let next = current_upgrade_plan.next_upgrade();
// TODO: /\
let mut running_daemon = daemon.execute_async(args)?;
let interrupt_handle = running_daemon.interrupt_handle();
@@ -53,12 +88,10 @@ impl DaemonLauncher {
let mut upgrade_timeout: OptionFuture<_> = None.into();
upgrade_timeout =
Some(Box::pin(tokio::time::sleep(Duration::from_secs(123))).fuse()).into();
let signal_fut = wait_for_signal();
pin!(signal_fut);
let sig_fut = wait_for_signal();
// note: this has to be in a loop because `upgrade_plan_watcher` might receive events that do not necessarily trigger the upgrade
let mut received_interrupt = false;
loop {
tokio::select! {
daemon_res = &mut fused_runner => {
@@ -67,43 +100,52 @@ impl DaemonLauncher {
info!("it finished with the following exit status: {exit_status}");
return Ok(false)
}
_ = &mut self.upgrade_plan_watcher.next() => {
//
event = &mut self.upgrade_plan_watcher.next() => {
let Some(event) = event else {
// this is a critical failure since the file watcher task should NEVER terminate by itself
error!("CRITICAL FAILURE: the upgrade plan watcher channel got closed");
panic!("CRITICAL FAILURE: the upgrade plan watcher channel got closed")
};
println!("the file has changed - {event:?}");
debug!("the file has changed - {event:?}");
if let Some(next_upgrade) = self.check_upgrade_plan_changes() {
info!("setting the upgrade timeout to {}", humantime::format_duration(next_upgrade));
upgrade_timeout = Some(Box::pin(sleep(next_upgrade)).fuse()).into()
}
}
_ = &mut upgrade_timeout, if !upgrade_timeout.is_terminated() => {
info!("the upgrade timeout has elapsed. the daemon will be now stopped in order to perform the upgrade");
break
}
_ = &mut signal_fut => {
received_interrupt = true;
info!("the nymvisor has received an interrupt. the daemon will be now stopped before exiting");
break
}
}
}
// if the runner hasn't terminated by itself (which should be the almost every single time!)
// send the interrupt and wait for it to be done
if fused_runner.is_terminated() {
todo!("error case")
return Ok(false);
}
interrupt_handle.interrupt_daemon();
let res = fused_runner.await;
/*
match fused_runner.await {
Ok(exit_status) => {
info!("the daemon finished with the following exit status: {exit_status}");
}
Err(err) => {
warn!("the daemon finished with an error: {err}");
}
}
tokio select on:
- daemon terminating
- upgrade-plan.json changes
- https://nymtech.net/.wellknown/<DAEMON_NAME>/update-info.json changes
// todo: maybe move to a higher layer
- signals received (to propagate them to daemon before terminating to prevent creating zombie processes)
*/
todo!()
// if we received an interrupt, don't try to perform upgrade, just exit the nymvisor
Ok(!received_interrupt)
}
async fn perform_backup(&self) {
fn perform_backup(&self) -> Result<(), NymvisorError> {
todo!()
}
}
@@ -3,23 +3,18 @@
use crate::config::Config;
use crate::error::NymvisorError;
use crate::helpers::TaskHandle;
use async_file_watcher::{AsyncFileWatcher, FileWatcherEventReceiver, NotifyResult};
use futures::channel::mpsc;
use futures::future::{AbortHandle, Abortable};
use tokio::task::JoinHandle;
pub(crate) fn start_upgrade_plan_watcher(
config: &Config,
) -> Result<(FileWatcherEventReceiver, TaskHandle<NotifyResult<()>>), NymvisorError> {
) -> Result<(FileWatcherEventReceiver, JoinHandle<NotifyResult<()>>), NymvisorError> {
let (events_sender, events_receiver) = mpsc::unbounded();
let mut watcher =
AsyncFileWatcher::new_file_changes_watcher(config.upgrade_plan_filepath(), events_sender)?;
let (abort_handle, abort_registration) = AbortHandle::new_pair();
let join_handle =
tokio::spawn(async move { Abortable::new(watcher.watch(), abort_registration).await });
let join_handle = tokio::spawn(async move { watcher.watch().await });
let task_handle = TaskHandle::new(abort_handle, join_handle);
Ok((events_receiver, task_handle))
Ok((events_receiver, join_handle))
}
+4 -9
View File
@@ -3,10 +3,9 @@
use crate::config::Config;
use crate::error::NymvisorError;
use crate::helpers::TaskHandle;
use crate::upgrades::{UpgradeInfo, UpgradePlan};
use futures::future::{AbortHandle, Abortable};
use crate::upgrades::types::{UpgradeInfo, UpgradePlan};
use reqwest::get;
use tokio::task::JoinHandle;
use tracing::{error, warn};
pub(crate) struct UpstreamPoller {
@@ -71,11 +70,7 @@ impl UpstreamPoller {
}
}
pub(crate) async fn start(mut self) -> TaskHandle<()> {
let (abort_handle, abort_registration) = AbortHandle::new_pair();
let join_handle =
tokio::spawn(async move { Abortable::new(self.run(), abort_registration).await });
TaskHandle::new(abort_handle, join_handle)
pub(crate) async fn start(mut self) -> JoinHandle<()> {
tokio::spawn(async move { self.run().await })
}
}
@@ -1,2 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
+10 -241
View File
@@ -1,251 +1,20 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::config::GENESIS_DIR;
use crate::error::NymvisorError;
use nym_bin_common::build_information::BinaryBuildInformationOwned;
use serde::{Deserialize, Serialize};
use serde_helpers::{base64, option_offsetdatetime};
use std::collections::{HashMap, VecDeque};
use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use std::{fs, io};
use time::OffsetDateTime;
use tracing::error;
use url::Url;
mod http_upstream;
mod serde_helpers;
pub(crate) mod types;
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub struct UpgradePlan {
// metadata indicating save location of the underlying file
#[serde(skip)]
_save_path: Option<PathBuf>,
pub(crate) fn upgrade_binary() -> Result<(), NymvisorError> {
// lock
current: UpgradeInfo,
/*
if binary already exist => swap symlink, write history and we're done
otherwise deal with download
next: VecDeque<UpgradeInfo>,
}
impl UpgradePlan {
pub(crate) fn new(current: UpgradeInfo) -> Self {
UpgradePlan {
_save_path: None,
current,
next: VecDeque::new(),
}
}
fn push_next_upgrade(&mut self, upgrade: UpgradeInfo) {
self.next.push_back(upgrade);
}
fn update_on_disk(&self) -> Result<(), NymvisorError> {
// it should be impossible to update an existing upgrade plan that wasn't loaded from disk
assert!(self._save_path.is_some());
// safety: the except here is fine as this failure implies failure in the underlying logic of the code
// as opposed to user error
#[allow(clippy::expect_used)]
let save_path = self
._save_path
.as_ref()
.expect("loaded upgrade plan does not have an associate save path!");
let file = OpenOptions::new()
.write(true)
.truncate(true)
.open(save_path)
.map_err(|source| NymvisorError::UpgradePlanSaveFailure {
path: save_path.to_path_buf(),
source,
})?;
// we're not using any non-standard serializer and thus the serialization should not ever fail
#[allow(clippy::expect_used)]
serde_json::to_writer_pretty(file, self)
.expect("unexpected UpgradeInfo serialization failure");
Ok(())
}
pub(crate) fn insert_new_upgrade(&mut self, upgrade: UpgradeInfo) -> Result<(), NymvisorError> {
self.push_next_upgrade(upgrade);
self.update_on_disk()
}
pub(crate) fn current(&self) -> &UpgradeInfo {
&self.current
}
pub(crate) fn next_upgrade(&self) -> Option<&UpgradeInfo> {
self.next.front()
}
pub(crate) fn has_planned(&self, upgrade: &UpgradeInfo) -> bool {
for planned in &self.next {
if planned.version == upgrade.version {
if planned.name != upgrade.name {
// TODO: should we maybe return a hard error here instead?
error!("we have already a planned upgrade for version {} under name '{}' which differs from provided '{}'", planned.version, planned.name, upgrade.name);
}
return true;
}
}
false
}
// pub(crate) fn update_current(&mut self) -> Result<(), NymvisorError> {
//
// }
pub(crate) fn save_new<P: AsRef<Path>>(&self, path: P) -> Result<(), NymvisorError> {
debug_assert!(self._save_path.is_none());
let path = path.as_ref();
let file = OpenOptions::new()
.create_new(true)
.write(true)
.open(path)
.map_err(|source| NymvisorError::UpgradePlanSaveFailure {
path: path.to_path_buf(),
source,
})?;
// we're not using any non-standard serializer and thus the serialization should not ever fail
#[allow(clippy::expect_used)]
serde_json::to_writer_pretty(file, self)
.expect("unexpected UpgradeInfo serialization failure");
Ok(())
}
pub(crate) fn try_load<P: AsRef<Path>>(path: P) -> Result<Self, NymvisorError> {
let path = path.as_ref();
let mut upgrade_plan: UpgradePlan = fs::File::open(path)
.and_then(|file| {
serde_json::from_reader(file)
.map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err))
})
.map_err(|source| NymvisorError::UpgradePlanLoadFailure {
path: path.to_path_buf(),
source,
})?;
upgrade_plan._save_path = Some(path.to_path_buf());
Ok(upgrade_plan)
}
}
#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "lowercase")]
pub enum DigestAlgorithm {
Sha256,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub struct DownloadUrl {
/// The checksum of the file behind the download url.
#[serde(with = "base64")]
pub checksum: Vec<u8>,
/// The algorithm used for computing the checksum
pub checksum_algorithm: DigestAlgorithm,
/// Download url for this particular platform
pub url: Url,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub struct UpgradeInfo {
/// Specifies whether this upgrade requires manual intervention and cannot be done automatically by the nymvisor.
pub manual: bool,
/// Name of this upgrade, for example `2023.4-galaxy`
pub name: String,
/// Additional information about this release
pub notes: String,
/// Optional rfc3339 datetime of the publish date of the release,
#[serde(with = "option_offsetdatetime")]
pub publish_date: Option<OffsetDateTime>,
/// Version of this upgrade, for example `1.1.69`
pub version: String,
/// Platform specific download urls, for example `linux-x86_64`
pub platforms: HashMap<String, DownloadUrl>,
/// Time when the upgrade should happen.
#[serde(with = "time::serde::rfc3339")]
pub upgrade_time: OffsetDateTime,
/// Optional build information of the upgraded binary for additional verification
pub binary_details: Option<BinaryBuildInformationOwned>,
}
impl UpgradeInfo {
pub(crate) fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), NymvisorError> {
let path = path.as_ref();
// in case we're saving brand new upgrade info, make sure the parent directory exists
#[allow(clippy::expect_used)]
let parent = path
.parent()
.expect("attempted to save the upgrade info as the root of the fs");
fs::create_dir_all(parent).map_err(|source| NymvisorError::PathInitFailure {
path: parent.to_path_buf(),
source,
})?;
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)
.map_err(|source| NymvisorError::UpgradeInfoSaveFailure {
name: self.name.clone(),
path: path.to_path_buf(),
source,
})?;
// we're not using any non-standard serializer and thus the serialization should not ever fail
#[allow(clippy::expect_used)]
serde_json::to_writer_pretty(file, self)
.expect("unexpected UpgradeInfo serialization failure");
Ok(())
}
pub(crate) fn try_load<P: AsRef<Path>>(path: P) -> Result<Self, NymvisorError> {
let path = path.as_ref();
std::fs::File::open(path)
.and_then(|file| {
serde_json::from_reader(file)
.map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err))
})
.map_err(|source| NymvisorError::UpgradeInfoLoadFailure {
path: path.to_path_buf(),
source,
})
}
pub(crate) fn is_genesis(&self) -> bool {
self.name == GENESIS_DIR
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub struct UpgradeHistory(Vec<UpgradeHistoryEntry>);
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub struct UpgradeHistoryEntry {
#[serde(with = "time::serde::rfc3339")]
performed_at: OffsetDateTime,
info: UpgradeInfo,
*/
todo!()
}
+249
View File
@@ -0,0 +1,249 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::serde_helpers::{base64, option_offsetdatetime};
use crate::config::GENESIS_DIR;
use crate::error::NymvisorError;
use nym_bin_common::build_information::BinaryBuildInformationOwned;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use std::{fs, io};
use time::OffsetDateTime;
use tracing::error;
use url::Url;
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub struct UpgradePlan {
// metadata indicating save location of the underlying file
#[serde(skip)]
_save_path: Option<PathBuf>,
current: UpgradeInfo,
// TODO: or maybe BTreeMap<OffsetDateTime, UpgradeInfo>, would be more appropriate?
queued_up: VecDeque<UpgradeInfo>,
}
impl UpgradePlan {
pub(crate) fn new(current: UpgradeInfo) -> Self {
UpgradePlan {
_save_path: None,
current,
queued_up: VecDeque::new(),
}
}
fn push_next_upgrade(&mut self, upgrade: UpgradeInfo) {
self.queued_up.push_back(upgrade);
}
fn update_on_disk(&self) -> Result<(), NymvisorError> {
// it should be impossible to update an existing upgrade plan that wasn't loaded from disk
assert!(self._save_path.is_some());
// safety: the except here is fine as this failure implies failure in the underlying logic of the code
// as opposed to user error
#[allow(clippy::expect_used)]
let save_path = self
._save_path
.as_ref()
.expect("loaded upgrade plan does not have an associate save path!");
let file = OpenOptions::new()
.write(true)
.truncate(true)
.open(save_path)
.map_err(|source| NymvisorError::UpgradePlanSaveFailure {
path: save_path.to_path_buf(),
source,
})?;
// we're not using any non-standard serializer and thus the serialization should not ever fail
#[allow(clippy::expect_used)]
serde_json::to_writer_pretty(file, self)
.expect("unexpected UpgradeInfo serialization failure");
Ok(())
}
pub(crate) fn insert_new_upgrade(&mut self, upgrade: UpgradeInfo) -> Result<(), NymvisorError> {
self.push_next_upgrade(upgrade);
self.update_on_disk()
}
pub(crate) fn current(&self) -> &UpgradeInfo {
&self.current
}
pub(crate) fn next_upgrade(&self) -> Option<&UpgradeInfo> {
self.queued_up.front()
}
pub(crate) fn has_planned(&self, upgrade: &UpgradeInfo) -> bool {
for planned in &self.queued_up {
if planned.version == upgrade.version {
if planned.name != upgrade.name {
// TODO: should we maybe return a hard error here instead?
error!("we have already a planned upgrade for version {} under name '{}' which differs from provided '{}'", planned.version, planned.name, upgrade.name);
}
return true;
}
}
false
}
// pub(crate) fn update_current(&mut self) -> Result<(), NymvisorError> {
//
// }
pub(crate) fn save_new<P: AsRef<Path>>(&self, path: P) -> Result<(), NymvisorError> {
debug_assert!(self._save_path.is_none());
let path = path.as_ref();
let file = OpenOptions::new()
.create_new(true)
.write(true)
.open(path)
.map_err(|source| NymvisorError::UpgradePlanSaveFailure {
path: path.to_path_buf(),
source,
})?;
// we're not using any non-standard serializer and thus the serialization should not ever fail
#[allow(clippy::expect_used)]
serde_json::to_writer_pretty(file, self)
.expect("unexpected UpgradeInfo serialization failure");
Ok(())
}
pub(crate) fn try_load<P: AsRef<Path>>(path: P) -> Result<Self, NymvisorError> {
let path = path.as_ref();
let mut upgrade_plan: UpgradePlan = fs::File::open(path)
.and_then(|file| {
serde_json::from_reader(file)
.map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err))
})
.map_err(|source| NymvisorError::UpgradePlanLoadFailure {
path: path.to_path_buf(),
source,
})?;
upgrade_plan._save_path = Some(path.to_path_buf());
Ok(upgrade_plan)
}
}
#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "lowercase")]
pub enum DigestAlgorithm {
Sha256,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub struct DownloadUrl {
/// The checksum of the file behind the download url.
#[serde(with = "base64")]
pub checksum: Vec<u8>,
/// The algorithm used for computing the checksum
pub checksum_algorithm: DigestAlgorithm,
/// Download url for this particular platform
pub url: Url,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub struct UpgradeInfo {
/// Specifies whether this upgrade requires manual intervention and cannot be done automatically by the nymvisor.
pub manual: bool,
/// Name of this upgrade, for example `2023.4-galaxy`
pub name: String,
/// Additional information about this release
pub notes: String,
/// Optional rfc3339 datetime of the publish date of the release,
#[serde(with = "option_offsetdatetime")]
pub publish_date: Option<OffsetDateTime>,
/// Version of this upgrade, for example `1.1.69`
pub version: String,
/// Platform specific download urls, for example `linux-x86_64`
pub platforms: HashMap<String, DownloadUrl>,
/// Time when the upgrade should happen.
#[serde(with = "time::serde::rfc3339")]
pub upgrade_time: OffsetDateTime,
/// Optional build information of the upgraded binary for additional verification
pub binary_details: Option<BinaryBuildInformationOwned>,
}
impl UpgradeInfo {
pub(crate) fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), NymvisorError> {
let path = path.as_ref();
// in case we're saving brand new upgrade info, make sure the parent directory exists
#[allow(clippy::expect_used)]
let parent = path
.parent()
.expect("attempted to save the upgrade info as the root of the fs");
fs::create_dir_all(parent).map_err(|source| NymvisorError::PathInitFailure {
path: parent.to_path_buf(),
source,
})?;
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)
.map_err(|source| NymvisorError::UpgradeInfoSaveFailure {
name: self.name.clone(),
path: path.to_path_buf(),
source,
})?;
// we're not using any non-standard serializer and thus the serialization should not ever fail
#[allow(clippy::expect_used)]
serde_json::to_writer_pretty(file, self)
.expect("unexpected UpgradeInfo serialization failure");
Ok(())
}
pub(crate) fn try_load<P: AsRef<Path>>(path: P) -> Result<Self, NymvisorError> {
let path = path.as_ref();
std::fs::File::open(path)
.and_then(|file| {
serde_json::from_reader(file)
.map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err))
})
.map_err(|source| NymvisorError::UpgradeInfoLoadFailure {
path: path.to_path_buf(),
source,
})
}
pub(crate) fn is_genesis(&self) -> bool {
self.name == GENESIS_DIR
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub struct UpgradeHistory(Vec<UpgradeHistoryEntry>);
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub struct UpgradeHistoryEntry {
#[serde(with = "time::serde::rfc3339")]
performed_at: OffsetDateTime,
info: UpgradeInfo,
}