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

NVIDIA / nvrc / 23919362613

02 Apr 2026 08:00PM UTC coverage: 94.153%. First build
23919362613

Pull #149

github

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

15 of 33 new or added lines in 2 files covered. (45.45%)

1884 of 2001 relevant lines covered (94.15%)

12.45 hits per line

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

86.31
/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::net::UnixDatagram;
18
use std::path::Path;
19
use std::sync::Mutex;
20

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

100
/// Write syslog message to persistent file for daemon synchronization.
101
/// Daemons like nvidia-persistenced signal readiness via syslog, but we can't
102
/// rely on /dev/kmsg because it requires trace logging. File-based approach works
103
/// 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
            .open(SYSLOG_FILE)
×
NEW
110
            .map(Mutex::new)
×
NEW
111
    })?;
×
112

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

119
    // Also forward to dmesg when debug logging is enabled (nvrc.log=debug)
NEW
120
    debug!("{}", msg);
×
121

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

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

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

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

149
    // === strip_priority tests ===
150

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

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

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

172
    // === bind tests ===
173

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

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

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

200
    // === poll_socket tests ===
201

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

273
    // === poll_at / poll_once tests ===
274

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

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

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

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

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

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

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

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

312
    #[test]
313
    fn test_poll_dev_log() {
2✔
314
        use std::panic;
315
        // poll() tries to bind /dev/log - may panic if already bound or no permission
316
        // Just exercise the code path, don't assert success
317
        let _ = panic::catch_unwind(poll);
2✔
318
    }
2✔
319
}
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