File: systemd.go

package info (click to toggle)
golang-blitiri-go-systemd 0.0+git20170821.0.aec3508-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye, buster, sid
  • size: 68 kB
  • ctags: 25
  • sloc: makefile: 2
file content (173 lines) | stat: -rw-r--r-- 5,220 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
// 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 listeners, to avoid repeatedly parsing which
// can be problematic (see parse).
var listeners map[string][]net.Listener
var parseError error
var mutex sync.Mutex

// parse listeners, 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 listeners map globally and reuse it on the user-visible functions.
func parse() {
	mutex.Lock()
	defer mutex.Unlock()

	if listeners != 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
	}

	// We should have as many names as we have descriptors.
	// Note that if we have no descriptors, fdNames will be [""] (due to how
	// strings.Split works), so we consider that special case.
	if nfds > 0 && (fdNamesStr == "" || len(fdNames) != nfds) {
		parseError = fmt.Errorf(
			"Incorrect LISTEN_FDNAMES, have you set FileDescriptorName?")
		return
	}

	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)
		lis, err := net.FileListener(os.NewFile(uintptr(fd), sysName))
		if err != nil {
			parseError = fmt.Errorf(
				"Error making listener out of fd %d: %v", fd, err)
			return
		}

		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 a map of listeners for the file descriptors passed by
// systemd via environment variables.
//
// It returns a map of the form (file descriptor name -> slice of listeners).
//
// 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.
//
// 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()
	return listeners, parseError
}

// OneListener returns a 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
	}

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

// Listen returns a 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)
	}
}