blob: ffbe8da6177ce8e47ed2ad7668022e85d11f81d8 [file] [log] [blame]
# Copyright 2017 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.
"""The abstraction of minimal functions needed to access a system."""
import glob
import logging
import pipes
import shutil
import subprocess
import tempfile
from . import process_utils
# Use process_utils.CalledProcessError for invocation exceptions.
CalledProcessError = process_utils.CalledProcessError
class SystemInterface:
"""Abstract interface for accessing a system."""
# Special values to make Popen work like subprocess.
PIPE = subprocess.PIPE
STDOUT = subprocess.STDOUT
def ReadFile(self, path, count=None, skip=None):
"""Returns file contents on target device.
By default the "most-efficient" way of reading file will be used, which may
not work for special files like device node or disk block file. Use
ReadSpecialFile for those files instead.
Meanwhile, if count or skip is specified, the file will also be fetched by
ReadSpecialFile.
Args:
path: A string for file path on target device.
count: Number of bytes to read. None to read whole file.
skip: Number of bytes to skip before reading. None to read from beginning.
Returns:
A string as file contents.
"""
if count is None and skip is None:
with open(path) as f:
return f.read()
return self.ReadSpecialFile(path, count=count, skip=skip)
def ReadSpecialFile(self, path, count=None, skip=None):
"""Returns contents of special file on target device.
Reads special files (device node, disk block, or sys driver files) on device
using the most portable approach.
Args:
path: A string for file path on target device.
count: Number of bytes to read. None to read whole file.
skip: Number of bytes to skip before reading. None to read from beginning.
Returns:
A string as file contents.
"""
with open(path, 'rb') as f:
if skip:
try:
f.seek(skip)
except IOError:
f.read(skip)
x = f.read() if count is None else f.read(count)
return x.decode('utf-8')
def WriteFile(self, path, content):
"""Writes some content into file on target device.
Args:
path: A string for file path on target device.
content: A string to be written into file.
"""
with open(path, 'w') as f:
f.write(content)
def WriteSpecialFile(self, path, content):
"""Writes some content into a special file on target device.
Args:
path: A string for file path on target device.
content: A string to be written into file.
"""
self.WriteFile(path, content)
def SendDirectory(self, local, remote):
"""Copies a local directory to target device.
`local` should be a local directory, and `remote` should be a non-existing
file path on target device.
Example::
SendDirectory('/path/to/local/dir', '/remote/path/to/some_dir')
Will create directory `some_dir` under `/remote/path/to` and copy
files and directories under `/path/to/local/dir/` to `some_dir`.
Args:
local: A string for directory path in local.
remote: A string for directory path on remote device.
"""
return shutil.copytree(local, remote)
def SendFile(self, local, remote):
"""Copies a local file to target device.
Args:
local: A string for file path in local.
remote: A string for file path on remote device.
"""
return shutil.copy(local, remote)
def Popen(self, command, stdin=None, stdout=None, stderr=None, cwd=None,
log=False, encoding='utf-8'):
"""Executes a command on target device using subprocess.Popen convention.
Compare to `subprocess.Popen`, the argument `shell=True/False` is not
available for this function. When `command` is a list, it treats each
item as a command to be invoked. When `command` is a string, it treats
the string as a shell script to invoke.
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.
cwd: The working directory for the command.
log: True (for logging.info) or a logger object to keep logs before
running the command.
encoding: Same as subprocess.Popen, we will use `utf-8` as default to make
it output str type.
Returns:
An object similar to subprocess.Popen.
"""
if log:
logger = logging.info if log is True else log
logger('%s Running: %r', type(self), command)
if not isinstance(command, str):
command = ' '.join(pipes.quote(param) for param in command)
return subprocess.Popen(command, cwd=cwd, shell=True, close_fds=True,
stdin=stdin, stdout=stdout, stderr=stderr,
encoding=encoding)
def Call(self, *args, **kargs):
"""Executes a command on target device, using subprocess.call convention.
The arguments are explained in Popen.
Returns:
Exit code from executed command.
"""
process = self.Popen(*args, **kargs)
process.wait()
return process.returncode
def CheckCall(self, command,
stdin=None, stdout=None, stderr=None, cwd=None, log=False):
"""Executes a command on device, 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.
cwd: The working directory for the command.
log: True (for logging.info) or a logger object to keep logs before
running the command.
Returns:
Exit code from executed command.
Raises:
CalledProcessError if the exit code is non-zero.
"""
exit_code = self.Call(
command, stdin=stdin, stdout=stdout, stderr=stderr, cwd=cwd, log=log)
if exit_code != 0:
raise CalledProcessError(returncode=exit_code, cmd=command)
return exit_code
def CheckOutput(self, command, stdin=None, stderr=None, cwd=None, log=False):
"""Executes a command on device, 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.
cwd: The working directory for the command.
log: True (for logging.info) or a logger object to keep logs before
running the command.
Returns:
The output on STDOUT from executed command.
Raises:
CalledProcessError if the exit code is non-zero.
"""
with tempfile.TemporaryFile('w+', encoding='utf-8') as stdout:
exit_code = self.Call(
command, stdin=stdin, stdout=stdout, stderr=stderr, cwd=cwd, log=log)
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 device and returns 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:
return None
def Glob(self, pattern):
"""Finds files on target device by pattern, similar to glob.glob.
Args:
pattern: A file path pattern (allows wild-card '*' and '?).
Returns:
A list of files matching pattern on target device.
"""
return glob.glob(pattern)