blob: 3549d1e5fe5bfa14897c15e6f190c554eaf1c142 [file] [log] [blame]
Neels Hofmeyr0898a002016-11-16 14:36:29 +01001#!/usr/bin/env python
2
3__doc__ = '''
4fsm-to-dot: convert FSM definitons to graph images
5
6Usage:
7 ./fsm-to-dot.py ~/openbsc/openbsc/src/libvlr/*.c
8 for f in *.dot ; do dot -Tpng $f > $f.png; done
9 # dot comes from 'apt-get install graphviz'
10
11Looks for osmo_fsm finite state machine definitions and madly parses .c files
12to draw graphs of them. This uses wild regexes that rely on coding style etc..
13No proper C parsing is done here (pycparser sucked, unfortunately).
14'''
15
16import sys, re
17
18def err(msg):
19 sys.stderr.write(msg + '\n')
20
21class listdict(object):
22 def __getattr__(ld, name):
23 if name == 'add':
24 return ld.__getattribute__(name)
25 return ld.__dict__.__getattribute__(name)
26
27 def _have(ld, name):
28 l = ld.__dict__.get(name)
29 if not l:
30 l = []
31 ld.__dict__[name] = l
32 return l
33
34 def add(ld, name, item):
35 l = ld._have(name)
36 l.append(item)
37 return ld
38
39 def add_dict(ld, d):
40 for k,v in d.items():
41 ld.add(k, v)
42
43 def __setitem__(ld, name, val):
44 return ld.__dict__.__setitem__(name, val)
45
46 def __getitem__(ld, name):
47 return ld.__dict__.__getitem__(name)
48
49 def __str__(ld):
50 return ld.__dict__.__str__()
51
52 def __repr__(ld):
53 return ld.__dict__.__repr__()
54
55 def update(ld, other_ld):
56 for name, items in other_ld.items():
57 ld.extend(name, items)
58 return ld
59
60 def extend(ld, name, vals):
61 l = ld._have(name)
62 l.extend(vals)
63 return ld
64
65re_state_start = re.compile(r'\[([A-Z_][A-Z_0-9]*)\]')
66re_event = re.compile(r'S\(([A-Z_][A-Z_0-9]*)\)')
67re_action = re.compile(r'.action *= *([a-z_][a-z_0-9]*)')
68
69def state_starts(line):
70 m = re_state_start.search(line)
71 if m:
72 return m.group(1)
73 return None
74
75def in_event_starts(line):
76 return line.find('in_event_mask') >= 0
77
78def out_state_starts(line):
79 return line.find('out_state_mask') >= 0
80
81def states_or_events(line):
82 return re_event.findall(line)
83
84def parse_action(line):
85 a = re_action.findall(line)
86 if a:
87 return a[0]
88 return None
89
90def _common_prefix(a, b):
91 for l in reversed(range(1,len(a))):
92 aa = a[:l+1]
93 if b.startswith(aa):
94 return aa
95 return ''
96
97def common_prefix(strs):
98 if not strs:
99 return ''
100 p = None
101 for s in strs:
102 if p is None:
103 p = s
104 continue
105 p = _common_prefix(p, s)
106 if not p:
107 return ''
108 return p
109
110KIND_STATE = 'KIND_STATE'
111KIND_FUNC = 'KIND_FUNC'
112KIND_FSM = 'KIND_FSM'
113BOX_SHAPES = {
114 KIND_STATE : None,
115 KIND_FUNC : 'box',
116 KIND_FSM : 'box3d',
117}
118
119class Event:
120 def __init__(event, name):
121 event.name = name
122 event.short_name = name
123
124 def __cmp__(event, other):
125 return cmp(event.name, other.name)
126
127class Edge:
128 def __init__(edge, to_state, event_name=None, style=None, action=None):
129 edge.to_state = to_state
130 edge.style = style
131 edge.events = []
132 edge.actions = []
133 edge.add_event_name(event_name)
134 edge.add_action(action)
135
136 def add_event_name(edge, event_name):
137 if not event_name:
138 return
139 edge.add_event(Event(event_name))
140
141 def add_event(edge, event):
142 if not event:
143 return
144 if event in edge.events:
145 return
146 edge.events.append(event)
147
148 def add_events(edge, events):
149 for event in events:
150 edge.add_event(event)
151
152 def add_action(edge, action):
153 if not action or action in edge.actions:
154 return
155 edge.actions.append(action)
156
157 def add_actions(edge, actions):
158 for action in actions:
159 edge.add_action(action)
160
161 def event_names(edge):
162 return sorted([event.name for event in edge.events])
163
164 def event_labels(edge):
165 return sorted([event.short_name for event in edge.events])
166
167 def action_labels(edge):
168 return sorted([action + '()' for action in edge.actions])
169
170 def has_event_name(edge, event_name):
171 return event_name in edge.event_names()
172
173class State:
174 name = None
175 short_name = None
176 action = None
177 label = None
178 in_event_names = None
179 out_state_names = None
180 out_edges = None
181 kind = None
182
183 def __init__(state):
184 state.in_event_names = []
185 state.out_state_names = []
186 state.out_edges = []
187 state.kind = KIND_STATE
188
189 def add_out_edge(state, edge):
190 for out_edge in state.out_edges:
191 if out_edge.to_state is edge.to_state:
192 if out_edge.style == edge.style:
193 out_edge.add_events(edge.events)
194 out_edge.add_actions(edge.actions)
195 return
196 # sanity
197 if out_edge.to_state.get_label() == edge.to_state.get_label():
198 raise Exception('Two distinct states exist with identical labels.')
199 state.out_edges.append(edge)
200
201 def get_label(state):
202 if state.label:
203 return state.label
204 l = [state.short_name]
205 if state.action:
206 if state.short_name == state.action:
207 l = []
208 l.append(state.action + '()')
209 return r'\n'.join(l)
210
211 def event_names(state):
212 event_names = []
213 for out_edge in state.out_edges:
214 event_names.extend(out_edge.event_names())
215 return event_names
216
217 def shape_str(state):
218 shape = BOX_SHAPES.get(state.kind, None)
219 if not shape:
220 return ''
221 return ',shape=%s' % shape
222
223 def __repr__(state):
224 return 'State(name=%r,short_name=%r,out=%d)' % (state.name, state.short_name, len(state.out_edges))
225
226class Fsm:
227 def __init__(fsm, struct_name, states_struct_name, from_file=None):
228 fsm.states = []
229 fsm.struct_name = struct_name
230 fsm.states_struct_name = states_struct_name
231 fsm.from_file = from_file
232 fsm.action_funcs = set()
233 fsm.event_names = set()
234
235 def parse_states(fsm, src):
236 state = None
237 started = None
238
239 IN_EVENTS = 'events'
240 OUT_STATES = 'states'
241
242 lines = src.splitlines()
243
244 for line in lines:
245 state_name = state_starts(line)
246 if state_name:
247 state = State()
248 fsm.states.append(state)
249 started = None
250 state.name = state_name
251
252 if in_event_starts(line):
253 started = IN_EVENTS
254 if out_state_starts(line):
255 started = OUT_STATES
256
257 if not state or not started:
258 continue
259
260 tokens = states_or_events(line)
261 if started == IN_EVENTS:
262 state.in_event_names.extend(tokens)
263 elif started == OUT_STATES:
264 state.out_state_names.extend(tokens)
265 else:
266 err('ignoring: %r' % tokens)
267
268 a = parse_action(line)
269 if a:
270 state.action = a
271
272
273 for state in fsm.states:
274 if state.action:
275 fsm.action_funcs.add(state.action)
276 if state.in_event_names:
277 fsm.event_names.update(state.in_event_names)
278
279 fsm.make_states_short_names()
280 fsm.ref_out_states()
281
282 def make_states_short_names(fsm):
283 p = common_prefix([s.name for s in fsm.states])
284 for s in fsm.states:
285 s.short_name = s.name[len(p):]
286 return p
287
288 def make_events_short_names(fsm):
289 p = common_prefix(fsm.event_names)
290 for state in fsm.states:
291 for edge in state.out_edges:
292 for event in edge.events:
293 event.short_name = event.name[len(p):]
294
295 def ref_out_states(fsm):
296 for state in fsm.states:
297 for e in [Edge(fsm.find_state_by_name(n, True)) for n in state.out_state_names]:
298 state.add_out_edge(e)
299
300 def find_state_by_name(fsm, name, strict=False):
301 for state in fsm.states:
302 if state.name == name:
303 return state
304 if strict:
305 raise Exception("State not found: %r" % name);
306 return None
307
308 def find_state_by_action(fsm, action):
309 for state in fsm.states:
310 if state.action == action:
311 return state
312 return None
313
314 def add_special_state(fsm, additional_states, name, in_state=None,
315 out_state=None, event_name=None, kind=KIND_FUNC,
316 state_action=None, label=None, edge_action=None):
317 additional_state = None
318 for s in additional_states:
319 if s.short_name == name:
320 additional_state = s
321 break;
322
323 if not additional_state:
324 for s in fsm.states:
325 if s.short_name == name:
326 additional_state = s
327 break;
328
329 if kind == KIND_FUNC and not state_action:
330 state_action = name
331
332 if not additional_state:
333 additional_state = State()
334 additional_state.short_name = name
335 additional_state.action = state_action
336 additional_state.kind = kind
337 additional_state.label = label
338 additional_states.append(additional_state)
339
340 if out_state:
341 additional_state.out_state_names.append(out_state.name)
342 additional_state.add_out_edge(Edge(out_state, event_name, 'dotted',
343 action=edge_action))
344
345 if in_state:
346 in_state.out_state_names.append(additional_state.name)
347 in_state.add_out_edge(Edge(additional_state, event_name, 'dotted',
348 action=edge_action))
349
350
351 def find_event_edges(fsm, c_files):
352 # enrich state transitions between the states with event labels
353 func_to_state_transitions = listdict()
354 for c_file in c_files:
355 func_to_state_transitions.update( c_file.find_state_transitions(fsm.event_names) )
356
357 # edges between explicit states
358 for state in fsm.states:
359 transitions = func_to_state_transitions.get(state.action)
360 if not transitions:
361 continue
362
363 for to_state_name, event_name in transitions:
364 if not event_name:
365 continue
366 for out_edge in state.out_edges:
367 if out_edge.to_state.name == to_state_name:
368 out_edge.add_event_name(event_name)
369
370 additional_states = []
371
372
373 # functions that aren't state actions but still effect state transitions
374 for func_name, transitions in func_to_state_transitions.items():
375 if func_name in fsm.action_funcs:
376 continue
377 for to_state_name, event_name in transitions:
378 to_state = fsm.find_state_by_name(to_state_name)
379 if not to_state:
380 continue
381 fsm.add_special_state(additional_states, func_name, None, to_state, event_name)
382
383
384 event_sources = c_files.find_event_sources(fsm.event_names)
385
386 for state in fsm.states:
387
388 for in_event_name in state.in_event_names:
389 funcs_for_in_event = event_sources.get(in_event_name)
390 if not funcs_for_in_event:
391 continue
392
393 found = False
394 for out_edge in state.out_edges:
395 if out_edge.has_event_name(in_event_name):
396 out_edge.action = r'\n'.join([(f + '()') for f in funcs_for_in_event
397 if f != state.action])
398
399 # if any functions that don't belong to a state trigger events, add
400 # them to the graph as well
401 additional_funcs = [f for f in funcs_for_in_event if f not in fsm.action_funcs]
402 for af in additional_funcs:
403 fsm.add_special_state(additional_states, af, None, state, in_event_name)
404
405 fsm.states.extend(additional_states)
406
407 # do any existing action functions by chance call other action functions?
408 for state in fsm.states:
409 if not state.action:
410 continue
411 callers = c_files.find_callers(state.action)
412 if not callers:
413 continue
414 for other_state in fsm.states:
415 if other_state.action in callers:
416 other_state.add_out_edge(Edge(state, None, 'dotted'))
417
418 def add_fsm_alloc(fsm, c_files):
419
420 allocating_funcs = []
421 for c_file in c_files:
422 allocating_funcs.extend(c_file.fsm_allocators.get(fsm.struct_name, []))
423
424 starting_state = None
425 if fsm.states:
426 # assume the first state starts
427 starting_state = fsm.states[0]
428
429 additional_states = []
430 for func_name in allocating_funcs:
431 fsm.add_special_state(additional_states, func_name, None, starting_state)
432
433 fsm.states.extend(additional_states)
434
435 def add_cross_fsm_links(fsm, fsms, c_files, fsm_meta):
436 for state in fsm.states:
437 if not state.action:
438 continue
439 if state.kind == KIND_FSM:
440 continue
441 callers = c_files.find_callers(state.action)
442
443 if state.kind == KIND_FUNC:
444 callers.append(state.action)
445
446 if not callers:
447 continue
448
449 for caller in callers:
450 for calling_fsm in fsms:
451 if calling_fsm is fsm:
452 continue
453 calling_state = calling_fsm.find_state_by_action(caller)
454 if not calling_state:
455 continue
456 if calling_state.kind == KIND_FSM:
457 continue
458
459 label = None
460 if state.kind == KIND_STATE:
461 label=fsm.struct_name + ': ' + state.short_name
462 edge_action = caller
463 if calling_state.action == edge_action:
464 edge_action = None
465 calling_fsm.add_special_state(calling_fsm.states, fsm.struct_name,
466 calling_state, kind=KIND_FSM, edge_action=edge_action, label=label)
467
468 label = None
469 if calling_state.kind == KIND_STATE:
470 label=calling_fsm.struct_name + ': ' + calling_state.short_name
471 edge_action = caller
472 if state.action == edge_action:
473 edge_action = None
474 fsm.add_special_state(fsm.states, calling_fsm.struct_name, None,
475 state, kind=KIND_FSM, edge_action=edge_action,
476 label=label)
477
478 # meta overview
479 meta_called_fsm = fsm_meta.have_state(fsm.struct_name, KIND_FSM)
480 meta_calling_fsm = fsm_meta.have_state(calling_fsm.struct_name, KIND_FSM)
481 meta_calling_fsm.add_out_edge(Edge(meta_called_fsm))
482
483
484 def have_state(fsm, name, kind=KIND_STATE):
485 state = fsm.find_state_by_name(name)
486 if not state:
487 state = State()
488 state.name = name
489 state.short_name = name
490 state.kind = kind
491 fsm.states.append(state)
492 return state
493
494 def to_dot(fsm):
495 out = ['digraph G {', 'rankdir=LR;']
496
497 for state in fsm.states:
498 out.append('%s [label="%s"%s]' % (state.short_name, state.get_label(),
499 state.shape_str()))
500
501 for state in fsm.states:
502 for out_edge in state.out_edges:
503 attrs = []
504 labels = []
505 if out_edge.events:
506 labels.extend(out_edge.event_labels())
507 if out_edge.actions:
508 labels.extend(out_edge.action_labels())
509 if labels:
510 attrs.append('label="%s"' % (r'\n'.join(labels)))
511 if out_edge.style:
512 attrs.append('style=%s'% out_edge.style)
513 attrs_str = ''
514 if attrs:
515 attrs_str = ' [%s]' % (','.join(attrs))
516 out.append('%s->%s%s' % (state.short_name, out_edge.to_state.short_name, attrs_str))
517
518 out.append('}\n')
519
520 return '\n'.join(out)
521
522 def write_dot_file(fsm):
523 dot_path = '%s.dot' % fsm.struct_name
524 f = open(dot_path, 'w')
525 f.write(fsm.to_dot())
526 f.close()
527 print(dot_path)
528
529
530re_fsm = re.compile(r'struct osmo_fsm ([a-z_][a-z_0-9]*) =')
531re_fsm_states_struct_name = re.compile(r'\bstates = ([a-z_][a-z_0-9]*)\W*,')
532re_fsm_states = re.compile(r'struct osmo_fsm_state ([a-z_][a-z_0-9]*)\[\] =')
533re_func = re.compile(r'(\b[a-z_][a-z_0-9]*\b)\([^)]*\)\W*^{', re.MULTILINE)
534re_state_trigger = re.compile(r'osmo_fsm_inst_state_chg\([^,]+,\W*([A-Z_][A-Z_0-9]*)\W*,', re.M)
535re_fsm_alloc = re.compile(r'osmo_fsm_inst_alloc[_child]*\(\W*&([a-z_][a-z_0-9]*),', re.M)
536re_fsm_event_dispatch = re.compile(r'osmo_fsm_inst_dispatch\(\W*[^,]+,\W*([A-Z_][A-Z_0-9]*)\W*,', re.M)
537
538class CFile():
539 def __init__(c_file, path):
540 c_file.path = path
541 c_file.src = open(path).read()
542 c_file.funcs = {}
543 c_file.fsm_allocators = listdict()
544
545 def extract_block(c_file, brace_open, brace_close, start):
546 pos = 0
547 try:
548 src = c_file.src
549 block_start = src.find(brace_open, start)
550
551 pos = block_start
552 level = 1
553 while level > 0:
554 pos += 1
555 if src[pos] == brace_open:
556 level += 1
557 elif src[pos] == brace_close:
558 level -= 1
559
560 return src[block_start+1:pos]
561 except:
562 print("Error while trying to extract a code block from %r char pos %d" % (c_file.path, pos))
563 print("Block start at char pos %d" % block_start)
564 try:
565 print(src[block_start - 20 : block_start + 20])
566 print('...')
567 print(src[pos - 20 : pos + 20])
568 except:
569 pass
570 return ''
571
572
573 def find_fsms(c_file):
574 fsms = []
575 for m in re_fsm.finditer(c_file.src):
576 struct_name = m.group(1)
577 struct_def = c_file.extract_block('{', '}', m.start())
578 states_struct_name = re_fsm_states_struct_name.findall(struct_def)[0]
579 fsm = Fsm(struct_name, states_struct_name, c_file)
580 fsms.append(fsm)
581 return fsms
582
583 def find_fsm_states(c_file, fsms):
584 for m in re_fsm_states.finditer(c_file.src):
585 states_struct_name = m.group(1)
586 for fsm in fsms:
587 if states_struct_name == fsm.states_struct_name:
588 fsm.parse_states(c_file.extract_block('{', '}', m.start()))
589
590 def parse_functions(c_file):
591 funcs = {}
592 for m in re_func.finditer(c_file.src):
593 name = m.group(1)
594 func_src = c_file.extract_block('{', '}', m.start())
595 funcs[name] = func_src
596 c_file.funcs = funcs
597 c_file.find_fsm_allocators()
598
599 def find_callers(c_file, func_name):
600 func_call = func_name + '('
601 callers = []
602 for func_name, src in c_file.funcs.items():
603 if src.find(func_call) >= 0:
604 callers.append(func_name)
605 return callers
606
607 def find_fsm_allocators(c_file):
608 c_file.fsm_allocators = listdict()
609 for func_name, src in c_file.funcs.items():
610 for m in re_fsm_alloc.finditer(src):
611 fsm_struct_name = m.group(1)
612 c_file.fsm_allocators.add(fsm_struct_name, func_name)
613
614 def find_state_transitions(c_file, event_names):
615 TO_STATE = 'TO_STATE'
616 EVENT = 'EVENT'
617 func_to_state_transitions = listdict()
618
619 for func_name, src in c_file.funcs.items():
620 found_tokens = []
621
622 for m in re_state_trigger.finditer(src):
623 to_state = m.group(1)
624 found_tokens.append((m.start(), TO_STATE, to_state))
625
626 for event in event_names:
627 re_event = re.compile(r'\b(' + event + r')\b')
628 for m in re_event.finditer(src):
629 event = m.group(1)
630 found_tokens.append((m.start(), EVENT, event))
631
632 found_tokens = sorted(found_tokens)
633
634 last_event = None
635 for start, kind, name in found_tokens:
636 if kind == EVENT:
637 last_event = name
638 else:
639 func_to_state_transitions.add(func_name, (name, last_event))
640
641 return func_to_state_transitions
642
643
644 def find_event_sources(c_file, event_names):
645 c_file.event_sources = listdict()
646 for func_name, src in c_file.funcs.items():
647 for m in re_fsm_event_dispatch.finditer(src):
648 event_name = m.group(1)
649 c_file.event_sources.add(event_name, func_name)
650
651class CFiles(list):
652
653 def find_callers(c_files, func_name):
654 callers = []
655 for c_file in c_files:
656 callers.extend(c_file.find_callers(func_name))
657 return callers
658
659 def find_func_to_state_transitions(c_files):
660 func_to_state_transitions = listdict()
661 for c_file in c_files:
662 func_to_state_transitions.update( c_file.find_state_transitions(fsm.event_names) )
663 return func_to_state_transitions
664
665 def find_event_sources(c_files, event_names):
666 event_sources = listdict()
667 for c_file in c_files:
668 for event, sources in c_file.event_sources.items():
669 if event in event_names:
670 event_sources.extend(event, sources)
671 return event_sources
672
673c_files = CFiles()
674paths_seen = set()
675for path in sys.argv[1:]:
676 if path in paths_seen:
677 continue
678 paths_seen.add(path)
679 c_file = CFile(path)
680 c_files.append(c_file)
681
682for c_file in c_files:
683 c_file.parse_functions()
684
685fsms = []
686for c_file in c_files:
687 fsms.extend(c_file.find_fsms())
688
689for c_file in c_files:
690 c_file.find_fsm_states(fsms)
691 c_file.find_event_sources(fsms)
692
693for fsm in fsms:
694 fsm.find_event_edges(c_files)
695 fsm.add_fsm_alloc(c_files)
696
697fsm_meta = Fsm("meta", "meta")
698for fsm in fsms:
699 fsm.add_cross_fsm_links(fsms, c_files, fsm_meta)
700
701for fsm in fsms:
702 fsm.make_events_short_names()
703
704for fsm in fsms:
705 fsm.write_dot_file()
706
707fsm_meta.write_dot_file()
708
709
710# vim: tabstop=2 shiftwidth=2 expandtab