1- use std:: fs:: OpenOptions ;
2- use std:: io:: { self , Write , Seek , SeekFrom } ;
1+ use std:: fs:: { OpenOptions , File } ;
2+ use std:: io;
33use std:: path:: Path ;
44use std:: os:: unix:: io:: AsRawFd ;
55use 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+
84124impl 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+
141210fn 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
179239fn 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-
564626fn print_terminal_output ( state : & DisplayState ) {
565627 println ! ( "Login Credentials" ) ;
566628 println ! ( " Root password: {}" , state. root_password) ;
0 commit comments