Class: Abalone::Terminal

Inherits:
Object
  • Object
show all
Defined in:
lib/abalone/terminal.rb

Instance Method Summary collapse

Constructor Details

#initialize(settings, ws, params) ⇒ Terminal

Returns a new instance of Terminal.



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
# File 'lib/abalone/terminal.rb', line 6

def initialize(settings, ws, params)
  @settings = settings
  @ws       = ws
  @params   = params
  @modes    = nil
  @buffer   = Abalone::Buffer.new

  ENV['TERM'] ||= 'xterm-256color' # make sure we've got a somewhat sane environment

  if settings.respond_to?(:bannerfile)
    @ws.send({'data' => File.read(settings.bannerfile).encode(crlf_newline: true)}.to_json)
    @ws.send({'data' => "\r\n\r\n"}.to_json)
  end

  reader, @writer, @pid = PTY.spawn(*shell_command)
  @writer.winsize = [24,80]

  # there must be some form of event driven pty interaction, EM or some gem maybe?
  reader.sync = true
  @term = Thread.new do
    carry = []
    loop do
      begin
        PTY.check(@pid, true)
        output = reader.read_nonblock(512).unpack('C*') # we read non-blocking to stream data as quickly as we can
        last_low = output.rindex { |x| x < 128 } # find the last low bit
        trailing = last_low +1

        # use inclusive slices here
        data  = (carry + output[0..last_low]).pack('C*').force_encoding('UTF-8') # repack into a string up until the last low bit
        carry = output[trailing..-1]             # save the any remaining high bits and partial chars for next go-round

        @ws.send({'data' => data}.to_json)

      rescue IO::WaitReadable
        IO.select([reader])
        retry

      rescue PTY::ChildExited => e
        warn('Terminal has exited!')
        @ws.close_connection

        @timer.terminate rescue nil
        @timer.join rescue nil
        Thread.exit
      end

      sleep(0.05)
    end
  end

  if @settings.respond_to? :timeout
    @timer = Thread.new do
      expiration = Time.now + @settings.timeout
      loop do
        remaining = expiration - Time.now
        if remaining < 0
          terminate!
          Thread.exit
        end

        format = (remaining > 3600) ? "%H:%M:%S" : "%M:%S"
        time = {
          'event' => 'time',
          'data'  => Time.at(remaining).utc.strftime(format),
        }
        @ws.send(time.to_json)
        sleep 1
      end
    end
  end
end

Instance Method Details

#alive?Boolean

Returns:

  • (Boolean)


79
80
81
# File 'lib/abalone/terminal.rb', line 79

def alive?
  @term.alive?
end

#modes=(message) ⇒ Object



109
110
111
112
113
114
# File 'lib/abalone/terminal.rb', line 109

def modes=(message)
  raise 'Invalid modes data type' unless message.is_a? Hash
  @modes = message.select do |key, val|
    ['cursorBlink', 'cursorVisible', 'bracketedPaste', 'applicationCursor'].include? key
  end
end

#reconnect(ws) ⇒ Object



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/abalone/terminal.rb', line 83

def reconnect(ws)
  if @ttl # stop the countdown
    warn "Stopping timeout"
    @ttl.terminate rescue nil
    @ttl.join rescue nil
    @ttl = nil
  end
  @ws.close_connection if @ws
  @ws = ws

  sleep 0.25 # allow the terminal to finish initialization before we blast it.

  if @modes
    @ws.send({
      'event' => 'modes',
      'data'  => @modes
    }.to_json)
  end
  @ws.send({'data' => @buffer.replay}.to_json)
  @writer.write "\cl" # ctrl-l forces a screen redraw
end

#resize(rows, cols) ⇒ Object



141
142
143
# File 'lib/abalone/terminal.rb', line 141

def resize(rows, cols)
  @writer.winsize = [rows, cols]
end

#stop!Object



116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/abalone/terminal.rb', line 116

def stop!
  if @settings.respond_to? :ttl
    @ws  = @buffer
    @ttl = Thread.new do
      warn "Providing a shutdown grace period of #{@settings.ttl} seconds."
      sleep @settings.ttl
      terminate!
    end
  else
    terminate!
  end
end

#terminate!Object



129
130
131
132
133
134
135
136
137
138
139
# File 'lib/abalone/terminal.rb', line 129

def terminate!
  warn "Terminating session."
  Process.kill('TERM', @pid) rescue nil
  sleep 1
  Process.kill('KILL', @pid) rescue nil

  [@ttl, @timer, @term].each do |thread|
    thread.terminate rescue nil
    thread.join rescue nil
  end
end

#write(message) ⇒ Object



105
106
107
# File 'lib/abalone/terminal.rb', line 105

def write(message)
  @writer.write message
end