notify-broadcast 0.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- notify_broadcast-0.0.3/LICENSE +21 -0
- notify_broadcast-0.0.3/PKG-INFO +170 -0
- notify_broadcast-0.0.3/README.md +151 -0
- notify_broadcast-0.0.3/notify_broadcast/__init__.py +5 -0
- notify_broadcast-0.0.3/notify_broadcast/dbussessionmanager.py +121 -0
- notify_broadcast-0.0.3/notify_broadcast/notify_broadcast.py +28 -0
- notify_broadcast-0.0.3/notify_broadcast/notifybroadcastargumentparser.py +321 -0
- notify_broadcast-0.0.3/notify_broadcast.egg-info/PKG-INFO +170 -0
- notify_broadcast-0.0.3/notify_broadcast.egg-info/SOURCES.txt +13 -0
- notify_broadcast-0.0.3/notify_broadcast.egg-info/dependency_links.txt +1 -0
- notify_broadcast-0.0.3/notify_broadcast.egg-info/entry_points.txt +2 -0
- notify_broadcast-0.0.3/notify_broadcast.egg-info/requires.txt +4 -0
- notify_broadcast-0.0.3/notify_broadcast.egg-info/top_level.txt +1 -0
- notify_broadcast-0.0.3/pyproject.toml +27 -0
- notify_broadcast-0.0.3/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [year] [fullname]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: notify-broadcast
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: Broadcast version of notify-send to allow root processes to send a notification to all users with an active DBUS session
|
|
5
|
+
Author-email: Jason But <jbut@swin.edu.au>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jason-but/notify-broadcast
|
|
8
|
+
Project-URL: Issues, https://github.com/jason-but/notify-broadcast/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: colorlog
|
|
15
|
+
Requires-Dist: dasbus
|
|
16
|
+
Requires-Dist: psutil
|
|
17
|
+
Requires-Dist: pygobject
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# notify-broadcast
|
|
21
|
+
|
|
22
|
+
This program was developed because I run a number of **cron** jobs as root that I would like to be
|
|
23
|
+
able to send notifications to the GUI User
|
|
24
|
+
|
|
25
|
+
`notify-send` only functions if run by the same user running the graphical session.
|
|
26
|
+
|
|
27
|
+
I created `notify-broadcast` to effectively tack the same parameters as `notify-send` but that
|
|
28
|
+
could be executed by the `root` user.
|
|
29
|
+
|
|
30
|
+
The application searches for all active DBUS Notification sessions, and sends the notification to
|
|
31
|
+
all currently attached users.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
When complete, should be:
|
|
36
|
+
|
|
37
|
+
```console
|
|
38
|
+
pip install notify-broadcast
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Seek information elsewhere about installing in a virtual environment
|
|
42
|
+
|
|
43
|
+
The install should pull in all dependencies. At present these are:
|
|
44
|
+
- colorlog: https://github.com/borntyping/python-colorlog/
|
|
45
|
+
- dasbus: https://github.com/dasbus-project/dasbus
|
|
46
|
+
- psutil: https://github.com/giampaolo/psutil
|
|
47
|
+
- PyGObject: https://pygobject.gnome.org
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
```console
|
|
52
|
+
# notify-broadcast --help
|
|
53
|
+
usage: notify-broadcast [--help] [-a APP_NAME] [-i ICON] [-t EXPIRE_TIME] [-h TYPE:NAME:VALUE] [-c TYPE] [-u {low,normal,critical}] [-A NAME=VALUE] [-r REPLACE_ID] [-p] [-e HINT] [--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
|
|
54
|
+
[--global-log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
|
|
55
|
+
summary body
|
|
56
|
+
|
|
57
|
+
Notify Broadcast Send a Broadcast DBUS notification to all users
|
|
58
|
+
|
|
59
|
+
positional arguments:
|
|
60
|
+
summary exam configuration file to be used for student collection - toml format
|
|
61
|
+
body exam solution file to be placed in collection directory
|
|
62
|
+
|
|
63
|
+
options:
|
|
64
|
+
--help show this help message and exit
|
|
65
|
+
-a APP_NAME, --app-name APP_NAME
|
|
66
|
+
Specifies the app name for the notification (default: )
|
|
67
|
+
-i ICON, --icon ICON Specifies an icon filename or stock icon to display. (default: dialog-information)
|
|
68
|
+
-t EXPIRE_TIME, --expire-time EXPIRE_TIME
|
|
69
|
+
The duration, in milliseconds, for the notification to appear on screen. Value of 0 means no expiry, while -1 uses the server default expiry. (default: -1)
|
|
70
|
+
-h TYPE:NAME:VALUE, --hint TYPE:NAME:VALUE
|
|
71
|
+
Notification hints to pass to server (e.g., int:urgency:2) (default: {})
|
|
72
|
+
-c TYPE, --category TYPE
|
|
73
|
+
Specifies the notification category. (default: {})
|
|
74
|
+
-u {low,normal,critical}, --urgency {low,normal,critical}
|
|
75
|
+
Specifies the urgency level (low, normal, critical). (default: {})
|
|
76
|
+
-A NAME=VALUE, --action NAME=VALUE
|
|
77
|
+
Specifies the actions to display to the user. Implies --wait to wait for user input. May be set multiple times. The NAME of the action is output to stdout. If NAME is not specified, the numerical index of the
|
|
78
|
+
option is used (starting with 1). (default: [])
|
|
79
|
+
-r REPLACE_ID, --replace-id REPLACE_ID
|
|
80
|
+
The ID of the notification to replace. (default: 0)
|
|
81
|
+
-p, --print-id Print the notification ID. (default: False)
|
|
82
|
+
-e HINT, --transient HINT
|
|
83
|
+
Show a transient notification. Transient notifications by-pass the server's persistence capability, if any. And so it won't be preserved until the user acknowledges it. (default: {})
|
|
84
|
+
--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
|
|
85
|
+
Set the logging level for the core application. (default: None)
|
|
86
|
+
--global-log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
|
|
87
|
+
Set the global logging level (includes third-party libraries). (default: None)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**NOTE:** For more support on these parameters, see `notify-send` man pages
|
|
91
|
+
|
|
92
|
+
## Examples
|
|
93
|
+
|
|
94
|
+
Some examples of how to use `notify-broadcast` are listed below. These are by no means exhaustive:
|
|
95
|
+
|
|
96
|
+
`notify-broadcast -a Daily-backup -t 0 -i dialog-information.png "Backup completed without error" ""`
|
|
97
|
+
|
|
98
|
+
Display a notification that the backup has completed without error:
|
|
99
|
+
- Display an information icon
|
|
100
|
+
- A timeout of 0 signifies that the notification will display until the user clears it
|
|
101
|
+
- Message contains a summary, but no body
|
|
102
|
+
|
|
103
|
+
`notify-broadcast -a Remote-rsync -t 6000 -i dialog-warning.png "Remote host not currently on the network" ""`
|
|
104
|
+
|
|
105
|
+
Display a notification that the remote host is not available:
|
|
106
|
+
- Display a warning icon
|
|
107
|
+
- A timeout of 6000 signifies that the notification will display for six seconds before clearing
|
|
108
|
+
- Message contains a summary, but no body
|
|
109
|
+
|
|
110
|
+
`notify-broadcast -a Daily-backup -t 0 -i dialog-error.png "Error running backup, please consult logs" ""`
|
|
111
|
+
|
|
112
|
+
Display a notification that the backup has completed without error:
|
|
113
|
+
- Display an error icon
|
|
114
|
+
- A timeout of 0 signifies that the notification will display until the user clears it
|
|
115
|
+
- Message contains a summary, but no body
|
|
116
|
+
|
|
117
|
+
`notify-broadcast -a "Disk Monitor" -h string:desktop-entry:org.kde.kinfocenter-i drive-harddisk "Disk" "SMART warning"`
|
|
118
|
+
|
|
119
|
+
Display a notification that a disk has encountered a SMART error:
|
|
120
|
+
- Display an disk icon
|
|
121
|
+
- No timeout signifies that the notification will display for the system default duration
|
|
122
|
+
|
|
123
|
+
## Comments
|
|
124
|
+
|
|
125
|
+
I am aware of a number of potential shortcomings that may impact broader distribution, as well as
|
|
126
|
+
some points about why things were coded this way, details listed below
|
|
127
|
+
|
|
128
|
+
### Finding DBUS Path via environment variables instead of `/run/user/{uid}/bus`
|
|
129
|
+
|
|
130
|
+
The code searches for the `DBUS_SESSION_BUS_ADDRESS` environment variable in a running program,
|
|
131
|
+
while many online examples suggest searching `/run/user/{uid}/bus`
|
|
132
|
+
|
|
133
|
+
My system (Gentoo) does not place the DBUS sockets in that location, so searching the environment
|
|
134
|
+
variables allows this to work regardless of the location of the socket.
|
|
135
|
+
|
|
136
|
+
### Program is hard-coded to KDE
|
|
137
|
+
|
|
138
|
+
As per the previous point, to search the environment variables, it means finding a running
|
|
139
|
+
application that has the environment variables set. This means that we need to know the application
|
|
140
|
+
name to search for.
|
|
141
|
+
|
|
142
|
+
Hence, the program currently looks for running instances of `kwin_wayland` or `kwin_x11` depending
|
|
143
|
+
on the current session type.
|
|
144
|
+
|
|
145
|
+
This works for me as I use KDE, I don't like Gnome or other environments.
|
|
146
|
+
|
|
147
|
+
However, I realise this means that this will not work everywhere. Some chat online suggests looking
|
|
148
|
+
for `dbus-launch`, however the environment for this process does not appear to contain the
|
|
149
|
+
`DBUS_SESSION_BUS_ADDRESS` environment variable.
|
|
150
|
+
|
|
151
|
+
I would like to support alternate desktops, but I do not have the will to test and develop a
|
|
152
|
+
solution. I am happy to take comments/suggestions on how to detect across multiple platforms.
|
|
153
|
+
|
|
154
|
+
### Some of the Options are Useless for Broadcast application
|
|
155
|
+
|
|
156
|
+
As a broadcast application, this really makes sense as a 1) send a message; and 2) do not wait for
|
|
157
|
+
replies scenario
|
|
158
|
+
|
|
159
|
+
1. `--print-id` makes no sense if sending multiple notifications
|
|
160
|
+
2. `--replace-id` make no sense if we are just blasting information to everyone
|
|
161
|
+
3. `--action` display buttons to the user and return values to the program. This is generally useless for this application
|
|
162
|
+
|
|
163
|
+
### Running as non-root
|
|
164
|
+
|
|
165
|
+
A non-root user will not be able to post notifications to other users in either case. The
|
|
166
|
+
application currently just prints warnings about being unable to access the environment and
|
|
167
|
+
does nothing else.
|
|
168
|
+
|
|
169
|
+
It might be better to abort early if the user does not have permissions, but a system could
|
|
170
|
+
allow multiple users the requisite permissions, so it is hard to manage this properly.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# notify-broadcast
|
|
2
|
+
|
|
3
|
+
This program was developed because I run a number of **cron** jobs as root that I would like to be
|
|
4
|
+
able to send notifications to the GUI User
|
|
5
|
+
|
|
6
|
+
`notify-send` only functions if run by the same user running the graphical session.
|
|
7
|
+
|
|
8
|
+
I created `notify-broadcast` to effectively tack the same parameters as `notify-send` but that
|
|
9
|
+
could be executed by the `root` user.
|
|
10
|
+
|
|
11
|
+
The application searches for all active DBUS Notification sessions, and sends the notification to
|
|
12
|
+
all currently attached users.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
When complete, should be:
|
|
17
|
+
|
|
18
|
+
```console
|
|
19
|
+
pip install notify-broadcast
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Seek information elsewhere about installing in a virtual environment
|
|
23
|
+
|
|
24
|
+
The install should pull in all dependencies. At present these are:
|
|
25
|
+
- colorlog: https://github.com/borntyping/python-colorlog/
|
|
26
|
+
- dasbus: https://github.com/dasbus-project/dasbus
|
|
27
|
+
- psutil: https://github.com/giampaolo/psutil
|
|
28
|
+
- PyGObject: https://pygobject.gnome.org
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```console
|
|
33
|
+
# notify-broadcast --help
|
|
34
|
+
usage: notify-broadcast [--help] [-a APP_NAME] [-i ICON] [-t EXPIRE_TIME] [-h TYPE:NAME:VALUE] [-c TYPE] [-u {low,normal,critical}] [-A NAME=VALUE] [-r REPLACE_ID] [-p] [-e HINT] [--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
|
|
35
|
+
[--global-log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
|
|
36
|
+
summary body
|
|
37
|
+
|
|
38
|
+
Notify Broadcast Send a Broadcast DBUS notification to all users
|
|
39
|
+
|
|
40
|
+
positional arguments:
|
|
41
|
+
summary exam configuration file to be used for student collection - toml format
|
|
42
|
+
body exam solution file to be placed in collection directory
|
|
43
|
+
|
|
44
|
+
options:
|
|
45
|
+
--help show this help message and exit
|
|
46
|
+
-a APP_NAME, --app-name APP_NAME
|
|
47
|
+
Specifies the app name for the notification (default: )
|
|
48
|
+
-i ICON, --icon ICON Specifies an icon filename or stock icon to display. (default: dialog-information)
|
|
49
|
+
-t EXPIRE_TIME, --expire-time EXPIRE_TIME
|
|
50
|
+
The duration, in milliseconds, for the notification to appear on screen. Value of 0 means no expiry, while -1 uses the server default expiry. (default: -1)
|
|
51
|
+
-h TYPE:NAME:VALUE, --hint TYPE:NAME:VALUE
|
|
52
|
+
Notification hints to pass to server (e.g., int:urgency:2) (default: {})
|
|
53
|
+
-c TYPE, --category TYPE
|
|
54
|
+
Specifies the notification category. (default: {})
|
|
55
|
+
-u {low,normal,critical}, --urgency {low,normal,critical}
|
|
56
|
+
Specifies the urgency level (low, normal, critical). (default: {})
|
|
57
|
+
-A NAME=VALUE, --action NAME=VALUE
|
|
58
|
+
Specifies the actions to display to the user. Implies --wait to wait for user input. May be set multiple times. The NAME of the action is output to stdout. If NAME is not specified, the numerical index of the
|
|
59
|
+
option is used (starting with 1). (default: [])
|
|
60
|
+
-r REPLACE_ID, --replace-id REPLACE_ID
|
|
61
|
+
The ID of the notification to replace. (default: 0)
|
|
62
|
+
-p, --print-id Print the notification ID. (default: False)
|
|
63
|
+
-e HINT, --transient HINT
|
|
64
|
+
Show a transient notification. Transient notifications by-pass the server's persistence capability, if any. And so it won't be preserved until the user acknowledges it. (default: {})
|
|
65
|
+
--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
|
|
66
|
+
Set the logging level for the core application. (default: None)
|
|
67
|
+
--global-log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
|
|
68
|
+
Set the global logging level (includes third-party libraries). (default: None)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**NOTE:** For more support on these parameters, see `notify-send` man pages
|
|
72
|
+
|
|
73
|
+
## Examples
|
|
74
|
+
|
|
75
|
+
Some examples of how to use `notify-broadcast` are listed below. These are by no means exhaustive:
|
|
76
|
+
|
|
77
|
+
`notify-broadcast -a Daily-backup -t 0 -i dialog-information.png "Backup completed without error" ""`
|
|
78
|
+
|
|
79
|
+
Display a notification that the backup has completed without error:
|
|
80
|
+
- Display an information icon
|
|
81
|
+
- A timeout of 0 signifies that the notification will display until the user clears it
|
|
82
|
+
- Message contains a summary, but no body
|
|
83
|
+
|
|
84
|
+
`notify-broadcast -a Remote-rsync -t 6000 -i dialog-warning.png "Remote host not currently on the network" ""`
|
|
85
|
+
|
|
86
|
+
Display a notification that the remote host is not available:
|
|
87
|
+
- Display a warning icon
|
|
88
|
+
- A timeout of 6000 signifies that the notification will display for six seconds before clearing
|
|
89
|
+
- Message contains a summary, but no body
|
|
90
|
+
|
|
91
|
+
`notify-broadcast -a Daily-backup -t 0 -i dialog-error.png "Error running backup, please consult logs" ""`
|
|
92
|
+
|
|
93
|
+
Display a notification that the backup has completed without error:
|
|
94
|
+
- Display an error icon
|
|
95
|
+
- A timeout of 0 signifies that the notification will display until the user clears it
|
|
96
|
+
- Message contains a summary, but no body
|
|
97
|
+
|
|
98
|
+
`notify-broadcast -a "Disk Monitor" -h string:desktop-entry:org.kde.kinfocenter-i drive-harddisk "Disk" "SMART warning"`
|
|
99
|
+
|
|
100
|
+
Display a notification that a disk has encountered a SMART error:
|
|
101
|
+
- Display an disk icon
|
|
102
|
+
- No timeout signifies that the notification will display for the system default duration
|
|
103
|
+
|
|
104
|
+
## Comments
|
|
105
|
+
|
|
106
|
+
I am aware of a number of potential shortcomings that may impact broader distribution, as well as
|
|
107
|
+
some points about why things were coded this way, details listed below
|
|
108
|
+
|
|
109
|
+
### Finding DBUS Path via environment variables instead of `/run/user/{uid}/bus`
|
|
110
|
+
|
|
111
|
+
The code searches for the `DBUS_SESSION_BUS_ADDRESS` environment variable in a running program,
|
|
112
|
+
while many online examples suggest searching `/run/user/{uid}/bus`
|
|
113
|
+
|
|
114
|
+
My system (Gentoo) does not place the DBUS sockets in that location, so searching the environment
|
|
115
|
+
variables allows this to work regardless of the location of the socket.
|
|
116
|
+
|
|
117
|
+
### Program is hard-coded to KDE
|
|
118
|
+
|
|
119
|
+
As per the previous point, to search the environment variables, it means finding a running
|
|
120
|
+
application that has the environment variables set. This means that we need to know the application
|
|
121
|
+
name to search for.
|
|
122
|
+
|
|
123
|
+
Hence, the program currently looks for running instances of `kwin_wayland` or `kwin_x11` depending
|
|
124
|
+
on the current session type.
|
|
125
|
+
|
|
126
|
+
This works for me as I use KDE, I don't like Gnome or other environments.
|
|
127
|
+
|
|
128
|
+
However, I realise this means that this will not work everywhere. Some chat online suggests looking
|
|
129
|
+
for `dbus-launch`, however the environment for this process does not appear to contain the
|
|
130
|
+
`DBUS_SESSION_BUS_ADDRESS` environment variable.
|
|
131
|
+
|
|
132
|
+
I would like to support alternate desktops, but I do not have the will to test and develop a
|
|
133
|
+
solution. I am happy to take comments/suggestions on how to detect across multiple platforms.
|
|
134
|
+
|
|
135
|
+
### Some of the Options are Useless for Broadcast application
|
|
136
|
+
|
|
137
|
+
As a broadcast application, this really makes sense as a 1) send a message; and 2) do not wait for
|
|
138
|
+
replies scenario
|
|
139
|
+
|
|
140
|
+
1. `--print-id` makes no sense if sending multiple notifications
|
|
141
|
+
2. `--replace-id` make no sense if we are just blasting information to everyone
|
|
142
|
+
3. `--action` display buttons to the user and return values to the program. This is generally useless for this application
|
|
143
|
+
|
|
144
|
+
### Running as non-root
|
|
145
|
+
|
|
146
|
+
A non-root user will not be able to post notifications to other users in either case. The
|
|
147
|
+
application currently just prints warnings about being unable to access the environment and
|
|
148
|
+
does nothing else.
|
|
149
|
+
|
|
150
|
+
It might be better to abort early if the user does not have permissions, but a system could
|
|
151
|
+
allow multiple users the requisite permissions, so it is hard to manage this properly.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module implements the DBUSSessionManager class which finds all user DBUS sessions and maps their NotificationProxy to allow broadcast of
|
|
3
|
+
notifications to all users with a DBUS session
|
|
4
|
+
"""
|
|
5
|
+
# Import System Libraries
|
|
6
|
+
import os
|
|
7
|
+
import psutil
|
|
8
|
+
import re
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
# Import dasbus library modules
|
|
12
|
+
from dasbus.connection import SystemMessageBus, AddressedMessageBus
|
|
13
|
+
from dasbus.client.proxy import InterfaceProxy
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DBUSSessionManager:
|
|
17
|
+
class DBUSSessionNotFound(Exception):
|
|
18
|
+
""" This exception is raised when we cannot find the DBUS session for the nominated user and session type """
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
def __init__(self, log_level: str):
|
|
22
|
+
"""
|
|
23
|
+
Initialise class and internal variables:
|
|
24
|
+
- Store UID of current user so we can change EUID during running
|
|
25
|
+
- Populate __users mapping uid to a DBUS InterfaceProxy that we can use to notify that user
|
|
26
|
+
|
|
27
|
+
:param log_level: Level to set for the internal class logger
|
|
28
|
+
"""
|
|
29
|
+
self.__log = logging.getLogger('DBUSSessionManager')
|
|
30
|
+
if log_level is not None:
|
|
31
|
+
self.__log.setLevel(log_level)
|
|
32
|
+
else:
|
|
33
|
+
self.__log.debug('using default log level')
|
|
34
|
+
self.__log.info(f'Constructing Class')
|
|
35
|
+
|
|
36
|
+
self.__app_uid = os.geteuid()
|
|
37
|
+
self.__log.debug(f'Storing UID of user running application: {self.__app_uid}')
|
|
38
|
+
|
|
39
|
+
self.__users: dict[int, InterfaceProxy] = {}
|
|
40
|
+
self.__find_users()
|
|
41
|
+
self.__log.debug(f'User DBUS sessions: {self.__users}')
|
|
42
|
+
|
|
43
|
+
def __get_notify_proxy(self, uid: int, name: str) -> InterfaceProxy:
|
|
44
|
+
"""
|
|
45
|
+
Return a DBUS Notification Proxy for the given user, running the graphical session from the provided process name
|
|
46
|
+
|
|
47
|
+
:param uid: User ID for a given session
|
|
48
|
+
:param name: Name of the program to search for
|
|
49
|
+
|
|
50
|
+
:return: The DBUS Notification Proxy to send notifications to the user via the attached session
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
# 1) Find the process ID for program "name" owned by self.__uid
|
|
54
|
+
self.__log.debug(f'Searching for process "{name}"')
|
|
55
|
+
manager_pid = [p.pid for p in psutil.process_iter(['name', 'pid', 'uids']) if p.info['name'] == name and p.info['uids'].real == uid][0]
|
|
56
|
+
self.__log.debug(f'Found Process ID: {manager_pid}')
|
|
57
|
+
|
|
58
|
+
# 2) Search the environment for manager_pid, and extract the DBUS session address
|
|
59
|
+
self.__log.debug('Reading Process environment')
|
|
60
|
+
with open(f'/proc/{manager_pid}/environ', 'rb') as f:
|
|
61
|
+
env_bytes = f.read()
|
|
62
|
+
|
|
63
|
+
# Regex pattern to locate DBUS addresses inside environment blocks
|
|
64
|
+
self.__log.debug('Searching for DBUS Session Bus Address')
|
|
65
|
+
dbus_pattern = re.compile(b"DBUS_SESSION_BUS_ADDRESS=(unix:path=[^\\x00]+)")
|
|
66
|
+
dbus_match = dbus_pattern.search(env_bytes)
|
|
67
|
+
|
|
68
|
+
if not dbus_match:
|
|
69
|
+
raise DBUSSessionManager.DBUSSessionNotFound(f'No DBUS Session Address found in environment for process ID ({manager_pid})')
|
|
70
|
+
|
|
71
|
+
dbus_path = dbus_match.group(1).decode('utf-8')
|
|
72
|
+
self.__log.debug(f'DBUS Session Path: {dbus_path}')
|
|
73
|
+
|
|
74
|
+
# 3) Create and return Notifications Proxy for the nominated path
|
|
75
|
+
self.__log.debug(f'Creating Notification Proxy')
|
|
76
|
+
return AddressedMessageBus(dbus_path).get_proxy("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
|
|
77
|
+
|
|
78
|
+
except IndexError:
|
|
79
|
+
raise DBUSSessionManager.DBUSSessionNotFound(f'Unable to locate process "{name}"')
|
|
80
|
+
|
|
81
|
+
except (IOError, PermissionError):
|
|
82
|
+
raise DBUSSessionManager.DBUSSessionNotFound(f'Unable to access environment variables for process ID ({manager_pid}) - Need to run as root or user with appropriate permissions')
|
|
83
|
+
|
|
84
|
+
def __find_users(self):
|
|
85
|
+
"""
|
|
86
|
+
Find all users with a login session, locate if they have a DBUS session attached to it, then populate __users with that information
|
|
87
|
+
"""
|
|
88
|
+
self.__log.info('Finding DBUS sessions for all users')
|
|
89
|
+
bus = SystemMessageBus()
|
|
90
|
+
login_bus = bus.get_proxy("org.freedesktop.login1", "/org/freedesktop/login1")
|
|
91
|
+
|
|
92
|
+
for session_id, uid, username, seat, path in login_bus.ListSessions():
|
|
93
|
+
# Get session details for this login
|
|
94
|
+
session_proxy = bus.get_proxy("org.freedesktop.login1", path)
|
|
95
|
+
if session_proxy.Type in ['wayland', 'x11']:
|
|
96
|
+
try:
|
|
97
|
+
self.__log.info(f'Login session {session_id}: User [{username}({uid})] on seat[{seat}] is a {session_proxy.Type} session')
|
|
98
|
+
self.__users[uid] = self.__get_notify_proxy(uid, f'kwin_{session_proxy.Type}')
|
|
99
|
+
except DBUSSessionManager.DBUSSessionNotFound as e:
|
|
100
|
+
self.__log.warning(e)
|
|
101
|
+
else:
|
|
102
|
+
self.__log.info(f'Non-graphical login session {session_id}: User [{username}({uid})]')
|
|
103
|
+
|
|
104
|
+
def broadcast_notification(self, notification: tuple, print_id: bool):
|
|
105
|
+
"""
|
|
106
|
+
Broadcast a notification to all users using the DBUS Notification Proxies as listed in __users
|
|
107
|
+
|
|
108
|
+
:param notification: Tuple containing parameters to pass to the Notify() method of the Notification Proxy
|
|
109
|
+
:param print_id: Boolean indicating whether the message ID should be printed to screen
|
|
110
|
+
"""
|
|
111
|
+
self.__log.info(f'Sending notification ({notification}) to all users')
|
|
112
|
+
for uid, notify_proxy in self.__users.items():
|
|
113
|
+
self.__log.debug(f'Changing UID to {uid} to send notification')
|
|
114
|
+
os.seteuid(uid)
|
|
115
|
+
|
|
116
|
+
self.__log.info(f'User({uid}): Sending Notification')
|
|
117
|
+
notify_id = notify_proxy.Notify(*notification)
|
|
118
|
+
if print_id: print(notify_id)
|
|
119
|
+
|
|
120
|
+
self.__log.debug(f'Reverting UID to {self.__app_uid}')
|
|
121
|
+
os.seteuid(self.__app_uid)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module implements the notify-broadcast application. The application is installed as a script which runs the notify_broadcast()
|
|
3
|
+
function contained within this file
|
|
4
|
+
"""
|
|
5
|
+
# Import System Libraries
|
|
6
|
+
import colorlog
|
|
7
|
+
|
|
8
|
+
# Import Package Modules
|
|
9
|
+
from notify_broadcast import NotifyBroadcastArgumentParser
|
|
10
|
+
from notify_broadcast import DBUSSessionManager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def notify_broadcast():
|
|
14
|
+
"""
|
|
15
|
+
This is the main application which will be called directly when running the installed notify-broadcast application
|
|
16
|
+
"""
|
|
17
|
+
# Create the command line argument parser and parse all arguments
|
|
18
|
+
parser = NotifyBroadcastArgumentParser()
|
|
19
|
+
parser.parse_args()
|
|
20
|
+
|
|
21
|
+
# Set the default logging format
|
|
22
|
+
colorlog.basicConfig(format='%(log_color)s[%(levelname)-8s] %(reset)s%(name)s.%(funcName)s() - %(log_color)s%(message)s%(reset)s',
|
|
23
|
+
log_colors={'DEBUG': 'cyan', 'INFO': 'green', 'WARNING': 'yellow', 'ERROR': 'red', 'CRITICAL': 'red,bg_white'}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Create the DBUSSessionManager, then broadcast the notification to all users
|
|
27
|
+
dbus_manager = DBUSSessionManager(parser.log_level)
|
|
28
|
+
dbus_manager.broadcast_notification(parser.notification, parser.print_id)
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module implements the NotifyBroadcastArgumentParser class which provides the command-line argument parser for the notify-broadcast command
|
|
3
|
+
"""
|
|
4
|
+
# Import System Libraries
|
|
5
|
+
import argparse
|
|
6
|
+
import logging
|
|
7
|
+
from gi.repository import GLib
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NotifyBroadcastArgumentParser(argparse.ArgumentParser):
|
|
11
|
+
"""
|
|
12
|
+
Extends ArgumentParser to provide parameter parsing for the notify-broadcast command.
|
|
13
|
+
|
|
14
|
+
Installs arguments similar to notify-send
|
|
15
|
+
"""
|
|
16
|
+
class NotifyHints(argparse.Action):
|
|
17
|
+
"""
|
|
18
|
+
Process one or more command line options in form TYPE:NAME:VALUE and appends a dictionary value mapping NAME -> VALUE cast to the
|
|
19
|
+
specified TYPE
|
|
20
|
+
|
|
21
|
+
NOTE: This class is designed to be used for validating a formatted parameter in the context of command-line argument parsing.
|
|
22
|
+
When an instance of this class is called with a string, it checks if the string conforms to the appropriate format specification.
|
|
23
|
+
If the provided string is in error, it raises an error suitable for argument parsing utilities.
|
|
24
|
+
"""
|
|
25
|
+
def __init__(self, option_strings, dest, nargs=None, **kwargs):
|
|
26
|
+
"""Initialise argument to empty dictionary"""
|
|
27
|
+
kwargs.setdefault('default', {})
|
|
28
|
+
super().__init__(option_strings, dest, **kwargs)
|
|
29
|
+
|
|
30
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
31
|
+
"""
|
|
32
|
+
Parse command line option as TYPE:NAME:VALUE and append to dictionary mapping name-->value(cast to type)
|
|
33
|
+
|
|
34
|
+
:param parser: ArgParse instance, used to send errors back to the parser.
|
|
35
|
+
:param namespace: Current parsed parameters.
|
|
36
|
+
:param values: Current option being parsed.
|
|
37
|
+
:param option_string: Actual option (e.g. --hint)
|
|
38
|
+
"""
|
|
39
|
+
# Get the pre-existing parameter dictionary
|
|
40
|
+
current_values = getattr(namespace, self.dest)
|
|
41
|
+
|
|
42
|
+
# Split parameter to TYPE:NAME:VALUE tuple and add to dictionary
|
|
43
|
+
try:
|
|
44
|
+
parts = values.split(':', 2)
|
|
45
|
+
if len(parts) != 3: raise ValueError(f'Parameter should contain three values in format TYPE:NAME:VALUE, only {len(parts)} values provided')
|
|
46
|
+
|
|
47
|
+
raw_type, name, raw_value = parts
|
|
48
|
+
|
|
49
|
+
match raw_type.lower():
|
|
50
|
+
case'int':
|
|
51
|
+
variant_val = GLib.Variant('i', int(raw_value))
|
|
52
|
+
case 'double':
|
|
53
|
+
variant_val = GLib.Variant('d', float(raw_value))
|
|
54
|
+
case 'boolean' | 'bool':
|
|
55
|
+
variant_val = GLib.Variant('b', raw_value.lower() in ('true', '1', 'yes'))
|
|
56
|
+
case 'byte':
|
|
57
|
+
variant_val = GLib.Variant('y', int(raw_value))
|
|
58
|
+
case _: # Default to string
|
|
59
|
+
variant_val = GLib.Variant('s', str(raw_value))
|
|
60
|
+
|
|
61
|
+
current_values[name] = variant_val
|
|
62
|
+
|
|
63
|
+
except ValueError as e:
|
|
64
|
+
parser.error(f'Invalid parameter "{option_string} {values}" is invalid: {e}')
|
|
65
|
+
|
|
66
|
+
# Save dictionary back to namespace
|
|
67
|
+
setattr(namespace, self.dest, current_values)
|
|
68
|
+
|
|
69
|
+
class NotifyUrgency(argparse.Action):
|
|
70
|
+
"""
|
|
71
|
+
Process command line options as human-readable "urgency" values and map directly into hints directory as corresponding byte values
|
|
72
|
+
under the name "urgency"
|
|
73
|
+
|
|
74
|
+
NOTE: String values should be checked by argparse, so no errors should be raised here, merely convert string->byte and append to
|
|
75
|
+
dictionary
|
|
76
|
+
"""
|
|
77
|
+
def __init__(self, option_strings, dest, **kwargs):
|
|
78
|
+
"""Initialise argument to empty dictionary"""
|
|
79
|
+
kwargs.setdefault('default', {})
|
|
80
|
+
super().__init__(option_strings, dest, **kwargs)
|
|
81
|
+
|
|
82
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
83
|
+
"""
|
|
84
|
+
Parse command line option as low, normal, or critical, and append to dictionary with corresponding byte value under key name 'urgency'
|
|
85
|
+
|
|
86
|
+
:param parser: ArgParse instance, used to send errors back to the parser.
|
|
87
|
+
:param namespace: Current parsed parameters.
|
|
88
|
+
:param values: Current option being parsed.
|
|
89
|
+
:param option_string: Actual option (e.g. --urgency)
|
|
90
|
+
"""
|
|
91
|
+
# Get the pre-existing parameter dictionary
|
|
92
|
+
current_values = getattr(namespace, self.dest)
|
|
93
|
+
|
|
94
|
+
urgency_mapping = {'low': 0, 'normal': 1, 'critical': 2}
|
|
95
|
+
|
|
96
|
+
# Inject the key directly into the dictionary as a D-Bus byte variant
|
|
97
|
+
current_values['urgency'] = GLib.Variant('y', urgency_mapping[values])
|
|
98
|
+
|
|
99
|
+
# Save dictionary back to namespace
|
|
100
|
+
setattr(namespace, self.dest, current_values)
|
|
101
|
+
|
|
102
|
+
class NotifyCategory(argparse.Action):
|
|
103
|
+
"""
|
|
104
|
+
Process command line options as string "category" value and map directly into hints directory as corresponding string under the
|
|
105
|
+
name "category"
|
|
106
|
+
|
|
107
|
+
NOTE: String value provided already, so no errors should be raised here, merely append string to dictionary
|
|
108
|
+
"""
|
|
109
|
+
def __init__(self, option_strings, dest, **kwargs):
|
|
110
|
+
"""Initialise argument to empty dictionary"""
|
|
111
|
+
kwargs.setdefault('default', {})
|
|
112
|
+
super().__init__(option_strings, dest, **kwargs)
|
|
113
|
+
|
|
114
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
115
|
+
"""
|
|
116
|
+
Parse command line option as string, and append to dictionary with corresponding string value under key name 'category'
|
|
117
|
+
|
|
118
|
+
:param parser: ArgParse instance, used to send errors back to the parser.
|
|
119
|
+
:param namespace: Current parsed parameters.
|
|
120
|
+
:param values: Current option being parsed.
|
|
121
|
+
:param option_string: Actual option (e.g. --option)
|
|
122
|
+
"""
|
|
123
|
+
# Get the pre-existing parameter dictionary
|
|
124
|
+
current_values = getattr(namespace, self.dest)
|
|
125
|
+
|
|
126
|
+
# Inject the key directly into the dictionary as a D-Bus string variant
|
|
127
|
+
current_values['category'] = GLib.Variant('s', str(values))
|
|
128
|
+
|
|
129
|
+
# Save dictionary back to namespace
|
|
130
|
+
setattr(namespace, self.dest, current_values)
|
|
131
|
+
|
|
132
|
+
class NotifyTransient(argparse.Action):
|
|
133
|
+
"""
|
|
134
|
+
Process command line flag to activate transient feature. Map directory into hints dictionary as "transient"->True
|
|
135
|
+
|
|
136
|
+
NOTE: Simple parameter flag, so no errors should be raised here, merely append True to dictionary
|
|
137
|
+
"""
|
|
138
|
+
def __init__(self, option_strings, dest, **kwargs):
|
|
139
|
+
"""Initialise argument to empty dictionary"""
|
|
140
|
+
kwargs.setdefault('default', {})
|
|
141
|
+
super().__init__(option_strings, dest, **kwargs)
|
|
142
|
+
|
|
143
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
144
|
+
"""
|
|
145
|
+
Transient should be a flag on the command line, so this action is called to set flag to True. Append True value to dictionary
|
|
146
|
+
under key name 'transient'
|
|
147
|
+
|
|
148
|
+
:param parser: ArgParse instance, used to send errors back to the parser.
|
|
149
|
+
:param namespace: Current parsed parameters.
|
|
150
|
+
:param values: Current option being parsed.
|
|
151
|
+
:param option_string: Actual option (e.g. --option)
|
|
152
|
+
"""
|
|
153
|
+
# Get the pre-existing parameter dictionary
|
|
154
|
+
current_values = getattr(namespace, self.dest)
|
|
155
|
+
|
|
156
|
+
# Inject the key directly into the dictionary as a D-Bus byte variant
|
|
157
|
+
current_values['transient'] = GLib.Variant('b', True)
|
|
158
|
+
|
|
159
|
+
# Save dictionary back to namespace
|
|
160
|
+
setattr(namespace, self.dest, current_values)
|
|
161
|
+
|
|
162
|
+
class NotifyAction(argparse.Action):
|
|
163
|
+
"""
|
|
164
|
+
Process one or more command line options in form KEY=LABEL or KEY:LABEL and append them to a flat "actions" list
|
|
165
|
+
|
|
166
|
+
NOTE: This class is designed to be used for validating a formatted parameter in the context of command-line argument parsing.
|
|
167
|
+
When an instance of this class is called with a string, it checks if the string conforms to the appropriate format specification.
|
|
168
|
+
If the provided string is in error, it raises an error suitable for argument parsing utilities.
|
|
169
|
+
"""
|
|
170
|
+
def __init__(self, option_strings, dest, nargs=None, **kwargs):
|
|
171
|
+
"""Initialise argument to empty list"""
|
|
172
|
+
kwargs.setdefault('default', [])
|
|
173
|
+
super().__init__(option_strings, dest, **kwargs)
|
|
174
|
+
|
|
175
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
176
|
+
"""
|
|
177
|
+
Parse command line option as KEY=LABEL or KEY:LABEL and extend the existing list with two new entries [KEY, LABEL]
|
|
178
|
+
|
|
179
|
+
:param parser: ArgParse instance, used to send errors back to the parser.
|
|
180
|
+
:param namespace: Current parsed parameters.
|
|
181
|
+
:param values: Current option being parsed.
|
|
182
|
+
:param option_string: Actual option (e.g. --hint)
|
|
183
|
+
"""
|
|
184
|
+
# Get the pre-existing parameter list
|
|
185
|
+
actions_list = getattr(namespace, self.dest)
|
|
186
|
+
|
|
187
|
+
# Allow splitting by either ':' or ',' to match common notify-send clones
|
|
188
|
+
delimiter = '=' if '=' in values else ':'
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
parts = values.split(delimiter, 1)
|
|
192
|
+
if len(parts) != 2: raise ValueError(f'Must be in "KEY=LABEL" or "KEY:LABEL" format')
|
|
193
|
+
|
|
194
|
+
key, label = parts[0].strip(), parts[1].strip()
|
|
195
|
+
if not key: raise ValueError('Key cannot be an empty string')
|
|
196
|
+
if not label: raise ValueError('Label cannot be an empty string')
|
|
197
|
+
|
|
198
|
+
# D-Bus expects a flat list: [key1, label1, key2, label2]
|
|
199
|
+
actions_list.extend([key, label])
|
|
200
|
+
|
|
201
|
+
except Exception as e:
|
|
202
|
+
parser.error(f'Invalid parameter "{option_string} {values}" is invalid: {e}')
|
|
203
|
+
|
|
204
|
+
# Save dictionary back to namespace
|
|
205
|
+
setattr(namespace, self.dest, actions_list)
|
|
206
|
+
|
|
207
|
+
class SetGlobalLogLevel(argparse.Action):
|
|
208
|
+
"""
|
|
209
|
+
Process the global-log-level parameter and set the default system log level as an action
|
|
210
|
+
|
|
211
|
+
NOTE: This class is designed to be used for validating a formatted parameter in the context of command-line argument parsing.
|
|
212
|
+
When an instance of this class is called with a string, it checks if the string conforms to the appropriate format specification.
|
|
213
|
+
If the provided string is in error, it raises an error suitable for argument parsing utilities.
|
|
214
|
+
"""
|
|
215
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
216
|
+
"""
|
|
217
|
+
Parse command line option as DEBUG, INFO, WARNING, ERROR, or CRITICAL, then set the logging log-level to match
|
|
218
|
+
|
|
219
|
+
:param parser: ArgParse instance, used to send errors back to the parser.
|
|
220
|
+
:param namespace: Current parsed parameters.
|
|
221
|
+
:param values: Current option being parsed.
|
|
222
|
+
:param option_string: Actual option (e.g. --urgency)
|
|
223
|
+
"""
|
|
224
|
+
logging.basicConfig(level=values)
|
|
225
|
+
|
|
226
|
+
# Save the value to the namespace for standard argparse behavior
|
|
227
|
+
setattr(namespace, self.dest, values)
|
|
228
|
+
|
|
229
|
+
# NotifyBroadcastArgumentParser member methods begin here
|
|
230
|
+
def __init__(self, *args, **kwargs):
|
|
231
|
+
"""
|
|
232
|
+
Overloaded Constructor
|
|
233
|
+
|
|
234
|
+
Set default description and argparse parameters before calling superclass constructor
|
|
235
|
+
|
|
236
|
+
Then call add_arguments() to add the required command-line parameters
|
|
237
|
+
"""
|
|
238
|
+
# Default options
|
|
239
|
+
kwargs.setdefault('description', 'Notify Broadcast\n\nSend a Broadcast DBUS notification to all users')
|
|
240
|
+
kwargs.setdefault('formatter_class', argparse.ArgumentDefaultsHelpFormatter)
|
|
241
|
+
kwargs.setdefault('allow_abbrev', False)
|
|
242
|
+
kwargs.setdefault('conflict_handler', 'resolve')
|
|
243
|
+
|
|
244
|
+
# Initialise the parent class
|
|
245
|
+
super().__init__(*args, **kwargs)
|
|
246
|
+
|
|
247
|
+
# Add parameter options
|
|
248
|
+
self.__add_arguments()
|
|
249
|
+
|
|
250
|
+
# Create variable to store generated notification from parsed arguments
|
|
251
|
+
self.__notification = None
|
|
252
|
+
self.__print_id = None
|
|
253
|
+
self.__log_level = None
|
|
254
|
+
|
|
255
|
+
def __add_arguments(self):
|
|
256
|
+
""" Add command line arguments to the argument parser """
|
|
257
|
+
self.add_argument('-a', '--app-name', type=str, default='', help='Specifies the app name for the notification')
|
|
258
|
+
self.add_argument('-i', '--icon', type=str, default='dialog-information', help='Specifies an icon filename or stock icon to display.')
|
|
259
|
+
self.add_argument('-t', '--expire-time', type=int, default=-1, help='The duration, in milliseconds, for the notification to appear on screen. Value of 0 means no expiry, while -1 uses the server default expiry.')
|
|
260
|
+
self.add_argument('-h', '--hint', action=NotifyBroadcastArgumentParser.NotifyHints, metavar='TYPE:NAME:VALUE', help='Notification hints to pass to server (e.g., int:urgency:2)')
|
|
261
|
+
self.add_argument('-c', '--category', action=NotifyBroadcastArgumentParser.NotifyCategory, metavar='TYPE', dest='hint', help='Specifies the notification category.')
|
|
262
|
+
self.add_argument('-u', '--urgency', action=NotifyBroadcastArgumentParser.NotifyUrgency, choices=['low', 'normal', 'critical'], dest='hint', help='Specifies the urgency level (low, normal, critical).')
|
|
263
|
+
|
|
264
|
+
self.add_argument('-A', '--action', action=NotifyBroadcastArgumentParser.NotifyAction, metavar='NAME=VALUE', help='Specifies the actions to display to the user. Implies --wait to wait for user input. May be set multiple times. The NAME of the action is output to stdout. If NAME is not specified, the numerical index of the option is used (starting with 1).')
|
|
265
|
+
|
|
266
|
+
self.add_argument('-r', '--replace-id', type=int, default=0, help='The ID of the notification to replace.')
|
|
267
|
+
self.add_argument('-p', '--print-id', action='store_true', help='Print the notification ID.')
|
|
268
|
+
|
|
269
|
+
self.add_argument('-e', '--transient', action=NotifyBroadcastArgumentParser.NotifyTransient, dest='hint', help='Show a transient notification. Transient notifications by-pass the server\'s persistence capability, if any. And so it won\'t be preserved until the user acknowledges it.')
|
|
270
|
+
|
|
271
|
+
self.add_argument('summary', type=str, help='exam configuration file to be used for student collection - toml format')
|
|
272
|
+
self.add_argument('body', type=str, help='exam solution file to be placed in collection directory')
|
|
273
|
+
|
|
274
|
+
self.add_argument("--log-level", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], help="Set the logging level for the core application.")
|
|
275
|
+
|
|
276
|
+
self.add_argument("--global-log-level", action=NotifyBroadcastArgumentParser.SetGlobalLogLevel, choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], help="Set the global logging level (includes third-party libraries).")
|
|
277
|
+
|
|
278
|
+
def parse_args(self, args=None, namespace=None):
|
|
279
|
+
"""
|
|
280
|
+
Overload parse_args method. Constructs the notification details (stored in __notification) from the parsed command line arguments.
|
|
281
|
+
- Call the base class method to parse the arguments and store in temporary variable
|
|
282
|
+
- Extract parameters into a tuple (ready to pass to Notify()) and store in __notification
|
|
283
|
+
- Extract other variables to make directly accessible via properties
|
|
284
|
+
|
|
285
|
+
:return: Return the parsed arguments Namespace as required by parse_args()
|
|
286
|
+
"""
|
|
287
|
+
# Call base class method to parse arguments and store in internal variable
|
|
288
|
+
parsed = super().parse_args(args=args, namespace=namespace)
|
|
289
|
+
|
|
290
|
+
self.__notification = (parsed.app_name, parsed.replace_id, parsed.icon, parsed.summary, parsed.body, parsed.action, parsed.hint, parsed.expire_time)
|
|
291
|
+
self.__print_id = parsed.print_id
|
|
292
|
+
self.__log_level = parsed.log_level
|
|
293
|
+
|
|
294
|
+
return parsed
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def notification(self) -> tuple:
|
|
298
|
+
"""
|
|
299
|
+
Retrieves the notification constructed from the command line arguments as a class property.
|
|
300
|
+
|
|
301
|
+
:return: The value stored in internal variable __notification
|
|
302
|
+
"""
|
|
303
|
+
return self.__notification
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def print_id(self) -> bool:
|
|
307
|
+
"""
|
|
308
|
+
Retrieves the print_id from the command line arguments as a class property.
|
|
309
|
+
|
|
310
|
+
:return: The value stored in internal variable __print_id
|
|
311
|
+
"""
|
|
312
|
+
return self.__print_id
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def log_level(self) -> str:
|
|
316
|
+
"""
|
|
317
|
+
Retrieves the log_level from the command line arguments as a class property.
|
|
318
|
+
|
|
319
|
+
:return: The value stored in internal variable __log_level
|
|
320
|
+
"""
|
|
321
|
+
return self.__log_level
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: notify-broadcast
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: Broadcast version of notify-send to allow root processes to send a notification to all users with an active DBUS session
|
|
5
|
+
Author-email: Jason But <jbut@swin.edu.au>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jason-but/notify-broadcast
|
|
8
|
+
Project-URL: Issues, https://github.com/jason-but/notify-broadcast/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: colorlog
|
|
15
|
+
Requires-Dist: dasbus
|
|
16
|
+
Requires-Dist: psutil
|
|
17
|
+
Requires-Dist: pygobject
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# notify-broadcast
|
|
21
|
+
|
|
22
|
+
This program was developed because I run a number of **cron** jobs as root that I would like to be
|
|
23
|
+
able to send notifications to the GUI User
|
|
24
|
+
|
|
25
|
+
`notify-send` only functions if run by the same user running the graphical session.
|
|
26
|
+
|
|
27
|
+
I created `notify-broadcast` to effectively tack the same parameters as `notify-send` but that
|
|
28
|
+
could be executed by the `root` user.
|
|
29
|
+
|
|
30
|
+
The application searches for all active DBUS Notification sessions, and sends the notification to
|
|
31
|
+
all currently attached users.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
When complete, should be:
|
|
36
|
+
|
|
37
|
+
```console
|
|
38
|
+
pip install notify-broadcast
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Seek information elsewhere about installing in a virtual environment
|
|
42
|
+
|
|
43
|
+
The install should pull in all dependencies. At present these are:
|
|
44
|
+
- colorlog: https://github.com/borntyping/python-colorlog/
|
|
45
|
+
- dasbus: https://github.com/dasbus-project/dasbus
|
|
46
|
+
- psutil: https://github.com/giampaolo/psutil
|
|
47
|
+
- PyGObject: https://pygobject.gnome.org
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
```console
|
|
52
|
+
# notify-broadcast --help
|
|
53
|
+
usage: notify-broadcast [--help] [-a APP_NAME] [-i ICON] [-t EXPIRE_TIME] [-h TYPE:NAME:VALUE] [-c TYPE] [-u {low,normal,critical}] [-A NAME=VALUE] [-r REPLACE_ID] [-p] [-e HINT] [--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
|
|
54
|
+
[--global-log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
|
|
55
|
+
summary body
|
|
56
|
+
|
|
57
|
+
Notify Broadcast Send a Broadcast DBUS notification to all users
|
|
58
|
+
|
|
59
|
+
positional arguments:
|
|
60
|
+
summary exam configuration file to be used for student collection - toml format
|
|
61
|
+
body exam solution file to be placed in collection directory
|
|
62
|
+
|
|
63
|
+
options:
|
|
64
|
+
--help show this help message and exit
|
|
65
|
+
-a APP_NAME, --app-name APP_NAME
|
|
66
|
+
Specifies the app name for the notification (default: )
|
|
67
|
+
-i ICON, --icon ICON Specifies an icon filename or stock icon to display. (default: dialog-information)
|
|
68
|
+
-t EXPIRE_TIME, --expire-time EXPIRE_TIME
|
|
69
|
+
The duration, in milliseconds, for the notification to appear on screen. Value of 0 means no expiry, while -1 uses the server default expiry. (default: -1)
|
|
70
|
+
-h TYPE:NAME:VALUE, --hint TYPE:NAME:VALUE
|
|
71
|
+
Notification hints to pass to server (e.g., int:urgency:2) (default: {})
|
|
72
|
+
-c TYPE, --category TYPE
|
|
73
|
+
Specifies the notification category. (default: {})
|
|
74
|
+
-u {low,normal,critical}, --urgency {low,normal,critical}
|
|
75
|
+
Specifies the urgency level (low, normal, critical). (default: {})
|
|
76
|
+
-A NAME=VALUE, --action NAME=VALUE
|
|
77
|
+
Specifies the actions to display to the user. Implies --wait to wait for user input. May be set multiple times. The NAME of the action is output to stdout. If NAME is not specified, the numerical index of the
|
|
78
|
+
option is used (starting with 1). (default: [])
|
|
79
|
+
-r REPLACE_ID, --replace-id REPLACE_ID
|
|
80
|
+
The ID of the notification to replace. (default: 0)
|
|
81
|
+
-p, --print-id Print the notification ID. (default: False)
|
|
82
|
+
-e HINT, --transient HINT
|
|
83
|
+
Show a transient notification. Transient notifications by-pass the server's persistence capability, if any. And so it won't be preserved until the user acknowledges it. (default: {})
|
|
84
|
+
--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
|
|
85
|
+
Set the logging level for the core application. (default: None)
|
|
86
|
+
--global-log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
|
|
87
|
+
Set the global logging level (includes third-party libraries). (default: None)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**NOTE:** For more support on these parameters, see `notify-send` man pages
|
|
91
|
+
|
|
92
|
+
## Examples
|
|
93
|
+
|
|
94
|
+
Some examples of how to use `notify-broadcast` are listed below. These are by no means exhaustive:
|
|
95
|
+
|
|
96
|
+
`notify-broadcast -a Daily-backup -t 0 -i dialog-information.png "Backup completed without error" ""`
|
|
97
|
+
|
|
98
|
+
Display a notification that the backup has completed without error:
|
|
99
|
+
- Display an information icon
|
|
100
|
+
- A timeout of 0 signifies that the notification will display until the user clears it
|
|
101
|
+
- Message contains a summary, but no body
|
|
102
|
+
|
|
103
|
+
`notify-broadcast -a Remote-rsync -t 6000 -i dialog-warning.png "Remote host not currently on the network" ""`
|
|
104
|
+
|
|
105
|
+
Display a notification that the remote host is not available:
|
|
106
|
+
- Display a warning icon
|
|
107
|
+
- A timeout of 6000 signifies that the notification will display for six seconds before clearing
|
|
108
|
+
- Message contains a summary, but no body
|
|
109
|
+
|
|
110
|
+
`notify-broadcast -a Daily-backup -t 0 -i dialog-error.png "Error running backup, please consult logs" ""`
|
|
111
|
+
|
|
112
|
+
Display a notification that the backup has completed without error:
|
|
113
|
+
- Display an error icon
|
|
114
|
+
- A timeout of 0 signifies that the notification will display until the user clears it
|
|
115
|
+
- Message contains a summary, but no body
|
|
116
|
+
|
|
117
|
+
`notify-broadcast -a "Disk Monitor" -h string:desktop-entry:org.kde.kinfocenter-i drive-harddisk "Disk" "SMART warning"`
|
|
118
|
+
|
|
119
|
+
Display a notification that a disk has encountered a SMART error:
|
|
120
|
+
- Display an disk icon
|
|
121
|
+
- No timeout signifies that the notification will display for the system default duration
|
|
122
|
+
|
|
123
|
+
## Comments
|
|
124
|
+
|
|
125
|
+
I am aware of a number of potential shortcomings that may impact broader distribution, as well as
|
|
126
|
+
some points about why things were coded this way, details listed below
|
|
127
|
+
|
|
128
|
+
### Finding DBUS Path via environment variables instead of `/run/user/{uid}/bus`
|
|
129
|
+
|
|
130
|
+
The code searches for the `DBUS_SESSION_BUS_ADDRESS` environment variable in a running program,
|
|
131
|
+
while many online examples suggest searching `/run/user/{uid}/bus`
|
|
132
|
+
|
|
133
|
+
My system (Gentoo) does not place the DBUS sockets in that location, so searching the environment
|
|
134
|
+
variables allows this to work regardless of the location of the socket.
|
|
135
|
+
|
|
136
|
+
### Program is hard-coded to KDE
|
|
137
|
+
|
|
138
|
+
As per the previous point, to search the environment variables, it means finding a running
|
|
139
|
+
application that has the environment variables set. This means that we need to know the application
|
|
140
|
+
name to search for.
|
|
141
|
+
|
|
142
|
+
Hence, the program currently looks for running instances of `kwin_wayland` or `kwin_x11` depending
|
|
143
|
+
on the current session type.
|
|
144
|
+
|
|
145
|
+
This works for me as I use KDE, I don't like Gnome or other environments.
|
|
146
|
+
|
|
147
|
+
However, I realise this means that this will not work everywhere. Some chat online suggests looking
|
|
148
|
+
for `dbus-launch`, however the environment for this process does not appear to contain the
|
|
149
|
+
`DBUS_SESSION_BUS_ADDRESS` environment variable.
|
|
150
|
+
|
|
151
|
+
I would like to support alternate desktops, but I do not have the will to test and develop a
|
|
152
|
+
solution. I am happy to take comments/suggestions on how to detect across multiple platforms.
|
|
153
|
+
|
|
154
|
+
### Some of the Options are Useless for Broadcast application
|
|
155
|
+
|
|
156
|
+
As a broadcast application, this really makes sense as a 1) send a message; and 2) do not wait for
|
|
157
|
+
replies scenario
|
|
158
|
+
|
|
159
|
+
1. `--print-id` makes no sense if sending multiple notifications
|
|
160
|
+
2. `--replace-id` make no sense if we are just blasting information to everyone
|
|
161
|
+
3. `--action` display buttons to the user and return values to the program. This is generally useless for this application
|
|
162
|
+
|
|
163
|
+
### Running as non-root
|
|
164
|
+
|
|
165
|
+
A non-root user will not be able to post notifications to other users in either case. The
|
|
166
|
+
application currently just prints warnings about being unable to access the environment and
|
|
167
|
+
does nothing else.
|
|
168
|
+
|
|
169
|
+
It might be better to abort early if the user does not have permissions, but a system could
|
|
170
|
+
allow multiple users the requisite permissions, so it is hard to manage this properly.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
notify_broadcast/__init__.py
|
|
5
|
+
notify_broadcast/dbussessionmanager.py
|
|
6
|
+
notify_broadcast/notify_broadcast.py
|
|
7
|
+
notify_broadcast/notifybroadcastargumentparser.py
|
|
8
|
+
notify_broadcast.egg-info/PKG-INFO
|
|
9
|
+
notify_broadcast.egg-info/SOURCES.txt
|
|
10
|
+
notify_broadcast.egg-info/dependency_links.txt
|
|
11
|
+
notify_broadcast.egg-info/entry_points.txt
|
|
12
|
+
notify_broadcast.egg-info/requires.txt
|
|
13
|
+
notify_broadcast.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
notify_broadcast
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools >= 77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "notify-broadcast"
|
|
7
|
+
version = "0.0.3"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Jason But", email="jbut@swin.edu.au" },
|
|
10
|
+
]
|
|
11
|
+
description = "Broadcast version of notify-send to allow root processes to send a notification to all users with an active DBUS session"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
license = "MIT"
|
|
19
|
+
license-files = ["LICEN[CS]E*"]
|
|
20
|
+
dependencies = ["colorlog", "dasbus", "psutil", "pygobject"]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
notify-broadcast = "notify_broadcast.notify_broadcast:notify_broadcast"
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/jason-but/notify-broadcast"
|
|
27
|
+
Issues = "https://github.com/jason-but/notify-broadcast/issues"
|