diff --git a/uplink/src/collector/mod.rs b/uplink/src/collector/mod.rs index 3e2742bd..903663c3 100644 --- a/uplink/src/collector/mod.rs +++ b/uplink/src/collector/mod.rs @@ -5,6 +5,7 @@ pub mod installer; pub mod journalctl; #[cfg(target_os = "android")] pub mod logcat; +pub mod preconditions; pub mod process; pub mod script_runner; pub mod simulator; diff --git a/uplink/src/collector/preconditions.rs b/uplink/src/collector/preconditions.rs new file mode 100644 index 00000000..e83c6e0b --- /dev/null +++ b/uplink/src/collector/preconditions.rs @@ -0,0 +1,84 @@ +use std::{fs::metadata, os::unix::fs::MetadataExt, sync::Arc}; + +use flume::Receiver; +use human_bytes::human_bytes; +use log::debug; +use serde::Deserialize; + +use crate::{base::bridge::BridgeTx, Action, ActionResponse, Config}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("File io Error: {0}")] + Io(#[from] std::io::Error), + #[error("Disk space is insufficient: {0}")] + InsufficientDisk(String), +} + +#[derive(Deserialize, Clone, PartialEq, Eq, Debug)] +pub struct Preconditions { + #[serde(alias = "content-length")] + content_length: usize, + #[serde(alias = "uncompressed-size")] + uncompressed_length: usize, +} + +pub struct PreconditionChecker { + config: Arc, + actions_rx: Receiver, + bridge_tx: BridgeTx, +} + +impl PreconditionChecker { + pub fn new(config: Arc, actions_rx: Receiver, bridge_tx: BridgeTx) -> Self { + Self { config, actions_rx, bridge_tx } + } + + #[tokio::main] + pub async fn start(self) { + while let Ok(action) = self.actions_rx.recv() { + let preconditions: Preconditions = match serde_json::from_str(&action.payload) { + Ok(c) => c, + Err(e) => { + let response = ActionResponse::failure(&action.action_id, e.to_string()); + self.bridge_tx.send_action_response(response).await; + continue; + } + }; + + if let Err(e) = self.check_disk_size(preconditions) { + let response = ActionResponse::failure(&action.action_id, e.to_string()); + self.bridge_tx.send_action_response(response).await; + continue; + } + + let response = ActionResponse::progress(&action.action_id, "Checked OK", 100); + self.bridge_tx.send_action_response(response).await; + } + } + + // Fails if there isn't enough space to download and/or install update + // NOTE: both download and installation could happen in the same partition + fn check_disk_size(&self, preconditions: Preconditions) -> Result<(), Error> { + let disk_free_space = + fs2::free_space(&self.config.precondition_checks.as_ref().unwrap().path)? as usize; + let mut required_free_space = preconditions.uncompressed_length; + + // Check if download and installation paths are on the same partition, if yes add download file size to required + if metadata(&self.config.downloader.path)?.dev() + == metadata(&self.config.precondition_checks.as_ref().unwrap().path)?.dev() + { + required_free_space += preconditions.content_length; + } + + let req_size = human_bytes(required_free_space as f64); + let free_size = human_bytes(disk_free_space as f64); + debug!("The installation requires {req_size}; Disk free space is {free_size}"); + + if required_free_space > disk_free_space { + return Err(Error::InsufficientDisk(free_size)); + } + + Ok(()) + } +} diff --git a/uplink/src/config.rs b/uplink/src/config.rs index beddf361..d7718a97 100644 --- a/uplink/src/config.rs +++ b/uplink/src/config.rs @@ -233,6 +233,12 @@ impl Default for DeviceShadowConfig { } } +#[derive(Debug, Clone, Deserialize)] +pub struct PreconditionCheckerConfig { + pub path: PathBuf, + pub actions: Vec, +} + #[derive(Debug, Clone, Deserialize, Default)] pub struct Config { pub project_id: String, @@ -274,4 +280,5 @@ pub struct Config { pub logging: Option, #[cfg(target_os = "android")] pub logging: Option, + pub precondition_checks: Option, } diff --git a/uplink/src/lib.rs b/uplink/src/lib.rs index e69185e1..85959456 100644 --- a/uplink/src/lib.rs +++ b/uplink/src/lib.rs @@ -67,6 +67,7 @@ use collector::installer::OTAInstaller; use collector::journalctl::JournalCtl; #[cfg(target_os = "android")] use collector::logcat::Logcat; +use collector::preconditions::PreconditionChecker; use collector::process::ProcessHandler; use collector::script_runner::ScriptRunner; use collector::systemstats::StatCollector; @@ -294,7 +295,7 @@ impl Uplink { if !self.config.script_runner.is_empty() { let actions_rx = bridge.register_action_routes(&self.config.script_runner)?; - let script_runner = ScriptRunner::new(actions_rx, bridge_tx); + let script_runner = ScriptRunner::new(actions_rx, bridge_tx.clone()); spawn_named_thread("Script Runner", || { if let Err(e) = script_runner.start() { error!("Script runner stopped!! Error = {:?}", e); @@ -302,6 +303,12 @@ impl Uplink { }); } + if let Some(checker_config) = &self.config.precondition_checks { + let actions_rx = bridge.register_action_routes(&checker_config.actions)?; + let checker = PreconditionChecker::new(self.config.clone(), actions_rx, bridge_tx); + spawn_named_thread("Logger", || checker.start()); + } + Ok(()) }