├── .github └── workflows │ ├── alpine-linux-test.yml │ └── static.yml ├── .gitignore ├── CHANGELOG ├── CONTRIBUTING ├── LICENSE ├── README.md ├── dev-tools ├── assets │ └── base.css ├── continuous-build.sh ├── github-action-build-alphine-musl.sh ├── github-action-build-docs.sh ├── hot-reload.sh ├── run └── scripts │ ├── build-html.js │ ├── package-lock.json │ ├── package.json │ └── run.js ├── docs ├── code-example.css ├── contributing.html ├── doc.css ├── images │ ├── screenshot-rofi.webp │ └── screenshot-zenity.webp ├── index.html ├── seq-diagram-1.svg ├── seq-diagram-parallel.svg └── seq-diagram-serial.svg ├── examples ├── dmenu-example.sh ├── rofi-example.sh ├── scripts │ └── rofi-example.py └── zenity-jq-example.sh ├── gcovr.cfg ├── meson.build ├── src ├── .gitignore ├── Makefile.am ├── accepted-actions.enum.c ├── accepted-actions.enum.h ├── app.c ├── app.cmdline ├── app.h ├── cmdline.c ├── cmdline.h ├── error-message.dialog.c ├── error-message.dialog.h ├── json-glib.extension.c ├── json-glib.extension.h ├── logger.c ├── logger.h ├── main.entrypoint.c ├── polkit-auth-handler.service.c ├── polkit-auth-handler.service.h ├── request-messages.c └── request-messages.h └── test ├── app.mock.c ├── app.mock.h ├── assets ├── test_response_cancel.sh ├── test_response_command.sh ├── test_response_fail_retry.sh ├── test_response_parallel.sh └── test_response_serial.sh ├── error-message.mock.c ├── error-message.mock.h ├── gtk.mock.c ├── gtk.mock.h ├── logger.mock.c ├── logger.mock.h ├── polkit-auth-handler.service.mock.c ├── polkit-auth-handler.service.mock.h ├── polkit.mock.c ├── polkit.mock.h ├── test-it-command-exec.entrypoint.c ├── test-it-parallel-mode.entrypoint.c ├── test-it-serial-mode.entrypoint.c └── test-unit.entrypoint.c /.github/workflows/alpine-linux-test.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Build and test for Alphine Linux aarch64 3 | 4 | on: 5 | # Runs when creating releases 6 | push: 7 | tags: 8 | - v0.* 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 14 | permissions: 15 | contents: read 16 | pages: write 17 | id-token: write 18 | 19 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 20 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 21 | concurrency: 22 | group: "alpine-musl" 23 | cancel-in-progress: false 24 | 25 | jobs: 26 | build: 27 | runs-on: ubuntu-latest 28 | strategy: 29 | matrix: 30 | include: 31 | - rust-target: aarch64-unknown-linux-musl 32 | os-arch: aarch64 33 | env: 34 | CROSS_SYSROOT: /mnt/alpine-${{ matrix.os-arch }} 35 | steps: 36 | - name: Set up Alpine Linux for ${{ matrix.os-arch }} (target arch) 37 | id: alpine-target 38 | uses: jirutka/setup-alpine@v1 39 | with: 40 | arch: ${{ matrix.os-arch }} 41 | branch: edge 42 | packages: > 43 | openrc 44 | dbus 45 | gcc 46 | meson 47 | musl 48 | musl-dev 49 | gcovr 50 | glib 51 | glib-dev 52 | json-glib 53 | json-glib-dev 54 | polkit 55 | polkit-dev 56 | gtk+3.0 57 | gtk+3.0-dev 58 | valgrind 59 | shell-name: alpine.sh 60 | - name: Checkout 61 | uses: actions/checkout@v4 62 | - name: Setup build-test 63 | run: ${{ github.workspace }}/dev-tools/github-action-build-alphine-musl.sh 64 | shell: alpine.sh {0} 65 | continue-on-error: true 66 | - name: Upload artifact 67 | uses: actions/upload-artifact@v4 68 | with: 69 | name: build-alphine-musl 70 | path: 'build-alphine-musl' 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["master"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - uses: awalsh128/cache-apt-pkgs-action@latest 35 | with: 36 | packages: meson gcovr libjson-glib-1.0-0 libjson-glib-dev libpolkit-agent-1-0 libpolkit-agent-1-dev libgtk-3-dev valgrind 37 | version: 1.0 38 | - uses: actions/setup-node@v4 39 | with: 40 | node-version: 21 41 | - name: Setup build-test 42 | run: ${{ github.workspace }}/dev-tools/github-action-build-docs.sh 43 | - name: Setup Pages 44 | uses: actions/configure-pages@v4 45 | - name: Upload artifact 46 | uses: actions/upload-pages-artifact@v3 47 | with: 48 | path: 'build-docs' 49 | - name: Deploy to GitHub Pages 50 | id: deployment 51 | uses: actions/deploy-pages@v4 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #build dirs 2 | build/ 3 | build-test/ 4 | build-docs/ 5 | 6 | #cache dir 7 | .cache 8 | 9 | #nodejs node_modules 10 | node_modules 11 | 12 | # Prerequisites 13 | *.d 14 | 15 | # Object files 16 | *.o 17 | *.ko 18 | *.obj 19 | *.elf 20 | 21 | # Linker output 22 | *.ilk 23 | *.map 24 | *.exp 25 | 26 | # Precompiled Headers 27 | *.gch 28 | *.pch 29 | 30 | # Libraries 31 | *.lib 32 | *.a 33 | *.la 34 | *.lo 35 | 36 | # Shared objects (inc. Windows DLLs) 37 | *.dll 38 | *.so 39 | *.so.* 40 | *.dylib 41 | 42 | # Executables 43 | *.exe 44 | *.out 45 | *.app 46 | *.i*86 47 | *.x86_64 48 | *.hex 49 | 50 | # Debug files 51 | *.dSYM/ 52 | *.su 53 | *.idb 54 | *.pdb 55 | 56 | # Kernel Module Compile Results 57 | *.mod* 58 | *.cmd 59 | .tmp_versions/ 60 | modules.order 61 | Module.symvers 62 | Mkfile.old 63 | dkms.conf 64 | 65 | # IDE settings 66 | .vscode -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.4.0] - Unreleased 9 | 10 | ### Added 11 | 12 | ## [0.3.0] - 2024-07-05 13 | 14 | ### Added 15 | 16 | - Add "polkit action" parameter on authentication request 17 | - Add zenity with jq example 18 | - Improve serial mode tests 19 | - Improve `--help` message 20 | - Add copyright/license info on `--version` message 21 | 22 | ## [0.2.0] - 2024-05-25 23 | 24 | ### Added 25 | 26 | - Tests 27 | - Github pages documentation 28 | 29 | ### Fixed 30 | 31 | - Serial mode not authenticating sequentially 32 | - Unescaped json 33 | 34 | 35 | ## [0.1.0] - 2021-06-21 36 | 37 | ### Added 38 | 39 | - cmd-polkit first release 40 | 41 | 42 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | # Contributing to "cmd-polkit" 2 | 3 | ## Code of conduct 4 | 5 | This project has adopted the Contributor Covenant as its Code of Conduct, and we expect project 6 | participants to adhere to it. Please read the full text so that you can understand what actions 7 | will and will not be tolerated. 8 | 9 | ## Open Development 10 | 11 | All work on "cmd-polkit" happens directly on GitHub. Both team members and contributors send pull 12 | requests which go through the same review process 13 | 14 | ## Versioning Policy 15 | 16 | "cmd-polkit" follows semantic versioning. We release patch versions for critical bugfixes, minor 17 | versions for new features or non-essential changes, and major versions for any breaking changes. 18 | When we make breaking changes, we also introduce deprecation warnings in a minor version so that 19 | our users learn about the upcoming changes and migrate their code in advance. 20 | 21 | Every significant change is documented in the CHANGELOG.md file. 22 | 23 | ## Branch Organization 24 | 25 | Submit all changes directly to the main branch. We don’t use separate branches for development or 26 | for upcoming releases. We do our best to keep main in good shape, with all tests passing. 27 | 28 | Code that lands in main must be compatible with the latest stable release. It may contain additional 29 | features, but no breaking changes. We should be able to release a new minor version from the tip of 30 | main at any time 31 | 32 | ## Issues 33 | 34 | We are using GitHub Issues for our bugs. We keep a close eye on this and try to make it 35 | clear when we have an internal fix in progress. Before filing a new task, try to make sure your 36 | problem does not already exist. 37 | 38 | 39 | ## Contribution Prerequisites 40 | 41 | - You have a C compiler and meson installed at latest stable. 42 | - you have the following dependencies 43 | 44 | | Dependency | Version | 45 | |--------------|---------| 46 | | glib | 2.0 | 47 | | json-glib | 1.0 | 48 | | polkit-agent | 2.0 | 49 | | gtk+ | 3.0 | 50 | 51 | - You are familiar with Git. 52 | - For testing, you have valgrind installed 53 | - For documentation: you have NodeJs installed (recommended to use the latest LTS version) 54 | 55 | ## Development Workflow 56 | 57 | After cloning the project's code repository, you can run several commands that are the shell 58 | scripts in `dev-tools` folder. 59 | 60 | - `dev-tools/continuous-build.sh` builds the project and run tests automatically for time 61 | there is a code file change. 62 | - `dev-tools/github-action-build.sh` builds the project, tests, publishes test reports and 63 | builds documentation, used for github pages workflow. 64 | - `dev-tools/hot-reload.sh` builds the project and runs cmd-polkit using 65 | `examples/scripts/rofi-example.py` as the command. Each time a code file changes, rebuilds 66 | the project and restarts cmd-polkit with the updated code. Keep in mind that only one polkit 67 | agent is allowed at the same time. 68 | - `dev-tools/run` simply runs NodeJs scripts for documentation purposes, run 69 | `dev-tools/run help` to list the scripts 70 | 71 | ## Style Guide 72 | 73 | Unlike C, other languages have their own style guides to follow. So, in this project, 74 | the single most important rule when writing code is this: check the surrounding code and 75 | try to imitate it. 76 | 77 | As a maintainer, it is dismaying to receive a patch that is obviously in a different coding 78 | style to the surrounding code. This is disrespectful, like someone tromping into a 79 | spotlessly-clean house with muddy shoes. 80 | 81 | So, whatever this document recommends, if there is already written code and you are 82 | contributing to it, keep its current style consistent even if it is not your favorite style. 83 | 84 | Most importantly, do not make your first contribution to a project a change in the coding style 85 | to suit your taste. That is incredibly disrespectful. 86 | 87 | The only thing we enforce is consistency. If you wish to follow a style guide, we recommend to 88 | use, the Gnome coding style 89 | ( https://developer.gnome.org/documentation/guidelines/programming/coding-style.html ) 90 | as both polkit and gtk follows this style guide. We followed the same style as to maintain 91 | consistency with the code and API. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cmd-polkit 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | A tool that allows to easily customize the UI used to authenticate on polkit 17 | 18 | ### Dependencies 19 | 20 | | Dependency | Version | 21 | |--------------|---------| 22 | | glib | 2.0 | 23 | | json-glib | 1.0 | 24 | | polkit-agent | 2.0 | 25 | | gtk+ | 3.0 | 26 | 27 | # Instalation 28 | 29 | It requires [meson](https://mesonbuild.com/index.html) build system and the dependencies installed 30 | 31 | 32 | ```bash 33 | $ meson setup build 34 | $ meson compile -C build 35 | $ meson install -C build 36 | ``` 37 | 38 | More documentation on https://omarcastro.github.io/cmd-polkit/ -------------------------------------------------------------------------------- /dev-tools/assets/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: sans-serif; 3 | font-size: 1rem; 4 | 5 | --tab_size: 4; 6 | --bg-color: #eee; 7 | --table-bg-color: LightSteelBlue; 8 | --table-bg-odd-line-color: aliceblue; 9 | --table-header-bg-color: SteelBlue; 10 | --table-header-fg-color: white; 11 | --table-hover-bg-color: #ddd; 12 | 13 | --fg-color: #111; 14 | --link-color: navy; 15 | --link-color-visited: maroon; 16 | --theme-blue: blue; 17 | --lineno-bg: #EFE383; 18 | --lineno-border-color: #BBB15F; 19 | --branch-line-bg: lightgray; 20 | --branch-line-border-color: gray; 21 | 22 | --meter-bg: whitesmoke; 23 | --meter-border-color: black; 24 | --meter-border-color-high: black; 25 | 26 | --unknown_color: lightgray; 27 | --low_color: #FF6666; 28 | --medium_color: #F9FD63; 29 | --partial_covered_color: var(--medium_color); 30 | --uncovered_color: #FF8C8C; 31 | --warning_color: orangered; 32 | --notTakenBranch_color: red; 33 | --uncheckedDecision_color: darkorange; 34 | --notTakenDecision_color: red; 35 | --notInvokedCall_color: red; 36 | --excluded_color: rgb(255, 241, 229); 37 | 38 | --high_color: #85E485; 39 | --covered_color: #85E485; 40 | --takenBranch_color: green; 41 | --takenDecision_color: green; 42 | --invokedCall_color: green; 43 | } 44 | 45 | @media screen and (prefers-color-scheme: dark) { 46 | :root { 47 | --bg-color: #202530; 48 | --table-bg-color: #36471c; 49 | --table-bg-odd-line-color: #36571c; 50 | --fg-color: #eee; 51 | --link-color: #66B4FF; 52 | --link-color-visited: #FF8C8C; 53 | --theme-blue: #A6B4FF; 54 | --table-header-bg-color: #36571c; 55 | --table-header-fg-color: white; 56 | --table-hover-bg-color: #303530; 57 | 58 | --lineno-bg: #755F00; 59 | --lineno-border-color: #753F00; 60 | --branch-line-bg: #303530; 61 | --branch-line-border-color: #101520; 62 | 63 | --meter-bg: #202530; 64 | --meter-border-color: gray; 65 | --meter-border-color-high: rgb(77 146 33); 66 | 67 | --unknown_color: #333; 68 | --low_color: #921F39; 69 | --medium_color: #755F00; 70 | --partial_covered_color: var(--medium_color); 71 | --uncovered_color: #921F39; 72 | --warning_color: #390911; 73 | --notTakenBranch_color: #390911; 74 | --uncheckedDecision_color: darkorange; 75 | --notTakenDecision_color: #921F39; 76 | --notInvokedCall_color: #921F39; 77 | --excluded_color: rgb(19, 17, 16); 78 | 79 | --high_color: #1d541a; 80 | --covered_color: #1d541a; 81 | --takenBranch_color: #0d540a; 82 | --takenDecision_color: #0d540a; 83 | --invokedCall_color: #0d540a; 84 | } 85 | } 86 | 87 | 88 | body 89 | { 90 | color: var(--fg-color); 91 | background-color: var(--bg-color); 92 | } 93 | 94 | h1 95 | { 96 | text-align: center; 97 | margin: 0; 98 | padding-bottom: 10px; 99 | font-size: 20pt; 100 | font-weight: bold; 101 | } 102 | 103 | hr 104 | { 105 | background-color: var(--link-color); 106 | height: 2px; 107 | border: 0; 108 | } 109 | 110 | /* Link formats: use maroon w/underlines */ 111 | a:link 112 | { 113 | color: var(--link-color); 114 | text-decoration: underline; 115 | } 116 | a:visited 117 | { 118 | color: var(--link-color-visited); 119 | text-decoration: underline; 120 | } 121 | 122 | /*** Summary formats ***/ 123 | 124 | .summary 125 | { 126 | display: flex; 127 | flex-flow: row wrap; 128 | max-width: 100%; 129 | justify-content: flex-start; 130 | } 131 | 132 | .summary > table 133 | { 134 | flex: 1 0 7em; 135 | border: 0; 136 | } 137 | 138 | .summary > :last-child { 139 | margin-left: auto; 140 | } 141 | 142 | table.legend 143 | { 144 | color: var(--fg-color); 145 | display: flex; 146 | flex-flow: row wrap; 147 | justify-content: flex-start; 148 | } 149 | 150 | table.legend th[scope=row] 151 | { 152 | font-weight: normal; 153 | text-align: right; 154 | white-space: nowrap; 155 | } 156 | 157 | table.legend td 158 | { 159 | color: var(--theme-blue); 160 | text-align: left; 161 | white-space: nowrap; 162 | padding-left: 5px; 163 | } 164 | 165 | table.legend td.legend 166 | { 167 | color: var(--fg-color); 168 | font-size: 80%; 169 | } 170 | 171 | table.legend td.warning_text 172 | { 173 | color: var(--warning_color); 174 | } 175 | 176 | table.coverage td, 177 | table.coverage th 178 | { 179 | text-align: right; 180 | color: var(--fg-color); 181 | font-weight: normal; 182 | white-space: nowrap; 183 | padding-left: 5px; 184 | padding-right: 4px; 185 | } 186 | 187 | table.coverage td 188 | { 189 | background-color: var(--table-bg-color); 190 | } 191 | 192 | table.coverage th[scope=row] 193 | { 194 | color: var(--fg-color);; 195 | font-weight: normal; 196 | white-space: nowrap; 197 | } 198 | 199 | table.coverage th[scope=col] 200 | { 201 | color: var(--theme-blue);; 202 | font-weight: normal; 203 | white-space: nowrap; 204 | } 205 | 206 | table.legend span 207 | { 208 | margin-right: 4px; 209 | padding: 2px; 210 | } 211 | 212 | table.legend span.coverage-unknown, 213 | table.legend span.coverage-none, 214 | table.legend span.coverage-low, 215 | table.legend span.coverage-medium, 216 | table.legend span.coverage-high 217 | { 218 | padding-left: 3px; 219 | padding-right: 3px; 220 | } 221 | 222 | table.legend span.coverage-unknown, 223 | table.coverage td.coverage-unknown, 224 | table.file-list td.coverage-unknow 225 | { 226 | background-color: var(--unknown_color) !important; 227 | } 228 | 229 | table.legend span.coverage-none, 230 | table.legend span.coverage-low, 231 | table.coverage td.coverage-none, 232 | table.coverage td.coverage-low, 233 | table.file-list td.coverage-none, 234 | table.file-list td.coverage-low 235 | { 236 | background-color: var(--low_color) !important; 237 | } 238 | 239 | table.legend span.coverage-medium, 240 | table.coverage td.coverage-medium, 241 | table.file-list td.coverage-medium 242 | { 243 | background-color: var(--medium_color) !important; 244 | } 245 | 246 | table.legend span.coverage-high, 247 | table.coverage td.coverage-high, 248 | table.file-list td.coverage-high 249 | { 250 | background-color: var(--high_color) !important; 251 | } 252 | /*** End of Summary formats ***/ 253 | /*** Meter formats ***/ 254 | 255 | /* Common */ 256 | meter { 257 | width: 30vw; 258 | min-width: 4em; 259 | max-width: 15em; 260 | height: 0.75em; 261 | padding: 0; 262 | vertical-align: baseline; 263 | margin-top: 3px; 264 | /* Outer background for Mozilla */ 265 | background: none; 266 | background-color: var(--meter-bg); 267 | } 268 | /* Webkit */ 269 | 270 | meter::-webkit-meter-bar { 271 | /* Outer background for Webkit */ 272 | background: none; 273 | background-color: var(--meter-bg); 274 | height: 0.75em; 275 | border-radius: 0px; 276 | } 277 | 278 | meter::-webkit-meter-optimum-value, 279 | meter::-webkit-meter-suboptimum-value, 280 | meter::-webkit-meter-even-less-good-value 281 | { 282 | /* Inner shadow for Webkit */ 283 | border: solid 1px var(--meter-border-color); 284 | } 285 | 286 | meter.coverage-none::-webkit-meter-optimum-value, 287 | meter.coverage-low::-webkit-meter-optimum-value 288 | { 289 | background: var(--low_color); 290 | } 291 | 292 | meter.coverage-medium::-webkit-meter-optimum-value 293 | { 294 | background: var(--medium_color); 295 | } 296 | 297 | meter.coverage-high::-webkit-meter-optimum-value 298 | { 299 | background: var(--high_color); 300 | border-color: var(--meter-border-color-high); 301 | } 302 | 303 | /* Mozilla */ 304 | 305 | meter::-moz-meter-bar 306 | { 307 | box-sizing: border-box; 308 | } 309 | 310 | meter:-moz-meter-optimum::-moz-meter-bar, 311 | meter:-moz-meter-sub-optimum::-moz-meter-bar, 312 | meter:-moz-meter-sub-sub-optimum::-moz-meter-bar 313 | { 314 | /* Inner shadow for Mozilla */ 315 | border: solid 1px var(--meter-border-color); 316 | } 317 | 318 | meter.coverage-none:-moz-meter-optimum::-moz-meter-bar, 319 | meter.coverage-low:-moz-meter-optimum::-moz-meter-bar 320 | { 321 | background: var(--low_color); 322 | } 323 | 324 | meter.coverage-medium:-moz-meter-optimum::-moz-meter-bar 325 | { 326 | background: var(--medium_color); 327 | } 328 | 329 | meter.coverage-high:-moz-meter-optimum::-moz-meter-bar 330 | { 331 | background: var(--high_color); 332 | border-color: var(--meter-border-color-high); 333 | } 334 | 335 | /*** End of Meter formats ***/ 336 | .file-list td, .file-list th { 337 | padding: 0.4em 0.8em; 338 | font-weight: bold; 339 | } 340 | 341 | .file-list th[scope^=col] 342 | { 343 | text-align: center; 344 | color: var(--table-header-fg-color); 345 | background-color: var(--table-header-bg-color); 346 | font-size: 120%; 347 | } 348 | 349 | .file-list th[scope=row] 350 | { 351 | text-align: left; 352 | color: black; 353 | font-family: monospace; 354 | font-weight: bold; 355 | font-size: 110%; 356 | } 357 | 358 | .file-list tr > td, 359 | .file-list tr > th { 360 | background: var(--table-bg-odd-line-color); 361 | } 362 | 363 | .file-list tr:nth-child(even) > td, 364 | .file-list tr:nth-child(even) > th { 365 | background: var(--table-bg-color) 366 | } 367 | 368 | .file-list tr:hover > td, 369 | .file-list tr:hover > th[scope=row] 370 | { 371 | background-color: var(--table-hover-bg-color); 372 | } 373 | td.CoverValue 374 | { 375 | text-align: right; 376 | white-space: nowrap; 377 | } 378 | 379 | td.coveredLine, 380 | span.coveredLine 381 | { 382 | background-color: var(--covered_color) !important; 383 | } 384 | 385 | td.partialCoveredLine, 386 | span.partialCoveredLine 387 | { 388 | background-color: var(--partial_covered_color) !important; 389 | } 390 | 391 | td.uncoveredLine, 392 | span.uncoveredLine 393 | { 394 | background-color: var(--uncovered_color) !important; 395 | } 396 | 397 | td.excludedLine, 398 | span.excludedLine 399 | { 400 | background-color: var(--excluded_color) !important; 401 | } 402 | 403 | .linebranch, .linedecision, .linecall, .linecount 404 | { 405 | font-family: monospace; 406 | border-right: 1px var(--branch-line-border-color) solid; 407 | background-color: var(--branch-line-bg); 408 | text-align: right; 409 | } 410 | 411 | 412 | .linebranchDetails, .linedecisionDetails, .linecallDetails 413 | { 414 | position: relative; 415 | } 416 | .linebranchSummary, .linedecisionSummary, .linecallSummary 417 | { 418 | cursor: help; 419 | } 420 | .linebranchContents, .linedecisionContents, .linecallContents 421 | { 422 | font-family: sans-serif; 423 | font-size: small; 424 | text-align: left; 425 | position: absolute; 426 | width: 15em; 427 | padding: 1em; 428 | background: white; 429 | border: solid gray 1px; 430 | box-shadow: 5px 5px 10px gray; 431 | z-index: 1; /* show in front of the table entries */ 432 | } 433 | 434 | .takenBranch 435 | { 436 | color: var(--takenBranch_color) !important; 437 | } 438 | 439 | .notTakenBranch 440 | { 441 | color: var(--notTakenBranch_color) !important; 442 | } 443 | 444 | .takenDecision 445 | { 446 | color: var(--takenDecision_color) !important; 447 | } 448 | 449 | .notTakenDecision 450 | { 451 | color: var(--notTakenDecision_color) !important; 452 | } 453 | 454 | .uncheckedDecision 455 | { 456 | color: var(--uncheckedDecision_color) !important; 457 | } 458 | 459 | .invokedCall 460 | { 461 | color: var(--invokedCall_color) !important; 462 | } 463 | 464 | .notInvokedCall 465 | { 466 | color: var(--notInvokedCall_color) !important; 467 | } 468 | 469 | .src 470 | { 471 | padding-left: 12px; 472 | text-align: left; 473 | 474 | font-family: monospace; 475 | white-space: pre; 476 | 477 | tab-size: var(--tab_size); 478 | -moz-tab-size: var(--tab_size); 479 | } 480 | 481 | span.takenBranch, 482 | span.notTakenBranch, 483 | span.takenDecision, 484 | span.notTakenDecision, 485 | span.uncheckedDecision 486 | { 487 | font-family: monospace; 488 | font-weight: bold; 489 | } 490 | 491 | pre 492 | { 493 | height : 15px; 494 | margin-top: 0; 495 | margin-bottom: 0; 496 | } 497 | 498 | .listOfFunctions td, .listOfFunctions th { 499 | padding: 0 10px; 500 | } 501 | .listOfFunctions th 502 | { 503 | text-align: center; 504 | color: var(--table-header-fg-color); 505 | background-color: var(--table-header-bg-color); 506 | } 507 | .listOfFunctions tr > td { 508 | background: var(--table-bg-odd-line-color); 509 | } 510 | .listOfFunctions tr:nth-child(even) > td { 511 | background: var(--table-bg-color) 512 | } 513 | .listOfFunctions tr:hover > td 514 | { 515 | background-color: var(--table-hover-bg-color); 516 | } 517 | .listOfFunctions tr > td > a 518 | { 519 | text-decoration: none; 520 | color: inherit; 521 | } 522 | 523 | .source-line 524 | { 525 | height : 15px; 526 | margin-top: 0; 527 | margin-bottom: 0; 528 | } 529 | 530 | .lineno 531 | { 532 | background-color: var(--lineno-bg); 533 | border-right: 1px solid var(--lineno-border-color); 534 | text-align: right; 535 | unicode-bidi: embed; 536 | font-family: monospace; 537 | white-space: pre; 538 | } 539 | 540 | .lineno > a 541 | { 542 | text-decoration: none; 543 | color: inherit; 544 | } 545 | 546 | .file-list 547 | { 548 | margin: 1em auto; 549 | border: 0; 550 | border-spacing: 1px; 551 | } 552 | 553 | .file-source table 554 | { 555 | border-spacing: 0; 556 | } 557 | 558 | .file-source table td, 559 | .file-source table th 560 | { 561 | padding: 1px 10px; 562 | } 563 | 564 | .file-source table th 565 | { 566 | font-family: monospace; 567 | font-weight: bold; 568 | } 569 | 570 | .file-source table td:last-child 571 | { 572 | width: 100%; 573 | } 574 | footer 575 | { 576 | text-align: center; 577 | padding-top: 3px; 578 | } -------------------------------------------------------------------------------- /dev-tools/continuous-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd "$(dirname "${BASH_SOURCE[0]}")" 3 | cd .. 4 | 5 | PROJECT_DIR=$(pwd) 6 | 7 | build_project() { 8 | cd $PROJECT_DIR/build-test 9 | meson build 10 | meson test > /dev/null 11 | RET=$? 12 | cat meson-logs/testlog.txt 13 | } 14 | 15 | rm -rf build-test 16 | meson setup build-test -Db_coverage=true --reconfigure 17 | build_project 18 | 19 | while inotifywait -e close_write $PROJECT_DIR/src $PROJECT_DIR/test; do 20 | build_project 21 | done 22 | -------------------------------------------------------------------------------- /dev-tools/github-action-build-alphine-musl.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) 3 | cd "$DIR/.." 4 | 5 | meson setup build-alphine-musl -Db_coverage=true --warnlevel 2 || exit 1 6 | meson compile -C build-alphine-musl || exit 1 7 | meson test --wrap='valgrind --leak-check=full' -C build-alphine-musl 8 | ninja coverage -C build-alphine-musl -------------------------------------------------------------------------------- /dev-tools/github-action-build-docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd "$(dirname "${BASH_SOURCE[0]}")" 3 | cd .. 4 | 5 | meson setup build-test -Db_coverage=true --warnlevel 2 || exit 1 6 | meson compile -C build-test || exit 1 7 | meson test --wrap='valgrind --leak-check=full' -C build-test 8 | ninja coverage -C build-test 9 | ls build-test/meson-logs/testlog.json || cp build-test/meson-logs/testlog-valgrind.json build-test/meson-logs/testlog.json 10 | ls build-test/meson-logs/testlog.txt || cp build-test/meson-logs/testlog-valgrind.txt build-test/meson-logs/testlog.txt 11 | dev-tools/run docs -------------------------------------------------------------------------------- /dev-tools/hot-reload.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd "$(dirname "${BASH_SOURCE[0]}")" 3 | cd .. 4 | 5 | PROJECT_DIR=$(pwd) 6 | 7 | rm -rf build 8 | mkdir build 9 | cd build 10 | meson --prefix=/usr .. 11 | cd .. 12 | 13 | current_pid="" 14 | 15 | function kill_running_app { 16 | if [[ "$current_pid" != "" ]]; then 17 | echo "killing running app with pid $current_pid" 18 | kill $current_pid; 19 | wait $current_pid; 20 | fi 21 | } 22 | 23 | function reload { 24 | kill_running_app 25 | cd $PROJECT_DIR 26 | $PROJECT_DIR/build/cmd-polkit-agent -sv -c "python examples/scripts/rofi-example.py" & 27 | current_pid=$! 28 | echo "app running with pid $current_pid" 29 | 30 | } 31 | 32 | function build_and_reload { 33 | cd $PROJECT_DIR/build && ninja 34 | reload 35 | } 36 | 37 | function handle_exit { 38 | kill_running_app 39 | } 40 | 41 | trap handle_exit EXIT 42 | 43 | build_and_reload 44 | while inotifywait -e close_write $PROJECT_DIR/src; do 45 | build_and_reload 46 | done 47 | -------------------------------------------------------------------------------- /dev-tools/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) 3 | echo "$DIR/scripts" 4 | cd "$DIR/scripts" && node run.js "$@" 5 | -------------------------------------------------------------------------------- /dev-tools/scripts/build-html.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase, max-lines-per-function, jsdoc/require-jsdoc, jsdoc/require-param-description */ 2 | import Prism from 'prismjs' 3 | import { minimatch } from 'minimatch' 4 | import { imageSize } from 'image-size' 5 | import { JSDOM } from 'jsdom' 6 | import { marked } from 'marked' 7 | import { existsSync } from 'node:fs' 8 | import { readdir, readFile } from 'node:fs/promises' 9 | import { resolve, relative } from 'node:path' 10 | 11 | const dom = new JSDOM('', { 12 | url: import.meta.url, 13 | }) 14 | /** @type {Window} */ 15 | const window = dom.window 16 | const document = window.document 17 | const DOMParser = window.DOMParser 18 | 19 | globalThis.window = dom.window 20 | globalThis.document = document 21 | 22 | if (document == null) { 23 | throw new Error('error parsing document') 24 | } 25 | // @ts-ignore 26 | await import('prismjs/plugins/keep-markup/prism-keep-markup.js') 27 | // @ts-ignore 28 | await import('prismjs/components/prism-json.js') 29 | await import('prismjs/components/prism-bash.js') 30 | 31 | const projectPath = new URL('../../', import.meta.url) 32 | const docsPath = new URL('docs', projectPath).pathname 33 | const docsOutputPath = new URL('build-docs', projectPath).pathname 34 | 35 | const fs = await import('fs') 36 | 37 | const data = fs.readFileSync(`${docsPath}/${process.argv[2]}`, 'utf8') 38 | 39 | const parsed = new DOMParser().parseFromString(data, 'text/html') 40 | document.replaceChild(parsed.documentElement, document.documentElement) 41 | 42 | const exampleCode = (strings, ...expr) => { 43 | let statement = strings[0] 44 | 45 | for (let i = 0; i < expr.length; i++) { 46 | statement += String(expr[i]).replace(/ [...document.documentElement.querySelectorAll(selector)] 57 | 58 | const readFileImport = (file) => existsSync(`${docsOutputPath}/${file}`) ? fs.readFileSync(`${docsOutputPath}/${file}`, 'utf8') : fs.readFileSync(`${docsPath}/${file}`, 'utf8') 59 | 60 | const promises = [] 61 | 62 | /** 63 | * @param {Element} element 64 | * @returns {string} code classes 65 | */ 66 | const exampleCodeClass = (element) => { 67 | const {classList} = element 68 | const lineNoClass = classList.contains("line-numbers") ? " line-numbers" : '' 69 | const wrapClass = classList.contains("wrap") ? " wrap" : '' 70 | return "keep-markup" + lineNoClass + wrapClass 71 | } 72 | 73 | queryAll('script.html-example').forEach(element => { 74 | const pre = document.createElement('pre') 75 | pre.innerHTML = exampleCode`${dedent(element.innerHTML)}` 76 | element.replaceWith(pre) 77 | }) 78 | 79 | queryAll('script.css-example').forEach(element => { 80 | const pre = document.createElement('pre') 81 | pre.innerHTML = exampleCode`${dedent(element.innerHTML)}` 82 | element.replaceWith(pre) 83 | }) 84 | 85 | queryAll('script.json-example').forEach(element => { 86 | const pre = document.createElement('pre') 87 | pre.innerHTML = exampleCode`${dedent(element.innerHTML)}` 88 | element.replaceWith(pre) 89 | }) 90 | 91 | queryAll('script.js-example').forEach(element => { 92 | const pre = document.createElement('pre') 93 | pre.innerHTML = exampleCode`${dedent(element.innerHTML)}` 94 | element.replaceWith(pre) 95 | }) 96 | 97 | queryAll('svg[ss:include]').forEach(element => { 98 | const ssInclude = element.getAttribute('ss:include') 99 | const svgText = readFileImport(ssInclude) 100 | element.outerHTML = svgText 101 | element.removeAttribute('ss:include') 102 | }) 103 | 104 | queryAll('[ss:markdown]:not([ss:include])').forEach(element => { 105 | const md = dedent(element.innerHTML) 106 | .replaceAll('\n>', '\n>') // for blockquotes, innerHTML escapes ">" chars 107 | console.error(md) 108 | element.innerHTML = marked(md, { mangle: false, headerIds: false }) 109 | element.removeAttribute('ss:markdown') 110 | }) 111 | 112 | queryAll('[ss:markdown][ss:include]').forEach(element => { 113 | const ssInclude = element.getAttribute('ss:include') 114 | const md = readFileImport(ssInclude) 115 | element.innerHTML = marked(md, { mangle: false, headerIds: false }) 116 | element.removeAttribute('ss:markdown') 117 | element.removeAttribute('ss:include') 118 | }) 119 | 120 | queryAll('code').forEach(highlightElement) 121 | 122 | queryAll('[ss:aria-label]').forEach(element => { 123 | element.removeAttribute('ss:aria-label') 124 | if(element.hasAttribute('title') && !element.hasAttribute('aria-label')){ 125 | element.setAttribute("aria-label", element.getAttribute("title")) 126 | } 127 | }) 128 | 129 | queryAll('img[ss:size]').forEach(element => { 130 | const imageSrc = element.getAttribute('src') 131 | const getdefinedLength = (attr) => { 132 | if(!element.hasAttribute(attr)){ return undefined } 133 | const length = element.getAttribute(attr) 134 | if(isNaN(parseInt(length)) || isNaN(+length)){ return undefined } 135 | return +length 136 | } 137 | const definedWidth = getdefinedLength('width') 138 | const definedHeight = getdefinedLength('height') 139 | if(definedWidth && definedHeight){ 140 | return 141 | } 142 | const size = imageSize(`${docsOutputPath}/${imageSrc}`) 143 | element.removeAttribute('ss:size') 144 | const {width, height} = size 145 | if(definedWidth){ 146 | element.setAttribute('width', `${definedWidth}`) 147 | element.setAttribute('height', `${Math.ceil(height * definedWidth/width)}`) 148 | return 149 | } 150 | if(definedHeight){ 151 | element.setAttribute('width', `${Math.ceil(width * definedHeight/height)}`) 152 | element.setAttribute('height', `${definedHeight}`) 153 | return 154 | } 155 | element.setAttribute('width', `${size.width}`) 156 | element.setAttribute('height', `${size.height}`) 157 | }) 158 | 159 | promises.push(...queryAll('img[ss:badge-attrs]').map(async (element) => { 160 | const imageSrc = element.getAttribute('src') 161 | const svgText = await readFile(`${docsOutputPath}/${imageSrc}`, 'utf8') 162 | const div = document.createElement('div') 163 | div.innerHTML = svgText 164 | const svg = div.querySelector('svg') 165 | if (!svg) { throw Error(`${docsOutputPath}/${imageSrc} is not a valid svg`) } 166 | 167 | if(!element.hasAttribute('alt') && !element.matches('[ss:badge-attrs~=-alt]')){ 168 | const alt = svg.getAttribute('aria-label') 169 | if (alt) { element.setAttribute('alt', alt) } 170 | } 171 | 172 | if(!element.hasAttribute('title') && !element.matches('[ss:badge-attrs~=-title]')){ 173 | const title = svg.querySelector('title')?.textContent 174 | if (title) { element.setAttribute('title', title) } 175 | } 176 | element.removeAttribute('ss:badge-attrs') 177 | })) 178 | 179 | promises.push(...queryAll('style').map(async element => { 180 | element.innerHTML = await minifyCss(element.innerHTML) 181 | })) 182 | 183 | promises.push(...queryAll('link[href][rel="stylesheet"][ss:inline]').map(async element => { 184 | const href = element.getAttribute('href') 185 | const cssText = readFileImport(href) 186 | element.outerHTML = `` 187 | })) 188 | 189 | promises.push(...queryAll('link[href][ss:repeat-glob]').map(async (element) => { 190 | const href = element.getAttribute('href') 191 | if (!href) { return } 192 | for await (const filename of getFiles(docsOutputPath)) { 193 | const relativePath = relative(docsOutputPath, filename) 194 | if (!minimatch(relativePath, href)) { continue } 195 | const link = document.createElement('link') 196 | for (const { name, value } of element.attributes) { 197 | link.setAttribute(name, value) 198 | } 199 | link.removeAttribute('ss:repeat-glob') 200 | link.setAttribute('href', filename) 201 | element.insertAdjacentElement('afterend', link) 202 | } 203 | element.remove() 204 | })) 205 | 206 | const tocUtils = { 207 | getOrCreateId: (element) => { 208 | const id = element.getAttribute('id') || element.textContent.trim().toLowerCase().replaceAll(/\s+/g, '-') 209 | if (!element.hasAttribute('id')) { 210 | element.setAttribute('id', id) 211 | } 212 | return id 213 | }, 214 | createMenuItem: (element) => { 215 | const a = document.createElement('a') 216 | const li = document.createElement('li') 217 | a.href = `#${element.id}` 218 | a.textContent = element.textContent 219 | li.append(a) 220 | return li 221 | }, 222 | getParentOL: (element, path) => { 223 | while (path.length > 0) { 224 | const [title, possibleParent] = path.at(-1) 225 | if (title.tagName < element.tagName) { 226 | const possibleParentList = possibleParent.querySelector('ol') 227 | if (!possibleParentList) { 228 | const ol = document.createElement('ol') 229 | possibleParent.append(ol) 230 | return ol 231 | } 232 | return possibleParentList 233 | } 234 | path.pop() 235 | } 236 | return null 237 | }, 238 | } 239 | 240 | await Promise.all(promises) 241 | 242 | queryAll('[ss:toc]').forEach(element => { 243 | const ol = document.createElement('ol') 244 | /** @type {[HTMLElement, HTMLElement][]} */ 245 | const path = [] 246 | for (const element of queryAll(':is(h2, h3, h4, h5, h6):not(.no-toc), h1.yes-toc')) { 247 | tocUtils.getOrCreateId(element) 248 | const parent = tocUtils.getParentOL(element, path) || ol 249 | const li = tocUtils.createMenuItem(element) 250 | parent.append(li) 251 | path.push([element, li]) 252 | } 253 | element.replaceWith(ol) 254 | }) 255 | 256 | const minifiedHtml = '' + minifyDOM(document.documentElement).outerHTML 257 | 258 | fs.writeFileSync(`${docsOutputPath}/${process.argv[2]}`, minifiedHtml) 259 | 260 | function dedent (templateStrings, ...values) { 261 | const matches = [] 262 | const strings = typeof templateStrings === 'string' ? [templateStrings] : templateStrings.slice() 263 | strings[strings.length - 1] = strings[strings.length - 1].replace(/\r?\n([\t ]*)$/, '') 264 | for (const string of strings) { 265 | const match = string.match(/\n[\t ]+/g) 266 | match && matches.push(...match) 267 | } 268 | if (matches.length) { 269 | const size = Math.min(...matches.map(value => value.length - 1)) 270 | const pattern = new RegExp(`\n[\t ]{${size}}`, 'g') 271 | for (let i = 0; i < strings.length; i++) { 272 | strings[i] = strings[i].replace(pattern, '\n') 273 | } 274 | } 275 | 276 | strings[0] = strings[0].replace(/^\r?\n/, '') 277 | let string = strings[0] 278 | for (let i = 0; i < values.length; i++) { 279 | string += values[i] + strings[i + 1] 280 | } 281 | return string 282 | } 283 | 284 | async function * getFiles (dir) { 285 | const dirents = await readdir(dir, { withFileTypes: true }) 286 | 287 | for (const dirent of dirents) { 288 | const res = resolve(dir, dirent.name) 289 | if (dirent.isDirectory()) { 290 | yield * getFiles(res) 291 | } else { 292 | yield res 293 | } 294 | } 295 | } 296 | 297 | async function minifyCss (cssText) { 298 | const esbuild = await import('esbuild') 299 | const result = await esbuild.transform(cssText, { loader: 'css', minify: true }) 300 | return result.code 301 | } 302 | 303 | /** 304 | * Minifies the DOM tree 305 | * @param {Element} domElement - target DOM tree root element 306 | * @returns {Element} root element of the minified DOM 307 | */ 308 | function minifyDOM (domElement) { 309 | const window = domElement.ownerDocument.defaultView 310 | const { TEXT_NODE, ELEMENT_NODE, COMMENT_NODE } = window.Node 311 | 312 | /** @typedef {"remove-blank" | "1-space" | "pre"} WhitespaceMinify */ 313 | /** 314 | * @typedef {object} MinificationState 315 | * @property {WhitespaceMinify} whitespaceMinify - current whitespace minification method 316 | */ 317 | 318 | /** 319 | * Minify the text node based con current minification status 320 | * @param {ChildNode} node - current text node 321 | * @param {WhitespaceMinify} whitespaceMinify - whitespace minification removal method 322 | */ 323 | function minifyTextNode (node, whitespaceMinify) { 324 | if (whitespaceMinify === 'pre') { 325 | return 326 | } 327 | // blank node is empty or contains whitespace only, so we remove it 328 | const isBlankNode = !/[^\s]/.test(node.nodeValue) 329 | if (isBlankNode && whitespaceMinify === 'remove-blank') { 330 | node.remove() 331 | return 332 | } 333 | if (whitespaceMinify === '1-space') { 334 | node.nodeValue = node.nodeValue.replace(/\s\s+/g, ' ') 335 | } 336 | } 337 | 338 | const defaultMinificationState = { whitespaceMinify: '1-space' } 339 | 340 | /** 341 | * @param {Element} element 342 | * @param {MinificationState} minificationState 343 | * @returns {MinificationState} update minification State 344 | */ 345 | function updateMinificationStateForElement (element, minificationState) { 346 | const tag = element.tagName.toLowerCase() 347 | // by default,
 renders whitespace as is, so we do not want to minify in this case
348 |     if (['pre'].includes(tag)) {
349 |       return { ...minificationState, whitespaceMinify: 'pre' }
350 |     }
351 |     //  and  are not rendered in the viewport, so we remove it
352 |     if (['html', 'head'].includes(tag)) {
353 |       return { ...minificationState, whitespaceMinify: 'remove-blank' }
354 |     }
355 |     // in the , the default whitespace behaviour is to merge multiple whitespaces to 1,
356 |     // there will stil have some whitespace that will be merged, but at this point, there is
357 |     // little benefit to remove even more duplicated whitespace
358 |     if (['body'].includes(tag)) {
359 |       return { ...minificationState, whitespaceMinify: '1-space' }
360 |     }
361 |     return minificationState
362 |   }
363 | 
364 |   /**
365 |    * @param {Element} currentElement - current element to minify
366 |    * @param {MinificationState} minificationState - current minificationState
367 |    */
368 |   function walkElementMinification (currentElement, minificationState) {
369 |     const { whitespaceMinify } = minificationState
370 |     // we have to make a copy of the iterator for traversal, because we cannot
371 |     // iterate through what we'll be modifying at the same time
372 |     const values = [...currentElement?.childNodes?.values()]
373 |     for (const node of values) {
374 |       if (node.nodeType === COMMENT_NODE) {
375 |       // remove comments node
376 |         currentElement.removeChild(node)
377 |       } else if (node.nodeType === TEXT_NODE) {
378 |         minifyTextNode(node, whitespaceMinify)
379 |       } else if (node.nodeType === ELEMENT_NODE) {
380 |         // process child elements recursively
381 |         const updatedState = updateMinificationStateForElement(node, minificationState)
382 |         walkElementMinification(node, updatedState)
383 |       }
384 |     }
385 |   }
386 |   const initialMinificationState = updateMinificationStateForElement(domElement, defaultMinificationState)
387 |   walkElementMinification(domElement, initialMinificationState)
388 |   return domElement
389 | }
390 | 
391 | /**
392 |  * Applies syntax highligth on elements
393 |  * @param {Element} domElement - target DOM tree root element
394 |  * @returns {Element} root element of the minified DOM
395 |  */
396 | function highlightElement(domElement){
397 |   Prism.highlightElement(domElement, false)
398 |   domElement.innerHTML = domElement.innerHTML.split('\n')
399 |   .map(line => `${line}`)
400 |   .join('\n')
401 | }


--------------------------------------------------------------------------------
/dev-tools/scripts/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |     "type": "module",
 3 |     "dependencies": {
 4 |         "badge-maker": "^4.1.0",
 5 |         "esbuild": "^0.24.2",
 6 |         "image-size": "^1.2.0",
 7 |         "js-yaml": "^4.1.0",
 8 |         "jsdom": "^26.0.0",
 9 |         "marked": "^15.0.6",
10 |         "minimatch": "^10.0.1",
11 |         "prismjs": "^1.29.0"
12 |     }
13 | }
14 | 


--------------------------------------------------------------------------------
/dev-tools/scripts/run.js:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env -S node --input-type=module
  2 | /* eslint-disable camelcase, max-lines-per-function, jsdoc/require-jsdoc, jsdoc/require-param-description */
  3 | /*
  4 | This file is purposely large to easily move the code to multiple projects, its build code, not production.
  5 | To help navigate this file is divided by sections:
  6 | @section 1 init
  7 | @section 2 tasks
  8 | @section 3 jobs
  9 | @section 4 utils
 10 | @section 5 Dev Server
 11 | @section 6 linters
 12 | @section 7 minifiers
 13 | @section 8 exec utilities
 14 | @section 9 filesystem utilities
 15 | @section 10 npm utilities
 16 | @section 11 versioning utilities
 17 | @section 12 badge utilities
 18 | @section 13 module graph utilities
 19 | @section 14 build tools plugins
 20 | */
 21 | import process from 'node:process'
 22 | import fs, { readFile as fsReadFile, writeFile } from 'node:fs/promises'
 23 | import { existsSync } from 'node:fs'
 24 | import { promisify } from 'node:util'
 25 | import { execFile as baseExecFile, exec as baseExec, spawn } from 'node:child_process'
 26 | const exec = promisify(baseExec)
 27 | const execFile = promisify(baseExecFile)
 28 | const readFile = (path) => fsReadFile(path, { encoding: 'utf8' })
 29 | 
 30 | // @section 1 init
 31 | 
 32 | const projectPathURL = new URL('../../', import.meta.url)
 33 | const pathFromProject = (path) => new URL(path, projectPathURL).pathname
 34 | process.chdir(pathFromProject('.'))
 35 | 
 36 | // @section 2 tasks
 37 | 
 38 | const helpTask = {
 39 |   description: 'show this help',
 40 |   cb: async () => { console.log(helpText()); process.exit(0) },
 41 | }
 42 | 
 43 | const tasks = {
 44 |   docs: {
 45 |     description: 'build documentation',
 46 |     cb: async () => { await buildDocs(); process.exit(0) },
 47 |   },
 48 |   help: helpTask,
 49 |   '--help': helpTask,
 50 |   '-h': helpTask,
 51 | }
 52 | 
 53 | async function main () {
 54 |   const args = process.argv.slice(2)
 55 |   if (args.length <= 0) {
 56 |     console.log(helpText())
 57 |     return process.exit(0)
 58 |   }
 59 | 
 60 |   const taskName = args[0]
 61 | 
 62 |   if (!Object.hasOwn(tasks, taskName)) {
 63 |     console.error(`unknown task ${taskName}\n\n${helpText()}`)
 64 |     return process.exit(1)
 65 |   }
 66 | 
 67 |   await checkNodeModulesFolder()
 68 |   await tasks[taskName].cb()
 69 |   return process.exit(0)
 70 | }
 71 | 
 72 | await main()
 73 | 
 74 | // @section 3 jobs
 75 | 
 76 | 
 77 | async function buildDocs () {
 78 |   logStartStage('build:docs', 'build docs')
 79 | 
 80 |   await rm_rf('build-docs')
 81 |   await cp_R('docs', 'build-docs')
 82 |   await cp_R('build-test/meson-logs', 'build-docs/reports')
 83 |   await cp_R('build-test/meson-logs', 'build-docs/reports')
 84 |   await createBadges()
 85 |   await Promise.all([
 86 |     exec(`${process.argv[0]} dev-tools/scripts/build-html.js index.html`),
 87 |     exec(`${process.argv[0]} dev-tools/scripts/build-html.js contributing.html`),
 88 |   ])
 89 | 
 90 |   logEndStage()
 91 | }
 92 | 
 93 | async function createBadges () {
 94 |   await makeBadgeForLicense(pathFromProject('build-docs/reports'))
 95 |   await makeBadgeForCoverages(pathFromProject('build-docs/reports'))
 96 |   await makeBadgeForTestResult(pathFromProject('build-docs/reports'))
 97 |   await makeBadgeForRepo(pathFromProject('build-docs/reports'))
 98 |   await makeBadgeForRelease(pathFromProject('build-docs/reports'))
 99 | }
100 | 
101 | // @section 4 utils
102 | 
103 | function helpText () {
104 |   const fromNPM = isRunningFromNPMScript()
105 | 
106 |   const helpArgs = fromNPM ? 'help' : 'help, --help, -h'
107 |   const maxTaskLength = Math.max(...[helpArgs, ...Object.keys(tasks)].map(text => text.length))
108 |   const tasksToShow = Object.entries(tasks).filter(([_, value]) => value !== helpTask)
109 |   const usageLine = fromNPM ? 'npm run ' : 'run '
110 |   return `Usage: ${usageLine}
111 | 
112 | Tasks: 
113 |   ${tasksToShow.map(([key, value]) => `${key.padEnd(maxTaskLength, ' ')}  ${value.description}`).join('\n  ')}
114 |   ${'help, --help, -h'.padEnd(maxTaskLength, ' ')}  ${helpTask.description}`
115 | }
116 | 
117 | /** @param {string[]} paths  */
118 | async function rm_rf (...paths) {
119 |   await Promise.all(paths.map(path => fs.rm(path, { recursive: true, force: true })))
120 | }
121 | 
122 | /** @param {string[]} paths  */
123 | async function mkdir_p (...paths) {
124 |   await Promise.all(paths.map(path => fs.mkdir(path, { recursive: true })))
125 | }
126 | 
127 | /**
128 |  * @param {string} src
129 |    @param {string} dest  */
130 | async function cp_R (src, dest) {
131 |   await cmdSpawn(`cp -r '${src}' '${dest}'`)
132 | 
133 |   // this command is a 1000 times slower that running the command, for that reason it is not used (30 000ms vs 30ms)
134 |   // await fs.cp(src, dest, { recursive: true })
135 | }
136 | 
137 | async function mv (src, dest) {
138 |   await fs.rename(src, dest)
139 | }
140 | 
141 | function logStage (stage) {
142 |   logEndStage(); logStartStage(logStage.currentJobName, stage)
143 | }
144 | 
145 | function logEndStage () {
146 |   const startTime = logStage.perfMarks[logStage.currentMark]
147 |   console.log(startTime ? `done (${Date.now() - startTime}ms)` : 'done')
148 | }
149 | 
150 | function logStartStage (jobname, stage) {
151 |   const markName = 'stage ' + stage
152 |   logStage.currentJobName = jobname
153 |   logStage.currentMark = markName
154 |   logStage.perfMarks ??= {}
155 |   stage && process.stdout.write(`[${jobname}] ${stage}...`)
156 |   logStage.perfMarks[logStage.currentMark] = Date.now()
157 | }
158 | 
159 | // @section 5 Dev server
160 | 
161 | // @section 6 linters
162 | 
163 | // @section 7 minifiers
164 | 
165 | // @section 8 exec utilities
166 | 
167 | /**
168 |  * @param {string} command
169 |  * @param {import('node:child_process').ExecFileOptions} options
170 |  * @returns {Promise} code exit
171 |  */
172 | function cmdSpawn (command, options = {}) {
173 |   const p = spawn('/bin/sh', ['-c', command], { stdio: 'inherit', ...options })
174 |   return new Promise((resolve) => { p.on('exit', resolve) })
175 | }
176 | 
177 | async function execCmd (command, args) {
178 |   const options = {
179 |     cwd: process.cwd(),
180 |     env: process.env,
181 |     stdio: 'pipe',
182 |     encoding: 'utf-8',
183 |   }
184 |   return await execFile(command, args, options)
185 | }
186 | 
187 | async function execGitCmd (args) {
188 |   return (await execCmd('git', args)).stdout.trim().toString().split('\n')
189 | }
190 | 
191 | // @section 9 filesystem utilities
192 | 
193 | async function listNonIgnoredFiles ({ ignorePath = '.gitignore', patterns } = {}) {
194 |   const { minimatch } = await import('minimatch')
195 |   const { join } = await import('node:path')
196 |   const { statSync, readdirSync } = await import('node:fs')
197 |   const ignorePatterns = await getIgnorePatternsFromFile(ignorePath)
198 |   const ignoreMatchers = ignorePatterns.map(pattern => minimatch.filter(pattern, { matchBase: true }))
199 |   const listFiles = (dir) => readdirSync(dir).reduce(function (list, file) {
200 |     const name = join(dir, file)
201 |     if (file === '.git' || ignoreMatchers.some(match => match(name))) { return list }
202 |     const isDir = statSync(name).isDirectory()
203 |     return list.concat(isDir ? listFiles(name) : [name])
204 |   }, [])
205 | 
206 |   const fileList = listFiles('.')
207 |   if (!patterns) { return fileList }
208 |   const intersection = patterns.flatMap(pattern => minimatch.match(fileList, pattern, { matchBase: true, dot: true }))
209 |   return [...new Set(intersection)]
210 | }
211 | 
212 | async function getIgnorePatternsFromFile (filePath) {
213 |   const content = await fs.readFile(filePath, 'utf8')
214 |   const lines = content.split('\n').filter(line => !line.startsWith('#') && line.trim() !== '')
215 |   return [...new Set(lines)]
216 | }
217 | 
218 | async function listChangedFilesMatching (...patterns) {
219 |   const { minimatch } = await import('minimatch')
220 |   const changedFiles = [...(await listChangedFiles())]
221 |   const intersection = patterns.flatMap(pattern => minimatch.match(changedFiles, pattern, { matchBase: true }))
222 |   return [...new Set(intersection)]
223 | }
224 | 
225 | async function listChangedFiles () {
226 |   const mainBranchName = 'main'
227 |   const mergeBase = await execGitCmd(['merge-base', 'HEAD', mainBranchName])
228 |   const diffExec = execGitCmd(['diff', '--name-only', '--diff-filter=ACMRTUB', mergeBase])
229 |   const lsFilesExec = execGitCmd(['ls-files', '--others', '--exclude-standard'])
230 |   return new Set([...(await diffExec), ...(await lsFilesExec)].filter(filename => filename.trim().length > 0))
231 | }
232 | 
233 | function isRunningFromNPMScript () {
234 |   return false
235 | }
236 | 
237 | // @section 10 npm utilities
238 | 
239 | async function checkNodeModulesFolder () {
240 |   if (existsSync(pathFromProject('dev-tools/scripts/node_modules'))) { return }
241 |   console.log('node_modules absent running "npm ci"...')
242 |   await cmdSpawn('npm ci', {cwd: pathFromProject('dev-tools/scripts')})
243 | }
244 | 
245 | 
246 | // @section 11 versioning utilities
247 | 
248 | async function getLatestReleasedVersion () {
249 |   const changelogContent = await readFile("CHANGELOG")
250 |   const versions = changelogContent.split('\n')
251 |     .map(line => {
252 |       const match = line.match(/^## \[([0-9]+\.[[0-9]+\.[[0-9]+)]\s+-\s+([^\s]+)/)
253 |       if(!match){
254 |         return null
255 |       }
256 |       return {version: match[1], releaseDate: match[2]}
257 |     }).filter(version => !!version)
258 |   const releasedVersions = versions.filter(version => {
259 |     return version.releaseDate.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)
260 |   })
261 |   return releasedVersions[0]
262 | }
263 | 
264 | // @section 12 badge utilities
265 | 
266 | function getBadgeColors () {
267 |   getBadgeColors.cache ??= {
268 |     green: '#007700',
269 |     yellow: '#777700',
270 |     orange: '#aa0000',
271 |     red: '#aa0000',
272 |     blue: '#007ec6',
273 |   }
274 |   return getBadgeColors.cache
275 | }
276 | 
277 | function asciiIconSvg (asciicode) {
278 |   return `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cstyle%3Etext %7Bfont-size: 10px; fill: %23333;%7D @media (prefers-color-scheme: dark) %7Btext %7B fill: %23ccc; %7D%7D %3C/style%3E%3Ctext x='0' y='10'%3E${asciicode}%3C/text%3E%3C/svg%3E`
279 | }
280 | 
281 | async function makeBadge (params) {
282 |   const { default: libMakeBadge } = await import('badge-maker/lib/make-badge.js')
283 |   return libMakeBadge({
284 |     style: 'for-the-badge',
285 |     ...params,
286 |   })
287 | }
288 | 
289 | function getLightVersionOfBadgeColor (color) {
290 |   const colors = getBadgeColors()
291 |   getLightVersionOfBadgeColor.cache ??= {
292 |     [colors.green]: '#90e59a',
293 |     [colors.yellow]: '#dd4',
294 |     [colors.orange]: '#fa7',
295 |     [colors.red]: '#f77',
296 |     [colors.blue]: '#acf',
297 |   }
298 |   return getLightVersionOfBadgeColor.cache[color]
299 | }
300 | 
301 | function badgeColor (pct) {
302 |   const colors = getBadgeColors()
303 |   if (pct > 80) { return colors.green }
304 |   if (pct > 60) { return colors.yellow }
305 |   if (pct > 40) { return colors.orange }
306 |   if (pct > 20) { return colors.red }
307 |   return 'red'
308 | }
309 | 
310 | async function svgStyle () {
311 |   const { document } = await loadDom()
312 |   const style = document.createElement('style')
313 |   style.innerHTML = `
314 |   text { fill: #333; }
315 |   .icon {fill: #444; }
316 |   rect.label { fill: #ccc; }
317 |   rect.body { fill: var(--light-fill); }
318 |   @media (prefers-color-scheme: dark) {
319 |     text { fill: #fff; }
320 |     .icon {fill: #ccc; }
321 |     rect.label { fill: #555; stroke: none; }
322 |     rect.body { fill: var(--dark-fill); }
323 |   }
324 |   `.replaceAll(/\n+\s*/g, '')
325 |   return style
326 | }
327 | 
328 | async function applyA11yTheme (svgContent, options = {}) {
329 |   const { document } = await loadDom()
330 |   const { body } = document
331 |   body.innerHTML = svgContent
332 |   const svg = body.querySelector('svg')
333 |   if (!svg) { return svgContent }
334 |   svg.querySelectorAll('text').forEach(el => el.removeAttribute('fill'))
335 |   if (options.replaceIconToText) {
336 |     const img = svg.querySelector('image')
337 |     if (img) {
338 |       const text = document.createElementNS('http://www.w3.org/2000/svg', 'text')
339 |       text.textContent = options.replaceIconToText
340 |       text.setAttribute('transform', 'scale(.15)')
341 |       text.classList.add('icon')
342 |       text.setAttribute('x', '90')
343 |       text.setAttribute('y', '125')
344 |       img.replaceWith(text)
345 |     }
346 |   }
347 |   const rects = Array.from(svg.querySelectorAll('rect'))
348 |   rects.slice(0, 1).forEach(el => {
349 |     el.classList.add('label')
350 |     el.removeAttribute('fill')
351 |   })
352 |   const colors = getBadgeColors()
353 |   let color = colors.red
354 |   rects.slice(1).forEach(el => {
355 |     color = el.getAttribute('fill') || colors.red
356 |     el.removeAttribute('fill')
357 |     el.classList.add('body')
358 |     el.style.setProperty('--dark-fill', color)
359 |     el.style.setProperty('--light-fill', getLightVersionOfBadgeColor(color))
360 |   })
361 |   svg.prepend(await svgStyle())
362 | 
363 |   return svg.outerHTML
364 | }
365 | 
366 | async function makeBadgeForCoverages (path) {
367 |   const coverateReport = await readFile(`${path}/coverage.txt`)
368 |   const percentage = coverateReport.split('\n')
369 |     .filter(line => line.startsWith("TOTAL"))
370 |     .map(line => line.replace(/TOTAL\s+[0-9]+\s+[0-9]+\s+([0-9]+)%.*/, "$1") )
371 |     [0]
372 |   const svg = await makeBadge({
373 |     label: 'coverage',
374 |     message: `${percentage}%`,
375 |     color: badgeColor(percentage),
376 |     logo: asciiIconSvg('🛡︎'),
377 |   })
378 | 
379 |   const badgeWrite = writeFile(`${path}/coverage-badge.svg`, svg)
380 |   const a11yBadgeWrite = writeFile(`${path}/coverage-badge-a11y.svg`, await applyA11yTheme(svg, { replaceIconToText: '🛡︎' }))
381 |   await Promise.all([badgeWrite, a11yBadgeWrite])
382 | }
383 | 
384 | 
385 | async function makeBadgeForRepo(path){
386 |     const svg = await makeBadge({
387 |       label: 'Code Repository',
388 |       message: 'Github',
389 |       color: getBadgeColors().blue,
390 |       logo: asciiIconSvg('❮❯'),
391 |     })
392 |   const badgeWrite = writeFile(`${path}/repo-badge.svg`, svg)
393 |   const a11yBadgeWrite = writeFile(`${path}/repo-badge-a11y.svg`, await applyA11yTheme(svg, { replaceIconToText: '❮❯' }))
394 |   await Promise.all([badgeWrite, a11yBadgeWrite])
395 | }
396 | 
397 | async function makeBadgeForRelease(path){
398 |   const releaseVersion = await getLatestReleasedVersion()
399 |   const svg = await makeBadge({
400 |     label: 'Release',
401 |     message: releaseVersion ? releaseVersion.version : "Unreleased",
402 |     color: getBadgeColors().blue,
403 |     logo: asciiIconSvg('⛴'),
404 |   })
405 | const badgeWrite = writeFile(`${path}/repo-release.svg`, svg)
406 | const a11yBadgeWrite = writeFile(`${path}/repo-release-a11y.svg`, await applyA11yTheme(svg, { replaceIconToText: '⛴' }))
407 | await Promise.all([badgeWrite, a11yBadgeWrite])
408 | }
409 | 
410 | makeBadgeForRelease
411 | 
412 | async function makeBadgeForTestResult (path) {
413 |   const stdout = await readFile(`${path}/testlog.json`).then(str => str.split("\n").map(line => line ? JSON.parse(line).stdout: "").join(''))
414 |   const tests = stdout.split('\n').filter(test => /^n?ok /.test(test) )
415 |   const passedTests = tests.filter(test => test.startsWith('ok'))
416 |   const testAmountFromTap = stdout.split('\n')
417 |     .filter(test => /^1../.test(test) )
418 |     .map(line => +line.split('..')[1] ?? 0)
419 |     .reduce((a, b) => a + b)
420 |   const testAmount =  testAmountFromTap || tests.length
421 |   const passedAmount = passedTests.length
422 |   const passed = passedAmount === testAmount
423 |   const svg = await makeBadge({
424 |     label: 'tests',
425 |     message: `${passedAmount} / ${testAmount}`,
426 |     color: passed ? '#007700' : '#aa0000',
427 |     logo: asciiIconSvg('✔'),
428 |     logoWidth: 16,
429 |   })
430 |   const badgeWrite = writeFile(`${path}/test-results-badge.svg`, svg)
431 |   const a11yBadgeWrite = writeFile(`${path}/test-results-badge-a11y.svg`, await applyA11yTheme(svg, { replaceIconToText: '✔' }))
432 |   await Promise.all([badgeWrite, a11yBadgeWrite])
433 | }
434 | 
435 | async function makeBadgeForLicense (path) {
436 |   const svg = await makeBadge({
437 |     label: ' license',
438 |     message: 'LGPL',
439 |     color: '#007700',
440 |     logo: asciiIconSvg('🏛'),
441 |   })
442 | 
443 |   const badgeWrite = writeFile(`${path}/license-badge.svg`, svg)
444 |   const a11yBadgeWrite = writeFile(`${path}/license-badge-a11y.svg`, await applyA11yTheme(svg, { replaceIconToText: '🏛' }))
445 |   await Promise.all([badgeWrite, a11yBadgeWrite])
446 | }
447 | 
448 | async function loadDom () {
449 |   if (!loadDom.cache) {
450 |     loadDom.cache = import('jsdom').then(({ JSDOM }) => {
451 |       const jsdom = new JSDOM('', { url: import.meta.url })
452 |       const window = jsdom.window
453 |       const DOMParser = window.DOMParser
454 |       /** @type {Document} */
455 |       const document = window.document
456 |       return { window, DOMParser, document }
457 |     })
458 |   }
459 |   return loadDom.cache
460 | }
461 | 
462 | // @section 13 module graph utilities
463 | 
464 | // @section 14 build tools plugins


--------------------------------------------------------------------------------
/docs/code-example.css:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * a11y-dark theme for JavaScript, CSS, and HTML
  3 |  * Based on a11y-syntax-highlighting theme: https://github.com/ericwbailey/a11y-syntax-highlighting
  4 |  * @author Omar Castro
  5 |  */
  6 | 
  7 | /*
  8 |  Light Theme
  9 |  */
 10 |  pre {
 11 |     --code-color: #545454;
 12 |     --code-bg: #fefefe;
 13 |     --comment-color: #696969;
 14 |     --punct-color: #545454;
 15 |     --prop-color:#007299;
 16 |     --bool-color:#008000;
 17 |     --str-color:#aa5d00;
 18 |     --oper-color:#008000;
 19 |     --func-color:#aa5d00;
 20 |     --kword-color:#d91e18;
 21 |     --regex-color:#d91e18;
 22 |  }
 23 |  
 24 |  /*
 25 |  Dark Theme
 26 |  */
 27 |  @media (prefers-color-scheme: dark) {
 28 |     pre {
 29 |         --code-color: #f8f8f2;
 30 |         --code-bg: #2b2b2b;
 31 |         --comment-color: #d4d0ab;
 32 |         --punct-color: #fefefe;
 33 |         --prop-color:#ffa07a;
 34 |         --bool-color:#00e0e0;
 35 |         --str-color:#abe338;
 36 |         --oper-color:#00e0e0;
 37 |         --func-color:#ffd700;
 38 |         --kword-color:#00e0e0;
 39 |         --regex-color:#ffd700;
 40 |         }
 41 | }
 42 | 
 43 |  code[class*="language-"],
 44 |  pre[class*="language-"] {
 45 |    color: var(--code-color);
 46 |    background: none;
 47 |    font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
 48 |    text-align: left;
 49 |    white-space: pre;
 50 |    word-spacing: normal;
 51 |    word-break: normal;
 52 |    word-wrap: normal;
 53 |    line-height: 1.5;
 54 |    tab-size: 4;
 55 |    hyphens: none;
 56 |  }
 57 |  
 58 |  /* Code blocks */
 59 |  pre[class*="language-"] {
 60 |    padding: 1em;
 61 |    margin: 0.5em 0;
 62 |    overflow: auto;
 63 |    border-radius: 0.3em;
 64 |  }
 65 |  
 66 |  :not(pre) > code[class*="language-"],
 67 |  pre[class*="language-"] {
 68 |    background: var(--code-bg);
 69 |  }
 70 |  
 71 |  /* Inline code */
 72 |  :not(pre) > code[class*="language-"] {
 73 |    padding: 0.1em;
 74 |    border-radius: 0.3em;
 75 |    white-space: normal;
 76 |  }
 77 | 
 78 |  pre:has(code.line-numbers[class*="language-"]){
 79 |   padding-left: 3em;
 80 |   counter-reset: line;
 81 |   position: relative;
 82 | 
 83 |   &::before {
 84 |     position: absolute;
 85 |     inset: 0 auto 0 3em;
 86 |     width: 1px;
 87 |     background: var(--code-color);
 88 |     opacity: 50%;
 89 |     content: " ";
 90 |   }
 91 | 
 92 |  }
 93 | 
 94 |  pre code.line-numbers .line{
 95 |   counter-increment: line;
 96 |   position: relative;
 97 |   padding-left: 0.5em;
 98 |   display: inline-block;
 99 | 
100 |   &::before {
101 |     content: counter(line);
102 |     position: absolute;
103 |     display: inline-block;
104 |     right: 100%;
105 |     width: 3em;
106 |     padding: 0 .5em;
107 |     text-align: right;
108 |     opacity: 50%;
109 |   }
110 |  }
111 |  
112 |  code.wrap {
113 |   text-wrap: wrap;
114 |  }
115 | 
116 |  .token.comment,
117 |  .token.prolog,
118 |  .token.doctype,
119 |  .token.cdata {
120 |    color: var(--comment-color);
121 |  }
122 |  
123 |  .token.punctuation {
124 |    color: var(--punct-color);
125 |  }
126 |  
127 |  .token.property,
128 |  .token.tag,
129 |  .token.constant,
130 |  .token.symbol,
131 |  .token.deleted {
132 |    color: var(--prop-color);
133 |  }
134 |  
135 |  .token.boolean,
136 |  .token.number {
137 |    color: var(--bool-color);
138 |  }
139 |  
140 |  .token.selector,
141 |  .token.attr-name,
142 |  .token.string,
143 |  .token.char,
144 |  .token.builtin,
145 |  .token.inserted {
146 |    color: var(--str-color);
147 |  }
148 |  
149 |  .token.operator,
150 |  .token.entity,
151 |  .token.url,
152 |  .language-css .token.string,
153 |  .style .token.string,
154 |  .token.variable {
155 |    color: var(--oper-color);
156 |  }
157 |  
158 |  .token.atrule,
159 |  .token.attr-value,
160 |  .token.function {
161 |    color: var(--func-color);
162 |  }
163 |  
164 |  .token.keyword {
165 |    color: var(--kword-color);
166 |  }
167 |  
168 |  .token.regex,
169 |  .token.important {
170 |    color: var(--regex-color);
171 |  }
172 |  
173 |  .token.important,
174 |  .token.bold {
175 |    font-weight: bold;
176 |  }
177 |  .token.italic {
178 |    font-style: italic;
179 |  }
180 |  
181 |  .token.entity {
182 |    cursor: help;
183 |  }
184 | 


--------------------------------------------------------------------------------
/docs/contributing.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |     
 4 |         Contributing to "cmd-polkit"
 5 |         
 6 |         
 7 |         
 8 |         
 9 |         
10 |         
11 |         
12 |         
13 |     
14 |     
15 |         
22 |         
23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/doc.css: -------------------------------------------------------------------------------- 1 | @import url('./code-example.css'); 2 | 3 | :root { 4 | --bg-color: #e5e5e5; 5 | --bg-code-color: #f3f3f3; 6 | --bg-sidebar-color: #ddd; 7 | --sidebar-border-color: #0002; 8 | --bg-color-active: #edecef; 9 | --fg-color: #303540; 10 | --fg-color-disabled: #404550; 11 | --resizable-border-color: #303540; 12 | --tab-active-color: #007299; 13 | --even-tr-bg: #00000008; 14 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; 15 | } 16 | 17 | 18 | @media (prefers-color-scheme: dark) { 19 | :root { 20 | --bg-color: #202530; 21 | --bg-code-color: #303540; 22 | --bg-sidebar-color: #303540; 23 | --sidebar-border-color: #fff2; 24 | --bg-color-active: #292d30; 25 | --fg-color: #dddcdf; 26 | --fg-color-disabled: #ccc; 27 | --resizable-border-color: #dddcdf; 28 | --tab-active-color: #fa5; 29 | --even-tr-bg: #ffffff08; 30 | } 31 | 32 | a { color: #6ad; } 33 | a:visited { color: #d8d;} 34 | } 35 | 36 | body { 37 | margin: 0; 38 | background-color: var(--bg-color); 39 | color: var(--fg-color); 40 | min-height: 100vh; 41 | display: grid; 42 | grid-template-columns: 1fr auto 1fr; 43 | flex-direction: row; 44 | counter-reset: heading; 45 | 46 | & > .sidebar { 47 | justify-self: start; 48 | background-color: var(--bg-sidebar-color); 49 | border-right: var(--sidebar-border-color) 1px solid; 50 | } 51 | 52 | & > .content { 53 | justify-self: stretch; 54 | padding: 3em; 55 | min-width: 0; 56 | } 57 | } 58 | 59 | /* Smartphones (portrait and landscape) ----------- */ 60 | @media only screen and ( 61 | ((max-width : 480px) and (orientation: portrait)) or 62 | ((max-height : 480px) and (orientation: landscape)) 63 | ){ 64 | body > .sidebar { 65 | display: none; 66 | } 67 | 68 | body { 69 | display: block; 70 | } 71 | 72 | body > .content { 73 | padding: 1em; 74 | } 75 | 76 | } 77 | 78 | /* large monitors (portrait and landscape) ----------- */ 79 | @media screen and (min-width: 1280px) { 80 | body > .content { 81 | max-width: 80rem; 82 | } 83 | } 84 | 85 | :not(pre) > code:not([class]), 86 | :not(pre) > code.language-none { 87 | background: var(--bg-code-color); 88 | border-radius: 0.3em; 89 | padding: .2em .4em; 90 | margin: 0; 91 | font-size: 0.9em; 92 | white-space: break-spaces; 93 | } 94 | 95 | .toc { 96 | position: sticky; 97 | top: 0; 98 | padding: 1em; 99 | & ol { 100 | counter-reset: section; 101 | list-style-type: none; 102 | padding-left: 1em; 103 | white-space: nowrap; 104 | } 105 | } 106 | 107 | .toc ol li::before { 108 | counter-increment: section; 109 | content: counters(section, ".") ". "; 110 | } 111 | 112 | .toc li a { 113 | color: inherit; 114 | text-decoration: none; 115 | &:hover { 116 | text-decoration: underline; 117 | } 118 | } 119 | 120 | h2::before { 121 | content: counter(heading)". "; 122 | counter-increment: heading; 123 | } 124 | 125 | h2 { 126 | counter-reset: subheading; 127 | } 128 | 129 | h3::before { 130 | content: counter(heading)"." counter(subheading)". "; 131 | counter-increment: subheading; 132 | } 133 | 134 | 135 | .section--badge { 136 | text-align: center 137 | } 138 | 139 | .section--badge img { 140 | object-fit: contain; 141 | } 142 | 143 | .section--badge a { 144 | display: inline-block; 145 | margin-right: 0.25em; 146 | margin-left: 0.25em; 147 | } 148 | 149 | h1 { 150 | text-align: center; 151 | } 152 | 153 | 154 | section.preview { 155 | text-align: center; 156 | } 157 | 158 | .resizable { 159 | display: block; 160 | position: relative; 161 | border: solid var(--resizable-border-color) 2px; 162 | resize: both; 163 | overflow:hidden; 164 | min-width: fit-content; 165 | width: fit-content; 166 | min-height: fit-content; 167 | height: fit-content; 168 | } 169 | 170 | .resizable::after { 171 | display: block; 172 | position: absolute; 173 | bottom: 1px; 174 | right: 1px; 175 | border: solid transparent 10px; 176 | padding: 0; 177 | content: " "; 178 | z-index: -2; 179 | width: 0; 180 | height: 0; 181 | border-left-color: var(--resizable-border-color); 182 | transform: translate(50%, 50%) rotate(45deg); 183 | } 184 | 185 | .ui-mode--mobile { 186 | --ui-mode: "mobile" 187 | } 188 | 189 | .ui-mode--desktop { 190 | --ui-mode: "desktop" 191 | } 192 | 193 | 194 | .example__json, .example__html, .example__view { 195 | flex: 1 0 25%; 196 | margin-left: 0.2em; 197 | margin-right: 0.2em; 198 | } 199 | 200 | .example__json .editor { 201 | border: #aaa 1px solid; 202 | border-radius: 0.3em; 203 | } 204 | 205 | span[contenteditable="true"] { 206 | border: 1px solid transparent; 207 | position: relative; 208 | transition: all linear 300ms; 209 | } 210 | 211 | span[contenteditable="true"]:empty { 212 | padding-right: 0.5em; 213 | } 214 | 215 | pre:hover code span[contenteditable="true"] { 216 | border: 1px solid #000; 217 | } 218 | 219 | span[contenteditable="true"]::before { 220 | content: "Editable"; 221 | position: absolute; 222 | font-size: 0.7em; 223 | opacity: 0; 224 | visibility: hidden; 225 | pointer-events: none; 226 | transition-property: visibility, opacity; 227 | transition-duration: 0s, 300ms; 228 | transition-delay: 300ms, 0s; 229 | transition-timing-function: linear; 230 | transform: translateY(-100%); 231 | } 232 | 233 | pre:hover code span[contenteditable="true"]::before { 234 | transition-property: visibility, opacity; 235 | transition-duration: 0s, 300ms; 236 | transition-delay: 0s; 237 | visibility: visible; 238 | opacity: 1; 239 | } 240 | 241 | .example .cm-editor .cm-scroller{ 242 | font-family: Consolas,Monaco,"Andale Mono","Ubuntu Mono",monospace; 243 | } 244 | 245 | .example__tabs { 246 | line-height: 2; 247 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 248 | white-space: nowrap; 249 | display: grid; 250 | grid-template: 251 | "tab1 tab2 tab3 tab4 tab5 ." auto 252 | "cont cont cont cont cont cont" auto 253 | / fit-content(30%) fit-content(30%) fit-content(30%) fit-content(30%) fit-content(30%) auto; 254 | } 255 | 256 | .example__view { 257 | position: relative; 258 | padding: 10px; 259 | border: 1px solid var(--fg-color); /* the color here */ 260 | border-radius: 5px; 261 | transition: .4s; 262 | 263 | } 264 | 265 | 266 | .example__view::before { 267 | content: "Result"; 268 | position: absolute; 269 | color: var(--tab-active-color); 270 | text-align: end; 271 | top: 0; 272 | right: 0; 273 | padding: 5px; 274 | font-family: Consolas,Monaco,"Andale Mono","Ubuntu Mono",monospace; 275 | background: var(--code-bg); 276 | border-top-right-radius: 5px; 277 | padding-left: 20px; 278 | clip-path: polygon(0 0, 100% 0, 100% 100%, 20px 100%); 279 | pointer-events: none; 280 | } 281 | 282 | .example__tabs > .tab:nth-of-type(1){ grid-area: tab1 } 283 | .example__tabs > .tab:nth-of-type(2){ grid-area: tab2 } 284 | .example__tabs > .tab:nth-of-type(3){ grid-area: tab3 } 285 | .example__tabs > .tab:nth-of-type(4){ grid-area: tab4 } 286 | .example__tabs > .tab:nth-of-type(5){ grid-area: tab5 } 287 | 288 | .example__tabs > .tab:nth-of-type(1):checked ~ .tab-content:nth-of-type(1), 289 | .example__tabs > .tab:nth-of-type(2):checked ~ .tab-content:nth-of-type(2), 290 | .example__tabs > .tab:nth-of-type(3):checked ~ .tab-content:nth-of-type(3), 291 | .example__tabs > .tab:nth-of-type(4):checked ~ .tab-content:nth-of-type(4), 292 | .example__tabs > .tab:nth-of-type(5):checked ~ .tab-content:nth-of-type(5){ 293 | display: initial; 294 | } 295 | 296 | 297 | .example__tabs > .tab-content { 298 | grid-area: cont; 299 | display: none; 300 | } 301 | 302 | .example__tabs .tab { 303 | display: inline-block; 304 | appearance: none; 305 | z-index: 1; 306 | } 307 | 308 | .example__tabs > .tab::after { 309 | content: attr(aria-label); 310 | color: var(--fg-color-disabled); 311 | font-size: 1.4em; 312 | padding: 0.2em 1em; 313 | border: transparent 1px solid; 314 | border-top-width: 0; 315 | padding-bottom: 1em; 316 | cursor: pointer; 317 | font-family: Consolas,Monaco,"Andale Mono","Ubuntu Mono",monospace; 318 | } 319 | 320 | .example__tabs input:checked::after { 321 | color: var(--fg-color); 322 | background: var(--bg-color-active); 323 | border: #aaa 1px solid; 324 | border-top-color: var(--tab-active-color); 325 | border-top-width: 2px; 326 | cursor: initial; 327 | } 328 | 329 | .example-ec-level-line { 330 | display: flex; 331 | justify-content: space-evenly; 332 | text-align: center; 333 | flex-wrap: wrap; 334 | } 335 | 336 | .example-ec-level-line > .example { 337 | max-width: 100%; 338 | } 339 | 340 | th, td { 341 | padding: 0.2em; 342 | } 343 | 344 | table, th, td { 345 | border-collapse: collapse; 346 | border: 1px solid var(--sidebar-border-color); 347 | padding: 0.3em 1em; 348 | } 349 | 350 | table tr:nth-child(2n){ 351 | background-color: var(--even-tr-bg); 352 | } 353 | 354 | .screenshots { 355 | display: flex; 356 | justify-content: center; 357 | flex-wrap: wrap; 358 | } 359 | 360 | button.zoom { 361 | background: none; 362 | border: none; 363 | } 364 | 365 | button.img-button { 366 | background: none; 367 | border: none; 368 | cursor: zoom-in; 369 | } 370 | 371 | 372 | 373 | button.img-button.zoom-close { 374 | background: var(--bg-color); 375 | border: none; 376 | } 377 | 378 | dialog.img-zoom { 379 | background: none; 380 | border: none; 381 | } 382 | 383 | dialog.img-zoom button.zoom-close { 384 | background: none; 385 | border: none; 386 | cursor: zoom-out; 387 | 388 | } 389 | 390 | dialog.img-zoom::backdrop { 391 | background: #00000080; 392 | } 393 | 394 | dialog.img-zoom img { 395 | max-width: 100vw; 396 | max-height: 100vh; 397 | } 398 | 399 | .section figure { 400 | text-align: center; 401 | & img { 402 | max-width: 100%; 403 | height: auto; 404 | } 405 | } -------------------------------------------------------------------------------- /docs/images/screenshot-rofi.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OmarCastro/cmd-polkit/71852e7760de264383859f350dd3a987d121775d/docs/images/screenshot-rofi.webp -------------------------------------------------------------------------------- /docs/images/screenshot-zenity.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OmarCastro/cmd-polkit/71852e7760de264383859f350dd3a987d121775d/docs/images/screenshot-zenity.webp -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cmd-polkit documentation 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 22 |
23 | 24 |

25 | cmd-polkit 26 |

27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 |
48 | ## Introducion 49 | 50 | `cmd-polkit` is a tool that allows to easily customize the UI used to authenticate on polkit. 51 | 52 |
53 |
54 | 57 |
Fig.1 - custom authentication using cmd-polkit with rofi
58 |
59 |
60 | 63 |
Fig.2 - custom authentication using cmd-polkit with zenity
64 |
65 |
66 |
67 | 68 | 69 |
70 | ## How it works 71 | 72 | It works by calling the defined command and communicate via stdin and stdout, 73 | the command receives the message from stdin and sends it by writing to stdout, 74 | each line represents a message, the format used for communication is JSON. 75 | 76 | The next figure show a sequence diagram of an authentication process. The 77 | "command" agent is the application to execute using the --command, 78 | or -c argument. 79 | 80 |
81 | sequence diagram of a single authentication process 82 |
Fig.3 - authentication process
83 |
84 | 85 |
86 | 87 |
88 | ## Authentication handling mode 89 | 90 | To run cmd-polkit, it is required to explicitly select the authentication handling 91 | mode: serial or parallel. This section explains each mode usage. 92 | 93 | ### Serial 94 | 95 | The serial mode, executed with `--serial`, or `-s` argument, 96 | all polkit authentication are handled one at a time. 97 | 98 | The next figure show a sequence diagram of an multiple authentication processes 99 | using serial mode. You will note that even after requesting a second authentication 100 | the command will only run after finishing the first authentication process. 101 | 102 |
103 | sequence diagram of mutliple authentication processes in serial mode 104 |
Fig.4 - authentication process in serial mode
105 |
106 | 107 | This is good for running GUI propmt applications that cannot have mutliple instances 108 | at the same time, like [rofi](https://github.com/davatorium/rofi). 109 | 110 | ### Parallel 111 | 112 | The parallel mode, executed with `--parallel`, or `-p` argument, 113 | Polkit authentication processes are handled in parallel. 114 | 115 | The next figure show a sequence diagram of an multiple authentication processes 116 | using serial mode. Each command can handle the process independently. 117 | 118 |
119 | sequence diagram of mutliple authentication processes in parallel mode 120 |
Fig.5 - authentication process in parallel mode
121 |
122 | 123 | 124 | This is good for running GUI propmt applications that can have mutliple instances. 125 | 126 | It is up to the user to define which mode is compatible with the GUI application that he 127 | wishes to use without needing to create and maintain a daemon for authentication in serie. 128 | 129 |
130 | 131 |
132 | 133 | ## Message schemas 134 | 135 | ### Authentication request 136 | 137 | When polkit request authentication, cmd-polkit will send a message 138 | to command stdin that respects the following schema: 139 | 140 |
{
141 |               "action": "request password",
142 |               "prompt": string 
143 |               "message": string 
144 |               "polkit action": null | {
145 |                 "id": string
146 |                 "description": string
147 |                 "message": string
148 |                 "vendor name": string
149 |                 "vendor url": string
150 |                 "icon name": string
151 |               }
152 |             }
153 | 154 | - `action` is hardcoded to show `"request password"` 155 | - `prompt` tha password input label, in other words, the prompt message to show to the user, just before the password input 156 | - `message` is the the message to show to the user 157 | - `polkit action` represents the polkit action related to the application that requests the authentication. Respects PolkitActionDescription[1] more about actions on the Polkit documentation[2] 158 | - `id` is the polkit action ID 159 | - `description` A human readable description of the action, e.g. “Install unsigned software”. 160 | - `message` A human readable message displayed to the user when asking for credentials when authentication is needed, similar but not always equal to root `message`. 161 | - `vendor name` is the name of the project or vendor that is supplying the action. 162 | - `vendor url` is the url of the project or vendor that is supplying the action. 163 | - `icon name` is the icon representing the project or vendor that is supplying the actions. 164 | 165 | To give an example when executing `pkexec echo 1` in a terminal with cmd-polkit active, 166 | the message it is send to command is similar to the following code: 167 | 168 | 171 | 172 | ### Authentication response 173 | 174 | After the _authentication message request_ is sent, it will expect 175 | a response from command stdout that respects the folloing schema, 176 | it will use the password to authenticate to polkit: 177 | 178 |
{
179 |               "action": "authenticate",
180 |               "password": string 
181 |             }
182 | 183 | ### Authentication result 184 | 185 | After authentication attempt is made is made, a message will be 186 | sent to command to show the result: 187 | 188 |
{
189 |               "action": "authorization response",
190 |               "authorized": boolean 
191 |             }
192 | 193 | If the authentication is successful, a SIGINT message is sent to command 194 | to finish it, otherwise, another _authentication message request_ is sent to command. 195 |
196 | 197 |
198 | ## References 199 | 200 | 1 https://www.freedesktop.org/software/polkit/docs/latest/PolkitActionDescription.html#polkit-action-description-get-annotation, last seen on .

201 | 2 https://www.freedesktop.org/software/polkit/docs/latest/polkit.8.html, last seen on .

202 |
203 |
204 | 205 | 206 | 209 | 210 | 211 | 212 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /docs/seq-diagram-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 54 | 55 | 56 | 57 | 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 | Polkit 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | cmd-polkit-agent 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | command 109 | 110 | 111 | 112 | 113 | 114 | 115 | Request authentication 116 | 117 | 118 | 119 | 120 | 121 | spawn command 122 | pipe stdin/stdout 123 | 124 | 125 | 126 | 127 | 128 | request authentication 129 | 130 | 131 | 132 | 133 | authenticate 134 | 135 | 136 | 137 | 138 | authenticate 139 | 140 | 141 | 142 | fail case 143 | 144 | 145 | 146 | 147 | 148 | authentication fail 149 | 150 | 151 | 152 | 153 | authentication fail 154 | 155 | 156 | 157 | 158 | request authentication 159 | 160 | 161 | 162 | 163 | authenticate 164 | 165 | 166 | 167 | 168 | authenticate 169 | 170 | 171 | 172 | 173 | authentication OK 174 | 175 | 176 | 177 | 178 | authentication OK 179 | 180 | 181 | 182 | 183 | close stdin/stdout 184 | 185 | 186 | 187 | 188 | SIGINT 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /docs/seq-diagram-parallel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 54 | 55 | 56 | 57 | 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 | Polkit 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | cmd-polkit-agent 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | command 117 | 118 | 119 | 120 | 121 | 122 | 123 | command 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | Request auth 1 132 | 133 | 134 | 135 | 136 | Request auth 2 137 | 138 | 139 | 140 | 141 | 142 | spawn command 143 | pipe stdin/stdout 144 | 145 | 146 | 147 | 148 | 149 | request authentication 150 | 151 | 152 | 153 | 154 | authenticate 155 | 156 | 157 | 158 | 159 | authenticate 1 160 | 161 | 162 | 163 | 164 | authentication 1 OK 165 | 166 | 167 | 168 | 169 | authentication OK 170 | 171 | 172 | 173 | 174 | close stdin/stdout 175 | 176 | 177 | 178 | 179 | SIGINT 180 | 181 | 182 | 183 | 184 | 185 | spawn command 186 | pipe stdin/stdout 187 | 188 | 189 | 190 | 191 | 192 | request authentication 193 | 194 | 195 | 196 | 197 | authenticate 198 | 199 | 200 | 201 | 202 | authenticate 2 203 | 204 | 205 | 206 | 207 | authentication 1 OK 208 | 209 | 210 | 211 | 212 | authentication OK 213 | 214 | 215 | 216 | 217 | close stdin/stdout 218 | 219 | 220 | 221 | 222 | SIGINT 223 | 224 | 225 | 226 | 227 | -------------------------------------------------------------------------------- /docs/seq-diagram-serial.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 54 | 55 | 56 | 57 | 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 | Polkit 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | cmd-polkit-agent 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | command 117 | 118 | 119 | 120 | 121 | 122 | 123 | command 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | Request auth 1 132 | 133 | 134 | 135 | 136 | Request auth 2 137 | 138 | 139 | 140 | 141 | 142 | spawn command 143 | pipe stdin/stdout 144 | 145 | 146 | 147 | 148 | 149 | request authentication 150 | 151 | 152 | 153 | 154 | authenticate 155 | 156 | 157 | 158 | 159 | authenticate 1 160 | 161 | 162 | 163 | 164 | authentication 1 OK 165 | 166 | 167 | 168 | 169 | authentication OK 170 | 171 | 172 | 173 | 174 | close stdin/stdout 175 | 176 | 177 | 178 | 179 | SIGINT 180 | 181 | 182 | 183 | 184 | 185 | spawn command 186 | pipe stdin/stdout 187 | 188 | 189 | 190 | 191 | 192 | request authentication 193 | 194 | 195 | 196 | 197 | authenticate 198 | 199 | 200 | 201 | 202 | authenticate 2 203 | 204 | 205 | 206 | 207 | authentication 1 OK 208 | 209 | 210 | 211 | 212 | authentication OK 213 | 214 | 215 | 216 | 217 | close stdin/stdout 218 | 219 | 220 | 221 | 222 | SIGINT 223 | 224 | 225 | 226 | 227 | -------------------------------------------------------------------------------- /examples/dmenu-example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Requirements: jq, notify-send 5 | # 6 | # If you use dmenu, apply this patch: 7 | # https://tools.suckless.org/dmenu/patches/password/ 8 | # 9 | 10 | # Set dmenu program and its options. 11 | # For example, on Wayland you can use "wmenu -P". 12 | MENU='dmenu -P' 13 | 14 | notify() { 15 | [ -z "$MESSAGE" ] && return 1 16 | [ -n "$NOTIFY_ID" ] && notify-send -h 'int:transient:1' -r "$NOTIFY_ID" "$@" || NOTIFY_ID="$(notify-send -h 'int:transient:1' -p "$@")" 17 | } 18 | 19 | notify_hide() { 20 | [ -n "$NOTIFY_ID" ] && notify -t 0 '' 21 | } 22 | 23 | body() { 24 | jq -rc --arg error "$ERROR" ' 25 | if $error != "" then $error + "\n" else empty end, 26 | if .["polkit action"].description then .["polkit action"].description else empty end, 27 | if .["polkit action"].id then "\n Action ID: " + .["polkit action"].id else empty end, 28 | if .["polkit action"]["vendor name"] then " Vendor: " + .["polkit action"]["vendor name"] else empty end, 29 | if .["polkit action"]["vendor url"] then " Vendor URL: " + .["polkit action"]["vendor url"] else empty end 30 | ' 31 | } 32 | 33 | password() { 34 | prompt="$(printf '%s' "$1" | jq -rc '.prompt // "Password"')" 35 | MESSAGE="$(printf '%s' "$1" | jq -rc '.message // empty')" 36 | body="$(printf '%s' "$1" | body)" 37 | 38 | notify -t 300000 "$MESSAGE" "$body" 39 | 40 | # Request a password prompt from dmenu 41 | response="$($MENU -p "${prompt%': '}" /dev/null; then 56 | notify_hide 57 | else 58 | ERROR='Authentication failed!' 59 | notify -u low "$MESSAGE" 'Authentication failed!' 60 | fi 61 | } 62 | 63 | if [ -z "$POLKIT_DMENU_INTERNAL" ]; then 64 | export POLKIT_DMENU_INTERNAL=1 65 | exec cmd-polkit-agent -s -c "$0" 66 | exit 0 67 | fi 68 | 69 | while IFS= read -r msg; do 70 | action="$(printf '%s' "$msg" | jq -rc '.action')" 71 | 72 | case "$action" in 73 | 'request password') 74 | password "$msg" 75 | ;; 76 | 'authorization response') 77 | response "$msg" 78 | ;; 79 | esac 80 | done 81 | -------------------------------------------------------------------------------- /examples/rofi-example.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd "$(dirname "${BASH_SOURCE[0]}")" 4 | ../build/cmd-polkit-agent -sv -c "python scripts/rofi-example.py" 5 | -------------------------------------------------------------------------------- /examples/scripts/rofi-example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | import sys 5 | import subprocess 6 | 7 | message = "" 8 | rofi_message = message 9 | prompt = "password: " 10 | 11 | def open_rofi(): 12 | cmd = ['rofi', '-password', '-dmenu', '-markup', '-no-fixed-num-lines', '-p', prompt, '-mesg', rofi_message ] 13 | p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 14 | out, err = p.communicate(); 15 | if out == b'': 16 | print( """{"action": "cancel"}""" ) 17 | exit(0) 18 | else: 19 | print( """{"action": "authenticate", "password": "%s"}""" % str(out, "utf-8").rstrip("\n"), flush=True) 20 | 21 | 22 | for line in sys.stdin: 23 | jsonObj = json.loads(line) 24 | if "action" not in jsonObj : 25 | continue 26 | elif jsonObj["action"] == "request password": 27 | #print( """{"password": "%s"}""" % "tesmar" , flush=True) 28 | if "prompt" in jsonObj: 29 | prompt = jsonObj["prompt"] 30 | if "message" in jsonObj: 31 | message = jsonObj["message"] 32 | rofi_message = message 33 | if "polkit action" in jsonObj and jsonObj["polkit action"] != None: 34 | polkit_action = jsonObj["polkit action"] 35 | rofi_message = rofi_message + "\n\n" 36 | if "description" in polkit_action: rofi_message = rofi_message + "\n Action: " + polkit_action["description"] 37 | if "id" in polkit_action: rofi_message = rofi_message + "\n Action ID: " + polkit_action["id"] 38 | if "vendor name" in polkit_action: rofi_message = rofi_message + "\n Vendor: " + polkit_action["vendor name"] 39 | if "vendor url" in polkit_action: rofi_message = rofi_message + "\nVendor URL: " + polkit_action["vendor url"] 40 | open_rofi() 41 | #elif jsonObj["action"] == "authorization response": 42 | # if "authorized" in jsonObj: 43 | # if jsonObj["authorized"]: 44 | # break 45 | # else: 46 | # rofi_message = message + "\n password failed" 47 | # open_rofi() 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /examples/zenity-jq-example.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | if test "$1" != '__internal_call'; then 4 | 5 | prepParams() { for i in "$@"; do printf "'%s' " "$i"; done } 6 | parameters="$(prepParams "$@")" 7 | 8 | exec cmd-polkit-agent -p -c "$0 __internal_call $parameters*" 9 | else 10 | shift 1 11 | 12 | # Read incoming messages one by one (line by line) 13 | # from stdin to variable $msg 14 | while read -r msg; do 15 | if echo "$msg" | jq -e '.action == "request password"' 1>/dev/null 2>/dev/null 16 | then 17 | prompt="$( printf '%s' "$msg" | jq -rc '.prompt // "Password:"')" 18 | message="$(printf '%s' "$msg" | jq -rc '.message')" 19 | message="$message$(printf '%s' "$msg" | jq -rc 'if .["polkit action"].description then "\n\n Action: \t\t" + .["polkit action"].description else empty end')" 20 | message="$message$(printf '%s' "$msg" | jq -rc 'if .["polkit action"].id then "\n Action ID: \t\t" + .["polkit action"].id else empty end')" 21 | message="$message$(printf '%s' "$msg" | jq -rc 'if .["polkit action"]["vendor name"] then "\n Vendor: \t\t" + .["polkit action"]["vendor name"] else empty end')" 22 | message="$message$(printf '%s' "$msg" | jq -rc 'if .["polkit action"]["vendor url"] then "\n Vendor URL: \t" + .["polkit action"]["vendor url"] else empty end')" 23 | 24 | 25 | response="$(zenity --entry --hide-text --text="$message\n\n$prompt" --title="Authentication required")" 26 | 27 | # Cancel authentication if no password was given, 28 | # otherwise respond with given password 29 | if test -z "$response" 30 | then echo '{"action":"cancel"}' 31 | else echo "{\"action\":\"authenticate\",\"password\": \"$response\"}" 32 | fi 33 | fi 34 | done 35 | fi -------------------------------------------------------------------------------- /gcovr.cfg: -------------------------------------------------------------------------------- 1 | filter = src/app\.c 2 | filter = src/accepted-actions.enum\.c 3 | # src/cmdline.c is generated by gengetopt so no testing, nor coverage will be done here 4 | filter = src/error-message.dialog\.c 5 | filter = src/json-glib.extension\.c 6 | filter = src/logger\.c 7 | # src/main.entrypoint.c contains the main(), so no testing, nor coverage will be done here 8 | filter = src/polkit-auth-handler.service\.c 9 | filter = src/request-messages\.c 10 | html-css = dev-tools/assets/base.css 11 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('cmd-polkit', 'c', version : '0.4.0-dev') 2 | 3 | prefix = get_option('prefix') 4 | libexecdir = join_paths(prefix, get_option('libexecdir')) 5 | sysconfdir = join_paths(prefix, get_option('sysconfdir')) 6 | autostartdir = join_paths(sysconfdir, 'xdg', 'autostart') 7 | 8 | deps = [ 9 | dependency('glib-2.0'), 10 | dependency('json-glib-1.0'), 11 | dependency('polkit-agent-1'), 12 | dependency('gtk+-3.0') 13 | ] 14 | 15 | common_sources = [ 16 | 'src/request-messages.c', 17 | 'src/accepted-actions.enum.c', 18 | 'src/json-glib.extension.c', 19 | ] 20 | 21 | main_sources = [ 22 | 'src/main.entrypoint.c', 23 | 'src/polkit-auth-handler.service.c', 24 | 'src/error-message.dialog.c', 25 | 'src/cmdline.c', 26 | 'src/logger.c', 27 | 'src/app.c', 28 | ] 29 | 30 | main_sources += common_sources 31 | 32 | test_sources = [ 33 | 'test/logger.mock.c', 34 | 'test/gtk.mock.c', 35 | 'test/app.mock.c', 36 | 'test/polkit.mock.c', 37 | 'test/error-message.mock.c', 38 | 'test/polkit-auth-handler.service.mock.c', 39 | ] + common_sources 40 | 41 | test_unit_sources = [ 'test/test-unit.entrypoint.c' ] + test_sources 42 | test_it_command_exec_sources = [ 'test/test-it-command-exec.entrypoint.c' ] + test_sources 43 | test_it_serial_mode = [ 'test/test-it-serial-mode.entrypoint.c' ] + test_sources 44 | test_it_parallel_mode = [ 'test/test-it-parallel-mode.entrypoint.c' ] + test_sources 45 | 46 | executable('cmd-polkit-agent', main_sources, 47 | dependencies: deps, 48 | install: true, 49 | ) 50 | 51 | test_env = [ 52 | 'LC_ALL=C', 53 | 'G_TEST_SRCDIR=@0@'.format(meson.current_source_dir()), 54 | 'G_TEST_BUILDDIR=@0@'.format(meson.current_build_dir()), 55 | ] 56 | 57 | test( 58 | 'test-suite-unit', 59 | executable('cmd-polkit-agent-test-unit', test_unit_sources, dependencies: deps), 60 | env: test_env, 61 | protocol: 'tap', 62 | workdir: join_paths(meson.current_source_dir(), 'test') 63 | ) 64 | 65 | test( 66 | 'test-suite-it', 67 | executable('cmd-polkit-agent-test-it-command-exec', test_it_command_exec_sources, dependencies: deps), 68 | env: test_env, 69 | protocol: 'tap', 70 | workdir: join_paths(meson.current_source_dir(), 'test') 71 | ) 72 | 73 | test( 74 | 'test-suite-it-serial-mode', 75 | executable('cmd-polkit-agent-test-it-serial-mode', test_it_serial_mode, dependencies: deps), 76 | env: test_env, 77 | protocol: 'tap', 78 | workdir: join_paths(meson.current_source_dir(), 'test') 79 | ) 80 | 81 | test( 82 | 'test-suite-it-parallel-mode', 83 | executable('cmd-polkit-agent-test-it-parallel-mode', test_it_parallel_mode, dependencies: deps), 84 | env: test_env, 85 | protocol: 'tap', 86 | workdir: join_paths(meson.current_source_dir(), 'test') 87 | ) 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | xfce-polkit 2 | -------------------------------------------------------------------------------- /src/Makefile.am: -------------------------------------------------------------------------------- 1 | 2 | NULL= 3 | 4 | libexec_PROGRAMS = xfce-polkit 5 | xfce_polkit_SOURCES = \ 6 | xfce-polkit.c \ 7 | xfce-polkit-listener.c \ 8 | xfce-polkit-listener.h \ 9 | $(NULL) 10 | 11 | xfce_polkit_CFLAGS = @GLIB_CFLAGS@ \ 12 | @LIBXFCE4UI_CFLAGS@ \ 13 | @POLKIT_AGENT_CFLAGS@ 14 | 15 | 16 | xfce_polkit_LDADD = @GLIB_LIBS@ \ 17 | @LIBXFCE4UI_LIBS@ \ 18 | @POLKIT_AGENT_LIBS@ 19 | 20 | 21 | #xfce-polkit-listener.o: xfce-polkit-listener.h 22 | 23 | -------------------------------------------------------------------------------- /src/accepted-actions.enum.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-2.1-or-later 2 | // Copyright (C) 2024 Omar Castro 3 | #include 4 | #include "accepted-actions.enum.h" 5 | 6 | const char * AcceptedAction_CANCEL_str_value = "cancel"; 7 | const char * AcceptedAction_AUTHENTICATE_str_value = "authenticate"; 8 | 9 | AcceptedAction accepted_action_value_of_str(const char * str){ 10 | if(str == NULL){ 11 | return AcceptedAction_UNKNOWN; 12 | } 13 | switch (str[0]) { 14 | case 'c': 15 | return strcmp(str, AcceptedAction_CANCEL_str_value) == 0 ? AcceptedAction_CANCEL: AcceptedAction_UNKNOWN; 16 | case 'a': 17 | return strcmp(str, AcceptedAction_AUTHENTICATE_str_value) == 0 ? AcceptedAction_AUTHENTICATE: AcceptedAction_UNKNOWN; 18 | } 19 | return AcceptedAction_UNKNOWN; 20 | } 21 | -------------------------------------------------------------------------------- /src/accepted-actions.enum.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-2.1-or-later 2 | // Copyright (C) 2024 Omar Castro 3 | #ifndef ENUM__H__ACCEPTED_ACTIONS 4 | #define ENUM__H__ACCEPTED_ACTIONS 5 | 6 | typedef enum { 7 | AcceptedAction_CANCEL = 1, 8 | AcceptedAction_AUTHENTICATE = 2, 9 | 10 | AcceptedAction_UNKNOWN = 0 11 | } AcceptedAction; 12 | 13 | AcceptedAction accepted_action_value_of_str(const char * str); 14 | 15 | #endif //EXTENSION__H__JSON_GLIB -------------------------------------------------------------------------------- /src/app.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-2.1-or-later 2 | // Copyright (C) 2024 Omar Castro 3 | #include 4 | #include 5 | #include "app.h" 6 | #include "cmdline.h" 7 | #include "logger.h" 8 | 9 | static AuthHandlingMode handling_mode; 10 | static char *cmd_line = NULL; 11 | static char ** cmd_line_argv = NULL; 12 | static int static_argc; 13 | static char ** static_argv; 14 | static bool isInitialized = false; 15 | 16 | const char* app__get_cmd_line(){ 17 | return cmd_line; 18 | } 19 | 20 | char** app__get_cmd_line_argv(){ 21 | return cmd_line_argv; 22 | } 23 | 24 | AuthHandlingMode app__get_auth_handling_mode(){ 25 | return handling_mode; 26 | } 27 | 28 | int app__get_argc(){ 29 | return static_argc; 30 | } 31 | char ** app__get_argv(){ 32 | return static_argv; 33 | } 34 | 35 | int app__init(int argc, char *argv[]){ 36 | if(isInitialized){ 37 | return 0; 38 | } 39 | 40 | static_argc = argc; 41 | static_argv = argv; 42 | 43 | struct gengetopt_args_info ai; 44 | if (cmdline_parser(argc, argv, &ai) != 0) { 45 | log__fail_cmdline__print_help(); 46 | goto cmd_exit_1; 47 | } 48 | 49 | bool runInSerie = ai.serial_given; 50 | bool runInParallel = ai.parallel_given; 51 | bool runInSilence = ai.quiet_given || ai.silent_given; 52 | bool runInVerbose = !runInSilence && ai.verbose_given; 53 | 54 | if(runInSilence){ 55 | log__silence(); 56 | } 57 | 58 | if(runInVerbose){ 59 | log__verbose(); 60 | } 61 | 62 | 63 | 64 | if( !ai.command_given ){ 65 | log__fail_cmdline__command_required(); 66 | return 1; 67 | } 68 | 69 | cmd_line = g_strdup(ai.command_arg); 70 | 71 | GError* error = NULL; 72 | 73 | if ( !g_shell_parse_argv ( cmd_line, NULL, &cmd_line_argv, &error ) ){ 74 | log__fail_cmdline__error_parsing_command(error->message); 75 | g_error_free ( error ); 76 | goto cmd_exit_1; 77 | } 78 | 79 | if(runInSerie && runInParallel){ 80 | log__fail_cmdline__either_parallel_or_series(); 81 | goto cmd_exit_1; 82 | } 83 | 84 | if(!runInSerie && !runInParallel){ 85 | log__fail_cmdline__parallel_or_series_required(); 86 | goto cmd_exit_1; 87 | } 88 | 89 | handling_mode = runInParallel ? AuthHandlingMode_PARALLEL : AuthHandlingMode_SERIE; 90 | 91 | log__verbose__cmd_and_mode(); 92 | cmdline_parser_free(&ai); 93 | 94 | return 0; 95 | 96 | cmd_exit_1: 97 | cmdline_parser_free(&ai); 98 | return 1; 99 | } -------------------------------------------------------------------------------- /src/app.cmdline: -------------------------------------------------------------------------------- 1 | version "0.4.0 - dev" 2 | versiontext "Copyright (C) 2024 Omar Castro. 3 | License LGPLv2.1+: GNU LGPL version 2.1 or later . 4 | This is free software: you are free to change and redistribute it. 5 | There is NO WARRANTY, to the extent permitted by law. 6 | 7 | Written by Omar Castro." 8 | package "cmd-polkit-agent" 9 | purpose "Polkit agent that allows to easily customize the UI to authenticate on polkit." 10 | usage "cmd-polkit-agent -s|--serial|-p|parallel -c|--command COMMAND" 11 | description "Runs COMMAND for each authentication request and communicates with it via JSON messages through stdin and stdout. It allows to easily create a GUI to authenticate on polkit." 12 | 13 | # Options 14 | option "command" c "Command to execute on authorization request" typestr="COMMAND" string optional 15 | option "serial" s "handle one authorization request at a time" optional 16 | option "parallel" p "handle authorization in parallel" optional 17 | option "verbose" v "Increase program verbosity" optional 18 | option "quiet" q "Do not print anything" optional 19 | option "silent" - "" optional 20 | 21 | text "\nFull documentation " -------------------------------------------------------------------------------- /src/app.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-2.1-or-later 2 | // Copyright (C) 2024 Omar Castro 3 | #ifndef APP_H 4 | #define APP_H 5 | 6 | typedef enum { 7 | AuthHandlingMode_SERIE, 8 | AuthHandlingMode_PARALLEL 9 | } AuthHandlingMode; 10 | 11 | 12 | int app__init(int argc, char *argv[]); 13 | int app__get_argc(); 14 | char ** app__get_argv(); 15 | const char* app__get_cmd_line(); 16 | char** app__get_cmd_line_argv(); 17 | AuthHandlingMode app__get_auth_handling_mode(); 18 | 19 | #endif 20 | 21 | -------------------------------------------------------------------------------- /src/cmdline.h: -------------------------------------------------------------------------------- 1 | /** @file cmdline.h 2 | * @brief The header file for the command line option parser 3 | * generated by GNU Gengetopt version 2.23 4 | * http://www.gnu.org/software/gengetopt. 5 | * DO NOT modify this file, since it can be overwritten 6 | * @author GNU Gengetopt */ 7 | 8 | #ifndef CMDLINE_H 9 | #define CMDLINE_H 10 | 11 | /* If we use autoconf. */ 12 | #ifdef HAVE_CONFIG_H 13 | #include "config.h" 14 | #endif 15 | 16 | #include /* for FILE */ 17 | 18 | #ifdef __cplusplus 19 | extern "C" { 20 | #endif /* __cplusplus */ 21 | 22 | #ifndef CMDLINE_PARSER_PACKAGE 23 | /** @brief the program name (used for printing errors) */ 24 | #define CMDLINE_PARSER_PACKAGE "cmd-polkit-agent" 25 | #endif 26 | 27 | #ifndef CMDLINE_PARSER_PACKAGE_NAME 28 | /** @brief the complete program name (used for help and version) */ 29 | #define CMDLINE_PARSER_PACKAGE_NAME "cmd-polkit-agent" 30 | #endif 31 | 32 | #ifndef CMDLINE_PARSER_VERSION 33 | /** @brief the program version */ 34 | #define CMDLINE_PARSER_VERSION "0.4.0 - dev" 35 | #endif 36 | 37 | /** @brief Where the command line options are stored */ 38 | struct gengetopt_args_info 39 | { 40 | const char *help_help; /**< @brief Print help and exit help description. */ 41 | const char *version_help; /**< @brief Print version and exit help description. */ 42 | char * command_arg; /**< @brief Command to execute on authorization request. */ 43 | char * command_orig; /**< @brief Command to execute on authorization request original value given at command line. */ 44 | const char *command_help; /**< @brief Command to execute on authorization request help description. */ 45 | const char *serial_help; /**< @brief handle one authorization request at a time help description. */ 46 | const char *parallel_help; /**< @brief handle authorization in parallel help description. */ 47 | const char *verbose_help; /**< @brief Increase program verbosity help description. */ 48 | const char *quiet_help; /**< @brief Do not print anything help description. */ 49 | const char *silent_help; /**< @brief help description. */ 50 | 51 | unsigned int help_given ; /**< @brief Whether help was given. */ 52 | unsigned int version_given ; /**< @brief Whether version was given. */ 53 | unsigned int command_given ; /**< @brief Whether command was given. */ 54 | unsigned int serial_given ; /**< @brief Whether serial was given. */ 55 | unsigned int parallel_given ; /**< @brief Whether parallel was given. */ 56 | unsigned int verbose_given ; /**< @brief Whether verbose was given. */ 57 | unsigned int quiet_given ; /**< @brief Whether quiet was given. */ 58 | unsigned int silent_given ; /**< @brief Whether silent was given. */ 59 | 60 | } ; 61 | 62 | /** @brief The additional parameters to pass to parser functions */ 63 | struct cmdline_parser_params 64 | { 65 | int override; /**< @brief whether to override possibly already present options (default 0) */ 66 | int initialize; /**< @brief whether to initialize the option structure gengetopt_args_info (default 1) */ 67 | int check_required; /**< @brief whether to check that all required options were provided (default 1) */ 68 | int check_ambiguity; /**< @brief whether to check for options already specified in the option structure gengetopt_args_info (default 0) */ 69 | int print_errors; /**< @brief whether getopt_long should print an error message for a bad option (default 1) */ 70 | } ; 71 | 72 | /** @brief the purpose string of the program */ 73 | extern const char *gengetopt_args_info_purpose; 74 | /** @brief the usage string of the program */ 75 | extern const char *gengetopt_args_info_usage; 76 | /** @brief the description string of the program */ 77 | extern const char *gengetopt_args_info_description; 78 | /** @brief all the lines making the help output */ 79 | extern const char *gengetopt_args_info_help[]; 80 | 81 | /** 82 | * The command line parser 83 | * @param argc the number of command line options 84 | * @param argv the command line options 85 | * @param args_info the structure where option information will be stored 86 | * @return 0 if everything went fine, NON 0 if an error took place 87 | */ 88 | int cmdline_parser (int argc, char **argv, 89 | struct gengetopt_args_info *args_info); 90 | 91 | /** 92 | * The command line parser (version with additional parameters - deprecated) 93 | * @param argc the number of command line options 94 | * @param argv the command line options 95 | * @param args_info the structure where option information will be stored 96 | * @param override whether to override possibly already present options 97 | * @param initialize whether to initialize the option structure my_args_info 98 | * @param check_required whether to check that all required options were provided 99 | * @return 0 if everything went fine, NON 0 if an error took place 100 | * @deprecated use cmdline_parser_ext() instead 101 | */ 102 | int cmdline_parser2 (int argc, char **argv, 103 | struct gengetopt_args_info *args_info, 104 | int override, int initialize, int check_required); 105 | 106 | /** 107 | * The command line parser (version with additional parameters) 108 | * @param argc the number of command line options 109 | * @param argv the command line options 110 | * @param args_info the structure where option information will be stored 111 | * @param params additional parameters for the parser 112 | * @return 0 if everything went fine, NON 0 if an error took place 113 | */ 114 | int cmdline_parser_ext (int argc, char **argv, 115 | struct gengetopt_args_info *args_info, 116 | struct cmdline_parser_params *params); 117 | 118 | /** 119 | * Save the contents of the option struct into an already open FILE stream. 120 | * @param outfile the stream where to dump options 121 | * @param args_info the option struct to dump 122 | * @return 0 if everything went fine, NON 0 if an error took place 123 | */ 124 | int cmdline_parser_dump(FILE *outfile, 125 | struct gengetopt_args_info *args_info); 126 | 127 | /** 128 | * Save the contents of the option struct into a (text) file. 129 | * This file can be read by the config file parser (if generated by gengetopt) 130 | * @param filename the file where to save 131 | * @param args_info the option struct to save 132 | * @return 0 if everything went fine, NON 0 if an error took place 133 | */ 134 | int cmdline_parser_file_save(const char *filename, 135 | struct gengetopt_args_info *args_info); 136 | 137 | /** 138 | * Print the help 139 | */ 140 | void cmdline_parser_print_help(void); 141 | /** 142 | * Print the version 143 | */ 144 | void cmdline_parser_print_version(void); 145 | 146 | /** 147 | * Initializes all the fields a cmdline_parser_params structure 148 | * to their default values 149 | * @param params the structure to initialize 150 | */ 151 | void cmdline_parser_params_init(struct cmdline_parser_params *params); 152 | 153 | /** 154 | * Allocates dynamically a cmdline_parser_params structure and initializes 155 | * all its fields to their default values 156 | * @return the created and initialized cmdline_parser_params structure 157 | */ 158 | struct cmdline_parser_params *cmdline_parser_params_create(void); 159 | 160 | /** 161 | * Initializes the passed gengetopt_args_info structure's fields 162 | * (also set default values for options that have a default) 163 | * @param args_info the structure to initialize 164 | */ 165 | void cmdline_parser_init (struct gengetopt_args_info *args_info); 166 | /** 167 | * Deallocates the string fields of the gengetopt_args_info structure 168 | * (but does not deallocate the structure itself) 169 | * @param args_info the structure to deallocate 170 | */ 171 | void cmdline_parser_free (struct gengetopt_args_info *args_info); 172 | 173 | /** 174 | * Checks that all the required options were specified 175 | * @param args_info the structure to check 176 | * @param prog_name the name of the program that will be used to print 177 | * possible errors 178 | * @return 179 | */ 180 | int cmdline_parser_required (struct gengetopt_args_info *args_info, 181 | const char *prog_name); 182 | 183 | 184 | #ifdef __cplusplus 185 | } 186 | #endif /* __cplusplus */ 187 | #endif /* CMDLINE_H */ 188 | -------------------------------------------------------------------------------- /src/error-message.dialog.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include 3 | #include 4 | #include "app.h" 5 | 6 | static bool is_gtk_initialized = false; 7 | 8 | void lazy_init_gtk(){ 9 | if(!is_gtk_initialized){ 10 | int argc = app__get_argc(); 11 | char **argv = app__get_argv(); 12 | gtk_init(&argc, &argv); 13 | is_gtk_initialized = true; 14 | } 15 | } 16 | 17 | 18 | void show_error_message_format(const char * const format, ...){ 19 | lazy_init_gtk(); 20 | 21 | char * result; 22 | va_list arglist; 23 | va_start( arglist, format ); 24 | vasprintf( &result, format, arglist ); 25 | va_end( arglist ); 26 | 27 | GtkWidget * dialog = gtk_message_dialog_new (NULL,0,GTK_MESSAGE_ERROR,GTK_BUTTONS_CLOSE,"%s",result); 28 | gtk_window_set_title(GTK_WINDOW(dialog), "cmd polkit agent"); 29 | gtk_dialog_run(GTK_DIALOG(dialog)); 30 | gtk_widget_destroy( GTK_WIDGET(dialog) ); 31 | free(result); 32 | } 33 | -------------------------------------------------------------------------------- /src/error-message.dialog.h: -------------------------------------------------------------------------------- 1 | #ifndef DIALOG__H__ERROR_MESSAGE 2 | #define DIALOG__H__ERROR_MESSAGE 3 | 4 | void show_error_message_format(const char * const format, ...); 5 | 6 | #endif 7 | -------------------------------------------------------------------------------- /src/json-glib.extension.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-2.1-or-later 2 | // Copyright (C) 2024 Omar Castro 3 | #include "json-glib.extension.h" 4 | 5 | const gchar * json_node_get_string_or_else(JsonNode * node, const gchar * else_value){ 6 | return node != NULL && 7 | json_node_get_value_type(node) == G_TYPE_STRING ? 8 | json_node_get_string(node) : else_value; 9 | } 10 | 11 | const gchar * json_object_get_string_member_or_else(JsonObject * node, const gchar * member, const gchar * else_value){ 12 | return json_node_get_string_or_else(json_object_get_member(node, member), else_value); 13 | } -------------------------------------------------------------------------------- /src/json-glib.extension.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-2.1-or-later 2 | // Copyright (C) 2024 Omar Castro 3 | #ifndef EXTENSION__H__JSON_GLIB 4 | #define EXTENSION__H__JSON_GLIB 5 | 6 | #include 7 | 8 | const gchar * json_node_get_string_or_else(JsonNode * node, const gchar * else_value); 9 | 10 | const gchar * json_object_get_string_member_or_else(JsonObject * node, const gchar * member, const gchar * else_value); 11 | 12 | #endif //EXTENSION__H__JSON_GLIB -------------------------------------------------------------------------------- /src/logger.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-2.1-or-later 2 | // Copyright (C) 2024 Omar Castro 3 | #define LOGGER_C 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "cmdline.h" 11 | #include "app.h" 12 | #include "logger.h" 13 | #include 14 | 15 | 16 | bool silenced_logs = false; 17 | bool verbose_logs = false; 18 | 19 | 20 | const char * currentFile = ""; 21 | const char * currentFunction = ""; 22 | int currentLine = 0; 23 | 24 | #define UPDATE_CURRENT_SOURCE_LOCATION() currentFile = file; currentFunction = function; currentLine = line; 25 | #define CHECK_VERBOSE() if(!verbose_logs || silenced_logs) { return; } 26 | 27 | static void log__fail_cmdline(const char* text); 28 | static void log__verbose_formatted(const char* format, ...); 29 | static inline void log__verbose_raw(const char* text){ log__verbose_formatted("%s", text); } 30 | 31 | 32 | void print_help (FILE *file) 33 | { 34 | size_t len_purpose = strlen(gengetopt_args_info_purpose); 35 | size_t len_usage = strlen(gengetopt_args_info_usage); 36 | 37 | if (len_usage > 0) { 38 | fprintf(file, "%s\n", gengetopt_args_info_usage); 39 | } 40 | if (len_purpose > 0) { 41 | fprintf(file, "%s\n", gengetopt_args_info_purpose); 42 | } 43 | 44 | if (len_usage || len_purpose) { 45 | fprintf(file, "\n"); 46 | } 47 | 48 | if (strlen(gengetopt_args_info_description) > 0) { 49 | fprintf(file, "%s\n\n", gengetopt_args_info_description); 50 | } 51 | 52 | int i = 0; 53 | while (gengetopt_args_info_help[i]) 54 | fprintf(file, "%s\n", gengetopt_args_info_help[i++]); 55 | } 56 | 57 | 58 | void log__silence(){ 59 | silenced_logs = true; 60 | } 61 | void log__verbose(){ 62 | verbose_logs = true; 63 | } 64 | 65 | // log error, print help and exits 66 | 67 | void log__fail_cmdline__command_required(MACRO__SOURCE_LOCATION_PARAMS){ 68 | UPDATE_CURRENT_SOURCE_LOCATION() 69 | log__fail_cmdline("command argument is required"); 70 | } 71 | 72 | void log__fail_cmdline__either_parallel_or_series(MACRO__SOURCE_LOCATION_PARAMS){ 73 | UPDATE_CURRENT_SOURCE_LOCATION() 74 | log__fail_cmdline("only serial or parallel mode must be selected, not both"); 75 | } 76 | 77 | void log__fail_cmdline__parallel_or_series_required(MACRO__SOURCE_LOCATION_PARAMS){ 78 | UPDATE_CURRENT_SOURCE_LOCATION() 79 | log__fail_cmdline("parallel or serial mode is required"); 80 | } 81 | 82 | void log__fail_cmdline__error_parsing_command(MACRO__SOURCE_LOCATION_PARAMS, const char * message){ 83 | UPDATE_CURRENT_SOURCE_LOCATION() 84 | if(!silenced_logs) { 85 | fprintf(stderr, "Error parsing command line: error parsing shell command: %s\n", message); 86 | log__fail_cmdline__print_help(); 87 | } 88 | } 89 | 90 | 91 | void log__fail_cmdline__print_help(){ 92 | fprintf(stderr, "\n"); 93 | print_help(stderr); 94 | } 95 | 96 | 97 | 98 | // logs on verbose only 99 | 100 | void log__verbose__cmd_and_mode(MACRO__SOURCE_LOCATION_PARAMS){ 101 | UPDATE_CURRENT_SOURCE_LOCATION() 102 | log__verbose_formatted("COMMAND TO EXECUTE: %s", app__get_cmd_line()); 103 | log__verbose_formatted("AUTH HANDLING MODE: %s", app__get_auth_handling_mode() == AuthHandlingMode_PARALLEL ? "PARALLEL" : "SERIE"); 104 | } 105 | 106 | void log__verbose__init_polkit_listener(MACRO__SOURCE_LOCATION_PARAMS){ 107 | UPDATE_CURRENT_SOURCE_LOCATION() 108 | log__verbose_raw("Polkit Listener initialized"); 109 | } 110 | 111 | void log__verbose__finalize_polkit_listener(MACRO__SOURCE_LOCATION_PARAMS){ 112 | UPDATE_CURRENT_SOURCE_LOCATION() 113 | log__verbose_raw("Polkit Listener finalized"); 114 | } 115 | 116 | void log__verbose__init_polkit_authentication(MACRO__SOURCE_LOCATION_PARAMS, const char *action_id, const char *message, const char *icon_name, const char * cookie ){ 117 | UPDATE_CURRENT_SOURCE_LOCATION() 118 | log__verbose_raw("initiate Polkit authentication"); 119 | log__verbose_formatted("└─ action id : %s", action_id); 120 | log__verbose_formatted("└─ message : %s", message); 121 | log__verbose_formatted("└─ icon name : %s", icon_name); 122 | log__verbose_formatted("└─ cookie : %s", cookie); 123 | 124 | } 125 | 126 | 127 | const gchar * polkit_auth_identity_to_json_string(PolkitIdentity * identity); 128 | void log__verbose__polkit_auth_identities(MACRO__SOURCE_LOCATION_PARAMS, const GList* const identities){ 129 | CHECK_VERBOSE() 130 | UPDATE_CURRENT_SOURCE_LOCATION() 131 | log__verbose_raw("Polkit identities"); 132 | for (const GList *p = identities; p != NULL; p = p->next) { 133 | PolkitIdentity *id = (PolkitIdentity *)p->data; 134 | g_autofree const gchar* json = polkit_auth_identity_to_json_string(id); 135 | log__verbose_formatted("└─ %s", json); 136 | } 137 | } 138 | 139 | void log__verbose__polkit_auth_details(MACRO__SOURCE_LOCATION_PARAMS, PolkitDetails* const details){ 140 | CHECK_VERBOSE() 141 | UPDATE_CURRENT_SOURCE_LOCATION() 142 | char** keys = polkit_details_get_keys(details); 143 | 144 | log__verbose_raw("Polkit details"); 145 | if (keys == NULL) { 146 | log__verbose_raw("└─ (empty)"); 147 | return; 148 | } 149 | for(char** key = keys;*key;key++) { 150 | log__verbose_formatted("└─ %s: %s", *key, polkit_details_lookup(details, *key)); 151 | } 152 | g_strfreev(keys); 153 | } 154 | 155 | void log__verbose__polkit_action_description(MACRO__SOURCE_LOCATION_PARAMS, PolkitActionDescription* const action_description){ 156 | CHECK_VERBOSE() 157 | UPDATE_CURRENT_SOURCE_LOCATION() 158 | 159 | log__verbose_raw("Polkit action description"); 160 | log__verbose_formatted("└─ id: %s", polkit_action_description_get_action_id(action_description)); 161 | log__verbose_formatted("└─ description: %s", polkit_action_description_get_description(action_description)); 162 | log__verbose_formatted("└─ message: %s", polkit_action_description_get_message(action_description)); 163 | log__verbose_formatted("└─ vendor name: %s", polkit_action_description_get_vendor_name(action_description)); 164 | log__verbose_formatted("└─ vendor url: %s", polkit_action_description_get_vendor_url(action_description)); 165 | log__verbose_raw("└─ annotations:"); 166 | 167 | const gchar *const * annotations = polkit_action_description_get_annotation_keys(action_description); 168 | for(const gchar *const * i = annotations; *i != NULL; ++i){ 169 | const gchar * annotation = *i; 170 | log__verbose_formatted(" └─ %s: %s", annotation, polkit_action_description_get_annotation(action_description, annotation)); 171 | } 172 | } 173 | 174 | void log__verbose__polkit_session_completed(MACRO__SOURCE_LOCATION_PARAMS, bool authorized, bool canceled){ 175 | CHECK_VERBOSE() 176 | UPDATE_CURRENT_SOURCE_LOCATION() 177 | log__verbose_raw("Polkit session completed"); 178 | log__verbose_formatted("└─ {\"authorized\": \"%s\", \"canceled\":\"%s\" })", authorized ? "yes": "no", canceled ? "yes": "no"); 179 | } 180 | 181 | void log__verbose__polkit_session_show_error(MACRO__SOURCE_LOCATION_PARAMS, const char *text){ 182 | CHECK_VERBOSE() 183 | UPDATE_CURRENT_SOURCE_LOCATION() 184 | log__verbose_formatted("Polkit session show error: %s", text); 185 | } 186 | 187 | 188 | void log__verbose__polkit_session_show_info(MACRO__SOURCE_LOCATION_PARAMS, const char *text){ 189 | UPDATE_CURRENT_SOURCE_LOCATION() 190 | log__verbose_formatted("Polkit session show info: %s", text); 191 | } 192 | 193 | 194 | void log__verbose__polkit_session_request(MACRO__SOURCE_LOCATION_PARAMS, const char *text, bool visibility){ 195 | UPDATE_CURRENT_SOURCE_LOCATION() 196 | log__verbose_formatted("Polkit session request: %s", text); 197 | log__verbose_formatted("└─ visibility: %s", visibility ? "yes" : "no"); 198 | 199 | } 200 | 201 | void log__verbose__finish_polkit_authentication(MACRO__SOURCE_LOCATION_PARAMS){ 202 | UPDATE_CURRENT_SOURCE_LOCATION() 203 | log__verbose_raw("finish Polkit authentication"); 204 | } 205 | 206 | void log__verbose__writing_to_command_stdin(MACRO__SOURCE_LOCATION_PARAMS, const char * message){ 207 | UPDATE_CURRENT_SOURCE_LOCATION() 208 | log__verbose_formatted("writing to command stdin: %s", message); 209 | } 210 | 211 | void log__verbose__received_from_command_stdout(MACRO__SOURCE_LOCATION_PARAMS, const char * message){ 212 | UPDATE_CURRENT_SOURCE_LOCATION() 213 | log__verbose_formatted("received line from command stdout: %s", message); 214 | } 215 | 216 | void log__verbose__reading_command_stdout(MACRO__SOURCE_LOCATION_PARAMS){ 217 | UPDATE_CURRENT_SOURCE_LOCATION() 218 | log__verbose_formatted("reading output"); 219 | } 220 | 221 | 222 | 223 | // private 224 | 225 | static void log__verbose_formatted( const char* format, ... ) 226 | { 227 | CHECK_VERBOSE() 228 | va_list arglist; 229 | printf( "Vrbos:%s:", currentFunction ); 230 | va_start( arglist, format ); 231 | vprintf( format, arglist ); 232 | va_end( arglist ); 233 | printf( "\n" ); 234 | 235 | } 236 | 237 | static void log__fail_cmdline(const char* text){ 238 | if(!silenced_logs) { 239 | fprintf(stderr, "Error parsing command line: %s\n", text); 240 | log__fail_cmdline__print_help(); 241 | } 242 | } 243 | 244 | 245 | const gchar * polkit_auth_identity_to_json_string(PolkitIdentity * identity){ 246 | g_autoptr(JsonBuilder) builder = json_builder_new (); 247 | 248 | json_builder_begin_object (builder); 249 | 250 | if(identity == NULL){ 251 | json_builder_set_member_name (builder, "type"); 252 | json_builder_add_string_value (builder, "error"); 253 | 254 | json_builder_set_member_name (builder, "error"); 255 | json_builder_add_string_value (builder, "identity is null"); 256 | } else if(POLKIT_IS_UNIX_USER(identity)) { 257 | uid_t uid = polkit_unix_user_get_uid(POLKIT_UNIX_USER(identity)); 258 | struct passwd *pwd = getpwuid(uid); 259 | json_builder_set_member_name (builder, "type"); 260 | json_builder_add_string_value (builder, "user"); 261 | 262 | json_builder_set_member_name (builder, "name"); 263 | json_builder_add_string_value (builder, pwd->pw_name); 264 | 265 | json_builder_set_member_name (builder, "id"); 266 | json_builder_add_int_value (builder, pwd->pw_uid); 267 | 268 | json_builder_set_member_name (builder, "group id"); 269 | json_builder_add_int_value (builder, pwd->pw_gid); 270 | 271 | } else if(POLKIT_IS_UNIX_GROUP(identity)) { 272 | gid_t gid = polkit_unix_group_get_gid(POLKIT_UNIX_GROUP(identity)); 273 | struct group *grp = getgrgid(gid); 274 | 275 | json_builder_set_member_name (builder, "type"); 276 | json_builder_add_string_value (builder, "group"); 277 | 278 | json_builder_set_member_name (builder, "name"); 279 | json_builder_add_string_value (builder, grp->gr_name); 280 | 281 | json_builder_set_member_name (builder, "id"); 282 | json_builder_add_int_value (builder, grp->gr_gid); 283 | } else if(POLKIT_IS_IDENTITY(identity)){ 284 | g_autofree gchar* value = polkit_identity_to_string(identity); 285 | 286 | json_builder_set_member_name (builder, "type"); 287 | json_builder_add_string_value (builder, "other"); 288 | 289 | json_builder_set_member_name (builder, "value"); 290 | json_builder_add_string_value (builder, value); 291 | } else { 292 | json_builder_set_member_name (builder, "type"); 293 | json_builder_add_string_value (builder, "error"); 294 | 295 | json_builder_set_member_name (builder, "error"); 296 | json_builder_add_string_value (builder, "invalid type: not a polkit identity"); 297 | } 298 | 299 | json_builder_end_object (builder); 300 | g_autoptr(JsonNode) root = json_builder_get_root (builder); 301 | g_autoptr(JsonGenerator) gen = json_generator_new (); 302 | json_generator_set_root (gen, root); 303 | return json_generator_to_data (gen, NULL); 304 | } 305 | -------------------------------------------------------------------------------- /src/logger.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-2.1-or-later 2 | // Copyright (C) 2024 Omar Castro 3 | #ifndef LOGGER_H 4 | #define LOGGER_H 5 | #include 6 | #include 7 | #include 8 | 9 | #define MACRO__SOURCE_LOCATION_PARAMS const char* file, const char* function, const int line 10 | #define MACRO__SOURCE_LOCATION_VALUES __FILE__, __func__ ,__LINE__ 11 | 12 | void log__silence(); 13 | void log__verbose(); 14 | 15 | // log error, print help and exits 16 | 17 | void log__fail_cmdline__command_required(MACRO__SOURCE_LOCATION_PARAMS); 18 | void log__fail_cmdline__either_parallel_or_series(MACRO__SOURCE_LOCATION_PARAMS); 19 | void log__fail_cmdline__parallel_or_series_required(MACRO__SOURCE_LOCATION_PARAMS); 20 | void log__fail_cmdline__error_parsing_command(MACRO__SOURCE_LOCATION_PARAMS, const char * error_message); 21 | void log__fail_cmdline__print_help(); 22 | 23 | #ifndef LOGGER_C 24 | #define log__fail_cmdline__command_required() log__fail_cmdline__command_required(MACRO__SOURCE_LOCATION_VALUES) 25 | #define log__fail_cmdline__either_parallel_or_series() log__fail_cmdline__either_parallel_or_series(MACRO__SOURCE_LOCATION_VALUES) 26 | #define log__fail_cmdline__parallel_or_series_required() log__fail_cmdline__parallel_or_series_required(MACRO__SOURCE_LOCATION_VALUES) 27 | #define log__fail_cmdline__error_parsing_command(message) log__fail_cmdline__error_parsing_command(MACRO__SOURCE_LOCATION_VALUES, message) 28 | #endif 29 | 30 | // logs on verbose only 31 | 32 | void log__verbose__cmd_and_mode(MACRO__SOURCE_LOCATION_PARAMS); 33 | void log__verbose__init_polkit_listener(MACRO__SOURCE_LOCATION_PARAMS); 34 | void log__verbose__init_polkit_authentication(MACRO__SOURCE_LOCATION_PARAMS, const char *action_id, const char *message, const char *icon_name, const char * cookie ); 35 | void log__verbose__polkit_auth_identities(MACRO__SOURCE_LOCATION_PARAMS, const GList* const list); 36 | void log__verbose__polkit_auth_details(MACRO__SOURCE_LOCATION_PARAMS, PolkitDetails* const details); 37 | void log__verbose__polkit_action_description(MACRO__SOURCE_LOCATION_PARAMS, PolkitActionDescription* const action_description); 38 | void log__verbose__polkit_session_completed(MACRO__SOURCE_LOCATION_PARAMS, bool authorized, bool canceled); 39 | void log__verbose__polkit_session_show_error(MACRO__SOURCE_LOCATION_PARAMS, const char *text); 40 | void log__verbose__polkit_session_show_info(MACRO__SOURCE_LOCATION_PARAMS, const char *text); 41 | void log__verbose__polkit_session_request(MACRO__SOURCE_LOCATION_PARAMS, const char *text, bool visibility); 42 | void log__verbose__finish_polkit_authentication(MACRO__SOURCE_LOCATION_PARAMS); 43 | void log__verbose__finalize_polkit_listener(MACRO__SOURCE_LOCATION_PARAMS); 44 | void log__verbose__writing_to_command_stdin(MACRO__SOURCE_LOCATION_PARAMS, const char * message); 45 | void log__verbose__received_from_command_stdout(MACRO__SOURCE_LOCATION_PARAMS, const char * message); 46 | void log__verbose__reading_command_stdout(MACRO__SOURCE_LOCATION_PARAMS); 47 | 48 | 49 | #ifndef LOGGER_C 50 | #define log__verbose__cmd_and_mode() log__verbose__cmd_and_mode(MACRO__SOURCE_LOCATION_VALUES) 51 | #define log__verbose__init_polkit_listener() log__verbose__init_polkit_listener(MACRO__SOURCE_LOCATION_VALUES) 52 | #define log__verbose__init_polkit_authentication(action_id, message, icon_name, cookie) log__verbose__init_polkit_authentication(MACRO__SOURCE_LOCATION_VALUES, action_id, message, icon_name, cookie) 53 | #define log__verbose__polkit_auth_identities(list) log__verbose__polkit_auth_identities(MACRO__SOURCE_LOCATION_VALUES, list) 54 | #define log__verbose__polkit_auth_details(details) log__verbose__polkit_auth_details(MACRO__SOURCE_LOCATION_VALUES, details) 55 | #define log__verbose__polkit_action_description(action_description) log__verbose__polkit_action_description(MACRO__SOURCE_LOCATION_VALUES, action_description) 56 | #define log__verbose__polkit_session_completed(authorized, canceled) log__verbose__polkit_session_completed(MACRO__SOURCE_LOCATION_VALUES, authorized, canceled) 57 | #define log__verbose__polkit_session_show_error(text) log__verbose__polkit_session_show_error(MACRO__SOURCE_LOCATION_VALUES, text) 58 | #define log__verbose__polkit_session_show_info(text) log__verbose__polkit_session_show_info(MACRO__SOURCE_LOCATION_VALUES, text) 59 | #define log__verbose__polkit_session_request(text, visibility) log__verbose__polkit_session_request(MACRO__SOURCE_LOCATION_VALUES, text, visibility) 60 | #define log__verbose__finish_polkit_authentication() log__verbose__finish_polkit_authentication(MACRO__SOURCE_LOCATION_VALUES) 61 | #define log__verbose__finalize_polkit_listener() log__verbose__finalize_polkit_listener(MACRO__SOURCE_LOCATION_VALUES) 62 | #define log__verbose__writing_to_command_stdin(message) log__verbose__writing_to_command_stdin(MACRO__SOURCE_LOCATION_VALUES, message) 63 | #define log__verbose__received_from_command_stdout(message) log__verbose__received_from_command_stdout(MACRO__SOURCE_LOCATION_VALUES, message) 64 | #define log__verbose__reading_command_stdout() log__verbose__reading_command_stdout(MACRO__SOURCE_LOCATION_VALUES) 65 | 66 | #endif 67 | 68 | #endif // LOGGER_H 69 | -------------------------------------------------------------------------------- /src/main.entrypoint.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-2.1-or-later 2 | // Copyright (C) 2024 Omar Castro 3 | #include 4 | #include 5 | #include "polkit-auth-handler.service.h" 6 | #include 7 | #include 8 | #include "app.h" 9 | #include "error-message.dialog.h" 10 | 11 | 12 | int main(int argc, char *argv[]) 13 | { 14 | 15 | const int return_code = app__init(argc, argv); 16 | if(return_code != 0){ 17 | return return_code; 18 | } 19 | PolkitAgentListener *listener; 20 | PolkitSubject* session; 21 | GError* error = NULL; 22 | GMainLoop *loop; 23 | 24 | loop = g_main_loop_new (NULL, FALSE); 25 | 26 | int rc = 0; 27 | 28 | listener = cmd_pk_agent_polkit_listener_new(); 29 | session = polkit_unix_session_new_for_process_sync(getpid(), NULL, NULL); 30 | 31 | if(!polkit_agent_listener_register(listener, 32 | POLKIT_AGENT_REGISTER_FLAGS_NONE, 33 | session, NULL, NULL, &error)) { 34 | 35 | show_error_message_format("Error initializing program\n %s\nThe application will exit", error->message); 36 | fprintf(stderr,"Error %s",error->message); 37 | g_error_free ( error ); 38 | rc = 1; 39 | } else { 40 | g_main_loop_run (loop); 41 | } 42 | 43 | g_object_unref(listener); 44 | g_object_unref(session); 45 | g_main_loop_unref (loop); 46 | return rc; 47 | } 48 | -------------------------------------------------------------------------------- /src/polkit-auth-handler.service.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-2.1-or-later 2 | // Copyright (C) 2024 Omar Castro 3 | #include "glib-object.h" 4 | #include "glib.h" 5 | #include "glibconfig.h" 6 | #include 7 | #define _GNU_SOURCE 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include "logger.h" 19 | #include "app.h" 20 | #include "json-glib.extension.h" 21 | #include "accepted-actions.enum.h" 22 | #include "error-message.dialog.h" 23 | #include "polkit-auth-handler.service.h" 24 | #include "request-messages.h" 25 | 26 | G_DEFINE_TYPE(CmdPkAgentPolkitListener, cmd_pk_agent_polkit_listener, POLKIT_AGENT_TYPE_LISTENER) 27 | 28 | typedef enum { 29 | IN_QUEUE, 30 | AUTHENTICATING, 31 | CANCELED, 32 | AUTHORIZED 33 | } AuthDlgDataStatus; 34 | 35 | typedef struct _AuthDlgData AuthDlgData; 36 | struct _AuthDlgData { 37 | PolkitAgentSession *session; 38 | PolkitActionDescription* action_description; 39 | gchar *action_id; 40 | gchar *cookie; 41 | gchar *message; 42 | GTask* task; 43 | GList *identities; 44 | GError *error; 45 | 46 | AuthDlgDataStatus status; 47 | 48 | GPid cmd_pid; 49 | int write_channel_fd; 50 | int read_channel_fd; 51 | guint read_channel_watcher; 52 | 53 | GIOChannel * write_channel; 54 | GIOChannel * read_channel; 55 | GString * buffer; 56 | GString * active_line; 57 | 58 | JsonParser *parser; 59 | JsonObject *root; 60 | 61 | }; 62 | 63 | /** 64 | * Authentication queue to be used in serial mode 65 | * The an authentication goes to the queur if there is one authentication currently being handled 66 | */ 67 | GAsyncQueue * serial_mode_queue = NULL; 68 | AuthDlgData * serial_mode_current_authentication = NULL; 69 | 70 | bool serie_mode_is_queue_empty(){ 71 | return serial_mode_queue == NULL || g_async_queue_length(serial_mode_queue) <= 0; 72 | } 73 | 74 | void serie_mode_push_auth_to_queue(AuthDlgData *d){ 75 | if(serial_mode_queue == NULL){ 76 | serial_mode_queue = g_async_queue_new(); 77 | } 78 | g_async_queue_push(serial_mode_queue, d); 79 | } 80 | 81 | AuthDlgData* serie_mode_pop_auth_from_queue(){ 82 | if(serial_mode_queue == NULL){ 83 | serial_mode_queue = g_async_queue_new(); 84 | } 85 | return (AuthDlgData *) g_async_queue_pop(serial_mode_queue); 86 | } 87 | 88 | static void build_session(AuthDlgData *d); 89 | static void spawn_command_for_authentication(AuthDlgData *d); 90 | 91 | void auth_dialog_data_write_to_channel ( AuthDlgData *data, const char * message){ 92 | GIOChannel * write_channel = data->write_channel; 93 | if(data->write_channel == NULL){ 94 | //gets here when the script exits or there was an error loading it 95 | return; 96 | } 97 | log__verbose__writing_to_command_stdin(message); 98 | gsize bytes_witten; 99 | g_io_channel_write_chars(write_channel, message, -1, &bytes_witten, &data->error); 100 | g_io_channel_write_unichar(write_channel, '\n', &data->error); 101 | g_io_channel_flush(write_channel, &data->error); 102 | } 103 | 104 | static void auth_dlg_data_run_and_free_task(AuthDlgData *d){ 105 | GTask *task = d->task; 106 | if(task != NULL){ 107 | g_task_return_boolean(task, true); 108 | g_object_unref(task); 109 | d->task = NULL; 110 | } 111 | } 112 | 113 | static void auth_dlg_data_free(AuthDlgData *d) 114 | { 115 | GError* error = NULL; 116 | 117 | auth_dlg_data_run_and_free_task(d); 118 | g_object_unref(d->session); 119 | g_free(d->action_id); 120 | g_free(d->cookie); 121 | g_free(d->message); 122 | g_list_free(d->identities); 123 | g_source_remove (d->read_channel_watcher); 124 | g_io_channel_shutdown(d->write_channel, TRUE, &error); 125 | if(error){ 126 | fprintf(stderr, "error closing write channel of pid %d: %s\n", d->cmd_pid, error->message); 127 | g_error_free ( error ); 128 | } 129 | g_io_channel_unref(d->write_channel); 130 | g_io_channel_shutdown(d->read_channel, FALSE, &error); 131 | if(error){ 132 | fprintf(stderr, "error closing read channel of pid %d: %s\n", d->cmd_pid, error->message); 133 | g_error_free ( error ); 134 | } 135 | if(d->action_description != NULL){ 136 | g_object_unref(d->action_description); 137 | } 138 | g_io_channel_unref(d->read_channel); 139 | g_string_free(d->active_line, true); 140 | g_string_free(d->buffer, true); 141 | g_object_unref(d->parser); 142 | g_slice_free(AuthDlgData, d); 143 | } 144 | 145 | static gboolean on_new_input ( GIOChannel *source, [[maybe_unused]] GIOCondition condition, gpointer context ) 146 | { 147 | log__verbose__reading_command_stdout(); 148 | AuthDlgData *data = (AuthDlgData *) context; 149 | GString * buffer = data->buffer; 150 | GString * active_line = data->active_line; 151 | 152 | gboolean newline = FALSE; 153 | 154 | GError * error = NULL; 155 | gunichar unichar; 156 | GIOStatus status; 157 | 158 | status = g_io_channel_read_unichar(source, &unichar, &error); 159 | 160 | //when there is nothing to read, status is G_IO_STATUS_AGAIN 161 | while(status == G_IO_STATUS_NORMAL) { 162 | g_string_append_unichar(buffer, unichar); 163 | if( unichar == '\n' ){ 164 | if(buffer->len > 1){ //input is not an empty line 165 | g_debug("received new line: %s", buffer->str); 166 | g_string_assign(active_line, buffer->str); 167 | newline=TRUE; 168 | } 169 | log__verbose__received_from_command_stdout(buffer->str); 170 | g_string_set_size(buffer, 0); 171 | } 172 | status = g_io_channel_read_unichar(source, &unichar, &error); 173 | } 174 | 175 | if(newline){ 176 | fprintf(stderr, "parsing line\n"); 177 | 178 | if(! json_parser_load_from_data(data->parser,data->active_line->str,data->active_line->len,&error)){ 179 | fprintf(stderr, "Unable to parse line: %s\n", error->message); 180 | g_error_free ( error ); 181 | } else { 182 | 183 | data->root = json_node_get_object(json_parser_get_root(data->parser)); 184 | const char * action = json_object_get_string_member_or_else(data->root, "action", NULL); 185 | if(action == NULL){ 186 | fprintf(stderr, "no action defined, ignored"); 187 | } else switch (accepted_action_value_of_str(action)) { 188 | case AcceptedAction_CANCEL: { 189 | fprintf(stderr, "action cancel"); 190 | data->status = CANCELED; 191 | polkit_agent_session_cancel(data->session); 192 | } 193 | break; 194 | case AcceptedAction_AUTHENTICATE: { 195 | fprintf(stderr, "action authenticate"); 196 | const char * password = json_object_get_string_member_or_else(data->root, "password", NULL); 197 | if(password != NULL){ 198 | polkit_agent_session_response(data->session, password); 199 | } 200 | } 201 | break; 202 | default: 203 | fprintf(stderr, "unknown action %s \n", action); 204 | } 205 | } 206 | } 207 | 208 | return G_SOURCE_CONTINUE; 209 | } 210 | 211 | static void on_session_completed([[maybe_unused]] PolkitAgentSession* session, gboolean authorized, AuthDlgData* d) 212 | { 213 | bool canceled = d->status == CANCELED; 214 | log__verbose__polkit_session_completed(authorized, canceled); 215 | 216 | if(authorized){ 217 | d->status = AUTHORIZED; 218 | g_autofree const char* message = request_message_authorization_authorized(); 219 | auth_dialog_data_write_to_channel(d, message); 220 | } 221 | if (authorized || canceled) { 222 | auth_dlg_data_run_and_free_task(d); 223 | auth_dlg_data_free(d); 224 | if(app__get_auth_handling_mode() == AuthHandlingMode_SERIE){ 225 | if(serie_mode_is_queue_empty()){ 226 | serial_mode_current_authentication = NULL; 227 | } else { 228 | AuthDlgData* data = serie_mode_pop_auth_from_queue(); 229 | data->status = AUTHENTICATING; 230 | serial_mode_current_authentication = data; 231 | spawn_command_for_authentication(data); 232 | polkit_agent_session_initiate(data->session); 233 | } 234 | } 235 | 236 | return; 237 | } 238 | g_object_unref(d->session); 239 | d->session = NULL; 240 | g_autofree const char* message = request_message_authorization_not_authorized(); 241 | auth_dialog_data_write_to_channel(d, message); 242 | build_session(d); 243 | polkit_agent_session_initiate(d->session); 244 | 245 | } 246 | 247 | static void on_session_request([[maybe_unused]] PolkitAgentSession* session, gchar *req, gboolean visibility, AuthDlgData *d) 248 | { 249 | log__verbose__polkit_session_request(req, visibility); 250 | g_autofree const char *write_message = request_message_request_password(req, d->message, d->action_description); 251 | auth_dialog_data_write_to_channel(d, write_message); 252 | } 253 | 254 | static void on_session_show_error([[maybe_unused]] PolkitAgentSession* session, gchar *text, [[maybe_unused]] AuthDlgData* d) 255 | { 256 | 257 | log__verbose__polkit_session_show_error(text); 258 | } 259 | 260 | static void on_session_show_info([[maybe_unused]] PolkitAgentSession *session, gchar *text, [[maybe_unused]] AuthDlgData* d) 261 | { 262 | log__verbose__polkit_session_show_info(text); 263 | } 264 | 265 | static void build_session(AuthDlgData *d){ 266 | if (G_UNLIKELY(d->session)) { 267 | g_signal_handlers_disconnect_matched(d->session, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, d); 268 | polkit_agent_session_cancel(d->session); 269 | g_object_unref(d->session); 270 | } 271 | 272 | PolkitIdentity *id = (PolkitIdentity *)d->identities->data; 273 | d->session = polkit_agent_session_new(id, d->cookie); 274 | g_signal_connect(d->session, "completed", G_CALLBACK(on_session_completed), d); 275 | g_signal_connect(d->session, "request", G_CALLBACK(on_session_request), d); 276 | g_signal_connect(d->session, "show-error", G_CALLBACK(on_session_show_error), d); 277 | g_signal_connect(d->session, "show-info", G_CALLBACK(on_session_show_info), d); 278 | 279 | } 280 | 281 | static void spawn_command_for_authentication(AuthDlgData *d){ 282 | GError *error = NULL; 283 | int cmd_input_fd; 284 | int cmd_output_fd; 285 | 286 | char ** const cmd_argv = app__get_cmd_line_argv(); 287 | 288 | if ( ! g_spawn_async_with_pipes ( NULL, cmd_argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, &(d->cmd_pid), &(cmd_input_fd), &(cmd_output_fd), NULL, &error)) { 289 | show_error_message_format("%s", error->message); 290 | polkit_agent_session_cancel(d->session); 291 | return; 292 | } 293 | d->read_channel_fd = cmd_output_fd; 294 | d->write_channel_fd = cmd_input_fd; 295 | 296 | int retval = fcntl( d->read_channel_fd, F_SETFL, fcntl(d->read_channel_fd, F_GETFL) | O_NONBLOCK); 297 | if (retval != 0){ 298 | fprintf(stderr,"Error setting non block on output pipe\n"); 299 | kill(d->cmd_pid, SIGTERM); 300 | polkit_agent_session_cancel(d->session); 301 | return; 302 | } 303 | 304 | d->read_channel = g_io_channel_unix_new(d->read_channel_fd); 305 | d->write_channel = g_io_channel_unix_new(d->write_channel_fd); 306 | d->read_channel_watcher = g_io_add_watch(d->read_channel, G_IO_IN, on_new_input, d); 307 | } 308 | 309 | 310 | /** 311 | * Authentication request handler of PolkitAgentListener. 312 | * 313 | */ 314 | static void initiate_authentication(PolkitAgentListener *listener, 315 | const gchar *action_id, 316 | const gchar *message, 317 | const gchar *icon_name, 318 | PolkitDetails *details, 319 | const gchar *cookie, 320 | GList *identities, 321 | GCancellable *cancellable, 322 | GAsyncReadyCallback callback, 323 | gpointer user_data) 324 | { 325 | 326 | log__verbose__init_polkit_authentication(action_id, message, icon_name, cookie); 327 | log__verbose__polkit_auth_identities(identities); 328 | log__verbose__polkit_auth_details(details); 329 | 330 | AuthDlgData *d = g_slice_new0(AuthDlgData); 331 | 332 | GError *error = NULL; 333 | PolkitAuthority* authority = polkit_authority_get_sync(NULL, &error); 334 | if(error == NULL){ 335 | GList* actions = polkit_authority_enumerate_actions_sync (authority,NULL,&error); 336 | if(error == NULL){ 337 | for(GList *elem = actions; elem; elem = elem->next) { 338 | PolkitActionDescription* action_description = elem->data; 339 | if(d->action_description != NULL){ 340 | // continue to g_object_unref the remaining elements on the list, as they are required 341 | // before freeing the `actions` GList 342 | g_object_unref(action_description); 343 | continue; 344 | } 345 | 346 | const gchar * action_description_action_id = polkit_action_description_get_action_id(action_description); 347 | if(strcmp(action_description_action_id, action_id) == 0){ 348 | log__verbose__polkit_action_description(action_description); 349 | g_object_ref(action_description); 350 | d->action_description = action_description; 351 | } 352 | 353 | g_object_unref(action_description); 354 | } 355 | g_list_free(actions); 356 | } 357 | g_object_unref(authority); 358 | } 359 | 360 | d->task = g_task_new(listener, cancellable, callback, user_data); 361 | d->action_id = g_strdup(action_id); 362 | d->message = g_strdup(message); 363 | d->cookie = g_strdup(cookie); 364 | d->identities = g_list_copy(identities); 365 | d->buffer = g_string_sized_new (1024); 366 | d->active_line = g_string_sized_new (1024); 367 | d->parser = json_parser_new (); 368 | build_session(d); 369 | if(app__get_auth_handling_mode() == AuthHandlingMode_PARALLEL){ 370 | spawn_command_for_authentication(d); 371 | polkit_agent_session_initiate(d->session); 372 | } else if(serial_mode_current_authentication != NULL){ 373 | d->status = IN_QUEUE; 374 | serie_mode_push_auth_to_queue(d); 375 | } else { 376 | d->status = AUTHENTICATING; 377 | serial_mode_current_authentication = d; 378 | spawn_command_for_authentication(d); 379 | polkit_agent_session_initiate(d->session); 380 | } 381 | } 382 | 383 | static gboolean initiate_authentication_finish( 384 | [[maybe_unused]] PolkitAgentListener *listener, 385 | GAsyncResult *res, 386 | GError **error) 387 | { 388 | log__verbose__finish_polkit_authentication(); 389 | return g_task_propagate_boolean(G_TASK(res), error); 390 | } 391 | 392 | static void cmd_pk_agent_polkit_listener_finalize(GObject *object) 393 | { 394 | log__verbose__finalize_polkit_listener(); 395 | g_return_if_fail(object != NULL); 396 | g_return_if_fail(CMD_PK_AGENT_IS_POLKIT_LISTENER(object)); 397 | G_OBJECT_CLASS(cmd_pk_agent_polkit_listener_parent_class)->finalize(object); 398 | } 399 | 400 | static void cmd_pk_agent_polkit_listener_class_init(CmdPkAgentPolkitListenerClass *klass) 401 | { 402 | log__verbose__init_polkit_listener(); 403 | GObjectClass *g_object_class; 404 | PolkitAgentListenerClass* pkal_class; 405 | g_object_class = G_OBJECT_CLASS(klass); 406 | g_object_class->finalize = cmd_pk_agent_polkit_listener_finalize; 407 | 408 | pkal_class = POLKIT_AGENT_LISTENER_CLASS(klass); 409 | pkal_class->initiate_authentication = initiate_authentication; 410 | pkal_class->initiate_authentication_finish = initiate_authentication_finish; 411 | } 412 | 413 | static void cmd_pk_agent_polkit_listener_init([[maybe_unused]] CmdPkAgentPolkitListener *self) 414 | { 415 | } 416 | 417 | PolkitAgentListener* cmd_pk_agent_polkit_listener_new(void) 418 | { 419 | return g_object_new(CMD_PK_AGENT_TYPE_POLKIT_LISTENER, NULL); 420 | } 421 | 422 | 423 | -------------------------------------------------------------------------------- /src/polkit-auth-handler.service.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-2.1-or-later 2 | // Copyright (C) 2024 Omar Castro 3 | #ifndef SERVICE__H__POLKIT_AUTHENTICATION_HANDLER 4 | #define SERVICE__H__POLKIT_AUTHENTICATION_HANDLER 5 | 6 | #define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE 7 | #include 8 | #include 9 | 10 | G_BEGIN_DECLS 11 | 12 | #define CMD_PK_AGENT_TYPE_POLKIT_LISTENER (cmd_pk_agent_polkit_listener_get_type()) 13 | #define CMD_PK_AGENT_POLKIT_LISTENER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), CMD_PK_AGENT_TYPE_POLKIT_LISTENER, CmdPkAgentPolkitListener)) 14 | #define CMD_PK_AGENT_POLKIT_LISTENER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), CMD_PK_AGENT_TYPE_POLKIT_LISTENER, CmdPkAgentPolkitListenerClass)) 15 | #define CMD_PK_AGENT_IS_POLKIT_LISTENER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), CMD_PK_AGENT_TYPE_POLKIT_LISTENER)) 16 | #define CMD_PK_AGENT_IS_POLKIT_LISTENER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), CMD_PK_AGENT_TYPE_POLKIT_LISTENER)) 17 | #define CMD_PK_AGENT_POLKIT_LISTENER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), CMD_PK_AGENT_TYPE_POLKIT_LISTENER, CmdPkAgentPolkitListenerClass)) 18 | 19 | 20 | typedef struct _CmdPkAgentPolkitListener CmdPkAgentPolkitListener; 21 | typedef struct _CmdPkAgentPolkitListenerClass CmdPkAgentPolkitListenerClass; 22 | 23 | struct _CmdPkAgentPolkitListener { 24 | PolkitAgentListener parent; 25 | }; 26 | 27 | struct _CmdPkAgentPolkitListenerClass { 28 | PolkitAgentListenerClass parent_class; 29 | }; 30 | 31 | GType cmd_pk_agent_polkit_listener_get_type(void); 32 | PolkitAgentListener* cmd_pk_agent_polkit_listener_new(void); 33 | 34 | G_END_DECLS 35 | 36 | #endif 37 | 38 | -------------------------------------------------------------------------------- /src/request-messages.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-2.1-or-later 2 | // Copyright (C) 2024 Omar Castro 3 | #include 4 | #include 5 | #include "request-messages.h" 6 | 7 | 8 | const gchar * request_message_authorization_authorized(){ 9 | return g_strdup("{\"action\":\"authorization response\",\"authorized\":true}"); 10 | } 11 | 12 | const gchar * request_message_authorization_not_authorized(){ 13 | return g_strdup("{\"action\":\"authorization response\",\"authorized\":false}"); 14 | 15 | } 16 | 17 | const gchar * request_message_request_password( 18 | const gchar * prompt, 19 | const gchar * message, 20 | PolkitActionDescription* action_description 21 | ){ 22 | g_autoptr(JsonBuilder) builder = json_builder_new (); 23 | 24 | json_builder_begin_object (builder); 25 | 26 | json_builder_set_member_name (builder, "action"); 27 | json_builder_add_string_value (builder, "request password"); 28 | 29 | json_builder_set_member_name (builder, "prompt"); 30 | json_builder_add_string_value (builder, prompt); 31 | 32 | json_builder_set_member_name (builder, "message"); 33 | json_builder_add_string_value (builder, message); 34 | 35 | json_builder_set_member_name(builder, "polkit action"); 36 | if(action_description == NULL){ 37 | json_builder_add_null_value(builder); 38 | } else { 39 | json_builder_begin_object (builder); 40 | json_builder_set_member_name (builder, "id"); 41 | json_builder_add_string_value (builder, polkit_action_description_get_action_id(action_description)); 42 | 43 | json_builder_set_member_name (builder, "description"); 44 | json_builder_add_string_value (builder, polkit_action_description_get_description(action_description)); 45 | 46 | json_builder_set_member_name (builder, "message"); 47 | json_builder_add_string_value (builder, polkit_action_description_get_message(action_description)); 48 | 49 | json_builder_set_member_name (builder, "vendor name"); 50 | json_builder_add_string_value (builder, polkit_action_description_get_vendor_name(action_description)); 51 | 52 | json_builder_set_member_name (builder, "vendor url"); 53 | json_builder_add_string_value (builder, polkit_action_description_get_vendor_url(action_description)); 54 | 55 | json_builder_set_member_name (builder, "icon name"); 56 | json_builder_add_string_value (builder, polkit_action_description_get_icon_name(action_description)); 57 | 58 | 59 | json_builder_end_object (builder); 60 | 61 | } 62 | 63 | json_builder_end_object (builder); 64 | 65 | g_autoptr(JsonNode) root = json_builder_get_root (builder); 66 | 67 | g_autoptr(JsonGenerator) gen = json_generator_new (); 68 | json_generator_set_root (gen, root); 69 | return json_generator_to_data (gen, NULL); 70 | } 71 | 72 | -------------------------------------------------------------------------------- /src/request-messages.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-2.1-or-later 2 | // Copyright (C) 2024 Omar Castro 3 | #ifndef EXTENSION__H__REQUEST_MESSAGES 4 | #define EXTENSION__H__REQUEST_MESSAGES 5 | 6 | #include "polkit/polkittypes.h" 7 | #include 8 | /** 9 | * Sent when password given is correct 10 | */ 11 | const gchar * request_message_authorization_authorized(); 12 | 13 | const gchar * request_message_authorization_not_authorized(); 14 | 15 | const gchar * request_message_request_password( 16 | const gchar * prompt, 17 | const gchar * message, 18 | PolkitActionDescription* action_description 19 | ); 20 | 21 | #endif //EXTENSION__H__REQUEST_MESSAGES -------------------------------------------------------------------------------- /test/app.mock.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-2.1-or-later 2 | // Copyright (C) 2024 Omar Castro 3 | #include "app.mock.h" 4 | #include "../src/app.c" 5 | #include 6 | 7 | void app__reset(){ 8 | isInitialized = false; 9 | if(cmd_line != NULL){ 10 | g_free(cmd_line); 11 | cmd_line = NULL; 12 | } 13 | if(cmd_line_argv != NULL){ 14 | g_strfreev (cmd_line_argv); 15 | cmd_line_argv = NULL; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/app.mock.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-2.1-or-later 2 | // Copyright (C) 2024 Omar Castro 3 | #ifndef APP_MOCK_H 4 | #define APP_MOCK_H 5 | #include "../src/app.h" 6 | 7 | void app__reset(); 8 | 9 | #endif -------------------------------------------------------------------------------- /test/assets/test_response_cancel.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | while read -r msg; do 4 | # --- shellcheck disable=SC2210 5 | if echo "$msg" | jq -e '.action == "request password"' 1>/dev/null 2>/dev/null 6 | then 7 | echo "{\"action\":\"cancel\"}" 8 | fi 9 | done -------------------------------------------------------------------------------- /test/assets/test_response_command.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | while read -r msg; do 4 | # --- shellcheck disable=SC2210 5 | if echo "$msg" | jq -e '.action == "request password"' 1>/dev/null 2>/dev/null 6 | then 7 | echo "{\"action\":\"authenticate\",\"password\": \"success\"}" 8 | fi 9 | done -------------------------------------------------------------------------------- /test/assets/test_response_fail_retry.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | retry=0 4 | 5 | while read -r msg; do 6 | # --- shellcheck disable=SC2210 7 | if echo "$msg" | jq -e '.action == "request password"' 1>/dev/null 2>/dev/null 8 | then 9 | if [ "$retry" -gt "2" ]; then 10 | echo "{\"action\":\"authenticate\",\"password\": \"success\"}" 11 | else 12 | ((retry+=1)) 13 | echo "{\"action\":\"authenticate\",\"password\": \"fail\"}" 14 | fi 15 | fi 16 | done -------------------------------------------------------------------------------- /test/assets/test_response_parallel.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | FILE="test-parallel.timings.txt" 3 | 4 | echo "start" >> "$FILE" 5 | 6 | while read -r msg; do 7 | # --- shellcheck disable=SC2210 8 | if echo "$msg" | jq -e '.action == "request password"' 1>/dev/null 2>/dev/null 9 | then 10 | sleep 0.25 11 | echo "end" >> "$FILE" 12 | echo "{\"action\":\"authenticate\",\"password\": \"success\"}" 13 | fi 14 | done -------------------------------------------------------------------------------- /test/assets/test_response_serial.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | FILE="test-serial.timings.txt" 3 | 4 | while read -r msg; do 5 | # --- shellcheck disable=SC2210 6 | if echo "$msg" | jq -e '.action == "request password"' 1>/dev/null 2>/dev/null 7 | then 8 | MESSAGE='`'"$(echo "$msg" | grep -o '/usr/bin/echo [0-9]')"'` auth' 9 | echo "start $MESSAGE" >> "$FILE" 10 | sleep 0.25 11 | echo " end $MESSAGE" >> "$FILE" 12 | echo "{\"action\":\"authenticate\",\"password\": \"success\"}" 13 | fi 14 | done -------------------------------------------------------------------------------- /test/error-message.mock.c: -------------------------------------------------------------------------------- 1 | #include "error-message.mock.h" 2 | #include "../src/error-message.dialog.c" 3 | #include 4 | 5 | void reset_lazy_init_gtk(){ 6 | is_gtk_initialized = false; 7 | } 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/error-message.mock.h: -------------------------------------------------------------------------------- 1 | #include "../src/error-message.dialog.h" // IWYU pragma: export 2 | #include "gtk.mock.h" 3 | 4 | 5 | void reset_lazy_init_gtk(); 6 | 7 | void lazy_init_gtk(); -------------------------------------------------------------------------------- /test/gtk.mock.c: -------------------------------------------------------------------------------- 1 | #include "gtk.mock.h" 2 | 3 | static int callTimesGtkInit = 0; 4 | static int callTimesGtkDialogRun = 0; 5 | 6 | void mock_gtk_init ([[maybe_unused]] int *argc, [[maybe_unused]] char ***argv){ 7 | callTimesGtkInit++; 8 | } 9 | 10 | int called_times_gtk_init(){ 11 | return callTimesGtkInit; 12 | } 13 | 14 | 15 | 16 | void setup_gtk_mock(){ 17 | callTimesGtkInit = 0; 18 | } 19 | 20 | gint mock_gtk_dialog_run ([[maybe_unused]] GtkDialog *dialog){ 21 | callTimesGtkDialogRun++; 22 | return GTK_RESPONSE_NONE; 23 | } 24 | 25 | int called_times_gtk_dialog_run(){ 26 | return callTimesGtkDialogRun; 27 | } 28 | 29 | GtkWidget* mock_gtk_message_dialog_new ([[maybe_unused]] GtkWindow *parent, 30 | [[maybe_unused]] GtkDialogFlags flags, 31 | [[maybe_unused]] GtkMessageType type, 32 | [[maybe_unused]] GtkButtonsType buttons, 33 | [[maybe_unused]] const gchar *message_format, 34 | ...) { 35 | // Mock implementation: return a dummy GtkWidget pointer 36 | return NULL; 37 | } 38 | 39 | void mock_gtk_widget_destroy ([[maybe_unused]] GtkWidget *widget){ 40 | // Mock implementation: do nothing 41 | } 42 | 43 | 44 | void mock_gtk_window_set_title ([[maybe_unused]] GtkWindow *window, [[maybe_unused]] const gchar *title){ 45 | // Mock implementation: do nothing 46 | } 47 | -------------------------------------------------------------------------------- /test/gtk.mock.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void mock_gtk_init (int *argc, 4 | char ***argv); 5 | 6 | 7 | GDK_AVAILABLE_IN_ALL 8 | GtkWidget* mock_gtk_message_dialog_new (GtkWindow *parent, 9 | GtkDialogFlags flags, 10 | GtkMessageType type, 11 | GtkButtonsType buttons, 12 | const gchar *message_format, 13 | ...) G_GNUC_PRINTF (5, 6); 14 | 15 | 16 | gint mock_gtk_dialog_run (GtkDialog *dialog); 17 | 18 | void mock_gtk_widget_destroy (GtkWidget *widget); 19 | 20 | void mock_gtk_window_set_title (GtkWindow *window, const gchar *title); 21 | 22 | 23 | 24 | int called_times_gtk_init(); 25 | int called_times_gtk_dialog_run(); 26 | void setup_gtk_mock(); 27 | 28 | #undef GTK_WINDOW 29 | #define GTK_WINDOW(window) ((GtkWindow*)(void *)window) 30 | 31 | #undef GTK_DIALOG 32 | #define GTK_DIALOG(window) ((GtkDialog*)(void *)window) 33 | 34 | #undef GTK_WIDGET 35 | #define GTK_WIDGET(window) ((GtkWidget*)(void *)window) 36 | 37 | 38 | #define gtk_init(argc, argv) mock_gtk_init(argc, argv) 39 | #define gtk_dialog_run(widget) mock_gtk_dialog_run(widget) 40 | #define gtk_widget_destroy(widget) mock_gtk_widget_destroy(widget) 41 | #define gtk_message_dialog_new(parent,flags,type,buttons,message_format, ...) mock_gtk_message_dialog_new (parent,flags,type,buttons,message_format, __VA_ARGS__) 42 | #define gtk_window_set_title(widget,title) mock_gtk_window_set_title (widget,title) 43 | 44 | -------------------------------------------------------------------------------- /test/logger.mock.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | GString * get_stdout(); 8 | GString * get_stderr(); 9 | GString *memory_stdout = NULL; 10 | GString *memory_stderr = NULL; 11 | 12 | static void mock_printf(const char * format, ...){ 13 | 14 | va_list arglist; 15 | va_start( arglist, format ); 16 | g_string_append_vprintf( get_stdout(), format, arglist ); 17 | va_end( arglist ); 18 | 19 | } 20 | 21 | static void mock_fprintf(FILE *stream, const char *format, ...){ 22 | GString * memoryfile = stream == stderr ? get_stderr() : get_stdout(); 23 | va_list arglist; 24 | va_start( arglist, format ); 25 | g_string_append_vprintf( memoryfile, format, arglist ); 26 | va_end( arglist ); 27 | 28 | } 29 | 30 | static void mock_vprintf(const char *format, va_list arglist){ 31 | g_string_append_vprintf( get_stdout(), format, arglist ); 32 | } 33 | 34 | 35 | // mock 36 | #define printf(f_, ...) mock_printf((f_), ##__VA_ARGS__) 37 | #define fprintf(s_, f_, ...) mock_fprintf((s_),(f_), ##__VA_ARGS__) 38 | #define vprintf(f_, a_) mock_vprintf((f_), (a_)) 39 | 40 | 41 | #include "../src/cmdline.c" 42 | #include "../src/logger.c" 43 | #include "logger.mock.h" 44 | 45 | GString * get_stdout(){ 46 | if(memory_stdout == NULL){ 47 | memory_stdout = g_string_new(""); 48 | } 49 | return memory_stdout; 50 | } 51 | 52 | GString * get_stderr(){ 53 | if(memory_stderr == NULL){ 54 | memory_stderr = g_string_new(""); 55 | } 56 | return memory_stderr; 57 | } 58 | 59 | 60 | void reset_logs() { 61 | g_string_truncate(get_stdout(), 0); 62 | g_string_truncate(get_stderr(), 0); 63 | silenced_logs = false; 64 | verbose_logs = false; 65 | } 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /test/logger.mock.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-2.1-or-later 2 | // Copyright (C) 2024 Omar Castro 3 | #ifndef LOGGER__TEST_H 4 | #define LOGGER__TEST_H 5 | #include 6 | 7 | GString * get_stdout(); 8 | GString * get_stderr(); 9 | void reset_logs(); 10 | 11 | 12 | #endif // LOGGER__TEST_H 13 | -------------------------------------------------------------------------------- /test/polkit-auth-handler.service.mock.c: -------------------------------------------------------------------------------- 1 | #include "polkit-auth-handler.service.mock.h" // IWYU pragma: export 2 | #include "../src/polkit-auth-handler.service.c" 3 | -------------------------------------------------------------------------------- /test/polkit-auth-handler.service.mock.h: -------------------------------------------------------------------------------- 1 | #include "polkit.mock.h" 2 | #include "../src/polkit-auth-handler.service.h" // IWYU pragma: export 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/polkit.mock.c: -------------------------------------------------------------------------------- 1 | #include "polkit.mock.h" 2 | 3 | static int request(gpointer session){ 4 | g_signal_emit_by_name (session, "request", "Password:", FALSE); 5 | return G_SOURCE_REMOVE; 6 | } 7 | 8 | static int complete_success(gpointer session){ 9 | g_signal_emit_by_name (session, "completed", TRUE); 10 | return G_SOURCE_REMOVE; 11 | } 12 | 13 | static int complete_error(gpointer session){ 14 | g_signal_emit_by_name (session, "completed", FALSE); 15 | return G_SOURCE_REMOVE; 16 | } 17 | 18 | void mock_polkit_agent_session_initiate (PolkitAgentSession *session){ 19 | g_idle_add(request, session); 20 | } 21 | 22 | void mock_polkit_agent_session_cancel (PolkitAgentSession *session){ 23 | g_idle_add(complete_error, session); 24 | } 25 | 26 | void mock_polkit_agent_session_response (PolkitAgentSession *session, const gchar *response){ 27 | const gboolean authenticated = g_str_equal(response, "success"); 28 | if(authenticated){ 29 | g_idle_add(complete_success, session); 30 | } else { 31 | g_timeout_add(500, complete_error, session); 32 | } 33 | } 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/polkit.mock.h: -------------------------------------------------------------------------------- 1 | #define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE 2 | #include 3 | #include 4 | 5 | 6 | void mock_polkit_agent_session_initiate (PolkitAgentSession *session); 7 | void mock_polkit_agent_session_cancel (PolkitAgentSession *session); 8 | void mock_polkit_agent_session_response (PolkitAgentSession *session, const gchar *response); 9 | 10 | 11 | #define polkit_agent_session_initiate(session) mock_polkit_agent_session_initiate(session) 12 | #define polkit_agent_session_cancel(session) mock_polkit_agent_session_cancel(session) 13 | #define polkit_agent_session_response(session,response) mock_polkit_agent_session_response(session,response) 14 | -------------------------------------------------------------------------------- /test/test-it-command-exec.entrypoint.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "app.mock.h" 7 | #include 8 | #include "error-message.mock.h" 9 | #include "polkit-auth-handler.service.mock.h" 10 | #include "src/app.h" 11 | #include "test/app.mock.h" 12 | 13 | 14 | typedef struct { 15 | PolkitAgentListener* listener; 16 | GList * identities; 17 | PolkitDetails *details; 18 | GMainLoop *loop; 19 | } Fixture; 20 | 21 | char *current_cmd_line = "bash ./assets/test_response_command.sh"; 22 | 23 | 24 | const int test_argc = 4; 25 | char * test_argv[] = { 26 | "cmd-polkit-agent", 27 | "-p", 28 | "-c", 29 | "bash ./assets/test_response_command.sh", 30 | NULL 31 | }; 32 | 33 | static int quitloop(gpointer fixture_ptr){ 34 | Fixture *fixture = fixture_ptr; 35 | g_main_loop_quit(fixture->loop); 36 | return G_SOURCE_REMOVE; 37 | 38 | } 39 | 40 | static void finish_autentication_and_exit([[maybe_unused]] GObject *obj, GAsyncResult * result, gpointer fixture_ptr){ 41 | Fixture *fixture = fixture_ptr; 42 | PolkitAgentListener *listener = fixture->listener; 43 | GError *error = NULL; 44 | POLKIT_AGENT_LISTENER_GET_CLASS (listener)->initiate_authentication_finish (listener, result, &error); 45 | g_idle_add(quitloop, fixture); 46 | } 47 | 48 | static int test_polkit_auth_handler_authentication_aux (gpointer fixture_ptr) { 49 | Fixture *fixture = fixture_ptr; 50 | fixture->listener = cmd_pk_agent_polkit_listener_new(); 51 | const gchar *action_id = "org.freedesktop.policykit.exec"; 52 | const gchar *message = "Authentication is needed to run `/usr/bin/echo 1' as the super user"; 53 | const gchar *icon_name = ""; 54 | const gchar *cookie = "3-97423289449bd6d0c3915fb1308b9814-1-a305f93fec6edd353d6d1845e7fcf1b2"; 55 | fixture->details = polkit_details_new(); 56 | PolkitIdentity *user = polkit_unix_user_new(1000); 57 | fixture->identities = g_list_append(fixture->identities, user); 58 | 59 | POLKIT_AGENT_LISTENER_GET_CLASS(fixture->listener)->initiate_authentication( 60 | fixture->listener, 61 | action_id,message, 62 | icon_name, 63 | fixture->details, 64 | cookie, 65 | fixture->identities, 66 | NULL, 67 | finish_autentication_and_exit, 68 | fixture 69 | ); 70 | 71 | 72 | return G_SOURCE_REMOVE; 73 | } 74 | 75 | 76 | static void test_polkit_auth_handler_authentication_success (Fixture *fixture, [[maybe_unused]] gconstpointer user_data) { 77 | test_argv[3] = "bash ./assets/test_response_command.sh"; 78 | app__init(test_argc, test_argv); 79 | fixture->loop = g_main_loop_new (NULL, FALSE); 80 | g_idle_add(test_polkit_auth_handler_authentication_aux, fixture); 81 | g_main_loop_run(fixture->loop); 82 | } 83 | 84 | static void test_polkit_auth_handler_authentication_cancel (Fixture *fixture, [[maybe_unused]] gconstpointer user_data) { 85 | test_argv[3] = "bash ./assets/test_response_cancel.sh"; 86 | app__init(test_argc, test_argv); 87 | fixture->loop = g_main_loop_new (NULL, FALSE); 88 | g_idle_add(test_polkit_auth_handler_authentication_aux, fixture); 89 | g_main_loop_run(fixture->loop); 90 | } 91 | 92 | static void test_polkit_auth_handler_authentication_fail_retry (Fixture *fixture, [[maybe_unused]] gconstpointer user_data) { 93 | test_argv[3] = "bash ./assets/test_response_fail_retry.sh"; 94 | app__init(test_argc, test_argv); 95 | fixture->loop = g_main_loop_new (NULL, FALSE); 96 | g_idle_add(test_polkit_auth_handler_authentication_aux, fixture); 97 | g_main_loop_run(fixture->loop); 98 | } 99 | 100 | 101 | static void test_set_up ([[maybe_unused]] Fixture *fixture, [[maybe_unused]] gconstpointer user_data){ 102 | app__reset(); 103 | } 104 | 105 | static void test_tear_down (Fixture *fixture, [[maybe_unused]] gconstpointer user_data){ 106 | if(fixture->listener != NULL){ 107 | g_object_unref(fixture->listener); 108 | fixture->listener = NULL; 109 | } 110 | if(fixture->identities != NULL){ 111 | g_list_free_full(fixture->identities, g_object_unref); 112 | fixture->identities = NULL; 113 | } 114 | if(fixture->details != NULL){ 115 | g_object_unref(fixture->details); 116 | fixture->details = NULL; 117 | } 118 | g_main_loop_unref(fixture->loop); 119 | 120 | } 121 | 122 | 123 | int main (int argc, char *argv[]) { 124 | 125 | setlocale (LC_ALL, ""); 126 | 127 | g_test_init (&argc, &argv, NULL); 128 | 129 | #define test(path, func) g_test_add (path, Fixture, NULL, test_set_up, func, test_tear_down); 130 | 131 | // Define the tests. 132 | test ("/ polkit auth handler / CmdPkAgentPolkitListener initiate_authentication procedure success testing", test_polkit_auth_handler_authentication_success); 133 | test ("/ polkit auth handler / CmdPkAgentPolkitListener initiate_authentication procedure cancel testing", test_polkit_auth_handler_authentication_cancel); 134 | test ("/ polkit auth handler / CmdPkAgentPolkitListener initiate_authentication procedure fail retry testing", test_polkit_auth_handler_authentication_fail_retry); 135 | 136 | #undef test 137 | return g_test_run (); 138 | } -------------------------------------------------------------------------------- /test/test-it-parallel-mode.entrypoint.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "app.mock.h" 7 | #include 8 | #include "error-message.mock.h" 9 | #include "polkit-auth-handler.service.mock.h" 10 | #include "src/app.h" 11 | #include "test/app.mock.h" 12 | 13 | 14 | typedef struct { 15 | PolkitAgentListener* listener; 16 | GList * identities; 17 | PolkitDetails *details; 18 | GMainLoop *loop; 19 | } Fixture; 20 | 21 | char *current_cmd_line = "bash ./assets/test_response_command.sh"; 22 | 23 | 24 | const int test_argc = 4; 25 | char * test_argv[] = { 26 | "cmd-polkit-agent", 27 | "-p", 28 | "-c", 29 | "bash ./assets/test_response_parallel.sh", 30 | NULL 31 | }; 32 | 33 | static int quitloop(gpointer fixture_ptr){ 34 | Fixture *fixture = fixture_ptr; 35 | g_main_loop_quit(fixture->loop); 36 | return G_SOURCE_REMOVE; 37 | 38 | } 39 | 40 | static void finish_autentication([[maybe_unused]] GObject *obj, GAsyncResult * result, gpointer fixture_ptr){ 41 | Fixture *fixture = fixture_ptr; 42 | PolkitAgentListener *listener = fixture->listener; 43 | GError *error = NULL; 44 | POLKIT_AGENT_LISTENER_GET_CLASS (listener)->initiate_authentication_finish (listener, result, &error); 45 | } 46 | 47 | static void finish_autentication_chek_parallel_exit([[maybe_unused]] GObject *obj, GAsyncResult * result, gpointer fixture_ptr){ 48 | Fixture *fixture = fixture_ptr; 49 | PolkitAgentListener *listener = fixture->listener; 50 | GError *error = NULL; 51 | POLKIT_AGENT_LISTENER_GET_CLASS (listener)->initiate_authentication_finish (listener, result, &error); 52 | gchar * contents = NULL; 53 | if(g_file_get_contents("test-parallel.timings.txt", &contents, NULL, NULL)){ 54 | GFile* file = g_file_new_for_path ("test-parallel.timings.txt"); 55 | g_file_delete(file, NULL, NULL); 56 | g_object_unref(file); 57 | /* 58 | Unlike finish_autentication_chek_serial_exit() method test-it-serial-mode, 59 | we do not care which command started or finished first, since they are 60 | meant to run in parallel, what matters is that they all started and finished 61 | */ 62 | g_assert_cmpstr(contents, ==, "\ 63 | start\n\ 64 | start\n\ 65 | start\n\ 66 | end\n\ 67 | end\n\ 68 | end\n\ 69 | "); 70 | } 71 | g_free(contents); 72 | g_idle_add(quitloop, fixture); 73 | } 74 | 75 | static int test_polkit_auth_handler_authentication_parallel_aux (gpointer fixture_ptr) { 76 | Fixture *fixture = fixture_ptr; 77 | fixture->listener = cmd_pk_agent_polkit_listener_new(); 78 | const gchar *action_id = "org.freedesktop.policykit.exec"; 79 | const gchar *message = "Authentication is needed to run `/usr/bin/echo 1' as the super user"; 80 | const gchar *icon_name = ""; 81 | const gchar *cookie = "3-97423289449bd6d0c3915fb1308b9814-1-a305f93fec6edd353d6d1845e7fcf1b2"; 82 | fixture->details = polkit_details_new(); 83 | PolkitIdentity *user = polkit_unix_user_new(1000); 84 | fixture->identities = g_list_append(fixture->identities, user); 85 | 86 | POLKIT_AGENT_LISTENER_GET_CLASS(fixture->listener)->initiate_authentication( 87 | fixture->listener, 88 | action_id,message, 89 | icon_name, 90 | fixture->details, 91 | cookie, 92 | fixture->identities, 93 | NULL, 94 | finish_autentication, 95 | fixture 96 | ); 97 | 98 | POLKIT_AGENT_LISTENER_GET_CLASS(fixture->listener)->initiate_authentication( 99 | fixture->listener, 100 | action_id,message, 101 | icon_name, 102 | fixture->details, 103 | cookie, 104 | fixture->identities, 105 | NULL, 106 | finish_autentication, 107 | fixture 108 | ); 109 | 110 | POLKIT_AGENT_LISTENER_GET_CLASS(fixture->listener)->initiate_authentication( 111 | fixture->listener, 112 | action_id,message, 113 | icon_name, 114 | fixture->details, 115 | cookie, 116 | fixture->identities, 117 | NULL, 118 | finish_autentication_chek_parallel_exit, 119 | fixture 120 | ); 121 | 122 | 123 | return G_SOURCE_REMOVE; 124 | } 125 | 126 | 127 | 128 | static void test_polkit_auth_handler_authentication_parallel_mode (Fixture *fixture, [[maybe_unused]] gconstpointer user_data) { 129 | 130 | app__init(test_argc, test_argv); 131 | fixture->loop = g_main_loop_new (NULL, FALSE); 132 | g_idle_add(test_polkit_auth_handler_authentication_parallel_aux, fixture); 133 | g_main_loop_run(fixture->loop); 134 | } 135 | 136 | 137 | 138 | static void test_set_up ([[maybe_unused]] Fixture *fixture, [[maybe_unused]] gconstpointer user_data){ 139 | app__reset(); 140 | } 141 | 142 | static void test_tear_down (Fixture *fixture, [[maybe_unused]] gconstpointer user_data){ 143 | if(fixture->listener != NULL){ 144 | g_object_unref(fixture->listener); 145 | fixture->listener = NULL; 146 | } 147 | if(fixture->identities != NULL){ 148 | g_list_free_full(fixture->identities, g_object_unref); 149 | fixture->identities = NULL; 150 | } 151 | if(fixture->details != NULL){ 152 | g_object_unref(fixture->details); 153 | fixture->details = NULL; 154 | } 155 | g_main_loop_unref(fixture->loop); 156 | 157 | } 158 | 159 | 160 | int main (int argc, char *argv[]) { 161 | 162 | setlocale (LC_ALL, ""); 163 | 164 | g_test_init (&argc, &argv, NULL); 165 | 166 | #define test(path, func) g_test_add (path, Fixture, NULL, test_set_up, func, test_tear_down); 167 | 168 | // Define the tests. 169 | test ("/ polkit auth handler / CmdPkAgentPolkitListener initiate_authentication procedure serial mode", test_polkit_auth_handler_authentication_parallel_mode); 170 | 171 | #undef test 172 | return g_test_run (); 173 | } -------------------------------------------------------------------------------- /test/test-it-serial-mode.entrypoint.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "app.mock.h" 7 | #include 8 | #include "error-message.mock.h" 9 | #include "polkit-auth-handler.service.mock.h" 10 | #include "src/app.h" 11 | #include "test/app.mock.h" 12 | 13 | 14 | typedef struct { 15 | PolkitAgentListener* listener; 16 | GList * identities; 17 | PolkitDetails *details; 18 | GMainLoop *loop; 19 | } Fixture; 20 | 21 | char *current_cmd_line = "bash ./assets/test_response_command.sh"; 22 | 23 | 24 | const int test_argc = 4; 25 | char * test_argv[] = { 26 | "cmd-polkit-agent", 27 | "-s", 28 | "-c", 29 | "bash ./assets/test_response_serial.sh", 30 | NULL 31 | }; 32 | 33 | static int quitloop(gpointer fixture_ptr){ 34 | Fixture *fixture = fixture_ptr; 35 | g_main_loop_quit(fixture->loop); 36 | return G_SOURCE_REMOVE; 37 | 38 | } 39 | 40 | static void finish_autentication([[maybe_unused]] GObject *obj, GAsyncResult * result, gpointer fixture_ptr){ 41 | Fixture *fixture = fixture_ptr; 42 | PolkitAgentListener *listener = fixture->listener; 43 | GError *error = NULL; 44 | POLKIT_AGENT_LISTENER_GET_CLASS (listener)->initiate_authentication_finish (listener, result, &error); 45 | } 46 | 47 | static void finish_autentication_chek_serial_exit([[maybe_unused]] GObject *obj, GAsyncResult * result, gpointer fixture_ptr){ 48 | Fixture *fixture = fixture_ptr; 49 | PolkitAgentListener *listener = fixture->listener; 50 | GError *error = NULL; 51 | POLKIT_AGENT_LISTENER_GET_CLASS (listener)->initiate_authentication_finish (listener, result, &error); 52 | gchar * contents = NULL; 53 | if(g_file_get_contents("test-serial.timings.txt", &contents, NULL, NULL)){ 54 | GFile* file = g_file_new_for_path ("test-serial.timings.txt"); 55 | g_file_delete(file, NULL, NULL); 56 | g_object_unref(file); 57 | g_assert_cmpstr(contents, ==, "\ 58 | start `/usr/bin/echo 1` auth\n\ 59 | end `/usr/bin/echo 1` auth\n\ 60 | start `/usr/bin/echo 2` auth\n\ 61 | end `/usr/bin/echo 2` auth\n\ 62 | start `/usr/bin/echo 3` auth\n\ 63 | end `/usr/bin/echo 3` auth\n\ 64 | "); 65 | } 66 | g_free(contents); 67 | g_idle_add(quitloop, fixture); 68 | } 69 | 70 | static int test_polkit_auth_handler_authentication_aux_serial (gpointer fixture_ptr) { 71 | Fixture *fixture = fixture_ptr; 72 | fixture->listener = cmd_pk_agent_polkit_listener_new(); 73 | const gchar *action_id = "org.freedesktop.policykit.exec"; 74 | const gchar *message_1 = "Authentication is needed to run `/usr/bin/echo 1' as the super user"; 75 | const gchar *message_2 = "Authentication is needed to run `/usr/bin/echo 2' as the super user"; 76 | const gchar *message_3 = "Authentication is needed to run `/usr/bin/echo 3' as the super user"; 77 | const gchar *icon_name = ""; 78 | const gchar *cookie = "3-97423289449bd6d0c3915fb1308b9814-1-a305f93fec6edd353d6d1845e7fcf1b2"; 79 | fixture->details = polkit_details_new(); 80 | PolkitIdentity *user = polkit_unix_user_new(1000); 81 | fixture->identities = g_list_append(fixture->identities, user); 82 | 83 | POLKIT_AGENT_LISTENER_GET_CLASS(fixture->listener)->initiate_authentication( 84 | fixture->listener, 85 | action_id,message_1, 86 | icon_name, 87 | fixture->details, 88 | cookie, 89 | fixture->identities, 90 | NULL, 91 | finish_autentication, 92 | fixture 93 | ); 94 | 95 | POLKIT_AGENT_LISTENER_GET_CLASS(fixture->listener)->initiate_authentication( 96 | fixture->listener, 97 | action_id,message_2, 98 | icon_name, 99 | fixture->details, 100 | cookie, 101 | fixture->identities, 102 | NULL, 103 | finish_autentication, 104 | fixture 105 | ); 106 | 107 | POLKIT_AGENT_LISTENER_GET_CLASS(fixture->listener)->initiate_authentication( 108 | fixture->listener, 109 | action_id,message_3, 110 | icon_name, 111 | fixture->details, 112 | cookie, 113 | fixture->identities, 114 | NULL, 115 | finish_autentication_chek_serial_exit, 116 | fixture 117 | ); 118 | 119 | 120 | return G_SOURCE_REMOVE; 121 | } 122 | 123 | 124 | 125 | static void test_polkit_auth_handler_authentication_serial_mode (Fixture *fixture, [[maybe_unused]] gconstpointer user_data) { 126 | app__init(test_argc, test_argv); 127 | fixture->loop = g_main_loop_new (NULL, FALSE); 128 | g_idle_add(test_polkit_auth_handler_authentication_aux_serial, fixture); 129 | g_main_loop_run(fixture->loop); 130 | } 131 | 132 | 133 | 134 | static void test_set_up ([[maybe_unused]] Fixture *fixture, [[maybe_unused]] gconstpointer user_data){ 135 | app__reset(); 136 | } 137 | 138 | static void test_tear_down (Fixture *fixture, [[maybe_unused]] gconstpointer user_data){ 139 | if(fixture->listener != NULL){ 140 | g_object_unref(fixture->listener); 141 | fixture->listener = NULL; 142 | } 143 | if(fixture->identities != NULL){ 144 | g_list_free_full(fixture->identities, g_object_unref); 145 | fixture->identities = NULL; 146 | } 147 | if(fixture->details != NULL){ 148 | g_object_unref(fixture->details); 149 | fixture->details = NULL; 150 | } 151 | g_main_loop_unref(fixture->loop); 152 | 153 | } 154 | 155 | 156 | int main (int argc, char *argv[]) { 157 | 158 | setlocale (LC_ALL, ""); 159 | 160 | g_test_init (&argc, &argv, NULL); 161 | 162 | #define test(path, func) g_test_add (path, Fixture, NULL, test_set_up, func, test_tear_down); 163 | 164 | // Define the tests. 165 | test ("/ polkit auth handler / CmdPkAgentPolkitListener initiate_authentication procedure serial mode", test_polkit_auth_handler_authentication_serial_mode); 166 | 167 | #undef test 168 | return g_test_run (); 169 | } --------------------------------------------------------------------------------