diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4d7bade..a58af45 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,9 +26,14 @@ jobs: run: | cd packages/embedded-postgres npm test + - name: Generate NL locale + if: matrix.os == 'ubuntu-latest' + run: | + sudo locale-gen nl_NL.UTF-8 + sudo update-locale - name: Run test (NL locale) env: - LC_ALL: nl_NL.utf8 + LC_ALL: nl_NL.UTF-8 if: matrix.os != 'windows-latest' run: | cd packages/embedded-postgres diff --git a/packages/embedded-postgres/src/index.ts b/packages/embedded-postgres/src/index.ts index 92f46ec..b11391b 100644 --- a/packages/embedded-postgres/src/index.ts +++ b/packages/embedded-postgres/src/index.ts @@ -2,7 +2,7 @@ import path from 'path'; import crypto from 'crypto'; import fs from 'fs/promises'; import { platform, tmpdir, userInfo } from 'os'; -import { ChildProcess, spawn, exec } from 'child_process'; +import { ChildProcess, spawn, exec, execSync } from 'child_process'; import pg from 'pg'; import AsyncExitHook from 'async-exit-hook'; @@ -19,7 +19,26 @@ const { Client } = pg; * for a particular string, we need to force that string into the right locale. * @see https://github.com/leinelissen/embedded-postgres/issues/15 */ -const LC_MESSAGES_LOCALE = 'en_US.UTF-8'; +function getBestLocale(): string { + // `locale -a` is not available on Windows. + if (platform() === 'win32') { + return 'C'; + } + try { + const availableLocales = new Set( + execSync('locale -a', { encoding: 'utf-8' }) + .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'; + } catch { + // Fallback to POSIX C locale + } + return 'C'; +} /** * Previosuly, options were specified in snake_case rather than camelCase. Old @@ -104,6 +123,7 @@ class EmbeddedPostgres { */ async initialise() { const { postgres, initdb } = await bin; + const locale = getBestLocale(); // GUARD: Check that a postgres user is available await this.checkForRootUser(); @@ -152,34 +172,48 @@ class EmbeddedPostgres { ensureBinIsExecutable(initdb); // Initialize the database - await new Promise((resolve, reject) => { - const process = spawn(initdb, [ - `--pgdata=${this.options.databaseDir}`, - `--auth=${this.options.authMethod}`, - `--username=${this.options.user}`, - `--pwfile=${passwordFile}`, - `--lc-messages=${LC_MESSAGES_LOCALE}`, - ...this.options.initdbFlags, - ], { ...permissionIds, env: { LC_MESSAGES: LC_MESSAGES_LOCALE } }); - - // Connect to stderr, as that is where the messages get sent - process.stdout?.on('data', (chunk: Buffer) => { - // Parse the data as a string and log it - const message = chunk.toString('utf-8'); - this.options.onLog(message); - }); - - 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.`); - } + try { + await new Promise((resolve, reject) => { + const childProcess = spawn(initdb, [ + `--pgdata=${this.options.databaseDir}`, + `--auth=${this.options.authMethod}`, + `--username=${this.options.user}`, + `--pwfile=${passwordFile}`, + `--lc-messages=${locale}`, + ...this.options.initdbFlags, + ], { + ...permissionIds, + env: { + ...process.env, + LC_MESSAGES: locale, + }, + }); + + // Connect to stderr, as that is where the messages get sent + let stderrOutput = ''; + childProcess.stdout?.on('data', (chunk: Buffer) => { + const message = chunk.toString('utf-8'); + this.options.onLog(message); + }); + + childProcess.stderr?.on('data', (chunk: Buffer) => { + const message = chunk.toString('utf-8'); + stderrOutput += message; + this.options.onLog(`[STDERR] ${message}`); + }); + + childProcess.on('close', (code, signal) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Postgres init script failed (code: ${code ?? 'null'}, signal: ${signal ?? 'null'}). ERROR OUTPUT: ${stderrOutput}`)); + } + }); }); - }); - - // Clean up the file - await fs.unlink(passwordFile); + } finally { + // Clean up the file even when initdb fails + await fs.unlink(passwordFile).catch(() => undefined); + } } /** @@ -189,6 +223,7 @@ class EmbeddedPostgres { */ async start() { const { postgres } = await bin; + const locale = getBestLocale(); // Optionally retrieve the uid and gid const permissionIds = await this.getUidAndGid() @@ -207,7 +242,13 @@ class EmbeddedPostgres { '-p', this.options.port.toString(), ...this.options.postgresFlags, - ], { ...permissionIds, env: { LC_MESSAGES: LC_MESSAGES_LOCALE } }); + ], { + ...permissionIds, + env: { + ...process.env, + LC_MESSAGES: locale, + }, + }); // Connect to stderr, as that is where the messages get sent this.process.stderr?.on('data', (chunk: Buffer) => {