You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Jan 15, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Jan 15, 2026. It is now read-only.
It seems polars 1.15 installs a hook to warn the user about possible deadlocks when using multiprocessing.Pool() with the fork method (default on Unix platforms). However, that hook triggers every time the controls goes back to the Python interpreter after a fork, which unfortunately happens when using the preexec_fn parameter of subprocess.Popen(), which we do.
Do we actually need these preexec_fn ?
defpreexec_function():
# Change process group in case we have to kill the subprocess and all of# its children later.# TODO: this is Unix-specific; would be good to find an OS-agnostic way# to do this in case we wanna port WA to Windows.os.setpgrp()
We very much need to allow having threads in an application using devlib.
Also, the following note provides a possible fix:
Note
If you need to modify the environment for the child use the env parameter rather than doing it in a preexec_fn. The start_new_session and process_group parameters should take the place of code using preexec_fn to call os.setsid() or os.setpgid() in the child.
In practice from my experimentation, processes are not killed upon Popen garbage collection. However, if the parent process exits due to an exception (e.g. KeyboardInterrupt), then the processes spawned by Popen seem to be terminated, even if the process outlived its Popen handler in the Python world. If the parent process is killed (so exits not via an exception), then the children outlive the parent process.
So to summarize:
I don't think the advantages of calling os.setpgrp() as described in preexec_fn comment justifies the risk of undefined behavior outlined in the Python doc, especially when correct resource control on the Python side (e.g. context managers) can allow clean handling in cooperative requests (KeyboardInterrupt).
In practice, this conflicts with polars. It can be argued that polars should not install that handler, but really the problem is that Python has a broken default that lets no-one win. This will change in 3.14, but until then we have to deal with it.
So I would suggest we simply remove those preexec_fn without replacement.
We faced an issue when using devlib adb background connection in a tool we are developing.
In the tool, we are creating AndroidTarget, and spawning background command -
'
The command being run is perf stat on the target with arguments corresponding to the different cpu types on the target.
Then, we wait for user to press CTRL + C.
With the exisiting AdbConnection, we are seeing that the CTRL+C pressed is getting intercepted by the adb connection and it gets terminated. We actually have a handling logic that is supposed to terminate the perf using SIGINT, but because of the process intercepting the CTRL+C, that path is not getting taken, and the process has already closed without flushing the data we need.
Adding a preexec function in subprocess.Popen command in android.py of devlib helps to fix the problem. p: 'Popen' = subprocess.Popen(full_command, stdout=stdout, stderr=stderr, stdin=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp)
Need to see if this is an acceptable generic fix or there needs to be other considerations.
Without session:
sh(6817)---sleep(6819)
With session:
sh(6818)---sleep(6821)
^C
Without session:
sh(6817)
With session:
sh(6818)---sleep(6821)
When start_new_session is not set (or False), the sleep command intercepts SIGINT and exits. However, the shell (sh) itself remains alive since it doesn't exit upon receiving SIGINT.
As noted by Arjun, this behavior impacts adb_background_shell because adb intercepts SIGINT but does not propagate it to the remote process (see: source code).
Depending of the use case the code using devlib may want to forward the signal to background processes or block it. To handle properly each use cases, the best option is to let the user do it explicitly which requires to not propagate the signal automatically to the background application (which may just be a proxy...).
Therefore we do need these os.setpgrp or call Popen with start_new_session=True.
To handle this correctly and maintain compatibility, I propose:
Use start_new_session=True for Python 3.11 and later.
Continue to use preexec_fn=os.setpgrp for backward compatibility with older Python versions.
Align the behavior of the AdbConnection background command with the functionality provided by both LocalConnection and SSHConnection.
I just realized start_new_session was actually added in 3.2, only process_group was added in 3.11 so if we only need the former, we are good.
For anything like Target.execute(), I don't have strong feelings on whether we should dispatch SIGINT or not to the process as long as an target.execute("sleep 24h") command can be interrupted. I'd assume anything like subprocess.run() would wake itself anyway and kill the process upon receiving SIGINT using the Python signal handlers, so whether the subprocess itself receives it or not is probably irrelevant. We need to check this assumption.
For anything like Target.background(), the process should be unaffected and it should be the responsibility of the Python code to decide what to do, as the user may want to implement some sort of loop on the Python side (e.g. wait for Ctrl-C, then do something to get data from target then wait again). We may want to in turn send SIGINT to the command using BackgroundCommand.send_signal() in BackgroundCommand.__exit__ if we were interrupted with KeyboardInterrupt, but maybe not. The current behavior of the context manager's __exit__ would be to close stdin/stdout/stderr and wait for completion.
It's not completely clear to me if start_new_session (setsid) is somewhat harmful compared to setpgrp as setsid() does more things. We need to figure out the implication of each solutions.
Signal forwarding from shells vary widely and now that you mention it, I vaguely remember coming across that fact about adb. Bash also probably differs from busybox sh and so on ... That problem is what sparked the recently merged work on BackgroundCommand.send_signal(): BackgroundCommand signals don't propagate through some shell setups #645 . We basically can't rely on any (or any absence of) forwarding, even things like sudo and su in the stack can change the behavior.
Thinking about it, considering shell/sudo/adb behaviors are inconsistent, we may want to simply never forward signals to any subprocess, and then encode the logic in Python so that each connection's execute() method catches KeyboardInterrupt and terminate the subprocess in a way we directly control (unless subprocess.run() does it nicely for us already).
This has been a recurring theme in devlib, where there is a variety of behavior that is one day unified by moving the logic away from connection-specific components and encoding that on the Python side, ideally in Target so it's fully shared (but here we want each connection class to behave well, as their own execute() method is sometimes used directly).
we may want to simply never forward signals to any subprocess, and then encode the logic in Python so that each connection's execute() method catches KeyboardInterrupt and terminate the subprocess in a way we directly control (unless subprocess.run() does it nicely for us already).
In my opinion that is clearly the option to follow as we can consistently "block" signals. Not so much for forwarding them.
Plus there are some applications that will want the signal to be propagated to the child while other application won't want it.
The main thing to solve with that approach is what do we do in details when we want to actually interrupt the command:
Target.execute() takes a shell snippet that may contain many commands, plus layers of sh and sudo/su
BackgroundCommand has a send_signal() method, but due to the same problem as above, only a single process is elected to receive the signal using the shell tool devlib-signal-target. SIGTERM, SIGKILL and SIGQUIT are special-cased though and sent to the process group of one of the "root" shell processes, so it gets delivered to all the processes created by the shell script snippet. We could also decide to deliver a SIGINT to the entire process group in a similar way I think.
Activity
arjpur01 commentedon May 22, 2025
We faced an issue when using devlib adb background connection in a tool we are developing.
In the tool, we are creating AndroidTarget, and spawning background command -
'
'
The command being run is perf stat on the target with arguments corresponding to the different cpu types on the target.
Then, we wait for user to press CTRL + C.
With the exisiting AdbConnection, we are seeing that the CTRL+C pressed is getting intercepted by the adb connection and it gets terminated. We actually have a handling logic that is supposed to terminate the perf using SIGINT, but because of the process intercepting the CTRL+C, that path is not getting taken, and the process has already closed without flushing the data we need.
Adding a preexec function in subprocess.Popen command in android.py of devlib helps to fix the problem.
p: 'Popen' = subprocess.Popen(full_command, stdout=stdout, stderr=stderr, stdin=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp)Need to see if this is an acceptable generic fix or there needs to be other considerations.
douglas-raillard-arm commentedon May 22, 2025
Something is going on as a (basic) use of Popen and CTRL-C while running shows that this is not the case in a normal scenario:
pan- commentedon May 27, 2025
I modified the example slightly for clarity:
The resulting behavior:
When
start_new_sessionis not set (orFalse), thesleepcommand interceptsSIGINTand exits. However, the shell (sh) itself remains alive since it doesn't exit upon receivingSIGINT.As noted by Arjun, this behavior impacts
adb_background_shellbecauseadbinterceptsSIGINTbut does not propagate it to the remote process (see: source code).Depending of the use case the code using devlib may want to forward the signal to background processes or block it. To handle properly each use cases, the best option is to let the user do it explicitly which requires to not propagate the signal automatically to the background application (which may just be a proxy...).
Therefore we do need these
os.setpgrpor callPopenwithstart_new_session=True.To handle this correctly and maintain compatibility, I propose:
start_new_session=Truefor Python 3.11 and later.preexec_fn=os.setpgrpfor backward compatibility with older Python versions.AdbConnectionbackground command with the functionality provided by bothLocalConnectionandSSHConnection.douglas-raillard-arm commentedon May 28, 2025
Thanks for the analysis Vincent.
start_new_sessionwas actually added in 3.2, onlyprocess_groupwas added in 3.11 so if we only need the former, we are good.Target.execute(), I don't have strong feelings on whether we should dispatch SIGINT or not to the process as long as antarget.execute("sleep 24h")command can be interrupted. I'd assume anything likesubprocess.run()would wake itself anyway and kill the process upon receiving SIGINT using the Python signal handlers, so whether the subprocess itself receives it or not is probably irrelevant. We need to check this assumption.Target.background(), the process should be unaffected and it should be the responsibility of the Python code to decide what to do, as the user may want to implement some sort of loop on the Python side (e.g. wait for Ctrl-C, then do something to get data from target then wait again). We may want to in turn send SIGINT to the command usingBackgroundCommand.send_signal()inBackgroundCommand.__exit__if we were interrupted withKeyboardInterrupt, but maybe not. The current behavior of the context manager's__exit__would be to close stdin/stdout/stderr and wait for completion.start_new_session(setsid) is somewhat harmful compared tosetpgrpassetsid()does more things. We need to figure out the implication of each solutions.adb. Bash also probably differs from busybox sh and so on ... That problem is what sparked the recently merged work onBackgroundCommand.send_signal(): BackgroundCommand signals don't propagate through some shell setups #645 . We basically can't rely on any (or any absence of) forwarding, even things likesudoandsuin the stack can change the behavior.douglas-raillard-arm commentedon May 28, 2025
Thinking about it, considering shell/sudo/adb behaviors are inconsistent, we may want to simply never forward signals to any subprocess, and then encode the logic in Python so that each connection's
execute()method catchesKeyboardInterruptand terminate the subprocess in a way we directly control (unlesssubprocess.run()does it nicely for us already).This has been a recurring theme in devlib, where there is a variety of behavior that is one day unified by moving the logic away from connection-specific components and encoding that on the Python side, ideally in Target so it's fully shared (but here we want each connection class to behave well, as their own execute() method is sometimes used directly).
pan- commentedon May 28, 2025
In my opinion that is clearly the option to follow as we can consistently "block" signals. Not so much for forwarding them.
Plus there are some applications that will want the signal to be propagated to the child while other application won't want it.
douglas-raillard-arm commentedon May 29, 2025
The main thing to solve with that approach is what do we do in details when we want to actually interrupt the command:
Target.execute()takes a shell snippet that may contain many commands, plus layers ofshandsudo/suBackgroundCommandhas asend_signal()method, but due to the same problem as above, only a single process is elected to receive the signal using the shell tooldevlib-signal-target. SIGTERM, SIGKILL and SIGQUIT are special-cased though and sent to the process group of one of the "root" shell processes, so it gets delivered to all the processes created by the shell script snippet. We could also decide to deliver a SIGINT to the entire process group in a similar way I think.arjpur01 commentedon Jun 4, 2025
created a PR - #727 pls review