Tailscale -> Netbird
Start Implement Idle Daemon with Dimming Display
This commit is contained in:
@@ -4,8 +4,9 @@ use brightness::blocking::{Brightness, BrightnessDevice};
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use ddc_hi::Ddc;
|
||||
use jmb_logger_rs::*;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::error::Error;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::os::unix::prelude::MetadataExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
@@ -18,6 +19,20 @@ pub enum DisplayBackend {
|
||||
DdcCi(Box<ddc_hi::Display>),
|
||||
}
|
||||
|
||||
impl Debug for DisplayBackend {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DisplayBackend::Sysfs(dev) => {
|
||||
return dev.fmt(f);
|
||||
}
|
||||
DisplayBackend::DdcCi(displ) => {
|
||||
f.write_str(&displ.info.id).expect("Error");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_sysfs_dev(id: &str) -> Option<BrightnessDevice> {
|
||||
brightness::blocking::brightness_devices()
|
||||
.find(|dev| {
|
||||
@@ -62,26 +77,45 @@ impl DisplayBackend {
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BrightnessMessage {
|
||||
Init(Sender<BrightnessMessage>),
|
||||
Value(String, u8),
|
||||
Value(String, u8, bool),
|
||||
Revert(String),
|
||||
Request(String),
|
||||
}
|
||||
|
||||
struct DisplayData(DisplayBackend, u8);
|
||||
#[derive(Debug)]
|
||||
struct DisplayData {
|
||||
backend: DisplayBackend,
|
||||
value: u8,
|
||||
saved_value: Option<u8>,
|
||||
}
|
||||
|
||||
impl DisplayData {
|
||||
fn new(backend: DisplayBackend, value: u8) -> Self {
|
||||
Self {
|
||||
backend,
|
||||
value,
|
||||
saved_value: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn save(&mut self) -> Option<&u8> {
|
||||
self.saved_value = Some(self.value);
|
||||
self.saved_value.as_ref()
|
||||
}
|
||||
|
||||
fn update(&mut self) -> bool {
|
||||
if let Some(value) = self.0.get_brightness()
|
||||
&& self.1 != value
|
||||
if let Some(value) = self.backend.get_brightness()
|
||||
&& self.value != value
|
||||
{
|
||||
self.1 = value;
|
||||
self.value = value;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn set_brightness(&mut self, percent: u8) {
|
||||
self.0.set_brightness(percent);
|
||||
self.1 = percent;
|
||||
self.backend.set_brightness(percent);
|
||||
self.value = percent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +153,7 @@ impl BackendThread<BrightnessMessage, Sender<BrightnessMessage>> for BrightnessM
|
||||
thread::spawn(move || {
|
||||
while let Ok(tx) = registration_rx.recv() {
|
||||
if tx.send(BrightnessMessage::Init(f2b_tx.clone())).is_ok() {
|
||||
trace!(&LOGGER, "Added Brightness Subscriber: {tx:?}");
|
||||
debug!(&LOGGER, "Added Brightness Subscriber: {tx:?}");
|
||||
instance_clone
|
||||
.lock()
|
||||
.expect("Couldn't get lock")
|
||||
@@ -130,36 +164,53 @@ impl BackendThread<BrightnessMessage, Sender<BrightnessMessage>> for BrightnessM
|
||||
});
|
||||
let instance_clone = instance.clone();
|
||||
thread::spawn(move || {
|
||||
while let Ok(mut msg) = f2b_rx.recv() {
|
||||
while let Ok(newer) = f2b_rx.try_recv() {
|
||||
msg = newer;
|
||||
}
|
||||
match msg {
|
||||
BrightnessMessage::Value(id, val) => {
|
||||
instance_clone
|
||||
.lock()
|
||||
.expect("Couldn't get lock")
|
||||
.set_brightness(id, val);
|
||||
loop {
|
||||
// Ensure all Messages are handled in the correct order
|
||||
let mut operations: HashMap<String, Vec<Operation>> = HashMap::new();
|
||||
let mut requests: HashSet<String> = HashSet::new();
|
||||
if let Ok(msg) = f2b_rx.recv() {
|
||||
Self::insert_message_operation(&mut operations, &mut requests, msg);
|
||||
while let Ok(msg) = f2b_rx.try_recv() {
|
||||
Self::insert_message_operation(&mut operations, &mut requests, msg);
|
||||
}
|
||||
BrightnessMessage::Request(id) => {
|
||||
instance_clone
|
||||
.lock()
|
||||
.expect("Couldn't get lock")
|
||||
.register_display(id.clone());
|
||||
if let Some(val) = instance_clone
|
||||
.lock()
|
||||
.expect("Couldn't get lock")
|
||||
.display_cache
|
||||
.get(&id)
|
||||
.map(|v| v.1)
|
||||
{
|
||||
instance_clone
|
||||
.lock()
|
||||
.expect("Couldn't get lock")
|
||||
.publish(BrightnessMessage::Value(id, val));
|
||||
}
|
||||
|
||||
let mut manager = instance_clone.lock().unwrap();
|
||||
for (id, operations) in operations {
|
||||
let mut last = manager
|
||||
.display_cache
|
||||
.get(&id)
|
||||
.and_then(|d| Some(d.value))
|
||||
.unwrap_or(60);
|
||||
let mut revert_flag = false;
|
||||
for operation in operations {
|
||||
match operation {
|
||||
Operation::Set(val, save) => {
|
||||
if save {
|
||||
manager.display_cache.get_mut(&id).and_then(|d| d.save());
|
||||
}
|
||||
last = val;
|
||||
revert_flag = false;
|
||||
}
|
||||
Operation::Revert => {
|
||||
last = manager
|
||||
.display_cache
|
||||
.get_mut(&id)
|
||||
.and_then(|d| d.saved_value.take())
|
||||
.unwrap_or(last);
|
||||
revert_flag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
if revert_flag {
|
||||
manager.publish(BrightnessMessage::Value(id.clone(), last, false));
|
||||
}
|
||||
manager.set_brightness(id, last);
|
||||
}
|
||||
for id in requests {
|
||||
if let Some(val) = manager.get_brightness(id.clone()) {
|
||||
manager.publish(BrightnessMessage::Value(id, val, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -168,9 +219,11 @@ impl BackendThread<BrightnessMessage, Sender<BrightnessMessage>> for BrightnessM
|
||||
thread::sleep(Duration::from_millis(10_000));
|
||||
let mut messages = vec![];
|
||||
|
||||
for monitor in &mut instance.lock().expect("Couldn't get lock").display_cache {
|
||||
if monitor.1.update() {
|
||||
messages.push(BrightnessMessage::Value(monitor.0.clone(), monitor.1.1));
|
||||
for (monitor, data) in
|
||||
&mut instance.lock().expect("Couldn't get lock").display_cache
|
||||
{
|
||||
if data.update() {
|
||||
messages.push(BrightnessMessage::Value(monitor.clone(), data.value, false));
|
||||
}
|
||||
}
|
||||
for message in messages {
|
||||
@@ -181,6 +234,12 @@ impl BackendThread<BrightnessMessage, Sender<BrightnessMessage>> for BrightnessM
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Operation {
|
||||
Set(u8, bool),
|
||||
Revert,
|
||||
}
|
||||
|
||||
impl BrightnessManager {
|
||||
pub fn set_brightness(&mut self, connector: String, percent: u8) {
|
||||
if !self.display_cache.contains_key(&connector) {
|
||||
@@ -191,12 +250,12 @@ impl BrightnessManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _get_brightness(&mut self, connector: String) -> Option<u8> {
|
||||
pub fn get_brightness(&mut self, connector: String) -> Option<u8> {
|
||||
if !self.display_cache.contains_key(&connector) {
|
||||
self.register_display(connector.clone());
|
||||
}
|
||||
if let Some(display) = self.display_cache.get_mut(&connector) {
|
||||
display.0.get_brightness()
|
||||
display.backend.get_brightness()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -220,7 +279,7 @@ impl BrightnessManager {
|
||||
.to_str()
|
||||
.is_some_and(|name| name.eq(&format!("card1-{connector}")))
|
||||
{
|
||||
println!("Found sysfs display {connector}");
|
||||
debug!(&LOGGER, "Found sysfs display {connector}");
|
||||
let path = bl_path.file_name();
|
||||
let Some(name) = path.to_str() else { continue };
|
||||
let Some(device) = get_sysfs_dev(name) else {
|
||||
@@ -229,7 +288,7 @@ impl BrightnessManager {
|
||||
let mut backend = DisplayBackend::Sysfs(device);
|
||||
let value = backend.get_brightness().unwrap_or(60);
|
||||
self.display_cache
|
||||
.insert(connector.clone(), DisplayData(backend, value));
|
||||
.insert(connector.clone(), DisplayData::new(backend, value));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -239,13 +298,13 @@ impl BrightnessManager {
|
||||
&& let Ok(data) = fs::metadata(path)
|
||||
{
|
||||
let id = data.rdev();
|
||||
println!("Found DDC/CI display {connector}");
|
||||
debug!(&LOGGER, "Found DDC/CI display {connector}");
|
||||
for display in ddc_hi::Display::enumerate() {
|
||||
if id.to_string().eq(&display.info.id) {
|
||||
let mut backend = DisplayBackend::DdcCi(Box::new(display));
|
||||
let value = backend.get_brightness().unwrap_or(60);
|
||||
self.display_cache
|
||||
.insert(connector.clone(), DisplayData(backend, value));
|
||||
.insert(connector.clone(), DisplayData::new(backend, value));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -318,4 +377,35 @@ impl BrightnessManager {
|
||||
)
|
||||
.into())
|
||||
}
|
||||
|
||||
fn insert_message_operation(
|
||||
operations: &mut HashMap<String, Vec<Operation>>,
|
||||
requests: &mut HashSet<String>,
|
||||
msg: BrightnessMessage,
|
||||
) {
|
||||
match msg {
|
||||
BrightnessMessage::Value(id, val, save) => {
|
||||
if !operations.contains_key(&id) {
|
||||
operations.insert(id.clone(), Vec::new());
|
||||
}
|
||||
let ops = operations.get_mut(&id);
|
||||
if let Some(ops) = ops {
|
||||
ops.push(Operation::Set(val, save));
|
||||
}
|
||||
}
|
||||
BrightnessMessage::Revert(id) => {
|
||||
if !operations.contains_key(&id) {
|
||||
operations.insert(id.clone(), Vec::new());
|
||||
}
|
||||
let ops = operations.get_mut(&id);
|
||||
if let Some(ops) = ops {
|
||||
ops.push(Operation::Revert);
|
||||
}
|
||||
}
|
||||
BrightnessMessage::Request(id) => {
|
||||
requests.insert(id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
454
src/backend/idle.rs
Normal file
454
src/backend/idle.rs
Normal file
@@ -0,0 +1,454 @@
|
||||
use crate::backend::{BackendThread, Registrator};
|
||||
use crate::{APP_NAME, LOGGER};
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use jmb_logger_rs::*;
|
||||
use nmrs::raw::zbus::interface;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::watch;
|
||||
use tokio::time::sleep;
|
||||
use wayland_client::globals::registry_queue_init;
|
||||
use wayland_client::protocol::wl_registry::WlRegistry;
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::{Connection, Dispatch, QueueHandle, delegate_noop};
|
||||
use wayland_protocols::ext::idle_notify::v1::client::ext_idle_notification_v1::{
|
||||
Event, ExtIdleNotificationV1,
|
||||
};
|
||||
use wayland_protocols::ext::idle_notify::v1::client::ext_idle_notifier_v1::ExtIdleNotifierV1;
|
||||
use zbus;
|
||||
use zbus::conn::Builder;
|
||||
use zbus::message::Header;
|
||||
|
||||
const SERVICE: &str = "org.freedesktop.ScreenSaver";
|
||||
const OBJECT_PATH: &str = "/org/freedesktop/ScreenSaver";
|
||||
const LEGACY_OBJECT_PATH: &str = "/ScreenSaver";
|
||||
|
||||
struct WlState {
|
||||
idle: Option<bool>,
|
||||
tx: watch::Sender<IdleStateInner>,
|
||||
}
|
||||
|
||||
delegate_noop!(WlState: ignore WlSeat);
|
||||
|
||||
impl Dispatch<WlRegistry, wayland_client::globals::GlobalListContents> for WlState {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
_: &WlRegistry,
|
||||
_: wayland_client::protocol::wl_registry::Event,
|
||||
_: &wayland_client::globals::GlobalListContents,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ExtIdleNotificationV1, ()> for WlState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_: &ExtIdleNotificationV1,
|
||||
event: Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
Event::Idled => state.idle = Some(true),
|
||||
Event::Resumed => state.idle = Some(false),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(idle) = state.idle {
|
||||
state.tx.send_modify(|state| {
|
||||
state.wayland_idle = idle;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ExtIdleNotifierV1, (), WlState> for WlState {
|
||||
fn event(
|
||||
_: &mut WlState,
|
||||
_: &ExtIdleNotifierV1,
|
||||
event: <ExtIdleNotifierV1 as wayland_client::Proxy>::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<WlState>,
|
||||
) {
|
||||
match event {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Inhibitor {
|
||||
owner: String,
|
||||
application: String,
|
||||
reason: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct IdleStateInner {
|
||||
dimmed: bool,
|
||||
wayland_idle: bool,
|
||||
dbus_inhibited: bool,
|
||||
caffeine: bool,
|
||||
}
|
||||
|
||||
impl IdleStateInner {
|
||||
fn should_dim(&self) -> bool {
|
||||
self.wayland_idle && !self.dbus_inhibited
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<IdleState> for IdleStateInner {
|
||||
fn into(self) -> IdleState {
|
||||
if self.should_dim() {
|
||||
IdleState::Dimmed
|
||||
} else if self.caffeine {
|
||||
IdleState::Caffeine
|
||||
} else if self.dbus_inhibited {
|
||||
IdleState::Inhibited
|
||||
} else {
|
||||
IdleState::Active
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum IdleState {
|
||||
Init(Sender<IdleMessage>),
|
||||
Dimmed,
|
||||
Inhibited,
|
||||
Caffeine,
|
||||
Active,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum IdleMessage {
|
||||
Inhibit,
|
||||
Release,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IdleDaemon {
|
||||
subscribers: Vec<Sender<IdleState>>,
|
||||
}
|
||||
|
||||
impl BackendThread<IdleState, Sender<IdleMessage>> for IdleDaemon {
|
||||
fn subscribe(
|
||||
registrator: Registrator<IdleState>,
|
||||
) -> (Receiver<IdleState>, Sender<IdleMessage>) {
|
||||
let (tx, rx) = crossbeam_channel::unbounded::<IdleState>();
|
||||
let _ = registrator.send(tx);
|
||||
while let Ok(message) = rx.recv() {
|
||||
return match message {
|
||||
IdleState::Init(sender) => (rx, sender),
|
||||
_ => continue,
|
||||
};
|
||||
}
|
||||
panic!("Channel closed unexpectedly, without returning anything")
|
||||
}
|
||||
|
||||
fn subscribers(&self) -> &Vec<Sender<IdleState>> {
|
||||
&self.subscribers
|
||||
}
|
||||
|
||||
fn init(registration_rx: Receiver<Sender<IdleState>>) {
|
||||
let (tx, rx) = crossbeam_channel::unbounded::<IdleMessage>();
|
||||
let tx_clone = tx.clone();
|
||||
let instance = Arc::new(Mutex::new(Self {
|
||||
subscribers: Vec::new(),
|
||||
}));
|
||||
let instance_clone = instance.clone();
|
||||
std::thread::spawn(move || {
|
||||
while let Ok(sender) = registration_rx.recv() {
|
||||
if sender.send(IdleState::Init(tx_clone.clone())).is_ok() {
|
||||
trace!(&LOGGER, "Added IdleDaemon Subscriber");
|
||||
instance_clone
|
||||
.lock()
|
||||
.expect("Couldn't get lock")
|
||||
.subscribers
|
||||
.push(sender);
|
||||
}
|
||||
}
|
||||
});
|
||||
let (state_tx, state_rx) = watch::channel(IdleStateInner {
|
||||
dimmed: false,
|
||||
wayland_idle: false,
|
||||
dbus_inhibited: false,
|
||||
caffeine: false,
|
||||
});
|
||||
let tx_clone = state_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
if let Err(err) = Self::start_idle_notifier(tx_clone) {
|
||||
error!(&LOGGER, "Idle Daemon unexpectedly quit, reason: {err}");
|
||||
}
|
||||
});
|
||||
std::thread::spawn(move || {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Failed to create Tokio runtime");
|
||||
runtime.block_on(async move {
|
||||
let mut idle_inhibitor = None;
|
||||
while let Ok(msg) = rx.recv() {
|
||||
match msg {
|
||||
IdleMessage::Inhibit => {
|
||||
if idle_inhibitor.is_none()
|
||||
&& let Ok(ii) = IdleInhibitor::inhibit_idle().await
|
||||
{
|
||||
idle_inhibitor = Some(ii);
|
||||
}
|
||||
}
|
||||
IdleMessage::Release => {
|
||||
if let Some(ii) = idle_inhibitor.take()
|
||||
&& let Err(e) = ii.uninhibit_idle().await
|
||||
{
|
||||
warn!(&LOGGER, "Error uninhibiting {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
std::thread::spawn(move || {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Failed to create Tokio runtime");
|
||||
|
||||
runtime.block_on(async move {
|
||||
let res = IdleDaemon::start_dbus(state_tx).await;
|
||||
match res {
|
||||
Ok((_, _)) => {
|
||||
Self::run_idle_timer(instance, state_rx, Duration::from_secs(10)).await;
|
||||
}
|
||||
Err(err) => error!(&LOGGER, "Error running D-Bus Screensaver: {err}"),
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl IdleDaemon {
|
||||
async fn run_idle_timer(
|
||||
instance: Arc<Mutex<Self>>,
|
||||
mut state_rx: watch::Receiver<IdleStateInner>,
|
||||
timeout: Duration,
|
||||
) {
|
||||
loop {
|
||||
let mut state = *state_rx.borrow_and_update();
|
||||
instance.lock().unwrap().publish(state.into());
|
||||
|
||||
if !state.should_dim() {
|
||||
if state_rx.changed().await.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
_ = sleep(timeout) => {
|
||||
state.dimmed = true;
|
||||
instance.lock().unwrap().publish(state.into());
|
||||
}
|
||||
result = state_rx.changed() => {
|
||||
if result.is_err() {
|
||||
return;
|
||||
}
|
||||
state.dimmed = false;
|
||||
instance.lock().unwrap().publish(state.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_idle_notifier(tx: watch::Sender<IdleStateInner>) -> Result<(), Box<dyn Error>> {
|
||||
let conn = Connection::connect_to_env()?;
|
||||
let (globals, mut event_queue) = registry_queue_init::<WlState>(&conn)?;
|
||||
let queue_handle = event_queue.handle();
|
||||
|
||||
let seat = globals.bind::<WlSeat, _, _>(&queue_handle, 1..=9, ())?;
|
||||
|
||||
let notifier = globals.bind::<ExtIdleNotifierV1, _, _>(&queue_handle, 2..=2, ())?;
|
||||
|
||||
let mut state = WlState { idle: None, tx };
|
||||
|
||||
let _notification = notifier.get_idle_notification(10_000, &seat, &queue_handle, ());
|
||||
|
||||
loop {
|
||||
event_queue.blocking_dispatch(&mut state)?;
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_dbus(
|
||||
tx: watch::Sender<IdleStateInner>,
|
||||
) -> zbus::Result<(ScreenSaver, zbus::connection::Connection)> {
|
||||
let service = ScreenSaver::new(tx);
|
||||
|
||||
let connection = Builder::session()?
|
||||
.name(SERVICE)?
|
||||
.serve_at(OBJECT_PATH, service.clone())?
|
||||
.serve_at(LEGACY_OBJECT_PATH, service.clone())?
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
info!(&LOGGER, "Providing {SERVICE}");
|
||||
|
||||
Ok((service, connection))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IdleInhibitor {
|
||||
connection: zbus::Connection,
|
||||
cookie: u32,
|
||||
}
|
||||
|
||||
impl IdleInhibitor {
|
||||
async fn inhibit_idle() -> zbus::Result<IdleInhibitor> {
|
||||
let connection = zbus::Connection::session().await?;
|
||||
|
||||
let reply = connection
|
||||
.call_method(
|
||||
Some("org.freedesktop.ScreenSaver"),
|
||||
"/org/freedesktop/ScreenSaver",
|
||||
Some("org.freedesktop.ScreenSaver"),
|
||||
"Inhibit",
|
||||
&(APP_NAME, "Caffeine mode active"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let cookie = reply.body().deserialize::<u32>()?;
|
||||
|
||||
Ok(Self { connection, cookie })
|
||||
}
|
||||
|
||||
async fn uninhibit_idle(self) -> zbus::Result<()> {
|
||||
self.connection
|
||||
.call_method(
|
||||
Some("org.freedesktop.ScreenSaver"),
|
||||
"/org/freedesktop/ScreenSaver",
|
||||
Some("org.freedesktop.ScreenSaver"),
|
||||
"UnInhibit",
|
||||
&(self.cookie,),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ScreenSaver {
|
||||
inner: Arc<Mutex<Inner>>,
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
next_cookie: u32,
|
||||
inhibitors: HashMap<u32, Inhibitor>,
|
||||
tx: watch::Sender<IdleStateInner>,
|
||||
}
|
||||
|
||||
impl ScreenSaver {
|
||||
fn new(tx: watch::Sender<IdleStateInner>) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(Inner {
|
||||
next_cookie: 1,
|
||||
inhibitors: HashMap::new(),
|
||||
tx,
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[interface(name = "org.freedesktop.ScreenSaver")]
|
||||
impl ScreenSaver {
|
||||
#[zbus(name = "Inhibit")]
|
||||
fn inhibit(
|
||||
&self,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
application_name: String,
|
||||
reason_for_inhibit: String,
|
||||
) -> zbus::fdo::Result<u32> {
|
||||
let owner = header
|
||||
.sender()
|
||||
.ok_or_else(|| zbus::fdo::Error::Failed("Missing D-Bus sender".into()))?
|
||||
.to_string();
|
||||
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
let start = inner.next_cookie;
|
||||
let cookie = loop {
|
||||
let candidate = inner.next_cookie;
|
||||
inner.next_cookie = inner.next_cookie.wrapping_add(1).max(1);
|
||||
|
||||
if !inner.inhibitors.contains_key(&candidate) {
|
||||
break candidate;
|
||||
}
|
||||
|
||||
if inner.next_cookie == start {
|
||||
return Err(zbus::fdo::Error::Failed(
|
||||
"No available inhibition cookies".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let caffeine_flag = application_name.eq(APP_NAME);
|
||||
|
||||
debug!(&LOGGER, "Inhibitor added: {application_name} {cookie}");
|
||||
inner.inhibitors.insert(
|
||||
cookie,
|
||||
Inhibitor {
|
||||
owner,
|
||||
application: application_name,
|
||||
reason: reason_for_inhibit,
|
||||
},
|
||||
);
|
||||
|
||||
inner.tx.send_modify(|state| {
|
||||
state.dbus_inhibited = !inner.inhibitors.is_empty();
|
||||
state.caffeine = caffeine_flag;
|
||||
});
|
||||
Ok(cookie)
|
||||
}
|
||||
|
||||
#[zbus(name = "UnInhibit")]
|
||||
fn uninhibit(&self, #[zbus(header)] header: Header<'_>, cookie: u32) -> zbus::fdo::Result<()> {
|
||||
let owner = header
|
||||
.sender()
|
||||
.ok_or_else(|| zbus::fdo::Error::Failed("Missing D-Bus sender".into()))?
|
||||
.to_string();
|
||||
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
match inner.inhibitors.get(&cookie) {
|
||||
Some(inhibitor) if inhibitor.owner == owner => {
|
||||
if let Some(removed) = inner.inhibitors.remove(&cookie) {
|
||||
debug!(
|
||||
&LOGGER,
|
||||
"Inhibitor removed: {} {cookie}", removed.application
|
||||
);
|
||||
}
|
||||
let mut caffeine_flag = false;
|
||||
for inhibitor in inner.inhibitors.values() {
|
||||
if inhibitor.application.eq(APP_NAME) {
|
||||
caffeine_flag = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
inner.tx.send_modify(|state| {
|
||||
state.dbus_inhibited = !inner.inhibitors.is_empty();
|
||||
state.caffeine = caffeine_flag;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
Some(_) => Err(zbus::fdo::Error::AccessDenied(
|
||||
"Cookie belongs to another D-Bus client".into(),
|
||||
)),
|
||||
None => Err(zbus::fdo::Error::Failed("Unknown inhibition cookie".into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
265
src/backend/json_socket.rs
Normal file
265
src/backend/json_socket.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
use crate::LOGGER;
|
||||
use crate::backend::{BackendThread, Registrator};
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use http::Request;
|
||||
use http_body_util::{BodyExt, Empty, Full};
|
||||
use hyper::body::{Bytes, Incoming};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use jmb_logger_rs::*;
|
||||
use std::cmp::PartialEq;
|
||||
use std::error::Error;
|
||||
use std::process::Command;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
pub struct JsonSocketBackend {
|
||||
subscribers: Vec<Sender<State>>,
|
||||
state: State,
|
||||
socket_address: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum State {
|
||||
Connected,
|
||||
Connecting,
|
||||
Disconnected,
|
||||
NeedsLogin,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn try_from_value(value: i64) -> Option<State> {
|
||||
match value {
|
||||
5 => Some(State::Connecting),
|
||||
6 => Some(State::Connected),
|
||||
_ => Some(State::Disconnected),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSocketBackend {
|
||||
async fn watch(instance: Arc<Mutex<Self>>) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let socket_address = instance
|
||||
.lock()
|
||||
.expect("Couldn't get lock")
|
||||
.socket_address
|
||||
.clone();
|
||||
let socket = UnixStream::connect(&socket_address).await?;
|
||||
let io = TokioIo::new(socket);
|
||||
|
||||
let (mut sender, connection) = hyper::client::conn::http1::handshake(io).await?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = connection.await {
|
||||
warn!(&LOGGER, "Tailscale socket HTTP connection error: {err}");
|
||||
}
|
||||
});
|
||||
|
||||
let request = Request::builder()
|
||||
.method("GET")
|
||||
.uri("http://local-tailscaled.sock/localapi/v0/watch-ipn-bus")
|
||||
.header("Host", "local-tailscaled.sock")
|
||||
.header("Accept", "application/json")
|
||||
.body(Empty::<Bytes>::new())?;
|
||||
|
||||
let response = sender.send_request(request).await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("Tailscale API returned {}", response.status()).into());
|
||||
}
|
||||
|
||||
let mut body: Incoming = response.into_body();
|
||||
let mut pending = Vec::<u8>::new();
|
||||
|
||||
while let Some(frame_result) = body.frame().await {
|
||||
let frame = frame_result?;
|
||||
|
||||
if let Ok(data) = frame.into_data() {
|
||||
pending.extend_from_slice(&data);
|
||||
|
||||
while let Some(newline) = pending.iter().position(|&b| b == b'\n') {
|
||||
let line: Vec<u8> = pending.drain(..=newline).collect();
|
||||
let line = &line[..line.len() - 1];
|
||||
|
||||
if line.iter().all(u8::is_ascii_whitespace) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match serde_json::from_slice::<serde_json::Value>(line) {
|
||||
Ok(notification) => {
|
||||
if let Some(state) = notification.get("State")
|
||||
&& !state.is_null()
|
||||
{
|
||||
let state = State::try_from_value(state.as_i64().unwrap_or(4))
|
||||
.unwrap_or(State::Disconnected);
|
||||
instance.lock().expect("Couldn't get lock").publish(state);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(&LOGGER, "Invalid Tailscale notification: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn watch_netbird(instance: Arc<Mutex<Self>>) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let socket_address = instance
|
||||
.lock()
|
||||
.expect("Couldn't get lock")
|
||||
.socket_address
|
||||
.clone();
|
||||
|
||||
let socket = UnixStream::connect(&socket_address).await?;
|
||||
let io = TokioIo::new(socket);
|
||||
|
||||
let (mut sender, connection) = hyper::client::conn::http1::handshake(io).await?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = connection.await {
|
||||
warn!(&LOGGER, "Netbird socket HTTP connection error: {err}");
|
||||
}
|
||||
});
|
||||
|
||||
let request = Request::builder()
|
||||
.method("POST")
|
||||
.uri("http://localhost/daemon.DaemonService/SubscribeStatus")
|
||||
.header("Host", "localhost")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.body(Full::new(Bytes::from_static(b"{}")))?;
|
||||
|
||||
let response = sender.send_request(request).await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("NetBird API returned {}", response.status()).into());
|
||||
}
|
||||
|
||||
let mut body = response.into_body();
|
||||
let mut pending = Vec::<u8>::new();
|
||||
|
||||
while let Some(frame_result) = body.frame().await {
|
||||
let frame = frame_result?;
|
||||
|
||||
if let Ok(data) = frame.into_data() {
|
||||
pending.extend_from_slice(&data);
|
||||
|
||||
while let Some(newline) = pending.iter().position(|&b| b == b'\n') {
|
||||
let line: Vec<u8> = pending.drain(..=newline).collect();
|
||||
let line = &line[..line.len() - 1];
|
||||
|
||||
if line.iter().all(u8::is_ascii_whitespace) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match serde_json::from_slice::<serde_json::Value>(line) {
|
||||
Ok(notification) => {
|
||||
let Some(status) = notification
|
||||
.get("result")
|
||||
.and_then(|v| v.get("status").and_then(|v| v.as_str()))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let state = match status {
|
||||
"Connected" => State::Connected,
|
||||
"Connecting" => State::Connecting,
|
||||
"Idle" => State::Disconnected,
|
||||
"NeedsLogin" => State::NeedsLogin,
|
||||
other => {
|
||||
warn!(&LOGGER, "Unknown NetBird status: {other}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
instance.lock().expect("Couldn't get lock").publish(state);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(&LOGGER, "Invalid NetBird notification: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BackendThread<State, ()> for JsonSocketBackend {
|
||||
fn subscribe(registrator: Registrator<State>) -> (Receiver<State>, ()) {
|
||||
let (tx, rx) = crossbeam_channel::unbounded::<State>();
|
||||
let _ = registrator.send(tx);
|
||||
(rx, ())
|
||||
}
|
||||
|
||||
fn subscribers(&self) -> &Vec<Sender<State>> {
|
||||
&self.subscribers
|
||||
}
|
||||
|
||||
fn init(registration_rx: Receiver<Sender<State>>) {
|
||||
let state = initial_state_tailscale();
|
||||
|
||||
let instance = Arc::new(Mutex::new(Self {
|
||||
subscribers: vec![],
|
||||
state,
|
||||
socket_address: String::from("/var/run/netbird-http.sock"),
|
||||
}));
|
||||
|
||||
let instance_clone = instance.clone();
|
||||
std::thread::spawn(move || {
|
||||
while let Ok(tx) = registration_rx.recv() {
|
||||
trace!(&LOGGER, "Added NetBird Subscriber: {tx:?}");
|
||||
|
||||
let _ = tx.send(
|
||||
instance_clone
|
||||
.lock()
|
||||
.expect("Couldn't get lock")
|
||||
.state
|
||||
.clone(),
|
||||
);
|
||||
|
||||
instance_clone
|
||||
.lock()
|
||||
.expect("Couldn't get lock")
|
||||
.subscribers
|
||||
.push(tx);
|
||||
}
|
||||
});
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Failed to create Tokio runtime");
|
||||
|
||||
runtime.block_on(async move {
|
||||
loop {
|
||||
if let Err(e) = Self::watch_netbird(instance.clone()).await {
|
||||
warn!(&LOGGER, "Error: {e:?}");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_state_tailscale() -> State {
|
||||
if let Ok(output) = Command::new("tailscale")
|
||||
.arg("status")
|
||||
.arg("--json")
|
||||
.output()
|
||||
{
|
||||
let val: Result<serde_json::Value, _> = serde_json::from_slice(&output.stdout);
|
||||
if let Ok(val) = val {
|
||||
if let Some(res) = val.get("BackendState").and_then(|v| v.as_str())
|
||||
&& res.eq("Running")
|
||||
{
|
||||
return State::Connected;
|
||||
}
|
||||
} else {
|
||||
error!(&LOGGER, "{:?}", val.err());
|
||||
}
|
||||
}
|
||||
State::Connecting
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
use crate::LOGGER;
|
||||
use crate::backend::{BackendThread, Registrator};
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use http::Request;
|
||||
use http_body_util::BodyExt;
|
||||
use hyper::body::Incoming;
|
||||
use hyper_util::rt::TokioIo;
|
||||
use jmb_logger_rs::*;
|
||||
use std::error::Error;
|
||||
use std::process::Command;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
pub struct TailscaleBackend {
|
||||
subscribers: Vec<Sender<TailscaleState>>,
|
||||
state: TailscaleState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TailscaleState {
|
||||
NoState = 0,
|
||||
InUseOtherUser = 1,
|
||||
NeedsLogin = 2,
|
||||
NeedsMachineAuth = 3,
|
||||
Stopped = 4,
|
||||
Starting = 5,
|
||||
Running = 6,
|
||||
}
|
||||
|
||||
impl TailscaleState {
|
||||
fn try_from_value(value: i64) -> Option<TailscaleState> {
|
||||
match value {
|
||||
0 => Some(TailscaleState::NoState),
|
||||
1 => Some(TailscaleState::InUseOtherUser),
|
||||
2 => Some(TailscaleState::NeedsLogin),
|
||||
3 => Some(TailscaleState::NeedsMachineAuth),
|
||||
4 => Some(TailscaleState::Stopped),
|
||||
5 => Some(TailscaleState::Starting),
|
||||
6 => Some(TailscaleState::Running),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TailscaleBackend {
|
||||
async fn watch_tailscale(
|
||||
instance: Arc<Mutex<Self>>,
|
||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let socket = UnixStream::connect("/var/run/tailscale/tailscaled.sock").await?;
|
||||
let io = TokioIo::new(socket);
|
||||
|
||||
let (mut sender, connection) = hyper::client::conn::http1::handshake(io).await?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = connection.await {
|
||||
warn!(&LOGGER, "Tailscale socket HTTP connection error: {err}");
|
||||
}
|
||||
});
|
||||
|
||||
let request = Request::builder()
|
||||
.method("GET")
|
||||
.uri("http://local-tailscaled.sock/localapi/v0/watch-ipn-bus")
|
||||
.header("Host", "local-tailscaled.sock")
|
||||
.header("Accept", "application/json")
|
||||
.body(http_body_util::Empty::<hyper::body::Bytes>::new())?;
|
||||
|
||||
let response = sender.send_request(request).await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("Tailscale API returned {}", response.status()).into());
|
||||
}
|
||||
|
||||
let mut body: Incoming = response.into_body();
|
||||
let mut pending = Vec::<u8>::new();
|
||||
|
||||
while let Some(frame_result) = body.frame().await {
|
||||
let frame = frame_result?;
|
||||
|
||||
if let Ok(data) = frame.into_data() {
|
||||
pending.extend_from_slice(&data);
|
||||
|
||||
while let Some(newline) = pending.iter().position(|&b| b == b'\n') {
|
||||
let line: Vec<u8> = pending.drain(..=newline).collect();
|
||||
let line = &line[..line.len() - 1];
|
||||
|
||||
if line.iter().all(u8::is_ascii_whitespace) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match serde_json::from_slice::<serde_json::Value>(line) {
|
||||
Ok(notification) => {
|
||||
if let Some(state) = notification.get("State")
|
||||
&& !state.is_null()
|
||||
{
|
||||
let state =
|
||||
TailscaleState::try_from_value(state.as_i64().unwrap_or(4))
|
||||
.unwrap_or(TailscaleState::Stopped);
|
||||
instance.lock().expect("Couldn't get lock").publish(state);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Invalid Tailscale notification: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BackendThread<TailscaleState, ()> for TailscaleBackend {
|
||||
fn subscribe(registrator: Registrator<TailscaleState>) -> (Receiver<TailscaleState>, ()) {
|
||||
let (tx, rx) = crossbeam_channel::unbounded::<TailscaleState>();
|
||||
let _ = registrator.send(tx);
|
||||
(rx, ())
|
||||
}
|
||||
|
||||
fn subscribers(&self) -> &Vec<Sender<TailscaleState>> {
|
||||
&self.subscribers
|
||||
}
|
||||
|
||||
fn init(registration_rx: Receiver<Sender<TailscaleState>>) {
|
||||
let state = initial_state();
|
||||
|
||||
let instance = Arc::new(Mutex::new(Self {
|
||||
subscribers: vec![],
|
||||
state,
|
||||
}));
|
||||
|
||||
let instance_clone = instance.clone();
|
||||
std::thread::spawn(move || {
|
||||
while let Ok(tx) = registration_rx.recv() {
|
||||
trace!(&LOGGER, "Added Tailscale Subscriber: {tx:?}");
|
||||
|
||||
let _ = tx.send(
|
||||
instance_clone
|
||||
.lock()
|
||||
.expect("Couldn't get lock")
|
||||
.state
|
||||
.clone(),
|
||||
);
|
||||
|
||||
instance_clone
|
||||
.lock()
|
||||
.expect("Couldn't get lock")
|
||||
.subscribers
|
||||
.push(tx);
|
||||
}
|
||||
});
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Failed to create Tokio runtime");
|
||||
|
||||
runtime.block_on(async move {
|
||||
loop {
|
||||
if let Err(e) = Self::watch_tailscale(instance.clone()).await {
|
||||
warn!(&LOGGER, "Error: {e:?}");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_state() -> TailscaleState {
|
||||
if let Ok(output) = Command::new("tailscale")
|
||||
.arg("status")
|
||||
.arg("--json")
|
||||
.output()
|
||||
{
|
||||
let val: Result<serde_json::Value, _> = serde_json::from_slice(&output.stdout);
|
||||
if let Ok(val) = val {
|
||||
if let Some(res) = val.get("BackendState").and_then(|v| v.as_str())
|
||||
&& res.eq("Running")
|
||||
{
|
||||
return TailscaleState::Running;
|
||||
}
|
||||
} else {
|
||||
error!(&LOGGER, "{:?}", val.err());
|
||||
}
|
||||
}
|
||||
TailscaleState::Stopped
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::LOGGER;
|
||||
use crate::backend::BackendThread;
|
||||
use crate::backend::{BackendThread, SimpleBackendThread};
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use hyprland::data::{Client, Monitor, Workspaces};
|
||||
use hyprland::event_listener::EventListener;
|
||||
@@ -23,14 +23,7 @@ pub struct HyprlandComms {
|
||||
subscribers: Vec<Sender<WindowManagerEvent>>,
|
||||
}
|
||||
|
||||
impl BackendThread<WindowManagerEvent, ()> for HyprlandComms {
|
||||
fn subscribe(
|
||||
registrator: Sender<Sender<WindowManagerEvent>>,
|
||||
) -> (Receiver<WindowManagerEvent>, ()) {
|
||||
let (tx, rx) = crossbeam_channel::unbounded::<WindowManagerEvent>();
|
||||
let _ = registrator.send(tx);
|
||||
(rx, ())
|
||||
}
|
||||
impl SimpleBackendThread<WindowManagerEvent> for HyprlandComms {
|
||||
|
||||
fn subscribers(&self) -> &Vec<Sender<WindowManagerEvent>> {
|
||||
&self.subscribers
|
||||
|
||||
Reference in New Issue
Block a user