| # -*- coding: utf-8 -*- |
| # Copyright 2018 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. |
| |
| """Utilities for working with chromeos sysbot bugs on issuetracker.""" |
| |
| from __future__ import print_function |
| |
| import base64 |
| import pickle |
| import subprocess |
| |
| import config |
| import simpledb |
| import utils |
| |
| |
| def clean_st(st): |
| """Cleans the stacktraces obtained from Issuetracker. |
| |
| Parses and cleans the stacktraces obtained from an Issuetracker |
| bug. It returns a list of sets, with each string in the set being |
| a function name in the stacktrace. |
| |
| Args: |
| st: A list of sets, with each set representing a stacktrace. |
| |
| Returns: |
| A list of sets, each of which contain cleaned function names. |
| An example below. |
| [set(['kthread', 'worker_thread', 'ret_from_fork'])] |
| """ |
| ret = [] |
| for stacktrace in st: |
| temp = set() |
| for fnname in stacktrace: |
| fnname = fnname.strip() |
| if fnname.startswith('<'): |
| index = 3 if '?' in fnname else 2 |
| elif fnname.startswith('['): |
| index = 1 |
| else: |
| index = 0 |
| |
| try: |
| fnname = fnname.split()[index] |
| except IndexError as _: |
| print('[x] Error parsing %s' % (repr(fnname))) |
| fnname = '' |
| |
| if not fnname: |
| continue |
| |
| fnname = fnname.split('+')[0] |
| if not fnname: |
| continue |
| |
| temp.add(fnname) |
| ret.append(temp) |
| return ret |
| |
| |
| def get_stacktrace(lineno, lines): |
| """Obtain the stacktrace from the an IssueTracker bug. |
| |
| Given the content of an issuetracker bug, obtain all stacktraces, |
| clean, and return. This returns a list of set of strings, with |
| each string representing a function in the stacktrace. |
| |
| Args: |
| lineno: This tells where to start processing lines from. |
| lines: A list of strings containing bug details. |
| |
| Returns: |
| A list of sets, each set corresponding to one stacktrace. |
| (One issuetracker bug might contain multiple stacktraces). |
| An example below. |
| [set(['kthread', 'worker_thread', 'ret_from_fork'])] |
| """ |
| remove_marker = lambda x: x[2:] if x.startswith('> ') else x |
| st, d = list(), set() |
| inside_st = False |
| for line in lines[lineno:]: |
| line = remove_marker(line) |
| if line == IssuetrackerBug.ST_START_MARKER: |
| if d: |
| st.append(d) |
| d = set() |
| inside_st = True |
| elif inside_st and line.startswith(' '): |
| d.add(line) |
| else: |
| inside_st = False |
| if d: |
| st.append(d) |
| |
| st = clean_st(st) |
| return st |
| |
| |
| class IssuetrackerBug(object): |
| """IssuetrackerBug represents one parsed issuetracker bug.""" |
| COMMIT_MARKER = 'syzbot hit the following crash on ' |
| KVER_MARKER = ('https://chromium.googlesource.com/chromiumos/' |
| 'third_party/kernel ') |
| ST_START_MARKER = 'Call Trace:' |
| |
| def __init__(self, bugid, title): |
| self.bugid = bugid |
| self.title = title |
| self.crashat = '' |
| self.kernel = '' |
| self.stacktrace = [] |
| self.parsebody() |
| |
| def __repr__(self): |
| print(self.stacktrace) |
| return ('bugid:%s title:"%s" crashat:%s kernel:%s\n' |
| % (self.bugid, self.title, self.crashat, self.kernel)) |
| |
| def setcrashat(self, line): |
| """Sets the kernel commit at which this crash occured.""" |
| self.crashat = line.strip().split()[-1] |
| |
| def setkver(self, line): |
| """Sets the kernel version for which this crash occured.""" |
| self.kernel = line.strip().split()[-1] |
| |
| def setst(self, st): |
| """Sets the stacktrace associated with the Issuetracker bug. |
| |
| A bug might have multiple stacktraces. |st| is a list of set |
| of strings, where each string is a function name in the stacktrace. |
| """ |
| if not st: |
| return |
| self.stacktrace = st |
| |
| def parsebody(self): |
| """Parse the body of an IssueTracker bug.""" |
| print('Bug = %s' % (self.bugid)) |
| cmd = Issuetracker.LIST_BUG_ASC % (self.bugid) |
| bugbody = subprocess.check_output(cmd, shell=True).split('\n') |
| |
| for i, line in enumerate(bugbody): |
| if IssuetrackerBug.COMMIT_MARKER in line: |
| self.setcrashat(line) |
| elif IssuetrackerBug.KVER_MARKER in line: |
| self.setkver(line) |
| elif IssuetrackerBug.ST_START_MARKER in line: |
| self.setst(get_stacktrace(i, bugbody)) |
| break |
| |
| |
| class Issuetracker(object): |
| """Issuetracker mananger a collection of chromeos IssuetrackerBug's.""" |
| LIST_ALL_BUGS = 'bugged hotlist %s' |
| LIST_BUG_ASC = 'bugged show --sort=asc %s' |
| |
| def __init__(self, hotlistid): |
| self.hotlistid = hotlistid |
| self.bugs = [] |
| utils.rmfile_if_exists(config.ISSUETRACKER_DB) |
| self.populate_bugs() |
| self.db = simpledb.SimpleDB(config.ISSUETRACKER_DB) |
| |
| def populate_bugs(self): |
| """Retrieve a list of all bugs in a hotlist and parse.""" |
| cmd = Issuetracker.LIST_ALL_BUGS % (self.hotlistid) |
| all_bug_lines = subprocess.check_output(cmd, shell=True).split('\n') |
| for i, line in enumerate(all_bug_lines[1:]): |
| print(i) |
| self.parse_bug_line(line) |
| |
| def parse_bug_line(self, line): |
| """Parse the details of a bug from a list of bugs.""" |
| if not line.strip(): |
| return |
| |
| parts = line.split() |
| bugid, status, title = parts[0], parts[7], ' '.join(parts[8:]) |
| if status != 'NEW': |
| return |
| |
| b = IssuetrackerBug(bugid, title) |
| self.bugs.append(b) |
| |
| def print_bugs(self): |
| """Dump all parsed Issuetracker bugs for debugging.""" |
| for bug in self.bugs: |
| print(bug) |
| |
| def save(self): |
| """Save parsed Issuetracker bugs to a local cache.""" |
| self.db.begin() |
| for bug in self.bugs: |
| self.db.insert(bugid=bug.bugid, title=bug.title, |
| crashat=bug.crashat, kernel=bug.kernel, |
| stacktrace=base64.b64encode( |
| pickle.dumps(bug.stacktrace))) |
| self.db.commit() |
| print('[+] Done writing %d records to "%s"' % (len(self.bugs), |
| config.ISSUETRACKER_DB)) |