rsql 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (C) 2011 by brad+rsql@gigglewax.com
1
+ Copyright (C) 2011-2012 by brad+rsql@gigglewax.com
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
@@ -100,7 +100,7 @@ downloaded with the source.
100
100
 
101
101
  RSQL is licensed under the MIT License:
102
102
 
103
- Copyright (C) 2011 by Brad Robel-Forrest <brad+rsql@gigglewax.com>
103
+ Copyright (C) 2011-2012 by Brad Robel-Forrest <brad+rsql@gigglewax.com>
104
104
 
105
105
  Permission is hereby granted, free of charge, to any person obtaining
106
106
  a copy of this software and associated documentation files (the
data/bin/rsql CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- # Copyright (C) 2011 by Brad Robel-Forrest <brad+rsql@gigglewax.com>
3
+ # Copyright (C) 2011-2012 by Brad Robel-Forrest <brad+rsql@gigglewax.com>
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the "Software"), to deal
@@ -52,6 +52,9 @@ bn = File.basename($0, '.rb')
52
52
 
53
53
  eval_context = EvalContext.new
54
54
 
55
+ # rewrite all double hyphen options into singles so both are supported
56
+ ARGV.map!{|a| a.sub(/^--/,'-')}
57
+
55
58
  if i = ARGV.index('-rc')
56
59
  ARGV.delete_at(i)
57
60
  rc_fn = ARGV.delete_at(i)
@@ -146,13 +149,25 @@ end
146
149
 
147
150
  if i = ARGV.index('-ssh')
148
151
  require 'net/ssh'
152
+
149
153
  ARGV.delete_at(i)
154
+
150
155
  (ssh_host, ssh_user, ssh_password, ssh_port) = split_login(ARGV.delete_at(i))
156
+
157
+ default_config = File.join(ENV['HOME'], '.ssh', 'config')
158
+ if File.exists?(default_config)
159
+ ssh_cfghash = Net::SSH::Config.load(default_config, ssh_host)
160
+ end
161
+
162
+ if ssh_user.nil? || ssh_user.empty?
163
+ ssh_user = ssh_cfghash['user'] || ENV['USER'] || ENV['USERNAME']
164
+ end
151
165
  end
152
166
 
153
167
  if i = ARGV.index('-sshconfig')
154
168
  ARGV.delete_at(i)
155
169
  ssh_config = ARGV.delete_at(i)
170
+ ssh_cfghash = Net::SSH::Config.load(ssh_config, ssh_host)
156
171
  end
157
172
 
158
173
  if i = ARGV.index('-e')
@@ -167,7 +182,15 @@ if i = ARGV.index('-e')
167
182
  batch_input.strip!
168
183
  end
169
184
 
170
- if ARGV.size < 1
185
+ show_usage = false
186
+ ARGV.each do |a|
187
+ if a.start_with?('-')
188
+ $stderr.puts "unknown argument: #{a}"
189
+ show_usage = true
190
+ end
191
+ end
192
+
193
+ if ARGV.size < 1 || show_usage
171
194
  prefix = ' ' << ' ' * bn.size
172
195
  $stderr.puts <<USAGE
173
196
 
@@ -203,7 +226,8 @@ end
203
226
 
204
227
  MySQLResults.database_name = ARGV.shift
205
228
 
206
- unless $stdin.tty?
229
+ if !$stdin.tty? && batch_input.nil?
230
+ # accept commands from stdin
207
231
  batch_input = $stdin.read.gsub(/\r?\n/,';')
208
232
  end
209
233
 
@@ -262,8 +286,9 @@ if ssh_host
262
286
  ssh_pid = Process.fork do
263
287
  File.open(ipc_fn,'w'){|f| f.puts('start')}
264
288
  ssh_enabled = false
265
- Signal.trap('INT') do
266
- $stderr.puts 'Shutting down...' unless batch_input
289
+ Signal.trap('INT', 'IGNORE')
290
+ Signal.trap('TERM') do
291
+ $stderr.puts 'Closing SSH connection...' unless batch_input
267
292
  ssh_enabled = false
268
293
  end
269
294
  opts = {:timeout => 15}
@@ -271,9 +296,7 @@ if ssh_host
271
296
  if verbose
272
297
  opts[:verbose] = :debug
273
298
  puts "SSH options: #{opts.inspect}"
274
- if ssh_config
275
- puts "SSH config: #{Net::SSH::Config.load(ssh_config, ssh_host).inspect}"
276
- end
299
+ puts "SSH config: #{ssh_cfghash.inspect}"
277
300
  end
