11import sys
22from datetime import timedelta
33
4+ import anyio
45import click
6+ from anyio import create_task_group , get_cancelled_exc_class
57from jumpstarter_cli_common .config import opt_config
68from jumpstarter_cli_common .exceptions import handle_exceptions_with_reauthentication
9+ from jumpstarter_cli_common .signal import signal_handler
710
811from .common import opt_duration_partial , opt_selector
912from .login import relogin_client
1215from jumpstarter .config .exporter import ExporterConfigV1Alpha1
1316
1417
18+ def _run_shell_with_lease (lease , exporter_logs , config , command ):
19+ """Run shell with lease context managers."""
20+ def launch_remote_shell (path : str ) -> int :
21+ return launch_shell (
22+ path , "remote" , config .drivers .allow , config .drivers .unsafe ,
23+ config .shell .use_profiles , command = command
24+ )
25+
26+ with lease .serve_unix () as path :
27+ with lease .monitor ():
28+ if exporter_logs :
29+ with lease .connect () as client :
30+ with client .log_stream ():
31+ return launch_remote_shell (path )
32+ else :
33+ return launch_remote_shell (path )
34+
35+
36+ async def _shell_with_signal_handling (config , selector , lease_name , duration , exporter_logs , command ):
37+ """Handle lease acquisition and shell execution with signal handling."""
38+ exit_code = 0
39+ cancelled_exc_class = get_cancelled_exc_class ()
40+
41+ try :
42+ async with create_task_group () as tg :
43+ tg .start_soon (signal_handler , tg .cancel_scope )
44+ try :
45+ try :
46+ async with anyio .from_thread .BlockingPortal () as portal :
47+ async with config .lease_async (selector , lease_name , duration , portal ) as lease :
48+ exit_code = await anyio .to_thread .run_sync (
49+ _run_shell_with_lease , lease , exporter_logs , config , command
50+ )
51+ except BaseExceptionGroup as eg :
52+ for exc in eg .exceptions :
53+ if isinstance (exc , TimeoutError ):
54+ raise exc from None
55+ raise
56+ except cancelled_exc_class :
57+ exit_code = 2
58+ finally :
59+ if not tg .cancel_scope .cancel_called :
60+ tg .cancel_scope .cancel ()
61+ except* TimeoutError :
62+ exit_code = 1
63+
64+ return exit_code
65+
66+
1567@click .command ("shell" )
1668@opt_config ()
1769@click .argument ("command" , nargs = - 1 )
@@ -38,27 +90,9 @@ def shell(config, command: tuple[str, ...], lease_name, selector, duration, expo
3890
3991 match config :
4092 case ClientConfigV1Alpha1 ():
41- exit_code = 0
42- def _launch_remote_shell (path : str ) -> int :
43- return launch_shell (
44- path ,
45- "remote" ,
46- config .drivers .allow ,
47- config .drivers .unsafe ,
48- config .shell .use_profiles ,
49- command = command ,
50- )
51-
52- with config .lease (selector = selector , lease_name = lease_name , duration = duration ) as lease :
53- with lease .serve_unix () as path :
54- with lease .monitor ():
55- if exporter_logs :
56- with lease .connect () as client :
57- with client .log_stream ():
58- exit_code = _launch_remote_shell (path )
59- else :
60- exit_code = _launch_remote_shell (path )
61- # we exit here to make sure that all the with clauses unwind
93+ exit_code = anyio .run (
94+ _shell_with_signal_handling , config , selector , lease_name , duration , exporter_logs , command
95+ )
6296 sys .exit (exit_code )
6397
6498 case ExporterConfigV1Alpha1 ():
0 commit comments