Commit 299b84f8 authored by Kevin Marshall's avatar Kevin Marshall Committed by Commit Bot

Fuchsia: Get kernel logs over SSH and and symbolize their stack traces.

Serial kernel logging is a deprecated feature of Zircon and should not
be used anymore. This uses a long-running SSH invocation of "dlog"
to pull the logs over the wire. It works equally well on QEMU and on
devices.

Created a stream multiplexer using "poll" to merge the kernel log and
application log into a single stream, which is passed to the symbolizing
filter.

Modified the symbolizer regex rules to allow kernel log timestamps.
Kernel log timestamps can be omitted from the output, but they seem
to be helpful at visually distinguishing kernel output from app output.


Bug: 838671
Change-Id: Icd45f902a49983b4e4ee262de474d69fbef56860
Reviewed-on: https://chromium-review.googlesource.com/1038423
Commit-Queue: Kevin Marshall <kmarshall@chromium.org>
Reviewed-by: default avatarFabrice de Gans-Riberi <fdegans@chromium.org>
Cr-Commit-Position: refs/heads/master@{#556170}
parent 5b0cb536
...@@ -40,8 +40,7 @@ def AddCommonArgs(arg_parser): ...@@ -40,8 +40,7 @@ def AddCommonArgs(arg_parser):
common_args.add_argument('--ssh-config', '-F', common_args.add_argument('--ssh-config', '-F',
help='The path to the SSH configuration used for ' help='The path to the SSH configuration used for '
'connecting to the target device.') 'connecting to the target device.')
common_args.add_argument('--no-system-logs', default=False, common_args.add_argument('--include-system-logs', default=True, type=bool,
action='store_true',
help='Do not show system log data.') help='Do not show system log data.')
common_args.add_argument('--verbose', '-v', default=False, common_args.add_argument('--verbose', '-v', default=False,
action='store_true', action='store_true',
...@@ -70,8 +69,7 @@ def GetDeploymentTargetForArgs(args): ...@@ -70,8 +69,7 @@ def GetDeploymentTargetForArgs(args):
command line arguments.""" command line arguments."""
if not args.device: if not args.device:
return QemuTarget(args.output_directory, args.target_cpu, return QemuTarget(args.output_directory, args.target_cpu)
not args.no_system_logs)
else: else:
return DeviceTarget(args.output_directory, args.target_cpu, return DeviceTarget(args.output_directory, args.target_cpu,
args.host, args.port, args.ssh_config) args.host, args.port, args.ssh_config)
...@@ -25,8 +25,9 @@ def main(): ...@@ -25,8 +25,9 @@ def main():
with GetDeploymentTargetForArgs(args) as target: with GetDeploymentTargetForArgs(args) as target:
target.Start() target.Start()
return RunPackage(args.output_directory, target, args.package, return RunPackage(
args.package_name, args.child_args, args.package_manifest) args.output_directory, target, args.package, args.package_name,
args.child_args, args.include_system_logs, args.package_manifest)
if __name__ == '__main__': if __name__ == '__main__':
......
...@@ -33,17 +33,15 @@ def _GetAvailableTcpPort(): ...@@ -33,17 +33,15 @@ def _GetAvailableTcpPort():
class QemuTarget(target.Target): class QemuTarget(target.Target):
def __init__(self, output_dir, target_cpu, system_logs, def __init__(self, output_dir, target_cpu,
ram_size_mb=2048): ram_size_mb=2048):
"""output_dir: The directory which will contain the files that are """output_dir: The directory which will contain the files that are
generated to support the QEMU deployment. generated to support the QEMU deployment.
target_cpu: The emulated target CPU architecture. target_cpu: The emulated target CPU architecture.
Can be 'x64' or 'arm64'. Can be 'x64' or 'arm64'."""
system_logs: If true, emit system log data."""
super(QemuTarget, self).__init__(output_dir, target_cpu) super(QemuTarget, self).__init__(output_dir, target_cpu)
self._qemu_process = None self._qemu_process = None
self._ram_size_mb = ram_size_mb self._ram_size_mb = ram_size_mb
self._system_logs = system_logs
def __enter__(self): def __enter__(self):
return self return self
...@@ -63,10 +61,6 @@ class QemuTarget(target.Target): ...@@ -63,10 +61,6 @@ class QemuTarget(target.Target):
# noisy ANSI spew from the user's terminal emulator. # noisy ANSI spew from the user's terminal emulator.
kernel_args.append('TERM=dumb') kernel_args.append('TERM=dumb')
# Enable logging to the serial port.
# TODO(sergeyu): Use loglistener instead of serial port to get zircon logs.
kernel_args.append('kernel.serial=legacy')
qemu_command = [qemu_path, qemu_command = [qemu_path,
'-m', str(self._ram_size_mb), '-m', str(self._ram_size_mb),
'-nographic', '-nographic',
...@@ -139,12 +133,9 @@ class QemuTarget(target.Target): ...@@ -139,12 +133,9 @@ class QemuTarget(target.Target):
logging.debug('Launching QEMU.') logging.debug('Launching QEMU.')
logging.debug(' '.join(qemu_command)) logging.debug(' '.join(qemu_command))
stdio_flags = {'stdin': open(os.devnull)} stdio_flags = {'stdin': open(os.devnull),
if not self._system_logs: 'stdout': open(os.devnull),
# Output the Fuchsia debug log. 'stderr': open(os.devnull)}
stdio_flags['stdout'] = open(os.devnull)
stdio_flags['stderr'] = open(os.devnull)
self._qemu_process = subprocess.Popen(qemu_command, **stdio_flags) self._qemu_process = subprocess.Popen(qemu_command, **stdio_flags)
self._WaitUntilReady(); self._WaitUntilReady();
......
...@@ -8,11 +8,14 @@ executable on a Target.""" ...@@ -8,11 +8,14 @@ executable on a Target."""
import common import common
import json import json
import logging import logging
import multiprocessing
import os import os
import shutil import shutil
import subprocess import subprocess
import tempfile import tempfile
import threading
import uuid import uuid
import select
from symbolizer import FilterStream from symbolizer import FilterStream
...@@ -20,8 +23,65 @@ FAR = os.path.join(common.SDK_ROOT, 'tools', 'far') ...@@ -20,8 +23,65 @@ FAR = os.path.join(common.SDK_ROOT, 'tools', 'far')
PM = os.path.join(common.SDK_ROOT, 'tools', 'pm') PM = os.path.join(common.SDK_ROOT, 'tools', 'pm')
def _AttachKernelLogReader(target):
"""Attaches a kernel log reader as a long-running SSH task."""
logging.info('Attaching kernel logger.')
return target.RunCommandPiped(['dlog', '-f'], stdin=open(os.devnull, 'r'),
stdout=subprocess.PIPE)
def _ReadMergedLines(streams):
"""Creates a generator which merges the buffered line output from |streams|.
The generator is terminated when the primary (first in sequence) stream
signals EOF. Absolute output ordering is not guaranteed."""
assert len(streams) > 0
poll = select.poll()
streams_by_fd = {}
primary_fd = streams[0].fileno()
for s in streams:
poll.register(s.fileno(), select.POLLIN)
streams_by_fd[s.fileno()] = s
try:
while primary_fd != None:
events = poll.poll(1)
for fileno, event in events:
if event & select.POLLIN:
yield streams_by_fd[fileno].readline()
elif event & select.POLLHUP:
poll.unregister(fileno)
del streams_by_fd[fileno]
if fileno == primary_fd:
primary_fd = None
finally:
for fd_to_cleanup, _ in streams_by_fd.iteritems():
poll.unregister(fd_to_cleanup)
def DrainStreamToStdout(stream, quit_event):
"""Outputs the contents of |stream| until |quit_event| is set."""
poll = select.poll()
poll.register(stream.fileno(), select.POLLIN)
try:
while not quit_event.is_set():
events = poll.poll(1)
for fileno, event in events:
if event & select.POLLIN:
print stream.readline().rstrip()
elif event & select.POLLHUP:
break
finally:
poll.unregister(stream.fileno())
def RunPackage(output_dir, target, package_path, package_name, run_args, def RunPackage(output_dir, target, package_path, package_name, run_args,
symbolizer_config=None): system_logging, symbolizer_config=None):
"""Copies the Fuchsia package at |package_path| to the target, """Copies the Fuchsia package at |package_path| to the target,
executes it with |run_args|, and symbolizes its output. executes it with |run_args|, and symbolizes its output.
...@@ -30,6 +90,7 @@ def RunPackage(output_dir, target, package_path, package_name, run_args, ...@@ -30,6 +90,7 @@ def RunPackage(output_dir, target, package_path, package_name, run_args,
package_path: The path to the .far package file. package_path: The path to the .far package file.
package_name: The name of app specified by package metadata. package_name: The name of app specified by package metadata.
run_args: The arguments which will be passed to the Fuchsia process. run_args: The arguments which will be passed to the Fuchsia process.
system_logging: If true, connects a system log reader to the target.
symbolizer_config: A newline delimited list of source files contained symbolizer_config: A newline delimited list of source files contained
in the package. Omitting this parameter will disable in the package. Omitting this parameter will disable
symbolization. symbolization.
...@@ -37,11 +98,25 @@ def RunPackage(output_dir, target, package_path, package_name, run_args, ...@@ -37,11 +98,25 @@ def RunPackage(output_dir, target, package_path, package_name, run_args,
Returns the exit code of the remote package process.""" Returns the exit code of the remote package process."""
logging.info('Copying package to target.') system_logger = _AttachKernelLogReader(target) if system_logging else None
install_path = os.path.join('/data', os.path.basename(package_path)) package_copied = False
target.PutFile(package_path, install_path) log_output_thread = None
try: try:
log_output_thread = None
if system_logger:
# Spin up a thread to asynchronously dump the system log to stdout
# for easier diagnoses of early, pre-execution failures.
log_output_quit_event = multiprocessing.Event()
log_output_thread = threading.Thread(
target=lambda: DrainStreamToStdout(system_logger.stdout,
log_output_quit_event))
log_output_thread.start()
logging.info('Copying package to target.')
install_path = os.path.join('/data', os.path.basename(package_path))
target.PutFile(package_path, install_path)
package_copied = True
logging.info('Installing package.') logging.info('Installing package.')
p = target.RunCommandPiped(['pm', 'install', install_path], p = target.RunCommandPiped(['pm', 'install', install_path],
stderr=subprocess.PIPE) stderr=subprocess.PIPE)
...@@ -51,7 +126,11 @@ def RunPackage(output_dir, target, package_path, package_name, run_args, ...@@ -51,7 +126,11 @@ def RunPackage(output_dir, target, package_path, package_name, run_args,
if p.returncode != 0: if p.returncode != 0:
# Don't error out if the package already exists on the device. # Don't error out if the package already exists on the device.
if len(output) != 1 or 'ErrAlreadyExists' not in output[0]: if len(output) != 1 or 'ErrAlreadyExists' not in output[0]:
raise Exception('Error when installing package: %s' % '\n'.join(output)) raise Exception('Error while installing: %s' % '\n'.join(output))
if log_output_thread:
log_output_quit_event.set()
log_output_thread.join()
logging.info('Running application.') logging.info('Running application.')
command = ['run', package_name] + run_args command = ['run', package_name] + run_args
...@@ -60,16 +139,21 @@ def RunPackage(output_dir, target, package_path, package_name, run_args, ...@@ -60,16 +139,21 @@ def RunPackage(output_dir, target, package_path, package_name, run_args,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT) stderr=subprocess.STDOUT)
if system_logger:
task_output = _ReadMergedLines([process.stdout, system_logger.stdout])
else:
task_output = process.stdout
if symbolizer_config: if symbolizer_config:
# Decorate the process output stream with the symbolizer. # Decorate the process output stream with the symbolizer.
output = FilterStream(process.stdout, package_name, output = FilterStream(task_output, package_name, symbolizer_config,
symbolizer_config, output_dir) output_dir)
else: else:
logging.warn('Symbolization is DISABLED.') logging.warn('Symbolization is DISABLED.')
output = process.stdout output = process.stdout
for next_line in output: for next_line in output:
print next_line.strip() print next_line.rstrip()
process.wait() process.wait()
if process.returncode == 0: if process.returncode == 0:
...@@ -81,8 +165,13 @@ def RunPackage(output_dir, target, package_path, package_name, run_args, ...@@ -81,8 +165,13 @@ def RunPackage(output_dir, target, package_path, package_name, run_args,
process.returncode) process.returncode)
finally: finally:
logging.info('Removing package source from device.') if system_logger:
target.RunCommand(['rm', install_path]) print 'Terminating kernel log reader.'
system_logger.kill()
if package_copied:
logging.info('Removing package source from device.')
target.RunCommand(['rm', install_path])
return process.returncode return process.returncode
...@@ -8,7 +8,7 @@ import re ...@@ -8,7 +8,7 @@ import re
import subprocess import subprocess
# Matches the coarse syntax of a backtrace entry. # Matches the coarse syntax of a backtrace entry.
_BACKTRACE_PREFIX_RE = re.compile(r'bt#(?P<frame_id>\d+): ') _BACKTRACE_PREFIX_RE = re.compile(r'(\[[0-9.]+\] )?bt#(?P<frame_id>\d+): ')
# Matches the specific fields of a backtrace entry. # Matches the specific fields of a backtrace entry.
# Back-trace line matcher/parser assumes that 'pc' is always present, and # Back-trace line matcher/parser assumes that 'pc' is always present, and
......
...@@ -98,9 +98,9 @@ def main(): ...@@ -98,9 +98,9 @@ def main():
if args.enable_test_server: if args.enable_test_server:
test_server = SetupTestServer(target, test_concurrency) test_server = SetupTestServer(target, test_concurrency)
returncode = RunPackage(args.output_directory, target, args.package, returncode = RunPackage(
args.package_name, child_args, args.output_directory, target, args.package, args.package_name,
args.package_manifest) child_args, args.include_system_logs, args.package_manifest)
if forwarder: if forwarder:
forwarder.terminate() forwarder.terminate()
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment