| #!/usr/bin/env python |
| # Copyright 2019 The Fuchsia 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 argparse |
| import glob |
| import os |
| import re |
| import shlex |
| import subprocess |
| |
| from host import Host |
| |
| |
| class Device(object): |
| """Represents a Fuchsia device attached to a host. |
| |
| This class abstracts the details of remotely running commands and |
| transferring data to and from the device. |
| |
| Attributes: |
| host: A Host object represent the local platform attached to this target |
| device. |
| """ |
| |
| @classmethod |
| def from_args(cls, host, args): |
| """Constructs a Device from command line arguments.""" |
| netaddr_cmd = ['netaddr', '--fuchsia', '--nowait'] |
| if args.device: |
| netaddr_cmd.append(args.device) |
| try: |
| netaddr = host.zircon_tool(netaddr_cmd) |
| except subprocess.CalledProcessError: |
| raise RuntimeError('Unable to find device') |
| device = cls(host, netaddr) |
| if not host.build_dir: |
| raise Host.ConfigError('Unable to find SSH configuration.') |
| device.set_ssh_config(Host.join(host.build_dir, 'ssh-keys', 'ssh_config')) |
| return device |
| |
| def __init__(self, host, addr, port=22): |
| self.host = host |
| self._addr = addr |
| self._ssh_opts = {} |
| if port != 22: |
| self._ssh_opts['p'] = [str(port)] |
| |
| def set_ssh_config(self, config_file): |
| """Sets the SSH arguments to use a config file.""" |
| if not os.path.exists(config_file): |
| raise Host.ConfigError('Unable to find SSH configuration.') |
| self._ssh_opts['F'] = [config_file] |
| |
| def set_ssh_identity(self, identity_file): |
| if not os.path.exists(identity_file): |
| raise Host.ConfigError('Unable to find SSH identity.') |
| self._ssh_opts['i'] = [identity_file] |
| |
| def set_ssh_option(self, option): |
| """Sets SSH configuration options. Can be used multiple times.""" |
| if 'o' in self._ssh_opts: |
| self._ssh_opts['o'].append(option) |
| else: |
| self._ssh_opts['o'] = [option] |
| |
| def set_ssh_verbosity(self, level): |
| """Sets how much debugging SSH prints. Default is 0 (none), max is 3.""" |
| for i in range(1, 4): |
| opt = 'v' * i |
| if level == i and not opt in self._ssh_opts: |
| self._ssh_opts[opt] = [] |
| elif level != i and opt in self._ssh_opts: |
| del self._ssh_opts[opt] |
| |
| def get_ssh_cmd(self, cmd): |
| """Returns the SSH executable and options.""" |
| result = cmd[:1] |
| for opt, args in self._ssh_opts.iteritems(): |
| if len(args) == 0: |
| result.append('-' + opt) |
| else: |
| for arg in args: |
| result.append('-' + opt) |
| result.append(arg) |
| return result + cmd[1:] |
| |
| def _ssh(self, cmdline, stdout=subprocess.PIPE): |
| """Internal wrapper around _rexec that adds the ssh command and config. |
| |
| Don't call this directly. This method exists to be overridden in testing. |
| |
| Args: |
| cmdline: List of command line arguments to execute on device |
| stdout: Same as for subprocess.Popen |
| |
| Returns: |
| If check was false, a subprocess.Popen object representing the running |
| child process. |
| |
| Raises: Same as subprocess.Popen |
| """ |
| return subprocess.Popen( |
| self.get_ssh_cmd(['ssh', self._addr] + cmdline), |
| stdout=stdout, |
| stderr=subprocess.STDOUT) |
| |
| def ssh(self, cmdline, quiet=True, logfile=None): |
| """Runs a command to completion on the device. |
| |
| Connects to the target device and executes a shell command. Output from |
| the shell command is sent to stdout, and may optionally be saved to a file |
| via the POSIX utility 'tee'. |
| |
| Args: |
| cmdline: A list of command line arguments, starting with the command to |
| execute. |
| logfile: An optional pathname to save a copy of the command output to. The |
| output will also still be sent to stdout. |
| """ |
| if quiet: |
| if logfile: |
| with open(logfile, 'w') as f: |
| self._ssh(cmdline, stdout=f).wait() |
| else: |
| self._ssh(cmdline, stdout=Host.DEVNULL).wait() |
| else: |
| if logfile: |
| proc = self._ssh(cmdline, stdout=subprocess.PIPE) |
| subprocess.check_call(['tee', logfile], stdin=proc.stdout) |
| else: |
| self._ssh(cmdline, stdout=None).wait() |
| |
| def getpids(self): |
| """Maps names to process IDs for running fuzzers. |
| |
| Connects to the device and checks which fuzz targets have a matching entry |
| in the component list given by 'cs'. This matches on *only* the first 32 |
| characters of the component manifest and package URL. This is due to 'cs' |
| being limited to returning strings of length `ZX_MAX_NAME_LEN`, as defined |
| in //zircon/system/public/zircon/types.h. |
| |
| Returns: |
| A dict mapping fuzz target names to process IDs. May be empty if no |
| fuzzers are running. |
| """ |
| out, _ = self._ssh(['cs'], stdout=subprocess.PIPE).communicate() |
| pids = {} |
| for fuzzer in self.host.fuzzers: |
| tgt = (fuzzer[1] + '.cmx')[:32] |
| url = ('fuchsia-pkg://fuchsia.com/%s#meta' % fuzzer[0])[:32] |
| for line in str(out).split('\n'): |
| match = re.search(tgt + r'\[(\d+)\]: ' + url, line) |
| if match: |
| pids[fuzzer[1]] = int(match.group(1)) |
| return pids |
| |
| def ls(self, path): |
| """Maps file names to sizes for the given path. |
| |
| Connects to a Fuchsia device and lists the files in a directory given by |
| the provided path. Ignore non-existent paths. |
| |
| Args: |
| path: Absolute path to a directory on the device. |
| |
| Returns: |
| A dict mapping file names to file sizes, or an empty dict if the path |
| does not exist. |
| """ |
| results = {} |
| try: |
| out, _ = self._ssh( |
| ['ls', '-l', path], stdout=subprocess.PIPE).communicate() |
| for line in str(out).split('\n'): |
| # Line ~= '-rw-r--r-- 1 0 0 8192 Mar 18 22:02 some-name' |
| parts = line.split() |
| if len(parts) > 8: |
| results[' '.join(parts[8:])] = int(parts[4]) |
| except subprocess.CalledProcessError: |
| pass |
| return results |
| |
| def _scp(self, srcs, dst): |
| """Copies `src` to `dst`. |
| |
| Don't call directly; use `fetch` or `store` instead.` |
| |
| Args: |
| srcs: Local or remote paths to copy from. |
| dst: Local or remote path to copy to. |
| """ |
| # Wild cards only work if shell=True and the whole line passed as a string. |
| cmd = self.get_ssh_cmd(['scp'] + srcs + [dst]) |
| subprocess.check_call(cmd, stdout=None, stderr=None) |
| |
| def fetch(self, data_src, host_dst): |
| """Copies `data_src` on the target to `host_dst` on the host.""" |
| if not os.path.isdir(host_dst): |
| raise ValueError(host_dst + ' is not a directory') |
| self._scp(['[{}]:{}'.format(self._addr, data_src)], host_dst) |
| |
| def store(self, host_src, data_dst): |
| """Copies `host_src` on the host to `data_dst` on the target.""" |
| self.ssh(['mkdir', '-p', data_dst]) |
| self._scp(glob.glob(host_src), '[{}]:{}'.format(self._addr, data_dst)) |