prompt-toolkit / prompt-toolkit/python-prompt-toolkit

Connect prompt_toolkit to stdout / stdin of an asyncssh server

Open
#902 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
10.6k
Forks
815
PR merge metrics
No merged PRs in 30d

Description

I'm experimenting with asyncssh to build a custom SSH server; I want to connect each client to a pseudo-shell based on prompt_toolkit. Clients will enter commands into that pseudo-shell to control the server.

In this case, I'm just building a chat app; the experiment largely follows the example under asyncssh's "Serving multiple clients" in the documentation.

My problem is I can't figure out how to reassign stdin and stdout for each client. prompt_toolkit seems to assume that the server's stdout is where prompts are sent; however, I need the prompts to use each individual client's stdin and stdout.

QUESTION

This is my first attempt to build anything with either asyncssh and prompt_toolkit, so perhaps I'm missing something. Is there a way to make prompt_toolkit use the client's stdin and stdout?

My (obviously broken) attempt at this is shown below; the problem is in the interact() method.

import asyncio
import crypt
import sys
import os

from prompt_toolkit.eventloop.defaults import use_asyncio_event_loop
from prompt_toolkit.patch_stdout import patch_stdout
from prompt_toolkit import prompt

import asyncssh

# Tell prompt_toolkit to use the asyncio event loop.
use_asyncio_event_loop()

class ChatClient:
    _clients = []

    def __init__(self, process):
        self._process = process

    @classmethod
    async def handle_client(cls, process):
        await cls(process).interact()

    def write(self, msg):
        self._process.stdout.write(msg)

    def broadcast(self, msg):
        for client in self._clients:
            if client != self:
                client.write(msg)

    async def interact(self):
        self.write('Welcome to chat!\n\n')

        self.write('Enter your name: ')
        name = (await self._process.stdin.readline()).rstrip('\n')

        self.write('\n%d other users are connected.\n\n' % len(self._clients))

        self._clients.append(self)
        self.broadcast('*** %s has entered chat ***\n' % name)

        try:
            ### This works, but it isn't using prompt toolkit...
            #async for line in self._process.stdin:
            #    self.broadcast('%s: %s' % (name, line))
            #    self._process.stdout.write('chat# ')
            while True:
                line = await prompt('chat# ', async_=True) # <--- Should use client's stdout
                self.broadcast('{0}: {1}'.format(name, line))
        except asyncssh.BreakReceived:
            pass

        self.broadcast('*** %s has left chat ***\n' % name)
        self._clients.remove(self)

passwords = {'guest': '',                 # guest account with no password
             'user123': 'qV2iEadIGV2rw'   # password of 'secretpw'
            }

class CustomSSHServer(asyncssh.SSHServer):
    def connection_made(self, conn):
        print('SSH connection received from %s.' %
                  conn.get_extra_info('peername')[0])

    def connection_lost(self, exc):
        if exc:
            print('SSH connection error: ' + str(exc), file=sys.stderr)
        else:
            print('SSH connection closed.')

    def begin_auth(self, username):
        # If the user's password is the empty string, no auth is required
        return passwords.get(username) != ''

    def password_auth_supported(self):
        return True

    def validate_password(self, username, password):
        pw = passwords.get(username, '*')
        return crypt.crypt(password, pw) == pw

async def start_server():
    await asyncssh.create_server(CustomSSHServer, '', 8022,
                        server_host_keys=['ssh_host_key'],
                        process_factory=ChatClient.handle_client)

loop = asyncio.get_event_loop()

try:
    loop.run_until_complete(start_server())
except (OSError, asyncssh.Error) as exc:
    sys.exit('Error starting server: ' + str(exc))

loop.run_forever()

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the ChatClient.interact() method and the prompt('chat# ', async_=True) call, then compare them with the per-client self._process.stdin and self._process.stdout streams. Determine whether prompt_toolkit supports supplying those streams; done means each connected client receives its own prompts and input without using the server process's standard streams.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
cli
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.