diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e256dde1..3bc4c81f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.11.4) project( linglong - VERSION 1.9.0 + VERSION 1.10.0 DESCRIPTION "a container based application package manager for Linux desktop" HOMEPAGE_URL "https://github.com/OpenAtom-Linyaps/linyaps" LANGUAGES CXX C) @@ -288,7 +288,7 @@ pfl_add_libraries( linglong oci-cfg-generators APPS - dumb-init + ll-init ll-dialog ll-builder ll-cli diff --git a/REUSE.toml b/REUSE.toml index e563847a1..218de29e2 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -142,7 +142,7 @@ SPDX-FileCopyrightText = "None" SPDX-License-Identifier = "CC0-1.0" [[annotations]] -path = "misc/share/linglong/**.json" +path = "misc/share/linglong/**.json" precedence = "aggregate" SPDX-FileCopyrightText = "None" SPDX-License-Identifier = "CC0-1.0" @@ -195,12 +195,6 @@ precedence = "aggregate" SPDX-FileCopyrightText = "AppImageCommunity" SPDX-License-Identifier = "MIT" -[[annotations]] -path = "apps/dumb-init/src/*" -precedence = "aggregate" -SPDX-FileCopyrightText = "Yelp Inc." -SPDX-License-Identifier = "MIT" - [[annotations]] path = "apps/ll-builder-utils/patch/libfuse.patch" precedence = "aggregate" diff --git a/apps/dumb-init/CMakeLists.txt b/apps/dumb-init/CMakeLists.txt deleted file mode 100644 index 1edc4980b..000000000 --- a/apps/dumb-init/CMakeLists.txt +++ /dev/null @@ -1,18 +0,0 @@ -# SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. -# -# SPDX-License-Identifier: LGPL-3.0-or-later - -pfl_add_executable( - SOURCES - ./src/dumb-init.c - OUTPUT_NAME - dumb-init - LIBEXEC - linglong -) - -set(DUMB_INIT_TARGET) - -get_real_target_name(DUMB_INIT_TARGET linglong::dumb-init) - -target_link_options(${DUMB_INIT_TARGET} PRIVATE -static) diff --git a/apps/dumb-init/src/VERSION.h b/apps/dumb-init/src/VERSION.h deleted file mode 100644 index 2a4fbf0d9..000000000 --- a/apps/dumb-init/src/VERSION.h +++ /dev/null @@ -1,2 +0,0 @@ -unsigned char VERSION[] = { 0x31, 0x2e, 0x32, 0x2e, 0x35, 0x0a }; -unsigned int VERSION_len = 6; diff --git a/apps/dumb-init/src/dumb-init.c b/apps/dumb-init/src/dumb-init.c deleted file mode 100644 index 288159903..000000000 --- a/apps/dumb-init/src/dumb-init.c +++ /dev/null @@ -1,335 +0,0 @@ -/* - * dumb-init is a simple wrapper program designed to run as PID 1 and pass - * signals to its children. - * - * Usage: - * ./dumb-init python -c 'while True: pass' - * - * To get debug output on stderr, run with '-v'. - */ - -#include "VERSION.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define PRINTERR(...) \ - do { \ - fprintf(stderr, "[dumb-init] " __VA_ARGS__); \ - } while (0) - -#define DEBUG(...) \ - do { \ - if (debug) { \ - PRINTERR(__VA_ARGS__); \ - } \ - } while (0) - -// Signals we care about are numbered from 1 to 31, inclusive. -// (32 and above are real-time signals.) -// TODO: this is likely not portable outside of Linux, or on strange architectures -#define MAXSIG 31 - -// Indices are one-indexed (signal 1 is at index 1). Index zero is unused. -// User-specified signal rewriting. -int signal_rewrite[MAXSIG + 1] = { [0 ... MAXSIG] = -1 }; -// One-time ignores due to TTY quirks. 0 = no skip, 1 = skip the next-received signal. -char signal_temporary_ignores[MAXSIG + 1] = { [0 ... MAXSIG] = 0 }; - -pid_t child_pid = -1; -char debug = 0; -char use_setsid = 1; - -int translate_signal(int signum) -{ - if (signum <= 0 || signum > MAXSIG) { - return signum; - } else { - int translated = signal_rewrite[signum]; - if (translated == -1) { - return signum; - } else { - DEBUG("Translating signal %d to %d.\n", signum, translated); - return translated; - } - } -} - -void forward_signal(int signum) -{ - signum = translate_signal(signum); - if (signum != 0) { - kill(use_setsid ? -child_pid : child_pid, signum); - DEBUG("Forwarded signal %d to children.\n", signum); - } else { - DEBUG("Not forwarding signal %d to children (ignored).\n", signum); - } -} - -/* - * The dumb-init signal handler. - * - * The main job of this signal handler is to forward signals along to our child - * process(es). In setsid mode, this means signaling the entire process group - * rooted at our child. In non-setsid mode, this is just signaling the primary - * child. - * - * In most cases, simply proxying the received signal is sufficient. If we - * receive a job control signal, however, we should not only forward it, but - * also sleep dumb-init itself. - * - * This allows users to run foreground processes using dumb-init and to - * control them using normal shell job control features (e.g. Ctrl-Z to - * generate a SIGTSTP and suspend the process). - * - * The libc manual is useful: - * https://www.gnu.org/software/libc/manual/html_node/Job-Control-Signals.html - * - */ -void handle_signal(int signum) -{ - DEBUG("Received signal %d.\n", signum); - - if (signal_temporary_ignores[signum] == 1) { - DEBUG("Ignoring tty hand-off signal %d.\n", signum); - signal_temporary_ignores[signum] = 0; - } else if (signum == SIGCHLD) { - int status, exit_status; - pid_t killed_pid; - while ((killed_pid = waitpid(-1, &status, WNOHANG)) > 0) { - if (WIFEXITED(status)) { - exit_status = WEXITSTATUS(status); - DEBUG("A child with PID %d exited with exit status %d.\n", killed_pid, exit_status); - } else { - assert(WIFSIGNALED(status)); - exit_status = 128 + WTERMSIG(status); - DEBUG("A child with PID %d was terminated by signal %d.\n", - killed_pid, - exit_status - 128); - } - - if (killed_pid == child_pid) { - forward_signal(SIGTERM); // send SIGTERM to any remaining children - DEBUG("Child exited with status %d. Goodbye.\n", exit_status); - exit(exit_status); - } - } - } else { - forward_signal(signum); - if (signum == SIGTSTP || signum == SIGTTOU || signum == SIGTTIN) { - DEBUG("Suspending self due to TTY signal.\n"); - kill(getpid(), SIGSTOP); - } - } -} - -void print_help(char *argv[]) -{ - fprintf(stderr, - "dumb-init v%.*s" - "Usage: %s [option] command [[arg] ...]\n" - "\n" - "dumb-init is a simple process supervisor that forwards signals to children.\n" - "It is designed to run as PID1 in minimal container environments.\n" - "\n" - "Optional arguments:\n" - " -c, --single-child Run in single-child mode.\n" - " In this mode, signals are only proxied to the\n" - " direct child and not any of its descendants.\n" - " -r, --rewrite s:r Rewrite received signal s to new signal r before proxying.\n" - " To ignore (not proxy) a signal, rewrite it to 0.\n" - " This option can be specified multiple times.\n" - " -v, --verbose Print debugging information to stderr.\n" - " -h, --help Print this help message and exit.\n" - " -V, --version Print the current version and exit.\n" - "\n" - "Full help is available online at https://github.com/Yelp/dumb-init\n", - VERSION_len, - VERSION, - argv[0]); -} - -void print_rewrite_signum_help() -{ - fprintf(stderr, - "Usage: -r option takes :, where " - "is between 1 and %d.\n" - "This option can be specified multiple times.\n" - "Use --help for full usage.\n", - MAXSIG); - exit(1); -} - -void parse_rewrite_signum(char *arg) -{ - int signum, replacement; - if (sscanf(arg, "%d:%d", &signum, &replacement) == 2 && (signum >= 1 && signum <= MAXSIG) - && (replacement >= 0 && replacement <= MAXSIG)) { - signal_rewrite[signum] = replacement; - } else { - print_rewrite_signum_help(); - } -} - -void set_rewrite_to_sigstop_if_not_defined(int signum) -{ - if (signal_rewrite[signum] == -1) { - signal_rewrite[signum] = SIGSTOP; - } -} - -char **parse_command(int argc, char *argv[]) -{ - int opt; - struct option long_options[] = { - { "help", no_argument, NULL, 'h' }, { "single-child", no_argument, NULL, 'c' }, - { "rewrite", required_argument, NULL, 'r' }, { "verbose", no_argument, NULL, 'v' }, - { "version", no_argument, NULL, 'V' }, { NULL, 0, NULL, 0 }, - }; - while ((opt = getopt_long(argc, argv, "+hvVcr:", long_options, NULL)) != -1) { - switch (opt) { - case 'h': - print_help(argv); - exit(0); - case 'v': - debug = 1; - break; - case 'V': - fprintf(stderr, "dumb-init v%.*s", VERSION_len, VERSION); - exit(0); - case 'c': - use_setsid = 0; - break; - case 'r': - parse_rewrite_signum(optarg); - break; - default: - exit(1); - } - } - - if (optind >= argc) { - fprintf(stderr, - "Usage: %s [option] program [args]\n" - "Try %s --help for full usage.\n", - argv[0], - argv[0]); - exit(1); - } - - char *debug_env = getenv("DUMB_INIT_DEBUG"); - if (debug_env && strcmp(debug_env, "1") == 0) { - debug = 1; - DEBUG("Running in debug mode.\n"); - } - - char *setsid_env = getenv("DUMB_INIT_SETSID"); - if (setsid_env && strcmp(setsid_env, "0") == 0) { - use_setsid = 0; - DEBUG("Not running in setsid mode.\n"); - } - - if (use_setsid) { - set_rewrite_to_sigstop_if_not_defined(SIGTSTP); - set_rewrite_to_sigstop_if_not_defined(SIGTTOU); - set_rewrite_to_sigstop_if_not_defined(SIGTTIN); - } - - return &argv[optind]; -} - -// A dummy signal handler used for signals we care about. -// On the FreeBSD kernel, ignored signals cannot be waited on by `sigwait` (but -// they can be on Linux). We must provide a dummy handler. -// https://lists.freebsd.org/pipermail/freebsd-ports/2009-October/057340.html -void dummy(int signum) { } - -int main(int argc, char *argv[]) -{ - char **cmd = parse_command(argc, argv); - sigset_t all_signals; - sigfillset(&all_signals); - sigprocmask(SIG_BLOCK, &all_signals, NULL); - - int i = 0; - for (i = 1; i <= MAXSIG; i++) { - signal(i, dummy); - } - - /* - * Detach dumb-init from controlling tty, so that the child's session can - * attach to it instead. - * - * We want the child to be able to be the session leader of the TTY so that - * it can do normal job control. - */ - if (use_setsid) { - if (ioctl(STDIN_FILENO, TIOCNOTTY) == -1) { - DEBUG("Unable to detach from controlling tty (errno=%d %s).\n", errno, strerror(errno)); - } else { - /* - * When the session leader detaches from its controlling tty via - * TIOCNOTTY, the kernel sends SIGHUP and SIGCONT to the process - * group. We need to be careful not to forward these on to the - * dumb-init child so that it doesn't receive a SIGHUP and - * terminate itself (#136). - */ - if (getsid(0) == getpid()) { - DEBUG("Detached from controlling tty, ignoring the first SIGHUP and SIGCONT we " - "receive.\n"); - signal_temporary_ignores[SIGHUP] = 1; - signal_temporary_ignores[SIGCONT] = 1; - } else { - DEBUG("Detached from controlling tty, but was not session leader.\n"); - } - } - } - - child_pid = fork(); - if (child_pid < 0) { - PRINTERR("Unable to fork. Exiting.\n"); - return 1; - } else if (child_pid == 0) { - /* child */ - sigprocmask(SIG_UNBLOCK, &all_signals, NULL); - if (use_setsid) { - if (setsid() == -1) { - PRINTERR("Unable to setsid (errno=%d %s). Exiting.\n", errno, strerror(errno)); - exit(1); - } - - if (ioctl(STDIN_FILENO, TIOCSCTTY, 0) == -1) { - DEBUG("Unable to attach to controlling tty (errno=%d %s).\n", - errno, - strerror(errno)); - } - DEBUG("setsid complete.\n"); - } - execvp(cmd[0], &cmd[0]); - - // if this point is reached, exec failed, so we should exit nonzero - PRINTERR("%s: %s\n", cmd[0], strerror(errno)); - return 2; - } else { - /* parent */ - DEBUG("Child spawned with PID %d.\n", child_pid); - if (chdir("/") == -1) { - DEBUG("Unable to chdir(\"/\") (errno=%d %s)\n", errno, strerror(errno)); - } - for (;;) { - int signum; - sigwait(&all_signals, &signum); - handle_signal(signum); - } - } -} diff --git a/apps/ll-init/CMakeLists.txt b/apps/ll-init/CMakeLists.txt new file mode 100644 index 000000000..737070fcb --- /dev/null +++ b/apps/ll-init/CMakeLists.txt @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +pfl_add_executable(SOURCES ./src/ll-init.cpp OUTPUT_NAME ll-init LIBEXEC + linglong) + +set(LL_INIT_TARGET) + +get_real_target_name(LL_INIT_TARGET linglong::ll-init) + +target_link_options(${LL_INIT_TARGET} PRIVATE -static -static-libgcc + -static-libstdc++) diff --git a/apps/ll-init/src/ll-init.cpp b/apps/ll-init/src/ll-init.cpp new file mode 100644 index 000000000..5b94a14c2 --- /dev/null +++ b/apps/ll-init/src/ll-init.cpp @@ -0,0 +1,223 @@ +// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +// no need to block these signals +constexpr std::array unblock_signals{ SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGSYS, + SIGTRAP, SIGXCPU, SIGXFSZ, SIGTTIN, SIGTTOU }; + +struct sig_conf +{ + sigset_t cur_set{}; + sigset_t old_set{}; + struct sigaction ttin_action{}; + struct sigaction ttou_action{}; +}; + +namespace { + +void print_sys_error(std::string_view msg) noexcept +{ + std::cerr << msg << ": " << ::strerror(errno) << std::endl; +} + +void print_info(std::string_view msg) noexcept +{ + std::cout << msg << std::endl; +} + +using sig_result = std::variant; + +sig_result handle_signals() noexcept +{ + sig_conf conf; + ::sigfillset(&conf.cur_set); + for (auto signal : unblock_signals) { + ::sigdelset(&conf.cur_set, signal); + } + + auto ret = ::sigprocmask(SIG_SETMASK, &conf.cur_set, &conf.old_set); + if (ret == -1) { + print_sys_error("Failed to set signal mask"); + return -1; + } + + // background process will be suspended by SIGTTIN and SIGTTOU if it try to read/write from/to + // terminal, so we need to ignore these signals. + struct sigaction ignore{}; + ignore.sa_handler = SIG_IGN; + ignore.sa_flags = 0; + + if (::sigaction(SIGTTIN, &ignore, &conf.ttin_action) == -1) { + print_sys_error("Failed to ignore SIGTTIN"); + return -1; + } + + if (::sigaction(SIGTTOU, &ignore, &conf.ttou_action) == -1) { + print_sys_error("Failed to ignore SIGTTOU"); + return -1; + } + + return conf; +} + +std::vector parse_args(int argc, char *argv[]) noexcept +{ + std::vector args; + int idx{ 1 }; + while (idx < argc) { + args.emplace_back(argv[idx++]); + } + + return args; +} + +pid_t run(std::vector args, const sig_conf &conf) noexcept +{ + auto pid = ::fork(); + if (pid == -1) { + print_sys_error("Failed to fork"); + return -1; + } + + if (pid == 0) { + auto ret = ::setpgid(0, 0); + if (ret == -1) { + print_sys_error("Failed to set process group"); + return -1; + } + + ret = ::tcsetpgrp(0, ::getpid()); + if (ret == -1 && errno != ENOTTY) { + print_sys_error("Failed to set terminal process group"); + return -1; + } + + ret = ::sigprocmask(SIG_SETMASK, &conf.old_set, nullptr); + if (ret == -1) { + print_sys_error("Failed to restore signal mask"); + return -1; + } + + ret = ::sigaction(SIGTTIN, &conf.ttin_action, nullptr); + if (ret == -1) { + print_sys_error("Failed to restore SIGTTIN action"); + return -1; + } + + ret = ::sigaction(SIGTTOU, &conf.ttou_action, nullptr); + if (ret == -1) { + print_sys_error("Failed to restore SIGTTOU action"); + return -1; + } + + args.emplace_back(nullptr); + + ::execv(args[0], const_cast(args.data())); + print_sys_error("Failed to exec"); + return -1; + } + + return pid; +} + +std::optional handle_exited_child(pid_t child) noexcept +{ + int status{}; + while (true) { + auto ret = ::waitpid(-1, &status, WNOHANG); + if (ret == 0) { + break; + } + + if (ret == child) { + return status; + } + } + + return std::nullopt; +} +} // namespace + +int main(int argc, char *argv[]) +{ + auto sig_ret = handle_signals(); + if (std::holds_alternative(sig_ret)) { + return std::get(sig_ret); + } + + auto conf = std::get(sig_ret); + + auto ret = ::prctl(PR_SET_CHILD_SUBREAPER, 1); + if (ret == -1) { + print_sys_error("Failed to set child subreaper"); + return -1; + } + + auto args = parse_args(argc, argv); + if (args.empty()) { + print_info("No arguments provided"); + return -1; + } + + auto child = run(args, conf); + if (child == -1) { + return -1; + } + + auto sigfd = ::signalfd(-1, &conf.cur_set, 0); + if (sigfd == -1) { + print_sys_error("Failed to create signalfd"); + return -1; + } + + int child_status{}; + signalfd_siginfo info{}; + while (true) { + std::memset(&info, 0, sizeof(info)); + auto ret = ::read(sigfd, &info, sizeof(info)); + if (ret == -1) { + print_sys_error("Failed to read from signalfd"); + return -1; + } + + if (info.ssi_signo != SIGCHLD) { + ret = ::kill(child, info.ssi_signo); + if (ret == -1) { + auto msg = std::string("Failed to forward signal ") + ::strsignal(info.ssi_signo); + print_sys_error(msg); + } + } + + auto child_exited = handle_exited_child(child); + if (child_exited) { + child_status = *child_exited; + break; + } + } + + ::close(sigfd); + + if (WIFEXITED(child_status)) { + print_info("Child exited with status " + std::to_string(WEXITSTATUS(child_status))); + } else if (WIFSIGNALED(child_status)) { + print_info("Child exited with signal " + std::to_string(WTERMSIG(child_status))); + } else { + print_info("Child exited with unknown status"); + } + + return 0; +} diff --git a/configure.h.in b/configure.h.in index 18d7b60d9..1d7c4c03b 100644 --- a/configure.h.in +++ b/configure.h.in @@ -18,7 +18,7 @@ #define LINGLONG_DEFAULT_OCI_RUNTIME "@LINGLONG_DEFAULT_OCI_RUNTIME@" #define LINGLONG_UAB_DATA_LOCATION "@CMAKE_INSTALL_FULL_LIBDIR@/linglong/builder/uab" #define LINGLONG_BUILDER_HELPER LINGLONG_LIBEXEC_DIR "/builder/helper" -#define LINGLONG_CONTAINER_INIT LINGLONG_LIBEXEC_DIR "/dumb-init" +#define LINGLONG_CONTAINER_INIT LINGLONG_LIBEXEC_DIR "/ll-init" #define LINGLONG_CLIENT_NAME "@LINGLONG_CLI_BIN@" #define LINGLONG_CLIENT_PATH "@CMAKE_INSTALL_FULL_BINDIR@/@LINGLONG_CLI_BIN@" #define LINGLONG_EXPORT_PATH "@LINGLONG_EXPORT_PATH@"