chukan 0.1.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.
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
+