• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NVIDIA / nvrc / 23920642300

02 Apr 2026 08:32PM UTC coverage: 94.091%. First build
23920642300

Pull #149

github

web-flow
Merge 52bd7a9de into b91526c2e
Pull Request #149: feat: implement always-file architecture for daemon synchronization

25 of 45 new or added lines in 2 files covered. (55.56%)

1895 of 2014 relevant lines covered (94.09%)

12.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.8
/src/syslog.rs
1
// SPDX-License-Identifier: Apache-2.0
2
// Copyright (c) NVIDIA CORPORATION
3

4
//! Minimal syslog sink for ephemeral init environments.
5
//!
6
//! Programs expect /dev/log to exist for logging. We provide this socket and
7
//! write all messages to /run/syslog.log. This file serves as the source of
8
//! truth for daemon synchronization - wait_for_marker() reads from it to detect
9
//! when daemons are ready. File-based approach works regardless of log level.
10

11
use log::debug;
12
use nix::poll::{PollFd, PollFlags, PollTimeout};
13
use once_cell::sync::OnceCell;
14
use std::fs::{File, OpenOptions};
15
use std::io::Write;
16
use std::os::fd::AsFd;
17
use std::os::unix::fs::OpenOptionsExt;
18
use std::os::unix::net::UnixDatagram;
19
use std::path::Path;
20
use std::sync::Mutex;
21

22
/// Global syslog socket—lazily initialized on first poll().
23
/// OnceCell ensures thread-safe one-time init. Ephemeral init runs once,
24
/// no need for reset capability.
25
static SYSLOG: OnceCell<UnixDatagram> = OnceCell::new();
26

27
/// Global log file for syslog messages - ALWAYS written for synchronization.
28
/// Mutex protects concurrent writes from multiple poll() calls.
29
static LOGFILE: OnceCell<Mutex<File>> = OnceCell::new();
30

31
const DEV_LOG: &str = "/dev/log";
32
const SYSLOG_FILE: &str = "/run/syslog.log";
33

34
/// Public path to the syslog file for cross-module access.
35
pub const SYSLOG_FILE_PATH: &str = SYSLOG_FILE;
36

37
/// Create and bind a Unix datagram socket at the given path.
38
fn bind(path: &Path) -> std::io::Result<UnixDatagram> {
66✔
39
    UnixDatagram::bind(path)
66✔
40
}
66✔
41

42
/// Check socket for pending messages (non-blocking).
43
/// Returns None if no data available, Some(msg) if a message was read.
44
fn poll_socket(sock: &UnixDatagram) -> std::io::Result<Option<String>> {
20✔
45
    let mut fds = [PollFd::new(sock.as_fd(), PollFlags::POLLIN)];
20✔
46
    // Non-blocking poll—init loop calls this frequently, can't afford to block
47
    let count = nix::poll::poll(&mut fds, PollTimeout::ZERO)
20✔
48
        .map_err(|e| std::io::Error::other(e.to_string()))?;
20✔
49

50
    if count == 0 {
20✔
51
        return Ok(None); // No events, no data waiting
8✔
52
    }
12✔
53

54
    let Some(revents) = fds[0].revents() else {
12✔
55
        return Ok(None); // Shouldn't happen, but handle gracefully
×
56
    };
57

58
    if !revents.contains(PollFlags::POLLIN) {
12✔
59
        return Ok(None); // Event wasn't POLLIN (e.g., error flag)
×
60
    }
12✔
61

62
    // Read the message—4KB buffer matches typical syslog max message size
63
    let mut buf = [0u8; 4096];
12✔
64
    let (len, _) = sock.recv_from(&mut buf)?;
12✔
65
    let msg = String::from_utf8_lossy(&buf[..len]);
12✔
66
    Ok(Some(strip_priority(msg.trim_end()).to_string()))
12✔
67
}
20✔
68

69
/// Poll the global /dev/log socket, logging any message via trace!().
70
/// Lazily initializes /dev/log on first call.
71
/// Drains one message per call—rate-limited to prevent DoS by syslog flooding.
72
/// Caller loops at ~2 msg/sec (500ms sleep between calls).
73
pub fn poll() {
6✔
74
    use crate::macros::ResultExt;
75
    poll_at(Path::new(DEV_LOG)).or_panic("syslog poll");
6✔
76
}
6✔
77

78
/// Best-effort syslog drain. Silently ignores errors (e.g. socket not bound yet).
79
/// Used by wait_for_marker where syslog drain is nice-to-have, not critical.
80
pub fn try_poll() {
36✔
81
    let _ = poll_at(Path::new(DEV_LOG));
36✔
82
}
36✔
83

84
/// Internal: poll a specific socket path (for unit tests).
85
/// Production code uses poll() which hardcodes /dev/log.
86
fn poll_at(path: &Path) -> std::io::Result<()> {
44✔
87
    let sock: &UnixDatagram = if path == Path::new(DEV_LOG) {
44✔
88
        SYSLOG.get_or_try_init(|| bind(path))?
42✔
89
    } else {
90
        // For testing: create a one-shot socket (caller manages lifecycle)
91
        return poll_once(path);
2✔
92
    };
93

94
    if let Some(msg) = poll_socket(sock)? {
×
NEW
95
        forward_message(&msg)?;
×
96
    }
×
97

98
    Ok(())
×
99
}
44✔
100

101
/// Write syslog message to persistent file for daemon synchronization.
102
/// Daemons like nvidia-persistenced signal readiness via syslog. File-based
103
/// approach works regardless of log level and survives for post-mortem debugging.
NEW
104
fn forward_message(msg: &str) -> std::io::Result<()> {
×
NEW
105
    let logfile = LOGFILE.get_or_try_init(|| {
×
NEW
106
        OpenOptions::new()
×
NEW
107
            .create(true)
×
NEW
108
            .append(true)
×
NEW
109
            .mode(0o600) // Restrict to owner only
×
NEW
110
            .open(SYSLOG_FILE)
×
NEW
111
            .map(Mutex::new)
×
NEW
112
    })?;
×
113

NEW
114
    let mut file = logfile
×
NEW
115
        .lock()
×
NEW
116
        .unwrap_or_else(|poisoned| poisoned.into_inner());
×
NEW
117
    writeln!(file, "{}", msg)?;
×
NEW
118
    file.flush()?;
×
119

120
    // Also log when debug enabled - may appear in dmesg depending on logger config
NEW
121
    debug!("{}", msg);
×
122

NEW
123
    Ok(())
×
NEW
124
}
×
125

126
/// One-shot poll for testing: bind, poll once, return.
127
/// Socket is dropped after call—suitable for tests with temp paths.
128
fn poll_once(path: &Path) -> std::io::Result<()> {
4✔
129
    let sock = bind(path)?;
4✔
130
    if let Some(msg) = poll_socket(&sock)? {
4✔
NEW
131
        forward_message(&msg)?;
×
132
    }
4✔
133
    Ok(())
4✔
134
}
4✔
135

136
/// Strip the syslog priority prefix <N> from a message.
137
/// Priority levels are noise for us—all messages go to trace!() equally.
138
/// Example: "<6>hello" → "hello"
139
fn strip_priority(msg: &str) -> &str {
30✔
140
    msg.strip_prefix('<')
30✔
141
        .and_then(|s| s.find('>').map(|i| &s[i + 1..]))
30✔
142
        .unwrap_or(msg)
30✔
143
}
30✔
144

145
#[cfg(test)]
146
mod tests {
147
    use super::*;
148
    use tempfile::TempDir;
149

150
    // === strip_priority tests ===
151

152
    #[test]
153
    fn test_strip_priority_normal() {
2✔
154
        assert_eq!(strip_priority("<6>test message"), "test message");
2✔
155
        assert_eq!(strip_priority("<13>another msg"), "another msg");
2✔
156
        assert_eq!(strip_priority("<191>high pri"), "high pri");
2✔
157
    }
2✔
158

159
    #[test]
160
    fn test_strip_priority_no_prefix() {
2✔
161
        assert_eq!(strip_priority("no prefix"), "no prefix");
2✔
162
    }
2✔
163

164
    #[test]
165
    fn test_strip_priority_edge_cases() {
2✔
166
        assert_eq!(strip_priority("<>empty"), "empty");
2✔
167
        assert_eq!(strip_priority("<6>"), "");
2✔
168
        assert_eq!(strip_priority(""), "");
2✔
169
        assert_eq!(strip_priority("<"), "<");
2✔
170
        assert_eq!(strip_priority("<6"), "<6"); // No closing >
2✔
171
    }
2✔
172

173
    // === bind tests ===
174

175
    #[test]
176
    fn test_bind_success() {
2✔
177
        let tmp = TempDir::new().unwrap();
2✔
178
        let path = tmp.path().join("test.sock");
2✔
179
        let sock = bind(&path);
2✔
180
        assert!(sock.is_ok());
2✔
181
    }
2✔
182

183
    #[test]
184
    fn test_bind_nonexistent_dir() {
2✔
185
        let path = Path::new("/nonexistent/dir/test.sock");
2✔
186
        let err = bind(path).unwrap_err();
2✔
187
        // Should fail with "No such file or directory" (ENOENT)
188
        assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
2✔
189
    }
2✔
190

191
    #[test]
192
    fn test_bind_already_exists() {
2✔
193
        let tmp = TempDir::new().unwrap();
2✔
194
        let path = tmp.path().join("test.sock");
2✔
195
        let _sock1 = bind(&path).unwrap();
2✔
196
        // Binding again to same path should fail with "Address already in use"
197
        let err = bind(&path).unwrap_err();
2✔
198
        assert_eq!(err.kind(), std::io::ErrorKind::AddrInUse);
2✔
199
    }
2✔
200

201
    // === poll_socket tests ===
202

203
    #[test]
204
    fn test_poll_socket_no_data() {
2✔
205
        let tmp = TempDir::new().unwrap();
2✔
206
        let path = tmp.path().join("test.sock");
2✔
207
        let sock = bind(&path).unwrap();
2✔
208

209
        let result = poll_socket(&sock).unwrap();
2✔
210
        assert_eq!(result, None);
2✔
211
    }
2✔
212

213
    #[test]
214
    fn test_poll_socket_with_data() {
2✔
215
        let tmp = TempDir::new().unwrap();
2✔
216
        let path = tmp.path().join("test.sock");
2✔
217
        let server = bind(&path).unwrap();
2✔
218

219
        let client = UnixDatagram::unbound().unwrap();
2✔
220
        client.send_to(b"<6>hello world", &path).unwrap();
2✔
221

222
        let result = poll_socket(&server).unwrap();
2✔
223
        assert_eq!(result, Some("hello world".to_string()));
2✔
224
    }
2✔
225

226
    #[test]
227
    fn test_poll_socket_strips_priority() {
2✔
228
        let tmp = TempDir::new().unwrap();
2✔
229
        let path = tmp.path().join("test.sock");
2✔
230
        let server = bind(&path).unwrap();
2✔
231

232
        let client = UnixDatagram::unbound().unwrap();
2✔
233
        client.send_to(b"<3>error message", &path).unwrap();
2✔
234

235
        let result = poll_socket(&server).unwrap();
2✔
236
        assert_eq!(result, Some("error message".to_string()));
2✔
237
    }
2✔
238

239
    #[test]
240
    fn test_poll_socket_multiple_messages() {
2✔
241
        let tmp = TempDir::new().unwrap();
2✔
242
        let path = tmp.path().join("test.sock");
2✔
243
        let server = bind(&path).unwrap();
2✔
244

245
        let client = UnixDatagram::unbound().unwrap();
2✔
246
        client.send_to(b"<6>first", &path).unwrap();
2✔
247
        client.send_to(b"<6>second", &path).unwrap();
2✔
248

249
        // poll_socket drains one at a time
250
        let result1 = poll_socket(&server).unwrap();
2✔
251
        assert_eq!(result1, Some("first".to_string()));
2✔
252

253
        let result2 = poll_socket(&server).unwrap();
2✔
254
        assert_eq!(result2, Some("second".to_string()));
2✔
255

256
        // No more messages
257
        let result3 = poll_socket(&server).unwrap();
2✔
258
        assert_eq!(result3, None);
2✔
259
    }
2✔
260

261
    #[test]
262
    fn test_poll_socket_trims_trailing_whitespace() {
2✔
263
        let tmp = TempDir::new().unwrap();
2✔
264
        let path = tmp.path().join("test.sock");
2✔
265
        let server = bind(&path).unwrap();
2✔
266

267
        let client = UnixDatagram::unbound().unwrap();
2✔
268
        client.send_to(b"<6>message with newline\n", &path).unwrap();
2✔
269

270
        let result = poll_socket(&server).unwrap();
2✔
271
        assert_eq!(result, Some("message with newline".to_string()));
2✔
272
    }
2✔
273

274
    // === poll_at / poll_once tests ===
275

276
    #[test]
277
    fn test_poll_once_no_data() {
2✔
278
        let tmp = TempDir::new().unwrap();
2✔
279
        let path = tmp.path().join("test.sock");
2✔
280

281
        // poll_once will bind and poll - should succeed with no messages
282
        let result = poll_once(&path);
2✔
283
        assert!(result.is_ok());
2✔
284
    }
2✔
285

286
    #[test]
287
    fn test_poll_once_with_data() {
2✔
288
        let tmp = TempDir::new().unwrap();
2✔
289
        let path = tmp.path().join("test.sock");
2✔
290

291
        // Create server socket first
292
        let server = bind(&path).unwrap();
2✔
293

294
        // Send data
295
        let client = UnixDatagram::unbound().unwrap();
2✔
296
        client.send_to(b"<6>poll_once test", &path).unwrap();
2✔
297

298
        // poll_socket on the server
299
        let result = poll_socket(&server).unwrap();
2✔
300
        assert_eq!(result, Some("poll_once test".to_string()));
2✔
301
    }
2✔
302

303
    #[test]
304
    fn test_poll_at_custom_path() {
2✔
305
        let tmp = TempDir::new().unwrap();
2✔
306
        let path = tmp.path().join("custom.sock");
2✔
307

308
        // poll_at with non-/dev/log path uses poll_once internally
309
        let result = poll_at(&path);
2✔
310
        assert!(result.is_ok());
2✔
311
    }
2✔
312

313
    #[test]
314
    fn test_poll_dev_log() {
2✔
315
        use std::panic;
316
        // poll() tries to bind /dev/log - may panic if already bound or no permission
317
        // Just exercise the code path, don't assert success
318
        let _ = panic::catch_unwind(poll);
2✔
319
    }
2✔
320
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc