├── .travis.yml ├── LICENSE ├── README.md ├── booted.go ├── booted_test.go ├── doc.go ├── socket.go └── socket_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.6 5 | - 1.7 6 | - 1.8 7 | - tip 8 | before_install: 9 | - go get golang.org/x/tools/cmd/cover 10 | - go get github.com/mattn/goveralls 11 | - go get github.com/golang/lint/golint 12 | script: 13 | - go test -v -covermode=count -coverprofile=coverage.out 14 | - go vet ./... 15 | - test -z "$(gofmt -d -s . | tee /dev/stderr)" 16 | - test -z "$(golint ./... | tee /dev/stderr)" 17 | - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Julien Schmidt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # systemd [![Build Status](https://travis-ci.org/julienschmidt/systemd.svg?branch=master)](https://travis-ci.org/julienschmidt/systemd) [![Coverage Status](https://coveralls.io/repos/github/julienschmidt/systemd/badge.svg?branch=master)](https://coveralls.io/github/julienschmidt/systemd?branch=master) [![GoDoc](https://godoc.org/github.com/julienschmidt/systemd?status.svg)](https://godoc.org/github.com/julienschmidt/systemd) 2 | 3 | This package provides native systemd integration for Go programs. 4 | 5 | ## Socket Activation 6 | 7 | systemd socket activation (or any other compatible socket passing system passing sockets via `LISTEN_FDS` enviornment variables) is enabled by [systemd.Listen](https://godoc.org/github.com/julienschmidt/systemd#Listen) and [systemd.ListenWithNames](https://godoc.org/github.com/julienschmidt/systemd#ListenWithNames). 8 | -------------------------------------------------------------------------------- /booted.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Julien Schmidt. All rights reserved. 2 | // Use of this source code is governed by MIT license, 3 | // a copy can be found in the LICENSE file. 4 | 5 | package systemd 6 | 7 | import ( 8 | "os" 9 | ) 10 | 11 | // Booted checks whether the system was booted up using the systemd init system. 12 | // This functions internally checks whether the runtime unit file directory 13 | // "/run/systemd/system" exists and is thus specific to systemd. 14 | func Booted() bool { 15 | fi, err := os.Lstat("/run/systemd/system") 16 | return err == nil && fi.IsDir() 17 | } 18 | -------------------------------------------------------------------------------- /booted_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Julien Schmidt. All rights reserved. 2 | // Use of this source code is governed by MIT license, 3 | // a copy can be found in the LICENSE file. 4 | 5 | package systemd 6 | 7 | import ( 8 | "os/exec" 9 | "testing" 10 | ) 11 | 12 | func TestBooted(t *testing.T) { 13 | booted := Booted() 14 | isDir := exec.Command("ls", "/run/systemd/system").Run() == nil 15 | if booted != isDir { 16 | t.Fatalf("/run/systemd/system is a dir: %t, Booted(): %t", isDir, booted) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Julien Schmidt. All rights reserved. 2 | // Use of this source code is governed by MIT license, 3 | // a copy can be found in the LICENSE file. 4 | 5 | // Package systemd provides functions for native systemd integration. 6 | package systemd 7 | -------------------------------------------------------------------------------- /socket.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Julien Schmidt. All rights reserved. 2 | // Use of this source code is governed by MIT license, 3 | // a copy can be found in the LICENSE file. 4 | 5 | package systemd 6 | 7 | import ( 8 | "errors" 9 | "net" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "syscall" 14 | ) 15 | 16 | // See https://github.com/systemd/systemd/blob/master/src/libsystemd/sd-daemon/sd-daemon.c 17 | 18 | const fdStart = 3 // first systemd socket 19 | 20 | // Socket is the abstraction of a socket passed as a file descriptor by systemd. 21 | type Socket struct { 22 | f *os.File 23 | } 24 | 25 | func newSocket(fd int, name string) Socket { 26 | // set the close-on-exec flag for the file descriptor 27 | syscall.CloseOnExec(fd) 28 | 29 | return Socket{os.NewFile(uintptr(fd), name)} 30 | } 31 | 32 | // Fd returns the integer Unix file descriptor referencing the open socket. 33 | // The file descriptor is valid only until s.Close is called or s is garbage 34 | // collected. 35 | func (s *Socket) Fd() uintptr { 36 | return s.f.Fd() 37 | } 38 | 39 | // Name returns the name assigned to the socket. 40 | func (s *Socket) Name() string { 41 | return s.f.Name() 42 | } 43 | 44 | // Close closes the Socket, rendering it unusable for I/O. 45 | // It returns an error, if any. 46 | func (s *Socket) Close() error { 47 | return s.f.Close() 48 | } 49 | 50 | // File returns the underlying os.File of the socket. 51 | // Closing f does also close s and closing s does also close f. 52 | func (s *Socket) File() (f *os.File) { 53 | return s.f 54 | } 55 | 56 | // Listener returns a copy of the network listener corresponding to the open 57 | // socket s. 58 | // It is the caller's responsibility to close ln when finished. 59 | // Closing ln does not affect s, and closing s does not affect ln. 60 | func (s *Socket) Listener() (ln net.Listener, err error) { 61 | return net.FileListener(s.f) 62 | } 63 | 64 | // Conn returns a copy of the network connection corresponding to the open 65 | // socket s. 66 | // It is the caller's responsibility to close s when finished. 67 | // Closing c does not affect s, and closing s does not affect c. 68 | func (s *Socket) Conn() (c net.Conn, err error) { 69 | return net.FileConn(s.f) 70 | } 71 | 72 | // PacketConn returns a copy of the packet network connection corresponding 73 | // to the open socket s. 74 | // It is the caller's responsibility to close s when finished. 75 | // Closing c does not affect s, and closing s does not affect c. 76 | func (s *Socket) PacketConn() (c net.PacketConn, err error) { 77 | return net.FilePacketConn(s.f) 78 | } 79 | 80 | func parseEnv() (n int, err error) { 81 | envPID := os.Getenv("LISTEN_PID") 82 | envFDs := os.Getenv("LISTEN_FDS") 83 | 84 | // In Go programs there should be no need to unset the environment variables 85 | // as there is no API for forking. 86 | // if unsetEnv { 87 | // os.Unsetenv("LISTEN_PID") 88 | // os.Unsetenv("LISTEN_FDS") 89 | // os.Unsetenv("LISTEN_FDNAMES") 90 | // } 91 | 92 | if len(envPID) == 0 { 93 | err = errors.New("listen environment not set") 94 | return 95 | } 96 | 97 | pid, err := strconv.Atoi(envPID) 98 | if err != nil { 99 | err = errors.New("invalid listen PID") 100 | return 101 | } 102 | 103 | if pid != os.Getpid() { 104 | err = errors.New("listen PID does not match") 105 | return 106 | } 107 | 108 | n, err = strconv.Atoi(envFDs) 109 | if err != nil { 110 | err = errors.New("invalid number of file descriptors") 111 | } 112 | return 113 | } 114 | 115 | func parseNames(n int) (names []string, err error) { 116 | envNames := os.Getenv("LISTEN_FDNAMES") 117 | if len(envNames) < 1 { 118 | return nil, errors.New("socket names not set") 119 | } 120 | 121 | names = strings.SplitN(envNames, ":", n) 122 | if len(names) != n || strings.IndexByte(names[n-1], ':') >= 0 { 123 | return nil, errors.New("mismatch between number of socket and socket names:" + 124 | " expected " + strconv.Itoa(n) + 125 | ", got " + strconv.Itoa(strings.Count(envNames, ":")+1)) 126 | } 127 | return 128 | } 129 | 130 | // Listen returns sockets passed by the service manager as part of the 131 | // socket-based activation logic. 132 | // If no sockets have been received, an empty slice is returned. 133 | // If more than one socket is received, they will be passed in the same order as 134 | // configured in the systemd socket unit file. 135 | func Listen() (sockets []Socket, err error) { 136 | n, err := parseEnv() 137 | if n < 1 { // includes err != nil case 138 | return 139 | } 140 | 141 | sockets = make([]Socket, n) 142 | for i := 0; i < n; i++ { 143 | fd := fdStart + i 144 | sockets[i] = newSocket(fd, "/proc/self/fd/"+strconv.Itoa(fd)) 145 | } 146 | return 147 | } 148 | 149 | // ListenWithNames is like Listen but also assigns passed names to the sockets. 150 | // The name can be used to identify a socket. 151 | // Names can be assigned in the systemd unit files. 152 | func ListenWithNames() (files []Socket, err error) { 153 | n, err := parseEnv() 154 | if n < 1 { // includes err != nil case 155 | return 156 | } 157 | 158 | names, err := parseNames(n) 159 | if err != nil { 160 | return 161 | } 162 | 163 | files = make([]Socket, n) 164 | for i := 0; i < n; i++ { 165 | fd := fdStart + i 166 | files[i] = newSocket(fd, names[i]) 167 | } 168 | return 169 | } 170 | -------------------------------------------------------------------------------- /socket_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Julien Schmidt. All rights reserved. 2 | // Use of this source code is governed by MIT license, 3 | // a copy can be found in the LICENSE file. 4 | 5 | package systemd 6 | 7 | import ( 8 | "errors" 9 | "io" 10 | "net" 11 | "os" 12 | "strconv" 13 | "strings" 14 | "testing" 15 | ) 16 | 17 | // https://github.com/golang/go/commit/c05b06a12d005f50e4776095a60d6bd9c2c91fac 18 | // causes file descriptors to remain open after the first file I/O. 19 | // We therefore can not rely on only 2 open file descriptors in our tests and 20 | // have to use a workaround and open 2 file descriptors right at init and keep 21 | // reusing them. 22 | var r, w *os.File 23 | 24 | func init() { 25 | r, w, _ = os.Pipe() 26 | } 27 | 28 | func prepareEnv(t *testing.T, setPID, setFDs, useFDs bool) { 29 | os.Clearenv() 30 | if setPID { 31 | os.Setenv("LISTEN_PID", strconv.Itoa(os.Getpid())) 32 | } 33 | 34 | if setFDs { 35 | os.Setenv("LISTEN_FDS", "2") 36 | } 37 | 38 | if useFDs { 39 | if rfd := r.Fd(); rfd != fdStart { 40 | cleanEnv(r, w) 41 | t.Fatalf("unexpected fd: expected %d, got %d", fdStart, rfd) 42 | } 43 | if wfd := w.Fd(); wfd != fdStart+1 { 44 | cleanEnv(r, w) 45 | t.Fatalf("unexpected fd: expected %d, got %d", fdStart, wfd) 46 | } 47 | } 48 | 49 | return 50 | } 51 | 52 | func prepareNames(n int) { 53 | if n < 1 { 54 | os.Setenv("LISTEN_FDNAMES", "") 55 | return 56 | } 57 | 58 | names := "" 59 | for i := 0; i < n; i++ { 60 | names += ":fd" + strconv.Itoa(i+fdStart) 61 | } 62 | os.Setenv("LISTEN_FDNAMES", names[1:]) 63 | } 64 | 65 | func cleanEnv(r, w *os.File) { 66 | os.Unsetenv("LISTEN_PID") 67 | os.Unsetenv("LISTEN_FDS") 68 | os.Unsetenv("LISTEN_FDNAMES") 69 | } 70 | 71 | func checkWrite(w io.Writer, r io.Reader) (err error) { 72 | testStr := "This test is totally sufficient\n" 73 | 74 | if _, err = w.Write([]byte(testStr)); err != nil { 75 | return 76 | } 77 | 78 | buf := make([]byte, 1024) 79 | n, err := io.ReadAtLeast(r, buf, len(testStr)) 80 | if err != nil { 81 | return 82 | } 83 | 84 | if n != len(testStr) || string(buf[:n]) != testStr { 85 | return errors.New("string mismatch") 86 | } 87 | 88 | return 89 | } 90 | 91 | func TestListen(t *testing.T) { 92 | prepareEnv(t, true, true, true) 93 | defer cleanEnv(r, w) 94 | 95 | sockets, err := Listen() 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | 100 | if len(sockets) != 2 { 101 | t.Fatalf("unexpected number of sockets: expected 2, got %d", len(sockets)) 102 | } 103 | 104 | if r.Fd() != sockets[0].Fd() || w.Fd() != sockets[1].Fd() { 105 | t.Fatalf("file descriptor mismatch: %d=%d, %d=%d", r.Fd(), sockets[0].Fd(), w.Fd(), sockets[1].Fd()) 106 | } 107 | 108 | if err = checkWrite(sockets[1].File(), sockets[0].File()); err != nil { 109 | t.Fatal(err) 110 | } 111 | } 112 | 113 | func TestListenNoPID(t *testing.T) { 114 | prepareEnv(t, false, true, true) 115 | defer cleanEnv(r, w) 116 | 117 | if _, err := Listen(); err == nil { 118 | t.Fatal("did not fail when PID was not set") 119 | } 120 | } 121 | 122 | func TestListenInvalidPID(t *testing.T) { 123 | prepareEnv(t, true, true, true) 124 | os.Setenv("LISTEN_PID", "Gordon") 125 | defer cleanEnv(r, w) 126 | 127 | if _, err := Listen(); err == nil { 128 | t.Fatal("did not fail when PID was invalid") 129 | } 130 | } 131 | 132 | func TestListenWrongPID(t *testing.T) { 133 | prepareEnv(t, true, true, true) 134 | os.Setenv("LISTEN_PID", "1") 135 | defer cleanEnv(r, w) 136 | 137 | if _, err := Listen(); err == nil { 138 | t.Fatal("did not fail when PID mismatched") 139 | } 140 | } 141 | 142 | func TestListenNoFDs(t *testing.T) { 143 | prepareEnv(t, true, false, true) 144 | defer cleanEnv(r, w) 145 | 146 | if _, err := Listen(); err == nil { 147 | t.Fatal("did not fail when FDs were not set") 148 | } 149 | } 150 | 151 | func checkListenWithNames(t *testing.T, names []string) { 152 | prepareEnv(t, true, true, true) 153 | os.Setenv("LISTEN_FDNAMES", strings.Join(names, ":")) 154 | defer cleanEnv(r, w) 155 | 156 | sockets, err := ListenWithNames() 157 | if err != nil { 158 | t.Fatal(err) 159 | } 160 | 161 | if len(sockets) != len(names) { 162 | t.Fatalf("unexpected number of sockets: expected %d, got %d", len(names), len(sockets)) 163 | } 164 | 165 | for i, name := range names { 166 | if sockets[i].Name() != name { 167 | t.Fatalf("unexpected socket name: expected %s, got %s", name, sockets[i].Name()) 168 | } 169 | } 170 | 171 | if r.Fd() != sockets[0].Fd() || w.Fd() != sockets[1].Fd() { 172 | t.Fatalf("file descriptor mismatch: %d=%d, %d=%d", r.Fd(), sockets[0].Fd(), w.Fd(), sockets[1].Fd()) 173 | } 174 | 175 | if err = checkWrite(sockets[1].File(), sockets[0].File()); err != nil { 176 | t.Fatal(err) 177 | } 178 | } 179 | 180 | func TestListenWithNames(t *testing.T) { 181 | checkListenWithNames(t, []string{"fd3", "fd4"}) 182 | } 183 | 184 | func TestListenWithNamesEmpty(t *testing.T) { 185 | checkListenWithNames(t, []string{"", ""}) 186 | } 187 | 188 | func TestListenWithNamesNoPID(t *testing.T) { 189 | prepareEnv(t, false, true, true) 190 | prepareNames(2) 191 | defer cleanEnv(r, w) 192 | 193 | if _, err := ListenWithNames(); err == nil { 194 | t.Fatal("did not fail when PID was not set") 195 | } 196 | } 197 | 198 | func TestListenWithNamesInvalidPID(t *testing.T) { 199 | prepareEnv(t, true, true, true) 200 | prepareNames(2) 201 | os.Setenv("LISTEN_PID", "Gordon") 202 | defer cleanEnv(r, w) 203 | 204 | if _, err := ListenWithNames(); err == nil { 205 | t.Fatal("did not fail when PID was invalid") 206 | } 207 | } 208 | 209 | func TestListenWithNamesWrongPID(t *testing.T) { 210 | prepareEnv(t, true, true, true) 211 | prepareNames(2) 212 | os.Setenv("LISTEN_PID", "1") 213 | defer cleanEnv(r, w) 214 | 215 | if _, err := ListenWithNames(); err == nil { 216 | t.Fatal("did not fail when PID mismatched") 217 | } 218 | } 219 | 220 | func TestListenWithNamesNoFDs(t *testing.T) { 221 | prepareEnv(t, true, false, true) 222 | prepareNames(2) 223 | defer cleanEnv(r, w) 224 | 225 | if _, err := ListenWithNames(); err == nil { 226 | t.Fatal("did not fail when FDs were not set") 227 | } 228 | } 229 | 230 | func TestListenWithNamesMismatch(t *testing.T) { 231 | prepareEnv(t, true, true, true) 232 | defer cleanEnv(r, w) 233 | 234 | if _, err := ListenWithNames(); err == nil { 235 | t.Fatal("no error when no names were set") 236 | } 237 | 238 | prepareNames(0) 239 | if _, err := ListenWithNames(); err == nil { 240 | t.Fatal("no error when no names were set") 241 | } 242 | 243 | prepareNames(1) 244 | if _, err := ListenWithNames(); err == nil { 245 | t.Fatal("no error when too few names were set") 246 | } 247 | 248 | prepareNames(3) 249 | if _, err := ListenWithNames(); err == nil { 250 | t.Fatal("no error when too many names were set") 251 | } 252 | } 253 | 254 | func TestSocket(t *testing.T) { 255 | prepareEnv(t, false, false, true) 256 | defer cleanEnv(r, w) 257 | 258 | s := Socket{w} 259 | 260 | if s.Fd() != w.Fd() { 261 | t.Fatalf("socket FD mismatch: expected %d, got %d", w.Fd(), s.Fd()) 262 | } 263 | 264 | if s.Name() != w.Name() { 265 | t.Fatalf("socket name mismatch: expected %s, got %s", w.Name(), s.Name()) 266 | } 267 | 268 | if err := s.Close(); err != nil { 269 | t.Fatalf("error while closing socket: %v", err) 270 | } 271 | } 272 | 273 | func TestSocketListener(t *testing.T) { 274 | l1, err := net.Listen("tcp", ":55555") 275 | if err != nil { 276 | t.Fatal(err) 277 | } 278 | defer l1.Close() 279 | 280 | f, err := l1.(*net.TCPListener).File() 281 | if err != nil { 282 | t.Fatal(err) 283 | } 284 | 285 | s := newSocket(int(f.Fd()), f.Name()) 286 | 287 | if s.Fd() != f.Fd() { 288 | t.Fatalf("socket FD mismatch: expected %d, got %d", f.Fd(), s.Fd()) 289 | } 290 | 291 | if s.Name() != f.Name() { 292 | t.Fatalf("socket name mismatch: expected %s, got %s", f.Name(), s.Name()) 293 | } 294 | 295 | l2, err := s.Listener() 296 | if err != nil { 297 | t.Fatal(err) 298 | } 299 | 300 | if err = l2.Close(); err != nil { 301 | t.Fatal(err) 302 | } 303 | } 304 | 305 | func TestSocketConn(t *testing.T) { 306 | l1, err := net.Listen("tcp", ":55556") 307 | if err != nil { 308 | t.Fatal(err) 309 | } 310 | defer l1.Close() 311 | 312 | c1, err := net.Dial("tcp", ":55556") 313 | if err != nil { 314 | t.Fatalf(err.Error()) 315 | } 316 | 317 | f, err := c1.(*net.TCPConn).File() 318 | if err != nil { 319 | t.Fatal(err) 320 | } 321 | 322 | s := newSocket(int(f.Fd()), f.Name()) 323 | 324 | if s.Fd() != f.Fd() { 325 | t.Fatalf("socket FD mismatch: expected %d, got %d", f.Fd(), s.Fd()) 326 | } 327 | 328 | if s.Name() != f.Name() { 329 | t.Fatalf("socket name mismatch: expected %s, got %s", f.Name(), s.Name()) 330 | } 331 | 332 | c2, err := s.Conn() 333 | if err != nil { 334 | t.Fatal(err) 335 | } 336 | 337 | if err = c2.Close(); err != nil { 338 | t.Fatal(err) 339 | } 340 | } 341 | 342 | func TestSocketPacketConn(t *testing.T) { 343 | c1, err := net.ListenPacket("udp", ":55557") 344 | if err != nil { 345 | t.Fatal(err) 346 | } 347 | defer c1.Close() 348 | 349 | f, err := c1.(*net.UDPConn).File() 350 | if err != nil { 351 | t.Fatal(err) 352 | } 353 | 354 | s := newSocket(int(f.Fd()), f.Name()) 355 | 356 | if s.Fd() != f.Fd() { 357 | t.Fatalf("socket FD mismatch: expected %d, got %d", f.Fd(), s.Fd()) 358 | } 359 | 360 | if s.Name() != f.Name() { 361 | t.Fatalf("socket name mismatch: expected %s, got %s", f.Name(), s.Name()) 362 | } 363 | 364 | c2, err := s.PacketConn() 365 | if err != nil { 366 | t.Fatal(err) 367 | } 368 | 369 | if err = c2.Close(); err != nil { 370 | t.Fatal(err) 371 | } 372 | } 373 | --------------------------------------------------------------------------------