mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-09 14:48:08 -05:00
script/research: create raft diagnostic tool
This commit is contained in:
@@ -8,7 +8,6 @@ use fxhash::FxHashMap;
|
||||
use log::{debug, error, info, warn};
|
||||
use smol::future;
|
||||
use structopt_toml::StructOptToml;
|
||||
use fxhash::FxHashMap;
|
||||
|
||||
use darkfi::{
|
||||
async_daemonize, net,
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
raft_db
|
||||
39
script/research/raft-diag/Cargo.toml
Normal file
39
script/research/raft-diag/Cargo.toml
Normal file
@@ -0,0 +1,39 @@
|
||||
[package]
|
||||
name = "raft-diag"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
darkfi = {path = "../../../", features = ["raft"]}
|
||||
|
||||
# Async
|
||||
smol = "1.2.5"
|
||||
async-std = {version = "1.12.0", features = ["attributes"]}
|
||||
async-trait = "0.1.56"
|
||||
async-channel = "1.6.1"
|
||||
async-executor = "1.4.1"
|
||||
easy-parallel = "3.2.0"
|
||||
futures = "0.3.21"
|
||||
|
||||
# Misc
|
||||
log = "0.4.17"
|
||||
simplelog = "0.12.0"
|
||||
rand = "0.8.5"
|
||||
chrono = "0.4.19"
|
||||
thiserror = "1.0.31"
|
||||
ctrlc-async = {version= "3.2.2", default-features = false, features = ["async-std", "termination"]}
|
||||
url = "2.2.2"
|
||||
fxhash = "0.2.1"
|
||||
|
||||
# Encoding and parsing
|
||||
serde = {version = "1.0.138", features = ["derive"]}
|
||||
serde_json = "1.0.82"
|
||||
structopt = "0.3.26"
|
||||
hex = "0.4.3"
|
||||
bs58 = "0.4.0"
|
||||
toml = "0.5.9"
|
||||
|
||||
[workspace]
|
||||
|
||||
228
script/research/raft-diag/src/main.rs
Normal file
228
script/research/raft-diag/src/main.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
use async_std::sync::{Arc, Mutex};
|
||||
use std::path::Path;
|
||||
|
||||
use async_executor::Executor;
|
||||
use fxhash::FxHashMap;
|
||||
use log::{error, info, warn};
|
||||
use smol::future;
|
||||
use structopt::StructOpt;
|
||||
use url::Url;
|
||||
|
||||
use darkfi::{
|
||||
net,
|
||||
raft::{DataStore, NetMsg, ProtocolRaft, Raft},
|
||||
util::{
|
||||
cli::{get_log_config, get_log_level},
|
||||
expand_path,
|
||||
serial::{SerialDecodable, SerialEncodable},
|
||||
sleep,
|
||||
},
|
||||
Result,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, StructOpt)]
|
||||
#[structopt(name = "raft-diag")]
|
||||
pub struct Args {
|
||||
/// JSON-RPC listen URL
|
||||
#[structopt(long = "rpc", default_value = "tcp://127.0.0.1:12055")]
|
||||
pub rpc_listen: Url,
|
||||
/// Inbound listen URL
|
||||
#[structopt(long = "inbound")]
|
||||
pub inbound_url: Option<Url>,
|
||||
/// Seed Urls
|
||||
#[structopt(long = "seeds")]
|
||||
pub seed_urls: Vec<Url>,
|
||||
/// Outbound connections
|
||||
#[structopt(long = "outbound", default_value = "0")]
|
||||
pub outbound_connections: u32,
|
||||
/// Sets Datastore Path
|
||||
#[structopt(long = "path", default_value = "test1.db")]
|
||||
pub datastore: String,
|
||||
/// Check if all datastore paths provided are synced
|
||||
#[structopt(long = "check")]
|
||||
pub check: Vec<String>,
|
||||
/// Datastore path to extract and print it
|
||||
#[structopt(long = "extract")]
|
||||
pub extract: Option<String>,
|
||||
/// Number of messages to broadcast
|
||||
#[structopt(short, default_value = "0")]
|
||||
pub broadcast: u32,
|
||||
/// Increase verbosity
|
||||
#[structopt(short, parse(from_occurrences))]
|
||||
pub verbose: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, SerialEncodable, SerialDecodable, PartialEq, Eq)]
|
||||
pub struct Message {
|
||||
payload: String,
|
||||
}
|
||||
|
||||
fn extract(path: &str) -> Result<()> {
|
||||
if !Path::new(path).exists() {
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
let db = DataStore::<Message>::new(path)?;
|
||||
let commits = db.commits.get_all()?;
|
||||
|
||||
println!("{:?}", commits);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check(args: Args) -> Result<()> {
|
||||
let mut commits_check = vec![];
|
||||
|
||||
for path in args.check {
|
||||
if !Path::new(&path).exists() {
|
||||
continue
|
||||
}
|
||||
let db = DataStore::<Message>::new(&path)?;
|
||||
let commits = db.commits.get_all()?;
|
||||
commits_check.push(commits);
|
||||
}
|
||||
|
||||
let result = commits_check.windows(2).all(|w| w[0] == w[1]);
|
||||
|
||||
println!("Synced: {}", result);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_broadcasting(n: u32, sender: async_channel::Sender<Message>) -> Result<()> {
|
||||
sleep(10).await;
|
||||
info!("Start broadcasting...");
|
||||
for id in 0..n {
|
||||
sleep(3).await;
|
||||
let msg = format!("msg_test_{}", id);
|
||||
info!("Send a message {:?}", msg);
|
||||
let msg = Message { payload: msg };
|
||||
sender.send(msg).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive_loop(receiver: async_channel::Receiver<Message>) -> Result<()> {
|
||||
loop {
|
||||
let msg = receiver.recv().await?;
|
||||
info!("Receive new msg {:?}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
async fn start(args: Args, executor: Arc<Executor<'_>>) -> Result<()> {
|
||||
let net_settings = net::Settings {
|
||||
outbound_connections: args.outbound_connections,
|
||||
inbound: args.inbound_url.clone(),
|
||||
external_addr: args.inbound_url,
|
||||
seeds: args.seed_urls,
|
||||
..net::Settings::default()
|
||||
};
|
||||
|
||||
//
|
||||
// Raft
|
||||
//
|
||||
|
||||
let datastore_raft = expand_path(&args.datastore)?;
|
||||
|
||||
let seen_net_msgs = Arc::new(Mutex::new(FxHashMap::default()));
|
||||
|
||||
let mut raft =
|
||||
Raft::<Message>::new(net_settings.inbound.clone(), datastore_raft, seen_net_msgs.clone())?;
|
||||
|
||||
//
|
||||
// P2p setup
|
||||
//
|
||||
|
||||
let (p2p_send_channel, p2p_recv_channel) = async_channel::unbounded::<NetMsg>();
|
||||
|
||||
let p2p = net::P2p::new(net_settings).await;
|
||||
let p2p = p2p.clone();
|
||||
|
||||
let registry = p2p.protocol_registry();
|
||||
|
||||
let raft_node_id = raft.id.clone();
|
||||
registry
|
||||
.register(net::SESSION_ALL, move |channel, p2p| {
|
||||
let raft_node_id = raft_node_id.clone();
|
||||
let sender = p2p_send_channel.clone();
|
||||
let seen_net_msgs_cloned = seen_net_msgs.clone();
|
||||
async move {
|
||||
ProtocolRaft::init(raft_node_id, channel, sender, p2p, seen_net_msgs_cloned).await
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
p2p.clone().start(executor.clone()).await?;
|
||||
|
||||
executor.spawn(p2p.clone().run(executor.clone())).detach();
|
||||
|
||||
//
|
||||
// Waiting Exit signal
|
||||
//
|
||||
let (signal, shutdown) = async_channel::bounded::<()>(1);
|
||||
ctrlc_async::set_async_handler(async move {
|
||||
warn!("Catch exit signal");
|
||||
// cleaning up tasks running in the background
|
||||
if let Err(e) = signal.send(()).await {
|
||||
error!("Error on sending exit signal: {}", e);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
if args.broadcast != 0 {
|
||||
executor.spawn(start_broadcasting(args.broadcast, raft.get_msgs_channel())).detach();
|
||||
}
|
||||
|
||||
executor.spawn(receive_loop(raft.get_commits_channel())).detach();
|
||||
|
||||
raft.start(p2p.clone(), p2p_recv_channel.clone(), executor.clone(), shutdown.clone()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::from_args();
|
||||
let log_level = get_log_level(args.verbose.into());
|
||||
let log_config = get_log_config();
|
||||
|
||||
let mut log_path = expand_path(&args.datastore)?;
|
||||
let log_name: String = log_path.file_name().as_ref().unwrap().to_str().unwrap().to_owned();
|
||||
log_path.pop();
|
||||
let log_path = log_path.join(&format!("{}.log", log_name));
|
||||
let env_log_file_path = std::fs::File::create(log_path).unwrap();
|
||||
|
||||
simplelog::CombinedLogger::init(vec![
|
||||
simplelog::TermLogger::new(
|
||||
log_level,
|
||||
log_config.clone(),
|
||||
simplelog::TerminalMode::Mixed,
|
||||
simplelog::ColorChoice::Auto,
|
||||
),
|
||||
simplelog::WriteLogger::new(log_level, log_config, env_log_file_path),
|
||||
])?;
|
||||
|
||||
if !args.check.is_empty() {
|
||||
return check(args)
|
||||
}
|
||||
|
||||
if args.extract.is_some() {
|
||||
return extract(&args.extract.unwrap())
|
||||
}
|
||||
|
||||
// https://docs.rs/smol/latest/smol/struct.Executor.html#examples
|
||||
let ex = Arc::new(async_executor::Executor::new());
|
||||
let (signal, shutdown) = async_channel::unbounded::<()>();
|
||||
let (_, result) = easy_parallel::Parallel::new()
|
||||
// Run four executor threads
|
||||
.each(0..4, |_| future::block_on(ex.run(shutdown.recv())))
|
||||
// Run the main future on the current thread.
|
||||
.finish(|| {
|
||||
future::block_on(async {
|
||||
start(args, ex.clone()).await?;
|
||||
drop(signal);
|
||||
Ok::<(), darkfi::Error>(())
|
||||
})
|
||||
});
|
||||
|
||||
result
|
||||
}
|
||||
12
script/research/raft-diag/tmux_sessions.sh
Executable file
12
script/research/raft-diag/tmux_sessions.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
export LOG_TARGETS='!net,!sled,!rustls'
|
||||
|
||||
tmux new-session -d "./target/release/raft-diag --inbound tcp://127.0.0.1:12001 --path test1.db -v"
|
||||
sleep 3
|
||||
tmux split-window -v "./target/release/raft-diag --inbound tcp://127.0.0.1:12002 --seeds tcp://127.0.0.1:12001 --outbound 3 --path test2.db -v"
|
||||
sleep 2
|
||||
tmux split-window -h "./target/release/raft-diag --seeds tcp://127.0.0.1:12001 --outbound 3 --path test3.db -v"
|
||||
sleep 1
|
||||
tmux select-pane -t 0
|
||||
tmux split-window -h "./target/release/raft-diag --seeds tcp://127.0.0.1:12001 --outbound 3 --path test4.db -b 3 -v"
|
||||
tmux attach
|
||||
@@ -1,28 +0,0 @@
|
||||
[package]
|
||||
name = "raft-tool"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies.darkfi]
|
||||
path = "../../../"
|
||||
features = ["raft"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
async-std = "1.12.0"
|
||||
sled = "0.34.7"
|
||||
|
||||
# for taud
|
||||
crypto_box = {version = "0.7.2", features = ["std"]}
|
||||
hex = "0.4.3"
|
||||
log = "0.4.17"
|
||||
simplelog = "0.12.0"
|
||||
rand = "0.8.5"
|
||||
chrono = "0.4.19"
|
||||
url = "2.2.2"
|
||||
serde = {version = "1.0.138", features = ["derive"]}
|
||||
serde_json = "1.0.82"
|
||||
thiserror = "1.0.31"
|
||||
|
||||
|
||||
[workspace]
|
||||
@@ -1,45 +0,0 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use darkfi::rpc::jsonrpc::{ErrorCode, JsonError, JsonResponse, JsonResult};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TaudError {
|
||||
#[error("Due timestamp invalid")]
|
||||
InvalidDueTime,
|
||||
#[error("Invalid Id")]
|
||||
InvalidId,
|
||||
#[error("Invalid Data/Params: `{0}` ")]
|
||||
InvalidData(String),
|
||||
#[error("InternalError")]
|
||||
Darkfi(#[from] darkfi::error::Error),
|
||||
#[error("Json serialization error: `{0}`")]
|
||||
SerdeJsonError(String),
|
||||
}
|
||||
|
||||
pub type TaudResult<T> = std::result::Result<T, TaudError>;
|
||||
|
||||
impl From<serde_json::Error> for TaudError {
|
||||
fn from(err: serde_json::Error) -> TaudError {
|
||||
TaudError::SerdeJsonError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json_result(res: TaudResult<Value>, id: Value) -> JsonResult {
|
||||
match res {
|
||||
Ok(v) => JsonResponse::new(v, id).into(),
|
||||
Err(err) => match err {
|
||||
TaudError::InvalidId => {
|
||||
JsonError::new(ErrorCode::InvalidParams, Some("invalid task id".into()), id).into()
|
||||
}
|
||||
TaudError::InvalidData(e) | TaudError::SerdeJsonError(e) => {
|
||||
JsonError::new(ErrorCode::InvalidParams, Some(e), id).into()
|
||||
}
|
||||
TaudError::InvalidDueTime => {
|
||||
JsonError::new(ErrorCode::InvalidParams, Some("invalid due time".into()), id).into()
|
||||
}
|
||||
TaudError::Darkfi(e) => {
|
||||
JsonError::new(ErrorCode::InternalError, Some(e.to_string()), id).into()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
use std::{fs::File, io::Write};
|
||||
|
||||
use crypto_box::{aead::Aead, Box, SecretKey, KEY_SIZE};
|
||||
|
||||
use darkfi::{
|
||||
raft::DataStore,
|
||||
util::{
|
||||
expand_path,
|
||||
serial::{deserialize, SerialDecodable, SerialEncodable},
|
||||
},
|
||||
Result,
|
||||
};
|
||||
|
||||
mod error;
|
||||
mod month_tasks;
|
||||
mod task_info;
|
||||
mod util;
|
||||
|
||||
use crate::{task_info::TaskInfo, util::load};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Info<T> {
|
||||
pub logs: Vec<Log<T>>,
|
||||
pub commits: Vec<T>,
|
||||
pub voted_for: Vec<Option<NodeId>>,
|
||||
pub terms: Vec<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Log<T> {
|
||||
pub term: u64,
|
||||
pub msg: T,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NodeId(Vec<u8>);
|
||||
|
||||
#[derive(Debug, SerialEncodable, SerialDecodable)]
|
||||
struct EncryptedTask {
|
||||
nonce: Vec<u8>,
|
||||
payload: Vec<u8>,
|
||||
}
|
||||
|
||||
fn decrypt_task(encrypt_task: &EncryptedTask, secret_key: &SecretKey) -> Option<TaskInfo> {
|
||||
let public_key = secret_key.public_key();
|
||||
let msg_box = Box::new(&public_key, secret_key);
|
||||
|
||||
let nonce = encrypt_task.nonce.as_slice();
|
||||
let decrypted_task = match msg_box.decrypt(nonce.into(), &encrypt_task.payload[..]) {
|
||||
Ok(m) => m,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
deserialize(&decrypted_task).ok()
|
||||
}
|
||||
|
||||
type PrivmsgId = u32;
|
||||
|
||||
#[derive(Debug, SerialEncodable, SerialDecodable)]
|
||||
struct Privmsg {
|
||||
id: PrivmsgId,
|
||||
nickname: String,
|
||||
channel: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
fn extract_taud() -> Result<String> {
|
||||
let db_path = expand_path(&"~/.config/darkfi/tau/tau.db").unwrap();
|
||||
let datastore = DataStore::<EncryptedTask>::new(&db_path.to_str().unwrap())?;
|
||||
|
||||
let sk_path = expand_path(&"~/.config/darkfi/tau/secret_key").unwrap();
|
||||
|
||||
let sk = {
|
||||
let loaded_key = load::<String>(&sk_path);
|
||||
|
||||
if loaded_key.is_err() {
|
||||
log::error!(
|
||||
"Could not load secret key from file, \
|
||||
please run \"taud --help\" for more information"
|
||||
);
|
||||
return Ok("Load secret_key error".into())
|
||||
}
|
||||
|
||||
let sk_bytes = hex::decode(loaded_key.unwrap())?;
|
||||
let sk_bytes: [u8; KEY_SIZE] = sk_bytes.as_slice().try_into()?;
|
||||
SecretKey::try_from(sk_bytes)?
|
||||
};
|
||||
|
||||
println!("Extracting db from: {:?}", db_path);
|
||||
|
||||
// Retrieve all data trees
|
||||
let sled_logs = datastore.logs.get_all()?;
|
||||
let sled_commits = datastore.commits.get_all()?;
|
||||
let sled_voted_for = datastore.voted_for.get_all()?;
|
||||
let sled_terms = datastore.current_term.get_all()?;
|
||||
|
||||
// Parse retrieved data trees
|
||||
println!("Data extracted, parsing to viewable form...");
|
||||
|
||||
// Logs
|
||||
let mut logs = vec![];
|
||||
for log in sled_logs {
|
||||
let encrypt_task: EncryptedTask = deserialize(&log.msg)?;
|
||||
let task_info = decrypt_task(&encrypt_task, &sk).unwrap();
|
||||
logs.push(Log { term: log.term, msg: task_info });
|
||||
}
|
||||
|
||||
// Commits
|
||||
let mut commits = vec![];
|
||||
for commit in &sled_commits {
|
||||
let task_info = decrypt_task(
|
||||
&EncryptedTask { nonce: commit.nonce.clone(), payload: commit.payload.clone() },
|
||||
&sk,
|
||||
)
|
||||
.unwrap();
|
||||
commits.push(task_info);
|
||||
}
|
||||
|
||||
// Voted for
|
||||
let mut voted_for = vec![];
|
||||
for vote in sled_voted_for {
|
||||
match vote {
|
||||
Some(v) => voted_for.push(Some(NodeId(v.0.clone()))),
|
||||
None => voted_for.push(None),
|
||||
}
|
||||
}
|
||||
|
||||
// Terms
|
||||
let mut terms = vec![];
|
||||
for term in sled_terms {
|
||||
terms.push(term);
|
||||
}
|
||||
|
||||
let info = Info::<TaskInfo> { logs, commits, voted_for, terms };
|
||||
let info_string = format!("{:#?}", info);
|
||||
Ok(info_string)
|
||||
}
|
||||
|
||||
fn extract_ircd() -> Result<String> {
|
||||
let db_path = expand_path(&"~/.config/darkfi/ircd/ircd.db").unwrap();
|
||||
let datastore = DataStore::<Privmsg>::new(&db_path.to_str().unwrap())?;
|
||||
println!("Extracting db from: {:?}", db_path);
|
||||
|
||||
// Retrieve all data trees
|
||||
let sled_logs = datastore.logs.get_all()?;
|
||||
let sled_commits = datastore.commits.get_all()?;
|
||||
let sled_voted_for = datastore.voted_for.get_all()?;
|
||||
let sled_terms = datastore.current_term.get_all()?;
|
||||
|
||||
// Parse retrieved data trees
|
||||
println!("Data extracted, parsing to viewable form...");
|
||||
|
||||
// Logs
|
||||
let mut logs = vec![];
|
||||
for log in sled_logs {
|
||||
logs.push(Log { term: log.term, msg: deserialize(&log.msg)? });
|
||||
}
|
||||
|
||||
// Commits
|
||||
let mut commits = vec![];
|
||||
for commit in &sled_commits {
|
||||
commits.push(Privmsg {
|
||||
id: commit.id,
|
||||
nickname: commit.nickname.clone(),
|
||||
channel: commit.channel.clone(),
|
||||
message: commit.message.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
// Voted for
|
||||
let mut voted_for = vec![];
|
||||
for vote in sled_voted_for {
|
||||
match vote {
|
||||
Some(v) => voted_for.push(Some(NodeId(v.0.clone()))),
|
||||
None => voted_for.push(None),
|
||||
}
|
||||
}
|
||||
|
||||
// Terms
|
||||
let mut terms = vec![];
|
||||
for term in sled_terms {
|
||||
terms.push(term);
|
||||
}
|
||||
|
||||
let info = Info::<Privmsg> { logs, commits, voted_for, terms };
|
||||
let info_string = format!("{:#?}", info);
|
||||
|
||||
Ok(info_string)
|
||||
}
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() -> Result<()> {
|
||||
let info_string = extract_taud()?;
|
||||
|
||||
// Generating file
|
||||
let file_path = "raft_db";
|
||||
println!("Data parsed, writing to file {:?}", file_path);
|
||||
let mut file = File::create(file_path)?;
|
||||
file.write(info_string.as_bytes())?;
|
||||
println!("File created!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
use std::{
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use chrono::{TimeZone, Utc};
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use darkfi::util::Timestamp;
|
||||
|
||||
use crate::{
|
||||
error::{TaudError, TaudResult},
|
||||
task_info::TaskInfo,
|
||||
util::{load, save},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MonthTasks {
|
||||
created_at: Timestamp,
|
||||
task_tks: Vec<String>,
|
||||
}
|
||||
|
||||
impl MonthTasks {
|
||||
pub fn new(task_tks: &[String]) -> Self {
|
||||
Self { created_at: Timestamp::current_time(), task_tks: task_tks.to_owned() }
|
||||
}
|
||||
|
||||
pub fn add(&mut self, ref_id: &str) {
|
||||
debug!(target: "tau", "MonthTasks::add()");
|
||||
if !self.task_tks.contains(&ref_id.into()) {
|
||||
self.task_tks.push(ref_id.into());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn objects(&self, dataset_path: &Path) -> TaudResult<Vec<TaskInfo>> {
|
||||
debug!(target: "tau", "MonthTasks::objects()");
|
||||
let mut tks: Vec<TaskInfo> = vec![];
|
||||
|
||||
for ref_id in self.task_tks.iter() {
|
||||
tks.push(TaskInfo::load(ref_id, dataset_path)?);
|
||||
}
|
||||
|
||||
Ok(tks)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, ref_id: &str) {
|
||||
debug!(target: "tau", "MonthTasks::remove()");
|
||||
if let Some(index) = self.task_tks.iter().position(|t| *t == ref_id) {
|
||||
self.task_tks.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_date(&mut self, date: &Timestamp) {
|
||||
debug!(target: "tau", "MonthTasks::set_date()");
|
||||
self.created_at = *date;
|
||||
}
|
||||
|
||||
fn get_path(date: &Timestamp, dataset_path: &Path) -> PathBuf {
|
||||
debug!(target: "tau", "MonthTasks::get_path()");
|
||||
dataset_path.join("month").join(Utc.timestamp(date.0, 0).format("%m%y").to_string())
|
||||
}
|
||||
|
||||
pub fn save(&self, dataset_path: &Path) -> TaudResult<()> {
|
||||
debug!(target: "tau", "MonthTasks::save()");
|
||||
save::<Self>(&Self::get_path(&self.created_at, dataset_path), self)
|
||||
.map_err(TaudError::Darkfi)
|
||||
}
|
||||
|
||||
fn get_all(dataset_path: &Path) -> io::Result<Vec<PathBuf>> {
|
||||
debug!(target: "tau", "MonthTasks::get_all()");
|
||||
|
||||
let mut entries = fs::read_dir(dataset_path.join("month"))?
|
||||
.map(|res| res.map(|e| e.path()))
|
||||
.collect::<Result<Vec<_>, io::Error>>()?;
|
||||
|
||||
entries.sort();
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn create(date: &Timestamp, dataset_path: &Path) -> TaudResult<Self> {
|
||||
debug!(target: "tau", "MonthTasks::create()");
|
||||
|
||||
let mut mt = Self::new(&[]);
|
||||
mt.set_date(date);
|
||||
mt.save(dataset_path)?;
|
||||
Ok(mt)
|
||||
}
|
||||
|
||||
pub fn load_or_create(date: Option<&Timestamp>, dataset_path: &Path) -> TaudResult<Self> {
|
||||
debug!(target: "tau", "MonthTasks::load_or_create()");
|
||||
|
||||
// if a date is given we load that date's month tasks
|
||||
// if not, we load tasks from all months
|
||||
match date {
|
||||
Some(date) => match load::<Self>(&Self::get_path(date, dataset_path)) {
|
||||
Ok(mt) => Ok(mt),
|
||||
Err(_) => Self::create(date, dataset_path),
|
||||
},
|
||||
None => {
|
||||
let path_all = match Self::get_all(dataset_path) {
|
||||
Ok(t) => t,
|
||||
Err(_) => vec![],
|
||||
};
|
||||
|
||||
let mut loaded_mt = Self::new(&[]);
|
||||
|
||||
for path in path_all {
|
||||
let mt = load::<Self>(&path)?;
|
||||
loaded_mt.created_at = mt.created_at;
|
||||
for tks in mt.task_tks {
|
||||
if !loaded_mt.task_tks.contains(&tks) {
|
||||
loaded_mt.task_tks.push(tks)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(loaded_mt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_current_open_tasks(dataset_path: &Path) -> TaudResult<Vec<TaskInfo>> {
|
||||
let mt = Self::load_or_create(None, dataset_path)?;
|
||||
Ok(mt.objects(dataset_path)?.into_iter().filter(|t| t.get_state() != "stop").collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
fs::{create_dir_all, remove_dir_all},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use darkfi::Result;
|
||||
|
||||
const TEST_DATA_PATH: &str = "/tmp/test_tau_data";
|
||||
|
||||
fn get_path() -> Result<PathBuf> {
|
||||
remove_dir_all(TEST_DATA_PATH).ok();
|
||||
|
||||
let path = PathBuf::from(TEST_DATA_PATH);
|
||||
|
||||
// mkdir dataset_path if not exists
|
||||
create_dir_all(path.join("month"))?;
|
||||
create_dir_all(path.join("task"))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_and_save_tasks() -> TaudResult<()> {
|
||||
let dataset_path = get_path()?;
|
||||
|
||||
// load and save TaskInfo
|
||||
///////////////////////
|
||||
|
||||
let mut task =
|
||||
TaskInfo::new("test_title", "test_desc", "NICKNAME", None, 0.0, &dataset_path)?;
|
||||
|
||||
task.save(&dataset_path)?;
|
||||
|
||||
let t_load = TaskInfo::load(&task.ref_id, &dataset_path)?;
|
||||
|
||||
assert_eq!(task, t_load);
|
||||
|
||||
task.set_title("test_title_2");
|
||||
|
||||
task.save(&dataset_path)?;
|
||||
|
||||
let t_load = TaskInfo::load(&task.ref_id, &dataset_path)?;
|
||||
|
||||
assert_eq!(task, t_load);
|
||||
|
||||
// load and save MonthTasks
|
||||
///////////////////////
|
||||
|
||||
let task_tks = vec![];
|
||||
|
||||
let mut mt = MonthTasks::new(&task_tks);
|
||||
|
||||
mt.save(&dataset_path)?;
|
||||
|
||||
let mt_load = MonthTasks::load_or_create(Some(&Timestamp::current_time()), &dataset_path)?;
|
||||
|
||||
assert_eq!(mt, mt_load);
|
||||
|
||||
mt.add(&task.ref_id);
|
||||
|
||||
mt.save(&dataset_path)?;
|
||||
|
||||
let mt_load = MonthTasks::load_or_create(Some(&Timestamp::current_time()), &dataset_path)?;
|
||||
|
||||
assert_eq!(mt, mt_load);
|
||||
|
||||
// activate task
|
||||
///////////////////////
|
||||
|
||||
let task =
|
||||
TaskInfo::new("test_title_3", "test_desc", "NICKNAME", None, 0.0, &dataset_path)?;
|
||||
|
||||
task.save(&dataset_path)?;
|
||||
|
||||
let mt_load = MonthTasks::load_or_create(Some(&Timestamp::current_time()), &dataset_path)?;
|
||||
|
||||
assert!(mt_load.task_tks.contains(&task.ref_id));
|
||||
|
||||
remove_dir_all(TEST_DATA_PATH).ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use darkfi::util::{
|
||||
serial::{Decodable, Encodable, SerialDecodable, SerialEncodable, VarInt},
|
||||
Timestamp,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::{TaudError, TaudResult},
|
||||
month_tasks::MonthTasks,
|
||||
util::{find_free_id, load, random_ref_id, save},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, SerialEncodable, SerialDecodable, PartialEq)]
|
||||
struct TaskEvent {
|
||||
action: String,
|
||||
timestamp: Timestamp,
|
||||
}
|
||||
|
||||
impl TaskEvent {
|
||||
fn new(action: String) -> Self {
|
||||
Self { action, timestamp: Timestamp::current_time() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, SerialDecodable, SerialEncodable, PartialEq)]
|
||||
pub struct Comment {
|
||||
content: String,
|
||||
author: String,
|
||||
timestamp: Timestamp,
|
||||
}
|
||||
|
||||
impl Comment {
|
||||
pub fn new(content: &str, author: &str) -> Self {
|
||||
Self {
|
||||
content: content.into(),
|
||||
author: author.into(),
|
||||
timestamp: Timestamp::current_time(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TaskEvents(Vec<TaskEvent>);
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TaskComments(Vec<Comment>);
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TaskProjects(Vec<String>);
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TaskAssigns(Vec<String>);
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, SerialEncodable, SerialDecodable, PartialEq)]
|
||||
pub struct TaskInfo {
|
||||
pub(crate) ref_id: String,
|
||||
id: u32,
|
||||
title: String,
|
||||
desc: String,
|
||||
owner: String,
|
||||
assign: TaskAssigns,
|
||||
project: TaskProjects,
|
||||
due: Option<Timestamp>,
|
||||
rank: f32,
|
||||
created_at: Timestamp,
|
||||
events: TaskEvents,
|
||||
comments: TaskComments,
|
||||
}
|
||||
|
||||
impl TaskInfo {
|
||||
pub fn new(
|
||||
title: &str,
|
||||
desc: &str,
|
||||
owner: &str,
|
||||
due: Option<Timestamp>,
|
||||
rank: f32,
|
||||
dataset_path: &Path,
|
||||
) -> TaudResult<Self> {
|
||||
// generate ref_id
|
||||
let ref_id = random_ref_id();
|
||||
|
||||
let created_at = Timestamp::current_time();
|
||||
|
||||
let task_ids: Vec<u32> =
|
||||
MonthTasks::load_current_open_tasks(dataset_path)?.into_iter().map(|t| t.id).collect();
|
||||
|
||||
let id: u32 = find_free_id(&task_ids);
|
||||
|
||||
if let Some(d) = &due {
|
||||
if *d < Timestamp::current_time() {
|
||||
return Err(TaudError::InvalidDueTime)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
ref_id,
|
||||
id,
|
||||
title: title.into(),
|
||||
desc: desc.into(),
|
||||
owner: owner.into(),
|
||||
assign: TaskAssigns(vec![]),
|
||||
project: TaskProjects(vec![]),
|
||||
due,
|
||||
rank,
|
||||
created_at,
|
||||
comments: TaskComments(vec![]),
|
||||
events: TaskEvents(vec![]),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(ref_id: &str, dataset_path: &Path) -> TaudResult<Self> {
|
||||
debug!(target: "tau", "TaskInfo::load()");
|
||||
let task = load::<Self>(&Self::get_path(ref_id, dataset_path))?;
|
||||
Ok(task)
|
||||
}
|
||||
|
||||
pub fn save(&self, dataset_path: &Path) -> TaudResult<()> {
|
||||
debug!(target: "tau", "TaskInfo::save()");
|
||||
save::<Self>(&Self::get_path(&self.ref_id, dataset_path), self)
|
||||
.map_err(TaudError::Darkfi)?;
|
||||
|
||||
if self.get_state() == "stop" {
|
||||
self.deactivate(dataset_path)?;
|
||||
} else {
|
||||
self.activate(dataset_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn activate(&self, path: &Path) -> TaudResult<()> {
|
||||
debug!(target: "tau", "TaskInfo::activate()");
|
||||
let mut mt = MonthTasks::load_or_create(Some(&self.created_at), path)?;
|
||||
mt.add(&self.ref_id);
|
||||
mt.save(path)
|
||||
}
|
||||
|
||||
pub fn deactivate(&self, path: &Path) -> TaudResult<()> {
|
||||
debug!(target: "tau", "TaskInfo::deactivate()");
|
||||
let mut mt = MonthTasks::load_or_create(Some(&self.created_at), path)?;
|
||||
mt.remove(&self.ref_id);
|
||||
mt.save(path)
|
||||
}
|
||||
|
||||
pub fn get_state(&self) -> String {
|
||||
debug!(target: "tau", "TaskInfo::get_state()");
|
||||
if let Some(ev) = self.events.0.last() {
|
||||
ev.action.clone()
|
||||
} else {
|
||||
"open".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_path(ref_id: &str, dataset_path: &Path) -> PathBuf {
|
||||
debug!(target: "tau", "TaskInfo::get_path()");
|
||||
dataset_path.join("task").join(ref_id)
|
||||
}
|
||||
|
||||
pub fn get_id(&self) -> u32 {
|
||||
debug!(target: "tau", "TaskInfo::get_id()");
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn set_title(&mut self, title: &str) {
|
||||
debug!(target: "tau", "TaskInfo::set_title()");
|
||||
self.title = title.into();
|
||||
}
|
||||
|
||||
pub fn set_desc(&mut self, desc: &str) {
|
||||
debug!(target: "tau", "TaskInfo::set_desc()");
|
||||
self.desc = desc.into();
|
||||
}
|
||||
|
||||
pub fn set_assign(&mut self, assign: &[String]) {
|
||||
debug!(target: "tau", "TaskInfo::set_assign()");
|
||||
self.assign = TaskAssigns(assign.to_owned());
|
||||
}
|
||||
|
||||
pub fn set_project(&mut self, project: &[String]) {
|
||||
debug!(target: "tau", "TaskInfo::set_project()");
|
||||
self.project = TaskProjects(project.to_owned());
|
||||
}
|
||||
|
||||
pub fn set_comment(&mut self, c: Comment) {
|
||||
debug!(target: "tau", "TaskInfo::set_comment()");
|
||||
self.comments.0.push(c);
|
||||
}
|
||||
|
||||
pub fn set_rank(&mut self, r: f32) {
|
||||
debug!(target: "tau", "TaskInfo::set_rank()");
|
||||
self.rank = r;
|
||||
}
|
||||
|
||||
pub fn set_due(&mut self, d: Option<Timestamp>) {
|
||||
debug!(target: "tau", "TaskInfo::set_due()");
|
||||
self.due = d;
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, action: &str) {
|
||||
debug!(target: "tau", "TaskInfo::set_state()");
|
||||
if self.get_state() == action {
|
||||
return
|
||||
}
|
||||
self.events.0.push(TaskEvent::new(action.into()));
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for TaskEvents {
|
||||
fn encode<S: io::Write>(&self, s: S) -> darkfi::Result<usize> {
|
||||
encode_vec(&self.0, s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for TaskEvents {
|
||||
fn decode<D: io::Read>(d: D) -> darkfi::Result<Self> {
|
||||
Ok(Self(decode_vec(d)?))
|
||||
}
|
||||
}
|
||||
impl Encodable for TaskComments {
|
||||
fn encode<S: io::Write>(&self, s: S) -> darkfi::Result<usize> {
|
||||
encode_vec(&self.0, s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for TaskComments {
|
||||
fn decode<D: io::Read>(d: D) -> darkfi::Result<Self> {
|
||||
Ok(Self(decode_vec(d)?))
|
||||
}
|
||||
}
|
||||
impl Encodable for TaskProjects {
|
||||
fn encode<S: io::Write>(&self, s: S) -> darkfi::Result<usize> {
|
||||
encode_vec(&self.0, s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for TaskProjects {
|
||||
fn decode<D: io::Read>(d: D) -> darkfi::Result<Self> {
|
||||
Ok(Self(decode_vec(d)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for TaskAssigns {
|
||||
fn encode<S: io::Write>(&self, s: S) -> darkfi::Result<usize> {
|
||||
encode_vec(&self.0, s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for TaskAssigns {
|
||||
fn decode<D: io::Read>(d: D) -> darkfi::Result<Self> {
|
||||
Ok(Self(decode_vec(d)?))
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_vec<T: Encodable, S: io::Write>(vec: &[T], mut s: S) -> darkfi::Result<usize> {
|
||||
let mut len = 0;
|
||||
len += VarInt(vec.len() as u64).encode(&mut s)?;
|
||||
for c in vec.iter() {
|
||||
len += c.encode(&mut s)?;
|
||||
}
|
||||
Ok(len)
|
||||
}
|
||||
|
||||
fn decode_vec<T: Decodable, D: io::Read>(mut d: D) -> darkfi::Result<Vec<T>> {
|
||||
let len = VarInt::decode(&mut d)?.0;
|
||||
let mut ret = Vec::with_capacity(len as usize);
|
||||
for _ in 0..len {
|
||||
ret.push(Decodable::decode(&mut d)?);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
use std::{fs::File, io::BufReader, path::Path};
|
||||
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
use darkfi::Result;
|
||||
|
||||
pub fn random_ref_id() -> String {
|
||||
thread_rng().sample_iter(&Alphanumeric).take(30).map(char::from).collect()
|
||||
}
|
||||
|
||||
pub fn find_free_id(task_ids: &[u32]) -> u32 {
|
||||
for i in 1.. {
|
||||
if !task_ids.contains(&i) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
pub fn load<T: DeserializeOwned>(path: &Path) -> Result<T> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let value: T = serde_json::from_reader(reader)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn save<T: Serialize>(path: &Path, value: &T) -> Result<()> {
|
||||
let file = File::create(path)?;
|
||||
serde_json::to_writer_pretty(file, value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn find_free_id_test() -> Result<()> {
|
||||
let mut ids: Vec<u32> = vec![1, 3, 8, 9, 10, 3];
|
||||
let ids_empty: Vec<u32> = vec![];
|
||||
let ids_duplicate: Vec<u32> = vec![1; 100];
|
||||
|
||||
let find_id = find_free_id(&ids);
|
||||
|
||||
assert_eq!(find_id, 2);
|
||||
|
||||
ids.push(find_id);
|
||||
|
||||
assert_eq!(find_free_id(&ids), 4);
|
||||
|
||||
assert_eq!(find_free_id(&ids_empty), 1);
|
||||
|
||||
assert_eq!(find_free_id(&ids_duplicate), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user