Skip to content

Commit 9055092

Browse files
committed
Use unique identifiers for sockets
This commit modifies the plugin binary to accept a single argument as a socket name which is generated upon calling `plugin.Use`. This now allows multiple instances of a plugin to be used on one machine (if that's desirable) and avoids possible clashes where plugins may not have gracefully exited. Signed-off-by: David Bond <davidsbond93@gmail.com>
1 parent 75d936c commit 9055092

File tree

5 files changed

+30
-37
lines changed

5 files changed

+30
-37
lines changed

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ via protocol buffers can be invoked.
1919
```mermaid
2020
graph TD
2121
;
22-
Application["Your application"] <-->|Sends Commands| Socket["/tmp/plugin_example.sock"]
22+
Application["Your application"] <-->|Sends Commands| Socket["/tmp/d1hf45ohpe2r63pr1bk.sock"]
2323
Plugin <-->|Handles Commands| Socket
24-
Application -->|Executes| Plugin["/var/plugins/example"]
24+
Application -->|Executes| Plugin["/var/plugins/example d1hf45ohpe2r63pr1bk"]
2525
```
2626

2727
### Defining commands
@@ -56,6 +56,7 @@ import (
5656
"log"
5757

5858
"github.com/davidsbond/plugin"
59+
command "github.com/yourusername/path/to/protos"
5960
)
6061

6162
func main() {
@@ -84,9 +85,7 @@ This `main.go` file should be compiled and placed on the same machine as the app
8485
### Using plugins
8586

8687
The `plugin.Use` function is used to execute the plugin binary, starting its gRPC server and creating a client that
87-
communicates with it via a UNIX domain socket. Only a single instance of the plugin should exist throughout your
88-
application as its lifecycle is managed via the `Use` and `Close` functions. Below is an example of how we start using
89-
our `add` command:
88+
communicates with it via a UNIX domain socket. Below is an example of how we start using our `add` command:
9089

9190
```go
9291
package main
@@ -96,16 +95,17 @@ import (
9695
"fmt"
9796

9897
"github.com/davidsbond/plugin"
98+
command "github.com/yourusername/path/to/protos"
9999
)
100100

101101
func main() {
102102
ctx := context.Background()
103-
103+
104104
p, err := plugin.Use(ctx, "./example")
105105
if err != nil {
106106
panic(err)
107107
}
108-
108+
109109
// Remember the call Close() when you're done using the plugin!
110110
defer p.Close()
111111

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ tool (
99
)
1010

1111
require (
12+
github.com/rs/xid v1.6.0
1213
github.com/stretchr/testify v1.10.0
1314
golang.org/x/sync v0.15.0
1415
google.golang.org/grpc v1.73.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR
158158
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
159159
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
160160
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
161+
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
162+
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
161163
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
162164
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
163165
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=

internal/plugin/client.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@ type (
1919
}
2020
)
2121

22-
// NewClient attempts to create a new connection to the plugin using the expected UNIX domain socket for the plugin
23-
// name.
24-
func NewClient(name string) (*Client, error) {
25-
conn, err := grpc.NewClient("unix:///tmp/plugin_"+name+".sock",
22+
// NewClient attempts to create a new connection to the plugin using the provided UNIX domain socket.
23+
func NewClient(socket string) (*Client, error) {
24+
conn, err := grpc.NewClient("unix:///tmp/"+socket+".sock",
2625
grpc.WithTransportCredentials(insecure.NewCredentials()),
2726
)
2827
if err != nil {

plugin.go

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
// the plugin interface.
66
//
77
// Plugins are external binaries that serve gRPC requests over a UNIX domain socket on the local machine. Each plugin
8-
// creates a socket using its name (which must be unique across all plugins used locally). Plugins make use of the
9-
// protobuf "Any" type in order to allow user-defined inputs and outputs to keep strong typing across application and
10-
// language boundaries.
8+
// creates a socket using a unique identifier passed as an argument from the host application to the plugin. Plugins
9+
// make use of the protobuf "Any" type in order to allow user-defined inputs and outputs to keep strong typing across
10+
// application and language boundaries.
1111
package plugin
1212

1313
import (
@@ -23,6 +23,7 @@ import (
2323
"syscall"
2424
"time"
2525

26+
"github.com/rs/xid"
2627
"golang.org/x/sync/errgroup"
2728
"google.golang.org/grpc"
2829
"google.golang.org/grpc/codes"
@@ -102,6 +103,10 @@ func Run(config Config) error {
102103
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL)
103104
defer cancel()
104105

106+
if len(os.Args) == 0 {
107+
return errors.New("plugin expects at least one argument")
108+
}
109+
105110
server := grpc.NewServer()
106111

107112
info := plugin.Info{
@@ -117,8 +122,8 @@ func Run(config Config) error {
117122

118123
plugin.NewAPI(info, handlers).Register(server)
119124

120-
socket := "/tmp/plugin_" + config.Name + ".sock"
121-
listener, err := createPluginListener(socket)
125+
socket := "/tmp/" + os.Args[0] + ".sock"
126+
listener, err := net.Listen("unix", socket)
122127
if err != nil {
123128
return err
124129
}
@@ -131,31 +136,12 @@ func Run(config Config) error {
131136
group.Go(func() error {
132137
<-ctx.Done()
133138
server.GracefulStop()
134-
return os.RemoveAll(socket)
139+
return listener.Close()
135140
})
136141

137142
return group.Wait()
138143
}
139144

140-
func createPluginListener(socket string) (net.Listener, error) {
141-
var errno syscall.Errno
142-
143-
listener, err := net.Listen("unix", socket)
144-
switch {
145-
case errors.As(err, &errno) && errors.Is(errno, syscall.EADDRINUSE):
146-
// This socket should only ever be in use by us, so if it's already in use, we'll recreate it.
147-
if err = os.Remove(socket); err != nil {
148-
return nil, err
149-
}
150-
151-
return net.Listen("unix", socket)
152-
case err != nil:
153-
return nil, err
154-
default:
155-
return listener, nil
156-
}
157-
}
158-
159145
func getPluginVersion() string {
160146
if info, ok := debug.ReadBuildInfo(); ok {
161147
return info.Main.Version
@@ -190,8 +176,13 @@ var (
190176
//
191177
// If successful, it is up to the caller to eventually call Plugin.Close when they no longer require use of the plugin.
192178
func Use(ctx context.Context, path string) (*Plugin, error) {
179+
socket := xid.New().String()
180+
193181
cmd := &exec.Cmd{
194182
Path: path,
183+
Args: []string{
184+
socket,
185+
},
195186
}
196187

197188
err := cmd.Start()
@@ -204,7 +195,7 @@ func Use(ctx context.Context, path string) (*Plugin, error) {
204195
}
205196

206197
name := filepath.Base(path)
207-
p.client, err = plugin.NewClient(name)
198+
p.client, err = plugin.NewClient(socket)
208199
if err != nil {
209200
return nil, fmt.Errorf("failed to dial plugin %q: %w", name, err)
210201
}

0 commit comments

Comments
 (0)