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 (
10+ TOKEN_EXPIRY_WARNING_SECONDS ,
11+ format_duration ,
12+ get_token_remaining_seconds ,
13+ )
914from jumpstarter_cli_common .signal import signal_handler
1015
1116from .common import opt_acquisition_timeout , opt_duration_partial , opt_selector
1520from jumpstarter .config .exporter import ExporterConfigV1Alpha1
1621
1722
23+ def _warn_about_expired_token (lease_name : str , selector : str ) -> None :
24+ """Warn user that lease won't be cleaned up due to expired token."""
25+ click .echo (click .style ("\n Token expired - lease cleanup will fail." , fg = "yellow" , bold = True ))
26+ click .echo (click .style (f"Lease '{ lease_name } ' will remain active." , fg = "yellow" ))
27+ click .echo (click .style (f"To reconnect: JMP_LEASE={ lease_name } jmp shell" , fg = "cyan" ))
28+
29+
30+ async def _monitor_token_expiry (config , cancel_scope ) -> None :
31+ """Monitor token expiry and warn user."""
32+ token = getattr (config , "token" , None )
33+ if not token :
34+ return
35+
36+ warned = False
37+ while not cancel_scope .cancel_called :
38+ try :
39+ remaining = get_token_remaining_seconds (token )
40+ if remaining is None :
41+ return
42+
43+ if remaining <= 0 :
44+ click .echo (click .style ("\n Token expired! Exiting shell." , fg = "red" , bold = True ))
45+ cancel_scope .cancel ()
46+ return
47+
48+ if remaining <= TOKEN_EXPIRY_WARNING_SECONDS and not warned :
49+ duration = format_duration (remaining )
50+ click .echo (
51+ click .style (
52+ f"\n Token expires in { duration } . Session will continue but cleanup may fail on exit." ,
53+ fg = "yellow" ,
54+ bold = True ,
55+ )
56+ )
57+ warned = True
58+
59+ await anyio .sleep (30 )
60+ except Exception :
61+ return
62+
63+
1864def _run_shell_with_lease (lease , exporter_logs , config , command ):
1965 """Run shell with lease context managers."""
66+
2067 def launch_remote_shell (path : str ) -> int :
2168 return launch_shell (
22- path , lease .exporter_name , config .drivers .allow , config .drivers .unsafe ,
23- config .shell .use_profiles , command = command , lease = lease
69+ path ,
70+ lease .exporter_name ,
71+ config .drivers .allow ,
72+ config .drivers .unsafe ,
73+ config .shell .use_profiles ,
74+ command = command ,
75+ lease = lease ,
2476 )
2577
2678 with lease .serve_unix () as path :
@@ -39,13 +91,28 @@ async def _shell_with_signal_handling(
3991 """Handle lease acquisition and shell execution with signal handling."""
4092 exit_code = 0
4193 cancelled_exc_class = get_cancelled_exc_class ()
94+ lease_used = None
95+
96+ # Check token before starting
97+ token = getattr (config , "token" , None )
98+ if token :
99+ remaining = get_token_remaining_seconds (token )
100+ if remaining is not None and remaining <= 0 :
101+ from jumpstarter .common .exceptions import ConnectionError
102+ raise ConnectionError ("token is expired" )
42103
43104 async with create_task_group () as tg :
44105 tg .start_soon (signal_handler , tg .cancel_scope )
106+
45107 try :
46108 try :
47109 async with anyio .from_thread .BlockingPortal () as portal :
48110 async with config .lease_async (selector , lease_name , duration , portal , acquisition_timeout ) as lease :
111+ lease_used = lease
112+
113+ # Start token monitoring only once we're in the shell
114+ tg .start_soon (_monitor_token_expiry , config , tg .cancel_scope )
115+
49116 exit_code = await anyio .to_thread .run_sync (
50117 _run_shell_with_lease , lease , exporter_logs , config , command
51118 )
@@ -55,6 +122,13 @@ async def _shell_with_signal_handling(
55122 raise exc from None
56123 raise
57124 except cancelled_exc_class :
125+ # Check if cancellation was due to token expiry
126+ token = getattr (config , "token" , None )
127+ if lease_used and token :
128+ remaining = get_token_remaining_seconds (token )
129+ if remaining is not None and remaining <= 0 :
130+ _warn_about_expired_token (lease_used .name , selector )
131+ return 3 # Exit code for token expiry
58132 exit_code = 2
59133 finally :
60134 if not tg .cancel_scope .cancel_called :
0 commit comments