gevent.pywsgi.WSGIServer and cmd2.Cmd, how to make them play nice

45 views
Skip to first unread message

Mike Schiffman

unread,
Jul 10, 2019, 12:21:16 PM7/10/19
to gevent: coroutine-based Python network library

tl;dr: What's the proper workflow to integrate gevent's WSGIServer to work alongside cmd2's Cmd?



Hello everyone. I have a daemon that I initially built around cmd2's cmdloop which is greedy with program flow control.

I have a daemon program I am working on that:
  • Exposes a command line interface to the console user via cmd2
  • Exposes a restful api to the remote user via flask, flask_restful and gevent.WSGIServer
I have not been able to successfully make gevent and cmd2 work properly together. I monkey patched everywhere but I run into problems where the WSGI server accepts a connection but then hangs.

I read this: https://cmd2.readthedocs.io/en/latest/integrating.html, but I am not sure how to fold that into the gevent flow.

I invoke both the WSGI server and the cmd2 cmdloop from a central "core" class (running in the main program thread). I tried running either cmdloop in its own thread or WSGIServer in its own thread -- neither of these worked properly.

API init/run:

    def __init__(self, tdns_core, api_port, secret_key, api_users_file, api_private_key_file, api_certificate_file):
        global core
        core = tdns_core

        # app setup etc
        pool = Pool(100)
        self.server = WSGIServer(('localhost', self.port), self.app, keyfile = api_private_key_file, certfile = api_certificate_file, spawn = pool)
        # route setup etc

    def run(self):
        """Start the WSGI server in a daemonized thread.
        """
        threading.Thread(target = self.server.serve_forever, daemon = True).start()


This results in the WSGI server hanging on connect. Do I need to cede control to the WSGI thread and let it run in the foreground? What is the prescribed way to do that?

Thank you for any help you guys can provide!

Jason Madden

unread,
Jul 10, 2019, 2:33:29 PM7/10/19
to gev...@googlegroups.com


> On Jul 10, 2019, at 11:19, Mike Schiffman <themikes...@gmail.com> wrote:
>
> Hello everyone. I have a daemon that I initially built around cmd2's cmdloop which is greedy with program flow control.
>
> I have a daemon program I am working on that:
> • Exposes a command line interface to the console user via cmd2
> • Exposes a restful api to the remote user via flask, flask_restful and gevent.WSGIServer
> I have not been able to successfully make gevent and cmd2 work properly together. I monkey patched everywhere but I run into problems where the WSGI server accepts a connection but then hangs.

Let me check my understanding. You're trying to launch a program on the command line, and have it run a gevent server that responds to network requests while *also* interacting with a user at the terminal? That may be tricky...

> I read this: https://cmd2.readthedocs.io/en/latest/integrating.html, but I am not sure how to fold that into the gevent flow.

I'm not very familiar with cmd/cmd2. The example seemed to be missing some key details, though. I've added some below.

app = Cmd2EventBased()
app.preloop()

while should_run_loop: # gevent added
# Do this within whatever event loop mechanism you wish to run a single command

cmd_line_text = wait_for_and_read_input() # gevent added
app.runcmds_plus_hooks([cmd_line_text])

app.postloop()


The main missing steps that require integration into the event loop are waiting for, and reading, the user's input *without blocking the event loop*.

The cmd/cmd2 code is complex, but it looks like normally getting input can be some combination of `input()`, `sys.stdin.readline()` or using the `readline` library directly. All of those are going to block the entire process and not cooperate with the event loop by default.

The trouble is that non-blocking, cooperative reads from the terminal can be difficult, especially when things like readline or control characters get involved. If your needs are simple enough, you may just be able to do something like this:

def wait_for_and_read_input(): # untested pseudo code
from gevent.fileobject import FileObjectThread
stdin = FileObjectThread(sys.stdin)
return stdin.read()

On POSIX platforms, you can also try setting the file descriptor for stdin into non-blocking mode and using FileObjectPosix to read --- but that can have its own problems, and you better be careful to restore it before you exit.

Alternately, you might consider three possibilities: Either use real operating system threads and run the cmd loop separate from the gevent loop (don't monkey-patch `threading`, or use a threadpool to run the WSGIServer), don't use gevent at all, or run the WSGIServer in a child process you spawn from the main process that runs the cmd2 loop (only the child process monkey-patches). All of those avoid the need to do non-blocking, cooperative, reads on sys.stdin, but they may or may not be applicable to your situation.

Jason

Reply all
Reply to author
Forward
0 new messages