feat(systemd): Prepare module for passing FDs to service

Signed-off-by: Steffen Vogel <post@steffenvogel.de>
This commit is contained in:
Steffen Vogel
2025-01-01 23:42:21 +01:00
parent ca039964d1
commit 6b279d54fa
9 changed files with 407 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
// SPDX-FileCopyrightText: 2015 CoreOS, Inc.
// SPDX-FileCopyrightText: 2023-2025 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0
package systemd
import (
"os"
"strconv"
"strings"
"syscall"
)
const (
// listenFdsStart corresponds to `SD_LISTEN_FDS_START`.
listenFdsStart = 3
)
// Files returns a slice containing a `os.File` object for each
// file descriptor passed to this process via systemd fd-passing protocol.
//
// The order of the file descriptors is preserved in the returned slice.
// `unsetEnv` is typically set to `true` in order to avoid clashes in
// fd usage and to avoid leaking environment flags to child processes.
func Files(unsetEnv bool) []*os.File {
if unsetEnv {
defer os.Unsetenv("LISTEN_PID")
defer os.Unsetenv("LISTEN_FDS")
defer os.Unsetenv("LISTEN_FDNAMES")
}
pid, err := strconv.Atoi(os.Getenv("LISTEN_PID"))
if err != nil || pid != os.Getpid() {
return nil
}
nfds, err := strconv.Atoi(os.Getenv("LISTEN_FDS"))
if err != nil || nfds == 0 {
return nil
}
names := strings.Split(os.Getenv("LISTEN_FDNAMES"), ":")
files := make([]*os.File, 0, nfds)
for fd := listenFdsStart; fd < listenFdsStart+nfds; fd++ {
syscall.CloseOnExec(fd)
name := "LISTEN_FD_" + strconv.Itoa(fd)
if offset := fd - listenFdsStart; offset < len(names) && len(names[offset]) > 0 {
name = names[offset]
}
files = append(files, os.NewFile(uintptr(fd), name))
}
return files
}
func NumFiles() int {
lpid, err := strconv.Atoi(os.Getenv("LISTEN_PID"))
if err != nil || lpid != os.Getpid() {
return 0
}
nfds, err := strconv.Atoi(os.Getenv("LISTEN_FDS"))
if err != nil {
return 0
}
return nfds
}

View File

@@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2015 CoreOS, Inc.
// SPDX-FileCopyrightText: 2023-2025 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0
//go:build !linux
package systemd
import (
"os"
)
// Files returns a slice containing a `os.File` object for each
// file descriptor passed to this process via systemd fd-passing protocol.
//
// The order of the file descriptors is preserved in the returned slice.
// `unsetEnv` is typically set to `true` in order to avoid clashes in
// fd usage and to avoid leaking environment flags to child processes.
func Files(_ bool) []*os.File {
return nil
}
func NumFiles() int {
return 0
}

View File

@@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: 2015 CoreOS, Inc.
// SPDX-FileCopyrightText: 2023-2025 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0
//go:build linux
package systemd_test
import (
"os"
"os/exec"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gbytes"
. "github.com/onsi/gomega/gexec"
)
var _ = Context("Files", func() {
var cmd *exec.Cmd
BeforeEach(func() {
path, err := Build("../../../test/systemd/activation.go")
Expect(err).To(Succeed())
cmd = exec.Command(path)
})
// Forks out a copy of activation.go example and reads back two
// strings from the pipes that are passed in.
It("can pass files as FDs", func() {
r1, w1, _ := os.Pipe()
r2, w2, _ := os.Pipe()
cmd.ExtraFiles = []*os.File{w1, w2}
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "LISTEN_FDS=2", "LISTEN_FDNAMES=fd1", "FIX_LISTEN_PID=1")
session, err := Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).To(Succeed())
Eventually(session).Should(Exit(0))
Eventually(BufferReader(r1)).Should(Say("Hello world: fd1"))
Eventually(BufferReader(r2)).Should(Say("Goodbye world: LISTEN_FD_4"))
})
It("fails when FIX_LISTEN_PID is not set", func() {
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "LISTEN_FDS=2")
session, err := Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).To(Succeed())
Eventually(session).Should(Exit(2))
Expect(session.Err).To(Say("No files"))
})
It("fails when no FDs are passed ", func() {
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "LISTEN_FDS=0", "FIX_LISTEN_PID=1")
session, err := Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).To(Succeed())
Eventually(session).Should(Exit(2))
Expect(session.Err).To(Say("No files"))
})
})

