From 3b1bdfbd2687e81bef85260f9cdfbf617ece3527 Mon Sep 17 00:00:00 2001 From: Luke Shumaker Date: Sun, 18 Dec 2016 03:20:47 -0500 Subject: Implement almost all of sd-daemon. BREAKING CHANGES. This does not include the sd_is_* utility functions. BREAKING CHANGES: - The import name is now "sd_daemon" instead of "sd". - The logger interface is now entirely different. - Notify now takes more arguments. --- sd_daemon/.gitignore | 1 + sd_daemon/Makefile | 25 +++++++++++++ sd_daemon/booted.go | 28 ++++++++++++++ sd_daemon/doc.go | 26 +++++++++++-- sd_daemon/listen_fds.go | 10 ++--- sd_daemon/log.go | 87 ++++++++++++++++++++++++++++++++++++++++++++ sd_daemon/log_util.go.gen | 37 +++++++++++++++++++ sd_daemon/logger/logger.go | 62 ------------------------------- sd_daemon/lsb/exit-status.go | 7 ++-- sd_daemon/notify.go | 84 ++++++++++++++++++++++++++++++++++-------- sd_daemon/watchdog.go | 68 ++++++++++++++++++++++++++++++++++ 11 files changed, 345 insertions(+), 90 deletions(-) create mode 100644 sd_daemon/.gitignore create mode 100644 sd_daemon/Makefile create mode 100644 sd_daemon/booted.go create mode 100644 sd_daemon/log.go create mode 100755 sd_daemon/log_util.go.gen delete mode 100644 sd_daemon/logger/logger.go create mode 100644 sd_daemon/watchdog.go diff --git a/sd_daemon/.gitignore b/sd_daemon/.gitignore new file mode 100644 index 0000000..1687796 --- /dev/null +++ b/sd_daemon/.gitignore @@ -0,0 +1 @@ +/log_util.go diff --git a/sd_daemon/Makefile b/sd_daemon/Makefile new file mode 100644 index 0000000..ec66e67 --- /dev/null +++ b/sd_daemon/Makefile @@ -0,0 +1,25 @@ +# Copyright (C) 2016 Luke Shumaker +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +files.src.gen = log_util.go + +files.generate: $(files.src.gen) +maintainer-clean: + rm -f -- $(files.src.gen) +.PHONY: files.generate maintainer-clean + +%.go: %.go.gen + ./$^ > $@ + +.DELETE_ON_ERROR: diff --git a/sd_daemon/booted.go b/sd_daemon/booted.go new file mode 100644 index 0000000..bfbc685 --- /dev/null +++ b/sd_daemon/booted.go @@ -0,0 +1,28 @@ +// Copyright 2015 CoreOS, Inc. +// Copyright 2016 Luke Shumaker +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sd_daemon + +import "os" + +// Returns whether the operating system booted using the Systemd init +// system. +// +// Please do not use this function. All of the other functionality in +// this package uses interfaces that are not Systemd-specific. +func SdBooted() bool { + fi, err := os.Lstat("/run/systemd/system") + return err != nil && fi.IsDir() +} diff --git a/sd_daemon/doc.go b/sd_daemon/doc.go index 665e25e..8f1fb00 100644 --- a/sd_daemon/doc.go +++ b/sd_daemon/doc.go @@ -1,4 +1,4 @@ -// Copyright 2015 Luke Shumaker +// Copyright 2015-2016 Luke Shumaker // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,5 +12,25 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package sd provides APIs for systemd new-style daemons. -package sd +// Package sd_daemon provides functions for writing "new-style" +// daemons. +// +// The daemon(7) manual page has historically documented the very long +// list of things that a daemon must do at start-up to be a +// well-behaved SysV daemon. Modern service managers allow daemons to +// be much simpler; modern versions of the daemon(7) page on GNU/Linux +// systems also describe "new-style" daemons. Though many of the +// mechanisms described there and implemented here originated with +// Systemd, they are all very simple mechanisms which can easily be +// implemented with a variety of service managers. +// +// [daemon(7)]: https://www.freedesktop.org/software/systemd/man/daemon.html +package sd_daemon + +import "errors" + +// ErrDisabled is the error returned when the service manager does not +// want/support a mechanism; or when that mechanism has been disabled +// for this process by setting unsetEnv=true when calling one of these +// functions. +var ErrDisabled = errors.New("Mechanism Disabled") diff --git a/sd_daemon/listen_fds.go b/sd_daemon/listen_fds.go index 434f7cc..f512384 100644 --- a/sd_daemon/listen_fds.go +++ b/sd_daemon/listen_fds.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package sd +package sd_daemon import ( "os" @@ -22,14 +22,10 @@ import ( "syscall" ) -///*#include */ -//#define SD_LISTEN_FDS_START 3 -import "C" - // ListenFds returns a list of file descriptors passed in by the // service manager as part of the socket-based activation logic. // -// If unsetEnv is true, then (regarless of whether the function call +// If unsetEnv is true, then (regardless of whether the function call // itself succeeds or not) it will unset the environmental variables // LISTEN_FDS and LISTEN_PID, which will cause further calls to this // function to fail. @@ -58,7 +54,7 @@ func ListenFds(unsetEnv bool) []*os.File { files := make([]*os.File, 0, nfds) for i := 0; i < nfds; i++ { - fd := i+C.SD_LISTEN_FDS_START + fd := i+3 syscall.CloseOnExec(fd) name := "unknown" if i < len(names) { diff --git a/sd_daemon/log.go b/sd_daemon/log.go new file mode 100644 index 0000000..76a735f --- /dev/null +++ b/sd_daemon/log.go @@ -0,0 +1,87 @@ +// Copyright 2015-2016 Luke Shumaker +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:generate make + +package sd_daemon + +import ( + "io" + "os" + "strings" + "sync" + "fmt" + "log/syslog" +) + +// Logger writes ""-prefixed lines to an io.Writer, where N is a +// syslog priority number. It implements mostly the same interface as +// "log/syslog".Writer. +// +// You probably don't need any instance of this other than the +// constant "Log", which uses os.Stderr as the writer. It is +// implemented as a struct rather than a set of functions so that it +// can be passed around as an implementation of an interface. +type Logger struct{ + mu sync.Mutex + out io.Writer +} + +func NewLogger(w io.Writer) Logger { + return Logger{out: w} +} + +// Log is a Logger that use used very similarly to +// "log/syslog".Writer, but writes to os.Stderr under the assumption +// that stderr is forwarded to syslog/journald. +// +// You are encouraged to use stderr unless you have a good reason to +// talk to syslog or journald directly. +var Log = Logger{out: os.Stderr} + +// WriteString writes a message with the specified priority to the +// log. +func (l Logger) WriteBytes(level syslog.Priority, msg []byte) (n int, err error) { + return l.WriteString(level, string(msg)) +} + +// WriteString writes a message with the specified priority to the +// log. +func (l Logger) WriteString(level syslog.Priority, msg string) (n int, err error) { + l.mu.Lock() + defer l.mu.Unlock() + + // BUG(lukeshu): Logger uses high-level string functions that + // do many small allocations, making it insuitable for in + // tight loops; it should use a buffer property to be + // essentially zero-allocation. + prefix := fmt.Sprintf("<%d>", level) + buf := prefix + strings.Replace(strings.TrimSuffix(msg, "\n"), "\n", "\n"+prefix, -1) + return io.WriteString(l.out, buf) +} + +type loggerWriter struct { + log Logger + level syslog.Priority +} + +func (lw loggerWriter) Write(p []byte) (n int, err error) { + return lw.log.WriteBytes(lw.level, p) +} + +// Writer returns an io.Writer that writes messages with the specified +// priority to the log. +func (l Logger) Writer(level syslog.Priority) io.Writer { + return loggerWriter{log: l, level: level} +} diff --git a/sd_daemon/log_util.go.gen b/sd_daemon/log_util.go.gen new file mode 100755 index 0000000..f7f43c0 --- /dev/null +++ b/sd_daemon/log_util.go.gen @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Copyright (C) 2016 Luke Shumaker +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{ + printf '//' + printf ' %q' "$0" "$@" + printf '\n// MACHINE GENERATED BY THE COMMAND ABOVE; DO NOT EDIT\n\n' + cat <*/ -//#define SD_EMERG "<0>" -//#define SD_ALERT "<1>" -//#define SD_CRIT "<2>" -//#define SD_ERR "<3>" -//#define SD_WARNING "<4>" -//#define SD_NOTICE "<5>" -//#define SD_INFO "<6>" -//#define SD_DEBUG "<7>" -import "C" - -func log(level string, format string, a ...interface{}) { - f := level + format + "\n" - fmt.Fprintf(os.Stderr, f, a...) -} - -// system is unusable -func Emerg( /* */ format string, a ...interface{}) { log(C.SD_EMERG /* */, format, a...) } - -// action must be taken immediately -func Alert( /* */ format string, a ...interface{}) { log(C.SD_ALERT /* */, format, a...) } - -// critical conditions -func Crit( /* */ format string, a ...interface{}) { log(C.SD_CRIT /* */, format, a...) } - -// error conditions -func Err( /* */ format string, a ...interface{}) { log(C.SD_ERR /* */, format, a...) } - -// warning conditions -func Warning( /**/ format string, a ...interface{}) { log(C.SD_WARNING /**/, format, a...) } - -// normal but significant condition -func Notice( /* */ format string, a ...interface{}) { log(C.SD_NOTICE /* */, format, a...) } - -// informational -func Info( /* */ format string, a ...interface{}) { log(C.SD_INFO /* */, format, a...) } - -// debug-level messages -func Debug( /* */ format string, a ...interface{}) { log(C.SD_DEBUG /* */, format, a...) } diff --git a/sd_daemon/lsb/exit-status.go b/sd_daemon/lsb/exit-status.go index 14e2128..fbbb5e3 100644 --- a/sd_daemon/lsb/exit-status.go +++ b/sd_daemon/lsb/exit-status.go @@ -1,4 +1,4 @@ -// Copyright 2015 Luke Shumaker +// Copyright 2015-2016 Luke Shumaker // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,9 +17,10 @@ package lsb import ( + "fmt" "os" - "lukeshu.com/git/go/libsystemd.git/sd_daemon/logger" + "lukeshu.com/git/go/libsystemd.git/sd_daemon" ) // systemd daemon(7) recommends using the exit codes defined in the @@ -87,7 +88,7 @@ const ( // panic. func Recover() { if r := recover(); r != nil { - logger.Err("panic: %v", r) + sd_daemon.Log.Err(fmt.Sprintf("panic: %v", r)) os.Exit(int(EXIT_FAILURE)) } } diff --git a/sd_daemon/notify.go b/sd_daemon/notify.go index b159a22..ad63659 100644 --- a/sd_daemon/notify.go +++ b/sd_daemon/notify.go @@ -1,6 +1,6 @@ // Copyright 2013-2015 Docker, Inc. // Copyright 2014 CoreOS, Inc. -// Copyright 2015 Luke Shumaker +// Copyright 2015-2016 Luke Shumaker // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,31 +14,47 @@ // See the License for the specific language governing permissions and // limitations under the License. -package sd +package sd_daemon import ( - "errors" "net" "os" + "syscall" + "bytes" ) -// errNotifyNoSocket is an error returned if no socket was specified. -var errNotifyNoSocket = errors.New("No socket") - -// Notify sends a message to the service manager aobout state -// changes. It is common to ignore the error. +// Notify sends a message from process pid to the service manager +// about state changes. If pid <= 0, or if the current process does +// not have priveleges to send messages on behalf of other processes, +// then the message is simply sent from the current process. // -// If unsetEnv is true, then (regarless of whether the function call +// If unsetEnv is true, then (regardless of whether the function call // itself succeeds or not) it will unset the environmental variable // NOTIFY_SOCKET, which will cause further calls to this function to // fail. // // The state parameter should countain a newline-separated list of -// variable assignments. +// variable assignments. See the documentation for sd_notify(3) for +// well-known variable assignments. +// https://www.freedesktop.org/software/systemd/man/sd_notify.html +// +// It is possible to include a set of file descriptors with the +// message. This is useful for keeping files open across restarts, as +// it enables the service manager will pass those files to the new +// process when it is restarted (see ListenFds). Note: The service +// manager will only actually store the file descriptors if you +// include "FDSTORE=1" in the state. // -// See the documentation for sd_notify(3) for well-known variable -// assignments. -func Notify(unsetEnv bool, state string) error { +// If the service manager is not listening for notifications from this +// process (or this has already been called with unsetEnv=true), then +// ErrDisabled is returned. If the service manager appears to be +// listening, but there is an error sending the message, then that +// error is returned. It is generally recommended that you ignore the +// return value: if there is an error, this is function no-op; meaning +// that by calling the function but ignoring the return value, you can +// easily support both service managers that support these +// notifications and those that do not. +func Notify(pid int, unsetEnv bool, state string, files []*os.File) error { if unsetEnv { defer func() { _ = os.Unsetenv("NOTIFY_SOCKET") }() } @@ -49,7 +65,7 @@ func Notify(unsetEnv bool, state string) error { } if socketAddr.Name == "" { - return errNotifyNoSocket + return ErrDisabled } conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr) @@ -58,6 +74,44 @@ func Notify(unsetEnv bool, state string) error { } defer func() { _ = conn.Close() }() - _, err = conn.Write([]byte(state)) + var cmsgs [][]byte + + if len(files) > 0 { + fds := make([]int, len(files)) + for i := range files { + fds[i] = int(files[i].Fd()) + } + cmsg := syscall.UnixRights(fds...) + cmsgs = append(cmsgs, cmsg) + } + + havePid := pid > 0 && pid != os.Getpid() + if havePid { + // The types of members of syscall.Ucred aren't + // guaranteed across processors. However, + // fortunately, they are the same on all supported + // processors as of go 1.7.4. + cmsg := syscall.UnixCredentials(&syscall.Ucred{ + Pid: int32(pid), + Uid: uint32(os.Getuid()), + Gid: uint32(os.Getgid()), + }) + cmsgs = append(cmsgs, cmsg) + } + + // If the 2nd argument is empty, this is equivalent to .Write([]byte(state)) + _, _, err = conn.WriteMsgUnix([]byte(state), bytes.Join(cmsgs, nil), nil) + + if err != nil && havePid { + // Maybe it failed because we don't have privileges to + // spoof our pid, retry without spoofing the pid. + // + // I'm not too happy that does this silently without + // notifying the user, but that's what + // sd_pid_notify_with_fds does. + cmsgs = cmsgs[:len(cmsgs)-1] + _, _, err = conn.WriteMsgUnix([]byte(state), bytes.Join(cmsgs, nil), nil) + } + return err } diff --git a/sd_daemon/watchdog.go b/sd_daemon/watchdog.go new file mode 100644 index 0000000..b32f470 --- /dev/null +++ b/sd_daemon/watchdog.go @@ -0,0 +1,68 @@ +// Copyright 2016 CoreOS, Inc. +// Copyright 2016 Luke Shumaker +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sd_daemon + +import ( + "os" + "strconv" + "time" +) + +// WatchdogEnabled returns how often the process is expected to send a +// keep-alive notification to the service manager. +// +// Notify(0, false, "WATCHDOG=1", nil) // send keep-alive notification +// +// If unsetEnv is true, then (regardless of whether the function call +// itself succeeds or not) it will unset the environmental variables +// WATCHDOG_USEC and WATCHDOG_PID, which will cause further calls to +// this function to fail. +// +// If an error is returned, then the duration is 0. If the service +// manager is not expecting a keep-alive notification from this +// process (or if this has already been called with unsetEnv=true), +// then ErrDisabled is returned. If there is an error parsing the +// service manager's watchdog request, then an appropriate other error +// is returned. +func WatchdogEnabled(unsetEnv bool) (time.Duration, error) { + if unsetEnv { + defer func() { + _ = os.Unsetenv("WATCHDOG_USEC") + _ = os.Unsetenv("WATCHDOG_PID") + }() + } + + usecStr, haveUsec := os.LookupEnv("WATCHDOG_USEC") + if !haveUsec { + return 0, ErrDisabled + } + usec, err := strconv.ParseInt(usecStr, 10, 64) + if err != nil || usec < 0 { + return 0, err + } + + if pidStr, havePid := os.LookupEnv("WATCHDOG_PID"); havePid { + pid, err := strconv.Atoi(pidStr) + if err != nil { + return 0, err + } + if pid != os.Getpid() { + return 0, ErrDisabled + } + } + + return time.Duration(usec) * time.Microsecond, nil +} -- cgit v1.2.3