F2QYIRKBFYFWSTB7Z5CNGSZVYI6XQO3MHPUHJLBHPGLIOG7UNVNQC
A4JWJ2GZLECVXZYT24PX2FNNXERZG5QDFNUZDT7NGA22QXBJHLDAC
54NTBFDPH7QNK7J5ACSLOIU7B5ELJ2FJUJYWW4LTCXZAS6NE227QC
NXADNFPSCM7ETIGZ676I7X3WENZPRBEZGIOLDWC2FSY652QK2UBAC
T2HK3ZSDLLLGHROM77MND4UZF663WSHT5J2CT7ZMXDH6MMIZFOAQC
B2MSSEJB4GIBFA6F2HRMK7FXUHJBZIZFK6NOHY7YZUSHRB53URGQC
LSRFYRWWQXJ2LVGZ7FWF5O57E4RK45EE6NWLNTRSRO5M7TFW7PZAC
VEN5WJYRT23IT77JAAZ5CJRSW3GUTTNMAECT3WVTHQA34HI4646AC
F6L2P7VTKQUV64O66XPEIRG46ZO36E4FWFN3KNMBPRHQLQWL6HSAC
K7M77GF5ILC4KKKYPTLZRZ2OND7DOQDQNRYKM3N6XV2DMJURYA3QC
B2BZZXWMRR2FYPBMSSQ6WBCOWX5R3JOXSESX4LFUAQF3FOS4APBQC
5WTKSBFRO522ILOHTH5OII4EES3DMLWMTA47PXCVWCIGMXIS77KAC
TXF4WACU3YJSUMTXJ2VZ3K7OXL2Z564R5KGMVMAFIVXPCLYCMD2AC
EEBZMKMLEADO3ODXOQ7VNZNT36L7BYFRQUS2S2HLDVBZI7FHZQFQC
[[package]]
name = "arrayref"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
[[package]]
name = "arrayvec"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8"
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
]
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "crossbeam-utils"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
dependencies = [
"autocfg",
"cfg-if",
"lazy_static",
]
[[package]]
name = "directories"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fed639d60b58d0f53498ab13d26f621fd77569cc6edb031f4cc36a2ad9da0f"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a"
dependencies = [
"libc",
"redox_users",
"winapi",
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
name = "structopt"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a33f6461027d7f08a13715659b2948e1602c31a3756aeae9378bfe7518c72e82"
dependencies = [
"clap",
"lazy_static",
"structopt-derive",
]
[[package]]
name = "structopt-derive"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92e775028122a4b3dd55d58f14fc5120289c69bee99df1d117ae30f84b225c9"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
[[package]]
use chrono::{
DateTime,
Utc,
};
use comfy_table::{
Attribute,
Cell,
Table,
};
use message::{
CommandFinished,
CommandStart,
Message,
};
use rusqlite::params;
use std::{
collections::{
BTreeSet,
HashMap,
},
convert::TryInto,
path::PathBuf,
};
use uuid::Uuid;
use opt::Opt;
use structopt::StructOpt;
let command = std::env::args().into_iter().nth(1).unwrap_or_default();
match command.as_str() {
"zshaddhistory" => {
let data = CommandStart::from_env()?;
client::new().send(Message::CommandStart(data))?;
Ok(())
}
"precmd" => {
let data = CommandFinished::from_env()?;
client::new().send(Message::CommandFinished(data))?;
Ok(())
}
"session_id" => {
println!("{}", Uuid::new_v4());
Ok(())
}
"stop" => {
client::new().send(Message::Stop)?;
Ok(())
}
"running" => {
client::new().send(Message::Running)?;
Ok(())
}
"import" => {
let path = std::env::args().into_iter().nth(2).unwrap_or_default();
dbg!(&path);
let db = rusqlite::Connection::open(&path)?;
let mut stmt = db.prepare(
"select * from history left join places on places.id=history.place_id left join \
commands on history.command_id=commands.id",
)?;
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)]
struct DBEntry {
session: i64,
start_time: i64,
duration: Option<i64>,
exit_status: Option<i64>,
hostname: String,
pwd: String,
command: String,
}
let entries = stmt
.query_map(params![], |row| {
Ok(DBEntry {
session: row.get(1)?,
exit_status: row.get(4)?,
start_time: row.get(5)?,
duration: row.get(6)?,
hostname: row.get(8)?,
pwd: row.get(9)?,
command: row.get(11)?,
})
})?
.collect::<Result<BTreeSet<_>, _>>()?;
println!("{:?}", entries.len());
let mut session_ids: HashMap<(i64, String), Uuid> = HashMap::new();
let store = crate::store::new();
let xdg_dirs = xdg::BaseDirectories::with_prefix("histdb-rs").unwrap();
let datadir_path = xdg_dirs.get_data_home();
for entry in entries {
if entry.duration.is_none()
|| entry.exit_status.is_none()
|| entry.command.trim().is_empty()
{
continue;
}
let session_id = session_ids
.entry((entry.session, entry.hostname.clone()))
.or_insert(Uuid::new_v4());
let start_time = entry.start_time;
let time_start = chrono::DateTime::<Utc>::from_utc(
chrono::NaiveDateTime::from_timestamp(start_time, 0),
Utc,
);
let time_finished = chrono::DateTime::<Utc>::from_utc(
chrono::NaiveDateTime::from_timestamp(start_time + entry.duration.unwrap(), 0),
Utc,
);
let hostname = entry.hostname;
let pwd = PathBuf::from(entry.pwd);
let result = entry.exit_status.unwrap().try_into().unwrap();
let user = String::new();
let command = entry.command;
let entry = crate::entry::Entry {
time_finished,
time_start,
hostname,
pwd,
result,
session_id: *session_id,
user,
command,
};
store.add_entry(&entry, &datadir_path).unwrap();
}
let hostname = hostname::get()?.to_string_lossy().to_string();
store.commit(&hostname)?;
Ok(())
}
"server" => {
let xdg_dirs = xdg::BaseDirectories::with_prefix("histdb-rs")?;
let server = match xdg_dirs.find_cache_file("server.json") {
None => server::new(),
Some(path) => {
let file = std::fs::File::open(path).unwrap();
let reader = std::io::BufReader::new(file);
serde_json::from_reader(reader).unwrap()
}
};
let server = server.start()?;
let path = xdg_dirs.place_cache_file("server.json").unwrap();
let file = std::fs::File::create(path).unwrap();
let writer = std::io::BufWriter::new(file);
serde_json::to_writer(writer, &server).unwrap();
Ok(())
}
_ => {
let hostname = hostname::get()?.to_string_lossy().to_string();
let entries = store::new().get_nth_entries(Some(&hostname), 25)?;
let mut table = Table::new();
table.load_preset(" ");
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
table.set_header(vec![
Cell::new("tmn").add_attribute(Attribute::Bold),
Cell::new("ses").add_attribute(Attribute::Bold),
Cell::new("res").add_attribute(Attribute::Bold),
Cell::new("pwd").add_attribute(Attribute::Bold),
Cell::new("cmd").add_attribute(Attribute::Bold),
]);
for entry in entries.into_iter() {
table.add_row(vec![
format_timestamp(entry.time_finished),
format_uuid(entry.session_id),
format!("{}", entry.result),
format_pwd(entry.pwd),
entry.command.trim().to_string(),
]);
}
println!("{}", table);
Ok(())
}
}
}
fn format_timestamp(timestamp: DateTime<Utc>) -> String {
let today = Utc::now().date();
let date = timestamp.date();
if date == today {
timestamp.format("%H:%M").to_string()
} else {
timestamp.date().format("%Y-%m-%d").to_string()
}
}
let opt = Opt::from_args();
fn format_pwd(pwd: PathBuf) -> String {
let home = std::env::var("HOME").unwrap();
if pwd.starts_with(home) {
let mut without_home = PathBuf::from("~");
let pwd_components = pwd.components().skip(3);
pwd_components.for_each(|component| without_home.push(component));
without_home.to_string_lossy().to_string()
} else {
pwd.to_string_lossy().to_string()
}
}
pub fn from_env() -> Result<Self, Error> {
let command = env::args()
.into_iter()
.nth(2)
.unwrap_or_default()
.trim()
.to_string();
pub fn from_env(command: String) -> Result<Self, Error> {
use crate::{
client,
message,
message::{
CommandFinished,
CommandStart,
Message,
},
server,
store,
};
use anyhow::Result;
use chrono::{
DateTime,
Utc,
};
use comfy_table::{
Attribute,
Cell,
Table,
};
use directories::ProjectDirs;
use rusqlite::params;
use std::{
convert::TryInto,
path::PathBuf,
};
use structopt::{
clap::AppSettings::*,
StructOpt,
};
use thiserror::Error;
use uuid::Uuid;
macro_rules! into_str {
($x:expr) => {{
structopt::lazy_static::lazy_static! {
static ref DATA: String = $x.to_string();
}
DATA.as_str()
}};
}
fn project_dir() -> ProjectDirs {
ProjectDirs::from("com", "histdb-rs", "histdb-rs").unwrap()
}
fn default_data_dir() -> String {
let project_dir = project_dir();
let data_dir = project_dir.data_dir();
data_dir.to_string_lossy().to_string()
}
fn default_cache_path() -> String {
let project_dir = project_dir();
let cache_path = project_dir.cache_dir().join("server.json");
cache_path.to_string_lossy().to_string()
}
fn default_socket_path() -> String {
let project_dir = project_dir();
let socket_path = project_dir.runtime_dir().unwrap().join("server_socket");
socket_path.to_string_lossy().to_string()
}
#[derive(StructOpt, Debug)]
struct ZSHAddHistory {
#[structopt(index = 1)]
command: String,
}
#[derive(StructOpt, Debug)]
struct Server {
#[structopt(short, long, default_value = into_str!(default_cache_path()))]
cache_path: PathBuf,
}
#[derive(StructOpt, Debug)]
struct Import {
#[structopt(short, long, default_value = into_str!(default_cache_path()))]
import_file: PathBuf,
}
#[derive(StructOpt, Debug)]
enum SubCommand {
#[structopt(name = "zshaddhistory")]
ZSHAddHistory(ZSHAddHistory),
#[structopt(name = "server")]
Server(Server),
#[structopt(name = "stop")]
Stop,
#[structopt(name = "precmd")]
PreCmd,
#[structopt(name = "session_id")]
SessionID,
#[structopt(name = "running")]
Running,
#[structopt(name = "import")]
Import(Import),
}
#[derive(StructOpt, Debug)]
#[structopt(
global_settings = &[ColoredHelp, VersionlessSubcommands, NextLineHelp, GlobalVersion]
)]
pub struct Opt {
/// Path to folder in which to store the history files.
#[structopt(
global = true,
short,
long,
default_value = into_str!(default_data_dir())
)]
data_dir: PathBuf,
/// Path to the socket for communication with the server
#[structopt(global = true, short, long, default_value = into_str!(default_socket_path()))]
socket_path: PathBuf,
#[structopt(subcommand)]
sub_command: Option<SubCommand>,
}
#[derive(Error, Debug)]
pub enum Error {
#[error("{0}")]
ClientError(client::Error),
#[error("{0}")]
MessageError(message::Error),
#[error("{0}")]
ServerError(server::Error),
#[error("{0}")]
StoreError(store::Error),
#[error("can not get hostname: {0}")]
GetHostname(std::io::Error),
#[error("can not open sqlite database: {0}")]
OpenSqliteDatabase(rusqlite::Error),
#[error("can not prepare sqlite query to get entries: {0}")]
PrepareSqliteQuery(rusqlite::Error),
#[error("can not convert sqlite row: {0}")]
ConvertSqliteRow(rusqlite::Error),
#[error("can not collect entries from sqlite query: {0}")]
CollectEntries(rusqlite::Error),
}
impl From<client::Error> for Error {
fn from(err: client::Error) -> Self {
Error::ClientError(err)
}
}
impl From<message::Error> for Error {
fn from(err: message::Error) -> Self {
Error::MessageError(err)
}
}
impl From<server::Error> for Error {
fn from(err: server::Error) -> Self {
Error::ServerError(err)
}
}
impl From<store::Error> for Error {
fn from(err: store::Error) -> Self {
Error::StoreError(err)
}
}
impl Opt {
pub fn run(self) -> Result<(), Error> {
let sub_command = self.sub_command;
match sub_command {
Some(sub_command) => match sub_command {
SubCommand::ZSHAddHistory(o) => {
Self::run_zsh_add_history(o.command, self.socket_path)
}
SubCommand::Server(o) => {
Self::run_server(o.cache_path, self.socket_path, self.data_dir)
}
SubCommand::Stop => Self::run_stop(self.socket_path),
SubCommand::PreCmd => Self::run_precmd(self.socket_path),
SubCommand::SessionID => Self::run_session_id(),
SubCommand::Running => Self::run_running(self.socket_path),
SubCommand::Import(o) => Self::run_import(o.import_file, self.data_dir),
},
None => Self::run_default(self.data_dir),
}
}
fn run_default(data_dir: PathBuf) -> Result<(), Error> {
let hostname = hostname::get()
.map_err(Error::GetHostname)?
.to_string_lossy()
.to_string();
let entries = store::new(data_dir).get_nth_entries(Some(&hostname), 25)?;
let mut table = Table::new();
table.load_preset(" ");
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
table.set_header(vec![
Cell::new("tmn").add_attribute(Attribute::Bold),
Cell::new("ses").add_attribute(Attribute::Bold),
Cell::new("res").add_attribute(Attribute::Bold),
Cell::new("pwd").add_attribute(Attribute::Bold),
Cell::new("cmd").add_attribute(Attribute::Bold),
]);
for entry in entries.into_iter() {
table.add_row(vec![
format_timestamp(entry.time_finished),
format_uuid(entry.session_id),
format!("{}", entry.result),
format_pwd(entry.pwd),
entry.command.trim().to_string(),
]);
}
println!("{}", table);
Ok(())
}
fn run_zsh_add_history(command: String, socket_path: PathBuf) -> Result<(), Error> {
let data = CommandStart::from_env(command)?;
client::new(socket_path).send(Message::CommandStart(data))?;
Ok(())
}
fn run_server(
cache_path: PathBuf,
socket_path: PathBuf,
data_dir: PathBuf,
) -> Result<(), Error> {
let server = server::new(cache_path, data_dir)?;
server.start(socket_path)?;
Ok(())
}
fn run_stop(socket_path: PathBuf) -> Result<(), Error> {
client::new(socket_path).send(Message::Stop)?;
Ok(())
}
fn run_precmd(socket_path: PathBuf) -> Result<(), Error> {
let data = CommandFinished::from_env()?;
client::new(socket_path).send(Message::CommandFinished(data))?;
Ok(())
}
fn run_session_id() -> Result<(), Error> {
println!("{}", Uuid::new_v4());
Ok(())
}
fn run_running(socket_path: PathBuf) -> Result<(), Error> {
client::new(socket_path).send(Message::Running)?;
Ok(())
}
fn run_import(import_file: PathBuf, data_dir: PathBuf) -> Result<(), Error> {
let db = rusqlite::Connection::open(&import_file).map_err(Error::OpenSqliteDatabase)?;
let mut stmt = db
.prepare(
"select * from history left join places on places.id=history.place_id
left join commands on history.command_id=commands.id",
)
.map_err(Error::PrepareSqliteQuery)?;
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)]
struct DBEntry {
session: i64,
start_time: i64,
duration: Option<i64>,
exit_status: Option<i64>,
hostname: String,
pwd: String,
command: String,
}
let entries = stmt
.query_map(params![], |row| {
Ok(DBEntry {
session: row.get(1)?,
exit_status: row.get(4)?,
start_time: row.get(5)?,
duration: row.get(6)?,
hostname: row.get(8)?,
pwd: row.get(9)?,
command: row.get(11)?,
})
})
.map_err(Error::ConvertSqliteRow)?
.collect::<Result<std::collections::BTreeSet<_>, _>>()
.map_err(Error::CollectEntries)?;
println!("importing {:?} entries", entries.len());
let mut session_ids = std::collections::HashMap::new();
let store = crate::store::new(data_dir);
for entry in entries {
if entry.duration.is_none()
|| entry.exit_status.is_none()
|| entry.command.trim().is_empty()
{
continue;
}
let session_id = session_ids
.entry((entry.session, entry.hostname.clone()))
.or_insert_with(Uuid::new_v4);
let start_time = entry.start_time;
let time_start = chrono::DateTime::<Utc>::from_utc(
chrono::NaiveDateTime::from_timestamp(start_time, 0),
Utc,
);
let time_finished = chrono::DateTime::<Utc>::from_utc(
chrono::NaiveDateTime::from_timestamp(start_time + entry.duration.unwrap(), 0),
Utc,
);
let hostname = entry.hostname;
let pwd = PathBuf::from(entry.pwd);
let result = entry.exit_status.unwrap().try_into().unwrap();
let user = String::new();
let command = entry.command;
let entry = crate::entry::Entry {
time_finished,
time_start,
hostname,
pwd,
result,
session_id: *session_id,
user,
command,
};
store.add_entry(&entry).unwrap();
}
let hostname = hostname::get()
.map_err(Error::GetHostname)?
.to_string_lossy()
.to_string();
store.commit(format!("imported histdb file from {:?}", &hostname))?;
Ok(())
}
}
fn format_timestamp(timestamp: DateTime<Utc>) -> String {
let today = Utc::now().date();
let date = timestamp.date();
if date == today {
timestamp.format("%H:%M").to_string()
} else {
timestamp.date().format("%Y-%m-%d").to_string()
}
}
fn format_uuid(uuid: Uuid) -> String {
let chars = uuid.to_string().chars().collect::<Vec<_>>();
vec![chars[0], chars[1], chars[2], chars[3]]
.into_iter()
.collect()
}
fn format_pwd(pwd: PathBuf) -> String {
let home = std::env::var("HOME").unwrap();
if pwd.starts_with(home) {
let mut without_home = PathBuf::from("~");
let pwd_components = pwd.components().skip(3);
pwd_components.for_each(|component| without_home.push(component));
without_home.to_string_lossy().to_string()
} else {
pwd.to_string_lossy().to_string()
}
}
pub fn new() -> Server {
Server {
entries: HashMap::new(),
pub fn new(cache_path: PathBuf, data_dir: PathBuf) -> Result<Server, Error> {
if cache_path.exists() {
from_cachefile(cache_path, data_dir)
} else {
Ok(Server {
entries: HashMap::new(),
store: store::new(data_dir),
cache_path,
})
impl Server {
pub fn start(mut self) -> Result<Self, Error> {
let xdg_dirs = xdg::BaseDirectories::with_prefix("histdb-rs").unwrap();
let socket_path = xdg_dirs.place_runtime_file("socket").unwrap();
fn from_cachefile(cache_path: PathBuf, data_dir: PathBuf) -> Result<Server, Error> {
let file = std::fs::File::open(&cache_path).unwrap();
let reader = std::io::BufReader::new(file);
let entries = serde_json::from_reader(reader).unwrap();
let store = store::new(data_dir);
Ok(Server {
entries,
store,
cache_path,
})
}