blob: 700d66a1e9d237c0c3a3ccab1b97aec8c77bbb28 [file] [log] [blame]
// Copyright 2018 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package symbolize provides support for symbolizing crashes.
package symbolize
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"go.chromium.org/tast/core/cmd/tast/internal/symbolize/breakpad"
"go.chromium.org/tast/core/internal/logging"
)
// Config contains parameters used when symbolizing crash files.
type Config struct {
// SymbolDir contains a directory used to store symbol files.
SymbolDir string
// BuilderPath, for example, "betty-release/R91-13892.0.0", identifies the location
// of debug symbols in gs://chromeos-image-archive.
BuilderPath string
// BuildRoot contains build root (e.g. "/build/lumpy") that produced the system image.
// If empty, inferred by extracting the board name from the minidump.
// The build root is only used if a builder path can't be extracted from the minidump.
BuildRoot string
}
// SymbolizeCrash attempts to symbolize a crash file.
// path can contain either raw minidump data or a Chrome crash report.
// The (possibly-unsuccessfully-)symbolized data is written to w.
func SymbolizeCrash(ctx context.Context, path string, w io.Writer, cfg Config) error { // NOLINT
if cfg.SymbolDir == "" {
return errors.New("symbol directory not supplied")
}
dumpPath, err := getMinidumpPath(ctx, path)
if err != nil {
return fmt.Errorf("failed to get minidump: %v", err)
}
// If we created a temporary file, delete it later.
if dumpPath != path {
defer os.Remove(dumpPath)
}
ri, err := getMinidumpReleaseInfo(ctx, dumpPath)
if err != nil {
return fmt.Errorf("failed to get release info from %v: %v", dumpPath, err)
}
if !ri.hasBuildInfo() && cfg.BuildRoot == "" && cfg.BuilderPath == "" {
return errors.New("minidump does not contain release info, please supply --builderpath or --buildroot parameter to fix this error")
}
logging.Debugf(ctx, "Got board %q and builder path %q from minidump", ri.board, ri.builderPath)
if cfg.BuildRoot == "" {
cfg.BuildRoot = filepath.Join("/build", ri.board)
}
if cfg.BuilderPath == "" {
cfg.BuilderPath = ri.builderPath
}
logging.Debugf(ctx, "Walking %v with symbol dir %v", dumpPath, cfg.SymbolDir)
b := bytes.Buffer{}
missing, err := breakpad.WalkMinidump(dumpPath, cfg.SymbolDir, &b)
if err != nil {
return fmt.Errorf("failed to walk %v: %v", dumpPath, err)
}
created := 0
if len(missing) > 0 {
if cfg.BuilderPath != "" {
url := breakpad.GetSymbolsURL(cfg.BuilderPath)
logging.Debugf(ctx, "Extracting %v symbol file(s) from %v", len(missing), url)
if created, err = breakpad.DownloadSymbols(url, cfg.SymbolDir, missing); err != nil {
// Keep going so we can print what we have.
logging.Infof(ctx, "Failed to get symbols from %v: %v", url, err)
}
if ri.lacrosVersion != "" {
lacrosURL := breakpad.GetLacrosSymbolsURL(ri.lacrosVersion)
logging.Debugf(ctx, "Extracting Lacros symbols from %v", lacrosURL)
err := breakpad.DownloadLacrosSymbols(lacrosURL, cfg.SymbolDir)
if err != nil {
return fmt.Errorf("cannot obtain Lacros symbols: %v", err)
}
// DownloadLacrosSymbols creates exactly one file.
created++
}
} else {
logging.Debugf(ctx, "Generating %v symbol file(s) from %v", len(missing), cfg.BuildRoot)
created = createSymbolFiles(ctx, &cfg, missing)
}
}
// If we didn't write any new symbol files (possibly because there were none missing),
// we're done -- nothing will change if we walk the minidump again.
if created == 0 {
_, err = io.Copy(w, &b)
return err
}
// Otherwise, walk the minidump again.
logging.Debugf(ctx, "Walking %v again with %v new symbol file(s)", dumpPath, created)
if _, err = breakpad.WalkMinidump(dumpPath, cfg.SymbolDir, w); err != nil {
return fmt.Errorf("failed to re-walk %v: %v", dumpPath, err)
}
return nil
}
// getMinidumpPath returns the path to a file containing minidump data from path.
// If path contains raw minidump data, it will be returned directly.
// If path contains a Chrome crash report, its minidump data will be written to a temporary file.
func getMinidumpPath(ctx context.Context, path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
// If this is a regular minidump file, we're done.
if isDump, err := breakpad.IsMinidump(f); err != nil {
return "", err
} else if isDump {
logging.Debugf(ctx, "Using minidump file %v", path)
return path, nil
}
// Otherwise, check if this is a Chrome crash report.
if _, err = f.Seek(0, 0); err != nil {
return "", err
}
var dumpOffset, dumpLen int
if _, dumpOffset, dumpLen, err = breakpad.ReadCrashReport(f); err != nil {
return "", err
}
// Copy the minidump data to a temp file.
if _, err = f.Seek(int64(dumpOffset), 0); err != nil {
return "", err
}
tf, err := ioutil.TempFile("", "tast_"+filepath.Base(path)+".")
if err != nil {
return "", err
}
defer tf.Close()
logging.Debugf(ctx, "Writing minidump data from %v to %v", path, tf.Name())
if _, err = io.CopyN(tf, f, int64(dumpLen)); err != nil {
os.Remove(tf.Name())
return "", err
}
return tf.Name(), nil
}
// getMinidumpReleaseInfo returns release information contained in the minidump file at path.
func getMinidumpReleaseInfo(ctx context.Context, path string) (*releaseInfo, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
data, err := breakpad.GetMinidumpReleaseInfo(f)
if err != nil {
return nil, err
}
// We do not expect minidumps with both /etc/lsb-release and Crashpad
// annotations, but will log both if given such a file.
found := false
if data.EtcLsbRelease != "" {
found = true
logging.Debug(ctx, "Found /etc/lsb-release.")
}
if data.CrashpadAnnotations != nil {
found = true
// Crashpad annotation may or may not contains board and builder path, so print them.
logging.Debugf(ctx, "Found Crashpad annotations: %v", data.CrashpadAnnotations)
}
if !found {
logging.Debug(ctx, "Minidump does not contain /etc/lsb-release or Crashpad annotations.")
}
return getReleaseInfo(data)
}