chukan 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README +100 -0
  2. data/lib/chukan.rb +490 -0
  3. metadata +56 -0
data/README ADDED
@@ -0,0 +1,100 @@
1
+ # Chukan
2
+ An automation library for distributed systems.
3
+
4
+
5
+ ## Basic usage
6
+
7
+ #!/usr/bin/env ruby
8
+ require 'chukan'
9
+ include Chukan # include Chukan
10
+
11
+ srv = spawn("server -arg1 -arg2") # run 'server' command
12
+ # with '-arg1 -arg2' arguments
13
+ srv.stdout_join("started") # wait until the server outputs "started"
14
+
15
+ cli = spawn("client -arg1 -arg2") # run 'client' command with some arguments
16
+ srv.stdout_join("connected") # wait until the server outputs "connected"
17
+
18
+ cli.kill # send SIGKILL signal to the client
19
+ cli.join # wait until the client is really dead
20
+ srv.stderr_join(/disconnected/) # stderr and regexp are also available
21
+
22
+ srv.stdin.write "status\n" # input "status\n" to the server
23
+ srv.stdout_join("done") # wait until the server outputs "done"
24
+
25
+ if srv.stdout.read =~ /^client:/ # read output of the server
26
+ puts "** TEST FAILED **" # this library is usable for tests
27
+ # see also "Unit test" example below
28
+ end
29
+
30
+
31
+ ## Remote process execution
32
+
33
+ #!/usr/bin/env ruby
34
+ require 'chukan'
35
+ include Chukan # include Chukan
36
+
37
+ mac = remote("mymac.local") # login to the remote host using ssh and run
38
+ # commands on the host
39
+ # use ssh-agent if your key is encrypted
40
+ mac.cd("work/myproject") # run on "work/myproject" directory
41
+
42
+ linux = remote("192.168.10.2", "myname", ".id_rsa_linux")
43
+ # user name and path of the key are optional
44
+
45
+ cli_on_mac = mac.spawn("client -arg1") # run 'client' on the remote host
46
+ cli_on_linux = linux.spawn("client -arg1")
47
+
48
+ cli_on_mac.stdout_join("started") # signals and I/Os are also available
49
+
50
+
51
+ ## Unit test
52
+
53
+ #!/usr/bin/env ruby
54
+ require 'chukan'
55
+ include Chukan::Test # include Chukan::Test
56
+
57
+ test "load mylibrary" do # Chukan::Test provides 'test' and 'run' methods
58
+ require "mylibrary" # test will fail if the block returns nil or false,
59
+ # or an exception is raised
60
+ end
61
+
62
+ run {|b| # 'run' iterates YAML documents written after
63
+ # __END__ line
64
+ test "score <= 100", :TODO do # second argument of 'test' is :TODO or :SKIP
65
+ b.score <= 100 # which is useful for Test Anything Protocol
66
+ end # (TAP) processor like 'prove'
67
+ }
68
+
69
+ __END__
70
+ --- # YAML documents are here
71
+ name: test A
72
+ user: a-san
73
+ score: 10
74
+ ---
75
+ name: test B
76
+ user: b-san
77
+ score: 100
78
+
79
+
80
+ ## License
81
+ Copyright (c) 2009 FURUHASHI Sadayuki
82
+
83
+ Permission is hereby granted, free of charge, to any person obtaining a copy
84
+ of this software and associated documentation files (the "Software"), to deal
85
+ in the Software without restriction, including without limitation the rights
86
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
87
+ copies of the Software, and to permit persons to whom the Software is
88
+ furnished to do so, subject to the following conditions:
89
+
90
+ The above copyright notice and this permission notice shall be included in
91
+ all copies or substantial portions of the Software.
92
+
93
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
94
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
95
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
96
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
97
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
98
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
99
+ THE SOFTWARE.
100
+
@@ -0,0 +1,490 @@
1
+ #
2
+ # Chukan automation library for distributed systems
3
+ #
4
+ # Copyright (c) 2009 FURUHASHI Sadayuki
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+ #
24
+
25
+
26
+ ####
27
+ ## Basic usage
28
+ ##
29
+ =begin
30
+ #!/usr/bin/env ruby
31
+ require 'chukan'
32
+ include Chukan # include Chukan
33
+
34
+ srv = spawn("server -arg1 -arg2") # run 'server' command
35
+ # with '-arg1 -arg2' arguments
36
+ srv.stdout_join("started") # wait until the server outputs "started"
37
+
38
+ cli = spawn("client -arg1 -arg2") # run 'client' command with some arguments
39
+ srv.stdout_join("connected") # wait until the server outputs "connected"
40
+
41
+ cli.kill # send SIGKILL signal to the client
42
+ cli.join # wait until the client is really dead
43
+ srv.stderr_join(/disconnected/) # stderr and regexp are also available
44
+
45
+ srv.stdin.write "status\n" # input "status\n" to the server
46
+ srv.stdout_join("done") # wait until the server outputs "done"
47
+
48
+ if srv.stdout.read =~ /^client:/ # read output of the server
49
+ puts "** TEST FAILED **" # this library is usable for tests
50
+ # see also "Unit test" example below
51
+ end
52
+ =end
53
+
54
+
55
+ ####
56
+ ## Remote process execution
57
+ ##
58
+ =begin
59
+ #!/usr/bin/env ruby
60
+ require 'chukan'
61
+ include Chukan # include Chukan
62
+
63
+ mac = remote("mymac.local") # login to the remote host using ssh and run
64
+ # commands on the host
65
+ # use ssh-agent if your key is encrypted
66
+ mac.cd("work/myproject") # run on "work/myproject" directory
67
+
68
+ linux = remote("192.168.10.2", "myname", ".id_rsa_linux")
69
+ # user name and path of the key is optional
70
+
71
+ cli_on_mac = mac.spawn("client -arg1") # run 'client' on the remote host
72
+ cli_on_linux = linux.spawn("client -arg1")
73
+
74
+ cli_on_mac.stdout_join("started") # signals and I/Os are also available
75
+ =end
76
+
77
+
78
+ ####
79
+ ## Unit test
80
+ ##
81
+ =begin
82
+ #!/usr/bin/env ruby
83
+ require 'chukan'
84
+ include Chukan::Test # include Chukan::Test
85
+
86
+ test "load mylibrary" do # Chukan::Test provides 'test' and 'run' methods
87
+ require "mylibrary" # test will fail if the block returns nil or false,
88
+ # or an exception is raised
89
+ end
90
+
91
+ run {|b| # 'run' iterates YAML documents written after
92
+ # __END__ line
93
+ test "score <= 100", :TODO do # second argument of 'test' is :TODO or :SKIP
94
+ b.score <= 100 # which is useful for Test Anything Protocol
95
+ end # (TAP) processor like 'prove'
96
+ }
97
+
98
+ __END__
99
+ --- # YAML documents are here
100
+ name: test A
101
+ user: a-san
102
+ score: 10
103
+ ---
104
+ name: test B
105
+ user: b-san
106
+ score: 100
107
+ =end
108
+
109
+
110
+ require 'stringio'
111
+ require 'strscan'
112
+ require 'monitor'
113
+ require 'fcntl'
114
+
115
+
116
+ module Chukan
117
+ IO_BUFFER_LIMIT = 1024*1024
118
+
119
+ class LocalProcess
120
+ def initialize(*cmdline)
121
+ @cmdline = cmdline.map {|x| x.to_s }
122
+ @status = nil
123
+ @shortname = File.basename(cmdline.first.split(/\s/,2).first)[0, 12]
124
+ start
125
+ end
126
+
127
+ attr_reader :cmdline
128
+ attr_reader :stdin, :stdout, :stderr
129
+ attr_reader :pid
130
+ attr_reader :status
131
+ attr_reader :msg_prefix
132
+
133
+ def join
134
+ @status = Process.waitpid2(@pid)[1]
135
+ @stdout_reader.join
136
+ @stderr_reader.join
137
+ @killer.killed
138
+ reason = @status.inspect
139
+ if m = reason.match(/\,([^\>]*)\>/)
140
+ reason = m[1]
141
+ end
142
+ $stderr.puts "#{@msg_prefix}#{reason}"
143
+ @status
144
+ end
145
+
146
+ def stdout_join(pattern, &block)
147
+ io_join(@stdout, pattern, &block)
148
+ end
149
+
150
+ def stderr_join(pattern, &block)
151
+ io_join(@stderr, pattern, &block)
152
+ end
153
+
154
+ def signal(sig)
155
+ Process.kill(sig, @pid) rescue nil
156
+ self
157
+ end
158
+
159
+ def kill
160
+ signal(:SIGKILL)
161
+ end
162
+
163
+ def term
164
+ signal(:SIGTERM)
165
+ end
166
+
167
+ def hup
168
+ signal(:SIGHUP)
169
+ end
170
+
171
+ private
172
+ def io_join(io, pattern, &block)
173
+ if pattern.is_a?(String)
174
+ pattern = Regexp.new(Regexp.escape(pattern))
175
+ end
176
+ if block
177
+ io.synchronize {
178
+ io.read
179
+ }
180
+ yield
181
+ end
182
+ match = nil
183
+ io.synchronize {
184
+ until match = io.scanner.scan_until(pattern)
185
+ if io.closed_write?
186
+ raise EOFError.new("io closed: #{pattern.inspect}")
187
+ end
188
+ io.cond.wait
189
+ end
190
+ }
191
+ match
192
+ end
193
+
194
+ private
195
+ def start
196
+ stdin, @stdin = IO.pipe
197
+ @pout, pout = IO.pipe
198
+ @perr, perr = IO.pipe
199
+ @pid = fork
200
+ unless @pid
201
+ @stdin.close
202
+ @pout.close
203
+ @perr.close
204
+ $stdin.reopen(stdin)
205
+ $stdout.reopen(pout)
206
+ $stderr.reopen(perr)
207
+ exec *cmdline
208
+ exit 127
209
+ end
210
+ stdin.close
211
+ pout.close
212
+ perr.close
213
+ @stdin.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
214
+ @pout.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
215
+ @perr.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
216
+
217
+ @msg_prefix = "[%-12s %6d] " % [@shortname, @pid]
218
+ $stdout.puts "#{@msg_prefix}#{@cmdline.join(' ')}"
219
+
220
+ @stdout, @stdout_reader = self.class.start_scan(@pout, $stdout, @msg_prefix)
221
+ @stderr, @stderr_reader = self.class.start_scan(@perr, $stderr, @msg_prefix)
222
+
223
+ @killer = ZombieKiller.define_finalizer(self, @pid)
224
+ end
225
+
226
+ def self.start_scan(pipe, out, msg_prefix)
227
+ io = StringIO.new
228
+ io.extend(MonitorMixin)
229
+ cond = io.new_cond
230
+ scanner = StringScanner.new(io.string)
231
+ (class<<io; self; end).instance_eval do
232
+ define_method(:cond) { cond }
233
+ define_method(:scanner) { scanner }
234
+ end
235
+
236
+ reader = Thread.start(pipe, io,
237
+ out, msg_prefix,
238
+ &method(:reader_thread))
239
+
240
+ return io, reader
241
+ end
242
+
243
+ def self.reader_thread(src, dst, msgout, msg_prefix)
244
+ buf = ""
245
+ line = ''
246
+ begin
247
+ while src.sysread(1024, buf)
248
+ dst.synchronize {
249
+ dst.string << buf
250
+ if dst.string.size > IO_BUFFER_LIMIT
251
+ cut = dst.string.size - IO_BUFFER_LIMIT
252
+ dst.string.slice!(0, cut)
253
+ dst.pos = (dst.pos > cut) ?
254
+ dst.pos - cut : 0
255
+ dst.scanner.pos = (dst.scanner.pos > cut) ?
256
+ dst.scanner.pos - cut : 0
257
+ end
258
+ dst.cond.signal
259
+ }
260
+ line << buf
261
+ line.gsub!(/.*\n/) {|l|
262
+ msgout.puts "#{msg_prefix}#{l}"
263
+ msgout.flush
264
+ ""
265
+ }
266
+ end
267
+ rescue
268
+ nil
269
+ ensure
270
+ src.close
271
+ dst.synchronize {
272
+ dst.close_write
273
+ dst.cond.signal
274
+ }
275
+ unless line.empty?
276
+ msgout.puts "#{msg_prefix}#{line}"
277
+ end
278
+ end
279
+ end
280
+ end
281
+
282
+
283
+ class ZombieKiller
284
+ def initialize(pid)
285
+ @pid = pid
286
+ end
287
+ def killed
288
+ @pid = nil
289
+ end
290
+ attr_reader :pid
291
+
292
+ def self.define_finalizer(obj, pid)
293
+ killer = self.new(pid)
294
+ ObjectSpace.define_finalizer(obj, self.finalizer(killer))
295
+ killer
296
+ end
297
+
298
+ def self.finalizer(killer)
299
+ proc {
300
+ if pid = killer.pid
301
+ [:SIGTERM, :SIGKILL].each {|sig|
302
+ Process.kill(sig, pid)
303
+ break if 10.times {
304
+ begin
305
+ if Process.waitpid(pid, Process::WNOHANG)
306
+ break true
307
+ end
308
+ sleep 0.1
309
+ rescue
310
+ break true
311
+ end
312
+ nil
313
+ }
314
+ }
315
+ end
316
+ }
317
+ end
318
+ end
319
+
320
+
321
+ class RemoteProcess < LocalProcess
322
+ def initialize(remote, *cmdline, &block)
323
+ @remote = remote
324
+
325
+ cmdline_real = ["echo","$$","&&","exec"] + cmdline
326
+ super(*remote.command(*cmdline_real), &block)
327
+
328
+ @shortname = File.basename(cmdline.first.split(/\s/,2).first)[0, 7] +
329
+ "@"+remote.host[0,5]
330
+
331
+ stdout_join("\n")
332
+ @rpid = stdout.gets.to_i
333
+ end
334
+ attr_reader :rpid
335
+
336
+ def signal(sig)
337
+ system(*@remote.command("kill -#{sig} #{@rpid}"))
338
+ self
339
+ end
340
+ end
341
+
342
+
343
+ class Remote
344
+ def initialize(host, user = nil, key = nil)
345
+ @host = host
346
+ @user = user
347
+ @key = key
348
+ @dir = nil
349
+ end
350
+ attr_reader :host
351
+
352
+ def cd(dir = nil)
353
+ @dir = dir
354
+ self
355
+ end
356
+
357
+ def command(*cmdline)
358
+ ssh = ENV["SSH"] || "ssh"
359
+ cmd = [ssh, "-o", "Batchmode yes"]
360
+ cmd += ["-i", @key] if @key
361
+ if @user
362
+ cmd.push "#{@user}:#{@host}"
363
+ else
364
+ cmd.push @host
365
+ end
366
+ if @dir
367
+ cmd += ["cd", @dir, "&&"]
368
+ end
369
+ cmd + cmdline.map {|x| x.to_s }
370
+ end
371
+
372
+ def spawn(*cmdline, &block)
373
+ RemoteProcess.new(self, *cmdline, &block)
374
+ end
375
+ end
376
+
377
+
378
+ def spawn(*cmdline, &block)
379
+ LocalProcess.new(*cmdline, &block)
380
+ end
381
+
382
+ def remote(host, user = nil, key = nil)
383
+ Remote.new(host, user, key)
384
+ end
385
+
386
+
387
+ module Test
388
+ @@start = nil
389
+ @@cases = Hash.new {|hash,key| hash[key] = [0,0,0] }
390
+ @@count = 0
391
+ @@data = nil
392
+
393
+ if ENV["TERM"] =~ /color/i && $stdout.stat.chardev?
394
+ SEPARATOR = ""
395
+ module Color
396
+ SUCCESS = "\e[0;32m"
397
+ FAIL = "\e[1;33m"
398
+ ERROR = "\e[0;31m"
399
+ NORMAL = "\e[00m"
400
+ end
401
+ else
402
+ SEPARATOR = "\n"
403
+ module Color
404
+ SUCCESS = ""
405
+ FAIL = ""
406
+ ERROR = ""
407
+ NORMAL = ""
408
+ end
409
+ end
410
+
411
+ def self.report
412
+ proc {
413
+ finish = Time.now
414
+ puts "\n1..#{@@count}"
415
+ $stderr.puts "Finished in #{finish - @@start} seconds."
416
+ $stderr.puts ""
417
+ succes = @@cases.to_a.inject(0) {|r,(n,c)| r + c[0] }
418
+ failure = @@cases.to_a.inject(0) {|r,(n,c)| r + c[1] }
419
+ error = @@cases.to_a.inject(0) {|r,(n,c)| r + c[2] }
420
+ $stderr.puts "#{@@cases.size} tests, " +
421
+ "#{succes+failure+error} assertions, " +
422
+ "#{Color::FAIL}#{failure} failures, " +
423
+ "#{Color::ERROR}#{error} errors" +
424
+ "#{Color::NORMAL}"
425
+ }
426
+ end
427
+
428
+ def self.included(mod)
429
+ unless @@start
430
+ ObjectSpace.define_finalizer(mod, report)
431
+ @@start = Time.now
432
+ end
433
+ end
434
+
435
+ def test(name = nil, directive = nil, &block)
436
+ if yield
437
+ tap(Color::SUCCESS, "ok", @@count+=1, name, directive)
438
+ @@cases[name][0] += 1
439
+ else
440
+ tap(Color::FAIL, "not ok", @@count+=1, name, directive)
441
+ print_backtrace(caller, "test failed")
442
+ @@cases[name][1] += 1
443
+ end
444
+ rescue Exception
445
+ tap(Color::ERROR, "not ok", @@count+=1, name, directive)
446
+ print_backtrace($!.backtrace, "#{$!} (#{$!.class})")
447
+ @@cases[name][2] += 1
448
+ ensure
449
+ print Color::NORMAL
450
+ end
451
+
452
+ def data
453
+ require 'yaml'
454
+ @@data ||= YAML.load_stream(DATA.read.gsub(/(^\t+)/) {
455
+ ' ' * $+.length
456
+ }).documents.map {|obj|
457
+ obj.each_pair {|k,v|
458
+ (class<<obj; self; end).instance_eval do
459
+ define_method(k) { v }
460
+ end rescue nil
461
+ } rescue obj
462
+ }
463
+ end
464
+
465
+ def run(&block)
466
+ data.each {|d|
467
+ yield d
468
+ }
469
+ end
470
+
471
+ private
472
+ def tap(color, stat, count, name, directive = nil)
473
+ if directive
474
+ directive = " # #{directive.to_s.upcase}"
475
+ end
476
+ puts "#{color}#{SEPARATOR}#{stat} #{count} - #{name}#{directive}"
477
+ end
478
+
479
+ def print_backtrace(trace, msg)
480
+ $stderr.puts "#{trace.shift}: #{msg}"
481
+ trace.each {|c|
482
+ unless c.to_s.include?(__FILE__)
483
+ $stderr.puts "\tfrom #{c}"
484
+ end
485
+ }
486
+ end
487
+ end
488
+
489
+ end
490
+
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chukan
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - FURUHASHI Sadayuki
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-25 00:00:00 +09:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: frsyuki@users.sourceforge.jp
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - lib/chukan.rb
26
+ - README
27
+ has_rdoc: true
28
+ homepage:
29
+ licenses: []
30
+
31
+ post_install_message:
32
+ rdoc_options: []
33
+
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: "0"
41
+ version:
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ version:
48
+ requirements: []
49
+
50
+ rubyforge_project:
51
+ rubygems_version: 1.3.5
52
+ signing_key:
53
+ specification_version: 3
54
+ summary: automation library for distributed systems
55
+ test_files: []
56
+