Detach forked process from linux service using dbus

97 Views Asked by At

I have a service on linux. This service needs to start different processes. For example, on some condition e.g. a network event, the service should start a process that has a GUI. For this i used fork() and execvp(). I furthermore dropped root privileges by setting the uid and gid according to the user, for which i want to run the process. I then perceded by setting the DISPLAY and XAUTHORITY environment variables accordingly.

This worked kinda. My process was indeed running and the GUI was displayed. However it is noticable that the process is not really detatched from the service. For example: pkexec does not work. It always tries to start the terminal version and fails. Also, i dont want my process to be killed, as soon as the service stops.

I read that for this i should use the dbus and use it to communicate with the systemd and either 1) start the process via systemd or 2) let systemd somehow detatch my process from the service.

Now i dont have any idea how that works. I can open the bus and i should also be able to call methods, however i dont know which method and what i am even trying to do.

Can someone guide me in the right direction, of which method i should call on the systemd to either start a process in a user session or detatch a process from my service.

Disclaimer: setting KillMode=none would solve the process killing issue, however this is, as far as i know, not recommended and also does not solve the pkexec problem.

3

There are 3 best solutions below

1
user1686 On BEST ANSWER

I have a service on linux. This service needs to start different processes. For example, on some condition e.g. a network event, the service should start a process that has a GUI

Don't do that. It's a really bad architecture, as you've found out. (Even if you do set DISPLAY and XAUTHORITY, how do you know what are the correct values for that? What guarantees that there's always exactly one user? What guarantees their XAUTHORITY is always the same?)

Instead, let the GUI itself start a "listener" process for the logged-in user, which waits for the events (either from system in general, or from your own system service) and simply starts the necessary programs because it's already running as the correct user in the correct cgroup. This "agent" pattern is commonly used in desktop environments; /etc/xdg/autostart is used to automatically start the agent process upon login.

I read that for this i should use the dbus and use it to communicate with the systemd and either 1) start the process via systemd or 2) let systemd somehow detatch my process from the service.

Now i dont have any idea how that works. I can open the bus and i should also be able to call methods, however i dont know which method and what i am even trying to do.

This is how the systemd approach would work:

  1. You can connect to the system service manager and ask it to start a transient service with parameters and environment that you specify through the D-Bus call. It will be completely detached from your main service.

  2. Or, you can connect to the user's personal systemd instance and ask that to start a transient service. The advantage here is that it will automatically have all the necessary environment inherited from the per-user systemd process – it's actually how some desktop environments launch apps nowadays.

    Unlike spawning the process directly, this only requires knowing the user's UID (and XDG_RUNTIME_DIR, but fortunately that is guaranteed to be at a fixed per-UID path).

  3. Or, you could spawn the process yourself (with the correct environment), then ask systemd to create a transient scope which will correspond to a new cgroup.

In all cases, the method call you want is StartTransientUnit(), with the only difference being the bus address you connect to. For option #1 it's the system bus; for option #2 you have to switch to the correct UID, then connect to the "user bus" at /run/user/<uid>/bus.

Start by experimenting with the systemd-run command; you can use dbus-monitor to see what calls it uses. (Or, in fact, you can just spawn systemd-run from your service.) For example:

runuser -u $username -- \
    env XDG_RUNTIME_DIR=/run/user/$uid \
    systemd-run --user --collect /usr/bin/xterm

systemd-run -M [email protected] --user --collect /usr/bin/xterm
8
Craig Estey On

Prefaced by my top comments ...

  1. Do double fork
  2. Do setsid in grandchild
  3. Do setpgrp in grandchild
  4. Close all standard units in grandchild

I've created a diagnostic program that appears to work. I've not tested it extensively. It doesn't set any environment, so I don't know how that will affect things.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

void
launch_top(int argc,char **argv)
{
    pid_t top = getpid();

    // parent launching child
    pid_t pcld = fork();
    if (pcld != 0) {
        waitpid(pcld,NULL,0);
        sleep(2);
        printf("launch_top: killing self %d\n",top);
        kill(top,SIGTERM);
        exit(0);
    }

    // child launching grandchild
    pid_t gcld = fork();
    if (gcld != 0)
        exit(0);

    // grandchild launching target
    setsid();
    setpgrp();

    fclose(stdin);
    fclose(stdout);
    fclose(stderr);

    char *av[3];

    av[0] = argv[0];
    av[1] = "forever";
    av[2] = NULL;

    execvp(av[0],av);
    perror("launch_top/execvp");
    exit(9);
}

void
stay_alive(int argc,char **argv)
{

    FILE *xflog = fopen("stay.log","w");
    setlinebuf(xflog);

    for (int loop = 0;  loop < 10;  ++loop) {
        fprintf(xflog,"stay_alive: %d\n",loop);
        sleep(1);
    }

    fprintf(xflog,"stay_alive: success\n");
    fclose(xflog);

    exit(0);
}

int
main(int argc,char **argv)
{

    setlinebuf(stdout);

    // ultimate program that should stay alive
    if (argc > 1)
        stay_alive(argc,argv);

    // launcher service
    else
        launch_top(argc,argv);

    return 0;
}

Here is the stdout output for the parent:

launch_top: killing self 2564383

Here is the sample output log stay.log for the target program that should stay alive and survive the original/parent being killed:

stay_alive: 0
stay_alive: 1
stay_alive: 2
stay_alive: 3
stay_alive: 4
stay_alive: 5
stay_alive: 6
stay_alive: 7
stay_alive: 8
stay_alive: 9
stay_alive: success
1
John Bollinger On

By a "linux service", you seem to mean a service managed via a SystemD service unit. Not all Linux distros rely on systemd, and there can be other kinds of services even on a systemd-based system.

Among the relevant aspects of systemd-based services is that systemd uses Linux control groups (cgroups) to help manage them. In particular, the recommended kill modes for systemd services (control-group and mixed) both are recommended exactly because they affect all processes in the service's cgroup. I advise against trying to evade that: a service, whether systemd-managed or not, should not start processes that exit its sphere of control.

But if you insist on having your service start processes that fully exit its control ("detach" from it), and if you want to start such processes by fork()ing them, then those processes that want to escape need to move themselves out of the service's cgroup. The root of that cgroup hierarchy is /sys/fs/cgroup/systemd on the systems with which I am familiar. To escape the service's cgroup in this cgroup hierarchy, the process would write (a decimal representation of) either 0 or its pid to file /sys/fs/cgroup/systemd/cgroup.procs. That should move it to the root cgroup of the hierarchy, out of systemd's control.

In that case, it would be wise to move the process to its own process group, too. If the service is system-level then I would accomplish that by moving the process to a new session (setsid()), but if it is a user-level service then I would consider keeping it in the same session and changing just its process group (there's more than one way to do it, but I would choose setpgid(0, 0)).

Note that although you probably don't need to double fork (see @Craig's answer) that might still be a good idea for the service's sake. FWIW, double-forking is always for the original process's sake, not that of the grandchild.