blob: 99d9e443517b5401f058842b264c3ca0ecca6f35 [file] [log] [blame]
// Copyright 2019 The Fuchsia 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 generator
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"sync"
"go.fuchsia.dev/fuchsia/tools/debug/breakpad"
"go.fuchsia.dev/fuchsia/tools/debug/elflib"
)
// The default module name for modules that don't have a soname, e.g., executables and
// loadable modules. This allows us to use the same module name at runtime as sonames are
// the only names that are guaranteed to be available at build and run times. This value
// must be kept in sync with what Crashpad uses at run time for symbol resolution to work
// properly.
const defaultModuleName = "<_>"
// The module OS used to overwrite existing OS values in generated symbol files, even if
// they're already set to something else.
const replacementModuleOS = "Fuchsia"
// Generate generates breakpad symbol data for each of the input elflib.BinaryFileRefs.
// Returns the path to a directory containing the generated files, or the empty string if
// an error occurred.
func Generate(bfrs []elflib.BinaryFileRef, dumpSymsPath string) (path string, err error) {
outc := make(chan string)
errc := make(chan error)
defer close(outc)
defer close(errc)
g := &generator{
dumpSymsPath: dumpSymsPath,
visited: make(map[string]struct{}),
visitedMutex: &sync.Mutex{},
}
jobs := make(chan elflib.BinaryFileRef)
go g.run(jobs, outc, errc)
for _, bfr := range bfrs {
jobs <- bfr
}
close(jobs)
select {
case err = <-errc:
return "", err
case path = <-outc:
return path, nil
}
}
// Generator is a helper class for executing Breakpad's dump_syms tool.
//
// The run method is meant to be executed as a go-routine. It will manage its own working
// directory, and publish the path to that directory only on success.
//
// The run method is threadsafe, and will skip files that have already been processed.
type generator struct {
// The path to the Breakpad dump_syms executable.
dumpSymsPath string
// Filepaths that have already been processed by this generator.
visited map[string]struct{}
visitedMutex *sync.Mutex
}
// Run executes this generator on the given channel of elflib.BinarFileRefs.
//
// A temp directory is created to store generated files. On success, the directory is
// emitted on out. On the first encountered error, the generator will emit the error on
// errs, delete the output directory, and exit.
func (g *generator) run(in <-chan elflib.BinaryFileRef, out chan<- string, errs chan<- error) {
outdir, err := ioutil.TempDir("", "breakpad")
if err != nil {
errs <- err
return
}
if err := g.generate(in, outdir); err != nil {
errs <- err
os.RemoveAll(outdir)
return
}
out <- outdir
}
func (g *generator) generate(in <-chan elflib.BinaryFileRef, outdir string) error {
for bfr := range in {
if !g.markVisited(bfr.Filepath) {
continue
}
sf, err := g.genFromBinaryFileRef(bfr)
if err != nil {
return err
}
fd, err := os.Create(filepath.Join(outdir, fmt.Sprintf("%s.sym", bfr.BuildID)))
if err != nil {
return err
}
defer fd.Close()
if _, err := sf.WriteTo(fd); err != nil {
return err
}
}
return nil
}
func (g *generator) genFromBinaryFileRef(bfr elflib.BinaryFileRef) (*breakpad.SymbolFile, error) {
log.Printf("generating symbols for %q", bfr.Filepath)
sf, err := g.genSymbolFile(bfr)
if err != nil {
return nil, err
}
sf.ModuleSection.OS = replacementModuleOS
sf.ModuleSection.ModuleName = defaultModuleName
soname, err := g.readSoName(bfr.Filepath)
if err == nil && soname != "" {
sf.ModuleSection.ModuleName = soname
}
return sf, nil
}
func (g *generator) genSymbolFile(bfr elflib.BinaryFileRef) (*breakpad.SymbolFile, error) {
var stdout bytes.Buffer
cmd := exec.Cmd{
Path: g.dumpSymsPath,
Args: []string{g.dumpSymsPath, bfr.Filepath},
Stdout: &stdout,
}
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("command failed %v: %w", cmd.Args, err)
}
return breakpad.ParseSymbolFile(&stdout)
}
func (g *generator) readSoName(path string) (string, error) {
fd, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("open failed %q: %w", path, err)
}
defer fd.Close()
return elflib.GetSoName(path, fd)
}
// Marks that path has been visited and returs true iff this generator has not alread
// visited path. Otherwise returns false.
func (g *generator) markVisited(path string) (succeeded bool) {
g.visitedMutex.Lock()
defer g.visitedMutex.Unlock()
if _, ok := g.visited[path]; ok {
return false
}
g.visited[path] = struct{}{}
return true
}