-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Handle propogating SIGTERM to spawned child processes #2347
Conversation
I looked over the repro you linked and it doesn't seem that your changes fix it. I experimented a bit and moving your changes around here: berry/packages/yarnpkg-shell/sources/pipe.ts Lines 50 to 57 in 63a77b5
However, the yarn v1 wrapper seems to have the same bug. Made a small screen recording of the behavior here (you can use "Download a Copy") |
I sent a PR to fix the issue in yarn v1 here: yarnpkg/yarn#8543 |
With above v1 PR and with the below fix at: berry/packages/yarnpkg-shell/sources/pipe.ts Lines 54 to 56 in 63a77b5
const sigtermHandler = () => child.kill(`SIGTERM`);
process.on(`SIGTERM`, sigtermHandler); |
@andreialecu I'm focused right now on solving the issue for yarn2 not using the yarn1 wrapper. We can follow up with a fix for yarn1 if necessary. I did double check and my changes do fix the issue when running |
Here's a diff with tests: diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/commands/exec.test.js b/packages/acceptance-tests/pkg-tests-specs/sources/commands/exec.test.js
index 94ba01f5..80f9cad2 100644
--- a/packages/acceptance-tests/pkg-tests-specs/sources/commands/exec.test.js
+++ b/packages/acceptance-tests/pkg-tests-specs/sources/commands/exec.test.js
@@ -26,5 +26,29 @@ describe(`Commands`, () => {
});
})
);
+
+ test(
+ `it should end child process on SIGTERM`,
+ makeTemporaryEnv({}, async ({ path, run, source }) => {
+ await xfs.writeFilePromise(`${path}/test.sh`, ([
+ `yarn node -e "console.log('Testing exec SIGTERM'); setTimeout(() => {}, 10000)" &`,
+ `sleep 1`,
+ `ps | grep -v "grep" | grep -q "Testing exec SIGTERM" || exit 1`, // check if it was started properly
+ `kill $!`,
+ `sleep 1`,
+ `if ps | grep -v "grep" | grep -q "Testing exec SIGTERM"`,
+ `then`,
+ ` echo "[FAIL] still running"; exit 1`,
+ `else`,
+ ` echo "[PASS] ok"; exit 0`,
+ `fi`
+ ]).join('\n'));
+
+ await expect(run(`exec`, `bash`, `test.sh`)).resolves.toMatchObject({
+ code: 0,
+ stdout: expect.stringContaining(`PASS`),
+ });
+ })
+ );
});
});
diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/commands/run.test.js b/packages/acceptance-tests/pkg-tests-specs/sources/commands/run.test.js
index 4b031094..389ab8fd 100644
--- a/packages/acceptance-tests/pkg-tests-specs/sources/commands/run.test.js
+++ b/packages/acceptance-tests/pkg-tests-specs/sources/commands/run.test.js
@@ -1,3 +1,5 @@
+const {xfs} = require(`@yarnpkg/fslib`);
+
describe(`Commands`, () => {
for (const [description, args] of [[`with prefix`, [`run`]], [`without prefix`, []]]) {
describe(`run ${description}`, () => {
@@ -123,6 +125,7 @@ describe(`Commands`, () => {
},
),
);
+
test(`it should print the list of available scripts if no parameters passed to command`,
makeTemporaryEnv(
{
@@ -137,5 +140,35 @@ describe(`Commands`, () => {
}
)
);
+
+ test(
+ `it should end child script on SIGTERM`,
+ makeTemporaryEnv({
+ scripts: {
+ sleep: `node -e "console.log('Testing script SIGTERM'); setTimeout(() => {}, 10000)"`
+ }
+ }, async ({ path, run, source }) => {
+ await run(`install`);
+
+ await xfs.writeFilePromise(`${path}/test.sh`, ([
+ `yarn sleep &`,
+ `sleep 1`,
+ `ps | grep -v "grep" | grep -q "Testing script SIGTERM" || exit 1`, // check if it was started properly
+ `kill $!`,
+ `sleep 1`,
+ `if ps | grep -v "grep" | grep -q "Testing script SIGTERM"`,
+ `then`,
+ ` echo "[FAIL] still running"; exit 1`,
+ `else`,
+ ` echo "[PASS] ok"; exit 0`,
+ `fi`
+ ]).join('\n'));
+
+ await expect(run(`exec`, `bash`, `test.sh`)).resolves.toMatchObject({
+ code: 0,
+ stdout: expect.stringContaining(`PASS`),
+ });
+ })
+ );
});
}); |
Nice, thank you both! @ganemone can you add the tests from @andreialecu to your PR? Perhaps we can even make a dedicated file in |
@@ -53,6 +53,9 @@ export function makeProcess(name: string, args: Array<string>, opts: ShellOption | |||
stderr, | |||
]}); | |||
|
|||
const sigtermHandler = () => child.kill(`SIGTERM`); | |||
process.on(`SIGTERM`, sigtermHandler); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't adding all these SIGTERM handlers going to make node complain?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might with yarn workspaces foreach
, yep. @ganemone can you use a similar refcount to register a single callback, and keep track of the children manually in a set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to explicitly forward the SIGINT as well? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, iirc SIGINT is purposefully ignored because the shell already sends it to the whole process tree, so we exit once the children have caught it and exited cleanly. I think there are discussions about that (perhaps on Discord).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW if you try to run kill -s SIGINT <yarn_pid>
nothing happens. Should it not be forwarded to the child process?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure under which circumstances yarn itself might receive a SIGINT though. I can't think of any, but I'm not sure why forwarding it should be prevented.
Babel for example forwards it to itself: https://github.com/babel/babel/blob/master/packages/babel-node/src/babel-node.js#L99
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The shell sends it to the whole process tree. Using kill
doesn't. More details here.
In any case, this is intentional, SIGINT is working as expected. Babel doesn't have to work with many types of binaries, some of them behaving weirdly if they receive the same signal multiple times 🙂
@arcanis finding a way to test this reliably is pretty difficult. I'm removing the tests for now |
} | ||
} | ||
|
||
process.on(`SIGTERM`, sigtermHandler); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs to follow the same refcount logic as the SIGINT
event
I've made a few fixes to unify the SIGINT / SIGTERM handling, and removed the |
Is there a planned release date for this bugfix? |
It'll be part of the next major. If you want to use it now, you can run |
Assuming you meant Yarn PnP then you can run it without using the CLI - https://yarnpkg.com/features/pnp#initializing-pnp |
@BERVOL as a workaround: you can bypass If you use pnp you need to load the pnp hook:
If you use yarn 1 to spawn yarn 2, note that bumping just v2 to sources isn't enough, because v1 has the same problem: |
Passing In our Dockerfile, we've got: ENTRYPOINT ["/usr/local/bin/yarn", "workspace", "example-workspace", "run", "start"] And then in that workspace's "scripts": {
"start": "node --unhandled-rejections=strict ./entry.js"
} Should we expect |
Using |
@WesCossick Did you read the comment right above yours? Specifically this part:
Make sure to update your v1 version as well |
@merceyz I did see that comment, yes. In many of our company's Docker images we manually install the latest version of Yarn (1.22.15 right now), but in the Docker image I was using for testing, it turns out it was just using the version of Yarn bundled with the Upgrading Yarn v1 from 1.22.5 to 1.22.15 fixed the issue. There's been some recent discussions and steps taken to upgrade the version of Yarn bundled with Node.js's official Docker images: nodejs/docker-node#1542. Seems like they should be updated in the near future. In the meantime, for others who find this, you can add the following instruction to update Yarn: RUN npm install -g [email protected] --force |
What's the problem this PR addresses?
Fixes #1741
See https://github.com/albertywu/yarn2-termination-bug#repro for more context
...
How did you fix it?
Add SIGTERM listener to the yarn process and send signal to spawned child process if still running. The current approach will add a new SIGTERM listener to the yarn process for each child process spawned, removing the listener when the child process closes. If this is a concern, we could go with an alternative approach that keeps track of the child process refs in a Set and use a single listener to close all child processes running when the yarn process receives SIGTERM.
NOTE: The current PR only addresses this in the
pipevp
function inexecUtils
, not theexecvp
function. Its unclear to me if the same problem can present itself in usage of theexecvp
function, but we can easily add the same functionality there....
Checklist