atagen

roundabout

public
Star0
Watch0

Commit caac091f257a

tarball

atagen <boss@atagen.co>2026-10-04 12:05 UTC

8 files changed+1255-40
run: enforce grants through systemd D-Bus scopes
Modified file: Cargo.lock+757 additions−2 deletions
@@ -3,24 +3,443 @@
33 version = 4
44
55 [[package]]
6+name = "async-broadcast"
7+version = "0.7.2"
8+source = "registry+https://github.com/rust-lang/crates.io-index"
9+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
10+dependencies = [
11+ "event-listener",
12+ "event-listener-strategy",
13+ "futures-core",
14+ "pin-project-lite",
15+]
16+
17+[[package]]
18+name = "async-channel"
19+version = "2.5.0"
20+source = "registry+https://github.com/rust-lang/crates.io-index"
21+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
22+dependencies = [
23+ "concurrent-queue",
24+ "event-listener-strategy",
25+ "futures-core",
26+ "pin-project-lite",
27+]
28+
29+[[package]]
30+name = "async-executor"
31+version = "1.14.0"
32+source = "registry+https://github.com/rust-lang/crates.io-index"
33+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
34+dependencies = [
35+ "async-task",
36+ "concurrent-queue",
37+ "fastrand",
38+ "futures-lite",
39+ "pin-project-lite",
40+ "slab",
41+]
42+
43+[[package]]
44+name = "async-io"
45+version = "2.6.0"
46+source = "registry+https://github.com/rust-lang/crates.io-index"
47+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
48+dependencies = [
49+ "autocfg",
50+ "cfg-if",
51+ "concurrent-queue",
52+ "futures-io",
53+ "futures-lite",
54+ "parking",
55+ "polling",
56+ "rustix",
57+ "slab",
58+ "windows-sys",
59+]
60+
61+[[package]]
62+name = "async-lock"
63+version = "3.4.2"
64+source = "registry+https://github.com/rust-lang/crates.io-index"
65+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
66+dependencies = [
67+ "event-listener",
68+ "event-listener-strategy",
69+ "pin-project-lite",
70+]
71+
72+[[package]]
73+name = "async-process"
74+version = "2.5.0"
75+source = "registry+https://github.com/rust-lang/crates.io-index"
76+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
77+dependencies = [
78+ "async-channel",
79+ "async-io",
80+ "async-lock",
81+ "async-signal",
82+ "async-task",
83+ "blocking",
84+ "cfg-if",
85+ "event-listener",
86+ "futures-lite",
87+ "rustix",
88+]
89+
90+[[package]]
91+name = "async-recursion"
92+version = "1.2.0"
93+source = "registry+https://github.com/rust-lang/crates.io-index"
94+checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451"
95+dependencies = [
96+ "proc-macro2",
97+ "quote",
98+ "syn 3.0.6",
99+]
100+
101+[[package]]
102+name = "async-signal"
103+version = "0.2.14"
104+source = "registry+https://github.com/rust-lang/crates.io-index"
105+checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
106+dependencies = [
107+ "async-io",
108+ "async-lock",
109+ "atomic-waker",
110+ "cfg-if",
111+ "futures-core",
112+ "futures-io",
113+ "rustix",
114+ "signal-hook-registry",
115+ "slab",
116+ "windows-sys",
117+]
118+
119+[[package]]
120+name = "async-task"
121+version = "4.7.1"
122+source = "registry+https://github.com/rust-lang/crates.io-index"
123+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
124+
125+[[package]]
126+name = "async-trait"
127+version = "0.1.92"
128+source = "registry+https://github.com/rust-lang/crates.io-index"
129+checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
130+dependencies = [
131+ "proc-macro2",
132+ "quote",
133+ "syn 3.0.6",
134+]
135+
136+[[package]]
137+name = "atomic-waker"
138+version = "1.1.2"
139+source = "registry+https://github.com/rust-lang/crates.io-index"
140+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
141+
142+[[package]]
143+name = "autocfg"
144+version = "1.5.1"
145+source = "registry+https://github.com/rust-lang/crates.io-index"
146+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
147+
148+[[package]]
149+name = "bitflags"
150+version = "2.13.2"
151+source = "registry+https://github.com/rust-lang/crates.io-index"
152+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
153+
154+[[package]]
155+name = "blocking"
156+version = "1.7.0"
157+source = "registry+https://github.com/rust-lang/crates.io-index"
158+checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
159+dependencies = [
160+ "async-channel",
161+ "async-task",
162+ "futures-io",
163+ "futures-lite",
164+ "piper",
165+]
166+
167+[[package]]
168+name = "bumpalo"
169+version = "3.20.3"
170+source = "registry+https://github.com/rust-lang/crates.io-index"
171+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
172+
173+[[package]]
174+name = "cfg-if"
175+version = "1.0.5"
176+source = "registry+https://github.com/rust-lang/crates.io-index"
177+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
178+
179+[[package]]
180+name = "concurrent-queue"
181+version = "2.5.0"
182+source = "registry+https://github.com/rust-lang/crates.io-index"
183+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
184+dependencies = [
185+ "crossbeam-utils",
186+]
187+
188+[[package]]
189+name = "crossbeam-utils"
190+version = "0.8.23"
191+source = "registry+https://github.com/rust-lang/crates.io-index"
192+checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
193+
194+[[package]]
195+name = "endi"
196+version = "1.1.1"
197+source = "registry+https://github.com/rust-lang/crates.io-index"
198+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
199+
200+[[package]]
201+name = "enumflags2"
202+version = "0.7.12"
203+source = "registry+https://github.com/rust-lang/crates.io-index"
204+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
205+dependencies = [
206+ "enumflags2_derive",
207+ "serde",
208+]
209+
210+[[package]]
211+name = "enumflags2_derive"
212+version = "0.7.12"
213+source = "registry+https://github.com/rust-lang/crates.io-index"
214+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
215+dependencies = [
216+ "proc-macro2",
217+ "quote",
218+ "syn 2.0.119",
219+]
220+
221+[[package]]
222+name = "equivalent"
223+version = "1.0.2"
224+source = "registry+https://github.com/rust-lang/crates.io-index"
225+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
226+
227+[[package]]
228+name = "errno"
229+version = "0.3.14"
230+source = "registry+https://github.com/rust-lang/crates.io-index"
231+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
232+dependencies = [
233+ "libc",
234+ "windows-sys",
235+]
236+
237+[[package]]
238+name = "event-listener"
239+version = "5.4.2"
240+source = "registry+https://github.com/rust-lang/crates.io-index"
241+checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
242+dependencies = [
243+ "parking",
244+ "pin-project-lite",
245+]
246+
247+[[package]]
248+name = "event-listener-strategy"
249+version = "0.5.4"
250+source = "registry+https://github.com/rust-lang/crates.io-index"
251+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
252+dependencies = [
253+ "event-listener",
254+ "pin-project-lite",
255+]
256+
257+[[package]]
258+name = "fastrand"
259+version = "2.5.0"
260+source = "registry+https://github.com/rust-lang/crates.io-index"
261+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
262+
263+[[package]]
264+name = "futures-core"
265+version = "0.3.34"
266+source = "registry+https://github.com/rust-lang/crates.io-index"
267+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
268+
269+[[package]]
270+name = "futures-io"
271+version = "0.3.34"
272+source = "registry+https://github.com/rust-lang/crates.io-index"
273+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
274+
275+[[package]]
276+name = "futures-lite"
277+version = "2.6.1"
278+source = "registry+https://github.com/rust-lang/crates.io-index"
279+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
280+dependencies = [
281+ "fastrand",
282+ "futures-core",
283+ "futures-io",
284+ "parking",
285+ "pin-project-lite",
286+]
287+
288+[[package]]
289+name = "futures-task"
290+version = "0.3.34"
291+source = "registry+https://github.com/rust-lang/crates.io-index"
292+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
293+
294+[[package]]
295+name = "futures-util"
296+version = "0.3.34"
297+source = "registry+https://github.com/rust-lang/crates.io-index"
298+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
299+dependencies = [
300+ "futures-core",
301+ "futures-task",
302+ "pin-project-lite",
303+ "slab",
304+]
305+
306+[[package]]
307+name = "getrandom"
308+version = "0.4.3"
309+source = "registry+https://github.com/rust-lang/crates.io-index"
310+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
311+dependencies = [
312+ "cfg-if",
313+ "libc",
314+ "r-efi",
315+]
316+
317+[[package]]
318+name = "hashbrown"
319+version = "0.17.1"
320+source = "registry+https://github.com/rust-lang/crates.io-index"
321+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
322+
323+[[package]]
324+name = "hermit-abi"
325+version = "0.5.3"
326+source = "registry+https://github.com/rust-lang/crates.io-index"
327+checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
328+
329+[[package]]
330+name = "hex"
331+version = "0.4.3"
332+source = "registry+https://github.com/rust-lang/crates.io-index"
333+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
334+
335+[[package]]
336+name = "indexmap"
337+version = "2.14.2"
338+source = "registry+https://github.com/rust-lang/crates.io-index"
339+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
340+dependencies = [
341+ "equivalent",
342+ "hashbrown",
343+]
344+
345+[[package]]
6346 name = "itoa"
7347 version = "1.0.18"
8348 source = "registry+https://github.com/rust-lang/crates.io-index"
9349 checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
10350
11351 [[package]]
352+name = "js-sys"
353+version = "0.3.106"
354+source = "registry+https://github.com/rust-lang/crates.io-index"
355+checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
356+dependencies = [
357+ "cfg-if",
358+ "futures-util",
359+ "wasm-bindgen",
360+]
361+
362+[[package]]
12363 name = "libc"
13364 version = "0.2.189"
14365 source = "registry+https://github.com/rust-lang/crates.io-index"
15366 checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
16367
17368 [[package]]
369+name = "linux-raw-sys"
370+version = "0.12.1"
371+source = "registry+https://github.com/rust-lang/crates.io-index"
372+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
373+
374+[[package]]
18375 name = "memchr"
19376 version = "2.8.3"
20377 source = "registry+https://github.com/rust-lang/crates.io-index"
21378 checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
22379
23380 [[package]]
381+name = "memoffset"
382+version = "0.9.1"
383+source = "registry+https://github.com/rust-lang/crates.io-index"
384+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
385+dependencies = [
386+ "autocfg",
387+]
388+
389+[[package]]
390+name = "once_cell"
391+version = "1.21.4"
392+source = "registry+https://github.com/rust-lang/crates.io-index"
393+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
394+
395+[[package]]
396+name = "ordered-stream"
397+version = "0.2.0"
398+source = "registry+https://github.com/rust-lang/crates.io-index"
399+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
400+dependencies = [
401+ "futures-core",
402+ "pin-project-lite",
403+]
404+
405+[[package]]
406+name = "parking"
407+version = "2.2.1"
408+source = "registry+https://github.com/rust-lang/crates.io-index"
409+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
410+
411+[[package]]
412+name = "pin-project-lite"
413+version = "0.2.17"
414+source = "registry+https://github.com/rust-lang/crates.io-index"
415+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
416+
417+[[package]]
418+name = "piper"
419+version = "0.2.5"
420+source = "registry+https://github.com/rust-lang/crates.io-index"
421+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
422+dependencies = [
423+ "atomic-waker",
424+ "fastrand",
425+ "futures-io",
426+]
427+
428+[[package]]
429+name = "polling"
430+version = "3.11.0"
431+source = "registry+https://github.com/rust-lang/crates.io-index"
432+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
433+dependencies = [
434+ "cfg-if",
435+ "concurrent-queue",
436+ "hermit-abi",
437+ "pin-project-lite",
438+ "rustix",
439+ "windows-sys",
440+]
441+
442+[[package]]
24443 name = "pound"
25444 version = "0.1.8"
26445 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -41,6 +460,15 @@ dependencies = [
41460 ]
42461
43462 [[package]]
463+name = "proc-macro-crate"
464+version = "3.5.0"
465+source = "registry+https://github.com/rust-lang/crates.io-index"
466+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
467+dependencies = [
468+ "toml_edit",
469+]
470+
471+[[package]]
44472 name = "proc-macro2"
45473 version = "1.0.107"
46474 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -59,17 +487,45 @@ dependencies = [
59487 ]
60488
61489 [[package]]
490+name = "r-efi"
491+version = "6.0.0"
492+source = "registry+https://github.com/rust-lang/crates.io-index"
493+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
494+
495+[[package]]
62496 name = "roundabout"
63497 version = "0.1.0"
64498 dependencies = [
499+ "async-io",
500+ "futures-lite",
65501 "libc",
66502 "pound",
67503 "serde",
68504 "serde_json",
69505 "toml",
506+ "zbus",
507+]
508+
509+[[package]]
510+name = "rustix"
511+version = "1.1.5"
512+source = "registry+https://github.com/rust-lang/crates.io-index"
513+checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
514+dependencies = [
515+ "bitflags",
516+ "errno",
517+ "libc",
518+ "linux-raw-sys",
519+ "windows-sys",
70520 ]
71521
72522 [[package]]
523+name = "rustversion"
524+version = "1.0.23"
525+source = "registry+https://github.com/rust-lang/crates.io-index"
526+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
527+
528+[[package]]
73529 name = "serde"
74530 version = "1.0.229"
75531 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -96,7 +552,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
96552 dependencies = [
97553 "proc-macro2",
98554 "quote",
99- "syn",
555+ "syn 3.0.6",
100556 ]
101557
102558 [[package]]
@@ -113,6 +569,17 @@ dependencies = [
113569 ]
114570
115571 [[package]]
572+name = "serde_repr"
573+version = "0.1.21"
574+source = "registry+https://github.com/rust-lang/crates.io-index"
575+checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
576+dependencies = [
577+ "proc-macro2",
578+ "quote",
579+ "syn 3.0.6",
580+]
581+
582+[[package]]
116583 name = "serde_spanned"
117584 version = "1.1.1"
118585 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -122,6 +589,33 @@ dependencies = [
122589 ]
123590
124591 [[package]]
592+name = "signal-hook-registry"
593+version = "1.4.8"
594+source = "registry+https://github.com/rust-lang/crates.io-index"
595+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
596+dependencies = [
597+ "errno",
598+ "libc",
599+]
600+
601+[[package]]
602+name = "slab"
603+version = "0.4.12"
604+source = "registry+https://github.com/rust-lang/crates.io-index"
605+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
606+
607+[[package]]
608+name = "syn"
609+version = "2.0.119"
610+source = "registry+https://github.com/rust-lang/crates.io-index"
611+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
612+dependencies = [
613+ "proc-macro2",
614+ "quote",
615+ "unicode-ident",
616+]
617+
618+[[package]]
125619 name = "syn"
126620 version = "3.0.6"
127621 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -133,6 +627,19 @@ dependencies = [
133627 ]
134628
135629 [[package]]
630+name = "tempfile"
631+version = "3.27.0"
632+source = "registry+https://github.com/rust-lang/crates.io-index"
633+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
634+dependencies = [
635+ "fastrand",
636+ "getrandom",
637+ "once_cell",
638+ "rustix",
639+ "windows-sys",
640+]
641+
642+[[package]]
136643 name = "toml"
137644 version = "0.9.12+spec-1.1.0"
138645 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -140,7 +647,7 @@ checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
140647 dependencies = [
141648 "serde_core",
142649 "serde_spanned",
143- "toml_datetime",
650+ "toml_datetime 0.7.5+spec-1.1.0",
144651 "toml_parser",
145652 "winnow 0.7.15",
146653 ]
@@ -155,6 +662,27 @@ dependencies = [
155662 ]
156663
157664 [[package]]
665+name = "toml_datetime"
666+version = "1.1.1+spec-1.1.0"
667+source = "registry+https://github.com/rust-lang/crates.io-index"
668+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
669+dependencies = [
670+ "serde_core",
671+]
672+
673+[[package]]
674+name = "toml_edit"
675+version = "0.25.15+spec-1.1.0"
676+source = "registry+https://github.com/rust-lang/crates.io-index"
677+checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614"
678+dependencies = [
679+ "indexmap",
680+ "toml_datetime 1.1.1+spec-1.1.0",
681+ "toml_parser",
682+ "winnow 1.0.4",
683+]
684+
685+[[package]]
158686 name = "toml_parser"
159687 version = "1.1.3+spec-1.1.0"
160688 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -164,12 +692,65 @@ dependencies = [
164692 ]
165693
166694 [[package]]
695+name = "tracing"
696+version = "0.1.44"
697+source = "registry+https://github.com/rust-lang/crates.io-index"
698+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
699+dependencies = [
700+ "pin-project-lite",
701+ "tracing-attributes",
702+ "tracing-core",
703+]
704+
705+[[package]]
706+name = "tracing-attributes"
707+version = "0.1.31"
708+source = "registry+https://github.com/rust-lang/crates.io-index"
709+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
710+dependencies = [
711+ "proc-macro2",
712+ "quote",
713+ "syn 2.0.119",
714+]
715+
716+[[package]]
717+name = "tracing-core"
718+version = "0.1.36"
719+source = "registry+https://github.com/rust-lang/crates.io-index"
720+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
721+dependencies = [
722+ "once_cell",
723+]
724+
725+[[package]]
726+name = "uds_windows"
727+version = "1.2.1"
728+source = "registry+https://github.com/rust-lang/crates.io-index"
729+checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
730+dependencies = [
731+ "memoffset",
732+ "tempfile",
733+ "windows-sys",
734+]
735+
736+[[package]]
167737 name = "unicode-ident"
168738 version = "1.0.26"
169739 source = "registry+https://github.com/rust-lang/crates.io-index"
170740 checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
171741
172742 [[package]]
743+name = "uuid"
744+version = "1.27.0"
745+source = "registry+https://github.com/rust-lang/crates.io-index"
746+checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a"
747+dependencies = [
748+ "js-sys",
749+ "serde_core",
750+ "wasm-bindgen",
751+]
752+
753+[[package]]
173754 name = "venial"
174755 version = "0.6.1"
175756 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -180,6 +761,66 @@ dependencies = [
180761 ]
181762
182763 [[package]]
764+name = "wasm-bindgen"
765+version = "0.2.129"
766+source = "registry+https://github.com/rust-lang/crates.io-index"
767+checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
768+dependencies = [
769+ "cfg-if",
770+ "once_cell",
771+ "rustversion",
772+ "wasm-bindgen-macro",
773+ "wasm-bindgen-shared",
774+]
775+
776+[[package]]
777+name = "wasm-bindgen-macro"
778+version = "0.2.129"
779+source = "registry+https://github.com/rust-lang/crates.io-index"
780+checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
781+dependencies = [
782+ "quote",
783+ "wasm-bindgen-macro-support",
784+]
785+
786+[[package]]
787+name = "wasm-bindgen-macro-support"
788+version = "0.2.129"
789+source = "registry+https://github.com/rust-lang/crates.io-index"
790+checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
791+dependencies = [
792+ "bumpalo",
793+ "proc-macro2",
794+ "quote",
795+ "syn 3.0.6",
796+ "wasm-bindgen-shared",
797+]
798+
799+[[package]]
800+name = "wasm-bindgen-shared"
801+version = "0.2.129"
802+source = "registry+https://github.com/rust-lang/crates.io-index"
803+checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
804+dependencies = [
805+ "unicode-ident",
806+]
807+
808+[[package]]
809+name = "windows-link"
810+version = "0.2.1"
811+source = "registry+https://github.com/rust-lang/crates.io-index"
812+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
813+
814+[[package]]
815+name = "windows-sys"
816+version = "0.61.2"
817+source = "registry+https://github.com/rust-lang/crates.io-index"
818+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
819+dependencies = [
820+ "windows-link",
821+]
822+
823+[[package]]
183824 name = "winnow"
184825 version = "0.7.15"
185826 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -190,9 +831,123 @@ name = "winnow"
190831 version = "1.0.4"
191832 source = "registry+https://github.com/rust-lang/crates.io-index"
192833 checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
834+dependencies = [
835+ "memchr",
836+]
837+
838+[[package]]
839+name = "zbus"
840+version = "5.19.0"
841+source = "registry+https://github.com/rust-lang/crates.io-index"
842+checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
843+dependencies = [
844+ "async-broadcast",
845+ "async-executor",
846+ "async-io",
847+ "async-lock",
848+ "async-process",
849+ "async-recursion",
850+ "async-task",
851+ "async-trait",
852+ "blocking",
853+ "enumflags2",
854+ "event-listener",
855+ "futures-core",
856+ "futures-lite",
857+ "hex",
858+ "libc",
859+ "ordered-stream",
860+ "rustix",
861+ "serde",
862+ "serde_repr",
863+ "tracing",
864+ "uds_windows",
865+ "uuid",
866+ "windows-sys",
867+ "winnow 1.0.4",
868+ "zbus_macros",
869+ "zbus_names",
870+ "zvariant",
871+]
872+
873+[[package]]
874+name = "zbus_macros"
875+version = "5.19.0"
876+source = "registry+https://github.com/rust-lang/crates.io-index"
877+checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
878+dependencies = [
879+ "proc-macro-crate",
880+ "proc-macro2",
881+ "quote",
882+ "syn 3.0.6",
883+ "zbus_names",
884+ "zvariant",
885+ "zvariant_utils",
886+]
887+
888+[[package]]
889+name = "zbus_names"
890+version = "4.3.4"
891+source = "registry+https://github.com/rust-lang/crates.io-index"
892+checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
893+dependencies = [
894+ "serde",
895+ "winnow 1.0.4",
896+ "zvariant",
897+]
898+
899+[[package]]
900+name = "zcheapstr"
901+version = "1.1.0"
902+source = "registry+https://github.com/rust-lang/crates.io-index"
903+checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
904+dependencies = [
905+ "serde",
906+]
193907
194908 [[package]]
195909 name = "zmij"
196910 version = "1.0.23"
197911 source = "registry+https://github.com/rust-lang/crates.io-index"
198912 checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
913+
914+[[package]]
915+name = "zvariant"
916+version = "5.15.0"
917+source = "registry+https://github.com/rust-lang/crates.io-index"
918+checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147"
919+dependencies = [
920+ "endi",
921+ "enumflags2",
922+ "serde",
923+ "winnow 1.0.4",
924+ "zcheapstr",
925+ "zvariant_derive",
926+ "zvariant_utils",
927+]
928+
929+[[package]]
930+name = "zvariant_derive"
931+version = "5.15.0"
932+source = "registry+https://github.com/rust-lang/crates.io-index"
933+checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497"
934+dependencies = [
935+ "proc-macro-crate",
936+ "proc-macro2",
937+ "quote",
938+ "syn 3.0.6",
939+ "zvariant_utils",
940+]
941+
942+[[package]]
943+name = "zvariant_utils"
944+version = "4.2.0"
945+source = "registry+https://github.com/rust-lang/crates.io-index"
946+checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3"
947+dependencies = [
948+ "proc-macro2",
949+ "quote",
950+ "serde",
951+ "syn 3.0.6",
952+ "winnow 1.0.4",
953+]
Modified file: Cargo.toml+5 additions
@@ -18,6 +18,11 @@ serde = { version = "1", features = [ "derive" ] }
1818 serde_json = "1"
1919 toml = { version = "0.9", default-features = false, features = [ "parse", "serde" ] }
2020
21+[target.'cfg(target_os = "linux")'.dependencies]
22+async-io = "2"
23+futures-lite = "2"
24+zbus = { version = "5", default-features = false, features = [ "async-io", "blocking-api" ] }
25+
2126 [lints.clippy]
2227 nursery = { level = "warn", priority = -1 }
2328 pedantic = { level = "warn", priority = -1 }
Modified file: README.md+4 additions−2 deletions
@@ -22,7 +22,9 @@ rabt artifacts check | list | gc [--cap] | pin <path>
2222
2323 `run` exits with the build's code, 124 if `--deadline` killed it, 75 if `--max-wait` ran out.
2424
25-a granted build gets `CARGO_BUILD_JOBS`, `MAKEFLAGS`, `CMAKE_BUILD_PARALLEL_LEVEL` and a `NIX_CONFIG` with `cores`/`max-jobs` to match its grant, so it actually uses the cores it got and no more.
25+a granted build gets `CARGO_BUILD_JOBS`, `MAKEFLAGS`, `CMAKE_BUILD_PARALLEL_LEVEL` and a `NIX_CONFIG` with `cores`/`max-jobs` to match its grant.
26+
27+on linux, `rabt` creates a transient scope through the systemd user manager's D-Bus `StartTransientUnit` method. the command waits until its scope is active, then runs with its original environment and terminal I/O. `CPUQuotaPerSecUSec` caps CPU time at the granted core count, and `MemoryMax` caps the whole build at its granted RAM multiplied by `overrun.ram_slack`, with at least the granted RAM available. these limits cover descendants too, even when a tool ignores the job-count environment variables. linux requires a running systemd user manager and user bus, a failed scope refuses the build rather than running it without limits. other platforms retain the job-count environment variables without cgroup enforcement.
2628
2729 the grant can be bigger than the ask. idle cores go to whoever's building, minus what other sessions have said they'll want via `rabt expect`. spare cores never hold up admission, oversubscribing cpu only slows things down, but each one is charged ram at the build's own ram per core since more jobs eat more memory. a session's build counts as an `expect` too, so agents that forget still leave a mark. `--exact` opts out.
2830
@@ -99,7 +101,7 @@ elastic = true # hand idle cores to whoever's building
99101 intent_ttl = "20m" # how long a `rabt expect` or a build keeps its claim
100102
101103 [overrun]
102-ram = "warn" # or "throttle", systemd-run MemoryHigh on linux
104+ram = "warn" # or "throttle", MemoryHigh at granted RAM on linux
103105 ram_slack = 1.25
104106 time_grace = 1.5 # overdue at this multiple of --time
105107 remind_every = "5m"
Modified file: nix/package.nix+1 additions
@@ -10,6 +10,7 @@ rustPlatform.buildRustPackage {
1010 ../Cargo.toml
1111 ../Cargo.lock
1212 ../src
13+ ../tests/systemd.rs
1314 ];
1415 };
1516
Modified file: src/main.rs+6 additions
@@ -11,11 +11,17 @@ mod run;
1111 mod sched;
1212 mod store;
1313 mod sys;
14+#[cfg(target_os = "linux")]
15+mod systemd;
1416 mod units;
1517
1618 use pound::Parse;
1719
1820 fn main() {
21+ #[cfg(target_os = "linux")]
22+ if let Some(code) = systemd::exec() {
23+ std::process::exit(code);
24+ }
1925 // a typo must not exit 2, that's a blocking pretooluse verdict and would
2026 // wall off every bash call in every session
2127 let cfg = config::Config::load().unwrap_or_else(|e| {
Modified file: src/run.rs+33 additions−36 deletions
@@ -13,7 +13,7 @@ use std::{
1313
1414 use crate::{
1515 artifacts, client,
16- config::{Config, OverCap, RamOverrun},
16+ config::{Config, OverCap},
1717 history, paths,
1818 proto::{self, Acquire, Event, Reader, Request},
1919 sched::Need,
@@ -39,8 +39,12 @@ pub struct Args {
3939 }
4040
4141 static PGID: AtomicI32 = AtomicI32::new(0);
42+#[cfg(target_os = "linux")]
43+static FORWARDED: AtomicI32 = AtomicI32::new(0);
4244
4345 extern "C" fn forward(sig: libc::c_int) {
46+ #[cfg(target_os = "linux")]
47+ FORWARDED.store(sig, Ordering::Relaxed);
4448 let pgid = PGID.load(Ordering::Relaxed);
4549 if pgid > 1 {
4650 unsafe { libc::kill(-pgid, sig) };
@@ -149,34 +153,10 @@ fn acquire(req: &Acquire, max_wait: Option<Dur>) -> Result<Grant, i32> {
149153 Err(1)
150154 }
151155
152-fn has_systemd_run() -> bool {
153- env::var_os("PATH")
154- .is_some_and(|p| env::split_paths(&p).any(|d| d.join("systemd-run").exists()))
155-}
156-
157-fn command(cfg: &Config, a: &Args, need: &Need, grant: &Grant) -> Command {
158- let mut argv = a.cmd.clone();
159- if cfg.overrun.ram == RamOverrun::Throttle && cfg!(target_os = "linux") && has_systemd_run() {
160- let high = Bytes((need.ram.0 as f64 * cfg.overrun.ram_slack) as u64);
161- let mut wrapped: Vec<String> = [
162- "systemd-run",
163- "--user",
164- "--scope",
165- "--quiet",
166- "--collect",
167- "-p",
168- ]
169- .map(Into::into)
170- .into();
171- wrapped.push(format!("MemoryHigh={}", high.0));
172- wrapped.push("--".into());
173- wrapped.append(&mut argv);
174- argv = wrapped;
175- }
176-
156+fn command(cfg: &Config, a: &Args, grant: &Grant) -> Command {
177157 let n = grant.cores.to_string();
178- let mut cmd = Command::new(&argv[0]);
179- cmd.args(&argv[1..])
158+ let mut cmd = Command::new(&a.cmd[0]);
159+ cmd.args(&a.cmd[1..])
180160 .process_group(0)
181161 .env("ROUNDABOUT_LEASE", grant.lease.to_string())
182162 .env("CARGO_BUILD_JOBS", &n)
@@ -196,13 +176,6 @@ fn command(cfg: &Config, a: &Args, need: &Need, grant: &Grant) -> Command {
196176 cmd.env("NIX_CONFIG", nix);
197177 }
198178
199- #[cfg(target_os = "linux")]
200- unsafe {
201- cmd.pre_exec(|| {
202- libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM);
203- Ok(())
204- });
205- }
206179 cmd
207180 }
208181
@@ -278,7 +251,20 @@ pub fn run(cfg: &Config, a: &Args) -> i32 {
278251 };
279252 need.ram = need.ram.max(grant.ram);
280253
281- let mut child = match command(cfg, a, &need, &grant).spawn() {
254+ let command = command(cfg, a, &grant);
255+ #[cfg(target_os = "linux")]
256+ let (mut child, scope) =
257+ match crate::systemd::spawn(&command, grant.lease, grant.cores, grant.ram, &cfg.overrun) {
258+ Ok(spawned) => spawned,
259+ Err(error) => {
260+ say(&format!("couldn't start {}: {error}", a.cmd[0]));
261+ return 127;
262+ }
263+ };
264+ #[cfg(not(target_os = "linux"))]
265+ let mut command = command;
266+ #[cfg(not(target_os = "linux"))]
267+ let mut child = match command.spawn() {
282268 Ok(c) => c,
283269 Err(e) => {
284270 say(&format!("couldn't start {}: {e}", a.cmd[0]));
@@ -319,6 +305,10 @@ pub fn run(cfg: &Config, a: &Args) -> i32 {
319305 let mut warned_time = false;
320306
321307 let status = loop {
308+ #[cfg(target_os = "linux")]
309+ if let signal @ 1.. = FORWARDED.swap(0, Ordering::Relaxed) {
310+ let _ = scope.signal(signal);
311+ }
322312 match child.try_wait() {
323313 Ok(Some(status)) => break status,
324314 Ok(None) => {}
@@ -339,9 +329,13 @@ pub fn run(cfg: &Config, a: &Args) -> i32 {
339329 term_sent = Some(Instant::now());
340330 }
341331 if term_sent.is_some() {
332+ #[cfg(target_os = "linux")]
333+ let _ = scope.signal(libc::SIGTERM);
342334 unsafe { libc::kill(-pgid, libc::SIGTERM) };
343335 }
344336 } else if term_sent.is_some_and(|t| t.elapsed() > Duration::from_secs(10)) {
337+ #[cfg(target_os = "linux")]
338+ let _ = scope.signal(libc::SIGKILL);
345339 unsafe { libc::kill(-pgid, libc::SIGKILL) };
346340 }
347341
@@ -368,6 +362,9 @@ pub fn run(cfg: &Config, a: &Args) -> i32 {
368362 thread::sleep(Duration::from_millis(250));
369363 };
370364
365+ PGID.store(0, Ordering::Relaxed);
366+ #[cfg(target_os = "linux")]
367+ drop(scope);
371368 let wall = started.elapsed();
372369 if let Some(s) = &mut out {
373370 let _ = proto::send(s, &Request::Done);
Added file: src/systemd.rs+302 additions
@@ -0,0 +1,302 @@
1+use std::{
2+ env, io,
3+ os::fd::{AsRawFd, FromRawFd, OwnedFd},
4+ os::unix::process::CommandExt,
5+ process::{Child, Command},
6+ time::Duration,
7+};
8+
9+use futures_lite::StreamExt;
10+use zbus::{
11+ blocking::{Connection, Proxy},
12+ proxy::SignalStream,
13+ zvariant::{OwnedObjectPath, Value},
14+};
15+
16+use crate::{
17+ config::{Overrun, RamOverrun},
18+ units::Bytes,
19+};
20+
21+const EXEC: &str = "__roundabout_exec";
22+const MANAGER: &str = "org.freedesktop.systemd1.Manager";
23+const BUS_NAME: &str = "org.freedesktop.systemd1";
24+const BUS_PATH: &str = "/org/freedesktop/systemd1";
25+
26+pub struct Scope {
27+ connection: Connection,
28+ name: String,
29+}
30+
31+fn properties(
32+ pid: u32,
33+ cores: u32,
34+ ram: Bytes,
35+ overrun: &Overrun,
36+) -> zbus::Result<Vec<(&'static str, Value<'static>)>> {
37+ let max = ram.0 as f64 * overrun.ram_slack.max(1.0);
38+ if !overrun.ram_slack.is_finite() || max >= u64::MAX as f64 {
39+ return Err(zbus::Error::Failure("RAM limit must be finite".into()));
40+ }
41+ let mut properties = vec![
42+ ("Description", Value::from("roundabout build")),
43+ ("PIDs", Value::from(vec![pid])),
44+ ("CollectMode", Value::from("inactive-or-failed")),
45+ ("CPUAccounting", Value::from(true)),
46+ ("MemoryAccounting", Value::from(true)),
47+ (
48+ "CPUQuotaPerSecUSec",
49+ Value::from(u64::from(cores.max(1)) * 1_000_000),
50+ ),
51+ ("MemoryMax", Value::from((max as u64).max(1))),
52+ ("OOMPolicy", Value::from("kill")),
53+ ("TimeoutStopUSec", Value::from(10_000_000_u64)),
54+ ];
55+ if overrun.ram == RamOverrun::Throttle {
56+ properties.push(("MemoryHigh", Value::from(ram.0.max(1))));
57+ }
58+ Ok(properties)
59+}
60+
61+fn wait_job(mut signals: SignalStream<'_>, job: &OwnedObjectPath) -> zbus::Result<()> {
62+ async_io::block_on(futures_lite::future::race(
63+ async {
64+ while let Some(signal) = signals.next().await {
65+ let (_, path, _, result): (u32, OwnedObjectPath, String, String) =
66+ signal.body().deserialize()?;
67+ if path == *job {
68+ return if result == "done" {
69+ Ok(())
70+ } else {
71+ Err(zbus::Error::Failure(format!("systemd job {result}")))
72+ };
73+ }
74+ }
75+ Err(zbus::Error::Failure(
76+ "systemd disconnected before the job finished".into(),
77+ ))
78+ },
79+ async {
80+ async_io::Timer::after(Duration::from_secs(15)).await;
81+ Err(zbus::Error::Failure("systemd job timed out".into()))
82+ },
83+ ))
84+}
85+
86+impl Scope {
87+ fn manager(&self) -> zbus::Result<Proxy<'_>> {
88+ Proxy::new(&self.connection, BUS_NAME, BUS_PATH, MANAGER)
89+ }
90+
91+ fn start(
92+ connection: Connection,
93+ pid: u32,
94+ lease: u64,
95+ cores: u32,
96+ ram: Bytes,
97+ overrun: &Overrun,
98+ ) -> zbus::Result<Self> {
99+ let scope = Self {
100+ connection,
101+ name: format!("roundabout-{}-{lease}-{pid}.scope", std::process::id()),
102+ };
103+ let manager = scope.manager()?;
104+ let signals = async_io::block_on(
105+ manager
106+ .inner()
107+ .receive_signal_with_args("JobRemoved", &[(2, &scope.name)]),
108+ )?;
109+ let _: () = manager.call("Subscribe", &())?;
110+ let auxiliary: Vec<(&str, Vec<(&str, Value<'_>)>)> = Vec::new();
111+ let job: OwnedObjectPath = manager.call(
112+ "StartTransientUnit",
113+ &(
114+ &scope.name,
115+ "fail",
116+ properties(pid, cores, ram, overrun)?,
117+ auxiliary,
118+ ),
119+ )?;
120+ wait_job(signals, &job)?;
121+ drop(manager);
122+ Ok(scope)
123+ }
124+
125+ pub fn signal(&self, signal: i32) -> zbus::Result<()> {
126+ self.manager()?
127+ .call("KillUnit", &(&self.name, "all", signal))
128+ }
129+
130+ fn stop(&self) -> zbus::Result<()> {
131+ let manager = self.manager()?;
132+ let signals = async_io::block_on(
133+ manager
134+ .inner()
135+ .receive_signal_with_args("JobRemoved", &[(2, &self.name)]),
136+ )?;
137+ let job: OwnedObjectPath = manager.call("StopUnit", &(&self.name, "replace"))?;
138+ wait_job(signals, &job)
139+ }
140+}
141+
142+impl Drop for Scope {
143+ fn drop(&mut self) {
144+ let _ = self.stop();
145+ }
146+}
147+
148+fn gate() -> io::Result<(OwnedFd, OwnedFd)> {
149+ let mut descriptors = [-1; 2];
150+ if unsafe { libc::pipe2(descriptors.as_mut_ptr(), libc::O_CLOEXEC) } != 0 {
151+ return Err(io::Error::last_os_error());
152+ }
153+ Ok(unsafe {
154+ (
155+ OwnedFd::from_raw_fd(descriptors[0]),
156+ OwnedFd::from_raw_fd(descriptors[1]),
157+ )
158+ })
159+}
160+
161+pub fn spawn(
162+ command: &Command,
163+ lease: u64,
164+ cores: u32,
165+ ram: Bytes,
166+ overrun: &Overrun,
167+) -> Result<(Child, Scope), String> {
168+ let connection =
169+ Connection::session().map_err(|error| format!("can't connect to the user bus, {error}"))?;
170+ let (read, write) = gate().map_err(|error| error.to_string())?;
171+ let descriptor = read.as_raw_fd();
172+ let mut runner = Command::new(env::current_exe().map_err(|error| error.to_string())?);
173+ runner
174+ .arg(EXEC)
175+ .arg(descriptor.to_string())
176+ .arg(command.get_program())
177+ .args(command.get_args())
178+ .process_group(0);
179+ for (name, value) in command.get_envs() {
180+ if let Some(value) = value {
181+ runner.env(name, value);
182+ } else {
183+ runner.env_remove(name);
184+ }
185+ }
186+ let parent = unsafe { libc::getpid() };
187+ unsafe {
188+ runner.pre_exec(move || {
189+ if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) != 0
190+ || libc::fcntl(descriptor, libc::F_SETFD, 0) != 0
191+ {
192+ return Err(io::Error::last_os_error());
193+ }
194+ if libc::getppid() != parent {
195+ return Err(io::Error::other("parent exited before launch"));
196+ }
197+ Ok(())
198+ });
199+ }
200+ let mut child = runner.spawn().map_err(|error| error.to_string())?;
201+ drop(read);
202+ let result = Scope::start(connection, child.id(), lease, cores, ram, overrun)
203+ .map_err(|error| format!("can't start the transient scope, {error}"))
204+ .and_then(|scope| {
205+ if unsafe { libc::write(write.as_raw_fd(), b"1".as_ptr().cast(), 1) } != 1 {
206+ return Err(io::Error::last_os_error().to_string());
207+ }
208+ Ok(scope)
209+ });
210+ drop(write);
211+ match result {
212+ Ok(scope) => Ok((child, scope)),
213+ Err(error) => {
214+ let _ = child.kill();
215+ let _ = child.wait();
216+ Err(error)
217+ }
218+ }
219+}
220+
221+pub fn exec() -> Option<i32> {
222+ let mut arguments = env::args_os().skip(1);
223+ if arguments.next()?.as_encoded_bytes() != EXEC.as_bytes() {
224+ return None;
225+ }
226+ let Some(descriptor) = arguments
227+ .next()
228+ .and_then(|arg| arg.to_str()?.parse::<i32>().ok())
229+ .filter(|fd| *fd >= 3)
230+ else {
231+ return Some(1);
232+ };
233+ let descriptor = unsafe { OwnedFd::from_raw_fd(descriptor) };
234+ let mut byte = [0_u8];
235+ loop {
236+ let count = unsafe { libc::read(descriptor.as_raw_fd(), byte.as_mut_ptr().cast(), 1) };
237+ if count == 1 && byte[0] == b'1' {
238+ break;
239+ }
240+ if count != -1 || io::Error::last_os_error().kind() != io::ErrorKind::Interrupted {
241+ return Some(1);
242+ }
243+ }
244+ drop(descriptor);
245+ let Some(program) = arguments.next() else {
246+ return Some(1);
247+ };
248+ let error = Command::new(&program).args(arguments).exec();
249+ eprintln!(
250+ "rabt: couldn't start {}: {error}",
251+ program.to_string_lossy()
252+ );
253+ Some(127)
254+}
255+
256+#[cfg(test)]
257+mod tests {
258+ use super::*;
259+
260+ #[test]
261+ fn limits_follow_the_grant_with_memory_headroom() {
262+ let overrun = Overrun::default();
263+ let properties = properties(123, 8, Bytes::gib(4), &overrun).unwrap();
264+ assert!(properties.contains(&("PIDs", Value::from(vec![123_u32]))));
265+ assert!(properties.contains(&("CPUQuotaPerSecUSec", Value::from(8_000_000_u64))));
266+ assert!(properties.contains(&("MemoryMax", Value::from(Bytes::gib(5).0))));
267+ assert!(!properties.iter().any(|(name, _)| *name == "MemoryHigh"));
268+ }
269+
270+ #[test]
271+ fn throttle_starts_at_the_granted_memory() {
272+ let overrun = Overrun {
273+ ram: RamOverrun::Throttle,
274+ ..Overrun::default()
275+ };
276+ let properties = properties(123, 2, Bytes::gib(2), &overrun).unwrap();
277+ assert!(properties.contains(&("MemoryHigh", Value::from(Bytes::gib(2).0))));
278+ assert!(properties.contains(&("MemoryMax", Value::from(Bytes::mib(2560).0))));
279+ }
280+
281+ #[test]
282+ fn small_grants_never_become_unlimited() {
283+ let overrun = Overrun {
284+ ram_slack: 0.5,
285+ ..Overrun::default()
286+ };
287+ let properties = properties(123, 0, Bytes::ZERO, &overrun).unwrap();
288+ assert!(properties.contains(&("CPUQuotaPerSecUSec", Value::from(1_000_000_u64))));
289+ assert!(properties.contains(&("MemoryMax", Value::from(1_u64))));
290+ }
291+
292+ #[test]
293+ fn nonfinite_limits_are_rejected() {
294+ for ram_slack in [f64::INFINITY, f64::NEG_INFINITY, f64::NAN, f64::MAX] {
295+ let overrun = Overrun {
296+ ram_slack,
297+ ..Overrun::default()
298+ };
299+ assert!(properties(123, 1, Bytes::gib(2), &overrun).is_err());
300+ }
301+ }
302+}
Added file: tests/systemd.rs+147 additions
@@ -0,0 +1,147 @@
1+#![cfg(target_os = "linux")]
2+
3+use std::{
4+ fs,
5+ io::Write,
6+ os::fd::{AsRawFd, FromRawFd, OwnedFd},
7+ os::unix::process::CommandExt,
8+ process::{Child, Command, Stdio},
9+ time::Duration,
10+};
11+
12+fn gated_command(program: &str, arguments: &[&str]) -> (Child, OwnedFd) {
13+ let mut descriptors = [-1; 2];
14+ assert_eq!(
15+ unsafe { libc::pipe2(descriptors.as_mut_ptr(), libc::O_CLOEXEC) },
16+ 0
17+ );
18+ let read = unsafe { OwnedFd::from_raw_fd(descriptors[0]) };
19+ let write = unsafe { OwnedFd::from_raw_fd(descriptors[1]) };
20+ let descriptor = read.as_raw_fd();
21+ let mut command = Command::new(env!("CARGO_BIN_EXE_rabt"));
22+ command
23+ .args(["__roundabout_exec", &descriptor.to_string(), program])
24+ .args(arguments)
25+ .stdout(Stdio::piped())
26+ .stderr(Stdio::piped());
27+ unsafe {
28+ command.pre_exec(move || {
29+ if libc::fcntl(descriptor, libc::F_SETFD, 0) == -1 {
30+ return Err(std::io::Error::last_os_error());
31+ }
32+ Ok(())
33+ });
34+ }
35+ let child = command.spawn().unwrap();
36+ drop(read);
37+ (child, write)
38+}
39+
40+#[test]
41+fn command_waits_for_scope_setup() {
42+ let (mut child, gate) = gated_command(env!("CARGO_BIN_EXE_rabt"), &["--version"]);
43+ let mut output = libc::pollfd {
44+ fd: child.stdout.as_ref().unwrap().as_raw_fd(),
45+ events: libc::POLLIN,
46+ revents: 0,
47+ };
48+ assert_eq!(unsafe { libc::poll(&raw mut output, 1, 100) }, 0);
49+ assert!(child.try_wait().unwrap().is_none());
50+ fs::File::from(gate).write_all(b"1").unwrap();
51+ let output = child.wait_with_output().unwrap();
52+ assert!(output.status.success());
53+ assert!(
54+ String::from_utf8(output.stdout)
55+ .unwrap()
56+ .starts_with("rabt ")
57+ );
58+}
59+
60+#[test]
61+fn failed_scope_setup_never_executes_the_command() {
62+ let (child, gate) = gated_command(env!("CARGO_BIN_EXE_rabt"), &["--version"]);
63+ drop(gate);
64+ let output = child.wait_with_output().unwrap();
65+ assert_eq!(output.status.code(), Some(1));
66+ assert!(output.stdout.is_empty());
67+}
68+
69+#[test]
70+fn missing_commands_keep_exit_127() {
71+ let (child, gate) = gated_command("/roundabout-missing-command", &[]);
72+ fs::File::from(gate).write_all(b"1").unwrap();
73+ assert_eq!(child.wait_with_output().unwrap().status.code(), Some(127));
74+}
75+
76+#[test]
77+#[ignore = "requires a systemd user manager and cgroup v2"]
78+fn transient_scope_enforces_the_grant() {
79+ if std::env::var_os("ROUNDABOUT_SCOPE_PROBE").is_some() {
80+ let membership = fs::read_to_string("/proc/self/cgroup").unwrap();
81+ let path = membership
82+ .lines()
83+ .find_map(|line| line.strip_prefix("0::"))
84+ .unwrap();
85+ assert!(path.contains("roundabout-"));
86+ let cgroup = std::path::Path::new("/sys/fs/cgroup").join(path.trim_start_matches('/'));
87+ let cpu = fs::read_to_string(cgroup.join("cpu.max")).unwrap();
88+ let limits: Vec<_> = cpu.split_whitespace().collect();
89+ assert_eq!(limits[0], limits[1]);
90+ assert_eq!(
91+ fs::read_to_string(cgroup.join("memory.max"))
92+ .unwrap()
93+ .trim(),
94+ (80 * 1024 * 1024).to_string()
95+ );
96+ assert_eq!(
97+ fs::read_to_string(cgroup.join("memory.high"))
98+ .unwrap()
99+ .trim(),
100+ (64 * 1024 * 1024).to_string()
101+ );
102+ assert_eq!(
103+ fs::read_to_string(cgroup.join("memory.oom.group"))
104+ .unwrap()
105+ .trim(),
106+ "1"
107+ );
108+ assert_eq!(std::env::var("CARGO_BUILD_JOBS").unwrap(), "1");
109+ assert!(std::env::var("ROUNDABOUT_LEASE").is_ok());
110+ return;
111+ }
112+ let directory =
113+ std::env::temp_dir().join(format!("roundabout-scope-test-{}", std::process::id()));
114+ fs::create_dir_all(&directory).unwrap();
115+ let config = directory.join("config.toml");
116+ fs::write(
117+ &config,
118+ "[overrun]\nram = \"throttle\"\n[queue]\nlinger = \"0s\"\n",
119+ )
120+ .unwrap();
121+ let output = Command::new(env!("CARGO_BIN_EXE_rabt"))
122+ .args([
123+ "run", "--ram", "64M", "--disk", "0", "--cores", "1", "--time", "5s", "--exact", "--",
124+ ])
125+ .arg(std::env::current_exe().unwrap())
126+ .args([
127+ "--exact",
128+ "transient_scope_enforces_the_grant",
129+ "--ignored",
130+ "--nocapture",
131+ ])
132+ .current_dir(&directory)
133+ .env("ROUNDABOUT_CONFIG", &config)
134+ .env("ROUNDABOUT_RUNTIME_DIR", directory.join("runtime"))
135+ .env("ROUNDABOUT_STATE_DIR", directory.join("state"))
136+ .env("ROUNDABOUT_SCOPE_PROBE", "1")
137+ .output()
138+ .unwrap();
139+ std::thread::sleep(Duration::from_secs(2));
140+ fs::remove_dir_all(directory).unwrap();
141+ assert!(
142+ output.status.success(),
143+ "{}\n{}",
144+ String::from_utf8_lossy(&output.stdout),
145+ String::from_utf8_lossy(&output.stderr)
146+ );
147+}