Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
101 changes: 71 additions & 30 deletions packages/embedded-postgres/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -152,34 +172,48 @@ class EmbeddedPostgres {
ensureBinIsExecutable(initdb);

// Initialize the database
await new Promise<void>((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<void>((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);
}
}

/**
Expand All @@ -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()
Expand All @@ -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) => {
Expand Down
Loading