blob: d57e9fbe5a629e27ad419a68c38b1aab2e0a71b2 [file] [log] [blame]
# Copyright 2018 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 inspect
import subprocess
import tempfile
import graphyte_common # pylint: disable=unused-import
from graphyte.utils import file_utils
from graphyte.default_setting import logger
CalledProcessError = subprocess.CalledProcessError
MODULE_PATH_MAPPING = {
'ADBLink': 'links.adb',
'SSHLink': 'links.ssh',
'SCPILink': 'links.scpi',
'GPIBLink': 'links.gpib'}
class LinkNotReady(Exception):
"""Exception for link is not ready."""
pass
class LinkOptionsError(Exception):
"""Exception for invalid link options."""
pass
class DeviceLink(object):
"""An abstract class for Device (DUT and instrument) Links."""
def Open(self):
"""Opens the connection of the link
This method should be called before any operation.
"""
pass
def Close(self):
"""Closes the connection of the link.
This method should be explicitly called before the device is destroyed.
"""
pass
def Push(self, local, remote):
"""Uploads a local file to DUT.
Args:
local: A string for local file path.
remote: A string for remote file path on DUT.
"""
raise NotImplementedError
def Pull(self, remote, local=None):
"""Downloads a file from DUT to local.
Args:
remote: A string for file path on remote DUT.
local: A string for local file path to receive downloaded content, or
None to return the contents directly.
Returns:
If local is None, return a string as contents in remote file.
Otherwise, do not return anything.
"""
raise NotImplementedError
def Shell(self, command, stdin=None, stdout=None, stderr=None):
"""Executes a command on DUT.
The calling convention is similar to subprocess.Popen, but only a subset of
parameters are supported due to platform limitation.
Args:
command: A string or a list of strings for command to execute.
stdin: A file object to override standard input.
stdout: A file object to override standard output.
stderr: A file object to override standard error.
Returns:
An object representing the process, similar to subprocess.Popen.
"""
raise NotImplementedError
def CheckReady(self):
if not self.IsReady():
raise LinkNotReady
def IsReady(self):
"""Checks if DUT is ready for connection.
Returns:
A boolean indicating if target DUT is ready.
"""
raise NotImplementedError
def Call(self, *args, **kargs):
"""Executes a command on DUT, using subprocess.call convention.
The arguments are explained in Shell.
Returns:
Exit code from executed command.
"""
process = self.Shell(*args, **kargs)
process.wait()
return process.returncode
def CheckCall(self, command, stdin=None, stdout=None, stderr=None):
"""Executes a command on DUT, using subprocess.check_call convention.
Args:
command: A string or a list of strings for command to execute.
stdin: A file object to override standard input.
stdout: A file object to override standard output.
stderr: A file object to override standard error.
Returns:
Exit code from executed command.
Raises:
CalledProcessError if the exit code is non-zero.
"""
exit_code = self.Call(command, stdin, stdout, stderr)
if exit_code != 0:
raise CalledProcessError(returncode=exit_code, cmd=command)
return exit_code
def CheckOutput(self, command, stdin=None, stderr=None):
"""Executes a command on DUT, using subprocess.check_output convention.
Args:
command: A string or a list of strings for command to execute.
stdin: A file object to override standard input.
stdout: A file object to override standard output.
stderr: A file object to override standard error.
Returns:
The output on STDOUT from executed command.
Raises:
CalledProcessError if the exit code is non-zero.
"""
with tempfile.TemporaryFile() as stdout:
exit_code = self.Call(command, stdin, stdout, stderr)
stdout.flush()
stdout.seek(0)
output = stdout.read()
if exit_code != 0:
raise CalledProcessError(
returncode=exit_code, cmd=command, output=output)
return output
def CallOutput(self, *args, **kargs):
"""Runs the command on DUT and return data from standard output if success.
Returns:
If command exits with zero (success), return the standard output;
otherwise None. If the command didn't output anything then the result is
empty string.
"""
try:
return self.CheckOutput(*args, **kargs)
except CalledProcessError as ex:
logger.debug('Failed to run command: %s', str(ex))
return None
def WriteFile(self, path, content):
"""Writes some content into file on DUT.
Args:
path: A string for file path on DUT.
content: A string to be written into file.
"""
with file_utils.UnopenedTemporaryFile() as temp_path:
with open(temp_path, 'w') as f:
f.write(content)
self.Push(temp_path, path)
def ReadFile(self, path):
with file_utils.UnopenedTemporaryFile() as temp_path:
self.Pull(path, temp_path)
return open(temp_path, 'r').read()
def ReadSpecialFile(self, path, count=None, skip=None):
cmd = ['dd', 'bs=1', 'if=%s' % path]
if count:
cmd += ['count=%d' % count]
if skip:
cmd += ['skip=%d' % skip]
return self.CheckOutput(cmd)
def _GetLinkClass(class_name):
"""Loads and returns a class object specified from the arguments.
The module and class will be constructed based on the arguments.
For example, ADBLink would load the class like
from graphyte.links.adb import ADBLink
Args:
class_name: A string for name of the class to load, or a class object.
Returns:
A class constructor from the arguments.
"""
if class_name not in MODULE_PATH_MAPPING:
raise LinkOptionsError('Unknown class name: %s' % class_name)
module_path = MODULE_PATH_MAPPING[class_name]
try:
return getattr(__import__(module_path, globals(), fromlist=[class_name]),
class_name)
except:
raise LinkOptionsError('Failed to load %s.%s' % (module_path, class_name))
def _ExtractArgs(func, kargs):
spec = inspect.getargspec(func)
if spec.keywords is None:
# if the function accepts ** arguments, we can just pass everything into it
# so we only need to filter kargs if spec.keywords is None
kargs = {k: v for (k, v) in kargs.iteritems() if k in spec.args}
return kargs
def Create(options):
"""Creates a link object.
Args:
options: Options to setup Device link.
Returns:
An instance of the sub-classed DeviceLink.
"""
link_name = options.pop('link_class')
link_class = _GetLinkClass(link_name)
link_args = _ExtractArgs(link_class.__init__, options)
return link_class(**link_args)