blob: 3d0c14851bd09419c02f79630872d06e9b2dcaec [file] [log] [blame]
#!/usr/bin/python -u
# -*- coding: utf-8 -*-
#
# Copyright 2014 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.
"""RPC methods exported from Goofy."""
from __future__ import print_function
import argparse
import inspect
import json
import logging
import os
import Queue
import random
import re
import subprocess
import tempfile
import threading
import time
import uuid
import yaml
from xml.sax import saxutils
import factory_common # pylint: disable=W0611
from cros.factory.device import device_utils
from cros.factory.goofy import goofy_remote
from cros.factory.test import factory
from cros.factory.test import shopfloor
from cros.factory.test.diagnosis.diagnosis_tool import DiagnosisToolRPC
from cros.factory.test.env import paths
from cros.factory.test.event import Event, EventClient
from cros.factory.test.test_lists.test_lists import SetActiveTestList
from cros.factory.tools import factory_bug
from cros.factory.utils import debug_utils
from cros.factory.utils import file_utils
from cros.factory.utils import process_utils
from cros.factory.utils import sys_utils
from cros.factory.utils import time_utils
from cros.factory.utils import type_utils
DEFAULT_GOOFY_RPC_TIMEOUT_SECS = 10
REBOOT_AFTER_UPDATE_DELAY_SECS = 5
PING_SHOPFLOOR_TIMEOUT_SECS = 2
UPLOAD_FACTORY_LOGS_TIMEOUT_SECS = 20
VAR_LOG_MESSAGES = '/var/log/messages'
RunState = type_utils.Enum(['UNINITIALIZED', 'STARTING', 'NOT_ACTIVE_RUN',
'RUNNING', 'FINISHED'])
class GoofyRPCException(Exception):
"""Goofy RPC exception."""
pass
class GoofyRPC(object):
"""Goofy RPC main class."""
def _InRunQueue(self, func, timeout_secs=None):
"""Runs a function in the Goofy run queue.
Args:
func: A callable to evaluate in Goofy run queue.
timeout_secs: The duration in seconds after which to abort the call. None
to block until the call is done.
Returns:
Any value returned by the function.
Raises:
Any exception raised by the function.
"""
# A queue to store the results of evaluating the function. This
# will contain a two-element tuple (ret, exc), where ret is the
# return value or exc is any exception thrown. Only one will be
# set.
result = Queue.Queue()
def Target():
try:
# Call the function, and put the the return value on success.
result.put((func(), None))
except Exception as e:
# Failure; put e.
logging.exception('Exception in RPC handler')
result.put((None, e))
except Exception:
# Failure (but not an Exception); wrap whatever it is in an exception.
result.put((None, GoofyRPCException(debug_utils.FormatExceptionOnly())))
def _GetFuncString():
func_string = func.__name__
if func.__name__ == '<lambda>':
try:
func_string = inspect.getsource(func).strip()
except IOError:
pass
return func_string
self.goofy.run_queue.put(Target)
try:
ret, exc = result.get(block=True, timeout=timeout_secs)
except Queue.Empty:
raise GoofyRPCException('Time out waiting for %s to complete' %
_GetFuncString())
if exc:
raise exc
return ret
def __init__(self, goofy):
self.goofy = goofy
# Creates delegate of RPC for diagnosis tool.
self.diagnosis_tool_rpc = DiagnosisToolRPC(self)
def RegisterMethods(self, state_instance):
"""Registers exported RPC methods in a state object."""
for name, m in inspect.getmembers(self):
# Find all non-private methods (except this one)
if ((not inspect.ismethod(m)) or
name.startswith('_') or
name == 'RegisterMethods'):
continue
# Bind the state instance method to our method. (We need to
# put this in a separate method to rebind m, since it will
# change during the next for loop iteration.)
def SetEntry(m):
# pylint: disable=W0108,W0640
state_instance.__dict__[name] = (
lambda *args, **kwargs: m(*args, **kwargs))
# pylint: enable=W0108,W0640
SetEntry(m)
def FlushEventLogs(self):
"""Flushes event logs if an event_log_watcher is available.
Raises an Exception if syncing fails.
"""
self.goofy.log_watcher.FlushEventLogs()
def UpdateFactory(self):
"""Performs a factory update.
Returns:
[success, updated, restart_time, error_msg] where:
success: Whether the operation was successful.
updated: Whether the update was a success and the system will reboot.
restart_time: The time at which the system will restart (on success).
error_msg: An error message (on failure).
"""
ret_value = Queue.Queue()
# Record the existence of the host-based tag files, so we can restore them
# after the update.
device_tag = os.path.join(
paths.FACTORY_PATH, 'init', goofy_remote.DEVICE_TAG)
presenter_tag = os.path.join(
paths.FACTORY_PATH, 'init', goofy_remote.PRESENTER_TAG)
is_device = os.path.exists(device_tag)
is_presenter = os.path.exists(presenter_tag)
def PostUpdateHook():
def _RestoreTag(tag_status, tag_path):
if tag_status:
file_utils.TouchFile(tag_path)
else:
file_utils.TryUnlink(tag_path)
# Restore all the host-based tag files after update.
_RestoreTag(is_device, device_tag)
_RestoreTag(is_presenter, presenter_tag)
# After update, wait REBOOT_AFTER_UPDATE_DELAY_SECS before the
# update, and return a value to the caller.
now = time.time()
ret_value.put([True, True, now + REBOOT_AFTER_UPDATE_DELAY_SECS, None])
time.sleep(REBOOT_AFTER_UPDATE_DELAY_SECS)
def Target():
try:
self.goofy.update_factory(
auto_run_on_restart=True,
post_update_hook=PostUpdateHook)
# Returned... which means that no update was necessary.
ret_value.put([True, False, None, None])
except: # pylint: disable=W0702
# There was an update available, but we couldn't get it.
logging.exception('Update failed')
ret_value.put([False, False, None, debug_utils.FormatExceptionOnly()])
self.goofy.run_queue.put(Target)
return ret_value.get()
def AddNote(self, note):
note['timestamp'] = int(time.time())
self.goofy.event_log.Log('note',
name=note['name'],
text=note['text'],
timestamp=note['timestamp'],
level=note['level'])
logging.info('Factory note from %s at %s (level=%s): %s',
note['name'], note['timestamp'], note['level'],
note['text'])
if note['level'] == 'CRITICAL':
self.goofy.run_queue.put(self.goofy.stop)
self.goofy.state_instance.append_shared_data_list(
'factory_note', note)
self.PostEvent(Event(Event.Type.UPDATE_NOTES))
def GetVarLogMessages(self, max_length=256 * 1024):
"""Returns the last n bytes of /var/log/messages.
Args:
max_length: Maximum number of bytes to return.
"""
offset = max(0, os.path.getsize(VAR_LOG_MESSAGES) - max_length)
with open(VAR_LOG_MESSAGES, 'r') as f:
f.seek(offset)
if offset != 0:
# Skip first (probably incomplete) line
offset += len(f.readline())
data = f.read()
if offset:
data = ('<truncated %d bytes>\n' % offset) + data
return unicode(data, encoding='utf-8', errors='replace')
def GetVarLogMessagesBeforeReboot(
self, lines=100, max_length=5 * 1024 * 1024):
"""Returns the last few lines in /var/log/messages before current boot.
Args:
See sys_utils.GetVarLogMessagesBeforeReboot.
"""
lines = sys_utils.GetVarLogMessagesBeforeReboot(lines=lines,
max_length=max_length)
if lines:
return unicode('\n'.join(lines) + '\n',
encoding='utf-8', errors='replace')
else:
return None
@staticmethod
def _ReadUptime():
return open('/proc/uptime').read()
def GetDmesg(self):
"""Returns the contents of dmesg.
Approximate timestamps are added to each line.
"""
dmesg = process_utils.Spawn(['dmesg'],
check_call=True, read_stdout=True).stdout_data
uptime = float(self._ReadUptime().split()[0])
boot_time = time.time() - uptime
def FormatTime(match):
return (time_utils.TimeString(boot_time + float(match.group(1))) + ' ' +
match.group(0))
# (?m) = multiline
return re.sub(r'(?m)^\[\s*([.\d]+)\]', FormatTime, dmesg)
def GetDeviceInfo(self, reload_function_array=None):
"""Returns system hardware info in XML format.
Since the commands can be separated into two categories, faster ones and
slower ones, we get our info in two stages. First, we execute the faster
commands and return their results. Then we will enter the second stage when
the frontend device manager needs the other ones.
Args:
reload_function_array: An array containing functions to be loaded. If
the array includes nothing, it means that we are in first stage, where
we need to execute all fast commands. Otherwise, we will only spawn the
functions inside the array.
Returns:
A string including system hardware info.
"""
def DeviceNodeString(
node_id, node_description, tag_list, slow_command=None):
"""Returns a XML format string of a specific device.
Args:
node_id: An id attribute to identify the device in XML document.
node_description: A human readable name of the device.
tag_list: A list of more detailed information of the device. Each
element, including (tag_name, tag_text, split_multiline), represents
a tag, which is a child node of the device node.
slow_command: If it is set, it means the command takes longer time and
we wish to execute it later. Therefore the method will return a
string without data but indicating the device needs to be executed
again. The value, which is the name of the method is stored in an
attribute of the device node.
Returns:
A string with XML format of the device node.
"""
def SplitMultilineToNodes(lines):
"""Returns a XML format string to split lines of a output string.
Since HTML cannot identify '\n' automatically, we should split the
multiline output into multiple tags (one line is transformed into one
tag) so that the device manager can show it in multiline format.
Args:
lines: The multiline output to split.
Returns:
A string with XML format of the lines.
"""
lines = lines.rstrip()
return ''.join(
'<line>%s</line>' % saxutils.escape(l) for l in lines.split('\n'))
result = []
result.append('<node id="%s"' % saxutils.escape(node_id))
if not slow_command:
result.append('>')
else:
result.append(' slow_command="%s">' % saxutils.escape(slow_command))
result.append(
'<description>%s</description>' % saxutils.escape(node_description))
for tag_name, tag_text, split_multiline in tag_list:
xml_tag_name = saxutils.escape(tag_name)
if split_multiline:
xml_tag_text = SplitMultilineToNodes(tag_text)
else:
xml_tag_text = saxutils.escape(tag_text)
result.append(
'<%s>%s</%s>' % (xml_tag_name, xml_tag_text, xml_tag_name))
result.append('</node>')
return ''.join(result)
def SlowCommandDeviceNodeString(node_id, node_description, function_name):
"""Returns a XML string of a device which needs longer time to get info.
Args:
node_id: An id attribute to identify the device in XML document.
node_description: A human readable name of the device.
function_name: The name of the method which is slow.
Return:
A string with XML format, including only the description of the node.
"""
return DeviceNodeString(
node_id, node_description, [], slow_command=function_name)
def GetBootDisk():
"""Returns boot disk info."""
boot_device = subprocess.check_output(['rootdev', '-s', '-d']).strip()
boot_device_removable_path = (
os.path.join('/sys/block/',
os.path.basename(boot_device),
'removable'))
boot_device_removable = open(boot_device_removable_path).read().strip()
if boot_device.startswith('/dev/sd'):
boot_device_type = (
'SSD' if boot_device_removable == '0' else 'USB drive')
elif boot_device.startswith('/dev/mmcblk'):
boot_device_type = (
'eMMC' if boot_device_removable == '0' else 'SD card')
else:
boot_device_type = 'unknown'
return DeviceNodeString(
'bootdisk', 'Boot disk',
[('device', boot_device, False), ('type', boot_device_type, False)])
def GetTPMStatus():
"""Returns TPM status info."""
tpm_root = '/sys/class/tpm/tpm0/device'
legacy_tpm_root = '/sys/class/misc/tpm0/device'
# TPM device path has been changed in kernel 3.18.
if not os.path.exists(tpm_root):
tpm_root = legacy_tpm_root
tpm_status = (open(os.path.join(tpm_root, 'enabled')).read(),
open(os.path.join(tpm_root, 'owned')).read())
tpm_stat = (
('Enabled: %s\nOwned: %s\n' % tpm_status) +
subprocess.check_output('crossystem | grep tpm_owner', shell=True))
return DeviceNodeString(
'tpm', 'TPM status', [('status', tpm_stat, True)])
def GetHWID():
"""Returns HWID."""
hwid = subprocess.check_output(['crossystem', 'hwid'])
return DeviceNodeString('hwid', 'HWID', [('hwid', hwid, False)])
def GetWPStatus():
"""Returns current write protection status info."""
host_wp_stat = (
subprocess.check_output(['flashrom', '-p', 'host', '--wp-status']))
try:
ec_wp_stat = (
subprocess.check_output(['flashrom', '-p', 'ec', '--wp-status']))
except subprocess.CalledProcessError:
ec_wp_stat = 'EC not available.'
return DeviceNodeString(
'wp', 'Current write protection status',
[('host', host_wp_stat, True), ('ec', ec_wp_stat, True)])
def GetVersion():
"""Returns EC/BIOS/Image version info."""
fw_version = (
subprocess.check_output(['crossystem', 'fwid']) +
subprocess.check_output(['crossystem', 'ro_fwid']))
try:
ec_version = subprocess.check_output(['ectool', 'version'])
except subprocess.CalledProcessError:
ec_version = 'EC not available.'
image_version = ''.join(open('/etc/lsb-release', 'r').readlines())
return DeviceNodeString(
'version', 'AP Firmware(BIOS)/EC/Image Version',
[('fw', fw_version, True), ('ec', ec_version, True),
('image', image_version, True)])
def GetVPD(): # pylint: disable=W0612
"""Returns RO VPD and RW VPD info."""
ro_vpd = subprocess.check_output(['vpd', '-i', 'RO_VPD', '-l'])
rw_vpd = subprocess.check_output(['vpd', '-i', 'RW_VPD', '-l'])
return DeviceNodeString(
'vpd', 'RO/RW VPD', [('ro', ro_vpd, True), ('rw', rw_vpd, True)])
def GetInputDeviceFirmwareVersion(device_name_list):
"""Returns firmware version of a specific hardware.
Args:
device_name_list: A list consists of possible human readable names of
the hardware.
Returns:
A string including the firmware version.
"""
re_device_name = (
re.compile(r'N: Name=".*(%s).*"' % ('|'.join(device_name_list))))
re_device_sysfs = re.compile(r'S: Sysfs=(.*)')
device_list = open('/proc/bus/input/devices').read().split('\n\n')
for device_data in device_list:
match_device_name = re_device_name.findall(device_data)
if not match_device_name:
continue
device_sysfs = re_device_sysfs.findall(device_data)[0].lstrip('/')
firmware_path_patterns = ['firmware_version', 'fw_version']
device_path = os.path.join('/sys', device_sysfs, 'device')
for path_pattern in firmware_path_patterns:
device_firmware_path = os.path.join(device_path, path_pattern)
if os.path.exists(device_firmware_path):
return open(device_firmware_path).read().strip()
return 'unknown'
return 'unknown'
def GetTouchscreenFirmwareVersion():
"""Returns touchscreen firmware version."""
return DeviceNodeString(
'touchscreen_version', 'Touchscreen firmware version',
[('fw_version', GetInputDeviceFirmwareVersion(['Touchscreen']),
False)])
def GetTouchpadFirmwareVersion():
"""Returns touchpad firmware version."""
return DeviceNodeString(
'touchpad_version', 'Touchpad firmware version',
[('fw_version',
GetInputDeviceFirmwareVersion(['Trackpad', 'Touchpad']),
False)])
def GetTouchpadStatus():
"""Returns touchpad status."""
touchpad_stat = subprocess.check_output(
['/opt/google/touchpad/tpcontrol_xinput', 'status'])
lines = (
[re.sub(r'\s\s+', '', line) for line in touchpad_stat.splitlines()])
return DeviceNodeString(
'touchpad_status', 'Touchpad status',
[('status', '\n'.join(lines), True)])
def GetPanelHDMIStatus():
"""Returns panel and HDMI status."""
panel_hdmi_stat = subprocess.check_output(['xrandr', '-d', ':0'])
return DeviceNodeString(
'panel_hdmi_stat', 'Panel/HDMI status',
[('status', panel_hdmi_stat, True)])
def GetModemStatus():
"""Returns modem status."""
modem_stat = subprocess.check_output(['modem', 'status'])
modem_stat_tag_list = []
tag_content = []
tag_name = ''
for line in modem_stat.splitlines():
if line.startswith(' '):
tag_content.append(line.strip())
elif line.startswith(' '):
if tag_content:
modem_stat_tag_list.append(
(tag_name, '\n'.join(tag_content), True))
tag_name = line.strip(' :').lower()
tag_content = []
if tag_content:
modem_stat_tag_list.append((tag_name, '\n'.join(tag_content), True))
return DeviceNodeString('modem', 'Modem status', modem_stat_tag_list)
def ComposeHTMLTable(table_data):
"""Constructs a HTML format string containing a table of the input.
If the input is [[data1, data2], [data3, data4]] then the output string
would be '<table><tr><td>data1</td><td>data2</td></tr><tr><td>data3</td><
td>data4</td></tr></table>'.
Args:
table_data: A list of list consisting of the table elements.
Returns:
HTML string.
"""
table_html_string = ['<table class="multi-column-table">']
first_row = True
for row in table_data:
table_html_string.append('<tr>')
if first_row:
table_html_string.extend(
('<th>' + element + '</th>') for element in row)
first_row = False
else:
table_html_string.extend(
('<td>' + element + '</td>') for element in row)
table_html_string.append('</tr>')
table_html_string.append('</table>')
return ''.join(table_html_string)
def GetCPUUsage():
"""Returns CPU usage detail in HTML format."""
cpu_usage_output = subprocess.check_output(['top', '-n', '1', '-b'])
cpu_usage_table = []
lines = cpu_usage_output.split('\n')
first_blank_line_index = lines.index('')
# CPU / memory info
cpu_usage_table.append('<ul>')
cpu_usage_table.extend(
('<li>' + line + '</li>') for line in lines[:first_blank_line_index])
cpu_usage_table.append('</ul>')
# Process info
cpu_usage_table.append(
ComposeHTMLTable(
[line.split() for line in lines[first_blank_line_index + 1:]]))
return DeviceNodeString(
'cpu_usage', 'CPU usage',
[('html_string', ''.join(cpu_usage_table), False)])
def GetDiskUsage():
"""Returns disk usage detail in HTML format."""
disk_usage_output = subprocess.check_output(['df', '-h'])
disk_usage_html_string = ComposeHTMLTable(
[line.split() for line in disk_usage_output.splitlines()])
return DeviceNodeString(
'disk_usage', 'Disk usage',
[('html_string', disk_usage_html_string, False)])
def GetMemoryUsage():
"""Returns memory usage detail."""
memory_usage_output = file_utils.ReadLines('/proc/meminfo')
memory_usage_list = []
for line in memory_usage_output:
line = re.sub(r'\s+', '', line.strip())
data = line.split(':')
memory_usage_list.append((data[0], data[1], False))
return DeviceNodeString(
'memory_usage', 'Memory usage', memory_usage_list)
def GetItemListFromPowerTOPHTML(html_file_path):
"""Returns a list of item information by extracting the PowerTOP report.
This method is used to get data from HTML file generated by PowerTOP.
The relationship between tag id and description of an item is defined
in 'blocks' structure, we need to extract it out.
Args:
html_file_path: Path to HTML generated by PowerTOP.
Returns:
A list of tuples (description, node_id) for each item.
"""
lines = file_utils.ReadLines(html_file_path)
found_start = False
found = False
for i, line in enumerate(lines):
if not found_start and 'blocks: {' in line:
start_index = i
found_start = True
elif found_start and '}' in line:
end_index = i
found = True
break
if not found:
return None
row_content = lines[start_index + 1:end_index]
item_list = []
re_item_line = re.compile(r'(.*): \'(.*)\'')
for line in row_content:
match = re_item_line.findall(line.strip().rstrip(','))
if match:
item_list.append((match[0][1], match[0][0]))
return item_list
def FetchFromHTML(html_file_path, tag_id):
"""Returns HTML data of the tag whose id is tag_id from a HTML file.
powertop command supports HTML/CSV output format. However, the CSV output
cannot be easily parsed to construct a HTML table (ex: each row of the
CSV file doesn't have the same number of fields). We need this function
to extract data from powertop's HTML output then modify it to our own
style.
Args:
html_file_path: Path to HTML generated by PowerTOP.
tag_id: A string used to identify which tag to find.
Returns:
A HTML format string.
"""
lines = file_utils.ReadLines(html_file_path)
found_tag_id = False
content_rows = []
for line in lines:
if not found_tag_id:
if 'id="%s"' % tag_id in line:
found_tag_id = True
else:
if not '</div>' in line:
content_rows.append(line.strip())
else:
break
html_string = ''.join(content_rows)
return html_string.replace('<table', '<table class="multi-column-table"')
def GetPowerUsage(fetch_time=20): # pylint: disable=W0612
"""Returns power usage detail.
Args:
fetch_time: A number indicating how long powertop should fetch data.
The default time is 20 seconds.
"""
html_file_path = tempfile.mkstemp(suffix='.html')[1]
subprocess.check_output(
['powertop', '--html=%s' % html_file_path, '--time=%d' % fetch_time])
power_usage_main_xml = []
power_usage_main_xml.append('<node id="power_usage">')
power_usage_main_xml.append('<description>Power usage</description>')
item_list = GetItemListFromPowerTOPHTML(html_file_path)
first_item = True
for description, tag_id in item_list:
if first_item:
power_usage_main_xml.append('<html_string>')
power_usage_main_xml.append(
saxutils.escape(FetchFromHTML(html_file_path, tag_id)))
power_usage_main_xml.append('</html_string>')
first_item = False
else:
power_usage_main_xml.append(
DeviceNodeString(
tag_id, description,
[('html_string',
FetchFromHTML(html_file_path, tag_id),
False)]))
power_usage_main_xml.append('</node>')
return ''.join(power_usage_main_xml)
# In first stage, we execute faster commands first.
if reload_function_array is None:
# lshw provides common hardware information.
lshw_output = subprocess.check_output(['lshw', '-xml'])
xml_lines = [line.strip() for line in lshw_output.splitlines()]
# Use cros-specific commands to get cros info.
cros_output = []
cros_output.append('<node id="cros">')
cros_output.append('<description>Chrome OS Specific</description>')
cros_output.append(GetBootDisk())
cros_output.append(GetTPMStatus())
cros_output.append(GetHWID())
cros_output.append(GetWPStatus())
cros_output.append(GetVersion())
cros_output.append(
SlowCommandDeviceNodeString('vpd', 'RO/RW VPD', 'GetVPD()'))
cros_output.append('</node>')
xml_lines.insert(xml_lines.index('</list>'), ''.join(cros_output))
# Get peripheral device info.
peripheral_output = []
peripheral_output.append('<node id="peripheral">')
peripheral_output.append('<description>Peripheral Devices</description>')
peripheral_output.append(GetTouchscreenFirmwareVersion())
peripheral_output.append(GetTouchpadFirmwareVersion())
peripheral_output.append(GetTouchpadStatus())
peripheral_output.append(GetPanelHDMIStatus())
peripheral_output.append(GetModemStatus())
peripheral_output.append('</node>')
xml_lines.insert(xml_lines.index('</list>'), ''.join(peripheral_output))
# Get system usage details.
system_usage = []
system_usage.append('<node id="usage">')
system_usage.append('<description>System Usage</description>')
system_usage.append(GetCPUUsage())
system_usage.append(
SlowCommandDeviceNodeString(
'power_usage', 'Power usage', 'GetPowerUsage()'))
system_usage.append(GetDiskUsage())
system_usage.append(GetMemoryUsage())
system_usage.append('</node>')
xml_lines.insert(xml_lines.index('</list>'), ''.join(system_usage))
return ''.join(xml_lines)
# In second stage, we execute slower commands and return their results.
else:
result = (
[eval(reload_function) # pylint: disable=W0123
for reload_function in json.loads(reload_function_array)])
return json.dumps(result)
def LogStackTraces(self):
"""Logs the stack backtraces of all threads."""
logging.info(debug_utils.DumpStackTracebacks())
def IsUSBDriveAvailable(self):
try:
with factory_bug.MountUSB(read_only=True):
return True
except (IOError, OSError):
return False
def SaveLogsToUSB(self, archive_id=None):
"""Saves logs to a USB stick.
Returns:
[dev, archive_name, archive_size, temporary]:
dev: The device that was mounted or used
archive_name: The file name of the archive
archive_size: The size of the archive
temporary: Whether the USB drive was temporarily mounted
"""
try:
with factory_bug.MountUSB() as mount:
output_file = factory_bug.SaveLogs(mount.mount_point,
archive_id=archive_id)
return [mount.dev, os.path.basename(output_file),
os.path.getsize(output_file),
mount.temporary]
except:
logging.exception('Unable to save logs to USB')
raise
def PingShopFloorServer(self):
"""Pings the shop floor server.
Raises:
Exception if unable to contact shop floor server.
"""
shopfloor.get_instance(
detect=True, timeout=PING_SHOPFLOOR_TIMEOUT_SECS).Ping()
def UploadFactoryLogs(self, name, serial, description):
"""Uploads logs to the shopfloor server.
Returns:
[archive_name, archive_size, archive_key]
archive_name: The uploaded file name.
archive_size: The size of the archive.
archive_key: A "key" that may later be used to refer to the archive.
This is just a randomly-chosen 8-digit number.
"""
archive_key = '%08d' % random.SystemRandom().randint(0, 1e8)
archive_id = '.'.join([re.sub('[^A-Za-z0-9.]', '_', x)
for x in (archive_key, name, serial, description)])
output_file = factory_bug.SaveLogs(tempfile.gettempdir(),
archive_id=archive_id)
try:
with open(output_file) as f:
data = f.read()
shopfloor.get_instance(
detect=True, timeout=UPLOAD_FACTORY_LOGS_TIMEOUT_SECS
).SaveAuxLog(os.path.basename(output_file),
shopfloor.Binary(data))
return [os.path.basename(output_file), os.path.getsize(output_file),
archive_key]
finally:
file_utils.TryUnlink(output_file)
def UpdateSkippedTests(self):
"""Updates skipped tests based on run_if."""
done = threading.Event()
def Target():
try:
self.goofy.update_skipped_tests()
finally:
done.set()
self.goofy.run_queue.put(Target)
done.wait()
def SyncTimeWithShopfloorServer(self):
# TODO(shunhsingou): remove this function when we have unified RPC interface
# for goofy plugin.
time_sanitizer = self.goofy.plugin_controller.GetPluginInstance(
'time_sanitizer.TimeSanitizer')
if time_sanitizer:
time_sanitizer.SyncTimeWithShopfloorServer()
def PostEvent(self, event):
"""Posts an event."""
self.goofy.event_client.post_event(event)
def StopTest(self, timeout_secs=DEFAULT_GOOFY_RPC_TIMEOUT_SECS):
"""Stops current tests."""
self._InRunQueue(
lambda: self.goofy.stop(reason='RPC call to stop tests',
fail=True),
timeout_secs=timeout_secs)
def ClearState(self, timeout_secs=DEFAULT_GOOFY_RPC_TIMEOUT_SECS):
"""Stops current tests and clear all test state."""
def Target():
self.goofy.stop(reason='RPC call to clear test state',
fail=True)
self.goofy.clear_state()
self._InRunQueue(Target, timeout_secs=timeout_secs)
def RunTest(self, path, timeout_secs=DEFAULT_GOOFY_RPC_TIMEOUT_SECS):
"""Runs a test."""
test = self.goofy.test_list.lookup_path(path)
if not test:
raise GoofyRPCException('Unknown test path %r' % path)
test = test.get_top_level_parent_or_group()
self._InRunQueue(lambda: self.goofy.restart_tests(root=test),
timeout_secs=timeout_secs)
return self.goofy.run_id
def RestartAllTests(self, timeout_secs=DEFAULT_GOOFY_RPC_TIMEOUT_SECS):
"""Restarts all tests.
Args:
timeout_secs: The duration in seconds after which to abort the call.
"""
self._InRunQueue(self.goofy.restart_tests, timeout_secs=timeout_secs)
return self.goofy.run_id
def ScheduleRestart(self, timeout_secs=DEFAULT_GOOFY_RPC_TIMEOUT_SECS):
"""Schedules to restart all tests when current test finished.
Args:
timeout_secs: The duration in seconds after which to abort the call.
"""
self._InRunQueue(self.goofy.schedule_restart, timeout_secs=timeout_secs)
def CancelPendingTests(self, timeout_secs=DEFAULT_GOOFY_RPC_TIMEOUT_SECS):
"""Cancels all pending tests."""
self._InRunQueue(self.goofy.cancel_pending_tests,
timeout_secs=timeout_secs)
def LogStartupMessages(self, timeout_secs=DEFAULT_GOOFY_RPC_TIMEOUT_SECS):
"""Logs the tail of var/log/messages and mosys and EC console logs."""
self._InRunQueue(self.goofy.log_startup_messages,
timeout_secs=timeout_secs)
def Shutdown(self, operation):
"""Starts a shutdown operation through Goofy.
Args:
operation: The shutdown operation to run ('halt', 'reboot',
or 'full_reboot').
"""
if operation not in ['halt', 'reboot', 'full_reboot']:
raise GoofyRPCException('Invalid shutdown operation %r' % operation)
# No timeout for shutdown as the operation can be delayed for arbitrary
# duration by the factory test.
self._InRunQueue(lambda: self.goofy.shutdown(operation))
def GetLastShutdownTime(self):
"""Gets last shutdown time detected by Goofy."""
return self.goofy.last_shutdown_time
def UIPresenterCountdown(self, message, timeout_secs, timeout_message,
timeout_is_error=True):
"""Starts a countdown on the presenter UI.
In situations like a reboot, Goofy is not available and on the UI
presenter side, it looks like a disconnected state. To avoid confusing
operators, this method may be used to inform the current status of Goofy
and set a timeout by which Goofy is expected to come back alive.
Args:
message: The text to show while counting down.
timeout_secs: The timeout for countdown.
timeout_message: The text to show when countdown ends.
timeout_is_error: True for red timeout message; False for black.
"""
if self.goofy.link_manager:
self.goofy.link_manager.StartCountdown(
message, timeout_secs, timeout_message,
'red' if timeout_is_error else 'black')
def SuspendDUTMonitoring(self, interval_sec):
"""Suspends monitoring of DUT connection.
For some tests, DUT is expected to go offline for a short period without
rebooting. In this case, we don't want the presenter to reload the UI;
otherwise, we lose the UI of the current running tests. By suspending
monitoring, the link manager on the presenter side knows to ignore
connection failure for a given amount of time.
Args:
interval_sec: Number of seconds to suspend.
"""
if self.goofy.link_manager:
self.goofy.link_manager.SuspendMonitoring(interval_sec)
def ResumeDUTMonitoring(self):
"""Immediately resume suspended monitoring of DUT connection."""
if self.goofy.link_manager:
self.goofy.link_manager.ResumeMonitoring()
def _GetTests(self):
"""Helper method to get a list of all tests and their states."""
paths_to_run = set([t.path for t in self.goofy.tests_to_run])
ret = []
states = self.goofy.state_instance.get_test_states()
for t in self.goofy.test_list.walk(in_order=True):
test_state = states.get(t.path)
ret.append(dict(path=t.path,
parent=(t.subtests != []),
pending=t.path in paths_to_run,
**test_state.__dict__))
return ret
def IsReadyForUIConnection(self):
"""Checks whether the Goofy backend is ready for UI connection.
Returns:
A boolean indicating whether the Goofy backend is ready for UI connection.
"""
return self.goofy.ready_for_ui_connection
def GetTests(self, timeout_secs=DEFAULT_GOOFY_RPC_TIMEOUT_SECS):
"""Returns a list of all tests and their states.
Args:
timeout_secs: The duration in seconds after which to abort the call.
"""
return self._InRunQueue(self._GetTests, timeout_secs=timeout_secs)
def GetTestLists(self):
"""Returns available test lists.
Returns:
An array of test lists, each a dict containing:
id: An identifier for the test list (empty for the default test list).
name: A human-readable name of the test list.
enabled: Whether this is the current-enabled test list.
"""
ret = []
for k, v in self.goofy.test_lists.iteritems():
ret.append(
dict(id=k, name=v.label_en,
enabled=(k == self.goofy.test_list.test_list_id)))
# Sort by name.
ret.sort(key=lambda x: x['name'].lower())
return ret
def GetGoofyStatus(self):
"""Returns a dictionary containing Goofy status information.
Returns:
A dict with the following elements:
uuid: A UUID identifying the current goofy run.
test_list_id: The active test_list ID.
status: The current status of Goofy.
"""
return {'uuid': self.goofy.uuid,
'test_list_id': (
self.goofy.test_list.test_list_id if self.goofy.test_list
else None),
'run_id': self.goofy.run_id,
'status': self.goofy.status}
def GetActiveRunID(self):
"""Gets the id of the current active test run."""
return self.goofy.run_id
def GetTestRunStatus(self, run_id,
timeout_secs=DEFAULT_GOOFY_RPC_TIMEOUT_SECS):
"""Returns the status of a given test run.
The given run id must match the last run id stored in Goofy to get the
status.
Args:
run_id: The id of a test run or None to get current test run status in
Goofy.
timeout_secs: The duration in seconds after which to abort the call.
Returns:
A dict with the following elements:
status: The status of the given run of factory tests.
- UNINITIALIZED: No run has been scheduled yet.
- STARTING: Goofy just went through a reboot and the latest test run
state has not been restored.
- NOT_ACTIVE_RUN: If the given run is not the current active one.
- RUNNING: Goofy is running the scheduled tests of the given run.
- FINISHED: Goofy has finished running the scheduled tests of the
given run.
If status is RUNNING or FINISHED, the following elements are also
included:
run_id: The id of the current active run.
scheduled_tests: A list of factory tests that were scheduled for
the active run and their status.
"""
def Target(run_id):
if not run_id:
run_id = self.goofy.run_id
ret_val = {}
if self.goofy.run_id is None:
if self.goofy.state_instance.get_shared_data('run_id', optional=True):
# A run ID is present in shared data but hasn't been restored.
ret_val['status'] = RunState.STARTING
else:
# No test run has ever been scheduled.
ret_val['status'] = RunState.UNINITIALIZED
elif run_id != self.goofy.run_id:
ret_val['status'] = RunState.NOT_ACTIVE_RUN
else:
tests = self._GetTests()
scheduled_tests_status = [t for t in tests if t['path'] in
self.goofy.scheduled_run_tests]
ret_val['run_id'] = self.goofy.run_id,
ret_val['scheduled_tests'] = scheduled_tests_status
if (self.goofy.tests_to_run or
any(t['status'] == factory.TestState.ACTIVE
for t in scheduled_tests_status)):
ret_val['status'] = RunState.RUNNING
else:
ret_val['status'] = RunState.FINISHED
return ret_val
return self._InRunQueue(lambda: Target(run_id), timeout_secs=timeout_secs)
def SwitchTestList(self, test_list_id, automation_mode='none'):
"""Switches test lists.
Args:
test_list_id: The test list ID.
automation_mode: The automation mode to enable. Valid values are:
('none', 'partial', 'full').
Raises:
TestListError: The test list does not exist.
"""
# Have goofy throw an error if the test list ID is invalid.
self.goofy.GetTestList(test_list_id)
SetActiveTestList(test_list_id)
if sys_utils.InChroot():
raise GoofyRPCException(
'Cannot switch test in chroot; please manually restart Goofy')
else:
# Restart Goofy and clear state.
process_utils.Spawn(
['nohup ' +
os.path.join(paths.FACTORY_PATH, 'bin', 'factory_restart') +
' --automation-mode %s -a &' % automation_mode],
shell=True, check_call=True)
# Wait for a while. This process should be killed long before
# 60 seconds have passed.
time.sleep(60)
# This should never be reached, but not much we can do but
# complain to the caller.
raise GoofyRPCException('Factory did not restart as expected')
def DeviceTakeScreenshot(self, output_file=None):
"""Takes screenshots of all the connected ports on the device.
Args:
output_file: The output file path to store the captured image file.
If not given, screenshots are saved to:
/var/log/screenshot_<TIME>-<PORT>.png
If a file path is given, screenshots are saved to:
<file path base>-<PORT>.<file path extension>
"""
if not output_file:
output_filename = ('/var/log/screenshot_%s-%%s.png' %
time.strftime('%Y%m%d-%H%M%S'))
else:
output_filename = '%s-%%s%s' % os.path.splitext(output_file)
display = device_utils.CreateDUTInterface().display
for port_id, port_info in display.GetPortInfo().iteritems():
if port_info.connected:
display.CaptureFramebuffer(port_id).save(output_filename % port_id)
def CallExtension(self, name, timeout=DEFAULT_GOOFY_RPC_TIMEOUT_SECS,
**kwargs):
"""Invokes a RPC call to Factory Test Chrome Extension.
Blocks until a return value is retrieved or if timeout is reached.
Args:
name: The name of extension RPC function to execute.
timeout: Seconds to wait before RPC timeout.
kwargs: Arguments to pass to the extension; they will be
available in an "args" dict within the execution context.
Returns:
An object representing RPC call return value.
Raises:
type_utils.TimeoutError: if no response until timeout reached.
"""
# To support timeout (and to avoid race condition), we need a dedicated
# event client.
rpc_id = str(uuid.uuid4())
rpc_event = Event(Event.Type.EXTENSION_RPC, name=name, is_response=False,
rpc_id=rpc_id, args=kwargs)
with EventClient() as event_client:
result = event_client.request_response(
rpc_event,
lambda e: (e.type == rpc_event.type and e.rpc_id == rpc_id and
e.is_response),
timeout)
if result is None:
raise type_utils.TimeoutError('Failed calling Extension RPC <%r>', name)
return result.args
def DeviceGetDisplayInfo(self, timeout=DEFAULT_GOOFY_RPC_TIMEOUT_SECS):
"""Returns display information on the device (by calling extension RPC).
Args:
timeout: Seconds to wait before RPC timeout.
Returns:
A list of objects for current display. See Chrome Extension API
chrome.system.display for the details.
Raises:
type_utils.TimeoutError: if no response until timeout.
"""
return self.CallExtension('GetDisplayInfo', timeout=timeout)
def DiagnosisToolRpc(self, *args):
"""Receives a rpc request for diagnosis tool."""
return getattr(self.diagnosis_tool_rpc, args[0])(*args[1:])
def DeviceCreateWindow(self, left, top,
timeout=DEFAULT_GOOFY_RPC_TIMEOUT_SECS):
"""Creates a Chrome window on the device and returns its ID.
Args:
left: The offset in pixels from left.
top: The offset in pixels from top.
timeout: Seconds to wait before RPC timeout.
Returns:
The attributes of the created window. See Chrome Extension API
chrome.windows for the details.
Raises:
type_utils.TimeoutError: if no response until timeout.
"""
return self.CallExtension('CreateWindow', timeout=timeout,
left=left, top=top)
def DeviceUpdateWindow(self, window_id, update_info,
timeout=DEFAULT_GOOFY_RPC_TIMEOUT_SECS):
"""Updates a Chrome window on the device.
See Chrome Extension API chrome.windows for the details.
Args:
window_id: The ID of the window.
update_info: A dict of update info.
timeout: Seconds to wait before RPC timeout.
Raises:
type_utils.TimeoutError: if no response until timeout.
"""
self.CallExtension('UpdateWindow', timeout=timeout,
window_id=window_id, update_info=update_info)
def DeviceRemoveWindow(self, window_id,
timeout=DEFAULT_GOOFY_RPC_TIMEOUT_SECS):
"""Removes a Chrome window on the device.
See Chrome Extension API chrome.windows for the details.
Args:
window_id: The ID of the window.
timeout: Seconds to wait before RPC timeout.
Raises:
type_utils.TimeoutError: if no response until timeout.
"""
self.CallExtension('RemoveWindow', timeout=timeout, window_id=window_id)
def DeviceQueryTabs(self, window_id, timeout=DEFAULT_GOOFY_RPC_TIMEOUT_SECS):
"""Queries the tabs of the given window on the device.
See Chrome Extension API chrome.tabs for the details.
Args:
window_id: The ID of the window.
timeout: Seconds to wait before RPC timeout.
Returns:
A list of the tab info.
Raises:
type_utils.TimeoutError: if no response until timeout.
"""
return self.CallExtension('QueryTabs', timeout=timeout, window_id=window_id)
def DeviceUpdateTab(self, tab_id, update_info,
timeout=DEFAULT_GOOFY_RPC_TIMEOUT_SECS):
"""Updates the tab on the device.
See Chrome Extension API chrome.tabs for the details.
Args:
tab_id: The ID of the tab.
update_info: A dict of update info.
timeout: Seconds to wait before RPC timeout.
Raises:
type_utils.TimeoutError: if no response until timeout.
"""
self.CallExtension('UpdateTab', timeout=timeout,
tab_id=tab_id, update_info=update_info)
def UpdateStatus(self, all_pass):
"""Updates the color of tab in presenter.
Args:
all_pass: A boolean value. True if no failed test.
"""
if self.goofy.link_manager:
self.goofy.link_manager.UpdateStatus(all_pass)
def main():
parser = argparse.ArgumentParser(
description='Sends an RPC to Goofy.')
parser.add_argument(
'command',
help=('The command to run (as a Python expression), e.g.: '
"""RunTest('RunIn.Stress.BadBlocks')"""))
args = parser.parse_args()
goofy = factory.get_state_instance()
logging.basicConfig(level=logging.INFO)
if '(' not in args.command:
parser.error('Expected parentheses in command, e.g.: '
"""RunTest('RunIn.Stress.BadBlocks')""")
logging.info('Evaluating expression: %s', args.command)
ret = eval(args.command, {}, # pylint: disable=W0123
dict((x, getattr(goofy, x))
for x in GoofyRPC.__dict__.keys()
if not x.startswith('_')))
if ret is not None:
print(yaml.safe_dump(ret))
if __name__ == '__main__':
main()