MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
DBusBackend.cpp
Go to the documentation of this file.
1#ifdef MAYAFLUX_PLATFORM_LINUX
2
3#include "DBusBackend.hpp"
4
6
7#if __has_include(<dbus/dbus.h>)
8#include <dbus/dbus.h>
9#elif __has_include(<dbus-1.0/dbus/dbus.h>)
10#include <dbus-1.0/dbus/dbus.h>
11#else
12#error "dbus/dbus.h not found"
13#endif
14
15namespace MayaFlux::Core {
16
17namespace {
18
19 constexpr const char* k_portal_service = "org.freedesktop.portal.Desktop";
20 constexpr const char* k_portal_path = "/org/freedesktop/portal/desktop";
21 constexpr const char* k_portal_iface = "org.freedesktop.portal.FileChooser";
22 constexpr const char* k_request_iface = "org.freedesktop.portal.Request";
23 constexpr const char* k_signal_response = "Response";
24
25 void append_string_variant(DBusMessageIter& arr, const char* key, const char* value)
26 {
27 DBusMessageIter entry, variant;
28 dbus_message_iter_open_container(&arr, DBUS_TYPE_DICT_ENTRY, nullptr, &entry);
29 dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, static_cast<const void*>(&key));
30 dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "s", &variant);
31 dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, static_cast<const void*>(&value));
32 dbus_message_iter_close_container(&entry, &variant);
33 dbus_message_iter_close_container(&arr, &entry);
34 }
35
36 /**
37 * @brief Append the filters option as a(sa(us)) to an open options dict iter.
38 *
39 * XDG portal filter format: array of (label, array of (type, pattern)).
40 * Type 0 = glob, type 1 = MIME type.
41 */
42 void append_filters(DBusMessageIter& opts, const std::vector<SystemFileFilter>& filters)
43 {
44 if (filters.empty()) {
45 return;
46 }
47
48 DBusMessageIter entry, variant, outer, filter_struct, pattern_arr, pattern_struct;
49 const char* key = "filters";
50
51 dbus_message_iter_open_container(&opts, DBUS_TYPE_DICT_ENTRY, nullptr, &entry);
52 dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, static_cast<const void*>(&key));
53 dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "a(sa(us))", &variant);
54 dbus_message_iter_open_container(&variant, DBUS_TYPE_ARRAY, "(sa(us))", &outer);
55
56 for (const auto& f : filters) {
57 dbus_message_iter_open_container(&outer, DBUS_TYPE_STRUCT, nullptr, &filter_struct);
58 const char* label = f.name.c_str();
59 dbus_message_iter_append_basic(&filter_struct, DBUS_TYPE_STRING, static_cast<const void*>(&label));
60 dbus_message_iter_open_container(&filter_struct, DBUS_TYPE_ARRAY, "(us)", &pattern_arr);
61
62 for (const auto& ext : f.extensions) {
63 std::string glob = (ext == "*") ? "*" : "*." + ext;
64 const char* glob_cstr = glob.c_str();
65 dbus_uint32_t type = 0;
66 dbus_message_iter_open_container(&pattern_arr, DBUS_TYPE_STRUCT, nullptr, &pattern_struct);
67 dbus_message_iter_append_basic(&pattern_struct, DBUS_TYPE_UINT32, &type);
68 dbus_message_iter_append_basic(&pattern_struct, DBUS_TYPE_STRING, static_cast<const void*>(&glob_cstr));
69 dbus_message_iter_close_container(&pattern_arr, &pattern_struct);
70 }
71
72 dbus_message_iter_close_container(&filter_struct, &pattern_arr);
73 dbus_message_iter_close_container(&outer, &filter_struct);
74 }
75
76 dbus_message_iter_close_container(&variant, &outer);
77 dbus_message_iter_close_container(&entry, &variant);
78 dbus_message_iter_close_container(&opts, &entry);
79 }
80
81 /**
82 * @brief Percent-decode a URI component, in-place.
83 *
84 * Only decodes valid %XX sequences; leaves invalid ones as-is.
85 */
86 std::string percent_decode(std::string_view sv)
87 {
88 std::string out;
89 out.reserve(sv.size());
90 for (size_t i = 0; i < sv.size(); ++i) {
91 if (sv[i] == '%' && i + 2 < sv.size()) {
92 auto hex = [](char c) -> int {
93 if (c >= '0' && c <= '9')
94 return c - '0';
95 if (c >= 'A' && c <= 'F')
96 return c - 'A' + 10;
97 if (c >= 'a' && c <= 'f')
98 return c - 'a' + 10;
99 return -1;
100 };
101 int hi = hex(sv[i + 1]);
102 int lo = hex(sv[i + 2]);
103 if (hi >= 0 && lo >= 0) {
104 out += static_cast<char>((hi << 4) | lo);
105 i += 2;
106 continue;
107 }
108 }
109 out += sv[i];
110 }
111 return out;
112 }
113
114 /**
115 * @brief Block on @p conn until the portal Request signal arrives,
116 * then fire @p callback and return.
117 *
118 * Runs on a dedicated thread. Does not touch the backend's m_conn;
119 * receives its own private connection pointer allocated per-call.
120 */
121 void dispatch_loop(DBusConnection* conn, std::string request_path, FileDialogCallback callback)
122 {
123 while (true) {
124 dbus_connection_read_write(conn, 100);
125 DBusMessage* msg = dbus_connection_pop_message(conn);
126 if (!msg) {
127 continue;
128 }
129
130 const bool is_signal = dbus_message_get_type(msg) == DBUS_MESSAGE_TYPE_SIGNAL;
131 const bool on_request = (dbus_message_get_path(msg) == request_path);
132 const bool is_response = dbus_message_has_member(msg, k_signal_response);
133
134 if (is_signal && on_request && is_response) {
135 dbus_uint32_t response_code = 0;
136 DBusMessageIter iter;
137 dbus_message_iter_init(msg, &iter);
138 dbus_message_iter_get_basic(&iter, &response_code);
139
140 if (response_code != 0) {
141 dbus_message_unref(msg);
142 dbus_connection_close(conn);
143 dbus_connection_unref(conn);
144 callback(std::unexpected(
145 response_code == 1
148 return;
149 }
150
151 dbus_message_iter_next(&iter);
152 DBusMessageIter results;
153 dbus_message_iter_recurse(&iter, &results);
154
155 std::string chosen_path;
156
157 while (dbus_message_iter_get_arg_type(&results) == DBUS_TYPE_DICT_ENTRY) {
158 DBusMessageIter kv;
159 dbus_message_iter_recurse(&results, &kv);
160
161 const char* key = nullptr;
162 dbus_message_iter_get_basic(&kv, static_cast<void*>(&key));
163
164 if (key && std::string_view(key) == "uris") {
165 dbus_message_iter_next(&kv);
166 DBusMessageIter variant, arr;
167 dbus_message_iter_recurse(&kv, &variant);
168 dbus_message_iter_recurse(&variant, &arr);
169
170 if (dbus_message_iter_get_arg_type(&arr) == DBUS_TYPE_STRING) {
171 const char* uri = nullptr;
172 dbus_message_iter_get_basic(&arr, static_cast<void*>(&uri));
173 if (uri) {
174 std::string_view sv(uri);
175 if (sv.starts_with("file://")) {
176 sv.remove_prefix(7);
177 }
178 chosen_path = percent_decode(sv);
179 }
180 }
181 break;
182 }
183 dbus_message_iter_next(&results);
184 }
185
186 dbus_message_unref(msg);
187 dbus_connection_close(conn);
188 dbus_connection_unref(conn);
189
190 if (chosen_path.empty()) {
191 callback(std::unexpected(SystemDialogError::BackendError));
192 } else {
193 callback(std::filesystem::path(chosen_path));
194 }
195 return;
196 }
197
198 dbus_message_unref(msg);
199 }
200 }
201
202} // namespace
203
204// =============================================================================
205// Lifecycle
206// =============================================================================
207
208DBusBackend::~DBusBackend()
209{
210 shutdown();
211}
212
213bool DBusBackend::initialize()
214{
215 if (m_initialized) {
216 return true;
217 }
218
219 DBusError err;
220 dbus_error_init(&err);
221
222 m_conn = dbus_bus_get_private(DBUS_BUS_SESSION, &err);
223
224 if (!m_conn || dbus_error_is_set(&err)) {
226 "DBusBackend: session bus connection failed: {}",
227 dbus_error_is_set(&err) ? err.message : "unknown");
228 dbus_error_free(&err);
229 return false;
230 }
231
232 dbus_connection_set_exit_on_disconnect(m_conn, FALSE);
233
234 m_initialized = true;
236 "DBusBackend initialized");
237 return true;
238}
239
240void DBusBackend::shutdown()
241{
242 if (!m_initialized) {
243 return;
244 }
245
246 dbus_connection_close(m_conn);
247 dbus_connection_unref(m_conn);
248 m_conn = nullptr;
249 m_initialized = false;
250
252 "DBusBackend shutdown");
253}
254
255// =============================================================================
256// Dialog operations
257// =============================================================================
258
259void DBusBackend::open_file(
260 FileDialogCallback callback,
261 std::vector<SystemFileFilter> filters,
262 std::filesystem::path start_dir)
263{
264 invoke_portal("OpenFile", std::move(callback), filters, start_dir, {});
265}
266
267void DBusBackend::save_file(
268 FileDialogCallback callback,
269 std::string suggested_name,
270 std::vector<SystemFileFilter> filters,
271 std::filesystem::path start_dir)
272{
273 invoke_portal("SaveFile", std::move(callback), filters, start_dir, suggested_name);
274}
275
276void DBusBackend::invoke_portal(
277 const char* method,
278 FileDialogCallback callback,
279 const std::vector<SystemFileFilter>& filters,
280 const std::filesystem::path& start_dir,
281 const std::string& suggested) const
282{
283 if (!m_initialized) {
284 callback(std::unexpected(SystemDialogError::BackendError));
285 return;
286 }
287
288 DBusError err;
289 dbus_error_init(&err);
290
291 // Each dialog call gets its own private connection for the dispatch thread
292 DBusConnection* call_conn = dbus_bus_get_private(DBUS_BUS_SESSION, &err);
293 if (!call_conn || dbus_error_is_set(&err)) {
295 "DBusBackend: failed to open per-call connection: {}",
296 dbus_error_is_set(&err) ? err.message : "unknown");
297 dbus_error_free(&err);
298 callback(std::unexpected(SystemDialogError::BackendError));
299 return;
300 }
301
302 dbus_connection_set_exit_on_disconnect(call_conn, FALSE);
303
304 DBusMessage* call_msg = dbus_message_new_method_call(
305 k_portal_service, k_portal_path, k_portal_iface, method);
306
307 if (!call_msg) {
308 dbus_connection_close(call_conn);
309 dbus_connection_unref(call_conn);
310 callback(std::unexpected(SystemDialogError::BackendError));
311 return;
312 }
313
314 const char* parent = "";
315 const char* title = (std::string_view(method) == "SaveFile") ? "Save File" : "Open File";
316
317 DBusMessageIter args, opts;
318 dbus_message_iter_init_append(call_msg, &args);
319 dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, static_cast<const void*>(&parent));
320 dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, static_cast<const void*>(&title));
321 dbus_message_iter_open_container(&args, DBUS_TYPE_ARRAY, "{sv}", &opts);
322
323 if (!start_dir.empty()) {
324 std::string uri = "file://" + start_dir.string();
325 append_string_variant(opts, "current_folder", uri.c_str());
326 }
327 if (!suggested.empty()) {
328 append_string_variant(opts, "current_name", suggested.c_str());
329 }
330
331 append_filters(opts, filters);
332
333 dbus_message_iter_close_container(&args, &opts);
334
335 DBusMessage* reply = dbus_connection_send_with_reply_and_block(call_conn, call_msg, 5000, &err);
336 dbus_message_unref(call_msg);
337
338 if (!reply || dbus_error_is_set(&err)) {
340 "DBusBackend: portal method call failed: {}",
341 dbus_error_is_set(&err) ? err.message : "no reply");
342 dbus_error_free(&err);
343 dbus_connection_close(call_conn);
344 dbus_connection_unref(call_conn);
345 callback(std::unexpected(SystemDialogError::BackendError));
346 return;
347 }
348
349 const char* request_path_cstr = nullptr;
350 DBusMessageIter reply_iter;
351 dbus_message_iter_init(reply, &reply_iter);
352 dbus_message_iter_get_basic(&reply_iter, static_cast<void*>(&request_path_cstr));
353 std::string request_path = request_path_cstr ? request_path_cstr : "";
354 dbus_message_unref(reply);
355
356 if (request_path.empty()) {
357 dbus_connection_close(call_conn);
358 dbus_connection_unref(call_conn);
359 callback(std::unexpected(SystemDialogError::BackendError));
360 return;
361 }
362
363 std::string match = "type='signal',path='" + request_path
364 + "',interface='" + k_request_iface
365 + "',member='" + k_signal_response + "'";
366
367 dbus_bus_add_match(call_conn, match.c_str(), &err);
368 dbus_connection_flush(call_conn);
369
370 if (dbus_error_is_set(&err)) {
372 "DBusBackend: add_match failed: {}", err.message);
373 dbus_error_free(&err);
374 dbus_connection_close(call_conn);
375 dbus_connection_unref(call_conn);
376 callback(std::unexpected(SystemDialogError::BackendError));
377 return;
378 }
379
380 std::thread(dispatch_loop, call_conn, std::move(request_path), std::move(callback)).detach();
381}
382
383} // namespace MayaFlux::Core
384
385#endif // MAYAFLUX_PLATFORM_LINUX
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
SystemDialogError
Failure modes for OS dialog operations.
@ BackendError
Platform backend failed to open or communicate.
@ Cancelled
User dismissed the dialog without completing it.
std::function< void(FileDialogResult)> FileDialogCallback
Callback type for all file dialog operations.
@ API
API calls from external code.
@ Core
Core engine, backend, subsystems.
void shutdown()
Release stored references.
Definition Forma.cpp:168