oterm 1.0.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.
- 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
|
+
[](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: []
|