How to create an X window which won’t close by any way other than a terminal command

openboxterminalx11

I'm trying to create a process that will start with X11, will be graphical and unkillable by my own user.

More specifically, I have an urxvt drop-down terminal starting at startup and running a tmux session right away. What annoys me, is that I sometimes kill it with Alt+F4 when I forget that it is the drop-down terminal and I'm supposed to hide it instead.

Sure, I'm not losing my tmux session since i can reattach to it from another terminal, but I'd have to re-launch the drop-down terminal (with the exact config) and reattach and that's tedious.

What I would want to do, is to not be able to Alt+F4 kill this specific process.

After reading this:
Make a process unkillable on Linux

I understood that I should launch the process as belonging to another user, yet even if I manage to do that within my own X server with:

sudo xhost +local:
sudo runuser -l my-secondary-user -c urxvt

I face two unwanted behaviours:

  1. The process is killable by my main user (the one who owns the X session)
  2. The virtual terminal starts logged in as my-secondary-user (of course)

The (2) may be rectifiable – just run a command to login as my main user instead – but I have no idea on how to fix the (1).

Is there way to make this work, or another way to do what I want? I'd rather avoid having to mess with creating kernel modules if possible.

Running Arch Linux with Openbox.

EDIT

I got thinking that I may be able to make Alt+F4 not work for this specific window.
found this:
Openbox: disable Alt-F4 on per application basis

and modified my rc.xml thus:

<keybind key="A-F4">
 <action name="If">
  <application class="URxvt"
    title="tmux-session">
  </application>
  <then><!-- Do nothing for urxvt-tmux-session --></then>
  <else>
   <action name="Close" />
  </else>
 </action>
</keybind>

This works slightly better than before: I cannot Alt+F4 that terminal, but also cannot Alt+F4 any urxvt window now (guess this is because new urxvt windows are all run under the same class-name?)

Besides that I can kill everything I need from the menu bar, or right clicking into my tint panel, or, of course, by command.
But this approach means that:

  1. I can still kill my drop-down terminal by right clicking into the tint2 panel (maybe fixable if I make the drop-down terminal not appearing as an icon to the panel – should be possible with some research)
  2. I cannot Alt+F4 other terminal windows (not liking it much, but maybe acceptable)

Any ideas?

SOLUTION

I found a solution that works for me based on Dmitry's answer and my above edit:

1) i set a custom title to my urxvt:

urxvt -title urxvt-drop-down

I didn't use a custom name because it appears that the

urxvt -name urxvt-drop-down

command ignores the .Xresources file that i currently use, while -title uses it.

2) i made the Alt+F4 keybind to ignore terminals titled urxvt-drop-down.
rc.xml:


<keybind key="A-F4">
<action name="If">
<title>urxvt-drop-down</title>
<then><!-- Do nothing for urxvt-drop-down --></then>
<else>
<action name="Close" />
</else>
</action>
</keybind>

3) I made the culprit never appear in the taskbar, and since the skip_taskbar also unfocuses the window, refocused it.
rc.xml:


<application title="urxvt-drop-down">
<skip_taskbar>yes</skip_taskbar>
<focus>yes</focus>
</application>

Even though this solves the problem for me, it is essentially a workaround.
Thus I'm not yet sure if Stephane's answer is the correct one for this question.

Need to do a little research on the commands presented to understand what's being done.

I won't be using xterm though, since i want fake-transparency and drop-down capability (some kind of drop-down might be possible with xterm too i think, but transparency certainly ain't without a compositor – and i won't use a compositor)

Best Answer

If switching to xterm is an option, you could use the hack below. There are a few caveats though. Once you address most of them, the solution ends up quite complicated, see the final script at the end.

xterm -e 'trap "" HUP; your-application'

Upon receiving the instruction to close from the window manager, xterm will send a SIGHUP to the process group of your-application, and only exit itself when the process returns.

That assumes your-application doesn't reset the handler for SIGHUP and could have unwanted side effects for the children of your-application.

Both of which seem to be a problem if your-application is tmux.

To work around those, you could do:

xterm -e sh -c 'bash -mc tmux <&1 & trap "" HUP; wait'

That way, tmux would be started in a different process group, so only the sh would receive the SIGHUP (and ignore it).

