summaryrefslogtreecommitdiff
path: root/deps/v8/build/fuchsia/device_target.py
blob: c35fc79b3e90d67de88b95fb33fbbad031338707 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Implements commands for running and interacting with Fuchsia on devices."""

import boot_data
import filecmp
import logging
import os
import re
import subprocess
import sys
import target
import tempfile
import time
import uuid

from common import SDK_ROOT, EnsurePathExists

# The maximum times to attempt mDNS resolution when connecting to a freshly
# booted Fuchsia instance before aborting.
_BOOT_DISCOVERY_ATTEMPTS = 30

# Number of seconds to wait when querying a list of all devices over mDNS.
_LIST_DEVICES_TIMEOUT_SECS = 3

#Number of failed connection attempts before redirecting system logs to stdout.
CONNECT_RETRY_COUNT_BEFORE_LOGGING = 10

TARGET_HASH_FILE_PATH = '/data/.hash'

class DeviceTarget(target.Target):
  """Prepares a device to be used as a deployment target. Depending on the
  command line parameters, it automatically handling a number of preparatory
  steps relating to address resolution, device provisioning, and SDK
  versioning.

  If |_node_name| is unset:
    If there is one running device, use it for deployment and execution. The
    device's SDK version is checked unless --os-check=ignore is set.
    If --os-check=update is set, then the target device is repaved if the SDK
    version doesn't match.

    If there are more than one running devices, then abort and instruct the
    user to re-run the command with |_node_name|

    Otherwise, if there are no running devices, then search for a device
    running Zedboot, and pave it.


  If |_node_name| is set:
    If there is a running device with a matching nodename, then it is used
    for deployment and execution.

    Otherwise, attempt to pave a device with a matching nodename, and use it
    for deployment and execution.

  If |_host| is set:
    Deploy to a device at the host IP address as-is."""

  def __init__(self, output_dir, target_cpu, host=None, node_name=None,
               port=None, ssh_config=None, fuchsia_out_dir=None,
               os_check='update', system_log_file=None):
    """output_dir: The directory which will contain the files that are
                   generated to support the deployment.
    target_cpu: The CPU architecture of the deployment target. Can be
                "x64" or "arm64".
    host: The address of the deployment target device.
    node_name: The node name of the deployment target device.
    port: The port of the SSH service on the deployment target device.
    ssh_config: The path to SSH configuration data.
    fuchsia_out_dir: The path to a Fuchsia build output directory, for
                     deployments to devices paved with local Fuchsia builds.
    os_check: If 'check', the target's SDK version must match.
              If 'update', the target will be repaved if the SDK versions
                  mismatch.
              If 'ignore', the target's SDK version is ignored."""

    super(DeviceTarget, self).__init__(output_dir, target_cpu)

    self._port = port if port else 22
    self._system_log_file = system_log_file
    self._loglistener = None
    self._host = host
    self._fuchsia_out_dir = fuchsia_out_dir
    self._node_name = node_name
    self._os_check = os_check,

    if self._host and self._node_name:
      raise Exception('Only one of "--host" or "--name" can be specified.')

    if fuchsia_out_dir:
      if ssh_config:
        raise Exception('Only one of "--fuchsia-out-dir" or "--ssh_config" can '
                        'be specified.')

      # Use SSH keys from the Fuchsia output directory.
      self._ssh_config_path = os.path.join(os.path.expanduser(fuchsia_out_dir),
                                           'ssh-keys', 'ssh_config')
      self._os_check = 'ignore'

    elif ssh_config:
      # Use the SSH config provided via the commandline.
      self._ssh_config_path = os.path.expanduser(ssh_config)

    else:
      # Default to using an automatically generated SSH config and keys.
      boot_data.ProvisionSSH(output_dir)
      self._ssh_config_path = boot_data.GetSSHConfigPath(output_dir)

  def __exit__(self, exc_type, exc_val, exc_tb):
    if self._loglistener:
      self._loglistener.kill()

  def _SDKHashMatches(self):
    """Checks if /data/.hash on the device matches SDK_ROOT/.hash.

    Returns True if the files are identical, or False otherwise.
    """
    with tempfile.NamedTemporaryFile() as tmp:
      try:
        self.GetFile(TARGET_HASH_FILE_PATH, tmp.name)
      except subprocess.CalledProcessError:
        # If the file is unretrievable for whatever reason, assume mismatch.
        return False

      return filecmp.cmp(tmp.name, os.path.join(SDK_ROOT, '.hash'), False)

  def __Discover(self):
    """Queries mDNS for the IP address of a booted Fuchsia instance whose name
    matches |_node_name| on the local area network. If |_node_name| isn't
    specified, and there is only one device on the network, then returns the
    IP address of that advice.

    Sets |_host_name| and returns True if the device was found,
    or waits up to |timeout| seconds and returns False if the device couldn't
    be found."""

    dev_finder_path = os.path.join(SDK_ROOT, 'tools', 'dev_finder')

    if self._node_name:
      command = [dev_finder_path, 'resolve',
                 '-device-limit', '1',  # Exit early as soon as a host is found.
                 self._node_name]
    else:
      command = [dev_finder_path, 'list', '-full',
                 '-timeout', str(_LIST_DEVICES_TIMEOUT_SECS * 1000)]

    proc = subprocess.Popen(command,
                            stdout=subprocess.PIPE,
                            stderr=open(os.devnull, 'w'))

    output = proc.communicate()[0].strip().split('\n')

    if proc.returncode != 0:
      return False

    if self._node_name:
      # Handle the result of "dev_finder resolve".
      self._host = output[0].strip()

    else:
      name_host_pairs = [x.strip().split(' ') for x in output]

      # Handle the output of "dev_finder list".
      if len(name_host_pairs) > 1:
        print 'More than one device was discovered on the network.'
        print 'Use --node-name <name> to specify the device to use.'
        print '\nList of devices:'
        for pair in name_host_pairs:
          print '  ' + pair[1]
        print
        raise Exception('Ambiguous target device specification.')

      assert len(name_host_pairs) == 1
      self._host, self._node_name = name_host_pairs[0]

    logging.info('Found device "%s" at address %s.' % (self._node_name,
                                                       self._host))

    return True

  def Start(self):
    if self._host:
      self._WaitUntilReady()

    else:
      should_provision = False

      if self.__Discover():
        self._WaitUntilReady()

        if self._os_check != 'ignore':
          if self._SDKHashMatches():
            if self._os_check == 'update':
              logging.info( 'SDK hash does not match; rebooting and repaving.')
              self.RunCommand(['dm', 'reboot'])
              should_provision = True
            elif self._os_check == 'check':
              raise Exception('Target device SDK version does not match.')

      else:
        should_provision = True

      if should_provision:
        self.__ProvisionDevice()

      assert self._node_name
      assert self._host


  def __ProvisionDevice(self):
    """Netboots a device with Fuchsia. If |_node_name| is set, then only a
    device with a matching node name is used.

    The device is up and reachable via SSH when the function is successfully
    completes."""

    bootserver_path = os.path.join(SDK_ROOT, 'tools', 'bootserver')
    bootserver_command = [
        bootserver_path,
        '-1',
        '--fvm',
        EnsurePathExists(boot_data.GetTargetFile(self._GetTargetSdkArch(),
                                                 'fvm.sparse.blk')),
        EnsurePathExists(boot_data.GetBootImage(self._output_dir,
                                                self._GetTargetSdkArch()))]

    if self._GetTargetSdkArch() == 'x64':
      bootserver_command += [
          '--efi',
          EnsurePathExists(boot_data.GetTargetFile(self._GetTargetSdkArch(),
                                                   'local.esp.blk'))]

    if self._node_name:
      bootserver_command += ['-n', self._node_name]

    bootserver_command += ['--']
    bootserver_command += boot_data.GetKernelArgs(self._output_dir)

    logging.debug(' '.join(bootserver_command))
    stdout = subprocess.check_output(bootserver_command,
                                     stderr=subprocess.STDOUT)

    # Parse the nodename from bootserver stdout.
    m = re.search(r'.*Proceeding with nodename (?P<nodename>.*)$', stdout,
                  re.MULTILINE)
    if not m:
      raise Exception('Couldn\'t parse nodename from bootserver output.')
    self._node_name = m.groupdict()['nodename']
    logging.info('Booted device "%s".' % self._node_name)

    # Start loglistener to save system logs.
    if self._system_log_file:
      loglistener_path = os.path.join(SDK_ROOT, 'tools', 'loglistener')
      self._loglistener = subprocess.Popen(
          [loglistener_path, self._node_name],
          stdout=self._system_log_file,
          stderr=subprocess.STDOUT, stdin=open(os.devnull))

    # Repeatdly query mDNS until we find the device, or we hit the timeout of
    # DISCOVERY_TIMEOUT_SECS.
    logging.info('Waiting for device to join network.')
    for _ in xrange(_BOOT_DISCOVERY_ATTEMPTS):
      if self.__Discover():
        break

    if not self._host:
      raise Exception('Device %s couldn\'t be discovered via mDNS.' %
                      self._node_name)

    self._WaitUntilReady();

    # Update the target's hash to match the current tree's.
    self.PutFile(os.path.join(SDK_ROOT, '.hash'), TARGET_HASH_FILE_PATH)

  def _GetEndpoint(self):
    return (self._host, self._port)

  def _GetSshConfigPath(self):
    return self._ssh_config_path