Branch data Line data Source code
1 : : /*
2 : : * Copyright (C) 2019 Canonical Limited
3 : : *
4 : : * SPDX-License-Identifier: LGPL-2.1-or-later
5 : : *
6 : : * This library is free software; you can redistribute it and/or
7 : : * modify it under the terms of the GNU Lesser General Public
8 : : * License as published by the Free Software Foundation; either
9 : : * version 2.1 of the License, or (at your option) any later version.
10 : : *
11 : : * This library is distributed in the hope that it will be useful,
12 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 : : * Lesser General Public License for more details.
15 : : *
16 : : * You should have received a copy of the GNU Lesser General
17 : : * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
18 : : *
19 : : * Authors: James Henstridge <james.henstridge@canonical.com>
20 : : */
21 : :
22 : : /* A stub implementation of xdg-document-portal covering enough to
23 : : * support g_document_portal_add_documents */
24 : :
25 : : #include <glib.h>
26 : : #include <glib/glib-unix.h>
27 : : #include <gio/gio.h>
28 : : #include <gio/gunixfdlist.h>
29 : :
30 : : #include "fake-document-portal.h"
31 : : #include "fake-document-portal-generated.h"
32 : :
33 : : struct _GFakeDocumentPortalThread
34 : : {
35 : : GObject parent_instance;
36 : :
37 : : char *address; /* (not nullable) */
38 : : char *app_id; /* (nullable) */
39 : : char *mount_point; /* (not nullable) */
40 : : GList *fake_documents; /* (nullable) (owned) (element-type Gio.File) */
41 : : GCancellable *cancellable; /* (not nullable) (owned) */
42 : : GThread *thread; /* (not nullable) (owned) */
43 : : GCond cond; /* (mutex mutex) */
44 : : GMutex mutex;
45 : : gboolean ready; /* (mutex mutex) */
46 : : };
47 : :
48 : 98 : G_DEFINE_FINAL_TYPE (GFakeDocumentPortalThread, g_fake_document_portal_thread, G_TYPE_OBJECT)
49 : :
50 : : static void g_fake_document_portal_thread_finalize (GObject *object);
51 : :
52 : : static void
53 : 3 : g_fake_document_portal_thread_class_init (GFakeDocumentPortalThreadClass *klass)
54 : : {
55 : 3 : GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
56 : :
57 : 3 : gobject_class->finalize = g_fake_document_portal_thread_finalize;
58 : 3 : }
59 : :
60 : : static void
61 : 11 : g_fake_document_portal_thread_init (GFakeDocumentPortalThread *self)
62 : : {
63 : 11 : self->cancellable = g_cancellable_new ();
64 : 11 : g_cond_init (&self->cond);
65 : 11 : g_mutex_init (&self->mutex);
66 : 11 : }
67 : :
68 : : static void
69 : 11 : g_fake_document_portal_thread_finalize (GObject *object)
70 : : {
71 : 11 : GFakeDocumentPortalThread *self = G_FAKE_DOCUMENT_PORTAL_THREAD (object);
72 : :
73 : 11 : g_assert (self->thread == NULL); /* should already have been joined */
74 : :
75 : 11 : g_mutex_clear (&self->mutex);
76 : 11 : g_cond_clear (&self->cond);
77 : 11 : g_clear_object (&self->cancellable);
78 : 11 : g_clear_pointer (&self->mount_point, g_free);
79 : 11 : g_clear_pointer (&self->address, g_free);
80 : 11 : g_clear_pointer (&self->app_id, g_free);
81 : 11 : g_clear_list (&self->fake_documents, g_object_unref);
82 : :
83 : 11 : G_OBJECT_CLASS (g_fake_document_portal_thread_parent_class)->finalize (object);
84 : 11 : }
85 : :
86 : : static gboolean
87 : 11 : on_handle_get_mount_point (FakeDocuments *object,
88 : : GDBusMethodInvocation *invocation,
89 : : gpointer user_data)
90 : : {
91 : 11 : GFakeDocumentPortalThread *self = G_FAKE_DOCUMENT_PORTAL_THREAD (user_data);
92 : :
93 : 11 : fake_documents_complete_get_mount_point (object,
94 : : invocation,
95 : 11 : self->mount_point);
96 : 11 : return TRUE;
97 : : }
98 : :
99 : : static gboolean
100 : 8 : on_handle_add_full (FakeDocuments *object,
101 : : GDBusMethodInvocation *invocation,
102 : : GUnixFDList *o_path_fds,
103 : : GVariant *o_path_fd,
104 : : guint flags,
105 : : const gchar *app_id,
106 : : const gchar * const *permissions,
107 : : gpointer user_data)
108 : : {
109 : 8 : GFakeDocumentPortalThread *self = G_FAKE_DOCUMENT_PORTAL_THREAD (user_data);
110 : 8 : gchar **doc_ids = NULL;
111 : 8 : GVariant *extra_out = NULL;
112 : : gsize length, i;
113 : :
114 : 8 : if (o_path_fds != NULL)
115 : 8 : length = g_unix_fd_list_get_length (o_path_fds);
116 : : else
117 : 0 : length = 0;
118 : :
119 : 8 : if (self->app_id)
120 : 8 : g_assert_cmpstr (self->app_id, ==, app_id);
121 : :
122 : 8 : doc_ids = g_new0 (gchar *, length + 1 /* NULL terminator */);
123 : 19 : for (i = 0; i < length; i++)
124 : : {
125 : : GFile *file;
126 : : GFile *file_dir;
127 : 11 : GError *local_error = NULL;
128 : : GFileIOStream *stream;
129 : : char *filename;
130 : : char *basename;
131 : : int fd;
132 : :
133 : 11 : doc_ids[i] = g_strdup_printf ("document-id-%" G_GSIZE_FORMAT, i);
134 : :
135 : 11 : if (g_str_equal (self->app_id, G_FAKE_DOCUMENT_PORTAL_NO_CREATE_DIR_APP_ID))
136 : 2 : continue;
137 : :
138 : 10 : g_test_message ("Creating Document ID %s folder", doc_ids[i]);
139 : :
140 : 10 : file_dir = g_file_new_build_filename (self->mount_point, doc_ids[i], NULL);
141 : 10 : g_file_make_directory (file_dir, self->cancellable, &local_error);
142 : 10 : g_assert_no_error (local_error);
143 : :
144 : 10 : if (g_str_equal (self->app_id, G_FAKE_DOCUMENT_PORTAL_NO_CREATE_FILE_APP_ID))
145 : : {
146 : 1 : g_clear_object (&file_dir);
147 : 1 : continue;
148 : : }
149 : :
150 : 9 : fd = g_unix_fd_list_get (o_path_fds, i, &local_error);
151 : 9 : g_assert_no_error (local_error);
152 : :
153 : 9 : filename = g_unix_fd_query_path (fd, &local_error);
154 : 9 : g_assert_no_error (local_error);
155 : :
156 : 9 : g_test_message ("Creating Document ID %s mapped to FD %d (%s)",
157 : 9 : doc_ids[i], fd, filename);
158 : :
159 : 9 : basename = g_path_get_basename (filename);
160 : 9 : g_clear_pointer (&filename, g_free);
161 : :
162 : 9 : file = g_file_get_child (file_dir, basename);
163 : 9 : stream = g_file_create_readwrite (file, G_FILE_CREATE_NONE,
164 : : self->cancellable, &local_error);
165 : 9 : g_assert_no_error (local_error);
166 : :
167 : 9 : g_io_stream_close (G_IO_STREAM (stream), self->cancellable, &local_error);
168 : 9 : g_assert_no_error (local_error);
169 : :
170 : 9 : self->fake_documents = g_list_prepend (self->fake_documents,
171 : : g_steal_pointer (&file));
172 : :
173 : 9 : g_clear_error (&local_error);
174 : 9 : g_clear_object (&file_dir);
175 : 9 : g_clear_object (&stream);
176 : 9 : g_clear_pointer (&basename, g_free);
177 : : }
178 : 8 : extra_out = g_variant_new_array (G_VARIANT_TYPE ("{sv}"), NULL, 0);
179 : :
180 : 8 : fake_documents_complete_add_full (object,
181 : : invocation,
182 : : NULL,
183 : : (const char **) doc_ids,
184 : : extra_out);
185 : :
186 : 8 : g_strfreev (doc_ids);
187 : :
188 : 8 : return TRUE;
189 : : }
190 : :
191 : : static void
192 : 11 : on_name_acquired (GDBusConnection *connection,
193 : : const gchar *name,
194 : : gpointer user_data)
195 : : {
196 : 11 : GFakeDocumentPortalThread *self = G_FAKE_DOCUMENT_PORTAL_THREAD (user_data);
197 : :
198 : 11 : g_test_message ("Acquired the name %s", name);
199 : :
200 : 11 : g_mutex_lock (&self->mutex);
201 : 11 : self->ready = TRUE;
202 : 11 : g_cond_signal (&self->cond);
203 : 11 : g_mutex_unlock (&self->mutex);
204 : 11 : }
205 : :
206 : : static void
207 : 0 : on_name_lost (GDBusConnection *connection,
208 : : const gchar *name,
209 : : gpointer user_data)
210 : : {
211 : 0 : g_test_message ("Lost the name %s", name);
212 : 0 : }
213 : :
214 : : static gboolean
215 : 11 : cancelled_cb (GCancellable *cancellable,
216 : : gpointer user_data)
217 : : {
218 : 11 : g_test_message ("fake-document-portal cancelled");
219 : 11 : return G_SOURCE_CONTINUE;
220 : : }
221 : :
222 : : static gpointer
223 : 11 : fake_document_portal_thread_cb (gpointer user_data)
224 : : {
225 : 11 : GFakeDocumentPortalThread *self = G_FAKE_DOCUMENT_PORTAL_THREAD (user_data);
226 : 11 : GMainContext *context = NULL;
227 : 11 : GDBusConnection *connection = NULL;
228 : 11 : GSource *source = NULL;
229 : : guint id;
230 : 11 : FakeDocuments *interface = NULL;
231 : 11 : GError *local_error = NULL;
232 : : char *tmpdir;
233 : :
234 : 11 : tmpdir = g_dir_make_tmp ("fake-document-portal-XXXXXXX", &local_error);
235 : 11 : self->mount_point = g_build_filename (tmpdir, "documents", NULL);
236 : 11 : g_mkdir (self->mount_point, 0700);
237 : 11 : g_assert_no_error (local_error);
238 : 11 : g_test_message ("Created mount point %s", self->mount_point);
239 : :
240 : 11 : context = g_main_context_new ();
241 : 11 : g_main_context_push_thread_default (context);
242 : :
243 : 11 : connection = g_dbus_connection_new_for_address_sync (self->address,
244 : : G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
245 : : G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION,
246 : : NULL,
247 : : self->cancellable,
248 : : &local_error);
249 : 11 : g_assert_no_error (local_error);
250 : :
251 : : /* Listen for cancellation. The source will wake up the context iteration
252 : : * which can then re-check its exit condition below. */
253 : 11 : source = g_cancellable_source_new (self->cancellable);
254 : 11 : g_source_set_callback (source, G_SOURCE_FUNC (cancelled_cb), NULL, NULL);
255 : 11 : g_source_attach (source, context);
256 : 11 : g_source_unref (source);
257 : :
258 : : /* Set up the interface */
259 : 11 : g_test_message ("Acquired a message bus connection");
260 : :
261 : 11 : interface = fake_documents_skeleton_new ();
262 : 11 : g_signal_connect_object (interface,
263 : : "handle-get-mount-point",
264 : : G_CALLBACK (on_handle_get_mount_point),
265 : : self, G_CONNECT_DEFAULT);
266 : 11 : g_signal_connect_object (interface,
267 : : "handle-add-full",
268 : : G_CALLBACK (on_handle_add_full),
269 : : self, G_CONNECT_DEFAULT);
270 : :
271 : 11 : g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (interface),
272 : : connection,
273 : : "/org/freedesktop/portal/documents",
274 : : &local_error);
275 : 11 : g_assert_no_error (local_error);
276 : :
277 : : /* Own the portal name */
278 : 11 : id = g_bus_own_name_on_connection (connection,
279 : : "org.freedesktop.portal.Documents",
280 : : G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT |
281 : : G_BUS_NAME_OWNER_FLAGS_REPLACE,
282 : : on_name_acquired,
283 : : on_name_lost,
284 : : self,
285 : : NULL);
286 : :
287 : 61 : while (!g_cancellable_is_cancelled (self->cancellable))
288 : 50 : g_main_context_iteration (context, TRUE);
289 : :
290 : : // Remove the stuff we've created.
291 : 20 : for (GList *l = self->fake_documents; l; l = l->next)
292 : : {
293 : 9 : GFile *file = G_FILE (l->data);
294 : 9 : GFile *parent_dir = g_file_get_parent (file);
295 : :
296 : 9 : g_file_delete (file, NULL, &local_error);
297 : 9 : g_assert_no_error (local_error);
298 : :
299 : 9 : g_file_delete (parent_dir, NULL, &local_error);
300 : 9 : g_assert_no_error (local_error);
301 : :
302 : 9 : g_clear_object (&parent_dir);
303 : : }
304 : 11 : g_rmdir (self->mount_point);
305 : 11 : g_assert_no_errno (errno);
306 : :
307 : 11 : g_bus_unown_name (id);
308 : 11 : g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (interface));
309 : 11 : g_clear_object (&interface);
310 : 11 : g_clear_object (&connection);
311 : 11 : g_main_context_pop_thread_default (context);
312 : 11 : g_clear_pointer (&context, g_main_context_unref);
313 : 11 : g_free (tmpdir);
314 : :
315 : 11 : return NULL;
316 : : }
317 : :
318 : : /*
319 : : * Create a new #GFakeDocumentPortalThread. The thread isn’t started until
320 : : * g_fake_document_portal_thread_run() is called on it.
321 : : *
322 : : * Returns: (transfer full): the new fake document portal wrapper
323 : : */
324 : : GFakeDocumentPortalThread *
325 : 11 : g_fake_document_portal_thread_new (const char *address,
326 : : const char *app_id)
327 : : {
328 : 11 : GFakeDocumentPortalThread *self = g_object_new (G_TYPE_FAKE_DOCUMENT_PORTAL_THREAD, NULL);
329 : 11 : self->address = g_strdup (address);
330 : 11 : self->app_id = g_strdup (app_id);
331 : 11 : return g_steal_pointer (&self);
332 : : }
333 : :
334 : : /*
335 : : * Start a worker thread which will run a fake
336 : : * `org.freedesktop.portal.Documents` portal on the bus at @address. This is
337 : : * intended to be used with #GTestDBus to mock up a portal from within a unit
338 : : * test process, rather than relying on D-Bus activation of a mock portal
339 : : * subprocess.
340 : : *
341 : : * It blocks until the thread has owned its D-Bus name and is ready to handle
342 : : * requests.
343 : : */
344 : : void
345 : 11 : g_fake_document_portal_thread_run (GFakeDocumentPortalThread *self)
346 : : {
347 : 11 : g_return_if_fail (G_IS_FAKE_DOCUMENT_PORTAL_THREAD (self));
348 : 11 : g_return_if_fail (self->thread == NULL);
349 : :
350 : 11 : self->thread = g_thread_new ("fake-document-portal", fake_document_portal_thread_cb, self);
351 : :
352 : : /* Block until the thread is ready. */
353 : 11 : g_mutex_lock (&self->mutex);
354 : 22 : while (!self->ready)
355 : 11 : g_cond_wait (&self->cond, &self->mutex);
356 : 11 : g_mutex_unlock (&self->mutex);
357 : : }
358 : :
359 : : /* Stop and join a worker thread started with fake_document_portal_thread_run().
360 : : * Blocks until the thread has stopped and joined.
361 : : *
362 : : * Once this has been called, it’s safe to drop the final reference on @self. */
363 : : void
364 : 11 : g_fake_document_portal_thread_stop (GFakeDocumentPortalThread *self)
365 : : {
366 : 11 : g_return_if_fail (G_IS_FAKE_DOCUMENT_PORTAL_THREAD (self));
367 : 11 : g_return_if_fail (self->thread != NULL);
368 : :
369 : 11 : g_cancellable_cancel (self->cancellable);
370 : 11 : g_thread_join (g_steal_pointer (&self->thread));
371 : : }
372 : :
373 : : /* Gets the thread mount point, this is valid only after the thread has been
374 : : * started. We do not care about thread-safety here since this is a value
375 : : * that is set on thread start-up.
376 : : */
377 : : const char *
378 : 7 : g_fake_document_portal_thread_get_mount_point (GFakeDocumentPortalThread *self)
379 : : {
380 : : const char *mount_point;
381 : :
382 : 7 : g_return_val_if_fail (G_IS_FAKE_DOCUMENT_PORTAL_THREAD (self), NULL);
383 : 7 : g_return_val_if_fail (self->thread != NULL, NULL);
384 : :
385 : 7 : g_mutex_lock (&self->mutex);
386 : 7 : mount_point = self->mount_point;
387 : 7 : g_mutex_unlock (&self->mutex);
388 : :
389 : 7 : return mount_point;
390 : : }
|