Django原始碼分析之執行入口

靈吾發表於2016-05-06

魔法門

一般我們啟動django,最簡單的方法是進入project 目錄,這時目錄結構是這樣的

然後我們執行python manage.py runserver,程式就開始執行了。

那django是如何從一個命令就啟動整個server,啟動的流程是如何的?

踏門而入

開啟目錄下的manage.py,內容是這樣的:

#!/usr/bin/env python
import os
import sys

if __name__ == "__main__":
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_learning.settings")

    from django.core.management import execute_from_command_line

    execute_from_command_line(sys.argv)

看來manage.py只是把命令列引數傳給django.core.management模組中的execute_from_command_line 函式。

檢視execute_from_command_line函式,可以發現實際執行的是ManagementUtility類的excute方法:

def execute(self):
        """
        Given the command-line arguments, this figures out which subcommand is
        being run, creates a parser appropriate to that command, and runs it.
        """
        try:
            subcommand = self.argv[1]
        except IndexError:
            subcommand = `help`  # Display help if no arguments were given.

        # Preprocess options to extract --settings and --pythonpath.
        # These options could affect the commands that are available, so they
        # must be processed early.
        parser = CommandParser(None, usage="%(prog)s subcommand [options] [args]", add_help=False)
        parser.add_argument(`--settings`)
        parser.add_argument(`--pythonpath`)
        parser.add_argument(`args`, nargs=`*`)  # catch-all
        try:
            options, args = parser.parse_known_args(self.argv[2:])
            handle_default_options(options)
        except CommandError:
            pass  # Ignore any option errors at this point.

        no_settings_commands = [
            `help`, `version`, `--help`, `--version`, `-h`,
            `compilemessages`, `makemessages`,
            `startapp`, `startproject`,
        ]

        try:
            settings.INSTALLED_APPS
        except ImproperlyConfigured as exc:
            self.settings_exception = exc
            # A handful of built-in management commands work without settings.
            # Load the default settings -- where INSTALLED_APPS is empty.
            if subcommand in no_settings_commands:
                settings.configure()

        if settings.configured:
            # Start the auto-reloading dev server even if the code is broken.
            # The hardcoded condition is a code smell but we can`t rely on a
            # flag on the command class because we haven`t located it yet.
            if subcommand == `runserver` and `--noreload` not in self.argv:
                try:
                    autoreload.check_errors(django.setup)()
                except Exception:
                    # The exception will be raised later in the child process
                    # started by the autoreloader. Pretend it didn`t happen by
                    # loading an empty list of applications.
                    apps.all_models = defaultdict(OrderedDict)
                    apps.app_configs = OrderedDict()
                    apps.apps_ready = apps.models_ready = apps.ready = True

            # In all other cases, django.setup() is required to succeed.
            else:
                django.setup()

        self.autocomplete()

        if subcommand == `help`:
            if `--commands` in args:
                sys.stdout.write(self.main_help_text(commands_only=True) + `
`)
            elif len(options.args) < 1:
                sys.stdout.write(self.main_help_text() + `
`)
            else:
                self.fetch_command(options.args[0]).print_help(self.prog_name, options.args[0])
        # Special-cases: We want `django-admin --version` and
        # `django-admin --help` to work, for backwards compatibility.
        elif subcommand == `version` or self.argv[1:] == [`--version`]:
            sys.stdout.write(django.get_version() + `
`)
        elif self.argv[1:] in ([`--help`], [`-h`]):
            sys.stdout.write(self.main_help_text() + `
`)
        else:
            self.fetch_command(subcommand).run_from_argv(self.argv)

其中

parser = CommandParser(None, usage="%(prog)s subcommand [options] [args]", add_help=False)
        parser.add_argument(`--settings`)
        parser.add_argument(`--pythonpath`)
        parser.add_argument(`args`, nargs=`*`)  # catch-all
        try:
            options, args = parser.parse_known_args(self.argv[2:])
            handle_default_options(options)
        except CommandError:
            pass  # Ignore any option errors at this point.

CommandParser其實類似於Argparse的一個解析命令列引數的類,從程式碼裡可以看出我們可以直接在命令列指定settings檔案和pythonpath。

no_settings_commands = [
            `help`, `version`, `--help`, `--version`, `-h`,
            `compilemessages`, `makemessages`,
            `startapp`, `startproject`,
        ]
try:
            settings.INSTALLED_APPS
except ImproperlyConfigured as exc:
            self.settings_exception = exc
            # A handful of built-in management commands work without settings.
            # Load the default settings -- where INSTALLED_APPS is empty.
            if subcommand in no_settings_commands:
                settings.configure()

這塊程式碼就可以解釋我們執行python manage.py start project 時django在背後會呼叫settings.configure方法,這裡的settings是指django.conf.LazySettings的一個例項,configure方法其實就是使用django.conf.global_settings.py中的預設設定建立一份新的配置檔案,作為我們新建立的project的settings.py

