How do I catch SocketExceptions in MonkeyRunner?

2020-07-11 07:44发布

问题:

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?

回答1:

You will get that error every odd time you start MonkeyRunner because the monkey --port 12345 command on the device isn't stopped when your script stops. It is a bug in monkey.

A nicer way to solve this issue is killing monkey when SIGINT is sent to your script (when you ctrl+c). In other words: $ killall com.android.commands.monkey.

Quick way to do it:

from sys, signal
from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice

device = None

def execute():
    device = MonkeyRunner.waitForConnection()
    # your code

def exitGracefully(self, signum, frame):
    signal.signal(signal.SIGINT, signal.getsignal(signal.SIGINT))
    device.shell('killall com.android.commands.monkey')
    sys.exit(1)

if __name__ == '__main__'
    signal.signal(signal.SIGINT, exitGracefully)
    execute()

Edit: as an addendum, I also found a way to notice the Java errors: Monkey Runner throwing socket exception broken pipe on touuch



回答2:

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')