@@ -11,6 +11,7 @@ SRC_URI += "file://CMakeLists.txt"
inherit cmake-qemu
PACKAGECONFIG[failing_test] = "-DFAILING_TEST=ON"
+PACKAGECONFIG[systemd] = "-DWITH_SYSTEMD=ON,-DWITH_SYSTEMD=OFF"
# Support installing all recipe variants in parallel
EXTRA_OECMAKE += "\
@@ -15,6 +15,8 @@ SRC_URI = "\
file://cpp-example.cpp \
file://cpp-example-lib.hpp \
file://cpp-example-lib.cpp \
+ file://daemonize.cpp \
+ file://daemonize.hpp \
file://test-cpp-example.cpp \
file://cpp-example.conf \
file://config.h.in \
@@ -33,6 +35,8 @@ SYSTEMD_SERVICE:${PN} = "${BPN}.service"
INITSCRIPT_NAME = "${BPN}"
INITSCRIPT_PARAMS = "defaults 99"
+PACKAGECONFIG ??= "${@bb.utils.contains('DISTRO_FEATURES', 'systemd', 'systemd', '', d)}"
+
# Create cpp-example user and group
USERADD_PACKAGES = "${PN}"
GROUPADD_PARAM:${PN} = "--system ${EX_SERVICE_USER}"
@@ -13,6 +13,7 @@ project(cmake-example
option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
option(FAILING_TEST "Compile a failing unit test to test the test infrastructure" OFF)
+option(WITH_SYSTEMD "Target is managed by systemd: skip the legacy SysV daemonize/pidfile/privilege-drop code" OFF)
set(BINARY_NAME "cmake-example" CACHE STRING "Name of the installed executable and library prefix")
set(TEST_BINARY_NAME "test-cmake-example" CACHE STRING "Name of the installed test executable")
@@ -54,6 +55,11 @@ install(TARGETS ${BINARY_NAME}-lib
add_executable(${BINARY_NAME} cpp-example.cpp)
target_include_directories(${BINARY_NAME} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(${BINARY_NAME} PRIVATE ${BINARY_NAME}-lib)
+if (WITH_SYSTEMD)
+ target_compile_definitions(${BINARY_NAME} PRIVATE WITH_SYSTEMD)
+else()
+ target_sources(${BINARY_NAME} PRIVATE daemonize.cpp daemonize.hpp)
+endif(WITH_SYSTEMD)
install(TARGETS ${BINARY_NAME}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
@@ -6,44 +6,117 @@
#include "cpp-example-lib.hpp"
+#include <cstdlib>
#include <iostream>
#include <unistd.h>
#include <string>
#include <vector>
+#ifndef WITH_SYSTEMD
+#include <sys/types.h>
+#include <syslog.h>
+
+#include "daemonize.hpp"
+#endif
+
+namespace {
+
+#ifndef WITH_SYSTEMD
+bool g_use_syslog = false;
+#endif
+
+// Prints an informational message. Under systemd, stdout is already captured
+// by the journal. Otherwise, use stdout until daemonized (stdio is then
+// redirected to /dev/null), and syslog afterwards so messages aren't lost.
+void log_info(const std::string& msg)
+{
+#ifndef WITH_SYSTEMD
+ if (g_use_syslog) {
+ syslog(LOG_INFO, "%s", msg.c_str());
+ return;
+ }
+#endif
+ std::cout << msg << std::endl;
+}
+
+} // namespace
+
int main(int argc, char* argv[])
{
bool endless_mode = false;
+#ifndef WITH_SYSTEMD
+ bool daemonize_mode = false;
+ std::string pidfile_path;
+ bool have_uid = false;
+ bool have_gid = false;
+ uid_t target_uid = 0;
+ gid_t target_gid = 0;
+#endif
// Parse command line arguments
for (int i = 1; i < argc; i++) {
- if (std::string(argv[i]) == "--endless") {
+ std::string arg = argv[i];
+ if (arg == "--endless") {
endless_mode = true;
- } else if (std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") {
+#ifndef WITH_SYSTEMD
+ } else if (arg == "--daemonize") {
+ daemonize_mode = true;
+ } else if (arg == "--pidfile" && i + 1 < argc) {
+ pidfile_path = argv[++i];
+ } else if (arg == "--uid" && i + 1 < argc) {
+ target_uid = static_cast<uid_t>(std::strtoul(argv[++i], nullptr, 10));
+ have_uid = true;
+ } else if (arg == "--gid" && i + 1 < argc) {
+ target_gid = static_cast<gid_t>(std::strtoul(argv[++i], nullptr, 10));
+ have_gid = true;
+#endif
+ } else if (arg == "--help" || arg == "-h") {
std::cout << "Usage: " << argv[0] << " [OPTIONS]" << std::endl;
std::cout << "Options:" << std::endl;
- std::cout << " --endless Run in endless loop mode (for service)" << std::endl;
- std::cout << " --help, -h Show this help message" << std::endl;
+ std::cout << " --endless Run in endless loop mode (for service)" << std::endl;
+#ifndef WITH_SYSTEMD
+ std::cout << " --daemonize Detach from the controlling terminal" << std::endl;
+ std::cout << " --pidfile <path> Write the daemon's PID to <path>" << std::endl;
+ std::cout << " --uid <uid> Drop root privileges to this user ID" << std::endl;
+ std::cout << " --gid <gid> Drop root privileges to this group ID" << std::endl;
+#endif
+ std::cout << " --help, -h Show this help message" << std::endl;
return 0;
}
}
+#ifndef WITH_SYSTEMD
+ if (daemonize_mode) {
+ daemonize();
+ openlog(argv[0], LOG_PID, LOG_DAEMON);
+ g_use_syslog = true;
+ }
+
+ if (!pidfile_path.empty()) {
+ write_pidfile(pidfile_path);
+ }
+
+ // Drop privileges after daemonizing/writing the pidfile (both may need
+ // root, e.g. to create files under /var/run), but before doing any work.
+ drop_privileges(have_gid, target_gid, have_uid, target_uid);
+#endif
+
auto cpp_example = CppExample();
if (endless_mode) {
- std::cout << "Starting cpp-example service in endless mode..." << std::endl;
+ log_info("Starting cpp-example service in endless mode...");
} else {
- std::cout << "Running cpp-example once..." << std::endl;
+ log_info("Running cpp-example once...");
}
- std::cout << "C++ example linking " << cpp_example.get_string() << std::endl;
- std::cout << "Linking json-c version " << cpp_example.get_json_c_version() << std::endl;
+ log_info("C++ example linking " + cpp_example.get_string());
+ log_info(std::string("Linking json-c version ") + cpp_example.get_json_c_version());
cpp_example.print_json();
do {
// Read and print message from config file
std::string config_message = cpp_example.read_config_message();
- std::cout << "Config file message: " << config_message << std::endl;
+ log_info("Config file message: " + config_message);
if (endless_mode) {
// Sleep for 1 second
@@ -25,8 +25,7 @@ start() {
fi
echo -n "Starting $DAEMON: "
- start-stop-daemon --start --quiet --pidfile $PIDFILE --make-pidfile \
- --background --chuid $USER --exec $DAEMON_PATH -- $DAEMON_ARGS
+ $DAEMON_PATH --daemonize --pidfile "$PIDFILE" --uid "$(id -u "$USER")" --gid "$(id -g "$USER")" $DAEMON_ARGS
RETVAL=$?
if [ $RETVAL -eq 0 ]; then
echo "OK"
@@ -39,8 +38,11 @@ start() {
stop() {
echo -n "Stopping $DAEMON: "
- start-stop-daemon --stop --quiet --pidfile $PIDFILE
- RETVAL=$?
+ if [ -f "$PIDFILE" ] && kill "$(cat "$PIDFILE")" 2>/dev/null; then
+ RETVAL=0
+ else
+ RETVAL=1
+ fi
if [ $RETVAL -eq 0 ]; then
echo "OK"
rm -f $PIDFILE $LOCK_FILE
new file mode 100644
@@ -0,0 +1,57 @@
+/*
+ * Copyright OpenEmbedded Contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include "daemonize.hpp"
+
+#include <cstdlib>
+#include <fcntl.h>
+#include <fstream>
+#include <grp.h>
+#include <unistd.h>
+
+void daemonize()
+{
+ pid_t pid = fork();
+ if (pid < 0) {
+ std::perror("fork");
+ std::exit(1);
+ }
+ if (pid > 0) {
+ _exit(0);
+ }
+ setsid();
+
+ // setsid() only drops the controlling-terminal association; stdio still
+ // points at the console, so redirect it or an --endless service keeps
+ // the console open/busy with its output forever.
+ int null_fd = open("/dev/null", O_RDWR);
+ if (null_fd >= 0) {
+ dup2(null_fd, STDIN_FILENO);
+ dup2(null_fd, STDOUT_FILENO);
+ dup2(null_fd, STDERR_FILENO);
+ if (null_fd > STDERR_FILENO) {
+ close(null_fd);
+ }
+ }
+}
+
+void write_pidfile(const std::string& path)
+{
+ std::ofstream pidfile(path, std::ios::trunc);
+ pidfile << getpid() << std::endl;
+}
+
+void drop_privileges(bool have_gid, gid_t gid, bool have_uid, uid_t uid)
+{
+ if (have_gid && (setgroups(0, nullptr) != 0 || setgid(gid) != 0)) {
+ std::perror("setgid");
+ std::exit(1);
+ }
+ if (have_uid && setuid(uid) != 0) {
+ std::perror("setuid");
+ std::exit(1);
+ }
+}
new file mode 100644
@@ -0,0 +1,27 @@
+/*
+ * Copyright OpenEmbedded Contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#pragma once
+
+// Legacy SysV-init daemonizing helpers. Not needed (and not built) when
+// WITH_SYSTEMD is set, since systemd already daemonizes, drops privileges,
+// and tracks the pid itself.
+
+#include <string>
+#include <sys/types.h>
+
+// Detaches from the controlling terminal. Uses a single fork (not the classic
+// double-fork) so this process becomes its own session AND process-group
+// leader (pgid == pid); a second fork would hand it off to a child with a
+// stale pgid, which breaks gdbserver's SIGINT-to-process-group interrupt.
+void daemonize();
+
+// Writes the current process's pid to the given path.
+void write_pidfile(const std::string& path);
+
+// Permanently drops from root to the given group/user. Must run gid before
+// uid: once uid is dropped, the process no longer has permission to setgid.
+void drop_privileges(bool have_gid, gid_t gid, bool have_uid, uid_t uid);
@@ -30,6 +30,13 @@ configure_file(input : 'config.h.in',
# Include the build directory for config.h
inc_dir = include_directories('.')
+example_sources = ['cpp-example.cpp']
+if get_option('WITH_SYSTEMD').enabled()
+ add_project_arguments('-DWITH_SYSTEMD', language: 'cpp')
+else
+ example_sources += ['daemonize.cpp', 'daemonize.hpp']
+endif
+
exlib = shared_library(binary_name + 'lib',
'cpp-example-lib.cpp', 'cpp-example-lib.hpp',
version: meson.project_version(),
@@ -40,7 +47,7 @@ exlib = shared_library(binary_name + 'lib',
)
executable(binary_name,
- 'cpp-example.cpp',
+ example_sources,
link_with : exlib,
include_directories : inc_dir,
install : true
@@ -1,6 +1,8 @@
option('FAILING_TEST', type : 'feature', value : 'disabled',
description : 'Compile a failing unit test to test the test infrastructure')
+option('WITH_SYSTEMD', type : 'feature', value : 'disabled',
+ description : 'Target is managed by systemd: skip the legacy SysV daemonize/pidfile/privilege-drop code')
option('BINARY_NAME', type : 'string', value : 'mesonex',
description : 'Name of the installed executable')
option('TEST_BINARY_NAME', type : 'string', value : 'test-mesonex',
@@ -16,6 +16,7 @@ SRC_URI += "\
inherit pkgconfig meson
PACKAGECONFIG[failing_test] = "-DFAILING_TEST=enabled"
+PACKAGECONFIG[systemd] = "-DWITH_SYSTEMD=enabled,-DWITH_SYSTEMD=disabled"
# Support installing all recipes variants in parallel
EXTRA_OEMESON += "\
@@ -3123,7 +3123,7 @@ class DevtoolIdeSdkTests(DevtoolBase):
# the first _gdb_cross_debugging_multi call above.
self._gdb_cross_debugging_multi(
qemu, recipe_name, example_exe, MAGIC_STRING_NEW,
- exe_break_line=63 + LINE_SHIFT, exe_list_line=55 + LINE_SHIFT,
+ exe_break_line=136 + LINE_SHIFT, exe_list_line=128 + LINE_SHIFT,
hpp_break_line=21 + LINE_SHIFT, lib_break_line=31 + LINE_SHIFT)
def _verify_cmake_preset(self, tempdir):
@@ -3239,7 +3239,7 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests):
self.assertIn("GNU gdb", r.output)
def _gdb_debug_cpp_example(self, magic_string, gdb_start_cmd="run",
- exe_break_line=63, exe_list_line=55, hpp_break_line=21,
+ exe_break_line=136, exe_list_line=128, hpp_break_line=21,
lib_break_line=31):
"""Get a series of gdb commands to debug the cpp-example-lib example"""
gdb_batch_cmd = " -ex 'break main' -ex '%s'" % gdb_start_cmd
@@ -3285,7 +3285,7 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests):
gdb_batch_cmd += " -ex 'continue'"
return gdb_batch_cmd
- def _gdb_debug_cpp_example_check(self, gdb_output, magic_string, exe_list_line=55, lib_break_line=31):
+ def _gdb_debug_cpp_example_check(self, gdb_output, magic_string, exe_list_line=128, lib_break_line=31):
self.assertIn("Breakpoint 1, main", gdb_output)
self.assertIn("$1 = 0", gdb_output) # test.string.compare equal
self.assertIn("$2 = -3", gdb_output) # test.string.compare longer
@@ -3313,7 +3313,7 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests):
self.assertIn("exited normally", gdb_output)
def _gdb_cross_debugging_multi(self, qemu, recipe_name, example_exe, magic_string,
- exe_break_line=63, exe_list_line=55, hpp_break_line=21,
+ exe_break_line=136, exe_list_line=128, hpp_break_line=21,
lib_break_line=31):
"""Verify gdb-cross is working