下の各ブロックは、Bug Report のテンプレートの同じ名前の欄に入れる。{NEW} は新しい Issue の番号。
bun#44693 として投稿した。投稿したのは英語版。
タイトル
Windows: bun が起動時にコンソールのコードページを 65001 に切り替え、出力を化けさせ、同じ窓のほかのプロセスを壊す(#43660 の根本原因)
bun の版(What version of Bun is running?)
1.4.2+744846f84(1.4.3-canary.1+bbdc5a519 でも再現)
環境(What platform is your computer?)
Microsoft Windows NT 10.0.26300.0 x64
再現手順(What steps can reproduce the bug?)
bun は起動時にコンソールの入力・出力コードページを控えて両方 65001 にし、終了時に控えた値へ戻す(ソースの `init()` と `restore()` へのリンク)。コードページはコンソールに属し、つながる全プロセスで共有されるため、1 つの窓で 2 本の bun が動くと、互いの控えを上書きしてしまう。これが #43660 の根本原因。
**新しい `cmd.exe` の窓**(65001 以外ならどのコードページでもよい。英語版 Windows は 437、日本語版は 932)を開き、次を流す:
(英語版と同じコマンド)
`ping` とタイマーは順番を決めるためだけのもの。送り手が先に起動し、受け手が約 1 秒後に起動し、受け手が動いているあいだに送り手が終わる。毎回再現した(437 で 3 回中 3 回、932 で 3 回中 3 回。両方の版)。
期待する動き(What is the expected behavior?)
文字列が正しく出て、窓のコードページは変わらない。両方の `bun -e` を `node -e` にしたときの出力(`deno eval` にしても同じ):
abc 東京大阪 xyz
Active code page: 437
実際(What do you see instead?)
437 の窓:
abc 東京大阪 xyz
Active code page: 65001
932 の窓(日本語版 Windows):
abc 譚ア莠ャ螟ァ髦ェ xyz
Active code page: 65001
起きていること:
1. 送り手が起動し、437 を控えて 65001 にする
2. 受け手が起動し、65001 を「元の値」として控える
3. 送り手が終わり、437 に戻す
4. 受け手が、437 になった窓へ UTF-8 のバイト列をそのまま書くので化ける
5. 受け手が終わり、65001 に戻す。2 本とも終わったあとも 65001 が窓に残る
bun#44693 の一部として投稿した。投稿したのは英語版。
**依頼: コンソールのコードページを一切変えないでほしい。** 画面への出力は `WriteConsoleW`(UTF-8 → UTF-16)、パイプ・ファイルへはバイト列をそのまま、コンソールからの入力は `ReadConsoleW` で読む。どれもコードページに依存しない。node と deno は一度も変えず、下のすべてのケースで正しく出す。GitHub のコード検索では、リポジトリに `WriteConsoleW` の使用は見つからない。
控えて戻すやり方は安全にできない。1 つの窓で複数のプロセスが同じ共有の値を控えては戻すので、終わる順で何が残るかが決まる。強制終了では戻す処理そのものが走らない(ソースのコメントにも「強制終了されると戻らないことがある」と書かれている)。ソースへのリンクはいまの `main` を指す。
**測った害**(Windows 11 10.0.26300、conhost、開始のコードページ 932 と 437 の窓、bun 1.4.2 と canary。比べる相手は node v26.10.0 と deno 2.9.7。各プロセスが自分で `GetConsoleCP` / `GetConsoleOutputCP` を読み、画面は `ReadConsoleOutputCharacterW` で読み取った)。node と deno ではどれも起きず、順番を決めたケースの判定は 932 と 437 ですべて同じだった。
1. **パイプでつないだ相手の出力が化ける**(#43660 の症状)。bun 同士が直接つながっていなくても起きる(`bun | node | bun`)
2. **回ごとに結果が変わる。** #43660 のコマンドを毎回新しい窓で 10 回流すと、932 では 1.4.2 が 6 回、canary が 7 回、437 では 3 回と 9 回化けた。回数も流すたびに変わる。node と deno は毎回正しかった。起動・終了の順を決めると、どの順も 3 回中 3 回同じ結果になった
3. **窓に 65001 が残る。** 通常終了でも順番によって残り、強制終了(`taskkill /F`)でも残る
4. **65001 が残ると、同じ窓のほかのプログラムが壊れる。** `cmd.exe` が SJIS のバッチファイルの残りを読み違え(日本語の行が `�������` になる)、`chcp` や `pause` のメッセージが英語になる
5. **パイプのデータそのものは壊れない。** 害はすべて、共有されたコンソールの状態を変えることから起きている
**証拠**
- 全結果(表 ・ 時系列 ・ 画面。結果の 1 つ 1 つから証拠ファイルと試験コードへリンク): https://lightspeedc.com/20261003-bun-codepage-problem/research/codepage-test-results.html
- 上のすべてを新しい窓で再現するテストコード: [`tests/codepage.test.ts`](https://github.com/LightSpeedC/20261003-bun-codepage-problem/blob/5b9f56b36bce4914d1bf90eff7e755022ccd1967/tests/codepage.test.ts)(リポジトリ: https://github.com/LightSpeedC/20261003-bun-codepage-problem )
- どの Windows の PC でも全件を流せる(版を決めた bun ・ node ・ deno ・ pwsh をリポジトリの中に取り、PC に入っているものは使わない): `tools\10_setup\setup-runtimes.cmd` のあと `tools\40_test\run-tests.cmd`
- 関連: #43660
bun#43660 へのコメントとして投稿した。{NEW} は 44693 にした。投稿したのは英語版。
続報: ここで報告した文字化けは、もっと広い問題の症状の 1 つだった。bun は起動時にコンソールのコードページを控えて変え、終了時に戻すが、コードページは窓の全プロセスで共有されるため、同時に動く bun 同士が互いの控えを上書きする。
順番を決めた再現(英語版 Windows の 437 でも再現する)と、node ・ deno と比べた実測を #{NEW} に出した。bun がコードページを変えなくなれば、この Issue も解消する。
bun#44693 へのコメントとして投稿した。この Issue を出したとき、bun#43660 への Open の修正 PR #43662 を見落としていた。そのあと PR の版を同じ試験一式で測った。投稿したのは英語版。
続報: この Issue を出したとき、#43660 にすでに Open の修正 #43662 があることを見落としていた。その CI の Windows x64 のビルド(1.4.3-canary.1+576eb251a)を、同じ試験一式で、コードページ 932 と 437 で始めた窓で流した。
**#43662 で直るもの**(どちらのコードページでも全ケース):
- `console.log` / `process.stdout` の出力は、どの起動・終了の順でも化けなくなった。#43660 のコマンド(20 回とも正しい。各コードページで 10 回)と `bun | node | bun` も含む。
- 起動・終了の順を決めた形では、通常終了のあとに 65001 が残らなくなった。
**#43662 のあとも残るもの:**
1. bun は起動時に窓を 65001 に切り替えたまま(入力と出力。プロセスの中から読んだ値)。bun が動いているあいだ、窓のほかのプロセスはすべて 65001 にさらされる。
2. 強制終了(`taskkill /F`)では 65001 が残り、そのあと同じ窓で流すバッチファイルは読み違えられる。
3. `fs.writeSync(1)` ・ `fs.write(1)` ・ `Bun.write(Bun.stdout)` は `WriteFile` のまま。送り手が先に終わる順では化ける(どの API も、932 と 437 で 3 回中 3 回)。PR の説明文とレビューにもそう書かれている。
4. 「自分が変えたコードページだけを戻す」でも、競合に負けることがある。#43660 のコマンドを 20 回流したうち 1 回、2 本とも終わったあと、入力側は 932 に戻ったのに、出力側は 65001 のまま残った。考えられる順: 送り手が 65001 にしたあとで受け手が出力側の値を控え、送り手が終わって 932 に戻し、そのあと受け手が 65001 にする。受け手は 65001 を控えているので、戻さない。
なので、ここでの依頼は変わらない。コンソールのコードページを一切変えず、画面への出力はすべて `WriteConsoleW` で書いてほしい。そうすれば控えて戻す処理は要らず、強制終了でも何も残らない。
#43662 の列を加えた結果(結果の 1 つ 1 つから証拠ファイルへリンク): https://lightspeedc.com/20261003-bun-codepage-problem/research/codepage-test-results-JP.html#ch02
PR #43662 へのコメントとして投稿した。投稿したのは英語版。
この PR の Windows x64 のビルド(1.4.3-canary.1+576eb251a)を、再現できる試験一式で、コードページ 932 と 437 で始めた窓で流した。Bun 1.4.2 ・ canary ・ Node.js ・ Deno と並べている。
#43660 は直っている。`console.log` / `process.stdout` の出力は、どの起動・終了の順でも正しかった。#43660 のコマンドも含む(20 回中 20 回)。
残るものが 4 つある:
1. bun は起動時に窓を 65001 にしたまま。bun が動いているあいだ、同じ窓のほかのプロセスにも 65001 が見える。
2. 強制終了(`taskkill /F`)では 65001 が残る。
3. `fs.writeSync(1)` ・ `fs.write(1)` ・ `Bun.write(Bun.stdout)` は `WriteFile` のままで、送り手が先に終わると化ける。
4. 「自分が変えたときだけ戻す」判定は、まだ競合に負けることがある。20 回中 1 回、出力側のコードページが 65001 のまま残った。
コンソールのコードページを一切変えないよう依頼する #44693 を出した。詳しい内容と証拠はそちら。
65001 が残った回について、窓につながる全プロセスの起動と終了の順を求めた robobun の返信への答え。PR #43662 への返信として投稿した。投稿したのは英語版。
詳しい回答をありがとうございます。
**1〜3:** 了解しました。起動時の 65001 の件は #44693 でメンテナーの判断を待ちます。`fs.writeSync` / `fs.write` / `Bun.write` は、#42819 のあとに `src/sys/windows/fs.rs::write_at` で手当てされる、と理解しました。
**4:** 同じ版(`1.4.3-canary.1+576eb251a`、開始は 932)で、順番を決めない形を 60 回流し直しました。同じ窓につないだ node の見張り役が、プロセスの出入りとコードページの変化を、すべて UTC の時刻で記録しました。
60 回中 3 回で、コードページが変わったまま残りました。入力 932 / 出力 65001 が 1 回、入力 65001 / 出力 932 が 2 回です。3 回とも、窓にいたのは cmd.exe ・ node の計測 ・ `chcp` ・ bun 2 本だけでした。65001 の間に起動した 3 本目のプロセスはいません。どの回も、2 本目の bun は、1 本目が 65001 にした 8〜11 ms 後に起動していました。
時系列 ・ 証拠 ・ 見張り役のコード:
https://lightspeedc.com/20261003-bun-codepage-problem/research/codepage-test-results-JP.html#asis-timeline
3 回の時系列の裏にある順番を、PR のソースから導いたもの。PR #43662 への続報として投稿した。投稿したのは英語版。
4 点目の続報です。3 回の時系列を PR のソース(先頭 `576eb251a`)と突き合わせました。どの回も、それを生む順番は 1 つしかありません。
`init()`(output.rs 581〜585 行)は、4 つの別々の呼び出しで動きます。出力側を控える、出力側を 65001 にする、入力側を控える、入力側を 65001 にする、の 4 つです。`restore()`(536〜544 行)は、それぞれを控えた値へ戻します。ただし、控えた値が 65001 の側は戻しません。W は送り手(先に起動)、R は受け手です。
**入力側が 65001 で残る(45・51 回目):**
1. W が 932 を控えて、65001 にする。
2. R が入力側を控える: 65001。
3. W が終わり、入力側を 932 に戻す。
4. R が入力側を 65001 にする。
5. R が終わる。控えた入力側が 65001 なので、戻さない。
**出力側が 65001 で残る(19 回目):** 出力側で同じ順番が起きました。R が出力側を控え(65001)、W が 932 に戻し、そのあと R が 65001 にします。この回で入力側が戻ったのは、R が入力側を控えたのが、W が戻した後だったためです。
つまり、この競合に 3 本目のプロセスは要りません。1 本目の後始末が、2 本目の「控える」と「変える」の間に入れば、2 本目は 65001 を「元の値」として控え、`!= CP_UTF8` の判定で戻さなくなります。窓を共有するプロセスの間では「控えてから変える」を 1 つの操作にできないので、プロセスごとの判定でこの隙間を塞ぐ方法は見当たりません。#44693 で、コードページを一切変えないよう頼んでいるのは、このためです。
これはソースと時刻からの推論で、呼び出しを 1 つずつ記録したものではありません。見張り役には、どのプロセスが呼んだかが見えません。また、R の「控える」と「変える」の間は、読み取りの間隔より短いです。時系列と導き方: https://lightspeedc.com/20261003-bun-codepage-problem/research/codepage-test-results-JP.html#asis-timeline