dtmilano / AndroidViewClient

Android ViewServer and ADB client
Apache License 2.0
1.63k stars 347 forks source link

Deadlock between UiAutomatorHelper (on main thread) and RunTestsThread #252

Closed nicktgn closed 2 years ago

nicktgn commented 6 years ago

TL;DR:

When running CulebraTestCase tests, this line in the AdbClient#shell() seems to cause deadlocks between main thread and RunTestsThread thread, as it should have either with self.lock or separate self.lock.acquire() and self.lock.release() statements, but not both.

Long version:

I have an issue with running code generated by Culebra Tester.

I'm currently trying to setup some python tests using Culebra Tester and AndroidViewClient. The following code, generated by Culebra Tester app (after recording a couple of user clicks), gets stuck without ever reaching the test function itself:

#! /usr/bin/env python
# -*- coding: utf-8 -*-
'''
Copyright (C) 2013-2018  Diego Torres Milano
Created on 2018-08-02 by CulebraTester 
                 __    __    __    __
                /  \  /  \  /  \  /  \ 
____________________/  __\/  __\/  __\/  __\_____________________________
___________________/  /__/  /__/  /__/  /________________________________
               | / \   / \   / \   / \   \___
               |/   \_/   \_/   \_/   \    o \ 
                                 \_____/--<
@author: Diego Torres Milano
@author: Jennifer E. Swofford (ascii art snake)
'''

import re
import sys
import os

import unittest
try:
   sys.path.insert(0, os.path.join(os.environ['ANDROID_VIEW_CLIENT_HOME'], 'src'))
except:
   pass

import pkg_resources
pkg_resources.require('androidviewclient>=12.4.0')
from com.dtmilano.android.viewclient import ViewClient, CulebraTestCase
from com.dtmilano.android.uiautomator.uiautomatorhelper import UiAutomatorHelper, UiScrollable, UiObject, UiObject2

TAG = 'CULEBRA'

class CulebraTests(CulebraTestCase):

   @classmethod
   def setUpClass(cls):
      cls.kwargs1 = {'ignoreversioncheck': False, 'verbose': False, 'ignoresecuredevice': False}
      cls.kwargs2 = {'forceviewserveruse': False, 'useuiautomatorhelper': True, 'ignoreuiautomatorkilled': True, 'autodump': False, 'startviewserver': True, 'compresseddump': True}
      cls.options = {'start-activity': None, 'concertina': False, 'device-art': None, 'use-jar': False, 'multi-device': False, 'unit-test-class': True, 'save-screenshot': None, 'use-dictionary': False, 'glare': False, 'dictionary-keys-from': 'id', 'scale': 1, 'find-views-with-content-description': True, 'window': -1, 'orientation-locked': None, 'save-view-screenshots': None, 'find-views-by-id': True, 'log-actions': False, 'use-regexps': False, 'null-back-end': False, 'auto-regexps': None, 'do-not-verify-screen-dump': True, 'verbose-comments': False, 'gui': False, 'find-views-with-text': True, 'prepend-to-sys-path': False, 'install-apk': None, 'drop-shadow': False, 'output': None, 'unit-test-method': None,'interactive': False}
      cls.sleep = 5

   def setUp(self):
      super(CulebraTests, self).setUp()

   def tearDown(self):
      super(CulebraTests, self).tearDown()

   def preconditions(self):
      if not super(CulebraTests, self).preconditions():
         return False
      return True

   def test_something(self):
      print("Something")       # Execution doesn't even reach this point

      if not self.preconditions():
         self.fail('Preconditions failed')

      _s = CulebraTests.sleep
      _v = CulebraTests.verbose

      #  a few UI clicks recorded here by Culebra Tester ...

      self.fail("Fail")   # added fail statement here
      pass

if __name__ == '__main__':
   CulebraTests.main()

After tracing the AndroidViewClient code, enabling DEBUG logs in viewclient.py, uiautomatorhelper.py, adbclient.py and debugging the code execution, turns out there's a deadlock between UiAutomatorHelper#__connectSession() and RunTestsThread#run().

Adding some log statements for adb shell executions shows that AdbClient#shell() command is not releasing its lock properly, as RunTestsThread gets stuck on that lock when it tries to execute its force-stop shell command:

4325522968 shell(_cmd=getprop ro.build.version.sdk)
4325522968 shell(_cmd=dumpsys display)
4325522968 shell(_cmd=getprop ro.sf.lcd_density)
4325522968 shell(_cmd=getprop ro.secure)
4325522968 shell(_cmd=getprop ro.debuggable)
4325522968 shell(_cmd=getprop ro.build.version.sdk)
4325522968 shell(_cmd=getprop ro.build.version.sdk)
4325522968 shell(_cmd=getprop ro.build.version.release)
4325522968 shell(_cmd=getprop ro.secure)
4325522968 shell(_cmd=getprop ro.debuggable)
4325522968 shell(_cmd=getprop ro.product.board)
4325522968 shell(_cmd=getprop ro.product.brand)
    ViewClient.__init__: useUiAutomator= True sdk= 27 forceviewserveruse= False
4325522968 shell(_cmd=pm list instrumentation com.dtmilano.android.culebratester)
__runTests: start
4326886792 shell(_cmd=getprop ro.build.version.sdk)
4326886792 shell(_cmd=dumpsys display)
4326886792 shell(_cmd=getprop ro.sf.lcd_density)
__runTests: starting thread
RunTestsThread: Acquiring lock
RunTestsThread: Lock acquired
Cleaning up before start. Stopping 'com.dtmilano.android.culebratester'__runTests: end
UiAutomatorHelper: Acquiring lock

4326886792 shell(_cmd=am force-stop com.dtmilano.android.culebratester)

The culprit here seems to be this line in the AdbClient#shell():

That part should have either with self.lock or separate self.lock.acquire() and self.lock.release() statements, but not both.

That force-stop command is the first one executed from a separate thread, so naturally as the lock in AdbClient is a re-entrant lock, all previous shell commands (executed from main thread) re-acquire lock normally.

I will send a pull request with a fix.