session 2.4.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 (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