diff --git a/libraries/node-core-library/src/Executable.ts b/libraries/node-core-library/src/Executable.ts index 97edafde57f..ceeefebd4db 100644 --- a/libraries/node-core-library/src/Executable.ts +++ b/libraries/node-core-library/src/Executable.ts @@ -597,6 +597,13 @@ export class Executable { const { exitCode, signal } = await new Promise( (resolve: (result: ISignalAndExitCode) => void, reject: (error: Error) => void) => { if (encoding) { + // Set the encoding on the streams to ensure proper handling of multi-byte characters. + // When encoding is set, Node.js uses StringDecoder internally which properly handles + // character boundaries that may be split across chunks. + if (encoding !== 'buffer') { + childProcess.stdout!.setEncoding(encoding); + childProcess.stderr!.setEncoding(encoding); + } childProcess.stdout!.on('data', (chunk: Buffer | string) => { collectedStdout.push(normalizeChunk(chunk)); }); @@ -667,6 +674,8 @@ export class Executable { if (process.stdout === null) { throw new InternalError('Child process did not provide stdout'); } + // Set the encoding to ensure proper handling of multi-byte characters + process.stdout.setEncoding('utf8'); const [processInfoByIdMap] = await Promise.all([ parseProcessListOutputAsync(process.stdout), // Don't collect output in the result since we process it directly diff --git a/libraries/node-core-library/src/test/Executable.test.ts b/libraries/node-core-library/src/test/Executable.test.ts index 2a6d6b6fac8..0f858838075 100644 --- a/libraries/node-core-library/src/test/Executable.test.ts +++ b/libraries/node-core-library/src/test/Executable.test.ts @@ -350,6 +350,30 @@ describe('Executable process tests', () => { Executable.waitForExitAsync(childProcess, { encoding: 'utf8', throwOnSignal: true }) ).rejects.toThrowError(/Process terminated by SIGTERM/); }); + + test('Executable.waitForExitAsync() handles multi-byte UTF-8 characters correctly', async () => { + // Test that multi-byte characters are properly decoded even when split across chunks + const executablePath: string = path.join(executableFolder, 'multibyte', 'output-multibyte.js'); + const childProcess: child_process.ChildProcess = Executable.spawn(process.argv0, [executablePath], { + environment, + currentWorkingDirectory: executableFolder, + stdio: ['ignore', 'pipe', 'pipe'] + }); + + const result: IWaitForExitResult = await Executable.waitForExitAsync(childProcess, { + encoding: 'utf8' + }); + + expect(result.exitCode).toEqual(0); + expect(result.signal).toBeNull(); + expect(typeof result.stdout).toEqual('string'); + + // The output should contain properly decoded multi-byte characters + // Chinese characters (δΈ–η•Œ) and emoji (πŸŽ‰) + expect(result.stdout).toContain('Hello, δΈ–η•Œ! πŸŽ‰'); + // Ensure no replacement characters (οΏ½) which would indicate improper decoding + expect(result.stdout).not.toContain('οΏ½'); + }); }); describe('Executable process list', () => { diff --git a/libraries/node-core-library/src/test/test-data/executable/multibyte/output-multibyte.js b/libraries/node-core-library/src/test/test-data/executable/multibyte/output-multibyte.js new file mode 100644 index 00000000000..37215251b12 --- /dev/null +++ b/libraries/node-core-library/src/test/test-data/executable/multibyte/output-multibyte.js @@ -0,0 +1,20 @@ +// This script writes multi-byte UTF-8 characters byte by byte to test proper decoding +const { setTimeout } = require('node:timers/promises'); + +const unicodeString = "Hello, δΈ–η•Œ! πŸŽ‰"; // "Hello, World" in Chinese with emoji +const encoded = Buffer.from(unicodeString, 'utf8'); + +async function writeChars() { + // Write each byte individually to force chunks that split multi-byte characters + for (let i = 0; i < encoded.length; i++) { + process.stdout.write(encoded.subarray(i, i + 1)); + // Small delay to ensure each byte is in a separate chunk + await setTimeout(1); + } + process.stdout.write('\n'); +} + +writeChars().catch((error) => { + console.error('Error:', error); + process.exit(1); +});