oterm 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +52 -0
- data/lib/oterm.rb +13 -0
- data/lib/oterm/errors.rb +10 -0
- data/lib/oterm/executor.rb +117 -0
- data/lib/oterm/listener.rb +257 -0
- data/lib/oterm/mock100.rb +11 -0
- data/lib/oterm/output.rb +31 -0
- data/lib/oterm/server.rb +36 -0
- data/lib/oterm/telnet.rb +37 -0
- data/lib/oterm/version.rb +4 -0
- data/lib/oterm/vt100.rb +282 -0
- data/test/server_test.rb +51 -0
- metadata +62 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c57083633eb77e165e8331ee3b28ce62a2a25c41
|
4
|
+
data.tar.gz: db9c45d79b15fa85e8c5a82c413e29108e7e1d41
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 59fb9a821cde9ba1c4974ecf18fa32cef38a1db7d3c187b8a274273c4459d225be3f308d4132cb4f4f126751d1a75eb786869542ffedb88f830ed63f2855d6df
|
7
|
+
data.tar.gz: 5702073e0ce8e0afe661019d0da7f8c42a845cbd2d9c09297332290996217b954ad7b063b944d4d1d13575225c0c55fef8ab941d9db8a1720e4c8369d224ac5c
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Peter Ohler
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
oterm
|
2
|
+
=====
|
3
|
+
|
4
|
+
Operations Terminal
|
5
|
+
|
6
|
+
# OTerm gem
|
7
|
+
|
8
|
+
A VT100 terminal (mostly) server that can be placed in an application to allow
|
9
|
+
telnet access to exposed features. Some of the features includes are:
|
10
|
+
|
11
|
+
- Line editing
|
12
|
+
|
13
|
+
- VT100 support with graphics and color
|
14
|
+
|
15
|
+
- Configurable help and command registration
|
16
|
+
|
17
|
+
- Auto completion
|
18
|
+
|
19
|
+
- History
|
20
|
+
|
21
|
+
The driver for writing this gem was to be able to control, inspect, and debug
|
22
|
+
multi-threaded applications while the application is running. Look for it's use
|
23
|
+
soon in the ODisk application and later in a more interesting project.
|
24
|
+
|
25
|
+
## <a name="installation">Installation</a>
|
26
|
+
gem install oterm
|
27
|
+
|
28
|
+
## <a name="documentation">Documentation</a>
|
29
|
+
|
30
|
+
*Documentation*: http://www.ohler.com/oterm
|
31
|
+
|
32
|
+
## <a name="source">Source</a>
|
33
|
+
|
34
|
+
*GitHub* *repo*: https://github.com/ohler55/oterm
|
35
|
+
|
36
|
+
*RubyGems* *repo*: https://rubygems.org/gems/oterm
|
37
|
+
|
38
|
+
Follow [@peterohler on Twitter](http://twitter.com/#!/peterohler) for announcements and news about the Oj gem.
|
39
|
+
|
40
|
+
## <a name="build_status">Build Status</a>
|
41
|
+
|
42
|
+
[![Build Status](https://secure.travis-ci.org/ohler55/oterm.png?branch=master)](http://travis-ci.org/ohler55/oterm)
|
43
|
+
|
44
|
+
### Current Release 1.0.0 - December 22, 2013
|
45
|
+
|
46
|
+
- First release. There will be rough edges.
|
47
|
+
|
48
|
+
## <a name="description">Description</a>
|
49
|
+
|
50
|
+
## Examples
|
51
|
+
|
52
|
+
TBD for now, look for a sample in the test directory.
|
data/lib/oterm.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
|
2
|
+
module OTerm
|
3
|
+
end # OTerm
|
4
|
+
|
5
|
+
require 'oterm/version'
|
6
|
+
require 'oterm/errors'
|
7
|
+
require 'oterm/telnet'
|
8
|
+
require 'oterm/vt100'
|
9
|
+
require 'oterm/output'
|
10
|
+
require 'oterm/listener'
|
11
|
+
require 'oterm/executor'
|
12
|
+
require 'oterm/server'
|
13
|
+
require 'oterm/mock100'
|
data/lib/oterm/errors.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
|
2
|
+
module OTerm
|
3
|
+
|
4
|
+
class Executor
|
5
|
+
|
6
|
+
def initialize()
|
7
|
+
@cmds = {}
|
8
|
+
register('help', self, :help, '[<command>] this help screen or help on a command',
|
9
|
+
%|Show help on a specific command or a list of all commands if a specific command is not specified.|)
|
10
|
+
register('shutdown', self, :shutdown, 'shuts down the application',
|
11
|
+
%|Shuts down the application.|)
|
12
|
+
register('close', self, :close, 'closes the connection',
|
13
|
+
%|Closes the connection to the application.|)
|
14
|
+
end
|
15
|
+
|
16
|
+
def greeting()
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def execute(cmd, listener)
|
21
|
+
name, args = cmd.split(' ', 2)
|
22
|
+
c = @cmds[name]
|
23
|
+
if nil == c
|
24
|
+
missing(cmd, listener)
|
25
|
+
return
|
26
|
+
end
|
27
|
+
c.target.send(c.op, listener, args)
|
28
|
+
end
|
29
|
+
|
30
|
+
def help(listener, arg=nil)
|
31
|
+
listener.out.pl()
|
32
|
+
if nil != arg
|
33
|
+
c = @cmds[arg]
|
34
|
+
if nil != c
|
35
|
+
listener.out.pl("#{arg} - #{c.summary}")
|
36
|
+
c.desc.each do |line|
|
37
|
+
listener.out.pl(" #{line}")
|
38
|
+
end
|
39
|
+
return
|
40
|
+
end
|
41
|
+
end
|
42
|
+
max = 1
|
43
|
+
@cmds.each do |name,cmd|
|
44
|
+
max = name.size if max < name.size
|
45
|
+
end
|
46
|
+
@cmds.each do |name,cmd|
|
47
|
+
listener.out.pl(" %1$*2$s - %3$s" % [name, -max, cmd.summary])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def close(listener, args)
|
52
|
+
listener.out.pl("Closing connection")
|
53
|
+
listener.close()
|
54
|
+
end
|
55
|
+
|
56
|
+
def shutdown(listener, args)
|
57
|
+
listener.out.pl("Shutting down")
|
58
|
+
listener.server.shutdown()
|
59
|
+
end
|
60
|
+
|
61
|
+
# This evaluates cmd as a Ruby expression. This is great for debugging but
|
62
|
+
# not a wise move for a public interface. To hide this just create a methid
|
63
|
+
# with the same name in the subclass of this one.
|
64
|
+
def missing(cmd, listener)
|
65
|
+
begin
|
66
|
+
result = "#{eval(cmd)}".split("\n")
|
67
|
+
rescue Exception => e
|
68
|
+
result = ["#{e.class}: #{e.message}"]
|
69
|
+
e.backtrace.each do |line|
|
70
|
+
break if line.include?('oterm/executor.rb')
|
71
|
+
result << "\t" + line
|
72
|
+
end
|
73
|
+
end
|
74
|
+
result.each do |line|
|
75
|
+
listener.out.pl(line)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def tab(cmd, listener)
|
80
|
+
comp = []
|
81
|
+
@cmds.each_key do |name|
|
82
|
+
comp << name if name.start_with?(cmd)
|
83
|
+
end
|
84
|
+
if 1 == comp.size
|
85
|
+
listener.move_col(1000)
|
86
|
+
listener.insert(comp[0][listener.buf.size..-1])
|
87
|
+
listener.out.prompt()
|
88
|
+
listener.out.p(listener.buf)
|
89
|
+
else
|
90
|
+
listener.out.pl()
|
91
|
+
comp.each do |name|
|
92
|
+
listener.out.pl(name)
|
93
|
+
end
|
94
|
+
listener.update_cmd(0)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def register(cmd, target, op, summary, desc)
|
99
|
+
@cmds[cmd] = Cmd.new(target, op, summary, desc)
|
100
|
+
end
|
101
|
+
|
102
|
+
class Cmd
|
103
|
+
attr_accessor :target
|
104
|
+
attr_accessor :op
|
105
|
+
attr_accessor :summary
|
106
|
+
attr_accessor :desc
|
107
|
+
|
108
|
+
def initialize(target, op, summary, desc)
|
109
|
+
@target = target
|
110
|
+
@op = op
|
111
|
+
@summary = summary
|
112
|
+
@desc = desc.split("\n")
|
113
|
+
end
|
114
|
+
end # Cmd
|
115
|
+
|
116
|
+
end # Executor
|
117
|
+
end # OTerm
|
@@ -0,0 +1,257 @@
|
|
1
|
+
|
2
|
+
module OTerm
|
3
|
+
|
4
|
+
class Listener
|
5
|
+
|
6
|
+
attr_accessor :server
|
7
|
+
attr_accessor :con
|
8
|
+
attr_accessor :executor
|
9
|
+
attr_accessor :buf
|
10
|
+
attr_accessor :history
|
11
|
+
attr_accessor :hp # history pointer
|
12
|
+
attr_accessor :out
|
13
|
+
attr_accessor :debug
|
14
|
+
|
15
|
+
def initialize(server, con, executor)
|
16
|
+
@debug = server.debug()
|
17
|
+
@server = server
|
18
|
+
@con = con
|
19
|
+
@executor = executor
|
20
|
+
@buf = ''
|
21
|
+
@kill_buf = nil
|
22
|
+
@history = []
|
23
|
+
@hp = 0
|
24
|
+
@out = Output.new(con)
|
25
|
+
@col = 0
|
26
|
+
@echo = false
|
27
|
+
@done = false
|
28
|
+
|
29
|
+
greeting = executor.greeting()
|
30
|
+
@out.pl(greeting) if nil != greeting
|
31
|
+
|
32
|
+
# initiate negotiations for single character mode and no echo
|
33
|
+
@out.p(Telnet.msg(Telnet::DO, Telnet::SGA) + Telnet.msg(Telnet::DO, Telnet::ECHO))
|
34
|
+
|
35
|
+
out.prompt()
|
36
|
+
while !@done
|
37
|
+
line = con.recv(100)
|
38
|
+
begin
|
39
|
+
len = line.size()
|
40
|
+
break if 0 == len
|
41
|
+
if @debug
|
42
|
+
line.each_byte { |x| print("#{x} ") }
|
43
|
+
plain = line.gsub(/./) { |c| c.ord < 32 || 127 <= c.ord ? '*' : c }
|
44
|
+
puts "[#{line.size()}] #{plain}"
|
45
|
+
end
|
46
|
+
# determine input type (telnet command, char mode, line mode)
|
47
|
+
o0 = line[0].ord()
|
48
|
+
case o0
|
49
|
+
when 255 # telnet command
|
50
|
+
process_telnet_cmd(line)
|
51
|
+
when 27 # escape, vt100 sequence
|
52
|
+
vt100_cmd(line)
|
53
|
+
when 13 # new line
|
54
|
+
exec_cmd(@buf)
|
55
|
+
when 0..12, 14..26, 28..31, 127 # other control character
|
56
|
+
process_ctrl_cmd(o0)
|
57
|
+
else
|
58
|
+
if 1 == len || (2 == len && "\000" == line[1]) # single char mode
|
59
|
+
@hp = 0
|
60
|
+
insert(line[0])
|
61
|
+
else # line mode
|
62
|
+
exec_cmd(line)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
rescue Exception => e
|
66
|
+
puts "#{e.class}: #{e.message}"
|
67
|
+
e.backtrace.each { |bline| puts ' ' + bline }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def close()
|
73
|
+
@done = true
|
74
|
+
end
|
75
|
+
|
76
|
+
def process_telnet_cmd(line)
|
77
|
+
reply = ''
|
78
|
+
Telnet.parse(line).each do |v,f|
|
79
|
+
case f
|
80
|
+
when Telnet::ECHO
|
81
|
+
case v
|
82
|
+
when Telnet::WILL
|
83
|
+
@echo = false
|
84
|
+
reply += Telnet.msg(Telnet::WONT, f)
|
85
|
+
when Telnet::WONT
|
86
|
+
@echo = true
|
87
|
+
reply += Telnet.msg(Telnet::WILL, f)
|
88
|
+
end
|
89
|
+
when Telnet::SGA
|
90
|
+
case v
|
91
|
+
when Telnet::WILL
|
92
|
+
reply += Telnet.msg(Telnet::WILL, f)
|
93
|
+
when Telnet::WONT
|
94
|
+
reply += Telnet.msg(Telnet::WONT, f)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
if 0 < reply.size
|
99
|
+
@con.print(reply)
|
100
|
+
else
|
101
|
+
# Done negotiating with telnet. Initiate negotiation for vt100 ANSI support.
|
102
|
+
@out.identify()
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def vt100_cmd(line)
|
107
|
+
puts "*** vt100 command"
|
108
|
+
# TBD
|
109
|
+
end
|
110
|
+
|
111
|
+
def exec_cmd(cmd)
|
112
|
+
@hp = 0
|
113
|
+
cmd.strip!()
|
114
|
+
if 0 == cmd.size()
|
115
|
+
@out.pl()
|
116
|
+
@out.prompt()
|
117
|
+
@col = 0
|
118
|
+
return
|
119
|
+
end
|
120
|
+
@buf = ""
|
121
|
+
@out.pl()
|
122
|
+
@history << cmd if 0 < cmd.size() && (0 == @history.size() || @history[-1] != cmd)
|
123
|
+
executor.execute(cmd, self)
|
124
|
+
@out.prompt()
|
125
|
+
@col = 0
|
126
|
+
end
|
127
|
+
|
128
|
+
def process_ctrl_cmd(o)
|
129
|
+
case o
|
130
|
+
when 1 # ^a
|
131
|
+
move_col(-@col)
|
132
|
+
when 2 # ^b
|
133
|
+
move_col(-1)
|
134
|
+
when 4 # ^d
|
135
|
+
@hp = 0
|
136
|
+
if @col < @buf.size
|
137
|
+
@col += 1
|
138
|
+
delete_char()
|
139
|
+
end
|
140
|
+
when 5 # ^e
|
141
|
+
move_col(@buf.size() - @col)
|
142
|
+
when 6 # ^f
|
143
|
+
move_col(1)
|
144
|
+
when 8, 127 # backspace or delete
|
145
|
+
@hp = 0
|
146
|
+
delete_char()
|
147
|
+
when 9 # tab
|
148
|
+
@hp = 0
|
149
|
+
@executor.tab(@buf, self)
|
150
|
+
when 11 # ^k
|
151
|
+
@hp = 0
|
152
|
+
if @col < @buf.size()
|
153
|
+
@kill_buf = @buf[@col..-1]
|
154
|
+
blen = @buf.size()
|
155
|
+
@buf = @buf[0...@col]
|
156
|
+
update_cmd(blen)
|
157
|
+
end
|
158
|
+
when 14 # ^n
|
159
|
+
if 0 < @hp && @hp <= @history.size()
|
160
|
+
@hp -= 1
|
161
|
+
blen = @buf.size()
|
162
|
+
if 0 == @hp
|
163
|
+
@buf = ''
|
164
|
+
else
|
165
|
+
@buf = @history[-@hp]
|
166
|
+
end
|
167
|
+
update_cmd(blen)
|
168
|
+
end
|
169
|
+
when 16 # ^p
|
170
|
+
if @hp < @history.size()
|
171
|
+
@hp += 1
|
172
|
+
blen = @buf.size()
|
173
|
+
@buf = @history[-@hp]
|
174
|
+
update_cmd(blen)
|
175
|
+
end
|
176
|
+
when 21 # ^u
|
177
|
+
@hp -= 1
|
178
|
+
@buf = ''
|
179
|
+
when 25 # ^y
|
180
|
+
insert(@kill_buf)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def update_cmd(blen)
|
185
|
+
@out.cr()
|
186
|
+
@out.prompt()
|
187
|
+
@out.p(@buf)
|
188
|
+
# erase to end of line and come back
|
189
|
+
if @buf.size() < blen
|
190
|
+
dif = blen - @buf.size()
|
191
|
+
@out.p(' ' * dif + "\b" * dif)
|
192
|
+
end
|
193
|
+
@col = @buf.size
|
194
|
+
end
|
195
|
+
|
196
|
+
def move_col(dif)
|
197
|
+
if 0 > dif
|
198
|
+
while 0 > dif && 0 < @col
|
199
|
+
@col -= 1
|
200
|
+
@out.p("\b")
|
201
|
+
dif += 1
|
202
|
+
end
|
203
|
+
else
|
204
|
+
max = @buf.size
|
205
|
+
while 0 < dif && @col < max
|
206
|
+
@out.p(@buf[@col])
|
207
|
+
@col += 1
|
208
|
+
dif -= 1
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def insert(str)
|
214
|
+
# TBD be smarter with vt100
|
215
|
+
if 0 == @col
|
216
|
+
@buf = str + @buf
|
217
|
+
@out.p("\r")
|
218
|
+
@out.prompt()
|
219
|
+
@out.p(@buf)
|
220
|
+
@out.p("\r")
|
221
|
+
@out.prompt()
|
222
|
+
@out.p(@buf[0...str.size])
|
223
|
+
elsif @buf.size <= @col
|
224
|
+
@buf << str
|
225
|
+
@out.pc(str)
|
226
|
+
@col = @buf.size
|
227
|
+
else
|
228
|
+
@buf = @buf[0...@col] + str + @buf[@col..-1]
|
229
|
+
@out.p("\r")
|
230
|
+
@out.prompt()
|
231
|
+
@out.p(@buf)
|
232
|
+
@out.p("\r")
|
233
|
+
@out.prompt()
|
234
|
+
@out.p(@buf[0...@col + str.size])
|
235
|
+
end
|
236
|
+
@col += str.size
|
237
|
+
end
|
238
|
+
|
239
|
+
def delete_char()
|
240
|
+
return if 0 == @col || 0 == @buf.size
|
241
|
+
if @buf.size <= @col
|
242
|
+
@buf.chop!()
|
243
|
+
@out.p("\x08 \x08")
|
244
|
+
else
|
245
|
+
@buf = @buf[0...@col - 1] + @buf[@col..-1]
|
246
|
+
@out.p("\r")
|
247
|
+
@out.prompt()
|
248
|
+
@out.p(@buf)
|
249
|
+
@out.p(" \r")
|
250
|
+
@out.prompt()
|
251
|
+
@out.p(@buf[0...@col - 1])
|
252
|
+
end
|
253
|
+
@col -= 1
|
254
|
+
end
|
255
|
+
|
256
|
+
end # Listener
|
257
|
+
end # OTerm
|
data/lib/oterm/output.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
module OTerm
|
3
|
+
|
4
|
+
class Output < VT100
|
5
|
+
def initialize(con)
|
6
|
+
super(con)
|
7
|
+
end
|
8
|
+
|
9
|
+
def prompt()
|
10
|
+
@con.print("\r> ")
|
11
|
+
end
|
12
|
+
|
13
|
+
def p(str)
|
14
|
+
@con.print(str)
|
15
|
+
end
|
16
|
+
|
17
|
+
def pc(c)
|
18
|
+
@con.putc(c)
|
19
|
+
end
|
20
|
+
|
21
|
+
def pl(line='')
|
22
|
+
@con.puts(line + "\r")
|
23
|
+
end
|
24
|
+
|
25
|
+
def cr()
|
26
|
+
@con.print("\r")
|
27
|
+
end
|
28
|
+
|
29
|
+
end # Output
|
30
|
+
end # OTerm
|
31
|
+
|
data/lib/oterm/server.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module OTerm
|
5
|
+
|
6
|
+
class Server
|
7
|
+
|
8
|
+
attr_accessor :acceptThread
|
9
|
+
attr_accessor :stop
|
10
|
+
attr_accessor :listeners
|
11
|
+
attr_accessor :debug
|
12
|
+
|
13
|
+
def initialize(executor, port=6060, debug=false)
|
14
|
+
@debug = debug
|
15
|
+
@stop = false
|
16
|
+
@listeners = []
|
17
|
+
@acceptThread = Thread.start() do
|
18
|
+
server = TCPServer.new(port)
|
19
|
+
while !stop do
|
20
|
+
Thread.start(server.accept()) do |con|
|
21
|
+
@listeners << Listener.new(self, con, executor)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def shutdown()
|
28
|
+
@acceptThread.exit()
|
29
|
+
end
|
30
|
+
|
31
|
+
def remove_listener(listener)
|
32
|
+
@listeners.delete(listener)
|
33
|
+
end
|
34
|
+
|
35
|
+
end # Server
|
36
|
+
end # OTerm
|
data/lib/oterm/telnet.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
|
2
|
+
module OTerm
|
3
|
+
|
4
|
+
class Telnet
|
5
|
+
IAC = 255.chr
|
6
|
+
|
7
|
+
# verbs
|
8
|
+
WILL = 251.chr
|
9
|
+
WONT = 252.chr
|
10
|
+
DO = 253.chr
|
11
|
+
DONT = 254.chr
|
12
|
+
|
13
|
+
# features
|
14
|
+
BIN = 0.chr
|
15
|
+
ECHO = 1.chr
|
16
|
+
SGA = 3.chr # one character at a time
|
17
|
+
|
18
|
+
def self.msg(verb, feature)
|
19
|
+
[IAC, verb, feature].join('')
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.parse(line)
|
23
|
+
msgs = []
|
24
|
+
v = nil
|
25
|
+
line.each_char do |c|
|
26
|
+
if nil == v
|
27
|
+
v = c unless IAC == c
|
28
|
+
else
|
29
|
+
msgs << [v, c]
|
30
|
+
v = nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
msgs
|
34
|
+
end
|
35
|
+
|
36
|
+
end # Telnet
|
37
|
+
end # OTerm
|
data/lib/oterm/vt100.rb
ADDED
@@ -0,0 +1,282 @@
|
|
1
|
+
|
2
|
+
module OTerm
|
3
|
+
|
4
|
+
class VT100
|
5
|
+
ESC = 27.chr
|
6
|
+
MAX_DIM = 10000
|
7
|
+
|
8
|
+
# graphic font characters
|
9
|
+
BLANK = '_'
|
10
|
+
DIAMOND = '`'
|
11
|
+
CHECKER = 'a'
|
12
|
+
HT = 'b'
|
13
|
+
FF = 'c'
|
14
|
+
CR = 'd'
|
15
|
+
LF = 'e'
|
16
|
+
DEGREE = 'f'
|
17
|
+
PLUS_MINUS = 'g'
|
18
|
+
NL = 'h'
|
19
|
+
VT = 'i'
|
20
|
+
LOW_RIGHT = 'j'
|
21
|
+
UP_RIGHT = 'k'
|
22
|
+
UP_LEFT = 'l'
|
23
|
+
LOW_LEFT = 'm'
|
24
|
+
CROSS = 'n'
|
25
|
+
DASH1 = 'o'
|
26
|
+
DASH3 = 'p'
|
27
|
+
DASH5 = 'q'
|
28
|
+
DASH7 = 'r'
|
29
|
+
DASH9 = 's'
|
30
|
+
LEFT_T = 't'
|
31
|
+
RIGHT_T = 'u'
|
32
|
+
LOW_T = 'v'
|
33
|
+
UP_T = 'w'
|
34
|
+
BAR = 'x'
|
35
|
+
LTE = 'y'
|
36
|
+
GTE = 'z'
|
37
|
+
PI = '{'
|
38
|
+
NE = '|'
|
39
|
+
DOT = '~'
|
40
|
+
|
41
|
+
# colors
|
42
|
+
BLACK = 30
|
43
|
+
RED = 31
|
44
|
+
GREEN = 32
|
45
|
+
YELLOW = 33
|
46
|
+
BLUE = 34
|
47
|
+
MAGENTA = 35
|
48
|
+
CYAN = 36
|
49
|
+
WHITE = 37
|
50
|
+
|
51
|
+
attr_accessor :con
|
52
|
+
|
53
|
+
def initialize(con)
|
54
|
+
@con = con
|
55
|
+
@is_vt100 = false
|
56
|
+
end
|
57
|
+
|
58
|
+
def is_vt100?()
|
59
|
+
return @is_vt100
|
60
|
+
end
|
61
|
+
|
62
|
+
def identify()
|
63
|
+
@con.print("\x1b[c")
|
64
|
+
# expect: ^[[?1;<n>0c
|
65
|
+
rx = /^\x1b\[\?1;.+c/
|
66
|
+
m = recv_wait(10, 1.0, rx)
|
67
|
+
# Don't care about the type, just that the reply is valid for a vt100.
|
68
|
+
@is_vt100 = nil != m
|
69
|
+
end
|
70
|
+
|
71
|
+
def get_cursor()
|
72
|
+
v, h = 0, 0
|
73
|
+
if @is_vt100
|
74
|
+
@con.print("\x1b[6n")
|
75
|
+
# expect: ^[<v>;<h>R
|
76
|
+
rx = /^\x1b\[(\d+);(\d+)R/
|
77
|
+
m = recv_wait(16, 1.0, rx)
|
78
|
+
v, h = m.captures.map { |s| s.to_i }
|
79
|
+
end
|
80
|
+
return v, h
|
81
|
+
end
|
82
|
+
|
83
|
+
# Move cursor to screen location v, h.
|
84
|
+
def set_cursor(v, h)
|
85
|
+
@con.print("\x1b[#{v};#{h}H") if @is_vt100
|
86
|
+
end
|
87
|
+
|
88
|
+
# Save cursor position and attributes.
|
89
|
+
def save_cursor()
|
90
|
+
@con.print("\x1b7") if @is_vt100
|
91
|
+
end
|
92
|
+
|
93
|
+
# Restore cursor position and attributes.
|
94
|
+
def restore_cursor()
|
95
|
+
@con.print("\x1b8") if @is_vt100
|
96
|
+
end
|
97
|
+
|
98
|
+
# Reset terminal to initial state.
|
99
|
+
def reset()
|
100
|
+
@con.print("\x1bc") if @is_vt100
|
101
|
+
end
|
102
|
+
|
103
|
+
def graphic_font()
|
104
|
+
@con.print("\x1b(2") if @is_vt100
|
105
|
+
end
|
106
|
+
|
107
|
+
def default_font()
|
108
|
+
# TBD allow to set to specific character set for original terminal, for now US font
|
109
|
+
@con.print("\x1b(B") if @is_vt100
|
110
|
+
end
|
111
|
+
|
112
|
+
def clear_screen()
|
113
|
+
@con.print("\x1b[2J") if @is_vt100
|
114
|
+
end
|
115
|
+
|
116
|
+
def clear_line()
|
117
|
+
@con.print("\x1b[2K") if @is_vt100
|
118
|
+
end
|
119
|
+
|
120
|
+
def clear_to_end()
|
121
|
+
@con.print("\x1b[0K") if @is_vt100
|
122
|
+
end
|
123
|
+
|
124
|
+
def clear_to_start()
|
125
|
+
@con.print("\x1b[1K") if @is_vt100
|
126
|
+
end
|
127
|
+
|
128
|
+
def relative_origin()
|
129
|
+
@con.print("\x1b[?6h") if @is_vt100
|
130
|
+
end
|
131
|
+
|
132
|
+
def absolute_origin()
|
133
|
+
@con.print("\x1b[?6l") if @is_vt100
|
134
|
+
end
|
135
|
+
|
136
|
+
def attrs_off()
|
137
|
+
@con.print("\x1b[m") if @is_vt100
|
138
|
+
end
|
139
|
+
|
140
|
+
def bold()
|
141
|
+
@con.print("\x1b[1m") if @is_vt100
|
142
|
+
end
|
143
|
+
alias bright bold
|
144
|
+
|
145
|
+
def dim()
|
146
|
+
@con.print("\x1b[2m") if @is_vt100
|
147
|
+
end
|
148
|
+
|
149
|
+
def underline()
|
150
|
+
@con.print("\x1b[4m") if @is_vt100
|
151
|
+
end
|
152
|
+
|
153
|
+
def blink()
|
154
|
+
@con.print("\x1b[5m") if @is_vt100
|
155
|
+
end
|
156
|
+
|
157
|
+
def reverse()
|
158
|
+
@con.print("\x1b[7m") if @is_vt100
|
159
|
+
end
|
160
|
+
|
161
|
+
def big_top()
|
162
|
+
@con.print("\x1b#3") if @is_vt100
|
163
|
+
end
|
164
|
+
|
165
|
+
def big_bottom()
|
166
|
+
@con.print("\x1b#4") if @is_vt100
|
167
|
+
end
|
168
|
+
|
169
|
+
def narrow()
|
170
|
+
@con.print("\x1b#5") if @is_vt100
|
171
|
+
end
|
172
|
+
|
173
|
+
def wide()
|
174
|
+
@con.print("\x1b#6") if @is_vt100
|
175
|
+
end
|
176
|
+
|
177
|
+
def set_colors(fg, bg)
|
178
|
+
return unless @is_vt100
|
179
|
+
if nil == fg
|
180
|
+
if nil != bg
|
181
|
+
bg += 10
|
182
|
+
@con.print("\x1b[#{bg}m")
|
183
|
+
end
|
184
|
+
else
|
185
|
+
if nil != bg
|
186
|
+
bg += 10
|
187
|
+
@con.print("\x1b[#{fg};#{bg}m")
|
188
|
+
else
|
189
|
+
@con.print("\x1b[#{fg}m")
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def up(n)
|
195
|
+
@con.print("\x1b[#{n}A") if @is_vt100
|
196
|
+
end
|
197
|
+
|
198
|
+
def down(n)
|
199
|
+
@con.print("\x1b[#{n}B") if @is_vt100
|
200
|
+
end
|
201
|
+
|
202
|
+
def left(n)
|
203
|
+
@con.print("\x1b[#{n}D") if @is_vt100
|
204
|
+
end
|
205
|
+
|
206
|
+
def right(n)
|
207
|
+
@con.print("\x1b[#{n}C") if @is_vt100
|
208
|
+
end
|
209
|
+
|
210
|
+
def home()
|
211
|
+
@con.print("\x1b[H") if @is_vt100
|
212
|
+
end
|
213
|
+
|
214
|
+
def scroll(n)
|
215
|
+
return unless @is_vt100
|
216
|
+
if 0 > n
|
217
|
+
n = -n
|
218
|
+
n.times { @con.print("\x1b[D") }
|
219
|
+
elsif 0 < n
|
220
|
+
n.times { @con.print("\x1b[M") }
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def screen_size()
|
225
|
+
save_cursor()
|
226
|
+
set_cursor(MAX_DIM, MAX_DIM)
|
227
|
+
h, w = get_cursor()
|
228
|
+
restore_cursor()
|
229
|
+
return h, w
|
230
|
+
end
|
231
|
+
|
232
|
+
def frame(y, x, h, w)
|
233
|
+
return if 2 > h || 2 > w
|
234
|
+
graphic_font()
|
235
|
+
set_cursor(y, x)
|
236
|
+
@con.print(UP_LEFT + DASH5 * (w - 2) + UP_RIGHT)
|
237
|
+
(h - 2).times do |i|
|
238
|
+
i += 1
|
239
|
+
set_cursor(y + i, x)
|
240
|
+
@con.print(BAR)
|
241
|
+
set_cursor(y + i, x + w - 1)
|
242
|
+
@con.print(BAR)
|
243
|
+
end
|
244
|
+
set_cursor(y + h - 1, x)
|
245
|
+
@con.print(LOW_LEFT + DASH5 * (w - 2) + LOW_RIGHT)
|
246
|
+
default_font()
|
247
|
+
end
|
248
|
+
|
249
|
+
def recv_wait(max, timeout, pat)
|
250
|
+
giveup = Time.now + timeout
|
251
|
+
reply = ''
|
252
|
+
begin
|
253
|
+
while nil == pat.match(reply)
|
254
|
+
# just peek incase the string is not what we want
|
255
|
+
reply = @con.recv_nonblock(max, Socket::MSG_PEEK)
|
256
|
+
|
257
|
+
# DEBUG
|
258
|
+
# reply.each_byte { |x| print("#{x} ") }
|
259
|
+
# plain = reply.gsub(/./) { |c| c.ord < 32 || 127 <= c.ord ? '*' : c }
|
260
|
+
# puts "[#{reply.size()}] #{plain}"
|
261
|
+
|
262
|
+
end
|
263
|
+
rescue IO::WaitReadable
|
264
|
+
now = Time.now
|
265
|
+
if now < giveup
|
266
|
+
IO.select([@con], [], [], giveup - now)
|
267
|
+
retry
|
268
|
+
end
|
269
|
+
end
|
270
|
+
m = pat.match(reply)
|
271
|
+
if nil != m
|
272
|
+
# There was a match so read the characters we already peeked.
|
273
|
+
cnt = m.to_s().size
|
274
|
+
if 0 < cnt
|
275
|
+
@con.recv_nonblock(cnt)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
m
|
279
|
+
end
|
280
|
+
|
281
|
+
end # VT100
|
282
|
+
end # OTerm
|
data/test/server_test.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: UTF-8
|
3
|
+
|
4
|
+
# Ubuntu does not accept arguments to ruby when called using env. To get warnings to show up the -w options is
|
5
|
+
# required. That can be set in the RUBYOPT environment variable.
|
6
|
+
# export RUBYOPT=-w
|
7
|
+
|
8
|
+
$VERBOSE = true
|
9
|
+
|
10
|
+
$: << File.join(File.dirname(__FILE__), "../lib")
|
11
|
+
|
12
|
+
require 'oterm'
|
13
|
+
|
14
|
+
class Ex < OTerm::Executor
|
15
|
+
|
16
|
+
def initialize()
|
17
|
+
super()
|
18
|
+
register('box', self, :box, 'draws a box',
|
19
|
+
%|Draws a box at the location described with the dimensions given.
|
20
|
+
> box <inset> <height> <width>|)
|
21
|
+
end
|
22
|
+
|
23
|
+
def greeting()
|
24
|
+
"Hello!"
|
25
|
+
end
|
26
|
+
|
27
|
+
def box(listener, args)
|
28
|
+
o = listener.out
|
29
|
+
# TBD get values for x, h, and w from args
|
30
|
+
x = 20
|
31
|
+
h = 4
|
32
|
+
w = 20
|
33
|
+
cy, _ = o.get_cursor()
|
34
|
+
(h - cy + 1).times { listener.out.pl() } if cy <= h
|
35
|
+
cy, _ = o.get_cursor()
|
36
|
+
o.save_cursor()
|
37
|
+
o.set_colors(OTerm::VT100::RED, nil)
|
38
|
+
o.frame(cy - h, x, h, w)
|
39
|
+
o.restore_cursor()
|
40
|
+
end
|
41
|
+
|
42
|
+
def missing(cmd, listener)
|
43
|
+
listerner.out.pl("'#{cmd}' is not a recognized command.")
|
44
|
+
end
|
45
|
+
|
46
|
+
end # Ex
|
47
|
+
|
48
|
+
executor = Ex.new()
|
49
|
+
|
50
|
+
server = OTerm::Server.new(executor, 6060, true)
|
51
|
+
server.acceptThread.join()
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: oterm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Peter Ohler
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-12-22 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: 'A remote terminal that can be used for interacting with an application
|
14
|
+
and invoking operations remotely. Telnet and VT100 over a telnet connection are
|
15
|
+
supported. '
|
16
|
+
email: peter@ohler.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files:
|
20
|
+
- README.md
|
21
|
+
files:
|
22
|
+
- lib/oterm/errors.rb
|
23
|
+
- lib/oterm/executor.rb
|
24
|
+
- lib/oterm/listener.rb
|
25
|
+
- lib/oterm/mock100.rb
|
26
|
+
- lib/oterm/output.rb
|
27
|
+
- lib/oterm/server.rb
|
28
|
+
- lib/oterm/telnet.rb
|
29
|
+
- lib/oterm/version.rb
|
30
|
+
- lib/oterm/vt100.rb
|
31
|
+
- lib/oterm.rb
|
32
|
+
- test/server_test.rb
|
33
|
+
- LICENSE
|
34
|
+
- README.md
|
35
|
+
homepage: http://www.ohler.com/oterm
|
36
|
+
licenses:
|
37
|
+
- MIT
|
38
|
+
- GPL-3.0
|
39
|
+
metadata: {}
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options:
|
42
|
+
- --main
|
43
|
+
- README.md
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - '>='
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
requirements: []
|
57
|
+
rubyforge_project: oterm
|
58
|
+
rubygems_version: 2.0.14
|
59
|
+
signing_key:
|
60
|
+
specification_version: 4
|
61
|
+
summary: A operations terminal server.
|
62
|
+
test_files: []
|