Skip to content

Commit 5a1e65d

Browse files
authored
Merge pull request #24 from PlexSheep/devel
cool new shit
2 parents ad280bb + 4165c6a commit 5a1e65d

File tree

9 files changed

+419
-94
lines changed

9 files changed

+419
-94
lines changed

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ zstd = { version = "0.13.2", optional = true }
2929
nix = { version = "0.29.0", features = ["signal", "process", "user"] }
3030
ping = { version = "0.5.2", optional = true }
3131
curl = { version = "0.4.47", optional = true, default-features = false }
32-
humantime = "2.1.0"
3332
caps = "0.5.5"
3433
deepsize = "0.2.0"
3534
tracing = "0.1.40"
3635
tracing-subscriber = { version = "0.3.18", optional = true }
36+
chrono = { version = "0.4.38", optional = false }
37+
blake3 = "1.5.4"
38+
serde_repr = "0.1.19"
3739

3840
[[bin]] # client
3941
name = "netpulse"

src/analyze.rs

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
//! - Outage analysis
3030
//! - Store metadata (hashes, versions)
3131
32+
use chrono::{DateTime, Local};
3233
use deepsize::DeepSizeOf;
3334

3435
use crate::errors::AnalysisError;
@@ -39,6 +40,16 @@ use std::fmt::{Display, Write};
3940
use std::hash::Hash;
4041
use std::os::unix::fs::MetadataExt;
4142

43+
/// Formatting rules for timestamps that are easily readable by humans.
44+
///
45+
/// ```rust
46+
/// use chrono::{DateTime, Local};
47+
/// # use netpulse::analyze::TIME_FORMAT_HUMANS;
48+
/// let datetime: DateTime<Local> = Local::now();
49+
/// println!("it is now: {}", datetime.format(TIME_FORMAT_HUMANS));
50+
/// ```
51+
pub const TIME_FORMAT_HUMANS: &str = "%Y-%m-%d %H:%M:%S %Z";
52+
4253
/// Represents a period of consecutive failed checks.
4354
///
4455
/// An outage is defined by:
@@ -86,14 +97,14 @@ impl Display for Outage<'_> {
8697
writeln!(
8798
f,
8899
"From {} To {}",
89-
humantime::format_rfc3339_seconds(self.start.timestamp_parsed()),
90-
humantime::format_rfc3339_seconds(self.end.unwrap().timestamp_parsed())
100+
fmt_timestamp(self.start.timestamp_parsed()),
101+
fmt_timestamp(self.end.unwrap().timestamp_parsed())
91102
)?;
92103
} else {
93104
writeln!(
94105
f,
95106
"From {} STILL ONGOING",
96-
humantime::format_rfc3339_seconds(self.start.timestamp_parsed()),
107+
fmt_timestamp(self.start.timestamp_parsed()),
97108
)?;
98109
}
99110
writeln!(f, "Checks: {}", self.all.len())?;
@@ -149,6 +160,28 @@ pub fn analyze(store: &Store) -> Result<String, AnalysisError> {
149160
Ok(f)
150161
}
151162

