hijack 0.1.9 → 0.2.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/hijack +2 -1
- data/lib/hijack/console.rb +25 -33
- data/lib/hijack/gdb.rb +27 -31
- data/lib/hijack/helper.rb +5 -5
- data/lib/hijack/output_receiver.rb +74 -0
- data/lib/hijack/payload.rb +15 -5
- data/lib/hijack/workspace.rb +4 -5
- data/lib/hijack.rb +5 -0
- data/test/test.rb +1 -1
- metadata +17 -7
- data/bin/hijack.compiled.rbc +0 -0
- data/lib/hijack.rbc +0 -0
- data/test/test.rbc +0 -0
data/bin/hijack
CHANGED
@@ -11,9 +11,10 @@ new_argv.delete(pid)
|
|
11
11
|
|
12
12
|
new_argv.options do |opts|
|
13
13
|
opts.banner = usage
|
14
|
-
opts.on("--
|
14
|
+
opts.on("--debug", "Enable debugging.") { |v| options[:debug] = true }
|
15
15
|
opts.on("--mute", "Ignore stdout/stderr writes from the remote process.") { |v| options[:mute] = true }
|
16
16
|
opts.on("-e", "--execute=FILE", String, "Execute the specified file in the remote process and then disconnect.") { |v| options[:execute] = v }
|
17
|
+
opts.on("--version", "Display this version of Hijack.") { |v| puts "Hijack #{Hijack.version}"; exit }
|
17
18
|
opts.on("-h", "--help", "Show this help message.") { puts opts; exit }
|
18
19
|
opts.parse!
|
19
20
|
end
|
data/lib/hijack/console.rb
CHANGED
@@ -9,13 +9,15 @@ module Hijack
|
|
9
9
|
$stdout.flush
|
10
10
|
Payload.inject(@pid)
|
11
11
|
connect
|
12
|
+
check_remote_ruby_version
|
12
13
|
$stdout.write("\b" * str.size)
|
13
14
|
$stdout.flush
|
14
15
|
mirror_process
|
15
16
|
banner
|
16
17
|
execute_file
|
18
|
+
setup_at_exit
|
17
19
|
OutputReceiver.start(@remote) unless Hijack.options[:mute]
|
18
|
-
start_irb
|
20
|
+
start_irb
|
19
21
|
end
|
20
22
|
|
21
23
|
protected
|
@@ -39,33 +41,6 @@ module Hijack
|
|
39
41
|
@remote = DRbObject.new(nil, Hijack.socket_for(@pid))
|
40
42
|
end
|
41
43
|
|
42
|
-
module OutputReceiver
|
43
|
-
class << self
|
44
|
-
def mute
|
45
|
-
@mute = true
|
46
|
-
end
|
47
|
-
|
48
|
-
def unmute(remote)
|
49
|
-
start(remote) unless @started
|
50
|
-
@mute = false
|
51
|
-
end
|
52
|
-
|
53
|
-
def write(where, str)
|
54
|
-
Object.const_get(where.upcase).write(str) unless @mute
|
55
|
-
end
|
56
|
-
|
57
|
-
def puts(where, str)
|
58
|
-
Object.const_get(where.upcase).puts(str) unless @mute
|
59
|
-
end
|
60
|
-
|
61
|
-
def start(remote)
|
62
|
-
DRb.start_service(Hijack.socket_for(Process.pid), self)
|
63
|
-
remote.evaluate("__hijack_output_receiver_ready_#{Process.pid}")
|
64
|
-
@started = true
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
44
|
def mirror_process
|
70
45
|
# Attempt to require all files currently loaded by the remote process so DRb can dump as many objects as possible.
|
71
46
|
#
|
@@ -101,16 +76,16 @@ module Hijack
|
|
101
76
|
end
|
102
77
|
|
103
78
|
def start_irb
|
104
|
-
ARGV.
|
79
|
+
ARGV.clear
|
80
|
+
IRB.conf[:PROMPT_MODE] = :SIMPLE
|
81
|
+
IRB.conf[:USE_READLINE] = true
|
105
82
|
IRB.setup(nil)
|
106
83
|
workspace = Hijack::Workspace.new
|
107
84
|
workspace.remote = @remote
|
108
85
|
workspace.pid = @pid
|
109
86
|
irb = IRB::Irb.new(workspace)
|
110
|
-
|
111
|
-
|
112
|
-
@CONF[:MAIN_CONTEXT] = irb.context
|
113
|
-
@CONF[:PROMPT_MODE] = :SIMPLE
|
87
|
+
IRB.conf[:IRB_RC].call irb.context if IRB.conf[:IRB_RC]
|
88
|
+
IRB.conf[:MAIN_CONTEXT] = irb.context
|
114
89
|
trap('SIGINT') { irb.signal_handle }
|
115
90
|
catch(:IRB_EXIT) { irb.eval_input }
|
116
91
|
end
|
@@ -119,6 +94,23 @@ module Hijack
|
|
119
94
|
script, ruby_version, platform, hijack_version = @remote.evaluate('[$0, RUBY_VERSION, RUBY_PLATFORM]')
|
120
95
|
puts "=> Hijacked #{@pid} (#{script}) (ruby #{ruby_version} [#{platform}])"
|
121
96
|
end
|
97
|
+
|
98
|
+
def check_remote_ruby_version
|
99
|
+
# TODO: Check patch-level.
|
100
|
+
remote_major, remote_minor = remote_ruby_version.split('.')
|
101
|
+
local_major, local_minor = RUBY_VERSION.split('.')
|
102
|
+
if remote_minor != local_minor
|
103
|
+
$stderr.puts "\nWARNING: The process you are hijacking is running Ruby #{remote_ruby_version} yet you are running #{RUBY_VERSION}."
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def remote_ruby_version
|
108
|
+
@remote_ruby_version ||= @remote.evaluate("RUBY_VERSION")
|
109
|
+
end
|
110
|
+
|
111
|
+
def setup_at_exit
|
112
|
+
at_exit { OutputReceiver.stop }
|
113
|
+
end
|
122
114
|
|
123
115
|
def execute_file
|
124
116
|
if Hijack.options[:execute]
|
data/lib/hijack/gdb.rb
CHANGED
@@ -4,28 +4,11 @@ module Hijack
|
|
4
4
|
class GDB
|
5
5
|
def initialize(pid)
|
6
6
|
@pid = pid
|
7
|
-
@verbose = Hijack.options[:
|
7
|
+
@verbose = Hijack.options[:debug]
|
8
8
|
@exec_path = File.join(Config::CONFIG['bindir'], Config::CONFIG['RUBY_INSTALL_NAME'] + Config::CONFIG['EXEEXT'])
|
9
9
|
attach
|
10
10
|
end
|
11
11
|
|
12
|
-
def ensure_attached_to_ruby_process
|
13
|
-
unless backtrace.any? {|line| line =~ /rb_/}
|
14
|
-
puts "\n=> #{@pid} doesn't appear to be a Ruby process!"
|
15
|
-
detach
|
16
|
-
exit 1
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
def ensure_main_thread_not_blocked_by_join
|
21
|
-
if backtrace.any? {|line| line =~ /rb_thread_join/}
|
22
|
-
puts "\n=> Unable to hijack #{@pid} because the main thread is blocked waiting for another thread to join."
|
23
|
-
puts "=> Check that you are using the most recent version of hijack, a newer version may have solved this shortcoming."
|
24
|
-
detach
|
25
|
-
exit 1
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
12
|
def eval(cmd)
|
30
13
|
call("(void)rb_eval_string(#{cmd.strip.gsub(/"/, '\"').inspect})")
|
31
14
|
end
|
@@ -40,40 +23,53 @@ module Hijack
|
|
40
23
|
end
|
41
24
|
|
42
25
|
protected
|
26
|
+
|
43
27
|
def attach
|
44
28
|
loop do
|
45
29
|
@gdb = IO.popen("gdb -q #{@exec_path} #{@pid} 2>&1", 'r+')
|
46
30
|
wait
|
47
|
-
if backtrace.first =~ /
|
31
|
+
if backtrace.first =~ /previous frame inner to this frame/i
|
48
32
|
detach
|
49
33
|
sleep 0.1
|
50
34
|
else
|
51
35
|
break
|
52
36
|
end
|
53
37
|
end
|
54
|
-
|
55
|
-
|
38
|
+
|
39
|
+
# TODO: Check for "Unable to attach to process-id 44528: No child processes (10)"
|
56
40
|
ensure_main_thread_not_blocked_by_join
|
57
|
-
set_trap_pending
|
58
41
|
set_breakpoint
|
59
42
|
continue
|
60
|
-
|
43
|
+
delete_breakpoint
|
44
|
+
end
|
45
|
+
|
46
|
+
def set_breakpoint
|
47
|
+
exec("break rb_call")
|
61
48
|
end
|
62
49
|
|
63
|
-
def
|
64
|
-
|
50
|
+
def delete_breakpoint
|
51
|
+
exec("delete breakpoints")
|
65
52
|
end
|
66
53
|
|
67
|
-
def
|
68
|
-
|
54
|
+
def ensure_main_thread_not_blocked_by_join
|
55
|
+
if backtrace.any? {|line| line =~ /rb_thread_join/}
|
56
|
+
puts "\n=> Unable to hijack #{@pid} because the main thread is blocked waiting for another thread to join."
|
57
|
+
puts "=> Check that you are using the most recent version of hijack, a newer version may have solved this shortcoming."
|
58
|
+
detach
|
59
|
+
exit 1
|
60
|
+
end
|
69
61
|
end
|
70
62
|
|
71
|
-
def
|
72
|
-
|
63
|
+
def ensure_attached_to_ruby_process
|
64
|
+
unless backtrace.any? {|line| line =~ /(rb|ruby)_/}
|
65
|
+
puts "\n=> #{@pid} doesn't appear to be a Ruby process!"
|
66
|
+
detach
|
67
|
+
exit 1
|
68
|
+
end
|
73
69
|
end
|
74
70
|
|
75
|
-
def
|
76
|
-
exec(
|
71
|
+
def backtrace
|
72
|
+
@backtrace ||= exec('bt').reverse
|
77
73
|
end
|
78
74
|
|
79
75
|
def continue
|
data/lib/hijack/helper.rb
CHANGED
@@ -2,7 +2,7 @@ module Hijack
|
|
2
2
|
module Helper
|
3
3
|
class << self
|
4
4
|
def helpers
|
5
|
-
methods.find_all {|meth| meth =~ /^hijack_/}
|
5
|
+
methods.find_all {|meth| meth =~ /^hijack_/}.map { |meth| meth.to_s }
|
6
6
|
end
|
7
7
|
|
8
8
|
def find_helper(statements)
|
@@ -15,12 +15,12 @@ module Hijack
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def hijack_mute(remote)
|
18
|
-
Hijack::
|
18
|
+
Hijack::OutputReceiver.mute
|
19
19
|
true
|
20
20
|
end
|
21
21
|
|
22
22
|
def hijack_unmute(remote)
|
23
|
-
Hijack::
|
23
|
+
Hijack::OutputReceiver.unmute
|
24
24
|
true
|
25
25
|
end
|
26
26
|
|
@@ -28,11 +28,11 @@ module Hijack
|
|
28
28
|
hijack_mute(remote)
|
29
29
|
require 'rubygems'
|
30
30
|
require 'ruby-debug'
|
31
|
-
remote.evaluate(<<-
|
31
|
+
remote.evaluate(<<-RUBY)
|
32
32
|
require 'rubygems'
|
33
33
|
require 'ruby-debug'
|
34
34
|
Debugger.start_remote
|
35
|
-
|
35
|
+
RUBY
|
36
36
|
true
|
37
37
|
end
|
38
38
|
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Hijack
|
2
|
+
class OutputReceiver
|
3
|
+
attr_reader :pid
|
4
|
+
|
5
|
+
def self.start(remote)
|
6
|
+
@instance = new(remote)
|
7
|
+
@instance.start
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.stop
|
11
|
+
@instance.stop if @instance
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.mute
|
15
|
+
@instance.mute if @instance
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.unmute
|
19
|
+
@instance.unmute if @instance
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(remote)
|
23
|
+
@remote = remote
|
24
|
+
end
|
25
|
+
|
26
|
+
def start
|
27
|
+
@pid = fork do
|
28
|
+
setup_signal_traps
|
29
|
+
DRb.start_service(Hijack.socket_for(Process.pid), self)
|
30
|
+
@remote.evaluate("__hijack_output_receiver_ready_#{Process.pid}")
|
31
|
+
DRb.thread.join
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def stop
|
36
|
+
Process.kill("KILL", @pid)
|
37
|
+
Process.waitpid(@pid)
|
38
|
+
end
|
39
|
+
|
40
|
+
def setup_signal_traps
|
41
|
+
Signal.trap("USR1") { @mute = true }
|
42
|
+
Signal.trap("USR2") { @mute = false }
|
43
|
+
end
|
44
|
+
|
45
|
+
def mute
|
46
|
+
Process.kill("USR1", @pid)
|
47
|
+
end
|
48
|
+
|
49
|
+
def unmute
|
50
|
+
Process.kill("USR2", @pid)
|
51
|
+
end
|
52
|
+
|
53
|
+
def write(io, str)
|
54
|
+
return if @mute
|
55
|
+
get_io(io).write(str)
|
56
|
+
end
|
57
|
+
|
58
|
+
def puts(io, str)
|
59
|
+
return if @mute
|
60
|
+
get_io(io).puts(str)
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_io(io)
|
64
|
+
case io.to_s
|
65
|
+
when 'stdout'
|
66
|
+
$stdout
|
67
|
+
when 'stderr'
|
68
|
+
$stderr
|
69
|
+
else
|
70
|
+
raise "get_io cannot handle '#{io}'"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/hijack/payload.rb
CHANGED
@@ -17,7 +17,9 @@ module Hijack
|
|
17
17
|
require 'stringio'
|
18
18
|
require 'drb'
|
19
19
|
|
20
|
-
|
20
|
+
debug = #{Hijack.options[:debug] || false}
|
21
|
+
if debug || !defined?(Hijack)
|
22
|
+
puts 'Hijack: Debugging enabled' if debug
|
21
23
|
module Hijack
|
22
24
|
class OutputCopier
|
23
25
|
def self.remote
|
@@ -38,13 +40,15 @@ module Hijack
|
|
38
40
|
|
39
41
|
def self.start(pid)
|
40
42
|
@remote = DRbObject.new(nil, 'drbunix://tmp/hijack.' + pid + '.sock')
|
43
|
+
puts @remote.inspect if Hijack.debug?
|
41
44
|
|
42
45
|
class << $stdout
|
43
46
|
def write_with_copying(str)
|
44
47
|
write_without_copying(str)
|
45
48
|
begin
|
46
49
|
Hijack::OutputCopier.remote.write('stdout', str)
|
47
|
-
rescue Exception
|
50
|
+
rescue Exception => e
|
51
|
+
write_without_copying(e.message) if Hijack.debug?
|
48
52
|
Hijack.stop
|
49
53
|
end
|
50
54
|
end
|
@@ -57,7 +61,8 @@ module Hijack
|
|
57
61
|
write_without_copying(str)
|
58
62
|
begin
|
59
63
|
Hijack::OutputCopier.remote.write('stderr', str)
|
60
|
-
rescue Exception
|
64
|
+
rescue Exception => e
|
65
|
+
write_without_copying(e.message) if Hijack.debug?
|
61
66
|
Hijack.stop
|
62
67
|
end
|
63
68
|
end
|
@@ -86,8 +91,13 @@ module Hijack
|
|
86
91
|
end
|
87
92
|
end
|
88
93
|
|
89
|
-
def self.
|
94
|
+
def self.debug?
|
95
|
+
@debug
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.start(context, debug)
|
90
99
|
return if @service && @service.alive?
|
100
|
+
@debug = debug
|
91
101
|
evaluator = Hijack::Evaluator.new(context)
|
92
102
|
@service = DRb.start_service('#{Hijack.socket_for(pid)}', evaluator)
|
93
103
|
File.chmod(0600, '#{Hijack.socket_path_for(pid)}')
|
@@ -103,7 +113,7 @@ module Hijack
|
|
103
113
|
end
|
104
114
|
end
|
105
115
|
end
|
106
|
-
Hijack.start(self)
|
116
|
+
Hijack.start(self, #{Hijack.options[:debug] || false})
|
107
117
|
RUBY
|
108
118
|
end
|
109
119
|
end
|
data/lib/hijack/workspace.rb
CHANGED
@@ -1,20 +1,19 @@
|
|
1
1
|
module Hijack
|
2
|
-
|
2
|
+
Readline.completion_proc = Proc.new do |input|
|
3
3
|
bind = IRB.conf[:MAIN_CONTEXT].workspace.binding
|
4
4
|
if helpers = Helper.helpers_like(input)
|
5
5
|
helpers
|
6
6
|
else
|
7
7
|
IRB::InputCompletor::CompletionProc.call(input)
|
8
8
|
end
|
9
|
-
|
10
|
-
Readline.completion_proc = HijackCompletionProc
|
9
|
+
end
|
11
10
|
|
12
11
|
class Workspace < IRB::WorkSpace
|
13
12
|
attr_accessor :remote, :pid
|
14
13
|
def evaluate(context, statements, file = __FILE__, line = __LINE__)
|
15
14
|
if statements =~ /IRB\./
|
16
15
|
super
|
17
|
-
elsif statements.strip =~ /^exit/
|
16
|
+
elsif statements.strip =~ /^(exit|quit)/
|
18
17
|
remote.evaluate('__hijack_exit') rescue nil
|
19
18
|
super
|
20
19
|
elsif helper = Hijack::Helper.find_helper(statements)
|
@@ -22,7 +21,7 @@ module Hijack
|
|
22
21
|
else
|
23
22
|
begin
|
24
23
|
result = remote.evaluate(statements)
|
25
|
-
rescue DRb::DRbConnError
|
24
|
+
rescue DRb::DRbConnError => e
|
26
25
|
puts "=> Lost connection to #{@pid}!"
|
27
26
|
exit 1
|
28
27
|
end
|
data/lib/hijack.rb
CHANGED
@@ -10,6 +10,7 @@ require 'hijack/gdb'
|
|
10
10
|
require 'hijack/payload'
|
11
11
|
require 'hijack/helper'
|
12
12
|
require 'hijack/workspace'
|
13
|
+
require 'hijack/output_receiver'
|
13
14
|
|
14
15
|
module Hijack
|
15
16
|
def self.start(pid, options)
|
@@ -28,4 +29,8 @@ module Hijack
|
|
28
29
|
def self.socket_path_for(pid)
|
29
30
|
"/tmp/hijack.#{pid}.sock"
|
30
31
|
end
|
32
|
+
|
33
|
+
def self.version
|
34
|
+
"0.2.0.rc1"
|
35
|
+
end
|
31
36
|
end
|
data/test/test.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hijack
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
hash: 977940574
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 2
|
8
|
+
- 0
|
9
|
+
- rc1
|
10
|
+
version: 0.2.0.rc1
|
5
11
|
platform: ruby
|
6
12
|
authors:
|
7
13
|
- Ian Leitch
|
@@ -9,12 +15,12 @@ autorequire:
|
|
9
15
|
bindir: bin
|
10
16
|
cert_chain: []
|
11
17
|
|
12
|
-
date: 2009-
|
18
|
+
date: 2009-09-20 14:00:00 +10:00
|
13
19
|
default_executable:
|
14
20
|
dependencies: []
|
15
21
|
|
16
22
|
description: Provides an irb session to an existing ruby process.
|
17
|
-
email:
|
23
|
+
email: port001@gmail.com
|
18
24
|
executables:
|
19
25
|
- hijack
|
20
26
|
extensions: []
|
@@ -31,15 +37,13 @@ files:
|
|
31
37
|
- lib/hijack/helper.rb
|
32
38
|
- lib/hijack/payload.rb
|
33
39
|
- lib/hijack/workspace.rb
|
40
|
+
- lib/hijack/output_receiver.rb
|
34
41
|
- lib/hijack.rb
|
35
|
-
- lib/hijack.rbc
|
36
42
|
- test/app.rb
|
37
43
|
- test/gc.rb
|
38
44
|
- test/test.rb
|
39
|
-
- test/test.rbc
|
40
45
|
- tasks/gem.rake
|
41
46
|
- bin/hijack
|
42
|
-
- bin/hijack.compiled.rbc
|
43
47
|
- examples/rails_dispatcher.rb
|
44
48
|
has_rdoc: true
|
45
49
|
homepage: http://github.com/ileitch/hijack
|
@@ -54,18 +58,24 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
54
58
|
requirements:
|
55
59
|
- - ">="
|
56
60
|
- !ruby/object:Gem::Version
|
61
|
+
hash: 3
|
62
|
+
segments:
|
63
|
+
- 0
|
57
64
|
version: "0"
|
58
65
|
version:
|
59
66
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
67
|
requirements:
|
61
68
|
- - ">="
|
62
69
|
- !ruby/object:Gem::Version
|
70
|
+
hash: 3
|
71
|
+
segments:
|
72
|
+
- 0
|
63
73
|
version: "0"
|
64
74
|
version:
|
65
75
|
requirements: []
|
66
76
|
|
67
77
|
rubyforge_project:
|
68
|
-
rubygems_version: 1.3.
|
78
|
+
rubygems_version: 1.3.7
|
69
79
|
signing_key:
|
70
80
|
specification_version: 3
|
71
81
|
summary: Provides an irb session to an existing ruby process.
|
data/bin/hijack.compiled.rbc
DELETED
Binary file
|
data/lib/hijack.rbc
DELETED
Binary file
|
data/test/test.rbc
DELETED
Binary file
|