Skip to content

Commit 821e6a3

Browse files
committed
HBASE-26469 correct HBase shell exit behavior to match code passed to exit (#4018)
* refactors how we handle running the passed in initialization script to make use of IRB sessions directly instead of reimplementing things ourselves * simplify how we initialize our IRB config * insert a shim for capturing exit codes passed via user calls to exit * make use of user provided exit code unless we're reading stdin in interactive mode This changes the exit code of the shell * a 0 return code, or no return code, passed to a call to exit from stdin in non-interactive mode will now exit cleanly. in prior versions this would have exitted with an error and non-zero exit code. * for other combinations of passing in an initilization script or reading from stdin with using the non-interactive flag, the exit code being 0 or non-0 should now line up with releases prior to 2.4.z, which is a change in behavior compared to 2.4.z. Signed-off-by: Peter Somogyi <psomogyi@apache.org>
1 parent 3a14cfc commit 821e6a3

File tree

5 files changed

+83
-151
lines changed

5 files changed

+83
-151
lines changed

hbase-shell/src/main/ruby/irb/hirb.rb

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ module IRB
2323

2424
# Subclass of IRB so can intercept methods
2525
class HIRB < Irb
26-
def initialize
26+
def initialize(workspace = nil, input_method = nil)
2727
# This is ugly. Our 'help' method above provokes the following message
2828
# on irb construction: 'irb: warn: can't alias help from irb_help.'
2929
# Below, we reset the output so its pointed at /dev/null during irb
@@ -44,7 +44,7 @@ def initialize
4444
# The stderr is an input to stty to re-adjust the terminal for the error('stdin isnt a terminal')
4545
# incase the command is piped with hbase shell(eg - >echo 'list' | bin/hbase shell)
4646
`stty icrnl <&2`
47-
super
47+
super(workspace, input_method)
4848
ensure
4949
f.close
5050
$stdout = STDOUT
@@ -57,4 +57,40 @@ def output_value
5757
super unless @context.last_value.nil?
5858
end
5959
end
60+
61+
##
62+
# HBaseLoader serves a similar purpose to IRB::IrbLoader, but with a different separation of
63+
# concerns. This loader allows us to directly get the path for a filename in ruby's load path,
64+
# and then use that in IRB::Irb
65+
module HBaseLoader
66+
##
67+
# Determine the loadable path for a given filename by searching through $LOAD_PATH
68+
#
69+
# This serves a similar purpose to IRB::IrbLoader#search_file_from_ruby_path, but uses JRuby's
70+
# loader, which allows us to find special paths like "uri:classloader" inside of a Jar.
71+
#
72+
# @param [String] filename
73+
# @return [String] path
74+
def self.path_for_load(filename)
75+
return File.absolute_path(filename) if File.exist? filename
76+
77+
# Get JRuby's LoadService from the global (singleton) instance of the runtime
78+
# (org.jruby.Ruby), which allows us to use JRuby's tools for searching the load path.
79+
runtime = org.jruby.Ruby.getGlobalRuntime
80+
loader = runtime.getLoadService
81+
search_state = loader.findFileForLoad filename
82+
raise LoadError, "no such file to load -- #{filename}" if search_state.library.nil?
83+
84+
search_state.loadName
85+
end
86+
87+
##
88+
# Return a file handle for the given file found in the load path
89+
#
90+
# @param [String] filename
91+
# @return [FileInputMethod] InputMethod for passing to IRB session
92+
def self.file_for_load(filename)
93+
FileInputMethod.new(File.new(path_for_load(filename)))
94+
end
95+
end
6096
end

hbase-shell/src/main/ruby/jar-bootstrap.rb

Lines changed: 29 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def add_to_configuration(c, arg)
105105
log_level = org.apache.log4j.Level::ERROR
106106
@shell_debug = false
107107
interactive = true
108+
full_backtrace = false
108109
top_level_definitions = false
109110

110111
opts.each do |opt, arg|
@@ -116,7 +117,7 @@ def add_to_configuration(c, arg)
116117
conf_from_cli = add_to_configuration(conf_from_cli, arg)
117118
when '--debug'
118119
log_level = org.apache.log4j.Level::DEBUG
119-
$fullBackTrace = true
120+
full_backtrace = true
120121
@shell_debug = true
121122
puts 'Setting DEBUG log level...'
122123
when '--noninteractive'
@@ -185,61 +186,39 @@ def debug?
185186
# instance variables (@hbase and @shell) onto Ruby's top-level receiver object known as "main".
186187
@shell.export_all(self) if top_level_definitions
187188

189+
require 'irb'
190+
require 'irb/ext/change-ws'
191+
require 'irb/hirb'
192+
193+
# Configure IRB
194+
IRB.setup(nil)
195+
IRB.conf[:PROMPT][:CUSTOM] = {
196+
PROMPT_I: '%N:%03n:%i> ',
197+
PROMPT_S: '%N:%03n:%i%l ',
198+
PROMPT_C: '%N:%03n:%i* ',
199+
RETURN: "=> %s\n"
200+
}
201+
202+
IRB.conf[:IRB_NAME] = 'hbase'
203+
IRB.conf[:AP_NAME] = 'hbase'
204+
IRB.conf[:PROMPT_MODE] = :CUSTOM
205+
IRB.conf[:BACK_TRACE_LIMIT] = 0 unless full_backtrace
206+
207+
# Create a workspace we'll use across sessions.
208+
workspace = @shell.get_workspace
209+
188210
# If script2run, try running it. If we're in interactive mode, will go on to run the shell unless
189211
# script calls 'exit' or 'exit 0' or 'exit errcode'.
190-
require 'shell/hbase_loader'
191212
if script2run
192-
::Shell::Shell.exception_handler(!$fullBackTrace) { @shell.eval_io(Hbase::Loader.file_for_load(script2run), filename = script2run) }
213+
::Shell::Shell.exception_handler(!full_backtrace) do
214+
IRB::HIRB.new(workspace, IRB::HBaseLoader.file_for_load(script2run)).run
215+
end
216+
exit @shell.exit_code unless @shell.exit_code.nil?
193217
end
194218

195-
# If we are not running interactively, evaluate standard input
196-
::Shell::Shell.exception_handler(!$fullBackTrace) { @shell.eval_io(STDIN) } unless interactive
197-
198219
if interactive
199220
# Output a banner message that tells users where to go for help
200221
@shell.print_banner
201-
202-
require 'irb'
203-
require 'irb/ext/change-ws'
204-
require 'irb/hirb'
205-
206-
module IRB
207-
# Override of the default IRB.start
208-
def self.start(ap_path = nil)
209-
$0 = File.basename(ap_path, '.rb') if ap_path
210-
211-
IRB.setup(ap_path)
212-
IRB.conf[:PROMPT][:CUSTOM] = {
213-
:PROMPT_I => "%N:%03n:%i> ",
214-
:PROMPT_S => "%N:%03n:%i%l ",
215-
:PROMPT_C => "%N:%03n:%i* ",
216-
:RETURN => "=> %s\n"
217-
}
218-
219-
@CONF[:IRB_NAME] = 'hbase'
220-
@CONF[:AP_NAME] = 'hbase'
221-
@CONF[:PROMPT_MODE] = :CUSTOM
222-
@CONF[:BACK_TRACE_LIMIT] = 0 unless $fullBackTrace
223-
224-
hirb = if @CONF[:SCRIPT]
225-
HIRB.new(nil, @CONF[:SCRIPT])
226-
else
227-
HIRB.new
228-
end
229-
230-
shl = TOPLEVEL_BINDING.receiver.instance_variable_get :'@shell'
231-
hirb.context.change_workspace shl.get_workspace
232-
233-
@CONF[:IRB_RC].call(hirb.context) if @CONF[:IRB_RC]
234-
# Storing our current HBase IRB Context as the main context is imperative for several reasons,
235-
# including auto-completion.
236-
@CONF[:MAIN_CONTEXT] = hirb.context
237-
238-
catch(:IRB_EXIT) do
239-
hirb.eval_input
240-
end
241-
end
242-
end
243-
244-
IRB.start
245222
end
223+
IRB::HIRB.new(workspace).run
224+
exit @shell.exit_code unless interactive || @shell.exit_code.nil?

hbase-shell/src/main/ruby/shell.rb

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,18 @@ class Shell
100100
@debug = false
101101
attr_accessor :debug
102102

103+
# keep track of the passed exit code. nil means never called.
104+
@exit_code = nil
105+
attr_accessor :exit_code
106+
107+
alias __exit__ exit
108+
# exit the interactive shell and save that this
109+
# happend via a call to exit
110+
def exit(ret = 0)
111+
@exit_code = ret
112+
IRB.irb_exit(IRB.CurrentContext.irb, ret)
113+
end
114+
103115
def initialize(hbase, interactive = true)
104116
self.hbase = hbase
105117
self.interactive = interactive
@@ -302,33 +314,13 @@ def get_workspace
302314
# Install all the hbase commands, constants, and instance variables @shell and @hbase. This
303315
# will override names that conflict with IRB methods like "help".
304316
export_all(hbase_receiver)
317+
# make it so calling exit will hit our pass-through rather than going directly to IRB
318+
hbase_receiver.send :define_singleton_method, :exit, lambda { |rc = 0|
319+
@shell.exit(rc)
320+
}
305321
::IRB::WorkSpace.new(hbase_receiver.get_binding)
306322
end
307323

308-
##
309-
# Read from an instance of Ruby's IO class and evaluate each line within the shell's workspace
310-
#
311-
# Unlike Ruby's require or load, this method allows us to execute code with a custom binding. In
312-
# this case, we are using the binding constructed with all the HBase shell constants and
313-
# methods.
314-
#
315-
# @param [IO] io instance of Ruby's IO (or subclass like File) to read script from
316-
# @param [String] filename to print in tracebacks
317-
def eval_io(io, filename = 'stdin')
318-
require 'irb/ruby-lex'
319-
# Mixing HBaseIOExtensions into IO allows us to pass IO objects to RubyLex.
320-
IO.include HBaseIOExtensions
321-
322-
workspace = get_workspace
323-
scanner = RubyLex.new
324-
scanner.set_input(io)
325-
326-
scanner.each_top_level_statement do |statement, linenum|
327-
puts(workspace.evaluate(nil, statement, filename, linenum))
328-
end
329-
nil
330-
end
331-
332324
##
333325
# Runs a block and logs exception from both Ruby and Java, optionally discarding the traceback
334326
#

hbase-shell/src/main/ruby/shell/hbase_loader.rb

Lines changed: 0 additions & 56 deletions
This file was deleted.

hbase-shell/src/test/ruby/shell/shell_test.rb

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -113,25 +113,6 @@ class FooC; end
113113

114114
#-----------------------------------------------------------------------------
115115

116-
define_test 'Shell::Shell#eval_io should evaluate IO' do
117-
StringIO.include HBaseIOExtensions
118-
# check that at least one of the commands is present while evaluating
119-
io = StringIO.new <<~EOF
120-
puts (respond_to? :list)
121-
EOF
122-
output = capture_stdout { @shell.eval_io(io) }
123-
assert_match(/true/, output)
124-
125-
# check that at least one of the HBase constants is present while evaluating
126-
io = StringIO.new <<~EOF
127-
ROWPREFIXFILTER.dump
128-
EOF
129-
output = capture_stdout { @shell.eval_io(io) }
130-
assert_match(/"ROWPREFIXFILTER"/, output)
131-
end
132-
133-
#-----------------------------------------------------------------------------
134-
135116
define_test 'Shell::Shell#exception_handler should hide traceback' do
136117
class TestException < RuntimeError; end
137118
# When hide_traceback is true, exception_handler should replace exceptions

0 commit comments

Comments
 (0)