1 # -*- coding: utf-8 -*-
2 # This code is based on AjaxTerm/Web-Shell which included a fairly complete
3 # vt100 implementation as well as a stable process multiplexer.
4 # I made some small fixes, improved some small parts and added a Session class
5 # which can be used by the widget.
14 from termios
import TIOCSWINSZ
16 from signal
import signal
, SIGCHLD
, SIG_IGN
, SIGTERM
18 from select
import select
19 from subprocess
import Popen
25 VT100_CHARSET_GRAPH
= ( 0x25ca, 0x2026, 0x2022, 0x3f,
26 0xb6, 0x3f, 0xb0, 0xb1,
27 0x3f, 0x3f, 0x2b, 0x2b,
28 0x2b, 0x2b, 0x2b, 0xaf,
29 0x2014, 0x2014, 0x2014, 0x5f,
30 0x2b, 0x2b, 0x2b, 0x2b,
31 0x7c, 0x2264, 0x2265, 0xb6,
32 0x2260, 0xa3, 0xb7, 0x7f )
34 VT100_KEYFILTER_ANSIKEYS
= {
35 '~': '~', 'A': '\x1b[A',
36 'B': '\x1b[B', 'C': '\x1b[C',
37 'D': '\x1b[D', 'F': '\x1b[F',
38 'H': '\x1b[H', '1': '\x1b[5~',
39 '2': '\x1b[6~', '3': '\x1b[2~',
40 '4': '\x1b[3~', 'a': '\x1bOP',
41 'b': '\x1bOQ', 'c': '\x1bOR',
42 'd': '\x1bOS', 'e': '\x1b[15~',
43 'f': '\x1b[17~', 'g': '\x1b[18~',
44 'h': '\x1b[19~', 'i': '\x1b[20~',
45 'j': '\x1b[21~', 'k': '\x1b[23~',
49 VT100_KEYFILTER_APPKEYS
= {
50 '~': '~', 'A': '\x1bOA',
51 'B': '\x1bOB', 'C': '\x1bOC',
52 'D': '\x1bOD', 'F': '\x1bOF',
53 'H': '\x1bOH', '1': '\x1b[5~',
54 '2': '\x1b[6~', '3': '\x1b[2~',
55 '4': '\x1b[3~', 'a': '\x1bOP',
56 'b': '\x1bOQ', 'c': '\x1bOR',
57 'd': '\x1bOS', 'e': '\x1b[15~',
58 'f': '\x1b[17~', 'g': '\x1b[18~',
59 'h': '\x1b[19~', 'i': '\x1b[20~',
60 'j': '\x1b[21~', 'k': '\x1b[23~',
65 class Terminal(object):
67 def __init__(self
, w
, h
):
71 '#8': self
.esc_DECALN
,
84 '=': self
.esc_DECKPAM
,
85 '>': self
.esc_DECKPNM
,
100 'c': self
.reset_hard
,
135 'r': self
.csi_DECSTBM
,
138 'x': self
.csi_DECREQTPARM
,
139 '!p': self
.csi_DECSTR
,
144 def reset_hard(self
):
145 # Attribute mask: 0x0XFB0000
146 # X: Bit 0 - Underlined
151 self
.attr
= 0x00fe0000
153 self
.utf8_units_count
= 0
154 self
.utf8_units_received
= 0
157 self
.vt100_keyfilter_escape
= False
159 self
.vt100_lastchar
= 0
161 self
.vt100_parse_len
= 0
162 self
.vt100_parse_state
= ""
163 self
.vt100_parse_func
= ""
164 self
.vt100_parse_param
= ""
167 # Invoke other resets
171 def reset_soft(self
):
172 # Attribute mask: 0x0XFB0000
173 # X: Bit 0 - Underlined
178 self
.attr
= 0x00fe0000
180 self
.scroll_area_y0
= 0
181 self
.scroll_area_y1
= self
.h
183 self
.vt100_charset_is_single_shift
= False
184 self
.vt100_charset_is_graphical
= False
185 self
.vt100_charset_g_sel
= 0
186 self
.vt100_charset_g
= [0, 0]
188 self
.vt100_mode_insert
= False
189 self
.vt100_mode_lfnewline
= False
190 self
.vt100_mode_cursorkey
= False
191 self
.vt100_mode_column_switch
= False
192 self
.vt100_mode_inverse
= False
193 self
.vt100_mode_origin
= False
194 self
.vt100_mode_autowrap
= True
195 self
.vt100_mode_cursor
= True
196 self
.vt100_mode_alt_screen
= False
197 self
.vt100_mode_backspace
= False
200 self
.vt100_saved2
= self
.vt100_saved
203 def reset_screen(self
):
205 self
.screen
= array
.array('i', [self
.attr |
0x20] * self
.w
* self
.h
)
206 self
.screen2
= array
.array('i', [self
.attr |
0x20] * self
.w
* self
.h
)
208 self
.scroll_area_y0
= 0
209 self
.scroll_area_y1
= self
.h
214 self
.tab_stops
= range(0, self
.w
, 8)
217 def utf8_decode(self
, d
):
221 if self
.utf8_units_count
!= self
.utf8_units_received
:
222 self
.utf8_units_received
+= 1
223 if (char
& 0xc0) == 0x80:
224 self
.utf8_char
= (self
.utf8_char
<< 6) |
(char
& 0x3f)
225 if self
.utf8_units_count
== self
.utf8_units_received
:
226 if self
.utf8_char
< 0x10000:
227 o
+= unichr(self
.utf8_char
)
228 self
.utf8_units_count
= self
.utf8_units_received
= 0
231 while self
.utf8_units_received
:
233 self
.utf8_units_received
-= 1
234 self
.utf8_units_count
= 0
236 if (char
& 0x80) == 0x00:
238 elif (char
& 0xe0) == 0xc0:
239 self
.utf8_units_count
= 1
240 self
.utf8_char
= char
& 0x1f
241 elif (char
& 0xf0) == 0xe0:
242 self
.utf8_units_count
= 2
243 self
.utf8_char
= char
& 0x0f
244 elif (char
& 0xf8) == 0xf0:
245 self
.utf8_units_count
= 3
246 self
.utf8_char
= char
& 0x07
252 def utf8_charwidth(char
):
257 # Low-level terminal functions
258 def peek(self
, y0
, x0
, y1
, x1
):
259 return self
.screen
[self
.w
* y0
+ x0
:self
.w
* (y1
- 1) + x1
]
261 def poke(self
, y
, x
, s
):
263 self
.screen
[pos
:pos
+ len(s
)] = s
265 def fill(self
, y0
, x0
, y1
, x1
, char
):
266 n
= self
.w
* (y1
- y0
- 1) + (x1
- x0
)
267 self
.poke(y0
, x0
, array
.array('i', [char
] * n
))
269 def clear(self
, y0
, x0
, y1
, x1
):
270 self
.fill(y0
, x0
, y1
, x1
, self
.attr |
0x20)
272 # Scrolling functions
273 def scroll_area_up(self
, y0
, y1
, n
=1):
275 self
.poke(y0
, 0, self
.peek(y0
+ n
, 0, y1
, self
.w
))
276 self
.clear(y1
- n
, 0, y1
, self
.w
)
278 def scroll_area_down(self
, y0
, y1
, n
=1):
280 self
.poke(y0
+ n
, 0, self
.peek(y0
, 0, y1
- n
, self
.w
))
281 self
.clear(y0
, 0, y0
+ n
, self
.w
)
283 def scroll_area_set(self
, y0
, y1
):
284 y0
= max(0, min(self
.h
- 1, y0
))
285 y1
= max(1, min(self
.h
, y1
))
287 self
.scroll_area_y0
= y0
288 self
.scroll_area_y1
= y1
290 def scroll_line_right(self
, y
, x
, n
=1):
292 n
= min(self
.w
- self
.cx
, n
)
293 self
.poke(y
, x
+ n
, self
.peek(y
, x
, y
+ 1, self
.w
- n
))
294 self
.clear(y
, x
, y
+ 1, x
+ n
)
296 def scroll_line_left(self
, y
, x
, n
=1):
298 n
= min(self
.w
- self
.cx
, n
)
299 self
.poke(y
, x
, self
.peek(y
, x
+ n
, y
+ 1, self
.w
))
300 self
.clear(y
, self
.w
- n
, y
+ 1, self
.w
)
303 def cursor_line_width(self
, next_char
):
304 wx
= self
.utf8_charwidth(next_char
)
306 for x
in xrange(min(self
.cx
, self
.w
)):
307 char
= self
.peek(self
.cy
, x
, self
.cy
+ 1, x
+ 1)[0] & 0xffff
308 wx
+= self
.utf8_charwidth(char
)
312 def cursor_up(self
, n
=1):
313 self
.cy
= max(self
.scroll_area_y0
, self
.cy
- n
)
315 def cursor_down(self
, n
=1):
316 self
.cy
= min(self
.scroll_area_y1
- 1, self
.cy
+ n
)
318 def cursor_left(self
, n
=1):
319 self
.cx
= max(0, self
.cx
- n
)
321 def cursor_right(self
, n
=1):
322 self
.cx
= min(self
.w
- 1, self
.cx
+ n
)
324 def cursor_set_x(self
, x
):
327 def cursor_set_y(self
, y
):
328 self
.cy
= max(0, min(self
.h
- 1, y
))
330 def cursor_set(self
, y
, x
):
336 delta_y
, cx
= divmod(self
.cx
- 1, self
.w
)
337 cy
= max(self
.scroll_area_y0
, self
.cy
+ delta_y
)
338 self
.cursor_set(cy
, cx
)
340 def ctrl_HT(self
, n
=1):
341 if n
> 0 and self
.cx
>= self
.w
:
343 if n
<= 0 and self
.cx
== 0:
346 for i
in xrange(len(self
.tab_stops
)):
347 if self
.cx
>= self
.tab_stops
[i
]:
350 if ts
< len(self
.tab_stops
) and ts
>= 0:
351 self
.cursor_set_x(self
.tab_stops
[ts
])
353 self
.cursor_set_x(self
.w
- 1)
356 if self
.vt100_mode_lfnewline
:
358 if self
.cy
== self
.scroll_area_y1
- 1:
359 self
.scroll_area_up(self
.scroll_area_y0
, self
.scroll_area_y1
)
366 def dumb_write(self
, char
):
372 elif char
>= 10 and char
<= 12:
379 def dumb_echo(self
, char
):
381 wx
, cx
= self
.cursor_line_width(char
)
384 if self
.vt100_mode_autowrap
:
389 if self
.vt100_mode_insert
:
390 self
.scroll_line_right(self
.cy
, self
.cx
)
391 if self
.vt100_charset_is_single_shift
:
392 self
.vt100_charset_is_single_shift
= False
393 elif self
.vt100_charset_is_graphical
and (char
& 0xffe0) == 0x0060:
394 char
= VT100_CHARSET_GRAPH
[char
- 0x60]
395 self
.poke(self
.cy
, self
.cx
, array
.array('i', [self
.attr | char
]))
396 self
.cursor_set_x(self
.cx
+ 1)
398 # VT100 CTRL, ESC, CSI handlers
399 def vt100_charset_update(self
):
400 self
.vt100_charset_is_graphical
= (
401 self
.vt100_charset_g
[self
.vt100_charset_g_sel
] == 2)
403 def vt100_charset_set(self
, g
):
404 # Invoke active character set
405 self
.vt100_charset_g_sel
= g
406 self
.vt100_charset_update()
408 def vt100_charset_select(self
, g
, charset
):
410 self
.vt100_charset_g
[g
] = charset
411 self
.vt100_charset_update()
413 def vt100_setmode(self
, p
, state
):
415 p
= self
.vt100_parse_params(p
, [], False)
418 # Insertion replacement mode
419 self
.vt100_mode_insert
= state
421 # Linefeed/new line mode
422 self
.vt100_mode_lfnewline
= state
425 self
.vt100_mode_cursorkey
= state
428 if self
.vt100_mode_column_switch
:
436 self
.vt100_mode_inverse
= state
439 self
.vt100_mode_origin
= state
441 self
.cursor_set(self
.scroll_area_y0
, 0)
443 self
.cursor_set(0, 0)
446 self
.vt100_mode_autowrap
= state
448 # Text cursor enable mode
449 self
.vt100_mode_cursor
= state
451 # Column switch control
452 self
.vt100_mode_column_switch
= state
454 # Alternate screen mode
455 if ((state
and not self
.vt100_mode_alt_screen
) or
456 (not state
and self
.vt100_mode_alt_screen
)):
457 self
.screen
, self
.screen2
= self
.screen2
, self
.screen
458 self
.vt100_saved
, self
.vt100_saved2
= self
.vt100_saved2
, self
.vt100_saved
459 self
.vt100_mode_alt_screen
= state
462 self
.vt100_mode_backspace
= state
466 self
.vt100_charset_set(1)
470 self
.vt100_charset_set(0)
474 self
.vt100_parse_reset('csi')
476 def esc_DECALN(self
):
477 # Screen alignment display
478 self
.fill(0, 0, self
.h
, self
.w
, 0x00fe0045)
481 self
.vt100_charset_select(0, 0)
484 self
.vt100_charset_select(0, 1)
487 self
.vt100_charset_select(0, 2)
490 self
.vt100_charset_select(0, 3)
493 self
.vt100_charset_select(0, 4)
496 self
.vt100_charset_select(1, 0)
499 self
.vt100_charset_select(1, 1)
502 self
.vt100_charset_select(1, 2)
505 self
.vt100_charset_select(1, 3)
508 self
.vt100_charset_select(1, 4)
512 self
.vt100_saved
= {}
513 self
.vt100_saved
['cx'] = self
.cx
514 self
.vt100_saved
['cy'] = self
.cy
515 self
.vt100_saved
['attr'] = self
.attr
516 self
.vt100_saved
['charset_g_sel'] = self
.vt100_charset_g_sel
517 self
.vt100_saved
['charset_g'] = self
.vt100_charset_g
[:]
518 self
.vt100_saved
['mode_autowrap'] = self
.vt100_mode_autowrap
519 self
.vt100_saved
['mode_origin'] = self
.vt100_mode_origin
523 self
.cx
= self
.vt100_saved
['cx']
524 self
.cy
= self
.vt100_saved
['cy']
525 self
.attr
= self
.vt100_saved
['attr']
526 self
.vt100_charset_g_sel
= self
.vt100_saved
['charset_g_sel']
527 self
.vt100_charset_g
= self
.vt100_saved
['charset_g'][:]
528 self
.vt100_charset_update()
529 self
.vt100_mode_autowrap
= self
.vt100_saved
['mode_autowrap']
530 self
.vt100_mode_origin
= self
.vt100_saved
['mode_origin']
532 def esc_DECKPAM(self
):
533 # Application keypad mode
536 def esc_DECKPNM(self
):
537 # Numeric keypad mode
550 # Character tabulation set
555 if self
.cy
== self
.scroll_area_y0
:
556 self
.scroll_area_down(self
.scroll_area_y0
, self
.scroll_area_y1
)
562 self
.vt100_charset_is_single_shift
= True
566 self
.vt100_charset_is_single_shift
= True
569 # Device control string
570 self
.vt100_parse_reset('str')
574 self
.vt100_parse_reset('str')
585 # Operating system command
586 self
.vt100_parse_reset('str')
590 self
.vt100_parse_reset('str')
593 # Application program command
594 self
.vt100_parse_reset('str')
596 def csi_ICH(self
, p
):
598 p
= self
.vt100_parse_params(p
, [1])
599 self
.scroll_line_right(self
.cy
, self
.cx
, p
[0])
601 def csi_CUU(self
, p
):
603 p
= self
.vt100_parse_params(p
, [1])
604 self
.cursor_up(max(1, p
[0]))
606 def csi_CUD(self
, p
):
608 p
= self
.vt100_parse_params(p
, [1])
609 self
.cursor_down(max(1, p
[0]))
611 def csi_CUF(self
, p
):
613 p
= self
.vt100_parse_params(p
, [1])
614 self
.cursor_right(max(1, p
[0]))
616 def csi_CUB(self
, p
):
618 p
= self
.vt100_parse_params(p
, [1])
619 self
.cursor_left(max(1, p
[0]))
621 def csi_CNL(self
, p
):
626 def csi_CPL(self
, p
):
627 # Cursor preceding line
631 def csi_CHA(self
, p
):
632 # Cursor character absolute
633 p
= self
.vt100_parse_params(p
, [1])
634 self
.cursor_set_x(p
[0] - 1)
636 def csi_CUP(self
, p
):
637 # Set cursor position
638 p
= self
.vt100_parse_params(p
, [1, 1])
639 if self
.vt100_mode_origin
:
640 self
.cursor_set(self
.scroll_area_y0
+ p
[0] - 1, p
[1] - 1)
642 self
.cursor_set(p
[0] - 1, p
[1] - 1)
644 def csi_CHT(self
, p
):
645 # Cursor forward tabulation
646 p
= self
.vt100_parse_params(p
, [1])
647 self
.ctrl_HT(max(1, p
[0]))
651 p
= self
.vt100_parse_params(p
, ['0'], False)
653 self
.clear(self
.cy
, self
.cx
, self
.h
, self
.w
)
655 self
.clear(0, 0, self
.cy
+ 1, self
.cx
+ 1)
657 self
.clear(0, 0, self
.h
, self
.w
)
661 p
= self
.vt100_parse_params(p
, ['0'], False)
663 self
.clear(self
.cy
, self
.cx
, self
.cy
+ 1, self
.w
)
665 self
.clear(self
.cy
, 0, self
.cy
+ 1, self
.cx
+ 1)
667 self
.clear(self
.cy
, 0, self
.cy
+ 1, self
.w
)
671 p
= self
.vt100_parse_params(p
, [1])
672 if (self
.cy
>= self
.scroll_area_y0
and self
.cy
< self
.scroll_area_y1
):
673 self
.scroll_area_down(self
.cy
, self
.scroll_area_y1
, max(1, p
[0]))
677 p
= self
.vt100_parse_params(p
, [1])
678 if (self
.cy
>= self
.scroll_area_y0
and self
.cy
< self
.scroll_area_y1
):
679 self
.scroll_area_up(self
.cy
, self
.scroll_area_y1
, max(1, p
[0]))
681 def csi_DCH(self
, p
):
683 p
= self
.vt100_parse_params(p
, [1])
684 self
.scroll_line_left(self
.cy
, self
.cx
, max(1, p
[0]))
688 p
= self
.vt100_parse_params(p
, [1])
690 self
.scroll_area_y0
, self
.scroll_area_y1
, max(1, p
[0]))
694 p
= self
.vt100_parse_params(p
, [1])
695 self
.scroll_area_down(
696 self
.scroll_area_y0
, self
.scroll_area_y1
, max(1, p
[0]))
698 def csi_CTC(self
, p
):
699 # Cursor tabulation control
700 p
= self
.vt100_parse_params(p
, ['0'], False)
703 if self
.cx
not in self
.tab_stops
:
704 self
.tab_stops
.append(self
.cx
)
705 self
.tab_stops
.sort()
708 self
.tab_stops
.remove(self
.cx
)
714 def csi_ECH(self
, p
):
716 p
= self
.vt100_parse_params(p
, [1])
717 n
= min(self
.w
- self
.cx
, max(1, p
[0]))
718 self
.clear(self
.cy
, self
.cx
, self
.cy
+ 1, self
.cx
+ n
)
720 def csi_CBT(self
, p
):
721 # Cursor backward tabulation
722 p
= self
.vt100_parse_params(p
, [1])
723 self
.ctrl_HT(1 - max(1, p
[0]))
725 def csi_HPA(self
, p
):
726 # Character position absolute
727 p
= self
.vt100_parse_params(p
, [1])
728 self
.cursor_set_x(p
[0] - 1)
730 def csi_HPR(self
, p
):
731 # Character position forward
734 def csi_REP(self
, p
):
736 p
= self
.vt100_parse_params(p
, [1])
737 if self
.vt100_lastchar
< 32:
739 n
= min(2000, max(1, p
[0]))
741 self
.dumb_echo(self
.vt100_lastchar
)
743 self
.vt100_lastchar
= 0
747 p
= self
.vt100_parse_params(p
, ['0'], False)
749 self
.vt100_out
= "\x1b[?1;2c"
750 elif p
[0] == '>0' or p
[0] == '>':
751 self
.vt100_out
= "\x1b[>0;184;0c"
753 def csi_VPA(self
, p
):
754 # Line position absolute
755 p
= self
.vt100_parse_params(p
, [1])
756 self
.cursor_set_y(p
[0] - 1)
758 def csi_VPR(self
, p
):
759 # Line position forward
762 def csi_HVP(self
, p
):
763 # Character and line position
766 def csi_TBC(self
, p
):
768 p
= self
.vt100_parse_params(p
, ['0'], False)
776 self
.vt100_setmode(p
, True)
780 self
.vt100_setmode(p
, False)
782 def csi_SGR(self
, p
):
783 # Select graphic rendition
784 p
= self
.vt100_parse_params(p
, [0])
788 self
.attr
= 0x00fe0000
791 self
.attr |
= 0x01000000
794 self
.attr |
= 0x02000000
797 self
.attr |
= 0x04000000
800 self
.attr
&= 0x7eff0000
803 self
.attr
&= 0x7dff0000
806 self
.attr
&= 0x7bff0000
807 elif m
>= 30 and m
<= 37:
809 self
.attr
= (self
.attr
& 0x7f0f0000) |
((m
- 30) << 20)
812 self
.attr
= (self
.attr
& 0x7f0f0000) |
0x00f00000
813 elif m
>= 40 and m
<= 47:
815 self
.attr
= (self
.attr
& 0x7ff00000) |
((m
- 40) << 16)
818 self
.attr
= (self
.attr
& 0x7ff00000) |
0x000e0000
820 def csi_DSR(self
, p
):
821 # Device status report
822 p
= self
.vt100_parse_params(p
, ['0'], False)
824 self
.vt100_out
= "\x1b[0n"
828 self
.vt100_out
= '\x1b[%d;%dR' % (y
, x
)
830 self
.vt100_out
= 'WebShell'
832 self
.vt100_out
= __version__
836 self
.vt100_out
= '\x1b[?%d;%dR' % (y
, x
)
838 self
.vt100_out
= '\x1b[?13n'
840 self
.vt100_out
= '\x1b[?20n'
842 self
.vt100_out
= '\x1b[?27;1n'
844 self
.vt100_out
= '\x1b[?53n'
846 def csi_DECSTBM(self
, p
):
847 # Set top and bottom margins
848 p
= self
.vt100_parse_params(p
, [1, self
.h
])
849 self
.scroll_area_set(p
[0] - 1, p
[1])
850 if self
.vt100_mode_origin
:
851 self
.cursor_set(self
.scroll_area_y0
, 0)
853 self
.cursor_set(0, 0)
855 def csi_SCP(self
, p
):
856 # Save cursor position
857 self
.vt100_saved_cx
= self
.cx
858 self
.vt100_saved_cy
= self
.cy
860 def csi_RCP(self
, p
):
861 # Restore cursor position
862 self
.cx
= self
.vt100_saved_cx
863 self
.cy
= self
.vt100_saved_cy
865 def csi_DECREQTPARM(self
, p
):
866 # Request terminal parameters
867 p
= self
.vt100_parse_params(p
, [], False)
869 self
.vt100_out
= "\x1b[2;1;1;112;112;1;0x"
871 self
.vt100_out
= "\x1b[3;1;1;112;112;1;0x"
873 def csi_DECSTR(self
, p
):
874 # Soft terminal reset
878 def vt100_parse_params(self
, p
, d
, to_int
=True):
879 # Process parameters (params p with defaults d)
880 # Add prefix to all parameters
883 if p
[0] >= '<' and p
[0] <= '?':
890 n
= max(len(p
), len(d
))
895 value
= prefix
+ p
[i
]
902 if (not value_def
) and i
< len(d
):
907 def vt100_parse_reset(self
, vt100_parse_state
=""):
908 self
.vt100_parse_state
= vt100_parse_state
909 self
.vt100_parse_len
= 0
910 self
.vt100_parse_func
= ""
911 self
.vt100_parse_param
= ""
913 def vt100_parse_process(self
):
914 if self
.vt100_parse_state
== 'esc':
916 f
= self
.vt100_parse_func
921 if self
.vt100_parse_state
== 'esc':
922 self
.vt100_parse_reset()
925 f
= self
.vt100_parse_func
926 p
= self
.vt100_parse_param
931 if self
.vt100_parse_state
== 'csi':
932 self
.vt100_parse_reset()
934 def vt100_write(self
, char
):
937 self
.vt100_parse_reset('esc')
943 elif (char
& 0xffe0) == 0x0080:
944 self
.vt100_parse_reset('esc')
945 self
.vt100_parse_func
= chr(char
- 0x40)
946 self
.vt100_parse_process()
949 if self
.vt100_parse_state
:
950 if self
.vt100_parse_state
== 'str':
953 self
.vt100_parse_reset()
956 if char
== 24 or char
== 26:
957 self
.vt100_parse_reset()
960 self
.vt100_parse_len
+= 1
961 if self
.vt100_parse_len
> 32:
962 self
.vt100_parse_reset()
964 char_msb
= char
& 0xf0
966 # Intermediate bytes (added to function)
967 self
.vt100_parse_func
+= unichr(char
)
968 elif char_msb
== 0x30 and self
.vt100_parse_state
== 'csi':
970 self
.vt100_parse_param
+= unichr(char
)
973 self
.vt100_parse_func
+= unichr(char
)
974 self
.vt100_parse_process()
976 self
.vt100_lastchar
= char
980 def set_size(self
, w
, h
):
981 if w
< 2 or w
> 256 or h
< 2 or h
> 256:
994 d
= self
.utf8_decode(d
)
997 if self
.vt100_write(char
):
999 if self
.dumb_write(char
):
1002 self
.dumb_echo(char
)
1009 if self
.vt100_keyfilter_escape
:
1010 self
.vt100_keyfilter_escape
= False
1012 if self
.vt100_mode_cursorkey
:
1013 o
+= VT100_KEYFILTER_APPKEYS
[c
]
1015 o
+= VT100_KEYFILTER_ANSIKEYS
[c
]
1019 self
.vt100_keyfilter_escape
= True
1021 if self
.vt100_mode_backspace
:
1027 if self
.vt100_mode_lfnewline
and char
== 13:
1034 cx
, cy
= min(self
.cx
, self
.w
- 1), self
.cy
1035 for y
in xrange(0, self
.h
):
1038 for x
in xrange(0, self
.w
):
1039 d
= self
.screen
[y
* self
.w
+ x
]
1043 if cy
== y
and cx
== x
and self
.vt100_mode_cursor
:
1044 attr
= attr
& 0xfff0 |
0x000c
1050 fg
= (attr
& 0x00f0) >> 4
1053 inv2
= self
.vt100_mode_inverse
1054 if (inv
and not inv2
) or (inv2
and not inv
):
1064 line
.append((fg
, bg
, ul
))
1067 wx
+= self
.utf8_charwidth(char
)
1069 line
[-1] += unichr(char
)
1072 return (cx
, cy
), screen
1075 def synchronized(func
):
1076 def wrapper(self
, *args
, **kwargs
):
1079 except AttributeError:
1080 self
.lock
= threading
.RLock()
1083 result
= func(self
, *args
, **kwargs
)
1090 class Multiplexer(object):
1092 def __init__(self
, cmd
="/bin/bash", env_term
="xterm-color", timeout
=60 * 60 * 24):
1093 # Set Linux signal handler
1094 if sys
.platform
in ("linux2", "linux3"):
1095 self
.sigchldhandler
= signal(SIGCHLD
, SIG_IGN
)
1099 self
.env_term
= env_term
1100 self
.timeout
= timeout
1103 self
.signal_stop
= 0
1104 self
.thread
= threading
.Thread(target
=self
.proc_thread
)
1108 # Stop supervisor thread
1109 self
.signal_stop
= 1
1112 def proc_resize(self
, sid
, w
, h
):
1113 fd
= self
.session
[sid
]['fd']
1118 struct
.pack('I', TIOCSWINSZ
)
1120 struct
.pack("HHHH", h
, w
, 0, 0))
1121 except (IOError, OSError):
1123 self
.session
[sid
]['term'].set_size(w
, h
)
1124 self
.session
[sid
]['w'] = w
1125 self
.session
[sid
]['h'] = h
1128 def proc_keepalive(self
, sid
, w
, h
, cmd
=None):
1129 if not sid
in self
.session
:
1130 # Start a new session
1131 self
.session
[sid
] = {
1133 'term': Terminal(w
, h
),
1134 'time': time
.time(),
1137 return self
.proc_spawn(sid
, cmd
)
1138 if self
.session
[sid
]['state'] == 'alive':
1139 self
.session
[sid
]['time'] = time
.time()
1140 # Update terminal size
1141 if self
.session
[sid
]['w'] != w
or self
.session
[sid
]['h'] != h
:
1142 self
.proc_resize(sid
, w
, h
)
1146 def proc_spawn(self
, sid
, cmd
=None):
1148 self
.session
[sid
]['state'] = 'alive'
1149 w
, h
= self
.session
[sid
]['w'], self
.session
[sid
]['h']
1152 pid
, fd
= pty
.fork()
1153 except (IOError, OSError):
1154 self
.session
[sid
]['state'] = 'dead'
1157 cmd
= cmd
or self
.cmd
1158 # Safe way to make it work under BSD and Linux
1160 ls
= os
.environ
['LANG'].split('.')
1164 ls
= ['en_US', 'UTF-8']
1166 os
.putenv('COLUMNS', str(w
))
1167 os
.putenv('LINES', str(h
))
1168 os
.putenv('TERM', self
.env_term
)
1169 os
.putenv('PATH', os
.environ
['PATH'])
1170 os
.putenv('LANG', ls
[0] + '.UTF-8')
1172 proc
= Popen(cmd
, shell
=False)
1173 # print "called with subprocess", proc.pid
1174 child_pid
, sts
= os
.waitpid(proc
.pid
, 0)
1175 # print "child_pid", child_pid, sts
1176 except (IOError, OSError):
1178 # self.proc_finish(sid)
1181 # Store session vars
1182 self
.session
[sid
]['pid'] = pid
1183 self
.session
[sid
]['fd'] = fd
1185 fcntl
.fcntl(fd
, fcntl
.F_SETFL
, os
.O_NONBLOCK
)
1187 self
.proc_resize(sid
, w
, h
)
1190 def proc_waitfordeath(self
, sid
):
1192 os
.close(self
.session
[sid
]['fd'])
1193 except (KeyError, IOError, OSError):
1195 if sid
in self
.session
:
1196 if 'fd' in self
.session
[sid
]:
1197 del self
.session
[sid
]['fd']
1199 os
.waitpid(self
.session
[sid
]['pid'], 0)
1200 except (KeyError, IOError, OSError):
1202 if sid
in self
.session
:
1203 if 'pid' in self
.session
[sid
]:
1204 del self
.session
[sid
]['pid']
1205 self
.session
[sid
]['state'] = 'dead'
1208 def proc_bury(self
, sid
):
1209 if self
.session
[sid
]['state'] == 'alive':
1211 os
.kill(self
.session
[sid
]['pid'], SIGTERM
)
1212 except (IOError, OSError):
1214 self
.proc_waitfordeath(sid
)
1215 if sid
in self
.session
:
1216 del self
.session
[sid
]
1220 def proc_buryall(self
):
1221 for sid
in self
.session
.keys():
1225 def proc_read(self
, sid
):
1229 if sid
not in self
.session
:
1231 elif self
.session
[sid
]['state'] != 'alive':
1234 fd
= self
.session
[sid
]['fd']
1235 d
= os
.read(fd
, 65536)
1237 # Process finished, BSD
1238 self
.proc_waitfordeath(sid
)
1240 except (IOError, OSError):
1241 # Process finished, Linux
1242 self
.proc_waitfordeath(sid
)
1244 term
= self
.session
[sid
]['term']
1246 # Read terminal response
1251 except (IOError, OSError):
1256 def proc_write(self
, sid
, d
):
1257 " Write to process "
1258 if sid
not in self
.session
:
1260 if self
.session
[sid
]['state'] != 'alive':
1263 term
= self
.session
[sid
]['term']
1265 fd
= self
.session
[sid
]['fd']
1267 except (IOError, OSError):
1272 def proc_dump(self
, sid
):
1273 " Dump terminal output "
1274 if sid
not in self
.session
:
1276 return self
.session
[sid
]['term'].dump()
1279 def proc_getalive(self
):
1281 Get alive sessions, bury timed out ones
1286 for sid
in self
.session
.keys():
1287 then
= self
.session
[sid
]['time']
1288 if (now
- then
) > self
.timeout
:
1291 if self
.session
[sid
]['state'] == 'alive':
1292 fds
.append(self
.session
[sid
]['fd'])
1293 fd2sid
[self
.session
[sid
]['fd']] = sid
1294 return (fds
, fd2sid
)
1296 def proc_thread(self
):
1300 while not self
.signal_stop
:
1302 (fds
, fd2sid
) = self
.proc_getalive()
1304 i
, o
, e
= select(fds
, [], [], 1.0)
1305 except (IOError, OSError):
1310 self
.session
[sid
]["changed"] = time
.time()
1316 def ssh_command(login
, executable
="ssh"):
1318 cmd
+= ' -oPreferredAuthentications=keyboard-interactive,password'
1319 cmd
+= ' -oNoHostAuthenticationForLocalhost=yes'
1320 cmd
+= ' -oLogLevel=FATAL'
1321 cmd
+= ' -F/dev/null -l' + login
+ ' localhost'
1325 class Session(object):
1332 def __init__(self
, cmd
=None, width
=80, height
=24):
1333 if not Session
._mux
:
1334 Session
._mux
= Multiplexer()
1335 self
._session
_id
= "%s-%s" % (time
.time(), id(self
))
1337 self
._height
= height
1338 self
._started
= False
1340 def resize(self
, width
, height
):
1342 self
._height
= height
1346 def start(self
, cmd
=None):
1347 self
._started
= Session
._mux
.proc_keepalive(
1348 self
._session
_id
, self
._width
, self
._height
, cmd
or self
.cmd
)
1349 return self
._started
1352 return Session
._mux
.proc_bury(self
._session
_id
)
1357 return Session
._mux
.session
.get(self
._session
_id
, {}).get('state') == 'alive'
1359 def keepalive(self
):
1360 return Session
._mux
.proc_keepalive(self
._session
_id
, self
._width
, self
._height
)
1363 if self
.keepalive():
1364 return Session
._mux
.proc_dump(self
._session
_id
)
1366 def write(self
, data
):
1367 if self
.keepalive():
1368 Session
._mux
.proc_write(self
._session
_id
, data
)
1370 def last_change(self
):
1371 return Session
._mux
.session
.get(self
._session
_id
, {}).get("changed", None)
1374 return Session
._mux
.session
.get(self
._session
_id
, {}).get("pid", None)
1377 if __name__
== "__main__":
1379 cmd
= "/bin/ls --color=yes"
1380 multiplex
= Multiplexer(cmd
)
1381 sessionID
= "session-id-%s"
1382 if multiplex
.proc_keepalive(sessionID
, w
, h
):
1383 #multiplex.proc_write(sessionID, k)
1385 # print multiplex.proc_dump(sessionID)
1386 print "Output:", multiplex
.proc_dump(sessionID
)