51
51
from devlib .exception import (DevlibTransientError , TargetStableError ,
52
52
TargetNotRespondingError , TimeoutError ,
53
53
TargetTransientError , KernelConfigKeyError ,
54
- TargetError , HostError , TargetCalledProcessError ) # pylint: disable=redefined-builtin
54
+ TargetError , HostError , TargetCalledProcessError ,
55
+ TooManyBackgroundCommandsError ) # pylint: disable=redefined-builtin
55
56
from devlib .utils .ssh import SshConnection
56
57
from devlib .utils .android import AdbConnection , AndroidProperties , LogcatMonitor , adb_command , adb_disconnect , INTENT_FLAGS
57
58
from devlib .utils .misc import memoized , isiterable , convert_new_lines , groupby_value
@@ -370,6 +371,9 @@ def connect(self, timeout=None, check_boot_completed=True):
370
371
self .execute ('mkdir -p {}' .format (quote (self .executables_directory )))
371
372
self .busybox = self .install (os .path .join (PACKAGE_BIN_DIRECTORY , self .abi , 'busybox' ), timeout = 30 )
372
373
self .conn .busybox = self .busybox
374
+ # Hit the cached property early but after having checked the connection
375
+ # works, and after having set self.busybox
376
+ self .conn ._max_bg
373
377
self .platform .update_from_target (self )
374
378
self ._update_modules ('connected' )
375
379
if self .platform .big_core and self .load_default_modules :
@@ -755,53 +759,70 @@ def _prepare_cmd(self, command, force_locale):
755
759
@call_conn
756
760
@asyn .asyncf
757
761
async def _execute_async (self , command , timeout = None , as_root = False , strip_colors = True , will_succeed = False , check_exit_code = True , force_locale = 'C' ):
758
- bg = self .background (
759
- command = command ,
760
- as_root = as_root ,
761
- force_locale = force_locale ,
762
- )
763
-
764
- def process (streams ):
765
- # Make sure we don't accidentally end up with "\n" if both streams
766
- # are empty
767
- res = b'\n ' .join (x for x in streams if x ).decode ()
768
- if strip_colors :
769
- res = strip_bash_colors (res )
770
- return res
771
-
772
- def thread_f ():
773
- streams = (None , None )
774
- excep = None
775
- try :
776
- with bg as _bg :
777
- streams = _bg .communicate (timeout = timeout )
778
- except BaseException as e :
779
- excep = e
780
-
781
- if isinstance (excep , subprocess .CalledProcessError ):
782
- if check_exit_code :
783
- excep = TargetStableError (excep )
784
- else :
785
- streams = (excep .output , excep .stderr )
786
- excep = None
762
+ sem = self .conn ._bg_async_sem
763
+ # If there is no BackgroundCommand slot available, fall back on the
764
+ # blocking path. This ensures complete separation between the internal
765
+ # use of background() to provide the async API and the external uses of
766
+ # background()
767
+ if sem .locked ():
768
+ return self ._execute (
769
+ command ,
770
+ timeout = timeout ,
771
+ as_root = as_root ,
772
+ strip_colors = strip_colors ,
773
+ will_succeed = will_succeed ,
774
+ check_exit_code = check_exit_code ,
775
+ force_locale = 'C'
776
+ )
777
+ else :
778
+ async with sem :
779
+ bg = self .background (
780
+ command = command ,
781
+ as_root = as_root ,
782
+ force_locale = force_locale ,
783
+ )
787
784
788
- if will_succeed and isinstance (excep , TargetStableError ):
789
- excep = TargetTransientError (excep )
785
+ def process (streams ):
786
+ # Make sure we don't accidentally end up with "\n" if both streams
787
+ # are empty
788
+ res = b'\n ' .join (x for x in streams if x ).decode ()
789
+ if strip_colors :
790
+ res = strip_bash_colors (res )
791
+ return res
790
792
791
- if excep is None :
792
- res = process (streams )
793
- loop .call_soon_threadsafe (future .set_result , res )
794
- else :
795
- loop .call_soon_threadsafe (future .set_exception , excep )
793
+ def thread_f (loop , future ):
794
+ streams = (None , None )
795
+ excep = None
796
+ try :
797
+ with bg as _bg :
798
+ streams = _bg .communicate (timeout = timeout )
799
+ except BaseException as e :
800
+ excep = e
801
+
802
+ if isinstance (excep , subprocess .CalledProcessError ):
803
+ if check_exit_code :
804
+ excep = TargetStableError (excep )
805
+ else :
806
+ streams = (excep .output , excep .stderr )
807
+ excep = None
808
+
809
+ if will_succeed and isinstance (excep , TargetStableError ):
810
+ excep = TargetTransientError (excep )
811
+
812
+ if excep is None :
813
+ res = process (streams )
814
+ loop .call_soon_threadsafe (future .set_result , res )
815
+ else :
816
+ loop .call_soon_threadsafe (future .set_exception , excep )
796
817
797
- loop = asyncio .get_running_loop ()
798
- future = asyncio . Future ()
799
- thread = threading . Thread (
800
- target = thread_f ,
801
- daemon = True ,
802
- )
803
- thread .start ()
804
- return await future
818
+ future = asyncio .Future ()
819
+ thread = threading . Thread (
820
+ target = thread_f ,
821
+ args = ( asyncio . get_running_loop (), future ) ,
822
+ daemon = True ,
823
+ )
824
+ thread .start ()
825
+ return await future
805
826
806
827
@call_conn
807
828
def _execute (self , command , timeout = None , check_exit_code = True ,
@@ -821,13 +842,26 @@ def _execute(self, command, timeout=None, check_exit_code=True,
821
842
@call_conn
822
843
def background (self , command , stdout = subprocess .PIPE , stderr = subprocess .PIPE , as_root = False ,
823
844
force_locale = 'C' , timeout = None ):
824
- command = self ._prepare_cmd (command , force_locale )
825
- bg_cmd = self .conn .background (command , stdout , stderr , as_root )
826
- if timeout is not None :
827
- timer = threading .Timer (timeout , function = bg_cmd .cancel )
828
- timer .daemon = True
829
- timer .start ()
830
- return bg_cmd
845
+ conn = self .conn
846
+ # Make sure only one thread tries to spawn a background command at the
847
+ # same time, so the count of _current_bg_cmds is accurate.
848
+ with conn ._bg_spawn_lock :
849
+ alive = list (conn ._current_bg_cmds )
850
+ alive = [bg for bg in alive if bg .poll () is None ]
851
+ # Since the async path self-regulates using a
852
+ # asyncio.BoundedSemaphore(), going over the combined max means the
853
+ # culprit is the user spawning too many BackgroundCommand.
854
+ if len (alive ) >= conn ._max_bg :
855
+ raise TooManyBackgroundCommandsError (
856
+ '{} sessions allowed for one connection on this server. Modify MaxSessions parameter for OpenSSH to allow more.' .format (conn ._max_bg ))
857
+
858
+ command = self ._prepare_cmd (command , force_locale )
859
+ bg_cmd = self .conn .background (command , stdout , stderr , as_root )
860
+ if timeout is not None :
861
+ timer = threading .Timer (timeout , function = bg_cmd .cancel )
862
+ timer .daemon = True
863
+ timer .start ()
864
+ return bg_cmd
831
865
832
866
def invoke (self , binary , args = None , in_directory = None , on_cpus = None ,
833
867
redirect_stderr = False , as_root = False , timeout = 30 ):
0 commit comments