File: session.rb

package info (click to toggle)
ruby-session 3.2.0-3
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 184 kB
  • sloc: ruby: 939; makefile: 2
file content (658 lines) | stat: -rwxr-xr-x 17,270 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
require 'open3'
require 'tmpdir'
require 'thread'
require 'yaml'
require 'tempfile'

module Session 
  VERSION = '3.2.0'
  def self.version() VERSION end

  def Session.description
    'persistent connections with external programs like bash'
  end

  @track_history = ENV['SESSION_HISTORY'] || ENV['SESSION_TRACK_HISTORY']
  @use_spawn     = ENV['SESSION_USE_SPAWN']
  @use_open3     = ENV['SESSION_USE_OPEN3']
  @debug         = ENV['SESSION_DEBUG']

  class << self
    attr :track_history, true
    attr :use_spawn, true
    attr :use_open3, true
    attr :debug, true
    def new(*a, &b)
      Sh::new(*a, &b)
    end
    alias [] new
  end

  class PipeError < StandardError; end
  class ExecutionError < StandardError; end

  class History
    def initialize; @a = []; end
    def method_missing(m,*a,&b); @a.send(m,*a,&b); end
    def to_yaml(*a,&b); @a.to_yaml(*a,&b); end
    alias to_s to_yaml 
    alias to_str to_yaml 
  end # class History
  class Command
    class << self
      def cmdno; @cmdno ||= 0; end
      def cmdno= n; @cmdno = n; end
    end

    # attributes
    attr :cmd
    attr :cmdno
    attr :out,true
    attr :err,true
    attr :cid
    attr :begin_out
    attr :end_out
    attr :begin_out_pat
    attr :end_out_pat
    attr :begin_err
    attr :end_err
    attr :begin_err_pat
    attr :end_err_pat

    def initialize(command)
      @cmd = command.to_s
      @cmdno = self.class.cmdno
      self.class.cmdno += 1
      @err = ''
      @out = ''
      @cid = "%d_%d_%d" % [$$, cmdno, rand(Time.now.usec)]
      @begin_out = "__CMD_OUT_%s_BEGIN__" % cid
      @end_out = "__CMD_OUT_%s_END__" % cid
      @begin_out_pat = %r/#{ Regexp.escape(@begin_out) }/
      @end_out_pat = %r/#{ Regexp.escape(@end_out) }/
      @begin_err = "__CMD_ERR_%s_BEGIN__" % cid
      @end_err = "__CMD_ERR_%s_END__" % cid
      @begin_err_pat = %r/#{ Regexp.escape(@begin_err) }/
      @end_err_pat = %r/#{ Regexp.escape(@end_err) }/
    end
    def to_hash
      %w(cmdno cmd out err cid).inject({}){|h,k| h.update k => send(k) }
    end
    def to_yaml(*a,&b)
      to_hash.to_yaml(*a,&b)
    end
    alias to_s to_yaml 
    alias to_str to_yaml 
  end # class Command
  class AbstractSession 

  # class methods
    class << self
      def default_prog
        return @default_prog if defined? @default_prog and @default_prog
        if defined? self::DEFAULT_PROG
          return @default_prog = self::DEFAULT_PROG 
        else
          @default_prog = ENV["SESSION_#{ self }_PROG"]
        end
        nil
      end
      def default_prog= prog
        @default_prog = prog 
      end
      attr :track_history, true
      attr :use_spawn, true
      attr :use_open3, true
      attr :debug, true
      def init
        @track_history = nil
        @use_spawn = nil
        @use_open3 = nil
        @debug = nil
      end
      alias [] new
    end

  # class init
    init

  # attributes
    attr :opts
    attr :prog
    attr :stdin
    alias i stdin
    attr :stdout
    alias o stdout
    attr :stderr
    alias e stderr
    attr :history
    attr :track_history
    attr :outproc, true
    attr :errproc, true
    attr :use_spawn
    attr :use_open3
    attr :debug, true
    alias debug? debug
    attr :threads

  # instance methods
    def initialize(*args)
      @opts = hashify(*args)

      @prog = getopt('prog', opts, getopt('program', opts, self.class::default_prog))

      raise(ArgumentError, "no program specified") unless @prog

      @track_history = nil
      @track_history = Session::track_history unless Session::track_history.nil?
      @track_history = self.class::track_history unless self.class::track_history.nil?
      @track_history = getopt('history', opts) if hasopt('history', opts) 
      @track_history = getopt('track_history', opts) if hasopt('track_history', opts) 

      @use_spawn = nil
      @use_spawn = Session::use_spawn unless Session::use_spawn.nil?
      @use_spawn = self.class::use_spawn unless self.class::use_spawn.nil?
      @use_spawn = getopt('use_spawn', opts) if hasopt('use_spawn', opts)

      if defined? JRUBY_VERSION
        @use_open3 = true
      else
        @use_open3 = nil
        @use_open3 = Session::use_open3 unless Session::use_open3.nil?
        @use_open3 = self.class::use_open3 unless self.class::use_open3.nil?
        @use_open3 = getopt('use_open3', opts) if hasopt('use_open3', opts)
      end

      @debug = nil
      @debug = Session::debug unless Session::debug.nil?
      @debug = self.class::debug unless self.class::debug.nil?
      @debug = getopt('debug', opts) if hasopt('debug', opts) 

      @history = nil
      @history = History::new if @track_history 

      @outproc = nil
      @errproc = nil

      @stdin, @stdout, @stderr =
        if @use_spawn
          Spawn::spawn @prog
        elsif @use_open3
          Open3::popen3 @prog
        else
          __popen3 @prog
        end

      @threads = []

      clear

      if block_given?
        ret = nil
        begin
          ret = yield self
        ensure
          self.close!
        end
        return ret
      end

      return self
    end
    def getopt opt, hash, default = nil
      key = opt
      return hash[key] if hash.has_key? key
      key = "#{ key }"
      return hash[key] if hash.has_key? key
      key = key.intern
      return hash[key] if hash.has_key? key
      return default
    end
    def hasopt opt, hash
      key = opt
      return key if hash.has_key? key
      key = "#{ key }"
      return key if hash.has_key? key
      key = key.intern
      return key if hash.has_key? key
      return false 
    end
    def __popen3(*cmd)
      pw = IO::pipe   # pipe[0] for read, pipe[1] for write
      pr = IO::pipe
      pe = IO::pipe

      pid =
        __fork{
          # child
          pw[1].close
          STDIN.reopen(pw[0])
          pw[0].close

          pr[0].close
          STDOUT.reopen(pr[1])
          pr[1].close

          pe[0].close
          STDERR.reopen(pe[1])
          pe[1].close

          exec(*cmd)
        }

      Process::detach pid   # avoid zombies

      pw[0].close
      pr[1].close
      pe[1].close
      pi = [pw[1], pr[0], pe[0]]
      pw[1].sync = true
      if defined? yield
        begin
          return yield(*pi)
        ensure
          pi.each{|p| p.close unless p.closed?}
        end
      end
      pi
    end
    def __fork(*a, &b)
      verbose = $VERBOSE
      begin
        $VERBOSE = nil 
        Kernel::fork(*a, &b)
      ensure
        $VERBOSE = verbose
      end
    end

  # abstract methods
    def clear
      raise NotImplementedError
    end
    alias flush clear
    def path 
      raise NotImplementedError
    end
    def path= 
      raise NotImplementedError
    end
    def send_command cmd
      raise NotImplementedError
    end

  # concrete methods
    def track_history= bool
      @history ||= History::new
      @track_history = bool
    end
    def ready?
      (stdin and stdout and stderr) and
      (IO === stdin and IO === stdout and IO === stderr) and
      (not (stdin.closed? or stdout.closed? or stderr.closed?))
    end
    def close!
      [stdin, stdout, stderr].each{|pipe| pipe.close}
      stdin, stdout, stderr = nil, nil, nil
      true
    end
    alias close close!
    def hashify(*a)
      a.inject({}){|o,h| o.update(h)}
    end
    private :hashify
    def execute(command, redirects = {})
      $session_command = command if @debug

      raise(PipeError, command) unless ready? 

    # clear buffers
      clear

    # setup redirects
      rerr = redirects[:e] || redirects[:err] || redirects[:stderr] || 
             redirects['stderr'] || redirects['e'] || redirects['err'] ||
             redirects[2] || redirects['2']

      rout = redirects[:o] || redirects[:out] || redirects[:stdout] || 
             redirects['stdout'] || redirects['o'] || redirects['out'] ||
             redirects[1] || redirects['1']

    # create cmd object and add to history
      cmd = Command::new command.to_s

    # store cmd if tracking history
      history << cmd if track_history

    # mutex for accessing shared data
      mutex = Mutex::new

    # io data for stderr and stdout 
      err = {
        :io        => stderr,
        :cmd       => cmd.err,
        :name      => 'stderr',
        :begin     => false,
        :end       => false,
        :begin_pat => cmd.begin_err_pat,
        :end_pat   => cmd.end_err_pat,
        :redirect  => rerr,
        :proc      => errproc,
        :yield     => lambda{|buf| yield(nil, buf)},
        :mutex     => mutex,
      }
      out = {
        :io        => stdout,
        :cmd       => cmd.out,
        :name      => 'stdout',
        :begin     => false,
        :end       => false,
        :begin_pat => cmd.begin_out_pat,
        :end_pat   => cmd.end_out_pat,
        :redirect  => rout,
        :proc      => outproc,
        :yield     => lambda{|buf| yield(buf, nil)},
        :mutex     => mutex,
      }

    begin
      # send command in the background so we can begin processing output
      # immediately - thanks to tanaka akira for this suggestion
        threads << Thread::new { send_command cmd }

      # init 
        main       = Thread::current
        exceptions = []

      # fire off reader threads
        [err, out].each do |iodat|
          threads <<
            Thread::new(iodat, main) do |iodat, main|

              loop do
                main.raise(PipeError, command) unless ready? 
                main.raise ExecutionError, iodat[:name] if iodat[:end] and not iodat[:begin]

                break if iodat[:end] or iodat[:io].eof?

                line = iodat[:io].gets

                # In case their are weird chars, this will avoid a "invalid byte sequence in US-ASCII" error
                line.force_encoding("binary") if line.respond_to? :force_encoding

                buf = nil

                case line
                  when iodat[:end_pat]
                    iodat[:end] = true
                  # handle the special case of non-newline terminated output
                    if((m = %r/(.+)__CMD/o.match(line)) and (pre = m[1]))
                      buf = pre
                    end
                  when iodat[:begin_pat]
                    iodat[:begin] = true
                  else
                    next unless iodat[:begin] and not iodat[:end] # ignore chaff
                    buf = line
                end

                if buf
                  iodat[:mutex].synchronize do
                    iodat[:cmd] << buf
                    iodat[:redirect] << buf if iodat[:redirect]
                    iodat[:proc].call buf  if iodat[:proc]
                    iodat[:yield].call buf  if block_given?
                  end
                end
              end

              true
          end
        end
      ensure
      # reap all threads - accumulating and rethrowing any exceptions
        begin
          while((t = threads.shift))
            t.join
            raise ExecutionError, 'iodat thread failure' unless t.value
          end
        rescue => e
          exceptions << e
          retry unless threads.empty?
        ensure
          unless exceptions.empty?
            meta_message = '<' << exceptions.map{|e| "#{ e.message } - (#{ e.class })"}.join('|') << '>'
            meta_backtrace = exceptions.map{|e| e.backtrace}.flatten
            raise ExecutionError, meta_message, meta_backtrace 
          end
        end
      end

    # this should only happen if eof was reached before end pat
      [err, out].each do |iodat|
        raise ExecutionError, iodat[:name] unless iodat[:begin] and iodat[:end]
      end


    # get the exit status
      get_status if respond_to? :get_status

      out = err = iodat = nil

      return [cmd.out, cmd.err]
    end
  end # class AbstractSession
  class Sh < AbstractSession
    DEFAULT_PROG    = 'sh'
    ECHO            = 'echo'

    attr :status
    alias exit_status status
    alias exitstatus status

    def clear
      stdin.puts "#{ ECHO } __clear__ 1>&2"
      stdin.puts "#{ ECHO } __clear__"
      stdin.flush
      while((line = stderr.gets) and line !~ %r/__clear__/o); end
      while((line = stdout.gets) and line !~ %r/__clear__/o); end
      self
    end
    def send_command cmd
      stdin.printf "%s '%s' 1>&2\n", ECHO, cmd.begin_err
      stdin.printf "%s '%s' \n", ECHO, cmd.begin_out
 
      stdin.printf "%s\n", cmd.cmd
      stdin.printf "export __exit_status__=$?\n"

      stdin.printf "%s '%s' 1>&2\n", ECHO, cmd.end_err
      stdin.printf "%s '%s' \n", ECHO, cmd.end_out
 
      stdin.flush
    end
    def get_status
      @status = get_var '__exit_status__' 
      unless @status =~ /^\s*\d+\s*$/o
        raise ExecutionError, "could not determine exit status from <#{ @status.inspect }>"
      end

      @status = Integer @status
    end
    def set_var name, value
      stdin.puts "export #{ name }=#{ value }"
      stdin.flush
    end
    def get_var name
      stdin.puts "#{ ECHO } \"#{ name }=${#{ name }}\""
      stdin.flush

      var = nil
      while((line = stdout.gets))
        m = %r/#{ name }\s*=\s*(.*)/.match line
        if m
          var = m[1] 
          raise ExecutionError, "could not determine <#{ name }> from <#{ line.inspect }>" unless var
          break
        end
      end

      var
    end
    def path 
      var = get_var 'PATH'
      var.strip.split %r/:/o
    end
    def path= arg 
      case arg
        when Array
          arg = arg.join ':'
        else
          arg = arg.to_s.strip
      end

      set_var 'PATH', "'#{ arg }'"
      self.path
    end
    def execute(command, redirects = {}, &block)
    # setup redirect on stdin
      rin = redirects[:i] || redirects[:in] || redirects[:stdin] || 
             redirects['stdin'] || redirects['i'] || redirects['in'] ||
             redirects[0] || redirects['0']

      if rin
        tmp = 
          begin
            Tempfile::new rand.to_s
          rescue
            Tempfile::new rand.to_s
          end

        begin
          tmp.write(
            if rin.respond_to? 'read'
              rin.read
            elsif rin.respond_to? 'to_s'
              rin.to_s
            else
              rin
            end
          )
          tmp.flush
          command = "{ #{ command } ;} < #{ tmp.path }"
          #puts command
          super(command, redirects, &block)
        ensure
          tmp.close! if tmp 
        end

      else
        super
      end
    end
  end # class Sh
  class Bash < Sh
    DEFAULT_PROG = 'bash'
    class Login < Bash
      DEFAULT_PROG = 'bash --login'
    end
  end # class Bash
  class Shell < Bash; end
  # IDL => interactive data language - see http://www.rsinc.com/
  class IDL < AbstractSession
    class LicenseManagerError < StandardError; end
    DEFAULT_PROG = 'idl'
    MAX_TRIES = 32 
    def initialize(*args)
      tries = 0 
      ret = nil
      begin
        ret = super
      rescue LicenseManagerError => e
        tries += 1 
        if tries < MAX_TRIES
          sleep 1
          retry
        else
          raise LicenseManagerError, "<#{ MAX_TRIES }> attempts <#{ e.message }>"
        end
      end
      ret
    end
    def clear
      stdin.puts "retall"
      stdin.puts "printf, -2, '__clear__'"
      stdin.puts "printf, -1, '__clear__'"
      stdin.flush
      while((line = stderr.gets) and line !~ %r/__clear__/o)
        raise LicenseManagerError, line if line =~ %r/license\s*manager/io
      end
      while((line = stdout.gets) and line !~ %r/__clear__/o)
        raise LicenseManagerError, line if line =~ %r/license\s*manager/io
      end
      self
    end
    def send_command cmd
      stdin.printf "printf, -2, '%s'\n", cmd.begin_err
      stdin.printf "printf, -1, '%s'\n", cmd.begin_out

      stdin.printf "%s\n", cmd.cmd
      stdin.printf "retall\n"

      stdin.printf "printf, -2, '%s'\n", cmd.end_err
      stdin.printf "printf, -1, '%s'\n", cmd.end_out
      stdin.flush
    end
    def path 
      stdout, stderr = execute "print, !path"
      stdout.strip.split %r/:/o
    end
    def path= arg 
      case arg
        when Array
          arg = arg.join ':'
        else
          arg = arg.to_s.strip
      end
      stdout, stderr = execute "!path='#{ arg }'"

      self.path
    end
  end # class IDL
  module Spawn
    class << self
      def spawn command
        ipath = tmpfifo
        opath = tmpfifo
        epath = tmpfifo

        cmd = "#{ command } < #{ ipath } 1> #{ opath } 2> #{ epath } &"
        system cmd 

        i = open ipath, 'w'
        o = open opath, 'r'
        e = open epath, 'r'

        [i,o,e]
      end
      def tmpfifo
        path = nil
        42.times do |i|
          tpath = File::join(Dir::tmpdir, "#{ $$ }.#{ rand }.#{ i }")
          v = $VERBOSE
          begin
            $VERBOSE = nil
            system "mkfifo #{ tpath }"
          ensure
            $VERBOSE = v 
          end
          next unless $? == 0
          path = tpath
          at_exit{ File::unlink(path) rescue STDERR.puts("rm <#{ path }> failed") }
          break
        end
        raise "could not generate tmpfifo" unless path
        path
      end
    end
  end # module Spawn
end # module Session