rsql 0.1.9 → 0.2.0

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.
@@ -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.