The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/pylxd/pull/241
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) === Hello there! This is an experimental implementation of LXD's [interactive `exec`](https://github.com/lxc/lxd/blob/master/doc/rest-api.md#10containersnameexec). As a contributor of the [lxdock](https://github.com/lxdock/lxdock) project, I would like to add this functionality into pylxd so that we can access a container's shell directly by Python code without having to call `lxc exec` externally. The implementation is inspired by [wssh](https://github.com/aluzzardi/wssh/blob/master/wssh/client.py) and you will find similar codes here. I am not good in interactive shell programming but this code works! But there are still two caveats on which I definitely need more help: 1. The console (bash) has no color even trying with colored commands like `ls / --color` (while `lxc exec xxx -- /bin/bash` always has colors), but what's strange is that `vi` has colors itself: Via this PR:   (I can even print colored text in bash)  Via lxc exec (for comparison):  2. When I want to exit the interactive shell, I press Ctrl-D or type `exit`, then the shell quits but the websocket doesn't close. I have to type another key (i.e., send one extra character) to trigger the termination of the websocket and get back to my shell. After Ctrl-D, it waits here:  Then press any key, it returns:  Any ideas? Your feedback and suggestions are very welcomed!
From 315aab47df2cd7bf3bec1a4e957c324d580cc832 Mon Sep 17 00:00:00 2001 From: Ling-Xiao Yang <[email protected]> Date: Wed, 19 Jul 2017 17:55:28 -0400 Subject: [PATCH] WIP: interactive exec. Caveats: 1. bash color... it's weird because if I `vi` a file the colors are all correct. 2. Ctrl+D... for some reason I have to type another key to let the remote close. --- pylxd/models/container.py | 100 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/pylxd/models/container.py b/pylxd/models/container.py index 415ea37..adbe2a6 100644 --- a/pylxd/models/container.py +++ b/pylxd/models/container.py @@ -12,12 +12,21 @@ # License for the specific language governing permissions and limitations # under the License. import collections +import fcntl +import json +import platform +import termios import time +import tty +import signal +import struct +import sys import six from six.moves.urllib import parse try: from ws4py.client import WebSocketBaseClient + from ws4py.client.threadedclient import WebSocketClient from ws4py.manager import WebSocketManager _ws4py_installed = True except ImportError: # pragma: no cover @@ -250,6 +259,65 @@ def execute(self, commands, environment={}): return _ContainerExecuteResult( operation.metadata['return'], stdout.data, stderr.data) + def execute_interactive(self, commands, environment={}): + if not _ws4py_installed: + raise ValueError( + 'This feature requires the optional ws4py library.') + if isinstance(commands, six.string_types): + raise TypeError("First argument must be a list.") + rows, cols = _pty_size() + response = self.api['exec'].post(json={ + 'command': commands, + 'environment': environment, + 'wait-for-websocket': True, + 'interactive': True, + 'width': cols, + 'height': rows, + }) + + fds = response.json()['metadata']['metadata']['fds'] + operation_id = response.json()['operation'].split('/')[-1] + parsed = parse.urlparse( + self.client.api.operations[operation_id].websocket._api_endpoint) + + pts = _InteractiveWebsocket(self.client.websocket_url) + pts.resource = '{}?secret={}'.format(parsed.path, fds['0']) + pts.connect() + + ctl = WebSocketClient(self.client.websocket_url) + ctl.resource = '{}?secret={}'.format(parsed.path, fds['control']) + ctl.connect() + + oldtty = termios.tcgetattr(sys.stdin) + old_handler = signal.getsignal(signal.SIGWINCH) + + def on_term_resize(signum, frame): + rows, cols = _pty_size() + # Refs: + # https://github.com/lxc/lxd/blob/master/lxd/container_exec.go#L190 + # https://github.com/lxc/lxd/blob/master/shared/api/container_exec.go + ctl.send(json.dumps({ + 'command': 'window-resize', + 'args': { + 'width': str(cols), + 'height': str(rows) + } + })) + signal.signal(signal.SIGWINCH, on_term_resize) + + try: + tty.setraw(sys.stdin.fileno()) + tty.setcbreak(sys.stdin.fileno()) + pts.run_forever() + except Exception as e: + raise + finally: + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty) + signal.signal(signal.SIGWINCH, old_handler) + + operation = self.client.operations.get(operation_id) + return _ContainerExecuteResult(operation.metadata['return'], '', '') + def migrate(self, new_client, wait=False): """Migrate a container. @@ -356,6 +424,38 @@ def handshake_ok(self): self.close() +class _InteractiveWebsocket(WebSocketClient): # pragma: no cover + def received_message(self, message): + if message.encoding: + m = message.data.decode(message.encoding) + else: + m = message.data.decode('utf-8') + sys.stdout.write(m) + sys.stdout.flush() + + def run_forever(self): + while not self.terminated: + if sys.stdin.isatty(): + x = sys.stdin.buffer.read(1) + self.send(x, binary=True) + # The timeout should let cursor move fluidly in vim + self._th.join(timeout=0.01) + + +def _pty_size(): + """ From wssh.client._pty_size """ + rows, cols = 24, 80 + # Can't do much for Windows + if platform.system() == 'Windows': + return rows, cols + fmt = 'HH' + buffer = struct.pack(fmt, 0, 0) + result = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, + buffer) + rows, cols = struct.unpack(fmt, result) + return rows, cols + + class Snapshot(model.Model): """A container snapshot."""
_______________________________________________ lxc-devel mailing list [email protected] http://lists.linuxcontainers.org/listinfo/lxc-devel
