Skip to content

Commit 3da28ad

Browse files
committed
cli-plugins: terminate plugin when CLI exits
Previously, long lived CLI plugin processes weren't properly handled (see: #4402) resulting in plugin processes being left behind running, after the CLI process exits. This commit changes the plugin handling code to open an abstract unix socket before running the plugin and passing it to the plugin process, and changes the signal handling on the CLI side to close this socket which tells the plugin that it should exit. This implementation makes use of sockets instead of simply setting PDEATHSIG on the plugin process so that it will work on both BSDs, assorted UNIXes and Windows. Signed-off-by: Laura Brehm <laurabrehm@hey.com>
1 parent a46f850 commit 3da28ad

7 files changed

Lines changed: 112 additions & 68 deletions

File tree

cli-plugins/plugin/plugin.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package plugin
22

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
7+
"io"
8+
"net"
69
"os"
710
"sync"
811

@@ -14,6 +17,11 @@ import (
1417
"github.com/spf13/cobra"
1518
)
1619

20+
// CliPluginCloseSocketEnvKey is used to pass the plugin being
21+
// executed the abstract socket name it should listen on to know
22+
// when the CLI has exited.
23+
const CliPluginCloseSocketEnvKey = "CLI_PLUGIN_EXIT_SOCKET"
24+
1725
// PersistentPreRunE must be called by any plugin command (or
1826
// subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins
1927
// which do not make use of `PersistentPreRun*` do not need to call
@@ -24,12 +32,45 @@ import (
2432
// called.
2533
var PersistentPreRunE func(*cobra.Command, []string) error
2634

35+
// closeOnParentSocketClose connects to the socket specified
36+
// by the CLI_PLUGIN_EXIT_SOCKET env var, if present, and attempts
37+
// to read from it until it receives an EOF, which signals that
38+
// the CLI is going to exit and the plugin should also exit.
39+
func closeOnParentSocketClose() {
40+
socketAddr, ok := os.LookupEnv(CliPluginCloseSocketEnvKey)
41+
if !ok {
42+
// if a plugin compiled against a more recent version of docker/cli
43+
// is executed by an older CLI binary, ignore missing environment
44+
// variable and behave as usual
45+
return
46+
}
47+
addr, err := net.ResolveUnixAddr("unix", socketAddr)
48+
if err != nil {
49+
return
50+
}
51+
cliCloseConn, err := net.DialUnix("unix", nil, addr)
52+
if err != nil {
53+
return
54+
}
55+
56+
go func() {
57+
for {
58+
b := make([]byte, 1)
59+
_, err := cliCloseConn.Read(b)
60+
if errors.Is(err, io.EOF) {
61+
os.Exit(0)
62+
}
63+
}
64+
}()
65+
}
66+
2767
// RunPlugin executes the specified plugin command
2868
func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) error {
2969
tcmd := newPluginCommand(dockerCli, plugin, meta)
3070

3171
var persistentPreRunOnce sync.Once
3272
PersistentPreRunE = func(_ *cobra.Command, _ []string) error {
73+
closeOnParentSocketClose()
3374
var err error
3475
persistentPreRunOnce.Do(func() {
3576
var opts []command.InitializeOpt

cmd/docker/docker.go

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,22 @@ package main
22

33
import (
44
"fmt"
5+
"net"
56
"os"
67
"os/exec"
8+
"os/signal"
79
"strings"
810
"syscall"
911

1012
"github.com/docker/cli/cli"
1113
pluginmanager "github.com/docker/cli/cli-plugins/manager"
14+
"github.com/docker/cli/cli-plugins/plugin"
1215
"github.com/docker/cli/cli/command"
1316
"github.com/docker/cli/cli/command/commands"
1417
cliflags "github.com/docker/cli/cli/flags"
1518
"github.com/docker/cli/cli/version"
16-
"github.com/docker/cli/cmd/docker/internal/appcontext"
19+
platformsignals "github.com/docker/cli/cmd/docker/internal/signals"
20+
"github.com/docker/distribution/uuid"
1721
"github.com/docker/docker/api/types/versions"
1822
"github.com/pkg/errors"
1923
"github.com/sirupsen/logrus"
@@ -187,17 +191,58 @@ func setValidateArgs(dockerCli command.Cli, cmd *cobra.Command) {
187191
})
188192
}
189193

194+
func setupPluginSocket() (*net.UnixListener, error) {
195+
uuid := uuid.Generate().String()
196+
pluginCloseSocketAddr := net.UnixAddr{Name: "@docker_cli_" + uuid, Net: "unix"}
197+
return net.ListenUnix("unix", &pluginCloseSocketAddr)
198+
}
199+
190200
func tryPluginRun(dockerCli command.Cli, cmd *cobra.Command, subcommand string, envs []string) error {
191201
plugincmd, err := pluginmanager.PluginRunCommand(dockerCli, subcommand, cmd)
192202
if err != nil {
193203
return err
194204
}
195205
plugincmd.Env = append(envs, plugincmd.Env...)
196206

197-
go func() {
198-
// override SIGTERM handler so we let the plugin shut down first
199-
<-appcontext.Context().Done()
200-
}()
207+
listener, err := setupPluginSocket()
208+
// if we somehow failed to set up the socket to alert the plugin
209+
// when the CLI process wants to exit, fallback to old behaviour
210+
if err == nil {
211+
defer listener.Close()
212+
plugincmd.Env = append(plugincmd.Env, plugin.CliPluginCloseSocketEnvKey+"="+listener.Addr().String())
213+
214+
go func() {
215+
signals := make(chan os.Signal, 2048)
216+
signal.Notify(signals, platformsignals.TerminationSignals...)
217+
218+
const exitLimit = 3
219+
retries := 0
220+
221+
var conn *net.UnixConn
222+
go func() {
223+
conn, err = listener.AcceptUnix()
224+
if err != nil {
225+
// TODO(laurazard): change me, halt execution or fallback to
226+
// previous behaviour if we couldn't set up socket connection
227+
println("got a weird error!", err.Error())
228+
}
229+
}()
230+
231+
go func() {
232+
for {
233+
<-signals
234+
if err := conn.Close(); err != nil {
235+
logrus.Errorf("failed to signal plugin to close: %v", err)
236+
}
237+
retries++
238+
if retries >= exitLimit {
239+
logrus.Errorf("got %d SIGTERM/SIGINTs, exiting without cleanup", retries)
240+
os.Exit(1)
241+
}
242+
}
243+
}()
244+
}()
245+
}
201246

202247
if err := plugincmd.Run(); err != nil {
203248
statusCode := 1

cmd/docker/internal/appcontext/appcontext.go

Lines changed: 0 additions & 44 deletions
This file was deleted.

cmd/docker/internal/appcontext/appcontext_unix.go

Lines changed: 0 additions & 12 deletions
This file was deleted.

cmd/docker/internal/appcontext/appcontext_windows.go

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//go:build !windows
2+
// +build !windows
3+
4+
package signals
5+
6+
import (
7+
"os"
8+
9+
"golang.org/x/sys/unix"
10+
)
11+
12+
// TerminationSignals represents the list of signals we
13+
// want to special-case handle, on this platform.
14+
var TerminationSignals = []os.Signal{unix.SIGTERM, unix.SIGINT}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package signals
2+
3+
import "os"
4+
5+
// TerminationSignals represents the list of signals we
6+
// want to special-case handle, on this platform.
7+
var TerminationSignals = []os.Signal{os.Interrupt}

0 commit comments

Comments
 (0)