Skip to content

Instantly share code, notes, and snippets.

@mscottford
Created December 16, 2009 15:09
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save mscottford/257889 to your computer and use it in GitHub Desktop.
require 'optparse'
require 'ostruct'
require 'Mono.Cecil'
class System::Reflection::Assembly
def get_type_from_name(name)
method(:GetType).overload(System::String).call(name)
end
end
class System::Type
def get_method_from_name_and_type(name, type)
self.method(:GetMethod).
overload(System::String, System::Array.of(System::Type)).
call(name, System::Array.of(System::Type).new([type.to_clr_type]))
end
end
module Log4NetInfo
def log_manager_get_logger
log_manager = assembly.get_type_from_name('log4net.LogManager')
raise "Could not load LogManager" if log_manager.nil?
log_manager.get_method_from_name_and_type('GetLogger', System::String)
end
def log_interface_debug
log_interface = assembly.get_type_from_name('log4net.ILog')
raise "Could not load ILog" if log_interface.nil?
log_interface.get_method_from_name_and_type('Debug', System::Object)
end
private
def assembly
IronRuby.require('log4net')
end
end
class CilWorkerHelper
def initialize(assembly, worker, insert_point)
@assembly = assembly
@worker = worker
@insert_point = insert_point
@last_instruction = nil
end
def ldstr(message)
insert(create_ldstr(message))
end
def call(clr_method)
insert(create_call(clr_method))
end
def call_virt(clr_method)
insert(create_call_virt(clr_method))
end
private
def insert(instruction)
if @last_instruction.nil?
@worker.InsertBefore(@insert_point, instruction)
else
@worker.InsertAfter(@last_instruction, instruction)
end
@last_instruction = instruction
end
def create_call(clr_method)
op_code = Mono::Cecil::Cil::OpCodes.Call
method_reference = @assembly.MainModule.Import(clr_method)
@worker.
method(:Create).
overload(Mono::Cecil::Cil::OpCode, Mono::Cecil::MethodReference).
call(op_code, method_reference)
end
def create_call_virt(clr_method)
op_code = Mono::Cecil::Cil::OpCodes.Callvirt
method_reference = @assembly.MainModule.Import(clr_method)
@worker.
method(:Create).
overload(Mono::Cecil::Cil::OpCode, Mono::Cecil::MethodReference).
call(op_code, method_reference)
end
def create_ldstr(string)
op_code = Mono::Cecil::Cil::OpCodes.Ldstr
@worker.
method(:Create).
overload(Mono::Cecil::Cil::OpCode, System::String).
call(op_code, string)
end
end
class Instrumentor
include Log4NetInfo
def initialize(assembly_name)
@assembly_name = assembly_name
@assembly = Mono::Cecil::AssemblyFactory.method(:GetAssembly).overload(System::String).call(assembly_name)
end
def apply
@assembly.MainModule.Types.each do |type|
type.Constructors.each do |constructor|
name = "#{type.Namespace}.#{type.Name}.ctor"
puts "Instrumenting #{name}"
instrument(constructor, "Entering #{name}", "Exiting #{name}")
end
type.Methods.each do |method|
name = "#{type.Namespace}.#{type.Name}.#{method.Name}"
puts "Instrumenting #{name}"
instrument(method, "Entering #{name}", "Exiting #{name}")
end
end
puts "Saving instrumented assembly..."
Mono::Cecil::AssemblyFactory.SaveAssembly(@assembly, @assembly_name)
puts "done."
end
def remove
modified = false
@assembly.MainModule.Types.each do |type|
type.Constructors.each do |constructor|
if was_instrumented?(constructor)
name = "#{type.Namespace}.#{type.Name}.ctor"
puts "De-Instrumenting #{name}"
de_instrument(constructor)
modified = true
end
end
type.Methods.each do |method|
if was_instrumented?(method)
name = "#{type.Namespace}.#{type.Name}.#{method.Name}"
puts "De-Instrumenting #{name}"
de_instrument(method)
modified = true
end
end
end
if modified
puts "Saving de-instrumented assembly..."
Mono::Cecil::AssemblyFactory.SaveAssembly(@assembly, @assembly_name)
puts "done."
else
puts "Assembly does not contain any instrumentation."
end
end
private
def log_message(worker, insert_point, message)
cil = CilWorkerHelper.new(@assembly, worker, insert_point)
# ldstr "TracingLog"
cil.ldstr("TracingLog")
# call class [log4net]log4net.ILog [log4net]log4net.LogManager::GetLogger(string)
cil.call(log_manager_get_logger)
# ldstr message
cil.ldstr(message)
# callvirt instance void [log4net]log4net.ILog::Debug(object)
cil.call_virt(log_interface_debug)
end
def instrument(method, enter_message, exit_message)
if !method.nil?
worker = method.Body.CilWorker
first_instruction = method.Body.Instructions[0]
log_message(worker, first_instruction, enter_message)
last_instruction = method.Body.Instructions[method.Body.Instructions.Count - 1]
log_message(worker, last_instruction, exit_message)
end
end
def was_instrumented?(method)
# make sure that the first instruction is one that we added
first_instruction = method.Body.Instructions[0]
return (first_instruction.OpCode == Mono::Cecil::Cil::OpCodes.Ldstr and
first_instruction.Operand == "TracingLog")
end
def de_instrument(method)
if !method.nil?
if was_instrumented?(method)
# remove the first 4 instructions
4.times do
method.Body.Instructions.RemoveAt(0)
end
# remove the last 4 instructions (before the return)
4.times do
method.Body.Instructions.RemoveAt(method.Body.Instructions.Count - 2)
end
end
end
end
end
class InstrumentorOptions
def self.parse(args)
options = OpenStruct.new
options.assembly_name = nil
options.operation = nil
optionParser = OptionParser.new do |optionParser|
optionParser.banner = "Usage: instrumentor.rb [options] -a assembly_name"
optionParser.separator ""
optionParser.separator "Specific options:"
optionParser.on("-a assembly_name", "--assembly assembly_name", "The assembly to modify") do |assembly_name|
if !options.assembly_name.nil?
puts "Error: assembly may only be specified once"
puts optionParser
exit
end
options.assembly_name = assembly_name
end
optionParser.on("--apply", "Instrument the specified assembly. This is the default if no options are specified.") do
if !options.operation.nil?
if options.operation == :apply
puts "Error: --apply may only be supplied once"
elsif options.operation == :remove
puts "Error: --apply cannot be combined with --remove"
else
raise "Unexpected operation value"
end
puts optionParser
exit
end
options.operation = :apply
end
optionParser.on("--remove", "De-instrument the specified assembly") do
if !options.operation.nil?
if options.operation == :apply
puts "Error: --remove cannot be combined with --apply"
elsif options.operation == :remove
puts "Error: --remove may only be supplied once"
else
raise "Unexpected operation value"
end
puts optionParser
exit
end
options.operation = :remove
end
optionParser.on_tail("-h", "--help", "Show this message") do
puts optionsParser
exit
end
end
optionParser.parse!(args)
if options.assembly_name.nil?
puts "Error: assembly name must be specified."
puts optionParser
exit
end
if options.assembly_name == true
puts "Error: assembly name must be specified."
puts optionParser
exit
end
if options.operation.nil?
options.operation = :apply
end
options
end
end
options = InstrumentorOptions.parse(ARGV)
instrumentor = Instrumentor.new(options.assembly_name)
if options.operation == :apply
instrumentor.apply
elsif options.operation == :remove
instrumentor.remove
else
raise "Unexpected operation value."
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment