mirror of
https://codeberg.org/cunicu/cunicu.git
synced 2025-09-26 21:01:14 +08:00
feat(systemd): Prepare module for passing FDs to service
Signed-off-by: Steffen Vogel <post@steffenvogel.de>
This commit is contained in:
72
pkg/os/systemd/files_linux.go
Normal file
72
pkg/os/systemd/files_linux.go
Normal 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
|
||||
}
|
25
pkg/os/systemd/files_others.go
Normal file
25
pkg/os/systemd/files_others.go
Normal 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
|
||||
}
|
69
pkg/os/systemd/files_test.go
Normal file
69
pkg/os/systemd/files_test.go
Normal 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"))
|
||||
})
|
||||
})
|
50
pkg/os/systemd/listeners_linux.go
Normal file
50
pkg/os/systemd/listeners_linux.go
Normal 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
|
||||
}
|
21
pkg/os/systemd/listeners_others.go
Normal file
21
pkg/os/systemd/listeners_others.go
Normal 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
|
||||
}
|
69
pkg/os/systemd/listeners_test.go
Normal file
69
pkg/os/systemd/listeners_test.go
Normal 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"))
|
||||
})
|
||||
})
|
@@ -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()
|
||||
})
|
||||
|
Reference in New Issue
Block a user