Implement pseudo terminal

This commit is contained in:
Florian Mounier
2014-01-16 13:00:43 +01:00
parent e1cb6ae064
commit 377d34b3c7
11 changed files with 215 additions and 334 deletions

View File

@@ -2,7 +2,9 @@ import os
import tornado.web
import tornado.options
import tornado.web
from logging import getLogger
log = getLogger('wsterm')
application = tornado.web.Application(
debug=tornado.options.options.debug,

View File

@@ -1,6 +1,20 @@
import pty
import os
import io
import sys
import struct
import signal
import fcntl
import termios
import tornado.websocket
import tornado.process
import tornado.ioloop
from subprocess import Popen
from app import url, Route
ioloop = tornado.ioloop.IOLoop.instance()
@url(r'/')
class Index(Route):
def get(self):
@@ -8,15 +22,83 @@ class Index(Route):
@url(r'/ws')
class EchoWebSocket(Route, tornado.websocket.WebSocketHandler):
class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
def open(self):
log.info('Websocket opened')
self.log.info('Websocket opened')
pid, fd = pty.fork()
if pid == 0:
# Child
try:
fd_list = [int(i) for i in os.listdir('/proc/self/fd')]
except OSError:
fd_list = range(256)
# Close all file descriptors other than
# stdin, stdout, and stderr (0, 1, 2)
for i in [i for i in fd_list if i > 2]:
try:
os.close(i)
except OSError:
pass
env = os.environ
env["TERM"] = "xterm"
env["COLORTERM"] = "wsterm"
command = os.getenv('SHELL')
env["SHELL"] = command[0]
p = Popen(command, env=env)
p.wait()
self.log.info('Exiting...')
sys.exit(0)
else:
self.pid = pid
self.fd = fd
self.log.debug('Adding handler')
fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
# Set the size of the terminal window:
s = struct.pack("HHHH", 80, 80, 0, 0)
fcntl.ioctl(fd, termios.TIOCSWINSZ, s)
self.reader = io.open(
fd,
'rt',
buffering=1024,
newline="",
encoding='UTF-8',
closefd=False,
errors='handle_special'
)
self.writer = io.open(
fd,
'wt',
buffering=1024,
newline="",
encoding='UTF-8',
closefd=False
)
ioloop.add_handler(fd, self.shell, ioloop.READ)
def on_message(self, message):
self.write_message(message)
self.log.info('WRIT<%s' % message)
self.writer.write(message)
self.writer.flush()
def shell(self, fd, events):
if events & ioloop.READ:
self.log.info('shell %d: %d' % (fd, events))
try:
read = self.reader.read()
except IOError:
self.log.info('READ>%s' % read)
self.write_message('Exited')
return
self.log.info('READ>%s' % read)
self.write_message(read)
def on_close(self):
log.info('Websocket closed')
self.writer.write('')
os.close(self.fd)
os.waitpid(self.pid, 0)
self.log.info('Websocket closed')

View File

@@ -1 +1,27 @@
console.log 'main js file loaded'
ws = null
$ ->
ws = new WebSocket 'ws://' + document.location.host + '/ws'
ws.onopen = -> console.log "WebSocket open", arguments
ws.onclose = -> console.log "WebSocket closed", arguments
ws.onerror = -> console.log "WebSocket error", arguments
ws.onmessage = (event) ->
$('.term code').html($('.term code').html() + event.data)
$('html,body').on('keypress', (event) ->
code = event.keyCode
ws.send(String.fromCharCode(code))
event.preventDefault()
event.stopPropagation()
return false
).on('keydown', (event) ->
code = event.keyCode
return if code == 17
if event.ctrlKey
code -= 64
ws.send(String.fromCharCode(code))
event.preventDefault()
event.stopPropagation()
return false
)

View File

@@ -1,2 +1,41 @@
// Generated by CoffeeScript 1.6.3
console.log('main js file loaded');
var ws;
ws = null;
$(function() {
ws = new WebSocket('ws://' + document.location.host + '/ws');
ws.onopen = function() {
return console.log("WebSocket open", arguments);
};
ws.onclose = function() {
return console.log("WebSocket closed", arguments);
};
ws.onerror = function() {
return console.log("WebSocket error", arguments);
};
ws.onmessage = function(event) {
return $('.term code').html($('.term code').html() + event.data);
};
return $('html,body').on('keypress', function(event) {
var code;
code = event.keyCode;
ws.send(String.fromCharCode(code));
event.preventDefault();
event.stopPropagation();
return false;
}).on('keydown', function(event) {
var code;
code = event.keyCode;
if (code === 17) {
return;
}
if (event.ctrlKey) {
code -= 64;
ws.send(String.fromCharCode(code));
event.preventDefault();
event.stopPropagation();
return false;
}
});
});

View File

@@ -4,79 +4,15 @@ html, body
body
color: #5a5a5a
padding-top: 70px
>main
min-height: 100%
height: auto
margin: 0 auto -60px
padding: 0 0 60px
main
display: flex
flex-direction: column
>footer
height: 60px
background-color: #f5f5f5
p
margin: 20px 0
.term
flex: 1
code
white-space: pre-line
.carousel
height: 500px
margin-bottom: 60px
.carousel-caption
z-index: 10
.carousel .item
height: 500px
background-color: #777
.carousel-inner > .item > img
position: absolute
top: 0
left: 0
min-width: 100%
height: 500px
.marketing
padding-left: 15px
padding-right: 15px
.col-lg-4
text-align: center
margin-bottom: 20px
h2
font-weight: normal
.col-lg-4 p
margin-left: 10px
margin-right: 10px
.featurette-divider
margin: 80px 0
.featurette-heading
font-weight: 300
line-height: 1
letter-spacing: -1px
@media (min-width: 768px)
.marketing
padding-left: 0
padding-right: 0
.navbar-wrapper
margin-top: 20px
.container
padding-left: 15px
padding-right: 15px
.navbar
padding-left: 0
padding-right: 0
border-radius: 4px
.carousel-caption p
margin-bottom: 20px
font-size: 21px
line-height: 1.4
.featurette-heading
font-size: 50px
@media (min-width: 992px)
.featurette-heading
margin-top: 120px
#prompt
border: 1px solid #777

View File

@@ -1,124 +1,26 @@
/* line 1, ../sass/main.sass */
body {
padding-bottom: 40px;
color: #5a5a5a;
/* line 2, ../sass/main.sass */
html, body {
height: 100%;
}
/* line 5, ../sass/main.sass */
.navbar-wrapper {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 20;
body {
color: #5a5a5a;
}
/* line 11, ../sass/main.sass */
.navbar-wrapper .container {
padding-left: 0;
padding-right: 0;
/* line 8, ../sass/main.sass */
body main {
display: flex;
flex-direction: column;
}
/* line 12, ../sass/main.sass */
body main .term {
flex: 1;
}
/* line 14, ../sass/main.sass */
.navbar-wrapper .navbar {
padding-left: 15px;
padding-right: 15px;
body main .term code {
white-space: pre-line;
}
/* line 18, ../sass/main.sass */
.carousel {
height: 500px;
margin-bottom: 60px;
}
/* line 22, ../sass/main.sass */
.carousel-caption {
z-index: 10;
}
/* line 25, ../sass/main.sass */
.carousel .item {
height: 500px;
background-color: #777777;
}
/* line 29, ../sass/main.sass */
.carousel-inner > .item > img {
position: absolute;
top: 0;
left: 0;
min-width: 100%;
height: 500px;
}
/* line 36, ../sass/main.sass */
.marketing {
padding-left: 15px;
padding-right: 15px;
}
/* line 39, ../sass/main.sass */
.marketing .col-lg-4 {
text-align: center;
margin-bottom: 20px;
}
/* line 42, ../sass/main.sass */
.marketing h2 {
font-weight: normal;
}
/* line 44, ../sass/main.sass */
.marketing .col-lg-4 p {
margin-left: 10px;
margin-right: 10px;
}
/* line 48, ../sass/main.sass */
.featurette-divider {
margin: 80px 0;
}
/* line 51, ../sass/main.sass */
.featurette-heading {
font-weight: 300;
line-height: 1;
letter-spacing: -1px;
}
@media (min-width: 768px) {
/* line 57, ../sass/main.sass */
.marketing {
padding-left: 0;
padding-right: 0;
}
/* line 60, ../sass/main.sass */
.navbar-wrapper {
margin-top: 20px;
}
/* line 62, ../sass/main.sass */
.navbar-wrapper .container {
padding-left: 15px;
padding-right: 15px;
}
/* line 65, ../sass/main.sass */
.navbar-wrapper .navbar {
padding-left: 0;
padding-right: 0;
border-radius: 4px;
}
/* line 69, ../sass/main.sass */
.carousel-caption p {
margin-bottom: 20px;
font-size: 21px;
line-height: 1.4;
}
/* line 73, ../sass/main.sass */
.featurette-heading {
font-size: 50px;
}
}
@media (min-width: 992px) {
/* line 77, ../sass/main.sass */
.featurette-heading {
margin-top: 120px;
}
/* line 17, ../sass/main.sass */
body main #prompt {
border: 1px solid #777777;
}

View File

@@ -1,91 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link rel="shortcut icon" href="{{ static_url('images/favicon.png') }}">
{% include "_ie.html" %}
<title>Apparatus</title>
<link href="{{ static_url('stylesheets/bootstrap/bootstrap.css') }}" rel="stylesheet">
<link href="{{ static_url('stylesheets/main.css') }}" rel="stylesheet">
</head>
<body>
{% include "_nav.html" %}
<section id="myCarousel" class="carousel slide" data-ride="carousel">
<ol class="carousel-indicators">
<li data-target="#myCarousel" data-slide-to="0" class="active"></li>
<li data-target="#myCarousel" data-slide-to="1"></li>
<li data-target="#myCarousel" data-slide-to="2"></li>
</ol>
<div class="carousel-inner">
{% for i in range(3) %}
<article class="item{{ ' active' if i == 0 else '' }}">
<div class="container">
<div class="carousel-caption">
<h1>Title</h1>
<p>Venenatis nullam purus fusce sed et, pellentesque dolor venenatis at donec. Pretium quam, rhoncus sed tempus dictum. Neque ut tincidunt vestibulum vel et, inceptos vestibulum blandit, pellentesque praesent sit. Posuere in praesent, volutpat accumsan felis ut veritatis metus, leo amet purus pede semper in ornare. Convallis sollicitudin at dapibus, phasellus eros felis aliquet nulla amet, in in et tristique enim. Justo gravida nulla rhoncus, faucibus est in lacus sollicitudin fringilla sed, aliquet eu magna morbi lectus nulla pede. Mi pellentesque dictumst nam suspendisse vero. Metus ut elit mauris sollicitudin elit, malesuada semper pede elementum. Id aliquet magna consequat nulla, erat amet et pharetra lacinia quisque vitae. Suspendisse luctus, in malesuada pellentesque, porta dui orci dui eget lorem. </p>
<p>
<a class="btn btn-lg btn-primary" href="#" role="button">Okay</a>
</p>
</div>
</div>
</article>
{% end %}
</div>
<a class="left carousel-control" href="#myCarousel" data-slide="prev">
<span class="glyphicon glyphicon-chevron-left"></span>
</a>
<a class="right carousel-control" href="#myCarousel" data-slide="next">
<span class="glyphicon glyphicon-chevron-right"></span>
</a>
</section>
<section class="container">
<article class="row">
{% for i in range(3) %}
<div class="col-lg-4">
<h2>Title</h2>
<p>Lorem ipsum dolor sit amet, aliquet porttitor tortor mattis nec mauris, duis nunc accumsan vel. Eget neque suspendisse nonummy turpis, tempus urna cum vestibulum lectus. Ut elit lectus neque, elementum nulla curabitur faucibus, qui augue cubilia. Neque erat nullam ullamco massa habitasse dolor, erat at sed donec vulputate sodales. Et elementum arcu vel blandit ultrices, eu quam, eget nunc ultricies quam tincidunt aenean. Et sem massa ac, egestas justo tempus nam nulla ac mauris, est neque maecenas imperdiet per ipsum. A beatae neque et incidunt mollis ipsum. Nulla lorem. Ullamcorper quisque commodo elit a elementum, suscipit etiam faucibus ante, suspendisse aliquet sed non, nec tellus mauris. Purus urna euismod, viverra etiam dis elit, phasellus quam wisi posuere lorem porttitor, pulvinar consequat nec eu fringilla malesuada. Nec leo dignissim lacus egestas nunc, lorem magna, sodales laoreet dignissim. A facilisis, quisque tristique lorem, gravida risus etiam, in lacinia, congue et nunc at.</p>
<p><a class="btn btn-default" href="#" role="button">View details &raquo;</a></p>
</div>
{% end %}
</article>
<hr class="featurette-divider">
{% for i in range(3) %}
<article class="row featurette">
{% if i % 2 %}
<div class="col-md-5">
<img src="http://lorempixel.com/g/400/400#{{ i }}" />
</div>
{% end %}
<div class="col-md-7">
<h2 class="featurette-heading">Title <span class="text-muted">Subtitle</span></h2>
<p class="lead">Lead</p>
</div>
{% if i % 2 == 0 %}
<div class="col-md-5">
<img src="http://lorempixel.com/g/400/400#{{ i }}" />
</div>
{% end %}
<hr class="featurette-divider">
</article>
{% end %}
<footer>
<p class="pull-right"><a href="#">Back to top</a></p>
<p>&copy; 2013 Company, Inc. &middot; <a href="#">Privacy</a> &middot; <a href="#">Terms</a></p>
</footer>
</section>
<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="{{ static_url('javascripts/bootstrap.min.js') }}"></script>
<script src="{{ static_url('javascripts/main.js') }}"></script>
</body>
</html>

View File

@@ -1,5 +0,0 @@
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->

View File

@@ -1,36 +0,0 @@
<aside class="navbar-wrapper">
<div class="container">
<nav class="navbar navbar-inverse navbar-static-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Project name</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Dropdown <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#">Action</a></li>
<li><a href="#">Another action</a></li>
<li><a href="#">Something else here</a></li>
<li class="divider"></li>
<li class="dropdown-header">Nav header</li>
<li><a href="#">Separated link</a></li>
<li><a href="#">One more separated link</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
</div>
</aside>

View File

@@ -1 +1,28 @@
{% extends "_base.html" %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link rel="shortcut icon" href="{{ static_url('images/favicon.png') }}">
<title>Apparatus</title>
<link href="{{ static_url('stylesheets/bootstrap/bootstrap.css') }}" rel="stylesheet">
<link href="{{ static_url('stylesheets/main.css') }}" rel="stylesheet">
</head>
<body>
<main>
<section class="term">
<code></code>
</section>
</main>
<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="{{ static_url('javascripts/bootstrap.min.js') }}"></script>
<script src="{{ static_url('javascripts/main.js') }}"></script>
</body>
</html>

View File

@@ -11,15 +11,14 @@ import tornado.ioloop
tornado.options.define("secret", default='secret', help="Secret")
tornado.options.define("debug", default=True, help="Debug mode")
tornado.options.define("host", default='apparatus.l', help="Server host")
tornado.options.define("port", default=2001, type=int, help="Server port")
tornado.options.define("host", default='wsterm.l', help="Server host")
tornado.options.define("port", default=11112, type=int, help="Server port")
host = 'apparatus.l'
tornado.options.parse_command_line()
from logging import getLogger
log = getLogger('apparatus')
log = getLogger('wsterm')
log.setLevel(10 if tornado.options.options.debug else 30)
log.debug('Starting server')