rsql 0.1.9 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -44,11 +44,11 @@ RSQL is invoked from the comamnd line using:
44
44
  separate fields with a tab character).
45
45
 
46
46
  -ssh _ssh_host_::
47
- Establish an SSH connection before connecting to the MySQL host.
47
+ Establish a SSH connection before connecting to the MySQL host.
48
48
 
49
49
  -sshconfig _ssh_config_::
50
- Use a specific SSH configuration file instead of the ones
51
- dynamically determined.
50
+ Use a specific SSH configuration file instead of the default files
51
+ loaded at runtime by Net::SSH.
52
52
 
53
53
  -e [_query_]::
54
54
  Run a query from the command line (i.e. not interactive). If a
@@ -58,21 +58,33 @@ RSQL is invoked from the comamnd line using:
58
58
  *must* be the last option specified.
59
59
 
60
60
  The _ssh_host_ and _mysql_host_ arguments may optionally include
61
- _user_ and _password_ values using the following syntax:
61
+ _user_, _password_, or _port_ values using the following syntax:
62
62
 
63
- [<user>[:<password>]@]<host>
64
-
65
- An empty password can be provided by simply listing nothing between
66
- demarcation points:
67
-
68
- root:@127.0.0.1
63
+ [<user>[:<password>]@]<host>[:<port>]
69
64
 
70
65
  Once at the +rsql+ prompt, normal MySQL queries can be entered as
71
66
  expected, ending each with a semicolon (;) for columnar output or \G
72
67
  for line-by-line output formatting.
73
68
 
74
69
  Ruby commands will be evaluated for any content entered at the RSQL
75
- prompt beginning with a period (.).
70
+ prompt beginning with a period.
71
+
72
+ ==== Command Line Examples
73
+
74
+ Connect as the "root" user to a MySQL server running on the local
75
+ host, with no password (because there are no characters listed between
76
+ the colon and the at sign):
77
+
78
+ rsql root:@127.0.0.1
79
+
80
+ Connect as the "readonly" user to the "internal.database.com" host's
81
+ MySQL server after establishing a SSH tunnel to the
82
+ "external.acme.com" gateway. In this case, we are either expecting
83
+ that our SSH configuration is set up with the right user name. Because
84
+ we did not provide a password for MySQL, one will be obtained directly
85
+ from the console (without echoing the characters typed):
86
+
87
+ rsql -ssh external.acme.com readonly@database.acme.com
76
88
 
77
89
  == GETTING STARTED
78
90
 
@@ -86,6 +98,8 @@ downloaded with the source.
86
98
 
87
99
  == LICENSE
88
100
 
101
+ RSQL is licensed under the MIT License:
102
+
89
103
  Copyright (C) 2011 by Brad Robel-Forrest <brad+rsql@gigglewax.com>
90
104
 
91
105
  Permission is hereby granted, free of charge, to any person obtaining
data/bin/rsql CHANGED
@@ -27,6 +27,7 @@ begin
27
27
  rescue LoadError
28
28
  end
29
29
 
30
+ require 'tmpdir'
30
31
  require 'thread'
31
32
  require 'timeout'
32
33
  require 'readline'
@@ -92,10 +93,12 @@ def get_password(prompt)
92
93
  end
93
94
 
94
95
  # safely separate login credentials while preserving "emtpy" values--
95
- # anything of the form [<username>[:<password]@]<host>
96
+ # anything of the form [<username>[:<password]@]<host>[:<port>]
96
97
  #
97
98
  def split_login(str)
98
99
  login = []
100
+ # search from the right so we don't pick out ampersands in a
101
+ # password or username
99
102
  if i = str.rindex(?@)
100
103
  login << str[i+1..-1]
101
104
  if 0 < i
@@ -114,6 +117,11 @@ def split_login(str)
114
117
  else
115
118
  login << str
116
119
  end
120
+ if login.first.sub!(/:(\d+)$/,'')
121
+ login << $1.to_i
122
+ else
123
+ login << nil
124
+ end
117
125
  end
118
126
 
119
127
  if ARGV.delete('-version')
@@ -139,7 +147,7 @@ end
139
147
  if i = ARGV.index('-ssh')
140
148
  require 'net/ssh'
141
149
  ARGV.delete_at(i)
142
- (ssh_host, ssh_user, ssh_password) = split_login(ARGV.delete_at(i))
150
+ (ssh_host, ssh_user, ssh_password, ssh_port) = split_login(ARGV.delete_at(i))
143
151
  end
144
152
 
145
153
  if i = ARGV.index('-sshconfig')
@@ -165,8 +173,9 @@ if ARGV.size < 1
165
173
 
166
174
  usage: #{bn} [-version] [-help] [-verbose]
167
175
  #{prefix}[-rc <rcfile>] [-maxrows <max>] [-batch <field_separator>]
168
- #{prefix}[-ssh [<ssh_user>[:<ssh_password>]@]<ssh_host>] [-sshconfig <ssh_config>]
169
- #{prefix}[<mysql_user>[:<mysql_password>]@]<mysql_host>
176
+ #{prefix}[-ssh [<ssh_user>[:<ssh_password>]@]<ssh_host>[:<ssh_port>]
177
+ #{prefix}[-sshconfig <ssh_config>]
178
+ #{prefix}[<mysql_user>[:<mysql_password>]@]<mysql_host>[:<mysql_port>]
170
179
  #{prefix}[<database>] [-e <remaining_args_as_input>]
171
180
 
172
181
  If -ssh is used, a SSH tunnel is established before trying to
@@ -180,14 +189,15 @@ USAGE
180
189
  exit 1
181
190
  end
182
191
 
183
- (mysql_host, mysql_user, mysql_password) = split_login(ARGV.shift)
184
- mysql_password ||= get_password("#{mysql_host}@#{mysql_host} MySQL password: ")
192
+ (mysql_host, mysql_user, mysql_password, mysql_port) = split_login(ARGV.shift)
193
+ mysql_password ||= get_password("#{mysql_user}@#{mysql_host} MySQL password: ")
185
194
  real_mysql_host = mysql_host
186
195
 
187
196
  if ssh_host
188
197
  # randomly pick a tcp port above 1024
198
+ remote_mysql_port = mysql_port || 3306
189
199
  mysql_port = rand(0xffff-1025) + 1025
190
- else
200
+ elsif mysql_port.nil?
191
201
  mysql_port = 3306
192
202
  end
193
203
 
@@ -227,20 +237,35 @@ end
227
237
 
228
238
  MySQLResults.max_rows ||= batch_output ? 5000 : 1000
229
239
 
230
- ssh_enabled = false
231
-
232
240
  if ssh_host
233
241
 
234
242
  # might need to open an idle channel here so server doesn't close on
235
243
  # us...or just loop reconnection here in the thread...
236
244
 
237
- port_opened = false
238
- getting_password = false
239
245
  password_retry_cnt = 0
240
246
 
241
- puts "SSH #{ssh_user}#{ssh_user ? '@' : ''}#{ssh_host}..." unless batch_input
247
+ unless batch_input
248
+ print "SSH #{ssh_user}#{ssh_user ? '@' : ''}#{ssh_host}..."
249
+ $stdout.flush
250
+ end
251
+
252
+ # we have to run mysql in a separate process due to the blocking
253
+ # nature of its calls interfering with the pure ruby ssh
254
+ # calls...so we'll run ssh in the background since its only
255
+ # purpose is to forward us in for accessing the mysql server
256
+
257
+ # we'll use a poor-man's ipc to determine when the ssh process is
258
+ # ready
259
+ ipc_fn = File.join(Dir.tmpdir, "rsql_ssh_#{$$}.pid")
260
+
242
261
  ssh = nil
243
- ssh_thread = Thread.new do
262
+ ssh_pid = Process.fork do
263
+ File.open(ipc_fn,'w'){|f| f.puts('start')}
264
+ ssh_enabled = false
265
+ Signal.trap('INT') do
266
+ $stderr.puts 'Shutting down...'
267
+ ssh_enabled = false
268
+ end
244
269
  opts = {:timeout => 15}
245
270
  opts[:config] = ssh_config if ssh_config
246
271
  if verbose
@@ -254,14 +279,14 @@ if ssh_host
254
279
  opts[:password] = ssh_password if ssh_password
255
280
  ssh = Net::SSH.start(ssh_host, ssh_user, opts)
256
281
  ssh_enabled = true
282
+ printf "connected (#{$$})..."
283
+ $stdout.flush
257
284
  rescue Net::SSH::AuthenticationFailed
258
285
  if 2 < password_retry_cnt
259
286
  $stderr.puts 'Permission denied. Giving up.'
260
287
  else
261
288
  $stderr.puts 'Permission denied, please try again.' if ssh_password
262
- getting_password = true
263
289
  ssh_password = get_password("#{ssh_user}@#{ssh_host} SSH password: ")
264
- getting_password = false
265
290
  unless ssh_password.empty?
266
291
  password_retry_cnt += 1
267
292
  retry
@@ -269,39 +294,25 @@ if ssh_host
269
294
  end
270
295
  end
271
296
  if ssh_enabled
272
- ssh.forward.local(mysql_port, mysql_host, 3306)
273
- port_opened = true
297
+ ssh.forward.local(mysql_port, mysql_host, remote_mysql_port)
298
+ puts(verbose ? "ready (#{mysql_port} => #{remote_mysql_port})" : 'ready')
299
+ File.open(ipc_fn,'w'){|f| f.puts('ready')}
274
300
  ssh.loop(1) { ssh_enabled }
275
301
  end
302
+ File.open(ipc_fn,'w'){|f| f.puts('fail')}
276
303
  end
277
304
 
278
- 15.times do
279
- break if !ssh_thread.alive? || (ssh_enabled && port_opened)
280
- while getting_password
281
- # give them extra time
282
- sleep(1)
283
- end
305
+ ipc_state = ''
306
+ 60.times do
284
307
  sleep(1)
308
+ File.open(ipc_fn,'r'){|f| ipc_state = f.gets.strip}
309
+ break if ipc_state == 'ready' || ipc_state == 'fail'
285
310
  end
286
311
 
287
- unless ssh_enabled
288
- $stderr.puts "failed to connect to #{ssh_host} SSH host"
289
- begin
290
- ssh_thread.join
291
- rescue Exception => ex
292
- $stderr.puts(" => #{ex.message} (#{ex.class})")
293
- end
294
- exit 1
295
- end
312
+ File.unlink(ipc_fn)
296
313
 
297
- unless port_opened
298
- $stderr.puts("failed to forward #{mysql_port}:#{mysql_host}:3306 via #{ssh_host} " \
299
- "ssh host in 15 seconds")
300
- begin
301
- ssh_thread.join
302
- rescue Exception => ex
303
- $stderr.puts(" => #{ex.message}: #{ex.class}")
304
- end
314
+ unless ipc_state == 'ready'
315
+ $stderr.puts "failed to connect to #{ssh_host} SSH host"
305
316
  exit 1
306
317
  end
307
318
 
@@ -309,16 +320,24 @@ if ssh_host
309
320
  mysql_host = '127.0.0.1'
310
321
  end
311
322
 
312
- puts "MySQL #{mysql_user}@#{real_mysql_host}..." unless batch_input
323
+ unless batch_input
324
+ print "MySQL #{mysql_user}@#{real_mysql_host}..."
325
+ $stdout.flush
326
+ end
327
+
328
+ mysql_conn = "#{mysql_host}:#{remote_mysql_port || mysql_port}"
329
+
313
330
  begin
