lab.nexedi.com/kirr/go123@v0.0.0-20240207185015-8299741fa871/tracing/cmd/pyruntraced (about) 1 #!/usr/bin/env python 2 # -*- coding: utf-8 -*- 3 # Copyright (C) 2018-2020 Nexedi SA and Contributors. 4 # Kirill Smelkov <kirr@nexedi.com> 5 # 6 # This program is free software: you can Use, Study, Modify and Redistribute 7 # it under the terms of the GNU General Public License version 3, or (at your 8 # option) any later version, as published by the Free Software Foundation. 9 # 10 # You can also Link and Combine this program with other software covered by 11 # the terms of any of the Free Software licenses or any of the Open Source 12 # Initiative approved licenses and Convey the resulting work. Corresponding 13 # source of such a combination shall include the source code for all other 14 # software used. 15 # 16 # This program is distributed WITHOUT ANY WARRANTY; without even the implied 17 # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 18 # 19 # See COPYING file for full licensing terms. 20 # See https://www.nexedi.com/licensing for rationale and options. 21 """pyruntraced ... - run python ... with tracepoints attached 22 23 This program runs Python code with tracepoints activated. 24 25 Whenever a tracepoint is reached, attached probe sends corresponding trace event 26 to driving process and awaits further commands from it. Commands could be: 27 28 - to execute arbitrary python code, thus allowing program state inspection, or 29 - to continue original code execution. 30 31 Overall this allows Go driver process to test several concurrent child 32 subprocesses via tracetest, the same way to how it is done in pure NEO/go. 33 34 Please see documentation for next Go packages for context: 35 36 - lab.nexedi.com/kirr/go123/tracing 37 - lab.nexedi.com/kirr/go123/tracing/tracetest 38 39 40 Implementation notes 41 -------------------- 42 43 The channel for tracing communication in between driver and traced process is 44 passed as opened file descriptor by driver, and its number is specified via 45 <fd> commandline argument. 46 47 The protocol in between driver and traced process is as follows (from the point 48 of view of traced process): 49 50 > tid T eventname event # event happenned on a thread 51 < tid C # driver tells tracee to continue execution 52 < tid !... # driver asks tracee to evaluate ... in context where probe is attached 53 > tid E exception # tracee reports result as exception 54 > tid R result # tracee reports result 55 56 eventname is just regular string 57 event, exception, result are JSON-encoded objects. 58 59 Fork is not allowed in order not to confuse the driver(*). 60 61 Tracepoints and probes definitions are provided in separate files that are 62 passed as <trace>* command line arguments. 63 64 See trace_entry() for documentation on how to define probes. 65 66 67 (*) fork could be supported by making every new process to reattach to the 68 driver via new file descriptor - e.g. connecting via socket. 69 70 We don't currently do that for simplicity. 71 """ 72 73 from functools import wraps 74 import json 75 import thread, threading 76 77 # Tracer implements synchronous tracing in compatible way to tracetest on 78 # NEO/go side. 79 # 80 # See top-level module description for details. 81 class Tracer(object): 82 83 def __init__(self, ftracetx, ftracerx): 84 self.ftx = ftracetx # file object to send data to tracer 85 self.txlock = threading.Lock() # serializes writes to .ftx 86 87 self.frx = ftracerx # file object to receive data from tracer 88 self.rxtab = {} # {} tid -> RxQueue 89 self.rxlock = threading.Lock() # serializes access to .rxtab 90 91 # NOTE 2 separate ftx/frx file objects are used because python for 92 # stdio objects does locking and this way if it would be only 1 file 93 # object readline in _serve_recv() would block tx until readline wakes up. 94 95 self.trecv = threading.Thread(target=self._serve_recv) 96 self.trecv.daemon = True # XXX better to gracefully stop it? 97 self.trecv.start() 98 99 100 # _send sends 1 line to tracer from current thread. 101 def _send(self, line): 102 assert '\n' not in line 103 tid = thread.get_ident() 104 self.txlock.acquire() 105 try: 106 self.ftx.write(('%d ' % tid) + line + '\n') 107 self.ftx.flush() 108 finally: 109 self.txlock.release() 110 111 # _serve_recv receives lines from .frx and multiplexes them to 112 # destination threads RX queues. 113 # 114 # runs in a dedicated thread. 115 def _serve_recv(self): 116 while 1: 117 line = self.frx.readline() 118 # tid SP rest \n 119 tid, line = line.split(None, 1) 120 line = line.rstrip('\n') 121 tid = int(tid) 122 123 self.rxlock.acquire() 124 rxq = self.rxtab.get(tid) 125 if rxq is None: 126 self.rxtab[tid] = rxq = RxQueue() 127 rxq.lineq.append(line) 128 rxq.event.set() 129 self.rxlock.release() 130 131 132 # _recv receives 1 line from tracer for current thread. 133 def _recv(self): 134 tid = thread.get_ident() 135 136 while 1: 137 self.rxlock.acquire() 138 rxq = self.rxtab.get(tid) 139 if rxq is None: 140 rxq = self.rxtab[tid] = RxQueue() 141 if len(rxq.lineq) > 0: 142 # data was already queued for us 143 line = rxq.lineq.pop(0) 144 self.rxlock.release() 145 return line 146 147 # there is no data - we have to wait for it 148 rxq.event.clear() 149 self.rxlock.release() 150 rxq.event.wait() 151 152 # woken up -> retry to dequeue data 153 154 155 # trace1 handles 1 trace event. 156 # 157 # it sends the event to tracer and awaits commands from it to either 158 # inspect current state or continue execution. 159 # 160 # globals/locals are the mapping used in eval, if driver asks to inspect 161 # program state. 162 def trace1(self, eventname, event, globals, locals): 163 # send trace event 164 evstr = json.dumps(event) 165 assert '\n' not in evstr 166 self._send('T %s %s' % (eventname, evstr)) 167 168 # wait for commands 169 # ! ... - eval python code 170 # C - continue 171 while 1: 172 line = self._recv() 173 if len(line) == 0 or line[0] not in "!C": 174 raise RuntimeError("trace1: got invalid line from driver: %r" % line) 175 176 if line[0] == 'C': 177 return # probe finishes - continue execution of original code 178 179 # eval python in context of probed function 180 try: 181 r = eval(line[1:], globals, locals) 182 except Exception as e: 183 reply = 'E %s' % json.dumps(str(e)) 184 else: 185 try: 186 reply = 'R %s' % json.dumps(r) 187 except Exception as e: 188 # some types are not json-serializable 189 # XXX ok to play such games here? 190 # XXX too many objects are not JSON-serializable. 191 reply = 'E %s' % json.dumps(str(e)) 192 193 self._send(reply) 194 195 196 # RxQueue represents receive queue for 1 thread 197 class RxQueue(object): 198 def __init__(self): 199 self.lineq = [] # [] of lines received 200 self.event = threading.Event() # sender signals consumer there is new data 201 202 203 # gtracer is the global tracer object 204 gtracer = None # Tracer 205 206 207 # trace_entry attaches probe to func entry. 208 # 209 # For example 210 # 211 # class MyClass: 212 # def meth(self, x, y): 213 # ... 214 # 215 # @trace_entry(MyClass.meth, 'MyEvent') 216 # def _(self, x, y): 217 # return {'x': x, 'y': y} 218 # 219 # will emit event 'MyEvent' with corresponding x/y dict on every call entry to 220 # MyClass.meth() . 221 def trace_entry(func, eventname): 222 klass = func.im_class 223 fname = func.im_func.func_name 224 225 def deco(f): 226 @wraps(func) 227 def probe(self, *args, **kw): 228 event = f(self, *args, **kw) 229 gtracer.trace1(eventname, event, func.func_globals, {'self': self, 'args': args, 'kw': kw}) 230 return func(self, *args, **kw) 231 232 setattr(klass, fname, probe) 233 234 return deco 235 236 237 # ---------------------------------------- 238 239 import os, sys 240 from gpython import pymain 241 242 def usage(out): 243 print >>out, "Usage: pyruntraced <fd> <trace>* -- ..." 244 245 def die(msg): 246 print >>sys.stderr, msg 247 sys.exit(2) 248 249 # main mimics `python ...`, 250 # but with tracepoints already attached. 251 def main(): 252 prog = sys.argv[0] 253 argv = sys.argv[1:] 254 if not argv: 255 usage(sys.stderr) 256 sys.exit(2) 257 258 # setup global tracer 259 global gtracer 260 fdtrace = argv[0] 261 fdtrace = int(fdtrace) 262 ttx = os.fdopen(fdtrace, 'w') 263 trx = os.fdopen(fdtrace, 'r') 264 gtracer = Tracer(ttx, trx) 265 argv = argv[1:] 266 267 # forbid fork (see top-level description about why) 268 def nofork(): raise RuntimeError('pyruntraced: fork forbidden') 269 os.fork = nofork 270 os.forkpty = nofork 271 272 # load trace probes 273 # NOTE list of trace files to load might be empty 274 tracev = [] 275 dashdash = False 276 for i, arg in enumerate(argv): 277 if arg == '--': 278 dashdash = True 279 break 280 if arg.startswith('-'): 281 die("option '%s' goes before '--'" % arg) 282 tracev.append(arg) 283 284 if not dashdash: 285 die("no '--' found") 286 argv = argv[i+1:] 287 288 for t in tracev: 289 # load in current context so that e.g. @trace_entry is visible in the 290 # tracing code. 291 execfile(t, globals()) 292 293 # now mimic `python ...` 294 pymain([prog] + argv) 295 296 297 if __name__ == '__main__': 298 main()