Repository: pyinstaller/pyinstaller

Issue:
Hanging at Extra DLL search directories (PATH)
## Description of the issue

Hanging on `INFO: Extra DLL search directories (PATH)`

Line of code which is hanging: https://github.com/pyinstaller/pyinstaller/blob/develop/PyInstaller/isolated/_parent.py#L233

possible solution

```python
        try:
            self._child.wait(timeout=5)
        except subprocess.TimeoutExpired:
            self._child.kill()
```

### Context information (for bug reports)

* Output of `pyinstaller --version`: ```5.6.2```
* Version of Python: 3.10.8
* Platform: Windows 10
* How you installed Python: python.org/downloads
* Did you also try this on another platform? No



### Make sure [everything is packaged correctly](https://github.com/pyinstaller/pyinstaller/wiki/How-to-Report-Bugs#make-sure-everything-is-packaged-correctly)

  * [x] start with clean installation
  * [ ] use the latest development version
  * [x] Run your frozen program **from a command window (shell)** — instead of double-clicking on it
  * [ ] Package your program in **--onedir mode**
  * [ ] Package **without UPX**, say: use the option `--noupx` or set `upx=False` in your .spec-file
  * [x] Repackage you application in **verbose/debug mode**. For this, pass the option `--debug` to `pyi-makespec` or `pyinstaller` or use `EXE(..., debug=1, ...)` in your .spec file.


### A minimal example program which shows the error

don't have it yet

### Stacktrace / full error message

No error message generated, hanging at

```
2082 INFO: Extra DLL search directories (PATH):
```
Stuck on "Extra DLL search directories (PATH)"

When I try to pack my Python project to a .exe it gets stuck on that message.

1768 INFO: Extra DLL search directories (PATH): ['C:\\Users\\jkobg\\Documents\\NeoCoding\\NeoRaffle_3.10\\venv\\Scripts', 'C:\\WINDOWS\\system32', 'C:\\WINDOWS', 'C:\\WINDOWS\\System32\\Wbem', 'C:\\WINDOWS\\System32\\WindowsPowerShell\
\v1.0\\', 'C:\\WINDOWS\\System32\\OpenSSH\\', 'C:\\Program Files\\Git\\cmd', 'C:\\Program Files\\nodejs\\', 'C:\\ProgramData\\chocolatey\\bin', 'C:\\Users\\jkobg\\AppData\\Local\\Android\\Sdk\\platform-tools\\', 'C:\\Users\\jkobg\\AppD
ata\\Local\\Android\\Sdk\\emulator', 'C:\\Program Files\\OpenSSL-Win64\\bin', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python38', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python38\\Scripts', 'C:\\Users\\jkobg\\A
ppData\\Local\\Microsoft\\WindowsApps', 'C:\\Users\\jkobg\\AppData\\Roaming\\npm', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Microsoft VS Code\\bin', 'C:\\Program Files\\mitmproxy\\bin', 'C:\\Users\\jkobg\\AppData\\Local\\Android\\Sdk\\platform-tools\\', 'C:\\Users\\jkobg\\AppData\\Local\\Android\\Sdk\\emulator', 'C:\\OpenSSL-Win64\\bin\\openssl.cfg', 'C:\\Program Files\\OpenSSL-Win64\\bin']


### Context information (for bug reports)

* Output of `pyinstaller --version`: 5.6.2
* Version of Python: 3.10.1
* Platform: Windows 11 and MacOS 16
* How you installed Python: python.org/downloads
* Did you also try this on another platform? Does it work there? No tried on Windows 10 and MacOS



Discussion:
Can you consistently reproduce this hang-up? I.e., does it occur every time you try to build your program, or just sometimes? If it is consistent, could you provide a minimal reproducer - I imagine a script that just imports all 3rd party packages that your real program uses should suffice?
Duplicate of #7269. But you've managed to pinpoint exactly where in the isolation process it's getting stuck so this one big step further than we ever got on #7269. Even though a brute force `self._child.kill()` would harmless in this case, I'd still like at least try to find out why it's getting stuck before injecting timeouts and kills. If you add `print(function, args, kwargs)` [here](https://github.com/pyinstaller/pyinstaller/blob/40f7eff90af08ef2b0ebb9cdbc69e005114c533b/PyInstaller/isolated/_parent.py#L249), does that give you a reproducer which you can then get to hang jus by running `from PyInstaller import isolated; isolated.call(function, *args, **kwargs)` from a terminal?
Issue is consistent.  However it's very hard to find the minimal program as
my program has many modules(100s).  Will take a lot of time to narrow it
down.