314
331
  MySQLResults.conn = Mysql.new(mysql_host, mysql_user, mysql_password,
315
332
  MySQLResults.database_name, mysql_port)
333
+ puts 'connected'
316
334
  rescue Mysql::Error => ex
317
335
  if ex.message.include?('Client does not support authentication')
318
- $stderr.puts "failed to connect to #{mysql_host} mysql server: unknown credentials?"
336
+ $stderr.puts "failed to connect to #{mysql_conn} mysql server: unknown credentials?"
319
337
  else
320
- $stderr.puts "failed to connect to #{mysql_host} mysql server: #{ex.message}"
338
+ $stderr.puts "failed to connect to #{mysql_conn} mysql server: #{ex.message}"
321
339
  end
340
+ $stderr.puts ex.backtrace if verbose
322
341
  exit 1
323
342
  rescue NoMethodError
324
343
  # this happens when mysql tries to read four bytes and assume it
@@ -326,10 +345,12 @@ rescue NoMethodError
326
345
  # because the connect succeeds due to the SSH forwarded port but
327
346
  # then there isn't anybody connected on the remote side of the
328
347
  # proxy
329
- $stderr.puts "failed to connect to #{mysql_host} mysql server"
348
+ $stderr.puts "failed to connect to #{mysql_conn} mysql server"
349
+ $stderr.puts ex.backtrace if verbose
330
350
  exit 1
331
351
  rescue Exception => ex
332
- $stderr.puts "failed to connect to #{mysql_host} mysql server: #{ex.message} (#{ex.class})"
352
+ $stderr.puts "failed to connect to #{mysql_conn} mysql server: #{ex.message} (#{ex.class})"
353
+ $stderr.puts ex.backtrace if verbose
333
354
  exit 1
334
355
  end
335
356
 
@@ -417,7 +438,6 @@ unless MySQLResults.conn.nil?
417
438
  end
418
439
 
419
440
  sleep(0.3)
420
- ssh_enabled = false
421
441
 
422
442
  if Readline::HISTORY.any?
423
443
  if 100 < Readline::HISTORY.size
@@ -428,6 +448,7 @@ if Readline::HISTORY.any?
428
448
  File.open(history_fn, 'w') {|f| YAML.dump(Readline::HISTORY.to_a, f)}
429
449
  end
430
450
 
431
- if ssh_thread
432
- safe_timeout(ssh_thread, :join, 'SSH')
451
+ if ssh_pid && 0 <= ssh_pid
452
+ Process.kill('INT', ssh_pid)
453
+ Process.waitpid(ssh_pid)
433
454
  end
@@ -92,7 +92,7 @@ INSERT IGNORE INTO #{@rsql_table}
92
92
  value=#{9**9},
93
93
  stuff=0x1234567891234567891234567890;
94
94
  }
95
- sqeeze!(sql)
95
+ squeeze!(sql)
96
96
  end
97
97
 
98
98
  # A very common reason for recipes is simply to add parameters to be
@@ -2,9 +2,8 @@
2
2
  # Commands using an EvalContext for handling recipes.
3
3
  #
4
4
  module RSQL
5
- VERSION = '0.1.9'
5
+ VERSION = '0.2.0'
6
6
 
7
- require 'rsql/mysql'
8
7
  require 'rsql/mysql_results'
9
8
  require 'rsql/eval_context'
10
9
  require 'rsql/commands'
@@ -149,13 +149,28 @@ module RSQL
149
149
  end
150
150
 
151
151
  begin
