MayaFlux 1.0.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 a NUL terminated byte array option (type ay) to an open options dict iter.
38 */
39 void append_path_variant(DBusMessageIter& arr, const char* key, const std::string& path)
40 {
41 DBusMessageIter entry, variant, bytes;
42 dbus_message_iter_open_container(&arr, DBUS_TYPE_DICT_ENTRY, nullptr, &entry);
43 dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, static_cast<const void*>(&key));
44 dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "ay", &variant);
45 dbus_message_iter_open_container(&variant, DBUS_TYPE_ARRAY, "y", &bytes);
46 const char* data = path.c_str();
47 dbus_message_iter_append_fixed_array(&bytes, DBUS_TYPE_BYTE, &data, static_cast<int>(path.size() + 1));
48 dbus_message_iter_close_container(&variant, &bytes);
49 dbus_message_iter_close_container(&entry, &variant);
50 dbus_message_iter_close_container(&arr, &entry);
51 }
52
53 /**
54 * @brief Append the filters option as a(sa(us)) to an open options dict iter.
55 *
56 * XDG portal filter format: array of (label, array of (type, pattern)).
57 * Type 0 = glob, type 1 = MIME type.
58 */
59 void append_filters(DBusMessageIter& opts, const std::vector<SystemFileFilter>& filters)
60 {
61 if (filters.empty()) {
62 return;
63 }
64
65 DBusMessageIter entry, variant, outer, filter_struct, pattern_arr, pattern_struct;
66 const char* key = "filters";
67
68 dbus_message_iter_open_container(&opts, DBUS_TYPE_DICT_ENTRY, nullptr, &entry);
69 dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, static_cast<const void*>(&key));
70 dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "a(sa(us))", &variant);
71 dbus_message_iter_open_container(&variant, DBUS_TYPE_ARRAY, "(sa(us))", &outer);
72
73 for (const auto& f : filters) {
74 dbus_message_iter_open_container(&outer, DBUS_TYPE_STRUCT, nullptr, &filter_struct);
75 const char* label = f.name.c_str();
76 dbus_message_iter_append_basic(&filter_struct, DBUS_TYPE_STRING, static_cast<const void*>(&label));
77 dbus_message_iter_open_container(&filter_struct, DBUS_TYPE_ARRAY, "(us)", &pattern_arr);
78
79 for (const auto& ext : f.extensions) {
80 std::string glob = (ext == "*") ? "*" : "*." + ext;
81 const char* glob_cstr = glob.c_str();
82 dbus_uint32_t type = 0;
83 dbus_message_iter_open_container(&pattern_arr, DBUS_TYPE_STRUCT, nullptr, &pattern_struct);
84 dbus_message_iter_append_basic(&pattern_struct, DBUS_TYPE_UINT32, &type);
85 dbus_message_iter_append_basic(&pattern_struct, DBUS_TYPE_STRING, static_cast<const void*>(&glob_cstr));
86 dbus_message_iter_close_container(&pattern_arr, &pattern_struct);
87 }
88
89 dbus_message_iter_close_container(&filter_struct, &pattern_arr);
90 dbus_message_iter_close_container(&outer, &filter_struct);
91 }
92
93 dbus_message_iter_close_container(&variant, &outer);
94 dbus_message_iter_close_container(&entry, &variant);
95 dbus_message_iter_close_container(&opts, &entry);
96 }
97
98 /**
99 * @brief Percent-decode a URI component, in-place.
100 *
101 * Only decodes valid %XX sequences; leaves invalid ones as-is.
102 */
103 std::string percent_decode(std::string_view sv)
104 {
105 std::string out;
106 out.reserve(sv.size());
107 for (size_t i = 0; i < sv.size(); ++i) {
108 if (sv[i] == '%' && i + 2 < sv.size()) {
109 auto hex = [](char c) -> int {
110 if (c >= '0' && c <= '9')
111 return c - '0';
112 if (c >= 'A' && c <= 'F')
113 return c - 'A' + 10;
114 if (c >= 'a' && c <= 'f')
115 return c - 'a' + 10;
116 return -1;
117 };
118 int hi = hex(sv[i + 1]);
119 int lo = hex(sv[i + 2]);
120 if (hi >= 0 && lo >= 0) {
121 out += static_cast<char>((hi << 4) | lo);
122 i += 2;
123 continue;
124 }
125 }
126 out += sv[i];
127 }
128 return out;
129 }
130
131 /**
132 * @brief Block on @p conn until the portal Request signal arrives,
133 * then fire @p callback and return.
134 *
135 * Runs on a dedicated thread. Does not touch the backend's m_conn;
136 * receives its own private connection pointer allocated per-call.
137 */
138 void dispatch_loop(DBusConnection* conn, std::string request_path, FileDialogCallback callback)
139 {
140 while (true) {
141 const bool connected = dbus_connection_read_write(conn, 100);
142 DBusMessage* msg = dbus_connection_pop_message(conn);
143 if (!msg) {
144 if (!connected) {
145 dbus_connection_close(conn);
146 dbus_connection_unref(conn);
148 return;
149 }
150 continue;
151 }
152
153 const bool is_signal = dbus_message_get_type(msg) == DBUS_MESSAGE_TYPE_SIGNAL;
154 const bool on_request = (dbus_message_get_path(msg) == request_path);
155 const bool is_response = dbus_message_has_member(msg, k_signal_response);
156
157 if (is_signal && on_request && is_response) {
158 dbus_uint32_t response_code = 0;
159 DBusMessageIter iter;
160 dbus_message_iter_init(msg, &iter);
161 dbus_message_iter_get_basic(&iter, &response_code);
162
163 if (response_code != 0) {
164 dbus_message_unref(msg);
165 dbus_connection_close(conn);
166 dbus_connection_unref(conn);
167 callback(std::unexpected(
168 response_code == 1
171 return;
172 }
173
174 dbus_message_iter_next(&iter);
175 DBusMessageIter results;
176 dbus_message_iter_recurse(&iter, &results);
177
178 std::string chosen_path;
179
180 while (dbus_message_iter_get_arg_type(&results) == DBUS_TYPE_DICT_ENTRY) {
181 DBusMessageIter kv;
182 dbus_message_iter_recurse(&results, &kv);
183
184 const char* key = nullptr;
185 dbus_message_iter_get_basic(&kv, static_cast<void*>(&key));
186
187 if (key && std::string_view(key) == "uris") {
188 dbus_message_iter_next(&kv);
189 DBusMessageIter variant, arr;
190 dbus_message_iter_recurse(&kv, &variant);
191 dbus_message_iter_recurse(&variant, &arr);
192
193 if (dbus_message_iter_get_arg_type(&arr) == DBUS_TYPE_STRING) {
194 const char* uri = nullptr;
195 dbus_message_iter_get_basic(&arr, static_cast<void*>(&uri));
196 if (uri) {
197 std::string_view sv(uri);
198 if (sv.starts_with("file://")) {
199 sv.remove_prefix(7);
200 }
201 chosen_path = percent_decode(sv);
202 }
203 }
204 break;
205 }
206 dbus_message_iter_next(&results);
207 }
208
209 dbus_message_unref(msg);
210 dbus_connection_close(conn);
211 dbus_connection_unref(conn);
212
213 if (chosen_path.empty()) {
215 } else {
216 callback(std::filesystem::path(chosen_path));
217 }
218 return;
219 }
220
221 dbus_message_unref(msg);
222 }
223 }
224
225} // namespace
226
227// =============================================================================
228// Lifecycle
229// =============================================================================
230
231DBusBackend::~DBusBackend()
232{
233 shutdown();
234}
235
236bool DBusBackend::initialize()
237{
238 if (m_initialized) {
239 return true;
240 }
241
242 DBusError err;
243 dbus_error_init(&err);
244
245 m_conn = dbus_bus_get_private(DBUS_BUS_SESSION, &err);
246
247 if (!m_conn || dbus_error_is_set(&err)) {
249 "DBusBackend: session bus connection failed: {}",
250 dbus_error_is_set(&err) ? err.message : "unknown");
251 dbus_error_free(&err);
252 return false;
253 }
254
255 dbus_connection_set_exit_on_disconnect(m_conn, FALSE);
256
257 m_initialized = true;
259 "DBusBackend initialized");
260 return true;
261}
262
263void DBusBackend::shutdown()
264{
265 if (!m_initialized) {
266 return;
267 }
268
269 dbus_connection_close(m_conn);
270 dbus_connection_unref(m_conn);
271 m_conn = nullptr;
272 m_initialized = false;
273
275 "DBusBackend shutdown");
276}
277
278// =============================================================================
279// Dialog operations
280// =============================================================================
281
282void DBusBackend::open_file(
284 std::vector<SystemFileFilter> filters,
285 std::filesystem::path start_dir)
286{
287 invoke_portal("OpenFile", std::move(callback), filters, start_dir, {});
288}
289
290void DBusBackend::save_file(
292 std::string suggested_name,
293 std::vector<SystemFileFilter> filters,
294 std::filesystem::path start_dir)
295{
296 invoke_portal("SaveFile", std::move(callback), filters, start_dir, suggested_name);
297}
298
299void DBusBackend::invoke_portal(
300 const char* method,
302 const std::vector<SystemFileFilter>& filters,
303 const std::filesystem::path& start_dir,
304 const std::string& suggested) const
305{
306 if (!m_initialized) {
308 return;
309 }
310
311 DBusError err;
312 dbus_error_init(&err);
313
314 // Each dialog call gets its own private connection for the dispatch thread
315 DBusConnection* call_conn = dbus_bus_get_private(DBUS_BUS_SESSION, &err);
316 if (!call_conn || dbus_error_is_set(&err)) {
318 "DBusBackend: failed to open per-call connection: {}",
319 dbus_error_is_set(&err) ? err.message : "unknown");
320 dbus_error_free(&err);
322 return;
323 }
324
325 dbus_connection_set_exit_on_disconnect(call_conn, FALSE);
326
327 const std::string response_match = std::string("type='signal',interface='") + k_request_iface
328 + "',member='" + k_signal_response + "'";
329
330 dbus_bus_add_match(call_conn, response_match.c_str(), &err);
331 if (dbus_error_is_set(&err)) {
333 "DBusBackend: add_match failed: {}", err.message);
334 dbus_error_free(&err);
335 dbus_connection_close(call_conn);
336 dbus_connection_unref(call_conn);
338 return;
339 }
340
341 DBusMessage* call_msg = dbus_message_new_method_call(
342 k_portal_service, k_portal_path, k_portal_iface, method);
343
344 if (!call_msg) {
345 dbus_connection_close(call_conn);
346 dbus_connection_unref(call_conn);
348 return;
349 }
350
351 const char* parent = "";
352 const char* title = (std::string_view(method) == "SaveFile") ? "Save File" : "Open File";
353
354 DBusMessageIter args, opts;
355 dbus_message_iter_init_append(call_msg, &args);
356 dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, static_cast<const void*>(&parent));
357 dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, static_cast<const void*>(&title));
358 dbus_message_iter_open_container(&args, DBUS_TYPE_ARRAY, "{sv}", &opts);
359
360 if (!start_dir.empty()) {
361 append_path_variant(opts, "current_folder", start_dir.string());
362 }
363 if (!suggested.empty()) {
364 append_string_variant(opts, "current_name", suggested.c_str());
365 }
366
367 append_filters(opts, filters);
368
369 dbus_message_iter_close_container(&args, &opts);
370
371 DBusMessage* reply = dbus_connection_send_with_reply_and_block(call_conn, call_msg, 5000, &err);
372 dbus_message_unref(call_msg);
373
374 if (!reply || dbus_error_is_set(&err)) {
376 "DBusBackend: portal method call failed: {}",
377 dbus_error_is_set(&err) ? err.message : "no reply");
378 dbus_error_free(&err);
379 dbus_connection_close(call_conn);
380 dbus_connection_unref(call_conn);
382 return;
383 }
384
385 const char* request_path_cstr = nullptr;
386 DBusMessageIter reply_iter;
387 dbus_message_iter_init(reply, &reply_iter);
388 dbus_message_iter_get_basic(&reply_iter, static_cast<void*>(&request_path_cstr));
389 std::string request_path = request_path_cstr ? request_path_cstr : "";
390 dbus_message_unref(reply);
391
392 if (request_path.empty()) {
393 dbus_connection_close(call_conn);
394 dbus_connection_unref(call_conn);
396 return;
397 }
398
399 std::thread(dispatch_loop, call_conn, std::move(request_path), std::move(callback)).detach();
400}
401
402} // namespace MayaFlux::Core
403
404#endif // MAYAFLUX_PLATFORM_LINUX
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
std::vector< uint32_t > parent
uint32_t label
Definition Contours.cpp:16
PickCallback callback
Definition Picker.cpp:22
std::vector< float > * out
glm::vec3 value
vk::PhysicalDeviceType type
Definition VKDevice.cpp:126
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:58