Now, that doesn't apply to tmux which resets the handler for those signals anyway, but in the general case, depending on your implementation of sh, the SIGINT, SIGQUIT signals and generally both will be ignored for your-application as that bash is started as an asynchronous command from a non-interactive sh. That means you couldn't interrupt your-application with Ctrl+C or Ctrl+\.

That's a POSIX requirement. Some shells like mksh don't honour it (at least not the current versions), or only in part like dash that does it for for SIGINT but not SIGQUIT. So, if mksh is available, you could do:

xterm -e mksh -c 'bash -mc your-application <&1 & trap "" HUP; wait'

(though that may not work in future versions of mksh if they decide to fix that non-conformance).

Or if you can't guarantee that mksh or bash will be available or would rather not rely on behaviour that may change in the future, you can do their work by hand with perl and for instance write an unclosable-xterm wrapper script like:

#! /bin/sh -
[ "$#" -gt 0 ] || set -- "${SHELL:-/bin/sh}"
exec xterm -e perl -MPOSIX -e '
  $pid = fork;
  if ($pid == 0) {
    setpgrp or die "setpgrp: $!";
    tcsetpgrp(0,getpid) or die "tcsetpgrp: $!";
    exec @ARGV;
    die "exec: $!";
  }
  die "fork: $!" if $pid < 0;
  $SIG{HUP} = "IGNORE";
  waitpid(-1,WUNTRACED)' "$@"

(to be called as unclosable-xterm your-application and its args).

Now, another side effect is that that new process group we're creating and putting in foreground (with bash -m or setpgrp+tcsetpgrp above) is no longer the session leader process group, so no longer an orphaned process group (there's a parent supposedly caring for it now (sh or perl)).

What that means is that upon pressing Ctrl+Z, that process will be suspended. Here, our careless parent will just exit, which means the process group will get a SIGHUP (and hopefully die).

To avoid it, we could just ignore the SIGTSTP in the child process, but then if your-application is an interactive shell, for some implementations like mksh, yash or rc, Ctrl-Z won't work either for the jobs they run.

Or we could implement a more careful parent that resumes the child each time it's stopped, like:

#! /bin/sh -
[ "$#" -gt 0 ] || set -- "${SHELL:-/bin/sh}"
exec xterm -e perl -MPOSIX -e '
  $pid = fork;
  if ($pid == 0) {
    setpgrp or die "setpgrp: $!";
    tcsetpgrp(0,getpid) or die "tcsetpgrp: $!";
    exec @ARGV;
    die "exec: $!";
  }
  die "fork: $!" if $pid < 0;
  $SIG{HUP} = "IGNORE";
  while (waitpid(-1,WUNTRACED) > 0 && WIFSTOPPED(${^CHILD_ERROR_NATIVE})) {
    kill "CONT", -$pid;
  }' "$@"

Another issue is that if xterm is gone for another reason than the close from the window manager, for example if xterm is killed or loses the connection to the X server (because of xkill, the destroy action of you Window manager, or the X server crashes for instance), then those processes won't die as SIGHUP would also be used in those cases to terminate them. To work around that, you could use poll() on the terminal device (which would be torn down when xterm goes):

#! /bin/sh -
[ "$#" -gt 0 ] || set -- "${SHELL:-/bin/sh}"
exec xterm -e perl -w -MPOSIX -MIO::Poll -e '
  $pid = fork; # start the command in a child process
  if ($pid == 0) {
    setpgrp or die "setpgrp: $!"; # new process group
    tcsetpgrp(0,getpid) or die "tcsetpgrp: $!"; # in foreground
    exec @ARGV;
    die "exec: $!";
  }
  die "fork: $!" if $pid < 0;
  $SIG{HUP} = "IGNORE"; # ignore SIGHUP in the parent
  $SIG{CHLD} = sub {
    if (waitpid(-1,WUNTRACED) == $pid) {
      if (WIFSTOPPED(${^CHILD_ERROR_NATIVE})) {
        # resume the process when stopped
        # we may want to do that only for SIGTSTP though
        kill "CONT", -$pid;
      } else {
        # exit when the process dies
        exit;
      }
    }
  };

  # watch for terminal hang-up
  $p = IO::Poll->new;
  $p->mask(STDIN, POLLERR);
  while ($p->poll <= 0 || $p->events(STDIN) & POLLHUP == 0) {};
  kill "HUP", -$pid;
  ' "$@"
Related Question