install_prereq: Download latest Jansson.
[asterisk/asterisk.git] / contrib / scripts / refcounter.py
1 #!/usr/bin/env python
2 """Process a ref debug log
3
4  This file will process a log file created by enabling
5  the refdebug config option in asterisk.conf.
6
7  See http://www.asterisk.org for more information about
8  the Asterisk project. Please do not directly contact
9  any of the maintainers of this project for assistance;
10  the project provides a web site, mailing lists and IRC
11  channels for your use.
12
13  This program is free software, distributed under the terms of
14  the GNU General Public License Version 2. See the LICENSE file
15  at the top of the source tree.
16
17  Copyright (C) 2014, Digium, Inc.
18  Matt Jordan <mjordan@digium.com>
19 """
20
21 import sys
22 import os
23
24 from optparse import OptionParser
25
26
27 def parse_line(line):
28     """Parse out a line into its constituent parts.
29
30     Keyword Arguments:
31     line The line from a ref debug log to parse out
32
33     Returns:
34     A dictionary containing the options, or None
35     """
36     tokens = line.strip().split(',', 7)
37     if len(tokens) < 8:
38         print "ERROR: ref debug line '%s' contains fewer tokens than " \
39               "expected: %d" % (line.strip(), len(tokens))
40         return None
41
42     processed_line = {'addr': tokens[0],
43                       'delta': tokens[1],
44                       'thread_id': tokens[2],
45                       'file': tokens[3],
46                       'line': tokens[4],
47                       'function': tokens[5],
48                       'state': tokens[6],
49                       'tag': tokens[7],
50                       }
51     return processed_line
52
53
54 def process_file(options):
55     """The routine that kicks off processing a ref file
56
57     Keyword Arguments:
58     filename The full path to the file to process
59
60     Returns:
61     A tuple containing:
62         - A list of objects whose lifetimes were completed
63             (i.e., finished objects)
64         - A list of objects referenced after destruction
65             (i.e., invalid objects)
66         - A list of objects whose lifetimes were not completed
67             (i.e., leaked objects)
68         - A list of objects whose lifetimes are skewed
69             (i.e., Object history starting with an unusual ref count)
70     """
71
72     finished_objects = []
73     invalid_objects = []
74     leaked_objects = []
75     skewed_objects = []
76     current_objects = {}
77     filename = options.filepath
78
79     with open(filename, 'r') as ref_file:
80         for line in ref_file:
81             parsed_line = parse_line(line)
82             if not parsed_line:
83                 continue
84
85             invalid = False
86             obj = parsed_line['addr']
87
88             if obj not in current_objects:
89                 current_objects[obj] = {'log': [], 'curcount': 1}
90                 if 'constructor' in parsed_line['state']:
91                     # This is the normal expected case
92                     pass
93                 elif 'invalid' in parsed_line['state']:
94                     invalid = True
95                     current_objects[obj]['curcount'] = 0
96                     if options.invalid:
97                         invalid_objects.append((obj, current_objects[obj]))
98                 elif 'destructor' in parsed_line['state']:
99                     current_objects[obj]['curcount'] = 0
100                     if options.skewed:
101                         skewed_objects.append((obj, current_objects[obj]))
102                 else:
103                     current_objects[obj]['curcount'] = int(
104                         parsed_line['state'])
105                     if options.skewed:
106                         skewed_objects.append((obj, current_objects[obj]))
107             else:
108                 current_objects[obj]['curcount'] += int(parsed_line['delta'])
109
110             current_objects[obj]['log'].append(
111                 "[%s] %s:%s %s: %s %s - [%s]" % (
112                     parsed_line['thread_id'],
113                     parsed_line['file'],
114                     parsed_line['line'],
115                     parsed_line['function'],
116                     parsed_line['delta'],
117                     parsed_line['tag'],
118                     parsed_line['state']))
119
120             # It is possible for curcount to go below zero if someone
121             # unrefs an object by two or more when there aren't that
122             # many refs remaining.  This condition abnormally finishes
123             # the object.
124             if current_objects[obj]['curcount'] <= 0:
125                 if current_objects[obj]['curcount'] < 0:
126                     current_objects[obj]['log'].append(
127                         "[%s] %s:%s %s: %s %s - [%s]" % (
128                             parsed_line['thread_id'],
129                             parsed_line['file'],
130                             parsed_line['line'],
131                             parsed_line['function'],
132                             "+0",
133                             "Object abnormally finalized",
134                             "**implied destructor**"))
135                     # Highlight the abnormally finished object in the
136                     # invalid section as well as reporting it in the normal
137                     # finished section.
138                     if options.invalid:
139                         invalid_objects.append((obj, current_objects[obj]))
140                 if not invalid and options.normal:
141                     finished_objects.append((obj, current_objects[obj]))
142                 del current_objects[obj]
143
144     if options.leaks:
145         for key, lines in current_objects.iteritems():
146             leaked_objects.append((key, lines))
147     return (finished_objects, invalid_objects, leaked_objects, skewed_objects)
148
149
150 def print_objects(objects, prefix=""):
151     """Prints out the objects that were processed
152
153     Keyword Arguments:
154     objects A list of objects to print
155     prefix  A prefix to print that specifies something about
156             this object
157     """
158
159     print "======== %s Objects ========" % prefix
160     print "\n"
161     for obj in objects:
162         print "==== %s Object %s history ====" % (prefix, obj[0])
163         for line in obj[1]['log']:
164             print line
165         print "\n"
166
167
168 def main(argv=None):
169     """Main entry point for the script"""
170
171     ret_code = 0
172
173     if argv is None:
174         argv = sys.argv
175
176     parser = OptionParser()
177
178     parser.add_option("-f", "--file", action="store", type="string",
179                       dest="filepath", default="/var/log/asterisk/refs",
180                       help="The full path to the refs file to process")
181     parser.add_option("-i", "--suppress-invalid", action="store_false",
182                       dest="invalid", default=True,
183                       help="If specified, don't output invalid object "
184                            "references")
185     parser.add_option("-l", "--suppress-leaks", action="store_false",
186                       dest="leaks", default=True,
187                       help="If specified, don't output leaked objects")
188     parser.add_option("-n", "--suppress-normal", action="store_false",
189                       dest="normal", default=True,
190                       help="If specified, don't output objects with a "
191                            "complete lifetime")
192     parser.add_option("-s", "--suppress-skewed", action="store_false",
193                       dest="skewed", default=True,
194                       help="If specified, don't output objects with a "
195                            "skewed lifetime")
196
197     (options, args) = parser.parse_args(argv)
198
199     if not options.invalid and not options.leaks and not options.normal \
200             and not options.skewed:
201         print >>sys.stderr, "All options disabled"
202         return -1
203
204     if not os.path.isfile(options.filepath):
205         print >>sys.stderr, "File not found: %s" % options.filepath
206         return -1
207
208     try:
209         (finished_objects,
210          invalid_objects,
211          leaked_objects,
212          skewed_objects) = process_file(options)
213
214         if options.invalid and len(invalid_objects):
215             print_objects(invalid_objects, "Invalid Referenced")
216             ret_code |= 4
217
218         if options.leaks and len(leaked_objects):
219             print_objects(leaked_objects, "Leaked")
220             ret_code |= 1
221
222         if options.skewed and len(skewed_objects):
223             print_objects(skewed_objects, "Skewed")
224             ret_code |= 2
225
226         if options.normal:
227             print_objects(finished_objects, "Finalized")
228
229     except (KeyboardInterrupt, SystemExit, IOError):
230         print >>sys.stderr, "File processing cancelled"
231         return -1
232
233     return ret_code
234
235
236 if __name__ == "__main__":
237     sys.exit(main(sys.argv))