Repository: microsoft/ptvsd

Issue:
Script to handle out-of-proc Wait on normal and abnormal exits



Discussion:

Resolving patch:
diff --git a/src/ptvsd/common/launcher.py b/src/ptvsd/common/launcher.py
new file mode 100644
--- /dev/null
+++ b/src/ptvsd/common/launcher.py
@@ -0,0 +1,77 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See LICENSE in the project root
+# for license information.
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+
+import subprocess
+import sys
+
+
+WAIT_ON_NORMAL_SWITCH = "--wait-on-normal"
+WAIT_ON_ABNORMAL_SWITCH = "--wait-on-abnormal"
+
+
+_wait_on_normal_exit = False
+_wait_on_abnormal_exit = False
+
+
+HELP = """Usage: launcher [{normal}] [{abnormal}] <args>
+python launcher.py {normal} {abnormal} -- <python args go here>
+""".format(normal=WAIT_ON_NORMAL_SWITCH, abnormal=WAIT_ON_ABNORMAL_SWITCH)
+
+
+def main(argv=sys.argv):
+    try:
+        process_args = [sys.executable] + list(parse(argv[1:]))
+    except Exception as ex:
+        print(HELP + "\nError: " + str(ex), file=sys.stderr)
+        sys.exit(2)
+
+    p = subprocess.Popen(args=process_args)
+    exit_code = p.wait()
+
+    if _wait_on_normal_exit and exit_code == 0:
+        _wait_for_user()
+    elif _wait_on_abnormal_exit and exit_code != 0:
+        _wait_for_user()
+
+    sys.exit(exit_code)
+
+
+def _wait_for_user():
+    if sys.__stdout__ and sys.__stdin__:
+        try:
+            import msvcrt
+        except ImportError:
+            sys.__stdout__.write("Press Enter to continue . . . ")
+            sys.__stdout__.flush()
+            sys.__stdin__.read(1)
+        else:
+            sys.__stdout__.write("Press any key to continue . . . ")
+            sys.__stdout__.flush()
+            msvcrt.getch()
+
+
+def parse_arg(arg):
+    if arg == WAIT_ON_NORMAL_SWITCH:
+        global _wait_on_normal_exit
+        _wait_on_normal_exit = True
+    elif arg == WAIT_ON_ABNORMAL_SWITCH:
+        global _wait_on_abnormal_exit
+        _wait_on_abnormal_exit = True
+    else:
+        raise AssertionError("Invalid argument passed to launcher.")
+
+def parse(argv):
+    it = iter(argv)
+    arg = next(it)
+    while arg != "--":
+        parse_arg(arg)
+        arg = next(it)
+    return it
+
+
+if __name__ == "__main__":
+    main()


Test patch:
diff --git a/tests/ptvsd/common/test_launcher.py b/tests/ptvsd/common/test_launcher.py
new file mode 100644
--- /dev/null
+++ b/tests/ptvsd/common/test_launcher.py
@@ -0,0 +1,128 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See LICENSE in the project root
+# for license information.
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import os.path
+import platform
+import pytest
+import subprocess
+import sys
+
+from ptvsd.common import launcher
+
+
+launcher_py = os.path.abspath(launcher.__file__)
+
+
+@pytest.mark.parametrize("run_as", ["file", "module", "code"])
+@pytest.mark.parametrize("mode", ["normal", "abnormal", "normal+abnormal", ""])
+@pytest.mark.parametrize("seperator", ["seperator", ""])
+def test_launcher_parser(mode, seperator, run_as):
+    args = []
+
+    switch = mode.split("+")
+
+    if "normal" in switch:
+        args += [launcher.WAIT_ON_NORMAL_SWITCH]
+
+    if "abnormal" in switch:
+        args += [launcher.WAIT_ON_ABNORMAL_SWITCH]
+
+    if seperator:
+        args += ["--"]
+
+    if run_as == "file":
+        expected = ["myscript.py", "--arg1", "--arg2", "--arg3", "--", "more args"]
+    elif run_as == "module":
+        expected = ["-m", "myscript", "--arg1", "--arg2", "--arg3", "--", "more args"]
+    else:
+        expected = ["-c", "some code"]
+
+    args += expected
+
+    if seperator:
+        actual = list(launcher.parse(args))
+        assert actual == expected
+    else:
+        with pytest.raises(AssertionError):
+            actual = launcher.parse(args)
+
+
+@pytest.mark.parametrize("run_as", ["file", "module", "code"])
+@pytest.mark.parametrize("mode", ["normal", "abnormal", "normal+abnormal", ""])
+@pytest.mark.parametrize("exit_code", [0, 10])
+@pytest.mark.timeout(5)
+@pytest.mark.skipif(platform.system() == "Windows", reason="Not reliable on windows.")
+def test_launcher(pyfile, mode, exit_code, run_as):
+    @pyfile
+    def code_to_run():
+        import sys
+
+        sys.exit(int(sys.argv[1]))
+
+    args = [sys.executable, launcher_py]
+
+    switch = mode.split("+")
+
+    if "normal" in switch:
+        args += [launcher.WAIT_ON_NORMAL_SWITCH]
+
+    if "abnormal" in switch:
+        args += [launcher.WAIT_ON_ABNORMAL_SWITCH]
+
+    args += ["--"]
+
+    if run_as == "file":
+        args += [code_to_run.strpath, str(exit_code)]
+    elif run_as == "module":
+        args += ["-m", "code_to_run", str(exit_code)]
+    else:
+        with open(code_to_run.strpath, "r") as f:
+            args += ["-c", f.read(), str(exit_code)]
+
+    wait_for_user = (exit_code, mode) in [
+        (0, "normal"),
+        (10, "abnormal"),
+        (0, "normal+abnormal"),
+        (10, "normal+abnormal"),
+    ]
+
+    if platform.system() == "Windows":
+        p = subprocess.Popen(
+            args=args,
+            cwd=os.path.dirname(code_to_run.strpath),
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            # CREATE_NEW_CONSOLE is needed other wise you cannot write to stdin.
+            creationflags=subprocess.CREATE_NEW_CONSOLE,
+        )
+        # NOTE: We disabled this test on windows because there is no
+        # reliable way to write to stdin without going though the Win32
+        # WriteConsoleInput.
+    else:
+        p = subprocess.Popen(
+            args=args,
+            cwd=os.path.dirname(code_to_run.strpath),
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+        )
+
+    if wait_for_user:
+        outstr = b""
+        while not outstr.endswith(b". . . "):
+            outstr += p.stdout.read(1)
+
+        while p.poll() is None:
+            try:
+                p.stdin.write(b"\n")
+                p.stdin.flush()
+            except BrokenPipeError:
+                # This can occur if the console exited before 
+                # flush was called.
+                pass
+    else:
+        p.wait()
+
+    assert exit_code == p.returncode