# -*- coding: us-ascii -*- # frozen_string_literal: false require 'test/unit' require 'tempfile' if Process.euid == 0 ok = true elsif (sudo = ENV["SUDO"]) and !sudo.empty? and (`#{sudo} echo ok` rescue false) ok = true else ok = false end impl = :dtrace # GNU/Linux distros with Systemtap support allows unprivileged users # in the stapusr and statdev groups to work. if RUBY_PLATFORM =~ /linux/ impl = :stap begin require 'etc' login = Etc.getlogin ok = Etc.getgrnam('stapusr').mem.include?(login) && Etc.getgrnam('stapdev').mem.include?(login) rescue LoadError, ArgumentError end unless ok end if ok case RUBY_PLATFORM when /darwin/i begin require 'pty' rescue LoadError ok = false end end end # use miniruby to reduce the amount of trace data we don't care about rubybin = "miniruby#{RbConfig::CONFIG["EXEEXT"]}" rubybin = File.join(File.dirname(EnvUtil.rubybin), rubybin) rubybin = EnvUtil.rubybin unless File.executable?(rubybin) # make sure ruby was built with --enable-dtrace and we can run # dtrace(1) or stap(1): cmd = "#{rubybin} --disable=gems -eexit" case impl when :dtrace; cmd = %W(dtrace -l -n ruby$target:::gc-sweep-end -c #{cmd}) when :stap; cmd = %W(stap -l process.mark("gc__sweep__end") -c #{cmd}) else warn "don't know how to check if built with #{impl} support" cmd = false end ok &= system(*cmd, err: IO::NULL, out: IO::NULL) if cmd module DTrace class TestCase < Test::Unit::TestCase INCLUDE = File.expand_path('..', File.dirname(__FILE__)) case RUBY_PLATFORM when /solaris/i # increase bufsize to 8m (default 4m on Solaris) DTRACE_CMD = %w[dtrace -b 8m] when /darwin/i READ_PROBES = proc do |cmd| lines = nil PTY.spawn(*cmd) do |io, _, pid| lines = io.readlines.each {|line| line.sub!(/\r$/, "")} Process.wait(pid) end lines end end # only handles simple cases, use a Hash for d_program # if there are more complex cases def dtrace2systemtap(d_program) translate = lambda do |str| # dtrace starts args with '0', systemtap with '1' and prefixes '$' str = str.gsub(/\barg(\d+)/) { "$arg#{$1.to_i + 1}" } # simple function mappings: str.gsub!(/\bcopyinstr\b/, 'user_string') str.gsub!(/\bstrstr\b/, 'isinstr') str end out = '' cond = nil d_program.split(/^/).each do |l| case l when /\bruby\$target:::([a-z-]+)/ name = $1.gsub(/-/, '__') out << %Q{probe process.mark("#{name}")\n} when %r{/(.+)/} cond = translate.call($1) when "{\n" out << l out << "if (#{cond}) {\n" if cond when "}\n" out << "}\n" if cond out << l else out << translate.call(l) end end out end DTRACE_CMD ||= %w[dtrace] READ_PROBES ||= proc do |cmd| IO.popen(cmd, err: [:child, :out], &:readlines) end def trap_probe d_program, ruby_program if Hash === d_program d_program = d_program[IMPL] or skip "#{d_program} not implemented for #{IMPL}" elsif String === d_program && IMPL == :stap d_program = dtrace2systemtap(d_program) end d = Tempfile.new(%w'probe .d') d.write d_program d.flush rb = Tempfile.new(%w'probed .rb') rb.write ruby_program rb.flush d_path = d.path rb_path = rb.path cmd = "#{RUBYBIN} --disable=gems -I#{INCLUDE} #{rb_path}" if IMPL == :stap cmd = %W(stap #{d_path} -c #{cmd}) else cmd = [*DTRACE_CMD, "-q", "-s", d_path, "-c", cmd ] end if sudo = @@sudo [RbConfig::CONFIG["LIBPATHENV"], "RUBY", "RUBYOPT"].each do |name| if name and val = ENV[name] cmd.unshift("#{name}=#{val}") end end cmd.unshift(sudo) end probes = READ_PROBES.(cmd) d.close(true) rb.close(true) yield(d_path, rb_path, probes) end end end if ok if ok DTrace::TestCase.class_variable_set(:@@sudo, sudo) DTrace::TestCase.const_set(:IMPL, impl) DTrace::TestCase.const_set(:RUBYBIN, rubybin) end