-
-
Notifications
You must be signed in to change notification settings - Fork 4
Improve shutdown for FwLiteWeb in Paratext #1992
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
base: develop
Are you sure you want to change the base?
Conversation
* Replace direct process killing with a dedicated `shutdownFwLite` function for graceful termination. * Update FwLiteWeb to properly handle `shutdown` commands via stdin. * Adjust process stdio configuration for consistency.
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the 📝 WalkthroughWalkthroughAdds a console-driven shutdown path: the web app listens for "shutdown" on stdin and stops asynchronously. The extension adjusts process launch to enable stdin and introduces a helper to send "shutdown\n", await clean exit, and force-kill after 10 seconds if needed. Cleanup now uses the graceful shutdown helper. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Pre-merge checks (2 passed, 1 warning)❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
Poem
✨ Finishing Touches🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 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.
Actionable comments posted: 2
🧹 Nitpick comments (2)
backend/FwLite/FwLiteWeb/Program.cs (1)
18-25
: Optional: guard against unexpected read errors and log.Wrap the loop in try/catch so unexpected I/O failures don’t tear down the background task silently. If you have logging on the host, log and call StopAsync().
platform.bible-extension/src/main.ts (1)
296-315
: Optional: kill the process tree on Windows if FwLiteWeb spawns children.If FwLiteWeb ever spawns child processes, consider terminating the whole tree (e.g., taskkill /T /F or a library like tree-kill) in the fallback, otherwise stdout/stderr handles may keep the parent alive.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
backend/FwLite/FwLiteWeb/Program.cs
(1 hunks)platform.bible-extension/src/main.ts
(3 hunks)
🧰 Additional context used
🪛 GitHub Actions: Platform.Bible extension CI/CD
platform.bible-extension/src/main.ts
[warning] 1-1: Code style issues found in 'src/main.ts'. Run 'prettier --write .' to fix.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: frontend
- GitHub Check: frontend-component-unit-tests
- GitHub Check: Build FW Lite and run tests
- GitHub Check: Analyze (csharp)
🔇 Additional comments (3)
platform.bible-extension/src/main.ts (3)
252-252
: Good: register graceful cleanup instead of hard kill.Hooking cleanup to a shutdown function is the right direction. Verify the activation host treats a rejected Promise from a disposer as a non-fatal deactivation; otherwise prefer resolving false instead of rejecting.
279-279
: Stdin pipe is required here—LGTM.Switching stdio to ['pipe','pipe','pipe'] is necessary so the backend can read the shutdown command.
1-1
: Run Prettier via the package’s format script
CI flagged formatting in platform.bible-extension/src/main.ts; execute in that folder:cd platform.bible-extension && pnpm run format
Likely an incorrect or invalid review comment.
//windows is dumb and so you can't send SIGINT to a process, so we need to listen for a shutdown command | ||
_ = Task.Run(async () => | ||
{ | ||
// Wait for the "shutdown" command from stdin | ||
while (await Console.In.ReadLineAsync() is not "shutdown") { } | ||
|
||
await app.StopAsync(); | ||
}); |
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.
🛠️ Refactor suggestion
Handle EOF and be resilient to whitespace/case; avoid a potential busy loop on closed stdin.
If stdin closes (parent dies or doesn’t pipe input), ReadLineAsync() returns null and the current loop spins forever without calling StopAsync(). Also, be lenient to trailing whitespace/casing.
Apply this diff:
- //windows is dumb and so you can't send SIGINT to a process, so we need to listen for a shutdown command
- _ = Task.Run(async () =>
- {
- // Wait for the "shutdown" command from stdin
- while (await Console.In.ReadLineAsync() is not "shutdown") { }
-
- await app.StopAsync();
- });
+ // Windows can't receive SIGINT as a child; listen for a "shutdown" command on stdin.
+ _ = Task.Run(async () =>
+ {
+ string? line;
+ while ((line = await Console.In.ReadLineAsync()) is not null
+ && !string.Equals(line.Trim(), "shutdown", StringComparison.OrdinalIgnoreCase))
+ { /* keep waiting */ }
+
+ await app.StopAsync();
+ });
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
//windows is dumb and so you can't send SIGINT to a process, so we need to listen for a shutdown command | |
_ = Task.Run(async () => | |
{ | |
// Wait for the "shutdown" command from stdin | |
while (await Console.In.ReadLineAsync() is not "shutdown") { } | |
await app.StopAsync(); | |
}); | |
// Windows can't receive SIGINT as a child; listen for a "shutdown" command on stdin. | |
_ = Task.Run(async () => | |
{ | |
string? line; | |
while ((line = await Console.In.ReadLineAsync()) is not null | |
&& !string.Equals(line.Trim(), "shutdown", StringComparison.OrdinalIgnoreCase)) | |
{ /* keep waiting */ } | |
await app.StopAsync(); | |
}); |
function shutdownFwLite(fwLiteProcess: ReturnType<typeof launchFwLiteFwLiteWeb>['fwLiteProcess']): Promise<boolean> { | ||
return new Promise((resolve, reject) => { | ||
fwLiteProcess.once('exit', (code, signal) => { | ||
if (code === 0) { | ||
resolve(true); | ||
} else { | ||
reject(new Error(`FwLiteWeb exited with code ${code}, signal ${signal}`)); | ||
} | ||
}); | ||
fwLiteProcess.once('error', (error) => { | ||
reject(error); | ||
}); | ||
|
||
fwLiteProcess.stdin.write('shutdown\n'); | ||
fwLiteProcess.stdin.end(); | ||
setTimeout(() => { | ||
fwLiteProcess.kill('SIGKILL'); | ||
}, 10000); | ||
}); | ||
} |
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.
🛠️ Refactor suggestion
Make shutdown idempotent, clear the kill timer, and avoid rejecting on forced-kill.
Today:
- A forced kill after 10s will likely cause exit with non-zero/Signal and the Promise rejects, which can bubble as an activation teardown error.
- The kill timer isn’t cleared on early exit.
- If the process already exited, the Promise never resolves.
- If stdin is unavailable, write() will error.
Refactor to: resolve(true) on clean or forced termination, resolve(false) on error, clear the timer, and short-circuit if already exited. Also guard stdin.
Apply this diff:
-function shutdownFwLite(fwLiteProcess: ReturnType<typeof launchFwLiteFwLiteWeb>['fwLiteProcess']): Promise<boolean> {
- return new Promise((resolve, reject) => {
- fwLiteProcess.once('exit', (code, signal) => {
- if (code === 0) {
- resolve(true);
- } else {
- reject(new Error(`FwLiteWeb exited with code ${code}, signal ${signal}`));
- }
- });
- fwLiteProcess.once('error', (error) => {
- reject(error);
- });
-
- fwLiteProcess.stdin.write('shutdown\n');
- fwLiteProcess.stdin.end();
- setTimeout(() => {
- fwLiteProcess.kill('SIGKILL');
- }, 10000);
- });
-}
+function shutdownFwLite(
+ fwLiteProcess: ReturnType<typeof launchFwLiteFwLiteWeb>['fwLiteProcess'],
+): Promise<boolean> {
+ return new Promise((resolve) => {
+ // Already exited?
+ if (fwLiteProcess.exitCode !== null) {
+ return resolve(fwLiteProcess.exitCode === 0);
+ }
+
+ let forced = false;
+ const killTimer = setTimeout(() => {
+ forced = true;
+ try { fwLiteProcess.kill('SIGKILL'); } catch { /* ignore */ }
+ }, 10_000);
+
+ const onExit = (code: number | null) => {
+ clearTimeout(killTimer);
+ // Treat forced kill as a successful shutdown to avoid failing deactivation.
+ resolve(forced || code === 0);
+ };
+ const onError = () => {
+ clearTimeout(killTimer);
+ resolve(false);
+ };
+
+ fwLiteProcess.once('exit', onExit);
+ fwLiteProcess.once('error', onError);
+
+ if (fwLiteProcess.stdin && !fwLiteProcess.stdin.destroyed) {
+ try {
+ fwLiteProcess.stdin.write('shutdown\n');
+ fwLiteProcess.stdin.end();
+ } catch {
+ // Fall back to forced termination if stdin write fails.
+ forced = true;
+ try { fwLiteProcess.kill('SIGKILL'); } catch { /* ignore */ }
+ }
+ } else {
+ forced = true;
+ try { fwLiteProcess.kill('SIGKILL'); } catch { /* ignore */ }
+ }
+ });
+}
Note: On Windows, Node emulates signals; child_process.kill('SIGKILL') forces termination and cannot be handled. This is fine for the fallback. (github.com, nodejs.org)
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
function shutdownFwLite(fwLiteProcess: ReturnType<typeof launchFwLiteFwLiteWeb>['fwLiteProcess']): Promise<boolean> { | |
return new Promise((resolve, reject) => { | |
fwLiteProcess.once('exit', (code, signal) => { | |
if (code === 0) { | |
resolve(true); | |
} else { | |
reject(new Error(`FwLiteWeb exited with code ${code}, signal ${signal}`)); | |
} | |
}); | |
fwLiteProcess.once('error', (error) => { | |
reject(error); | |
}); | |
fwLiteProcess.stdin.write('shutdown\n'); | |
fwLiteProcess.stdin.end(); | |
setTimeout(() => { | |
fwLiteProcess.kill('SIGKILL'); | |
}, 10000); | |
}); | |
} | |
function shutdownFwLite( | |
fwLiteProcess: ReturnType<typeof launchFwLiteFwLiteWeb>['fwLiteProcess'], | |
): Promise<boolean> { | |
return new Promise((resolve) => { | |
// Already exited? | |
if (fwLiteProcess.exitCode !== null) { | |
return resolve(fwLiteProcess.exitCode === 0); | |
} | |
let forced = false; | |
const killTimer = setTimeout(() => { | |
forced = true; | |
try { fwLiteProcess.kill('SIGKILL'); } catch { /* ignore */ } | |
}, 10_000); | |
const onExit = (code: number | null) => { | |
clearTimeout(killTimer); | |
// Treat forced kill as a successful shutdown to avoid failing deactivation. | |
resolve(forced || code === 0); | |
}; | |
const onError = () => { | |
clearTimeout(killTimer); | |
resolve(false); | |
}; | |
fwLiteProcess.once('exit', onExit); | |
fwLiteProcess.once('error', onError); | |
if (fwLiteProcess.stdin && !fwLiteProcess.stdin.destroyed) { | |
try { | |
fwLiteProcess.stdin.write('shutdown\n'); | |
fwLiteProcess.stdin.end(); | |
} catch { | |
// Fall back to forced termination if stdin write fails. | |
forced = true; | |
try { fwLiteProcess.kill('SIGKILL'); } catch { /* ignore */ } | |
} | |
} else { | |
forced = true; | |
try { fwLiteProcess.kill('SIGKILL'); } catch { /* ignore */ } | |
} | |
}); | |
} |
🤖 Prompt for AI Agents
In platform.bible-extension/src/main.ts around lines 296-315, make
shutdownFwLite idempotent and robust: short-circuit and immediately resolve if
the child already has an exitCode or is killed; install a single timeout kill
timer and clear it when the process exits or an error occurs; on 'exit'
resolve(true) for clean (code === 0) or forced termination (non-null signal or
non-zero code after our kill) instead of rejecting, and resolve(false) only for
genuine errors; on 'error' resolve(false) (do not reject); guard writes to stdin
by checking fwLiteProcess.stdin && fwLiteProcess.stdin.writable and wrapping
write/end in try/catch; ensure listeners/timer are cleaned up exactly once by
using a local finished flag to short-circuit further callbacks.
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 already did this
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.
@hahn-kev, I can see in the current code that the shutdownFwLite
function still has the original implementation from the diff. Looking at the current version, it still:
- Uses
reject
instead of onlyresolve
- Doesn't check if the process has already exited
- Doesn't clear the kill timer on exit/error
- Doesn't guard the stdin write operations
The function currently looks like this:
function shutdownFwLite(fwLiteProcess: ReturnType<typeof launchFwLiteFwLiteWeb>['fwLiteProcess']): Promise<boolean> {
return new Promise((resolve, reject) => {
fwLiteProcess.once('exit', (code, signal) => {
if (code === 0) {
resolve(true);
} else {
reject(new Error(`FwLiteWeb exited with code ${code}, signal ${signal}`));
}
});
fwLiteProcess.once('error', (error) => {
reject(error);
});
fwLiteProcess.stdin.write('shutdown\n');
fwLiteProcess.stdin.end();
setTimeout(() => {
fwLiteProcess.kill('SIGKILL');
}, 10000);
});
}
Would you like me to verify if there are any recent commits that haven't been reflected in this view, or are you planning to implement the suggested improvements in a follow-up commit?
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
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.
LGTM 🙃 😬
|
||
function shutdownFwLite(fwLiteProcess: ReturnType<typeof launchFwLiteFwLiteWeb>['fwLiteProcess']): Promise<boolean> { | ||
return new Promise((resolve, _) => { | ||
fwLiteProcess.once('exit', (code, signal) => { |
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.
Does this make the fwLiteProcess.once('exit', ...
in lines 280-282 unnecessary?
It turns out that windows doesn't have a gracefull shutdown of a process outside of Ctrl+C which you can't just send to a process you've spawned. Hence this fix.
shutdownFwLite
function for graceful termination, replacing direct process killing.shutdown
commands via stdin.