コードページ試験の結果

計画の T1〜T6 を、コードページ 932 と 437 で、bun 1.4.3 ・ bun canary ・ node ・ deno に流した結果。bun がコンソールのコードページを変えることで起きる害を 1 つずつ実測で示し、結果の 1 つ 1 つから証拠ファイルへリンクする

📅 作成: 2026-10-04 / 更新: 2026-10-17

要点

  1. bun は起動時にコンソールのコードページを 65001 に切り替え、終了時に控えた値へ戻す。コードページはコンソールに属し、つながる全プロセスで共有される。ソース: init()(output.rs の 561〜567 行)と restore()(output.rs の 517〜524 行)。証拠、bun が動いているあいだのコードページ: 932 / 437(T1)
  2. 1 つの窓で 2 本の bun が動くと、互いの控えを上書きする。出力が化け、窓に 65001 が残り、同じ窓のほかのプログラムが壊れる。証拠: 化けた画面 932 / 437(T5)、壊れた隣のプロセス 932 / 437(T4)、強制終了 932 / 437(T3)
  3. bun#43660 への Open の修正 PR #43662 は、原因を取り除かない。console.log の出力は直すが、動いているあいだは窓を 65001 にしたまま(932 / 437)、強制終了では 65001 が残り(932 / 437)、fs.writeSync(1) ・ fs.write(1) ・ Bun.write(Bun.stdout) は化け(932 / 437)、戻し方は、順番を決めない形でまれに競合に負ける(時系列)
  4. node と deno はコードページを一度も変えず、すべてのケースで正しく出す: node 932 / 437、deno 932 / 437(T1)

依頼(bun#44693): コンソールのコードページを一切変えず、画面への出力はすべて WriteConsoleW で書いてほしい。判定は、窓が 932(日本語版 Windows)で始まっても 437(英語版 Windows)で始まっても同じだった。詳しくは 結論、試験の流し方は 条件 にある。

結論

bun は、起動した瞬間(スクリプトが動く前)に窓の入力・出力コードページを 65001 に変え、終了時に「起動時に控えた値」へ戻す。この動きから、計画の害 H1〜H7 がすべて起きることを確かめた。node と deno はコードページを一度も変えず、すべてのケースで正しく動いた。

行は重要な順に並べた。PR #43662 のあとも残る害を先に置く。

害内容bun ・ canarybun PR #43662node ・ deno証拠(932 / 437)
H4同じ窓の別のプロセスが巻き込まれる❌ 起きた❌ 起きる。bun が動いているあいだと、強制終了のあと、窓は 65001✅ 起きない画面 / 画面(T4。隣のプロセス役が化け、chcp が 65001 を示す)。PR: ログ / ログ(T1、実行中は 65001)
H6強制終了すると元に戻らない❌ 起きた❌ 起きる✅ 起きないログ / ログ(T3)。PR: 画面 / 画面
H1パイプでつないだ相手の表示が化ける❌ 起きた⚠️ console.log と process.stdout は直った。fs.writeSync(1) ・ fs.write(1) ・ Bun.write(Bun.stdout) では起きる✅ 起きない画面 / 画面(T5 の ww)。PR: 画面 / 画面(T5 の ww を fs.writeSync で)
H5コードページが開始の値に戻った状態で、bun が UTF-8 のバイト列をそのまま書いて化ける❌ 起きた⚠️ H1 と同じ✅ 起きないログ / ログ(T5 の ww。受け手が書く直前のコードページが開始の値)
H3終了後に 65001 が窓に残る❌ 起きた⚠️ 順番を決めたケースでは直ったが、順番を決めない形では、まれに入力側か出力側が 65001 のまま残る✅ 起きないログ / ログ(T2、cmd から bun → bun)。PR: ログ(932、見張り役つきの 19 回目。終了後は入力 932・出力 65001)。回数は 時系列 の表
H2結果が起動・終了の順で決まり、実行のたびに変わる❌ 起きた⚠️ 順番を決めない形で画面は毎回正しかったが、まれに 65001 が残る(H3)✅ 起きないT5 の bun#43660 と同じ形。T5 の 4 通りの順で結果が 4 つに分かれる
H7bun 同士が直接つながっていなくても起きる❌ 起きた✅ 直った✅ 起きない画面 / 画面(T5 の relay、bun | node | bun)

PR #43662(bun#43660 への、Open の修正)

PR #43662 は、bun_sys::write を通る画面への出力を WriteConsoleW にし、終了時は控えた値が 65001 でないときだけ戻す。CI の Windows x64 のビルドを、同じ試験一式で流した。bun#43660 で報告した症状は直るが、起動時に 65001 にすることは残るため、共有の状態を変えることから起きる害は残る。

パイプを流れるデータそのものは、どの組み合わせでも壊れなかった(T2)。害はすべて、窓のコードページという共有の状態を書き換えることから起きている。順番を決めたケースは、安定版と canary 版ですべて同じ判定になり(T6)、932 と 437 でもすべて同じだった。

関連する PR

PR #43662 での robobun の返信に出てきたもの。2026-10-10 の時点で、どちらも open で、マージされていない。

bun のソースの該当箇所

リンクはいまの main を指す。T5 の時系列は、このコードとそのまま一致する。各プロセスは起動時に見えた値を控えるので、2 本目は 65001 を控える。

T1. コードページの推移

1 本だけを窓で動かし、画面に abc 東京大阪 xyz を出す。値は 入力/出力。

試験コード: T1(codepage.test.ts の 35〜51 行)

T2. パイプの組み合わせ

送り手と受け手を同時に起動し、受け手は受け取ったバイト列をファイルに書く。各セルは「バイト列、終了後のコードページ」を、932 で始めた窓と 437 で始めた窓の順に並べたもの。

試験コード: T2(codepage.test.ts の 53〜81 行)

起動元ごとのコマンドの組み立て: 58〜61 行

送り手 → 受け手cmdpwsh 7Windows PowerShell 5.1
bun → bun932: ✅ ❌
437: ✅ ❌
932: ✅ ❌
437: ✅ ❌
932: ✅ ✅
437: ✅ ✅
bun → node932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
bun → deno932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
canary → canary932: ✅ ❌
437: ✅ ❌
932: ✅ ❌
437: ✅ ❌
932: ✅ ✅
437: ✅ ✅
canary → node932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
canary → deno932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
pr43662 → pr43662932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
pr43662 → node932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
pr43662 → deno932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
node → bun932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
node → canary932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
node → pr43662932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
node → node932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
node → deno932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
deno → bun932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
deno → canary932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
deno → pr43662932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
deno → node932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
deno → deno932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅
932: ✅ ✅
437: ✅ ✅

T3・T4. 強制終了と隣のプロセス

T3. 強制終了

画面に書いたあと待機している 1 本を taskkill /F で止める。

試験コード: T3(codepage.test.ts の 83〜102 行)

bun は終了時に戻す作りのため、強制終了されると 65001 のまま残る(H6)。そのあと同じ窓で動く cmd は、自分のスクリプトを UTF-8 として読み違えて化ける(H4)。

T4. 通常終了の後

bun | bun が普通に終わった後(2 本の寿命が重なり、送り手が先に終わる順)の窓。

試験コード: T4(codepage.test.ts の 104〜121 行)

bun の窓の画面(左が 932、右が 437):

abc 譚ア莠ャ螟ァ髦ェ xyz          abc µ¥▒Σ║¼σñºΘÿ¬ xyz
--- tail ---                  --- tail ---
Active code page: 65001       Active code page: 65001
neighbor: �������             neighbor: caf�

node で同じことをした窓の画面(左が 932、右が 437):

abc 東京大阪 xyz              abc 東京大阪 xyz
--- tail ---                  --- tail ---
現在のコード ページ: 932      Active code page: 437
neighbor: 東京大阪            neighbor: café

日本語版 Windows の chcp は、出力側が 65001 になると英語のメッセージ(Active code page)を出す。bun ・ canary とも、強制終了しなくても同じ窓の隣のプロセス役と chcp まで巻き込んだ(H4)。

T5. 順番を固定した再現

2 本目の起動を約 1 秒遅らせ、終了の順は待ち時間で決める。2 本の寿命は必ず重なるようにした。各組 3 回。セルは「画面またはバイト列 / 終了後に開始のコードページ」を、932 と 437 の順に並べたもの。順番ごとの予想は 計画の T5 にある(ww は化けて 65001 が残る、wr と rw は正しい、rr は正しいが 65001 が残る)。

試験コード: T5(codepage.test.ts の 123〜210 行)

形と順番bun 1.4.3bun canarybun PR #43662nodedeno
送り手が先に起動し、先に終わる (ww)
受け手は画面に書く
932: ❌❌❌ / ❌❌❌
437: ❌❌❌ / ❌❌❌
932: ❌❌❌ / ❌❌❌
437: ❌❌❌ / ❌❌❌
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
送り手が先に起動し、先に終わる (ww)
受け手はファイルに書く
932: ✅✅✅ / ❌❌❌
437: ✅✅✅ / ❌❌❌
932: ✅✅✅ / ❌❌❌
437: ✅✅✅ / ❌❌❌
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
送り手が先に起動し、受け手が先に終わる (wr)
受け手は画面に書く
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
送り手が先に起動し、受け手が先に終わる (wr)
受け手はファイルに書く
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
受け手が先に起動し、送り手が先に終わる (rw)
受け手は画面に書く
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
受け手が先に起動し、送り手が先に終わる (rw)
受け手はファイルに書く
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
受け手が先に起動し、受け手が先に終わる (rr)
受け手は画面に書く
932: ✅✅✅ / ❌❌❌
437: ✅✅✅ / ❌❌❌
932: ✅✅✅ / ❌❌❌
437: ✅✅✅ / ❌❌❌
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
受け手が先に起動し、受け手が先に終わる (rr)
受け手はファイルに書く
932: ✅✅✅ / ❌❌❌
437: ✅✅✅ / ❌❌❌
932: ✅✅✅ / ❌❌❌
437: ✅✅✅ / ❌❌❌
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
間に node を挟み、送り手が先に起動して先に終わる (relay)
受け手は画面に書く
932: ❌❌❌ / ❌❌❌
437: ❌❌❌ / ❌❌❌
932: ❌❌❌ / ❌❌❌
437: ❌❌❌ / ❌❌❌
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
932: ✅✅✅ / ✅✅✅
437: ✅✅✅ / ✅✅✅
ww で、受け手は fs.writeSync(1) で書く (ww-fswritesync)
受け手は画面に書く
932: ❌❌❌ / ❌❌❌
437: ❌❌❌ / ❌❌❌
932: ❌❌❌ / ❌❌❌
437: ❌❌❌ / ❌❌❌
932: ❌❌❌ / ✅✅✅
437: ❌❌❌ / ✅✅✅
932: —
437: —
932: —
437: —
ww で、受け手は fs.write(1) で書く (ww-fswrite)
受け手は画面に書く
932: ❌❌❌ / ❌❌❌
437: ❌❌❌ / ❌❌❌
932: ❌❌❌ / ❌❌❌
437: ❌❌❌ / ❌❌❌
932: ❌❌❌ / ✅✅✅
437: ❌❌❌ / ✅✅✅
932: —
437: —
932: —
437: —
ww で、受け手は Bun.write(Bun.stdout) で書く (ww-bunwrite)
受け手は画面に書く
932: ❌❌❌ / ❌❌❌
437: ❌❌❌ / ❌❌❌
932: ❌❌❌ / ❌❌❌
437: ❌❌❌ / ❌❌❌
932: ❌❌❌ / ✅✅✅
437: ❌❌❌ / ✅✅✅
932: —
437: —
932: —
437: —

bun 1.4.3 と canary の結果は、932 と 437 のどちらでも、すべての組で計画の予想と一致し、3 回とも同じだった。順番さえ決まれば結果は決まる。PR #43662 の版は、受け手が process.stdout で書くときはどの順でも正しかった。一方、受け手が同じ文字列を fs.writeSync(1) ・ fs.write(1) ・ Bun.write(Bun.stdout) で書く最後の 3 行は、どの版でも化ける。これらの API は WriteFile のままのため。この 3 行は bun の版だけで流している(Bun.write は bun にしか無い)。

化ける順番での時系列(bun 1.4.3、ww ・ 画面、932)

このログ から。値は 入力/出力。w は送り手、r は受け手、probe は観測役の node。437 で流した回も、932 が 437 に替わるだけで同じ形になる(ログ)。

   0ms probe  窓の前          932/932
  45ms w      起動直後        65001/65001   ← 送り手が 932 を控えて 65001 にする
  47ms w      書く直前        65001/65001
1093ms r      起動直後        65001/65001   ← 受け手は 65001 を「元の値」として控える
2050ms w      終了直前        65001/65001   → 送り手が終了し、控えた 932 に戻す
2854ms r      書く直前        932/932       ← 受け手は 932 の窓に UTF-8 のバイト列を書く(化ける)
2860ms r      終了直前        932/932       → 受け手が終了し、控えた 65001 に戻す
2989ms probe  窓の後          65001/65001   ← 65001 が残る

T5. bun#43660 と同じ形

bun#43660 と同じ形で、2 本を同時に起動し、順番を決めずに 10 回流した(PR #43662 の版は 60 回)。文字列は \u エスケープで渡した abc 東京大阪 xyz(deno は deno eval)。

bun -e "console.log('abc \u6771\u4eac\u5927\u962a xyz')" | bun -e "process.stdin.pipe(process.stdout)"

試験コード: bun#43660 と同じ形(codepage.test.ts の 140〜161 行)

ランタイム開始 932: 画面 / 終了後開始 437: 画面 / 終了後
bun 1.4.3❌❌❌❌❌✅✅❌❌❌ / ❌❌❌❌❌✅✅❌❌❌
化けた回数 8 / 10
❌❌❌❌❌❌❌❌❌✅ / ❌❌❌❌❌❌❌❌❌✅
化けた回数 9 / 10
bun canary❌❌❌❌❌❌❌❌❌❌ / ❌❌❌❌❌❌❌❌❌❌
化けた回数 10 / 10
❌❌❌❌❌❌✅❌❌❌ / ❌❌❌❌❌❌✅❌❌❌
化けた回数 9 / 10
bun PR #43662✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅ / ✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅
化けた回数 0 / 60
✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅ / ✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅
化けた回数 0 / 60
node✅✅✅✅✅✅✅✅✅✅ / ✅✅✅✅✅✅✅✅✅✅
化けた回数 0 / 10
✅✅✅✅✅✅✅✅✅✅ / ✅✅✅✅✅✅✅✅✅✅
化けた回数 0 / 10
deno✅✅✅✅✅✅✅✅✅✅ / ✅✅✅✅✅✅✅✅✅✅
化けた回数 0 / 10
✅✅✅✅✅✅✅✅✅✅ / ✅✅✅✅✅✅✅✅✅✅
化けた回数 0 / 10

窓の全プロセスの時系列(PR #43662)

コードページが変わったまま残ったとき、窓にどのプロセスがいたかを見るため、同じコマンドを PR #43662 の版で、932 から 60 回流し直した。その間、同じ窓に node の見張り役をつないだ。見張り役は GetConsoleProcessList ・ GetConsoleCP ・ GetConsoleOutputCP を読み続け、プロセスの出入りとコードページの変化を、すべて UTC の時刻で記録した(watch.ndjson)。node はコードページを変えない(T1)。

同じ形を、見張り役の有無を変えて何度か流した。開始のコードページに戻らなかった回数は、日によって大きく変わる。うまくいった回も、選ばずにすべて載せる。

流した日(UTC)見張り役回数戻らなかった回証拠
2026-10-07なし(全件の試験)20❌ 1(932 の 6 回目、出力側)log.jsonl(その時点の commit)
2026-10-10あり60❌ 3(19・45・51 回目)results.jsonl
2026-10-10なし(全件の試験)120✅ 0(932 と 437 で 60 回ずつ)932 / 437
2026-10-11あり20❌ 1(9 回目、出力側)watch.ndjson、results.jsonl
2026-10-11あり60✅ 0results.jsonl
2026-10-11あり(437 で開始)60❌ 2(48・55 回目、入力側)results.jsonl
2026-10-11あり + 呼び出しの記録40❌ 3(6・15・21 回目、出力側)下の記録

437 で始めても同じように残った。残った側は 2 回とも入力側で、2026-10-10 の 45・51 回目と同じ形だった。

2026-10-11 の 9 回目は、下の 19 回目と同じ形だった。R は W が 65001 にした 7 ms 後に起動し、終了後は入力 932 ・ 出力 65001 で残った。以下は 2026-10-10 の 60 回の分析。

回終了後(入力 / 出力)窓にいたプロセス時系列
19❌ 932 / 65001バッチを動かす cmd.exe、前後の node の計測、chcp、bun 2 本。ほかにはいないwatch.ndjson
45❌ 65001 / 932watch.ndjson
51❌ 65001 / 932watch.ndjson

ほかの 57 回は 932 / 932 に戻った(results.jsonl、environment.json)。3 回の bun の行だけを抜くと(UTC)。W は送り手 bun -e "console.log(...)"、R は受け手 bun -e "process.stdin.pipe(process.stdout)":

run 19
13:39:12.205  attach  W
13:39:12.208  codepage in 932   / out 65001
13:39:12.209  codepage in 65001 / out 65001
13:39:12.216  attach  R
13:39:12.223  codepage in 932   / out 65001
13:39:12.226  detach  W
13:39:12.226  codepage in 65001 / out 65001
13:39:12.263  codepage in 932   / out 65001
13:39:12.266  detach  R
              (left at in 932 / out 65001)

run 45
13:39:50.176  attach  W
13:39:50.179  codepage in 932   / out 65001
13:39:50.181  codepage in 65001 / out 65001
13:39:50.189  attach  R
13:39:50.192  codepage in 65001 / out 932
13:39:50.194  detach  W
13:39:50.227  detach  R
              (left at in 65001 / out 932)

run 51
13:39:58.957  attach  W
13:39:58.959  codepage in 932   / out 65001
13:39:58.960  codepage in 65001 / out 65001
13:39:58.970  attach  R
13:39:58.974  codepage in 65001 / out 932
13:39:58.975  detach  W
13:39:59.011  detach  R
              (left at in 65001 / out 932)

PR のソースから導いた順番

試した版は PR の先頭コミット 576eb251a。コードページを変えるのは、その output.rs だけ:

呼ぶのが W と R だけなら、どの終わり方にも、それを生む順番は 1 つしかない:

ここまではソースと時刻からの推論。見張り役にはどのプロセスが呼んだかは見えず、R の「控える」と「変える」の間は読み取りの間隔より短い(19 回目で、出力側が 932 になってまた 65001 に戻った変化は記録に出ていない)。そこで次に、呼び出しを 1 つずつ記録した。

呼び出しを 1 つずつ記録した結果

W と R をそれぞれ cdb(WinDbg 1.2610)の下で動かし、kernelbase の GetConsoleOutputCP ・ SetConsoleOutputCP ・ GetConsoleCP ・ SetConsoleCP にブレークポイントを置いた。呼ばれるたびに、プロセス ・ 値(Set は引数、Get は戻り値)・ 呼び出し元の番地を記録して、そのまま続行させた(trace-writer.ndjson ・ trace-reader.ndjson)。見張り役も同時に動かした。932 から 40 回流し、3 回で出力側が 65001 のまま残った。

6 回目(UTC)。W は pid 30744、R は pid 55480:

21:30:27.486  W  GetConsoleOutputCP -> 932     (saves 932)
21:30:27.500  W  SetConsoleOutputCP(65001)
21:30:27.509  W  GetConsoleCP       -> 932
21:30:27.511  W  SetConsoleCP(65001)
21:30:27.514  R  GetConsoleOutputCP -> 65001   (saves 65001)
21:30:27.525  W  SetConsoleOutputCP(932)       (W restores output)
21:30:27.527  W  SetConsoleCP(932)             (W restores input)
21:30:27.528  R  SetConsoleOutputCP(65001)
21:30:27.537  R  GetConsoleCP       -> 932
21:30:27.539  R  SetConsoleCP(65001)
21:30:27.594  R  SetConsoleCP(932)             (R restores input only)
              (left at in 932 / out 65001)

デバッガは呼ばれるたびに bun を止めるので、競合の割合はデバッガなしの回とは比べられない。時刻は 2 つのデバッガがそれぞれ記録したもので、2 本の間の前後は数 ms ずれることがある(21 回目は、記録上の時刻だけを見ると R が控えたのが W の後始末の後になっている)。値(何を控えたか)は、このずれの影響を受けない。デバッガは、出力を隠す前に名乗りを出すため、W の分はその名乗りがパイプに入り、R が画面に出している(screen.json)。

証拠(6 回目): trace-writer.ndjson、trace-reader.ndjson、watch.ndjson。40 回分:results.jsonl。コード: tools/50_run/asis-timeline.ts --trace、tools/50_run/cdb-trace.js、まとめは tools/50_run/summarize-trace.ts

コード: 見張り役(watch.ts)、流す側(asis-timeline.ts)

条件

項目値
OSWindows 11 Home 10.0.26300(日本語版)
窓ケースごとに conhost.exe で新しい窓を起こす。開始のコードページは、ほかと共有しないその専用の窓の中で chcp して 932 か 437 にする。そのあと試験はコードページに触らない
bun1.4.3+c6da4a4d3
bun canary1.4.3-canary.1+bbdc5a519(準備の時点の最新)
bun PR #436621.4.3-canary.1+6b5271cb1。bun#43660 への修正で Open の PR #43662 の、CI の Windows x64 のビルド(先頭コミット 6b5271cb1、Buildkite のビルド 126042)。それまでの結果は 576eb251a(Buildkite のビルド 119166)で測った。窓の全プロセスの時系列と、そのあとの cdb の記録は、576eb251a のまま
nodev26.10.0
deno2.9.7
pwsh / Windows PowerShell7.6.6 / 5.1.26100.9549
送る文字列abc 東京大阪 xyz。化けると、932 では abc 譚ア莠ャ螟ァ髦ェ xyz、437 では abc µ¥▒Σ║¼σñºΘÿ¬ xyz
流し方tools\10_setup\setup-runtimes.cmd が上のランタイムを公式の配布元から _bin\ に取り、tools\40_test\run-tests.cmd がそれを PATH の先頭に置いて全件を流す。PC に入っているものは使わない

判定の方法

✅ 期待どおり(あるべき姿と同じ) / ❌ 期待と違う。下の表の ✅ と ❌ は、1 つ 1 つがそのケースの証拠ファイルへのリンクになっている。画面(screen.json)、コードページの記録(log.jsonl)、受け取ったバイト列(out.bin)のどれか。

証拠

証拠はすべて research/evidence/ にあり、ケースごとに 1 フォルダ(cp<コードページ>/<bun か bun-canary>/<流した道具>/<ケース>/)。各フォルダには、流したバッチ(run.cmd)・ログ・画面と、あれば受け取ったバイト列がある。ログには日時も pid も無いので、2 回の結果を diff で比べられる。実行日時と道具の版は、回ごとの environment.json にだけある。

試験コード

リンクはすべてコミット 5b9f56b を指すので、行番号はずれない。