View File

@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2015 CoreOS, Inc.
// SPDX-FileCopyrightText: 2023-2025 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0
package systemd
import (
"net"
)
// Listeners returns a slice of net.Listener instances.
func Listeners() (listeners []net.Listener, err error) {
files := Files(true)
for _, f := range files {
l, err := net.FileListener(f)
if err != nil {
continue
}
listeners = append(listeners, l)
f.Close()
}
return listeners, nil
}
// ListenersWithNames maps a listener name to a set of net.Listener instances.
func ListenersWithNames() (map[string][]net.Listener, error) {
files := Files(true)
listeners := map[string][]net.Listener{}
for _, f := range files {
l, err := net.FileListener(f)
if err != nil {
continue
}
if current, ok := listeners[f.Name()]; !ok {
listeners[f.Name()] = []net.Listener{l}
} else {
listeners[f.Name()] = append(current, l)
}
f.Close()
}
return listeners, nil
}

View File

@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2015 CoreOS, Inc.
// SPDX-FileCopyrightText: 2023-2025 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0
//go:build !linux
package systemd
import (
"net"
)
// Listeners returns a slice of net.Listener instances.
func Listeners() (listeners []net.Listener, err error) {
return listeners, nil
}
// ListenersWithNames maps a listener name to a set of net.Listener instances.
func ListenersWithNames() (map[string][]net.Listener, error) {
return map[string][]net.Listener{}, nil
}

View File

@@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: 2015 CoreOS, Inc.
// SPDX-FileCopyrightText: 2023-2025 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0
//go:build linux
package systemd_test
import (
"net"
"os"
"os/exec"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gbytes"
. "github.com/onsi/gomega/gexec"
)
var _ = Context("Listeners", func() {
var cmd *exec.Cmd
BeforeEach(func() {
path, err := Build("../../../test/systemd/listen.go")
Expect(err).To(Succeed())
cmd = exec.Command(path)
})
// Forks out a copy of activation.go example and reads back two
// strings from the pipes that are passed in.
It("can pass listeners", func() {
t1, err := net.ListenTCP("tcp", &net.TCPAddr{Port: 9999})
Expect(err).To(Succeed())
t2, err := net.ListenTCP("tcp", &net.TCPAddr{Port: 1234})
Expect(err).To(Succeed())
f1, err := t1.File()
Expect(err).To(Succeed())
f2, err := t2.File()
Expect(err).To(Succeed())
cmd.ExtraFiles = []*os.File{f1, f2}
r1, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.IPv6loopback, Port: 9999})
Expect(err).To(Succeed())
_, err = r1.Write([]byte("Hi"))
Expect(err).To(Succeed())
r2, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.IPv6loopback, Port: 1234})
Expect(err).To(Succeed())
_, err = r2.Write([]byte("Hi"))
Expect(err).To(Succeed())
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "LISTEN_FDS=2", "LISTEN_FDNAMES=fd1:fd2", "FIX_LISTEN_PID=1")
session, err := Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).To(Succeed())
Eventually(session).Should(Exit(0))
Eventually(BufferReader(r1)).Should(Say("Hello world: fd1"))
Eventually(BufferReader(r2)).Should(Say("Goodbye world: fd2"))
})
})

View File

@@ -11,6 +11,7 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gexec"
)
func TestSuite(t *testing.T) {
@@ -18,3 +19,7 @@ func TestSuite(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "SystemD Suite")
}
var _ = AfterSuite(func() {
CleanupBuildArtifacts()
})