bun#43660 で、cp932 の窓では bun | bun の出力が化け、終了後に 65001 が残ると報告した。これは症状の 1 つだった。
bun は実行中にコンソールの入力・出力コードページを書き換える。コードページは同じ窓の全プロセスで共有されるため、bun の外にまで影響が及ぶ。
✅ 観測済み / ⬜ 未測定。
| 害 | 証拠 | 状態 |
|---|---|---|
| H1 | パイプでつないだ相手の表示が化ける(bun#43660 の症状)。調査の 1.4.2・canary 版、T2・T5 | ✅ 観測済み |
| H2 | 結果が実行のたびに変わる。起動・終了の順で決まる。調査の 3 回の差、T5 | ✅ 観測済み |
| H3 | 終了後に 65001 が窓に残る。調査、T1・T5 | ✅ 観測済み |
| H4 | 同じ窓の別のプロセスが巻き込まれる。cmd が SJIS のスクリプトを読み違え、pause などのメッセージが英語になる。調査、T4 | ✅ 観測済み |
| H5 | ほかの bun が窓を 932 に戻した後に、bun が UTF-8 のバイト列をそのまま書いて化ける(WriteConsoleW を使っていない)。調査の 1.4.2 の 3 回目、T5 | ✅ 観測済み |
| H6 | 強制終了すると元に戻らない。T3 | ✅ 観測済み |
| H7 | bun 同士が直接つながっていなくても起きる(bun | node | bun)。T5 | ✅ 観測済み |
観測した内容は コードページ試験の結果 と bun#43660 の再現 にある。H2 の原因(起動・終了の順で決まる)は T5 で確かめた。
コンソールのコードページを変えないでほしい。画面への出力は WriteConsoleW で UTF-16 のまま渡し、パイプ・ファイルにはバイト列をそのまま書けば、コードページに依存しない(「あるべき姿」)。node と deno が変えずに正しく動くことを、実装できる裏付けにする(T1・T2)。
コードページを変えなければ、H1〜H7 はどれも起きない。bun#43660 の症状(H1)もここに含まれる。
日本語版 Windows で、開いた直後の cmd の窓(コードページ 932)を基準にする。テストはコードページに一切触らない。
conhost.exe で新しいコンソールを起こし、その中で走らせる。結果はファイルに書かせて読む。conhost.exe を名指しするのは、既定の端末が Windows Terminal だと新しい窓も ConPTY 配下になりうるため(未検証)。bun#43660 も Windows Terminal ではなく cmd.exe を直接使った条件で報告されている。| 相手 | 書き方・読み方 |
|---|---|
| コンソールへの出力 | WriteConsoleW で UTF-16 のまま渡す |
| パイプ・ファイルへの出力 | UTF-8 のバイト列をそのまま書く |
| コンソールからの入力 | ReadConsoleW で UTF-16 として読む |
| パイプからの入力 | 届いたバイト列をそのまま受け取る |
どれもコードページに依存しないため、書き換える理由がない。T1 で、node と deno がコードページを一度も変えずに 932 の窓へ日本語を正しく出すことを確かめた(内部で WriteConsoleW を使っているかまでは見ていない)。
| 報告されていること | この環境 |
|---|---|
| 開始時のコードページが 65001 でないと、bun | bun で文字化けする | 再現した(安定版・canary 版とも) |
| 片方が node なら起きない | 確かめた(T2: 片方が node か deno なら 65001 は残らない) |
| 終了後に窓が 65001 のまま残る | 再現した。ただし残らない回もあった |
| ランタイム | 版 | 役割 |
|---|---|---|
| bun | 1.4.3+c6da4a4d3 | 調べる対象(安定版) |
| bun canary | 準備の時点の最新(全件の試験では 1.4.3-canary.1+bbdc5a519) | 調べる対象。_bin/bun-canary/ に置き、PATH を入れ替えて呼ぶ |
| bun PR #43662 | 1.4.3-canary.1+6b5271cb1 | bun#43660 への修正の PR の版。何が直り、何が残るかを見る。_bin/bun-pr-43662/ に置き、PATH を入れ替えて呼ぶ |
| node | v26.10.0 | 比べる相手 |
| deno | 2.9.7 | 比べる相手 |
「あるべき姿」の実装なら、全項目で次のとおりになる。
| 項目 | 示す害 | 期待値 |
|---|---|---|
| T1 | H3 | 実行前・実行中・終了後のすべてで、入力・出力とも 932 のまま |
| T2 | H1 | 27 通りすべてで、受け手に UTF-8 のバイト列がそのまま届く(Windows PowerShell 5.1 経由の分は node | node と同じ結果になる) |
| T3 | H6 | 強制終了の後も 932 のまま |
| T4 | H4 | SJIS の cmd が日本語を正しく出す |
| T5 | H1・H2・H3・H7 | 起動・終了の順に関わらず、画面は abc 東京大阪 xyz と正しく出て、終了後も 932 のまま |
| T6 | 全部 | 安定版・canary 版とも、T1〜T5 の期待値どおり |
基準の窓で、実行前・実行中・終了後の GetConsoleCP / GetConsoleOutputCP を記録する。実行中の値は各ランタイム自身が FFI で読む(読むだけ)。bun は bun:ffi、node は koffi、deno は Deno.dlopen。期待値は全時点で 932。
| 軸 | 値 |
|---|---|
| 送り手 | bun / node / deno |
| 受け手 | bun / node / deno |
| 起動元 | cmd / pwsh 7 / Windows PowerShell 5.1(いずれも基準の窓の中から起動) |
3 × 3 × 3 = 27 通り。受け手は受け取ったバイト列をそのままファイルへ書き、期待する UTF-8 のバイト列と突き合わせる。画面の見た目では判定しない。
? になる既知の事象がある。5.1 の行は node | node を基準にし、bun 固有の差だけを見る。待機中の bun を taskkill /F で止め、窓が 932 に戻っているかを確かめる。
bun | bun の直後(通常終了・強制終了)に、SJIS で書いた cmd が日本語を正しく出すかを確かめる。
2 本以上のプロセスが起動・終了する順番を待ち時間で決め、偶然に頼らずに結果を出す。各プロセスは起動直後・書く直前・終了直前に GetConsoleCP / GetConsoleOutputCP を時刻つきでログへ書く(読むだけ)。送る文字列は abc 東京大阪 xyz にする。偶数の文字数(4 文字・12 バイト)で、CP932 として読まれても 2 バイトずつ別の文字に読み替わり、壊れたバイトが残らない。化けると abc 譚ア莠ャ螟ァ髦ェ xyz になるため、前後の ASCII と改行は残り、正しい形と化けた形を機械的に見分けられる(node の TextDecoder('shift_jis') による計算。コンソールの表示とは未照合)。
| 形 | 順番の決め方 | 確かめること |
|---|---|---|
bun | bun | 起動の順は片側の起動前に ping -n 2 127.0.0.1 >nul、終了の順はスクリプト内の setTimeout | 起動の順 × 終了の順 × 画面に書く/書かない |
bun | node wait | bun | 間の node が受け取ったデータを決めた時間だけ持ってから流す | bun 同士が直接つながっていなくても起きるか(H7) |
node | node wait | node | 同上 | 比べる相手 |
deno | node wait | deno | 同上 | 比べる相手 |
| bun#43660 と同じ形 | 順番を決めない(-e で 2 本を同時に起動)。10 回流す | 結果が回ごとに変わるか(H2) |
| ww を別の書き方で | ww の順で、受け手が fs.writeSync(1) ・ fs.write(1) ・ Bun.write(Bun.stdout) で画面に書く。bun の版だけ | PR #43662 が手当てしていない書き方で化けるか |
仮説(各 bun が起動時に今の値を控えて 65001 にし、終了時に控えた値へ戻す)から予想する bun | bun の結果:
| 起動の順 | 終了の順 | 画面 | 終了後 |
|---|---|---|---|
| 送り手が先 | 送り手が先 | 化ける | 65001 が残る |
| 送り手が先 | 受け手が先 | 正しい | 932 に戻る |
| 受け手が先 | 送り手が先 | 正しい | 932 に戻る |
| 受け手が先 | 受け手が先 | 正しい | 65001 が残る |
各組を 3 回ずつ流し、回の順に結果を並べる。経緯は bun#43660 の再現 を参照。
T1〜T5 を bun の安定版と canary 版の両方で流す。node ・ deno は基準として版を 1 つに固定し、記録する。
tests/
codepage.test.ts T1〜T5 本体(node:test、bun test でも流す)。bun と canary を同じ表の列に並べて T6 にする
fixtures/
console.ts コードページを読むだけの共通部品(bun:ffi / koffi / Deno.dlopen)
emit.ts abc 東京大阪 xyz を console.log で出す送り手
sink.ts 受け取ったバイト列を画面かファイルへそのまま書く受け手
hold.ts 受け取ったデータを決めた時間だけ持ってから流す中継(T5 の node wait)
probe.ts 観測役(node)。コードページを記録し、窓の画面の文字を読み取る
neighbor-932.cmd 932 用の隣のプロセス役(SJIS + CRLF)
neighbor-437.cmd 437 用の隣のプロセス役(CP437 + CRLF)
harness/
new-console.ts conhost.exe で新しいコンソールを起こし、その中で実行して終わるまで待つ
judge.ts 画面・バイト列・コードページの判定と、証拠の置き場の results.jsonl への記録
manual/
issue-43660-repro.cmd bun#43660 と同じ形のコマンドを手で流す(画面を目で見る)
tools/10_setup/
setup-runtimes.ps1 / .cmd 準備フェーズ。ランタイムを _bin/ に取り、npm ci を流す
fetch-bun-pr.ts bun の PR の Windows x64 のビルドを、bun の CI(Buildkite)から取る
tools/40_test/
run-tests.ps1 / run-tests.cmd 試験実施フェーズ。932 と 437 で、node --test と bun test を流す
tools/50_run/
summarize-results.ts 証拠の置き場の記録から、結果表の材料を出す
compare-evidence.ts research/evidence_last/ と research/evidence/ の判定を比べる
build-result-tables.ts research/evidence/ から結果資料の表を作り、証拠ファイルと試験コードへリンクする
verify-issue-repro.ts Issue の本文に載せる再現コマンドを新しい窓で流し、画面を出す
.ts にするtmp/timeout を付けるReadConsoleOutputCharacterW で窓の文字を読み取って行う。人が目で見なくてよいリポジトリを clone した人が、Windows なら誰でも同じ試験を流せるようにする。その PC に入っている bun ・ node ・ deno には頼らない。
tools/10_setup/setup-runtimes.cmd(中身は同名の ps1)が、版を決めた実行ファイルを公式の配布元から取り、_bin/ に置く。_bin/ は Git の管理外。すでに置いてあれば取り直さない。
| ランタイム | 版 | 置き場 |
|---|---|---|
| bun | 1.4.3 | _bin/bun/ |
| bun canary | 取った時点の最新(版を固定できないので、取った版を記録する) | _bin/bun-canary/ |
| node | v26.10.0 | _bin/node/ |
| deno | 2.9.7 | _bin/deno/ |
| pwsh | 7.6.6 | _bin/pwsh/ |
| bun PR #43662 | PR の先頭コミットのビルド | _bin/bun-pr-43662/。bun の CI(Buildkite)の成果物を tools/10_setup/fetch-bun-pr.ts で取り、SHA-1 を照らし合わせる |
取ったあと、_bin/node/ の npm で npm ci を流す(koffi と型チェック用)。
tools/40_test/run-tests.cmd が set PATH=…\_bin\…;%PATH% で、取った実行ファイルを PATH に置くwhere で、bun ・ node ・ deno ・ pwsh が _bin/ の下のものに解決されることを確かめる。違えば止めるbun --revision ・ node -v ・ deno --version ・ pwsh の版)を versions.txt に書くrun.cmd)は、実行ファイルを名前だけで呼び、パスは %~dp0 からの相対にする。ユーザ名を含むパスはどこにも入らないbun の名前なので、PATH を入れ替えて別の回に流す。canary の回は、bun を含むケースだけを流すchcp してから始める(窓はほかと共有しない)。判定は開始の値を基準にするneighbor: 東京大阪)と 437 用(CP437 で neighbor: café)を分けるenvironment.json を置き、開始のコードページ ・ ツール名 ・ ツールの版 ・ 実行日時(UTC の Z 形式。例 2026-10-07T03:45:12Z)を書く。日時を書くのはこのファイルだけ| 置き場 | 扱い |
|---|---|
research/evidence_last/ | 流すたびに上書きする。Git で除外しないが、commit しない |
research/evidence/ | 基準として commit する。evidence_last を確かめてから写す。再試験のあとは、指示を待たずに写す(ローカルルール) |
versions.txt、集計(node --test と bun test の分)、ケースごとの run.cmd ・ ログ ・ 画面の読み取り ・ 受け手のバイト列tools/50_run/compare-evidence.ts で 2 つを比べ、判定(✅ / ❌)の違いだけを出す。時刻は比べないresearch/evidence/ の証拠ファイルへリンクするresearch/evidence/ に残す(済。932 と 437 で全件を流し、research/evidence/ に写した。結果資料の表の ✅ / ❌ から証拠ファイルへリンクした)node tools/80_ops/check-replies.ts を流す。前回の確認から増えたものだけを出し、投稿した本人の分は除く。前回の確認の時刻は etc/check-replies.json に残る(Git 管理外)releases/latest のタグが前回と変わったら、新着として出す。canary は毎日変わるので見ない。新版が出たら、再試験して資料を更新し、push するところまでやる(ローカルルール「Issue と公開」)。PR #43662 の先頭コミットが変わったときも同じ✅月/日 時:分 返事の確認: 新着なし)