278
301
  begin
279
302
  opts[:password] = ssh_password if ssh_password
@@ -292,20 +315,44 @@ if ssh_host
292
315
  retry
293
316
  end
294
317
  end
318
+ rescue Timeout::Error => ex
319
+ $stderr.puts ex.message
320
+ ensure
321
+ if ssh_enabled
322
+ ssh.forward.local(mysql_port, mysql_host, remote_mysql_port)
323
+ unless batch_input
324
+ puts(verbose ? "ready (#{mysql_port} => #{remote_mysql_port})" : 'ready')
325
+ end
326
+ File.open(ipc_fn,'w'){|f| f.puts('ready')}
327
+ ssh.loop(1) { ssh_enabled }
328
+ end
329
+ File.open(ipc_fn,'w'){|f| f.puts('fail')}
295
330
  end
296
- if ssh_enabled
297
- ssh.forward.local(mysql_port, mysql_host, remote_mysql_port)
298
- unless batch_input
299
- puts(verbose ? "ready (#{mysql_port} => #{remote_mysql_port})" : 'ready')
331
+ end
332
+
333
+ at_exit do
334
+ if ssh_pid && 0 <= ssh_pid
335
+ begin
336
+ Process.kill('TERM', ssh_pid)
337
+ killed = false
338
+ 5.times do
339
+ if Process.waitpid(ssh_pid, Process::WNOHANG)
340
+ killed = true
341
+ break
342
+ end
343
+ sleep 1
344
+ end
345
+ Process.kill('KILL', ssh_pid) unless killed
346
+ rescue Exception => ex
347
+ $stderr.puts ex.message
348
+ $stderr.puts ex.backtrace if verbose
300
349
  end
301
- File.open(ipc_fn,'w'){|f| f.puts('ready')}
302
- ssh.loop(1) { ssh_enabled }
303
350
  end
304
- File.open(ipc_fn,'w'){|f| f.puts('fail')}
351
+ File.unlink(ipc_fn) if File.exists?(ipc_fn)
305
352
  end
306
353
 
307
354
  ipc_state = ''
308
- 60.times do
355
+ 15.times do
309
356
  sleep(1)
310
357
  File.open(ipc_fn,'r'){|f| ipc_state = f.gets.strip}
311
358
  break if ipc_state == 'ready' || ipc_state == 'fail'
@@ -314,6 +361,8 @@ if ssh_host
314
361
  File.unlink(ipc_fn)
315
362
 
316
363
  unless ipc_state == 'ready'
364
+ # give the child time to exit
365
+ sleep(0.5)
317
366
  $stderr.puts "failed to connect to #{ssh_host} SSH host"
318
367
  exit 1
319
368
  end
@@ -332,6 +381,7 @@ mysql_conn = "#{mysql_host}:#{remote_mysql_port || mysql_port}"
332
381
  begin
333
382
  MySQLResults.conn = Mysql.new(mysql_host, mysql_user, mysql_password,
334
383
  MySQLResults.database_name, mysql_port)
384
+ MySQLResults.conn.reconnect = true
335
385
  puts 'connected' unless batch_input
336
386
  rescue Mysql::Error => ex
337
387
  if ex.message.include?('Client does not support authentication')
@@ -377,7 +427,7 @@ cmd_thread = Thread.new do
377
427
  else
378
428
  puts '',"[#{mysql_user}@#{ssh_host||mysql_host}:#{MySQLResults.database_name}]"
379
429
  input = ''
380
- prompt = bn + '> '
430
+ prompt = eval_context.prompt || (bn + '> ')
381
431
  loop do
382
432
  str = Readline.readline(prompt)
383
433
  if str.nil?
@@ -410,28 +460,37 @@ cmd_thread = Thread.new do
410
460
  next
411
461
  end
412
462
 
413
- me[:running] = true
414
463
  break if cmds.run!(eval_context) == :done
415
- me[:running] = false
416
464
  end
417
465
  end
418
466
 
467
+ # keep a secondary connection to allow us to kill off a running query
468
+ kill_conn = Mysql.new(mysql_host, mysql_user, mysql_password,
469
+ MySQLResults.database_name, mysql_port)
470
+
419
471
  Signal.trap('INT') do
