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()
})

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
//go:build ignore
// Activation example used by the activation unit tests.
package main
import (
"fmt"
"os"
"cunicu.li/cunicu/pkg/os/systemd"
)
func fixListenPid() {
if os.Getenv("FIX_LISTEN_PID") != "" {
// HACK: real systemd would set LISTEN_PID before exec'ing but
// this is too difficult in golang for the purpose of a test.
// Do not do this in real code.
os.Setenv("LISTEN_PID", fmt.Sprintf("%d", os.Getpid()))
}
}
func main() {
fixListenPid()
files := systemd.Files(false)
if len(files) == 0 {
panic("No files")
}
if os.Getenv("LISTEN_PID") == "" || os.Getenv("LISTEN_FDS") == "" || os.Getenv("LISTEN_FDNAMES") == "" {
panic("Should not unset envs")
}
files = systemd.Files(true)
if os.Getenv("LISTEN_PID") != "" || os.Getenv("LISTEN_FDS") != "" || os.Getenv("LISTEN_FDNAMES") != "" {
panic("Can not unset envs")
}
// Write out the expected strings to the two pipes
files[0].Write([]byte("Hello world: " + files[0].Name()))
files[1].Write([]byte("Goodbye world: " + files[1].Name()))
return
}

46
test/systemd/listen.go Normal file
View File

@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2015 CoreOS, Inc.
// SPDX-FileCopyrightText: 2023-2025 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0
//go:build ignore
// Activation example used by the activation unit tests.
package main
import (
"fmt"
"os"
"cunicu.li/cunicu/pkg/os/systemd"
)
func fixListenPid() {
if os.Getenv("FIX_LISTEN_PID") != "" {
// HACK: real systemd would set LISTEN_PID before exec'ing but
// this is too difficult in golang for the purpose of a test.
// Do not do this in real code.
os.Setenv("LISTEN_PID", fmt.Sprintf("%d", os.Getpid()))
}
}
func main() {
fixListenPid()
listenersWithNames, err := systemd.ListenersWithNames()
if err != nil {
panic(err)
}
if os.Getenv("LISTEN_PID") != "" || os.Getenv("LISTEN_FDS") != "" || os.Getenv("LISTEN_FDNAMES") != "" {
panic("Can not unset envs")
}
c0, _ := listenersWithNames["fd1"][0].Accept()
c1, _ := listenersWithNames["fd2"][0].Accept()
// Write out the expected strings to the two pipes
c0.Write([]byte("Hello world: fd1"))
c1.Write([]byte("Goodbye world: fd2"))
return
}