Skip to content

Quart development reloader may relinquish control of terminal to bash on windows when reloading #441

@tmijieux

Description

@tmijieux

I use quart reloader in a "git bash" windows from git for windows.
When quart reloads the bash process take control again of the terminal. Further presses of CTRL+C keystrokes does not stop the process. Quart continue running in background and writting in the terminal.


Contrary to the werkzeug reloader used in flask, the reloader in quart use execve at the end to restart the process.

On windows execve does not behave exactly like on Unix.

python documentations says
"These functions all execute a new program, replacing the current process; they do not return. On Unix, the new executable is loaded into the current process, and will have the same process id as the caller."

But on unix the exec family function actually call the CreateProcess system calls with create a new process (different from the caller ) and then exits, My understanding is that it is roughly equivalent on unix by fork followed by exec on the new process and "detach" new process from parent to ignore signal that would kill the child upon parent termination, and then the parent exits.


I understand that the implementation with execve is better on unix because contrary to the flask reloader, it prevent having the python process loaded twice into memory, but this behavior is undesirable for me because because it makes it less practical to develop with quart.

I currently use the following work around inspired from flask reloader implementation (that involves two process running at the same time, with a master process and a subprocess):

import sys
import logging
import subprocess
import os
import quart.utils
from quart import Quart


def is_running_in_reloader() -> bool:
    return os.environ.get("QUART_RUNNING_IN_RELOADER", "") == "yes"


def _get_args_for_reloading() -> list[str]:
    """Determine how the script was executed, and return the args needed
    to execute it again in a new process.
    """
    return [sys.executable, *sys.orig_argv[1:]]


def my_windows_reloader(app: Quart, **kwargs):

    if "use_reloader" in kwargs:
        raise Exception("cannot specify use_reloader in my_quart_reloader()")

    if is_running_in_reloader():

        def exit_with_code_3() -> None:
            exit(3)

        # Here I Monkey patch the quart module to change its behavior and skip the execve
        setattr(quart.utils, "restart", exit_with_code_3)
        setattr(quart.app, "restart", exit_with_code_3)

        return app.run(**kwargs, use_reloader=True)
    else:
        while True:
            logging.info(" * Restarting ")
            args = _get_args_for_reloading()
            new_environ = os.environ.copy()
            new_environ["QUART_RUNNING_IN_RELOADER"] = "yes"
            exit_code = subprocess.call(args, env=new_environ, close_fds=False)
            print("exit_code=", exit_code)

            if exit_code != 3:
                return exit_code

def my_quart_reloader(app: Quart, **kwargs):
    if os.name == "nt":
        my_windows_reloader(app, **kwargs)
    else:
        app.run(**kwargs, use_reloader=True)

instead of app.run(.**kwargs) i use my_quart_reloader(app, **kwargs)

I was wondering if you would be willing to implement such a strategy on windows and have this directly built in in the run() method ? if you think this would be an undesirable breaking change, an intersting idea could be to have this as an alternative strategy with a new keyword argument like reloader_use_subprocess=False

I am willing to propose a pull request if you are interested in this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions