| // 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 breakpad |
| |
| import ( |
| "archive/tar" |
| "bufio" |
| "errors" |
| "fmt" |
| "io" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "strings" |
| ) |
| |
| const ( |
| imageArchiveBaseURL = "gs://chromeos-image-archive" // contains build artifacts |
| imageArchiveFilename = "debug_breakpad.tar.xz" // filename within builder path |
| lacrosSymbolArchiveBaseURL = "gs://chrome-unsigned/desktop-5c0tCh" // gs:// location with Lacros symbols |
| lacrosSymbolArchivePath = "lacros64/lacros_debug.zip" // path to Lacros symbols archive |
| ) |
| |
| // symbol dir in .tar.xz can have a prefix |
| var imageArchiveTarPrefixes = []string{"debug/breakpad/", "./"} |
| |
| // moduleRegex extracts module ID from the output of dump_syms -i. |
| var moduleRegexp = regexp.MustCompile(`MODULE \S+ \S+ ([0-9A-F]+) (\S+)\.debug`) |
| |
| // GetSymbolsURL returns the Cloud Storage URL of the .tar.xz file containing Breakpad |
| // debug symbols for builderPath (e.g. "cave-release/R65-10286.0.0"). |
| func GetSymbolsURL(builderPath string) string { |
| return fmt.Sprintf("%s/%s/%s", imageArchiveBaseURL, builderPath, imageArchiveFilename) |
| } |
| |
| // GetLacrosSymbolsURL returns the Cloud Storage URL of the file containing |
| // ELF debug symbols of Lacros Chrome for a particular version, for example, |
| // "95.0.4637.0". |
| func GetLacrosSymbolsURL(version string) string { |
| return fmt.Sprintf("%s/%s/%s", lacrosSymbolArchiveBaseURL, version, lacrosSymbolArchivePath) |
| } |
| |
| // DownloadSymbols downloads url (see GetSymbolsURL) and extracts the symbol files specified |
| // in files to destDir. The number of files that were created is returned. |
| func DownloadSymbols(url, destDir string, files SymbolFileMap) (created int, err error) { |
| // Create a set of relative symbol file paths. |
| wanted := make(map[string]struct{}, len(files)) |
| for p, id := range files { |
| wanted[GetSymbolFilePath("", filepath.Base(p), id)] = struct{}{} |
| } |
| |
| as, err := newArchiveStreamer(url) |
| if err != nil { |
| return 0, err |
| } |
| defer as.close() |
| |
| tr := tar.NewReader(as.out) |
| for { |
| hdr, err := tr.Next() |
| if err == io.EOF { |
| break |
| } else if err != nil { |
| return created, err |
| } |
| |
| // Strip off the weird leading directories used in archive files and check if this |
| // is one of the files we're looking for. |
| p, err := stripAnyPrefix(hdr.Name, imageArchiveTarPrefixes) |
| if err != nil { |
| return 0, err |
| } |
| if _, ok := wanted[p]; !ok || hdr.Typeflag != tar.TypeReg { |
| continue |
| } |
| |
| // tar.Reader functions as an io.Reader and returns data from the current entry |
| // until Next is called. |
| if err := writeSymbolFile(filepath.Join(destDir, p), tr); err != nil { |
| return created, err |
| } |
| |
| created++ |
| delete(wanted, p) |
| if len(wanted) == 0 { |
| break |
| } |
| } |
| |
| return created, nil |
| } |
| |
| func stripAnyPrefix(s string, prefixes []string) (string, error) { |
| for _, prefix := range prefixes { |
| if strings.HasPrefix(s, prefix) { |
| return s[len(prefix):], nil |
| } |
| } |
| return "", fmt.Errorf("could not strip prefix from %s", s) |
| } |
| |
| // DownloadLacrosSymbols downloads the specified url and extract symbols to |
| // the destDir. On success, exactly one file is created. |
| func DownloadLacrosSymbols(url, destDir string) error { |
| if err := os.MkdirAll(destDir, 0755); err != nil { |
| return err |
| } |
| |
| // Download lacros_debug.zip. |
| lacrosDebugZip := filepath.Join(destDir, "lacros_debug.zip") |
| defer os.Remove(lacrosDebugZip) |
| if err := exec.Command("gsutil", "cp", url, destDir).Run(); err != nil { |
| return err |
| } |
| |
| // Unzip it. |
| chromeDebug := filepath.Join(destDir, "chrome.debug") |
| defer os.Remove(chromeDebug) |
| if err := exec.Command("unzip", "-d", destDir, lacrosDebugZip).Run(); err != nil { |
| return err |
| } |
| |
| // Run dump_syms -i to determine the module ID. It is needed for to build |
| // directory structure for minidump_stackwalk. |
| moduleStdout, err := exec.Command("dump_syms", "-i", chromeDebug).Output() |
| if err != nil { |
| return err |
| } |
| sc := bufio.NewScanner(strings.NewReader(string(moduleStdout))) |
| var module string |
| for sc.Scan() { |
| if m := moduleRegexp.FindStringSubmatch(sc.Text()); m != nil { |
| module = m[1] |
| break |
| } |
| } |
| if module == "" { |
| return errors.New("could not determine the module ID") |
| } |
| |
| moduleDir := filepath.Join(destDir, "chrome", module) |
| if err := os.MkdirAll(moduleDir, 0755); err != nil { |
| return err |
| } |
| |
| // Convert ELF to Breakpad format with dump_syms. |
| chromeSym := filepath.Join(moduleDir, "chrome.sym") |
| if _, err := os.Stat(chromeSym); os.IsNotExist(err) { |
| dumpSyms := exec.Command("dump_syms", chromeDebug) |
| symFile, err := os.Create(chromeSym) |
| if err != nil { |
| return err |
| } |
| defer symFile.Close() |
| dumpSyms.Stdout = symFile |
| if err := dumpSyms.Run(); err != nil { |
| return err |
| } |
| } else if err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| // writeSymbolFile creates a new file (including parent directory) at p |
| // and copies data from r into it until io.EOF is reached. |
| func writeSymbolFile(p string, r io.Reader) error { |
| if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil { |
| return err |
| } |
| |
| f, err := os.Create(p) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| |
| _, err = io.Copy(f, r) |
| return err |
| } |
| |
| // archiveStreamer uses gsutil to read a file and xz to decompress its contents. |
| type archiveStreamer struct { |
| out io.Reader // provides uncompressed data |
| gs, xz *exec.Cmd // gsutil and xz commands |
| } |
| |
| // newArchiveStreamer starts and returns a new archiveStreamer that decompresses |
| // the xz-compressed file at src. |
| func newArchiveStreamer(src string) (*archiveStreamer, error) { |
| // TODO(derat): If google-cloud-go is packaged at some point, consider trying to |
| // use it instead of gsutil. The Go standard library doesn't support xz, though, so |
| // some of this will need to happen out of process regardless. |
| as := archiveStreamer{ |
| gs: exec.Command("gsutil", "cp", src, "/dev/stdout"), |
| xz: exec.Command("xz", "-d"), |
| } |
| as.gs.Stderr = os.Stderr |
| as.xz.Stderr = os.Stderr |
| |
| var err error |
| if as.xz.Stdin, err = as.gs.StdoutPipe(); err != nil { |
| return nil, err |
| } |
| if as.out, err = as.xz.StdoutPipe(); err != nil { |
| return nil, err |
| } |
| |
| if err = as.xz.Start(); err != nil { |
| return nil, err |
| } |
| if err = as.gs.Start(); err != nil { |
| as.close() // kill previously-started xz process |
| return nil, err |
| } |
| return &as, nil |
| } |
| |
| // close kills the gsutil and xz processes if they're still running and lets |
| // the system reclaim their resources. |
| func (as *archiveStreamer) close() error { |
| var gserr, xzerr error |
| if as.gs.Process != nil { |
| as.gs.Process.Kill() |
| _, gserr = as.gs.Process.Wait() |
| } |
| if as.xz.Process != nil { |
| as.xz.Process.Kill() |
| _, xzerr = as.xz.Process.Wait() |
| } |
| if gserr != nil { |
| return gserr |
| } |
| return xzerr |
| } |