観測
portable-pty-patch/src/win/mod.rs の kill 経路が、TerminateProcess の戻り値を逆に解釈している。
fn do_kill(&mut self) -> IoResult<()> {
let proc = self.proc.lock().unwrap().try_clone().unwrap();
let res = unsafe { TerminateProcess(proc.as_raw_handle() as _, 1) };
let err = IoError::last_os_error();
if res != 0 {
Err(err) // TerminateProcess は成功で非ゼロを返す
} else {
Ok(()) // ゼロが失敗である
}
}
Win32 の TerminateProcess は成功で非ゼロ、失敗でゼロを返す。このコードは成功したときに Err(last_os_error()) を返し、失敗したときに Ok(()) を返す。
同じ反転が WinChildKiller::kill(同ファイル)にもある。
今それが表に出ていない理由
WinChild::kill が結果を握り潰している。
impl ChildKiller for WinChild {
fn kill(&mut self) -> IoResult<()> {
self.do_kill().ok();
Ok(())
}
}
src-tauri/src/pty.rs の PtyInstance.child は Box<dyn portable_pty::Child + Send> であり、Child は ChildKiller を継承する。したがって pty.child.kill() はこの WinChild::kill に解決し、常に Ok(()) を返す。
結果として:
プロセスが実際に落ちることは実機で確認済みである(2026-08-25、AI が操作)。cmd.exe とその孫の claude.exe が両方消えることを PID で確認した。木が落ちるのは TerminateProcess ではなく、PtyInstance が drop されて killer: Box<dyn MasterPty> が閉じ、ConPTY に繋がったプロセスが道連れになる経路による。したがってこの反転は現状の動作を壊していない。壊しているのは、失敗を報告する能力そのものである。
なぜ直すか
kill が失敗しうる経路(ハンドルが無効、権限が無い、既に終了している)で、呼び出し側は成功と区別できない。#85 はアプリ終了時に一括で kill する経路を入れたが、その失敗を検出する手段が無いため、孤児が残った場合でも黙って閉じる。
未決
- 反転を直すだけにするか、
WinChild::kill の握り潰しも解くか。 反転だけ直すと do_kill は正しくなるが .ok() が捨てているため呼び出し側からは何も変わらない。握り潰しも解くと、kill_pty と kill_all の既存のエラー経路が初めて生きる——ただし「既に終了しているプロセスへの kill」が失敗として上がってくるようになるため、その扱いを決める必要がある。
- vendored patch を直すか、上流へ出すか。
portable-pty-patch/ はこのリポジトリが抱えている改変版である。ローカルで直すと上流との差分が増える。
対象ファイル
portable-pty-patch/src/win/mod.rs
src-tauri/src/pty.rs — 握り潰しを解く場合、呼び出し側の扱い
docs/0-requirements.md — 失敗の扱いを記述する場合
関連
観測
portable-pty-patch/src/win/mod.rsの kill 経路が、TerminateProcessの戻り値を逆に解釈している。Win32 の
TerminateProcessは成功で非ゼロ、失敗でゼロを返す。このコードは成功したときにErr(last_os_error())を返し、失敗したときにOk(())を返す。同じ反転が
WinChildKiller::kill(同ファイル)にもある。今それが表に出ていない理由
WinChild::killが結果を握り潰している。src-tauri/src/pty.rsのPtyInstance.childはBox<dyn portable_pty::Child + Send>であり、ChildはChildKillerを継承する。したがってpty.child.kill()はこのWinChild::killに解決し、常にOk(())を返す。結果として:
kill_ptyの.map_err(|e| format!(\"Kill failed: {e}\"))?は発火しない。PtyState::kill_all(fix(session): sessions outlive the app that started them #85)のErr(_)の腕は到達不能であり、resistedは常に空になる。kill_all_ptysは常に成功を返す。プロセスが実際に落ちることは実機で確認済みである(2026-08-25、AI が操作)。
cmd.exeとその孫のclaude.exeが両方消えることを PID で確認した。木が落ちるのはTerminateProcessではなく、PtyInstanceが drop されてkiller: Box<dyn MasterPty>が閉じ、ConPTY に繋がったプロセスが道連れになる経路による。したがってこの反転は現状の動作を壊していない。壊しているのは、失敗を報告する能力そのものである。なぜ直すか
kill が失敗しうる経路(ハンドルが無効、権限が無い、既に終了している)で、呼び出し側は成功と区別できない。#85 はアプリ終了時に一括で kill する経路を入れたが、その失敗を検出する手段が無いため、孤児が残った場合でも黙って閉じる。
未決
WinChild::killの握り潰しも解くか。 反転だけ直すとdo_killは正しくなるが.ok()が捨てているため呼び出し側からは何も変わらない。握り潰しも解くと、kill_ptyとkill_allの既存のエラー経路が初めて生きる——ただし「既に終了しているプロセスへの kill」が失敗として上がってくるようになるため、その扱いを決める必要がある。portable-pty-patch/はこのリポジトリが抱えている改変版である。ローカルで直すと上流との差分が増える。対象ファイル
portable-pty-patch/src/win/mod.rssrc-tauri/src/pty.rs— 握り潰しを解く場合、呼び出し側の扱いdocs/0-requirements.md— 失敗の扱いを記述する場合関連