Skip to content
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

process: make exitCode configurable again #49579

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

ljharb
Copy link
Member

@ljharb ljharb commented Sep 9, 2023

This change was done in #44711, and it was unintentional. It caused #45683, and also makes it impossible to mock out the exitCode in tests. It affects all of node 19, and all of node 20 thus far (so it should not be backported to node 18 or earlier)

Filing this PR per #44711 (comment)

Refs: #44711
Fixes: #45683

cc @nodejs/process @nlf @daeyeon

This change was done in nodejs#44711, and it's not clear it was intentional.
It caused nodejs#45683, and also makes it impossible to mock out the exitCode
in tests.

Filing this PR per nodejs#44711 (comment)

Fixes nodejs#45683.
@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/startup

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Sep 9, 2023
@isaacs
Copy link
Contributor

isaacs commented Sep 9, 2023

I like this change, solves my problem. (I worked around it by just letting the code in my tests update the "real" process.exitCode and then verifying it's been set, and setting it back to 0. But that's less good, because it means I can't also verify that it was set the number of times I expect, in the places I expect, etc.)

Another idea to consider, which would preserve the air-tight protection against non-number exitCode:

  • Make process.exitCode a getter/setter, configurable: true
  • The setter verifies that the argument is a number, and then sets the "real" exit code somewhere internal
  • Downside: if you delete process.exitCode then setting process.exitCode = 1 just won't do anything.

@Trott
Copy link
Member

Trott commented Sep 10, 2023

Is it worthwhile to add a test for this?

@ljharb
Copy link
Member Author

ljharb commented Sep 10, 2023

Happy to if you can suggest where to put it :-)

@Trott
Copy link
Member

Trott commented Sep 10, 2023

Happy to if you can suggest where to put it :-)

Maybe see if it's possible/reasonable to include it in test/parallel/test-process-exit-code-validation.js? That has some code that runs delete process.exitCode which seems like something we're interested in here.

If that doesn't seem like the right place, perhaps create a separate test file as test/parallel/test-proces-exitCode-configurable or test/parallel/test-process-exitCode-delete or something similar to that?

@ljharb
Copy link
Member Author

ljharb commented Sep 10, 2023

What's interesting is that that test isn't failing now.

Copy link
Member

@benjamingr benjamingr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 (agree with @Trott this should have a test)

@daeyeon
Copy link
Member

daeyeon commented Sep 10, 2023

The configurable: false is intended to prevent the deletion of the setter, ensuring that the API remains intact. Allowing the API removed could result in a mismatch between the exit code set by users in JavaScript and the one returned by the native side.

process.exitCode = 1;
delete process.exitCode;
process.exitCode = 0; 

// With this patch, node will exit with 1, not 0.

Previously, the JavaScript side hold the exitCode, and the native side had to make several cross-boundary calls during exit to retrieve the value. And, exiting Node.js with a clear exit code was not always valid since not all objects allowed for the exitCode could be converted to a clear integer value.

validateInteger(value, 'code');
fields[kExitCode] = value;

Since v20, JavaScript side and the native side share the exitCode through fields[kExitCode], eliminating the need for cross-boundary calls from the native side. The setter now only accepts values that can be straightforwardly converted to an integer as the exit code.

@isaacs I'm not entirely sure if I grasp your issue correctly, but it seems like you want to monitor changes in the exit code during testing. I'm not certain if this is a good idea, but would it resolve your issue if we would trigger an event along with a modified exit code whenever it's changed?

isaacs added a commit to tapjs/tapjs that referenced this pull request Sep 10, 2023
If plugins export an `importLoader`, and Module.register exists, then
use that instead of their `loader` export.

process.exitCode became {configurable:false} in
nodejs/node#44711

Can bring back exitCode interception when/if it becomes configurable
again, re nodejs/node#49579

For now, just set it, and then verify it's the expected value, and put
it back to 0 if so.
isaacs added a commit to tapjs/tapjs that referenced this pull request Sep 10, 2023
If plugins export an `importLoader`, and Module.register exists, then
use that instead of their `loader` export.

process.exitCode became {configurable:false} in
nodejs/node#44711

Can bring back exitCode interception when/if it becomes configurable
again, re nodejs/node#49579

For now, just set it, and then verify it's the expected value, and put
it back to 0 if so.
@isaacs
Copy link
Contributor

isaacs commented Sep 10, 2023

@daeyeon

I'm not entirely sure if I grasp your issue correctly, but it seems like you want to monitor changes in the exit code during testing. I'm not certain if this is a good idea, but would it resolve your issue if we would trigger an event along with a modified exit code whenever it's changed?

Yes, it's for testing. It's somewhat less convenient to not be able to just do t.intercept(process, 'exitCode') and verify that it's set the exact number of times in the exact places expected, and have it automatically revert at the end of the test. But it's not that big a deal, really. I would not recommend adding a new API just for monitoring exitCode changes.

The configurable: false is intended to prevent the deletion of the setter, ensuring that the API remains intact. Allowing the API removed could result in a mismatch between the exit code set by users in JavaScript and the one returned by the native side.

Personally, I think that's fine. Put a note in the docs that delete process.exitCode is no longer equivalent to process.exitCode = 0, and trust that no one is going to intentionally break their own programs. But I can see the argument either way. 🤷

@ruyadorno ruyadorno added dont-land-on-v16.x dont-land-on-v18.x PRs that should not land on the v18.x-staging branch and should not be released in v18.x. labels Sep 11, 2023
@ljharb
Copy link
Member Author

ljharb commented Sep 11, 2023

I'm happy to add a test, but the existing delete process.exitCode test should be failing, and isn't, so I'd like to debug that first. Any ideas?

@daeyeon
Copy link
Member

daeyeon commented Sep 11, 2023

the existing delete process.exitCode test should be failing, and isn't, so I'd like to debug that first. Any ideas?

It seems that process.exitCode has been deleted, causing breaking the core that uses process.exitCode.

process.exitCode = 0;

throws(() => {
  delete process.exitCode; // configurable: true
}, /Cannot delete property 'exitCode' of #<process>/);

In the test, we last set exitCode to 0 and then do delete process.exitCode. When an exception is thrown by assert.throws(), it is caught by the global uncaught exception handler in the code below.

process.emit('uncaughtExceptionMonitor', er, type);
if (exceptionHandlerState.captureFn !== null) {
exceptionHandlerState.captureFn(er);
} else if (!process.emit('uncaughtException', er, type)) {
// If someone handled it, then great. Otherwise, die in C++ land
// since that means that we'll exit the process, emit the 'exit' event.
try {
if (!process._exiting) {
process._exiting = true;
process.exitCode = kGenericUserError;
process.emit('exit', kGenericUserError);
}

In 165 line, the handler set exitCode to kGenericUserError, but it's not passed to the native side. Because process.exitCode is undefined. As a result, although the exception causes node process to exit, the exitCode remains as 0 instead of kGenericUserError. The test.py does not catch the error.

@ljharb
Copy link
Member Author

ljharb commented Sep 11, 2023

Is that a deeper bug that I should try to fix?

@daeyeon
Copy link
Member

daeyeon commented Sep 12, 2023

Yes, it's needed to handle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dont-land-on-v18.x PRs that should not land on the v18.x-staging branch and should not be released in v18.x. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

unexpected breaking changes with process.exitCode/process.exit()
8 participants