session 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/lib/session-2.4.0.rb +744 -0
  2. data/lib/session.rb +744 -0
  3. data/test/session.rb +227 -0
  4. metadata +41 -0
@@ -0,0 +1,744 @@
1
+ require 'open3'
2
+ require 'tmpdir'
3
+ require 'thread'
4
+ require 'yaml'
5
+ require 'tempfile'
6
+
7
+ module Session
8
+ #--{{{
9
+ VERSION = '2.4.0'
10
+
11
+ @track_history = ENV['SESSION_HISTORY'] || ENV['SESSION_TRACK_HISTORY']
12
+ @use_spawn = ENV['SESSION_USE_SPAWN']
13
+ @use_open3 = ENV['SESSION_USE_OPEN3']
14
+ @debug = ENV['SESSION_DEBUG']
15
+
16
+ class << self
17
+ #--{{{
18
+ attr :track_history, true
19
+ attr :use_spawn, true
20
+ attr :use_open3, true
21
+ attr :debug, true
22
+ def new(*a, &b)
23
+ #--{{{
24
+ Sh::new(*a, &b)
25
+ #--}}}
26
+ end
27
+ alias [] new
28
+ #--}}}
29
+ end
30
+
31
+ class PipeError < StandardError; end
32
+ class ExecutionError < StandardError; end
33
+
34
+ class History
35
+ #--{{{
36
+ def initialize; @a = []; end
37
+ def method_missing(m,*a,&b); @a.send(m,*a,&b); end
38
+ def to_yaml(*a,&b); @a.to_yaml(*a,&b); end
39
+ alias to_s to_yaml
40
+ alias to_str to_yaml
41
+ #--}}}
42
+ end # class History
43
+ class Command
44
+ #--{{{
45
+ class << self
46
+ #--{{{
47
+ def cmdno; @cmdno ||= 0; end
48
+ def cmdno= n; @cmdno = n; end
49
+ #--}}}
50
+ end
51
+
52
+ # attributes
53
+ #--{{{
54
+ attr :cmd
55
+ attr :cmdno
56
+ attr :out,true
57
+ attr :err,true
58
+ attr :cid
59
+ attr :begin_out
60
+ attr :end_out
61
+ attr :begin_out_pat
62
+ attr :end_out_pat
63
+ attr :begin_err
64
+ attr :end_err
65
+ attr :begin_err_pat
66
+ attr :end_err_pat
67
+ #--}}}
68
+
69
+ def initialize(command)
70
+ #--{{{
71
+ @cmd = command.to_s
72
+ @cmdno = self.class.cmdno
73
+ self.class.cmdno += 1
74
+ @err = ''
75
+ @out = ''
76
+ @cid = "%d_%d_%d" % [$$, cmdno, rand(Time.now.usec)]
77
+ @begin_out = "__CMD_OUT_%s_BEGIN__" % cid
78
+ @end_out = "__CMD_OUT_%s_END__" % cid
79
+ @begin_out_pat = %r/#{ Regexp.escape(@begin_out) }/
80
+ @end_out_pat = %r/#{ Regexp.escape(@end_out) }/
81
+ @begin_err = "__CMD_ERR_%s_BEGIN__" % cid
82
+ @end_err = "__CMD_ERR_%s_END__" % cid
83
+ @begin_err_pat = %r/#{ Regexp.escape(@begin_err) }/
84
+ @end_err_pat = %r/#{ Regexp.escape(@end_err) }/
85
+ #--}}}
86
+ end
87
+ def to_hash
88
+ #--{{{
89
+ %w(cmdno cmd out err cid).inject({}){|h,k| h.update k => send(k) }
90
+ #--}}}
91
+ end
92
+ def to_yaml(*a,&b)
93
+ #--{{{
94
+ to_hash.to_yaml(*a,&b)
95
+ #--}}}
96
+ end
97
+ alias to_s to_yaml
98
+ alias to_str to_yaml
99
+ #--}}}
100
+ end # class Command
101
+ class AbstractSession
102
+ #--{{{
103
+
104
+ # class methods
105
+ class << self
106
+ #--{{{
107
+ def default_prog
108
+ #--{{{
109
+ return @default_prog if defined? @default_prog and @default_prog
110
+ if defined? self::DEFAULT_PROG
111
+ return @default_prog = self::DEFAULT_PROG
112
+ else
113
+ @default_prog = ENV["SESSION_#{ self }_PROG"]
114
+ end
115
+ nil
116
+ #--}}}
117
+ end
118
+ def default_prog= prog
119
+ #--{{{
120
+ @default_prog = prog
121
+ #--}}}
122
+ end
123
+ attr :track_history, true
124
+ attr :use_spawn, true
125
+ attr :use_open3, true
126
+ attr :debug, true
127
+ def init
128
+ #--{{{
129
+ @track_history = nil
130
+ @use_spawn = nil
131
+ @use_open3 = nil
132
+ @debug = nil
133
+ #--}}}
134
+ end
135
+ alias [] new
136
+ #--}}}
137
+ end
138
+
139
+ # class init
140
+ init
141
+
142
+ # attributes
143
+ #--{{{
144
+ attr :opts
145
+ attr :prog
146
+ attr :stdin
147
+ alias i stdin
148
+ attr :stdout
149
+ alias o stdout
150
+ attr :stderr
151
+ alias e stderr
152
+ attr :history
153
+ attr :track_history
154
+ attr :outproc, true
155
+ attr :errproc, true
156
+ attr :use_spawn
157
+ attr :use_open3
158
+ attr :debug, true
159
+ alias debug? debug
160
+ attr :threads
161
+ #--}}}
162
+
163
+ # instance methods
164
+ def initialize(*args)
165
+ #--{{{
166
+ @opts = hashify(*args)
167
+
168
+ @prog = getopt('prog', opts, getopt('program', opts, self.class::default_prog))
169
+
170
+ raise(ArgumentError, "no program specified") unless @prog
171
+
172
+ @track_history = nil
173
+ @track_history = Session::track_history unless Session::track_history.nil?
174
+ @track_history = self.class::track_history unless self.class::track_history.nil?
175
+ @track_history = getopt('history', opts) if hasopt('history', opts)
176
+ @track_history = getopt('track_history', opts) if hasopt('track_history', opts)
177
+
178
+ @use_spawn = nil
179
+ @use_spawn = Session::use_spawn unless Session::use_spawn.nil?
180
+ @use_spawn = self.class::use_spawn unless self.class::use_spawn.nil?
181
+ @use_spawn = getopt('use_spawn', opts) if hasopt('use_spawn', opts)
182
+
183
+ @use_open3 = nil
184
+ @use_open3 = Session::use_open3 unless Session::use_open3.nil?
185
+ @use_open3 = self.class::use_open3 unless self.class::use_open3.nil?
186
+ @use_open3 = getopt('use_open3', opts) if hasopt('use_open3', opts)
187
+
188
+ @debug = nil
189
+ @debug = Session::debug unless Session::debug.nil?
190
+ @debug = self.class::debug unless self.class::debug.nil?
191
+ @debug = getopt('debug', opts) if hasopt('debug', opts)
192
+
193
+ @history = nil
194
+ @history = History::new if @track_history
195
+
196
+ @outproc = nil
197
+ @errproc = nil
198
+
199
+ @stdin, @stdout, @stderr =
200
+ if @use_spawn
201
+ Spawn::spawn @prog
202
+ elsif @use_open3
203
+ Open3::popen3 @prog
204
+ else
205
+ __popen3 @prog
206
+ end
207
+
208
+ @threads = []
209
+
210
+ clear
211
+
212
+ if block_given?
213
+ ret = nil
214
+ begin
215
+ ret = yield self
216
+ ensure
217
+ self.close!
218
+ end
219
+ return ret
220
+ end
221
+
222
+ return self
223
+ #--}}}
224
+ end
225
+ def getopt opt, hash, default = nil
226
+ #--{{{
227
+ key = opt
228
+ return hash[key] if hash.has_key? key
229
+ key = "#{ key }"
230
+ return hash[key] if hash.has_key? key
231
+ key = key.intern
232
+ return hash[key] if hash.has_key? key
233
+ return default
234
+ #--}}}
235
+ end
236
+ def hasopt opt, hash
237
+ #--{{{
238
+ key = opt
239
+ return key if hash.has_key? key
240
+ key = "#{ key }"
241
+ return key if hash.has_key? key
242
+ key = key.intern
243
+ return key if hash.has_key? key
244
+ return false
245
+ #--}}}
246
+ end
247
+ def __popen3(*cmd)
248
+ #--{{{
249
+ pw = IO::pipe # pipe[0] for read, pipe[1] for write
250
+ pr = IO::pipe
251
+ pe = IO::pipe
252
+
253
+ pid =
254
+ __fork{
255
+ # child
256
+ pw[1].close
257
+ STDIN.reopen(pw[0])
258
+ pw[0].close
259
+
260
+ pr[0].close
261
+ STDOUT.reopen(pr[1])
262
+ pr[1].close
263
+
264
+ pe[0].close
265
+ STDERR.reopen(pe[1])
266
+ pe[1].close
267
+
268
+ exec(*cmd)
269
+ }
270
+
271
+ Process::detach pid # avoid zombies
272
+
273
+ pw[0].close
274
+ pr[1].close
275
+ pe[1].close
276
+ pi = [pw[1], pr[0], pe[0]]
277
+ pw[1].sync = true
278
+ if defined? yield
279
+ begin
280
+ return yield(*pi)
281
+ ensure
282
+ pi.each{|p| p.close unless p.closed?}
283
+ end
284
+ end
285
+ pi
286
+ #--}}}
287
+ end
288
+ def __fork(*a, &b)
289
+ #--{{{
290
+ verbose = $VERBOSE
291
+ begin
292
+ $VERBOSE = nil
293
+ Kernel::fork(*a, &b)
294
+ ensure
295
+ $VERBOSE = verbose
296
+ end
297
+ #--}}}
298
+ end
299
+
300
+ # abstract methods
301
+ def clear
302
+ #--{{{
303
+ raise NotImplementedError
304
+ #--}}}
305
+ end
306
+ alias flush clear
307
+ def path
308
+ #--{{{
309
+ raise NotImplementedError
310
+ #--}}}
311
+ end
312
+ def path=
313
+ #--{{{
314
+ raise NotImplementedError
315
+ #--}}}
316
+ end
317
+ def send_command cmd
318
+ #--{{{
319
+ raise NotImplementedError
320
+ #--}}}
321
+ end
322
+
323
+ # concrete methods
324
+ def track_history= bool
325
+ #--{{{
326
+ @history ||= History::new
327
+ @track_history = bool
328
+ #--}}}
329
+ end
330
+ def ready?
331
+ #--{{{
332
+ (stdin and stdout and stderr) and
333
+ (IO === stdin and IO === stdout and IO === stderr) and
334
+ (not (stdin.closed? or stdout.closed? or stderr.closed?))
335
+ #--}}}
336
+ end
337
+ def close!
338
+ #--{{{
339
+ [stdin, stdout, stderr].each{|pipe| pipe.close}
340
+ stdin, stdout, stderr = nil, nil, nil
341
+ true
342
+ #--}}}
343
+ end
344
+ alias close close!
345
+ def hashify(*a)
346
+ #--{{{
347
+ a.inject({}){|o,h| o.update(h)}
348
+ #--}}}
349
+ end
350
+ private :hashify
351
+ def execute(command, redirects = {})
352
+ #--{{{
353
+ $session_command = command if @debug
354
+
355
+ raise(PipeError, command) unless ready?
356
+
357
+ # clear buffers
358
+ clear
359
+
360
+ # setup redirects
361
+ rerr = redirects[:e] || redirects[:err] || redirects[:stderr] ||
362
+ redirects['stderr'] || redirects['e'] || redirects['err'] ||
363
+ redirects[2] || redirects['2']
364
+
365
+ rout = redirects[:o] || redirects[:out] || redirects[:stdout] ||
366
+ redirects['stdout'] || redirects['o'] || redirects['out'] ||
367
+ redirects[1] || redirects['1']
368
+
369
+ # create cmd object and add to history
370
+ cmd = Command::new command.to_s
371
+
372
+ # store cmd if tracking history
373
+ history << cmd if track_history
374
+
375
+ # mutex for accessing shared data
376
+ mutex = Mutex::new
377
+
378
+ # io data for stderr and stdout
379
+ err = {
380
+ :io => stderr,
381
+ :cmd => cmd.err,
382
+ :name => 'stderr',
383
+ :begin => false,
384
+ :end => false,
385
+ :begin_pat => cmd.begin_err_pat,
386
+ :end_pat => cmd.end_err_pat,
387
+ :redirect => rerr,
388
+ :proc => errproc,
389
+ :yield => lambda{|buf| yield(nil, buf)},
390
+ :mutex => mutex,
391
+ }
392
+ out = {
393
+ :io => stdout,
394
+ :cmd => cmd.out,
395
+ :name => 'stdout',
396
+ :begin => false,
397
+ :end => false,
398
+ :begin_pat => cmd.begin_out_pat,
399
+ :end_pat => cmd.end_out_pat,
400
+ :redirect => rout,
401
+ :proc => outproc,
402
+ :yield => lambda{|buf| yield(buf, nil)},
403
+ :mutex => mutex,
404
+ }
405
+
406
+ begin
407
+ # send command in the background so we can begin processing output
408
+ # immediately - thanks to tanaka akira for this suggestion
409
+ threads << Thread::new { send_command cmd }
410
+
411
+ # init
412
+ main = Thread::current
413
+ exceptions = []
414
+
415
+ # fire off reader threads
416
+ [err, out].each do |iodat|
417
+ threads <<
418
+ Thread::new(iodat, main) do |iodat, main|
419
+
420
+ loop do
421
+ main.raise(PipeError, command) unless ready?
422
+ main.raise ExecutionError, iodat[:name] if iodat[:end] and not iodat[:begin]
423
+
424
+ break if iodat[:end] or iodat[:io].eof?
425
+
426
+ line = iodat[:io].gets
427
+
428
+ buf = nil
429
+
430
+ case line
431
+ when iodat[:end_pat]
432
+ iodat[:end] = true
433
+ # handle the special case of non-newline terminated output
434
+ if((m = %r/(.+)__CMD/o.match(line)) and (pre = m[1]))
435
+ buf = pre
436
+ end
437
+ when iodat[:begin_pat]
438
+ iodat[:begin] = true
439
+ else
440
+ next unless iodat[:begin] and not iodat[:end] # ignore chaff
441
+ buf = line
442
+ end
443
+
444
+ if buf
445
+ iodat[:mutex].synchronize do
446
+ iodat[:cmd] << buf
447
+ iodat[:redirect] << buf if iodat[:redirect]
448
+ iodat[:proc].call buf if iodat[:proc]
449
+ iodat[:yield].call buf if block_given?
450
+ end
451
+ end
452
+ end
453
+
454
+ true
455
+ end
456
+ end
457
+ ensure
458
+ # reap all threads - accumulating and rethrowing any exceptions
459
+ begin
460
+ while((t = threads.shift))
461
+ t.join
462
+ raise ExecutionError, 'iodat thread failure' unless t.value
463
+ end
464
+ rescue => e
465
+ exceptions << e
466
+ retry unless threads.empty?
467
+ ensure
468
+ unless exceptions.empty?
469
+ meta_message = '<' << exceptions.map{|e| "#{ e.message } - (#{ e.class })"}.join('|') << '>'
470
+ meta_backtrace = exceptions.map{|e| e.backtrace}.flatten
471
+ raise ExecutionError, meta_message, meta_backtrace
472
+ end
473
+ end
474
+ end
475
+
476
+ # this should only happen if eof was reached before end pat
477
+ [err, out].each do |iodat|
478
+ raise ExecutionError, iodat[:name] unless iodat[:begin] and iodat[:end]
479
+ end
480
+
481
+
482
+ # get the exit status
483
+ get_status if respond_to? :get_status
484
+
485
+ out = err = iodat = nil
486
+
487
+ return [cmd.out, cmd.err]
488
+ #--}}}
489
+ end
490
+ #--}}}
491
+ end # class AbstractSession
492
+ class Sh < AbstractSession
493
+ #--{{{
494
+ DEFAULT_PROG = 'sh'
495
+ ECHO = 'echo'
496
+
497
+ attr :status
498
+ alias exit_status status
499
+ alias exitstatus status
500
+
501
+ def clear
502
+ #--{{{
503
+ stdin.puts "#{ ECHO } __clear__ 1>&2"
504
+ stdin.puts "#{ ECHO } __clear__"
505
+ stdin.flush
506
+ while((line = stderr.gets) and line !~ %r/__clear__/o); end
507
+ while((line = stdout.gets) and line !~ %r/__clear__/o); end
508
+ self
509
+ #--}}}
510
+ end
511
+ def send_command cmd
512
+ #--{{{
513
+ stdin.printf "%s '%s' 1>&2\n", ECHO, cmd.begin_err
514
+ stdin.printf "%s '%s' \n", ECHO, cmd.begin_out
515
+
516
+ stdin.printf "%s\n", cmd.cmd
517
+ stdin.printf "export __exit_status__=$?\n"
518
+
519
+ stdin.printf "%s '%s' 1>&2\n", ECHO, cmd.end_err
520
+ stdin.printf "%s '%s' \n", ECHO, cmd.end_out
521
+
522
+ stdin.flush
523
+ #--}}}
524
+ end
525
+ def get_status
526
+ #--{{{
527
+ @status = get_var '__exit_status__'
528
+ unless @status =~ /^\s*\d+\s*$/o
529
+ raise ExecutionError, "could not determine exit status from <#{ @status.inspect }>"
530
+ end
531
+
532
+ @status = Integer @status
533
+ #--}}}
534
+ end
535
+ def set_var name, value
536
+ #--{{{
537
+ stdin.puts "export #{ name }=#{ value }"
538
+ stdin.flush
539
+ #--}}}
540
+ end
541
+ def get_var name
542
+ #--{{{
543
+ stdin.puts "#{ ECHO } \"#{ name }=${#{ name }}\""
544
+ stdin.flush
545
+
546
+ var = nil
547
+ while((line = stdout.gets))
548
+ m = %r/#{ name }\s*=\s*(.*)/.match line
549
+ if m
550
+ var = m[1]
551
+ raise ExecutionError, "could not determine <#{ name }> from <#{ line.inspect }>" unless var
552
+ break
553
+ end
554
+ end
555
+
556
+ var
557
+ #--}}}
558
+ end
559
+ def path
560
+ #--{{{
561
+ var = get_var 'PATH'
562
+ var.strip.split %r/:/o
563
+ #--}}}
564
+ end
565
+ def path= arg
566
+ #--{{{
567
+ case arg
568
+ when Array
569
+ arg = arg.join ':'
570
+ else
571
+ arg = arg.to_s.strip
572
+ end
573
+
574
+ set_var 'PATH', "'#{ arg }'"
575
+ self.path
576
+ #--}}}
577
+ end
578
+ def execute(command, redirects = {}, &block)
579
+ #--{{{
580
+ # setup redirect on stdin
581
+ rin = redirects[:i] || redirects[:in] || redirects[:stdin] ||
582
+ redirects['stdin'] || redirects['i'] || redirects['in'] ||
583
+ redirects[0] || redirects['0']
584
+
585
+ if rin
586
+ tmp =
587
+ begin
588
+ Tempfile::new rand.to_s
589
+ rescue
590
+ Tempfile::new rand.to_s
591
+ end
592
+
593
+ begin
594
+ tmp.write(
595
+ if rin.respond_to? 'read'
596
+ rin.read
597
+ elsif rin.respond_to? 'to_s'
598
+ rin.to_s
599
+ else
600
+ rin
601
+ end
602
+ )
603
+ tmp.flush
604
+ command = "{ #{ command } ;} < #{ tmp.path }"
605
+ #puts command
606
+ super(command, redirects, &block)
607
+ ensure
608
+ tmp.close! if tmp
609
+ end
610
+
611
+ else
612
+ super
613
+ end
614
+ #--}}}
615
+ end
616
+ #--}}}
617
+ end # class Sh
618
+ class Bash < Sh
619
+ #--{{{
620
+ DEFAULT_PROG = 'bash'
621
+ class Login < Bash
622
+ DEFAULT_PROG = 'bash --login'
623
+ end
624
+ #--}}}
625
+ end # class Bash
626
+ class Shell < Bash; end
627
+ # IDL => interactive data language - see http://www.rsinc.com/
628
+ class IDL < AbstractSession
629
+ #--{{{
630
+ class LicenseManagerError < StandardError; end
631
+ DEFAULT_PROG = 'idl'
632
+ MAX_TRIES = 32
633
+ def initialize(*args)
634
+ #--{{{
635
+ tries = 0
636
+ ret = nil
637
+ begin
638
+ ret = super
639
+ rescue LicenseManagerError => e
640
+ tries += 1
641
+ if tries < MAX_TRIES
642
+ sleep 1
643
+ retry
644
+ else
645
+ raise LicenseManagerError, "<#{ MAX_TRIES }> attempts <#{ e.message }>"
646
+ end
647
+ end
648
+ ret
649
+ #--}}}
650
+ end
651
+ def clear
652
+ #--{{{
653
+ stdin.puts "retall"
654
+ stdin.puts "printf, -2, '__clear__'"
655
+ stdin.puts "printf, -1, '__clear__'"
656
+ stdin.flush
657
+ while((line = stderr.gets) and line !~ %r/__clear__/o)
658
+ raise LicenseManagerError, line if line =~ %r/license\s*manager/io
659
+ end
660
+ while((line = stdout.gets) and line !~ %r/__clear__/o)
661
+ raise LicenseManagerError, line if line =~ %r/license\s*manager/io
662
+ end
663
+ self
664
+ #--}}}
665
+ end
666
+ def send_command cmd
667
+ #--{{{
668
+ stdin.printf "printf, -2, '%s'\n", cmd.begin_err
669
+ stdin.printf "printf, -1, '%s'\n", cmd.begin_out
670
+
671
+ stdin.printf "%s\n", cmd.cmd
672
+ stdin.printf "retall\n"
673
+
674
+ stdin.printf "printf, -2, '%s'\n", cmd.end_err
675
+ stdin.printf "printf, -1, '%s'\n", cmd.end_out
676
+ stdin.flush
677
+ #--}}}
678
+ end
679
+ def path
680
+ #--{{{
681
+ stdout, stderr = execute "print, !path"
682
+ stdout.strip.split %r/:/o
683
+ #--}}}
684
+ end
685
+ def path= arg
686
+ #--{{{
687
+ case arg
688
+ when Array
689
+ arg = arg.join ':'
690
+ else
691
+ arg = arg.to_s.strip
692
+ end
693
+ stdout, stderr = execute "!path='#{ arg }'"
694
+
695
+ self.path
696
+ #--}}}
697
+ end
698
+ #--}}}
699
+ end # class IDL
700
+ module Spawn
701
+ #--{{{
702
+ class << self
703
+ def spawn command
704
+ #--{{{
705
+ ipath = tmpfifo
706
+ opath = tmpfifo
707
+ epath = tmpfifo
708
+
709
+ cmd = "#{ command } < #{ ipath } 1> #{ opath } 2> #{ epath } &"
710
+ system cmd
711
+
712
+ i = open ipath, 'w'
713
+ o = open opath, 'r'
714
+ e = open epath, 'r'
715
+
716
+ [i,o,e]
717
+ #--}}}
718
+ end
719
+ def tmpfifo
720
+ #--{{{
721
+ path = nil
722
+ 42.times do |i|
723
+ tpath = File::join(Dir::tmpdir, "#{ $$ }.#{ rand }.#{ i }")
724
+ v = $VERBOSE
725
+ begin
726
+ $VERBOSE = nil
727
+ system "mkfifo #{ tpath }"
728
+ ensure
729
+ $VERBOSE = v
730
+ end
731
+ next unless $? == 0
732
+ path = tpath
733
+ at_exit{ File::unlink(path) rescue STDERR.puts("rm <#{ path }> failed") }
734
+ break
735
+ end
736
+ raise "could not generate tmpfifo" unless path
737
+ path
738
+ #--}}}
739
+ end
740
+ end
741
+ #--}}}
742
+ end # module Spawn
743
+ #--}}}
744
+ end # module Session