152
- value = Thread.new{ eval('$SAFE=2;' + content) }.value
153
- rescue Exception => ex
154
- if @verbose
155
- $stderr.puts("#{ex.class}: #{ex.message}", ex.backtrace)
156
- else
157
- $stderr.puts(ex.message.gsub(/\(eval\):\d+:/,''))
152
+ # in order to print out errors in a loaded script so
153
+ # that we have file/line info, we need to rescue their
154
+ # exceptions inside the evaluation
155
+ th = Thread.new do
156
+ eval('$SAFE=2;begin;' << content << %q{
157
+ rescue Exception => ex
158
+ if @verbose
159
+ $stderr.puts("#{ex.class}: #{ex.message}", ex.backtrace)
160
+ else
161
+ bt = []
162
+ ex.backtrace.each do |t|
163
+ break if t.include?('bin/rsql')
164
+ bt << t unless t.include?('lib/rsql/') || t.include?('(eval)')
165
+ end
166
+ $stderr.puts(ex.message.gsub(/\(eval\):\d+:/,''),bt)
167
+ end
168
+ end
169
+ })
158
170
  end
171
+ value = th.value
172
+ rescue Exception => ex
173
+ $stderr.puts(ex.message.gsub(/\(eval\):\d+:/,''))
159
174
  ensure
160
175
  $stdout = orig_stdout if stdout
161
176
  end
@@ -409,7 +424,7 @@ module RSQL
409
424
 
410
425
  if block.nil?
411
426
  source = args.pop
412
- sql = sqeeze!(source.dup)
427
+ sql = squeeze!(source.dup)
413
428
 
414
429
  argstr = args.join(',')
415
430
  usage << "(#{argstr})" unless argstr.empty?
@@ -534,7 +549,7 @@ module RSQL
534
549
 
535
550
  # Squeeze out any spaces.
536
551
  #
537
- def sqeeze!(sql) # :doc:
552
+ def squeeze!(sql) # :doc:
538
553
  sql.gsub!(/\s+/,' ')
539
554
  sql.strip!
540
555
  sql << ';' unless sql[-1] == ?;
@@ -21,12 +21,17 @@
21
21
 
22
22
  module RSQL
23
23
 
24
+ require 'mysql'
25
+
24
26
  ########################################
25
27
  # A wrapper to make it easier to work with MySQL results (and prettier).
26
28
  #
27
29
  class MySQLResults
28
30
 
29
- HEX_RANGE = (Mysql::Field::TYPE_TINY_BLOB..Mysql::Field::TYPE_STRING)
31
+ HEX_RANGE = [
32
+ Mysql::Field::TYPE_BLOB,
33
+ Mysql::Field::TYPE_STRING,
34
+ ]
30
35
 
31
36
  @@conn = nil
32
37
  @@field_separator = ' '
@@ -9,7 +9,6 @@ end
9
9
  require 'mocha'
10
10
 
11
11
  $: << File.expand_path(File.join(File.dirname(__FILE__),'..','lib')) << File.dirname(__FILE__)
12
- require 'dummy_mysql.rb'
13
12
  require 'rsql/mysql_results.rb'
14
13
  require 'rsql/eval_context.rb'
15
14
  require 'rsql/commands.rb'
@@ -10,7 +10,6 @@ end
10
10
  require 'mocha'
11
11
 
12
12
  $: << File.expand_path(File.join(File.dirname(__FILE__),'..','lib')) << File.dirname(__FILE__)
13
- require 'dummy_mysql.rb'
14
13
  require 'rsql/mysql_results.rb'
15
14
  require 'rsql/eval_context.rb'
16
15
 
@@ -9,7 +9,6 @@ end
9
9
  require 'mocha'
10
10
 
11
11
  $: << File.expand_path(File.join(File.dirname(__FILE__),'..','lib')) << File.dirname(__FILE__)
12
- require 'dummy_mysql.rb'
13
12
  require 'rsql/mysql_results.rb'
14
13
 
15
14
  class TestMySQLResults < Test::Unit::TestCase
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: 9
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 1
9
- - 9
10
- version: 0.1.9
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
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-06-05 00:00:00 Z
18
+ date: 2011-09-18 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: net-ssh
@@ -34,9 +34,25 @@ dependencies:
34
34
  type: :runtime
35
35
  version_requirements: *id001
36
36
  - !ruby/object:Gem::Dependency
