Skip to content

Commit 4b6228e

Browse files
committed
fix(ssh-for-commander): fix clissh goroutine leaks
Signed-off-by: Ilya Mayorov <ilya.mayorov@flant.com>
1 parent d2d5cbe commit 4b6228e

4 files changed

Lines changed: 620 additions & 64 deletions

File tree

pkg/ssh/clissh/process/executor.go

Lines changed: 151 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ import (
1818
"bufio"
1919
"bytes"
2020
"context"
21+
"errors"
2122
"fmt"
2223
"io"
2324
"os"
2425
"os/exec"
2526
"reflect"
2627
"sync"
28+
"sync/atomic"
2729
"syscall"
2830
"time"
2931

@@ -117,17 +119,24 @@ type Executor struct {
117119
pipesMutex sync.Mutex
118120
stdoutPipeFile *os.File
119121
stderrPipeFile *os.File
122+
// Read ends of pipes must be closed to unblock goroutines stuck in Read().
123+
stdoutReadPipe *os.File
124+
stderrReadPipe *os.File
125+
stdoutHandlerReadPipe *os.File
126+
stderrHandlerReadPipe *os.File
120127

121128
StderrBuffer *bytes.Buffer
122129
StderrSplitter bufio.SplitFunc
123130
StderrHandler func(l string)
124131

125132
WaitHandler func(err error)
126133

127-
started bool
128-
stop bool
129-
waitCh chan struct{}
130-
stopCh chan struct{}
134+
started atomic.Bool
135+
stop atomic.Bool
136+
waitCh chan struct{}
137+
stopCh chan struct{}
138+
doneCh chan struct{}
139+
stopOnce sync.Once
131140

132141
lockWaitError sync.RWMutex
133142
waitError error
@@ -258,6 +267,7 @@ func (e *Executor) SetupStreamHandlers() error {
258267

259268
e.pipesMutex.Lock()
260269
e.stdoutPipeFile = stdoutWritePipe
270+
e.stdoutReadPipe = stdoutReadPipe
261271
e.pipesMutex.Unlock()
262272

263273
// create pipe for StdoutHandler
@@ -266,6 +276,9 @@ func (e *Executor) SetupStreamHandlers() error {
266276
if err != nil {
267277
return fmt.Errorf("unable to create os pipe for stdoutHandler: %s", err)
268278
}
279+
e.pipesMutex.Lock()
280+
e.stdoutHandlerReadPipe = stdoutHandlerReadPipe
281+
e.pipesMutex.Unlock()
269282
}
270283
}
271284

@@ -283,6 +296,7 @@ func (e *Executor) SetupStreamHandlers() error {
283296

284297
e.pipesMutex.Lock()
285298
e.stderrPipeFile = stderrWritePipe
299+
e.stderrReadPipe = stderrReadPipe
286300
e.pipesMutex.Unlock()
287301

288302
// create pipe for StderrHandler
@@ -291,6 +305,9 @@ func (e *Executor) SetupStreamHandlers() error {
291305
if err != nil {
292306
return fmt.Errorf("unable to create os pipe for stderrHandler: %s", err)
293307
}
308+
e.pipesMutex.Lock()
309+
e.stderrHandlerReadPipe = stderrHandlerReadPipe
310+
e.pipesMutex.Unlock()
294311
}
295312
}
296313

@@ -307,6 +324,12 @@ func (e *Executor) SetupStreamHandlers() error {
307324
// - Copy to buffer if capture is enabled
308325
// - Copy to pipe if StdoutHandler is set
309326
go func() {
327+
defer func() {
328+
if stdoutHandlerWritePipe != nil {
329+
_ = stdoutHandlerWritePipe.Close()
330+
}
331+
e.closeStdoutReadPipe()
332+
}()
310333
e.readFromStreams(stdoutReadPipe, stdoutHandlerWritePipe)
311334
}()
312335

@@ -316,6 +339,7 @@ func (e *Executor) SetupStreamHandlers() error {
316339
if e.StdoutHandler == nil {
317340
return
318341
}
342+
defer e.closeStdoutHandlerReadPipe()
319343
e.ConsumeLines(stdoutHandlerReadPipe, e.StdoutHandler)
320344
logger.DebugF("stop line consumer for '%s'", e.cmd.Args[0])
321345
}()
@@ -331,6 +355,12 @@ func (e *Executor) SetupStreamHandlers() error {
331355

332356
logger.DebugF("Start reading from stderr pipe")
333357
defer logger.DebugF("Stop reading from stderr pipe")
358+
defer func() {
359+
if stderrHandlerWritePipe != nil {
360+
_ = stderrHandlerWritePipe.Close()
361+
}
362+
e.closeStderrReadPipe()
363+
}()
334364

335365
buf := make([]byte, 16)
336366
for {
@@ -347,7 +377,11 @@ func (e *Executor) SetupStreamHandlers() error {
347377
_, _ = stderrHandlerWritePipe.Write(buf[:n])
348378
}
349379

350-
if err == io.EOF {
380+
if err != nil {
381+
if errors.Is(err, io.EOF) || errors.Is(err, os.ErrClosed) {
382+
break
383+
}
384+
logger.DebugF("Error reading from stderr: %s\n", err)
351385
break
352386
}
353387
}
@@ -357,6 +391,7 @@ func (e *Executor) SetupStreamHandlers() error {
357391
if e.StderrHandler == nil {
358392
return
359393
}
394+
defer e.closeStderrHandlerReadPipe()
360395
e.ConsumeLines(stderrHandlerReadPipe, e.StderrHandler)
361396
logger.DebugF("stop sdterr line consumer for '%s'", e.cmd.Args[0])
362397
}()
@@ -384,14 +419,6 @@ func (e *Executor) readFromStreams(stdoutReadPipe io.Reader, stdoutHandlerWriteP
384419
errorsCount := 0
385420
for {
386421
n, err := stdoutReadPipe.Read(buf)
387-
if err != nil && err != io.EOF {
388-
logger.DebugF("Error reading from stdout: %s\n", err)
389-
errorsCount++
390-
if errorsCount > 1000 {
391-
panic(fmt.Errorf("readFromStreams: too many errors, last error %v", err))
392-
}
393-
continue
394-
}
395422

396423
m := 0
397424
if !matchersDone {
@@ -429,9 +456,18 @@ func (e *Executor) readFromStreams(stdoutReadPipe io.Reader, stdoutHandlerWriteP
429456
_, _ = stdoutHandlerWritePipe.Write(buf[m:n])
430457
}
431458

432-
if err == io.EOF {
433-
logger.DebugF("readFromStreams: EOF")
434-
break
459+
if err != nil {
460+
if errors.Is(err, io.EOF) || errors.Is(err, os.ErrClosed) {
461+
logger.DebugF("readFromStreams: EOF")
462+
break
463+
}
464+
465+
logger.DebugF("Error reading from stdout: %s\n", err)
466+
errorsCount++
467+
if errorsCount > 1000 {
468+
panic(fmt.Errorf("readFromStreams: too many errors, last error %v", err))
469+
}
470+
continue
435471
}
436472
}
437473
}
@@ -468,9 +504,9 @@ func (e *Executor) Start() error {
468504
if err != nil {
469505
return err
470506
}
471-
e.started = true
472507

473508
e.ProcessWait()
509+
e.started.Store(true)
474510

475511
logger.DebugF("Register stoppable: '%s'", e.cmd.String())
476512
e.Session.RegisterStoppable(e)
@@ -482,34 +518,31 @@ func (e *Executor) ProcessWait() {
482518
waitErrCh := make(chan error, 1)
483519
e.waitCh = make(chan struct{}, 1)
484520
e.stopCh = make(chan struct{}, 1)
521+
e.doneCh = make(chan struct{})
485522

486523
// wait for process in go routine
487524
go func() {
488525
waitErrCh <- e.cmd.Wait()
489526
}()
490527

491-
go func() {
492-
if e.timeout > 0 {
493-
time.Sleep(e.timeout)
494-
if e.stopCh != nil {
495-
e.stopCh <- struct{}{}
496-
}
497-
}
498-
}()
528+
go e.waitForTimeout()
499529

500530
// watch for wait or stop
501531
go func() {
502532
defer func() {
533+
e.closeWritePipes()
534+
close(e.doneCh)
503535
close(e.waitCh)
504536
close(waitErrCh)
505537
}()
506538
// Wait until Stop() is called or/and Wait() is returning.
507539
for {
508540
select {
509541
case err := <-waitErrCh:
510-
if e.stop {
542+
if e.stop.Load() {
511543
// Ignore error if Stop() was called.
512544
// close(e.waitCh)
545+
e.killProcessGroup()
513546
return
514547
}
515548
e.setWaitError(err)
@@ -519,26 +552,45 @@ func (e *Executor) ProcessWait() {
519552
// close(e.waitCh)
520553
return
521554
case <-e.stopCh:
522-
e.stop = true
523-
// Prevent next readings from the closed channel.
524-
e.stopCh = nil
525555
// The usual e.cmd.Process.Kill() is not working for the process
526556
// started with the new process group (Setpgid: true).
527557
// Negative pid number is used to send a signal to all processes in the group.
528-
err := syscall.Kill(-e.cmd.Process.Pid, syscall.SIGKILL)
529-
if err != nil {
530-
e.killError = err
531-
}
558+
e.stop.Store(true)
559+
e.killProcessGroup()
560+
e.forceClosePipes()
532561
}
533562
}
534563
}()
535564
}
536565

537-
func (e *Executor) closePipes() {
566+
func (e *Executor) waitForTimeout() {
567+
if e.timeout <= 0 {
568+
return
569+
}
570+
571+
timer := time.NewTimer(e.timeout)
572+
defer timer.Stop()
573+
574+
select {
575+
case <-timer.C:
576+
e.signalStop()
577+
case <-e.doneCh:
578+
return
579+
}
580+
}
581+
582+
func (e *Executor) killProcessGroup() {
583+
err := syscall.Kill(-e.cmd.Process.Pid, syscall.SIGKILL)
584+
if err != nil {
585+
e.killError = err
586+
}
587+
}
588+
589+
func (e *Executor) closeWritePipes() {
538590
logger := e.settings.Logger()
539591

540-
logger.DebugF("Starting close piped")
541-
defer logger.DebugF("Stop close piped")
592+
logger.DebugF("Starting close write pipes")
593+
defer logger.DebugF("Stop close write pipes")
542594

543595
e.pipesMutex.Lock()
544596
defer e.pipesMutex.Unlock()
@@ -560,30 +612,80 @@ func (e *Executor) closePipes() {
560612
}
561613
}
562614

615+
func (e *Executor) forceClosePipes() {
616+
e.closeWritePipes()
617+
618+
logger := e.settings.Logger()
619+
620+
logger.DebugF("Starting force close pipes")
621+
defer logger.DebugF("Stop force close pipes")
622+
623+
e.closeStdoutReadPipe()
624+
e.closeStderrReadPipe()
625+
e.closeStdoutHandlerReadPipe()
626+
e.closeStderrHandlerReadPipe()
627+
}
628+
629+
func (e *Executor) closeStdoutReadPipe() {
630+
e.closePipeFile(&e.stdoutReadPipe, "stdout read pipe")
631+
}
632+
633+
func (e *Executor) closeStderrReadPipe() {
634+
e.closePipeFile(&e.stderrReadPipe, "stderr read pipe")
635+
}
636+
637+
func (e *Executor) closeStdoutHandlerReadPipe() {
638+
e.closePipeFile(&e.stdoutHandlerReadPipe, "stdout handler read pipe")
639+
}
640+
641+
func (e *Executor) closeStderrHandlerReadPipe() {
642+
e.closePipeFile(&e.stderrHandlerReadPipe, "stderr handler read pipe")
643+
}
644+
645+
func (e *Executor) closePipeFile(pipe **os.File, name string) {
646+
e.pipesMutex.Lock()
647+
defer e.pipesMutex.Unlock()
648+
649+
if *pipe != nil {
650+
err := (*pipe).Close()
651+
if err != nil {
652+
e.settings.Logger().DebugF("Cannot close %s: %v", name, err)
653+
}
654+
*pipe = nil
655+
}
656+
}
657+
563658
func (e *Executor) Stop() {
564659
logger := e.settings.Logger()
565660

566-
if e.stop {
567-
logger.DebugF("Stop '%s': already stopped", e.cmd.String())
661+
if e.cmd == nil {
662+
logger.DebugF("Possible BUG: Call Executor.Stop with Cmd==nil")
568663
return
569664
}
570-
if !e.started {
665+
if !e.started.Load() {
571666
logger.DebugF("Stop '%s': not started yet", e.cmd.String())
572667
return
573668
}
574-
if e.cmd == nil {
575-
logger.DebugF("Possible BUG: Call Executor.Stop with Cmd==nil")
576-
return
577-
}
578669

579-
e.stop = true
580-
logger.DebugF("Stop '%s'", e.cmd.String())
581-
if e.stopCh != nil {
582-
close(e.stopCh)
670+
if e.stop.CompareAndSwap(false, true) {
671+
logger.DebugF("Stop '%s'", e.cmd.String())
672+
e.signalStop()
673+
} else {
674+
logger.DebugF("Stop '%s': already stopping", e.cmd.String())
583675
}
676+
677+
e.forceClosePipes()
584678
<-e.waitCh
585679
logger.DebugF("Stopped '%s': %d", e.cmd.String(), e.cmd.ProcessState.ExitCode())
586-
e.closePipes()
680+
}
681+
682+
func (e *Executor) signalStop() {
683+
e.stopOnce.Do(func() {
684+
select {
685+
case e.stopCh <- struct{}{}:
686+
default:
687+
}
688+
})
587689
}
588690

589691
// Run executes a command and blocks until it is finished or stopped.
@@ -597,7 +699,7 @@ func (e *Executor) Run(_ context.Context) error {
597699

598700
<-e.waitCh
599701

600-
e.closePipes()
702+
e.closeWritePipes()
601703

602704
return e.WaitError()
603705
}

0 commit comments

Comments
 (0)