File: systemd.go

package info (click to toggle)
golang-blitiri-go-systemd 1.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, sid, trixie
  • size: 80 kB
  • sloc: makefile: 2
file content (225 lines) | stat: -rw-r--r-- 6,900 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
// Package systemd implements a way to get network listeners from systemd,
// similar to C's sd_listen_fds(3) and sd_listen_fds_with_names(3).
package systemd // import "blitiri.com.ar/go/systemd"

import (
	"errors"
	"fmt"
	"net"
	"os"
	"strconv"
	"strings"
	"sync"
	"syscall"
)

var (
	// Error to return when $LISTEN_PID does not refer to us.
	ErrPIDMismatch = errors.New("$LISTEN_PID != our PID")

	// First FD for listeners.
	// It's 3 by definition, but using a variable simplifies testing.
	firstFD = 3
)

// Keep a single global map of files/listeners, to avoid repeatedly parsing
// which can be problematic (see parse).
var files map[string][]*os.File
var listeners map[string][]net.Listener
var parseError error
var listenError error
var mutex sync.Mutex

// parse files, updating the global state.
// This function messes with file descriptors and environment, so it is not
// idempotent and must be called only once. For the callers' convenience, we
// save the files and listener maps globally, and reuse them on the
// user-visible functions.
func parse() {
	mutex.Lock()
	defer mutex.Unlock()

	if files != nil {
		return
	}

	pidStr := os.Getenv("LISTEN_PID")
	nfdsStr := os.Getenv("LISTEN_FDS")
	fdNamesStr := os.Getenv("LISTEN_FDNAMES")
	fdNames := strings.Split(fdNamesStr, ":")

	// Nothing to do if the variables are not set.
	if pidStr == "" || nfdsStr == "" {
		return
	}

	pid, err := strconv.Atoi(pidStr)
	if err != nil {
		parseError = fmt.Errorf(
			"error converting $LISTEN_PID=%q: %v", pidStr, err)
		return
	} else if pid != os.Getpid() {
		parseError = ErrPIDMismatch
		return
	}

	nfds, err := strconv.Atoi(os.Getenv("LISTEN_FDS"))
	if err != nil {
		parseError = fmt.Errorf(
			"error reading $LISTEN_FDS=%q: %v", nfdsStr, err)
		return
	}

	// If LISTEN_FDNAMES is set at all, it should have as many names as we
	// have descriptors. If it isn't set, then we map them all to "".
	if fdNamesStr == "" {
		fdNames = []string{}
		for i := 0; i < nfds; i++ {
			fdNames = append(fdNames, "")
		}
	} else {
		if nfds > 0 && len(fdNames) != nfds {
			parseError = fmt.Errorf(
				"Incorrect LISTEN_FDNAMES, have you set FileDescriptorName?")
			return
		}
	}

	files = map[string][]*os.File{}
	listeners = map[string][]net.Listener{}

	for i := 0; i < nfds; i++ {
		fd := firstFD + i
		// We don't want childs to inherit these file descriptors.
		syscall.CloseOnExec(fd)

		name := fdNames[i]

		sysName := fmt.Sprintf("[systemd-fd-%d-%v]", fd, name)
		f := os.NewFile(uintptr(fd), sysName)
		files[name] = append(files[name], f)

		// Note this can fail for non-TCP listeners, so we put the error in a
		// separate variable.
		lis, err := net.FileListener(f)
		if err != nil {
			listenError = fmt.Errorf(
				"Error making listener out of fd %d: %v", fd, err)
		} else {
			listeners[name] = append(listeners[name], lis)
		}
	}

	// Remove them from the environment, to prevent accidental reuse (by
	// us or children processes).
	os.Unsetenv("LISTEN_PID")
	os.Unsetenv("LISTEN_FDS")
	os.Unsetenv("LISTEN_FDNAMES")
}

// Listeners returns net.Listeners corresponding to the file descriptors
// passed by systemd via environment variables.
//
// It returns a map of the form (file descriptor name -> []net.Listener).
//
// The file descriptor name comes from the "FileDescriptorName=" option in the
// systemd socket unit. Multiple socket units can have the same name, hence
// the slice of listeners for each name.
//
// If the "FileDescriptorName=" option is not used, then all file descriptors
// are mapped to the "" name.
//
// Ideally you should not need to call this more than once. If you do, the
// same listeners will be returned, as repeated calls to this function will
// return the same results: the parsing is done only once, and the results are
// saved and reused.
//
// See sd_listen_fds(3) and sd_listen_fds_with_names(3) for more details on
// how the passing works.
func Listeners() (map[string][]net.Listener, error) {
	parse()
	if parseError != nil {
		return listeners, parseError
	}
	return listeners, listenError
}

// OneListener returns a net.Listener for the first systemd socket with the
// given name. If there are none, the listener and error will both be nil. An
// error will be returned only if there were issues parsing the file
// descriptors.
//
// This function can be convenient for simple callers where you know there's
// only one file descriptor being passed with the given name.
//
// This is a convenience function built on top of Listeners().
func OneListener(name string) (net.Listener, error) {
	parse()
	if parseError != nil {
		return nil, parseError
	}
	if listenError != nil {
		return nil, listenError
	}

	lis := listeners[name]
	if len(lis) < 1 {
		return nil, nil
	}
	return lis[0], nil
}

// Listen returns a net.Listener for the given address, similar to net.Listen.
//
// If the address begins with "&" it is interpreted as a systemd socket being
// passed.  For example, using "&http" would mean we expect a systemd socket
// passed to us, named with "FileDescriptorName=http" in its unit.
//
// Otherwise, it uses net.Listen to create a new listener with the given net
// and local address.
//
// This function can be convenient for simple callers where you get the
// address from a user, and want to let them specify either "use systemd" or a
// normal address without too much additional complexity.
//
// This is a convenience function built on top of Listeners().
func Listen(netw, laddr string) (net.Listener, error) {
	if strings.HasPrefix(laddr, "&") {
		name := laddr[1:]
		lis, err := OneListener(name)
		if lis == nil && err == nil {
			err = fmt.Errorf("systemd socket %q not found", name)
		}
		return lis, err
	} else {
		return net.Listen(netw, laddr)
	}
}

// Files returns the open files passed by systemd via environment variables.
//
// It returns a map of the form (file descriptor name -> []*os.File).
//
// The file descriptor name comes from the "FileDescriptorName=" option in the
// systemd socket unit. Multiple socket units can have the same name, hence
// the slice of listeners for each name.
//
// If the "FileDescriptorName=" option is not used, then all file descriptors
// are mapped to the "" name.
//
// Ideally you should not need to call this more than once. If you do, the
// same files will be returned, as repeated calls to this function will return
// the same results: the parsing is done only once, and the results are saved
// and reused.
//
// See sd_listen_fds(3) and sd_listen_fds_with_names(3) for more details on
// how the passing works.
//
// Normally you would use Listeners instead; however, access to the file
// descriptors can be useful if you need more fine-grained control over
// listener creation, for example if you need to create packet connections
// from them.
func Files() (map[string][]*os.File, error) {
	parse()
	return files, parseError
}