66from anyio import create_task_group , get_cancelled_exc_class
77from jumpstarter_cli_common .config import opt_config
88from jumpstarter_cli_common .exceptions import handle_exceptions_with_reauthentication
9+ from jumpstarter_cli_common .oidc import get_token_remaining_seconds
910from jumpstarter_cli_common .signal import signal_handler
1011
1112from .common import opt_acquisition_timeout , opt_duration_partial , opt_selector
1516from jumpstarter .config .exporter import ExporterConfigV1Alpha1
1617
1718
19+ def _warn_about_expired_token (lease_name : str , selector : str ):
20+ """Warn user that lease won't be cleaned up due to expired token."""
21+ click .echo (click .style ("\n Token expired - lease cleanup will fail." , fg = "yellow" , bold = True ))
22+ click .echo (click .style (f"Lease '{ lease_name } ' will remain active." , fg = "yellow" ))
23+ click .echo (click .style (f"To reconnect: JMP_LEASE={ lease_name } jmp shell -s { selector } " , fg = "cyan" ))
24+
25+
26+ async def _monitor_token_expiry (config , cancel_scope ):
27+ """Monitor token expiry and warn user."""
28+ token = getattr (config , "token" , None )
29+ if not token :
30+ return
31+
32+ warned = False
33+ while not cancel_scope .cancel_called :
34+ try :
35+ remaining = get_token_remaining_seconds (token )
36+ if remaining is None :
37+ return
38+
39+ if remaining <= 0 :
40+ click .echo (click .style ("\n Token expired! Exiting shell." , fg = "red" , bold = True ))
41+ cancel_scope .cancel ()
42+ return
43+ elif remaining <= 300 and not warned : # 5 minutes
44+ mins , secs = int (remaining // 60 ), int (remaining % 60 )
45+ click .echo (
46+ click .style (
47+ f"\n Token expires in { mins } m { secs } s. Session will continue but cleanup may fail on exit." ,
48+ fg = "yellow" ,
49+ bold = True ,
50+ )
51+ )
52+ warned = True
53+
54+ await anyio .sleep (30 )
55+ except Exception :
56+ return
57+
58+
1859def _run_shell_with_lease (lease , exporter_logs , config , command ):
1960 """Run shell with lease context managers."""
61+
2062 def launch_remote_shell (path : str ) -> int :
2163 return launch_shell (
22- path , lease .exporter_name , config .drivers .allow , config .drivers .unsafe ,
23- config .shell .use_profiles , command = command , lease = lease
64+ path ,
65+ lease .exporter_name ,
66+ config .drivers .allow ,
67+ config .drivers .unsafe ,
68+ config .shell .use_profiles ,
69+ command = command ,
70+ lease = lease ,
2471 )
2572
2673 with lease .serve_unix () as path :
@@ -39,13 +86,17 @@ async def _shell_with_signal_handling(
3986 """Handle lease acquisition and shell execution with signal handling."""
4087 exit_code = 0
4188 cancelled_exc_class = get_cancelled_exc_class ()
89+ lease_used = None
4290
4391 async with create_task_group () as tg :
4492 tg .start_soon (signal_handler , tg .cancel_scope )
93+ tg .start_soon (_monitor_token_expiry , config , tg .cancel_scope )
94+
4595 try :
4696 try :
4797 async with anyio .from_thread .BlockingPortal () as portal :
4898 async with config .lease_async (selector , lease_name , duration , portal , acquisition_timeout ) as lease :
99+ lease_used = lease
49100 exit_code = await anyio .to_thread .run_sync (
50101 _run_shell_with_lease , lease , exporter_logs , config , command
51102 )
@@ -55,6 +106,13 @@ async def _shell_with_signal_handling(
55106 raise exc from None
56107 raise
57108 except cancelled_exc_class :
109+ # Check if cancellation was due to token expiry
110+ token = getattr (config , "token" , None )
111+ if lease_used and token :
112+ remaining = get_token_remaining_seconds (token )
113+ if remaining is not None and remaining <= 0 :
114+ _warn_about_expired_token (lease_used .name , selector )
115+ return 3 # Exit code for token expiry
58116 exit_code = 2
59117 finally :
60118 if not tg .cancel_scope .cancel_called :
0 commit comments