420
- if cmd_thread[:running] && MySQLResults.conn
421
- $stderr.puts 'Interrupting MySQL query...'
422
- safe_timeout(MySQLResults.conn, :close, 'MySQL')
423
- MySQLResults.conn = nil
472
+ # emulate MySQL's behavior
473
+ if MySQLResults.conn && MySQLResults.conn.busy?
474
+ tid = MySQLResults.conn.thread_id
475
+ $stderr.puts "Ctrl-C -- sending \"KILL QUERY #{tid}\" to server..."
476
+ kill_conn.kill(tid)
477
+ 10.times do
478
+ break unless MySQLResults.conn.busy?
479
+ sleep(0.5)
480
+ end
481
+ MySQLResults.conn.select_db(MySQLResults.database_name)
482
+ else
483
+ $stderr.puts 'Ctrl-C -- exit!'
484
+ cmd_thread[:shutdown] = true
485
+ sleep(0.3)
486
+ cmd_thread.kill
424
487
  end
425
- $stderr.puts 'Shutting down...'
426
- cmd_thread[:shutdown] = true
427
- sleep(0.3)
428
- cmd_thread.kill
429
488
  end
430
489
 
431
490
  Signal.trap('CHLD') do
432
491
  $stderr.puts "SSH child (#{ssh_pid}) stopped--shutting down..."
433
- if cmd_thread[:running] && MySQLResults.conn
434
- $stderr.puts 'Interrupting MySQL query...'
492
+ if MySQLResults.conn && MySQLResults.conn.busy?
493
+ $stderr.puts 'Closing MySQL connection...'
435
494
  safe_timeout(MySQLResults.conn, :close, 'MySQL')
436
495
  MySQLResults.conn = nil
437
496
  end
@@ -461,13 +520,3 @@ if Readline::HISTORY.any?
461
520
  end
462
521
  File.open(history_fn, 'w') {|f| YAML.dump(Readline::HISTORY.to_a, f)}
463
522
  end
464
-
465
- if ssh_pid && 0 <= ssh_pid
466
- begin
467
- Process.kill('INT', ssh_pid)
468
- Process.waitpid(ssh_pid)
469
- rescue Exception => ex
470
- $stderr.puts ex.message
471
- $stderr.puts ex.backtrace if verbose
472
- end
473
- end
@@ -2,7 +2,7 @@
2
2
  # Commands using an EvalContext for handling recipes.
3
3
  #
4
4
  module RSQL
5
- VERSION = '0.2.4'
5
+ VERSION = '0.2.5'
6
6
 
7
7
  require 'rsql/mysql_results'
8
8
  require 'rsql/eval_context'
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (C) 2011 by Brad Robel-Forrest <brad+rsql@gigglewax.com>
2
+ # Copyright (C) 2011-2012 by Brad Robel-Forrest <brad+rsql@gigglewax.com>
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -82,7 +82,11 @@ module RSQL
82
82
  match_before_bang = nil
83
83
  break
84
84
  end
85
- new_bangs[key.strip] = val.to_sym
85
+ if val.strip == 'nil'
86
+ new_bangs[key.strip] = nil
87
+ else
88
+ new_bangs[key.strip] = val.to_sym
89
+ end
86
90
  end
87
91
  next unless match_before_bang
88
92
  match = match_before_bang
@@ -145,10 +149,16 @@ module RSQL
145
149
  results.send(cmd.displayer)
146
150
  elsif EvalResults === results
147
151
  last_results = nil
148
- if results.stdout && 0 < results.stdout.size
149
- puts results.stdout.string
152
+ if MySQLResults === results.value
153
+ # This happens if their recipe returns MySQL
154
+ # results...just display it like above.
155
+ results.value.send(cmd.displayer)
156
+ else
157
+ if results.stdout && 0 < results.stdout.size
158
+ puts results.stdout.string
159
+ end
160
+ puts "=> #{results.value.inspect}" if results.value
150
161
  end
151
- puts "=> #{results.value.inspect}" if results.value
152
162
  end
153
163
  end
154
164
  end
@@ -213,7 +223,8 @@ module RSQL
213
223
  begin
214
224
  last_results = MySQLResults.query(value, eval_context)
215
225
  rescue MySQLResults::MaxRowsException => ex
216
- $stderr.puts "refusing to process #{ex.rows} rows (max: #{ex.max})"
226
+ $stderr.puts "refusing to process #{ex.rows} rows (max: #{ex.max})--" <<
227
+ "consider raising this via set_max_rows"
217
228
  rescue Mysql::Error => ex
218
229
  $stderr.puts ex.message
219
230
  rescue Exception => ex
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (C) 2011 by Brad Robel-Forrest <brad+rsql@gigglewax.com>
2
+ # Copyright (C) 2011-2012 by Brad Robel-Forrest <brad+rsql@gigglewax.com>
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -24,8 +24,8 @@ module RSQL
24
24
  require 'time'
