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
check for currently running ping processes: ps aux | grep ping (confirm no ping process is running)
run the test script (see below) python ./test.py
check list of processes again, while the test script is running and after the test script terminates
Expected result is that a shell + ping + cat are all running only for 5 seconds.
Actual result that a shell process is running for 5 seconds, but the ping and also cat processes remain running not just beyond 5 seconds, but even after a script terminates. It looks like the subprocesses get detached from the shell and are not considered childs any more. I am not sure if this is 1 or 2 separate problems. I am not even sure if this is a proper way to run a shell script from within trio.
In any case, is there a workaround or a better way to run shell commands?
Reading the docs page about run_process, it is implied that on unix systems, processes are sent the SIGTERM signal and after 5 seconds SIGKILL is sent. If you want to immediately kill subprocesses, you might want to change deliver_cancel, though I've not worked with sub processes extensively before and I might be missing something.
@CoolCat467 thanks for the suggestion.
I have tryed to use deliver_cancel to force stop the process, but it does not help.
This is a new test scenario. It shows that deliver_cancel actually gets executed and the (shell) process had actually stopped. But it looks like childs of a shell did not receive termination signal. The ping process still gets orphaned and remains running.
@A5rocks I am not exactly sure how to translate your suggestion to current trio defaults. Do I suppose to use trio.run_process or any other low level method? I would appreciate a short trio example to demonstrate start/stop of a shell pileline.
It is sufficient for my application. Let me know I you want me to close the issue or keep it open as a reminder to improve trio. Either by better defaults or at least documentation update to highlight the problem and a solution.
There is another observation. trio correctly stops all subprocesses on CTRL-C on all cases, either during the first 5 seconds or during the second 5 seconds interval. The behaviour is correct even with the original example (that is: even after the subprocess was already improperly canceled). This makes me believe that there is another problem, namely the CTRL-C does better (expected) cleanup than regular application exit. Not sure if this observation is related to the original issue.
I guess I should explain what I think the problem is.
The issue is that process.kill() kills a process -- and processes don't have to propagate things to subprocesses. Instead, we can put them all in a group and this means that we can kill the group, which sends a signal to every member.
The reason why Ctrl+C works by default is that subprocesses by default share a process group with the process that spawned them. So, the shell has the same process group as the python program. When you press Ctrl+C, every process in the process group receives a SIGINT. We can't use this for process.kill() because we don't want to kill the current process; just the subprocesses. Hence the process_group=0 to create a new process group (0 = "use my pid for pgid").
(I'm not sure where "it worked without pipes" fits into this. I assume the shell does some exec if there's no pipes and sub processes if there are? That's the main bit that makes me couch this explanation in "what I think the problem is")
I think trio should improve its defaults here:
make shell=True subprocesses pass process_group=0 by default unless that overrides something
if process_group is in the kwargs, then the default deliver_cancel should use os.killpg(os.getpgid(process.pid), signal)
probably also backport process_group by updating preexec_fn on pre-3.11
Though we should make sure we don't make it worse in response to ctrl+c (since the subprocess is no longer in the process group I don't think they automatically received SIGINT. This is probably the correct behavior though? We want them to go through typical shutdown behavior as deliver_cancel specifies.)
Not an expert, but I understand enough to say this is not a Trio-specific issue, nor a shell=True / pipe specific issue (though this does often come up in that use case).
Trio can’t in general enforce structured concurrency across the subprocess boundary: It can’t stop an arbitrary subprocess from e.g. spawning its own subprocess in a new session that Trio doesn’t know about / can't supervise, and which outlives the ancestor Trio program.
But in the common enough case that you control all the descendent subprocesses you’re spawning with Trio (and can be make sure they don't misbehave), you can use the process group trick mentioned above along with os.killpg to honor structured concurrency. It’s a subtle enough yet common enough use case that it probably deserves at least a recipe in the docs, if not also some higher-level API support.
Would you happen to know why this only works on 3.12 and 3.14, not 3.13 or anything less than 3.12? (I'll spend some time debugging this at some point but this specific matrix of success makes no sense and yet it persists across reruns): A5rocks@c7991d4
Turns out it was just a race condition in the test, unless my tests locally were wrong. Guess we have to give a process some time to die? (I'd rather if we only exited the nursery if it was indeed dead, but I'm not sure that's something Trio can do)
Activity
CoolCat467 commentedon Apr 1, 2025
Reading the docs page about
run_process, it is implied that on unix systems, processes are sent theSIGTERMsignal and after 5 secondsSIGKILLis sent. If you want to immediately kill subprocesses, you might want to changedeliver_cancel, though I've not worked with sub processes extensively before and I might be missing something.zoranbosnjak commentedon Apr 1, 2025
@CoolCat467 thanks for the suggestion.
I have tryed to use
deliver_cancelto force stop the process, but it does not help.This is a new test scenario. It shows that
deliver_cancelactually gets executed and the (shell) process had actually stopped. But it looks like childs of a shell did not receive termination signal. The ping process still gets orphaned and remains running.Do you have any other idea?
A5rocks commentedon Apr 1, 2025
This isn't trio-specific. I haven't tried looking more but I suspect
/bin/shdoesn't propagate signals through pipes.A5rocks commentedon Apr 1, 2025
On Python 3.11+ (on Unix at least) you can do this:
and for earlier I think you need to use like
preexec_fnwith likeos.setpgid(0, 0). Maybe trio should do this by default ifshell=True(on Unix).zoranbosnjak commentedon Apr 2, 2025
@A5rocks I am not exactly sure how to translate your suggestion to current
triodefaults. Do I suppose to usetrio.run_processor any other low level method? I would appreciate a shorttrioexample to demonstrate start/stop of a shell pileline.A5rocks commentedon Apr 2, 2025
This should work, though I haven't tested it:
zoranbosnjak commentedon Apr 2, 2025
@A5rocks thanks. This approach works.
It is sufficient for my application. Let me know I you want me to close the issue or keep it open as a reminder to improve
trio. Either by better defaults or at least documentation update to highlight the problem and a solution.There is another observation.
triocorrectly stops all subprocesses onCTRL-Con all cases, either during the first 5 seconds or during the second 5 seconds interval. The behaviour is correct even with the original example (that is: even after the subprocess was already improperly canceled). This makes me believe that there is another problem, namely theCTRL-Cdoes better (expected) cleanup than regular application exit. Not sure if this observation is related to the original issue.A5rocks commentedon Apr 2, 2025
I guess I should explain what I think the problem is.
The issue is that
process.kill()kills a process -- and processes don't have to propagate things to subprocesses. Instead, we can put them all in a group and this means that we can kill the group, which sends a signal to every member.The reason why Ctrl+C works by default is that subprocesses by default share a process group with the process that spawned them. So, the shell has the same process group as the python program. When you press Ctrl+C, every process in the process group receives a
SIGINT. We can't use this forprocess.kill()because we don't want to kill the current process; just the subprocesses. Hence theprocess_group=0to create a new process group (0= "use my pid for pgid").(I'm not sure where "it worked without pipes" fits into this. I assume the shell does some
execif there's no pipes and sub processes if there are? That's the main bit that makes me couch this explanation in "what I think the problem is")I think trio should improve its defaults here:
shell=Truesubprocesses passprocess_group=0by default unless that overrides somethingprocess_groupis in the kwargs, then the defaultdeliver_cancelshould useos.killpg(os.getpgid(process.pid), signal)process_groupby updatingpreexec_fnon pre-3.11Though we should make sure we don't make it worse in response to ctrl+c (since the subprocess is no longer in the process group I don't think they automatically received
SIGINT. This is probably the correct behavior though? We want them to go through typical shutdown behavior asdeliver_cancelspecifies.)Implement helpful features from python-trio#3237
jab commentedon Apr 12, 2025
https://biriukov.dev/docs/fd-pipe-session-terminal/3-process-groups-jobs-and-sessions/ explains what’s going on here.
Not an expert, but I understand enough to say this is not a Trio-specific issue, nor a shell=True / pipe specific issue (though this does often come up in that use case).
Trio can’t in general enforce structured concurrency across the subprocess boundary: It can’t stop an arbitrary subprocess from e.g. spawning its own subprocess in a new session that Trio doesn’t know about / can't supervise, and which outlives the ancestor Trio program.
But in the common enough case that you control all the descendent subprocesses you’re spawning with Trio (and can be make sure they don't misbehave), you can use the process group trick mentioned above along with
os.killpgto honor structured concurrency. It’s a subtle enough yet common enough use case that it probably deserves at least a recipe in the docs, if not also some higher-level API support.A5rocks commentedon Apr 13, 2025
Would you happen to know why this only works on 3.12 and 3.14, not 3.13 or anything less than 3.12? (I'll spend some time debugging this at some point but this specific matrix of success makes no sense and yet it persists across reruns): A5rocks@c7991d4
Here's the CI run: https://github.com/A5rocks/trio/actions/runs/14330484295
jab commentedon May 4, 2025
I haven't had time to look into the CI failures, sorry.
I did put together a (not-Trio-specific)
killpgdemo though. In case it's helpful: https://github.com/ctcjab/killpg-demoA5rocks commentedon May 25, 2025
Turns out it was just a race condition in the test, unless my tests locally were wrong. Guess we have to give a process some time to die? (I'd rather if we only exited the nursery if it was indeed dead, but I'm not sure that's something Trio can do)
Implement helpful features from python-trio#3237
Implement helpful features from python-trio#3237
Implement helpful features from python-trio#3237