1
+ use std:: collections:: HashMap ;
1
2
use std:: ffi:: OsStr ;
2
3
use std:: os:: unix:: process:: CommandExt ;
3
4
use std:: path:: Path ;
4
5
use std:: process:: { Child , Command , Stdio } ;
5
6
use std:: sync:: atomic:: { AtomicBool , Ordering } ;
6
- use std:: sync:: RwLock ;
7
+ use std:: sync:: { OnceLock , RwLock } ;
7
8
use std:: { io, thread} ;
8
9
10
+ use anyhow:: Context ;
9
11
use atomic:: Atomic ;
10
12
use libc:: { getrlimit, rlim_t, rlimit, setrlimit, RLIMIT_NOFILE } ;
11
13
use niri_config:: Environment ;
14
+ use smithay:: reexports:: wayland_server:: { DisplayHandle , Resource } ;
15
+ use smithay:: wayland:: compositor;
16
+ use smithay:: wayland:: shell:: xdg:: { ToplevelSurface , XdgToplevelSurfaceData } ;
17
+ use zbus:: zvariant:: Value ;
12
18
13
19
use crate :: utils:: expand_home;
14
20
@@ -173,9 +179,12 @@ use systemd::do_spawn;
173
179
mod systemd {
174
180
use std:: os:: fd:: { AsFd , AsRawFd , FromRawFd , OwnedFd } ;
175
181
182
+ use serde:: { Deserialize , Serialize } ;
176
183
use smithay:: reexports:: rustix;
177
184
use smithay:: reexports:: rustix:: io:: { close, read, retry_on_intr, write} ;
178
185
use smithay:: reexports:: rustix:: pipe:: { pipe_with, PipeFlags } ;
186
+ use zbus:: dbus_proxy;
187
+ use zbus:: zvariant:: { OwnedObjectPath , OwnedValue , Type , Value } ;
179
188
180
189
use super :: * ;
181
190
@@ -382,6 +391,20 @@ mod systemd {
382
391
383
392
let _ = write ! ( scope_name, "-{child_pid}.scope" ) ;
384
393
394
+ let mut slice_name = format ! ( "app-niri-" ) ;
395
+
396
+ // Escape for systemd similarly to libgnome-desktop, which says it had adapted this from
397
+ // systemd source.
398
+ for & c in name. as_bytes ( ) {
399
+ if c. is_ascii_alphanumeric ( ) || matches ! ( c, b':' | b'_' | b'.' ) {
400
+ slice_name. push ( char:: from ( c) ) ;
401
+ } else {
402
+ let _ = write ! ( slice_name, "\\ x{c:02x}" ) ;
403
+ }
404
+ }
405
+
406
+ let _ = write ! ( slice_name, ".slice" ) ;
407
+
385
408
// Ask systemd to start a transient scope.
386
409
static CONNECTION : OnceLock < zbus:: Result < zbus:: blocking:: Connection > > = OnceLock :: new ( ) ;
387
410
let conn = CONNECTION
@@ -405,6 +428,7 @@ mod systemd {
405
428
let properties: & [ _ ] = & [
406
429
( "PIDs" , Value :: new ( pids) ) ,
407
430
( "CollectMode" , Value :: new ( "inactive-or-failed" ) ) ,
431
+ ( "Slice" , Value :: new ( & slice_name) ) ,
408
432
] ;
409
433
let aux: & [ ( & str , & [ ( & str , Value ) ] ) ] = & [ ] ;
410
434
@@ -425,4 +449,232 @@ mod systemd {
425
449
426
450
Ok ( ( ) )
427
451
}
452
+
453
+ #[ dbus_proxy(
454
+ interface = "org.freedesktop.systemd1.Manager" ,
455
+ default_service = "org.freedesktop.systemd1" ,
456
+ default_path = "/org/freedesktop/systemd1"
457
+ ) ]
458
+ trait Manager {
459
+ #[ dbus_proxy( name = "GetUnitByPID" ) ]
460
+ fn get_unit_by_pid ( & self , pid : u32 ) -> zbus:: Result < OwnedObjectPath > ;
461
+
462
+ #[ dbus_proxy( name = "StartTransientUnit" ) ]
463
+ fn start_transient_unit (
464
+ & self ,
465
+ name : & str ,
466
+ mode : & str ,
467
+ properties : & [ ( & str , Value < ' _ > ) ] ,
468
+ aux : & [ ( & str , & [ ( & str , Value < ' _ > ) ] ) ] ,
469
+ ) -> zbus:: Result < OwnedObjectPath > ;
470
+
471
+ #[ dbus_proxy( signal) ]
472
+ fn job_removed (
473
+ & self ,
474
+ id : u32 ,
475
+ job : zbus:: zvariant:: ObjectPath < ' _ > ,
476
+ unit : & str ,
477
+ result : & str ,
478
+ ) -> zbus:: Result < ( ) > ;
479
+ }
480
+
481
+ /// A process spawned by systemd for a unit.
482
+ #[ derive( Debug , PartialEq , Eq , Clone , Type , Serialize , Deserialize , Value , OwnedValue ) ]
483
+ pub struct Process {
484
+ /// The cgroup controller of the process.
485
+ pub cgroup_controller : String ,
486
+
487
+ /// The PID of the process.
488
+ pub pid : u32 ,
489
+
490
+ /// The command line of the process.
491
+ pub command_line : String ,
492
+ }
493
+
494
+ #[ dbus_proxy(
495
+ interface = "org.freedesktop.systemd1.Scope" ,
496
+ default_service = "org.freedesktop.systemd1"
497
+ ) ]
498
+ trait Scope {
499
+ #[ dbus_proxy( property) ]
500
+ fn control_group ( & self ) -> zbus:: Result < String > ;
501
+
502
+ fn get_processes ( & self ) -> zbus:: Result < Vec < Process > > ;
503
+ }
504
+
505
+ #[ dbus_proxy(
506
+ interface = "org.freedesktop.systemd1.Unit" ,
507
+ default_service = "org.freedesktop.systemd1" ,
508
+ default_path = "/org/freedesktop/systemd1/unit"
509
+ ) ]
510
+ trait Unit {
511
+ fn freeze ( & self ) -> zbus:: Result < ( ) > ;
512
+ fn thaw ( & self ) -> zbus:: Result < ( ) > ;
513
+ }
514
+ }
515
+
516
+ pub fn test_scope ( toplevel : & ToplevelSurface , dh : & DisplayHandle ) -> anyhow:: Result < ( ) > {
517
+ static CONNECTION : OnceLock < zbus:: Result < zbus:: blocking:: Connection > > = OnceLock :: new ( ) ;
518
+ let conn = CONNECTION
519
+ . get_or_init ( zbus:: blocking:: Connection :: session)
520
+ . clone ( )
521
+ . context ( "error connecting to session bus" ) ?;
522
+
523
+ let manager = systemd:: ManagerProxyBlocking :: new ( & conn) . context ( "error creating a Proxy" ) ?;
524
+
525
+ let wl_surface = toplevel. wl_surface ( ) ;
526
+ let Some ( client) = wl_surface. client ( ) else {
527
+ return Ok ( ( ) ) ;
528
+ } ;
529
+
530
+ let credentials = client. get_credentials ( dh) ?;
531
+ let pid = credentials. pid as u32 ;
532
+
533
+ let Some ( app_id) = compositor:: with_states ( & wl_surface, |states| {
534
+ states
535
+ . data_map
536
+ . get :: < XdgToplevelSurfaceData > ( )
537
+ . and_then ( |surface_data| surface_data. lock ( ) . unwrap ( ) . app_id . clone ( ) )
538
+ } ) else {
539
+ return Ok ( ( ) ) ;
540
+ } ;
541
+
542
+ let unit_path = manager. get_unit_by_pid ( pid) ?;
543
+
544
+ use std:: fmt:: Write ;
545
+ let mut expected_scope_name = format ! ( "app-niri-" ) ;
546
+
547
+ // Escape for systemd similarly to libgnome-desktop, which says it had adapted this from
548
+ // systemd source.
549
+ for & c in app_id. as_bytes ( ) {
550
+ if c. is_ascii_alphanumeric ( ) || matches ! ( c, b':' | b'_' | b'.' ) {
551
+ expected_scope_name. push ( char:: from ( c) ) ;
552
+ } else {
553
+ let _ = write ! ( expected_scope_name, "\\ x{c:02x}" ) ;
554
+ }
555
+ }
556
+
557
+ let _ = write ! ( expected_scope_name, "-{pid}.scope" ) ;
558
+
559
+ let scope = systemd:: ScopeProxyBlocking :: builder ( & conn)
560
+ . path ( & unit_path) ?
561
+ . build ( )
562
+ . with_context ( || format ! ( "failed to get scope for: {unit_path:?}" ) ) ?;
563
+ let control_group = scope. control_group ( ) ?;
564
+
565
+ let existing_scope_name = control_group. split_terminator ( '/' ) . last ( ) . unwrap ( ) ;
566
+
567
+ if existing_scope_name. eq_ignore_ascii_case ( & expected_scope_name)
568
+ || !existing_scope_name. starts_with ( "app-niri-" )
569
+ {
570
+ return Ok ( ( ) ) ;
571
+ }
572
+
573
+ let unit = systemd:: UnitProxyBlocking :: builder ( & conn)
574
+ . path ( & unit_path) ?
575
+ . build ( ) ?;
576
+
577
+ let frozen = match unit. freeze ( ) {
578
+ Ok ( _) => true ,
579
+ Err ( err) => {
580
+ tracing:: warn!( ?unit_path, ?err, "failed to freeze unit" ) ;
581
+ false
582
+ }
583
+ } ;
584
+
585
+ let apply = || {
586
+ let processes = scope. get_processes ( ) ?;
587
+
588
+ let mut pids = processes
589
+ . iter ( )
590
+ . map ( |process| process. pid )
591
+ . collect :: < Vec < _ > > ( ) ;
592
+
593
+ let mut ppid_map: HashMap < u32 , u32 > = HashMap :: new ( ) ;
594
+
595
+ let mut i = 0 ;
596
+ while i < pids. len ( ) {
597
+ // self check
598
+ if pids[ i] == pid {
599
+ i += 1 ;
600
+ continue ;
601
+ }
602
+
603
+ let pid: u32 = pids[ i] ;
604
+
605
+ let stat = std:: fs:: read_to_string ( format ! ( "/proc/{}/stat" , pid) )
606
+ . context ( "failed to parse stat" ) ?;
607
+ let ppid_start = stat. rfind ( ')' ) . unwrap_or_default ( ) + 4 ;
608
+ let ppid_end = ppid_start + stat[ ppid_start..] . find ( ' ' ) . unwrap_or ( 0 ) ;
609
+ let ppid = & stat[ ppid_start..ppid_end] ;
610
+ let ppid = ppid
611
+ . parse :: < u32 > ( )
612
+ . with_context ( || format ! ( "failed to parse ppid from stat: {stat}" ) ) ?;
613
+
614
+ if !pids. contains ( & ppid) {
615
+ pids. remove ( i) ;
616
+ } else {
617
+ ppid_map. insert ( pid, ppid) ;
618
+ i += 1 ;
619
+ }
620
+ }
621
+
622
+ let mut i = 0 ;
623
+ while i < pids. len ( ) {
624
+ // self check
625
+ if pids[ i] == pid {
626
+ i += 1 ;
627
+ continue ;
628
+ }
629
+
630
+ let mut root_pid = pids[ i] ;
631
+ while let Some ( & ppid) = ppid_map. get ( & root_pid) {
632
+ root_pid = ppid;
633
+ }
634
+
635
+ if root_pid != pid {
636
+ pids. remove ( i) ;
637
+ } else {
638
+ i += 1 ;
639
+ }
640
+ }
641
+
642
+ let mut slice_name = format ! ( "app-niri-" ) ;
643
+
644
+ // Escape for systemd similarly to libgnome-desktop, which says it had adapted this from
645
+ // systemd source.
646
+ for & c in app_id. as_bytes ( ) {
647
+ if c. is_ascii_alphanumeric ( ) || matches ! ( c, b':' | b'_' | b'.' ) {
648
+ slice_name. push ( char:: from ( c) ) ;
649
+ } else {
650
+ let _ = write ! ( slice_name, "\\ x{c:02x}" ) ;
651
+ }
652
+ }
653
+
654
+ let _ = write ! ( slice_name, ".slice" ) ;
655
+
656
+ let properties: & [ _ ] = & [
657
+ ( "PIDs" , Value :: new ( pids) ) ,
658
+ ( "CollectMode" , Value :: new ( "inactive-or-failed" ) ) ,
659
+ ( "Slice" , Value :: new ( & slice_name) ) ,
660
+ ] ;
661
+
662
+ tracing:: info!(
663
+ ?expected_scope_name,
664
+ ?existing_scope_name,
665
+ "trying to move to different scope"
666
+ ) ;
667
+
668
+ manager. start_transient_unit ( & expected_scope_name, "fail" , properties, & [ ] ) ?;
669
+ Result :: < ( ) , anyhow:: Error > :: Ok ( ( ) )
670
+ } ;
671
+
672
+ let res = apply ( ) ;
673
+
674
+ if frozen {
675
+ let _ = unit. thaw ( ) ;
676
+ }
677
+
678
+ res?;
679
+ Ok ( ( ) )
428
680
}
0 commit comments