-
Notifications
You must be signed in to change notification settings - Fork 360
[CLI] Fix temp dir cleanup when Playground CLI server is killed #2836
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
Conversation
|
This is pretty straightforward, but the fact that the signal handlers explicitly kill all the workers causes the following to be printed while exiting: It would be good to fix this before merging. |
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.
There are some test failures that may be due to these changes, so I'll look at those next. In the meantime, here are some comments about the changes.
| return; | ||
| } | ||
| process.exit(1); | ||
| // @TODO: Should we respawn the worker if it exited with an error and the CLI is not shutting down? |
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 removed the explicit exit for primary worker because I believe that runCLI()'s promise will be rejected if the primary worker fails during boot. And I do not believe the primary worker is special any longer.
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 the user requested 5 workers then yes, let's keep that amount of workers around. Otherwise we may run out of workers eventually if one crashes every 5 minutes.
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 love not having a special primary worker btw
| await disposeCLI(); | ||
| return; | ||
| } else if (args.command === 'run-blueprint') { | ||
| logger.log(`Blueprint executed`); | ||
| process.exit(0); | ||
| await disposeCLI(); | ||
| return; |
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.
Here we remove explicit exits from runCLI() and let the callers like parseOptionsAndRunCLI() decide based on whether commands succeed or fail.
| export async function runCLI( | ||
| args: RunCLIArgs & { command: 'build-snapshot' | 'run-blueprint' } | ||
| ): Promise<void>; | ||
| export async function runCLI( | ||
| args: RunCLIArgs & { command: 'server' } | ||
| ): Promise<RunCLIServer>; | ||
| export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer | void>; |
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.
These overloads are declared for convenience so runCLI() can return different things depending on the CLI command without forcing the callers (mostly automated tests) to check return values.
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.
^ Will add this as an inline comment.
| return startServer({ | ||
| port: args['port'] as number, | ||
| onBind: async (server: Server, port: number): Promise<RunCLIServer> => { | ||
| onBind: async (server: Server, port: number) => { |
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 type is removed here because it is easier if TypeScript to infers it. There should be little risk. What this handler returns becomes the return value of runCLI() which is further type-checked to match the runCLI() return value.
| process.exit(1); | ||
| throw new Error('Could not configure Xdebug', { | ||
| cause: error, | ||
| }); |
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 removed the explicit exit here so the callers can decide whether to exit or not. In automated tests, this case will no longer cause the test runner process (or worker) to exit, and in regular operation parseOptionsAndRunCLI() will catch this error and exit.
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 happy as long as we're protected from this scenario:
- The CLI user starts CLI and asks for XDebug with configuration
- Configuration failed, there's a faint log line in the CLI
- The user never noticed it
- They're scrambling to get Xdebug to work and start thinking "this Playground CLI thing doesn't work"
I've struggled to get Xdebug to work too many times to let an Xdebug-related failure through.
In automated tests, this case will no longer cause the test runner process (or worker) to exit
Oh, it exited the test runner process? Should we run that in a sub-process instead?
| // - we can avoid multiple, conflicting dispose attempts | ||
| // - logging that a worker exited while the CLI itself is exiting | ||
| let disposing = false; | ||
| const disposeCLI = async function disposeCLI() { |
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.
This function is now also used to clean up after run-blueprint and build-snapshot.
The tmp-promise lib handles temp dir cleanup in the case of exit, and our full cleanup is async which cannot be guaranteed to finish when exiting via process.exit().
|
The worker-exited messages are no longer printed while the CLI itself exits. This is ready for review. |
|
|
||
| await runCLI(cliArgs); | ||
| const cliServer = await runCLI(cliArgs); | ||
| if (cliServer === 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.
Do we have explicit result modes for every outcome? undefined means "success, no server", Server instance means there's a server running, an exception means an error. That sounds fine for the initial launch. Do we have any way of reporting errors further down the road other than the logger? If not, that may be a good exploration after this PR.
| } | ||
|
|
||
| const cleanUpCliAndExit = (() => { | ||
| // Remember we are already cleaning up to preclude the possibility |
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.
❤️
| // clean up after ourselves even if this process is being killed. | ||
| // NOTE: Windows does not support SIGTERM, but Node.js provides some emulation. | ||
| process.on('SIGINT', cleanUpCliAndExit); | ||
| process.on('SIGTERM', cleanUpCliAndExit); |
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.
nice. I just confirmed 'SIGKILL' cannot have a listener installed. Would SIGHUP be interesting for us here as well? Also, this caught my eye:
'SIGBUS', 'SIGFPE', 'SIGSEGV', and 'SIGILL', when not raised artificially using kill(2), inherently leave the process in a state from which it is not safe to call JS listeners. Doing so might cause the process to stop responding.
| process.stdout.isTTY ? `\x1b[33m${text}\x1b[0m` : text; | ||
|
|
||
| export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer> { | ||
| // These overloads are declared for convenience so runCLI() can return |
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.
good idea, thank you
| // Remember whether we are already disposing so we can avoid: | ||
| // - we can avoid multiple, conflicting dispose attempts |
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.
| // Remember whether we are already disposing so we can avoid: | |
| // - we can avoid multiple, conflicting dispose attempts | |
| // Remember whether we are already disposing so we can avoid: | |
| // - multiple, conflicting dispose attempts |
|
I left a few notes, but this is a good change and helps so I'll go ahead and merge it as is. |
## Motivation for the change, related issues Today, when Playground CLI is killed via Ctrl-C (SIGINT), the temp dir is left behind. This is a problem because Playground CLI server runs until it is killed in such a manner. ## Implementation details This PR adds explicit temp dir cleanup when receiving the signals SIGINT or SIGTERM. ## Testing Instructions (or ideally a Blueprint) - Run Playground CLI server in the console with the `--verbose=debug` flag. - Note the temp dir it created and printed in the CLI output - Kill the server with Ctrl-C - Confirm that the director no longer exists Prior to this patch, the temp dir still exists after this test. After this patch, the temp dir is removed.
Motivation for the change, related issues
Today, when Playground CLI is killed via Ctrl-C (SIGINT), the temp dir is left behind. This is a problem because Playground CLI server runs until it is killed in such a manner.
Implementation details
This PR adds explicit temp dir cleanup when receiving the signals SIGINT or SIGTERM.
Testing Instructions (or ideally a Blueprint)
--verbose=debugflag.Prior to this patch, the temp dir still exists after this test. After this patch, the temp dir is removed.