Skip to content

Commit be92b53

Browse files
authored
Merge pull request #381 from nix-community/serial-access
network-status: support both serial/vga output at the same time
2 parents 1b38d17 + ac19d10 commit be92b53

File tree

2 files changed

+158
-94
lines changed

2 files changed

+158
-94
lines changed

nix/image-installer/module.nix

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,14 @@ in
5656
"EEEEEC"
5757
];
5858

59-
programs.bash.interactiveShellInit = ''
59+
# FIXME: CTRL-C somehow stops evaluating the shell init, so as a workaround we do this code last,
60+
# we should make network-status the terminal leader instead.
61+
programs.bash.interactiveShellInit = lib.mkAfter ''
6062
if [[ "$(tty)" =~ /dev/(tty1|hvc0|ttyS0)$ ]]; then
6163
# workaround for https://github.com/NixOS/nixpkgs/issues/219239
6264
systemctl restart systemd-vconsole-setup.service
6365
64-
${network-status}/bin/network-status
66+
${network-status}/bin/network-status || true
6567
fi
6668
'';
6769

nix/network-status/src/main.rs

Lines changed: 154 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use std::fs::OpenOptions;
2-
use std::io::{self, Write, Seek, SeekFrom};
1+
use std::fs::{OpenOptions, File};
2+
use std::io;
33
use std::path::Path;
44
use std::os::unix::io::AsRawFd;
55
use qrcode::QrCode;
@@ -81,6 +81,46 @@ struct FramebufferMap {
8181
size: usize,
8282
}
8383

