--- /dev/null
+# 'zigbuild' is needed to build for target 'x86_64-unknown-linux-gnu' on linux:
+# https://github.com/rust-cross/cargo-zigbuild
+
+def main [host: string, destination: string, ssh_key: path] {
+ let ssh_args = [-i $ssh_key $host]
+ let scp_args = [-r -i $ssh_key]
+ let target = "x86_64-unknown-linux-gnu"
+ let app_name = "staking_watchdog_watchdog"
+ let build = "release" # "debug" or "release".
+
+ def invoke_ssh [command: string] {
+ let args = $ssh_args ++ $command
+ print $"Executing: ssh ($args)"
+ ssh $args
+ }
+
+ def copy_ssh [source: string, destination: string] {
+ let args = $scp_args ++ [$source $"($host):($destination)"]
+ print $"Executing: scp ($args)"
+ scp $args
+ }
+
+ # Don't know how to dynamically pass variable arguments.
+ if $build == "release" {
+ cargo zigbuild --target $target --release
+ } else {
+ cargo zigbuild --target $target
+ }
+
+ invoke_ssh $"systemctl --user stop ($app_name)"
+ copy_ssh ./target/($target)/($build)/($app_name) $destination
+ invoke_ssh $"chmod u+x ($destination)/($app_name)"
+ invoke_ssh $"systemctl --user start ($app_name)"
+ print "Deployment finished"
+}
\ No newline at end of file
--- /dev/null
+use std::fs::File;
+
+use anyhow::Result;
+use ron::{
+ de::from_reader,
+ ser::{to_writer_pretty, PrettyConfig},
+};
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct Config {
+ pub staking_address: String,
+ pub smtp_login: String,
+ pub smtp_password: String,
+}
+
+impl Config {
+ pub fn default() -> Self {
+ Config {
+ staking_address: "192.168.2.102:8739".to_string(),
+ smtp_login: "login".to_string(),
+ smtp_password: "password".to_string(),
+ }
+ }
+
+ pub fn read(file_path: &str) -> Result<Config> {
+ match File::open(file_path) {
+ Ok(file) => from_reader(file).map_err(|e| e.into()),
+ // The file doesn't exit -> create it with default values.
+ Err(_) => {
+ let file = File::create(file_path)?;
+ let default_config = Config::default();
+ to_writer_pretty(file, &default_config, PrettyConfig::new())?;
+ Ok(default_config)
+ }
+ }
+ }
+}
--- /dev/null
+use std::{
+ net::UdpSocket,
+ thread,
+ time::{self, Duration},
+};
+
+use anyhow::Result;
+use lettre::{
+ message::header::ContentType, transport::smtp::authentication::Credentials, Message,
+ SmtpTransport, Transport,
+};
+use rand::{rngs::ThreadRng, Rng};
+
+use crate::config::Config;
+
+mod config;
+
+const FILE_CONF: &str = "config.ron";
+const PING_PERIOD: Duration = Duration::from_secs(5); // 5 s.
+const EMAIL_RESEND_PERIOD: Duration = Duration::from_secs(2 * 60 * 60); // 2 h.
+const STATE_PRINT_PERIOD: Duration = Duration::from_secs(15 * 60); // 15 min.
+
+fn main() -> Result<()> {
+ println!("Staking Watchdog Watchdog");
+
+ let config = Config::read(FILE_CONF)?;
+
+ println!(
+ "Configuration: {:?}",
+ Config {
+ smtp_password: "*****".to_string(),
+ ..config.clone()
+ }
+ );
+
+ let mut time_last_email_send = time::Instant::now() - EMAIL_RESEND_PERIOD;
+ let mut time_last_state_printed = time::Instant::now() - STATE_PRINT_PERIOD;
+ let mut error_state = false;
+
+ let mut rng = rand::thread_rng();
+
+ let socket = UdpSocket::bind("0.0.0.0:0").unwrap();
+ socket.connect("192.168.2.102:8739").unwrap();
+ socket
+ .set_read_timeout(Some(Duration::from_secs(5)))
+ .unwrap();
+ socket
+ .set_write_timeout(Some(Duration::from_secs(5)))
+ .unwrap();
+
+ loop {
+ let time_beginning_loop = time::Instant::now();
+
+ if let Err(error) = ping(&socket, &mut rng) {
+ error_state = true;
+ println!("Error: {:?}", error);
+ if time::Instant::now() - time_last_email_send >= EMAIL_RESEND_PERIOD {
+ // Send e-mail.
+ println!("Sending email...");
+ match send_email(
+ "Watchdog ERROR",
+ &format!("Error: {:?}", error),
+ &config.smtp_login,
+ &config.smtp_password,
+ ) {
+ Err(email_error) => println!("Error sending email: {:?}", email_error),
+ _ => {
+ println!("Email successfully sent");
+ time_last_email_send = time::Instant::now();
+ }
+ }
+ }
+ } else {
+ if error_state {
+ error_state = false;
+ println!("End of erroneous state");
+ }
+
+ if time::Instant::now() - time_last_state_printed >= STATE_PRINT_PERIOD {
+ println!("No error detected");
+ time_last_state_printed = time::Instant::now();
+ }
+ }
+
+ let elapsed = time::Instant::now() - time_beginning_loop;
+
+ if elapsed < PING_PERIOD {
+ let to_wait = PING_PERIOD - elapsed;
+ thread::sleep(to_wait);
+ }
+ }
+}
+
+#[derive(Debug)]
+enum PingError {
+ SocketError(std::io::Error),
+ WrongMessageReceived(String),
+}
+
+fn ping(socket: &UdpSocket, rng: &mut ThreadRng) -> std::result::Result<(), PingError> {
+ let number: u64 = rng.gen();
+ let mut buffer = number.to_le_bytes();
+
+ match socket.send(&buffer) {
+ Ok(_size_sent) => {
+ buffer.fill(0);
+ match socket.recv(&mut buffer) {
+ Ok(size_received) => {
+ if size_received == 8 {
+ let number_received = u64::from_le_bytes(buffer);
+ if number_received != number {
+ return Err(PingError::WrongMessageReceived(format!(
+ "Message number receceived ({}) is not equal to the one sent ({})",
+ number_received, number
+ )));
+ }
+ } else {
+ return Err(PingError::WrongMessageReceived(format!(
+ "Size of packet must be 8, received size: {}",
+ size_received
+ )));
+ }
+ }
+ Err(error) => return Err(PingError::SocketError(error)),
+ }
+ }
+ Err(error) => return Err(PingError::SocketError(error)),
+ }
+
+ Ok(())
+}
+
+fn send_email(title: &str, body: &str, login: &str, pass: &str) -> Result<()> {
+ let email = Message::builder()
+ .message_id(None)
+ .from("Staking Watchdog <redmine@d-lan.net>".parse()?)
+ .to("Greg Burri <greg.burri@gmail.com>".parse()?)
+ .subject(title)
+ .header(ContentType::TEXT_PLAIN)
+ .body(body.to_string())?;
+
+ let creds = Credentials::new(login.to_string(), pass.to_string());
+
+ // Open a remote connection to gmail
+ let mailer = SmtpTransport::relay("mail.gandi.net")?
+ .credentials(creds)
+ .build();
+
+ // Send the email
+ let response = mailer.send(&email)?;
+
+ println!("{:?}", response);
+
+ Ok(())
+}