cosmic/
dbus_activation.rs

1// Copyright 2024 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4use {
5    crate::ApplicationExt,
6    iced::Subscription,
7    iced_futures::futures::{
8        SinkExt,
9        channel::mpsc::{Receiver, Sender},
10    },
11    std::{any::TypeId, collections::HashMap},
12    url::Url,
13    zbus::{interface, proxy, zvariant::Value},
14};
15
16#[cold]
17pub fn subscription<App: ApplicationExt>() -> Subscription<crate::Action<App::Message>> {
18    use iced_futures::futures::StreamExt;
19    iced_futures::Subscription::run_with(TypeId::of::<DbusActivation>(), |_| {
20        iced::stream::channel(
21            10,
22            move |mut output: Sender<crate::Action<App::Message>>| async move {
23                let mut single_instance: DbusActivation = DbusActivation::new();
24                let mut rx = single_instance.rx();
25                if let Ok(builder) = zbus::connection::Builder::session() {
26                    let path: String = format!("/{}", App::APP_ID.replace('.', "/"));
27                    if let Ok(conn) = builder.build().await {
28                        // XXX Setup done this way seems to be more reliable.
29                        //
30                        // the docs for serve_at seem to imply it will replace the
31                        // existing interface at the requested path, but it doesn't
32                        // seem to work that way all the time. The docs for
33                        // object_server().at() imply it won't replace the existing
34                        // interface.
35                        //
36                        // request_name is used either way, with the builder or
37                        // with the connection, but it must be done after the
38                        // object server is setup.
39                        if conn.object_server().at(path, single_instance).await != Ok(true) {
40                            tracing::error!("Failed to serve dbus");
41                            std::process::exit(1);
42                        }
43                        if conn.request_name(App::APP_ID).await.is_err() {
44                            tracing::error!("Failed to serve dbus");
45                            std::process::exit(1);
46                        }
47
48                        output
49                            .send(crate::Action::Cosmic(crate::app::Action::DbusConnection(
50                                conn.clone(),
51                            )))
52                            .await;
53
54                        #[cfg(feature = "smol")]
55                        let handle = {
56                            std::thread::spawn(move || {
57                                let conn_clone = _conn.clone();
58
59                                zbus::block_on(async move {
60                                    loop {
61                                        conn_clone.executor().tick().await;
62                                    }
63                                })
64                            })
65                        };
66                        while let Some(mut msg) = rx.next().await {
67                            if let Some(token) = msg.activation_token.take() {
68                                if let Err(err) = output
69                                    .send(crate::Action::Cosmic(crate::app::Action::Activate(
70                                        token,
71                                    )))
72                                    .await
73                                {
74                                    tracing::error!(?err, "Failed to send message");
75                                }
76                            }
77                            if let Err(err) = output.send(crate::Action::DbusActivation(msg)).await
78                            {
79                                tracing::error!(?err, "Failed to send message");
80                            }
81                        }
82                    }
83                } else {
84                    tracing::warn!("Failed to connect to dbus for single instance");
85                }
86
87                loop {
88                    iced::futures::pending!();
89                }
90            },
91        )
92    })
93}
94
95#[derive(Debug, Clone)]
96pub struct Message<Action = String, Args = Vec<String>> {
97    pub activation_token: Option<String>,
98    pub desktop_startup_id: Option<String>,
99    pub msg: Details<Action, Args>,
100}
101
102#[derive(Debug, Clone)]
103pub enum Details<Action = String, Args = Vec<String>> {
104    Activate,
105    Open {
106        url: Vec<Url>,
107    },
108    /// action can be deserialized as Flags
109    ActivateAction {
110        action: Action,
111        args: Args,
112    },
113}
114
115#[derive(Debug, Default)]
116pub struct DbusActivation(Option<Sender<Message>>);
117
118impl DbusActivation {
119    #[must_use]
120    #[inline]
121    pub fn new() -> Self {
122        Self(None)
123    }
124
125    #[inline]
126    pub fn rx(&mut self) -> Receiver<Message> {
127        let (tx, rx) = iced_futures::futures::channel::mpsc::channel(10);
128        self.0 = Some(tx);
129        rx
130    }
131}
132
133#[proxy(interface = "org.freedesktop.DbusActivation", assume_defaults = true)]
134pub trait DbusActivationInterface {
135    /// Activate the application.
136    fn activate(&mut self, platform_data: HashMap<&str, Value<'_>>) -> zbus::Result<()>;
137
138    /// Open the given URIs.
139    fn open(
140        &mut self,
141        uris: Vec<&str>,
142        platform_data: HashMap<&str, Value<'_>>,
143    ) -> zbus::Result<()>;
144
145    /// Activate the given action.
146    fn activate_action(
147        &mut self,
148        action_name: &str,
149        parameter: Vec<&str>,
150        platform_data: HashMap<&str, Value<'_>>,
151    ) -> zbus::Result<()>;
152}
153
154#[interface(name = "org.freedesktop.DbusActivation")]
155impl DbusActivation {
156    #[cold]
157    async fn activate(&mut self, platform_data: HashMap<&str, Value<'_>>) {
158        if let Some(tx) = &mut self.0 {
159            let _ = tx
160                .send(Message {
161                    activation_token: platform_data.get("activation-token").and_then(|t| match t {
162                        Value::Str(t) => Some(t.to_string()),
163                        _ => None,
164                    }),
165                    desktop_startup_id: platform_data.get("desktop-startup-id").and_then(
166                        |t| match t {
167                            Value::Str(t) => Some(t.to_string()),
168                            _ => None,
169                        },
170                    ),
171                    msg: Details::Activate,
172                })
173                .await;
174        }
175    }
176
177    #[cold]
178    async fn open(&mut self, uris: Vec<&str>, platform_data: HashMap<&str, Value<'_>>) {
179        if let Some(tx) = &mut self.0 {
180            let _ = tx
181                .send(Message {
182                    activation_token: platform_data.get("activation-token").and_then(|t| match t {
183                        Value::Str(t) => Some(t.to_string()),
184                        _ => None,
185                    }),
186                    desktop_startup_id: platform_data.get("desktop-startup-id").and_then(
187                        |t| match t {
188                            Value::Str(t) => Some(t.to_string()),
189                            _ => None,
190                        },
191                    ),
192                    msg: Details::Open {
193                        url: uris.iter().filter_map(|u| Url::parse(u).ok()).collect(),
194                    },
195                })
196                .await;
197        }
198    }
199
200    #[cold]
201    async fn activate_action(
202        &mut self,
203        action_name: &str,
204        parameter: Vec<&str>,
205        platform_data: HashMap<&str, Value<'_>>,
206    ) {
207        if let Some(tx) = &mut self.0 {
208            let _ = tx
209                .send(Message {
210                    activation_token: platform_data.get("activation-token").and_then(|t| match t {
211                        Value::Str(t) => Some(t.to_string()),
212                        _ => None,
213                    }),
214                    desktop_startup_id: platform_data.get("desktop-startup-id").and_then(
215                        |t| match t {
216                            Value::Str(t) => Some(t.to_string()),
217                            _ => None,
218                        },
219                    ),
220                    msg: Details::ActivateAction {
221                        action: action_name.to_string(),
222                        args: parameter
223                            .iter()
224                            .map(std::string::ToString::to_string)
225                            .collect(),
226                    },
227                })
228                .await;
229        }
230    }
231}