1package swarm
2
3import (
4 "crypto/sha1"
5 "encoding/hex"
6 "errors"
7 "fmt"
8 "net"
9 "os"
10 "path/filepath"
11 "runtime"
12)
13
14// maxUnixSocketPath is the conservative platform-portable path limit
15// for unix sockets. macOS allows 104, linux 108 (including the NUL
16// terminator). We pick 100 so the path itself plus a small filename
17// tail stays under both caps with a safety margin.
18const maxUnixSocketPath = 100
19
20// inboxSocketPath returns a per-agent unix-socket path in a transient
21// runtime directory. Durable swarm state can live on filesystems such as
22// NFS, 9p, or shared VM mounts that support regular files but reject unix
23// socket nodes, so the socket must not be placed below root.
24//
25// Candidate directories are tried in this order:
26//
27// 1. $XDG_RUNTIME_DIR, when absolute;
28// 2. the platform temporary directory; and
29// 3. /tmp as a Unix fallback.
30//
31// Each candidate is probed with a real unix listener before it is selected.
32// The root hash keeps separate zot homes from colliding while preserving a
33// stable path across parent and child processes and across Resume calls.
34func inboxSocketPath(root, agentID string) (string, error) {
35 if runtime.GOOS == "windows" {
36 // Keep swarm construction usable for callers with custom runners.
37 // The production listener still reports that its unix transport is
38 // unavailable, as it did before runtime-directory probing was added.
39 return filepath.Join(root, "agents", agentID, "in.sock"), nil
40 }
41
42 var bases []string
43 if runtimeDir := os.Getenv("XDG_RUNTIME_DIR"); filepath.IsAbs(runtimeDir) {
44 bases = append(bases, runtimeDir)
45 }
46 bases = append(bases, os.TempDir(), "/tmp")
47
48 seen := make(map[string]bool)
49 var candidateErrs []error
50 for _, base := range bases {
51 base = filepath.Clean(base)
52 if base == "." || seen[base] {
53 continue
54 }
55 seen[base] = true
56
57 path, err := socketPathInBase(base, root, agentID)
58 if err != nil {
59 candidateErrs = append(candidateErrs, err)
60 continue
61 }
62 return path, nil
63 }
64 return "", fmt.Errorf("no usable unix socket directory: %w", errors.Join(candidateErrs...))
65}
66
67func socketPathInBase(base, root, agentID string) (string, error) {
68 dir := filepath.Join(base, "zot-swarm-"+rootTag(root))
69 if err := os.MkdirAll(dir, 0o700); err != nil {
70 return "", fmt.Errorf("socket dir %s: %w", dir, err)
71 }
72
73 candidate := filepath.Join(dir, agentID+".sock")
74 if len(candidate) > maxUnixSocketPath {
75 candidate = filepath.Join(dir, shortHash(agentID)+".sock")
76 }
77 if len(candidate) > maxUnixSocketPath {
78 return "", fmt.Errorf("unix socket path too long (%s, %d > %d)", candidate, len(candidate), maxUnixSocketPath)
79 }
80 if err := probeUnixSocket(dir); err != nil {
81 return "", fmt.Errorf("unix sockets unavailable in %s: %w", dir, err)
82 }
83 return candidate, nil
84}
85
86func probeUnixSocket(dir string) error {
87 probe, err := os.CreateTemp(dir, ".socket-probe-")
88 if err != nil {
89 return err
90 }
91 path := probe.Name()
92 if err := probe.Close(); err != nil {
93 _ = os.Remove(path)
94 return err
95 }
96 if err := os.Remove(path); err != nil {
97 return err
98 }
99
100 ln, err := net.Listen("unix", path)
101 if err != nil {
102 return err
103 }
104 closeErr := ln.Close()
105 removeErr := os.Remove(path)
106 if os.IsNotExist(removeErr) {
107 removeErr = nil
108 }
109 return errors.Join(closeErr, removeErr)
110}
111
112// rootTag returns a stable 8-hex-char tag for the swarm root. Used
113// in the runtime-directory name so two parallel zot instances with
114// different roots don't share sockets.
115func rootTag(root string) string { return shortHash(root) }
116
117func shortHash(s string) string {
118 sum := sha1.Sum([]byte(s))
119 return hex.EncodeToString(sum[:4])
120}