prompt-toolkit / prompt-toolkit/python-prompt-toolkit
Connect prompt_toolkit to stdout / stdin of an asyncssh server
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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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