werkzeug中reloader的實現


在用flask開發時,如果把use_reloader設為True(debug設為True也能實現),那當你修改了app代碼或調用環境發生改變時,服務器會自動重啟,如下

 * Detected change in '/home/steinliber/flask-source-code/route/a.py', reloading
 * Restarting with stat
 * Debugger is active!
 * Debugger pin code: 167-130-643

可以看出服務器會自動檢測是哪個文件發生了改變並自動重啟,接下來就看看它是怎么實現的,這是當use_reloader開啟時相關函數調用情況

/usr/lib/python2.7/threading.py(783)__bootstrap()
-> self.__bootstrap_inner()
/usr/lib/python2.7/threading.py(810)__bootstrap_inner()
-> self.run()
/usr/lib/python2.7/threading.py(763)run()
-> self.__target(*self.__args, **self.__kwargs)
/home/steinliber/flask-source-code/env/local/lib/python2.7/site-packages/werkzeug/serving.py(657)inner()

可以看出該進程創建了線程並實現服務器功能

在函數run_simple中,會對use_reloader進行判斷

  if use_reloader:
        # If we're not running already in the subprocess that is the
        # reloader we want to open up a socket early to make sure the
        # port is actually available.
        if os.environ.get('WERKZEUG_RUN_MAIN') != 'true':
            if port == 0 and not can_open_by_fd:
                raise ValueError('Cannot bind to a random port with enabled '
                                 'reloader if the Python interpreter does '
                                 'not support socket opening by fd.')

            # Create and destroy a socket so that any exceptions are
            # raised before we spawn a separate Python interpreter and
            # lose this ability.
            address_family = select_ip_version(hostname, port)
            s = socket.socket(address_family, socket.SOCK_STREAM)
            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            s.bind((hostname, port))
            if hasattr(s, 'set_inheritable'):
                s.set_inheritable(True)

            # If we can open the socket by file descriptor, then we can just
            # reuse this one and our socket will survive the restarts.
            if can_open_by_fd:
                os.environ['WERKZEUG_SERVER_FD'] = str(s.fileno())
                s.listen(LISTEN_QUEUE)
                log_startup(s)
            else:
                s.close()

        from ._reloader import run_with_reloader
        run_with_reloader(inner, extra_files, reloader_interval,
                          reloader_type)

WERKZEUG_RUN_MAIN用於判斷是否開啟了子進程,如果沒有進入子進程,就創建socket,(注釋說的是創建並關閉socket去產生所有異常在創建子進程之前)若socket可以用fd打開就保持socket打開方便重用。接着就是調用run_with_reloader了,這是相關測試代碼

def run_with_reloader(main_func, extra_files=None, interval=1,
                      reloader_type='auto'):
    """Run the given function in an independent python interpreter."""
    import signal
    reloader = reloader_loops[reloader_type](extra_files, interval)
    signal.signal(signal.SIGTERM, doit)
    try:
        if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
            t = threading.Thread(target=main_func, args=())
            t.setDaemon(True)
            t.start()
            print reloader,t
            reloader.run()
        else:
            print 'here'
            sys.exit(reloader.restart_with_reloader())
    except KeyboardInterrupt:
        pass

def doit(*args):
    print 'here im'
    sys.exit(0)

 
         
class ReloaderLoop(object):
    name = None

    # monkeypatched by testsuite. wrapping with `staticmethod` is required in
    # case time.sleep has been replaced by a non-c function (e.g. by
    # `eventlet.monkey_patch`) before we get here
    _sleep = staticmethod(time.sleep)

    def __init__(self, extra_files=None, interval=1):
        self.extra_files = set(os.path.abspath(x)
                               for x in extra_files or ())
        self.interval = interval

    def run(self):
        pass

    def restart_with_reloader(self):
        """Spawn a new Python interpreter with the same arguments as this one,
        but running the reloader thread.
        """
        while 1:
            _log('info', ' * Restarting with %s' % self.name)
            args = [sys.executable] + sys.argv
            new_environ = os.environ.copy()
            new_environ['WERKZEUG_RUN_MAIN'] = 'true'

            # a weird bug on windows. sometimes unicode strings end up in the
            # environment and subprocess.call does not like this, encode them
            # to latin1 and continue.
            print new_environ
            print args
            if os.name == 'nt' and PY2:
                for key, value in iteritems(new_environ):
                    if isinstance(value, text_type):
                        new_environ[key] = value.encode('iso-8859-1')

            exit_code = subprocess.call(args, env=new_environ,
                                        close_fds=False)
            if exit_code != 3:
                return exit_code

    def trigger_reload(self, filename):
        self.log_reload(filename)
        sys.exit(3)

    def log_reload(self, filename):
        filename = os.path.abspath(filename)
        _log('info', ' * Detected change in %r, reloading' % filename)

里面我加入了一些測試的print函數,當重啟服務器時可以看到

 

* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
here
* Restarting with stat
{'LC_NUMERIC': 'zh_CN.UTF-8', 'WERKZEUG_SERVER_FD': '3', 'XDG_GREETER_DATA_DIR': '/var/lib/lightdm-data/steinliber', 'GNOME_DESKTOP_SESSION_ID': 'this-is-deprecated', 'LC_MEASUREMENT': 'zh_CN.UTF-8', 'UPSTART_EVENTS': 'started starting', 'XDG_CURRENT_DESKTOP': 'Unity', 'LC_PAPER': 'zh_CN.UTF-8', 'LOGNAME': 'steinliber', 'XDG_SEAT': 'seat0', 'PATH': '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games', 'XDG_VTNR': '7', 'GNOME_KEYRING_CONTROL': '/run/user/1000/keyring-ZGJRph', 'ZSH': '/home/steinliber/.oh-my-zsh', 'DISPLAY': ':0', 'LANG': 'en_US.UTF-8', 'TERM': 'xterm', 'SHELL': '/usr/bin/zsh', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session0', 'XAUTHORITY': '/home/steinliber/.Xauthority', 'LANGUAGE': 'en_US', 'SHLVL': '1', 'MANDATORY_PATH': '/usr/share/gconf/ubuntu.mandatory.path', 'COMPIZ_CONFIG_PROFILE': 'ubuntu', 'UPSTART_INSTANCE': '', 'JOB': 'gnome-session', 'WINDOWID': '62921612', 'SESSIONTYPE': 'gnome-session', 'XMODIFIERS': '@im=fcitx', 'GPG_AGENT_INFO': '/run/user/1000/keyring-ZGJRph/gpg:0:1', 'HOME': '/home/steinliber', 'QT4_IM_MODULE': 'fcitx', 'SELINUX_INIT': 'YES', 'QT_QPA_PLATFORMTHEME': 'appmenu-qt5', 'XDG_RUNTIME_DIR': '/run/user/1000', 'GTK_IM_MODULE': 'fcitx', 'LC_ADDRESS': 'zh_CN.UTF-8', 'WERKZEUG_RUN_MAIN': 'true', 'SSH_AUTH_SOCK': '/run/user/1000/keyring-ZGJRph/ssh', 'VTE_VERSION': '3409', 'LC_CTYPE': 'en_US.UTF-8', 'GDMSESSION': 'ubuntu', 'UPSTART_JOB': 'unity-settings-daemon', 'UPSTART_SESSION': 'unix:abstract=/com/ubuntu/upstart-session/1000/1673', 'XDG_DATA_DIRS': '/usr/share/ubuntu:/usr/share/gnome:/usr/local/share/:/usr/share/', 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'XDG_SESSION_ID': 'c1', 'DBUS_SESSION_BUS_ADDRESS': 'unix:abstract=/tmp/dbus-8A5Emo2M1r', '_': '/usr/bin/python', 'DEFAULTS_PATH': '/usr/share/gconf/ubuntu.default.path', 'LC_IDENTIFICATION': 'zh_CN.UTF-8', 'DESKTOP_SESSION': 'ubuntu', 'LSCOLORS': 'Gxfxcxdxbxegedabagacad', 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', 'GNOME_KEYRING_PID': '1763', 'OLDPWD': '/home/steinliber', 'GDM_LANG': 'en_US', 'LC_TELEPHONE': 'zh_CN.UTF-8', 'GTK_MODULES': 'overlay-scrollbar:unity-gtk-module', 'LC_MONETARY': 'zh_CN.UTF-8', 'INSTANCE': 'Unity', 'PWD': '/home/steinliber/werkzeug', 'COLORTERM': 'gnome-terminal', 'LC_NAME': 'zh_CN.UTF-8', 'LC_TIME': 'zh_CN.UTF-8', 'LESS': '-R', 'PAGER': 'less', 'USER': 'steinliber'}
['/usr/bin/python', 'a.py']
* Debugger is active!
* Debugger pin code: 291-357-613
<werkzeug._reloader.StatReloaderLoop object at 0x7f34c55c7950> <Thread(Thread-1, started daemon 139864609179392)>

 

 

也就是說在調用run_with_reloader時因為此時並不是子進程,會調用sys.exit(reloader.restart_with_reloader())這個方法,之后就調用reloader.restart_with_reloader(),想了好久不知道為什么以及如何通過sys.exit()來調用這個方法,信號處理函數也沒觸發,哪位大牛可以幫忙解答下?

之后在restart_with_reloader中,得到了當時的環境變量以及啟動的程序名,把環境變量設為子進程運行,通過subprocess方法來創建子進程即重新運行app,在子進程中,在run_with_reloader中會創建一個線程來運行服務器函數inner(),並將該線程設為守護線程,當主線程終止時,這個進程會被強制結束,而這個子線程就負責創建服務器服務。父線程調用reloader.run(),

class StatReloaderLoop(ReloaderLoop):
    name = 'stat'

    def run(self):
        mtimes = {}
        while 1:
            for filename in chain(_iter_module_files(),
                                  self.extra_files):
                try:
                    mtime = os.stat(filename).st_mtime
                except OSError:
                    continue

                old_time = mtimes.get(filename)
                if old_time is None:
                    mtimes[filename] = mtime
                    continue
                elif mtime > old_time:
                    self.trigger_reload(filename)
            self._sleep(self.interval)

這是一個比較簡單的reloader類,基本上的作用就是每隔參數提供的秒數就檢測app所用的模塊或文件的最后修改時間是否發生了改變,如果改變,就說明文件發生了修改,就調用trigger.reloader(filename)方法。這個方法就是登記了發現的改變,然后調用sys.exit(3),在run_with_reloader中可以看到若返回的狀態碼是3,就重新循環。重新取得模塊環境以及APP名,然后再根據這些創建子進程。

這里要實現APP代碼更改后,服務器馬上根據新的配置重啟,要先創建個子進程該進程取得當時的環境變量來調用,子進程又創建了子線程來運行服務器,而主線程就負責監視相關文件的變化,若發生了改變,就退出進程,子進程是守護進程也馬上結束。主進程接受到狀態碼若為3,就重新進入循環,講的有點繞,我的理解基本就是這樣。

搞不明白的就是為什么要用sys.exit()來調用函數,程序中也沒地方捕獲這個異常啊以及那個信號處理函數的作用,兩者有關系嗎有大牛幫忙解答一下嗎

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM