1
0
mirror of https://github.com/rclone/rclone.git synced 2025-11-23 21:44:49 +02:00

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>
This commit is contained in:
Copilot
2025-11-04 12:09:47 +00:00
committed by GitHub
parent 55655efabf
commit ee92673e1b
2 changed files with 101 additions and 10 deletions

View File

@@ -10,6 +10,7 @@ import (
"os/exec"
"slices"
"strings"
"sync"
"time"
"github.com/rclone/rclone/fs"
@@ -50,6 +51,9 @@ func (s *sshClientExternal) Close() error {
func (s *sshClientExternal) NewSession() (sshSession, error) {
session := s.f.newSSHSessionExternal()
if s.session == nil {
// Store the first session so Wait() and Close() can use it
s.session = session
} else {
fs.Debugf(s.f, "ssh external: creating additional session")
}
return session, nil
@@ -76,6 +80,8 @@ type sshSessionExternal struct {
cancel func()
startCalled bool
runningSFTP bool
waitOnce sync.Once // ensure Wait() is only called once
waitErr error // result of the Wait() call
}
func (f *Fs) newSSHSessionExternal() *sshSessionExternal {
@@ -175,16 +181,17 @@ func (s *sshSessionExternal) exited() bool {
// Wait for the command to exit
func (s *sshSessionExternal) Wait() error {
if s.exited() {
return nil
}
err := s.cmd.Wait()
if err == nil {
fs.Debugf(s.f, "ssh external: command exited OK")
} else {
fs.Debugf(s.f, "ssh external: command exited with error: %v", err)
}
return err
// Use sync.Once to ensure we only wait for the process once.
// This is safe even if Wait() is called from multiple goroutines.
s.waitOnce.Do(func() {
s.waitErr = s.cmd.Wait()
if s.waitErr == nil {
fs.Debugf(s.f, "ssh external: command exited OK")
} else {
fs.Debugf(s.f, "ssh external: command exited with error: %v", s.waitErr)
}
})
return s.waitErr
}
// Run runs cmd on the remote host. Typically, the remote