Repository: marcelotduarte/cx_Freeze (python)

Issue:
hooks: improve multiprocessing hook to work with pytorch
Fixes #2376 

Version 7.1.0 (and 7.1.0.post0) break FastAPI/hypercorn
**Describe the bug**
When running a program built with cx_freeze starting version 7.1.0, it adds additional arguments, which hypercorn does not accept. These arguments seem to be related to multiprocessing integration.

This causes programs built with 7.1.0 or up, to no longer be functional.

A temporary workaround is to downgrade back to 7.0.0.

**To Reproduce**
All that is required is to run a hypercorn/FastAPI server (or it seems anything that uses multiprocessing) built with cx_Freeze on version 7.0.0 or 7.1.0.post0 (and also 7.1.0 was affected).

I added a full example to a reproduction repository:
https://github.com/Daniel-I-Am/cx_freeze-fastapi-repro

**Expected behavior**
I would expect programs that can be built with 7.0.0 to also be able to be built with 7.1.0.

**Desktop (please complete the following information):**
 - Platform information: Docker `python:3.12-alpine` on linux
 - OS architecture: amd64
 - cx_Freeze version: 7.1.0.post0
 - Python version: 3.12



Discussion:
## [Codecov](https://app.codecov.io/gh/marcelotduarte/cx_Freeze/pull/2382?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcelo+Duarte) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 83.29%. Comparing base [(`cde0912`)](https://app.codecov.io/gh/marcelotduarte/cx_Freeze/commit/cde09120951af0c22f68ff81d1b8124127f920b7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcelo+Duarte) to head [(`6e97ef6`)](https://app.codecov.io/gh/marcelotduarte/cx_Freeze/pull/2382?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcelo+Duarte).


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #2382   +/-   ##
=======================================
  Coverage   83.29%   83.29%           
=======================================
  Files          27       27           
  Lines        4058     4058           
=======================================
  Hits         3380     3380           
  Misses        678      678           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/marcelotduarte/cx_Freeze/pull/2382?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcelo+Duarte).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcelo+Duarte).

```python
from multiprocessing import Process, freeze_support


def f():
    print("Hello from cx_Freeze")


if __name__ == "__main__":
    freeze_support()
    Process(target=f).start()
```

Potentially related broken case on macos without fastapi
@Daniel-I-Am I confirm the bug. I'll work on it.
@ntindle Can you check if #2435 is more related to your use case?
I think you're right that the issue you linked is more related

Patch:
diff --git a/cx_Freeze/hooks/multiprocessing.py b/cx_Freeze/hooks/multiprocessing.py
--- a/cx_Freeze/hooks/multiprocessing.py
+++ b/cx_Freeze/hooks/multiprocessing.py
@@ -43,16 +43,71 @@ def load_multiprocessing(finder: ModuleFinder, module: Module) -> None:
             sys.exit()
     # workaround: inject freeze_support call to avoid an infinite loop
     from multiprocessing.spawn import freeze_support as _spawn_freeze_support
-    from multiprocessing.context import BaseContext
-    BaseContext._get_context = BaseContext.get_context
-    def _get_freeze_context(self, method=None):
-        ctx = self._get_context(method)
-        _spawn_freeze_support()
-        return ctx
-    BaseContext.get_context = \
-        lambda self, method=None: _get_freeze_context(self, method)
-    # disable freeze_support, because it cannot be run twice
-    BaseContext.freeze_support = lambda self: None
+    from multiprocessing.spawn import is_forking as _spawn_is_forking
+    from multiprocessing.context import BaseContext, DefaultContext
+    BaseContext.freeze_support = lambda self: _spawn_freeze_support()
+    DefaultContext.freeze_support = lambda self: _spawn_freeze_support()
+    if _spawn_is_forking(sys.argv):
+        main_module = sys.modules["__main__"]
+        main_spec = main_module.__spec__
+        main_code = main_spec.loader.get_code(main_spec.name)
+        _names = main_code.co_names
+        del main_module, main_spec, main_code
+        if "freeze_support" not in _names:
+            import BUILD_CONSTANTS as _contants
+            _ignore = getattr(_contants, "ignore_freeze_support_message", 0)
+            if not _ignore:
+                print(
+    '''
+        An attempt has been made to start a new process before the
+        current process has finished its bootstrapping phase.
+
+        This probably means that you are not using fork to start your
+        child processes and you have forgotten to use the proper idiom
+        in the main module:
+
+            if __name__ == "__main__":
+                freeze_support()
+                ...
+
+        To fix this issue, or to hide this message, refer to the documentation:
+            \
+    https://cx-freeze.readthedocs.io/en/stable/faq.html#multiprocessing-support
+    ''', file=sys.stderr)
+            #import os, signal
+            #os.kill(os.getppid(), signal.SIGHUP)
+            #sys.exit(os.EX_SOFTWARE)
+            _spawn_freeze_support()
+    # cx_Freeze patch end
+    """
+    code_string = module.file.read_text(encoding="utf_8") + dedent(source)
+    module.code = compile(
+        code_string,
+        module.file.as_posix(),
+        "exec",
+        dont_inherit=True,
+        optimize=finder.optimize,
+    )
+
+
+def load_multiprocessing_context(finder: ModuleFinder, module: Module) -> None:
+    """Monkeypath get_context to do automatic freeze_support."""
+    if IS_MINGW or IS_WINDOWS:
+        return
+    if module.file.suffix == ".pyc":  # source unavailable
+        return
+    source = r"""
+    # cx_Freeze patch start
+    BaseContext._get_base_context = BaseContext.get_context
+    def _get_base_context(self, method=None):
+        self.freeze_support()
+        return self._get_base_context(method)
+    BaseContext.get_context = _get_base_context
+    DefaultContext._get_default_context = DefaultContext.get_context
+    def _get_default_context(self, method=None):
+        self.freeze_support()
+        return self._get_default_context(method)
+    DefaultContext.get_context = _get_default_context
     # cx_Freeze patch end
     """
     code_string = module.file.read_text(encoding="utf_8") + dedent(source)


Test patch:
diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py
--- a/tests/test_multiprocessing.py
+++ b/tests/test_multiprocessing.py
@@ -21,14 +21,13 @@
 
 SOURCE = """\
 sample1.py
-    import multiprocessing, sys
+    import multiprocessing
 
     def foo(q):
-        q.put('hello')
+        q.put("Hello from cx_Freeze")
 
-    if __name__ == '__main__':
-        if sys.platform == 'win32':  # the conditional is unecessary
-            multiprocessing.freeze_support()
+    if __name__ == "__main__":
+        multiprocessing.freeze_support()
         multiprocessing.set_start_method('spawn')
         q = multiprocessing.SimpleQueue()
         p = multiprocessing.Process(target=foo, args=(q,))
@@ -36,25 +35,23 @@ def foo(q):
         print(q.get())
         p.join()
 sample2.py
-    import multiprocessing, sys
+    import multiprocessing
 
     def foo(q):
-        q.put('hello')
+        q.put("Hello from cx_Freeze")
 
-    if __name__ == '__main__':
+    if __name__ == "__main__":
         ctx = multiprocessing.get_context('spawn')
-        if sys.platform == 'win32':  # the conditional is unecessary
-            ctx.freeze_support()
+        ctx.freeze_support()
         q = ctx.Queue()
         p = ctx.Process(target=foo, args=(q,))
         p.start()
         print(q.get())
         p.join()
 sample3.py
-    if __name__ ==  "__main__":
+    if __name__ == "__main__":
         import multiprocessing, sys
-        if sys.platform == 'win32':  # the conditional is unecessary
-            multiprocessing.freeze_support()
+        multiprocessing.freeze_support()
         multiprocessing.set_start_method('spawn')
         mgr = multiprocessing.Manager()
         var = [1] * 10000000
@@ -80,18 +77,22 @@ def foo(q):
         }
     )
 """
-EXPECTED_OUTPUT = ["hello", "hello", "creating dict...done!"]
+EXPECTED_OUTPUT = [
+    "Hello from cx_Freeze",
+    "Hello from cx_Freeze",
+    "creating dict...done!",
+]
 
 
 def _parameters_data() -> Iterator:
     methods = mp.get_all_start_methods()
     for method in methods:
         source = SOURCE.replace("('spawn')", f"('{method}')")
-        for i, expected in enumerate(EXPECTED_OUTPUT):
+        for i, expected in enumerate(EXPECTED_OUTPUT, 1):
             if method == "forkserver" and i != 3:
                 continue  # only sample3 works with forkserver method
-            sample = f"sample{i+1}"
-            test_id = f"{sample},{method}"
+            sample = f"sample{i}"
+            test_id = f"{sample}-{method}"
             yield pytest.param(source, sample, expected, id=test_id)