1
0
mirror of https://github.com/rclone/rclone.git synced 2025-11-29 05:47:23 +02:00
Files
rclone/backend/sftp/ssh_external_test.go
Copilot ee92673e1b sftp: fix zombie SSH processes with --sftp-ssh - Fixes #8929
Before this fix using --sftp-ssh with the sftp backend could leave
zombie processes.

This patch fixes the problem that sshClientExternal.session was never
assigned, so Wait() always returned nil without waiting for the SSH
process to exit. This caused zombie processes because the process was
never reaped.

It also ensures that Wait() is only called once on each process.

I gave this issue to Copilot to fix as an experiment. It went off in
the wrong direction to start with and fixed something which wasn't the
problem but still needed fixing. With a bit of a nudge it fixed the
correct problem too.

Co-authored-by: Nick Craig-Wood <nick@craig-wood.com>
2025-11-04 12:09:47 +00:00

85 lines
2.0 KiB
Go

//go:build !plan9
package sftp
import (
"testing"
"time"
"github.com/rclone/rclone/fs"
"github.com/stretchr/testify/assert"
)
// TestSSHExternalWaitMultipleCalls verifies that calling Wait() multiple times
// doesn't cause zombie processes
func TestSSHExternalWaitMultipleCalls(t *testing.T) {
// Create a minimal Fs object for testing
opt := &Options{
SSH: fs.SpaceSepList{"echo", "test"},
}
f := &Fs{
opt: *opt,
}
// Create a new SSH session
session := f.newSSHSessionExternal()
// Start a simple command that exits quickly
err := session.Start("exit 0")
assert.NoError(t, err)
// Give the command time to complete
time.Sleep(100 * time.Millisecond)
// Call Wait() multiple times - this should not cause issues
err1 := session.Wait()
err2 := session.Wait()
err3 := session.Wait()
// All calls should return the same result (no error in this case)
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.NoError(t, err3)
// Verify the process has exited
assert.True(t, session.exited())
}
// TestSSHExternalCloseMultipleCalls verifies that calling Close() multiple times
// followed by Wait() calls doesn't cause zombie processes
func TestSSHExternalCloseMultipleCalls(t *testing.T) {
// Create a minimal Fs object for testing
opt := &Options{
SSH: fs.SpaceSepList{"sleep", "10"},
}
f := &Fs{
opt: *opt,
}
// Create a new SSH session
session := f.newSSHSessionExternal()
// Start a long-running command
err := session.Start("sleep 10")
if err != nil {
t.Skip("Cannot start sleep command:", err)
}
// Close should cancel and wait for the process
_ = session.Close()
// Additional Wait() calls should return the same error
err2 := session.Wait()
err3 := session.Wait()
// All should complete without panicking
// err1 could be nil or an error depending on how the process was killed
// err2 and err3 should be the same
assert.Equal(t, err2, err3, "Subsequent Wait() calls should return same result")
// Verify the process has exited
assert.True(t, session.exited())
}