25
25
 
26
26
  ################################################################################
27
- # This class wraps all dynamic evaluation and serves as the reflection
28
- # class for adding methods dynamically.
27
+ # This class wraps all dynamic evaluation and serves as the reflection class
28
+ # for adding methods dynamically.
29
29
  #
30
30
  class EvalContext
31
31
 
@@ -35,19 +35,26 @@ module RSQL
35
35
  HEXSTR_LIMIT = 32
36
36
 
37
37
  def initialize(verbose=false)
38
+ @prompt = nil
38
39
  @verbose = verbose
39
40
  @hexstr_limit = HEXSTR_LIMIT
40
41
  @results = nil
41
42
 
42
43
  @loaded_fns = []
44
+ @loaded_fns_state = {}
43
45
  @init_registrations = []
44
46
  @bangs = {}
47
+ @global_bangs = {}
45
48
 
46
49
  @registrations = {
47
50
  :version => Registration.new('version', [], {},
48
51
  method(:version),
49
52
  'version',
50
53
  'Version information about RSQL, the client, and the server.'),
54
+ :help => Registration.new('help', [], {},
55
+ method(:help),
56
+ 'help',
57
+ 'Show short syntax help.'),
51
58
  :reload => Registration.new('reload', [], {},
52
59
  method(:reload),
53
60
  'reload',
@@ -71,6 +78,7 @@ module RSQL
71
78
  }
72
79
  end
73
80
 
81
+ attr_reader :prompt
74
82
  attr_accessor :bangs, :verbose
75
83
 
76
84
  def call_init_registrations
@@ -82,6 +90,9 @@ module RSQL
82
90
  end
83
91
 
84
92
  def load(fn, opt=nil)
93
+ @loaded_fns << fn unless @loaded_fns_state.key?(fn)
94
+ @loaded_fns_state[fn] = :loading
95
+
85
96
  # this should only be done after we have established a
86
97
  # mysql connection, so this option allows rsql to load the
87
98
  # init file immediately and then later make the init
@@ -102,6 +113,7 @@ module RSQL
102
113
  }.value
103
114
 
104
115
  if Exception === ret
116
+ @loaded_fns_state[fn] = :failed
105
117
  if @verbose
106
118
  $stderr.puts("#{ex.class}: #{ex.message}", ex.backtrace)
107
119
  else
@@ -110,7 +122,7 @@ module RSQL
110
122
  end
111
123
  ret = false
112
124
  else
113
- @loaded_fns << fn unless @loaded_fns.include?(fn)
125
+ @loaded_fns_state[fn] = :loaded
114
126
  call_init_registrations unless @skipping_init_registrations
115
127
  ret = true
116
128
  end
@@ -121,12 +133,35 @@ module RSQL
121
133
  end
122
134
 
123
135
  def reload
124
- @loaded_fns.each{|fn| self.load(fn, false)}
125
- puts "loaded: #{@loaded_fns.inspect}"
136
+ # some files may be loaded by other files, if so, we don't want to
137
+ # reload them again here
138
+ @loaded_fns.each{|fn| @loaded_fns_state[fn] = nil}
139
+ @loaded_fns.each{|fn| self.load(fn, :skip_init_registrations) if @loaded_fns_state[fn] == nil}
140
+
141
+ # load up the inits after all the normal registrations are ready
142
+ call_init_registrations
143
+
144
+ # report all the successfully loaded ones
145
+ loaded = []
146
+ @loaded_fns.each{|fn,state| loaded << fn if @loaded_fns_state[fn] == :loaded}
147
+ puts "loaded: #{loaded.inspect}"
126
148
  end
127
149
 
128
150
  def bang_eval(field, val)
129
- if bang = @bangs[field]
151
+ # allow individual bangs to override global ones, even if they're nil
152
+ if @bangs.key?(field)
153
+ bang = @bangs[field]
154
+ else
155
+ @global_bangs.each do |m,b|
156
+ if (String === m && m == field.to_s) ||
157
+ (Regexp === m && m.match(field.to_s))
158
+ bang = b
159
+ break
160
+ end
161
+ end
162
+ end
163
+
164
+ if bang
130
165
  begin
131
166
  val = Thread.new{ eval("#{bang}(val)") }.value
132
167
  rescue Exception => ex
@@ -421,6 +456,18 @@ module RSQL
421
456
  "server:v#{MySQLResults.conn.server_info}"