if settings.configured:
            # Start the auto-reloading dev server even if the code is broken.
            # The hardcoded condition is a code smell but we can`t rely on a
            # flag on the command class because we haven`t located it yet.
            if subcommand == `runserver` and `--noreload` not in self.argv:
                try:
                    autoreload.check_errors(django.setup)()
                except Exception:
                    # The exception will be raised later in the child process
                    # started by the autoreloader. Pretend it didn`t happen by
                    # loading an empty list of applications.
                    apps.all_models = defaultdict(OrderedDict)
                    apps.app_configs = OrderedDict()
                    apps.apps_ready = apps.models_ready = apps.ready = True

            # In all other cases, django.setup() is required to succeed.
            else:
                django.setup()

autoreload.check_errors(django.setup)()其實也是呼叫django.setup方法,而django.setup方法

def setup():
    """
    Configure the settings (this happens as a side effect of accessing the
    first setting), configure logging and populate the app registry.
    """
    from django.apps import apps
    from django.conf import settings
    from django.utils.log import configure_logging

    configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)
    apps.populate(settings.INSTALLED_APPS)

負責初始化日誌模組以及所有應用.

抽絲剝繭

剩下的程式碼最重要的就是這一句:

self.fetch_command(subcommand).run_from_argv(self.argv)

fetch_command會根據subcommand(這是我們執行python manage.py rumserver時傳入的第二個引數:runserver),去django.core.management.commands中查詢對應的command類,然後把所有命令列引數傳給run_from_argv方法並執行,在runserver這個示例中,最終會呼叫django.utils.autoreload中的python_reloader或者jython_reloader新開一個執行緒:

def python_reloader(main_func, args, kwargs):
    if os.environ.get("RUN_MAIN") == "true":
        thread.start_new_thread(main_func, args, kwargs)
        try:
            reloader_thread()
        except KeyboardInterrupt:
            pass
    else:
        try:
            exit_code = restart_with_reloader()
            if exit_code < 0:
                os.kill(os.getpid(), -exit_code)
            else:
                sys.exit(exit_code)
        except KeyboardInterrupt:
            pass

這裡的main_func是commands/runserver.py中的inner_run方法:

def inner_run(self, *args, **options):
        # If an exception was silenced in ManagementUtility.execute in order
        # to be raised in the child process, raise it now.
        autoreload.raise_last_exception()

        threading = options.get(`use_threading`)
        shutdown_message = options.get(`shutdown_message`, ``)
        quit_command = `CTRL-BREAK` if sys.platform == `win32` else `CONTROL-C`

        self.stdout.write("Performing system checks...

")
        self.check(display_num_errors=True)
        self.check_migrations()
        now = datetime.now().strftime(`%B %d, %Y - %X`)
        if six.PY2:
            now = now.decode(get_system_encoding())
        self.stdout.write(now)
        self.stdout.write((
            "Django version %(version)s, using settings %(settings)r
"
            "Starting development server at http://%(addr)s:%(port)s/
"
            "Quit the server with %(quit_command)s.
"
        ) % {
            "version": self.get_version(),
            "settings": settings.SETTINGS_MODULE,
            "addr": `[%s]` % self.addr if self._raw_ipv6 else self.addr,
            "port": self.port,
            "quit_command": quit_command,
        })

        try:
            handler = self.get_handler(*args, **options)
            run(self.addr, int(self.port), handler,
                ipv6=self.use_ipv6, threading=threading)
        except socket.error as e:
            # Use helpful error messages instead of ugly tracebacks.
            ERRORS = {
                errno.EACCES: "You don`t have permission to access that port.",
                errno.EADDRINUSE: "That port is already in use.",
                errno.EADDRNOTAVAIL: "That IP address can`t be assigned to.",
            }
            try:
                error_text = ERRORS[e.errno]
            except KeyError:
                error_text = force_text(e)
            self.stderr.write("Error: %s" % error_text)
            # Need to use an OS exit because sys.exit doesn`t work in a thread
            os._exit(1)
        except KeyboardInterrupt:
            if shutdown_message:
                self.stdout.write(shutdown_message)
            sys.exit(0)

最關鍵的是這兩條語句:

handler = self.get_handler(*args, **options)
run(self.addr, int(self.port), handler,ipv6=self.use_ipv6, threading=threading)

get_handler會返回django.core.servers.basehttp中定義的一個application(其實就是我們project下的wigs.py中定義的application)

這是run函式的內容

def run(addr, port, wsgi_handler, ipv6=False, threading=False):
    server_address = (addr, port)
    if threading:
        httpd_cls = type(str(`WSGIServer`), (socketserver.ThreadingMixIn, WSGIServer), {})
    else:
        httpd_cls = WSGIServer
    httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6)
    if threading:
        # ThreadingMixIn.daemon_threads indicates how threads will behave on an
        # abrupt shutdown; like quitting the server by the user or restarting
        # by the auto-reloader. True means the server will not wait for thread
        # termination before it quits. This will make auto-reloader faster
        # and will prevent the need to kill the server manually if a thread
        # isn`t terminating correctly.
        httpd.daemon_threads = True
    httpd.set_app(wsgi_handler)
    httpd.serve_forever()

可以看出run函式其實就是啟動一個WSGIServer例項(WSGIServer繼承python內建類simple_server.WSGIServer),並把handler設定為前面get_handler的返回值

水落石出

這樣,一條python manage.py runserver命令的執行生命週期就一覽無餘了。
接下來,server就開始接收請求了。


相關文章