23

Using the command line, I'd like show a notification on every running X display. ( and running console )

Something like:

notify-send-all 'Warning' 'Nuclear launch in 5 minutes, please evacuate'

Is there a program that will do this? If not, can this be implemented with bash?

Caleb
  • 70,105
Stefan
  • 25,300
  • 1
    For people coming here years later, there is a simple notify_all function in this answer which works in Ubuntu 16.04, and can be used in scripts started by root. – mivk Feb 12 '17 at 11:48

8 Answers8

18

You can send a message to all consoles with the command wall.

For sending notifications under X there is notify-send which sends a notification to the current user on the current display. (From your question, I guess you already know this one.) You can build upon this with some bash scripting. Basically you have to find out which users are on which X-Displays. Once you got this info you can use notify-send like this:

DISPLAY=:0 sudo -u fschmitt notify-send "Message"

Where fschmitt is the user at display 0. You can parse the output of the "who" command to find all users and their displays. The output looks like this

[edinburgh:~]$ who
markmerk3 tty7         2010-09-23 10:59 (:0)
markmerk3 pts/1        2010-09-30 13:30 (:0.0)
fschmitt pts/2        2010-10-08 11:44 (ip-77-25-137-234.web.vodafone.de)
markmerk3 pts/0        2010-09-29 18:51 (:0.0)
seamonkey pts/6        2010-09-27 15:50 (:1.0)
markmerk3 pts/5        2010-09-27 14:04 (:0.0)
seamonkey tty8         2010-09-27 15:49 (:1)
markmerk3 pts/13       2010-09-28 17:23 (:0.0)
markmerk3 pts/3        2010-10-05 10:40 (:0.0)

You see, there are two users running X sessions, markmerk3 at display 0 and seamonkey at display 1. I think you need to grep for tty[0-9]* then assure that at the end of the line there is (:[0-9.]*) to get rid of console logins and extract the display id from the string between the parentheses.

fschmitt
  • 8,790
  • 2
    The command who tells you who is logged in and on which X display that login is. You just might have to filter it somewhat. – tante Oct 08 '10 at 09:49
  • 1
    While it is probably better to just use a loop in a shell script you could always do something like who | awk '/\(:[0-9]+\)/ {gsub("[:|(|)]","");print "DISPLAY=:"$5 " sudo -u " $1 " notify-send \"Message\""}' | bash. Also, you might want to see http://unix.stackexchange.com/questions/1596/as-root-can-i-launch-a-graphical-program-on-another-users-desktop/1598#1598 – Steven D Oct 08 '10 at 15:34
  • It doesn't work: Error spawning command line “dbus-launch --autolaunch=8c77b800842a42468876d979195a5a03 --binary-syntax --close-stderr”: Child process exited with code 1 – Michael Tsang Sep 14 '23 at 12:42
17

This thread is a bit old, sorry, but I hope I still can add something useful to the topic. (also Josef Kufner wrote a nice script, it was just a little bit too long for my taste, and it uses PHP).

I also needed a tool as described in the original question (to send a message to all active X-users). And based on the answers here, I wrote this little bash-only script, which searches for active X-users (using who), and then running notify-send for every active user.

And the best: you can use my script exactly like "notify-send", with all of its parameters! ;-)

notify-send-all:

#!/bin/bash
PATH=/usr/bin:/bin

XUSERS=($(who|grep -E "(:0-9*)"|awk '{print $1$NF}'|sort -u)) for XUSER in "${XUSERS[@]}"; do NAME=(${XUSER/(/ }) DISPLAY=${NAME[1]/)/} DBUS_ADDRESS=unix:path=/run/user/$(id -u ${NAME[0]})/bus sudo -u ${NAME[0]} DISPLAY=${DISPLAY}
DBUS_SESSION_BUS_ADDRESS=${DBUS_ADDRESS}
PATH=${PATH}
notify-send "$@" done

Copy the above code into a file named notify-send-all, make it executable and copy it to /usr/local/bin or /usr/bin (as you like). Then run it e.g. as root in a console session like this:

notify-send-all -t 10000 "Warning" "The hovercraft is full of eels!"

I'm using it several months now, on different machines, and didn't have any problems so far, and I've tested it with MATE and Cinnamon desktops. Also successfully running it within cron and anacron.

I wrote this script for/under Arch Linux, so please tell me if you're having problems on another Linux distributions or desktops.

fra-san
  • 10,205
  • 2
  • 22
  • 43
