Skip to content

Commit 1748c60

Browse files
committed
fix: plugin and container context cancellation
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
1 parent 79fdc57 commit 1748c60

3 files changed

Lines changed: 61 additions & 30 deletions

File tree

cli/command/container/attach.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,13 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
105105

106106
if opts.Proxy && !c.Config.Tty {
107107
sigc := notifyAllSignals()
108-
go ForwardAllSignals(ctx, apiClient, containerID, sigc)
108+
// since the ForwardAllSignals already registers its own signal handler
109+
// and should notify the daemon independently of the clients ctx cancellation
110+
// we should use a background context to avoid the ForwardAllSignals from
111+
// returning before all signals are forwarded to the daemon
112+
bgCtx, cancel := context.WithCancel(context.Background())
113+
defer cancel()
114+
go ForwardAllSignals(bgCtx, apiClient, containerID, sigc)
109115
defer signal.StopCatch(sigc)
110116
}
111117

cli/command/container/run.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,13 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption
150150
}
151151
if runOpts.sigProxy {
152152
sigc := notifyAllSignals()
153-
go ForwardAllSignals(ctx, apiClient, containerID, sigc)
153+
// since the ForwardAllSignals already registers its own signal handler
154+
// and should notify the daemon independently of the clients ctx cancellation
155+
// we should use a background context to avoid the ForwardAllSignals from
156+
// returning before all signals are forwarded to the daemon
157+
bgCtx, cancel := context.WithCancel(context.Background())
158+
defer cancel()
159+
go ForwardAllSignals(bgCtx, apiClient, containerID, sigc)
154160
defer signal.StopCatch(sigc)
155161
}
156162

cmd/docker/docker.go

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ func main() {
3737
dockerCli, err := command.NewDockerCli(command.WithBaseContext(ctx))
3838
if err != nil {
3939
fmt.Fprintln(os.Stderr, err)
40-
os.Exit(1)
40+
defer os.Exit(1)
41+
return
4142
}
4243
logrus.SetOutput(dockerCli.Err())
4344
otel.SetErrorHandler(debug.OTELErrorHandler)
@@ -50,15 +51,19 @@ func main() {
5051
// StatusError should only be used for errors, and all errors should
5152
// have a non-zero exit status, so never exit with 0
5253
if sterr.StatusCode == 0 {
53-
os.Exit(1)
54+
defer os.Exit(1)
55+
return
5456
}
55-
os.Exit(sterr.StatusCode)
57+
defer os.Exit(sterr.StatusCode)
58+
return
5659
}
5760
if errdefs.IsCancelled(err) {
58-
os.Exit(0)
61+
defer os.Exit(0)
62+
return
5963
}
6064
fmt.Fprintln(dockerCli.Err(), err)
61-
os.Exit(1)
65+
defer os.Exit(1)
66+
return
6267
}
6368
}
6469

@@ -248,42 +253,56 @@ func tryPluginRun(ctx context.Context, dockerCli command.Cli, cmd *cobra.Command
248253
// notify the plugin via the PluginServer (or signal) as appropriate.
249254
const exitLimit = 2
250255

256+
tryTerminatePlugin := func(force bool) {
257+
// If stdin is a TTY, the kernel will forward
258+
// signals to the subprocess because the shared
259+
// pgid makes the TTY a controlling terminal.
260+
//
261+
// The plugin should have it's own copy of this
262+
// termination logic, and exit after 3 retries
263+
// on it's own.
264+
if dockerCli.Out().IsTerminal() {
265+
return
266+
}
267+
268+
// Terminate the plugin server, which will
269+
// close all connections with plugin
270+
// subprocesses, and signal them to exit.
271+
//
272+
// Repeated invocations will result in EINVAL,
273+
// or EBADF; but that is fine for our purposes.
274+
_ = srv.Close()
275+
276+
// If we're still running after 3 interruptions
277+
// (SIGINT/SIGTERM), send a SIGKILL to the plugin as a
278+
// final attempt to terminate, and exit.
279+
if force {
280+
_, _ = fmt.Fprint(dockerCli.Err(), "got 3 SIGTERM/SIGINTs, forcefully exiting\n")
281+
_ = plugincmd.Process.Kill()
282+
os.Exit(1)
283+
}
284+
}
285+
251286
go func() {
252287
retries := 0
288+
force := false
289+
// catch the first signal through context cancellation
253290
<-ctx.Done()
291+
tryTerminatePlugin(force)
254292

293+
// register subsequent signals
255294
signals := make(chan os.Signal, exitLimit)
256295
signal.Notify(signals, platformsignals.TerminationSignals...)
257296

258297
for range signals {
259-
// If stdin is a TTY, the kernel will forward
260-
// signals to the subprocess because the shared
261-
// pgid makes the TTY a controlling terminal.
262-
//
263-
// The plugin should have it's own copy of this
264-
// termination logic, and exit after 3 retries
265-
// on it's own.
266-
if dockerCli.Out().IsTerminal() {
267-
continue
268-
}
269-
270-
// Terminate the plugin server, which will
271-
// close all connections with plugin
272-
// subprocesses, and signal them to exit.
273-
//
274-
// Repeated invocations will result in EINVAL,
275-
// or EBADF; but that is fine for our purposes.
276-
_ = srv.Close()
277-
298+
retries++
278299
// If we're still running after 3 interruptions
279300
// (SIGINT/SIGTERM), send a SIGKILL to the plugin as a
280301
// final attempt to terminate, and exit.
281-
retries++
282302
if retries >= exitLimit {
283-
_, _ = fmt.Fprint(dockerCli.Err(), "got 3 SIGTERM/SIGINTs, forcefully exiting\n")
284-
_ = plugincmd.Process.Kill()
285-
os.Exit(1)
303+
force = true
286304
}
305+
tryTerminatePlugin(force)
287306
}
288307
}()
289308

0 commit comments

Comments
 (0)