Browse Source

Provide a simple start/stop interface for gunicorn

Peter Justin 7 years ago
parent
commit
311febcff4
3 changed files with 148 additions and 64 deletions
  1. 1 0
      flaskbb/cli/__init__.py
  2. 0 64
      flaskbb/cli/main.py
  3. 147 0
      flaskbb/cli/server.py

+ 1 - 0
flaskbb/cli/__init__.py

@@ -20,3 +20,4 @@ from flaskbb.cli.plugins import plugins  # noqa
 from flaskbb.cli.themes import themes  # noqa
 from flaskbb.cli.translations import translations  # noqa
 from flaskbb.cli.users import users  # noqa
+from flaskbb.cli.server import server  # noqa

+ 0 - 64
flaskbb/cli/main.py

@@ -28,7 +28,6 @@ from sqlalchemy_utils.functions import database_exists
 from flask_alembic import alembic_click
 
 from flaskbb import create_app
-from flaskbb._compat import iteritems
 from flaskbb.extensions import db, whooshee, celery, alembic
 from flaskbb.cli.utils import (prompt_save_user, prompt_config_path,
                                write_config, get_version, FlaskBBCLIError,
@@ -297,69 +296,6 @@ def start_celery(ctx):
     )
 
 
-@flaskbb.command()
-@click.option("--server", "-s", default="gunicorn",
-              type=click.Choice(["gunicorn", "gevent"]),
-              help="The WSGI Server to run FlaskBB on.")
-@click.option("--host", "-h", default="127.0.0.1",
-              help="The interface to bind FlaskBB to.")
-@click.option("--port", "-p", default="8000", type=int,
-              help="The port to bind FlaskBB to.")
-@click.option("--workers", "-w", default=4,
-              help="The number of worker processes for handling requests.")
-@click.option("--daemon", "-d", default=False, is_flag=True,
-              help="Starts gunicorn as daemon.")
-@click.option("--config", "-c",
-              help="The configuration file to use for FlaskBB.")
-def start(server, host, port, workers, config, daemon):
-    """Starts a production ready wsgi server.
-    TODO: Figure out a way how to forward additional args to gunicorn
-          without causing any errors.
-    """
-    if server == "gunicorn":
-        try:
-            from gunicorn.app.base import Application
-
-            class FlaskBBApplication(Application):
-                def __init__(self, app, options=None):
-                    self.options = options or {}
-                    self.application = app
-                    super(FlaskBBApplication, self).__init__()
-
-                def load_config(self):
-                    config = dict([
-                        (key, value) for key, value in iteritems(self.options)
-                        if key in self.cfg.settings and value is not None
-                    ])
-                    for key, value in iteritems(config):
-                        self.cfg.set(key.lower(), value)
-
-                def load(self):
-                    return self.application
-
-            options = {
-                "bind": "{}:{}".format(host, port),
-                "workers": workers,
-                "daemon": daemon,
-            }
-            FlaskBBApplication(create_app(config=config), options).run()
-        except ImportError:
-            raise FlaskBBCLIError("Cannot import gunicorn. "
-                                  "Make sure it is installed.", fg="red")
-
-    elif server == "gevent":
-        try:
-            from gevent import __version__
-            from gevent.pywsgi import WSGIServer
-            click.secho("* Starting gevent {}".format(__version__))
-            click.secho("* Listening on http://{}:{}/".format(host, port))
-            http_server = WSGIServer((host, port), create_app(config=config))
-            http_server.serve_forever()
-        except ImportError:
-            raise FlaskBBCLIError("Cannot import gevent. "
-                                  "Make sure it is installed.", fg="red")
-
-
 @flaskbb.command("shell", short_help="Runs a shell in the app context.")
 @with_appcontext
 def shell_command():

+ 147 - 0
flaskbb/cli/server.py

