diff --git a/.project b/.project deleted file mode 100644 index f62d591..0000000 --- a/.project +++ /dev/null @@ -1,17 +0,0 @@ - - - python-dtrace - - - - - - org.python.pydev.PyDevBuilder - - - - - - org.python.pydev.pythonNature - - diff --git a/.pydevproject b/.pydevproject deleted file mode 100644 index 3573661..0000000 --- a/.pydevproject +++ /dev/null @@ -1,10 +0,0 @@ - - - - -Default -python 2.7 - -/python-dtrace - - diff --git a/ACKNOWLEDGEMENTS b/ACKNOWLEDGEMENTS index 74808c2..86f4514 100644 --- a/ACKNOWLEDGEMENTS +++ b/ACKNOWLEDGEMENTS @@ -5,8 +5,11 @@ Acknowledgements Thanks to Marc Abramowitz (https://github.com/msabramo) for testing and porting this to Mac OS X. -Thanks to James Clarke (https://github.com/jrtc27) for looking into leaking of -the DTrace handles. +Thanks to Jessica Clarke (https://github.com/jrtc27) for looking into leaking +of the DTrace handles. Thanks to Alexander Richardson (https://github.com/arichardson) for doing a huge refactoring and looking into properly supporting python3. + +Thanks to Konrad Witaszczyk (https://github.com/kwitaszczyk) for looking into +properly freeing up DTrace resources. diff --git a/README.md b/README.md index c532121..80d0ff7 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,14 @@ Python as a DTrace consumer This is a first shot into looking at getting something up and running like https://github.com/bcantrill/node-libdtrace for Python. -Note: This is far from ready - documentation on writing own DTrace consumers +**_Note_**: This is far from ready - documentation on writing own DTrace consumers is very rare :-/ The codes in the **examples** folder should give an overview of how to use Python as a DTrace consumer. -Currently this package provides two modules: one wraps libdtrace using ctypes. -The other one uses cython. Should you not have cython installed the ctypes +Currently, this package provides two modules: one wraps libdtrace using ctypes; +The other one uses cython. Should you not have cython installed, the ctypes wrapper can still be used. The Python DTrace consumer can be installed via source here on GitHub, or using @@ -23,8 +23,11 @@ Cython based wrapper The Cython wrapper is more sophisticated and generally easier to use. Initializing the Python based DTrace consumer is as simple as: - from dtrace import DTraceConsumer - consumer = DTraceConsumer(walk_func=my_walk [...]) + import dtrace + + SCRIPT = 'dtrace:::BEGIN {trace("Hello World");}' + + consumer = dtrace.DTraceConsumer(walk_func=my_walk [...]) consumer.run(SCRIPT, runtime=3) The simple DTraceConsumer can be initialized with self written callbacks which @@ -54,22 +57,22 @@ The DTraceConsumer has a *run_script* function which will run a provided DTrace script for some time or till it is finished. The time on how long it is run can be provided as parameter just like the script. During the DTrace chew, chewrec and the out callbacks are called. When the run is finished the aggregation will -be walked - Thus you can aggregate for 3 seconds and than see the results. +be walked - Thus you can aggregate for 3 seconds and then see the results. There also exists a DTraceConsumerThread which can be used to continuously run DTrace in the background. This might come in handy when you want to continuously aggregate data for e.g. an GUI. The chew, chewrec, out and walk callbacks are now called as the snapshot function of DTrace is called. -The DTraceConsumerThread has an parameter sleep which defaults to 0. This means +The DTraceConsumerThread has a parameter sleep which defaults to 0. This means that the Thread will wait for DTrace for new aggregation data to arrive. This -has a major drawback since during the wait the wait the Python GIL is acquired -and your program will stop if it needs to wait for DTrace to get new data. -Setting the parameter to 1 (or higher) will let the Thread snapshot the DTrace -aggregate every second instead of waiting for new data. Both usages might make -sense - Set the sleep parameter if you know data will arrive sporadically or -simple let it default to 0 if you know data comes in all the time - so nothing -will be blocked. +has a major drawback since during the wait the Python GIL is acquired and your +program will stop if it needs to wait for DTrace to get new data. Setting the +parameter to 1 (or higher) will let the Thread snapshot the DTrace aggregate +every second instead of waiting for new data. Both usages might make sense - +set the sleep parameter if you know data will arrive sporadically or simply +let it default to 0 if you know data comes in all the time - so nothing will +be blocked. thr = DTraceConsumerThread(SCRIPT) thr.start() diff --git a/constraints.txt b/constraints.txt new file mode 100644 index 0000000..25e9025 --- /dev/null +++ b/constraints.txt @@ -0,0 +1 @@ +Cython~=3.0a6 diff --git a/dtrace_ctypes/consumer.py b/dtrace_ctypes/consumer.py index 4ac7291..d879436 100644 --- a/dtrace_ctypes/consumer.py +++ b/dtrace_ctypes/consumer.py @@ -9,8 +9,9 @@ import platform import threading import time -from ctypes import (byref, c_char_p, c_int, c_uint, c_void_p, cast, cdll, - CFUNCTYPE, POINTER) +from ctypes import (byref, c_char_p, c_int, c_size_t, c_uint, c_void_p, cast, + cdll, CDLL, CFUNCTYPE, POINTER, Structure) +from ctypes.util import find_library from threading import Thread from dtrace_ctypes.dtrace_structs import (dtrace_aggdata, dtrace_bufdata, @@ -20,11 +21,8 @@ DTRACE_WORKSTATUS_DONE, DTRACE_WORKSTATUS_ERROR) -if platform.system().startswith("Darwin"): - _LIBNAME = "libdtrace.dylib" -else: - _LIBNAME = "libdtrace.so" -_LIBRARY = cdll.LoadLibrary(_LIBNAME) +_LIBRARY = cdll.LoadLibrary(find_library("dtrace")) +_IS_MACOS = platform.system().startswith("Darwin") # ============================================================================= # chewing and output walkers @@ -54,6 +52,13 @@ def simple_chew_func(data, _arg): return 0 +def noop_chew_func(_data, _arg): + """ + No-op chew function. + """ + return 0 + + def simple_chewrec_func(_data, rec, _arg): """ Callback for record chewing. @@ -63,6 +68,9 @@ def simple_chewrec_func(_data, rec, _arg): return 0 +noop_chewrec_func = simple_chewrec_func + + def simple_buffered_out_writer(bufdata, _arg): """ In case dtrace_work is given None as filename - this one is called. @@ -72,6 +80,13 @@ def simple_buffered_out_writer(bufdata, _arg): return 0 +def noop_buffered_out_writer(_bufdata, _arg): + """ + No-op buffered out writer. + """ + return 0 + + def simple_walk(data, _arg): """ Aggregate walker capable of reading a name and one value. @@ -86,6 +101,13 @@ def simple_walk(data, _arg): return 0 + +def noop_walk(_data, _arg): + """ + No-op walker. + """ + return 0 + # ============================================================================= # LibDTrace function wrapper class # ============================================================================= @@ -186,37 +208,83 @@ def _dtrace_open(): return handle +class FILE(Structure): + """ + Basic dummy structure. + """ + + +if _IS_MACOS: + # These are needed to work around a broken libdtrace on macOS. + libc_open_memstream = CDLL('libc.dylib').open_memstream + libc_open_memstream.restype = POINTER(FILE) + libc_open_memstream.argtypes = [POINTER(c_void_p), POINTER(c_size_t)] + libc_fclose = CDLL('libc.dylib').fclose + libc_fclose.argtypes = [POINTER(FILE)] + libc_free = CDLL('libc.dylib').free + libc_free.argtypes = [c_void_p] + + +def _get_dtrace_work_fp(): + if _IS_MACOS: + # Note: macOS crashes if we pass NULL for fp (FreeBSD works fine) + # Use open_memstream() as a workaround for this bug. + memstream = c_void_p(None) + size = c_size_t(0) + f_p = libc_open_memstream(byref(memstream), byref(size)) + assert cast(f_p, c_void_p).value != c_void_p(None).value + return f_p, memstream + return None, None + + +def _dtrace_sleep_and_work(consumer): + LIBRARY.dtrace_sleep(consumer.handle) + f_p, memstream = _get_dtrace_work_fp() + status = LIBRARY.dtrace_work(consumer.handle, f_p, consumer.chew, + consumer.chew_rec, None) + if f_p is not None: + assert memstream.value != 0, memstream + libc_fclose(f_p) # buffer is valid after fclose(). + tmp = dtrace_bufdata() + tmp.dtbda_buffered = cast(memstream, c_char_p) + consumer.buf_out(byref(tmp), None) + if status == DTRACE_WORKSTATUS_ERROR: + raise Exception('dtrace_work failed: ', + get_error_msg(consumer.handle)) + return status + + class DTraceConsumer: """ A Pyton based DTrace consumer. """ def __init__(self, - chew_func=None, - chew_rec_func=None, - walk_func=None, - out_func=None): + chew_func=simple_chew_func, + chew_rec_func=simple_chewrec_func, + walk_func=simple_walk, + out_func=simple_buffered_out_writer): """ Constructor. will get the DTrace handle """ if chew_func is not None: self.chew = CHEW_FUNC(chew_func) else: - self.chew = CHEW_FUNC(simple_chew_func) + self.chew = CHEW_FUNC(noop_chew_func) if chew_rec_func is not None: self.chew_rec = CHEWREC_FUNC(chew_rec_func) else: - self.chew_rec = CHEWREC_FUNC(simple_chewrec_func) + self.chew_rec = CHEWREC_FUNC(noop_chewrec_func) if walk_func is not None: self.walk = WALK_FUNC(walk_func) else: - self.walk = WALK_FUNC(simple_walk) + self.walk = WALK_FUNC(noop_walk) if out_func is not None: self.buf_out = BUFFERED_FUNC(out_func) else: - self.buf_out = BUFFERED_FUNC(simple_buffered_out_writer) + self.buf_out = BUFFERED_FUNC(noop_buffered_out_writer) # get dtrace handle self.handle = _dtrace_open() @@ -263,12 +331,7 @@ def run(self, script, runtime=1): # aggregate data for a few sec... i = 0 while i < runtime: - LIBRARY.dtrace_sleep(self.handle) - status = LIBRARY.dtrace_work(self.handle, None, self.chew, - self.chew_rec, None) - if status == DTRACE_WORKSTATUS_ERROR: - raise Exception('dtrace_work failed: ', - get_error_msg(self.handle)) + status = _dtrace_sleep_and_work(self) if status == DTRACE_WORKSTATUS_DONE: break # No more work assert status == DTRACE_WORKSTATUS_OKAY, status @@ -293,10 +356,10 @@ class DTraceConsumerThread(Thread): def __init__(self, script, - chew_func=None, - chew_rec_func=None, - walk_func=None, - out_func=None): + chew_func=simple_chew_func, + chew_rec_func=simple_chewrec_func, + walk_func=simple_walk, + out_func=simple_buffered_out_writer): """ Constructor. will get the DTrace handle """ @@ -307,22 +370,22 @@ def __init__(self, if chew_func is not None: self.chew = CHEW_FUNC(chew_func) else: - self.chew = CHEW_FUNC(simple_chew_func) + self.chew = CHEW_FUNC(noop_chew_func) if chew_rec_func is not None: self.chew_rec = CHEWREC_FUNC(chew_rec_func) else: - self.chew_rec = CHEWREC_FUNC(simple_chewrec_func) + self.chew_rec = CHEWREC_FUNC(noop_chewrec_func) if walk_func is not None: self.walk = WALK_FUNC(walk_func) else: - self.walk = WALK_FUNC(simple_walk) + self.walk = WALK_FUNC(noop_walk) if out_func is not None: self.buf_out = BUFFERED_FUNC(out_func) else: - self.buf_out = BUFFERED_FUNC(simple_buffered_out_writer) + self.buf_out = BUFFERED_FUNC(noop_buffered_out_writer) # get dtrace handle self.handle = _dtrace_open() @@ -360,12 +423,7 @@ def run(self): # aggregate data for a few sec... while not self.stopped(): - LIBRARY.dtrace_sleep(self.handle) - status = LIBRARY.dtrace_work(self.handle, None, self.chew, - self.chew_rec, None) - if status == DTRACE_WORKSTATUS_ERROR: - raise Exception('dtrace_work failed: ', - get_error_msg(self.handle)) + status = _dtrace_sleep_and_work(self) if status == DTRACE_WORKSTATUS_DONE: break # No more work assert status == DTRACE_WORKSTATUS_OKAY, status diff --git a/dtrace_cython/consumer.pyx b/dtrace_cython/consumer.pyx index 7ffd9b4..0401000 100644 --- a/dtrace_cython/consumer.pyx +++ b/dtrace_cython/consumer.pyx @@ -2,8 +2,14 @@ from __future__ import print_function, division import time import threading from threading import Thread + from dtrace_cython.dtrace_h cimport * from libc.stdint cimport INT64_MAX, INT64_MIN +from libc.stdio cimport fclose +from libc.stdlib cimport free +from posix.stdio cimport open_memstream + +from cython.operator cimport dereference # ---------------------------------------------------------------------------- # The DTrace callbacks @@ -73,6 +79,9 @@ cdef int walk(const dtrace_aggdata_t * data, void * arg) with gil: cdef dtrace_recdesc_t *rec cdef int64_t *tmp + cdef dtrace_syminfo_t foo + cdef GElf_Sym sym + aggrec = &desc.dtagd_rec[desc.dtagd_nrecs - 1] action = aggrec.dtrd_action @@ -82,7 +91,17 @@ cdef int walk(const dtrace_aggdata_t * data, void * arg) with gil: # TODO: need to extend this. if rec.dtrd_size == sizeof(uint32_t): - keys.append((address)[0]) + keys.append((address)[0]) + elif rec.dtrd_size == sizeof(uint64_t): + keys.append(( address)[0]) + elif rec.dtrd_size == 20 * sizeof(uint64_t): + # case stack() has been used --> need to lookup symbols. + for j in range(rec.dtrd_arg): + pc = dereference(address) + ret = dtrace_lookup_by_addr(data.dtada_handle, pc, &sym, &foo) + if ret == 0: + keys.append((foo.dts_object,foo.dts_name)) + address += sizeof(uint64_t) else: keys.append(address) @@ -186,6 +205,13 @@ def simple_chew(cpu): print('Running on CPU:', cpu) +def noop_chew(_cpu): + """ + No-op chew function. + """ + pass + + def simple_chewrec(action): """ Simple chewrec callback. @@ -195,6 +221,13 @@ def simple_chewrec(action): print('Called action was:', action) +def noop_chewrec(_action): + """ + No-op chewrec function. + """ + pass + + def simple_out(value): """ A buffered output handler for all those prints. @@ -204,6 +237,13 @@ def simple_out(value): print('Value is:', value) +def noop_out(_value): + """ + No-op out function. + """ + pass + + def simple_walk(action, identifier, keys, value): """ Simple aggregation walker. @@ -216,14 +256,40 @@ def simple_walk(action, identifier, keys, value): """ print(action, identifier, keys, value) + +def noop_walk(_action, _identifier, _keys, _value): + """ + No-op walker. + """ + pass + # ---------------------------------------------------------------------------- # The consumers # ---------------------------------------------------------------------------- +cdef int _dtrace_sleep_and_work(dtrace_hdl_t * handle, void * args, out_func): + cdef FILE *fp = NULL + IF UNAME_SYSNAME == "Darwin": + cdef char *memstream + cdef size_t size + # Note: macOS crashes if we pass NULL for fp (FreeBSD works fine) + # As a workaround we use open_memstream to write to memory. + fp = open_memstream(&memstream, &size) + assert fp != NULL + status = dtrace_work(handle, fp, &chew, &chewrec, args) + IF UNAME_SYSNAME == "Darwin": + fclose(fp) + out_func(( memstream).strip()) + free(memstream) + if status == DTRACE_WORKSTATUS_ERROR: + raise Exception('dtrace_work failed: ', + dtrace_errmsg(handle, dtrace_errno(handle))) + return status + cdef class DTraceConsumer: """ - A Pyton based DTrace consumer. + A Python based DTrace consumer. """ cdef dtrace_hdl_t * handle @@ -232,18 +298,21 @@ cdef class DTraceConsumer: cdef object chew_func cdef object chewrec_func - def __init__(self, chew_func=None, chewrec_func=None, out_func=None, - walk_func=None): + def __init__(self, chew_func=simple_chew, chewrec_func=simple_chewrec, + out_func=simple_out, walk_func=simple_walk): """ Constructor. Gets a DTrace handle and sets some options. + Passing None as one of the callback arguments installs a no-op + callback. """ - self.chew_func = chew_func or simple_chew - self.chewrec_func = chewrec_func or simple_chewrec - self.out_func = out_func or simple_out - self.walk_func = walk_func or simple_walk + self.chew_func = noop_chew if chew_func is None else chew_func + self.chewrec_func = noop_chewrec if chewrec_func is None \ + else chewrec_func + self.out_func = noop_out if out_func is None else out_func + self.walk_func = noop_walk if walk_func is None else walk_func cdef int err - if not hasattr(self, 'handle'): + if self.handle == NULL: # ensure we only grab 1 - cython might call init twice, of more. self.handle = dtrace_open(3, 0, &err) if self.handle == NULL: @@ -262,10 +331,11 @@ cdef class DTraceConsumer: """ Release DTrace handle. """ - if hasattr(self, 'handle') and self.handle != NULL: + if self.handle != NULL: dtrace_close(self.handle) + self.handle = NULL - cpdef compile(self, unicode script): + cpdef compile(self, str script): """ Compile a DTrace script and return errors if any. @@ -279,7 +349,7 @@ cdef class DTraceConsumer: dtrace_errmsg(self.handle, dtrace_errno(self.handle))) - cpdef run(self, unicode script, runtime=1): + cpdef run(self, str script, runtime=1): """ Run a DTrace script for a number of seconds defined by the runtime. @@ -316,11 +386,10 @@ cdef class DTraceConsumer: i = 0 args = (self.chew_func, self.chewrec_func) - while i < runtime: dtrace_sleep(self.handle) - status = dtrace_work(self.handle, NULL, & chew, & chewrec, - args) + status = _dtrace_sleep_and_work(self.handle, args, + self.out_func) if status == DTRACE_WORKSTATUS_DONE: break elif status == DTRACE_WORKSTATUS_ERROR: @@ -355,19 +424,21 @@ cdef class DTraceContinuousConsumer: cdef object chewrec_func cdef object script - def __init__(self, unicode script, chew_func=None, chewrec_func=None, - out_func=None, walk_func=None): + def __init__(self, str script, chew_func=simple_chew, + chewrec_func=simple_chewrec, out_func=simple_out, + walk_func=simple_walk): """ Constructor. will get the DTrace handle """ - self.chew_func = chew_func or simple_chew - self.chewrec_func = chewrec_func or simple_chewrec - self.out_func = out_func or simple_out - self.walk_func = walk_func or simple_walk + self.chew_func = noop_chew if chew_func is None else chew_func + self.chewrec_func = noop_chewrec if chewrec_func is None \ + else chewrec_func + self.out_func = noop_out if out_func is None else out_func + self.walk_func = noop_walk if walk_func is None else walk_func self.script = script.encode("utf-8") cdef int err - if not hasattr(self, 'handle'): + if self.handle == NULL: # ensure we only grab 1 - cython might call init twice, of more. self.handle = dtrace_open(3, 0, &err) if self.handle == NULL: @@ -386,9 +457,10 @@ cdef class DTraceContinuousConsumer: """ Release DTrace handle. """ - if hasattr(self, 'handle') and self.handle != NULL: + if self.handle != NULL: dtrace_stop(self.handle) dtrace_close(self.handle) + self.handle = NULL cpdef go(self): """ @@ -431,12 +503,8 @@ cdef class DTraceContinuousConsumer: Snapshot the data and walk the aggregate. """ args = (self.chew_func, self.chewrec_func) - status = dtrace_work(self.handle, NULL, & chew, & chewrec, - args) - if status == DTRACE_WORKSTATUS_ERROR: - raise Exception('dtrace_work failed: ', - dtrace_errmsg(self.handle, - dtrace_errno(self.handle))) + status = _dtrace_sleep_and_work(self.handle, args, + self.out_func) if dtrace_aggregate_snap(self.handle) != 0: raise Exception('Failed to get the aggregate: ', dtrace_errmsg(self.handle, @@ -455,13 +523,14 @@ class DTraceConsumerThread(Thread): Helper Thread which can be used to continuously aggregate. """ - def __init__(self, script, consume=True, chew_func=None, chewrec_func=None, - out_func=None, walk_func=None, sleep=0): + def __init__(self, script, consume=True, chew_func=simple_chew, + chewrec_func=simple_chewrec, out_func=simple_out, + walk_func=simple_walk, sleep=0): """ Initilizes the Thread. """ Thread.__init__(self) - self._stop = threading.Event() + self._should_stop = threading.Event() self.sleep_time = sleep self.consume = consume self.consumer = DTraceContinuousConsumer(script, chew_func, @@ -472,9 +541,12 @@ class DTraceConsumerThread(Thread): """ Make sure DTrace stops. """ - if not self.consume: - self.consumer.snapshot() - del self.consumer + # This is called even if __init__ raises, so we have to check whether + # consumer was initialized. + if hasattr(self, "consumer"): + if not self.consume: + self.consumer.snapshot() + del self.consumer def run(self): """ @@ -491,17 +563,17 @@ class DTraceConsumerThread(Thread): if self.consume: status = self.consumer.snapshot() - if status == 1: + if status == DTRACE_WORKSTATUS_DONE: self.stop() def stop(self): """ Stop DTrace. """ - self._stop.set() + self._should_stop.set() def stopped(self): """ Used to check the status. """ - return self._stop.isSet() + return self._should_stop.isSet() diff --git a/dtrace_cython/dtrace_h.pxd b/dtrace_cython/dtrace_h.pxd index a841a80..cad75e1 100644 --- a/dtrace_cython/dtrace_h.pxd +++ b/dtrace_cython/dtrace_h.pxd @@ -1,5 +1,12 @@ from libc.stdint cimport uint16_t, int32_t, uint32_t, int64_t, uint64_t +from libc.stdio cimport FILE + +cdef extern from "gelf.h": + # used for symbol lookup while using stack() + + ctypedef struct GElf_Sym: + pass cdef extern from "libelf_workaround.h": @@ -17,7 +24,7 @@ cdef extern from "sys/dtrace.h": int DTRACEAGG_MAX int DTRACEAGG_AVG int DTRACEAGG_SUM - int DTRACEAGG_STDDEV # (unsupported) + int DTRACEAGG_STDDEV # (unsupported) int DTRACEAGG_QUANTIZE int DTRACEAGG_LQUANTIZE @@ -26,6 +33,7 @@ cdef extern from "sys/dtrace.h": uint16_t dtrd_action uint32_t dtrd_offset uint32_t dtrd_size + uint64_t dtrd_arg ctypedef struct dtrace_aggdesc_t: # Taken from sys/dtrace.h:950 @@ -88,9 +96,15 @@ cdef extern from "dtrace.h": ctypedef struct dtrace_aggdata_t: # Taken from dtrace.h:351 + dtrace_hdl_t * dtada_handle dtrace_aggdesc_t * dtada_desc char * dtada_data + ctypedef struct dtrace_syminfo_t: + const char * dts_object + const char * dts_name + long dts_id + # from dtrace.h ctypedef int dtrace_handle_buffered_f(const dtrace_bufdata_t * buf_data, void * arg) ctypedef int dtrace_consume_probe_f(const dtrace_probedata_t *, void *) @@ -118,13 +132,16 @@ cdef extern from "dtrace.h": int dtrace_go(dtrace_hdl_t *) int dtrace_stop(dtrace_hdl_t *) void dtrace_sleep(dtrace_hdl_t *) - dtrace_workstatus_t dtrace_work(dtrace_hdl_t * , char * , dtrace_consume_probe_f * , dtrace_consume_rec_f * , void *) + dtrace_workstatus_t dtrace_work(dtrace_hdl_t * , FILE * , dtrace_consume_probe_f * , dtrace_consume_rec_f * , void *) # walking aggregate int dtrace_aggregate_walk_valsorted(dtrace_hdl_t * , dtrace_aggregate_f * , void *) int dtrace_aggregate_snap(dtrace_hdl_t *) int dtrace_aggregate_walk(dtrace_hdl_t *, dtrace_aggregate_f *, void *) + # Dealing with stack() + int dtrace_lookup_by_addr(dtrace_hdl_t *, uint64_t addr, GElf_Sym *, dtrace_syminfo_t *) + # error handling... int dtrace_errno(dtrace_hdl_t * handle) char * dtrace_errmsg(dtrace_hdl_t * handle, int error) diff --git a/examples/cli.py b/examples/cli.py index 44e7afa..6adfd43 100755 --- a/examples/cli.py +++ b/examples/cli.py @@ -8,10 +8,13 @@ @author: tmetsch """ + from __future__ import print_function -from dtrace import DTraceConsumerThread + import sys +import dtrace + def print_lquantize(values): """ @@ -26,14 +29,14 @@ def print_lquantize(values): for item in values: if item[0][0] > 0: print('%10s ' % item[0][0], end=' ') - for i in range(0, ((40 * int(item[1])) / maxi)): + for _ in range(0, ((40 * int(item[1])) / maxi)): sys.stdout.write('*') - for i in range(((40 * int(item[1])) / maxi), 40): + for _ in range(((40 * int(item[1])) / maxi), 40): sys.stdout.write(' ') print(' %5s' % item[1]) -def pretty_print(iden, action, keys, values): +def pretty_print(_iden, action, keys, values): """ Pretty print aggregation walker. """ @@ -60,7 +63,7 @@ def run_dtrace(script): """ Run DTrace till Ctrl+C is pressed... """ - thr = DTraceConsumerThread(script, False, walk_func=pretty_print) + thr = dtrace.DTraceConsumerThread(script, False, walk_func=pretty_print) thr.start() brendan() try: @@ -70,6 +73,7 @@ def run_dtrace(script): thr.stop() thr.join() + if __name__ == '__main__': # run_dtrace('dtrace:::BEGIN {trace("Hello World"); exit(0);}') # run_dtrace('syscall:::entry { @num[pid,execname] = count(); }') diff --git a/examples/hello_world.py b/examples/hello_world.py index 3484f45..c345bee 100755 --- a/examples/hello_world.py +++ b/examples/hello_world.py @@ -8,7 +8,7 @@ @author: tmetsch """ -from dtrace import DTraceConsumer +import dtrace SCRIPT = 'dtrace:::BEGIN {trace("Hello World"); exit(0);}' @@ -17,10 +17,11 @@ def main(): """ Run DTrace... """ - consumer = DTraceConsumer() + consumer = dtrace.DTraceConsumer() # Even when the runtime is set to run 10sec this will terminate immediately # because of the exit statement in the D script. consumer.run(SCRIPT, runtime=10) + if __name__ == '__main__': main() diff --git a/examples/read_bytes.py b/examples/read_bytes.py index c542459..fa1d06d 100755 --- a/examples/read_bytes.py +++ b/examples/read_bytes.py @@ -8,7 +8,8 @@ @author: tmetsch """ from __future__ import print_function -from dtrace import DTraceConsumer + +import dtrace SCRIPT = 'sysinfo:::readch { @bytes[execname] = sum(arg0); }' @@ -19,8 +20,9 @@ def main(): """ print('Hint: if you don\'t get any output try running it with pfexec...') - consumer = DTraceConsumer() + consumer = dtrace.DTraceConsumer() consumer.run(SCRIPT, 4) + if __name__ == '__main__': main() diff --git a/examples/read_distribution_quantize.py b/examples/read_distribution_quantize.py index cb2c7e0..1ce625b 100755 --- a/examples/read_distribution_quantize.py +++ b/examples/read_distribution_quantize.py @@ -8,13 +8,14 @@ @author: tmetsch """ from __future__ import print_function -from dtrace import DTraceConsumer + +import dtrace # SCRIPT = 'io:::start { @bytes = quantize(args[0]->b_bcount); }' SCRIPT = 'sysinfo:::readch { @dist[execname] = quantize(arg0); }' -def my_walk(action, identifier, key, values): +def my_walk(_action, _identifier, key, values): """ Walk the aggregrate. """ @@ -28,8 +29,9 @@ def main(): """ Run DTrace... """ - consumer = DTraceConsumer(walk_func=my_walk) + consumer = dtrace.DTraceConsumer(walk_func=my_walk) consumer.run(SCRIPT, 10) + if __name__ == '__main__': main() diff --git a/examples/stack_example.py b/examples/stack_example.py new file mode 100644 index 0000000..2d4cd9a --- /dev/null +++ b/examples/stack_example.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +""" +Use the Python DTrace consumer and use stack(). +""" + +import dtrace + +SCRIPT = "profile-99 /arg0/ { @foo[stack()] = count();}" + + +def main(): + """ + Run DTrace... + """ + print('Hint: if you don\'t get any output try running it with pfexec...') + + consumer = dtrace.DTraceConsumer() + consumer.run(SCRIPT, 4) + + +if __name__ == '__main__': + main() diff --git a/examples/syscall_by_zone.py b/examples/syscall_by_zone.py index cfa3ebb..ca73ae1 100755 --- a/examples/syscall_by_zone.py +++ b/examples/syscall_by_zone.py @@ -8,24 +8,26 @@ @author: tmetsch """ from __future__ import print_function -from dtrace import DTraceConsumerThread + import time +import dtrace + SCRIPT = 'syscall:::entry { @num[zonename] = count(); }' -def my_walk(action, identifier, key, value): +def my_walk(_action, _identifier, key, value): """ A simple aggregate walker. """ - print('Zone "{0:s}" made {1:d} syscalls.'.format(key[0], value)) + print('Zone "{0:s}" made {1:d} syscalls.'.format(key[0].decode(), value)) def main(): """ Run DTrace... """ - thr = DTraceConsumerThread(SCRIPT, walk_func=my_walk, sleep=1) + thr = dtrace.DTraceConsumerThread(SCRIPT, walk_func=my_walk, sleep=1) thr.start() # we will stop the thread after some time... @@ -35,5 +37,6 @@ def main(): thr.stop() thr.join() + if __name__ == '__main__': main() diff --git a/examples/syscall_count.py b/examples/syscall_count.py index b597622..f4fe339 100755 --- a/examples/syscall_count.py +++ b/examples/syscall_count.py @@ -8,7 +8,7 @@ @author: tmetsch """ -from dtrace import DTraceConsumer +import dtrace SCRIPT = 'syscall:::entry { @num[pid,execname] = count(); }' @@ -17,8 +17,9 @@ def main(): """ Run DTrace... """ - consumer = DTraceConsumer() + consumer = dtrace.DTraceConsumer() consumer.run(SCRIPT, 2) + if __name__ == '__main__': main() diff --git a/examples/syscall_count_continuous.py b/examples/syscall_count_continuous.py index 34e4113..08604a8 100755 --- a/examples/syscall_count_continuous.py +++ b/examples/syscall_count_continuous.py @@ -9,9 +9,10 @@ @author: tmetsch """ -from dtrace import DTraceConsumerThread import time +import dtrace + SCRIPT = 'syscall:::entry { @num[execname] = count(); }' @@ -19,7 +20,7 @@ def main(): """ Run DTrace... """ - thr = DTraceConsumerThread(SCRIPT) + thr = dtrace.DTraceConsumerThread(SCRIPT) thr.start() # we will stop the thread after some time... @@ -29,5 +30,6 @@ def main(): thr.stop() thr.join() + if __name__ == '__main__': main() diff --git a/examples/syscall_count_own_walk.py b/examples/syscall_count_own_walk.py index 87ca7a2..6a9c47f 100755 --- a/examples/syscall_count_own_walk.py +++ b/examples/syscall_count_own_walk.py @@ -9,12 +9,13 @@ @author: tmetsch """ from __future__ import print_function -from dtrace import DTraceConsumer + +import dtrace SCRIPT = 'syscall:::entry { @num[execname] = count(); }' -def my_walk(action, identifier, key, value): +def my_walk(_action, identifier, key, value): """ Aggregate walker. """ @@ -25,8 +26,9 @@ def main(): """ Run DTrace... """ - consumer = DTraceConsumer(walk_func=my_walk) + consumer = dtrace.DTraceConsumer(walk_func=my_walk) consumer.run(SCRIPT, 4) + if __name__ == '__main__': main() diff --git a/examples/syscall_lquantize.py b/examples/syscall_lquantize.py index 8656027..3d429c9 100755 --- a/examples/syscall_lquantize.py +++ b/examples/syscall_lquantize.py @@ -8,12 +8,13 @@ @author: tmetsch """ from __future__ import print_function -from dtrace import DTraceConsumer + +import dtrace SCRIPT = 'syscall::read:entry { @dist[execname] = lquantize(arg0, 0, 12, 2); }' -def my_walk(action, identifier, key, values): +def my_walk(_action, _identifier, key, values): """ Walk the aggregate. """ @@ -27,8 +28,9 @@ def main(): """ Run DTrace... """ - consumer = DTraceConsumer(walk_func=my_walk) + consumer = dtrace.DTraceConsumer(walk_func=my_walk) consumer.run(SCRIPT, 5) + if __name__ == '__main__': main() diff --git a/setup.py b/setup.py index 968aa29..6f573ea 100644 --- a/setup.py +++ b/setup.py @@ -10,15 +10,40 @@ from distutils.core import setup from distutils.extension import Extension +import os +import platform import sys try: from Cython.Build import build_ext, cythonize - + extra_args = {} + if platform.system().lower().startswith("freebsd"): + # On older FreeBSD versions the dtrace headers are not + # installed by default, so we need to find the full sources. + if not os.path.exists("/usr/include/dtrace.h"): + src_dir = os.getenv("FREEBSD_SRC_DIR", "/usr/src") + if not os.path.exists(os.path.join(src_dir, "sys/cddl")): + raise ImportError("Cannot find FreeBSD DTrace headers") + extra_args["include_dirs"] = [ + os.path.join(src_dir, + "sys/cddl/compat/opensolaris"), + os.path.join(src_dir, + "sys/cddl/contrib/opensolaris/uts/common"), + os.path.join(src_dir, + "cddl/contrib/opensolaris/lib/libdtrace/common"), + ] + if os.getenv("ENABLE_ASAN", None) is not None: + extra_args["extra_compile_args"] = ["-fsanitize=address"] + extra_args["extra_link_args"] = ["-fsanitize=address"] BUILD_EXTENSION = {'build_ext': build_ext} - EXT_MODULES = cythonize([Extension("dtrace", ["dtrace_cython/dtrace_h.pxd", - "dtrace_cython/consumer.pyx"], - libraries=["dtrace"])], - language_level=sys.version_info.major) + EXT_MODULES = cythonize( + [ + Extension("dtrace", ["dtrace_cython/dtrace_h.pxd", + "dtrace_cython/consumer.pyx"], + libraries=["dtrace"], + **extra_args) + ], + language_level=sys.version_info.major + ) except ImportError: BUILD_EXTENSION = {} @@ -29,13 +54,13 @@ setup(name='python-dtrace', - version='0.0.11', + version='0.0.15', description='DTrace consumer for Python based on libdtrace. Use Python' - + ' as DTrace Consumer and Provider! See the homepage for' - + ' more information.', + ' as DTrace Consumer and Provider! See the homepage for' + ' more information.', license='MIT', keywords='DTrace', - url='http://tmetsch.github.com/python-dtrace/', + url='http://tmetsch.github.io/python-dtrace/', packages=['dtrace_ctypes'], cmdclass=BUILD_EXTENSION, ext_modules=EXT_MODULES, diff --git a/tests/__init__.py b/tests/__init__.py index 49a2fb9..6aa8002 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1,4 @@ +""" +Unit tests. +""" __author__ = 'tmetsch' diff --git a/tests/dtrace_ctypes_test.py b/tests/dtrace_ctypes_test.py index 576b45c..8cf729f 100644 --- a/tests/dtrace_ctypes_test.py +++ b/tests/dtrace_ctypes_test.py @@ -4,11 +4,12 @@ __author__ = 'tmetsch' -from dtrace_ctypes import consumer + +import unittest from ctypes import c_char_p -import unittest +from dtrace_ctypes import consumer SCRIPT = 'dtrace:::BEGIN {trace("Hello World");}' @@ -19,16 +20,23 @@ class TestDTraceConsumer(unittest.TestCase): """ def setUp(self): - self.out = '' + self.out = b'' self.consumer = consumer.DTraceConsumer(out_func=self._get_output) def test_run_for_success(self): + """ + Test for success. + """ self.consumer.run(SCRIPT) def test_run_for_sanity(self): + """ + Test for sanity. + """ self.consumer.run(SCRIPT) self.assertEqual(self.out, b'Hello World') - def _get_output(self, data, arg): + def _get_output(self, data, _arg): tmp = c_char_p(data.contents.dtbda_buffered).value.strip() - self.out = tmp + self.out += tmp + return 0 diff --git a/tests/dtrace_cython_test.py b/tests/dtrace_cython_test.py index 75dcdc6..cc47cc3 100644 --- a/tests/dtrace_cython_test.py +++ b/tests/dtrace_cython_test.py @@ -4,9 +4,10 @@ __author__ = 'tmetsch' -import dtrace import unittest +import dtrace + SCRIPT = 'dtrace:::BEGIN {trace("Hello World");}' @@ -22,24 +23,42 @@ def setUp(self): # Test fo success. def test_compile_for_success(self): + """ + Test for success. + """ self.consumer.compile(SCRIPT) def test_run_for_success(self): + """ + Test for success. + """ self.consumer.run(SCRIPT) # Test fo failure. def test_compile_for_failure(self): + """ + Test for failure. + """ self.assertRaises(Exception, self.consumer.compile, 'foo') def test_run_for_failure(self): + """ + Test for failure. + """ self.assertRaises(Exception, self.consumer.run, 'sadf') # Test fo sanity. def test_run_for_sanity(self): + """ + Test for sanity. + """ self.consumer.run(SCRIPT) self.assertEqual(b'Hello World', self.out) def _get_output(self, tmp): + """ + Test for sanity. + """ self.out = tmp