163+
/// Formats a [SystemTime](std::time::SystemTime) as an easily readable timestamp for humans.
164+
///
165+
/// Works with [`std::time::SystemTime`] and [`chrono::DateTime<Local>`].
166+
///
167+
/// # Examples
168+
///
169+
/// ```rust
170+
/// # use netpulse::analyze::fmt_timestamp;
171+
/// use std::time::SystemTime;
172+
/// use chrono;
173+
/// let datetime: SystemTime = SystemTime::now();
174+
/// println!("it is now: {}", fmt_timestamp(datetime));
175+
/// let datetime: chrono::DateTime<chrono::Local> = chrono::Local::now();
176+
/// println!("it is now: {}", fmt_timestamp(datetime));
177+
/// let datetime: chrono::DateTime<chrono::Utc> = chrono::Utc::now();
178+
/// println!("it is now: {}", fmt_timestamp(datetime));
179+
/// ```
180+
pub fn fmt_timestamp(timestamp: impl Into<DateTime<Local>>) -> String {
181+
let a: chrono::DateTime<chrono::Local> = timestamp.into();
182+
format!("{}", a.format(TIME_FORMAT_HUMANS))
183+
}
184+
152185
/// Adds a section divider to the report with a title.
153186
///
154187
/// Creates a divider line of '=' characters with the title centered.
@@ -288,12 +321,12 @@ fn analyze_check_type_set(
288321
key_value_write(
289322
f,
290323
"first check at",
291-
humantime::format_rfc3339_seconds(all.first().unwrap().timestamp_parsed()),
324+
fmt_timestamp(all.first().unwrap().timestamp_parsed()),
292325
)?;
293326
key_value_write(
294327
f,
295328
"last check at",
296-
humantime::format_rfc3339_seconds(all.last().unwrap().timestamp_parsed()),
329+
fmt_timestamp(all.last().unwrap().timestamp_parsed()),
297330
)?;
298331
writeln!(f)?;
299332
Ok(())
@@ -374,11 +407,10 @@ fn store_meta(store: &Store, f: &mut String) -> Result<(), AnalysisError> {
374407
let store_size_mem = store.deep_size_of();
375408
let store_size_fs = std::fs::metadata(Store::path())?.size();
376409

377-
key_value_write(f, "Hash Datastructure", store.display_hash())?;
378-
key_value_write(f, "Hash Store File", store.display_hash_of_file()?)?;
410+
key_value_write(f, "Hash mem blake3", store.get_hash())?;
411+
key_value_write(f, "Hash file sha256", store.get_hash_of_file()?)?;
379412
key_value_write(f, "Store Version (mem)", store.version())?;
380-
// TODO: find a way to get the version just from file without deserializing it
381-
key_value_write(f, "Store Version (file)", "<TODO>")?;
413+
key_value_write(f, "Store Version (file)", Store::peek_file_version()?)?;
382414
key_value_write(f, "Store Size (mem)", store_size_mem)?;
383415
key_value_write(f, "Store Size (file)", store_size_fs)?;
384416
key_value_write(

src/bins/daemon.rs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
//! 3. Logs any cleanup errors
2020
2121
use std::sync::atomic::AtomicBool;
22-
use std::time::{self, Duration, UNIX_EPOCH};
2322

2423
use netpulse::errors::RunError;
2524
use netpulse::records::display_group;
@@ -59,19 +58,12 @@ pub(crate) fn daemon() {
5958
info!("restarting the daemon");
6059
store = load_store();
6160
}
62-
let time = time::SystemTime::now();
63-
if time
64-
.duration_since(UNIX_EPOCH)
65-
.expect("time is before the UNIX_EPOCH")
66-
.as_secs()
67-
% store.period_seconds()
68-
== 0
69-
{
61+
if chrono::Utc::now().timestamp() % store.period_seconds() == 0 {
7062
if let Err(err) = wakeup(&mut store) {
7163
error!("error in the wakeup turn: {err}");
7264
}
7365
}
74-
std::thread::sleep(Duration::from_secs(1));
66+
std::thread::sleep(std::time::Duration::from_secs(1));
7567
}
7668
}
7769

@@ -97,7 +89,7 @@ fn load_store() -> Store {
9789
///
9890
/// # Errors
9991
///
100-
/// Returns [DaemonError] if store operations fail.
92+
/// Returns [RunError] if store operations fail.
10193
fn wakeup(store: &mut Store) -> Result<(), RunError> {
10294
info!("waking up!");
10395

@@ -128,7 +120,7 @@ fn signal_hook() {
128120
///
129121
/// # Errors
130122
///
131-
/// Returns [DaemonError] if cleanup operations fail.
123+
/// Returns [RunError] if cleanup operations fail.
132124
fn cleanup(store: &Store) -> Result<(), RunError> {
133125
if let Err(err) = store.save() {
134126
error!("error while saving to file: {err:#?}");

src/bins/netpulse.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313
1414
use getopts::Options;
1515
use netpulse::analyze;
16-
use netpulse::common::{init_logging, print_usage};
16+
use netpulse::common::{init_logging, print_usage, setup_panic_handler};
1717
use netpulse::errors::RunError;
1818
use netpulse::records::{display_group, Check};
1919
use netpulse::store::Store;
20+
use tracing::error;
2021

2122
fn main() {
23+
setup_panic_handler();
2224
init_logging(tracing::Level::INFO);
2325
let args: Vec<String> = std::env::args().collect();
2426
let program = &args[0];
@@ -28,6 +30,11 @@ fn main() {
2830
opts.optflag("V", "version", "print the version");
2931
opts.optflag("t", "test", "test run all checks");
3032
opts.optflag("d", "dump", "print out all checks");
33+
opts.optflag(
34+
"r",
35+
"rewrite",
36+
"load store and immediately save to rewrite the file",
37+
);
3138
opts.optflag("f", "failed", "only consider failed checks for dumping");
3239
let matches = match opts.parse(&args[1..]) {
3340
Ok(m) => m,
@@ -50,7 +57,12 @@ fn main() {
5057
dump(failed_only);
5158
} else if matches.opt_present("test") {
5259
if let Err(e) = test_checks() {
53-
eprintln!("{e}");
60+
error!("{e}");
61+
std::process::exit(1)
62+
}
63+
} else if matches.opt_present("rewrite") {
64+
if let Err(e) = rewrite() {
65+
error!("{e}");
5466
std::process::exit(1)
5567
}
5668
} else {
@@ -93,6 +105,12 @@ fn dump(failed_only: bool) {
93105
println!("{buf}")
94106
}
95107

108+
fn rewrite() -> Result<(), RunError> {
109+
let s = Store::load()?;
110+
s.save()?;
111+
Ok(())
112+
}
113+
96114
fn analysis() {
97115
let store = store_load();
98116
match analyze::analyze(&store) {

src/bins/netpulsed.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use daemonize::Daemonize;
3131
use getopts::Options;
3232
use netpulse::common::{
3333
confirm, exec_cmd_for_user, getpid, getpid_running, init_logging, print_usage, root_guard,
34+
setup_panic_handler,
3435
};
3536
use netpulse::errors::RunError;
3637
use netpulse::store::Store;
@@ -53,6 +54,7 @@ const SYSTEMD_SERVICE_PATH: &str = "/etc/systemd/system/netpulsed.service";
5354
static USES_DAEMON_SYSTEM: AtomicBool = AtomicBool::new(false);
5455

5556
fn main() -> Result<(), RunError> {
57+
setup_panic_handler();
5658
init_logging(tracing::Level::DEBUG);
5759
let args: Vec<String> = std::env::args().collect();
5860
let program = &args[0];

src/common.rs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@
3333
//! println!("Daemon running with PID: {}", pid);
3434
//! }
3535
//! ```
36-
3736
use std::io::{self, Write};
3837
use std::process::Command;
3938
use std::str::FromStr;
4039

4140
use crate::DAEMON_PID_FILE;
41+
4242
use getopts::Options;
4343
use tracing::{error, info, trace};
4444
use tracing_subscriber::FmtSubscriber;
@@ -261,3 +261,82 @@ pub fn getpid() -> Option<i32> {
261261
Some(pid)
262262
}
263263
}
264+
265+
/// Sets up a custom panic handler for user-friendly error reporting.
266+
///
267+
/// Should be called early in the program startup, ideally before any other operations.
268+
/// In debug builds, uses the default panic handler for detailed debugging output.
269+
/// In release builds, provides a user-friendly error message with reporting instructions.
270+
///
271+
/// # Example Output
272+
///
273+
/// ```text
274+
/// Well, this is embarrassing.
275+
///
276+
/// netpulse had a problem and crashed. This is a bug and should be reported!
277+
///
278+
/// Technical details:
279+
/// Version: 0.1.0
280+
/// OS: linux x86_64
281+
/// Command: netpulse --check
282+
/// Error: called `Option::unwrap()` on a `None` value
283+
/// Location: src/store.rs:142
284+
///
285+
/// Please create a new issue at https://github.com/PlexSheep/netpulse/issues
286+
/// with the above technical details and what you were doing when this happened.
287+
/// ```
288+
pub fn setup_panic_handler() {
289+
if !cfg!(debug_assertions) {
290+
// Only override in release builds
291+
std::panic::set_hook(Box::new(|panic_info| {
292+
let mut message = String::new();
293+
message.push_str("\nWell, this is embarrassing.\n\n");
294+
message.push_str(&format!(
295+
"{} had a problem and crashed. This is a bug and should be reported!\n\n",
296+
env!("CARGO_PKG_NAME")
297+
));
298+
299+
message.push_str("Technical details:\n");
300+
message.push_str(&format!("Version: {}\n", env!("CARGO_PKG_VERSION")));
301+
302+
// Get OS info
303+
#[cfg(target_os = "linux")]
304+
let os = "linux";
305+
#[cfg(target_os = "macos")]
306+
let os = "macos";
307+
#[cfg(target_os = "windows")]
308+
let os = "windows";
309+
310+
message.push_str(&format!("OS: {} {}\n", os, std::env::consts::ARCH));
311+
312+
// Get command line
313+
let args: Vec<_> = std::env::args().collect();
314+
message.push_str(&format!("Command: {}\n", args.join(" ")));
315+
316+
// Extract error message and location
317+
if let Some(msg) = panic_info.payload().downcast_ref::<&str>() {
318+
message.push_str(&format!("Error: {}\n", msg));
319+
} else if let Some(msg) = panic_info.payload().downcast_ref::<String>() {
320+
message.push_str(&format!("Error: {}\n", msg));
321+
}
322+
323+
if let Some(location) = panic_info.location() {
324+
message.push_str(&format!(
325+
"Location: {}:{}\n",
326+
location.file(),
327+
location.line()
328+
));
329+
}
330+
331+
message.push_str(
332+
"\nPlease create a new issue at https://github.com/PlexSheep/netpulse/issues\n",
333+
);
334+
message.push_str(
335+
"with the above technical details and what you were doing when this happened.\n",
336+
);
337+
338+
eprintln!("{}", message);
339+
std::process::exit(1);
340+
}));
341+
}
342+
}

src/errors.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//! This module provides specialized error types for different components of netpulse:
44
//! - [`StoreError`] - Errors related to store operations (loading, saving, versioning)
55
//! - [`CheckError`] - Errors that occur during network checks (HTTP, ICMP)
6-
//! - [`DaemonError`] - Errors specific to daemon operations
6+
//! - [`RunError`] - Errors specific to executable operations
77
//! - [`AnalysisError`] - Errors that occur during analysis and report generation
88
//!
99
//! All error types implement the standard Error trait and provide detailed error information.
@@ -53,7 +53,7 @@ pub enum StoreError {
5353
/// Failed to load store data from file.
5454
///
5555
/// This typically indicates corruption or an incompatible / outdated store format.
56-
#[error("Could not load the store from file: {source}")]
56+
#[error("Could not deserialize the store from the loaded data: {source}")]
5757
Load {
5858
/// Underlying error
5959
#[from]
@@ -86,14 +86,15 @@ pub enum StoreError {
8686
///
8787
/// This variant contains a [FlagSet] with only the flags [CheckFlags](CheckFlag) set that
8888
/// would make it a valid state. Exactly one of these flags must be set.
89-
///
90-
/// # Example
91-
///
92-
/// Every check should have either [CheckFlag::IPv4] or [CheckFlag::IPv6] set. If none of these
93-
/// are set, this error will be returned with a [FlagSet] that has both flags set, to indicate
94-
/// that one of these should be set.
9589
#[error("Check is missing at least one of these flags: {0:?}")]
9690
MissingFlag(FlagSet<CheckFlag>),
91+
/// Occurs when trying to convert an arbitrary [u8] to a [Version](crate::store::Version) that
92+
/// is not defined. Only known [Versions][crate::store::Version] are valid.
93+
#[error("Tried to load a store version that does not exist: {0}")]
94+
BadStoreVersion(u8),
95+
/// A store can be loaded as readonly if it's corrupted or there is a version mismatch
96+
#[error("Tried to save a readonly store")]
97+
IsReadonly,
9798
}
9899

99100
/// Errors that can occur during network checks.

0 commit comments

Comments
 (0)