fix: implement dynamic locale detection and enhance error logging#26
Conversation
a45a47e to
106639b
Compare
|
Hey @ken-jo, thanks for your pull request! Sorry for only getting back to you now. This looks promising and I'm inclined to merge it. The only open question is that |
There was a problem hiding this comment.
Pull request overview
This PR improves embedded-postgres initialization robustness across minimal environments by selecting an available locale at runtime and by surfacing initdb stderr output to make failures easier to diagnose.
Changes:
- Add runtime locale detection (via
locale -a) and setLC_MESSAGES_LOCALEdynamically. - Capture and log
initdbstderr output and include it in the rejection message on failure.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const availableLocales = execSync('locale -a', { encoding: 'utf-8' }); | ||
| if (availableLocales.includes('en_US.UTF-8')) return 'en_US.UTF-8'; | ||
| if (availableLocales.includes('C.UTF-8')) return 'C.UTF-8'; | ||
| if (availableLocales.includes('en_US.utf8')) return 'en_US.utf8'; |
There was a problem hiding this comment.
getBestLocale() uses substring checks (includes) on the full locale -a output. This can return a locale that is not actually installed (e.g., if only en_US.UTF-8@something exists, includes('en_US.UTF-8') is true), causing initdb to still fail. Consider splitting by line and doing exact matches (or a regex anchored to line boundaries) before selecting the locale.
| const availableLocales = execSync('locale -a', { encoding: 'utf-8' }); | |
| if (availableLocales.includes('en_US.UTF-8')) return 'en_US.UTF-8'; | |
| if (availableLocales.includes('C.UTF-8')) return 'C.UTF-8'; | |
| if (availableLocales.includes('en_US.utf8')) return 'en_US.utf8'; | |
| const localeOutput = execSync('locale -a', { encoding: 'utf-8' }); | |
| const availableLocales = new Set( | |
| localeOutput | |
| .split(/\r?\n/) | |
| .map((locale) => locale.trim()) | |
| .filter(Boolean), | |
| ); | |
| if (availableLocales.has('en_US.UTF-8')) return 'en_US.UTF-8'; | |
| if (availableLocales.has('C.UTF-8')) return 'C.UTF-8'; | |
| if (availableLocales.has('en_US.utf8')) return 'en_US.utf8'; |
| const LC_MESSAGES_LOCALE = getBestLocale(); | ||
|
|
There was a problem hiding this comment.
getBestLocale() is executed at module load via const LC_MESSAGES_LOCALE = getBestLocale();, which runs a synchronous shell command (execSync('locale -a')) on import. This can add startup latency and can be problematic in restricted environments. Prefer computing this lazily (e.g., during initialise()/start(), or caching on first use) and/or using an async variant to avoid blocking the event loop.
| const LC_MESSAGES_LOCALE = getBestLocale(); | |
| let lcMessagesLocale: string | undefined; | |
| function getLcMessagesLocale(): string { | |
| if (lcMessagesLocale === undefined) { | |
| lcMessagesLocale = getBestLocale(); | |
| } | |
| return lcMessagesLocale; | |
| } |
| @@ -163,20 +174,24 @@ class EmbeddedPostgres { | |||
| ], { ...permissionIds, env: { LC_MESSAGES: LC_MESSAGES_LOCALE } }); | |||
There was a problem hiding this comment.
The spawn(..., { env: { LC_MESSAGES: LC_MESSAGES_LOCALE } }) options object replaces the child process environment entirely, dropping process.env (PATH, LANG, etc.). This can cause initdb to behave differently across environments. Merge with the parent env (e.g., env: { ...process.env, LC_MESSAGES: LC_MESSAGES_LOCALE }) rather than overwriting it.
| ], { ...permissionIds, env: { LC_MESSAGES: LC_MESSAGES_LOCALE } }); | |
| ], { ...permissionIds, env: { ...process.env, LC_MESSAGES: LC_MESSAGES_LOCALE } }); |
| process.stderr?.on('data', (chunk: Buffer) => { | ||
| stderrOutput += chunk.toString('utf-8'); | ||
| this.options.onLog(`[STDERR] ${chunk.toString('utf-8')}`); | ||
| }); | ||
|
|
||
| process.on('exit', (code) => { | ||
| if (code === 0) { | ||
| resolve(); | ||
| } else { | ||
| reject(`Postgres init script exited with code ${code}. Please check the logs for extra info. The data directory might already exist.`); | ||
| reject(`Postgres init script exited with code ${code}. ERROR OUTPUT: ${stderrOutput}`); | ||
| } | ||
| }); | ||
| }); | ||
| }); }); |
There was a problem hiding this comment.
The promise waits for the child process 'exit' event, but 'exit' can fire before stdio streams have fully flushed/closed, so stderrOutput may be incomplete. Use the 'close' event (and include signal if present) to ensure all output is captured before resolving/rejecting.
| }); }); | ||
|
|
||
| // Clean up the file | ||
| await fs.unlink(passwordFile); |
There was a problem hiding this comment.
The password file is deleted only after the init promise resolves. If initdb fails and the promise rejects, await fs.unlink(passwordFile) is skipped and the plaintext password file may be left behind. Wrap the initdb call in a try/finally (and best-effort unlink in finally) to guarantee cleanup on both success and failure.
| } | ||
| }); | ||
| }); | ||
| }); }); |
There was a problem hiding this comment.
There appears to be a formatting issue on the promise close brace line (}); });), which makes the block hard to read and may violate the project's formatter/linter rules. Please reformat so the closing braces/parentheses are on separate, properly indented lines.
| }); }); | |
| }); | |
| }); |
| function getBestLocale(): string { | ||
| try { | ||
| const availableLocales = execSync('locale -a', { encoding: 'utf-8' }); | ||
| if (availableLocales.includes('en_US.UTF-8')) return 'en_US.UTF-8'; | ||
| if (availableLocales.includes('C.UTF-8')) return 'C.UTF-8'; | ||
| if (availableLocales.includes('en_US.utf8')) return 'en_US.utf8'; | ||
| } catch { | ||
| // Fallback to POSIX C locale | ||
| } | ||
| return 'C'; | ||
| } | ||
| const LC_MESSAGES_LOCALE = getBestLocale(); |
There was a problem hiding this comment.
The new locale-selection logic and the new stderr-in-error behavior aren’t covered by tests. Since this package already has Vitest coverage for initialise()/start(), consider adding tests that mock child_process.execSync (and/or spawn) to verify locale prioritization/fallback and that rejected initdb errors include captured stderr.
- add Windows guard for locale detection - use exact locale matching and per-run locale resolution - merge child env vars, use close event, and ensure pwfile cleanup
|
Thanks for the feedback. After reviewing the Copilot recommendations, I confirmed they align with your requested requirements, so I went ahead and applied those improvements in this PR. I also tested the updated changes on my local Windows machine and verified that they pass. |
|
@leinelissen Plz check again :) |
Description
This PR addresses an issue where
initdbfails on systems that do not have theen_US.UTF-8locale installed (e.g., some WSL environments or minimal Docker images). It also improves the developer experience by capturing and includingstderrin error messages.Changes
en_US.UTF-8, the library now checks available locales usinglocale -aand prioritizesen_US.UTF-8>C.UTF-8>en_US.utf8>C. This ensures compatibility while maintaining English output for parsing.spawnlogic forinitdbto capture the standard error stream. If initialization fails, the fullstderroutput is now included in the rejection message, making it much easier to diagnose issues.Verification
en_US.UTF-8.