@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.cli.plugins
+    ~~~~~~~~~~~~~~~~~~~
+
+    This module contains all plugin commands.
+
+    :copyright: (c) 2016 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+import os
+import signal
+
+from flask.cli import pass_script_info
+import click
+
+from flaskbb._compat import iteritems
+from flaskbb.cli.main import flaskbb
+from flaskbb.cli.utils import FlaskBBCLIError
+
+
+def get_gunicorn_pid(pid, instance_path):
+    if os.path.exists(pid):  # pidfile provided
+        with open(pid) as pidfile:
+            pid = int(pidfile.readline().strip())
+    elif pid:  # pid provided
+        pid = int(pid)
+        try:
+            os.kill(pid, 0)
+        except OSError:
+            pid = None
+    else:  # nothing provided, lets try to get the pid from flaskbb.pid
+        pid = None
+        pidfile = os.path.join(instance_path, "flaskbb.pid")
+        if os.path.exists(pidfile):
+            with open(pidfile) as f:
+                pid = int(f.readline().strip())
+
+    return pid
+
+
+@flaskbb.group()
+def server():
+    """Manages the start and stop process of the gunicorn WSGI server. \n
+    Gunicorn is made for UNIX-like operating systems and thus Windows is not
+    supported.\n
+
+    For proper monitoring of the Gunicorn/FlaskBB process it is advised
+    to use a real process monitoring system like 'supervisord' or
+    'systemd'.
+    """
+    pass
+
+
+@server.command()
+@click.option("--host", "-h", default="127.0.0.1", show_default=True,
+              help="The interface to bind to.")
+@click.option("--port", "-p", default="8000", type=int, show_default=True,
+              help="The port to bind to.")
+@click.option("--workers", "-w", default=4, show_default=True,
+              help="The number of worker processes for handling requests.")
+@click.option("--daemon", "-d", default=False, is_flag=True, show_default=True,
+              help="Starts gunicorn as daemon.")
+@click.option("--pid", "-p", default=None, help="Path to a PID file. "
+              "If the instance directory exists, it will store the Pidfile "
+              "there.")
+@pass_script_info
+def start(info, host, port, workers, daemon, pid):
+    """Starts a preconfigured gunicorn instance."""
+    try:
+        from gunicorn.app.base import Application
+    except ImportError:
+        raise FlaskBBCLIError("Cannot import gunicorn. "
+                              "Make sure it is installed.", fg="red")
+
+    class FlaskBBApplication(Application):
+        def __init__(self, app, options=None, args=None):
+            self.options = options or {}
+            self.application = app
+            super(FlaskBBApplication, self).__init__()
+
+        def load_config(self):
+            config = dict([
+                (key, value) for key, value in iteritems(self.options)
+                if key in self.cfg.settings and value is not None
+            ])
+            for key, value in iteritems(config):
+                print(key, value)
+                self.cfg.set(key.lower(), value)
+
+        def load(self):
+            return self.application
+
+    flaskbb_app = info.load_app()
+    default_pid = None
+    if os.path.exists(flaskbb_app.instance_path):
+        default_pid = os.path.join(flaskbb_app.instance_path, "flaskbb.pid")
+
+    options = {
+        "bind": "{}:{}".format(host, port),
+        "workers": workers,
+        "daemon": daemon,
+        "pidfile": pid or default_pid
+    }
+    FlaskBBApplication(flaskbb_app, options).run()
+
+
+@server.command()
+@click.option("--pid", "-p", default="", help="The PID or the path to "
+              "the PID file. By default it tries to get the PID file from the "
+              "instance folder.")
+@click.option("--force", "-f", default=False, is_flag=True, show_default=True,
+              help="Kills gunicorn ungratefully.")
+@pass_script_info
+def stop(info, pid, force):
+    """Stops the gunicorn process."""
+    app = info.load_app()
+    pid = get_gunicorn_pid(pid, app.instance_path)
+
+    if pid is None:
+        raise FlaskBBCLIError("Neither a valid PID File nor a PID that exist "
+                              "was provided.", fg="red")
+
+    try:
+        if force:
+            os.kill(pid, signal.SIGKILL)
+        else:
+            os.kill(pid, signal.SIGTERM)
+    except ProcessLookupError:
+        click.secho("Process with PID '{}' not found. Are you sure that "
+                    "Gunicorn/FlaskBB is running?".format(pid), fg="yellow")
+
+
+@server.command()
+@click.option("--pid", "-p", default="", help="The PID or the path to "
+              "the PID file. By default it tries to get the PID file from the "
+              "instance folder.")
+@pass_script_info
+def status(info, pid):
+    """Shows the status of gunicorn."""
+    app = info.load_app()
+    pid = get_gunicorn_pid(pid, app.instance_path)
+
+    if pid is None:
+        click.secho("Gunicorn/FlaskBB is not running.", fg="red")
+    else:
+        click.secho("Gunicorn/FlaskBB is running.", fg="green")