Skip to content

Commit 8753b39

Browse files
committed
cmd/go-cache-plugin: use TCP loopback instead of a Unix socket
A Unix-domain socket does not mesh well when the server may run as a different user from the Go toolchain. Specifically, connecting to a Unix socket requires the caller to have write permission to the socket, and it is tricky (and OS dependent) to chmod a socket once created. So, instead use a loopback TCP socket. Update the environment, help text, and flags to use "--plugin" and "GOCACHE_PLUGIN" instead of "socket". Update the connect command to accept a port instead of a path.
1 parent bd97937 commit 8753b39

File tree

3 files changed

+54
-29
lines changed

3 files changed

+54
-29
lines changed

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ The `go-cache-plugin` program supports two modes of operation:
3131
This is the default mode of operation, and requires no additional setup.
3232

3333
2. **Server mode**: The program runs as a separate process and the Go toolchain
34-
communicates with it over a Unix-domain socket.
34+
communicates with it over a local socket.
3535

3636
This mode requires the server to be started up ahead of time, but makes the
3737
configuration for the toolchain simpler. This mode also permits running an
@@ -42,18 +42,19 @@ The `go-cache-plugin` program supports two modes of operation:
4242
To run in server mode, use the `serve` subcommand:
4343

4444
```sh
45-
# N.B.: The --socket flag is required.
45+
# N.B.: The --plugin flag is required.
4646
go-cache-plugin serve \
47-
--socket=/tmp/gocache.sock \
47+
--plugin=5930 \
4848
--cache-dir=/tmp/gocache \
4949
--bucket=some-s3-bucket
5050
```
5151

5252
To connect to a server running in this mode, use the `connect` subcommand:
5353

5454
```sh
55-
# Use the same socket path given to the server's --socket flag.
56-
export GOCACHEPROG="go-cache-plugin connect /tmp/gocache.sock"
55+
# Use the same port given to the server's --plugin flag.
56+
# Mnemonic: 5930 == (Go) (C)ache (P)lugin
57+
export GOCACHEPROG="go-cache-plugin connect 5930
5758
go build ./...
5859
```
5960
@@ -67,7 +68,7 @@ module proxy uses HTTP, not the plugin interface, use `--http` to set the addres
6768
6869
```sh
6970
go-cache-plugin serve \
70-
--socket=/tmp/gocache.sock \
71+
--plugin=5930 \
7172
--http=localhost:5970 --modproxy \
7273
--cache-dir=/tmp/gocache \
7374
# ... other flags

cmd/go-cache-plugin/commands.go

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"os/signal"
1414
"path"
1515
"path/filepath"
16+
"strconv"
1617
"strings"
1718
"syscall"
1819
"time"
@@ -119,7 +120,7 @@ func runDirect(env *command.Env) error {
119120
}
120121

121122
var serveFlags struct {
122-
Socket string `flag:"socket,default=$GOCACHE_SOCKET,Socket path (required)"`
123+
Plugin int `flag:"plugin,default=$GOCACHE_PLUGIN,Plugin service port (required)"`
123124
HTTP string `flag:"http,default=$GOCACHE_HTTP,HTTP service address ([host]:port)"`
124125
ModProxy bool `flag:"modproxy,default=$GOCACHE_MODPROXY,Enable a Go module proxy (requires --http)"`
125126
RevProxy string `flag:"revproxy,default=$GOCACHE_REVPROXY,Reverse proxy these hosts (comma-separated)"`
@@ -128,10 +129,10 @@ var serveFlags struct {
128129

129130
func noopClose(context.Context) error { return nil }
130131

131-
// runServe runs a cache communicating over a Unix-domain socket.
132+
// runServe runs a cache communicating over a local TCP socket.
132133
func runServe(env *command.Env) error {
133-
if serveFlags.Socket == "" {
134-
return env.Usagef("you must provide a --socket path")
134+
if serveFlags.Plugin <= 0 {
135+
return env.Usagef("you must provide a --plugin port")
135136
}
136137

137138
// Initialize the cache server. Unlike a direct server, only close down and
@@ -144,11 +145,11 @@ func runServe(env *command.Env) error {
144145
s.Close = noopClose
145146

146147
// Listen for connections from the Go toolchain on the specified socket.
147-
lst, err := net.Listen("unix", serveFlags.Socket)
148+
lst, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", serveFlags.Plugin))
148149
if err != nil {
149150
return fmt.Errorf("listen: %w", err)
150151
}
151-
defer os.Remove(serveFlags.Socket) // best-effort
152+
log.Printf("plugin listening at %q", lst.Addr())
152153

153154
ctx, cancel := signal.NotifyContext(env.Context(), syscall.SIGINT, syscall.SIGTERM)
154155
defer cancel()
@@ -171,7 +172,7 @@ func runServe(env *command.Env) error {
171172
Handler: mux,
172173
}
173174
g.Go(srv.ListenAndServe)
174-
vprintf("started HTTP server at %q", serveFlags.HTTP)
175+
vprintf("HTTP server listening at %q", serveFlags.HTTP)
175176
g.Go(taskgroup.NoError(func() {
176177
<-ctx.Done()
177178
vprintf("signal received, stopping HTTP service")
@@ -271,30 +272,53 @@ func runServe(env *command.Env) error {
271272
return nil
272273
}
273274

274-
// runConnect implements a direct cache proxy by connecting to a remote server
275-
// over a Unix-domain socket.
276-
func runConnect(env *command.Env, socketPath string) error {
277-
if socketPath == "" {
278-
return env.Usagef("you must provide a socket path")
275+
// runConnect implements a direct cache proxy by connecting to a remote server.
276+
func runConnect(env *command.Env, plugin string) error {
277+
port, err := strconv.Atoi(plugin)
278+
if err != nil {
279+
return fmt.Errorf("invalid plugin port: %w", err)
279280
}
280-
conn, err := net.Dial("unix", socketPath)
281+
282+
conn, err := net.Dial("tcp", fmt.Sprintf(":%d", port))
281283
if err != nil {
282-
return fmt.Errorf("dial socket: %w", err)
284+
return fmt.Errorf("dial: %w", err)
283285
}
284286
start := time.Now()
285-
vprintf("connected to %q", socketPath)
287+
vprintf("connected to %q", conn.RemoteAddr())
286288

287289
out := taskgroup.Go(func() error {
288-
_, err := io.Copy(os.Stdout, conn)
289-
return err
290+
defer conn.(*net.TCPConn).CloseWrite() // let the server finish
291+
return copy(conn, os.Stdin)
290292
})
291-
292-
_, rerr := io.Copy(conn, os.Stdin)
293-
if rerr != nil {
294-
vprintf("error sending: %v", rerr)
293+
if rerr := copy(os.Stdout, conn); rerr != nil {
294+
vprintf("read responses: %v", err)
295295
}
296-
conn.Close()
297296
out.Wait()
297+
conn.Close()
298298
vprintf("connection closed (%v elapsed)", time.Since(start))
299299
return nil
300300
}
301+
302+
// copy emulates the base case of io.Copy, but does not attempt to use the
303+
// io.ReaderFrom or io.WriterTo implementations.
304+
//
305+
// TODO(creachadair): For some reason io.Copy does not work correctly when r is
306+
// a pipe (e.g., stdin) and w is a TCP socket. Figure out why.
307+
func copy(w io.Writer, r io.Reader) error {
308+
var buf [4096]byte
309+
for {
310+
nr, err := r.Read(buf[:])
311+
if nr > 0 {
312+
if nw, err := w.Write(buf[:nr]); err != nil {
313+
return fmt.Errorf("copy to: %w", err)
314+
} else if nw < nr {
315+
return fmt.Errorf("wrote %d < %d bytes: %w", nw, nr, io.ErrShortWrite)
316+
}
317+
}
318+
if err == io.EOF {
319+
return nil
320+
} else if err != nil {
321+
return fmt.Errorf("copy from: %w", err)
322+
}
323+
}
324+
}

cmd/go-cache-plugin/help.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ settings can be set via environment variables as well as flags.
4747
--------------------------------------------------------------------
4848
Flag (serve) Variable Format Default
4949
--------------------------------------------------------------------
50-
--socket GOCACHE_SOCKET path (required)
50+
--plugin GOCACHE_PLUGIN port (required)
5151
--http GOCACHE_HTTP [host]:port ""
5252
--modproxy GOCACHE_MODPROXY bool false
5353
--revproxy GOCACHE_REVPROXY host,... ""

0 commit comments

Comments
 (0)