You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
221 lines
8.0 KiB
221 lines
8.0 KiB
# Copyright 2015 The Chromium OS Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
import collections
|
|
import dbus
|
|
import logging
|
|
import pipes
|
|
import re
|
|
import shlex
|
|
|
|
from autotest_lib.client.bin import utils
|
|
from autotest_lib.client.common_lib import error
|
|
|
|
|
|
# Represents the result of a dbus-send call. |sender| refers to the temporary
|
|
# bus name of dbus-send, |responder| to the remote process, and |response|
|
|
# contains the parsed response.
|
|
DBusSendResult = collections.namedtuple('DBusSendResult', ['sender',
|
|
'responder',
|
|
'response'])
|
|
# Used internally.
|
|
DictEntry = collections.namedtuple('DictEntry', ['key', 'value'])
|
|
|
|
|
|
def _build_token_stream(headerless_dbus_send_output):
|
|
"""A tokenizer for dbus-send output.
|
|
|
|
The output is basically just like splitting on whitespace, except that
|
|
strings are kept together by " characters.
|
|
|
|
@param headerless_dbus_send_output: list of lines of dbus-send output
|
|
without the meta-information prefix.
|
|
@return list of tokens in dbus-send output.
|
|
"""
|
|
return shlex.split(' '.join(headerless_dbus_send_output))
|
|
|
|
|
|
def _parse_value(token_stream):
|
|
"""Turn a stream of tokens from dbus-send output into native python types.
|
|
|
|
@param token_stream: output from _build_token_stream() above.
|
|
|
|
"""
|
|
if len(token_stream) == 0:
|
|
# Return None for dbus-send output with no return values.
|
|
return None
|
|
# Assumes properly tokenized output (strings with spaces handled).
|
|
# Assumes tokens are pre-stripped
|
|
token_type = token_stream.pop(0)
|
|
if token_type == 'variant':
|
|
token_type = token_stream.pop(0)
|
|
if token_type == 'object':
|
|
token_type = token_stream.pop(0) # Should be 'path'
|
|
token_value = token_stream.pop(0)
|
|
INT_TYPES = ('int16', 'uint16', 'int32', 'uint32',
|
|
'int64', 'uint64', 'byte')
|
|
if token_type in INT_TYPES:
|
|
return int(token_value)
|
|
if token_type == 'string' or token_type == 'path':
|
|
return token_value # shlex removed surrounding " chars.
|
|
if token_type == 'boolean':
|
|
return token_value == 'true'
|
|
if token_type == 'double':
|
|
return float(token_value)
|
|
if token_type == 'array':
|
|
values = []
|
|
while token_stream[0] != ']':
|
|
values.append(_parse_value(token_stream))
|
|
token_stream.pop(0)
|
|
if values and all([isinstance(x, DictEntry) for x in values]):
|
|
values = dict(values)
|
|
return values
|
|
if token_type == 'dict':
|
|
assert token_value == 'entry('
|
|
key = _parse_value(token_stream)
|
|
value = _parse_value(token_stream)
|
|
assert token_stream.pop(0) == ')'
|
|
return DictEntry(key=key, value=value)
|
|
raise error.TestError('Unhandled DBus type found: %s' % token_type)
|
|
|
|
|
|
def _parse_dbus_send_output(dbus_send_stdout):
|
|
"""Turn dbus-send output into usable Python types.
|
|
|
|
This looks like:
|
|
|
|
localhost ~ # dbus-send --system --dest=org.chromium.flimflam \
|
|
--print-reply --reply-timeout=2000 / \
|
|
org.chromium.flimflam.Manager.GetProperties
|
|
method return time=1490931987.170070 sender=org.chromium.flimflam -> \
|
|
destination=:1.37 serial=6 reply_serial=2
|
|
array [
|
|
dict entry(
|
|
string "ActiveProfile"
|
|
variant string "/profile/default"
|
|
)
|
|
dict entry(
|
|
string "ArpGateway"
|
|
variant boolean true
|
|
)
|
|
...
|
|
]
|
|
|
|
@param dbus_send_output: string stdout from dbus-send
|
|
@return a DBusSendResult.
|
|
|
|
"""
|
|
lines = dbus_send_stdout.strip().splitlines()
|
|
# The first line contains meta-information about the response
|
|
header = lines[0]
|
|
lines = lines[1:]
|
|
dbus_address_pattern = r'[:\d\\.]+|[a-zA-Z.]+'
|
|
# The header may or may not have a time= field.
|
|
match = re.match(r'method return (time=[\d\\.]+ )?sender=(%s) -> '
|
|
r'destination=(%s) serial=\d+ reply_serial=\d+' %
|
|
(dbus_address_pattern, dbus_address_pattern), header)
|
|
|
|
if match is None:
|
|
raise error.TestError('Could not parse dbus-send header: %s' % header)
|
|
|
|
sender = match.group(2)
|
|
responder = match.group(3)
|
|
token_stream = _build_token_stream(lines)
|
|
ret_val = _parse_value(token_stream)
|
|
# Note that DBus permits multiple response values, and this is not handled.
|
|
logging.debug('Got DBus response: %r', ret_val)
|
|
return DBusSendResult(sender=sender, responder=responder, response=ret_val)
|
|
|
|
|
|
def _dbus2string(raw_arg):
|
|
"""Turn a dbus.* type object into a string that dbus-send expects.
|
|
|
|
@param raw_dbus dbus.* type object to stringify.
|
|
@return string suitable for dbus-send.
|
|
|
|
"""
|
|
int_map = {
|
|
dbus.Int16: 'int16:',
|
|
dbus.Int32: 'int32:',
|
|
dbus.Int64: 'int64:',
|
|
dbus.UInt16: 'uint16:',
|
|
dbus.UInt32: 'uint32:',
|
|
dbus.UInt64: 'uint64:',
|
|
dbus.Double: 'double:',
|
|
dbus.Byte: 'byte:',
|
|
}
|
|
|
|
if isinstance(raw_arg, dbus.String):
|
|
return pipes.quote('string:%s' % raw_arg.replace('"', r'\"'))
|
|
|
|
if isinstance(raw_arg, dbus.Boolean):
|
|
if raw_arg:
|
|
return 'boolean:true'
|
|
else:
|
|
return 'boolean:false'
|
|
|
|
for prim_type, prefix in int_map.iteritems():
|
|
if isinstance(raw_arg, prim_type):
|
|
return prefix + str(raw_arg)
|
|
|
|
raise error.TestError('No support for serializing %r' % raw_arg)
|
|
|
|
|
|
def _build_arg_string(raw_args):
|
|
"""Construct a string of arguments to a DBus method as dbus-send expects.
|
|
|
|
@param raw_args list of dbus.* type objects to seriallize.
|
|
@return string suitable for dbus-send.
|
|
|
|
"""
|
|
return ' '.join([_dbus2string(arg) for arg in raw_args])
|
|
|
|
|
|
def dbus_send(bus_name, interface, object_path, method_name, args=None,
|
|
host=None, timeout_seconds=2, tolerate_failures=False, user=None):
|
|
"""Call dbus-send without arguments.
|
|
|
|
@param bus_name: string identifier of DBus connection to send a message to.
|
|
@param interface: string DBus interface of object to call method on.
|
|
@param object_path: string DBus path of remote object to call method on.
|
|
@param method_name: string name of method to call.
|
|
@param args: optional list of arguments. Arguments must be of types
|
|
from the python dbus module.
|
|
@param host: An optional host object if running against a remote host.
|
|
@param timeout_seconds: number of seconds to wait for a response.
|
|
@param tolerate_failures: boolean True to ignore problems receiving a
|
|
response.
|
|
@param user: An option argument to run dbus-send as a given user.
|
|
|
|
"""
|
|
run = utils.run if host is None else host.run
|
|
cmd = ('dbus-send --system --print-reply --reply-timeout=%d --dest=%s '
|
|
'%s %s.%s' % (int(timeout_seconds * 1000), bus_name,
|
|
object_path, interface, method_name))
|
|
|
|
if user is not None:
|
|
cmd = ('sudo -u %s %s' % (user, cmd))
|
|
if args is not None:
|
|
cmd = cmd + ' ' + _build_arg_string(args)
|
|
result = run(cmd, ignore_status=tolerate_failures)
|
|
if result.exit_status != 0:
|
|
logging.debug('%r', result.stdout)
|
|
return None
|
|
return _parse_dbus_send_output(result.stdout)
|
|
|
|
|
|
def get_property(bus_name, interface, object_path, property_name, host=None):
|
|
"""A helpful wrapper that extracts the value of a DBus property.
|
|
|
|
@param bus_name: string identifier of DBus connection to send a message to.
|
|
@param interface: string DBus interface exposing the property.
|
|
@param object_path: string DBus path of remote object to call method on.
|
|
@param property_name: string name of property to get.
|
|
@param host: An optional host object if running against a remote host.
|
|
|
|
"""
|
|
return dbus_send(bus_name, dbus.PROPERTIES_IFACE, object_path, 'Get',
|
|
args=[dbus.String(interface), dbus.String(property_name)],
|
|
host=host)
|