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:
![image](https://user-images.githubusercontent.com/8630726/28435530-6464e35a-6d61-11e7-9eff-17dc39c717fe.png)
![image](https://user-images.githubusercontent.com/8630726/28435537-720cf402-6d61-11e7-84c1-46aff9a0002e.png)

(I can even print colored text in bash)
![image](https://user-images.githubusercontent.com/8630726/28435653-f081b87c-6d61-11e7-82cc-5b193c9b436e.png)


Via lxc exec (for comparison):
![image](https://user-images.githubusercontent.com/8630726/28434971-544faf2e-6d5f-11e7-8347-07af81e2c40e.png)

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:
![image](https://user-images.githubusercontent.com/8630726/28435548-8009a10e-6d61-11e7-8640-d16da35bad47.png)
Then press any key, it returns:
![image](https://user-images.githubusercontent.com/8630726/28435559-868fe9f2-6d61-11e7-8dd6-fac9ac0c3bb9.png)

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

Reply via email to