rsql 0.2.4 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
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