syslog_rs/sync/
syslog_sync_internal.rs

1/*-
2 * syslog-rs - a syslog client translated from libc to rust
3 * 
4 * Copyright 2025 Aleksandr Morozov
5 * 
6 * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by
7 * the European Commission - subsequent versions of the EUPL (the "Licence").
8 * 
9 * You may not use this work except in compliance with the Licence.
10 * 
11 * You may obtain a copy of the Licence at:
12 * 
13 *    https://joinup.ec.europa.eu/software/page/eupl
14 * 
15 * Unless required by applicable law or agreed to in writing, software
16 * distributed under the Licence is distributed on an "AS IS" basis, WITHOUT
17 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
18 * Licence for the specific language governing permissions and limitations
19 * under the Licence.
20 */
21
22
23use std::borrow::Cow;
24use std::marker::PhantomData;
25use std::thread::sleep;
26use std::time::Duration;
27
28use nix::libc;
29
30#[cfg(any(
31    target_os = "freebsd",
32    target_os = "dragonfly",
33    target_os = "openbsd",
34    target_os = "netbsd",
35    target_os = "macos"
36))]
37use chrono::SecondsFormat;
38
39use crate::formatters::SyslogFormatter;
40use crate::map_error_os;
41use crate::portable;
42use crate::common::*;
43use crate::error::{SyRes/* , SyslogError*/};
44use crate::socket::TapType;
45use crate::socket::TapTypeData;
46
47use super::socket::*;
48
49/// A common instance which describes the syslog state
50#[derive(Debug)]
51pub struct SyncSyslogInternal<F: SyslogFormatter>
52{   
53    /// An identification i.e program name, thread name
54    logtag: String, 
55
56    /// A pid of the program.
57    logpid: String,
58
59    /// Defines how syslog operates
60    logstat: LogStat,
61
62    /// Holds the facility 
63    facility: LogFacility,
64
65    /// A logmask
66    logmask: i32,
67
68    /// A stream (unixdatagram, udp, tcp)
69    stream: Box<dyn SyslogTap + Send>,
70
71    _p: PhantomData<F>,
72}
73
74
75impl<F: SyslogFormatter> Drop for SyncSyslogInternal<F> 
76{
77    fn drop(&mut self) 
78    {
79        // call disconnect log to sync all data
80        let _ = self.disconnectlog();
81    }
82}
83
84/// Private realization. It is assumed that functions which are called,
85/// are called after 'lock' is locked.
86impl<F: SyslogFormatter> SyncSyslogInternal<F>
87{
88    /// Creates new instance of [SyncSyslogInternal] which contains all 
89    /// client syslog logic.
90    ///
91    /// # Arguments
92    ///
93    /// * `ident` - An optional argument which takes ref to str. If none, the
94    ///     ident will be set later. Yje ident will be trucated to 48 UTF8
95    ///     chars.
96    /// 
97    /// * `logstat` - A [LogStat] flags separated by '|'
98    /// 
99    /// * `facility` - A [LogFacility] flag
100    /// 
101    /// * `req_tap` - A type of the syslog instance reuired. See [TapTypeData].
102    /// 
103    /// # Returns
104    ///
105    /// A [SyRes] is returned with following:
106    /// 
107    /// * [Result::Ok] - with the instance.
108    /// 
109    /// * [Result::Err] - with error description.
110    pub(crate) 
111    fn new(
112        ident: Option<&str>, 
113        logstat: LogStat, 
114        facility: LogFacility, 
115        req_tap: TapTypeData
116    ) -> SyRes<Self>
117    {
118        // check if log_facility is invalid
119        let log_facility =
120            if facility.is_empty() == false && 
121                (facility & !LogMask::LOG_FACMASK).is_empty() == true
122            {
123                facility
124            }
125            else
126            {
127                // default facility code
128                LogFacility::LOG_USER
129            };
130
131        let logtag = 
132            match ident
133            {
134                Some(r) => 
135                    truncate_n(r, RFC_MAX_APP_NAME).to_string(),
136                None => 
137                    truncate_n(
138                        portable::p_getprogname()
139                            .unwrap_or("".to_string())
140                            .as_str(),
141                        RFC_MAX_APP_NAME
142                    )
143                    .to_string()
144            };
145
146        return Ok(
147            Self
148            {
149                logtag: logtag,
150                logpid: portable::get_pid().to_string(),
151                logstat: logstat, 
152                facility: log_facility,
153                logmask: 0xff,
154                stream: Tap::<()>::new(req_tap)?,
155                _p: PhantomData
156            }
157        );
158    }
159
160    /// Tap data should be the same type.
161    pub(crate) 
162    fn update_tap_data(&mut self, tap_data: TapTypeData) -> SyRes<()>
163    {
164        let is_con = self.stream.is_connected();
165
166        if is_con == true
167        {
168            self
169                .stream
170                .disconnectlog()
171                .map_err(|e|
172                    map_error_os!(e, "update_tap_data() can not disconnect log properly")
173                )?;
174        }
175
176        self.stream.update_tap_data(tap_data)?;
177
178        if is_con == true
179        {
180            // replace with new instance
181            self.stream.connectlog()?;
182        }
183
184        return Ok(());
185    }
186
187    #[inline]
188    pub(super)
189    fn send_to_stderr(&self, msg: &[Cow<'_, str>])
190    {
191        if self.logstat.intersects(LogStat::LOG_PERROR) == true
192        {
193            let stderr_lock = std::io::stderr().lock();
194            let newline = "\n";
195
196            let _ = send_to_fd(stderr_lock, msg, &newline);
197        }
198    }
199
200    #[inline]
201    fn send_to_syscons(&self, msg_payload: &[Cow<'_, str>])
202    {
203        use std::fs::File;
204        use std::os::unix::fs::OpenOptionsExt;
205
206        if self.logstat.intersects(LogStat::LOG_CONS)
207        {
208            let syscons = 
209                File
210                    ::options()
211                        .create(false)
212                        .read(false)
213                        .write(true)
214                        .custom_flags(libc::O_NONBLOCK | libc::O_CLOEXEC)
215                        .open(*PATH_CONSOLE);
216
217            if let Ok(file) = syscons
218            {
219                let newline = "\n";
220                let _ = send_to_fd(file, msg_payload, newline);
221            }
222        }
223    }
224
225    #[inline]
226    fn is_logmasked(&self, pri: i32) -> bool
227    {
228        return ((1 << (pri & LogMask::LOG_PRIMASK)) & self.logmask) == 0;
229    }
230
231    /// Returns the type of the socket.
232    #[inline]
233    pub(crate)
234    fn get_tap_type(&self) -> TapType
235    {
236        return self.stream.get_type();
237    }
238
239    pub(crate) 
240    fn set_logmask(&mut self, logmask: i32) -> i32
241    {
242        let oldmask = self.logmask;
243
244        if logmask != 0
245        {
246            self.logmask = logmask;
247        }
248
249        return oldmask;
250    }
251
252    #[inline]
253    pub(crate) 
254    fn set_logtag<L: AsRef<str>>(&mut self, logtag: L, update_pid: bool)
255    {
256        self.logtag = 
257            truncate_n(logtag.as_ref(), RFC_MAX_APP_NAME).to_string();
258
259        if update_pid == true
260        {
261            self.logpid = portable::get_pid().to_string();
262        }
263
264        return;
265    }
266
267    /// Disconnects the unix stream from syslog.
268    #[inline]
269    pub(crate) 
270    fn disconnectlog(&mut self) -> SyRes<()>
271    {
272        return 
273            self
274                .stream
275                .disconnectlog()
276                .map_err(|e| map_error_os!(e, "can not disconnect log properly"));
277    }
278
279    /// Connects unix stream to the syslog and sets up the properties of
280    /// the unix stream.
281    #[inline]
282    pub(crate) 
283    fn connectlog(&mut self) -> SyRes<()>
284    {
285        return self.stream.connectlog();
286    }
287
288    /// An internal function which is called by the syslog or vsyslog.
289    /// A glibc implementation RFC3164
290    pub(crate) 
291    fn vsyslog1(&mut self, mut pri: Priority, fmt: &str)
292    {
293        // check for invalid bits
294        if let Err(e) = pri.check_invalid_bits()
295        {
296            self.send_to_stderr(&[Cow::Owned(e.to_string())]);
297        }
298
299        /* ??? match check_invalid_bits(&mut pri)
300        {
301            Ok(_) => {},
302            Err(_e) => self.vsyslog1(get_internal_log(), fmt.as_ref())
303        }*/
304
305        // check priority against setlogmask
306        if self.is_logmasked(pri.bits()) == true
307        {
308            return;
309        }
310
311        // set default facility if not specified in pri
312        if (pri.bits() & LOG_FACMASK) == 0
313        {
314            pri.set_facility(self.facility);
315        }
316
317        let progname = self.logtag.clone();
318        let pid = self.logpid.clone();
319
320        let msg_formatted = F::vsyslog1_format(self.get_tap_type(), pri, &progname, &pid, fmt);
321
322        // output to stderr if required
323        self.send_to_stderr(msg_formatted.get_stderr_output());
324        
325        if self.stream.is_connected() == false
326        {
327            // open connection
328            match self.connectlog()
329            {
330                Ok(_) => {},
331                Err(e) =>
332                {
333                    self.send_to_stderr(&[Cow::Owned(e.into_inner())]);
334                    return;
335                }
336            }
337        }
338
339        let fullmsg = msg_formatted.concat();
340
341        // There are two possible scenarios when send may fail:
342        // 1. syslog temporary unavailable
343        // 2. syslog out of buffer space
344        // If we are connected to priv socket then in case of 1 we reopen connection
345        //      and retry once.
346        // If we are connected to unpriv then in case of 2 repeatedly retrying to send
347        //      until syslog socket buffer space will be cleared
348
349        loop
350        {
351            match self.stream.send(fullmsg.as_bytes())
352            {
353                Ok(_) => return,
354                Err(err) =>
355                {  
356                    if self.get_tap_type().is_network() == false
357                    { 
358                        if let Some(libc::ENOBUFS) = err.raw_os_error()
359                        {
360                            // scenario 2
361                            if self.get_tap_type().is_priv() == true
362                            {
363                                break;
364                            }
365
366                            sleep(Duration::from_micros(1));
367                        }
368                        else
369                        {
370                            // scenario 1
371
372                            let _ = self.disconnectlog();
373                            match self.connectlog()
374                            {
375                                Ok(_) => {},
376                                Err(_e) => break,
377                            }
378
379                            // if resend will fail then probably the scn 2 will take place
380                        }   
381                    }
382                    else
383                    {
384                        let _ = self.disconnectlog();
385                        match self.connectlog()
386                        {
387                            Ok(_) => {},
388                            Err(_e) => break,
389                        }
390                    }
391                }
392            }
393        } // loop
394
395
396        // If program reached this point then transmission over socket failed.
397        // Try to output message to console
398
399        let _ = self.send_to_syscons(msg_formatted.get_stderr_output());
400
401        return;
402    }
403}
404
405
406#[cfg(test)]
407mod tests
408{
409    use super::*;
410
411    #[cfg(target_os = "linux")]
412    #[test]
413    fn test_log_cons()
414    {
415        use crate::formatters::FormatRfc3146;
416
417
418        let net_tap = TapTypeData::new_unix(None, true).unwrap();
419
420        // needs root priv
421        let correct = 
422            SyncSyslogInternal::<FormatRfc3146>::new(Some("test1"), LogStat::LOG_PID | LogStat::LOG_CONS, LogFacility::LOG_DAEMON, net_tap)
423                .unwrap();
424        
425        let msg = &[Cow::Borrowed("header msg message payload")];
426
427        
428        correct.send_to_syscons(msg);
429
430        return;
431    }
432
433    #[cfg(any(
434        target_os = "freebsd",
435        target_os = "dragonfly",
436        target_os = "openbsd",
437        target_os = "netbsd",
438        target_os = "macos"
439    ))]
440    #[test]
441    fn test_log_cons()
442    {
443        use crate::formatters::FormatRfc5424;
444
445
446        let net_tap = TapTypeData::new_unix(None, true).unwrap();
447
448        // needs root priv
449        let correct = 
450            SyncSyslogInternal::<FormatRfc5424>::new(Some("test1"), LogStat::LOG_PID | LogStat::LOG_CONS, LogFacility::LOG_DAEMON, net_tap)
451                .unwrap();
452        
453        correct.send_to_syscons(&[Cow::Borrowed("header msg"), Cow::Borrowed("message payload")]);
454    }
455
456    #[cfg(target_os = "linux")]
457    #[test]
458    fn test_bit_operations()
459    {
460        use crate::formatters::FormatRfc3146;
461
462        let net_tap = TapTypeData::new_unix(None, true).unwrap();
463
464        let correct = 
465            SyncSyslogInternal::<FormatRfc3146>::new(Some("test1"), LogStat::LOG_PID, LogFacility::LOG_DAEMON, net_tap)
466                .unwrap();
467
468        assert_eq!(correct.facility, LogFacility::LOG_DAEMON);
469        assert_eq!((correct.facility & !LogFacility::LOG_DAEMON), LogFacility::empty());
470    }
471
472	#[cfg(any(
473        target_os = "freebsd",
474        target_os = "dragonfly",
475        target_os = "openbsd",
476        target_os = "netbsd",
477        target_os = "macos"
478    ))]
479    #[test]
480    fn test_bit_operations()
481    {
482        use crate::formatters::FormatRfc5424;
483
484
485        let net_tap = TapTypeData::new_unix(None, true).unwrap();
486
487        let correct = 
488            SyncSyslogInternal::<FormatRfc5424>::new(Some("test1"), LogStat::LOG_PID, LogFacility::LOG_DAEMON, net_tap)
489                .unwrap();
490
491        assert_eq!(correct.facility, LogFacility::LOG_DAEMON);
492        assert_eq!((correct.facility & !LogFacility::LOG_DAEMON), LogFacility::empty());
493    }
494
495    #[cfg(target_os = "linux")]
496    #[test]
497    fn test_bit_operations2()
498    {
499        use crate::formatters::FormatRfc3146;
500
501        let net_tap = TapTypeData::new_unix(None, true).unwrap();
502
503        let _correct = 
504            SyncSyslogInternal::<FormatRfc3146>::new(Some("test2"), LogStat::LOG_PID, LogFacility::LOG_DAEMON, net_tap)
505                .unwrap();
506
507        let mut pri = Priority::LOG_ALERT;
508
509        let res = pri.check_invalid_bits();
510
511        assert_eq!(res.is_ok(), true);
512        assert_eq!(pri.bits(), Priority::LOG_ALERT.bits());
513    }
514
515	#[cfg(any(
516        target_os = "freebsd",
517        target_os = "dragonfly",
518        target_os = "openbsd",
519        target_os = "netbsd",
520        target_os = "macos"
521    ))]
522	#[test]
523    fn test_bit_operations2()
524    {
525        use crate::formatters::FormatRfc5424;
526
527        let net_tap = TapTypeData::new_unix(None, true).unwrap();
528
529        let _correct = 
530            SyncSyslogInternal::<FormatRfc5424>::new(Some("test2"), LogStat::LOG_PID, LogFacility::LOG_DAEMON, net_tap)
531                .unwrap();
532
533        let mut pri = Priority::LOG_ALERT.bits();
534
535        let res = check_invalid_bits(&mut pri);
536
537        assert_eq!(res.is_ok(), true);
538        assert_eq!(pri, Priority::LOG_ALERT.bits());
539    }
540
541    #[cfg(target_os = "linux")]
542    #[test]
543    fn test_set_priority()
544    {
545        use crate::{ formatters::FormatRfc3146, LOG_MASK};
546
547        let net_tap = TapTypeData::new_unix(None, true).unwrap();
548
549        let mut correct = 
550            SyncSyslogInternal::<FormatRfc3146>::new(Some("test1"), LogStat::LOG_PID, LogFacility::LOG_DAEMON, net_tap)
551                .unwrap();
552
553        let ret = correct.set_logmask(LOG_MASK!(Priority::LOG_ERR));
554
555        assert_eq!(ret, 0xff);
556
557        let ret = correct.set_logmask(LOG_MASK!(Priority::LOG_ERR));
558
559        assert_eq!(ret, LOG_MASK!(Priority::LOG_ERR));
560
561        let ret = correct.is_logmasked(Priority::LOG_WARNING.bits());
562        assert_eq!(ret, true);
563
564        let ret = correct.is_logmasked(Priority::LOG_ERR.bits());
565        assert_eq!(ret, false);
566
567        let ret = correct.is_logmasked(Priority::LOG_CRIT.bits());
568        assert_eq!(ret, true);
569    }
570
571	#[cfg(any(
572		    target_os = "freebsd",
573		    target_os = "dragonfly",
574		    target_os = "openbsd",
575		    target_os = "netbsd",
576		    target_os = "macos"
577		))]
578	#[test]
579    fn test_set_priority()
580    {
581        use crate::{ formatters::FormatRfc5424, LOG_MASK};
582
583        let net_tap = TapTypeData::new_unix(None, true).unwrap();
584
585        let mut correct = 
586            SyncSyslogInternal::<FormatRfc5424>::new(Some("test1"), LogStat::LOG_PID, LogFacility::LOG_DAEMON, net_tap)
587                .unwrap();
588
589        let ret = correct.set_logmask(LOG_MASK!(Priority::LOG_ERR));
590
591        assert_eq!(ret, 0xff);
592
593        let ret = correct.set_logmask(LOG_MASK!(Priority::LOG_ERR));
594
595        assert_eq!(ret, LOG_MASK!(Priority::LOG_ERR));
596
597        let ret = correct.is_logmasked(Priority::LOG_WARNING.bits());
598        assert_eq!(ret, true);
599
600        let ret = correct.is_logmasked(Priority::LOG_ERR.bits());
601        assert_eq!(ret, false);
602
603        let ret = correct.is_logmasked(Priority::LOG_CRIT.bits());
604        assert_eq!(ret, true);
605    }
606
607    #[cfg(target_os = "linux")]
608    #[test]
609    fn test_set_priority2()
610    {
611        use crate::{ formatters::FormatRfc3146, LOG_MASK};
612        
613        let net_tap = TapTypeData::new_unix(None, true).unwrap();
614
615        let mut correct = 
616            SyncSyslogInternal::<FormatRfc3146>::new(Some("test1"), LogStat::LOG_PID, LogFacility::LOG_DAEMON, net_tap)
617                .unwrap();
618
619        let ret = correct.set_logmask(!LOG_MASK!(Priority::LOG_ERR));
620
621        assert_eq!(ret, 0xff);
622
623        let ret = correct.is_logmasked(Priority::LOG_WARNING.bits());
624        assert_eq!(ret, false);
625
626        let ret = correct.is_logmasked(Priority::LOG_ERR.bits());
627        assert_eq!(ret, true);
628
629        let ret = correct.is_logmasked(Priority::LOG_CRIT.bits());
630        assert_eq!(ret, false);
631    }
632
633	#[cfg(any(
634		target_os = "freebsd",
635		target_os = "dragonfly",
636		target_os = "openbsd",
637		target_os = "netbsd",
638		target_os = "macos"
639	))]
640	#[test]
641    fn test_set_priority2()
642    {
643        use crate::{ formatters::FormatRfc5424, LOG_MASK};
644        
645        let net_tap = TapTypeData::new_unix(None, true).unwrap();
646
647        let mut correct = 
648            SyncSyslogInternal::<FormatRfc5424>::new(Some("test1"), LogStat::LOG_PID, LogFacility::LOG_DAEMON, net_tap)
649                .unwrap();
650
651        let ret = correct.set_logmask(!LOG_MASK!(Priority::LOG_ERR));
652
653        assert_eq!(ret, 0xff);
654
655        let ret = correct.is_logmasked(Priority::LOG_WARNING.bits());
656        assert_eq!(ret, false);
657
658        let ret = correct.is_logmasked(Priority::LOG_ERR.bits());
659        assert_eq!(ret, true);
660
661        let ret = correct.is_logmasked(Priority::LOG_CRIT.bits());
662        assert_eq!(ret, false);
663    }
664}