-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathlib.rs
282 lines (252 loc) · 9.57 KB
/
lib.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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
// Copyright 2020 Google LLC
//
// Use of this source code is governed by an MIT-style license that can be found
// in the LICENSE file or at https://opensource.org/licenses/MIT.
//! A [Fleetspeak] client connector library.
//!
//! This library exposes a set of functions for writing client-side Fleetspeak
//! services. Each of these functions operates on a global connection object
//! that is lazily established. If this global connection cannot be established,
//! the library will panic (because without this connection Fleetspeak will shut
//! the service down anyway).
//!
//! Note that each service should send startup information upon its inception
//! and continue to heartbeat from time to time to notify the Fleetspeak client
//! that it did not get stuck.
//!
//! [Fleetspeak]: https://github.com/google/fleetspeak
mod io;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use lazy_static::lazy_static;
/// A Fleetspeak client communication message.
///
/// This structure represents incoming or outgoing message objects delivered by
/// Fleetspeak. This is a simplified version of the underlying Protocol Buffers
/// message that exposes too much irrelevant fields and makes the protocol easy
/// to misuse.
pub struct Message {
/// A name of the server-side service that sent or should receive the data.
pub service: String,
/// An optional message type that can be used by the server-side service.
pub kind: Option<String>,
/// The data to sent to the specified service.
pub data: Vec<u8>,
}
/// Sends a heartbeat signal to the Fleetspeak client.
///
/// All client services should heartbeat from time to time. Otherwise, from the
/// Fleetspeak perspective, the service is unresponsive and should be restarted.
///
/// The exact frequency of the required heartbeat is defined in the service
/// configuration file.
pub fn heartbeat() {
execute(&CONNECTION.output, |buf| self::io::write_heartbeat(buf))
}
/// Sends a heartbeat signal to the Fleetspeak client but no more frequently
/// than the specified `rate`.
///
/// Note that the specified `rate` should be at least the rate defined in the
/// Fleetspeak service configuration file. Because of potential slowdowns, some
/// margin of error should be left.
///
/// See documentation for the [`heartbeat`] function for more details.
///
/// [`heartbeat`]: crate::heartbeat
pub fn heartbeat_with_throttle(rate: Duration) {
lazy_static! {
static ref LAST_HEARTBEAT: Mutex<Option<Instant>> = Mutex::new(None);
}
let mut last_heartbeat = LAST_HEARTBEAT.lock()
.expect("poisoned heartbeat mutex");
match *last_heartbeat {
Some(last_heartbeat) if last_heartbeat.elapsed() < rate => {
// Do nothing if the last heartbeat happened more recently than the
// specified heartbeat rate.
return;
}
_ => (),
}
heartbeat();
*last_heartbeat = Some(Instant::now());
}
/// Sends a system message with startup information to the Fleetspeak client.
///
/// All clients are required to send this information on startup. If the client
/// does not receive this information quickly enough, the service will be
/// killed.
///
/// The `version` string should contain a self-reported version of the service.
/// This data is used primarily for statistics.
pub fn startup(version: &str) {
execute(&CONNECTION.output, |buf| self::io::write_startup(buf, version))
}
/// Sends the message to the Fleetspeak server.
///
/// The data is delivered to the server-side service as specified by the message
/// and optionally tagged with a type if specified. This optional message type
/// is irrelevant for Fleetspeak but might be useful for the service the message
/// is delivered to.
///
/// In case of any I/O failure or malformed message (e.g. due to encoding
/// problems), an error is reported.
///
/// # Examples
///
/// ```no_run
/// use fleetspeak::Message;
///
/// fleetspeak::send(Message {
/// service: String::from("example"),
/// kind: None,
/// data: String::from("Hello, world!").into_bytes(),
/// });
/// ```
pub fn send(message: Message) {
execute(&CONNECTION.output, |buf| self::io::write_message(buf, message))
}
/// Receives a message from the Fleetspeak server.
///
/// This function will block until there is a message to be read from the input.
/// Note that in particular it means your service will be unable to heartbeat
/// properly. If you are not expecting the message to arrive quickly, you should
/// use [`receive_with_heartbeat`] instead.
///
/// In case of any I/O failure or malformed message (e.g. due to parsing issues
/// or when some fields are not being present), an error is reported.
///
/// [`receive_with_heartbeat`]: crate::receive_with_heartbeat
///
/// # Examples
///
/// ```no_run
/// let message = fleetspeak::receive();
///
/// let name = std::str::from_utf8(&message.data)
/// .expect("invalid message content");
///
/// println!("Hello, {name}!");
/// ```
pub fn receive() -> Message {
execute(&CONNECTION.input, |buf| self::io::read_message(buf))
}
/// Receive a message from the Fleetspeak server, heartbeating in background.
///
/// Unlike [`receive`], `collect` will send heartbeat signals at the specified
/// `rate` while waiting for the message.
///
/// This function is useful in the main loop of your service when it is not
/// supposed to do anything until a request from the server arrives. If your
/// service is actually awaiting for a specific message to come, you should
/// use [`receive`] instead.
///
/// In case of any I/O failure or malformed message (e.g. due to parsing issues
/// or when some fields are not being present), an error is reported.
///
/// [`receive`]: crate::receive
///
/// # Examples
///
/// ```no_run
/// use std::time::Duration;
///
/// let message = fleetspeak::receive_with_heartbeat(Duration::from_secs(1));
///
/// let name = std::str::from_utf8(&message.data)
/// .expect("invalid message content");
///
/// println!("Hello, {name}!");
/// ```
pub fn receive_with_heartbeat(rate: Duration) -> Message {
// TODO(rust-lang/rust#35121): Replace with `!` once stable.
enum Never {
}
let (sender, receiver) = std::sync::mpsc::channel::<Never>();
std::thread::spawn(move || {
loop {
use std::sync::mpsc::TryRecvError::*;
// We keep hearbeating until the sender disconnects (in which case
// the receiver will receive a disconnection error).
match receiver.try_recv() {
Ok(never) => match never {},
Err(Empty) => (),
Err(Disconnected) => return,
}
heartbeat();
std::thread::sleep(rate);
}
});
let message = receive();
// Notify the heartbeat thread to shut down. However, instead of sending any
// real message we just shut the sender down and the receiver will receive
// a disconnection error.
drop(sender);
message
}
/// A connection to the Fleetspeak client.
///
/// The connection is realized through two files (specified by descriptors given
/// by the Fleetspeak client as environment variables): input and output. Each
/// of these files is guarded by a separate mutex to allow writing (e.g. for
/// sending heartbeat signals) when another thread might be busy with reading
/// messages.
struct Connection {
input: Mutex<std::fs::File>,
output: Mutex<std::fs::File>,
}
lazy_static! {
static ref CONNECTION: Connection = {
let mut input = file_from_env_var("FLEETSPEAK_COMMS_CHANNEL_INFD");
let mut output = file_from_env_var("FLEETSPEAK_COMMS_CHANNEL_OUTFD");
crate::io::handshake(&mut input, &mut output)
.expect("handshake failure");
log::info!(target: "fleetspeak", "handshake successful");
Connection {
input: Mutex::new(input),
output: Mutex::new(output),
}
};
}
/// Executes the given function with a file extracted from the mutex.
///
/// It might happen that the mutex becomes poisoned and this call will panic in
/// result. This should not be a problem in practice, because mutex poisoning
/// is a result of one of the threads being aborted. In case of a such scenario,
/// it is likely the service needs to be restarted anyway.
///
/// Any I/O error returned by the executed function indicates a fatal connection
/// failure and ends with a panic.
fn execute<F, T>(mutex: &Mutex<std::fs::File>, f: F) -> T
where
F: FnOnce(&mut std::fs::File) -> std::io::Result<T>,
{
let mut file = mutex.lock().expect("poisoned connection mutex");
match f(&mut file) {
Ok(value) => value,
Err(error) => panic!("connection failure: {}", error),
}
}
/// Creates a [`File`] object specified in the given environment variable.
///
/// Note that this function will panic if the environment variable `var` is not
/// a valid file descriptor (in which case the library cannot be initialized and
/// the service is unlikely to work anyway).
///
/// [`File`]: std::fs::File
fn file_from_env_var(var: &str) -> std::fs::File {
let fd = std::env::var(var)
.expect(&format!("invalid variable `{}`", var))
.parse()
.expect(&format!("failed to parse file descriptor"));
#[cfg(target_family = "unix")]
unsafe {
std::os::unix::io::FromRawFd::from_raw_fd(fd)
}
#[cfg(target_family = "windows")]
unsafe {
// We use `identity` to specify the type for the `parse` call above and
// then cast it to an appropriate Windows-specific pointer type.
let handle = std::convert::identity::<usize>(fd) as _;
std::os::windows::io::FromRawHandle::from_raw_handle(handle)
}
}