Skip to content

Commit

Permalink
feat: handle context cancelation during docker exec
Browse files Browse the repository at this point in the history
To allow interrupting docker exec (which could be long running)
we process the log output in a go routine and handle
context cancelation as well as command result.

In case of context cancelation a CTRL+C is written into the docker
container. This should be enough to terminate the running
command.

To make sure we do not get stuck during cleanup, we do
set the cleanup contexts with a timeout of 5 minutes

Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se>
Co-authored-by: Philipp Hinrichsen <philipp.hinrichsen@new-work.se>
  • Loading branch information
3 people authored and github-actions committed May 24, 2022
1 parent b487a45 commit 3986d0b
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 9 deletions.
47 changes: 39 additions & 8 deletions pkg/container/docker_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,30 @@ func (cr *containerReference) exec(cmd []string, env map[string]string, user, wo
}
defer resp.Close()

err = cr.waitForCommand(ctx, isTerminal, resp, idResp, user, workdir)
if err != nil {
return err
}

inspectResp, err := cr.cli.ContainerExecInspect(ctx, idResp.ID)
if err != nil {
return errors.WithStack(err)
}

if inspectResp.ExitCode == 0 {
return nil
}

return fmt.Errorf("exit with `FAILURE`: %v", inspectResp.ExitCode)
}
}

func (cr *containerReference) waitForCommand(ctx context.Context, isTerminal bool, resp types.HijackedResponse, idResp types.IDResponse, user string, workdir string) error {
logger := common.Logger(ctx)

cmdResponse := make(chan error)

go func() {
var outWriter io.Writer
outWriter = cr.input.Stdout
if outWriter == nil {
Expand All @@ -567,25 +591,32 @@ func (cr *containerReference) exec(cmd []string, env map[string]string, user, wo
errWriter = os.Stderr
}

var err error
if !isTerminal || os.Getenv("NORAW") != "" {
_, err = stdcopy.StdCopy(outWriter, errWriter, resp.Reader)
} else {
_, err = io.Copy(outWriter, resp.Reader)
}
if err != nil {
logger.Error(err)
}
cmdResponse <- err
}()

inspectResp, err := cr.cli.ContainerExecInspect(ctx, idResp.ID)
select {
case <-ctx.Done():
// send ctrl + c
_, err := resp.Conn.Write([]byte("\x03"))
if err != nil {
return errors.WithStack(err)
logger.Warnf("Failed to send CTRL+C: %+s", err)
}

if inspectResp.ExitCode == 0 {
return nil
// we return the context canceled error to prevent other steps
// from executing
return ctx.Err()
case err := <-cmdResponse:
if err != nil {
logger.Error(err)
}

return fmt.Errorf("exit with `FAILURE`: %v", inspectResp.ExitCode)
return nil
}
}

Expand Down
12 changes: 11 additions & 1 deletion pkg/runner/job_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package runner
import (
"context"
"fmt"
"time"

"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/model"
Expand Down Expand Up @@ -105,7 +106,16 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
pipeline = append(pipeline, steps...)

return common.NewPipelineExecutor(pipeline...).
Finally(postExecutor).
Finally(func(ctx context.Context) error {
var cancel context.CancelFunc
if ctx.Err() == context.Canceled {
// in case of an aborted run, we still should execute the
// post steps to allow cleanup.
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
}
return postExecutor(ctx)
}).
Finally(info.interpolateOutputs()).
Finally(info.closeContainer())
}
Expand Down
8 changes: 8 additions & 0 deletions pkg/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"regexp"
"runtime"
"strings"
"time"

log "github.com/sirupsen/logrus"

Expand Down Expand Up @@ -172,7 +173,14 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
}

if runner.config.AutoRemove && isLastRunningContainer(s, r) {
var cancel context.CancelFunc
if ctx.Err() == context.Canceled {
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
}

logger.Infof("Cleaning up container for job %s", rc.JobName)

if err := rc.stopJobContainer()(ctx); err != nil {
logger.Errorf("Error while cleaning container: %v", err)
}
Expand Down

0 comments on commit 3986d0b

Please sign in to comment.