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()