├── .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 |
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 |
55 |
56 |
57 | Fig.1 - custom authentication using cmd-polkit with rofi
58 |
59 |
60 |
61 |
62 |
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 |
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 |
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 |
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 2024-06-16 .
201 | 2 https://www.freedesktop.org/software/polkit/docs/latest/polkit.8.html, last seen on 2024-06-16 .
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
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 | }
--------------------------------------------------------------------------------