blob: 6ffc5d40f73fe23cb0f2ff9a6cd59455d9b0b929 [file] [log] [blame]
// Copyright 2019 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.
package crash
import (
"context"
"io"
"io/ioutil"
"os"
"regexp"
"strconv"
"strings"
"time"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
"chromiumos/tast/local/syslog"
"chromiumos/tast/shutil"
"chromiumos/tast/testing"
)
const (
// SendRecordDir is the path to the directory containing send record files.
// A send record file represents an upload event of a crash report. Its content is
// serialized crash.SendRecord protocol buffers message, and its timestamp indicates
// when the upload was performed.
SendRecordDir = "/var/lib/crash_sender"
mockSendingPath = "/run/crash_reporter/mock-crash-sending"
)
// EnableMockSending tells crash_sender to not send crash reports to the server
// actually. If success is true, crash_sender always emulates successful uploads;
// otherwise it emulates failed uploads.
func EnableMockSending(success bool) error {
return enableMockSending(mockSendingPath, success)
}
func enableMockSending(path string, success bool) error {
var b []byte
if !success {
b = []byte{'1'}
}
if err := ioutil.WriteFile(path, b, 0644); err != nil {
return errors.Wrap(err, "failed to enable crash_sender mock")
}
return nil
}
// disableMockSending tells crash_sender to send crash reports to the server actually.
// Usually path should be mockSendingPath.
func disableMockSending(path string) error {
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return errors.Wrap(err, "failed to disable crash_sender mock")
}
return nil
}
// resetSendRecords clears send record files under path. Usually path should be SendRecordDir.
func resetSendRecords(dir string) error {
if err := os.RemoveAll(dir); err != nil {
return errors.Wrap(err, "failed to reset sent reports")
}
return nil
}
// ListSendRecords returns a list of send record files under SendRecordDir.
// A send record file represents an upload event of a crash report. Its content is
// serialized crash.SendRecord protocol buffers message, and its timestamp indicates
// when the upload was performed.
func ListSendRecords() ([]os.FileInfo, error) {
fis, err := ioutil.ReadDir(SendRecordDir)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, errors.Wrap(err, "failed to count send records")
}
var rs []os.FileInfo
for _, fi := range fis {
if fi.Mode().IsRegular() {
rs = append(rs, fi)
}
}
return rs, nil
}
// SendResult is the result of crash_sender sending a single crash dump entry.
type SendResult struct {
Schedule time.Time
Success bool
Data SendData
}
// SendData is the data of a single crash dump entry sent to the server by
// crash_sender.
type SendData struct {
MetadataPath string
PayloadPath string
PayloadKind string
Product string
Version string
Board string
HWClass string
Executable string
ImageType string
BootMode string
}
// RunSender runs crash_sender to process pending crash dumps and returns the send
// results by parsing its syslog output.
// crash_sender is run with --ignore_pause_file to ignore the pause file
// created by crash.SetUpCrashTest.
func RunSender(ctx context.Context) ([]*SendResult, error) {
return runSenderWithArgs(ctx, "--ignore_pause_file")
}
// RunSenderNoIgnorePauseFile is similar to RunSender but does not instruct crash_sender to
// ignore the pause file.
func RunSenderNoIgnorePauseFile(ctx context.Context) ([]*SendResult, error) {
return runSenderWithArgs(ctx)
}
func runSenderWithArgs(ctx context.Context, args ...string) ([]*SendResult, error) {
sr, err := syslog.NewReader(ctx, syslog.Program(syslog.CrashSender))
if err != nil {
return nil, err
}
defer sr.Close()
testing.ContextLog(ctx, "Running: crash_sender ", shutil.EscapeSlice(args))
cmd := testexec.CommandContext(ctx, "/sbin/crash_sender", args...)
// crash_sender does not output anything to stdout/stderr. For debugging,
// always proceed to syslog processing.
runErr := cmd.Run()
var es []*syslog.Entry
if err := testing.Poll(ctx, func(ctx context.Context) error {
for {
e, err := sr.Read()
if err == io.EOF {
return errors.New("crash_sender end message not seen yet")
}
if err != nil {
return testing.PollBreak(err)
}
// Log crash_sender syslog entries for debugging.
testing.ContextLog(ctx, "crash_sender: ", e.Content)
es = append(es, e)
// crash_sender runs its main function in minijail, so this message is
// printed by two processes. Catch the message from the parent process.
// Otherwise, when running crash_sender multiple times, tests can
// confuse two messages from one crash_sender run with those from
// two crash_sender runs.
if e.PID == cmd.Process.Pid && strings.Contains(e.Content, "crash_sender done.") {
return nil
}
}
}, &testing.PollOptions{Timeout: 10 * time.Second}); err != nil {
return nil, errors.Wrap(err, "failed to wait for crash_sender reports")
}
if runErr != nil {
return nil, errors.Wrap(runErr, "crash_sender failed (see logs for output)")
}
return parseLogs(es)
}
func parseLogs(es []*syslog.Entry) ([]*SendResult, error) {
// startReportLog appears at the beginning of each send result. We split the
// syslog entries using this string.
const startReportLog = "Evaluating crash report:"
var rs []*SendResult
for i := 0; i < len(es); i++ {
if !strings.HasPrefix(es[i].Content, startReportLog) {
continue
}
j := i + 1
for ; j < len(es); j++ {
if strings.HasPrefix(es[j].Content, startReportLog) {
break
}
}
r, err := parseLogsForResult(es[i:j])
if err != nil {
return nil, err
}
rs = append(rs, r)
i = j - 1
}
return rs, nil
}
var (
schedulePattern = regexp.MustCompile(`Scheduled to send in (\d+)s`)
payloadKindPattern = regexp.MustCompile(` \(([^)]+)\)$`)
)
func parseLogsForResult(es []*syslog.Entry) (*SendResult, error) {
var r SendResult
for _, e := range es {
if m := schedulePattern.FindStringSubmatch(e.Content); m != nil {
sec, err := strconv.Atoi(m[1])
if err != nil {
return nil, errors.Wrapf(err, "corrupted report line: %q", e.Content)
}
r.Schedule = e.Timestamp.Add(time.Duration(sec) * time.Second)
} else if strings.Contains(e.Content, "Mocking successful send") {
r.Success = true
} else if strings.HasPrefix(e.Content, " ") {
// This is a key-value pair.
kv := strings.SplitN(strings.TrimSpace(e.Content), ": ", 2)
if len(kv) != 2 {
return nil, errors.Errorf("corrupted report line: %q", e.Content)
}
k, v := kv[0], kv[1]
switch k {
case "Metadata":
m := payloadKindPattern.FindStringSubmatch(v)
if m == nil {
return nil, errors.Errorf("corrupted metadata line: %q", e.Content)
}
r.Data.MetadataPath = strings.TrimSuffix(v, m[0])
r.Data.PayloadKind = m[1]
case "Payload":
r.Data.PayloadPath = v
case "Product":
r.Data.Product = v
case "Version":
r.Data.Version = v
case "Board":
r.Data.Board = v
case "HWClass":
r.Data.HWClass = v
case "Exec name":
r.Data.Executable = v
case "Image type":
r.Data.ImageType = v
case "Boot mode":
r.Data.BootMode = v
}
}
}
return &r, nil
}