exec: Spawn processes as direct children (#11735)

* exec: Spawn processes as direct children

Spawn processes as children rather than grandchildren.
This way, spawned processes may track Hyprland's state
by watching their parent, either directly or indirectly
(e.g., Linux's `PR_SET_PDEATH_SIG`).

Fixes #11728

* tests/exec: Add the test on process spawning

Add a test that ensures that:
- A spawned process remains a direct child of Hyprland;
- Upon termination, the process does not become a zombie.
This commit is contained in:
Nikolai Nechaev 2025-09-24 02:32:48 +09:00 committed by GitHub
parent 70a7047ee1
commit 29b103c376
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 95 additions and 38 deletions

View file

@ -5,6 +5,7 @@
#include "init/initHelpers.hpp"
#include "debug/HyprCtl.hpp"
#include <csignal>
#include <cstdio>
#include <hyprutils/string/String.hpp>
#include <hyprutils/memory/Casts.hpp>
@ -35,6 +36,17 @@ static void help() {
--version -v - Print this binary's version)");
}
static void reapZombieChildrenAutomatically() {
struct sigaction act;
act.sa_handler = SIG_DFL;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_NOCLDWAIT;
#ifdef SA_RESTORER
act.sa_restorer = NULL;
#endif
sigaction(SIGCHLD, &act, nullptr);
}
int main(int argc, char** argv) {
if (!getenv("XDG_RUNTIME_DIR"))
@ -183,6 +195,8 @@ int main(int argc, char** argv) {
if (!envEnabled("HYPRLAND_NO_RT"))
NInit::gainRealTime();
reapZombieChildrenAutomatically();
Debug::log(LOG, "Hyprland init finished.");
// If all's good to go, start.

View file

@ -949,17 +949,9 @@ uint64_t CKeybindManager::spawnRawProc(std::string args, PHLWORKSPACE pInitialWo
const auto HLENV = getHyprlandLaunchEnv(pInitialWorkspace);
int socket[2];
if (pipe(socket) != 0) {
Debug::log(LOG, "Unable to create pipe for fork");
}
CFileDescriptor pipeSock[2] = {CFileDescriptor{socket[0]}, CFileDescriptor{socket[1]}};
pid_t child, grandchild;
child = fork();
pid_t child = fork();
if (child < 0) {
Debug::log(LOG, "Fail to create the first fork");
Debug::log(LOG, "Fail to fork");
return 0;
}
if (child == 0) {
@ -970,41 +962,28 @@ uint64_t CKeybindManager::spawnRawProc(std::string args, PHLWORKSPACE pInitialWo
sigemptyset(&set);
sigprocmask(SIG_SETMASK, &set, nullptr);
grandchild = fork();
if (grandchild == 0) {
// run in grandchild
for (auto const& e : HLENV) {
setenv(e.first.c_str(), e.second.c_str(), 1);
}
setenv("WAYLAND_DISPLAY", g_pCompositor->m_wlDisplaySocket.c_str(), 1);
int devnull = open("/dev/null", O_WRONLY | O_CLOEXEC);
if (devnull != -1) {
dup2(devnull, STDOUT_FILENO);
dup2(devnull, STDERR_FILENO);
close(devnull);
}
execl("/bin/sh", "/bin/sh", "-c", args.c_str(), nullptr);
// exit grandchild
_exit(0);
for (auto const& e : HLENV) {
setenv(e.first.c_str(), e.second.c_str(), 1);
}
write(pipeSock[1].get(), &grandchild, sizeof(grandchild));
setenv("WAYLAND_DISPLAY", g_pCompositor->m_wlDisplaySocket.c_str(), 1);
int devnull = open("/dev/null", O_WRONLY | O_CLOEXEC);
if (devnull != -1) {
dup2(devnull, STDOUT_FILENO);
dup2(devnull, STDERR_FILENO);
close(devnull);
}
execl("/bin/sh", "/bin/sh", "-c", args.c_str(), nullptr);
// exit child
_exit(0);
}
// run in parent
read(pipeSock[0].get(), &grandchild, sizeof(grandchild));
// clear child and leave grandchild to init
waitpid(child, nullptr, 0);
if (grandchild < 0) {
Debug::log(LOG, "Fail to create the second fork");
return 0;
}
Debug::log(LOG, "Process Created with pid {}", grandchild);
Debug::log(LOG, "Process Created with pid {}", child);
return grandchild;
return child;
}
SDispatchResult CKeybindManager::killActive(std::string args) {