@@ -147,15 +147,31 @@ pub async fn get_unit_files(scope: Scope) -> Result<Vec<UnitFile>> {
147147
148148 let mut ret = vec ! [ ] ;
149149 let is_root = nix:: unistd:: geteuid ( ) . is_root ( ) ;
150+ info ! ( "get_unit_files: is_root={}, scope={:?}" , is_root, scope) ;
150151
151152 for unit_scope in unit_scopes {
152- let connection = get_connection ( unit_scope) . await ?;
153+ info ! ( "get_unit_files: fetching {:?} unit files" , unit_scope) ;
154+ let connection = match get_connection ( unit_scope) . await {
155+ Ok ( conn) => conn,
156+ Err ( e) => {
157+ error ! ( "get_unit_files: failed to get {:?} connection: {:?}" , unit_scope, e) ;
158+ if is_root && unit_scope == UnitScope :: User {
159+ info ! ( "get_unit_files: skipping user scope because we're root" ) ;
160+ continue ;
161+ }
162+ return Err ( e) ;
163+ } ,
164+ } ;
153165 let manager_proxy = ManagerProxy :: new ( & connection) . await ?;
154166 let unit_files = match manager_proxy. list_unit_files_by_patterns ( vec ! [ ] , vec ! [ "*.service" . into( ) ] ) . await {
155- Ok ( files) => files,
167+ Ok ( files) => {
168+ info ! ( "get_unit_files: got {} {:?} unit files" , files. len( ) , unit_scope) ;
169+ files
170+ } ,
156171 Err ( e) => {
172+ error ! ( "get_unit_files: list_unit_files_by_patterns failed for {:?}: {:?}" , unit_scope, e) ;
157173 if is_root && unit_scope == UnitScope :: User {
158- error ! ( "Failed to get user unit files, ignoring because we're running as root" ) ;
174+ info ! ( "get_unit_files: ignoring user scope error because we're root" ) ;
159175 vec ! [ ]
160176 } else {
161177 return Err ( e. into ( ) ) ;
@@ -638,6 +654,105 @@ pub fn get_unit_path(full_service_name: &str) -> String {
638654 format ! ( "/org/freedesktop/systemd1/unit/{}" , encode_as_dbus_object_path( full_service_name) )
639655}
640656
657+ /// Diagnostic result explaining why logs might be missing
658+ #[ derive( Debug , Clone ) ]
659+ pub enum LogDiagnostic {
660+ /// Unit has never been activated (ActiveEnterTimestamp is 0)
661+ NeverRun { unit_name : String } ,
662+ /// Journal is not accessible (likely permissions)
663+ JournalInaccessible { error : String } ,
664+ /// Unit-specific permission issue
665+ PermissionDenied { error : String } ,
666+ /// Journal is available but no logs exist for this unit
667+ NoLogsRecorded { unit_name : String } ,
668+ /// journalctl command failed with an error
669+ JournalctlError { stderr : String } ,
670+ }
671+
672+ impl LogDiagnostic {
673+ /// Returns a human-readable message for display
674+ pub fn message ( & self ) -> String {
675+ match self {
676+ Self :: NeverRun { unit_name } => format ! ( "No logs: {} has never been started" , unit_name) ,
677+ Self :: JournalInaccessible { error } => {
678+ format ! ( "Cannot access journal: {}\n \n Check that systemd-journald is running" , error)
679+ } ,
680+ Self :: PermissionDenied { error } => format ! ( "Permission denied: {}\n \n Try: sudo systemctl-tui" , error) ,
681+ Self :: NoLogsRecorded { unit_name } => {
682+ format ! ( "No logs recorded for {} (unit has run but produced no journal output)" , unit_name)
683+ } ,
684+ Self :: JournalctlError { stderr } => format ! ( "journalctl error: {}" , stderr) ,
685+ }
686+ }
687+ }
688+
689+ /// Check if a unit has ever been activated using systemctl show
690+ pub fn check_unit_has_run ( unit : & UnitId ) -> bool {
691+ let mut args = vec ! [ "show" , "-P" , "ActiveEnterTimestampMonotonic" ] ;
692+ if unit. scope == UnitScope :: User {
693+ args. insert ( 0 , "--user" ) ;
694+ }
695+ args. push ( & unit. name ) ;
696+
697+ Command :: new ( "systemctl" )
698+ . args ( & args)
699+ . output ( )
700+ . ok ( )
701+ . and_then (
702+ |output| if output. status . success ( ) { std:: str:: from_utf8 ( & output. stdout ) . ok ( ) . map ( String :: from) } else { None } ,
703+ )
704+ . map ( |s| s. trim ( ) . parse :: < u64 > ( ) . unwrap_or ( 0 ) > 0 )
705+ . unwrap_or ( false )
706+ }
707+
708+ /// Check if the journal is accessible at all (tests general read access)
709+ fn can_access_journal ( scope : UnitScope ) -> Result < ( ) , String > {
710+ let mut args = vec ! [ "--lines=1" , "--quiet" ] ;
711+ if scope == UnitScope :: User {
712+ args. push ( "--user" ) ;
713+ }
714+
715+ match Command :: new ( "journalctl" ) . args ( & args) . output ( ) {
716+ Ok ( output) => {
717+ if output. status . success ( ) {
718+ Ok ( ( ) )
719+ } else {
720+ Err ( String :: from_utf8_lossy ( & output. stderr ) . trim ( ) . to_string ( ) )
721+ }
722+ } ,
723+ Err ( e) => Err ( e. to_string ( ) ) ,
724+ }
725+ }
726+
727+ /// Parse journalctl stderr to determine the specific error type
728+ pub fn parse_journalctl_error ( stderr : & str ) -> LogDiagnostic {
729+ let stderr_lower = stderr. to_lowercase ( ) ;
730+
731+ if stderr_lower. contains ( "permission denied" ) || stderr_lower. contains ( "access denied" ) {
732+ LogDiagnostic :: PermissionDenied { error : stderr. trim ( ) . to_string ( ) }
733+ } else if stderr_lower. contains ( "no such file" ) || stderr_lower. contains ( "failed to open" ) {
734+ LogDiagnostic :: JournalInaccessible { error : stderr. trim ( ) . to_string ( ) }
735+ } else {
736+ LogDiagnostic :: JournalctlError { stderr : stderr. trim ( ) . to_string ( ) }
737+ }
738+ }
739+
740+ /// Diagnose why logs are missing for a unit
741+ pub fn diagnose_missing_logs ( unit : & UnitId ) -> LogDiagnostic {
742+ // Check 1: Has unit ever run?
743+ if !check_unit_has_run ( unit) {
744+ return LogDiagnostic :: NeverRun { unit_name : unit. name . clone ( ) } ;
745+ }
746+
747+ // Check 2: Can we access the journal at all?
748+ if let Err ( error) = can_access_journal ( unit. scope ) {
749+ return parse_journalctl_error ( & error) ;
750+ }
751+
752+ // If we get here, journal is accessible but no logs for this specific unit
753+ LogDiagnostic :: NoLogsRecorded { unit_name : unit. name . clone ( ) }
754+ }
755+
641756#[ cfg( test) ]
642757mod tests {
643758 use super :: * ;
@@ -652,4 +767,22 @@ mod tests {
652767 assert_eq ! ( encode_as_dbus_object_path( "test.service" ) , "test_2eservice" ) ;
653768 assert_eq ! ( encode_as_dbus_object_path( "test-with-hyphen.service" ) , "test_2dwith_2dhyphen_2eservice" ) ;
654769 }
770+
771+ #[ test]
772+ fn test_parse_journalctl_error_permission ( ) {
773+ let diagnostic = parse_journalctl_error ( "Failed to get journal access: Permission denied" ) ;
774+ assert ! ( matches!( diagnostic, LogDiagnostic :: PermissionDenied { .. } ) ) ;
775+ }
776+
777+ #[ test]
778+ fn test_parse_journalctl_error_no_file ( ) {
779+ let diagnostic = parse_journalctl_error ( "No such file or directory" ) ;
780+ assert ! ( matches!( diagnostic, LogDiagnostic :: JournalInaccessible { .. } ) ) ;
781+ }
782+
783+ #[ test]
784+ fn test_parse_journalctl_error_generic ( ) {
785+ let diagnostic = parse_journalctl_error ( "Something unexpected happened" ) ;
786+ assert ! ( matches!( diagnostic, LogDiagnostic :: JournalctlError { .. } ) ) ;
787+ }
655788}
0 commit comments