diff --git a/app/__init__.py b/app/__init__.py index f2f46db..9266301 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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, diff --git a/app/routes.py b/app/routes.py index ef2dc82..5d90e52 100644 --- a/app/routes.py +++ b/app/routes.py @@ -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') diff --git a/app/static/coffees/main.coffee b/app/static/coffees/main.coffee index cddd917..b3303d4 100644 --- a/app/static/coffees/main.coffee +++ b/app/static/coffees/main.coffee @@ -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 + ) diff --git a/app/static/javascripts/main.js b/app/static/javascripts/main.js index bfba0d4..f1d30e8 100644 --- a/app/static/javascripts/main.js +++ b/app/static/javascripts/main.js @@ -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; + } + }); +}); diff --git a/app/static/sass/main.sass b/app/static/sass/main.sass index 6c94c34..76972d4 100644 --- a/app/static/sass/main.sass +++ b/app/static/sass/main.sass @@ -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 diff --git a/app/static/stylesheets/main.css b/app/static/stylesheets/main.css index 36d63d6..df94882 100644 --- a/app/static/stylesheets/main.css +++ b/app/static/stylesheets/main.css @@ -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; } diff --git a/app/templates/_base.html b/app/templates/_base.html deleted file mode 100644 index dc91669..0000000 --- a/app/templates/_base.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - {% include "_ie.html" %} - - Apparatus - - - - - - {% include "_nav.html" %} - - - -
-
- {% for i in range(3) %} -
-

Title

-

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.

-

View details »

-
- {% end %} -
- -
- {% for i in range(3) %} -
- {% if i % 2 %} -
- -
- {% end %} -
-

Title Subtitle

-

Lead

-
- {% if i % 2 == 0 %} -
- -
- {% end %} -
-
- {% end %} - - -
- - - - - - diff --git a/app/templates/_ie.html b/app/templates/_ie.html deleted file mode 100644 index 1169812..0000000 --- a/app/templates/_ie.html +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/app/templates/_nav.html b/app/templates/_nav.html deleted file mode 100644 index e39752d..0000000 --- a/app/templates/_nav.html +++ /dev/null @@ -1,36 +0,0 @@ - diff --git a/app/templates/index.html b/app/templates/index.html index 9789436..d9dea5d 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1 +1,28 @@ -{% extends "_base.html" %} + + + + + + + + + + + Apparatus + + + + + + +
+
+ +
+
+ + + + + + diff --git a/serve.py b/serve.py index 3e6f2f4..cab20e9 100644 --- a/serve.py +++ b/serve.py @@ -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')