jythonmonkeyrunner

How do I catch SocketExceptions in MonkeyRunner?


When using MonkeyRunner, every so often I get an error like:

120830 18:39:32.755:S [MainThread] [com.android.chimpchat.adb.AdbChimpDevice] Unable to get variable: display.density
120830 18:39:32.755:S [MainThread] [com.android.chimpchat.adb.AdbChimpDevice]java.net.SocketException: Connection reset

From what I've read, sometimes the adb connection goes bad, and you need to reconnect. The only problem is, I'm not able to catch the SocketException. I'll wrap my code like so:

try:
    density = self.device.getProperty('display.density')
except:
    print 'This will never print.'

But the exception is apparently not raised all the way to the caller. I've verified that MonkeyRunner/jython can catch Java exceptions the way I'd expect:

>>> from java.io import FileInputStream
>>> def test_java_exceptions():
...     try:
...         FileInputStream('bad mojo')
...     except:
...         print 'Caught it!'
...
>>> test_java_exceptions()
Caught it!

How can I deal with these socket exceptions?


Solution

  • Below is the workaround I ended up using. Any function that can suffer from adb failures just needs to use the following decorator:

    from subprocess import call, PIPE, Popen
    from time import sleep
    
    def check_connection(f):
        """
        adb is unstable and cannot be trusted.  When there's a problem, a
        SocketException will be thrown, but caught internally by MonkeyRunner
        and simply logged.  As a hacky solution, this checks if the stderr log 
        grows after f is called (a false positive isn't going to cause any harm).
        If so, the connection will be repaired and the decorated function/method
        will be called again.
    
        Make sure that stderr is redirected at the command line to the file
        specified by config.STDERR. Also, this decorator will only work for 
        functions/methods that take a Device object as the first argument.
        """
        def wrapper(*args, **kwargs):
            while True:
                cmd = "wc -l %s | awk '{print $1}'" % config.STDERR
                p = Popen(cmd, shell=True, stdout=PIPE)
                (line_count_pre, stderr) = p.communicate()
                line_count_pre = line_count_pre.strip()
    
                f(*args, **kwargs)
    
                p = Popen(cmd, shell=True, stdout=PIPE)
                (line_count_post, stderr) = p.communicate()
                line_count_post = line_count_post.strip()
    
                if line_count_pre == line_count_post:
                    # the connection was fine
                    break
                print 'Connection error. Restarting adb...'
                sleep(1)
                call('adb kill-server', shell=True)
                call('adb start-server', shell=True)
                args[0].connection = MonkeyRunner.waitForConnection()
    
        return wrapper
    

    Because this may create a new connection, you need to wrap your current connection in a Device object so that it can be changed. Here's my Device class (most of the class is for convenience, the only thing that's necessary is the connection member:

    class Device:
        def __init__(self):
            self.connection = MonkeyRunner.waitForConnection()
            self.width = int(self.connection.getProperty('display.width'))
            self.height = int(self.connection.getProperty('display.height'))
            self.model = self.connection.getProperty('build.model')
    
        def touch(self, x, y, press=MonkeyDevice.DOWN_AND_UP):
            self.connection.touch(x, y, press)
    

    An example on how to use the decorator:

    @check_connection
    def screenshot(device, filename):
        screen = device.connection.takeSnapshot()
        screen.writeToFile(filename + '.png', 'png')