99from pathlib import Path
1010from dataclasses import dataclass
1111from contextlib import asynccontextmanager
12- from typing import Callable , TypeAlias , ContextManager , AsyncIterator
12+ from typing import AnyStr , Callable , TypeAlias , ContextManager , AsyncIterator
1313from enum import Enum , auto
1414from concurrent .futures import ThreadPoolExecutor , CancelledError
1515
1616import asyncssh
17- from asyncssh import SSHServerProcess , SSHAcceptor
17+ from asyncssh import (
18+ SFTPServerFactory ,
19+ SSHServerConnection ,
20+ SSHServerProcess ,
21+ SSHAcceptor ,
22+ SSHServer ,
23+ SSHServerProcessFactory ,
24+ )
25+ from asyncssh .channel import SSHChannel
1826from blessed import Terminal
1927
2028from .run import run
@@ -176,7 +184,7 @@ def ssh_terminal_handler(
176184 terminal_type : str ,
177185 executor : ThreadPoolExecutor ,
178186) -> int :
179- # Now is a good time to instanciate the console
187+ # Now is a good time to instantiate the console
180188 # (it might fail if the ROM does not exist for instance)
181189 console = console_callback ()
182190
@@ -285,7 +293,28 @@ class NoAuthentication:
285293)
286294
287295
288- class SSHServer (asyncssh .SSHServer ):
296+ class GambatermSSHServerProcess (SSHServerProcess [str ]):
297+ def __init__ (
298+ self ,
299+ process_factory : SSHServerProcessFactory [str ],
300+ sftp_factory : SFTPServerFactory | None ,
301+ sftp_version : int ,
302+ allow_scp : bool ,
303+ active_sessions : set [GambatermSSHServerProcess ],
304+ ):
305+ super ().__init__ (process_factory , sftp_factory , sftp_version , allow_scp )
306+ self ._gambaterm_active_sessions = active_sessions
307+
308+ def connection_made (self , chan : SSHChannel [AnyStr ]) -> None :
309+ self ._gambaterm_active_sessions .add (self )
310+ return super ().connection_made (chan )
311+
312+ def connection_lost (self , exc : Exception | None ) -> None :
313+ self ._gambaterm_active_sessions .discard (self )
314+ return super ().connection_lost (exc )
315+
316+
317+ class GambatermSSHServer (SSHServer ):
289318 def __init__ (
290319 self ,
291320 authentication : AuthenticationMethod ,
@@ -294,27 +323,38 @@ def __init__(
294323 command_parser : CommandParser ,
295324 users_directory : Path ,
296325 executor : ThreadPoolExecutor ,
326+ active_connections : dict [GambatermSSHServer , SSHServerConnection ],
297327 ):
298328 self ._gambaterm_console_cls = console_cls
299329 self ._gambaterm_namespace = namespace
300330 self ._gambaterm_command_parser = command_parser
301331 self ._gambaterm_users_directory = users_directory
302332 self ._gambaterm_executor = executor
303333 self ._gambaterm_authentication = authentication
334+ self ._gambaterm_active_connections = active_connections
335+ self ._gambaterm_active_sessions : set [GambatermSSHServerProcess ] = set ()
304336
305- def connection_made (self , conn : asyncssh . SSHServerConnection ) -> None :
337+ def connection_made (self , conn : SSHServerConnection ) -> None :
306338 conn .set_extra_info (console_cls = self ._gambaterm_console_cls )
307339 conn .set_extra_info (executor = self ._gambaterm_executor )
308340 conn .set_extra_info (namespace = self ._gambaterm_namespace )
309341 conn .set_extra_info (command_parser = self ._gambaterm_command_parser )
310342 conn .set_extra_info (users_directory = self ._gambaterm_users_directory )
343+ self ._gambaterm_active_connections [self ] = conn
344+
345+ def connection_lost (self , exc : Exception | None ) -> None :
346+ self ._gambaterm_active_connections .pop (self )
311347
312348 def begin_auth (self , username : str ) -> bool :
313349 return not isinstance (self ._gambaterm_authentication , NoAuthentication )
314350
315351 def session_requested (self ) -> SSHServerProcess [str ]:
316- return asyncssh .SSHServerProcess (
317- safe_ssh_process_handler , sftp_factory = None , sftp_version = 3 , allow_scp = False
352+ return GambatermSSHServerProcess (
353+ safe_ssh_process_handler ,
354+ sftp_factory = None ,
355+ sftp_version = 3 ,
356+ allow_scp = False ,
357+ active_sessions = self ._gambaterm_active_sessions ,
318358 )
319359
320360 def password_auth_supported (self ) -> bool :
@@ -391,14 +431,16 @@ async def run_ssh_server(
391431 "aes128-ctr" ,
392432 ]
393433
434+ active_connections : dict [GambatermSSHServer , SSHServerConnection ] = {}
394435 server = await asyncssh .create_server (
395- lambda : SSHServer (
436+ lambda : GambatermSSHServer (
396437 authentication ,
397438 console_cls ,
398439 namespace ,
399440 command_parser ,
400441 users_directory ,
401442 executor ,
443+ active_connections ,
402444 ),
403445 bind ,
404446 port ,
@@ -428,13 +470,23 @@ async def run_ssh_server(
428470 try :
429471 yield server
430472 finally :
431- # Stop listening
473+ # Stop listening for new connections
432474 server .close ()
433475
434- # server.close_clients()
435- for transport in server ._clients :
436- for channel in transport ._protocol ._channels .values ():
437- channel ._session ._writers [None ].write_eof ()
476+ # Freeze active connections
477+ for ssh_server , connection in list (active_connections .items ()):
478+ # Freeze active sessions
479+ for session in list (ssh_server ._gambaterm_active_sessions ):
480+ # Graceful teardown
481+ # This is important to make sure the client receives the cleanup data
482+ session .eof_received ()
483+ await session .wait_closed ()
484+
485+ # Close the connection
486+ # This is important for clients stuck in authentication phase for instance
487+ connection .close ()
488+
489+ # Now nothing should keep the server from closing
438490 await server .wait_closed ()
439491
440492
@@ -452,7 +504,7 @@ def main(
452504 "-b" ,
453505 type = str ,
454506 default = "127.0.0.1" ,
455- help = "Bind adress of the SSH server, "
507+ help = "Bind address of the SSH server, "
456508 "use `0.0.0.0` for all interfaces (default is localhost)" ,
457509 )
458510 parser .add_argument (
@@ -467,7 +519,7 @@ def main(
467519 "--pw" ,
468520 type = str ,
469521 default = None ,
470- help = "Enable password authentification with the given global password" ,
522+ help = "Enable password authentication with the given global password" ,
471523 )
472524 parser .add_argument (
473525 "--no-auth" ,
0 commit comments