Andy
  • 171
  • |egrep ?? is egrep a command ? – Sw0ut Apr 21 '17 at 09:06
  • @Sw0ut, egrep is indeed a command. But in the man page of grep(1) says that egrep, fgrep and rgrep are deprecated, and the use of their equivalent forms "grep -E", "grep -F" and "grep -r" is recommended. – rsuarez May 21 '17 at 16:49
  • 1
    Instead of awk '{print $1$5}' it is better to use awk '{print $1$NF}', so that it doesn't break on some locales where date is formatted with spaces (e.g. Jun 3 instead of 2017-06-03). Here is also a version to notify specific user instead of all users: https://gist.github.com/shvchk/ba2f0da49bf2f571d6bf606d96f289d7 – Shevchuk Jun 03 '17 at 19:11
  • 1
    Works splendidly on Ubuntu after using grep -E and adding /bin to the path (see the edit). Feel free to revert if you object – serv-inc Oct 18 '18 at 08:14
  • I bodged in some help text:

    if [ $# -eq 0 ] ; then echo "No summary specified"; notify-send --help | sed -e 's/notify-send/&-all/g' -e 's/create a notification/& to all users/g'; exit 0; fi

    – iheggie Feb 11 '20 at 00:09
  • had the same prob as @Shevchuk . I first prefixed LC_ALL=en_US to the who command but the awk fix is cleaner – mx1up Aug 09 '20 at 15:01
  • for the for loop to work over multiple users, I had to change it to: for XUSER in ${XUSERS[@]}; do – mx1up Aug 09 '20 at 15:04
  • This actually worked, with a small modification. On bash v5.0.17 I had to set $NAME as NAME=(${XUSER/\(/ }) and $DISPLAY as DISPLAY=${NAME[1]/\)/} -- added '' before '(' and before ')'. Thank you! – kavadias Sep 04 '22 at 00:39
  • This does not work on the upcoming Debian 12 (bookworm), unless users log in using Xorg instead of Wayland. – hackerb9 Jun 08 '23 at 00:51
  • @andy: you've inspired me to write my own solution which works on both X and Wayland. It also runs asynchronously which is important because notify-send can hang for a long time. While I was at it, I prefix any output from notify-send with the username so the --action option works. Please see my answer below. – hackerb9 Jun 08 '23 at 07:09
4

In Ubuntu 16.04, I wanted notifications from a script running as root from crontab. After setting the environment variables, sudo -u $user didn't work for some reason, but sh -c "..." $user does work.

So I now use this function:

notify_all() {
    local title=$1
    local msg=$2

    who | awk '{print $1, $NF}' | tr -d "()" |
    while read u d; do
        id=$(id -u $u)
        . /run/user/$id/dbus-session
        export DBUS_SESSION_BUS_ADDRESS
        export DISPLAY=$d
        su $u -c "/usr/bin/notify-send '$title' '$msg'"
    done 
}

How to find the DBUS_SESSION_BUS_ADDRESS variable probably depends on your distribution. In Ubuntu 16.04, it is in /run/user/$UID/dbus-session, which can simply be sourced. id -u is used in the function above to get the UID from the username returned by who.

mivk
  • 3,596
3

I needed this too for some system-wide notifications. Here is my solution. It scans /proc to find all session busses and then it executes notify-send on each of it (once per bus). All arguments are passed unchanged to real notify-send.

#!/bin/bash

/bin/grep -sozZe '^DBUS_SESSION_BUS_ADDRESS=[a-zA-Z0-9:=,/-]*$' /proc/*/environ \
| /usr/bin/php -r '
        $busses = array();
        array_shift($argv);
        while($ln = fgets(STDIN)) {
                list($f, $env) = explode("\0", $ln, 2);
                if (file_exists($f)) {
                        $user = fileowner($f);
                        $busses[$user][trim($env)] = true;
                }
        }
        foreach ($busses as $user => $user_busses) {
                foreach ($user_busses as $env => $true) {
                        if (pcntl_fork()) {
                                posix_seteuid($user);
                                $env_array = array("DBUS_SESSION_BUS_ADDRESS" => preg_replace("/^DBUS_SESSION_BUS_ADDRESS=/", "", $env));
                                pcntl_exec("/usr/bin/notify-send", $argv, $env_array);
                        }
                }
        }
' -- "$@"
1

Here's an update of Andy's script: The way it determined the DBUS_SESSION_BUS_ADDRESS does not work on Centos 7. Also the who command did not list some sessions for some reason, so I parse the ps aux output instead. This script assumes users are logged in using X2GO (nxagent), but it should be simple to adjust for other cases.

#!/bin/bash
PATH=/usr/bin:/bin
NOTIFY_ARGS='-u critical "Shutdown notice" "THE SYSTEM IS GOING DOWN TODAY AT 23:00.\nWe recommend you to save your work in time\!" -i /usr/share/icons/Adwaita/32x32/devices/computer.png -t 28800000'

function extract_displays {
    local processes=$1
    processes=$(printf '%s\n' "$processes" | grep -P "nxagent.+:\d+")
    ids=$(printf '%s\n' "$processes" | grep -oP "\W\K:(\d)+")
    echo $ids
}


function find_dbus_address {
    local name=$1
    PID=$(pgrep 'mate-session' -u $name)
    if [ -z "$PID" ]; then
        PID=$(pgrep 'gnome-session' -u $name)
    fi
    if [ -z "$PID" ]; then
        PID=$(pgrep 'xfce4-session' -u $name)
    fi

    exp=$(cat /proc/$PID/environ | grep -z "^DBUS_SESSION_BUS_ADDRESS=")
    echo $exp
}

PROCESSES=$(ps aux)
DISPLAY_IDS=$(extract_displays "$PROCESSES")
echo "Found the following DISPLAYS: $DISPLAY_IDS"
for DISPLAY in $DISPLAY_IDS; do
    NAME=$(printf '%s\n' "$PROCESSES" | grep -P "nxagent.+$DISPLAY" | cut -f1 -d ' ')
    DBUS_ADDRESS=$(find_dbus_address $NAME)
    echo "Sending message to NAME=$NAME DISPLAY=$DISPLAY DBUS_ADDRESS=$DBUS_ADDRESS"
    echo "NOTIFY_ARGS=$NOTIFY_ARGS"
    eval sudo -u ${NAME} DISPLAY=${DISPLAY} ${DBUS_ADDRESS} PATH=${PATH} notify-send $NOTIFY_ARGS
done
jpf
  • 111
1

Too low rep to comment, so here goes a separate answer.

My company has very strict rules enforced and passing shellcheck is one of them. I've adapted Andy's answer a bit and wanted to share it in case anybody else has to address SC2086 and SC2206.

XUSERS=("$(who | grep -E "\(:[0-9](\.[0-9])*\)" | awk '{print $1$NF}' | sort -u)" )
for XUSER in "${XUSERS[@]}"
do
    NAME="${XUSER%%(*}"
    DISPLAY="${XUSER#*\(}"
    DISPLAY="${DISPLAY%*)}"
    DBUS_ADDRESS=unix:path=/run/user/$(id -u "${NAME[0]}")/bus
    sudo -u "${NAME[0]}" DISPLAY="${DISPLAY}" \
         DBUS_SESSION_BUS_ADDRESS="${DBUS_ADDRESS}" \
         PATH="${PATH}" \
         notify-send "$@"
done
1

Solution: use my notify-send-all script

#!/bin/bash
# notify-send-all

PATH=/usr/bin:/bin

send-to() {
    local name busroute
    name="$1";  shift
    busroute="/run/user/$(id -u "$name")/bus"  ||  return 1
    sudo -u "$name" \
        PATH="$PATH" \
        DBUS_SESSION_BUS_ADDRESS="unix:path=$busroute" \
        -- \
        notify-send "$@"  2>&1  |  sed "s/^/$name\t/" 
}

send-all() {
    for name in $(who | cut -f1 -d" " | sort -u)
    do
        send-to "$name" "$@" &
    done
    wait
}

sudo --validate
send-all "$@"

Features

  • Works in X and Wayland (as of 2023).
  • Runs asynchronously, does not block when libnotify waits for a timeout or response.
  • Handles --action in a reasonable way; prefixes output with the username and then a TAB character.
  • Does not rely on DISPLAY to send the notification.

Example usage

notify-send-all "My hovercraft is full of eels"

More complex example:

notify-send-all --urgency=critical \
                --action=run="Run away!" \
                --action=hide="Hide!" \
                "Warning: Nuclear launch imminent"

Example output:

    andy        run
    circus      hide
    hackerb9    run

Download the script

A more full featured version of this script is available at https://github.com/hackerb9/notify-send-all/ . The main improvements are help, error handling, and ability to send to a specific user.

hackerb9
  • 1,569
0
users=$(who | awk '{print $1}')

for user in $users<br>
do
        DISPLAY=:0 sudo -u $user notify-send "hello!!"
done
Anthon
  • 79,293
Vicent
  • 11