-
Notifications
You must be signed in to change notification settings - Fork 30.3k
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
base: main
Are you sure you want to change the base?
Conversation
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.
Review requested:
|
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:
|
Is it worthwhile to add a test for this? |
Happy to if you can suggest where to put it :-) |
Maybe see if it's possible/reasonable to include it in If that doesn't seem like the right place, perhaps create a separate test file as |
What's interesting is that that test isn't failing now. |
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.
+1 (agree with @Trott this should have a test)
The 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 node/lib/internal/bootstrap/node.js Lines 123 to 124 in c55625f
Since v20, JavaScript side and the native side share the @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? |
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.
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.
Yes, it's for testing. It's somewhat less convenient to not be able to just do
Personally, I think that's fine. Put a note in the docs that |
I'm happy to add a test, but the existing |
It seems that 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 node/lib/internal/process/execution.js Lines 156 to 167 in 48fcb20
In 165 line, the handler set exitCode to |
Is that a deeper bug that I should try to fix? |
Yes, it's needed to handle. |
ok, i was able to verify that the test failed locally, and then i updated the test to pass with the change, so in theory this is good to go :-D |
|
||
const origExitCode = Object.getOwnPropertyDescriptor(process, 'exitCode'); | ||
try { | ||
delete process.exitCode; |
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 think it needs to be handled so that even when users delete process.exitCode
, the exit code set by the core code will still be applied.
process.exitCode = kGenericUserError;
process.exitCode
is also used by the core. This is concerning because if users delete it, the intended behavior in the core, like the code mentioned above (#49579 (comment)), won't be applied.
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 how that would be possible. If it's deleted, there's nothing there.
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 note that process.exitCode = kGenericUserError;
will still work after it's been deleted, it just won't have the magic getter/setter anymore.
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.
If process.exitCode = kGenericUserError;
worked, the error code after terminating the node should be kGenericUserError
, not 0. Last time I checked, it was 0... right?
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 mean it should not be 0 when calling the exit status. echo $?
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 can be restricted to any domain you like :-) just integer numbers then?
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 I can give you the answer you want, as I can't guess how you'd implement it based on your current description. Currently, the types allowed are:
* {integer|string|null|undefined} The exit code. For string type, only
integer strings (e.g.,'1') are allowed. **Default:** `undefined`.
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.
ok, so I’ll move forward with that schema, thanks.
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.
hmm - seems like the exitCode is read in C++, so i'm not sure how it can read the JS-set value once it's overridden.
@daeyeon are you comfortable with this proceeding with the understanding that if a user mocks process.exitCode, it's up to them to restore it? (just like other globals)
If not, what about #49579 (comment) ?
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.
@daeyeon are you comfortable with this proceeding with the understanding that if a user mocks process.exitCode, it's up to them to restore it? (just like other globals)
The core's behavior related to exitCode will be breaking during the time gap between mocking and restoring. It's probably not enough to rely on the restoring after finishing the mocking.
If not, what about #49579 (comment) ?
Looks like a good idea. In addition to moving the getters/setters to the process prototype, I think that the core code should use the exitCode of the process prototype, rather than the process itself. This would address concerns about breaking and can provide guidance on how users can monitor it.
What if the getter/setter was on the prototype of the process, rather than the process object itself? Something like this: class Process {
#exitCode = 0
get exitCode() {
return this.#exitCode
}
set exitCode(ec) {
if (ec === Math.floor(ec)) {
this.#exitCode = ec
} else {
throw new Error('cannot set exit code to non-int')
}
}
}
// make the property on the prototype non-configurable
Object.defineProperty(Process.prototype, 'exitCode', {
...Object.getOwnPropertyDescriptor(Process.prototype, 'exitCode'),
configurable: false,
})
const process = new Process()
Object.defineProperty(process, 'exitCode', {
value: 2,
configurable: true,
writable: true,
})
console.log(process.exitCode) // 2, from defined property on object
delete process.exitCode // deletes from object, not prototype
console.log(process.exitCode) // 0
try {
Object.defineProperty(process.__proto__, 'exitCode', { value: 99 })
} catch {
console.log('cannot redefine') // this is printed
} With this approach:
That is, you get this weirdness, which imo is way less weird than what we have now: process.exitCode = 1
delete process.exitCode
console.log(process.exitCode) // 1
// process exits with code 1, not zero as would've happened prior to the non-configurable propdef |
That's a clever alternative - but then the actual exit code still wouldn't read from the overridden value? |
Correct, if you But isn't that sort of what happens in the code right now? Like, if you make the getter/setter configurable, then if someone redefines it, it no longer affects the actual process exit status code, until/unless they put it back. The difference is that "put it back" is |
|
||
const origExitCode = Object.getOwnPropertyDescriptor(process, 'exitCode'); | ||
try { | ||
delete process.exitCode; |
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 think we should just test that redefining process.exitCode
with accessors that call out to the original at the end still works as expected. That means users can still mock it if they call out to the original accessors. If they don't, or if they delete it, then it is just not going to get picked up as the actual exit code. And we can just put a note in the doc warning that delete process.exitCode
is not going to work, like what @isaacs suggested in #49579 (comment)
Maybe we can have both? By default we define it both on the prototype and the process object itself. The own version is configurable, and mocking code can define different accessors on the process itself, calling out to the original own accessors for Node.js to pick up the exit code if necessary. If they delete the own version then there's still the prototype version for compat. |
Sounds like a good direction. I'll update this soon. |
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