├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ └── test.yml ├── .gitignore ├── Dockerfile ├── Dockerfile.toolbox ├── LICENSE ├── Makefile ├── README.md ├── RELEASING.md ├── dbus └── org.sessiond.session1.xml ├── man ├── sessionctl.1.pod ├── sessiond-dbus.8.pod ├── sessiond-hooks.5.pod ├── sessiond-inhibit.1.pod ├── sessiond.1.pod └── sessiond.conf.5.pod ├── meson.build ├── meson_options.txt ├── python-sessiond ├── .gitignore ├── Makefile ├── sessiond.py └── setup.py ├── scripts └── install_sysfs_writer.sh ├── sessionctl ├── sessiond-inhibit ├── sessiond.conf ├── sessiond.desktop ├── spec ├── sessiond-0.6.1.spec └── sessiond.rpkg.spec ├── src ├── backlight.c ├── backlight.h ├── common.c ├── common.h ├── config.c ├── config.h ├── dbus-audiosink.c ├── dbus-audiosink.h ├── dbus-backlight.c ├── dbus-backlight.h ├── dbus-gen.c ├── dbus-gen.h ├── dbus-logind.c ├── dbus-logind.h ├── dbus-server.c ├── dbus-server.h ├── dbus-systemd.c ├── dbus-systemd.h ├── helper │ └── sessiond-sysfs-writer.c ├── hooks.c ├── hooks.h ├── sessiond.c ├── timeline.c ├── timeline.h ├── toml │ ├── .gitignore │ ├── LICENSE │ ├── Makefile │ ├── README.md │ ├── test1 │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.sh │ │ ├── extra │ │ │ ├── array_of_tables.toml │ │ │ ├── inline_array.toml │ │ │ └── inline_table.toml │ │ └── run.sh │ ├── test2 │ │ ├── .gitignore │ │ ├── build.sh │ │ └── run.sh │ ├── toml.c │ ├── toml.h │ ├── toml_cat.c │ ├── toml_json.c │ └── unittest │ │ ├── Makefile │ │ └── t1.c ├── version.h ├── wireplumber.c ├── wireplumber.h ├── xsource.c └── xsource.h ├── systemd ├── graphical-idle.target ├── graphical-lock.target ├── graphical-unidle.target ├── graphical-unlock.target ├── sessiond-session.target ├── sessiond.service ├── user-shutdown.target ├── user-sleep-finished.target └── user-sleep.target └── test ├── config_test.c ├── hooks.conf ├── hooks.d ├── 00-inactive.hook └── 01-lock.hook ├── hooks_test.c ├── meson.build ├── test.conf └── timeline_test.c /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: jcrd_dev 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: master 6 | workflow_dispatch: 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Set up Docker Buildx 13 | uses: docker/setup-buildx-action@v1 14 | 15 | - name: Login to DockerHub 16 | uses: docker/login-action@v1 17 | with: 18 | username: ${{ secrets.DOCKERHUB_USERNAME }} 19 | password: ${{ secrets.DOCKERHUB_TOKEN }} 20 | 21 | - name: Build and push 22 | id: docker_build 23 | uses: docker/build-push-action@v2 24 | with: 25 | push: true 26 | tags: supplantr/sessiond:latest 27 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ master, devel ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ master ] 9 | schedule: 10 | - cron: '41 5 * * 1' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | container: supplantr/sessiond:latest 17 | permissions: 18 | actions: read 19 | contents: read 20 | security-events: write 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | language: [ 'cpp', 'python' ] 26 | 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v2 30 | 31 | # Initializes the CodeQL tools for scanning. 32 | - name: Initialize CodeQL 33 | uses: github/codeql-action/init@v1 34 | with: 35 | languages: ${{ matrix.language }} 36 | 37 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 38 | # If this step fails, then you should remove it and run the build manually (see below) 39 | - name: Autobuild 40 | uses: github/codeql-action/autobuild@v1 41 | 42 | #- run: | 43 | # make bootstrap 44 | # make release 45 | 46 | - name: Perform CodeQL Analysis 47 | uses: github/codeql-action/analyze@v1 48 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | container: supplantr/sessiond:latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Build 13 | run: meson builddir && ninja -C builddir 14 | 15 | - name: Test 16 | run: meson test -C builddir 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | builddir 2 | .clangd 3 | sessiond.org 4 | compile_commands.json 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:testing-slim 2 | 3 | RUN apt-get update 4 | RUN apt-get install -y meson 5 | RUN apt-get install -y git 6 | RUN apt-get install -y gcc 7 | RUN apt-get install -y pkgconf 8 | RUN apt-get install -y libx11-dev 9 | RUN apt-get install -y libxi-dev 10 | RUN apt-get install -y libglib2.0-dev 11 | RUN apt-get install -y libudev-dev 12 | RUN apt-get install -y libpipewire-0.3-dev 13 | RUN apt-get install -y libwireplumber-0.4-dev 14 | -------------------------------------------------------------------------------- /Dockerfile.toolbox: -------------------------------------------------------------------------------- 1 | FROM supplantr/python-toolbox 2 | 3 | RUN dnf install -y gcc 4 | RUN dnf install -y meson 5 | RUN dnf install -y libX11-devel 6 | RUN dnf install -y libXi-devel 7 | RUN dnf install -y glib2-devel 8 | RUN dnf install -y systemd-devel 9 | RUN dnf install -y dbus-devel 10 | RUN dnf install -y wireplumber-devel 11 | RUN dnf install -y python-dbus 12 | 13 | RUN dnf install -y valgrind 14 | 15 | # clangd 16 | RUN dnf install -y clang-tools-extra 17 | 18 | # Install Pod::Markdown perl module to convert .pod manpages to .md for site. 19 | RUN dnf install -y perl 20 | RUN curl -L http://cpanmin.us | perl - App::cpanminus 21 | RUN cpanm Pod::Markdown 22 | 23 | RUN pip install sphinx-markdown-builder 24 | 25 | RUN dnf copr enable -y jcrd/hugo-extended 26 | RUN dnf install -y hugo 27 | 28 | RUN dnf install -y npm 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SITE_URL = git@github.com:jcrd/sessiond.org 2 | 3 | BUILDDIR ?= builddir 4 | 5 | all: compile_commands.json sessiond.org 6 | build: $(BUILDDIR)/sessiond 7 | 8 | $(BUILDDIR): 9 | meson $(BUILDDIR) 10 | 11 | compile_commands.json: $(BUILDDIR) 12 | ln -s $(BUILDDIR)/compile_commands.json compile_commands.json 13 | 14 | $(BUILDDIR)/sessiond: $(BUILDDIR) 15 | ninja -C $(BUILDDIR) 16 | 17 | sessiond.org: 18 | git clone $(SITE_URL) $@ 19 | 20 | clean: 21 | rm -fr $(BUILDDIR) 22 | rm -f compile_commands.json 23 | 24 | .PHONY: all build clean 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [][site] sessiond 2 | 3 | [![test][test-badge]][test] 4 | [![CodeQL][codeql-badge]][codeql] 5 | [![Copr build status][copr-badge]][copr] 6 | 7 | [test-badge]: https://github.com/jcrd/sessiond/actions/workflows/test.yml/badge.svg 8 | [test]: https://github.com/jcrd/sessiond/actions/workflows/test.yml 9 | [codeql-badge]: https://github.com/jcrd/sessiond/actions/workflows/codeql-analysis.yml/badge.svg 10 | [codeql]: https://github.com/jcrd/sessiond/actions/workflows/codeql-analysis.yml 11 | [copr-badge]: https://copr.fedorainfracloud.org/coprs/jcrd/sessiond/package/sessiond/status_image/last_build.png 12 | [copr]: https://copr.fedorainfracloud.org/coprs/jcrd/sessiond/package/sessiond/ 13 | 14 | ## Overview 15 | 16 | sessiond is a daemon for **systemd**-based Linux systems that interfaces with 17 | **systemd-logind** to provide session management features to X11 window managers. 18 | 19 | Its primary responsibility is to monitor keyboard and mouse activity to 20 | determine when a session has become idle, and to then act accordingly. 21 | 22 | It also provides a DBus service with interfaces to backlights and audio sinks. 23 | 24 | ## Features 25 | 26 | * automatic screen locking on session idle and before sleeping 27 | * automatic backlight dimming on session idle 28 | * automatic muting of audio while session is locked 29 | * systemd targets activated by systemd-logind's lock, unlock, sleep, 30 | and shutdown signals 31 | * hooks triggered by inactivity or signals 32 | * a DBus service 33 | * backlight interaction 34 | * audio sink interaction 35 | * (optional) management of DPMS settings 36 | 37 | ## Documentation 38 | 39 | Documentation is available at [here][site]. 40 | 41 | See the [Getting started](https://jcrd.github.io/sessiond/getting-started/) section to get 42 | started using sessiond. 43 | 44 | ## License 45 | 46 | sessiond is licensed under the GNU General Public License v3.0 or later 47 | (see [LICENSE](LICENSE)). 48 | 49 | [site]: https://jcrd.github.io/sessiond/ 50 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | 1. Bump version in `meson.build` 2 | 2. Rename `spec/sessiond-$VERSION.spec` 3 | - Bump Version 4 | - Adjust source URL 5 | - Add changelog entry 6 | 3. Ensure `spec/sessiond.rpkg.spec` reflects packaging changes 7 | 4. Test spec with `tb rpkg-install` 8 | 5. Ensure `spec/sessiond-$VERSION.spec` reflects rpkg spec 9 | 6. Commit with `Update version to $VERSION` 10 | 7. Tag release 11 | 8. Push commits and tag 12 | 9. Bump download version on sessiond.org 13 | -------------------------------------------------------------------------------- /dbus/org.sessiond.session1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /man/sessionctl.1.pod: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | sessionctl - standalone X session manager client 4 | 5 | =head1 SYNOPSIS 6 | 7 | B [command] 8 | 9 | =head1 DESCRIPTION 10 | 11 | sessionctl is responsible for running a sessiond session and interacting with 12 | its DBus service. 13 | 14 | =head1 COMMANDS 15 | 16 | =over 17 | 18 | =item B [I] 19 | 20 | Run a new session, with I as the window manager service if provided. 21 | By default, the service installed under the I alias is 22 | used. 23 | 24 | =item B 25 | 26 | Stop the running session. 27 | 28 | =item B 29 | 30 | Show session status. 31 | 32 | =item B 33 | 34 | Lock the session. 35 | 36 | =item B 37 | 38 | Unlock the session. 39 | 40 | =item B 41 | 42 | List sessiond properties. 43 | 44 | =item B [I] 45 | 46 | Interact with backlights. 47 | If backlight I is given with no options, print brightness. 48 | If no arguments are given, list backlights. 49 | 50 | Options: 51 | 52 | =over 53 | 54 | =item B<-s> I, B<--set> I 55 | 56 | Set backlight brightness. 57 | 58 | =item B<-i> I, B<--inc> I 59 | 60 | Increment backlight brightness. 61 | Prints the new brightness value. 62 | 63 | =back 64 | 65 | =item B [I] 66 | 67 | Interact with audio sinks. 68 | If audio sink I is given with no options, print volume and mute state. 69 | If no arguments are given, list audio sinks. 70 | 71 | Options: 72 | 73 | =over 74 | 75 | =item B<-s> I, B<--set> I 76 | 77 | Set audio sink volume. 78 | 79 | =item B<-i> I, B<--inc> I 80 | 81 | Increment audio sink volume. 82 | Prints the new volume value. 83 | 84 | =item B<-m>, B<--mute> 85 | 86 | Mute audio sink. 87 | 88 | =item B<-u>, B<--unmute> 89 | 90 | Unmute audio sink. 91 | 92 | =item B<-t>, B<--toggle-mute> 93 | 94 | Toggle audio sink mute state. 95 | Prints the new mute state. 96 | 97 | =back 98 | 99 | =item B 100 | 101 | Show sessiond version. 102 | 103 | =back 104 | 105 | =head1 AUTHOR 106 | 107 | James Reed Ejcrd@sessiond.orgE 108 | 109 | =head1 REPORTING BUGS 110 | 111 | Bugs and issues can be reported here: L 112 | 113 | =head1 COPYRIGHT 114 | 115 | Copyright 2019-2020 James Reed. sessiond is licensed under the 116 | GNU General Public License v3.0 or later. 117 | 118 | =head1 SEE ALSO 119 | 120 | B(1), B(1) 121 | -------------------------------------------------------------------------------- /man/sessiond-dbus.8.pod: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | sessiond-dbus - sessiond DBus service information 4 | 5 | =head1 SYNOPSIS 6 | 7 | DBus service information. 8 | 9 | =head1 DESCRIPTION 10 | 11 | sessiond provides a DBus service on the session bus at the well-known name 12 | I. 13 | 14 | =head2 Session interface 15 | 16 | The B object implements the 17 | B interface, which exposes the following 18 | methods, properties, and signals: 19 | 20 | =head3 METHODS 21 | 22 | =over 23 | 24 | =item B 25 | 26 | Lock the session. Returns an error if the session is already locked. 27 | 28 | =item B 29 | 30 | Unlock the session. Returns an error if the session is not locked. 31 | 32 | =item B 33 | 34 | Inhibit inactivity. The session will always be considered active if at least 35 | one inhibitor is running. Takes two arguments: 36 | 37 | =over 38 | 39 | =item I 40 | 41 | A string describing who is inhibiting. 42 | 43 | =item I 44 | 45 | A string describing why the inhibitor is running. 46 | 47 | =back 48 | 49 | Returns a unique ID used to stop the inhibitor. 50 | 51 | =item B 52 | 53 | Stop an inhibitor. Takes one argument: 54 | 55 | =over 56 | 57 | =item I 58 | 59 | The unique ID of the inhibitor to stop. 60 | 61 | =back 62 | 63 | Returns an error if the ID is not valid or does not exist. 64 | 65 | =item B 66 | 67 | Stop running inhibitors. Returns the number of inhibitors stopped. 68 | 69 | =item B 70 | 71 | List running inhibitors. Returns a dictionary mapping IDs to tuples of the 72 | creation timestamp and I and I strings. 73 | 74 | =back 75 | 76 | =head3 PROPERTIES 77 | 78 | =over 79 | 80 | =item B 81 | 82 | The inhibited state of the session. 83 | 84 | =item B 85 | 86 | The locked state of the session. 87 | 88 | =item B 89 | 90 | The idle state of the session. 91 | 92 | =item B 93 | 94 | The timestamp of the last change to B. 95 | 96 | =item B 97 | 98 | The timestamp of the last change to B in monotonic time. 99 | 100 | =item B 101 | 102 | An array of object paths to exported Backlights. 103 | 104 | =item B 105 | 106 | An array of object paths to exported AudioSinks. 107 | 108 | =item B 109 | 110 | Object path to the default AudioSink. 111 | 112 | =item B 113 | 114 | The version of sessiond. 115 | 116 | =back 117 | 118 | =head3 SIGNALS 119 | 120 | =over 121 | 122 | =item B 123 | 124 | Emitted when the session is locked. B will be true. 125 | 126 | =item B 127 | 128 | Emitted when the session is unlocked. B will be false. 129 | 130 | =item B 131 | 132 | Emitted when the session becomes idle. B will be true. 133 | 134 | =item B 135 | 136 | Emitted when activity resumes in an inactive session. 137 | B will be false. 138 | 139 | =item B B 140 | 141 | Emitted when the session becomes inactive, with the B argument being 142 | the number of seconds since activity. Its value will be equal to either the 143 | I or I configuration option (see B(5)), or the 144 | I option of a hook with an B trigger 145 | (see B(5)). 146 | 147 | =item B B 148 | 149 | Emitted before and after system sleep, with the B argument being true 150 | and false respectively. 151 | 152 | =item B B 153 | 154 | Emitted before and after system shutdown, with the B argument being true 155 | and false respectively. 156 | 157 | =item B B 158 | 159 | Emitted when a backlight is added, with B being the object path of the 160 | new backlight. 161 | 162 | =item B B 163 | 164 | Emitted when a backlight is removed, with B being the old object path of 165 | the backlight. 166 | 167 | =item B B 168 | 169 | Emitted when an audio sink is added, with B being the object path of the 170 | new audio sink. 171 | 172 | =item B B 173 | 174 | Emitted when an audio sink is removed, with B being the old object path of 175 | the audio sink. 176 | 177 | =item B B 178 | 179 | Emitted when the default audio sink changes, with B being the object path 180 | of the default audio sink. 181 | 182 | =back 183 | 184 | =head2 Backlight interface 185 | 186 | The B objects implement the 187 | B interface, which exposes the following 188 | methods and properties: 189 | 190 | =head3 METHODS 191 | 192 | =over 193 | 194 | =item B 195 | 196 | Set the brightness of the backlight. Takes one argument: 197 | 198 | =over 199 | 200 | =item I 201 | 202 | An unsigned integer value. 203 | 204 | =back 205 | 206 | Returns an error if unable to set brightness. 207 | 208 | =item B 209 | 210 | Increment the brightness of the backlight. Takes one argument: 211 | 212 | =over 213 | 214 | =item I 215 | 216 | An integer value added to the backlight's current brightness. 217 | 218 | =back 219 | 220 | Returns the new brightness value or an error if unable to set brightness. 221 | 222 | =back 223 | 224 | =head3 PROPERTIES 225 | 226 | =over 227 | 228 | =item B 229 | 230 | True if the backlight is online, false otherwise. 231 | 232 | =item B 233 | 234 | Path to the backlight device without the sys mount point. 235 | 236 | =item B 237 | 238 | Name of the backlight. 239 | 240 | =item B 241 | 242 | Subsystem to which the backlight belongs. Possible values are: "backlight" or 243 | "leds". 244 | 245 | =item B 246 | 247 | Path to the device via sys mount point. Format is: 248 | "/sys/class/I/I". 249 | 250 | =item B 251 | 252 | Current brightness of backlight. 253 | 254 | =item B 255 | 256 | Max brightness of backlight. 257 | 258 | =back 259 | 260 | =head2 AudioSink interface 261 | 262 | The B objects implement the 263 | B interface, which exposes the following 264 | methods, properties, and signals: 265 | 266 | =head3 METHODS 267 | 268 | =over 269 | 270 | =item B 271 | 272 | Set the volume of the audio sink. Takes one argument: 273 | 274 | =over 275 | 276 | =item I 277 | 278 | A double value. 279 | 280 | =back 281 | 282 | Returns an error if unable to set volume. 283 | 284 | =item B 285 | 286 | Increment the volume of the audio sink. Takes one argument: 287 | 288 | =over 289 | 290 | =item I 291 | 292 | A double value added to the audio sink's current volume. 293 | 294 | =back 295 | 296 | Returns the new volume value or an error if unable to set volume. 297 | 298 | =item B 299 | 300 | Set the mute state of the audio sink. Takes one argument: 301 | 302 | =over 303 | 304 | =item I 305 | 306 | A boolean value indicating the mute state. 307 | 308 | =back 309 | 310 | Returns an error if unable to set mute state. 311 | 312 | =item B 313 | 314 | Toggle the mute state of the audio sink. 315 | Returns the new mute state or an error if unable to set mute state. 316 | 317 | =back 318 | 319 | =head3 PROPERTIES 320 | 321 | =over 322 | 323 | =item B 324 | 325 | ID of the audio sink. 326 | 327 | =item B 328 | 329 | Name of the audio sink. 330 | 331 | =item B 332 | 333 | Mute state of the audio sink. 334 | 335 | =item B 336 | 337 | Volume of the audio sink. 338 | 339 | =back 340 | 341 | =head3 SIGNALS 342 | 343 | =over 344 | 345 | =item B B 346 | 347 | Emitted when the mute state changes, with the B argument being the new 348 | mute state. 349 | 350 | =item B B 351 | 352 | Emitted when the volume changes, with the B argument being the new 353 | volume value. 354 | 355 | =back 356 | 357 | =head1 INTROSPECTION 358 | 359 | =over 360 | 361 | =item For complete introspection data, use B(1): 362 | 363 | B introspect --session --dest I --object-path 364 | I 365 | 366 | =back 367 | 368 | =head1 AUTHOR 369 | 370 | James Reed Ejcrd@sessiond.orgE 371 | 372 | =head1 REPORTING BUGS 373 | 374 | Bugs and issues can be reported here: L 375 | 376 | =head1 COPYRIGHT 377 | 378 | Copyright 2018-2020 James Reed. sessiond is licensed under the 379 | GNU General Public License v3.0 or later. 380 | 381 | =head1 SEE ALSO 382 | 383 | B(1), B(8) 384 | -------------------------------------------------------------------------------- /man/sessiond-hooks.5.pod: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | sessiond-hooks - sessiond hook file format 4 | 5 | =head1 SYNOPSIS 6 | 7 | =over 8 | 9 | =item [Hook] 10 | 11 | =item Trigger=Lock|Idle|Sleep|Shutdown|Inactive 12 | 13 | =item InactiveSec=EsecondsE (Inactive only) 14 | 15 | =item ExecStart=EcommandE 16 | 17 | =item ExecStop=EcommandE (Lock|Idle|Inactive only) 18 | 19 | =back 20 | 21 | =head1 DESCRIPTION 22 | 23 | sessiond provides the ability to define hooks that are triggered by events. 24 | The "Inactive" event is unique to hooks. It allows commands to be run after a 25 | period of inactivity. It is more general than the "Idle" event, which occurs 26 | after I (see B(5)) seconds of inactivity. 27 | 28 | Hooks can be specified in the configuration file using the section "[[Hook]]". 29 | See B(5). 30 | 31 | Hook files with the ".hook" suffix are read from 32 | I/sessiond/hooks.d or I/.config/sessiond/hooks.d. 33 | 34 | =head1 OPTIONS 35 | 36 | =over 37 | 38 | =item I 39 | 40 | Event type that will trigger the hook. Values are "Lock", "Idle", "Sleep", 41 | "Shutdown", "Inactive". 42 | 43 | =item I 44 | 45 | Seconds of inactivity after which the hook is triggered. 46 | 47 | =item I 48 | 49 | Command to execute when the hook is triggered. 50 | 51 | =item I 52 | 53 | Command to execute when the trigger event ends. For "Lock", this is when the 54 | screen is unlocked. For "Idle" and "Inactive", this is when activity resumes. 55 | 56 | =back 57 | 58 | =head1 SEE ALSO 59 | 60 | B(1), B(5) 61 | -------------------------------------------------------------------------------- /man/sessiond-inhibit.1.pod: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | sessiond-inhibit - manage sessiond inhibitors 4 | 5 | =head1 SYNOPSIS 6 | 7 | B [options] [COMMAND] 8 | 9 | =head1 DESCRIPTION 10 | 11 | sessiond-inhibit creates an inhibitor lock before running I and 12 | releases it when the command returns. 13 | If no command is provided, it lists running inhibitors. 14 | 15 | =head1 OPTIONS 16 | 17 | =over 18 | 19 | =item B<-h> 20 | 21 | Show help message. 22 | 23 | =item B<-w> I 24 | 25 | Set who is inhibiting. 26 | 27 | =item B<-y> I 28 | 29 | Set why this inhibitor is running. 30 | 31 | =item B<-s> 32 | 33 | Stop running inhibitors. 34 | 35 | =item B<-i> 36 | 37 | Inhibit without a command. 38 | 39 | =item B<-u> [I] 40 | 41 | Uninhibit last inhibitor or by ID. 42 | 43 | =back 44 | 45 | =head1 AUTHOR 46 | 47 | James Reed Ejcrd@sessiond.orgE 48 | 49 | =head1 REPORTING BUGS 50 | 51 | Bugs and issues can be reported here: L 52 | 53 | =head1 COPYRIGHT 54 | 55 | Copyright 2019-2020 James Reed. sessiond is licensed under the 56 | GNU General Public License v3.0 or later. 57 | 58 | =head1 SEE ALSO 59 | 60 | B(1), B(1) 61 | -------------------------------------------------------------------------------- /man/sessiond.1.pod: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | sessiond - standalone X session manager 4 | 5 | =head1 SYNOPSIS 6 | 7 | B [OPTIONS] 8 | 9 | =head1 DESCRIPTION 10 | 11 | sessiond is a standalone X session manager that reports the idle status of a 12 | session to B(8) and handles its lock, unlock, sleep, and 13 | shutdown signals. sessiond also provides hooks triggered by inactivity or a 14 | signal, automatic backlight dimming on idle, and optional management of DPMS 15 | settings. 16 | 17 | =head1 OPTIONS 18 | 19 | =over 20 | 21 | =item B<-h>, B<--help> 22 | 23 | Show help options. 24 | 25 | =item B<-c>, B<--config>=I 26 | 27 | Path to config file. See B(5) for configuration options. 28 | 29 | =item B<-i>, B<--idle-sec>=I 30 | 31 | Seconds the session must be inactive before considered idle. 32 | 33 | =item B<-v>, B<--version> 34 | 35 | Show version. 36 | 37 | =back 38 | 39 | =head1 DEBUGGING 40 | 41 | Running sessiond with the environment variable I set to "all" 42 | will print debug messages. 43 | 44 | =head1 AUTHOR 45 | 46 | James Reed Ejcrd@sessiond.orgE 47 | 48 | =head1 REPORTING BUGS 49 | 50 | Bugs and issues can be reported here: L 51 | 52 | =head1 COPYRIGHT 53 | 54 | Copyright 2018-2020 James Reed. sessiond is licensed under the 55 | GNU General Public License v3.0 or later. 56 | 57 | =head1 SEE ALSO 58 | 59 | B(5), B(5), B(8), B(8) 60 | -------------------------------------------------------------------------------- /man/sessiond.conf.5.pod: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | sessiond.conf - sessiond configuration file format 4 | 5 | =head1 SYNOPSIS 6 | 7 | I/sessiond/sessiond.conf or 8 | I/.config/sessiond/sessiond.conf 9 | 10 | =head1 DESCRIPTION 11 | 12 | This file configures the X session manager B(1). 13 | Its syntax is toml v0.5.0. 14 | See: https://github.com/toml-lang/toml/tree/v0.5.0. 15 | 16 | =head1 OPTIONS 17 | 18 | =head2 [Idle] 19 | 20 | =over 21 | 22 | =item I 23 | 24 | A list (of the format ["item", "item"]) of input event types used to determine 25 | activity. Values are "motion", "button-press", "button-release", "key-press", 26 | "key-release". 27 | 28 | =item I 29 | 30 | Seconds the session must be inactive before considered idle. 31 | 32 | =back 33 | 34 | =head2 [Lock] 35 | 36 | =over 37 | 38 | =item I 39 | 40 | If "true", lock the session when it becomes idle. 41 | 42 | =item I 43 | 44 | If "true", lock the session when B(8) sends the 45 | "PrepareForSleep" signal. 46 | 47 | =item I 48 | 49 | DPMS standby timeout in seconds to use while session is locked. 50 | Must occur before or simultaneously with Suspend timeout. 51 | 52 | =item I 53 | 54 | DPMS suspend timeout in seconds to use while session is locked. 55 | Must occur before or simultaneously with Off timeout. 56 | 57 | =item I 58 | 59 | DPMS off timeout in seconds to use while session is locked. 60 | 61 | =item I 62 | 63 | If "true", mute the default audio sink while the session is locked. 64 | The mute state will be restored when unlocked. 65 | 66 | =back 67 | 68 | =head2 [DPMS] 69 | 70 | =over 71 | 72 | =item I 73 | 74 | If "true", apply DPMS settings, including those in the "[Lock]" section. 75 | 76 | =item I 77 | 78 | DPMS standby timeout in seconds. Must occur before or simultaneously with 79 | Suspend timeout. 80 | 81 | =item I 82 | 83 | DPMS suspend timeout in seconds. Must occur before or simultaneously with 84 | Off timeout. 85 | 86 | =item I 87 | 88 | DPMS off timeout in seconds. 89 | 90 | =back 91 | 92 | =head2 [[Backlight]] 93 | 94 | Backlights are configured as an array of tables, using the section 95 | "[[Backlight]]". The options will be applied to backlights with the same path. 96 | 97 | =over 98 | 99 | =item I 100 | 101 | Path to the backlight device via sys mount point. Should be of the format: 102 | "/sys/class/I/I". 103 | 104 | =item I 105 | 106 | Seconds the session must be inactive before the backlight is dimmed. 107 | 108 | =item I 109 | 110 | Value of the backlight brightness when dimming. 111 | 112 | =item I 113 | 114 | Percentage to lower backlight brightness when dimming. 115 | 116 | =back 117 | 118 | =head2 [[Hook]] 119 | 120 | Hooks are configured as an array of tables, using the section "[[Hook]]". 121 | See B(5) for a description of options. 122 | 123 | =head1 SEE ALSO 124 | 125 | B(1), B(8), B(5) 126 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('sessiond', 'c', 2 | version : 'v0.6.1', 3 | meson_version : '>=0.47.0') 4 | 5 | deps = [ 6 | dependency('gio-2.0'), 7 | dependency('gio-unix-2.0'), 8 | dependency('x11'), 9 | dependency('xi'), 10 | dependency('libudev'), 11 | ] 12 | 13 | # alternative to vcs_tag that allows reuse of version 14 | # see https://github.com/mesonbuild/meson/issues/3903 15 | version = meson.project_version() 16 | git = find_program('git', required : false) 17 | if git.found() 18 | r = run_command('git', 'describe', '--dirty', '--tags', '--always') 19 | if r.returncode() == 0 20 | version = r.stdout().strip() 21 | endif 22 | endif 23 | 24 | add_project_arguments('-DVERSION="@0@"'.format(version), language : 'c') 25 | add_project_arguments('-DG_LOG_USE_STRUCTURED', language : 'c') 26 | add_project_arguments('-DPREFIX="@0@"'.format(get_option('prefix')), language : 'c') 27 | 28 | if get_option('dpms').enabled() 29 | deps += dependency('xext') 30 | add_project_arguments('-DDPMS', language : 'c') 31 | endif 32 | 33 | if get_option('backlight_helper').enabled() 34 | add_project_arguments('-DBACKLIGHT_HELPER', language : 'c') 35 | executable('sessiond-sysfs-writer', 'src/helper/sessiond-sysfs-writer.c') 36 | 37 | # install setuid helper 38 | meson.add_install_script('scripts/install_sysfs_writer.sh') 39 | endif 40 | 41 | # generate dbus sources 42 | gdbus_codegen = find_program('gdbus-codegen') 43 | 44 | dbus_srcs = custom_target('dbus sources', 45 | command : [gdbus_codegen, 46 | '--generate-c-code', 'dbus-gen', 47 | '--interface-prefix', 'org.sessiond.session1.', 48 | '--c-namespace', 'DBus', 49 | '@INPUT@'], 50 | input : ['dbus/org.sessiond.session1.xml'], 51 | output : ['dbus-gen.c', 'dbus-gen.h']) 52 | 53 | srcs = [ 54 | dbus_srcs, 55 | 'src/backlight.c', 56 | 'src/common.c', 57 | 'src/config.c', 58 | 'src/dbus-logind.c', 59 | 'src/dbus-systemd.c', 60 | 'src/dbus-server.c', 61 | 'src/dbus-backlight.c', 62 | 'src/hooks.c', 63 | 'src/sessiond.c', 64 | 'src/timeline.c', 65 | 'src/xsource.c', 66 | 'src/toml/toml.c', 67 | ] 68 | 69 | if get_option('wireplumber').enabled() 70 | deps += dependency('wireplumber-0.4') 71 | deps += dependency('libpipewire-0.3') 72 | srcs += 'src/dbus-audiosink.c' 73 | srcs += 'src/wireplumber.c' 74 | add_project_arguments('-DWIREPLUMBER', language : 'c') 75 | endif 76 | 77 | executable('sessiond', sources : srcs, dependencies : deps, install : true) 78 | 79 | install_data('sessiond.conf', 80 | install_dir : join_paths(get_option('datadir'), meson.project_name())) 81 | 82 | install_data('sessionctl', install_dir : get_option('bindir')) 83 | install_data('sessiond-inhibit', install_dir : get_option('bindir')) 84 | 85 | install_data('sessiond.desktop', 86 | install_dir : join_paths(get_option('datadir'), 'xsessions')) 87 | 88 | units = [ 89 | 'systemd/graphical-idle.target', 90 | 'systemd/graphical-unidle.target', 91 | 'systemd/graphical-lock.target', 92 | 'systemd/graphical-unlock.target', 93 | 'systemd/user-sleep.target', 94 | 'systemd/user-sleep-finished.target', 95 | 'systemd/user-shutdown.target', 96 | 'systemd/sessiond-session.target', 97 | 'systemd/sessiond.service', 98 | ] 99 | 100 | install_data(sources : units, 101 | install_dir : join_paths(get_option('libdir'), 'systemd/user')) 102 | 103 | manpages = { 104 | 'sessiond': '1', 105 | 'sessiond.conf': '5', 106 | 'sessiond-hooks': '5', 107 | 'sessiond-dbus': '8', 108 | 'sessionctl': '1', 109 | 'sessiond-inhibit': '1', 110 | } 111 | 112 | # install manpages built with pod2man 113 | mandir = get_option('mandir') 114 | pod2man = find_program('pod2man') 115 | 116 | foreach name, section : manpages 117 | manpage = '.'.join([name, section]) 118 | custom_target( 119 | 'manpage @0@'.format(manpage), 120 | input : join_paths('man', '@0@.pod'.format(manpage)), 121 | output : manpage, 122 | command : [ 123 | pod2man, 124 | '--name=@0@'.format(name), 125 | '--center=@0@'.format(name), 126 | '--section=@0@'.format(section), 127 | '--release=@0@'.format(version), 128 | '@INPUT@', '@OUTPUT@'], 129 | install : true, 130 | install_dir : join_paths(mandir, 'man@0@'.format(section))) 131 | endforeach 132 | 133 | subdir('test') 134 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('dpms', type : 'feature', value : 'enabled') 2 | option('wireplumber', type : 'feature', value : 'enabled') 3 | option('backlight_helper', type : 'feature', value : 'disabled') 4 | -------------------------------------------------------------------------------- /python-sessiond/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build 3 | dist 4 | sessiond.egg-info 5 | -------------------------------------------------------------------------------- /python-sessiond/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | python setup.py install --user 3 | 4 | uninstall: 5 | pip uninstall -y sessiond 6 | 7 | clean: 8 | rm -rf __pycache__ build dist sessiond.egg-info 9 | 10 | .PHONY: install uninstall clean 11 | -------------------------------------------------------------------------------- /python-sessiond/sessiond.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the MIT License. 2 | 3 | """ 4 | .. module:: sessiond 5 | :synopsis: Interface to the sessiond DBus service. 6 | 7 | .. moduleauthor:: James Reed 8 | """ 9 | 10 | import dbus 11 | 12 | BUS_NAME = "org.sessiond.session1" 13 | 14 | 15 | class DBusIFace: 16 | """ 17 | Base interface to the sessiond DBus service. 18 | 19 | :param path: DBus object path 20 | :param iface: DBus interface name 21 | """ 22 | 23 | bus = dbus.SessionBus() 24 | 25 | @staticmethod 26 | def convert(val): 27 | """ 28 | Convert a DBus-typed value to its python-type. 29 | 30 | :param val: Value to convert 31 | :return: The python-typed value 32 | """ 33 | if isinstance(val, (dbus.String, dbus.ObjectPath)): 34 | return str(val) 35 | if isinstance(val, dbus.Boolean): 36 | return bool(val) 37 | if isinstance(val, dbus.UInt32) or isinstance(val, dbus.UInt64): 38 | return int(val) 39 | if isinstance(val, dbus.Double): 40 | return float(val) 41 | if isinstance(val, dbus.Array): 42 | return list(map(DBusIFace.convert, list(val))) 43 | 44 | return None 45 | 46 | def __init__(self, path, iface): 47 | self.obj = DBusIFace.bus.get_object(BUS_NAME, path) 48 | self.iface = "{}.{}".format(BUS_NAME, iface) 49 | self.props = dbus.Interface( 50 | self.obj, dbus_interface="org.freedesktop.DBus.Properties" 51 | ) 52 | self.interface = dbus.Interface(self.obj, dbus_interface=self.iface) 53 | 54 | def get_properties(self): 55 | """ 56 | Get all DBus properties and values. 57 | 58 | :return: A dictionary mapping property names to values 59 | """ 60 | return { 61 | DBusIFace.convert(k): DBusIFace.convert(v) 62 | for k, v in self.props.GetAll(self.iface).items() 63 | } 64 | 65 | def get_property(self, name): 66 | """ 67 | Get a DBus property's value. 68 | 69 | :param name: The property's name 70 | :return: The property's value 71 | """ 72 | return DBusIFace.convert(self.props.Get(self.iface, name)) 73 | 74 | 75 | class Backlight(DBusIFace): 76 | """ 77 | An interface to a sessiond Backlight object. 78 | 79 | :param name: The backlight's name 80 | """ 81 | 82 | def __init__(self, name): 83 | self.name = name 84 | path = "/org/sessiond/session1/backlight/{}".format(self.name) 85 | super().__init__(path, "Backlight") 86 | 87 | def set_brightness(self, v): 88 | """ 89 | Set the backlight's brightness. 90 | 91 | :param v: Brightness value 92 | :raises dbus.exception.DBusException: Raised if unable to set brightness 93 | """ 94 | self.interface.SetBrightness(v) 95 | 96 | def inc_brightness(self, v): 97 | """ 98 | Increment the backlight's brightness. 99 | 100 | :param v: Brightness value by which to increment 101 | :return: The new brightness value 102 | :raises dbus.exception.DBusException: Raised if unable to set brightness 103 | """ 104 | return self.interface.IncBrightness(v) 105 | 106 | @property 107 | def online(self): 108 | """ 109 | Backlight's online status. 110 | 111 | :return: `True` if online, `False` otherwise 112 | """ 113 | return self.get_property("Online") 114 | 115 | @property 116 | def subsystem(self): 117 | """ 118 | Backlight's subsystem. 119 | 120 | :return: Name of backlight's subsystem 121 | """ 122 | return self.get_property("Subsystem") 123 | 124 | @property 125 | def sys_path(self): 126 | """ 127 | Backlight device's /sys path. 128 | 129 | :return: Path to the device via sys mount point 130 | """ 131 | return self.get_property("SysPath") 132 | 133 | @property 134 | def dev_path(self): 135 | """ 136 | Backlight device's path. 137 | 138 | :return: Path to the backlight device without the sys mount point 139 | """ 140 | return self.get_property("DevPath") 141 | 142 | @property 143 | def max_brightness(self): 144 | """ 145 | Backlight's maximum brightness. 146 | 147 | :return: Maximum brightness value 148 | """ 149 | return self.get_property("MaxBrightness") 150 | 151 | @property 152 | def brightness(self): 153 | """ 154 | Backlight's brightness. 155 | 156 | :return: Brightness value 157 | """ 158 | return self.get_property("Brightness") 159 | 160 | 161 | class AudioSink(DBusIFace): 162 | """ 163 | An interface to a sessiond AudioSink object. 164 | 165 | :param id: The audio sink's ID 166 | """ 167 | 168 | def __init__(self, id): 169 | self.as_id = id 170 | path = "/org/sessiond/session1/audiosink/{}".format(self.as_id) 171 | super().__init__(path, "AudioSink") 172 | 173 | def set_volume(self, v): 174 | """ 175 | Set the audio sink's volume. 176 | 177 | :param v: Volume value 178 | :raises dbus.exception.DBusException: Raised if unable to set volume 179 | """ 180 | self.interface.SetVolume(v) 181 | 182 | def inc_volume(self, v): 183 | """ 184 | Increment the audio sink's volume. 185 | 186 | :param v: Volume value by which to increment 187 | :return: The new volume value 188 | :raises dbus.exception.DBusException: Raised if unable to increment volume 189 | """ 190 | return self.interface.IncVolume(v) 191 | 192 | def set_mute(self, m): 193 | """ 194 | Set the audio sink's mute state. 195 | 196 | :param m: Mute state 197 | :raises dbus.exception.DBusException: Raised if unable to set mute state 198 | """ 199 | self.interface.SetMute(m) 200 | 201 | def toggle_mute(self): 202 | """ 203 | Toggle the audio sink's mute state. 204 | 205 | :return: The new mute state 206 | :raises dbus.exception.DBusException: Raised if unable to toggle mute state 207 | """ 208 | return self.interface.ToggleMute() 209 | 210 | @property 211 | def id(self): 212 | """ 213 | Audio sink's ID. 214 | 215 | :return: Audio sink's ID 216 | """ 217 | return self.get_property("Id") 218 | 219 | @property 220 | def name(self): 221 | """ 222 | Audio sink's name. 223 | 224 | :return: Audio sink's name 225 | """ 226 | return self.get_property("Name") 227 | 228 | @property 229 | def mute(self): 230 | """ 231 | Audio sink's mute state. 232 | 233 | :return: `True` if muted, `False` otherwise 234 | """ 235 | return self.get_property("Mute") 236 | 237 | @property 238 | def volume(self): 239 | """ 240 | Audio sink's volume. 241 | 242 | :return: Audio sink's volume 243 | """ 244 | return self.get_property("Volume") 245 | 246 | 247 | class Session(DBusIFace): 248 | """ 249 | An interface to sessiond's Session. 250 | """ 251 | 252 | def __init__(self): 253 | super().__init__("/org/sessiond/session1", "Session") 254 | 255 | def inhibit(self, who="", why=""): 256 | """ 257 | Add an inhibitor. 258 | 259 | :param who: A string describing who is inhibiting 260 | :param why: A string describing why this inhibitor is running 261 | :return: The inhibitor's ID 262 | """ 263 | return self.interface.Inhibit(who, why) 264 | 265 | def uninhibit(self, id): 266 | """ 267 | Remove an inhibitor. 268 | 269 | :param id: The inhibitor's ID 270 | :raises dbus.exception.DBusException: Raised if the ID is not valid or \ 271 | does not exist 272 | """ 273 | self.interface.Uninhibit(id) 274 | 275 | def stop_inhibitors(self): 276 | """ 277 | Stop running inhibitors. 278 | 279 | :return: The number of inhibitors stopped 280 | """ 281 | return self.interface.StopInhibitors() 282 | 283 | def list_inhibitors(self): 284 | """ 285 | List running inhibitors. 286 | 287 | :return: A dictionary mapping IDs to tuples of the creation timestamp \ 288 | and 'who' and 'why' strings 289 | """ 290 | return { 291 | str(k): (int(s[0]), str(s[1]), str(s[2])) 292 | for k, s in self.interface.ListInhibitors().items() 293 | } 294 | 295 | def lock(self): 296 | """ 297 | Lock the session. 298 | 299 | :raises dbus.exception.DBusException: Raised if the session is already \ 300 | locked 301 | """ 302 | self.interface.Lock() 303 | 304 | def unlock(self): 305 | """ 306 | Unlock the session. 307 | 308 | :raises dbus.exception.DBusException: Raised if the session is not \ 309 | locked 310 | """ 311 | self.interface.Unlock() 312 | 313 | @property 314 | def backlights(self): 315 | """ 316 | List of backlights. 317 | 318 | :return: A list of Backlight DBus object paths 319 | """ 320 | return self.get_property("Backlights") 321 | 322 | @property 323 | def audiosinks(self): 324 | """ 325 | List of audio sinks. 326 | 327 | :return: A list of AudioSink DBus object paths 328 | """ 329 | return self.get_property("AudioSinks") 330 | 331 | @property 332 | def default_audiosink(self): 333 | """ 334 | Get the default audio sink. 335 | 336 | :return: The default audio sink's DBus object path 337 | """ 338 | return self.get_property("DefaultAudioSink") 339 | 340 | @property 341 | def idle_hint(self): 342 | """ 343 | Session idle hint. 344 | 345 | :return: `True` if session is idle, `False` otherwise 346 | """ 347 | return self.get_property("IdleHint") 348 | 349 | @property 350 | def inhibited_hint(self): 351 | """ 352 | Session inhibited hint. 353 | 354 | :return: `True` if session is inhibited, `False` otherwise 355 | """ 356 | return self.get_property("InhibitedHint") 357 | 358 | @property 359 | def locked_hint(self): 360 | """ 361 | Session locked hint. 362 | 363 | :return: `True` if session is locked, `False` otherwise 364 | """ 365 | return self.get_property("LockedHint") 366 | 367 | @property 368 | def version(self): 369 | """ 370 | sessiond version. 371 | 372 | :return: sessiond's version string 373 | """ 374 | return self.get_property("Version") 375 | 376 | @property 377 | def idle_since_hint(self): 378 | """ 379 | The timestamp of the last change to IdleHint. 380 | 381 | :return: The timestamp 382 | """ 383 | return self.get_property("IdleSinceHint") 384 | 385 | @property 386 | def idle_since_hint_monotonic(self): 387 | """ 388 | The timestamp of the last change to IdleHint in monotonic time. 389 | 390 | :return: The timestamp 391 | """ 392 | return self.get_property("IdleSinceHintMonotonic") 393 | -------------------------------------------------------------------------------- /python-sessiond/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | name="sessiond", 6 | version="0.1.0", 7 | py_modules=["sessiond"], 8 | install_requires=["dbus-python"], 9 | description="Interface to sessiond DBus service", 10 | url="https://github.com/jcrd/sessiond/python-sessiond", 11 | license="MIT", 12 | author="James Reed", 13 | author_email="jcrd@sessiond.org", 14 | keywords="dbus sessiond", 15 | classifiers=[ 16 | "Development Status :: 2 - Pre-Alpha", 17 | "License :: OSI Approved :: MIT License", 18 | ], 19 | ) 20 | -------------------------------------------------------------------------------- /scripts/install_sysfs_writer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # install setuid helper 4 | install -Dm4755 "$MESON_BUILD_ROOT"/sessiond-sysfs-writer \ 5 | "$DESTDIR/$MESON_INSTALL_PREFIX"/lib/sessiond/sessiond-sysfs-writer 6 | -------------------------------------------------------------------------------- /sessionctl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # sessiond - standalone X session manager 4 | # Copyright (C) 2020 James Reed 5 | # 6 | # This program is free software: you can redistribute it and/or modify it under 7 | # the terms of the GNU General Public License as published by the Free Software 8 | # Foundation, either version 3 of the License, or (at your option) any later 9 | # version. 10 | # 11 | # This program is distributed in the hope that it will be useful, but WITHOUT 12 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License along with 16 | # this program. If not, see . 17 | 18 | import logging 19 | import sys 20 | from argparse import ArgumentParser 21 | from pathlib import PurePath 22 | from subprocess import run 23 | from time import sleep 24 | 25 | from dbus.exceptions import DBusException 26 | from sessiond import Session, Backlight, AudioSink 27 | 28 | SESSIOND_SESSION = "sessiond-session.target" 29 | GRAPHICAL_SESSION = "graphical-session.target" 30 | WINDOW_MANAGER_ALIAS = "window-manager.service" 31 | 32 | 33 | def log_error(msg): 34 | sys.stderr.write(msg) 35 | logging.error(msg) 36 | 37 | 38 | def _run(cmd, **kw): 39 | return run(cmd.split(), **kw) 40 | 41 | 42 | def consists_of(unit=GRAPHICAL_SESSION): 43 | cmd = "systemctl --user show -p ConsistsOf --value {}".format(unit) 44 | return _run(cmd, capture_output=True, text=True, check=True).stdout.split() 45 | 46 | 47 | def stop_session(step=0.1, timeout=5): 48 | def check_active(): 49 | for u in consists_of(): 50 | if Unit(u).active(): 51 | return True 52 | return False 53 | 54 | Unit(SESSIOND_SESSION).stop() 55 | 56 | while check_active(): 57 | if timeout == 0: 58 | for n in consists_of(): 59 | u = Unit(n) 60 | if u.active(): 61 | u.kill() 62 | break 63 | sleep(step) 64 | timeout -= step 65 | 66 | 67 | def get_window_manager(): 68 | cmd = "systemctl --user show -p Id --value {}".format(WINDOW_MANAGER_ALIAS) 69 | wm = _run(cmd, capture_output=True, text=True).stdout.rstrip() 70 | if wm == WINDOW_MANAGER_ALIAS: 71 | raise ValueError("No window manager is enabled") 72 | return wm 73 | 74 | 75 | class Unit: 76 | def __init__(self, name): 77 | self.name = name 78 | 79 | def active(self): 80 | cmd = "systemctl --user -q is-active {}".format(self.name) 81 | return _run(cmd).returncode == 0 82 | 83 | def failed(self): 84 | cmd = "systemctl --user -q is-failed {}".format(self.name) 85 | return _run(cmd).returncode == 0 86 | 87 | def status(self): 88 | cmd = "systemctl --user is-active {}".format(self.name) 89 | return _run(cmd, capture_output=True, text=True).stdout.rstrip() 90 | 91 | def stop(self): 92 | _run("systemctl --user stop {}".format(self.name), check=True) 93 | 94 | def reset(self): 95 | if not self.failed(): 96 | return 97 | _run("systemctl --user reset-failed {}".format(self.name), check=True) 98 | 99 | def kill(self): 100 | _run("systemctl --user kill {}".format(self.name), check=True) 101 | 102 | def start_wait(self): 103 | _run("systemctl --user --wait restart {}".format(self.name)) 104 | stop_session() 105 | 106 | 107 | def status(): 108 | g = Unit(GRAPHICAL_SESSION) 109 | if not g.active(): 110 | sys.stderr.write("Session is not active\n") 111 | sys.exit(1) 112 | units = consists_of() 113 | units.sort() 114 | n = len(max(units, key=len)) 115 | status = {} 116 | for u in units: 117 | status[u] = Unit(u).status() 118 | for u, s in status.items(): 119 | print("{u: <{n}} {s}".format(u=u + ":", n=n + 1, s=s)) 120 | 121 | 122 | def get_session(): 123 | try: 124 | return Session() 125 | except DBusException as e: 126 | log_error( 127 | "Failed to connect to sessiond DBus service: {}".format( 128 | e.get_dbus_message() 129 | ) 130 | ) 131 | sys.exit(1) 132 | 133 | 134 | def add_audiosinks_parser(sp): 135 | p = sp.add_parser("audiosink", help="Interact with audio sinks") 136 | p.add_argument("id", nargs="?", metavar="ID") 137 | p.add_argument( 138 | "-s", 139 | "--set", 140 | type=float, 141 | metavar="VALUE", 142 | help="Set audio sink volume", 143 | ) 144 | p.add_argument( 145 | "-i", 146 | "--inc", 147 | type=float, 148 | metavar="VALUE", 149 | help="Increment audio sink volume", 150 | ) 151 | p.add_argument( 152 | "-m", 153 | "--mute", 154 | action="store_true", 155 | help="Mute audio sink", 156 | ) 157 | p.add_argument( 158 | "-u", 159 | "--unmute", 160 | action="store_true", 161 | help="Unmute audio sink", 162 | ) 163 | p.add_argument( 164 | "-t", 165 | "--toggle-mute", 166 | action="store_true", 167 | help="Toggle audio sink mute state", 168 | ) 169 | 170 | return p 171 | 172 | 173 | if __name__ == "__main__": 174 | logging.basicConfig( 175 | filename=".sessionctl.log", filemode="w", encoding="utf-8", level=logging.ERROR 176 | ) 177 | 178 | p = ArgumentParser(description="With no arguments, show session status.") 179 | subp = p.add_subparsers(dest="cmd") 180 | 181 | r = subp.add_parser("run", help="Run session") 182 | r.add_argument("service", nargs="?", help="Window manager service to run") 183 | 184 | subp.add_parser("stop", help="Stop the running session") 185 | subp.add_parser("status", help="Show session status") 186 | subp.add_parser("lock", help="Lock the session") 187 | subp.add_parser("unlock", help="Unlock the session") 188 | subp.add_parser("properties", help="List session properties") 189 | 190 | blp = subp.add_parser("backlight", help="Interact with backlights") 191 | blp.add_argument("name", nargs="?", metavar="NAME") 192 | blp.add_argument( 193 | "-s", 194 | "--set", 195 | type=int, 196 | metavar="VALUE", 197 | help="Set backlight brightness", 198 | ) 199 | blp.add_argument( 200 | "-i", 201 | "--inc", 202 | type=int, 203 | metavar="VALUE", 204 | help="Increment backlight brightness", 205 | ) 206 | 207 | asp = add_audiosinks_parser(subp) 208 | 209 | subp.add_parser("version", help="Show sessiond version") 210 | 211 | args = p.parse_args() 212 | 213 | if not args.cmd: 214 | status() 215 | sys.exit(0) 216 | 217 | if args.cmd == "run": 218 | if Unit(SESSIOND_SESSION).active(): 219 | log_error("A session is already running") 220 | sys.exit(1) 221 | if not args.service: 222 | try: 223 | args.service = get_window_manager() 224 | except ValueError as e: 225 | log_error(str(e)) 226 | sys.exit(1) 227 | _run("systemctl --user import-environment", check=True) 228 | for u in consists_of(): 229 | Unit(u).reset() 230 | Unit(args.service).start_wait() 231 | elif args.cmd == "stop": 232 | stop_session() 233 | elif args.cmd == "status": 234 | status() 235 | elif args.cmd == "lock": 236 | get_session().lock() 237 | elif args.cmd == "unlock": 238 | get_session().unlock() 239 | elif args.cmd == "properties": 240 | for k, v in get_session().get_properties().items(): 241 | print("{}={}".format(k, v)) 242 | elif args.cmd == "backlight": 243 | 244 | def check_name(): 245 | if not args.name: 246 | sys.stderr.write("Backlight NAME required\n\n") 247 | blp.print_help() 248 | sys.exit(2) 249 | 250 | if args.set: 251 | check_name() 252 | Backlight(args.name).set_brightness(args.set) 253 | elif args.inc: 254 | check_name() 255 | print(Backlight(args.name).inc_brightness(args.inc)) 256 | elif args.name: 257 | print(Backlight(args.name).brightness) 258 | else: 259 | bls = get_session().backlights 260 | if len(bls) == 0: 261 | sys.stderr.write("No backlights\n") 262 | sys.exit(1) 263 | for o in bls: 264 | print(PurePath(o).name) 265 | elif args.cmd == "audiosink": 266 | try: 267 | s = get_session() 268 | assert s.audiosinks 269 | except DBusException: 270 | sys.stderr.write( 271 | "sessiond {} does not support audio sinks\n".format( 272 | s.version 273 | ) 274 | ) 275 | sys.exit(1) 276 | 277 | def check_id(): 278 | if not args.id: 279 | sys.stderr.write("Audio sink ID required\n\n") 280 | asp.print_help() 281 | sys.exit(2) 282 | 283 | def muted(m): 284 | return "MUTED" if m else "UNMUTED" 285 | 286 | if args.set: 287 | check_id() 288 | AudioSink(args.id).set_volume(args.set) 289 | elif args.inc: 290 | check_id() 291 | print(round(AudioSink(args.id).inc_volume(args.inc), 2)) 292 | elif args.mute: 293 | check_id() 294 | AudioSink(args.id).set_mute(True) 295 | elif args.unmute: 296 | check_id() 297 | AudioSink(args.id).set_mute(False) 298 | elif args.toggle_mute: 299 | check_id() 300 | print(muted(AudioSink(args.id).toggle_mute())) 301 | elif args.id: 302 | a = AudioSink(args.id) 303 | print(round(a.volume, 2), muted(a.mute)) 304 | else: 305 | s = get_session() 306 | if len(s.audiosinks) == 0: 307 | sys.stderr.write("No audio sinks\n") 308 | sys.exit(1) 309 | def_id = 0 310 | try: 311 | def_id = int(PurePath(s.default_audiosink).name) 312 | except ValueError: 313 | pass 314 | for o in s.audiosinks: 315 | a = AudioSink(PurePath(o).name) 316 | print( 317 | "{}: {}{}".format( 318 | a.id, a.name, " [DEFAULT]" if a.id == def_id else "" 319 | ) 320 | ) 321 | elif args.cmd == "version": 322 | print(get_session().version) 323 | -------------------------------------------------------------------------------- /sessiond-inhibit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # sessiond - standalone X session manager 4 | # Copyright (C) 2020 James Reed 5 | # 6 | # This program is free software: you can redistribute it and/or modify it under 7 | # the terms of the GNU General Public License as published by the Free Software 8 | # Foundation, either version 3 of the License, or (at your option) any later 9 | # version. 10 | # 11 | # This program is distributed in the hope that it will be useful, but WITHOUT 12 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License along with 16 | # this program. If not, see . 17 | 18 | import sys 19 | from argparse import ArgumentParser 20 | from subprocess import run 21 | 22 | from dbus.exceptions import DBusException 23 | from sessiond import Session 24 | 25 | 26 | def get_inhibitors(session): 27 | # Sort by timestamp. 28 | inh = sorted(session.list_inhibitors().items(), key=lambda x: x[1][0], reverse=True) 29 | if len(inh) == 0: 30 | sys.stderr.write("No inhibitors\n") 31 | sys.exit(1) 32 | return inh 33 | 34 | 35 | if __name__ == "__main__": 36 | p = ArgumentParser(description="With no command, list running inhibitors.") 37 | p.add_argument("command", nargs="?", help="Command to run") 38 | p.add_argument("-w", "--who", default="", help="Set who is inhibiting") 39 | p.add_argument("-y", "--why", default="", help="Set why this inhibitor is running") 40 | p.add_argument("-s", "--stop", action="store_true", help="Stop running inhibitors") 41 | p.add_argument( 42 | "-i", "--inhibit", action="store_true", help="Inhibit without a command" 43 | ) 44 | p.add_argument( 45 | "-u", 46 | "--uninhibit", 47 | nargs="?", 48 | const=True, 49 | metavar="ID", 50 | help="Uninhibit last inhibitor or by ID", 51 | ) 52 | 53 | args = p.parse_args() 54 | s = Session() 55 | 56 | if args.stop: 57 | n = s.stop_inhibitors() 58 | if n == 0: 59 | sys.exit(1) 60 | elif args.inhibit: 61 | print(s.inhibit(args.who, args.why)) 62 | elif args.uninhibit: 63 | if args.uninhibit is True: 64 | # Uninhibit by first ID. 65 | args.uninhibit = get_inhibitors(s)[0][0] 66 | s.uninhibit(args.uninhibit) 67 | print(args.uninhibit) 68 | elif args.command: 69 | i = s.inhibit(args.who or args.command, args.why) 70 | 71 | def try_uninhibit(): 72 | try: 73 | s.uninhibit(i) 74 | except DBusException: 75 | pass 76 | 77 | try: 78 | run(args.command, shell=True) 79 | except KeyboardInterrupt: 80 | try_uninhibit() 81 | sys.exit(130) 82 | try_uninhibit() 83 | else: 84 | for (k, t) in get_inhibitors(s): 85 | out = "id='{}' time='{}' who='{}'".format(k, t[0], t[1]) 86 | if t[2]: 87 | out += " why='{}'".format(t[2]) 88 | print(out) 89 | -------------------------------------------------------------------------------- /sessiond.conf: -------------------------------------------------------------------------------- 1 | # Entries in this file show the compile time defaults. 2 | # See sessiond.conf(5) for details. 3 | 4 | [Idle] 5 | #Inputs=motion,button-press,key-press 6 | #IdleSec=1200 7 | 8 | [Lock] 9 | #OnIdle=true 10 | #OnSleep=true 11 | #StandbySec=60 12 | #SuspendSec=60 13 | #OffSec=60 14 | #MuteAudio=true 15 | 16 | [DPMS] 17 | #Enable=true 18 | #StandbySec=600 19 | #SuspendSec=600 20 | #OffSec=600 21 | 22 | [[Backlight]] 23 | #Path= 24 | #DimSec=480 25 | #DimPercent=0.3 26 | -------------------------------------------------------------------------------- /sessiond.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=sessiond session 3 | Comment=Runs a sessiond session 4 | TryExec=sessionctl 5 | Exec=sessionctl run 6 | Type=Application 7 | -------------------------------------------------------------------------------- /spec/sessiond-0.6.1.spec: -------------------------------------------------------------------------------- 1 | Name: sessiond 2 | Version: 0.6.1 3 | Release: 1%{?dist} 4 | Summary: Standalone X11 session manager for logind 5 | 6 | License: GPLv3+ 7 | URL: https://github.com/jcrd/sessiond 8 | Source0: https://github.com/jcrd/sessiond/archive/v0.6.1.tar.gz 9 | 10 | BuildRequires: meson 11 | BuildRequires: gcc 12 | BuildRequires: perl 13 | BuildRequires: pkgconfig(glib-2.0) 14 | BuildRequires: pkgconfig(libudev) 15 | BuildRequires: pkgconfig(x11) 16 | BuildRequires: pkgconfig(xi) 17 | BuildRequires: pkgconfig(xext) 18 | BuildRequires: pkgconfig(wireplumber-0.4) 19 | BuildRequires: python3-devel 20 | BuildRequires: python3-setuptools 21 | 22 | Requires: python3-dbus 23 | 24 | %description 25 | sessiond is a standalone X session manager that reports the idle status of a 26 | graphical session to systemd-logind. It can be used alongside a window manager 27 | or desktop environment that does not provide its own session management. 28 | 29 | %prep 30 | %setup 31 | 32 | %build 33 | %meson --libdir lib 34 | %meson_build 35 | 36 | cd python-sessiond 37 | %py3_build 38 | 39 | %install 40 | %meson_install 41 | 42 | cd python-sessiond 43 | %py3_install 44 | 45 | %check 46 | %meson_test 47 | 48 | %files 49 | %license LICENSE 50 | %doc README.md 51 | %{_bindir}/sessionctl 52 | %{_bindir}/sessiond 53 | %{_bindir}/sessiond-inhibit 54 | /usr/lib/systemd/user/graphical-idle.target 55 | /usr/lib/systemd/user/graphical-lock.target 56 | /usr/lib/systemd/user/graphical-unidle.target 57 | /usr/lib/systemd/user/graphical-unlock.target 58 | /usr/lib/systemd/user/sessiond-session.target 59 | /usr/lib/systemd/user/sessiond.service 60 | /usr/lib/systemd/user/user-shutdown.target 61 | /usr/lib/systemd/user/user-sleep.target 62 | /usr/lib/systemd/user/user-sleep-finished.target 63 | %{_mandir}/man1/sessionctl.1.gz 64 | %{_mandir}/man1/sessiond-inhibit.1.gz 65 | %{_mandir}/man1/sessiond.1.gz 66 | %{_mandir}/man5/sessiond-hooks.5.gz 67 | %{_mandir}/man5/sessiond.conf.5.gz 68 | %{_mandir}/man8/sessiond-dbus.8.gz 69 | %{_datadir}/sessiond/sessiond.conf 70 | %{_datadir}/xsessions/sessiond.desktop 71 | 72 | %{python3_sitelib}/%{name}-*.egg-info/ 73 | %{python3_sitelib}/%{name}.py 74 | %{python3_sitelib}/__pycache__/%{name}.* 75 | 76 | %changelog 77 | * Wed Jan 26 2022 James Reed - 0.6.1-1 78 | - Release v0.6.1 hotfix 79 | 80 | * Mon Jan 24 2022 James Reed - 0.6.0-3 81 | - Add missing file declaration 82 | - Third time's a charm... 83 | 84 | * Mon Jan 24 2022 James Reed - 0.6.0-2 85 | - Add missing build requirements 86 | 87 | * Mon Jan 24 2022 James Reed - 0.6.0-1 88 | - Release v0.6.0 89 | 90 | * Mon Apr 12 2021 James Reed - 0.5.0-1 91 | - Release v0.5.0 92 | 93 | * Fri Apr 2 2021 James Reed - 0.4.0-1 94 | - Release v0.4.0 95 | 96 | * Thu Dec 31 2020 James Reed - 0.3.1-1 97 | - Release v0.3.1 98 | 99 | * Sun Nov 1 2020 James Reed - 0.3.0-1 100 | - Release v0.3.0 101 | 102 | * Wed Jun 17 2020 James Reed - 0.2.0-1 103 | - Release v0.2.0 104 | 105 | * Sat May 23 2020 James Reed - 0.1.0-2 106 | - Use pkgconfig in build requires 107 | 108 | * Mon May 11 2020 James Reed - 0.1.0 109 | - Initial package 110 | -------------------------------------------------------------------------------- /spec/sessiond.rpkg.spec: -------------------------------------------------------------------------------- 1 | Name: {{{ git_cwd_name name="sessiond" }}} 2 | Version: {{{ git_cwd_version lead="$(git tag | sed -n 's/^v//p' | sort --version-sort -r | head -n1)" }}} 3 | Release: 1%{?dist} 4 | Summary: Standalone X11 session manager for logind 5 | 6 | License: GPLv3+ 7 | URL: https://github.com/jcrd/sessiond 8 | VCS: {{{ git_cwd_vcs }}} 9 | Source0: {{{ git_cwd_pack }}} 10 | 11 | BuildRequires: meson 12 | BuildRequires: gcc 13 | BuildRequires: perl 14 | BuildRequires: pkgconfig(glib-2.0) 15 | BuildRequires: pkgconfig(libudev) 16 | BuildRequires: pkgconfig(x11) 17 | BuildRequires: pkgconfig(xi) 18 | BuildRequires: pkgconfig(xext) 19 | BuildRequires: pkgconfig(wireplumber-0.4) 20 | BuildRequires: python3-devel 21 | BuildRequires: python3-setuptools 22 | 23 | Requires: python3-dbus 24 | 25 | %description 26 | sessiond is a standalone X session manager that reports the idle status of a 27 | graphical session to systemd-logind. It can be used alongside a window manager 28 | or desktop environment that does not provide its own session management. 29 | 30 | %prep 31 | {{{ git_cwd_setup_macro }}} 32 | 33 | %build 34 | %meson --libdir lib 35 | %meson_build 36 | 37 | cd python-sessiond 38 | %py3_build 39 | 40 | %install 41 | %meson_install 42 | 43 | cd python-sessiond 44 | %py3_install 45 | 46 | %check 47 | %meson_test 48 | 49 | %files 50 | %license LICENSE 51 | %doc README.md 52 | %{_bindir}/sessionctl 53 | %{_bindir}/sessiond 54 | %{_bindir}/sessiond-inhibit 55 | /usr/lib/systemd/user/graphical-idle.target 56 | /usr/lib/systemd/user/graphical-lock.target 57 | /usr/lib/systemd/user/graphical-unidle.target 58 | /usr/lib/systemd/user/graphical-unlock.target 59 | /usr/lib/systemd/user/sessiond-session.target 60 | /usr/lib/systemd/user/sessiond.service 61 | /usr/lib/systemd/user/user-shutdown.target 62 | /usr/lib/systemd/user/user-sleep.target 63 | /usr/lib/systemd/user/user-sleep-finished.target 64 | %{_mandir}/man1/sessionctl.1.gz 65 | %{_mandir}/man1/sessiond-inhibit.1.gz 66 | %{_mandir}/man1/sessiond.1.gz 67 | %{_mandir}/man5/sessiond-hooks.5.gz 68 | %{_mandir}/man5/sessiond.conf.5.gz 69 | %{_mandir}/man8/sessiond-dbus.8.gz 70 | %{_datadir}/sessiond/sessiond.conf 71 | %{_datadir}/xsessions/sessiond.desktop 72 | 73 | %{python3_sitelib}/%{name}-*.egg-info/ 74 | %{python3_sitelib}/%{name}.py 75 | %{python3_sitelib}/__pycache__/%{name}.* 76 | 77 | %changelog 78 | {{{ git_cwd_changelog }}} 79 | -------------------------------------------------------------------------------- /src/backlight.c: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2018-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #define G_LOG_DOMAIN "sessiond" 19 | 20 | #include "backlight.h" 21 | #include "common.h" 22 | #include "config.h" 23 | #include "dbus-logind.h" 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #ifndef PREFIX 33 | #define PREFIX "/usr/local" 34 | #endif /* PREFIX */ 35 | 36 | #define SYSFS_WRITER PREFIX "/lib/sessiond/sessiond-sysfs-writer" 37 | 38 | static gchar * 39 | get_sys_path(const char *subsystem, const char *name) 40 | { 41 | return g_strjoin("/", "", "sys", "class", subsystem, name, NULL); 42 | } 43 | 44 | static gint32 45 | get_uint_sysattr(struct Backlight *b, const char *attr) 46 | { 47 | const char *str = udev_device_get_sysattr_value(b->device, attr); 48 | if (!str) 49 | return -1; 50 | gchar *end; 51 | errno = 0; 52 | guint64 i = g_ascii_strtoull(str, &end, 10); 53 | 54 | if (errno || end == str) { 55 | g_warning("Failed to parse backlight (%s) attr: %s", b->sys_path, attr); 56 | return -1; 57 | } 58 | 59 | return i; 60 | } 61 | 62 | static gboolean 63 | update_backlight(struct Backlight *bl, struct udev_device *dev) 64 | { 65 | if (bl->device) 66 | udev_device_unref(bl->device); 67 | if (!((bl->name = udev_device_get_sysname(dev)) && 68 | (bl->subsystem = udev_device_get_subsystem(dev)) && 69 | (bl->sys_path = get_sys_path(bl->subsystem, bl->name)) && 70 | (bl->dev_path = udev_device_get_devpath(dev)))) 71 | return FALSE; 72 | bl->device = udev_device_ref(dev); 73 | bl->brightness = get_uint_sysattr(bl, "brightness"); 74 | bl->max_brightness = get_uint_sysattr(bl, "max_brightness"); 75 | 76 | return TRUE; 77 | } 78 | 79 | static struct Backlight * 80 | new_backlight(struct udev_device *dev) 81 | { 82 | struct Backlight *bl = g_malloc(sizeof(struct Backlight)); 83 | 84 | bl->device = NULL; 85 | bl->online = TRUE; 86 | bl->pre_dim_brightness = -1; 87 | if (!update_backlight(bl, dev)) { 88 | g_free(bl); 89 | return NULL; 90 | } 91 | 92 | return bl; 93 | } 94 | 95 | static void 96 | free_backlight(struct Backlight *bl) 97 | { 98 | udev_device_unref(bl->device); 99 | g_free(bl->sys_path); 100 | g_free(bl); 101 | } 102 | 103 | static BacklightAction 104 | get_action(struct udev_device *dev) 105 | { 106 | const char *action = udev_device_get_action(dev); 107 | 108 | if (!action) 109 | return BL_ACTION_ADD; 110 | 111 | #define X(a, str) \ 112 | if (g_strcmp0(action, str) == 0) \ 113 | return BL_ACTION_##a; 114 | BL_ACTION_LIST 115 | #undef X 116 | 117 | return BL_ACTION_ADD; 118 | } 119 | 120 | static gboolean 121 | device_is_backlight(struct udev_device *dev) 122 | { 123 | const char *sys = udev_device_get_subsystem(dev); 124 | const char *path = udev_device_get_syspath(dev); 125 | gboolean bl = g_strcmp0(sys, "backlight") == 0; 126 | return bl || g_str_has_suffix(path, "::kbd_backlight"); 127 | } 128 | 129 | static gboolean 130 | source_prepare(GSource *source, gint *timeout) 131 | { 132 | Backlights *self = (Backlights *)source; 133 | *timeout = -1; 134 | return !g_queue_is_empty(self->queue); 135 | } 136 | 137 | static gboolean 138 | source_check(GSource *source) 139 | { 140 | Backlights *self = (Backlights *)source; 141 | GIOCondition revents = g_source_query_unix_fd(source, self->fd); 142 | 143 | if (revents & G_IO_IN) { 144 | struct udev_device *dev; 145 | while ((dev = udev_monitor_receive_device(self->udev_mon))) 146 | if (device_is_backlight(dev)) 147 | g_queue_push_tail(self->queue, dev); 148 | } 149 | 150 | return !g_queue_is_empty(self->queue); 151 | } 152 | 153 | static gboolean 154 | source_dispatch(GSource *source, GSourceFunc func, UNUSED gpointer user_data) 155 | { 156 | Backlights *self = (Backlights *)source; 157 | struct udev_device *dev = g_queue_pop_head(self->queue); 158 | gchar *sys_path = get_sys_path(udev_device_get_subsystem(dev), 159 | udev_device_get_sysname(dev)); 160 | BacklightAction action = get_action(dev); 161 | struct Backlight *bl = NULL; 162 | gboolean ret = TRUE; 163 | 164 | switch (action) { 165 | case BL_ACTION_ADD: 166 | bl = new_backlight(dev); 167 | if (!bl || !g_hash_table_insert(self->devices, (char *)sys_path, bl)) 168 | goto end; 169 | break; 170 | case BL_ACTION_REMOVE: 171 | if (!g_hash_table_remove(self->devices, sys_path)) 172 | goto end; 173 | break; 174 | case BL_ACTION_CHANGE: 175 | bl = g_hash_table_lookup(self->devices, sys_path); 176 | if (!bl) 177 | goto end; 178 | update_backlight(bl, dev); 179 | break; 180 | case BL_ACTION_ONLINE: 181 | bl = g_hash_table_lookup(self->devices, sys_path); 182 | if (!bl || bl->online) 183 | goto end; 184 | bl->online = TRUE; 185 | break; 186 | case BL_ACTION_OFFLINE: 187 | bl = g_hash_table_lookup(self->devices, sys_path); 188 | if (!bl || !bl->online) 189 | goto end; 190 | bl->online = FALSE; 191 | break; 192 | } 193 | 194 | ret = ((BacklightsFunc)func)(action, sys_path, bl); 195 | 196 | end: 197 | g_free(sys_path); 198 | udev_device_unref(dev); 199 | return ret; 200 | } 201 | 202 | static void 203 | source_finalize(GSource *source) 204 | { 205 | Backlights *self = (Backlights *)source; 206 | 207 | g_queue_free_full(self->queue, (GDestroyNotify)udev_device_unref); 208 | g_hash_table_unref(self->devices); 209 | udev_monitor_unref(self->udev_mon); 210 | udev_unref(self->udev); 211 | } 212 | 213 | static GSourceFuncs source_funcs = { 214 | source_prepare, 215 | source_check, 216 | source_dispatch, 217 | source_finalize, 218 | NULL, 219 | NULL, 220 | }; 221 | 222 | static void 223 | backlights_init_devices(Backlights *self, BacklightsFunc func) 224 | { 225 | struct udev_enumerate *e = udev_enumerate_new(self->udev); 226 | 227 | udev_enumerate_add_match_subsystem(e, "backlight"); 228 | udev_enumerate_add_match_subsystem(e, "leds"); 229 | udev_enumerate_scan_devices(e); 230 | 231 | struct udev_list_entry *devs = udev_enumerate_get_list_entry(e); 232 | struct udev_list_entry *entry; 233 | 234 | udev_list_entry_foreach(entry, devs) { 235 | const char *name = udev_list_entry_get_name(entry); 236 | struct udev_device *dev = udev_device_new_from_syspath(self->udev, 237 | name); 238 | 239 | if (!device_is_backlight(dev)) 240 | continue; 241 | 242 | struct Backlight *bl = new_backlight(dev); 243 | udev_device_unref(dev); 244 | 245 | if (g_hash_table_insert(self->devices, (char *)bl->sys_path, bl)) 246 | func(BL_ACTION_ADD, bl->sys_path, bl); 247 | } 248 | 249 | udev_enumerate_unref(e); 250 | } 251 | 252 | static gboolean 253 | set_backlight_brightness(struct Backlight *bl, guint32 v) 254 | { 255 | #ifndef BACKLIGHT_HELPER 256 | return FALSE; 257 | #endif 258 | 259 | if (bl->max_brightness == -1) 260 | return FALSE; 261 | 262 | v = MIN(v, bl->max_brightness); 263 | 264 | gchar *str = g_strdup_printf("%u", v); 265 | gchar *brightness = g_strjoin("/", bl->sys_path, "brightness", NULL); 266 | gchar *argv[] = {SYSFS_WRITER, brightness, str, NULL}; 267 | 268 | if (spawn_exec(argv) == 0) 269 | g_debug("Set %s brightness: %u", bl->sys_path, v); 270 | 271 | g_free(brightness); 272 | g_free(str); 273 | 274 | return TRUE; 275 | } 276 | 277 | static void 278 | backlight_dim_value(struct Backlight *bl, guint32 v, LogindContext *ctx) 279 | { 280 | bl->pre_dim_brightness = bl->brightness; 281 | backlight_set_brightness(bl, v, ctx); 282 | } 283 | 284 | static void 285 | backlight_dim_percent(struct Backlight *bl, gdouble percent, LogindContext *ctx) 286 | { 287 | gint32 v = bl->brightness; 288 | if (v == -1) 289 | return; 290 | bl->pre_dim_brightness = v; 291 | gdouble d = v - v * percent; 292 | backlight_set_brightness(bl, (guint32)(d > 0 ? d + 0.5 : d), ctx); 293 | } 294 | 295 | static void 296 | backlight_restore(struct Backlight *bl, LogindContext *ctx) 297 | { 298 | if (bl->pre_dim_brightness == -1) 299 | return; 300 | backlight_set_brightness(bl, bl->pre_dim_brightness, ctx); 301 | bl->pre_dim_brightness = -1; 302 | } 303 | 304 | Backlights * 305 | backlights_new(GMainContext *ctx, BacklightsFunc func) 306 | { 307 | GSource *source = g_source_new(&source_funcs, sizeof(Backlights)); 308 | Backlights *self = (Backlights *)source; 309 | 310 | self->udev = udev_new(); 311 | self->udev_mon = udev_monitor_new_from_netlink(self->udev, "udev"); 312 | 313 | udev_monitor_filter_add_match_subsystem_devtype(self->udev_mon, 314 | "backlight", NULL); 315 | udev_monitor_filter_add_match_subsystem_devtype(self->udev_mon, 316 | "leds", NULL); 317 | udev_monitor_enable_receiving(self->udev_mon); 318 | 319 | self->queue = g_queue_new(); 320 | self->devices = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, 321 | (GDestroyNotify)free_backlight); 322 | 323 | backlights_init_devices(self, func); 324 | 325 | self->fd = g_source_add_unix_fd(source, 326 | udev_monitor_get_fd(self->udev_mon), G_IO_IN); 327 | 328 | g_source_set_callback(source, (GSourceFunc)func, NULL, NULL); 329 | g_source_attach(source, ctx); 330 | 331 | return self; 332 | } 333 | 334 | void 335 | backlights_free(Backlights *bls) 336 | { 337 | if (!bls) 338 | return; 339 | GSource *source = (GSource *)bls; 340 | g_source_destroy(source); 341 | g_source_unref(source); 342 | } 343 | 344 | void 345 | backlights_restore(GHashTable *devs, LogindContext *ctx) 346 | { 347 | GHashTableIter iter; 348 | gpointer bl; 349 | 350 | g_hash_table_iter_init(&iter, devs); 351 | while (g_hash_table_iter_next(&iter, NULL, &bl)) 352 | backlight_restore(bl, ctx); 353 | } 354 | 355 | gchar * 356 | backlight_normalize_name(const char *name) 357 | { 358 | GRegex *regex = g_regex_new("::", 0, 0, NULL); 359 | gchar *norm = g_regex_replace(regex, name, -1, 0, "_", 0, NULL); 360 | g_regex_unref(regex); 361 | 362 | return norm; 363 | } 364 | 365 | gboolean 366 | backlight_set_brightness(struct Backlight *bl, guint32 v, LogindContext *ctx) 367 | { 368 | if (!logind_set_brightness(ctx, bl->subsystem, bl->name, v)) 369 | return set_backlight_brightness(bl, v); 370 | 371 | return TRUE; 372 | } 373 | 374 | void 375 | backlights_on_timeout(GHashTable *devs, GHashTable *cs, guint timeout, 376 | gboolean state, LogindContext *ctx) 377 | { 378 | GHashTableIter iter; 379 | gpointer key, val; 380 | 381 | g_hash_table_iter_init(&iter, cs); 382 | while (g_hash_table_iter_next(&iter, &key, &val)) { 383 | const char *sys_path = key; 384 | struct BacklightConf *c = val; 385 | 386 | if (c->dim_sec != timeout) 387 | continue; 388 | 389 | struct Backlight *bl = g_hash_table_lookup(devs, sys_path); 390 | 391 | if (!bl) 392 | continue; 393 | 394 | if (state) { 395 | if (c->dim_value != -1) 396 | backlight_dim_value(bl, c->dim_value, ctx); 397 | else 398 | backlight_dim_percent(bl, c->dim_percent, ctx); 399 | } else { 400 | backlight_restore(bl, ctx); 401 | } 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /src/backlight.h: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2018-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #pragma once 19 | 20 | #include "dbus-logind.h" 21 | 22 | #include 23 | #include 24 | 25 | #define BL_ACTION_LIST \ 26 | X(ADD, "add") \ 27 | X(REMOVE, "remove") \ 28 | X(CHANGE, "change") \ 29 | X(ONLINE, "online") \ 30 | X(OFFLINE, "offline") 31 | 32 | typedef enum { 33 | #define X(action, _) BL_ACTION_##action, 34 | BL_ACTION_LIST 35 | #undef X 36 | } BacklightAction; 37 | 38 | typedef enum { 39 | BL_TYPE_DISPLAY, 40 | BL_TYPE_KEYBOARD, 41 | } BacklightType; 42 | 43 | struct Backlight { 44 | struct udev_device *device; 45 | const char *name; 46 | const char *subsystem; 47 | gchar *sys_path; 48 | const char *dev_path; 49 | gboolean online; 50 | gint32 brightness; 51 | gint32 max_brightness; 52 | gint32 pre_dim_brightness; 53 | }; 54 | 55 | typedef gboolean (*BacklightsFunc)(BacklightAction a, const gchar *path, 56 | struct Backlight *bl); 57 | 58 | typedef struct { 59 | GSource source; 60 | gpointer fd; 61 | struct udev *udev; 62 | struct udev_monitor *udev_mon; 63 | GQueue *queue; 64 | GHashTable *devices; 65 | } Backlights; 66 | 67 | extern Backlights * 68 | backlights_new(GMainContext *ctx, BacklightsFunc func); 69 | extern void 70 | backlights_free(Backlights *bls); 71 | extern void 72 | backlights_restore(GHashTable *devs, LogindContext *ctx); 73 | extern gchar * 74 | backlight_normalize_name(const char *name); 75 | extern gboolean 76 | backlight_set_brightness(struct Backlight *bl, guint32 v, LogindContext *ctx); 77 | extern void 78 | backlights_on_timeout(GHashTable *devs, GHashTable *cs, guint timeout, 79 | gboolean state, LogindContext *ctx); 80 | -------------------------------------------------------------------------------- /src/common.c: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2018-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #include "common.h" 19 | 20 | #include 21 | #include 22 | 23 | gint 24 | spawn_exec(gchar **argv) 25 | { 26 | gint status = -1; 27 | gchar *std_out = NULL; 28 | gchar *std_err = NULL; 29 | GError *err = NULL; 30 | 31 | g_spawn_sync(NULL, argv, NULL, G_SPAWN_DEFAULT, NULL, NULL, 32 | &std_out, &std_err, &status, &err); 33 | 34 | if (err) 35 | goto error; 36 | 37 | if (std_out) { 38 | fprintf(stdout, "%s\n", std_out); 39 | g_free(std_out); 40 | } 41 | if (std_err) { 42 | fprintf(stderr, "%s\n", std_err); 43 | g_free(std_err); 44 | } 45 | 46 | g_spawn_check_wait_status(status, &err); 47 | 48 | if (err) { 49 | error: 50 | g_warning("%s", err->message); 51 | g_error_free(err); 52 | return status; 53 | } 54 | 55 | return status; 56 | } 57 | -------------------------------------------------------------------------------- /src/common.h: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2018-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #pragma once 19 | 20 | #include 21 | 22 | #define UNUSED G_GNUC_UNUSED 23 | #define BOOLSTR(b) ((b) ? "true" : "false") 24 | 25 | extern gint 26 | spawn_exec(gchar **argv); 27 | -------------------------------------------------------------------------------- /src/config.c: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2018-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #define G_LOG_DOMAIN "sessiond" 19 | 20 | #include "config.h" 21 | #include "backlight.h" 22 | #include "hooks.h" 23 | #include "xsource.h" 24 | 25 | #include 26 | #include 27 | #include "toml/toml.h" 28 | 29 | static gchar * 30 | subsystem_from_path(const gchar *path) 31 | { 32 | GRegex *regex = g_regex_new("/sys/class/(.*)/.*", 0, 0, NULL); 33 | 34 | GMatchInfo *match; 35 | g_regex_match(regex, path, 0, &match); 36 | gchar *sys = g_match_info_fetch(match, 1); 37 | 38 | g_match_info_free(match); 39 | g_regex_unref(regex); 40 | 41 | return sys; 42 | } 43 | 44 | static struct BacklightConf * 45 | new_backlight(const gchar *path) 46 | { 47 | struct BacklightConf *bl = g_malloc(sizeof(struct BacklightConf)); 48 | 49 | gchar *subsystem = subsystem_from_path(path); 50 | 51 | if (g_strcmp0(subsystem, "backlight") == 0) { 52 | bl->dim_sec = 60 * 8; 53 | bl->dim_value = -1; 54 | bl->dim_percent = 0.3; 55 | } else if (g_strcmp0(subsystem, "leds") == 0) { 56 | bl->dim_sec = 60; 57 | bl->dim_value = 0; 58 | bl->dim_percent = -1; 59 | } else { 60 | g_warning("Unrecognized backlight path: %s", path); 61 | g_free(bl); 62 | return NULL; 63 | } 64 | 65 | g_free(subsystem); 66 | return bl; 67 | } 68 | 69 | static toml_table_t * 70 | parse_file(const gchar *path) 71 | { 72 | FILE *fp = fopen(path, "r"); 73 | 74 | if (!fp) { 75 | perror("fopen"); 76 | return NULL; 77 | } 78 | 79 | char errbuf[BUFSIZ]; 80 | toml_table_t *tab = toml_parse_file(fp, errbuf, sizeof(errbuf)); 81 | 82 | if (!tab) 83 | g_warning("Failed to parse configuration %s: %s", path, errbuf); 84 | 85 | fclose(fp); 86 | 87 | return tab; 88 | } 89 | 90 | static gint 91 | load_int(toml_table_t *tab, const char *key, gint *ret) 92 | { 93 | const char *raw = toml_raw_in(tab, key); 94 | 95 | if (!raw) 96 | return 0; 97 | 98 | int64_t i; 99 | 100 | if (toml_rtoi(raw, &i) == -1) { 101 | g_warning("Failed to parse %s: expected integer", key); 102 | return -1; 103 | } 104 | 105 | *ret = (gint)i; 106 | return 0; 107 | } 108 | 109 | static gint 110 | load_uint(toml_table_t *tab, const char *key, guint *ret) 111 | { 112 | gint i = -1; 113 | gint r = load_int(tab, key, &i); 114 | if (i > -1) 115 | *ret = (guint)i; 116 | return r; 117 | } 118 | 119 | static gdouble 120 | load_double(toml_table_t *tab, const char *key, gdouble *ret) 121 | { 122 | const char *raw = toml_raw_in(tab, key); 123 | 124 | if (!raw) 125 | return 0; 126 | 127 | double d; 128 | 129 | if (toml_rtod(raw, &d) == -1) { 130 | g_warning("Failed to parse %s: expected double", key); 131 | return -1; 132 | } 133 | 134 | *ret = (gdouble)d; 135 | return 0; 136 | } 137 | 138 | static gint 139 | load_bool(toml_table_t *tab, const char *key, gboolean *ret) 140 | { 141 | const char *raw = toml_raw_in(tab, key); 142 | 143 | if (!raw) 144 | return 0; 145 | 146 | int b; 147 | 148 | if (toml_rtob(raw, &b) == -1) { 149 | g_warning("Failed to parse %s: expected boolean", key); 150 | return -1; 151 | } 152 | 153 | *ret = (gboolean)b; 154 | return 0; 155 | } 156 | 157 | static gint 158 | load_str(toml_table_t *tab, const char *key, gchar **ret) 159 | { 160 | const char *raw = toml_raw_in(tab, key); 161 | 162 | if (!raw) 163 | return 0; 164 | 165 | char *str; 166 | 167 | if (toml_rtos(raw, &str) == -1) { 168 | g_warning("Failed to parse %s: expected string", key); 169 | return -1; 170 | } 171 | 172 | *ret = g_strdup(str); 173 | return 0; 174 | } 175 | 176 | static gint 177 | load_exec(toml_table_t *tab, const char *key, gchar ***ret) 178 | { 179 | gchar *str = NULL; 180 | load_str(tab, key, &str); 181 | 182 | if (!str) 183 | return 0; 184 | 185 | gchar **argv; 186 | GError *err = NULL; 187 | g_shell_parse_argv(str, NULL, &argv, &err); 188 | 189 | if (err) { 190 | g_warning("Failed to parse %s: %s", key, err->message); 191 | g_error_free(err); 192 | g_free(str); 193 | return -1; 194 | } 195 | 196 | g_free(str); 197 | *ret = argv; 198 | return 0; 199 | } 200 | 201 | static gint 202 | load_input_mask(toml_table_t *tab, const char *key, guint *ret) 203 | { 204 | int len; 205 | toml_array_t *inputs = toml_array_in(tab, key); 206 | 207 | if (!inputs || (len = toml_array_nelem(inputs)) == 0) 208 | return 0; 209 | 210 | if (toml_array_kind(inputs) != 'v') { 211 | g_warning("Failed to parse %s: expected array of values", key); 212 | return -1; 213 | } 214 | 215 | if (toml_array_type(inputs) != 's') { 216 | g_warning("Failed to parse %s: expected array of strings", key); 217 | return -1; 218 | } 219 | 220 | guint mask = 0; 221 | 222 | for (int i = 0; i < toml_array_nelem(inputs); i++) { 223 | const char *raw = toml_raw_at(inputs, i); 224 | char *str; 225 | if (toml_rtos(raw, &str) == -1) { 226 | g_warning("Failed to parse %s at index %d", key, i); 227 | return -1; 228 | } 229 | #define X(t, n) \ 230 | if (g_strcmp0(str, n) == 0) { \ 231 | mask |= INPUT_TYPE_MASK(t); \ 232 | goto end; \ 233 | } 234 | INPUT_TYPE_LIST 235 | #undef X 236 | end: 237 | free(str); 238 | } 239 | 240 | *ret = mask; 241 | return 0; 242 | } 243 | 244 | static gint 245 | load_backlights(toml_table_t *tab, const char *key, GHashTable *out) 246 | { 247 | int len; 248 | toml_array_t *backlights = toml_array_in(tab, key); 249 | 250 | if (!backlights || (len = toml_array_nelem(backlights)) == 0) 251 | return 0; 252 | 253 | if (toml_array_kind(backlights) != 't') { 254 | g_warning("Failed to parse %s: expected array of tables", key); 255 | return -1; 256 | } 257 | 258 | for (int i = 0; i < len; i++) { 259 | toml_table_t *t = toml_table_at(backlights, i); 260 | 261 | gchar *path = NULL; 262 | load_str(t, "Path", &path); 263 | 264 | if (!path) 265 | continue; 266 | 267 | struct BacklightConf *bl = new_backlight(path); 268 | 269 | if (!bl) 270 | continue; 271 | 272 | #define X(key, type, name) \ 273 | load_##type(t, key, &bl->name); 274 | BACKLIGHT_TABLE_LIST 275 | #undef X 276 | 277 | if (bl->dim_percent != -1) 278 | bl->dim_percent = CLAMP(bl->dim_percent, 0.01, 1.0); 279 | 280 | g_hash_table_insert(out, path, bl); 281 | } 282 | 283 | return 0; 284 | } 285 | 286 | static gint 287 | load_trigger(toml_table_t *tab, const char *key, guint *ret) 288 | { 289 | gchar *str = NULL; 290 | load_str(tab, key, &str); 291 | 292 | if (!str) 293 | return 0; 294 | 295 | #define X(t, n) \ 296 | if (g_strcmp0(str, n) == 0) { \ 297 | *ret = HOOK_TRIGGER_##t; \ 298 | return 0; \ 299 | } 300 | HOOK_TRIGGER_LIST 301 | #undef X 302 | 303 | g_warning("Failed to parse %s: unknown trigger %s", key, str); 304 | return -1; 305 | } 306 | 307 | static void 308 | free_hook(struct Hook *hook) 309 | { 310 | if (!hook) 311 | return; 312 | g_strfreev(hook->exec_start); 313 | g_strfreev(hook->exec_stop); 314 | g_free(hook); 315 | } 316 | 317 | static gint 318 | load_hook(toml_table_t *tab, GPtrArray *out, const gchar **err) 319 | { 320 | struct Hook *h = g_malloc0(sizeof(struct Hook)); 321 | 322 | #define X(key, type, name) \ 323 | load_##type(tab, key, &h->name); 324 | HOOKS_TABLE_LIST 325 | #undef X 326 | 327 | if (!h->trigger) { 328 | *err = "expected Trigger key"; 329 | goto err; 330 | } else if (h->trigger == HOOK_TRIGGER_INACTIVE) { 331 | if (!h->inactive_sec) { 332 | *err = "expected InactiveSec key"; 333 | goto err; 334 | } 335 | } else if (!(h->exec_start || h->exec_stop)) { 336 | *err = "expected ExecStart key or ExecStop key"; 337 | goto err; 338 | } 339 | 340 | g_ptr_array_add(out, h); 341 | return 0; 342 | 343 | err: 344 | free_hook(h); 345 | return -1; 346 | } 347 | 348 | static gint 349 | load_hooks(toml_table_t *tab, const char *key, GPtrArray *out) 350 | { 351 | int len; 352 | toml_array_t *hooks = toml_array_in(tab, key); 353 | 354 | if (!hooks || (len = toml_array_nelem(hooks)) == 0) 355 | return 0; 356 | 357 | if (toml_array_kind(hooks) != 't') { 358 | g_warning("Failed to parse %s: expected array of tables", key); 359 | return -1; 360 | } 361 | 362 | for (int i = 0; i < len; i++) { 363 | const gchar *err; 364 | if (load_hook(toml_table_at(hooks, i), out, &err) == -1) { 365 | g_warning("Failed to parse %s at index %d: %s", key, i, err); 366 | return -1; 367 | } 368 | } 369 | 370 | return 0; 371 | } 372 | 373 | static gint 374 | load_hooks_dir(const gchar *path, GPtrArray *out) 375 | { 376 | if (!g_file_test(path, G_FILE_TEST_IS_DIR)) { 377 | g_debug("Load hooks: directory not found at %s; skipping", path); 378 | return 0; 379 | } 380 | 381 | GError *err = NULL; 382 | GDir *dir = g_dir_open(path, 0, &err); 383 | 384 | if (err) { 385 | g_warning("Failed to load hook files: %s", err->message); 386 | g_error_free(err); 387 | return -1; 388 | } 389 | 390 | gint ret = 0; 391 | const gchar *name; 392 | 393 | while ((name = g_dir_read_name(dir))) { 394 | if (!g_str_has_suffix(name, ".hook")) 395 | continue; 396 | 397 | gchar *p = g_strjoin("/", path, name, NULL); 398 | toml_table_t *hook = parse_file(p); 399 | 400 | toml_table_t *tab = toml_table_in(hook, "Hook"); 401 | 402 | if (!tab) { 403 | g_warning("Failed to parse hook %s: expected Hook table", p); 404 | ret = -1; 405 | goto end; 406 | } 407 | 408 | const gchar *err; 409 | if (load_hook(tab, out, &err) == -1) { 410 | g_warning("Failed to parse hook %s: %s", p, err); 411 | ret = -1; 412 | } 413 | 414 | end: 415 | g_free(p); 416 | } 417 | 418 | g_dir_close(dir); 419 | return ret; 420 | } 421 | 422 | Config 423 | config_new(void) 424 | { 425 | Config c; 426 | 427 | c.input_mask = INPUT_TYPE_MASK(RawMotion) 428 | | INPUT_TYPE_MASK(RawButtonPress) | INPUT_TYPE_MASK(RawKeyPress); 429 | c.idle_sec = 60 * 20; 430 | c.on_idle = TRUE; 431 | c.on_sleep = TRUE; 432 | c.backlights = NULL; 433 | c.hooks = NULL; 434 | #ifdef DPMS 435 | c.dpms_enable = TRUE; 436 | c.standby_sec = 60 * 10; 437 | c.suspend_sec = 60 * 10; 438 | c.off_sec = 60 * 10; 439 | 440 | c.lock_standby_sec = 60; 441 | c.lock_suspend_sec = 60; 442 | c.lock_off_sec = 60; 443 | #endif /* DPMS */ 444 | 445 | return c; 446 | } 447 | 448 | gboolean 449 | config_load(const gchar *path, const gchar *hooksd, Config *c) 450 | { 451 | toml_table_t *conf = parse_file(path); 452 | 453 | if (!conf) 454 | return FALSE; 455 | 456 | gint ret = 0; 457 | toml_table_t *tab; 458 | 459 | #define X(key, type, name) \ 460 | ret += load_##type(tab, key, &c->name); 461 | 462 | if ((tab = toml_table_in(conf, "Idle"))) { 463 | IDLE_TABLE_LIST 464 | } 465 | 466 | if ((tab = toml_table_in(conf, "Lock"))) { 467 | LOCK_TABLE_LIST 468 | } 469 | 470 | #ifdef DPMS 471 | if ((tab = toml_table_in(conf, "DPMS"))) { 472 | DPMS_TABLE_LIST 473 | } 474 | 475 | if ((tab = toml_table_in(conf, "Lock"))) { 476 | DPMS_LOCK_TABLE_LIST 477 | } 478 | #endif /* DPMS */ 479 | 480 | #ifdef WIREPLUMBER 481 | if ((tab = toml_table_in(conf, "Lock"))) { 482 | WP_LOCK_TABLE_LIST 483 | } 484 | #endif /* WIREPLUMBER */ 485 | 486 | #undef X 487 | 488 | c->backlights = g_hash_table_new_full(g_str_hash, g_str_equal, 489 | (GDestroyNotify)g_free, (GDestroyNotify)g_free); 490 | 491 | ret += load_backlights(conf, "Backlight", c->backlights); 492 | 493 | if (!g_hash_table_size(c->backlights)) { 494 | g_hash_table_unref(c->backlights); 495 | c->backlights = NULL; 496 | } 497 | 498 | c->hooks = g_ptr_array_new_with_free_func((GDestroyNotify)free_hook); 499 | 500 | ret += load_hooks(conf, "Hook", c->hooks); 501 | 502 | if (hooksd) 503 | ret += load_hooks_dir(hooksd, c->hooks); 504 | 505 | if (!c->hooks->len) { 506 | g_ptr_array_unref(c->hooks); 507 | c->hooks = NULL; 508 | } 509 | 510 | toml_free(conf); 511 | 512 | return ret == 0; 513 | } 514 | 515 | void 516 | config_free(Config *c) 517 | { 518 | if (!c) 519 | return; 520 | 521 | if (c->backlights) 522 | g_hash_table_unref(c->backlights); 523 | if (c->hooks) 524 | g_ptr_array_free(c->hooks, TRUE); 525 | } 526 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2018-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #pragma once 19 | 20 | #include 21 | 22 | #define IDLE_TABLE_LIST \ 23 | X("Inputs", input_mask, input_mask) \ 24 | X("IdleSec", uint, idle_sec) 25 | 26 | #define LOCK_TABLE_LIST \ 27 | X("OnIdle", bool, on_idle) \ 28 | X("OnSleep", bool, on_sleep) 29 | 30 | #define BACKLIGHT_TABLE_LIST \ 31 | X("DimSec", uint, dim_sec) \ 32 | X("DimValue", int, dim_value) \ 33 | X("DimPercent", double, dim_percent) 34 | 35 | #ifdef DPMS 36 | #define DPMS_TABLE_LIST \ 37 | X("Enable", bool, dpms_enable) \ 38 | X("StandbySec", uint, standby_sec) \ 39 | X("SuspendSec", uint, suspend_sec) \ 40 | X("OffSec", uint, off_sec) 41 | 42 | #define DPMS_LOCK_TABLE_LIST \ 43 | X("StandbySec", uint, lock_standby_sec) \ 44 | X("SuspendSec", uint, lock_suspend_sec) \ 45 | X("OffSec", uint, lock_off_sec) 46 | #endif /* DPMS */ 47 | 48 | #ifdef WIREPLUMBER 49 | #define WP_LOCK_TABLE_LIST \ 50 | X("MuteAudio", bool, mute_audio) 51 | #endif /* WIREPLUMBER */ 52 | 53 | struct BacklightConf { 54 | guint dim_sec; 55 | gint dim_value; 56 | gdouble dim_percent; 57 | }; 58 | 59 | typedef struct { 60 | /* Idle */ 61 | guint input_mask; 62 | guint idle_sec; 63 | /* Lock */ 64 | gboolean on_idle; 65 | gboolean on_sleep; 66 | /* Backlights */ 67 | GHashTable *backlights; 68 | /* Hooks */ 69 | GPtrArray *hooks; 70 | #ifdef DPMS 71 | /* DPMS */ 72 | gboolean dpms_enable; 73 | guint standby_sec; 74 | guint suspend_sec; 75 | guint off_sec; 76 | 77 | guint lock_standby_sec; 78 | guint lock_suspend_sec; 79 | guint lock_off_sec; 80 | #endif /* DPMS */ 81 | #ifdef WIREPLUMBER 82 | /* WIREPLUMBER */ 83 | gboolean mute_audio; 84 | #endif /* WIREPLUMBER */ 85 | } Config; 86 | 87 | extern Config 88 | config_new(void); 89 | extern gboolean 90 | config_load(const gchar *path, const gchar *hooksd, Config *c); 91 | extern void 92 | config_free(Config *c); 93 | -------------------------------------------------------------------------------- /src/dbus-audiosink.c: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2021 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #define G_LOG_DOMAIN "sessiond" 19 | 20 | #include "dbus-audiosink.h" 21 | #include "dbus-server.h" 22 | #include "dbus-gen.h" 23 | #include "wireplumber.h" 24 | 25 | #include 26 | 27 | #define INT_PTR(i) GINT_TO_POINTER(i) 28 | 29 | static void 30 | set_default_audio_sink_property(DBusServer *s, DBusAudioSink *das) 31 | { 32 | const gchar *path = g_dbus_interface_skeleton_get_object_path( 33 | G_DBUS_INTERFACE_SKELETON(das)); 34 | dbus_session_set_default_audio_sink(s->session, path); 35 | dbus_session_emit_change_default_audio_sink(s->session, path); 36 | } 37 | 38 | static void 39 | set_audio_sinks_property(DBusServer *s) 40 | { 41 | GList *dass = g_hash_table_get_values(s->audiosinks); 42 | const gchar **paths = g_malloc0_n(g_list_length(dass) + 1, sizeof(gchar *)); 43 | 44 | guint n = 0; 45 | for (GList *i = dass; i; i = i->next, n++) { 46 | DBusAudioSink *das = i->data; 47 | const gchar *path = g_dbus_interface_skeleton_get_object_path( 48 | G_DBUS_INTERFACE_SKELETON(das)); 49 | if (path) 50 | paths[n] = path; 51 | } 52 | 53 | dbus_session_set_audio_sinks(s->session, paths); 54 | g_free(paths); 55 | } 56 | 57 | static gboolean 58 | on_handle_set_volume(DBusAudioSink *das, GDBusMethodInvocation *i, 59 | gdouble v, DBusServer *s) 60 | { 61 | if (!s->wp_conn) 62 | return FALSE; 63 | 64 | guint32 id = dbus_audio_sink_get_id(das); 65 | 66 | if (!audiosink_set_volume(id, v, s->wp_conn)) { 67 | g_dbus_method_invocation_return_dbus_error(i, 68 | DBUS_AUDIOSINK_ERROR ".SetVolume", 69 | "Failed to set volume"); 70 | return TRUE; 71 | } 72 | 73 | dbus_audio_sink_complete_set_volume(das, i); 74 | 75 | return TRUE; 76 | } 77 | 78 | static gboolean 79 | on_handle_inc_volume(DBusAudioSink *das, GDBusMethodInvocation *i, 80 | gdouble v, DBusServer *s) 81 | { 82 | if (!s->wp_conn) 83 | return FALSE; 84 | 85 | guint32 id = dbus_audio_sink_get_id(das); 86 | gdouble vol = dbus_audio_sink_get_volume(das); 87 | vol = MIN(MAX(vol + v, 0.0), 1.0); 88 | 89 | if (!audiosink_set_volume(id, vol, s->wp_conn)) { 90 | g_dbus_method_invocation_return_dbus_error(i, 91 | DBUS_AUDIOSINK_ERROR ".IncVolume", 92 | "Failed to increment volume"); 93 | return TRUE; 94 | } 95 | 96 | dbus_audio_sink_complete_inc_volume(das, i, vol); 97 | 98 | return TRUE; 99 | } 100 | 101 | static gboolean 102 | on_handle_set_mute(DBusAudioSink *das, GDBusMethodInvocation *i, 103 | gboolean m, DBusServer *s) 104 | { 105 | if (!s->wp_conn) 106 | return FALSE; 107 | 108 | guint32 id = dbus_audio_sink_get_id(das); 109 | 110 | if (!audiosink_set_mute(id, m, s->wp_conn)) { 111 | g_dbus_method_invocation_return_dbus_error(i, 112 | DBUS_AUDIOSINK_ERROR ".SetMute", 113 | "Failed to set mute state"); 114 | return TRUE; 115 | } 116 | 117 | dbus_audio_sink_complete_set_mute(das, i); 118 | 119 | return TRUE; 120 | } 121 | 122 | static gboolean 123 | on_handle_toggle_mute(DBusAudioSink *das, GDBusMethodInvocation *i, 124 | DBusServer *s) 125 | { 126 | if (!s->wp_conn) 127 | return FALSE; 128 | 129 | guint32 id = dbus_audio_sink_get_id(das); 130 | gboolean mute = !dbus_audio_sink_get_mute(das); 131 | 132 | if (!audiosink_set_mute(id, mute, s->wp_conn)) { 133 | g_dbus_method_invocation_return_dbus_error(i, 134 | DBUS_AUDIOSINK_ERROR ".ToggleMute", 135 | "Failed to toggle mute state"); 136 | return TRUE; 137 | } 138 | 139 | dbus_audio_sink_complete_toggle_mute(das, i, mute); 140 | 141 | return TRUE; 142 | } 143 | 144 | static void 145 | update_audiosink(DBusAudioSink *das, struct AudioSink *as) 146 | { 147 | if (as->name) { 148 | const gchar *str = dbus_audio_sink_get_name(das); 149 | if (!str || g_strcmp0(str, as->name) != 0) 150 | dbus_audio_sink_set_name(das, as->name); 151 | } 152 | 153 | if (dbus_audio_sink_get_id(das) != as->id) 154 | dbus_audio_sink_set_id(das, as->id); 155 | 156 | if (dbus_audio_sink_get_mute(das) != as->mute) { 157 | dbus_audio_sink_set_mute(das, as->mute); 158 | dbus_audio_sink_emit_change_mute(das, as->mute); 159 | } 160 | 161 | if (dbus_audio_sink_get_volume(das) != as->volume) { 162 | dbus_audio_sink_set_volume(das, as->volume); 163 | dbus_audio_sink_emit_change_volume(das, as->volume); 164 | } 165 | } 166 | 167 | gboolean 168 | dbus_server_export_audiosink(DBusServer *s, DBusAudioSink *das) 169 | { 170 | guint32 id = dbus_audio_sink_get_id(das); 171 | gchar *path = g_strdup_printf("%s/%d", DBUS_AUDIOSINK_PATH, id); 172 | 173 | GError *err = NULL; 174 | g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(das), s->conn, 175 | path, &err); 176 | 177 | if (err) { 178 | g_error("Failed to export DBus AudioSink interface: %s", err->message); 179 | g_error_free(err); 180 | g_free(path); 181 | return FALSE; 182 | } 183 | 184 | set_audio_sinks_property(s); 185 | 186 | dbus_session_emit_add_audio_sink(s->session, path); 187 | g_free(path); 188 | 189 | return TRUE; 190 | } 191 | 192 | void 193 | dbus_server_unexport_audiosink(DBusServer *s, DBusAudioSink *das) 194 | { 195 | GDBusInterfaceSkeleton *skel = G_DBUS_INTERFACE_SKELETON(das); 196 | gchar *path = g_strdup(g_dbus_interface_skeleton_get_object_path(skel)); 197 | 198 | g_dbus_interface_skeleton_unexport(skel); 199 | set_audio_sinks_property(s); 200 | 201 | dbus_session_emit_remove_audio_sink(s->session, path); 202 | g_free(path); 203 | } 204 | 205 | void 206 | dbus_server_add_audiosink(DBusServer *s, struct AudioSink *as) 207 | { 208 | 209 | DBusAudioSink *das = dbus_audio_sink_skeleton_new(); 210 | g_hash_table_insert(s->audiosinks, INT_PTR(as->id), das); 211 | 212 | g_signal_connect(das, "handle-set-volume", 213 | G_CALLBACK(on_handle_set_volume), s); 214 | g_signal_connect(das, "handle-inc-volume", 215 | G_CALLBACK(on_handle_inc_volume), s); 216 | g_signal_connect(das, "handle-set-mute", 217 | G_CALLBACK(on_handle_set_mute), s); 218 | g_signal_connect(das, "handle-toggle-mute", 219 | G_CALLBACK(on_handle_toggle_mute), s); 220 | 221 | update_audiosink(das, as); 222 | 223 | if (s->name_acquired) 224 | dbus_server_export_audiosink(s, das); 225 | } 226 | 227 | void 228 | dbus_server_remove_audiosink(DBusServer *s, guint32 id) 229 | { 230 | DBusAudioSink *das = g_hash_table_lookup(s->audiosinks, INT_PTR(id)); 231 | 232 | if (!das) 233 | return; 234 | 235 | g_hash_table_steal(s->audiosinks, INT_PTR(id)); 236 | dbus_server_unexport_audiosink(s, das); 237 | g_object_unref(das); 238 | } 239 | 240 | void 241 | dbus_server_update_audiosink(DBusServer *s, struct AudioSink *as) 242 | { 243 | DBusAudioSink *das = g_hash_table_lookup(s->audiosinks, INT_PTR(as->id)); 244 | 245 | if (das) 246 | update_audiosink(das, as); 247 | } 248 | 249 | void 250 | dbus_server_update_default_audiosink(DBusServer *s, guint32 id) 251 | { 252 | DBusAudioSink *das = g_hash_table_lookup(s->audiosinks, INT_PTR(id)); 253 | 254 | if (das) 255 | set_default_audio_sink_property(s, das); 256 | } 257 | -------------------------------------------------------------------------------- /src/dbus-audiosink.h: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2021 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #pragma once 19 | 20 | #include "dbus-server.h" 21 | #include "wireplumber.h" 22 | 23 | #include 24 | 25 | #define DBUS_AUDIOSINK_ERROR DBUS_NAME ".AudioSink.Error" 26 | #define DBUS_AUDIOSINK_PATH DBUS_PATH "/audiosink" 27 | 28 | extern gboolean 29 | dbus_server_export_audiosink(DBusServer *s, DBusAudioSink *das); 30 | extern void 31 | dbus_server_unexport_audiosink(DBusServer *s, DBusAudioSink *das); 32 | extern void 33 | dbus_server_add_audiosink(DBusServer *s, struct AudioSink *as); 34 | extern void 35 | dbus_server_remove_audiosink(DBusServer *s, guint32 id); 36 | extern void 37 | dbus_server_update_audiosink(DBusServer *s, struct AudioSink *as); 38 | extern void 39 | dbus_server_update_default_audiosink(DBusServer *s, guint32 id); 40 | -------------------------------------------------------------------------------- /src/dbus-backlight.c: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2019-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #define G_LOG_DOMAIN "sessiond" 19 | 20 | #include "dbus-backlight.h" 21 | #include "dbus-server.h" 22 | #include "dbus-gen.h" 23 | #include "backlight.h" 24 | 25 | #include 26 | 27 | static void 28 | set_backlights_property(DBusServer *s) 29 | { 30 | GList *dbls = g_hash_table_get_values(s->backlights); 31 | const gchar **paths = g_malloc0_n(g_list_length(dbls) + 1, sizeof(gchar *)); 32 | 33 | guint n = 0; 34 | for (GList *i = dbls; i; i = i->next, n++) { 35 | DBusBacklight *dbl = i->data; 36 | const gchar *path = g_dbus_interface_skeleton_get_object_path( 37 | G_DBUS_INTERFACE_SKELETON(dbl)); 38 | if (path) 39 | paths[n] = path; 40 | } 41 | 42 | dbus_session_set_backlights(s->session, paths); 43 | g_free(paths); 44 | } 45 | 46 | static gboolean 47 | on_handle_set_brightness(DBusBacklight *dbl, GDBusMethodInvocation *i, 48 | guint32 v, gpointer user_data) 49 | { 50 | DBusServer *s = (DBusServer *)user_data; 51 | 52 | if (!s->bl_devices) 53 | return FALSE; 54 | 55 | const gchar *sys_path = dbus_backlight_get_sys_path(dbl); 56 | if (!sys_path) 57 | return FALSE; 58 | struct Backlight *bl = g_hash_table_lookup(s->bl_devices, sys_path); 59 | if (!bl) 60 | return FALSE; 61 | 62 | if (!backlight_set_brightness(bl, v, s->ctx)) { 63 | g_dbus_method_invocation_return_dbus_error(i, 64 | DBUS_BACKLIGHT_ERROR ".SetBrightness", 65 | "Failed to set brightness"); 66 | return TRUE; 67 | } 68 | 69 | dbus_backlight_complete_set_brightness(dbl, i); 70 | return TRUE; 71 | } 72 | 73 | static gboolean 74 | on_handle_inc_brightness(DBusBacklight *dbl, GDBusMethodInvocation *i, 75 | gint v, gpointer user_data) 76 | { 77 | DBusServer *s = (DBusServer *)user_data; 78 | 79 | if (!s->bl_devices) 80 | return FALSE; 81 | 82 | const gchar *sys_path = dbus_backlight_get_sys_path(dbl); 83 | if (!sys_path) 84 | return FALSE; 85 | struct Backlight *bl = g_hash_table_lookup(s->bl_devices, sys_path); 86 | if (!bl) 87 | return FALSE; 88 | 89 | guint b = MAX(bl->brightness + v, 0); 90 | if (!backlight_set_brightness(bl, b, s->ctx)) { 91 | g_dbus_method_invocation_return_dbus_error(i, 92 | DBUS_BACKLIGHT_ERROR ".IncBrightness", 93 | "Failed to increment brightness"); 94 | return TRUE; 95 | } 96 | 97 | dbus_backlight_complete_inc_brightness(dbl, i, b); 98 | return TRUE; 99 | } 100 | 101 | static void 102 | update_backlight(DBusBacklight *dbl, struct Backlight *bl) 103 | { 104 | const gchar *str = NULL; 105 | #define SET_STR(n) \ 106 | str = dbus_backlight_get_##n(dbl); \ 107 | if (!str || g_strcmp0(str, bl->n) != 0) \ 108 | dbus_backlight_set_##n(dbl, bl->n) 109 | SET_STR(name); 110 | SET_STR(subsystem); 111 | SET_STR(sys_path); 112 | SET_STR(dev_path); 113 | #undef SET_STR 114 | 115 | #define SET_INT(n) \ 116 | if (dbus_backlight_get_##n(dbl) != bl->n) \ 117 | dbus_backlight_set_##n(dbl, bl->n) 118 | SET_INT(online); 119 | SET_INT(brightness); 120 | SET_INT(max_brightness); 121 | #undef SET_INT 122 | } 123 | 124 | gboolean 125 | dbus_server_export_backlight(DBusServer *s, DBusBacklight *dbl) 126 | { 127 | const gchar *name = dbus_backlight_get_name(dbl); 128 | gchar *norm = backlight_normalize_name(name); 129 | gchar *path = g_strdup_printf("%s/%s", DBUS_BACKLIGHT_PATH, 130 | norm ? norm : name); 131 | 132 | if (norm) 133 | g_free(norm); 134 | 135 | GError *err = NULL; 136 | g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(dbl), s->conn, 137 | path, &err); 138 | 139 | if (err) { 140 | g_error("Failed to export DBus Backlight interface: %s", err->message); 141 | g_error_free(err); 142 | g_free(path); 143 | return FALSE; 144 | } 145 | 146 | set_backlights_property(s); 147 | 148 | dbus_session_emit_add_backlight(s->session, path); 149 | g_free(path); 150 | 151 | return TRUE; 152 | } 153 | 154 | void 155 | dbus_server_unexport_backlight(DBusServer *s, DBusBacklight *dbl) 156 | { 157 | GDBusInterfaceSkeleton *skel = G_DBUS_INTERFACE_SKELETON(dbl); 158 | gchar *path = g_strdup(g_dbus_interface_skeleton_get_object_path(skel)); 159 | 160 | g_dbus_interface_skeleton_unexport(skel); 161 | set_backlights_property(s); 162 | 163 | dbus_session_emit_remove_backlight(s->session, path); 164 | g_free(path); 165 | } 166 | 167 | void 168 | dbus_server_add_backlight(DBusServer *s, struct Backlight *bl) 169 | { 170 | DBusBacklight *dbl = dbus_backlight_skeleton_new(); 171 | g_hash_table_insert(s->backlights, (char *)bl->sys_path, dbl); 172 | 173 | g_signal_connect(dbl, "handle-set-brightness", 174 | G_CALLBACK(on_handle_set_brightness), s); 175 | g_signal_connect(dbl, "handle-inc-brightness", 176 | G_CALLBACK(on_handle_inc_brightness), s); 177 | 178 | update_backlight(dbl, bl); 179 | 180 | if (s->name_acquired) 181 | dbus_server_export_backlight(s, dbl); 182 | } 183 | 184 | void 185 | dbus_server_remove_backlight(DBusServer *s, const char *path) 186 | { 187 | DBusBacklight *dbl = g_hash_table_lookup(s->backlights, path); 188 | 189 | if (!dbl) 190 | return; 191 | 192 | g_hash_table_steal(s->backlights, path); 193 | dbus_server_unexport_backlight(s, dbl); 194 | g_object_unref(dbl); 195 | } 196 | 197 | void 198 | dbus_server_update_backlight(DBusServer *s, struct Backlight *bl) 199 | { 200 | DBusBacklight *dbl = g_hash_table_lookup(s->backlights, bl->sys_path); 201 | 202 | if (dbl) 203 | update_backlight(dbl, bl); 204 | } 205 | -------------------------------------------------------------------------------- /src/dbus-backlight.h: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2019-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #pragma once 19 | 20 | #include "dbus-server.h" 21 | 22 | #include 23 | 24 | #define DBUS_BACKLIGHT_ERROR DBUS_NAME ".Backlight.Error" 25 | #define DBUS_BACKLIGHT_PATH DBUS_PATH "/backlight" 26 | 27 | extern gboolean 28 | dbus_server_export_backlight(DBusServer *s, DBusBacklight *dbl); 29 | extern void 30 | dbus_server_unexport_backlight(DBusServer *s, DBusBacklight *dbl); 31 | extern void 32 | dbus_server_add_backlight(DBusServer *s, struct Backlight *bl); 33 | extern void 34 | dbus_server_remove_backlight(DBusServer *s, const char *path); 35 | extern void 36 | dbus_server_update_backlight(DBusServer *s, struct Backlight *bl); 37 | -------------------------------------------------------------------------------- /src/dbus-gen.c: -------------------------------------------------------------------------------- 1 | ../builddir/dbus-gen.c -------------------------------------------------------------------------------- /src/dbus-gen.h: -------------------------------------------------------------------------------- 1 | ../builddir/dbus-gen.h -------------------------------------------------------------------------------- /src/dbus-logind.c: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2018-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #define G_LOG_DOMAIN "sessiond" 19 | 20 | #include "dbus-logind.h" 21 | #include "common.h" 22 | #include "config.h" 23 | 24 | #include 25 | #include 26 | 27 | #define LOGIND_NAME "org.freedesktop.login1" 28 | #define LOGIND_MANAGER_IFACE LOGIND_NAME ".Manager" 29 | #define LOGIND_SESSION_IFACE LOGIND_NAME ".Session" 30 | #define LOGIND_USER_IFACE LOGIND_NAME ".User" 31 | #define LOGIND_PATH "/org/freedesktop/login1" 32 | #define LOGIND_USER_PATH LOGIND_PATH "/user/self" 33 | 34 | G_DEFINE_TYPE(LogindContext, logind_context, G_TYPE_OBJECT); 35 | 36 | enum { 37 | LOCK_SIGNAL, 38 | SLEEP_SIGNAL, 39 | SHUTDOWN_SIGNAL, 40 | APPEAR_SIGNAL, 41 | VANISH_SIGNAL, 42 | LAST_SIGNAL, 43 | }; 44 | 45 | static guint signals[LAST_SIGNAL] = {0}; 46 | 47 | static void 48 | logind_on_session_signal(UNUSED GDBusProxy *proxy, UNUSED gchar *sender, 49 | gchar *signal, UNUSED GVariant *params, 50 | gpointer user_data) 51 | { 52 | LogindContext *c = (LogindContext *)user_data; 53 | 54 | if (g_strcmp0(signal, "Lock") == 0) { 55 | g_debug("Lock signal received"); 56 | g_signal_emit(c, signals[LOCK_SIGNAL], 0, TRUE); 57 | } else if (g_strcmp0(signal, "Unlock") == 0) { 58 | g_debug("Unlock signal received"); 59 | g_signal_emit(c, signals[LOCK_SIGNAL], 0, FALSE); 60 | } 61 | } 62 | 63 | static void 64 | logind_on_manager_signal(UNUSED GDBusProxy *proxy, UNUSED gchar *sender, 65 | gchar *signal, GVariant *params, gpointer user_data) 66 | { 67 | LogindContext *c = (LogindContext *)user_data; 68 | 69 | gboolean state; 70 | 71 | if (g_strcmp0(signal, "PrepareForSleep") == 0) { 72 | g_variant_get(params, "(b)", &state); 73 | g_debug("PrepareForSleep signal received: %s", BOOLSTR(state)); 74 | g_signal_emit(c, signals[SLEEP_SIGNAL], 0, state); 75 | } else if (g_strcmp0(signal, "PrepareForShutdown") == 0) { 76 | g_variant_get(params, "(b)", &state); 77 | g_debug("PrepareForShutdown signal received: %s", BOOLSTR(state)); 78 | g_signal_emit(c, signals[SHUTDOWN_SIGNAL], 0, state); 79 | } 80 | } 81 | 82 | static gchar * 83 | logind_get_session(GDBusConnection *conn, gchar **id) 84 | { 85 | GError *err = NULL; 86 | GDBusProxy *proxy = g_dbus_proxy_new_sync( 87 | conn, G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS, NULL, LOGIND_NAME, 88 | LOGIND_USER_PATH, LOGIND_USER_IFACE, NULL, &err); 89 | 90 | if (err) { 91 | g_warning("%s", err->message); 92 | g_error_free(err); 93 | return NULL; 94 | } 95 | 96 | gchar *path = NULL; 97 | GVariant *display = g_dbus_proxy_get_cached_property(proxy, "Display"); 98 | 99 | if (!display) { 100 | g_warning("%s does not have Display property", LOGIND_USER_IFACE); 101 | goto error; 102 | } 103 | 104 | g_variant_get(display, "(so)", id, &path); 105 | 106 | if (!*id) { 107 | g_warning("Failed to read Display session Id"); 108 | goto error; 109 | } 110 | 111 | if (!path) 112 | g_warning("Failed to read Display session object path"); 113 | 114 | error: 115 | if (display) 116 | g_variant_unref(display); 117 | g_object_unref(proxy); 118 | 119 | return path; 120 | } 121 | 122 | static void 123 | logind_get_property(LogindContext *c, const gchar *prop, const gchar *fmt, 124 | gpointer out) 125 | { 126 | if (!c->logind_session) { 127 | g_warning("Cannot get %s: %s does not exist", prop, LOGIND_NAME); 128 | return; 129 | } 130 | 131 | GVariant *v = g_dbus_proxy_get_cached_property(c->logind_session, 132 | prop); 133 | if (v) { 134 | g_variant_get(v, fmt, out); 135 | g_variant_unref(v); 136 | } else { 137 | g_warning("Failed to get logind %s", prop); 138 | } 139 | } 140 | 141 | gboolean 142 | logind_get_locked_hint(LogindContext *c) 143 | { 144 | gboolean b = FALSE; 145 | logind_get_property(c, "LockedHint", "b", &b); 146 | return b; 147 | } 148 | 149 | gboolean 150 | logind_get_idle_hint(LogindContext *c) 151 | { 152 | gboolean b = FALSE; 153 | logind_get_property(c, "IdleHint", "b", &b); 154 | return b; 155 | } 156 | 157 | guint64 158 | logind_get_idle_since_hint(LogindContext *c) 159 | { 160 | guint64 i = 0; 161 | logind_get_property(c, "IdleSinceHint", "t", &i); 162 | return i; 163 | } 164 | 165 | guint64 166 | logind_get_idle_since_hint_monotonic(LogindContext *c) 167 | { 168 | guint64 i = 0; 169 | logind_get_property(c, "IdleSinceHintMonotonic", "t", &i); 170 | return i; 171 | } 172 | 173 | static void 174 | logind_on_appear(GDBusConnection *conn, const gchar *name, const gchar *owner, 175 | gpointer user_data) 176 | { 177 | g_debug("%s appeared (owned by %s)", name, owner); 178 | 179 | LogindContext *c = (LogindContext *)user_data; 180 | gchar *path = logind_get_session(conn, &c->session_id); 181 | GError *err = NULL; 182 | 183 | if (path) { 184 | c->logind_session = g_dbus_proxy_new_sync( 185 | conn, G_DBUS_PROXY_FLAGS_NONE, NULL, LOGIND_NAME, 186 | path, LOGIND_SESSION_IFACE, NULL, &err); 187 | 188 | if (err) { 189 | g_warning("%s", err->message); 190 | g_error_free(err); 191 | err = NULL; 192 | } else { 193 | g_signal_connect(c->logind_session, "g-signal", 194 | G_CALLBACK(logind_on_session_signal), user_data); 195 | g_debug("Using logind session %s: %s", c->session_id, path); 196 | } 197 | g_free(path); 198 | } 199 | 200 | c->logind_manager = g_dbus_proxy_new_sync( 201 | conn, G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES, NULL, LOGIND_NAME, 202 | LOGIND_PATH, LOGIND_MANAGER_IFACE, NULL, &err); 203 | 204 | if (err) { 205 | g_warning("%s", err->message); 206 | g_error_free(err); 207 | } else { 208 | g_signal_connect(c->logind_manager, "g-signal", 209 | G_CALLBACK(logind_on_manager_signal), user_data); 210 | } 211 | 212 | g_signal_emit(c, signals[APPEAR_SIGNAL], 0); 213 | } 214 | 215 | static void 216 | logind_on_vanish(UNUSED GDBusConnection *conn, const gchar *name, 217 | gpointer user_data) 218 | { 219 | g_debug("%s vanished", name); 220 | 221 | LogindContext *c = (LogindContext *)user_data; 222 | g_signal_emit(c, signals[VANISH_SIGNAL], 0); 223 | logind_context_free(c); 224 | } 225 | 226 | static void 227 | logind_context_class_init(LogindContextClass *c) 228 | { 229 | GType type = G_OBJECT_CLASS_TYPE(c); 230 | 231 | signals[LOCK_SIGNAL] = g_signal_new("lock", 232 | type, G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, 233 | G_TYPE_BOOLEAN); 234 | signals[SLEEP_SIGNAL] = g_signal_new("sleep", 235 | type, G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, 236 | G_TYPE_BOOLEAN); 237 | signals[SHUTDOWN_SIGNAL] = g_signal_new("shutdown", 238 | type, G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, 239 | G_TYPE_BOOLEAN); 240 | signals[APPEAR_SIGNAL] = g_signal_new("appear", 241 | type, G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); 242 | signals[VANISH_SIGNAL] = g_signal_new("vanish", 243 | type, G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); 244 | } 245 | 246 | static void 247 | logind_context_init(LogindContext *self) 248 | { 249 | self->logind_watcher = g_bus_watch_name( 250 | G_BUS_TYPE_SYSTEM, LOGIND_NAME, G_BUS_NAME_WATCHER_FLAGS_NONE, 251 | logind_on_appear, logind_on_vanish, self, NULL); 252 | } 253 | 254 | void 255 | logind_set_idle_hint(LogindContext *c, gboolean state) 256 | { 257 | if (!c->logind_session) { 258 | g_warning("Cannot set IdleHint: %s does not exist", LOGIND_NAME); 259 | return; 260 | } 261 | 262 | GError *err = NULL; 263 | g_dbus_proxy_call_sync(c->logind_session, "SetIdleHint", 264 | g_variant_new("(b)", state), G_DBUS_CALL_FLAGS_NONE, 265 | -1, NULL, &err); 266 | 267 | if (err) { 268 | g_warning("%s", err->message); 269 | g_error_free(err); 270 | } else { 271 | g_debug("IdleHint set to %s", BOOLSTR(state)); 272 | } 273 | } 274 | 275 | /* Locking session automatically updates logind LockedHint. */ 276 | void 277 | logind_lock_session(LogindContext *c, gboolean state) 278 | { 279 | #define STR(b) ((b) ? "Lock" : "Unlock") 280 | 281 | if (!c->logind_session) { 282 | g_warning("Cannot %s session: %s does not exist", STR(state), 283 | LOGIND_NAME); 284 | return; 285 | } 286 | 287 | GError *err = NULL; 288 | g_dbus_proxy_call_sync(c->logind_session, STR(state), NULL, 289 | G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); 290 | 291 | if (err) { 292 | g_warning("%s", err->message); 293 | g_error_free(err); 294 | } else { 295 | g_debug("%sed session", STR(state)); 296 | } 297 | 298 | #undef STR 299 | } 300 | 301 | /* Set LockedHint manually when responding to Lock signal. */ 302 | void 303 | logind_set_locked_hint(LogindContext *c, gboolean state) 304 | { 305 | if (!c->logind_session) { 306 | g_warning("Cannot set LockedHint: %s does not exist", LOGIND_NAME); 307 | return; 308 | } 309 | 310 | GError *err = NULL; 311 | g_dbus_proxy_call_sync(c->logind_session, "SetLockedHint", 312 | g_variant_new("(b)", state), G_DBUS_CALL_FLAGS_NONE, 313 | -1, NULL, &err); 314 | 315 | if (err) { 316 | g_warning("%s", err->message); 317 | g_error_free(err); 318 | } else { 319 | g_debug("LockedHint set to %s", BOOLSTR(state)); 320 | } 321 | } 322 | 323 | gboolean 324 | logind_set_brightness(LogindContext *c, const char *sys, const char *name, 325 | guint32 v) 326 | { 327 | if (!c->logind_session) { 328 | g_warning("Cannot set brightness: %s does not exist", LOGIND_NAME); 329 | return FALSE; 330 | } 331 | 332 | GError *err = NULL; 333 | g_dbus_proxy_call_sync(c->logind_session, "SetBrightness", 334 | g_variant_new("(ssu)", sys, name, v), 335 | G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); 336 | if (err) { 337 | g_error_free(err); 338 | return FALSE; 339 | } 340 | 341 | return TRUE; 342 | } 343 | 344 | LogindContext * 345 | logind_context_new(void) 346 | { 347 | return g_object_new(LOGIND_TYPE_CONTEXT, NULL); 348 | } 349 | 350 | void 351 | logind_context_free(LogindContext *c) 352 | { 353 | if (!c) 354 | return; 355 | if (c->session_id) 356 | g_free(c->session_id); 357 | if (c->logind_watcher) { 358 | g_bus_unwatch_name(c->logind_watcher); 359 | c->logind_watcher = 0; 360 | } 361 | if (c->logind_session) { 362 | g_object_unref(c->logind_session); 363 | c->logind_session = NULL; 364 | } 365 | if (c->logind_manager) { 366 | g_object_unref(c->logind_manager); 367 | c->logind_manager = NULL; 368 | } 369 | g_object_unref(c); 370 | } 371 | -------------------------------------------------------------------------------- /src/dbus-logind.h: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2018-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #pragma once 19 | 20 | #include 21 | #include 22 | 23 | #define LOGIND_TYPE_CONTEXT logind_context_get_type() 24 | G_DECLARE_FINAL_TYPE(LogindContext, logind_context, LOGIND, CONTEXT, GObject); 25 | 26 | struct _LogindContext { 27 | GObject parent; 28 | gchar *session_id; 29 | guint logind_watcher; 30 | GDBusProxy *logind_session; 31 | GDBusProxy *logind_manager; 32 | }; 33 | 34 | extern void 35 | logind_set_idle_hint(LogindContext *c, gboolean state); 36 | extern void 37 | logind_lock_session(LogindContext *c, gboolean state); 38 | extern void 39 | logind_set_locked_hint(LogindContext *c, gboolean state); 40 | extern gboolean 41 | logind_get_locked_hint(LogindContext *c); 42 | extern gboolean 43 | logind_get_idle_hint(LogindContext *c); 44 | extern guint64 45 | logind_get_idle_since_hint(LogindContext *c); 46 | extern guint64 47 | logind_get_idle_since_hint_monotonic(LogindContext *c); 48 | extern gboolean 49 | logind_set_brightness(LogindContext *c, const char *sys, const char *name, 50 | guint32 v); 51 | extern LogindContext * 52 | logind_context_new(void); 53 | extern void 54 | logind_context_free(LogindContext *c); 55 | -------------------------------------------------------------------------------- /src/dbus-server.h: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2019-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #pragma once 19 | 20 | #include "dbus-logind.h" 21 | #include "dbus-gen.h" 22 | #include "backlight.h" 23 | 24 | #ifdef WIREPLUMBER 25 | #include "wireplumber.h" 26 | #endif /* WIREPLUMBER */ 27 | 28 | #include 29 | 30 | #define DBUS_NAME "org.sessiond.session1" 31 | #define DBUS_SESSION_ERROR DBUS_NAME ".Session.Error" 32 | #define DBUS_PATH "/org/sessiond/session1" 33 | 34 | #define DBUS_TYPE_SERVER dbus_server_get_type() 35 | G_DECLARE_FINAL_TYPE(DBusServer, dbus_server, DBUS, SERVER, GObject); 36 | 37 | struct _DBusServer { 38 | GObject parent; 39 | guint bus_id; 40 | GDBusConnection *conn; 41 | gboolean name_acquired; 42 | DBusSession *session; 43 | LogindContext *ctx; 44 | GHashTable *inhibitors; 45 | GHashTable *backlights; 46 | GHashTable *bl_devices; 47 | 48 | #ifdef WIREPLUMBER 49 | WpConn *wp_conn; 50 | GHashTable *audiosinks; 51 | #endif /* WIREPLUMBER */ 52 | }; 53 | 54 | extern void 55 | dbus_server_free(DBusServer *s); 56 | extern void 57 | dbus_server_emit_active(DBusServer *s); 58 | extern void 59 | dbus_server_emit_inactive(DBusServer *s, guint i); 60 | extern DBusServer * 61 | dbus_server_new(LogindContext *c); 62 | -------------------------------------------------------------------------------- /src/dbus-systemd.c: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2018-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #define G_LOG_DOMAIN "sessiond" 19 | 20 | #include "dbus-systemd.h" 21 | #include "common.h" 22 | #include "config.h" 23 | 24 | #include 25 | #include 26 | 27 | #define SYSTEMD_NAME "org.freedesktop.systemd1" 28 | #define SYSTEMD_MANAGER_IFACE SYSTEMD_NAME ".Manager" 29 | #define SYSTEMD_PATH "/org/freedesktop/systemd1" 30 | 31 | static void 32 | systemd_on_appear(GDBusConnection *conn, const gchar *name, const gchar *owner, 33 | gpointer user_data) 34 | { 35 | g_debug("%s appeared (owned by %s)", name, owner); 36 | 37 | SystemdContext *c = (SystemdContext *)user_data; 38 | GError *err = NULL; 39 | 40 | c->systemd_manager = g_dbus_proxy_new_sync( 41 | conn, G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | 42 | G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS, NULL, SYSTEMD_NAME, 43 | SYSTEMD_PATH, SYSTEMD_MANAGER_IFACE, NULL, &err); 44 | 45 | if (err) { 46 | g_warning("%s", err->message); 47 | g_error_free(err); 48 | } 49 | } 50 | 51 | static void 52 | systemd_on_vanish(UNUSED GDBusConnection *conn, const gchar *name, 53 | gpointer user_data) 54 | { 55 | g_debug("%s vanished", name); 56 | systemd_context_free((SystemdContext *)user_data); 57 | } 58 | 59 | void 60 | systemd_start_unit(SystemdContext *c, const gchar *name) 61 | { 62 | if (!c->systemd_manager) { 63 | g_warning("Cannot start unit %s: %s does not exist", name, 64 | SYSTEMD_NAME); 65 | return; 66 | } 67 | 68 | GError *err = NULL; 69 | g_dbus_proxy_call_sync(c->systemd_manager, "StartUnit", 70 | g_variant_new("(ss)", name, "replace"), 71 | G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); 72 | 73 | if (err) { 74 | g_warning("%s", err->message); 75 | g_error_free(err); 76 | } else { 77 | g_debug("Started unit %s", name); 78 | } 79 | } 80 | 81 | SystemdContext * 82 | systemd_context_new(void) 83 | { 84 | SystemdContext *c = g_malloc0(sizeof(SystemdContext)); 85 | 86 | c->systemd_watcher = g_bus_watch_name( 87 | G_BUS_TYPE_SESSION, SYSTEMD_NAME, G_BUS_NAME_WATCHER_FLAGS_NONE, 88 | systemd_on_appear, systemd_on_vanish, c, NULL); 89 | 90 | return c; 91 | } 92 | 93 | void 94 | systemd_context_free(SystemdContext *c) 95 | { 96 | if (!c) 97 | return; 98 | if (c->systemd_watcher) { 99 | g_bus_unwatch_name(c->systemd_watcher); 100 | c->systemd_watcher = 0; 101 | } 102 | if (c->systemd_manager) { 103 | g_object_unref(c->systemd_manager); 104 | c->systemd_manager = NULL; 105 | } 106 | g_free(c); 107 | } 108 | -------------------------------------------------------------------------------- /src/dbus-systemd.h: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2018-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #pragma once 19 | 20 | #include 21 | #include 22 | 23 | typedef struct { 24 | guint systemd_watcher; 25 | GDBusProxy *systemd_manager; 26 | } SystemdContext; 27 | 28 | extern void 29 | systemd_start_unit(SystemdContext *c, const gchar *name); 30 | extern SystemdContext * 31 | systemd_context_new(void); 32 | extern void 33 | systemd_context_free(SystemdContext *c); 34 | -------------------------------------------------------------------------------- /src/helper/sessiond-sysfs-writer.c: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2018-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | int 24 | main(int argc, char *argv[]) 25 | { 26 | char path[MAXPATHLEN]; 27 | FILE *f; 28 | 29 | if (argc != 3) { 30 | fprintf(stderr, "Expected 2 arguments: PATH VALUE\n"); 31 | return EXIT_FAILURE; 32 | } 33 | 34 | if (strncmp("/sys", argv[1], 4) == 0) { 35 | f = fopen(argv[1], "w"); 36 | } else { 37 | snprintf(path, sizeof(path), "/sys%s", argv[1]); 38 | f = fopen(path, "w"); 39 | } 40 | 41 | if (!f) { 42 | perror("Failed to open path"); 43 | return EXIT_FAILURE; 44 | } 45 | 46 | if (fprintf(f, "%s", argv[2]) < 0) { 47 | fclose(f); 48 | fprintf(stderr, "Failed to write to path\n"); 49 | return EXIT_FAILURE; 50 | } 51 | 52 | fclose(f); 53 | } 54 | -------------------------------------------------------------------------------- /src/hooks.c: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2018-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #include "hooks.h" 19 | #include "common.h" 20 | #include "timeline.h" 21 | 22 | #include 23 | 24 | static void 25 | run_hooks_timeout(GPtrArray *hooks, HookTrigger trigger, gboolean state, 26 | guint timeout) 27 | { 28 | for (guint i = 0; i < hooks->len; i++) { 29 | struct Hook *h = g_ptr_array_index(hooks, i); 30 | if (h->trigger != trigger 31 | || (trigger == HOOK_TRIGGER_INACTIVE 32 | && h->inactive_sec != timeout)) 33 | continue; 34 | if (state && h->exec_start) 35 | spawn_exec(h->exec_start); 36 | else if (!state && h->exec_stop) 37 | spawn_exec(h->exec_stop); 38 | } 39 | } 40 | 41 | void 42 | hooks_add_timeouts(GPtrArray *hooks, Timeline *tl) 43 | { 44 | for (guint i = 0; i < hooks->len; i++) { 45 | struct Hook *h = g_ptr_array_index(hooks, i); 46 | if (h->trigger == HOOK_TRIGGER_INACTIVE) 47 | timeline_add_timeout(tl, h->inactive_sec); 48 | } 49 | } 50 | 51 | void 52 | hooks_run(GPtrArray *hooks, HookTrigger trigger, gboolean state) 53 | { 54 | if (trigger == HOOK_TRIGGER_INACTIVE) 55 | return; 56 | run_hooks_timeout(hooks, trigger, state, 0); 57 | } 58 | 59 | void 60 | hooks_on_timeout(GPtrArray *hooks, guint timeout, gboolean state) 61 | { 62 | run_hooks_timeout(hooks, HOOK_TRIGGER_INACTIVE, state, timeout); 63 | } 64 | -------------------------------------------------------------------------------- /src/hooks.h: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2018-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #pragma once 19 | 20 | #include "timeline.h" 21 | 22 | #include 23 | 24 | #define HOOKS_TABLE_LIST \ 25 | X("Trigger", trigger, trigger) \ 26 | X("InactiveSec", uint, inactive_sec) \ 27 | X("ExecStart", exec, exec_start) \ 28 | X("ExecStop", exec, exec_stop) 29 | 30 | #define HOOK_TRIGGER_LIST \ 31 | X(LOCK, "Lock") \ 32 | X(IDLE, "Idle") \ 33 | X(SLEEP, "Sleep") \ 34 | X(SHUTDOWN, "Shutdown") \ 35 | X(INACTIVE, "Inactive") 36 | 37 | typedef enum { 38 | HOOK_TRIGGER_NONE, 39 | #define X(trigger, _) HOOK_TRIGGER_##trigger, 40 | HOOK_TRIGGER_LIST 41 | #undef X 42 | } HookTrigger; 43 | 44 | struct Hook { 45 | HookTrigger trigger; 46 | guint inactive_sec; 47 | gchar **exec_start; 48 | gchar **exec_stop; 49 | }; 50 | 51 | extern void 52 | hooks_add_timeouts(GPtrArray *hooks, Timeline *tl); 53 | extern void 54 | hooks_run(GPtrArray *hooks, HookTrigger trigger, gboolean state); 55 | extern void 56 | hooks_on_timeout(GPtrArray *hooks, guint timeout, gboolean state); 57 | -------------------------------------------------------------------------------- /src/timeline.c: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2018-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #define G_LOG_DOMAIN "sessiond-activity" 19 | 20 | #include "timeline.h" 21 | 22 | #include 23 | 24 | #define INACTIVE_SEC(tl) \ 25 | ((g_get_monotonic_time() - tl->inactive_since) / 1000000) 26 | 27 | static void 28 | add_timeout(Timeline *tl, guint timeout); 29 | 30 | static gint 31 | compare_timeouts(gconstpointer a, gconstpointer b) 32 | { 33 | guint time_a = *(guint *)a; 34 | guint time_b = *(guint *)b; 35 | 36 | if (time_a < time_b) 37 | return -1; 38 | if (time_a > time_b) 39 | return 1; 40 | 41 | return 0; 42 | } 43 | 44 | static guint 45 | get_timeout(Timeline *tl) 46 | { 47 | return g_array_index(tl->timeouts, guint, tl->index); 48 | } 49 | 50 | static gboolean 51 | on_timeout(gpointer user_data) 52 | { 53 | Timeline *tl = (Timeline *)user_data; 54 | 55 | if (!tl || !tl->running) 56 | return G_SOURCE_REMOVE; 57 | 58 | guint inactive = INACTIVE_SEC(tl); 59 | guint timeout = get_timeout(tl); 60 | 61 | if (inactive >= timeout) { 62 | tl->index++; 63 | tl->func(timeout, TRUE, tl->user_data); 64 | if (tl->index < tl->timeouts->len) 65 | add_timeout(tl, get_timeout(tl) - inactive); 66 | else 67 | timeline_stop(tl); 68 | } else { 69 | add_timeout(tl, timeout - inactive); 70 | } 71 | 72 | return G_SOURCE_REMOVE; 73 | } 74 | 75 | static void 76 | remove_source(Timeline *tl) 77 | { 78 | if (!tl->source) 79 | return; 80 | g_debug("timeline: Removed timeout source"); 81 | g_source_destroy(tl->source); 82 | g_source_unref(tl->source); 83 | tl->source = NULL; 84 | } 85 | 86 | static void 87 | add_timeout(Timeline *tl, guint timeout) 88 | { 89 | remove_source(tl); 90 | 91 | tl->source = g_timeout_source_new_seconds(timeout); 92 | g_source_set_callback(tl->source, on_timeout, tl, NULL); 93 | g_source_attach(tl->source, tl->ctx); 94 | 95 | g_debug("timeline: Added %us timeout source", timeout); 96 | } 97 | 98 | Timeline 99 | timeline_new(GMainContext *ctx, TimelineFunc func, gconstpointer user_data) 100 | { 101 | Timeline tl; 102 | tl.running = FALSE; 103 | tl.ctx = ctx; 104 | tl.source = NULL; 105 | tl.timeouts = g_array_new(FALSE, FALSE, sizeof(guint)); 106 | tl.index = 0; 107 | tl.inactive_since = -1; 108 | tl.func = func; 109 | tl.user_data = user_data; 110 | return tl; 111 | } 112 | 113 | gboolean 114 | timeline_add_timeout(Timeline *tl, guint timeout) 115 | { 116 | for (guint i = 0; i < tl->timeouts->len; i++) 117 | if (g_array_index(tl->timeouts, guint, i) == timeout) 118 | return FALSE; 119 | 120 | g_array_append_val(tl->timeouts, timeout); 121 | g_debug("timeline: Added %us timeout", timeout); 122 | 123 | if (tl->running) { 124 | remove_source(tl); 125 | g_array_sort(tl->timeouts, compare_timeouts); 126 | tl->index = 0; 127 | guint inactive = INACTIVE_SEC(tl); 128 | for (guint i = 0; i < tl->timeouts->len; i++) { 129 | guint t = g_array_index(tl->timeouts, guint, i); 130 | if (t <= inactive) 131 | tl->index++; 132 | } 133 | if (inactive >= timeout) 134 | tl->func(timeout, TRUE, tl->user_data); 135 | add_timeout(tl, get_timeout(tl) - inactive); 136 | } 137 | 138 | return TRUE; 139 | } 140 | 141 | gboolean 142 | timeline_remove_timeout(Timeline *tl, guint timeout) 143 | { 144 | for (guint i = 0; i < tl->timeouts->len; i++) 145 | if (g_array_index(tl->timeouts, guint, i) == timeout) { 146 | g_array_remove_index(tl->timeouts, i); 147 | if (i <= tl->index) 148 | tl->index--; 149 | return TRUE; 150 | } 151 | 152 | return FALSE; 153 | } 154 | 155 | void 156 | timeline_start(Timeline *tl) 157 | { 158 | if (tl->running) { 159 | while (tl->index) { 160 | tl->index--; 161 | tl->func(get_timeout(tl), FALSE, tl->user_data); 162 | } 163 | } else if (tl->timeouts->len > 0) { 164 | g_array_sort(tl->timeouts, compare_timeouts); 165 | tl->running = TRUE; 166 | } else { 167 | return; 168 | } 169 | 170 | tl->index = 0; 171 | add_timeout(tl, get_timeout(tl)); 172 | tl->inactive_since = g_get_monotonic_time(); 173 | } 174 | 175 | void 176 | timeline_stop(Timeline *tl) 177 | { 178 | if (!tl->running) 179 | return; 180 | if (!tl->index) 181 | tl->running = FALSE; 182 | tl->inactive_since = -1; 183 | remove_source(tl); 184 | } 185 | 186 | guint 187 | timeline_pending_timeouts(Timeline *tl) 188 | { 189 | return tl->timeouts->len - tl->index; 190 | } 191 | 192 | void 193 | timeline_free(Timeline *tl) 194 | { 195 | if (!tl) 196 | return; 197 | timeline_stop(tl); 198 | if (tl->timeouts) 199 | g_array_free(tl->timeouts, TRUE); 200 | } 201 | -------------------------------------------------------------------------------- /src/timeline.h: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2018-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #pragma once 19 | 20 | #include 21 | 22 | typedef void (*TimelineFunc)(guint timeout, gboolean state, 23 | gconstpointer user_data); 24 | 25 | typedef struct { 26 | gboolean running; 27 | GMainContext *ctx; 28 | GSource *source; 29 | GArray *timeouts; 30 | guint index; 31 | gint64 inactive_since; 32 | TimelineFunc func; 33 | gconstpointer user_data; 34 | } Timeline; 35 | 36 | extern Timeline 37 | timeline_new(GMainContext *ctx, TimelineFunc func, gconstpointer user_data); 38 | extern gboolean 39 | timeline_add_timeout(Timeline *tl, guint timeout); 40 | extern gboolean 41 | timeline_remove_timeout(Timeline *tl, guint timeout); 42 | extern void 43 | timeline_start(Timeline *tl); 44 | extern void 45 | timeline_stop(Timeline *tl); 46 | extern guint 47 | timeline_pending_timeouts(Timeline *tl); 48 | extern void 49 | timeline_free(Timeline *tl); 50 | -------------------------------------------------------------------------------- /src/toml/.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | 3 | # Object files 4 | *.o 5 | *.ko 6 | *.obj 7 | *.elf 8 | 9 | # Precompiled Headers 10 | *.gch 11 | *.pch 12 | 13 | # Libraries 14 | *.lib 15 | *.a 16 | *.la 17 | *.lo 18 | 19 | # Shared objects (inc. Windows DLLs) 20 | *.dll 21 | *.so 22 | *.so.* 23 | *.dylib 24 | 25 | # Executables 26 | *.exe 27 | *.out 28 | *.app 29 | *.i*86 30 | *.x86_64 31 | *.hex 32 | toml_cat 33 | toml_json 34 | 35 | # Debug files 36 | *.dSYM/ 37 | *.su 38 | -------------------------------------------------------------------------------- /src/toml/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 CK Tan 4 | https://github.com/cktan/tomlc99 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/toml/Makefile: -------------------------------------------------------------------------------- 1 | CFILES = toml.c 2 | 3 | CFLAGS = -std=c99 -Wall -Wextra -fpic 4 | # to compile for debug: make DEBUG=1 5 | # to compile for no debug: make 6 | ifdef DEBUG 7 | CFLAGS += -O0 -g 8 | else 9 | CFLAGS += -O2 -DNDEBUG 10 | endif 11 | 12 | EXEC = toml_json toml_cat 13 | 14 | LIB = libtoml.a 15 | LIB_SHARED = libtoml.so 16 | 17 | all: $(LIB) $(LIB_SHARED) $(EXEC) 18 | 19 | 20 | libtoml.a: toml.o 21 | ar -rcs $@ $^ 22 | 23 | libtoml.so: toml.o 24 | $(CC) -shared -o $@ $^ 25 | 26 | toml_json: toml_json.c $(LIB) 27 | 28 | toml_cat: toml_cat.c $(LIB) 29 | 30 | prefix ?= /usr/local 31 | 32 | install: all 33 | install -d ${prefix}/include ${prefix}/lib 34 | install toml.h ${prefix}/include 35 | install $(LIB) ${prefix}/lib 36 | install $(LIB_SHARED) ${prefix}/lib 37 | 38 | clean: 39 | rm -f *.o $(EXEC) $(LIB) $(LIB_SHARED) 40 | -------------------------------------------------------------------------------- /src/toml/README.md: -------------------------------------------------------------------------------- 1 | # tomlc99 2 | TOML in c99; v0.5.0 compliant. 3 | 4 | 5 | # Usage 6 | 7 | Please see the `toml.h` file for details. What follows is a simple example that 8 | parses this config file: 9 | 10 | ``` 11 | [server] 12 |    host = "www.example.com" 13 | port = 80 14 | ``` 15 | 16 | For each config param, the code first extracts a raw value and then 17 | convert it to a string or integer depending on context. 18 | 19 | ``` 20 | 21 | FILE* fp; 22 | toml_table_t* conf; 23 | toml_table_t* server; 24 | const char* raw; 25 | char* host; 26 | int64_t port; 27 | char errbuf[200]; 28 | 29 | /* open file and parse */ 30 | if (0 == (fp = fopen(FNAME, "r"))) { 31 | return handle_error(); 32 | } 33 | conf = toml_parse_file(fp, errbuf, sizeof(errbuf)); 34 | fclose(fp); 35 | if (0 == conf) { 36 | return handle_error(); 37 | } 38 | 39 | /* locate the [server] table */ 40 | if (0 == (server = toml_table_in(conf, "server"))) { 41 | return handle_error(); 42 | } 43 | 44 | /* extract host config value */ 45 | if (0 == (raw = toml_raw_in(server, "host"))) { 46 | return handle_error(); 47 | } 48 | if (toml_rtos(raw, &host)) { 49 | return handle_error(); 50 | } 51 | 52 | /* extract port config value */ 53 | if (0 == (raw = toml_raw_in(server, "port"))) { 54 | return handle_error(); 55 | } 56 | if (toml_rtoi(raw, &port)) { 57 | return handle_error(); 58 | } 59 | 60 | /* done with conf */ 61 | toml_free(conf); 62 | 63 | /* use host and port */ 64 | do_work(host, port); 65 | 66 | /* clean up */ 67 | free(host); 68 | ``` 69 | 70 | 71 | # Building 72 | 73 | A normal *make* suffices. Alternately, you can also simply include the 74 | `toml.c` and `toml.h` files in your project. 75 | 76 | # Testing 77 | 78 | To test against the standard test set provided by BurntSushi/toml-test: 79 | 80 | ``` 81 | % make 82 | % cd test1 83 | % bash build.sh # do this once 84 | % bash run.sh # this will run the test suite 85 | ``` 86 | 87 | 88 | To test against the standard test set provided by iarna/toml: 89 | 90 | ``` 91 | % make 92 | % cd test2 93 | % bash build.sh # do this once 94 | % bash run.sh # this will run the test suite 95 | ``` 96 | -------------------------------------------------------------------------------- /src/toml/test1/.gitignore: -------------------------------------------------------------------------------- 1 | /goworkspace 2 | /toml-test 3 | /toml-test-decoder 4 | -------------------------------------------------------------------------------- /src/toml/test1/README.md: -------------------------------------------------------------------------------- 1 | How to run the tests 2 | === 3 | 4 | ``` 5 | % bash build.sh 6 | % bash run.sh 7 | 77 passed, 0 failed 8 | ``` 9 | 10 | -------------------------------------------------------------------------------- /src/toml/test1/build.sh: -------------------------------------------------------------------------------- 1 | 2 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 3 | 4 | mkdir -p $DIR/goworkspace 5 | export GOPATH=$DIR/goworkspace 6 | go get github.com/BurntSushi/toml-test # install test suite 7 | go get github.com/BurntSushi/toml/cmd/toml-test-decoder # e.g., install my parser 8 | cp $GOPATH/bin/* . 9 | 10 | -------------------------------------------------------------------------------- /src/toml/test1/extra/array_of_tables.toml: -------------------------------------------------------------------------------- 1 | x = [ {'a'= 1}, {'a'= 2} ] 2 | -------------------------------------------------------------------------------- /src/toml/test1/extra/inline_array.toml: -------------------------------------------------------------------------------- 1 | x = [1,2,3] 2 | -------------------------------------------------------------------------------- /src/toml/test1/extra/inline_table.toml: -------------------------------------------------------------------------------- 1 | x = {'a'= 1, 'b'= 2 } 2 | -------------------------------------------------------------------------------- /src/toml/test1/run.sh: -------------------------------------------------------------------------------- 1 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 2 | export GOPATH=$DIR/goworkspace # if it isn't already set 3 | # $GOPATH/bin/toml-test $GOPATH/bin/toml-test-decoder # e.g., run tests on my parser 4 | $GOPATH/bin/toml-test ../toml_json 5 | -------------------------------------------------------------------------------- /src/toml/test2/.gitignore: -------------------------------------------------------------------------------- 1 | /toml-spec-tests -------------------------------------------------------------------------------- /src/toml/test2/build.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | [ -d toml-spec-tests ] || git clone https://github.com/cktan/toml-spec-tests.git 6 | 7 | -------------------------------------------------------------------------------- /src/toml/test2/run.sh: -------------------------------------------------------------------------------- 1 | if ! (which jq >& /dev/null); then 2 | echo "ERROR: please install the 'jq' utility" 3 | exit 1 4 | fi 5 | 6 | # 7 | # POSITIVE tests 8 | # 9 | for i in toml-spec-tests/values/*.toml; do 10 | fname="$i" 11 | ext="${fname##*.}" 12 | fname="${fname%.*}" 13 | echo -n $fname ' ' 14 | res='[OK]' 15 | if (../toml_json $fname.toml >& $fname.json.out); then 16 | jq -S . $fname.json.out > t.json 17 | mv t.json $fname.json.out 18 | if [ -f $fname.json ]; then 19 | if ! (diff $fname.json $fname.json.out >& /dev/null); then 20 | res='[FAILED]' 21 | else 22 | rm -f $fname.json.out 23 | fi 24 | else 25 | res='[??]' 26 | fi 27 | fi 28 | echo ... $res 29 | done 30 | 31 | 32 | # 33 | # NEGATIVE tests 34 | # 35 | for i in toml-spec-tests/errors/*.toml; do 36 | echo -n $i ' ' 37 | res='[OK]' 38 | if (../toml_json $i >& $i.json.out); then 39 | res='[FAILED]' 40 | fi 41 | echo ... $res 42 | done 43 | -------------------------------------------------------------------------------- /src/toml/toml.h: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2017 - 2019 CK Tan 5 | https://github.com/cktan/tomlc99 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | #ifndef TOML_H 26 | #define TOML_H 27 | 28 | 29 | #include 30 | #include 31 | 32 | 33 | #ifdef __cplusplus 34 | #define TOML_EXTERN extern "C" 35 | #else 36 | #define TOML_EXTERN extern 37 | #endif 38 | 39 | typedef struct toml_table_t toml_table_t; 40 | typedef struct toml_array_t toml_array_t; 41 | 42 | /* Parse a file. Return a table on success, or 0 otherwise. 43 | * Caller must toml_free(the-return-value) after use. 44 | */ 45 | TOML_EXTERN toml_table_t* toml_parse_file(FILE* fp, 46 | char* errbuf, 47 | int errbufsz); 48 | 49 | /* Parse a string containing the full config. 50 | * Return a table on success, or 0 otherwise. 51 | * Caller must toml_free(the-return-value) after use. 52 | */ 53 | TOML_EXTERN toml_table_t* toml_parse(char* conf, /* NUL terminated, please. */ 54 | char* errbuf, 55 | int errbufsz); 56 | 57 | /* Free the table returned by toml_parse() or toml_parse_file(). */ 58 | TOML_EXTERN void toml_free(toml_table_t* tab); 59 | 60 | /* Retrieve the key in table at keyidx. Return 0 if out of range. */ 61 | TOML_EXTERN const char* toml_key_in(toml_table_t* tab, int keyidx); 62 | 63 | /* Lookup table by key. Return the element or 0 if not found. */ 64 | TOML_EXTERN const char* toml_raw_in(toml_table_t* tab, const char* key); 65 | TOML_EXTERN toml_array_t* toml_array_in(toml_table_t* tab, const char* key); 66 | TOML_EXTERN toml_table_t* toml_table_in(toml_table_t* tab, const char* key); 67 | 68 | /* Return the array kind: 't'able, 'a'rray, 'v'alue */ 69 | TOML_EXTERN char toml_array_kind(toml_array_t* arr); 70 | 71 | /* For array kind 'v'alue, return the type of values 72 | i:int, d:double, b:bool, s:string, t:time, D:date, T:timestamp 73 | 0 if unknown 74 | */ 75 | TOML_EXTERN char toml_array_type(toml_array_t* arr); 76 | 77 | 78 | /* Return the number of elements in the array */ 79 | TOML_EXTERN int toml_array_nelem(toml_array_t* arr); 80 | 81 | /* Return the key of an array */ 82 | TOML_EXTERN const char* toml_array_key(toml_array_t* arr); 83 | 84 | /* Return the number of key-values in a table */ 85 | TOML_EXTERN int toml_table_nkval(toml_table_t* tab); 86 | 87 | /* Return the number of arrays in a table */ 88 | TOML_EXTERN int toml_table_narr(toml_table_t* tab); 89 | 90 | /* Return the number of sub-tables in a table */ 91 | TOML_EXTERN int toml_table_ntab(toml_table_t* tab); 92 | 93 | /* Return the key of a table*/ 94 | TOML_EXTERN const char* toml_table_key(toml_table_t* tab); 95 | 96 | /* Deref array by index. Return the element at idx or 0 if out of range. */ 97 | TOML_EXTERN const char* toml_raw_at(toml_array_t* arr, int idx); 98 | TOML_EXTERN toml_array_t* toml_array_at(toml_array_t* arr, int idx); 99 | TOML_EXTERN toml_table_t* toml_table_at(toml_array_t* arr, int idx); 100 | 101 | 102 | /* Raw to String. Caller must call free(ret) after use. 103 | * Return 0 on success, -1 otherwise. 104 | */ 105 | TOML_EXTERN int toml_rtos(const char* s, char** ret); 106 | 107 | /* Raw to Boolean. Return 0 on success, -1 otherwise. */ 108 | TOML_EXTERN int toml_rtob(const char* s, int* ret); 109 | 110 | /* Raw to Integer. Return 0 on success, -1 otherwise. */ 111 | TOML_EXTERN int toml_rtoi(const char* s, int64_t* ret); 112 | 113 | /* Raw to Double. Return 0 on success, -1 otherwise. */ 114 | TOML_EXTERN int toml_rtod(const char* s, double* ret); 115 | /* Same as toml_rtod, but return the sanitized double in string form as well */ 116 | TOML_EXTERN int toml_rtod_ex(const char* s, double* ret, char* buf, int buflen); 117 | 118 | /* Timestamp types. The year, month, day, hour, minute, second, z 119 | * fields may be NULL if they are not relevant. e.g. In a DATE 120 | * type, the hour, minute, second and z fields will be NULLs. 121 | */ 122 | typedef struct toml_timestamp_t toml_timestamp_t; 123 | struct toml_timestamp_t { 124 | struct { /* internal. do not use. */ 125 | int year, month, day; 126 | int hour, minute, second, millisec; 127 | char z[10]; 128 | } __buffer; 129 | int *year, *month, *day; 130 | int *hour, *minute, *second, *millisec; 131 | char* z; 132 | }; 133 | 134 | /* Raw to Timestamp. Return 0 on success, -1 otherwise. */ 135 | TOML_EXTERN int toml_rtots(const char* s, toml_timestamp_t* ret); 136 | 137 | /* misc */ 138 | TOML_EXTERN int toml_utf8_to_ucs(const char* orig, int len, int64_t* ret); 139 | TOML_EXTERN int toml_ucs_to_utf8(int64_t code, char buf[6]); 140 | TOML_EXTERN void toml_set_memutil(void* (*xxmalloc)(size_t), 141 | void (*xxfree)(void*), 142 | void* (*xxcalloc)(size_t, size_t), 143 | void* (*xxrealloc)(void*, size_t)); 144 | 145 | #endif /* TOML_H */ 146 | -------------------------------------------------------------------------------- /src/toml/toml_cat.c: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2017 CK Tan 5 | https://github.com/cktan/tomlc99 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | 26 | #ifdef NDEBUG 27 | #undef NDEBUG 28 | #endif 29 | 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include "toml.h" 37 | 38 | typedef struct node_t node_t; 39 | struct node_t { 40 | const char* key; 41 | toml_table_t* tab; 42 | }; 43 | 44 | node_t stack[20]; 45 | int stacktop = 0; 46 | 47 | 48 | static void print_table_title(const char* arrname) 49 | { 50 | int i; 51 | printf("%s", arrname ? "[[" : "["); 52 | for (i = 1; i < stacktop; i++) { 53 | printf("%s", stack[i].key); 54 | if (i + 1 < stacktop) 55 | printf("."); 56 | } 57 | if (arrname) 58 | printf(".%s]]\n", arrname); 59 | else 60 | printf("]\n"); 61 | } 62 | 63 | 64 | static void print_array_of_tables(toml_array_t* arr, const char* key); 65 | static void print_array(toml_array_t* arr); 66 | 67 | 68 | static void print_table(toml_table_t* curtab) 69 | { 70 | int i; 71 | const char* key; 72 | const char* raw; 73 | toml_array_t* arr; 74 | toml_table_t* tab; 75 | 76 | 77 | for (i = 0; 0 != (key = toml_key_in(curtab, i)); i++) { 78 | if (0 != (raw = toml_raw_in(curtab, key))) { 79 | printf("%s = %s\n", key, raw); 80 | } else if (0 != (arr = toml_array_in(curtab, key))) { 81 | if (toml_array_kind(arr) == 't') { 82 | print_array_of_tables(arr, key); 83 | } 84 | else { 85 | printf("%s = [\n", key); 86 | print_array(arr); 87 | printf(" ]\n"); 88 | } 89 | } else if (0 != (tab = toml_table_in(curtab, key))) { 90 | stack[stacktop].key = key; 91 | stack[stacktop].tab = tab; 92 | stacktop++; 93 | print_table_title(0); 94 | print_table(tab); 95 | stacktop--; 96 | } else { 97 | abort(); 98 | } 99 | } 100 | } 101 | 102 | static void print_array_of_tables(toml_array_t* arr, const char* key) 103 | { 104 | int i; 105 | toml_table_t* tab; 106 | printf("\n"); 107 | for (i = 0; 0 != (tab = toml_table_at(arr, i)); i++) { 108 | print_table_title(key); 109 | print_table(tab); 110 | printf("\n"); 111 | } 112 | } 113 | 114 | 115 | static void print_array(toml_array_t* curarr) 116 | { 117 | toml_array_t* arr; 118 | const char* raw; 119 | toml_table_t* tab; 120 | int i; 121 | 122 | switch (toml_array_kind(curarr)) { 123 | 124 | case 'v': 125 | for (i = 0; 0 != (raw = toml_raw_at(curarr, i)); i++) { 126 | printf(" %d: %s,\n", i, raw); 127 | } 128 | break; 129 | 130 | case 'a': 131 | for (i = 0; 0 != (arr = toml_array_at(curarr, i)); i++) { 132 | printf(" %d: \n", i); 133 | print_array(arr); 134 | } 135 | break; 136 | 137 | case 't': 138 | for (i = 0; 0 != (tab = toml_table_at(curarr, i)); i++) { 139 | print_table(tab); 140 | } 141 | printf("\n"); 142 | break; 143 | 144 | case '\0': 145 | break; 146 | 147 | default: 148 | abort(); 149 | } 150 | } 151 | 152 | 153 | 154 | static void cat(FILE* fp) 155 | { 156 | char errbuf[200]; 157 | 158 | toml_table_t* tab = toml_parse_file(fp, errbuf, sizeof(errbuf)); 159 | if (!tab) { 160 | fprintf(stderr, "ERROR: %s\n", errbuf); 161 | return; 162 | } 163 | 164 | stack[stacktop].tab = tab; 165 | stack[stacktop].key = ""; 166 | stacktop++; 167 | print_table(tab); 168 | stacktop--; 169 | 170 | toml_free(tab); 171 | } 172 | 173 | 174 | int main(int argc, const char* argv[]) 175 | { 176 | int i; 177 | if (argc == 1) { 178 | cat(stdin); 179 | } else { 180 | for (i = 1; i < argc; i++) { 181 | 182 | FILE* fp = fopen(argv[i], "r"); 183 | if (!fp) { 184 | fprintf(stderr, "ERROR: cannot open %s: %s\n", 185 | argv[i], strerror(errno)); 186 | exit(1); 187 | } 188 | cat(fp); 189 | fclose(fp); 190 | } 191 | } 192 | return 0; 193 | } 194 | -------------------------------------------------------------------------------- /src/toml/toml_json.c: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2017 CK Tan 5 | https://github.com/cktan/tomlc99 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | #ifdef NDEBUG 26 | #undef NDEBUG 27 | #endif 28 | 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include "toml.h" 35 | 36 | 37 | static void print_escape_string(const char* s) 38 | { 39 | for ( ; *s; s++) { 40 | int ch = *s; 41 | switch (ch) { 42 | case '\b': printf("\\b"); break; 43 | case '\t': printf("\\t"); break; 44 | case '\n': printf("\\n"); break; 45 | case '\f': printf("\\f"); break; 46 | case '\r': printf("\\r"); break; 47 | case '"': printf("\\\""); break; 48 | case '\\': printf("\\\\"); break; 49 | default: printf("%c", ch); break; 50 | } 51 | } 52 | } 53 | 54 | static void print_raw(const char* s) 55 | { 56 | char* sval; 57 | int64_t ival; 58 | int bval; 59 | double dval; 60 | toml_timestamp_t ts; 61 | char dbuf[100]; 62 | 63 | if (0 == toml_rtos(s, &sval)) { 64 | printf("{\"type\":\"string\",\"value\":\""); 65 | print_escape_string(sval); 66 | printf("\"}"); 67 | free(sval); 68 | } else if (0 == toml_rtoi(s, &ival)) { 69 | printf("{\"type\":\"integer\",\"value\":\"%" PRId64 "\"}", ival); 70 | } else if (0 == toml_rtob(s, &bval)) { 71 | printf("{\"type\":\"bool\",\"value\":\"%s\"}", bval ? "true" : "false"); 72 | } else if (0 == toml_rtod_ex(s, &dval, dbuf, sizeof(dbuf))) { 73 | printf("{\"type\":\"float\",\"value\":\"%s\"}", dbuf); 74 | } else if (0 == toml_rtots(s, &ts)) { 75 | char millisec[10]; 76 | if (ts.millisec) 77 | sprintf(millisec, ".%d", *ts.millisec); 78 | else 79 | millisec[0] = 0; 80 | if (ts.year && ts.hour) { 81 | printf("{\"type\":\"datetime\",\"value\":\"%04d-%02d-%02dT%02d:%02d:%02d%s%s\"}", 82 | *ts.year, *ts.month, *ts.day, *ts.hour, *ts.minute, *ts.second, 83 | millisec, 84 | (ts.z ? ts.z : "")); 85 | } else if (ts.year) { 86 | printf("{\"type\":\"date\",\"value\":\"%04d-%02d-%02d\"}", 87 | *ts.year, *ts.month, *ts.day); 88 | } else if (ts.hour) { 89 | printf("{\"type\":\"time\",\"value\":\"%02d:%02d:%02d%s\"}", 90 | *ts.hour, *ts.minute, *ts.second, millisec); 91 | } 92 | } else { 93 | fprintf(stderr, "unknown type\n"); 94 | exit(1); 95 | } 96 | } 97 | 98 | 99 | static void print_array(toml_array_t* arr); 100 | static void print_table(toml_table_t* curtab) 101 | { 102 | int i; 103 | const char* key; 104 | const char* raw; 105 | toml_array_t* arr; 106 | toml_table_t* tab; 107 | 108 | 109 | printf("{"); 110 | for (i = 0; 0 != (key = toml_key_in(curtab, i)); i++) { 111 | 112 | printf("%s\"", i > 0 ? "," : ""); 113 | print_escape_string(key); 114 | printf("\":"); 115 | 116 | if (0 != (raw = toml_raw_in(curtab, key))) { 117 | print_raw(raw); 118 | } else if (0 != (arr = toml_array_in(curtab, key))) { 119 | print_array(arr); 120 | } else if (0 != (tab = toml_table_in(curtab, key))) { 121 | print_table(tab); 122 | } else { 123 | abort(); 124 | } 125 | } 126 | printf("}"); 127 | } 128 | 129 | static void print_table_array(toml_array_t* curarr) 130 | { 131 | int i; 132 | toml_table_t* tab; 133 | 134 | printf("["); 135 | for (i = 0; 0 != (tab = toml_table_at(curarr, i)); i++) { 136 | printf("%s", i > 0 ? "," : ""); 137 | print_table(tab); 138 | } 139 | printf("]"); 140 | } 141 | 142 | static void print_array(toml_array_t* curarr) 143 | { 144 | toml_array_t* arr; 145 | const char* raw; 146 | int i; 147 | 148 | if (toml_array_kind(curarr) == 't') { 149 | print_table_array(curarr); 150 | return; 151 | } 152 | 153 | printf("{\"type\":\"array\",\"value\":["); 154 | switch (toml_array_kind(curarr)) { 155 | 156 | case 'v': 157 | for (i = 0; 0 != (raw = toml_raw_at(curarr, i)); i++) { 158 | printf("%s", i > 0 ? "," : ""); 159 | print_raw(raw); 160 | } 161 | break; 162 | 163 | case 'a': 164 | for (i = 0; 0 != (arr = toml_array_at(curarr, i)); i++) { 165 | printf("%s", i > 0 ? "," : ""); 166 | print_array(arr); 167 | } 168 | break; 169 | 170 | default: 171 | break; 172 | } 173 | printf("]}"); 174 | } 175 | 176 | 177 | 178 | static void cat(FILE* fp) 179 | { 180 | char errbuf[200]; 181 | 182 | toml_table_t* tab = toml_parse_file(fp, errbuf, sizeof(errbuf)); 183 | if (!tab) { 184 | fprintf(stderr, "ERROR: %s\n", errbuf); 185 | exit(1); 186 | } 187 | 188 | print_table(tab); 189 | printf("\n"); 190 | 191 | toml_free(tab); 192 | } 193 | 194 | 195 | int main(int argc, const char* argv[]) 196 | { 197 | int i; 198 | if (argc == 1) { 199 | cat(stdin); 200 | } else { 201 | for (i = 1; i < argc; i++) { 202 | 203 | FILE* fp = fopen(argv[i], "r"); 204 | if (!fp) { 205 | fprintf(stderr, "ERROR: cannot open %s: %s\n", 206 | argv[i], strerror(errno)); 207 | exit(1); 208 | } 209 | cat(fp); 210 | fclose(fp); 211 | } 212 | } 213 | return 0; 214 | } 215 | -------------------------------------------------------------------------------- /src/toml/unittest/Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS = -g -I.. 2 | 3 | TESTS = t1 4 | 5 | all: $(TESTS) 6 | 7 | t1: t1.c ../toml.c 8 | 9 | clean: 10 | rm -f $(TESTS) 11 | 12 | -------------------------------------------------------------------------------- /src/toml/unittest/t1.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "../toml.h" 6 | 7 | 8 | 9 | int main(int argc, const char* argv[]) 10 | { 11 | char xxbuf[6], buf[6]; 12 | int64_t xxcode, code; 13 | int xxsize; 14 | 15 | 16 | xxsize = 2, xxcode = 0x80; memcpy(xxbuf, "\xc2\x80", xxsize); 17 | assert(toml_ucs_to_utf8(xxcode, buf) == xxsize && 0 == memcmp(buf, xxbuf, xxsize)); 18 | assert(toml_utf8_to_ucs(buf, xxsize, &code) == xxsize && code == xxcode); 19 | 20 | xxsize = 2, xxcode = 0x7ff; memcpy(xxbuf, "\xdf\xbf", xxsize); 21 | assert(toml_ucs_to_utf8(xxcode, buf) == xxsize && 0 == memcmp(buf, xxbuf, xxsize)); 22 | assert(toml_utf8_to_ucs(buf, xxsize, &code) == xxsize && code == xxcode); 23 | 24 | xxsize = 3, xxcode = 0x800; memcpy(xxbuf, "\xe0\xa0\x80", xxsize); 25 | assert(toml_ucs_to_utf8(xxcode, buf) == xxsize && 0 == memcmp(buf, xxbuf, xxsize)); 26 | assert(toml_utf8_to_ucs(buf, xxsize, &code) == xxsize && code == xxcode); 27 | 28 | xxsize = 3, xxcode = 0xfffd; memcpy(xxbuf, "\xef\xbf\xbd", xxsize); 29 | assert(toml_ucs_to_utf8(xxcode, buf) == xxsize && 0 == memcmp(buf, xxbuf, xxsize)); 30 | assert(toml_utf8_to_ucs(buf, xxsize, &code) == xxsize && code == xxcode); 31 | 32 | xxsize = 4, xxcode = 0x10000; memcpy(xxbuf, "\xf0\x90\x80\x80", xxsize); 33 | assert(toml_ucs_to_utf8(xxcode, buf) == xxsize && 0 == memcmp(buf, xxbuf, xxsize)); 34 | assert(toml_utf8_to_ucs(buf, xxsize, &code) == xxsize && code == xxcode); 35 | 36 | xxsize = 4, xxcode = 0x1fffff; memcpy(xxbuf, "\xf7\xbf\xbf\xbf", xxsize); 37 | assert(toml_ucs_to_utf8(xxcode, buf) == xxsize && 0 == memcmp(buf, xxbuf, xxsize)); 38 | assert(toml_utf8_to_ucs(buf, xxsize, &code) == xxsize && code == xxcode); 39 | 40 | xxsize = 5, xxcode = 0x200000; memcpy(xxbuf, "\xf8\x88\x80\x80\x80", xxsize); 41 | assert(toml_ucs_to_utf8(xxcode, buf) == xxsize && 0 == memcmp(buf, xxbuf, xxsize)); 42 | assert(toml_utf8_to_ucs(buf, xxsize, &code) == xxsize && code == xxcode); 43 | 44 | xxsize = 5, xxcode = 0x3ffffff; memcpy(xxbuf, "\xfb\xbf\xbf\xbf\xbf", xxsize); 45 | assert(toml_ucs_to_utf8(xxcode, buf) == xxsize && 0 == memcmp(buf, xxbuf, xxsize)); 46 | assert(toml_utf8_to_ucs(buf, xxsize, &code) == xxsize && code == xxcode); 47 | 48 | xxsize = 6, xxcode = 0x4000000; memcpy(xxbuf, "\xfc\x84\x80\x80\x80\x80", xxsize); 49 | assert(toml_ucs_to_utf8(xxcode, buf) == xxsize && 0 == memcmp(buf, xxbuf, xxsize)); 50 | assert(toml_utf8_to_ucs(buf, xxsize, &code) == xxsize && code == xxcode); 51 | 52 | xxsize = 6, xxcode = 0x7fffffff; memcpy(xxbuf, "\xfd\xbf\xbf\xbf\xbf\xbf", xxsize); 53 | assert(toml_ucs_to_utf8(xxcode, buf) == xxsize && 0 == memcmp(buf, xxbuf, xxsize)); 54 | assert(toml_utf8_to_ucs(buf, xxsize, &code) == xxsize && code == xxcode); 55 | 56 | return 0; 57 | } 58 | -------------------------------------------------------------------------------- /src/version.h: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2019-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #pragma once 19 | 20 | #ifndef VERSION 21 | #define VERSION "unknown" 22 | #endif /* VERSION */ 23 | -------------------------------------------------------------------------------- /src/wireplumber.c: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2021 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #define G_LOG_DOMAIN "sessiond-wireplumber" 19 | 20 | #include "wireplumber.h" 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #define WP_MIXER_API "libwireplumber-module-mixer-api" 28 | #define WP_DEFAULT_NODES_API "libwireplumber-module-default-nodes-api" 29 | 30 | static const gchar * 31 | get_object_name(WpPipewireObject *obj) 32 | { 33 | const gchar *name = 34 | wp_pipewire_object_get_property(obj, PW_KEY_NODE_DESCRIPTION); 35 | if (!name) 36 | name = wp_pipewire_object_get_property(obj, PW_KEY_APP_NAME); 37 | if (!name) 38 | name = wp_pipewire_object_get_property(obj, PW_KEY_NODE_NAME); 39 | 40 | return name; 41 | } 42 | 43 | static void 44 | update_default_id(WpConn *conn) 45 | { 46 | guint32 id; 47 | g_signal_emit_by_name(conn->nodes_api, "get-default-node", "Audio/Sink", 48 | &id); 49 | 50 | if (id == conn->default_id) 51 | return; 52 | 53 | conn->default_id = id; 54 | conn->func(AS_ACTION_CHANGE_DEFAULT, id, NULL); 55 | } 56 | 57 | static void 58 | on_disconnect(WpConn *conn) 59 | { 60 | if (!conn) 61 | return; 62 | if (conn->mixer_api) 63 | g_object_unref(conn->mixer_api); 64 | if (conn->nodes_api) 65 | g_object_unref(conn->nodes_api); 66 | } 67 | 68 | static struct AudioSink * 69 | new_audiosink(WpConn *conn, guint32 id, WpPipewireObject *obj) 70 | { 71 | struct AudioSink *as = g_malloc0(sizeof(struct AudioSink)); 72 | 73 | as->id = id; 74 | if (obj) 75 | as->name = get_object_name(obj); 76 | 77 | if (!audiosink_get_volume_mute(conn, id, &as->volume, &as->mute)) 78 | return NULL; 79 | 80 | return as; 81 | } 82 | 83 | static void 84 | on_object_changed(WpConn *conn, guint32 id) 85 | { 86 | struct AudioSink *as = new_audiosink(conn, id, NULL); 87 | conn->func(AS_ACTION_CHANGE, id, as); 88 | g_free(as); 89 | } 90 | 91 | static void 92 | on_object_added(WpConn *conn, WpPipewireObject *obj) 93 | { 94 | guint32 id = wp_proxy_get_bound_id(WP_PROXY(obj)); 95 | 96 | g_debug("Added audio sink: %d", id); 97 | 98 | g_signal_connect_swapped(conn->mixer_api, "changed", 99 | G_CALLBACK(on_object_changed), conn); 100 | 101 | struct AudioSink *as = new_audiosink(conn, id, obj); 102 | conn->func(AS_ACTION_ADD, id, as); 103 | g_free(as); 104 | } 105 | 106 | static void 107 | on_object_removed(WpConn *conn, WpPipewireObject *obj) 108 | { 109 | guint32 id = wp_proxy_get_bound_id(WP_PROXY(obj)); 110 | 111 | g_debug("Removed audio sink: %d", id); 112 | 113 | conn->func(AS_ACTION_REMOVE, id, NULL); 114 | } 115 | 116 | static void 117 | on_plugin_activated(GObject *obj, GAsyncResult *res, gpointer data) 118 | { 119 | WpObject *p = (WpObject *)obj; 120 | WpConn *conn = (WpConn *)data; 121 | GError *err = NULL; 122 | 123 | if (!wp_object_activate_finish(p, res, &err)) { 124 | g_warning("%s", err->message); 125 | return; 126 | } 127 | 128 | if (--conn->pending_plugins == 0) 129 | wp_core_install_object_manager(conn->core, conn->manager); 130 | } 131 | 132 | gboolean 133 | audiosink_set_volume(guint32 id, gdouble v, WpConn *conn) 134 | { 135 | GVariantBuilder b = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE_VARDICT); 136 | g_variant_builder_add(&b, "{sv}", "volume", g_variant_new_double(v)); 137 | GVariant *variant = g_variant_builder_end(&b); 138 | 139 | gboolean res = FALSE; 140 | g_signal_emit_by_name(conn->mixer_api, "set-volume", id, variant, 141 | &res); 142 | 143 | if (!res) { 144 | g_warning("Failed to set audio sink volume: %d", id); 145 | return FALSE; 146 | } 147 | 148 | return TRUE; 149 | } 150 | 151 | gboolean 152 | audiosink_set_mute(guint32 id, gboolean m, WpConn *conn) 153 | { 154 | GVariantBuilder b = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE_VARDICT); 155 | g_variant_builder_add(&b, "{sv}", "mute", g_variant_new_boolean(m)); 156 | GVariant *variant = g_variant_builder_end(&b); 157 | 158 | gboolean res = FALSE; 159 | g_signal_emit_by_name(conn->mixer_api, "set-volume", id, variant, 160 | &res); 161 | 162 | if (!res) { 163 | g_warning("Failed to set audio sink mute state: %d", id); 164 | return FALSE; 165 | } 166 | 167 | return TRUE; 168 | } 169 | 170 | gboolean 171 | audiosink_get_volume_mute(WpConn *conn, guint32 id, gdouble *v, gboolean *m) 172 | { 173 | GVariant *dict = NULL; 174 | g_signal_emit_by_name(conn->mixer_api, "get-volume", id, &dict); 175 | 176 | if (!dict) { 177 | g_warning("Failed to get audio sink volume: %d", id); 178 | return FALSE; 179 | } 180 | 181 | if (v) 182 | g_variant_lookup(dict, "volume", "d", v); 183 | if (m) 184 | g_variant_lookup(dict, "mute", "b", m); 185 | 186 | g_variant_unref(dict); 187 | 188 | return TRUE; 189 | } 190 | 191 | WpConn * 192 | wireplumber_connect(GMainContext *ctx, AudioSinkFunc func) 193 | { 194 | wp_init(WP_INIT_ALL & ~WP_INIT_SET_GLIB_LOG); 195 | 196 | WpConn *conn = g_malloc0(sizeof(WpConn)); 197 | conn->core = wp_core_new(ctx, NULL); 198 | conn->manager = wp_object_manager_new(); 199 | conn->func = func; 200 | 201 | wp_object_manager_add_interest(conn->manager, WP_TYPE_NODE, 202 | WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", "*/Sink*", 203 | NULL); 204 | 205 | GError *err = NULL; 206 | 207 | /* Load API modules. */ 208 | if (!wp_core_load_component(conn->core, WP_MIXER_API, "module", 209 | NULL, &err)) { 210 | g_warning("%s", err->message); 211 | goto error; 212 | } 213 | 214 | if (!wp_core_load_component(conn->core, WP_DEFAULT_NODES_API, "module", 215 | NULL, &err)) { 216 | g_warning("%s", err->message); 217 | goto error; 218 | } 219 | 220 | if (!wp_core_connect(conn->core)) { 221 | g_warning("Failed to connect to WirePlumber"); 222 | goto error; 223 | } 224 | 225 | /* Activate API modules. */ 226 | conn->mixer_api = wp_plugin_find(conn->core, "mixer-api"); 227 | g_object_set(G_OBJECT(conn->mixer_api), "scale", 1 /* cubic */, NULL); 228 | 229 | conn->nodes_api = wp_plugin_find(conn->core, "default-nodes-api"); 230 | 231 | conn->pending_plugins = 2; 232 | 233 | wp_object_activate(WP_OBJECT(conn->mixer_api), WP_PLUGIN_FEATURE_ENABLED, 234 | NULL, (GAsyncReadyCallback)on_plugin_activated, conn); 235 | wp_object_activate(WP_OBJECT(conn->nodes_api), WP_PLUGIN_FEATURE_ENABLED, 236 | NULL, (GAsyncReadyCallback)on_plugin_activated, conn); 237 | 238 | /* Connect signals. */ 239 | g_signal_connect_swapped(conn->core, "disconnected", 240 | G_CALLBACK(on_disconnect), conn); 241 | 242 | g_signal_connect_swapped(conn->manager, "installed", 243 | G_CALLBACK(update_default_id), conn); 244 | g_signal_connect_swapped(conn->manager, "object-added", 245 | G_CALLBACK(on_object_added), conn); 246 | g_signal_connect_swapped(conn->manager, "object-removed", 247 | G_CALLBACK(on_object_removed), conn); 248 | 249 | g_signal_connect_swapped(conn->nodes_api, "changed", 250 | G_CALLBACK(update_default_id), conn); 251 | 252 | return conn; 253 | 254 | error: 255 | wireplumber_disconnect(conn); 256 | return NULL; 257 | } 258 | 259 | void 260 | wireplumber_disconnect(WpConn *conn) 261 | { 262 | if (conn && wp_core_is_connected(conn->core)) 263 | wp_core_disconnect(conn->core); 264 | } 265 | -------------------------------------------------------------------------------- /src/wireplumber.h: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2021 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #pragma once 19 | 20 | #include 21 | #include 22 | 23 | typedef enum { 24 | AS_ACTION_ADD, 25 | AS_ACTION_REMOVE, 26 | AS_ACTION_CHANGE, 27 | AS_ACTION_CHANGE_DEFAULT, 28 | } AudioSinkAction; 29 | 30 | struct AudioSink { 31 | guint32 id; 32 | const gchar *name; 33 | gboolean mute; 34 | gdouble volume; 35 | }; 36 | 37 | typedef void (*AudioSinkFunc)(AudioSinkAction a, guint32 id, 38 | struct AudioSink *as); 39 | 40 | typedef struct { 41 | WpCore *core; 42 | WpObjectManager *manager; 43 | guint pending_plugins; 44 | WpPlugin *mixer_api; 45 | WpPlugin *nodes_api; 46 | guint32 default_id; 47 | AudioSinkFunc func; 48 | } WpConn; 49 | 50 | extern gboolean 51 | audiosink_set_volume(guint32 id, gdouble v, WpConn *conn); 52 | extern gboolean 53 | audiosink_set_mute(guint32 id, gboolean m, WpConn *conn); 54 | extern gboolean 55 | audiosink_get_volume_mute(WpConn *conn, guint32 id, gdouble *v, gboolean *m); 56 | extern WpConn * 57 | wireplumber_connect(GMainContext *ctx, AudioSinkFunc func); 58 | extern void 59 | wireplumber_disconnect(WpConn *conn); 60 | -------------------------------------------------------------------------------- /src/xsource.c: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2018-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #define G_LOG_DOMAIN "sessiond-activity" 19 | 20 | #include "xsource.h" 21 | #include "common.h" 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #define DEBOUNCE_US (1 * 1000000) 30 | 31 | #define XI_MAJOR_VERSION 2 32 | #define XI_MINOR_VERSION 0 33 | 34 | static gboolean 35 | xsource_prepare(GSource *source, gint *timeout) 36 | { 37 | XSource *self = (XSource *)source; 38 | XSync(self->dpy, FALSE); 39 | *timeout = -1; 40 | 41 | return FALSE; 42 | } 43 | 44 | static gboolean 45 | xsource_check(GSource *source) 46 | { 47 | XSource *self = (XSource *)source; 48 | GIOCondition revents = g_source_query_unix_fd(source, self->fd); 49 | 50 | if (revents & (G_IO_HUP | G_IO_ERR)) { 51 | self->connected = FALSE; 52 | return TRUE; 53 | } 54 | 55 | if (!(revents & G_IO_IN)) 56 | return FALSE; 57 | 58 | guint events = 0; 59 | 60 | while (XPending(self->dpy)) { 61 | XEvent ev; 62 | XNextEvent(self->dpy, &ev); 63 | 64 | if (ev.type != GenericEvent) 65 | continue; 66 | 67 | XGenericEventCookie c = ev.xcookie; 68 | XGetEventData(self->dpy, &c); 69 | XIDeviceEvent *iev = (XIDeviceEvent *)c.data; 70 | 71 | switch (iev->evtype) { 72 | #define X(t, n) \ 73 | case XI_##t: \ 74 | events++; \ 75 | g_debug("%s @ %lu", n, iev->time); \ 76 | break; 77 | INPUT_TYPE_LIST 78 | #undef X 79 | } 80 | 81 | XFreeEventData(self->dpy, &c); 82 | } 83 | 84 | if (events > 0) { 85 | self->last_event_time = g_get_monotonic_time(); 86 | g_source_set_ready_time(source, self->last_event_time + DEBOUNCE_US); 87 | } 88 | 89 | return FALSE; 90 | } 91 | 92 | static gboolean 93 | xsource_dispatch(GSource *source, GSourceFunc func, gpointer user_data) 94 | { 95 | XSource *self = (XSource *)source; 96 | 97 | if (g_get_monotonic_time() - self->last_event_time >= DEBOUNCE_US) { 98 | g_source_set_ready_time(source, -1); 99 | return func(user_data); 100 | } 101 | 102 | return G_SOURCE_CONTINUE; 103 | } 104 | 105 | static void 106 | xsource_finalize(GSource *source) 107 | { 108 | XSource *self = (XSource *)source; 109 | 110 | XCloseDisplay(self->dpy); 111 | self->connected = FALSE; 112 | } 113 | 114 | static GSourceFuncs xsource_funcs = { 115 | xsource_prepare, 116 | xsource_check, 117 | xsource_dispatch, 118 | xsource_finalize, 119 | NULL, 120 | NULL, 121 | }; 122 | 123 | XSource * 124 | xsource_new(GMainContext *ctx, guint input_mask, GSourceFunc func, 125 | gpointer user_data, GDestroyNotify destroy) 126 | { 127 | Display *dpy = XOpenDisplay(NULL); 128 | if (!dpy) { 129 | g_warning("Failed to open X display"); 130 | return NULL; 131 | } 132 | 133 | int opcode, event, error; 134 | if (!XQueryExtension(dpy, "XInputExtension", &opcode, &event, &error)) { 135 | g_warning("XInputExtension is not available"); 136 | return NULL; 137 | } 138 | 139 | int major = XI_MAJOR_VERSION; 140 | int minor = XI_MINOR_VERSION; 141 | if (XIQueryVersion(dpy, &major, &minor) != Success) { 142 | g_warning("XInputExtension %i.%i is not supported", major, minor); 143 | return NULL; 144 | } 145 | 146 | XIEventMask evmask; 147 | evmask.deviceid = XIAllMasterDevices; 148 | evmask.mask_len = XIMaskLen(XI_LASTEVENT); 149 | evmask.mask = g_malloc0_n(evmask.mask_len, sizeof(char)); 150 | 151 | #define X(type, _) \ 152 | if (input_mask & INPUT_TYPE_MASK(type)) \ 153 | XISetMask(evmask.mask, XI_##type); 154 | INPUT_TYPE_LIST 155 | #undef X 156 | 157 | XISelectEvents(dpy, DefaultRootWindow(dpy), &evmask, 1); 158 | free(evmask.mask); 159 | 160 | GSource *source = g_source_new(&xsource_funcs, sizeof(XSource)); 161 | XSource *self = (XSource *)source; 162 | self->dpy = dpy; 163 | self->fd = g_source_add_unix_fd(source, ConnectionNumber(dpy), 164 | G_IO_IN | G_IO_HUP | G_IO_ERR); 165 | self->connected = TRUE; 166 | 167 | self->last_event_time = 0; 168 | 169 | g_source_set_callback(source, func, user_data, destroy); 170 | g_source_attach(source, ctx); 171 | 172 | return self; 173 | } 174 | 175 | void 176 | xsource_free(XSource *self) 177 | { 178 | if (!self) 179 | return; 180 | 181 | GSource *source = (GSource *)self; 182 | g_source_destroy(source); 183 | g_source_unref(source); 184 | } 185 | -------------------------------------------------------------------------------- /src/xsource.h: -------------------------------------------------------------------------------- 1 | /* 2 | sessiond - standalone X session manager 3 | Copyright (C) 2018-2020 James Reed 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 11 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 12 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along with 15 | this program. If not, see . 16 | */ 17 | 18 | #pragma once 19 | 20 | #include 21 | #include 22 | 23 | #define INPUT_TYPE_LIST \ 24 | X(RawMotion, "motion") \ 25 | X(RawButtonPress, "button-press") \ 26 | X(RawButtonRelease, "button-release") \ 27 | X(RawKeyPress, "key-press") \ 28 | X(RawKeyRelease, "key-release") 29 | 30 | #define INPUT_TYPE_MASK(type) (1 << INPUT_TYPE_##type) 31 | 32 | enum { 33 | #define X(type, _) INPUT_TYPE_##type, 34 | INPUT_TYPE_LIST 35 | #undef X 36 | INPUT_TYPE_COUNT 37 | }; 38 | 39 | typedef struct { 40 | GSource source; 41 | Display *dpy; 42 | gpointer fd; 43 | gboolean connected; 44 | gint64 last_event_time; 45 | } XSource; 46 | 47 | extern XSource * 48 | xsource_new(GMainContext *ctx, guint input_mask, GSourceFunc func, 49 | gpointer user_data, GDestroyNotify destroy); 50 | extern void 51 | xsource_free(XSource *self); 52 | -------------------------------------------------------------------------------- /systemd/graphical-idle.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Graphical session idle 3 | Conflicts=graphical-unidle.target 4 | -------------------------------------------------------------------------------- /systemd/graphical-lock.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Graphical session lock 3 | Conflicts=graphical-unlock.target 4 | -------------------------------------------------------------------------------- /systemd/graphical-unidle.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Graphical session unidle 3 | Conflicts=graphical-idle.target 4 | StopWhenUnneeded=yes 5 | -------------------------------------------------------------------------------- /systemd/graphical-unlock.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Graphical session unlock 3 | Conflicts=graphical-lock.target 4 | StopWhenUnneeded=yes 5 | -------------------------------------------------------------------------------- /systemd/sessiond-session.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=sessiond session 3 | Wants=sessiond.service 4 | BindsTo=graphical-session.target 5 | -------------------------------------------------------------------------------- /systemd/sessiond.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Standalone X session manager 3 | Documentation=man:sessiond(1) 4 | Requires=dbus.socket 5 | After=dbus.socket 6 | PartOf=graphical-session.target 7 | 8 | [Service] 9 | Type=dbus 10 | BusName=org.sessiond.session1 11 | ExecStart=/usr/bin/sessiond 12 | ExecReload=/usr/bin/kill -HUP $MAINPID 13 | Restart=always 14 | -------------------------------------------------------------------------------- /systemd/user-shutdown.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=User shutdown 3 | StopWhenUnneeded=yes 4 | -------------------------------------------------------------------------------- /systemd/user-sleep-finished.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=User sleep finished 3 | Conflicts=user-sleep.target 4 | StopWhenUnneeded=yes 5 | -------------------------------------------------------------------------------- /systemd/user-sleep.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=User sleep 3 | Conflicts=user-sleep-finished.target 4 | -------------------------------------------------------------------------------- /test/config_test.c: -------------------------------------------------------------------------------- 1 | #include "../src/config.h" 2 | #include "../src/xsource.h" 3 | 4 | #include 5 | #include 6 | 7 | typedef struct { 8 | Config c; 9 | gboolean success; 10 | } ConfigFixture; 11 | 12 | static void 13 | config_fixture_set_up(ConfigFixture *f, gconstpointer user_data) 14 | { 15 | gchar *path = (gchar *)user_data; 16 | f->success = config_load(path, NULL, &f->c); 17 | } 18 | 19 | static void 20 | config_fixture_tear_down(ConfigFixture *f, gconstpointer user_data) 21 | { 22 | config_free(&f->c); 23 | } 24 | 25 | static void 26 | test_load(ConfigFixture *f, gconstpointer user_data) 27 | { 28 | g_assert_true(f->success); 29 | } 30 | 31 | static void 32 | test_input(ConfigFixture *f, gconstpointer user_data) 33 | { 34 | const guint input_mask = INPUT_TYPE_MASK(RawKeyRelease) \ 35 | | INPUT_TYPE_MASK(RawButtonRelease); 36 | const Config *c = &f->c; 37 | g_assert_cmpuint(c->input_mask, ==, input_mask); 38 | g_assert_cmpuint(c->idle_sec, ==, 600); 39 | } 40 | 41 | static void 42 | test_idle(ConfigFixture *f, gconstpointer user_data) 43 | { 44 | const Config *c = &f->c; 45 | g_assert_false(c->on_idle); 46 | g_assert_false(c->on_sleep); 47 | } 48 | 49 | static void 50 | test_dpms(ConfigFixture *f, gconstpointer user_data) 51 | { 52 | const Config *c = &f->c; 53 | g_assert_false(c->dpms_enable); 54 | g_assert_cmpuint(c->standby_sec, ==, 60); 55 | g_assert_cmpuint(c->suspend_sec, ==, 61); 56 | g_assert_cmpuint(c->off_sec, ==, 62); 57 | } 58 | 59 | static void 60 | test_backlights(ConfigFixture *f, gconstpointer user_data) 61 | { 62 | GHashTable *bls = f->c.backlights; 63 | g_assert_nonnull(bls); 64 | 65 | struct BacklightConf *bl = g_hash_table_lookup(bls, "/sys/class/backlight/1"); 66 | g_assert_nonnull(bl); 67 | g_assert_cmpuint(bl->dim_sec, ==, 600); 68 | g_assert_cmpint(bl->dim_percent, ==, 0.66); 69 | g_assert_cmpint(bl->dim_value, ==, -1); 70 | 71 | struct BacklightConf *led = g_hash_table_lookup(bls, "/sys/class/leds/1"); 72 | g_assert_nonnull(led); 73 | g_assert_cmpuint(led->dim_sec, ==, 600); 74 | g_assert_cmpint(led->dim_percent, ==, -1); 75 | g_assert_cmpint(led->dim_value, ==, 1); 76 | } 77 | 78 | int 79 | main(int argc, char *argv[]) 80 | { 81 | setlocale(LC_ALL, ""); 82 | 83 | g_test_init(&argc, &argv, NULL); 84 | 85 | gchar *path = g_test_build_filename(G_TEST_DIST, "test.conf", NULL); 86 | 87 | #define TEST(name) \ 88 | g_test_add("/config/" #name, ConfigFixture, path, \ 89 | config_fixture_set_up, test_##name, config_fixture_tear_down) 90 | 91 | TEST(load); 92 | TEST(input); 93 | TEST(idle); 94 | TEST(dpms); 95 | TEST(backlights); 96 | #undef TEST 97 | 98 | int ret = g_test_run(); 99 | 100 | g_free(path); 101 | 102 | return ret; 103 | } 104 | -------------------------------------------------------------------------------- /test/hooks.conf: -------------------------------------------------------------------------------- 1 | [[Hook]] 2 | Trigger="Idle" 3 | ExecStart="test start" 4 | 5 | [[Hook]] 6 | Trigger="Sleep" 7 | ExecStop="test stop" 8 | -------------------------------------------------------------------------------- /test/hooks.d/00-inactive.hook: -------------------------------------------------------------------------------- 1 | [Hook] 2 | Trigger="Inactive" 3 | InactiveSec=10 4 | ExecStart="/usr/bin/touch /tmp/test_run_inactive" 5 | ExecStop="/bin/rm /tmp/test_run_inactive" 6 | -------------------------------------------------------------------------------- /test/hooks.d/01-lock.hook: -------------------------------------------------------------------------------- 1 | [Hook] 2 | Trigger="Lock" 3 | ExecStart="/usr/bin/touch /tmp/test_run_lock" 4 | ExecStop="/bin/rm /tmp/test_run_lock" 5 | -------------------------------------------------------------------------------- /test/hooks_test.c: -------------------------------------------------------------------------------- 1 | #include "../src/config.h" 2 | #include "../src/hooks.h" 3 | 4 | #include 5 | #include 6 | 7 | struct Paths { 8 | gchar *config; 9 | gchar *hooksd; 10 | }; 11 | 12 | typedef struct { 13 | Config c; 14 | gboolean success; 15 | } ConfigFixture; 16 | 17 | static void 18 | config_fixture_set_up(ConfigFixture *f, gconstpointer user_data) 19 | { 20 | const struct Paths *paths = (struct Paths *)user_data; 21 | f->success = config_load(paths->config, paths->hooksd, &f->c); 22 | } 23 | 24 | static void 25 | config_fixture_tear_down(ConfigFixture *f, gconstpointer user_data) 26 | { 27 | config_free(&f->c); 28 | } 29 | 30 | static void 31 | test_load(ConfigFixture *f, gconstpointer user_data) 32 | { 33 | g_assert_true(f->success); 34 | } 35 | 36 | static void 37 | test_hooks(ConfigFixture *f, gconstpointer user_data) 38 | { 39 | GPtrArray *hooks = f->c.hooks; 40 | g_assert_nonnull(hooks); 41 | 42 | guint passed = 0; 43 | 44 | for (int i = 0; i < hooks->len; i++) { 45 | const struct Hook *h = g_ptr_array_index(hooks, i); 46 | switch (h->trigger) { 47 | case HOOK_TRIGGER_IDLE: 48 | g_assert_cmpstr(h->exec_start[0], ==, "test"); 49 | g_assert_cmpstr(h->exec_start[1], ==, "start"); 50 | passed++; 51 | break; 52 | case HOOK_TRIGGER_SLEEP: 53 | g_assert_cmpstr(h->exec_stop[0], ==, "test"); 54 | g_assert_cmpstr(h->exec_stop[1], ==, "stop"); 55 | passed++; 56 | break; 57 | case HOOK_TRIGGER_INACTIVE: 58 | g_assert_cmpint(h->inactive_sec, ==, 10); 59 | g_assert_cmpstr(h->exec_start[0], ==, "/usr/bin/touch"); 60 | g_assert_cmpstr(h->exec_start[1], ==, "/tmp/test_run_inactive"); 61 | g_assert_cmpstr(h->exec_stop[0], ==, "/bin/rm"); 62 | g_assert_cmpstr(h->exec_stop[1], ==, "/tmp/test_run_inactive"); 63 | passed++; 64 | break; 65 | case HOOK_TRIGGER_LOCK: 66 | g_assert_cmpstr(h->exec_start[0], ==, "/usr/bin/touch"); 67 | g_assert_cmpstr(h->exec_start[1], ==, "/tmp/test_run_lock"); 68 | g_assert_cmpstr(h->exec_stop[0], ==, "/bin/rm"); 69 | g_assert_cmpstr(h->exec_stop[1], ==, "/tmp/test_run_lock"); 70 | passed++; 71 | break; 72 | default: 73 | break; 74 | } 75 | } 76 | 77 | g_assert_cmpuint(passed, ==, hooks->len); 78 | } 79 | 80 | static void 81 | test_run_inactive_start(ConfigFixture *f, gconstpointer user_data) 82 | { 83 | if (g_test_subprocess()) { 84 | hooks_on_timeout(f->c.hooks, 10, TRUE); 85 | return; 86 | } 87 | 88 | g_assert_nonnull(f->c.hooks); 89 | g_test_trap_subprocess(NULL, 0, 0); 90 | g_assert_true(g_file_test("/tmp/test_run_inactive", G_FILE_TEST_EXISTS)); 91 | } 92 | 93 | static void test_run_inactive_stop(ConfigFixture *f, gconstpointer user_data) 94 | { 95 | if (g_test_subprocess()) { 96 | hooks_on_timeout(f->c.hooks, 10, FALSE); 97 | return; 98 | } 99 | 100 | g_assert_nonnull(f->c.hooks); 101 | g_test_trap_subprocess(NULL, 0, 0); 102 | g_assert_false(g_file_test("/tmp/test_run_inactive", G_FILE_TEST_EXISTS)); 103 | } 104 | 105 | static void 106 | test_run_lock_start(ConfigFixture *f, gconstpointer user_data) 107 | { 108 | if (g_test_subprocess()) { 109 | hooks_run(f->c.hooks, HOOK_TRIGGER_LOCK, TRUE); 110 | return; 111 | } 112 | 113 | g_assert_nonnull(f->c.hooks); 114 | g_test_trap_subprocess(NULL, 0, 0); 115 | g_assert_true(g_file_test("/tmp/test_run_lock", G_FILE_TEST_EXISTS)); 116 | } 117 | 118 | static void 119 | test_run_lock_stop(ConfigFixture *f, gconstpointer user_data) 120 | { 121 | if (g_test_subprocess()) { 122 | hooks_run(f->c.hooks, HOOK_TRIGGER_LOCK, FALSE); 123 | return; 124 | } 125 | 126 | g_assert_nonnull(f->c.hooks); 127 | g_test_trap_subprocess(NULL, 0, 0); 128 | g_assert_false(g_file_test("/tmp/test_run_lock", G_FILE_TEST_EXISTS)); 129 | } 130 | 131 | 132 | int 133 | main(int argc, char *argv[]) 134 | { 135 | setlocale(LC_ALL, ""); 136 | 137 | g_test_init(&argc, &argv, NULL); 138 | 139 | struct Paths paths; 140 | paths.config = g_test_build_filename(G_TEST_DIST, "hooks.conf", NULL); 141 | paths.hooksd = g_test_build_filename(G_TEST_DIST, "hooks.d", NULL); 142 | 143 | #define TEST(name, func) \ 144 | g_test_add("/hooks/" #name, ConfigFixture, &paths, \ 145 | config_fixture_set_up, test_##func, config_fixture_tear_down) 146 | 147 | TEST(load, load); 148 | TEST(hooks, hooks); 149 | TEST(run-inactive/start, run_inactive_start); 150 | TEST(run-inactive/stop, run_inactive_stop); 151 | TEST(run-lock/start, run_lock_start); 152 | TEST(run-lock/stop, run_lock_stop); 153 | #undef TEST 154 | 155 | int ret = g_test_run(); 156 | 157 | g_free(paths.config); 158 | g_free(paths.hooksd); 159 | 160 | return ret; 161 | } 162 | -------------------------------------------------------------------------------- /test/meson.build: -------------------------------------------------------------------------------- 1 | g_test_env = [ 2 | 'G_MESSAGES_DEBUG=all', 3 | 'G_TEST_SRCDIR=@0@'.format(meson.current_source_dir()), 4 | 'G_TEST_BUILDDIR=@0@'.format(meson.current_build_dir()), 5 | 'XDG_CONFIG_HOME=@0@'.format(join_paths(meson.current_source_dir(), 'test')), 6 | ] 7 | 8 | test( 9 | 'test config loading', 10 | executable('config_test', [ 11 | 'config_test.c', 12 | '../src/toml/toml.c', 13 | '../src/config.c', 14 | ], dependencies : deps), 15 | env : g_test_env, 16 | ) 17 | 18 | test( 19 | 'test hooks', 20 | executable('hooks_test', [ 21 | 'hooks_test.c', 22 | '../src/toml/toml.c', 23 | '../src/config.c', 24 | '../src/hooks.c', 25 | '../src/timeline.c', 26 | '../src/common.c', 27 | ], dependencies : deps), 28 | env : g_test_env, 29 | ) 30 | 31 | test( 32 | 'test timeline', 33 | executable('timeline_test', [ 34 | 'timeline_test.c', 35 | '../src/timeline.c', 36 | ], dependencies : deps), 37 | env : g_test_env, 38 | ) 39 | -------------------------------------------------------------------------------- /test/test.conf: -------------------------------------------------------------------------------- 1 | [Idle] 2 | Inputs=["key-release", "button-release"] 3 | IdleSec=600 4 | 5 | [Lock] 6 | OnIdle=false 7 | OnSleep=false 8 | 9 | [DPMS] 10 | Enable=false 11 | StandbySec=60 12 | SuspendSec=61 13 | OffSec=62 14 | 15 | [[Backlight]] 16 | Path="/sys/class/backlight/1" 17 | DimSec=600 18 | DimPercent=0.66 19 | 20 | [[Backlight]] 21 | Path="/sys/class/leds/1" 22 | DimSec=600 23 | DimValue=1 24 | -------------------------------------------------------------------------------- /test/timeline_test.c: -------------------------------------------------------------------------------- 1 | #include "../src/timeline.h" 2 | 3 | #include 4 | #include 5 | 6 | typedef struct { 7 | GMainContext *ctx; 8 | GMainLoop *loop; 9 | Timeline tl; 10 | GArray *times; 11 | } TimelineFixture; 12 | 13 | static void 14 | tl_func(guint timeout, gboolean state, gconstpointer user_data) 15 | { 16 | TimelineFixture *f = (TimelineFixture *)user_data; 17 | double t = g_test_timer_elapsed(); 18 | 19 | g_array_append_val(f->times, t); 20 | 21 | if (!timeline_pending_timeouts(&f->tl)) 22 | g_main_loop_quit(f->loop); 23 | } 24 | 25 | static void 26 | tl_fixture_set_up(TimelineFixture *f, gconstpointer user_data) 27 | { 28 | f->ctx = g_main_context_new(); 29 | f->loop = g_main_loop_new(f->ctx, FALSE); 30 | f->tl = timeline_new(f->ctx, tl_func, f); 31 | f->times = g_array_new(FALSE, FALSE, sizeof(double)); 32 | } 33 | 34 | static void 35 | tl_fixture_tear_down(TimelineFixture *f, gconstpointer user_data) 36 | { 37 | g_array_unref(f->times); 38 | timeline_free(&f->tl); 39 | g_main_loop_unref(f->loop); 40 | g_main_context_unref(f->ctx); 41 | } 42 | 43 | static void 44 | test_timeline_timeout(TimelineFixture *f, gconstpointer user_data) 45 | { 46 | guint timeouts[] = {1, 2, 3}; 47 | 48 | for (guint i = 0; i < G_N_ELEMENTS(timeouts); i++) 49 | timeline_add_timeout(&f->tl, timeouts[i]); 50 | 51 | timeline_start(&f->tl); 52 | g_test_timer_start(); 53 | g_main_loop_run(f->loop); 54 | timeline_stop(&f->tl); 55 | 56 | g_assert_cmpint(f->times->len, ==, G_N_ELEMENTS(timeouts)); 57 | 58 | for (guint i = 0; i < f->times->len; i++) 59 | g_assert_cmpint(g_array_index(f->times, double, i), <=, timeouts[i]); 60 | } 61 | 62 | int 63 | main(int argc, char *argv[]) 64 | { 65 | setlocale(LC_ALL, ""); 66 | 67 | g_test_init(&argc, &argv, NULL); 68 | 69 | g_test_add("/timeline/timeout", TimelineFixture, NULL, 70 | tl_fixture_set_up, test_timeline_timeout, tl_fixture_tear_down); 71 | 72 | return g_test_run(); 73 | } 74 | --------------------------------------------------------------------------------