diff --git a/Cargo.lock b/Cargo.lock index 5434dda..4d8f3e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,6 +122,21 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.14" @@ -823,6 +838,21 @@ dependencies = [ "libc", ] +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.6", +] + [[package]] name = "cipher" version = "0.4.4" @@ -2076,6 +2106,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -3536,6 +3589,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "redox_users" version = "0.4.4" @@ -3787,6 +3849,7 @@ dependencies = [ "azure_storage", "azure_storage_blobs", "backtrace", + "chrono", "clap", "conv", "dirs", @@ -3817,6 +3880,7 @@ dependencies = [ "tracing-subscriber", "uuid", "walkdir", + "whoami", "zip", ] @@ -4746,6 +4810,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.93" @@ -4991,6 +5061,17 @@ dependencies = [ "rustix", ] +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall 0.5.7", + "wasite", + "web-sys", +] + [[package]] name = "wide" version = "0.7.13" @@ -5041,6 +5122,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-registry" version = "0.2.0" diff --git a/rvimage/Cargo.toml b/rvimage/Cargo.toml index b5ae937..e1db7d9 100644 --- a/rvimage/Cargo.toml +++ b/rvimage/Cargo.toml @@ -59,7 +59,7 @@ serde_json = "1.0.128" ssh2 = { version = "0.9.4", features = ["vendored-openssl"] } toml = "0.8.19" walkdir = "2.5" -tokio = { version = "1.40.0", optional = true, features = ["rt-multi-thread"]} +tokio = { version = "1.40.0", optional = true, features = ["rt-multi-thread"] } uuid = { version = "1.10.0", features = ["v4", "fast-rng"] } exmex = "0.20.3" tracing = "0.1.40" @@ -69,3 +69,5 @@ backtrace = "0.3.74" zip = "2.2.0" rvimage-domain = "0.4.5" clap = { version = "4.5.19", features = ["derive"] } +chrono = { version = "0.4.38", features = ["serde"] } +whoami = "1.5.2" diff --git a/rvimage/src/rvlib/cfg.rs b/rvimage/src/rvlib/cfg.rs index 4afbdd3..3837846 100644 --- a/rvimage/src/rvlib/cfg.rs +++ b/rvimage/src/rvlib/cfg.rs @@ -4,6 +4,7 @@ use crate::{ sort_params::SortParams, ssh, }; +use chrono::{DateTime, Utc}; use rvimage_domain::{rverr, to_rv, RvError, RvResult}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ @@ -76,6 +77,7 @@ impl CfgLegacy { prefix: ab.prefix, }), sort_params: SortParams::default(), + deadmansswitch: Utc::now(), }; Cfg { usr, prj } } @@ -339,6 +341,8 @@ pub struct CfgPrj { pub azure_blob: Option, #[serde(default)] pub sort_params: SortParams, + #[serde(default = "Utc::now")] + pub deadmansswitch: DateTime, } #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default)] pub struct Cfg { @@ -370,6 +374,10 @@ impl Cfg { } } + pub fn push_deadmansswitch(&mut self) { + self.prj.deadmansswitch = Utc::now(); + } + pub fn tmpdir(&self) -> &str { match &self.usr.tmpdir { Some(td) => td.as_str(), diff --git a/rvimage/src/rvlib/control/mod.rs b/rvimage/src/rvlib/control/mod.rs index db92e30..01661d9 100644 --- a/rvimage/src/rvlib/control/mod.rs +++ b/rvimage/src/rvlib/control/mod.rs @@ -1,5 +1,7 @@ use crate::cfg::{self, get_log_folder, Connection}; -use crate::file_util::{osstr_to_str, PathPair, DEFAULT_PRJ_NAME, DEFAULT_PRJ_PATH}; +use crate::file_util::{ + self, osstr_to_str, PathPair, SavedCfg, DEFAULT_PRJ_NAME, DEFAULT_PRJ_PATH, +}; use crate::history::{History, Record}; use crate::meta_data::{ConnectionData, MetaData, MetaDataFlags}; use crate::result::trace_ok_err; @@ -8,10 +10,13 @@ use crate::world::{DataRaw, ToolsDataMap, World}; use crate::{ cfg::Cfg, image_reader::ReaderFromCfg, threadpool::ThreadPool, types::AsyncResultImage, }; -use rvimage_domain::{RvError, RvResult}; +use chrono::{DateTime, Utc}; +use rvimage_domain::{to_rv, RvError, RvResult}; +use serde::{Deserialize, Serialize}; use std::fmt::Debug; use std::fs; use std::io::Write; +use std::mem; use std::path::{Path, PathBuf}; use std::thread::{self, JoinHandle}; use std::time::Duration; @@ -31,15 +36,21 @@ mod detail { use crate::{ cfg::{read_cfg, write_cfg, Cfg, CfgPrj}, + control::{Deadmansswitch, SavePrjData}, file_util::{self, tf_to_annomap_key, SavedCfg}, result::trace_ok_err, util::version_label, world::{ToolsDataMap, World}, }; - use rvimage_domain::result::{to_rv, RvResult}; + use rvimage_domain::result::RvResult; use rvimage_domain::ShapeI; - fn serialize_opened_folder(folder: &Option, serializer: S) -> Result + use super::load_prj; + + pub fn serialize_opened_folder( + folder: &Option, + serializer: S, + ) -> Result where S: Serializer, { @@ -50,7 +61,7 @@ mod detail { .map(|folder| tf_to_annomap_key(folder, prj_path)); folder.serialize(serializer) } - fn deserialize_opened_folder<'de, D>(deserializer: D) -> Result, D::Error> + pub fn deserialize_opened_folder<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, { @@ -60,15 +71,6 @@ mod detail { Ok(folder.map(|p| tf_to_annomap_key(p, prj_path))) } - #[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] - pub struct SaveData { - pub version: Option, - #[serde(serialize_with = "serialize_opened_folder")] - #[serde(deserialize_with = "deserialize_opened_folder")] - pub opened_folder: Option, - pub tools_data_map: ToolsDataMap, - pub cfg: SavedCfg, - } pub(super) fn idx_change_check( file_selected_idx: Option, @@ -82,15 +84,6 @@ mod detail { } }) } - pub(super) fn load(file_path: &Path) -> RvResult<(ToolsDataMap, Option, CfgPrj)> { - let s = file_util::read_to_string(file_path)?; - let save_data = serde_json::from_str::(s.as_str()).map_err(to_rv)?; - let cfg_prj = match save_data.cfg { - SavedCfg::CfgLegacy(cfg) => cfg.to_cfg().prj, - SavedCfg::CfgPrj(cfg_prj) => cfg_prj, - }; - Ok((save_data.tools_data_map, save_data.opened_folder, cfg_prj)) - } fn write( tools_data_map: &ToolsDataMap, @@ -109,9 +102,7 @@ mod detail { }) .collect::(); let data = make_data(&tools_data_map); - let data_str = serde_json::to_string(&data).map_err(to_rv)?; - file_util::write(export_path, data_str)?; - Ok(()) + file_util::save(export_path, data) } pub fn save( @@ -123,11 +114,12 @@ mod detail { // we need to write the cfg for correct prj-path mapping during serialization // of annotations trace_ok_err(write_cfg(cfg)); - let make_data = |tdm: &ToolsDataMap| SaveData { + let make_data = |tdm: &ToolsDataMap| SavePrjData { version: Some(version_label()), opened_folder: opened_folder.map(|of| of.to_string()), tools_data_map: tdm.clone(), cfg: SavedCfg::CfgPrj(cfg.prj.clone()), + deadmansswitch: Deadmansswitch::new(), }; tracing::info!("saved to {file_path:?}"); write(tools_data_map, make_data, file_path)?; @@ -164,9 +156,53 @@ mod detail { image::Rgb([77u8, 77u8, 87u8]) })) } + pub(super) fn load(file_path: &Path) -> RvResult<(ToolsDataMap, Option, CfgPrj)> { + let save_data = load_prj(file_path)?; + let cfg_prj = match save_data.cfg { + SavedCfg::CfgLegacy(cfg) => cfg.to_cfg().prj, + SavedCfg::CfgPrj(cfg_prj) => cfg_prj, + }; + Ok((save_data.tools_data_map, save_data.opened_folder, cfg_prj)) + } } const LOAD_ACTOR_NAME: &str = "Load"; +pub fn load_prj(file_path: &Path) -> RvResult { + let s = file_util::read_to_string(file_path)?; + serde_json::from_str::(s.as_str()).map_err(to_rv) +} +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +pub struct Deadmansswitch { + time: DateTime, + username: String, + realname: String, +} +impl Deadmansswitch { + pub fn new() -> Self { + Deadmansswitch { + time: Utc::now(), + username: whoami::username(), + realname: whoami::realname(), + } + } +} +impl Default for Deadmansswitch { + fn default() -> Self { + Self::new() + } +} +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +pub struct SavePrjData { + pub version: Option, + #[serde(serialize_with = "detail::serialize_opened_folder")] + #[serde(deserialize_with = "detail::deserialize_opened_folder")] + pub opened_folder: Option, + pub tools_data_map: ToolsDataMap, + pub cfg: SavedCfg, + #[serde(default = "Deadmansswitch::new")] + pub deadmansswitch: Deadmansswitch, +} + #[derive(Clone, Debug, Default)] pub enum Info { Error(String), @@ -197,6 +233,7 @@ pub struct Control { flags: ControlFlags, pub loading_screen_animation_counter: u128, pub log_export_path: Option, + save_handle: Option>, } impl Control { @@ -248,12 +285,33 @@ impl Control { Ok(tools_data_map) } + fn wait_for_save(&mut self) { + if self.save_handle.is_some() { + mem::take(&mut self.save_handle).map(|h| trace_ok_err(h.join().map_err(to_rv))); + } + } + + pub fn push_deadmansswitch(&mut self) { + self.wait_for_save(); + let prj_path = self.cfg.current_prj_path().to_path_buf(); + let handle = thread::spawn(move || { + tracing::info!("pushing dead man's switch..."); + let prj_data = trace_ok_err(load_prj(&prj_path)); + if let Some(mut prj_data) = prj_data { + prj_data.deadmansswitch = Deadmansswitch::new(); + trace_ok_err(file_util::save(&prj_path, prj_data)); + } + tracing::info!("pushing dead man's done!"); + }); + self.save_handle = Some(handle); + } + pub fn save( &mut self, prj_path: PathBuf, tools_data_map: &ToolsDataMap, set_cur_prj: bool, - ) -> RvResult> { + ) -> RvResult<()> { let path = if let Some(of) = self.opened_folder() { if DEFAULT_PRJ_PATH.as_os_str() == prj_path.as_os_str() { PathBuf::from(of.path_relative()).join(DEFAULT_PRJ_NAME) @@ -272,6 +330,7 @@ impl Control { let opened_folder = self.opened_folder().cloned(); let tdm = tools_data_map.clone(); let cfg = self.cfg.clone(); + self.wait_for_save(); let handle = thread::spawn(move || { trace_ok_err(detail::save( opened_folder.as_ref().map(|of| of.path_relative()), @@ -280,7 +339,8 @@ impl Control { &cfg, )); }); - Ok(handle) + self.save_handle = Some(handle); + Ok(()) } pub fn new() -> Self { diff --git a/rvimage/src/rvlib/file_util.rs b/rvimage/src/rvlib/file_util.rs index d6a563d..c0268e9 100644 --- a/rvimage/src/rvlib/file_util.rs +++ b/rvimage/src/rvlib/file_util.rs @@ -186,6 +186,13 @@ pub enum SavedCfg { CfgLegacy(CfgLegacy), } +pub fn save(file_path: &Path, data: T) -> RvResult<()> +where + T: Serialize, +{ + let data_str = serde_json::to_string(&data).map_err(to_rv)?; + write(file_path, data_str) +} pub struct Defer { pub func: F, } diff --git a/rvimage/src/rvlib/main_loop.rs b/rvimage/src/rvlib/main_loop.rs index ac600eb..79cb707 100644 --- a/rvimage/src/rvlib/main_loop.rs +++ b/rvimage/src/rvlib/main_loop.rs @@ -29,6 +29,7 @@ const START_WIDTH: u32 = 640; const START_HEIGHT: u32 = 480; const AUTOSAVE_INTERVAL_S: u64 = 120; +const DEAD_MANS_SWITCH_S: u64 = 60; fn pos_2_string_gen(im: &T, x: u32, y: u32) -> String where @@ -124,6 +125,7 @@ pub struct MainEventLoop { rx_from_http: Option>>, http_addr: String, autosave_timer: Instant, + deadmansswitch_timer: Instant, } impl Default for MainEventLoop { fn default() -> Self { @@ -133,7 +135,7 @@ impl Default for MainEventLoop { } impl MainEventLoop { pub fn new(prj_file_path: Option) -> Self { - let ctrl = Control::new(); + let mut ctrl = Control::new(); let mut world = empty_world(); let mut tools = make_tool_vec(); @@ -149,6 +151,7 @@ impl MainEventLoop { } else { None }; + ctrl.push_deadmansswitch(); let mut self_ = Self { world, ctrl, @@ -160,6 +163,7 @@ impl MainEventLoop { recently_clicked_tool_idx: None, rx_from_http, autosave_timer: Instant::now(), + deadmansswitch_timer: Instant::now(), }; trace_ok_err(self_.load_prj(prj_file_path)); @@ -384,7 +388,10 @@ impl MainEventLoop { }; self.world.update_view.image_info = Some(s); } - + if self.deadmansswitch_timer.elapsed().as_secs() > DEAD_MANS_SWITCH_S { + self.ctrl.push_deadmansswitch(); + self.deadmansswitch_timer = Instant::now(); + } if let Some(n_autosaves) = self.ctrl.cfg.usr.n_autosaves { if self.autosave_timer.elapsed().as_secs() > AUTOSAVE_INTERVAL_S { self.autosave_timer = Instant::now();