422
457
  end
423
458
 
459
+ # Show a short amount of information about acceptable syntax.
460
+ #
461
+ def help # :doc:
462
+ puts <<EOF
463
+
464
+ Converting values on the fly:
465
+
466
+ rsql> select name, value from rsql_example ! value => humanize_bytes;
467
+
468
+ EOF
469
+ end
470
+
424
471
  # Provide a helper utility in the event a registered
425
472
  # method would like to make its own queries.
426
473
  #
@@ -438,13 +485,21 @@ module RSQL
438
485
  nil
439
486
  end
440
487
 
488
+ # Register bangs to evaluate on all displayers as long as a column
489
+ # match is located. Bang keys may be either exact string matches or
490
+ # regular expressions.
491
+ #
492
+ def register_global_bangs(bangs)
493
+ @global_bangs.merge!(bangs)
494
+ end
495
+
441
496
  # Exactly like register below except in addition to registering as
442
497
  # a usable call for later, we will also use these as soon as we
443
498
  # have a connection to MySQL.
444
499
  #
445
500
  def register_init(sym, *args, &block) # :doc:
446
501
  register(sym, *args, &block)
447
- @init_registrations << sym
502
+ @init_registrations << sym unless @init_registrations.include?(sym)
448
503
  end
449
504
 
450
505
  # If given a block, allow the block to be called later, otherwise,
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (C) 2011 by Brad Robel-Forrest <brad+rsql@gigglewax.com>
2
+ # Copyright (C) 2011-2012 by Brad Robel-Forrest <brad+rsql@gigglewax.com>
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -19,9 +19,16 @@
19
19
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
20
  # THE SOFTWARE.
21
21
 
22
- module RSQL
22
+ # The standard MySQL hooks block Ruby threads, this interface provides an
23
+ # asynchronous query.
24
+ #
25
+ require 'mysqlplus'
23
26
 
24
- require 'mysql'
27
+ class Mysql
28
+ alias :query :async_query
29
+ end
30
+
31
+ module RSQL
25
32
 
26
33
  ########################################
27
34
  # A wrapper to make it easier to work with MySQL results (and prettier).
@@ -30,8 +30,8 @@ class TestEvalContext < Test::Unit::TestCase
30
30
  def test_reload
31
31
  orig = $stdout
32
32
  $stdout = out = StringIO.new
33
- @conn.expects(:query).with(instance_of(String)).returns(nil).times(2)
34
- @conn.expects(:affected_rows).returns(0).times(2)
33
+ @conn.expects(:query).with(instance_of(String)).returns(nil)
34
+ @conn.expects(:affected_rows).returns(0)
35
35
  @ctx.safe_eval('reload', nil, out)
36
36
  assert_match(/loaded: .+?example.rsqlrc/, out.string)
37
37
  ensure
@@ -117,7 +117,8 @@ class TestEvalContext < Test::Unit::TestCase
117
117
  end
118
118
 
119
119
  def test_complete
120
- assert_equal(18, @ctx.complete('').size)
120
+ out = @ctx.complete('')
121
+ assert_equal(19, out.size, out.inspect)
121
122
  assert_equal(['version'], @ctx.complete('v'))
122
123
  assert_equal(['.version'], @ctx.complete('.v'))
123
124
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rsql
3
3
  version: !ruby/object:Gem::Version
4
- hash: 31
4
+ hash: 29
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 2
9
- - 4
10
- version: 0.2.4
9
+ - 5
10
+ version: 0.2.5
11
11
  platform: ruby
12
12
  authors:
13
13
  - Brad Robel-Forrest
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-10-09 00:00:00 Z
18
+ date: 2012-01-14 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: net-ssh
@@ -34,19 +34,19 @@ dependencies:
34
34
  type: :runtime
35
35
  version_requirements: *id001
36
36
  - !ruby/object:Gem::Dependency
37
- name: mysql
37
+ name: mysqlplus
38
38
  prerelease: false
39
39
  requirement: &id002 !ruby/object:Gem::Requirement
40
40
  none: false
41
41
  requirements:
42
42
  - - ">="
43
43
  - !ruby/object:Gem::Version
44
- hash: 47
44
+ hash: 31
45
45
  segments:
46
- - 2
47
- - 8
48
46
  - 0
49
- version: 2.8.0
47
+ - 1
48
+ - 2
49
+ version: 0.1.2
50
50
  type: :runtime
51
51
  version_requirements: *id002
52
52
  - !ruby/object:Gem::Dependency