Search code examples
pythonpython-3.xos.systemnotify-send

Run os.system as if python was not executed with sudo


When I run the following command, everyhting works as expected. No error and I get a system notification saying "Hello":

$ python3
>>> import os
>>> os.system("notify-send Hello")
0

However, when I do this:

$ sudo python3
>>> import os
>>> os.system("notify-send Hello")

The script gets stuck and nothing happens.

I then tried to do this:

$ sudo python3
>>> import os
>>> os.seteuid(1000)
>>> os.system("notify-send Hello")

(1000 being my normal non-root user account)
But still, the script gets stuck and nothing happens.

I also tried this:

$ sudo python3
>>> import os
>>> os.system("su my-user-name -c 'notify-send Hello'")

and this:

$ sudo python3
>>> import os
>>> os.seteuid(1000)
>>> os.system("su my-user-name -c 'notify-send Hello'")

They all have the same issue...

I'm not looking for an alternative way of creating notifications. I'm not interested in subprocess or things like notify2 which cause a whole new category of problems on my system. Oh and please don't tell me not to use sudo. I have my reasons.


Solution

  • The implementation detail I've discovered through trial-and-error is notify-send requires the XDG_RUNTIME_DIR environment variable to function -- at least with these versions:

    $ dpkg -l | grep libnotify
    ii  libnotify-bin                              0.7.7-3                                      amd64        sends desktop notifications to a notification daemon (Utilities)
    ii  libnotify4:amd64                           0.7.7-3                                      amd64        sends desktop notifications to a notification daemon
    

    I first determined it needed some sort of environment variable by using env -i notify-send hello, which produced no notification.

    I then bisected the environment with a modified version of this script

    How you get that environment variable is up to you, but you need to be running notify-send as the proper user and with that variable set.

    Here's a sample python script, I refuse to use os.system due to its security issues:

    import os
    import pwd
    import subprocess
    import sys
    
    
    def main():
        if len(sys.argv) != 2:
            raise SystemExit(f'usage `{sys.argv[0]} USER`')
        if os.getuid() != 0:
            raise SystemExit('expected to run as root')
    
        # find the `gnome-session` executable, we'll use that to grab
        # XDG_RUNTIME_DIR
        cmd = ('pgrep', '-u', sys.argv[1], 'gnome-session')
        pid = int(subprocess.check_output(cmd))
    
        # read that process's environment
        with open(f'/proc/{pid}/environ') as f:
            for line in f.read().split('\0'):
                if line.startswith('XDG_RUNTIME_DIR='):
                    _, _, xdg_runtime_dir = line.partition('=')
                    break
            else:
                raise SystemExit('Could not find XDG_RUNTIME_DIR')
    
        # run the notify send as the right user
        uid = pwd.getpwnam(sys.argv[1]).pw_uid
        os.seteuid(uid)
        os.environ['XDG_RUNTIME_DIR'] = xdg_runtime_dir
        os.execvp('notify-send', ('notify-send', 'ohai'))
    
    
    if __name__ == '__main__':
        exit(main())
    

    DISCLAIMER: this script is doing some very hacky things that I wouldn't necessarily suggest in production code. Notably:

    • shelling out to pgrep to find a process
    • reading another process's environment variables
    • sudo

    Sample usage:

    $ python3 t.py
    usage `t.py USER`
    $ python3 t.py asottile
    expected to run as root
    $ sudo python3 t.py asottile
    # (I get a notification for `ohai`)