-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathmain.rs
175 lines (161 loc) · 5.83 KB
/
main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
use chrono::Local;
use clap::Parser;
use error::MyResult;
use nix::sys::signal::{self, Signal};
use nix::sys::wait::{WaitPidFlag, WaitStatus};
use nix::unistd::{fork, Pid};
use nix::{sys::wait::waitpid, unistd::ForkResult};
use std::f64::consts::E;
use std::thread;
use std::time::SystemTime;
use std::{thread::sleep, time::Duration};
use crate::error::MyError;
mod clipboard;
mod error;
mod log;
mod mustatex;
mod sync;
fn main() {
let args = Args::parse();
configure_logging(&args);
if args.run_forked {
run_forked()
} else {
run()
}
}
/// cli arguments
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None, max_term_width = 120)]
struct Args {
/// granularity to log
#[arg(long, value_enum, default_value_t = log::Level::default())]
log_level: log::Level,
/// whether to include timestamps in the logs (systemd already includes
/// timestamps so you'll want to enable this for systemd)
#[arg(long)]
hide_timestamp: bool,
/// whether to run the sync forked so the state can be cleaned up
/// periodically. typically, this should be true.
#[arg(long)]
#[cfg_attr(not(debug_assertions), arg(default_value_t = true))]
run_forked: bool,
/// when debug logging is enabled, it won't show clipboard contents, because
/// clipboard contents are sensitive user information. but if you set this
/// to true, in addition to enabling debug logging, then it will log the
/// clipboard contents.
#[arg(long)]
log_clipboard_contents: bool,
}
fn configure_logging(args: &Args) {
log::level::set(args.log_level);
log::timestamp::set(!args.hide_timestamp);
log::log_sensitive_information::set(args.log_clipboard_contents);
}
#[allow(dead_code)]
fn run_forked() {
log::info!("started clipboard sync manager");
let mut panics = 0;
loop {
match unsafe { fork() }.expect("Failed to fork") {
ForkResult::Parent { child } => {
log::debug!("child process {child} successfully initialized.");
kill_after(child, 600);
let status = waitpid(Some(child), None)
.expect("there was a problem managing the child process, so the service is exiting. check that pid {child} is not running before restarting this service");
log::debug!("child process {child} completed with: {status:?}");
if let WaitStatus::Exited(_, 101) = status {
panics += 1;
if panics < 4 {
log::fatal!("child process {child} panicked. giving it another try");
} else {
panic!("child process {child} panicked too many times.");
}
}
sleep(Duration::from_secs(1));
}
ForkResult::Child => run(),
}
}
}
fn run() {
log::info!("starting clipboard sync");
loop_with_error_pain_management(
sync::get_clipboards().unwrap(),
|cb| sync::keep_synced(cb),
|_| sync::get_clipboards().unwrap(),
)
.unwrap();
}
pub fn kill_after(pid: Pid, seconds: u64) {
thread::spawn(move || {
log::debug!("waiting {seconds} seconds and then killing {pid}.");
thread::sleep(Duration::from_secs(seconds));
match waitpid(Some(pid), Some(WaitPidFlag::WNOHANG)) {
Ok(WaitStatus::StillAlive) => log::debug!("child {pid} is still alive, as expected."),
Ok(ok) => {
log::warning!("expected child process {pid} to be StillAlive but got: {ok:?}")
}
Err(e) => log::error!("error getting status of child process {pid}: {e}"),
}
log::debug!("routinely attempting to kill child process {pid}.");
if let Err(e) = signal::kill(pid, Signal::SIGTERM) {
log::error!("error killing child process {pid}: {e}")
}
});
}
/// Execute an action with a sophisticated retry mechanism
/// If the action fails:
/// - 1. run a recovery step to manipulate the input
/// - 2. attempt to execute the action again
/// If the action fails too frequently, exit
fn loop_with_error_pain_management<
Input,
Return,
Action: Fn(&Input) -> MyResult<Return>,
Recovery: Fn(Input) -> Input,
>(
// data passed into action and reset by recovery
initial_input: Input,
// action to attempt on every iteration
action: Action,
// action to attempt on every error - errors here are not yet handled. you can panic if necessary
recovery: Recovery,
) -> MyResult<Return> {
let mut input = initial_input;
let mut error_times = vec![];
let mut errors = vec![];
loop {
match action(&input) {
Ok(ret) => return Ok(ret),
Err(err) => {
log::fatal!("action exited with error: {:?}", err);
let now = SystemTime::now();
error_times.push(now);
errors.push(err);
if total_pain(now, error_times.clone()) > 5.0 {
return Err(MyError::Crash {
msg: "too many errors, exiting".to_string(),
cause: errors,
});
}
input = recovery(input);
sleep(Duration::from_millis(1000));
}
}
log::info!("retrying");
}
}
/// Sum the pain of numerous painful events, measured by how long ago they
/// happened.
fn total_pain(now: SystemTime, errors: Vec<SystemTime>) -> f64 {
errors
.into_iter()
.map(|et| remaining_pain(now.duration_since(et).unwrap().as_secs()))
.sum()
}
/// Looks at a painful event and determines how much of its pain is left.
/// calculated as exponential decay with a half-life of 1 minute.
fn remaining_pain(seconds_ago: u64) -> f64 {
E.powf(-(seconds_ago as f64) / 86.561_702_453_337_8)
}