@@ -5,9 +5,56 @@ use std::time::Duration;
55use crate :: daemon:: protocol:: {
66 ChargeSession , CycleSummary , DaemonRequest , DaemonResponse , DaemonStatus , DailyCycle ,
77 DailyStat , DailyTopProcess , DataSnapshot , HourlyStat , KillProcessResult , KillSignal , Sample ,
8+ MIN_SUPPORTED_VERSION , PROTOCOL_VERSION ,
89} ;
910use crate :: daemon:: socket_path;
1011
12+ #[ derive( Debug , Clone ) ]
13+ pub struct VersionMismatchError {
14+ pub tui_protocol_version : u32 ,
15+ pub tui_min_supported : u32 ,
16+ pub daemon_protocol_version : u32 ,
17+ pub daemon_min_supported : u32 ,
18+ pub daemon_binary_version : String ,
19+ pub kind : VersionMismatchKind ,
20+ }
21+
22+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
23+ pub enum VersionMismatchKind {
24+ TuiTooOld ,
25+ DaemonTooOld ,
26+ }
27+
28+ impl std:: fmt:: Display for VersionMismatchError {
29+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
30+ match self . kind {
31+ VersionMismatchKind :: TuiTooOld => {
32+ write ! (
33+ f,
34+ "Protocol version mismatch: TUI uses protocol v{}, but daemon (v{}) requires v{}+.\n \n \
35+ Please update jolt:\n \
36+ brew upgrade jolt\n \
37+ # or: cargo install jolt-tui",
38+ self . tui_protocol_version,
39+ self . daemon_binary_version,
40+ self . daemon_min_supported
41+ )
42+ }
43+ VersionMismatchKind :: DaemonTooOld => {
44+ write ! (
45+ f,
46+ "Protocol version mismatch: daemon (v{}) uses protocol v{}, but this TUI requires v{}+.\n \n \
47+ Please restart the daemon:\n \
48+ jolt daemon restart",
49+ self . daemon_binary_version,
50+ self . daemon_protocol_version,
51+ self . tui_min_supported
52+ )
53+ }
54+ }
55+ }
56+ }
57+
1158#[ derive( Debug , thiserror:: Error ) ]
1259pub enum ClientError {
1360 #[ error( "Connection failed: {0}" ) ]
@@ -21,10 +68,43 @@ pub enum ClientError {
2168
2269 #[ error( "Subscription rejected: {0}" ) ]
2370 SubscriptionRejected ( String ) ,
71+
72+ #[ error( "{0}" ) ]
73+ VersionMismatch ( VersionMismatchError ) ,
2474}
2575
2676pub type Result < T > = std:: result:: Result < T , ClientError > ;
2777
78+ /// Checks if the TUI and daemon protocol versions are compatible.
79+ /// Returns Ok(()) if compatible, or Err with detailed mismatch info.
80+ pub fn check_version_compatibility ( status : & DaemonStatus ) -> Result < ( ) > {
81+ // Check 1: Can daemon understand TUI's messages?
82+ if PROTOCOL_VERSION < status. min_supported_version {
83+ return Err ( ClientError :: VersionMismatch ( VersionMismatchError {
84+ tui_protocol_version : PROTOCOL_VERSION ,
85+ tui_min_supported : MIN_SUPPORTED_VERSION ,
86+ daemon_protocol_version : status. protocol_version ,
87+ daemon_min_supported : status. min_supported_version ,
88+ daemon_binary_version : status. version . clone ( ) ,
89+ kind : VersionMismatchKind :: TuiTooOld ,
90+ } ) ) ;
91+ }
92+
93+ // Check 2: Can TUI understand daemon's messages?
94+ if status. protocol_version < MIN_SUPPORTED_VERSION {
95+ return Err ( ClientError :: VersionMismatch ( VersionMismatchError {
96+ tui_protocol_version : PROTOCOL_VERSION ,
97+ tui_min_supported : MIN_SUPPORTED_VERSION ,
98+ daemon_protocol_version : status. protocol_version ,
99+ daemon_min_supported : status. min_supported_version ,
100+ daemon_binary_version : status. version . clone ( ) ,
101+ kind : VersionMismatchKind :: DaemonTooOld ,
102+ } ) ) ;
103+ }
104+
105+ Ok ( ( ) )
106+ }
107+
28108pub struct DaemonClient {
29109 stream : UnixStream ,
30110 read_buffer : Vec < u8 > ,
@@ -42,6 +122,15 @@ impl DaemonClient {
42122 } )
43123 }
44124
125+ /// Connects to the daemon and validates protocol version compatibility.
126+ /// This is the preferred connection method for the TUI.
127+ pub fn connect_with_version_check ( ) -> Result < Self > {
128+ let mut client = Self :: connect ( ) ?;
129+ let status = client. get_status ( ) ?;
130+ check_version_compatibility ( & status) ?;
131+ Ok ( client)
132+ }
133+
45134 fn read_line_blocking ( & mut self ) -> Result < String > {
46135 let mut temp_buf = [ 0u8 ; 8192 ] ;
47136 loop {
@@ -293,3 +382,107 @@ impl DaemonClient {
293382 Ok ( ( ) )
294383 }
295384}
385+
386+ #[ cfg( test) ]
387+ mod tests {
388+ use super :: * ;
389+
390+ fn make_status (
391+ protocol_version : u32 ,
392+ min_supported_version : u32 ,
393+ version : & str ,
394+ ) -> DaemonStatus {
395+ DaemonStatus {
396+ running : true ,
397+ uptime_secs : 0 ,
398+ sample_count : 0 ,
399+ last_sample_time : None ,
400+ database_size_bytes : 0 ,
401+ version : version. to_string ( ) ,
402+ subscriber_count : 0 ,
403+ history_enabled : false ,
404+ protocol_version,
405+ min_supported_version,
406+ }
407+ }
408+
409+ #[ test]
410+ fn test_version_compatible_same_version ( ) {
411+ let status = make_status ( PROTOCOL_VERSION , MIN_SUPPORTED_VERSION , "1.0.0" ) ;
412+ assert ! ( check_version_compatibility( & status) . is_ok( ) ) ;
413+ }
414+
415+ #[ test]
416+ fn test_version_compatible_daemon_newer ( ) {
417+ let status = make_status ( PROTOCOL_VERSION + 1 , MIN_SUPPORTED_VERSION , "2.0.0" ) ;
418+ assert ! ( check_version_compatibility( & status) . is_ok( ) ) ;
419+ }
420+
421+ #[ test]
422+ fn test_version_compatible_at_min_boundary ( ) {
423+ let status = make_status ( MIN_SUPPORTED_VERSION , MIN_SUPPORTED_VERSION , "0.5.0" ) ;
424+ assert ! ( check_version_compatibility( & status) . is_ok( ) ) ;
425+ }
426+
427+ #[ test]
428+ fn test_version_tui_too_old ( ) {
429+ let status = make_status ( 10 , PROTOCOL_VERSION + 1 , "3.0.0" ) ;
430+ let result = check_version_compatibility ( & status) ;
431+ assert ! ( result. is_err( ) ) ;
432+ if let Err ( ClientError :: VersionMismatch ( e) ) = result {
433+ assert_eq ! ( e. kind, VersionMismatchKind :: TuiTooOld ) ;
434+ assert_eq ! ( e. tui_protocol_version, PROTOCOL_VERSION ) ;
435+ assert_eq ! ( e. daemon_min_supported, PROTOCOL_VERSION + 1 ) ;
436+ assert ! ( e. to_string( ) . contains( "update jolt" ) ) ;
437+ } else {
438+ panic ! ( "Expected VersionMismatch error" ) ;
439+ }
440+ }
441+
442+ #[ test]
443+ fn test_version_daemon_too_old ( ) {
444+ let status = make_status ( 0 , 0 , "0.1.0" ) ;
445+ let result = check_version_compatibility ( & status) ;
446+ assert ! ( result. is_err( ) ) ;
447+ if let Err ( ClientError :: VersionMismatch ( e) ) = result {
448+ assert_eq ! ( e. kind, VersionMismatchKind :: DaemonTooOld ) ;
449+ assert_eq ! ( e. daemon_protocol_version, 0 ) ;
450+ assert_eq ! ( e. tui_min_supported, MIN_SUPPORTED_VERSION ) ;
451+ assert ! ( e. to_string( ) . contains( "restart the daemon" ) ) ;
452+ } else {
453+ panic ! ( "Expected VersionMismatch error" ) ;
454+ }
455+ }
456+
457+ #[ test]
458+ fn test_version_mismatch_error_display_tui_too_old ( ) {
459+ let error = VersionMismatchError {
460+ tui_protocol_version : 1 ,
461+ tui_min_supported : 1 ,
462+ daemon_protocol_version : 3 ,
463+ daemon_min_supported : 2 ,
464+ daemon_binary_version : "2.0.0" . to_string ( ) ,
465+ kind : VersionMismatchKind :: TuiTooOld ,
466+ } ;
467+ let msg = error. to_string ( ) ;
468+ assert ! ( msg. contains( "TUI uses protocol v1" ) ) ;
469+ assert ! ( msg. contains( "daemon (v2.0.0) requires v2+" ) ) ;
470+ assert ! ( msg. contains( "brew upgrade jolt" ) ) ;
471+ }
472+
473+ #[ test]
474+ fn test_version_mismatch_error_display_daemon_too_old ( ) {
475+ let error = VersionMismatchError {
476+ tui_protocol_version : 3 ,
477+ tui_min_supported : 2 ,
478+ daemon_protocol_version : 1 ,
479+ daemon_min_supported : 1 ,
480+ daemon_binary_version : "0.5.0" . to_string ( ) ,
481+ kind : VersionMismatchKind :: DaemonTooOld ,
482+ } ;
483+ let msg = error. to_string ( ) ;
484+ assert ! ( msg. contains( "daemon (v0.5.0) uses protocol v1" ) ) ;
485+ assert ! ( msg. contains( "TUI requires v2+" ) ) ;
486+ assert ! ( msg. contains( "jolt daemon restart" ) ) ;
487+ }
488+ }
0 commit comments