Replacing(patching) child.wait() call in _parent.Python class fixes the
issue.  And the resulting binary file works fine.

Subprocess docs have something about .wait() call deadlocks when using
pipes. Probably related to that.


On Tue, Nov 29, 2022, 04:30 Rok Mandeljc ***@***.***> wrote:

> Can you consistently reproduce this hang-up? I.e., does it occur every
> time you try to build your program, or just sometimes? If it is consistent,
> could you provide a minimal reproducer - I imagine a script that just
> imports all 3rd party packages that your real program uses should suffice?
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/pyinstaller/pyinstaller/issues/7290#issuecomment-1330415010>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABHLCL26QN55KT7LK4XJPPDWKXLLPANCNFSM6AAAAAASN7EBOI>
> .
> You are receiving this because you authored the thread.Message ID:
> ***@***.***>
>

> Issue is consistent. However it's very hard to find the minimal program as my program has many modules(100s). Will take a lot of time to narrow it down.

Do you have a `requirements.txt` file for your program, then? Or perhaps you can share you `build/<name>/Analysis-00.toc` file and we can figure out the main imports/packages from that...

> Replacing(patching) child.wait() call in _parent.Python class fixes the issue.  And the resulting binary file works fine.

Yeah, but we'd like to know why the issue occurs in the first place and fix the underlying cause, if possible.
Please find attached `requirements.txt` and `Analysis-00.toc` (updated to mask private info).

[files.zip](https://github.com/pyinstaller/pyinstaller/files/10115627/files.zip)

Please note that `Analysis-00.toc` is from the last successful run (patched `Python` class)

One more thing I noticed during testing.

`self._write_handle.write(b"\n")` succeeds sending the new line to the `_child.py`. And `_child.py` actually exits (the code under `if __name__ == '__main__':` finishes its execution - confirmed with bunch of print statements.

So the only possible explanation is some kind of race condition with `child.wait()`

Documentation talks about something related to deadlock:

![image](https://user-images.githubusercontent.com/5157167/204602610-9b4abc56-831f-4332-9507-ac858742f036.png)

Seems like it might be related to the **amount** of data being sent to/from the child process.
Thanks for the files! I'll see if I can reproduce this and see what's going on (although the main question, whether the child receives the EOF line and supposedly exists, has already been answered by your analysis).

> Documentation talks about something related to deadlock:

We are not using `stdout=PIPE` nor `stderr=PIPE`, though. Nor should at this point any more data be transferred through *any* of the pipes, really.
Hmm, so even if with all packages imported in the test script, I cannot reproduce this...

@qalexma While you're able to consistently reproduce the problem, can you please check two more things?

1) You say that the code in `_child.py` reaches the end - do you also see the actual process exit, for example in Task Manager? (If you instrument `_parent.py` to print the child's Process ID, do you still see that process in Task Manager when the hang-up occurs?)

2) If you add the timeout to `self._child.wait()` as you suggested, but then instead of trying to kill the process, you do a `self._child.poll()` to check its state, does this report that the process is still running (i.e., returns `None`) or does it report to be finished?
So turns out here is what's happening. `isolated.call()` is creating a `_child.py` and passing a function to it. Seems like this function sometimes can be the whole program under analysis. In my case that program was creating threads, and one of them was `daemon = False` which means closing the main program will not stop the thread. So that is what was blocking `_child.py` from exiting. Figured this out by adding 

```python
import threading
for thread in threading.enumerate(): 
    print(thread.name)
```

at the end of the `_child.py`. It became apparent that 2 threads were still running when `_child.py` was supposed to close.

adding `daemon = True` to the thread instantiation (in the program which is being parsed by pyinstaller) fixes the issue. However that's not necessarily what is always the desired option.

I guess the question is how to make `_child.py` more robust for such source code patterns (where threads are created without daemon=True)
Nice detective work there!

The child sub-process in question,

https://github.com/pyinstaller/pyinstaller/blob/d46744f487035aee97f27966847f9a88f209b840/PyInstaller/building/build_main.py#L690

runs this `find_binary_dependencies` function:

https://github.com/pyinstaller/pyinstaller/blob/d46744f487035aee97f27966847f9a88f209b840/PyInstaller/building/build_main.py#L127-L192

This function first attempts to import all top-level modules and packages used by your program (in case their initialization adjusts `PATH` or adds extra DLL search directories via `os.add_dll_directory`), and then runs the binary dependency analysis on the collected binaries (i.e., tries to resolve the DLLs that they are linked against).

So to clarify the situation, does some top-level package or module that is part of your program perform automatic thread instantiation during its import phase, as opposed to "on-demand" by explicitly calling a function from your program? That's the only scenario I can imagine where this dependency analysis sub-process will end up running arbitrary user code.

Yes exactly this thread instantiation is executed during module imports.
And the way it is instantiated doesn't allow it to stop automatically when
the main program stops (non-daemon thread).

When i adjusted it to be daemon thread (meaning it will be killed when main
program stops), the hanging of the _child.py is not observed.

On Wed, Nov 30, 2022, 04:00 Rok Mandeljc ***@***.***> wrote:

> Nice detective work there!
>
> The child sub-process in question,
>
>
> https://github.com/pyinstaller/pyinstaller/blob/d46744f487035aee97f27966847f9a88f209b840/PyInstaller/building/build_main.py#L690
>
> runs this find_binary_dependencies function:
>
>
> https://github.com/pyinstaller/pyinstaller/blob/d46744f487035aee97f27966847f9a88f209b840/PyInstaller/building/build_main.py#L127-L192
>
> This function first attempts to import all top-level modules and packages
> used by your program (in case their initialization adjusts PATH or adds
> extra DLL search directories via os.add_dll_directory), and then runs the
> binary dependency analysis on the collected binaries (i.e., tries to
> resolve the DLLs that they are linked against).
>
> So to clarify the situation, does some top-level package or module that is
> part of your program perform automatic thread instantiation during its
> import phase, as opposed to "on-demand" by explicitly calling a function
> from your program? That's the only scenario I can imagine where this
> dependency analysis sub-process will end up running arbitrary user code.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/pyinstaller/pyinstaller/issues/7290#issuecomment-1331904571>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABHLCLYYPNRDIP5CFH3K4M3WK4QVDANCNFSM6AAAAAASN7EBOI>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

OK, great, this clears up the failure mode and its underlying cause, and also explains why we haven't been able to reproduce the problem on our side.

I'd argue that having modules whose import causes side effects like that is a questionable design choice, and it affects other areas of analysis where module may end up imported (e.g., when analyzing a submodule of a package, importing the package itself is unavoidable). As extreme examples, the module popping up a dialog, or running a system-restart command come to mind...

I can't really think of any sane defense against such arbitrary code execution.

In this particular case, I'm now leaning towards adding the timeout for that final wait in parent, and attempting to kill the subprocess (and then wait again for the sake of formality, possibly with timeout again). As well as displaying a warning (or raising an error if we have strict build mode enabled, to catch potential occurrences of this on our CI).

Are we importing the top level script in this analysis? I don't really expect everyone to be disciplined enough to wrap their top level code in `if __name__ == "__main__"` guards.
As far as I know, the entry-point script itself is not imported. If it were, UI-based scripts without the `__main__` guard would end up popping their UI during analysis and block it...
so what's the next step, do you want me to add the timeout/kill. What is the process exactly
I think so. It's not due to some horrible but fixable Windows API/pipe deadlocking like I had previously expected. And it's not going to truncate output and miss data since it's not getting killed until all data is already sent back.

One last option would be to call `os._exit(0)` right at the end of `PyInstaller.isolated._child`. That at least cuts through threads where `sys.exit()` doesn't.
Yeah, let's go with timeout + kill, now that we know that we are not trying to mask a pipe deadlock problem. If you feel like putting together a PR, go ahead, otherwise we'll pick this up in the upcoming days.
how do I go about creating a PR? Reading the [docs](https://pyinstaller.org/en/stable/development/pull-requests.html) it is saying I will need to create a fork of the whole project first. Is that actually necessary?

Let me know if that doc is the correct one to follow.
Yes that's correct. Forking is the easy bit though, just press the Fork button at the top right of https://github.com/pyinstaller/pyinstaller. If you're not familiar with git and GitHub though it's probably easier just to leave it to us.
What if you run with `--log-level DEBUG` - do you see any messages coming after that one?
No
I already waited for 30+ min and nothing happened

Well, this is something specific to either your dependencies or your environment. So you'll have to debug it on your own.

The `INFO: Extra DLL search directories (PATH)` is generated within this call to `find_binary_dependencies`:

https://github.com/pyinstaller/pyinstaller/blob/e83c6029d09fe1dc77509f969a41b16f3802c01f/PyInstaller/building/build_main.py#L685-L693

So if you see "Looking for dynamic libraries" and " Extra DLL search directories (PATH)", but not "Looking for eggs", it means the process is stuck within the `find_binary_dependencies`.

The code for `find_binary_dependencies` looks like this, so if you saw "Extra DLL search directories", it means it made it to the return statement at the end:

https://github.com/pyinstaller/pyinstaller/blob/e83c6029d09fe1dc77509f969a41b16f3802c01f/PyInstaller/building/build_main.py#L126-L191

So you'll need to add some print() or logger.info() statements to `bindepend.Dependencies()` function to see where exactly it is stuck:

https://github.com/pyinstaller/pyinstaller/blob/e83c6029d09fe1dc77509f969a41b16f3802c01f/PyInstaller/depend/bindepend.py#L257-L302

Maybe for starters, add a print() or logger.info() call into the top-level loop within that function, to print each `nm, pth, typ` as they are processed.


Ok I added some prints and it actually reached the return of `bindepend.Dependencies()`
Does it make it out of the `find_binary_dependencies`? If at end of `find_binary_dependencies`, you assign return value of `bindepend.Dependencies()` to a variable, add a print, and then return the variable, do you see that message?

And similarly, if you reorganize

https://github.com/pyinstaller/pyinstaller/blob/e83c6029d09fe1dc77509f969a41b16f3802c01f/PyInstaller/building/build_main.py#L688-L690

into 
``` python
_extra_binaries = isolated.call(find_binary_dependencies, list(self.binaries), self.binding_redirects, collected_packages) 
print("EXTRA BINARY DEPENDENCIES", _extra_binaries)
self.binaries.extend(_extra_binaries)
```
do you get the output of that print?

No I don't. It is stuck before that
`self.binaries.extend( 
     isolated.call(find_binary_dependencies, list(self.binaries), self.binding_redirects, collected_packages) 
 ) `
Can you reorganize this part into 

``` 
_extra_binaries = isolated.call(find_binary_dependencies, list(self.binaries), self.binding_redirects, collected_packages) 
print("EXTRA BINARY DEPENDENCIES", _extra_binaries)
self.binaries.extend(_extra_binaries)
```

so you can determine whether it fails to return from `isolated.call` or is it stuck at `self.binaries.extend(_extra_binaries)`?
It doesnt return from the isolated.call() method
And this consistently happens on both Windows and macOS?
Yes
In that case, could you provide a reproducer so I can try reproducing it locally.?
What exactly would you need?

Ok i fixed it. I just called find_binary_dependencies() directly. Is that gonna be a problem?
> Ok i fixed it. I just called find_binary_dependencies() directly. Is that gonna be a problem?

It should not make a difference in a scenario where you build only one program. The whole isolation part is mostly there for our CI that runs several different builds within the same process...

But I would still be interested in knowing why the subprocess communication failed. So if you could simplify your program to a single script that imports all packages that your program imports (and still triggers the problem when you try to freeze it), I would like to try reproducing the problem on my systems.
Ok let me see if i can do that. Because we have many modules haha
Hmm, is the list returned by `find_binary_dependencies` very long? How many entries are there? Could you, for starters, just dump that list into a txt file so I can try loading it and see if the problem is size-related?
Its minimum like 20 or so

I don't suppose you can make it hang just by opening a console and entering something like:
```python
from PyInstaller import isolated
_ = isolated.call(lambda x: x, [100] * 10000000)
```

```
[('api-ms-win-crt-stdio-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-crt-stdio-l1-1-0.dll', 'BINARY'), ('api-ms-win-crt-heap-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-crt-heap-l1-1-0.dll', 'BINARY'), ('python310.dll', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\python310.dll', 'BINARY'), ('api-ms-win-crt-locale-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-crt-locale-l1-1-0.dll', 'BINARY'), ('VCRUNTIME140.dll', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\VCRUNTIME140.dll', 'BINARY'), ('api-ms-win-crt-math-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-crt-math-l1-1-0.dll', 'BINARY'), ('api-ms-win-crt-runtime-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-crt-runtime-l1-1-0.dll', 'BINARY'), ('ucrtbase.dll', 'C:\\Program Files\\mitmproxy\\bin\\ucrtbase.dll', 'BINARY'), ('api-ms-win-crt-convert-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-crt-convert-l1-1-0.dll', 'BINARY'), ('api-ms-win-crt-process-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-crt-process-l1-1-0.dll', 'BINARY'), ('api-ms-win-crt-filesystem-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-crt-filesystem-l1-1-0.dll', 'BINARY'), ('api-ms-win-crt-string-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-crt-string-l1-1-0.dll', 'BINARY'), ('api-ms-win-crt-environment-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-crt-environment-l1-1-0.dll', 'BINARY'), ('api-ms-win-crt-conio-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-crt-conio-l1-1-0.dll', 'BINARY'), ('api-ms-win-crt-time-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-crt-time-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-string-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-string-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-synch-l1-2-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-synch-l1-2-0.dll', 'BINARY'), ('api-ms-win-core-interlocked-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-interlocked-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-timezone-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-timezone-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-libraryloader-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-libraryloader-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-synch-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-synch-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-heap-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-heap-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-errorhandling-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-errorhandling-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-rtlsupport-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-rtlsupport-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-sysinfo-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-sysinfo-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-file-l2-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-file-l2-1-0.dll', 'BINARY'), ('api-ms-win-core-console-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-console-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-profile-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-profile-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-localization-l1-2-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-localization-l1-2-0.dll', 'BINARY'), ('api-ms-win-core-processthreads-l1-1-1.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-processthreads-l1-1-1.dll', 'BINARY'), ('api-ms-win-core-namedpipe-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-namedpipe-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-handle-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-handle-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-fibers-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-fibers-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-util-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-util-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-debug-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-debug-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-memory-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-memory-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-processenvironment-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-processenvironment-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-datetime-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-datetime-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-processthreads-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-processthreads-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-file-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-file-l1-1-0.dll', 'BINARY'), ('api-ms-win-core-file-l1-2-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-core-file-l1-2-0.dll', 'BINARY'), ('Crypto\\Cipher\\_raw_cast.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_raw_cast.pyd', 'BINARY'), ('Crypto\\Hash\\_BLAKE2b.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Hash\\_BLAKE2b.pyd', 'BINARY'), ('Crypto\\Hash\\_SHA224.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Hash\\_SHA224.pyd', 'BINARY'), ('Crypto\\Cipher\\_pkcs1_decode.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_pkcs1_decode.pyd', 'BINARY'), ('Crypto\\Cipher\\_raw_ctr.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_raw_ctr.pyd', 'BINARY'), ('Crypto\\Hash\\_MD2.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Hash\\_MD2.pyd', 'BINARY'), ('Crypto\\Hash\\_ghash_clmul.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Hash\\_ghash_clmul.pyd', 'BINARY'), ('Crypto\\Hash\\_poly1305.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Hash\\_poly1305.pyd', 'BINARY'), ('Crypto\\Hash\\_SHA256.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Hash\\_SHA256.pyd', 'BINARY'), ('Crypto\\Hash\\_SHA384.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Hash\\_SHA384.pyd', 'BINARY'), ('Crypto\\Cipher\\_raw_ecb.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_raw_ecb.pyd', 'BINARY'), ('Crypto\\Cipher\\_raw_des3.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_raw_des3.pyd', 'BINARY'), ('Crypto\\Hash\\_keccak.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Hash\\_keccak.pyd', 'BINARY'), ('Crypto\\Hash\\_RIPEMD160.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Hash\\_RIPEMD160.pyd', 'BINARY'), ('Crypto\\Cipher\\_Salsa20.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_Salsa20.pyd', 'BINARY'), ('Crypto\\Cipher\\_raw_ofb.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_raw_ofb.pyd', 'BINARY'), ('Crypto\\Cipher\\_raw_cfb.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_raw_cfb.pyd', 'BINARY'), ('Crypto\\Cipher\\_ARC4.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_ARC4.pyd', 'BINARY'), ('Crypto\\Util\\_cpuid_c.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Util\\_cpuid_c.pyd', 'BINARY'), ('Crypto\\Cipher\\_raw_blowfish.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_raw_blowfish.pyd', 'BINARY'), ('Crypto\\Hash\\_ghash_portable.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Hash\\_ghash_portable.pyd', 'BINARY'), ('Crypto\\Math\\_modexp.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Math\\_modexp.pyd', 'BINARY'), ('Crypto\\Cipher\\_chacha20.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_chacha20.pyd', 'BINARY'), ('Crypto\\Cipher\\_raw_aes.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_raw_aes.pyd', 'BINARY'), ('Crypto\\Hash\\_BLAKE2s.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Hash\\_BLAKE2s.pyd', 'BINARY'), ('Crypto\\Cipher\\_raw_arc2.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_raw_arc2.pyd', 'BINARY'), ('Crypto\\Cipher\\_raw_eksblowfish.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_raw_eksblowfish.pyd', 'BINARY'), ('Crypto\\Hash\\_MD4.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Hash\\_MD4.pyd', 'BINARY'), ('Crypto\\Hash\\_SHA512.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Hash\\_SHA512.pyd', 'BINARY'), ('Crypto\\Hash\\_MD5.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Hash\\_MD5.pyd', 'BINARY'), ('Crypto\\Cipher\\_raw_aesni.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_raw_aesni.pyd', 'BINARY'), ('Crypto\\Util\\_strxor.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Util\\_strxor.pyd', 'BINARY'), ('Crypto\\Cipher\\_raw_cbc.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_raw_cbc.pyd', 'BINARY'), ('Crypto\\Cipher\\_raw_des.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_raw_des.pyd', 'BINARY'), ('Crypto\\Protocol\\_scrypt.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Protocol\\_scrypt.pyd', 'BINARY'), ('Crypto\\Cipher\\_raw_ocb.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Cipher\\_raw_ocb.pyd', 'BINARY'), ('Crypto\\Hash\\_SHA1.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\Hash\\_SHA1.pyd', 'BINARY'), ('Crypto\\PublicKey\\_ec_ws.pyd', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\Crypto\\PublicKey\\_ec_ws.pyd', 'BINARY'), ('_decimal', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\_decimal.pyd', 'EXTENSION'), ('_lzma', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\_lzma.pyd', 'EXTENSION'), ('_bz2', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\_bz2.pyd', 'EXTENSION'), ('_hashlib', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\_hashlib.pyd', 'EXTENSION'), ('select', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\select.pyd', 'EXTENSION'), ('_socket', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\_socket.pyd', 'EXTENSION'), ('_uuid', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\_uuid.pyd', 'EXTENSION'), ('_ssl', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\_ssl.pyd', 'EXTENSION'), ('_ctypes', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\_ctypes.pyd', 'EXTENSION'), ('_queue', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\_queue.pyd', 'EXTENSION'), ('_multiprocessing', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\_multiprocessing.pyd', 'EXTENSION'), ('pyexpat', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\pyexpat.pyd', 'EXTENSION'), ('unicodedata', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\unicodedata.pyd', 'EXTENSION'), ('_brotli', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\_brotli.cp310-win_amd64.pyd', 'EXTENSION'), ('_cffi_backend', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\_cffi_backend.cp310-win_amd64.pyd', 'EXTENSION'), ('cryptography.hazmat.bindings._openssl', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\cryptography\\hazmat\\bindings\\_openssl.pyd', 'EXTENSION'), ('cryptography.hazmat.bindings._rust', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\cryptography\\hazmat\\bindings\\_rust.pyd', 'EXTENSION'), ('helheim.pytransform_vax_000061', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\helheim\\pytransform_vax_000061.pyd', 'EXTENSION'), ('_overlapped', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\_overlapped.pyd', 'EXTENSION'), ('_asyncio', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\_asyncio.pyd', 'EXTENSION'), ('_elementtree', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\_elementtree.pyd', 'EXTENSION'), ('websockets.speedups', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\websockets\\speedups.cp310-win_amd64.pyd', 'EXTENSION'), ('netifaces', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\netifaces.cp310-win_amd64.pyd', 'EXTENSION'), ('markupsafe._speedups', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\markupsafe\\_speedups.cp310-win_amd64.pyd', 'EXTENSION'), ('msgpack._cmsgpack', 'c:\\users\\jkobg\\documents\\neocoding\\neoraffle_3.10\\venv\\lib\\site-packages\\msgpack\\_cmsgpack.cp310-win_amd64.pyd', 'EXTENSION'), ('libcrypto-1_1.dll', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\libcrypto-1_1.dll', 'BINARY'), ('libssl-1_1.dll', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\libssl-1_1.dll', 'BINARY'), ('libffi-7.dll', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\libffi-7.dll', 'BINARY'), ('api-ms-win-crt-utility-l1-1-0.dll', 'C:\\Program Files\\mitmproxy\\bin\\api-ms-win-crt-utility-l1-1-0.dll', 'BINARY'), ('VCRUNTIME140_1.dll', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\VCRUNTIME140_1.dll', 'BINARY'), ('MSVCP140.dll', 'C:\\WINDOWS\\system32\\MSVCP140.dll', 'BINARY'), ('python3.dll', 'C:\\Users\\jkobg\\AppData\\Local\\Programs\\Python\\Python310\\python3.dll', 'BINARY')]
```
This is the return value.
That `C:\\Program Files\\mitmproxy\\bin` directory seems to contain its own copy of the C standard library which is discomforting. Other than that though, the list looks fairly benign.
Yeah, length-wise, that list doesn't look like it should be problematic (our CI is likely dealing with much worse)... looks like we'll need a consistent reproducer to look into this any further.
Another thing to try would be to delete [these two lines](https://github.com/pyinstaller/pyinstaller/blob/e83c6029d09fe1dc77509f969a41b16f3802c01f/PyInstaller/__main__.py#L181-L182), run again until it gets stuck then kill it with Ctrl+C. Then see what line the stack trace points to.

Resolving patch:
diff --git a/PyInstaller/isolated/_parent.py b/PyInstaller/isolated/_parent.py
--- a/PyInstaller/isolated/_parent.py
+++ b/PyInstaller/isolated/_parent.py
@@ -16,7 +16,10 @@
 import functools
 import subprocess
 
-from PyInstaller.compat import __wrap_python
+from PyInstaller import compat
+from PyInstaller import log as logging
+
+logger = logging.getLogger(__name__)
 
 # WinAPI bindings for Windows-specific codepath
 if os.name == "nt":
@@ -153,7 +156,7 @@ def child(read_from_parent: int, write_to_parent: int):
         }
 
     # Run the _child.py script directly passing it the two file descriptors it needs to talk back to the parent.
-    cmd, options = __wrap_python([str(CHILD_PY), str(read_from_parent), str(write_to_parent)], extra_kwargs)
+    cmd, options = compat.__wrap_python([str(CHILD_PY), str(read_from_parent), str(write_to_parent)], extra_kwargs)
 
     # I'm intentionally leaving stdout and stderr alone so that print() can still be used for emergency debugging and
     # unhandled errors in the child are still visible.
@@ -181,6 +184,11 @@ class Python:
     that it allows multiple functions to be evaluated in a single subprocess, making it faster than multiple calls to
     :func:`call`.
 
+    The ``strict_mode`` argument controls behavior when the child process fails to shut down; if strict mode is enabled,
+    an error is raised, otherwise only warning is logged. If the value of ``strict_mode`` is ``None``, the value of
+    ``PyInstaller.compat.strict_collect_mode`` is used (which in turn is controlled by the
+    ``PYINSTALLER_STRICT_COLLECT_MODE`` environment variable.
+
     Examples:
         To call some predefined functions ``x = foo()``, ``y = bar("numpy")`` and ``z = bazz(some_flag=True)`` all using
         the same isolated subprocess use::
@@ -191,9 +199,13 @@ class Python:
                 z = child.call(bazz, some_flag=True)
 
     """
-    def __init__(self):
+    def __init__(self, strict_mode=None):
         self._child = None
 
+        # Re-use the compat.strict_collect_mode and its PYINSTALLER_STRICT_COLLECT_MODE environment variable for
+        # default strict-mode setting.
+        self._strict_mode = strict_mode if strict_mode is not None else compat.strict_collect_mode
+
     def __enter__(self):
         # We need two pipes. One for the child to send data to the parent. The (write) end-point passed to the
         # child needs to be marked as inheritable.
@@ -230,7 +242,27 @@ def __exit__(self, exc_type, exc_val, exc_tb):
         # Send the signal (a blank line) to the child to tell it that it's time to stop.
         self._write_handle.write(b"\n")
         self._write_handle.flush()
-        self._child.wait()
+
+        # Wait for the child process to exit. The timeout is necessary for corner cases when the sub-process fails to
+        # exit (such as due to dangling non-daemon threads; see #7290). At this point, the subprocess already did all
+        # its work, so it should be safe to terminate. And as we expect it to shut down quickly (or not at all), the
+        # timeout is relatively short.
+        #
+        # In strict build mode, we raise an error when the subprocess fails to exit on its own, but do so only after
+        # we attempt to kill the subprocess, to avoid leaving zombie processes.
+        shutdown_error = False
+
+        try:
+            self._child.wait(timeout=5)
+        except subprocess.TimeoutExpired:
+            logger.warning("Timed out while waiting for the child process to exit!")
+            shutdown_error = True
+            self._child.kill()
+            try:
+                self._child.wait(timeout=15)
+            except subprocess.TimeoutExpired:
+                logger.warning("Timed out while waiting for the child process to be killed!")
+                # Give up and fall through
 
         # Close the handles. This should also close the underlying file descriptors.
         self._write_handle.close()
@@ -239,6 +271,10 @@ def __exit__(self, exc_type, exc_val, exc_tb):
 
         self._child = None
 
+        # Raise an error in strict mode, after all clean-up has been performed.
+        if shutdown_error and self._strict_mode:
+            raise RuntimeError("Timed out while waiting for the child process to exit!")
+
     def call(self, function, *args, **kwargs):
         """
         Call a function in the child Python. Retrieve its return value. Usage of this method is identical to that


Test patch:
diff --git a/tests/unit/test_isolation.py b/tests/unit/test_isolation.py
--- a/tests/unit/test_isolation.py
+++ b/tests/unit/test_isolation.py
@@ -10,6 +10,7 @@
 #-----------------------------------------------------------------------------
 
 import os
+import logging
 
 import pytest
 
@@ -256,3 +257,45 @@ def isolated_function(*args, kwarg1='default1', kwarg2='default2', kwarg3='defau
     with isolated.Python() as child:
         actual = child.call(isolated_function, kwarg1='override1', kwarg3='override3')
     assert actual == expected
+
+
+@pytest.mark.parametrize("strict_mode", [True, False], ids=['strict', 'lax'])
+def test_shutdown_timeout_dangling_threads(strict_mode, caplog):
+    """
+    Simulate the scenario from #7290, where the isolated sub-process ends up spawning a non-daemon thread, which
+    prevents the sub-process from shutting down and in turn blocks the parent process on its _child.wait() call.
+
+    This situation might arise when, as part of analysis, PyInstaller performs an isolated import of a module
+    that spawns threads as part of executable statements that are ran on the first time the module is imported.
+    """
+    def isolated_function(*args):
+        import threading
+        import time
+
+        # Simulate some long-running background task with periodic activity
+        def background_task():
+            while True:
+                time.sleep(1)
+
+        thread = threading.Thread(target=background_task, name="Test non-daemon thread", daemon=False)
+        thread.start()
+
+        return args
+
+    expected = ('a', 'b', 'c', 1, 2, 3)
+
+    if strict_mode:
+        # In strict mode, we expect an error to be raised
+        with pytest.raises(RuntimeError, match="Timed out while waiting for the child process to exit!"):
+            with isolated.Python(strict_mode=strict_mode) as child:
+                actual = child.call(isolated_function, *expected)
+    else:
+        # In lax mode, we expect a warning message
+        with caplog.at_level(logging.WARNING):
+            with isolated.Python(strict_mode=strict_mode) as child:
+                actual = child.call(isolated_function, *expected)
+
+            assert "Timed out while waiting for the child process to exit!" in caplog.text
+
+        # The isolated function should finish and return its expected results, regardless of the shutdown timeout.
+        assert actual == expected