37
- name: mocha
37
+ name: mysql
38
38
  prerelease: false
39
39
  requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 47
45
+ segments:
46
+ - 2
47
+ - 8
48
+ - 0
49
+ version: 2.8.0
50
+ type: :runtime
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: mocha
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
40
56
  none: false
41
57
  requirements:
42
58
  - - ">="
@@ -48,11 +64,11 @@ dependencies:
48
64
  - 12
49
65
  version: 0.9.12
50
66
  type: :development
51
- version_requirements: *id002
67
+ version_requirements: *id003
52
68
  - !ruby/object:Gem::Dependency
53
69
  name: rake
54
70
  prerelease: false
55
- requirement: &id003 !ruby/object:Gem::Requirement
71
+ requirement: &id004 !ruby/object:Gem::Requirement
56
72
  none: false
57
73
  requirements:
58
74
  - - ">="
@@ -62,11 +78,11 @@ dependencies:
62
78
  - 0
63
79
  version: "0"
64
80
  type: :development
65
- version_requirements: *id003
81
+ version_requirements: *id004
66
82
  - !ruby/object:Gem::Dependency
67
83
  name: rdoc
68
84
  prerelease: false
69
- requirement: &id004 !ruby/object:Gem::Requirement
85
+ requirement: &id005 !ruby/object:Gem::Requirement
70
86
  none: false
71
87
  requirements:
72
88
  - - ">="
@@ -76,11 +92,11 @@ dependencies:
76
92
  - 0
77
93
  version: "0"
78
94
  type: :development
79
- version_requirements: *id004
95
+ version_requirements: *id005
80
96
  - !ruby/object:Gem::Dependency
81
97
  name: rcov
82
98
  prerelease: false
83
- requirement: &id005 !ruby/object:Gem::Requirement
99
+ requirement: &id006 !ruby/object:Gem::Requirement
84
100
  none: false
85
101
  requirements:
86
102
  - - ">="
@@ -90,7 +106,7 @@ dependencies:
90
106
  - 0
91
107
  version: "0"
92
108
  type: :development
93
- version_requirements: *id005
109
+ version_requirements: *id006
94
110
  description: |
95
111
  RSQL makes working with a MySQL command line more convenient through
96
112
  the use of recipes and embedding the common operation of using a SSH
@@ -112,11 +128,9 @@ files:
112
128
  - lib/rsql/commands.rb
113
129
  - lib/rsql/eval_context.rb
114
130
  - lib/rsql/mysql_results.rb
115
- - test/dummy_mysql.rb
116
131
  - test/test_commands.rb
117
132
  - test/test_eval_context.rb
118
133
  - test/test_mysql_results.rb
119
- - lib/rsql/mysql.rb
120
134
  homepage: https://rubygems.org/gems/rsql
121
135
  licenses: []
122
136
 
@@ -126,8 +140,6 @@ rdoc_options:
126
140
  - RSQL Documentation
127
141
  - --main
128
142
  - README.rdoc
129
- - --exclude
130
- - mysql.rb
131
143
  require_paths:
132
144
  - lib
133
145
  required_ruby_version: !ruby/object:Gem::Requirement
@@ -135,12 +147,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
135
147
  requirements:
136
148
  - - ">="
137
149
  - !ruby/object:Gem::Version
138
- hash: 51
150
+ hash: 55
139
151
  segments:
140
152
  - 1
141
153
  - 8
142
- - 2
143
- version: 1.8.2
154
+ - 0
155
+ version: 1.8.0
144
156
  required_rubygems_version: !ruby/object:Gem::Requirement
145
157
  none: false
146
158
  requirements:
@@ -153,7 +165,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
165
  requirements: []
154
166
 
155
167
  rubyforge_project:
156
- rubygems_version: 1.8.5
168
+ rubygems_version: 1.8.10
157
169
  signing_key:
158
170
  specification_version: 3
159
171
  summary: Ruby based MySQL command line with recipes.