Repository: agronholm/anyio

Issue:
run_process fails when running from .exe console_script entrypoint on windows
### Things to check first

- [X] I have searched the existing issues and didn't find my bug already reported there

- [X] I have checked that my bug is still present in the latest release


### AnyIO version

4.3.0

### Python version

3.11.3

### What happened?

running:
```python
import anyio.to_process

from fastapi import FastAPI


app = FastAPI()

def sync_example():
    return 1

@app.get("/")
async def example() -> int:
    return await anyio.to_process.run_sync(sync_example)
```

with `uvicorn demo:app` fails with:

```pytb
Traceback (most recent call last):
  File "C:\Users\User\uvicron_demo\Lib\site-packages\anyio\to_process.py", line 163, in run_sync
    await send_raw_command(pickled)
  File "C:\Users\User\uvicron_demo\Lib\site-packages\anyio\to_process.py", line 89, in send_raw_command
    raise retval
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\User\\uvicron_demo\\Scripts\\uvicorn.exe\\__main__.py'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "C:\Users\User\uvicron_demo\Lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 412, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\uvicron_demo\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 84, in __call__
    return await self.app(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\uvicron_demo\Lib\site-packages\fastapi\applications.py", line 1054, in __call__
    await super().__call__(scope, receive, send)
  File "C:\Users\User\uvicron_demo\Lib\site-packages\starlette\applications.py", line 123, in __call__
    await self.middleware_stack(scope, receive, send)
  File "C:\Users\User\uvicron_demo\Lib\site-packages\starlette\middleware\errors.py", line 186, in __call__
    raise exc
  File "C:\Users\User\uvicron_demo\Lib\site-packages\starlette\middleware\errors.py", line 164, in __call__
    await self.app(scope, receive, _send)
  File "C:\Users\User\uvicron_demo\Lib\site-packages\starlette\middleware\exceptions.py", line 62, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File "C:\Users\User\uvicron_demo\Lib\site-packages\starlette\_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "C:\Users\User\uvicron_demo\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "C:\Users\User\uvicron_demo\Lib\site-packages\starlette\routing.py", line 758, in __call__
    await self.middleware_stack(scope, receive, send)
  File "C:\Users\User\uvicron_demo\Lib\site-packages\starlette\routing.py", line 778, in app
    await route.handle(scope, receive, send)
  File "C:\Users\User\uvicron_demo\Lib\site-packages\starlette\routing.py", line 299, in handle
    await self.app(scope, receive, send)
  File "C:\Users\User\uvicron_demo\Lib\site-packages\starlette\routing.py", line 79, in app
    await wrap_app_handling_exceptions(app, request)(scope, receive, send)
  File "C:\Users\User\uvicron_demo\Lib\site-packages\starlette\_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "C:\Users\User\uvicron_demo\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "C:\Users\User\uvicron_demo\Lib\site-packages\starlette\routing.py", line 74, in app
    response = await func(request)
               ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\uvicron_demo\Lib\site-packages\fastapi\routing.py", line 278, in app
    raw_response = await run_endpoint_function(
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\uvicron_demo\Lib\site-packages\fastapi\routing.py", line 191, in run_endpoint_function
    return await dependant.call(**values)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\demo.py", line 13, in example
    return await anyio.to_process.run_sync(sync_example)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\User\uvicron_demo\Lib\site-packages\anyio\to_process.py", line 168, in run_sync
    raise BrokenWorkerProcess(
anyio.BrokenWorkerProcess: Error during worker process initialization
```

### How can we reproduce the bug?

see above

Discussion:

Patch:
diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst
index f157acc..6904718 100644
--- a/docs/versionhistory.rst
+++ b/docs/versionhistory.rst
@@ -9,6 +9,9 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
   in ``anyio.Path``, newly added in Python 3.13
 - Changed the ``ResourceWarning`` from an unclosed memory object stream to include its
   address for easier identification
+- Fixed ``to_process.run_sync()`` failing to initialize if ``__main__.__file__`` pointed
+  to a file in a nonexistent directory
+  (`#696 <https://github.com/agronholm/anyio/issues/696>`_)
 
 **4.4.0**
 
diff --git a/src/anyio/to_process.py b/src/anyio/to_process.py
index 1ff06f0..5050dee 100644
--- a/src/anyio/to_process.py
+++ b/src/anyio/to_process.py
@@ -223,7 +223,7 @@ def process_worker() -> None:
                 main_module_path: str | None
                 sys.path, main_module_path = args
                 del sys.modules["__main__"]
-                if main_module_path:
+                if main_module_path and os.path.isfile(main_module_path):
                     # Load the parent's main module but as __mp_main__ instead of
                     # __main__ (like multiprocessing does) to avoid infinite recursion
                     try:
@@ -234,7 +234,6 @@ def process_worker() -> None:
                             sys.modules["__main__"] = main
                     except BaseException as exc:
                         exception = exc
-
         try:
             if exception is not None:
                 status = b"EXCEPTION"


Test patch:
diff --git a/tests/test_to_process.py b/tests/test_to_process.py
index d944216..2853fb0 100644
--- a/tests/test_to_process.py
+++ b/tests/test_to_process.py
@@ -4,9 +4,11 @@ import os
 import sys
 import time
 from functools import partial
+from pathlib import Path
 from unittest.mock import Mock
 
 import pytest
+from pytest import MonkeyPatch
 
 from anyio import (
     CancelScope,
@@ -113,3 +115,17 @@ async def test_exec_while_pruning() -> None:
         assert idle_workers[0][0] is real_worker
     finally:
         workers.discard(fake_idle_process)
+
+
+async def test_nonexistent_main_module(
+    monkeypatch: MonkeyPatch, tmp_path: Path
+) -> None:
+    """
+    Test that worker process creation won't fail if the detected path to the `__main__`
+    module doesn't exist. Regression test for #696.
+    """
+
+    script_path = tmp_path / "badscript"
+    script_path.touch()
+    monkeypatch.setattr("__main__.__file__", str(script_path / "__main__.py"))
+    await to_process.run_sync(os.getpid)