diff --git a/fixtures/async/container/a_container.rb b/fixtures/async/container/a_container.rb index 6aea25a..b3ceb16 100644 --- a/fixtures/async/container/a_container.rb +++ b/fixtures/async/container/a_container.rb @@ -107,6 +107,27 @@ module Container ) end end + + it "can exec with ready: true without premature termination" do + container.spawn(restart: false) do |instance| + # Using exec with ready: true should not cause the process to be killed + # by hang prevention, even though the notification pipe stays open. + instance.exec("sleep", "1", ready: true) + end + + # Wait for the process to become ready: + container.wait_until_ready + + # Sleep longer than the hang prevention timeout (0.1s) to verify + # the process isn't prematurely killed: + sleep(0.2) + + # The process should still be running (not killed by hang prevention): + expect(container).to be(:running?) + + # Now stop the container: + container.stop(false) + end end with "#sleep" do diff --git a/lib/async/container/forked.rb b/lib/async/container/forked.rb index 7cde0ba..15cc05f 100644 --- a/lib/async/container/forked.rb +++ b/lib/async/container/forked.rb @@ -80,13 +80,15 @@ def name # This method replaces the child process with the new executable, thus this method never returns. # # @parameter arguments [Array] The arguments to pass to the new process. - # @parameter ready [Boolean] If true, informs the parent process that the child is ready. Otherwise, the child process will need to use a notification protocol to inform the parent process that it is ready. + # @parameter ready [Boolean] If true, informs the parent process that the child is ready before exec. The notification pipe will still be passed to the exec'd process to prevent premature termination. # @parameter options [Hash] Additional options to pass to {::Process.exec}. def exec(*arguments, ready: true, **options) + # Always set up the notification pipe to be inherited by the exec'd process. + # This prevents the pipe from closing, which would trigger hang prevention and SIGKILL. + self.before_spawn(arguments, options) + if ready self.ready!(status: "(exec)") - else - self.before_spawn(arguments, options) end ::Process.exec(*arguments, **options) diff --git a/lib/async/container/threaded.rb b/lib/async/container/threaded.rb index 84648ae..b1be095 100644 --- a/lib/async/container/threaded.rb +++ b/lib/async/container/threaded.rb @@ -91,10 +91,11 @@ def name # Execute a child process using {::Process.spawn}. In order to simulate {::Process.exec}, an {Exit} instance is raised to propagage exit status. # This creates the illusion that this method does not return (normally). def exec(*arguments, ready: true, **options) + # Always set up the notification pipe to be inherited by the spawned process. + self.before_spawn(arguments, options) + if ready self.ready!(status: "(spawn)") - else - self.before_spawn(arguments, options) end begin diff --git a/releases.md b/releases.md index 0d14ff8..43ce82e 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,9 @@ # Releases +## Unreleased + + - **Fixed**: `instance.exec` with `ready: true` no longer causes premature termination. The notification pipe is now always passed to the exec'd process. + ## v0.34.4 - Add missing `bake` and `context` files to the release.