Aufruf eines Programms beim Ende einer X11-Sitzung

Aufgabenstellung: beim Ende einer X11-Sitzung soll ein Programm aufgerufen werden.

Während beim Start einer Sitzung Dateien aus dem HOME-Verzeichnis des Users, z.B. $HOME/.xsession und $HOME/.xsessionrc, eingebunden werden, gibt es keinen solchen Haken für das Ende einer Sitzung.

Die bei Stackexchange vorgeschlagenen Lösungen empfinde ich als unbefriedigend:

  • Die global genutzte Datei /etc/gdm/PostSession/Default kann nur von root und sollte nicht für einen einzelnen User angepasst werden;
  • Die Datei .bash_logout wird auch beim Ende einer (Bash-)Shell-Sitzung aufgerufen und ist damit zu unspezifisch.

Was passiert beim Ende einer Sitzung?

  1. Der Window-Manager terminiert.

    Der Window-Manager wird von dem Prozess ausgeführt, der vorher die .xsession eingelesen hat. Man kann also aus der .xsession ein Skript im Hintergrund starten, das auf das Ende seines Parent-Prozesses wartet und dann das gewünschte Programm aufruft. Dieses Warten ist leider nur möglich mit einer Linux-spezifischen Erweiterung.

  2. Der X11-Server trennt alle Verbindungen.

    Man kann also aus der .xsession ein Skript im Hintergrund starten, das sich mit der X-Server verbindet, auf das Trennen der Verbindung wartet und dann das gewünschte Programm aufruft.


Methode 1: warten auf Ende des Window-Managers

Die clib für Linux definiert eine Funktion prctl() (man 7 prctl), mit der ein Prozess das Prozess-System manipulieren kann. Nach dem Aufruf von prset(PR_SET_PDEATHSIG, SIGTERM) wird das gewünschte Signal an den aufrufenden Prozess gesendet, sobald der Eltern-Prozess terminiert. Dieses Signal wird abgefangen und das gewünschte Programm gestartet.

Compiliert man diesen Aufruf in eine Shared-Library, so kann man mit Hilfe der Enviroment-Variable LD_PRELOAD diese Funktion jedem Programm unterschieben. (Diese Idee ist schamlos bei Stackoverflow geklaut.)

Und so wird's gemacht:

  1. Laden Sie das kleine C-Programm prctl_set_pdeathsig_term.c herunter:

    #include <sys/prctl.h>
    #include <sys/types.h>
    #include <signal.h>
    
    __attribute__((constructor))
    static void
    on_load() {
    	prctl(PR_SET_PDEATHSIG, SIGTERM);
    }
    
  2. Compilieren Sie dieses Programm:
    gcc -fPIC -shared -o prctl_set_pdeathsig_term.so prctl_set_pdeathsig_term.c
  3. Wählen Sie ein Verzeichnis für die erzeugte dynamische Bibliothek, und schieben sie die Datei in dieses Verzeichnis.

    In meinem Beispiel nutze ich das Unterverzeichnis bin in meinem HOME-Verzeichnis:

    mv -iv prctl_set_pdeathsig_term.so $HOME/bin/
  4. Wählen Sie das Skript oder Programm, das bei Ende der Sitzung gestartet werden soll. Wenn das Programm nicht in ihrem Standard-Suchpfad liegt, nutzen sie den vollen Pfad.

    In meinem Beispiel heißt das Programm onlogout.sh, es liegt im Standard-Suchpfad.

  5. Ergänzen Sie die Datei .xsession oder .xsessionrc in ihrem $HOME-Verzeichnis um eine Zeile, wobei Sie die blau umrahmten Teile anpassen:

    LD_PRELOAD=$HOME/bin/prctl_set_pdeathsig_term.so perl -e '\$SIG{"TERM"}=sub{};sleep;system"onlogout.sh&"' &

    Wichtig: LD_PRELOAD= verlangt einen absolutenPfad.

Beim Start der Sitzung wird das Perl-Skript im Hintergrund gestartet und diesem das Signal beim Ende des Eltern-Prozesses, also beim Ende des Window-Managers, untergeschoben.

Das Perl-Skript fängt dieses Signal ab und begibt sich danach in einen endlosen Schlaf, endlos bis zum gewünschten Signal. Danach ruft es das gewünschte Programm auf.

Achtung Race-Conditions: wenn der Sitzungsprozess stirbt, bevor prctl() aufgerufen wurde, wird das Perl-Skript endlos schlafen; wenn der Sitzungsprozess nach dem Aufruf von prctl() stirbt, aber vor der Zuweisung \$SIG{"TERM"}=sub{}, wird das Perl-Skript durch das Signal beendet, ohne dass das gewünschte Programm aufgerufen wird. Die Verbesserung ist left as an exercise to the students™.

Faulsäcke wählen stattdessen die Methode 2.


Methode 2: warten auf Trennen der Verbindung zum X-Server

Die Idee ist bestechend einfach: beim Ende der Sitzung trennt der Server alle Verbindungen. Also verbindet man sich mit dem Server und wartet auf die Trennung. Leider muss man auch hier ein kleines Programm compilieren:

  1. Laden Sie das kleine C-Programm xconnect.c herunter:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <X11/Xlib.h>
    
    int ioErrorHandler(Display *display) {
    	exit(0);
    }
    
    int main(int argc, char** argv) {
    
    	char *prog = argv[0];
    	Display* display = XOpenDisplay(NULL);
    
    	if (display == NULL) {
    		fprintf(stderr, "%s: Cannot open display \\"%s\\"\\n", prog, getenv("DISPLAY"));
    		exit(1);
    	}
    
    	XSetIOErrorHandler(&ioErrorHandler);
    
    	for(;;) {
    		XEvent event;
    		XNextEvent(display, &event);
    		fprintf (stderr, "%s: Unexpected XEvent (type: %d)\\n", prog, event.type);
    	}
    }
    

    Diese Programm verbindet sich mit dem Default-X-Server, beim Scheitern des Verbindungsaufbaus liefert es den Return-Code 1.

    Danach wartet es in einer Schleife auf Events (es sollten keine kommen) und ignoriert diese. Beim Ende der X11-Sitzung trennt der Server alle Verbingungen, die xlib erzeugt einen XIO-Error, der vom ioErrorHandler() bearbeitet wird: dieser beendet das Programm mit dem Return-Code 0.

    Die Return-Codes sind also:

     0: X11-Sitzung wurde beendet;
     1: Programm wurde außerhalb einer X11-Sitzung gestartet;
    >1: Programm wurde durch ein Signal beendet.

    (Falls man diese Funktion mit einem der Standard-X11-Programm nachbilden kann, bin ich für einen kurzen Hinweis sehr dankbar.)

  2. Compilieren Sie dieses Programm:
    gcc -Werror -Wall -o xconnect xconnect.c -lX11
  3. Wählen Sie ein Verzeichnis für das erzeugte Programm, und schieben sie die Datei in dieses Verzeichnis.

    In meinem Beispiel nutze ich das Unterverzeichnis bin in meinem HOME-Verzeichnis:

    mv -iv xconnect $HOME/bin/
  4. Wählen Sie das Skript oder Programm, das bei Ende der Sitzung gestartet werden soll. Wenn das Programm nicht in ihrem Standard-Suchpfad liegt, nutzen sie den vollen Pfad.

    In meinem Beispiel heißt das Programm onlogout.sh, es liegt im Standard-Suchpfad.

  5. Ergänzen Sie die Datei .xsession oder .xsessionrc in ihrem $HOME-Verzeichnis um eine Zeile, wobei Sie den blau umrahmten Teil anpassen:

    xconnect && onlogout.sh &

Beim Start der Sitzung wird dieses Sub-Skript im Hintergrund gestartet und das xconnect versucht sich mit dem X-Server zu verbinden. Nur wenn das gelingt, terminiert xconnect beim Auftrennen der Verbindung mit dem Return-Code 0, und nur dann wird das gewünschte Programm gestartet.

Wenn die Verbindung fehlschlägt, oder wenn xconnect durch ein Signal beendet wird, liefert es einen Return-Code ungleich 0, und das gewünschte Programm wird nicht gestartet.