]>
Commit | Line | Data |
---|---|---|
51b499db DM |
1 | //! Tools and utilities |
2 | //! | |
3 | //! This is a collection of small and useful tools. | |
f12f8ff1 DM |
4 | use failure::*; |
5 | use nix::unistd; | |
6 | use nix::sys::stat; | |
7 | ||
74a69302 DM |
8 | use lazy_static::lazy_static; |
9 | ||
365bb90f | 10 | use std::fs::{File, OpenOptions}; |
f12f8ff1 DM |
11 | use std::io::Write; |
12 | use std::path::Path; | |
43eeef28 DM |
13 | use std::io::Read; |
14 | use std::io::ErrorKind; | |
1628a4c7 | 15 | use std::time::Duration; |
f12f8ff1 | 16 | |
443f3743 | 17 | use std::os::unix::io::RawFd; |
eae8aa3a | 18 | use std::os::unix::io::AsRawFd; |
365bb90f | 19 | |
496a6784 DM |
20 | use std::collections::HashMap; |
21 | ||
af926291 | 22 | use serde_json::Value; |
0fe5d605 | 23 | |
8cf6e764 | 24 | pub mod timer; |
7f0d67cf | 25 | pub mod wrapped_reader_stream; |
8f973f81 DM |
26 | #[macro_use] |
27 | pub mod common_regex; | |
8d04280b | 28 | pub mod ticket; |
6ed25cbe | 29 | pub mod borrow; |
b4d5787d | 30 | pub mod fs; |
c9b296f1 | 31 | pub mod tty; |
f54c1998 | 32 | pub mod signalfd; |
dce94d0e | 33 | pub mod daemon; |
8cf6e764 | 34 | |
3b151414 DM |
35 | #[macro_use] |
36 | mod file_logger; | |
37 | pub use file_logger::*; | |
38 | ||
e80503d2 DM |
39 | /// Macro to write error-handling blocks (like perl eval {}) |
40 | /// | |
41 | /// #### Example: | |
42 | /// ``` | |
fe651dd6 DM |
43 | /// # #[macro_use] extern crate proxmox_backup; |
44 | /// # use failure::*; | |
45 | /// # let some_condition = false; | |
e80503d2 DM |
46 | /// let result = try_block!({ |
47 | /// if (some_condition) { | |
48 | /// bail!("some error"); | |
49 | /// } | |
50 | /// Ok(()) | |
51 | /// }) | |
52 | /// .map_err(|e| format_err!("my try block returned an error - {}", e)); | |
53 | /// ``` | |
54 | ||
f0dbba8c DM |
55 | #[macro_export] |
56 | macro_rules! try_block { | |
57 | { $($token:tt)* } => {{ (|| -> Result<_,_> { $($token)* })() }} | |
58 | } | |
59 | ||
5d14eb6a | 60 | |
fded74d0 | 61 | /// The `BufferedRead` trait provides a single function |
0a72e267 DM |
62 | /// `buffered_read`. It returns a reference to an internal buffer. The |
63 | /// purpose of this traid is to avoid unnecessary data copies. | |
fded74d0 | 64 | pub trait BufferedRead { |
318564ac DM |
65 | /// This functions tries to fill the internal buffers, then |
66 | /// returns a reference to the available data. It returns an empty | |
67 | /// buffer if `offset` points to the end of the file. | |
0a72e267 DM |
68 | fn buffered_read(&mut self, offset: u64) -> Result<&[u8], Error>; |
69 | } | |
70 | ||
51b499db DM |
71 | /// Directly map a type into a binary buffer. This is mostly useful |
72 | /// for reading structured data from a byte stream (file). You need to | |
73 | /// make sure that the buffer location does not change, so please | |
74 | /// avoid vec resize while you use such map. | |
75 | /// | |
76 | /// This function panics if the buffer is not large enough. | |
50ea4396 | 77 | pub fn map_struct<T>(buffer: &[u8]) -> Result<&T, Error> { |
dc3de618 DM |
78 | if buffer.len() < ::std::mem::size_of::<T>() { |
79 | bail!("unable to map struct - buffer too small"); | |
80 | } | |
95bd5dfe | 81 | Ok(unsafe { & * (buffer.as_ptr() as *const T) }) |
dc3de618 DM |
82 | } |
83 | ||
51b499db DM |
84 | /// Directly map a type into a mutable binary buffer. This is mostly |
85 | /// useful for writing structured data into a byte stream (file). You | |
86 | /// need to make sure that the buffer location does not change, so | |
87 | /// please avoid vec resize while you use such map. | |
88 | /// | |
89 | /// This function panics if the buffer is not large enough. | |
50ea4396 | 90 | pub fn map_struct_mut<T>(buffer: &mut [u8]) -> Result<&mut T, Error> { |
dc3de618 DM |
91 | if buffer.len() < ::std::mem::size_of::<T>() { |
92 | bail!("unable to map struct - buffer too small"); | |
93 | } | |
95bd5dfe | 94 | Ok(unsafe { &mut * (buffer.as_ptr() as *mut T) }) |
dc3de618 DM |
95 | } |
96 | ||
6235a418 | 97 | pub fn file_read_firstline<P: AsRef<Path>>(path: P) -> Result<String, Error> { |
447787ab DM |
98 | |
99 | let path = path.as_ref(); | |
100 | ||
6235a418 DM |
101 | try_block!({ |
102 | let file = std::fs::File::open(path)?; | |
447787ab | 103 | |
6235a418 | 104 | use std::io::{BufRead, BufReader}; |
447787ab | 105 | |
6235a418 | 106 | let mut reader = BufReader::new(file); |
447787ab | 107 | |
6235a418 | 108 | let mut line = String::new(); |
447787ab | 109 | |
6235a418 | 110 | let _ = reader.read_line(&mut line)?; |
447787ab | 111 | |
6235a418 DM |
112 | Ok(line) |
113 | }).map_err(|err: Error| format_err!("unable to read {:?} - {}", path, err)) | |
447787ab DM |
114 | } |
115 | ||
12400210 DM |
116 | pub fn file_get_contents<P: AsRef<Path>>(path: P) -> Result<Vec<u8>, Error> { |
117 | ||
118 | let path = path.as_ref(); | |
119 | ||
120 | try_block!({ | |
121 | std::fs::read(path) | |
122 | }).map_err(|err| format_err!("unable to read {:?} - {}", path, err)) | |
53157ca6 DM |
123 | } |
124 | ||
53cafb59 DM |
125 | pub fn file_get_json<P: AsRef<Path>>(path: P) -> Result<Value, Error> { |
126 | ||
127 | let path = path.as_ref(); | |
128 | ||
129 | let raw = file_get_contents(path)?; | |
130 | ||
131 | try_block!({ | |
132 | let data = String::from_utf8(raw)?; | |
133 | let json = serde_json::from_str(&data)?; | |
134 | Ok(json) | |
135 | }).map_err(|err: Error| format_err!("unable to read json from {:?} - {}", path, err)) | |
136 | } | |
137 | ||
51b499db DM |
138 | /// Atomically write a file. We first create a temporary file, which |
139 | /// is then renamed. | |
f12f8ff1 DM |
140 | pub fn file_set_contents<P: AsRef<Path>>( |
141 | path: P, | |
142 | data: &[u8], | |
143 | perm: Option<stat::Mode>, | |
144 | ) -> Result<(), Error> { | |
145 | ||
146 | let path = path.as_ref(); | |
147 | ||
d64d80d2 DM |
148 | // Note: we use mkstemp heŕe, because this worka with different |
149 | // processes, threads, and even tokio tasks. | |
f12f8ff1 DM |
150 | let mut template = path.to_owned(); |
151 | template.set_extension("tmp_XXXXXX"); | |
152 | let (fd, tmp_path) = match unistd::mkstemp(&template) { | |
153 | Ok((fd, path)) => (fd, path), | |
154 | Err(err) => bail!("mkstemp {:?} failed: {}", template, err), | |
155 | }; | |
156 | ||
157 | let tmp_path = tmp_path.as_path(); | |
158 | ||
1a7bc3dd | 159 | let mode : stat::Mode = perm.unwrap_or(stat::Mode::from( |
f12f8ff1 DM |
160 | stat::Mode::S_IRUSR | stat::Mode::S_IWUSR | |
161 | stat::Mode::S_IRGRP | stat::Mode::S_IROTH | |
1a7bc3dd | 162 | )); |
f12f8ff1 DM |
163 | |
164 | if let Err(err) = stat::fchmod(fd, mode) { | |
165 | let _ = unistd::unlink(tmp_path); | |
166 | bail!("fchmod {:?} failed: {}", tmp_path, err); | |
167 | } | |
168 | ||
169 | use std::os::unix::io::FromRawFd; | |
170 | let mut file = unsafe { File::from_raw_fd(fd) }; | |
171 | ||
172 | if let Err(err) = file.write_all(data) { | |
173 | let _ = unistd::unlink(tmp_path); | |
174 | bail!("write failed: {}", err); | |
175 | } | |
176 | ||
177 | if let Err(err) = std::fs::rename(tmp_path, path) { | |
178 | let _ = unistd::unlink(tmp_path); | |
179 | bail!("Atomic rename failed for file {:?} - {}", path, err); | |
180 | } | |
181 | ||
182 | Ok(()) | |
183 | } | |
43eeef28 | 184 | |
51b499db DM |
185 | /// Create a file lock using fntl. This function allows you to specify |
186 | /// a timeout if you want to avoid infinite blocking. | |
1628a4c7 WB |
187 | pub fn lock_file<F: AsRawFd>( |
188 | file: &mut F, | |
189 | exclusive: bool, | |
190 | timeout: Option<Duration>, | |
eb90c9e3 | 191 | ) -> Result<(), Error> { |
1628a4c7 WB |
192 | let lockarg = |
193 | if exclusive { | |
194 | nix::fcntl::FlockArg::LockExclusive | |
195 | } else { | |
196 | nix::fcntl::FlockArg::LockShared | |
197 | }; | |
198 | ||
199 | let timeout = match timeout { | |
200 | None => { | |
201 | nix::fcntl::flock(file.as_raw_fd(), lockarg)?; | |
202 | return Ok(()); | |
203 | } | |
204 | Some(t) => t, | |
205 | }; | |
206 | ||
207 | // unblock the timeout signal temporarily | |
208 | let _sigblock_guard = timer::unblock_timeout_signal(); | |
209 | ||
210 | // setup a timeout timer | |
211 | let mut timer = timer::Timer::create( | |
212 | timer::Clock::Realtime, | |
213 | timer::TimerEvent::ThisThreadSignal(timer::SIGTIMEOUT))?; | |
214 | ||
215 | timer.arm(timer::TimerSpec::new() | |
216 | .value(Some(timeout)) | |
217 | .interval(Some(Duration::from_millis(10))))?; | |
218 | ||
219 | nix::fcntl::flock(file.as_raw_fd(), lockarg)?; | |
220 | Ok(()) | |
221 | } | |
365bb90f | 222 | |
51b499db DM |
223 | /// Open or create a lock file (append mode). Then try to |
224 | /// aquire a lock using `lock_file()`. | |
1628a4c7 WB |
225 | pub fn open_file_locked<P: AsRef<Path>>(path: P, timeout: Duration) |
226 | -> Result<File, Error> | |
227 | { | |
228 | let path = path.as_ref(); | |
229 | let mut file = | |
230 | match OpenOptions::new() | |
231 | .create(true) | |
232 | .append(true) | |
233 | .open(path) | |
234 | { | |
365bb90f DM |
235 | Ok(file) => file, |
236 | Err(err) => bail!("Unable to open lock {:?} - {}", | |
237 | path, err), | |
238 | }; | |
28b96b56 DM |
239 | match lock_file(&mut file, true, Some(timeout)) { |
240 | Ok(_) => Ok(file), | |
241 | Err(err) => bail!("Unable to aquire lock {:?} - {}", | |
242 | path, err), | |
243 | } | |
365bb90f DM |
244 | } |
245 | ||
51b499db DM |
246 | /// Split a file into equal sized chunks. The last chunk may be |
247 | /// smaller. Note: We cannot implement an `Iterator`, because iterators | |
248 | /// cannot return a borrowed buffer ref (we want zero-copy) | |
43eeef28 DM |
249 | pub fn file_chunker<C, R>( |
250 | mut file: R, | |
251 | chunk_size: usize, | |
606ce64b | 252 | mut chunk_cb: C |
43eeef28 | 253 | ) -> Result<(), Error> |
606ce64b | 254 | where C: FnMut(usize, &[u8]) -> Result<bool, Error>, |
43eeef28 DM |
255 | R: Read, |
256 | { | |
257 | ||
258 | const READ_BUFFER_SIZE: usize = 4*1024*1024; // 4M | |
259 | ||
260 | if chunk_size > READ_BUFFER_SIZE { bail!("chunk size too large!"); } | |
261 | ||
262 | let mut buf = vec![0u8; READ_BUFFER_SIZE]; | |
263 | ||
264 | let mut pos = 0; | |
265 | let mut file_pos = 0; | |
266 | loop { | |
267 | let mut eof = false; | |
268 | let mut tmp = &mut buf[..]; | |
269 | // try to read large portions, at least chunk_size | |
270 | while pos < chunk_size { | |
271 | match file.read(tmp) { | |
272 | Ok(0) => { eof = true; break; }, | |
273 | Ok(n) => { | |
274 | pos += n; | |
275 | if pos > chunk_size { break; } | |
276 | tmp = &mut tmp[n..]; | |
277 | } | |
278 | Err(ref e) if e.kind() == ErrorKind::Interrupted => { /* try again */ } | |
5f0c2d56 | 279 | Err(e) => bail!("read chunk failed - {}", e.to_string()), |
43eeef28 DM |
280 | } |
281 | } | |
43eeef28 DM |
282 | let mut start = 0; |
283 | while start + chunk_size <= pos { | |
284 | if !(chunk_cb)(file_pos, &buf[start..start+chunk_size])? { break; } | |
285 | file_pos += chunk_size; | |
286 | start += chunk_size; | |
287 | } | |
288 | if eof { | |
289 | if start < pos { | |
290 | (chunk_cb)(file_pos, &buf[start..pos])?; | |
291 | //file_pos += pos - start; | |
292 | } | |
293 | break; | |
294 | } else { | |
295 | let rest = pos - start; | |
296 | if rest > 0 { | |
297 | let ptr = buf.as_mut_ptr(); | |
298 | unsafe { std::ptr::copy_nonoverlapping(ptr.add(start), ptr, rest); } | |
299 | pos = rest; | |
300 | } else { | |
301 | pos = 0; | |
302 | } | |
303 | } | |
304 | } | |
305 | ||
306 | Ok(()) | |
43eeef28 | 307 | } |
0fe5d605 | 308 | |
5d14eb6a DM |
309 | // Returns the Unix uid/gid for the sepcified system user. |
310 | pub fn getpwnam_ugid(username: &str) -> Result<(libc::uid_t,libc::gid_t), Error> { | |
311 | let info = unsafe { libc::getpwnam(std::ffi::CString::new(username).unwrap().as_ptr()) }; | |
312 | if info == std::ptr::null_mut() { | |
313 | bail!("getwpnam '{}' failed", username); | |
314 | } | |
315 | ||
316 | let info = unsafe { *info }; | |
317 | ||
318 | Ok((info.pw_uid, info.pw_gid)) | |
319 | } | |
320 | ||
321 | // Returns the hosts node name (UTS node name) | |
74a69302 DM |
322 | pub fn nodename() -> &'static str { |
323 | ||
324 | lazy_static!{ | |
325 | static ref NODENAME: String = { | |
326 | ||
0d38dcb4 DM |
327 | nix::sys::utsname::uname() |
328 | .nodename() | |
329 | .split('.') | |
330 | .next() | |
331 | .unwrap() | |
332 | .to_owned() | |
74a69302 DM |
333 | }; |
334 | } | |
335 | ||
336 | &NODENAME | |
337 | } | |
338 | ||
f5f13ebc DM |
339 | pub fn json_object_to_query(data: Value) -> Result<String, Error> { |
340 | ||
341 | let mut query = url::form_urlencoded::Serializer::new(String::new()); | |
342 | ||
343 | let object = data.as_object().ok_or_else(|| { | |
344 | format_err!("json_object_to_query: got wrong data type (expected object).") | |
345 | })?; | |
346 | ||
347 | for (key, value) in object { | |
348 | match value { | |
349 | Value::Bool(b) => { query.append_pair(key, &b.to_string()); } | |
350 | Value::Number(n) => { query.append_pair(key, &n.to_string()); } | |
351 | Value::String(s) => { query.append_pair(key, &s); } | |
352 | Value::Array(arr) => { | |
353 | for element in arr { | |
354 | match element { | |
355 | Value::Bool(b) => { query.append_pair(key, &b.to_string()); } | |
356 | Value::Number(n) => { query.append_pair(key, &n.to_string()); } | |
357 | Value::String(s) => { query.append_pair(key, &s); } | |
358 | _ => bail!("json_object_to_query: unable to handle complex array data types."), | |
359 | } | |
360 | } | |
361 | } | |
362 | _ => bail!("json_object_to_query: unable to handle complex data types."), | |
363 | } | |
364 | } | |
365 | ||
366 | Ok(query.finish()) | |
367 | } | |
368 | ||
0fe5d605 DM |
369 | pub fn required_string_param<'a>(param: &'a Value, name: &str) -> Result<&'a str, Error> { |
370 | match param[name].as_str() { | |
371 | Some(s) => Ok(s), | |
372 | None => bail!("missing parameter '{}'", name), | |
373 | } | |
374 | } | |
0d38dcb4 DM |
375 | |
376 | pub fn required_integer_param<'a>(param: &'a Value, name: &str) -> Result<i64, Error> { | |
377 | match param[name].as_i64() { | |
378 | Some(s) => Ok(s), | |
379 | None => bail!("missing parameter '{}'", name), | |
f8dfbb45 DM |
380 | } |
381 | } | |
382 | ||
383 | pub fn required_array_param<'a>(param: &'a Value, name: &str) -> Result<Vec<Value>, Error> { | |
384 | match param[name].as_array() { | |
385 | Some(s) => Ok(s.to_vec()), | |
386 | None => bail!("missing parameter '{}'", name), | |
0d38dcb4 DM |
387 | } |
388 | } | |
383e8577 | 389 | |
496a6784 | 390 | pub fn complete_file_name(arg: &str, _param: &HashMap<String, String>) -> Vec<String> { |
383e8577 DM |
391 | |
392 | let mut result = vec![]; | |
393 | ||
394 | use nix::fcntl::OFlag; | |
395 | use nix::sys::stat::Mode; | |
396 | use nix::fcntl::AtFlags; | |
397 | ||
806500cd | 398 | let mut dirname = std::path::PathBuf::from(if arg.len() == 0 { "./" } else { arg }); |
383e8577 DM |
399 | |
400 | let is_dir = match nix::sys::stat::fstatat(libc::AT_FDCWD, &dirname, AtFlags::empty()) { | |
401 | Ok(stat) => (stat.st_mode & libc::S_IFMT) == libc::S_IFDIR, | |
402 | Err(_) => false, | |
403 | }; | |
404 | ||
405 | if !is_dir { | |
406 | if let Some(parent) = dirname.parent() { | |
407 | dirname = parent.to_owned(); | |
408 | } | |
409 | } | |
410 | ||
411 | let mut dir = match nix::dir::Dir::openat(libc::AT_FDCWD, &dirname, OFlag::O_DIRECTORY, Mode::empty()) { | |
412 | Ok(d) => d, | |
413 | Err(_) => return result, | |
414 | }; | |
415 | ||
416 | for item in dir.iter() { | |
417 | if let Ok(entry) = item { | |
418 | if let Ok(name) = entry.file_name().to_str() { | |
419 | if name == "." || name == ".." { continue; } | |
420 | let mut newpath = dirname.clone(); | |
421 | newpath.push(name); | |
422 | ||
423 | if let Ok(stat) = nix::sys::stat::fstatat(libc::AT_FDCWD, &newpath, AtFlags::empty()) { | |
424 | if (stat.st_mode & libc::S_IFMT) == libc::S_IFDIR { | |
425 | newpath.push(""); | |
426 | if let Some(newpath) = newpath.to_str() { | |
427 | result.push(newpath.to_owned()); | |
428 | } | |
429 | continue; | |
430 | } | |
431 | } | |
432 | if let Some(newpath) = newpath.to_str() { | |
433 | result.push(newpath.to_owned()); | |
434 | } | |
435 | ||
436 | } | |
437 | } | |
438 | } | |
439 | ||
440 | result | |
441 | } | |
443f3743 DM |
442 | |
443 | /// Scan directory for matching file names. | |
444 | /// | |
445 | /// Scan through all directory entries and call `callback()` function | |
446 | /// if the entry name matches the regular expression. This function | |
447 | /// used unix `openat()`, so you can pass absolute or relative file | |
448 | /// names. This function simply skips non-UTF8 encoded names. | |
449 | pub fn scandir<P, F>( | |
450 | dirfd: RawFd, | |
121f18ef | 451 | path: &P, |
443f3743 | 452 | regex: ®ex::Regex, |
cce1676a | 453 | mut callback: F |
443f3743 | 454 | ) -> Result<(), Error> |
cce1676a | 455 | where F: FnMut(RawFd, &str, nix::dir::Type) -> Result<(), Error>, |
121f18ef | 456 | P: ?Sized + nix::NixPath, |
443f3743 | 457 | { |
121f18ef | 458 | for entry in self::fs::scan_subdir(dirfd, path, regex)? { |
443f3743 DM |
459 | let entry = entry?; |
460 | let file_type = match entry.file_type() { | |
461 | Some(file_type) => file_type, | |
462 | None => bail!("unable to detect file type"), | |
463 | }; | |
443f3743 | 464 | |
121f18ef | 465 | callback(entry.parent_fd(), unsafe { entry.file_name_utf8_unchecked() }, file_type)?; |
443f3743 DM |
466 | } |
467 | Ok(()) | |
468 | } | |
7e13b2d6 DM |
469 | |
470 | pub fn get_hardware_address() -> Result<String, Error> { | |
471 | ||
1631c54f | 472 | static FILENAME: &str = "/etc/ssh/ssh_host_rsa_key.pub"; |
7e13b2d6 | 473 | |
1631c54f | 474 | let contents = file_get_contents(FILENAME)?; |
7e13b2d6 DM |
475 | let digest = md5::compute(contents); |
476 | ||
477 | Ok(format!("{:0x}", digest)) | |
478 | } | |
22968600 DM |
479 | |
480 | pub fn digest_to_hex(digest: &[u8]) -> String { | |
481 | ||
482 | const HEX_CHARS: &'static [u8; 16] = b"0123456789abcdef"; | |
483 | ||
484 | let mut buf = Vec::<u8>::with_capacity(digest.len()*2); | |
485 | ||
486 | for i in 0..digest.len() { | |
487 | buf.push(HEX_CHARS[(digest[i] >> 4) as usize]); | |
488 | buf.push(HEX_CHARS[(digest[i] & 0xf) as usize]); | |
489 | } | |
490 | ||
491 | unsafe { String::from_utf8_unchecked(buf) } | |
492 | } | |
493 | ||
af2fddea DM |
494 | pub fn assert_if_modified(digest1: &str, digest2: &str) -> Result<(), Error> { |
495 | if digest1 != digest2 { | |
496 | bail!("detected modified configuration - file changed by other user? Try again."); | |
497 | } | |
498 | Ok(()) | |
499 | } | |
b9903d63 DM |
500 | |
501 | /// Extract authentication cookie from cookie header. | |
502 | /// We assume cookie_name is already url encoded. | |
503 | pub fn extract_auth_cookie(cookie: &str, cookie_name: &str) -> Option<String> { | |
504 | ||
505 | for pair in cookie.split(';') { | |
506 | ||
507 | let (name, value) = match pair.find('=') { | |
508 | Some(i) => (pair[..i].trim(), pair[(i + 1)..].trim()), | |
509 | None => return None, // Cookie format error | |
510 | }; | |
511 | ||
512 | if name == cookie_name { | |
513 | use url::percent_encoding::percent_decode; | |
514 | if let Ok(value) = percent_decode(value.as_bytes()).decode_utf8() { | |
515 | return Some(value.into()); | |
516 | } else { | |
517 | return None; // Cookie format error | |
518 | } | |
519 | } | |
520 | } | |
521 | ||
522 | None | |
523 | } | |
af53186e DM |
524 | |
525 | pub fn join(data: &Vec<String>, sep: char) -> String { | |
526 | ||
527 | let mut list = String::new(); | |
528 | ||
529 | for item in data { | |
530 | if list.len() != 0 { list.push(sep); } | |
531 | list.push_str(item); | |
532 | } | |
533 | ||
534 | list | |
535 | } | |
ff7049d4 WB |
536 | |
537 | pub fn fd_change_cloexec(fd: RawFd, on: bool) -> Result<(), Error> { | |
538 | use nix::fcntl::{fcntl, F_GETFD, F_SETFD, FdFlag}; | |
539 | let mut flags = FdFlag::from_bits(fcntl(fd, F_GETFD)?) | |
540 | .ok_or_else(|| format_err!("unhandled file flags"))?; // nix crate is stupid this way... | |
541 | flags.set(FdFlag::FD_CLOEXEC, on); | |
542 | fcntl(fd, F_SETFD(flags))?; | |
543 | Ok(()) | |
544 | } |