84+
struct FramebufferState {
85+
_fb: File,
86+
config: FramebufferConfig,
87+
map: FramebufferMap,
88+
qr_code: QrCode,
89+
qr_size: usize,
90+
qr_pixel_size: usize,
91+
x_offset: usize,
92+
y_offset: usize,
93+
}
94+
95+
impl FramebufferState {
96+
/// Update the QR code and recalculate layout if needed
97+
fn update_qr_code(&mut self, login_json: &str) {
98+
self.qr_code = QrCode::new(login_json)
99+
.unwrap_or_else(|_| QrCode::new(r#"{"status": "waiting"}"#).unwrap());
100+
101+
let layout = calculate_qr_layout(&self.config, &self.qr_code);
102+
self.qr_size = layout.0;
103+
self.qr_pixel_size = layout.1;
104+
self.x_offset = layout.2;
105+
self.y_offset = layout.3;
106+
}
107+
108+
/// Render the display state to the framebuffer
109+
fn render(&mut self, state: &DisplayState) {
110+
render_display(
111+
self.map.as_slice_mut(),
112+
&self.config,
113+
&self.qr_code,
114+
self.qr_size,
115+
self.qr_pixel_size,
116+
self.x_offset,
117+
self.y_offset,
118+
state,
119+
);
120+
let _ = self.map.sync();
121+
}
122+
}
123+
84124
impl FramebufferMap {
85125
/// Create a new memory-mapped framebuffer
86126
///
@@ -138,6 +178,35 @@ impl Drop for FramebufferMap {
138178
}
139179
}
140180

181+
/// Check if stdout is a serial console (not a framebuffer-backed console)
182+
/// Returns true if stdout is a real serial device, not a virtual terminal
183+
///
184+
/// Uses TIOCGSERIAL ioctl which is only supported by real serial devices.
185+
/// Virtual terminals (tty1, tty2, etc.) will fail this ioctl with ENOTTY.
186+
fn is_serial_console() -> bool {
187+
let stdout_fd = io::stdout().as_raw_fd();
188+
189+
// Check if stdout is a tty
190+
let is_tty = unsafe { libc::isatty(stdout_fd) == 1 };
191+
if !is_tty {
192+
return false;
193+
}
194+
195+
// TIOCGSERIAL ioctl is only supported by real serial devices
196+
// Virtual terminals will return ENOTTY (Inappropriate ioctl for device)
197+
const TIOCGSERIAL: libc::c_ulong = 0x541E;
198+
199+
// We don't actually care about the serial_struct contents, just whether the ioctl succeeds
200+
let mut serial_struct: [u8; 128] = [0; 128]; // Large enough for serial_struct
201+
let result = unsafe {
202+
libc::ioctl(stdout_fd, TIOCGSERIAL as _, serial_struct.as_mut_ptr())
203+
};
204+
205+
// If ioctl succeeds, it's a serial console
206+
// If it fails, it's likely a virtual terminal
207+
result == 0
208+
}
209+
141210
fn main() -> io::Result<()> {
142211
let args: Vec<String> = std::env::args().collect();
143212

@@ -164,16 +233,7 @@ fn main() -> io::Result<()> {
164233
return Err(io::Error::new(io::ErrorKind::Other, "image-output feature not enabled"));
165234
}
166235

167-
// Check if framebuffer is available
168-
if Path::new(FB_PATH).exists() {
169-
// Try framebuffer display
170-
if display_on_framebuffer().is_ok() {
171-
return Ok(());
172-
}
173-
}
174-
175-
// Fall back to terminal display
176-
display_in_terminal()
236+
display()
177237
}
178238

179239
fn print_framebuffer_info() -> io::Result<()> {
@@ -342,76 +402,105 @@ fn calculate_qr_layout(fb_config: &FramebufferConfig, qr_code: &QrCode) -> (usiz
342402
(qr_size, qr_pixel_size, x_offset, y_offset)
343403
}
344404

345-
fn display_on_framebuffer() -> io::Result<()> {
346-
let mut current_state = DisplayState::read_current();
347-
348-
// Generate QR code (use placeholder if not available yet)
349-
let mut code = QrCode::new(&current_state.login_json)
350-
.unwrap_or_else(|_| QrCode::new(r#"{"status": "waiting"}"#).unwrap());
405+
/// Try to open and initialize the framebuffer
406+
/// Returns None if framebuffer is not available or initialization fails
407+
fn open_framebuffer(state: &DisplayState) -> Option<FramebufferState> {
408+
if !Path::new(FB_PATH).exists() {
409+
return None;
410+
}
351411

352-
// Open framebuffer
353-
let fb = OpenOptions::new()
354-
.read(true)
355-
.write(true)
356-
.open(FB_PATH)?;
412+
(|| -> io::Result<FramebufferState> {
413+
let fb = OpenOptions::new()
414+
.read(true)
415+
.write(true)
416+
.open(FB_PATH)?;
357417

358-
// Get screen info
359-
let mut vinfo: FbVarScreeninfo = Default::default();
360-
unsafe {
361-
let ret = libc::ioctl(fb.as_raw_fd(), 0x4600, &mut vinfo);
362-
if ret < 0 {
363-
return Err(io::Error::last_os_error());
418+
let mut vinfo: FbVarScreeninfo = Default::default();
419+
unsafe {
420+
let ret = libc::ioctl(fb.as_raw_fd(), 0x4600, &mut vinfo);
421+
if ret < 0 {
422+
return Err(io::Error::last_os_error());
423+
}
364424
}
365-
}
366425

367-
let fb_config = FramebufferConfig {
368-
width: vinfo.xres as usize,
369-
height: vinfo.yres as usize,
370-
stride: vinfo.xres_virtual as usize,
371-
bytes_per_pixel: (vinfo.bits_per_pixel / 8) as usize,
372-
red_offset: (vinfo.red.offset / 8) as usize,
373-
green_offset: (vinfo.green.offset / 8) as usize,
374-
blue_offset: (vinfo.blue.offset / 8) as usize,
375-
};
426+
let config = FramebufferConfig {
427+
width: vinfo.xres as usize,
428+
height: vinfo.yres as usize,
429+
stride: vinfo.xres_virtual as usize,
430+
bytes_per_pixel: (vinfo.bits_per_pixel / 8) as usize,
431+
red_offset: (vinfo.red.offset / 8) as usize,
432+
green_offset: (vinfo.green.offset / 8) as usize,
433+
blue_offset: (vinfo.blue.offset / 8) as usize,
434+
};
435+
436+
let screen_size = config.stride * config.height * config.bytes_per_pixel;
437+
let map = unsafe { FramebufferMap::new(fb.as_raw_fd(), screen_size)? };
438+
439+
let qr_code = QrCode::new(&state.login_json)
440+
.unwrap_or_else(|_| QrCode::new(r#"{"status": "waiting"}"#).unwrap());
441+
let (qr_size, qr_pixel_size, x_offset, y_offset) = calculate_qr_layout(&config, &qr_code);
442+
443+
Ok(FramebufferState {
444+
_fb: fb,
445+
config,
446+
map,
447+
qr_code,
448+
qr_size,
449+
qr_pixel_size,
450+
x_offset,
451+
y_offset,
452+
})
453+
})().ok()
454+
}
376455

377-
// Memory map the framebuffer instead of using write()
378-
// This is the standard approach - see kernel fb.h: write() is for "strange non linear layouts"
379-
let screen_size = fb_config.stride * fb_config.height * fb_config.bytes_per_pixel;
456+
fn display() -> io::Result<()> {
457+
let mut current_state = DisplayState::read_current();
458+
459+
// Determine if we're on a serial console (this won't change during runtime)
460+
let has_serial = is_serial_console();
380461

381-
// Create safe RAII wrapper for mmap - automatically unmaps on drop
382-
let mut fb_map = unsafe { FramebufferMap::new(fb.as_raw_fd(), screen_size)? };
462+
// Try to initialize framebuffer immediately
463+
let mut fb_state = open_framebuffer(&current_state);
383464

384465
// Initial render
385-
let (mut qr_size, mut qr_pixel_size, mut x_offset, mut y_offset) = calculate_qr_layout(&fb_config, &code);
386-
render_display(fb_map.as_slice_mut(), &fb_config, &code, qr_size, qr_pixel_size, x_offset, y_offset, &current_state);
466+
if let Some(ref mut fb) = fb_state {
467+
fb.render(&current_state);
468+
}
387469

388-
// Ensure changes are visible to hardware (flush CPU cache)
389-
// MS_SYNC ensures the call blocks until the data is actually written to the device
390-
fb_map.sync()?;
470+
// Also render to terminal if on serial or no framebuffer available
471+
if has_serial || fb_state.is_none() {
472+
print_terminal_output(&current_state);
473+
}
391474

392-
// Poll for changes and redraw only when needed
475+
// Poll for changes and update all available outputs
393476
loop {
394477
std::thread::sleep(std::time::Duration::from_secs(2));
395478

479+
// Try to initialize framebuffer if not already done and it becomes available
480+
if fb_state.is_none() {
481+
fb_state = open_framebuffer(&current_state);
482+
if let Some(ref mut fb) = fb_state {
483+
// Do initial framebuffer render
484+
fb.render(&current_state);
485+
}
486+
}
487+
396488
let new_state = DisplayState::read_current();
397489
if new_state.has_changed(&current_state) {
398-
// Update QR code if login.json changed
399-
if new_state.login_json != current_state.login_json {
400-
code = QrCode::new(&new_state.login_json)
401-
.unwrap_or_else(|_| QrCode::new(r#"{"status": "waiting"}"#).unwrap());
402-
403-
// Recalculate layout for the new QR code size
404-
let layout = calculate_qr_layout(&fb_config, &code);
405-
qr_size = layout.0;
406-
qr_pixel_size = layout.1;
407-
x_offset = layout.2;
408-
y_offset = layout.3;
490+
// Update framebuffer if available
491+
if let Some(ref mut fb) = fb_state {
492+
// Update QR code if login.json changed
493+
if new_state.login_json != current_state.login_json {
494+
fb.update_qr_code(&new_state.login_json);
495+
}
496+
fb.render(&new_state);
409497
}
410498

411-
render_display(fb_map.as_slice_mut(), &fb_config, &code, qr_size, qr_pixel_size, x_offset, y_offset, &new_state);
412-
413-
// Sync to ensure display controller sees the changes
414-
fb_map.sync()?;
499+
// Update terminal if on serial console or no framebuffer available
500+
if has_serial || fb_state.is_none() {
501+
print!("\x1B[2J\x1B[H"); // ANSI clear screen and move cursor to home
502+
print_terminal_output(&new_state);
503+
}
415504

416505
current_state = new_state;
417506
}
@@ -534,33 +623,6 @@ fn render_display(
534623
left_margin, footer_y + 20);
535624
}
536625

537-
fn display_in_terminal() -> io::Result<()> {
538-
let mut current_state = DisplayState::read_current();
539-
540-
// Initial display
541-
print_terminal_output(&current_state);
542-
543-
// Poll for changes and check if framebuffer becomes available
544-
loop {
545-
std::thread::sleep(std::time::Duration::from_secs(2));
546-
547-
// Check if framebuffer became available
548-
if Path::new(FB_PATH).exists() {
549-
if display_on_framebuffer().is_ok() {
550-
return Ok(());
551-
}
552-
}
553-
554-
let new_state = DisplayState::read_current();
555-
if new_state.has_changed(&current_state) {
556-
// Clear screen and redraw
557-
print!("\x1B[2J\x1B[H"); // ANSI clear screen and move cursor to home
558-
print_terminal_output(&new_state);
559-
current_state = new_state;
560-
}
561-
}
562-
}
563-
564626
fn print_terminal_output(state: &DisplayState) {
565627
println!("Login Credentials");
566628
println!(" Root password: {}", state.root_password);

0 commit comments

Comments
 (0)