From 057ec0552ccc967b994cb3f99b6a641bbf4ad7cc Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Fri, 19 Jun 2026 09:28:25 +0200 Subject: [PATCH] fix(cookbook): stop Windows process trees (#4283) --- static/js/cookbookRunning.js | 39 ++++++++----- ...okbook_dependency_completion_regression.py | 8 ++- tests/test_cookbook_helpers.py | 2 +- tests/test_cookbook_windows_stop_tree_js.py | 58 +++++++++++++++++++ 4 files changed, 87 insertions(+), 20 deletions(-) create mode 100644 tests/test_cookbook_windows_stop_tree_js.py diff --git a/static/js/cookbookRunning.js b/static/js/cookbookRunning.js index 28365d49e..abf2ab6ea 100644 --- a/static/js/cookbookRunning.js +++ b/static/js/cookbookRunning.js @@ -784,40 +784,47 @@ function _winSessionCmd(task, tmuxArgs) { const ps = host ? `Get-Content '${sd}\\${sid}.log' -Tail ${lines} -ErrorAction SilentlyContinue` : `Get-Content (Join-Path $env:TEMP 'odysseus-tmux\\${sid}.log') -Tail ${lines} -ErrorAction SilentlyContinue`; - return host ? `ssh ${pf}${host} "powershell -Command \\"${ps}\\""` : `powershell -Command "${ps}"`; + return _winPowerShellCmd(task, ps); } if (tmuxArgs.includes('has-session')) { const ps = host ? `$p = Get-Content '${sd}\\${sid}.pid' -ErrorAction SilentlyContinue; if ($p) { Get-Process -Id $p -ErrorAction SilentlyContinue | Out-Null; if ($?) { exit 0 } else { exit 1 } } else { exit 1 }` : `$p = Get-Content (Join-Path $env:TEMP 'odysseus-tmux\\${sid}.pid') -ErrorAction SilentlyContinue; if ($p) { Get-Process -Id $p -ErrorAction SilentlyContinue | Out-Null; if ($?) { exit 0 } else { exit 1 } } else { exit 1 }`; - return host ? `ssh ${pf}${host} "powershell -Command \\"${ps}\\""` : `powershell -Command "${ps}"`; + return _winPowerShellCmd(task, ps); } if (tmuxArgs.includes('kill-session')) { - const stopTree = `function Stop-Tree([int]$Id) { Get-CimInstance Win32_Process -Filter "ParentProcessId = $Id" -ErrorAction SilentlyContinue | ForEach-Object { Stop-Tree ([int]$_.ProcessId) }; Stop-Process -Id $Id -Force -ErrorAction SilentlyContinue }`; - const ps = host - ? `${stopTree}; $p = Get-Content '${sd}\\${sid}.pid' -ErrorAction SilentlyContinue; if ($p -match '^\\d+$') { Stop-Tree ([int]$p) }; Remove-Item '${sd}\\${sid}.*' -Force -ErrorAction SilentlyContinue` - : `${stopTree}; $p = Get-Content (Join-Path $env:TEMP 'odysseus-tmux\\${sid}.pid') -ErrorAction SilentlyContinue; if ($p -match '^\\d+$') { Stop-Tree ([int]$p) }; Remove-Item (Join-Path $env:TEMP 'odysseus-tmux\\${sid}.*') -Force -ErrorAction SilentlyContinue`; - return host ? `ssh ${pf}${host} "powershell -Command \\"${ps}\\""` : `powershell -Command "${ps}"`; + const ps = _winSessionStopTreePs(task); + return _winPowerShellCmd(task, ps); } if (tmuxArgs.includes('send-keys') && tmuxArgs.includes('C-c')) { const ps = host ? `$p = Get-Content '${sd}\\${sid}.pid' -ErrorAction SilentlyContinue; if ($p) { Stop-Process -Id $p -ErrorAction SilentlyContinue }` : `$p = Get-Content (Join-Path $env:TEMP 'odysseus-tmux\\${sid}.pid') -ErrorAction SilentlyContinue; if ($p) { Stop-Process -Id $p -ErrorAction SilentlyContinue }`; - return host ? `ssh ${pf}${host} "powershell -Command \\"${ps}\\""` : `powershell -Command "${ps}"`; + return _winPowerShellCmd(task, ps); } return host ? `ssh ${pf}${host} 'tmux ${tmuxArgs}' 2>/dev/null` : `tmux ${tmuxArgs} 2>/dev/null`; } +function _winPowerShellCmd(task, ps) { + const command = `powershell -Command "${ps}"`; + if (!task.remoteHost) return command; + return `ssh ${_sshPrefix(_getPort(task))}${task.remoteHost} ${_shQuote(command)}`; +} + +function _winSessionStopTreePs(task) { + const host = task.remoteHost; + const sd = host ? '$env:TEMP\\odysseus-sessions' : '$env:TEMP\\odysseus-tmux'; + const sid = task.sessionId; + const stopTree = `function Stop-Tree([int]$Id) { Get-CimInstance Win32_Process -Filter ('ParentProcessId = ' + $Id) -ErrorAction SilentlyContinue | ForEach-Object { Stop-Tree ([int]$_.ProcessId) }; Stop-Process -Id $Id -Force -ErrorAction SilentlyContinue }`; + return host + ? `${stopTree}; $p = Get-Content '${sd}\\${sid}.pid' -ErrorAction SilentlyContinue; if ($p -match '^\\d+$') { Stop-Tree ([int]$p) }; Remove-Item '${sd}\\${sid}.*' -Force -ErrorAction SilentlyContinue` + : `${stopTree}; $p = Get-Content (Join-Path $env:TEMP 'odysseus-tmux\\${sid}.pid') -ErrorAction SilentlyContinue; if ($p -match '^\\d+$') { Stop-Tree ([int]$p) }; Remove-Item (Join-Path $env:TEMP 'odysseus-tmux\\${sid}.*') -Force -ErrorAction SilentlyContinue`; +} + export function _tmuxGracefulKill(task) { if (_isWindows(task)) { - const host = task.remoteHost; - const sd = host ? '$env:TEMP\\odysseus-sessions' : '$env:TEMP\\odysseus-tmux'; - const sid = task.sessionId; - const pf = _sshPrefix(_getPort(task)); - const ps = host - ? `$p = Get-Content '${sd}\\${sid}.pid' -ErrorAction SilentlyContinue; if ($p) { Stop-Process -Id $p -Force -ErrorAction SilentlyContinue }; Remove-Item '${sd}\\${sid}.*' -Force -ErrorAction SilentlyContinue` - : `$p = Get-Content (Join-Path $env:TEMP 'odysseus-tmux\\${sid}.pid') -ErrorAction SilentlyContinue; if ($p) { Stop-Process -Id $p -Force -ErrorAction SilentlyContinue }; Remove-Item (Join-Path $env:TEMP 'odysseus-tmux\\${sid}.*') -Force -ErrorAction SilentlyContinue`; - return host ? `ssh ${pf}${host} "powershell -Command \\"${ps}\\""` : `powershell -Command "${ps}"`; + const ps = _winSessionStopTreePs(task); + return _winPowerShellCmd(task, ps); } if (task.remoteHost) { return `ssh ${_sshPrefix(_getPort(task))}${task.remoteHost} 'tmux send-keys -t ${task.sessionId} C-c 2>/dev/null; sleep 2; tmux kill-session -t ${task.sessionId} 2>/dev/null'`; diff --git a/tests/test_cookbook_dependency_completion_regression.py b/tests/test_cookbook_dependency_completion_regression.py index 1427cebaa..bc8c3ada6 100644 --- a/tests/test_cookbook_dependency_completion_regression.py +++ b/tests/test_cookbook_dependency_completion_regression.py @@ -28,13 +28,15 @@ def test_background_status_poll_reconciles_into_local_tasks(): assert "completedDeps.forEach(t => _refreshDepsAfterInstall(t));" in source -def test_local_windows_session_commands_use_local_powershell_log_dir(): +def test_windows_session_commands_use_shared_powershell_wrapper_and_local_log_dir(): source = _read("static/js/cookbookRunning.js") assert "const host = task.remoteHost;" in source assert "host ? '$env:TEMP\\\\odysseus-sessions' : '$env:TEMP\\\\odysseus-tmux'" in source - assert "return host ? `ssh ${pf}${host}" in source - assert ": `powershell -Command \"${ps}\"`;" in source + assert "function _winPowerShellCmd(task, ps)" in source + assert "const command = `powershell -Command \"${ps}\"`;" in source + assert "if (!task.remoteHost) return command;" in source + assert "return `ssh ${_sshPrefix(_getPort(task))}${task.remoteHost} ${_shQuote(command)}`;" in source def test_dep_install_success_recognized_from_exit_sentinel(): diff --git a/tests/test_cookbook_helpers.py b/tests/test_cookbook_helpers.py index 72b72a079..a7c3ee017 100644 --- a/tests/test_cookbook_helpers.py +++ b/tests/test_cookbook_helpers.py @@ -718,7 +718,7 @@ def test_local_windows_download_pid_tracks_inner_bash_and_stop_kills_tree(): assert 'printf \'%s\\\\n\' \\"$$\\" > {pp}' in routes_src assert "function Stop-Tree([int]$Id)" in running_src - assert "ParentProcessId = $Id" in running_src + assert "('ParentProcessId = ' + $Id)" in running_src assert "Stop-Tree ([int]$p)" in running_src diff --git a/tests/test_cookbook_windows_stop_tree_js.py b/tests/test_cookbook_windows_stop_tree_js.py new file mode 100644 index 000000000..79f84ec23 --- /dev/null +++ b/tests/test_cookbook_windows_stop_tree_js.py @@ -0,0 +1,58 @@ +import shlex +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +RUNNING_JS = ROOT / "static" / "js" / "cookbookRunning.js" + + +def _between(source, start, end): + start_idx = source.index(start) + end_idx = source.index(end, start_idx) + return source[start_idx:end_idx] + + +def test_windows_graceful_kill_reuses_recursive_stop_tree_helper(): + source = RUNNING_JS.read_text(encoding="utf-8") + wrapper = _between(source, "function _winPowerShellCmd(task, ps)", "function _winSessionStopTreePs(task)") + helper = _between(source, "function _winSessionStopTreePs(task)", "function _tmuxGracefulKill(task)") + graceful = _between(source, "function _tmuxGracefulKill(task)", "function _shQuote(value)") + win_session = _between(source, "function _winSessionCmd(task, tmuxArgs)", "function _winPowerShellCmd(task, ps)") + + assert "function Stop-Tree([int]$Id)" in helper + assert "('ParentProcessId = ' + $Id)" in helper + assert "Stop-Tree ([int]$p)" in helper + assert "${_shQuote(command)}" in wrapper + assert "_winSessionStopTreePs(task)" in win_session + assert "_winPowerShellCmd(task, ps)" in win_session + assert "_winSessionStopTreePs(task)" in graceful + assert "_winPowerShellCmd(task, ps)" in graceful + assert "Stop-Process -Id $p -Force" not in graceful + assert '-Filter "ParentProcessId = $Id"' not in helper + assert 'powershell -Command \\\\"${ps}\\\\"' not in source + + +def _posix_quote(value): + return "'" + value.replace("'", "'\\''") + "'" + + +def test_remote_windows_stop_tree_payload_survives_shell_parsing(): + ps = ( + "function Stop-Tree([int]$Id) { " + "Get-CimInstance Win32_Process -Filter ('ParentProcessId = ' + $Id) " + "-ErrorAction SilentlyContinue | ForEach-Object { Stop-Tree ([int]$_.ProcessId) }; " + "Stop-Process -Id $Id -Force -ErrorAction SilentlyContinue }; " + "$p = Get-Content '$env:TEMP\\odysseus-sessions\\serve_abc.pid' " + "-ErrorAction SilentlyContinue; " + "if ($p -match '^\\d+$') { Stop-Tree ([int]$p) }" + ) + remote_command = f'powershell -Command "{ps}"' + shell_command = f"ssh -p 2222 winbox {_posix_quote(remote_command)}" + + argv = shlex.split(shell_command) + + assert argv == ["ssh", "-p", "2222", "winbox", remote_command] + assert "$Id" in argv[-1] + assert "$_.ProcessId" in argv[-1] + assert "$env:TEMP" in argv[-1] + assert "$p" in argv[-1]