Branch data Line data Source code
1 : : /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
2 : : *
3 : : * Copyright © 2017, 2018 Endless Mobile, Inc.
4 : : *
5 : : * This library is free software; you can redistribute it and/or
6 : : * modify it under the terms of the GNU Lesser General Public
7 : : * License as published by the Free Software Foundation; either
8 : : * version 2.1 of the License, or (at your option) any later version.
9 : : *
10 : : * This library is distributed in the hope that it will be useful,
11 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 : : * Lesser General Public License for more details.
14 : : *
15 : : * You should have received a copy of the GNU Lesser General Public
16 : : * License along with this library; if not, write to the Free Software
17 : : * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 : : *
19 : : * Authors:
20 : : * - Philip Withnall <withnall@endlessm.com>
21 : : */
22 : :
23 : : #include "config.h"
24 : :
25 : : #include <glib.h>
26 : : #include <glib-object.h>
27 : : #include <glib/gi18n-lib.h>
28 : : #include <gio/gio.h>
29 : : #include <libmogwai-schedule/clock.h>
30 : : #include <libmogwai-schedule/connection-monitor.h>
31 : : #include <libmogwai-schedule/peer-manager.h>
32 : : #include <libmogwai-schedule/schedule-entry.h>
33 : : #include <libmogwai-schedule/scheduler.h>
34 : :
35 : :
36 : : /* These errors do go over the bus, and are registered in schedule-service.c. */
37 [ + + ]: 14 : G_DEFINE_QUARK (MwsSchedulerError, mws_scheduler_error)
38 : :
39 : : /* Cached state for a schedule entry, including its current active state, and
40 : : * any calculated state which is not trivially derivable from the properties of
41 : : * the #MwsScheduleEntry itself. */
42 : : typedef struct
43 : : {
44 : : gboolean is_active;
45 : : } EntryData;
46 : :
47 : : static EntryData *entry_data_new (void);
48 : : static void entry_data_free (EntryData *data);
49 : :
50 [ - + ]: 116 : G_DEFINE_AUTOPTR_CLEANUP_FUNC (EntryData, entry_data_free);
51 : :
52 : : /* Create a new #EntryData struct with default values. */
53 : : static EntryData *
54 : 58 : entry_data_new (void)
55 : : {
56 : 116 : g_autoptr(EntryData) data = g_new0 (EntryData, 1);
57 : 58 : return g_steal_pointer (&data);
58 : : }
59 : :
60 : : static void
61 : 58 : entry_data_free (EntryData *data)
62 : : {
63 : 58 : g_free (data);
64 : 58 : }
65 : :
66 : : static void mws_scheduler_constructed (GObject *object);
67 : : static void mws_scheduler_dispose (GObject *object);
68 : :
69 : : static void mws_scheduler_get_property (GObject *object,
70 : : guint property_id,
71 : : GValue *value,
72 : : GParamSpec *pspec);
73 : : static void mws_scheduler_set_property (GObject *object,
74 : : guint property_id,
75 : : const GValue *value,
76 : : GParamSpec *pspec);
77 : :
78 : : static void connection_monitor_connections_changed_cb (MwsConnectionMonitor *connection_monitor,
79 : : GPtrArray *added,
80 : : GPtrArray *removed,
81 : : gpointer user_data);
82 : : static void connection_monitor_connection_details_changed_cb (MwsConnectionMonitor *connection_monitor,
83 : : const gchar *connection_id,
84 : : gpointer user_data);
85 : : static void peer_manager_peer_vanished_cb (MwsPeerManager *manager,
86 : : const gchar *name,
87 : : gpointer user_data);
88 : : static void clock_offset_changed_cb (MwsClock *clock,
89 : : gpointer user_data);
90 : :
91 : : /**
92 : : * MwsScheduler:
93 : : *
94 : : * A scheduler object which stores a set of #MwsScheduleEntrys and allows
95 : : * managing them using bulk add and remove operations. It looks at their
96 : : * properties and the current network status and schedules them appropriately.
97 : : *
98 : : * Since: 0.1.0
99 : : */
100 : : struct _MwsScheduler
101 : : {
102 : : GObject parent;
103 : :
104 : : /* Scheduling data sources. */
105 : : MwsConnectionMonitor *connection_monitor; /* (owned) */
106 : : MwsPeerManager *peer_manager; /* (owned) */
107 : : MwsClock *clock; /* (owned) */
108 : :
109 : : /* Time tracking. */
110 : : guint reschedule_alarm_id; /* 0 when no reschedule is scheduled */
111 : :
112 : : /* Mapping from entry ID to (not nullable) entry. */
113 : : GHashTable *entries; /* (owned) (element-type utf8 MwsScheduleEntry) */
114 : : gsize max_entries;
115 : :
116 : : /* Mapping from entry ID to (not nullable) entry data. We can’t use the same
117 : : * hash table as @entries since we need to be able to return that one in
118 : : * mws_scheduler_get_entries(). Always has the same set of keys as @entries. */
119 : : GHashTable *entries_data; /* (owned) (element-type utf8 EntryData) */
120 : :
121 : : /* Maximum number of downloads allowed to be active at the same time. */
122 : : guint max_active_entries;
123 : :
124 : : /* Cache of some of the connection data used by our properties. */
125 : : gboolean cached_allow_downloads;
126 : :
127 : : /* Sanity check that we don’t reschedule re-entrantly. */
128 : : gboolean in_reschedule;
129 : : };
130 : :
131 : : /* Arbitrarily chosen. */
132 : : static const gsize DEFAULT_MAX_ENTRIES = 1024;
133 : : /* Chosen for a few reasons:
134 : : * 1. OSTree app updates and installs take ungodly amounts of I/O and CPU —
135 : : * doing more than one of these at a time in the background is an
136 : : * aggressively bad UX
137 : : * 2. Over-parallelisation causes bandwidth hogging and reduces the amount of
138 : : * bandwidth available for foreground applications or user interactivity
139 : : * 3. We don’t want head-of-line blocking by large OS updates to block smaller,
140 : : * more-regular content updates.
141 : : */
142 : : static const guint DEFAULT_MAX_ACTIVE_ENTRIES = 1;
143 : :
144 : : typedef enum
145 : : {
146 : : PROP_ENTRIES = 1,
147 : : PROP_MAX_ENTRIES,
148 : : PROP_CONNECTION_MONITOR,
149 : : PROP_MAX_ACTIVE_ENTRIES,
150 : : PROP_PEER_MANAGER,
151 : : PROP_ALLOW_DOWNLOADS,
152 : : PROP_CLOCK,
153 : : } MwsSchedulerProperty;
154 : :
155 [ + + + - : 1087 : G_DEFINE_TYPE (MwsScheduler, mws_scheduler, G_TYPE_OBJECT)
+ + ]
156 : :
157 : : static void
158 : 2 : mws_scheduler_class_init (MwsSchedulerClass *klass)
159 : : {
160 : 2 : GObjectClass *object_class = (GObjectClass *) klass;
161 : 2 : GParamSpec *props[PROP_CLOCK + 1] = { NULL, };
162 : :
163 : 2 : object_class->constructed = mws_scheduler_constructed;
164 : 2 : object_class->dispose = mws_scheduler_dispose;
165 : 2 : object_class->get_property = mws_scheduler_get_property;
166 : 2 : object_class->set_property = mws_scheduler_set_property;
167 : :
168 : : /**
169 : : * MwsScheduler:entries: (type GHashTable(utf8,MwsScheduleEntry)) (transfer none)
170 : : *
171 : : * Set of schedule entries known to the scheduler, which might be empty. It is
172 : : * a mapping from entry ID to #MwsScheduleEntry instance. Use
173 : : * mws_scheduler_update_entries() to modify the mapping.
174 : : *
175 : : * Since: 0.1.0
176 : : */
177 : 2 : props[PROP_ENTRIES] =
178 : 2 : g_param_spec_boxed ("entries", "Entries",
179 : : "Set of schedule entries known to the scheduler.",
180 : : G_TYPE_HASH_TABLE,
181 : : G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
182 : :
183 : : /**
184 : : * MwsScheduler:max-entries:
185 : : *
186 : : * Maximum number of schedule entries which can be present in the scheduler at
187 : : * any time. This is not a limit on the number of active schedule entries. It
188 : : * exists to make explicit the avoidance of array bounds overflows in the
189 : : * scheduler. It should be considered high enough to not be reached apart from
190 : : * in exception circumstances.
191 : : *
192 : : * Since: 0.1.0
193 : : */
194 : 2 : props[PROP_MAX_ENTRIES] =
195 : 2 : g_param_spec_uint ("max-entries", "Max. Entries",
196 : : "Maximum number of schedule entries present in the scheduler.",
197 : : 0, G_MAXUINT, DEFAULT_MAX_ENTRIES,
198 : : G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
199 : :
200 : : /**
201 : : * MwsScheduler:connection-monitor:
202 : : *
203 : : * A #MwsConnectionMonitor instance to provide information about the currently
204 : : * active network connections which is relevant to scheduling downloads, such
205 : : * as whether they are currently metered, how much of their capacity has been
206 : : * used in the current time period, or any user-provided policies for them.
207 : : *
208 : : * Since: 0.1.0
209 : : */
210 : 2 : props[PROP_CONNECTION_MONITOR] =
211 : 2 : g_param_spec_object ("connection-monitor", "Connection Monitor",
212 : : "A #MwsConnectionMonitor instance to provide "
213 : : "information about the currently active network connections.",
214 : : MWS_TYPE_CONNECTION_MONITOR,
215 : : G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
216 : :
217 : : /**
218 : : * MwsScheduler:max-active-entries:
219 : : *
220 : : * Maximum number of schedule entries which can be active at any time. This
221 : : * effectively limits the parallelisation of the scheduler. In contrast with
222 : : * #MwsScheduler:max-entries, this limit is expected to be reached routinely.
223 : : *
224 : : * Since: 0.1.0
225 : : */
226 : 2 : props[PROP_MAX_ACTIVE_ENTRIES] =
227 : 2 : g_param_spec_uint ("max-active-entries", "Max. Active Entries",
228 : : "Maximum number of schedule entries which can be active at any time.",
229 : : 1, G_MAXUINT, DEFAULT_MAX_ACTIVE_ENTRIES,
230 : : G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
231 : :
232 : : /**
233 : : * MwsScheduler:peer-manager:
234 : : *
235 : : * A #MwsPeerManager instance to provide information about the peers who are
236 : : * adding schedule entries to the scheduler. (Typically, these are D-Bus peers
237 : : * using the scheduler’s D-Bus interface.)
238 : : *
239 : : * Since: 0.1.0
240 : : */
241 : 2 : props[PROP_PEER_MANAGER] =
242 : 2 : g_param_spec_object ("peer-manager", "Peer Manager",
243 : : "A #MwsPeerManager instance to provide information "
244 : : "about the peers who are adding schedule entries to "
245 : : "the scheduler.",
246 : : MWS_TYPE_PEER_MANAGER,
247 : : G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
248 : :
249 : : /**
250 : : * MwsScheduler:allow-downloads:
251 : : *
252 : : * Whether any of the currently active network connections are configured to
253 : : * allow any large downloads. It is up to the clients which use Mogwai to
254 : : * decide what ’large’ is.
255 : : *
256 : : * This is not a guarantee that a schedule entry
257 : : * will be scheduled; it is a reflection of the user’s intent for the use of
258 : : * the currently active network connections, intended to be used in UIs to
259 : : * remind the user of how they have configured the network.
260 : : *
261 : : * Programs must not use this value to check whether to schedule an entry.
262 : : * Schedule the entry unconditionally; the scheduler will work out whether
263 : : * (and when) to download the entry.
264 : : *
265 : : * Since: 0.1.0
266 : : */
267 : 2 : props[PROP_ALLOW_DOWNLOADS] =
268 : 2 : g_param_spec_boolean ("allow-downloads", "Allow Downloads",
269 : : "Whether any of the currently active network "
270 : : "connections are configured to allow any large "
271 : : "downloads.",
272 : : TRUE,
273 : : G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
274 : :
275 : : /**
276 : : * MwsScheduler:clock:
277 : : *
278 : : * A #MwsClock instance to provide wall clock timing and alarms for time-based
279 : : * scheduling. Typically, this is provided by a #MwsClockSystem, using the
280 : : * system clock.
281 : : *
282 : : * Since: 0.1.0
283 : : */
284 : 2 : props[PROP_CLOCK] =
285 : 2 : g_param_spec_object ("clock", "Clock",
286 : : "A #MwsClock instance to provide wall clock timing "
287 : : "and alarms for time-based scheduling.",
288 : : MWS_TYPE_CLOCK,
289 : : G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
290 : :
291 : 2 : g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props);
292 : :
293 : : /**
294 : : * MwsScheduler::entries-changed:
295 : : * @self: a #MwsScheduler
296 : : * @added: (element-type MwsScheduleEntry) (nullable): potentially empty or
297 : : * %NULL array of added entries (a %NULL array is equivalent to an empty one)
298 : : * @removed: (element-type MwsScheduleEntry) (nullable): potentially empty or
299 : : * %NULL array of removed entries (a %NULL array is equivalent to an empty one)
300 : : *
301 : : * Emitted when the set of schedule entries known to the scheduler changes. It
302 : : * is emitted at the same time as #GObject::notify for the
303 : : * #MwsScheduler:entries property, but contains the delta of which
304 : : * entries have been added and removed.
305 : : *
306 : : * There will be at least one entry in one of the arrays.
307 : : *
308 : : * Since: 0.1.0
309 : : */
310 : 2 : g_signal_new ("entries-changed", G_TYPE_FROM_CLASS (klass),
311 : : G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
312 : : G_TYPE_NONE, 2, G_TYPE_PTR_ARRAY, G_TYPE_PTR_ARRAY);
313 : :
314 : : /**
315 : : * MwsScheduler::active-entries-changed:
316 : : * @self: a #MwsScheduler
317 : : * @added: (element-type MwsScheduleEntry) (nullable): potentially empty or
318 : : * %NULL array of newly-active entries (a %NULL array is equivalent to an
319 : : * empty one)
320 : : * @removed: (element-type MwsScheduleEntry) (nullable): potentially empty or
321 : : * %NULL array of newly-inactive entries (a %NULL array is equivalent to
322 : : * an empty one)
323 : : *
324 : : * Emitted when the set of active entries changes; i.e. when an entry is
325 : : * allowed to start downloading, or when one is requested to stop downloading.
326 : : *
327 : : * There will be at least one entry in one of the arrays.
328 : : *
329 : : * Since: 0.1.0
330 : : */
331 : 2 : g_signal_new ("active-entries-changed", G_TYPE_FROM_CLASS (klass),
332 : : G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
333 : : G_TYPE_NONE, 2, G_TYPE_PTR_ARRAY, G_TYPE_PTR_ARRAY);
334 : 2 : }
335 : :
336 : : static void
337 : 48 : mws_scheduler_init (MwsScheduler *self)
338 : : {
339 : 48 : self->entries = g_hash_table_new_full (g_str_hash, g_str_equal,
340 : : NULL, g_object_unref);
341 : 48 : self->max_entries = DEFAULT_MAX_ENTRIES;
342 : 48 : self->entries_data = g_hash_table_new_full (g_str_hash, g_str_equal,
343 : : NULL, (GDestroyNotify) entry_data_free);
344 : 48 : self->max_active_entries = DEFAULT_MAX_ACTIVE_ENTRIES;
345 : 48 : }
346 : :
347 : : static void
348 : 48 : mws_scheduler_constructed (GObject *object)
349 : : {
350 : 48 : MwsScheduler *self = MWS_SCHEDULER (object);
351 : :
352 : 48 : G_OBJECT_CLASS (mws_scheduler_parent_class)->constructed (object);
353 : :
354 : : /* Check we have our construction properties. */
355 [ - + ]: 48 : g_assert (MWS_IS_CONNECTION_MONITOR (self->connection_monitor));
356 [ - + ]: 48 : g_assert (MWS_IS_PEER_MANAGER (self->peer_manager));
357 [ - + ]: 48 : g_assert (MWS_IS_CLOCK (self->clock));
358 : :
359 : : /* Connect to signals from the connection monitor, which will trigger
360 : : * rescheduling. */
361 : 48 : g_signal_connect (self->connection_monitor, "connections-changed",
362 : : (GCallback) connection_monitor_connections_changed_cb, self);
363 : 48 : g_signal_connect (self->connection_monitor, "connection-details-changed",
364 : : (GCallback) connection_monitor_connection_details_changed_cb, self);
365 : :
366 : : /* Connect to signals from the peer manager, which will trigger removal of
367 : : * entries when a peer disappears. */
368 : 48 : g_signal_connect (self->peer_manager, "peer-vanished",
369 : : (GCallback) peer_manager_peer_vanished_cb, self);
370 : :
371 : : /* Connect to signals from the clock, which will trigger rescheduling when
372 : : * the clock offset changes. */
373 : 48 : g_signal_connect (self->clock, "offset-changed",
374 : : (GCallback) clock_offset_changed_cb, self);
375 : :
376 : : /* Initialise self->cached_allow_downloads. */
377 : 48 : mws_scheduler_reschedule (self);
378 : 48 : }
379 : :
380 : : static void
381 : 47 : mws_scheduler_dispose (GObject *object)
382 : : {
383 : 47 : MwsScheduler *self = MWS_SCHEDULER (object);
384 : :
385 [ + - ]: 47 : g_clear_pointer (&self->entries, g_hash_table_unref);
386 [ + - ]: 47 : g_clear_pointer (&self->entries_data, g_hash_table_unref);
387 : :
388 [ + - ]: 47 : if (self->connection_monitor != NULL)
389 : : {
390 : 47 : g_signal_handlers_disconnect_by_func (self->connection_monitor,
391 : : connection_monitor_connections_changed_cb,
392 : : self);
393 : 47 : g_signal_handlers_disconnect_by_func (self->connection_monitor,
394 : : connection_monitor_connection_details_changed_cb,
395 : : self);
396 : : }
397 : :
398 [ + - ]: 47 : g_clear_object (&self->connection_monitor);
399 : :
400 [ + - ]: 47 : if (self->peer_manager != NULL)
401 : : {
402 : 47 : g_signal_handlers_disconnect_by_func (self->peer_manager,
403 : : peer_manager_peer_vanished_cb,
404 : : self);
405 : : }
406 : :
407 [ + - ]: 47 : g_clear_object (&self->peer_manager);
408 : :
409 [ + - - + ]: 47 : if (self->clock != NULL && self->reschedule_alarm_id != 0)
410 : : {
411 : 0 : mws_clock_remove_alarm (self->clock, self->reschedule_alarm_id);
412 : 0 : self->reschedule_alarm_id = 0;
413 : : }
414 : :
415 [ + - ]: 47 : if (self->clock != NULL)
416 : : {
417 : 47 : g_signal_handlers_disconnect_by_func (self->clock,
418 : : clock_offset_changed_cb,
419 : : self);
420 : : }
421 : :
422 [ + - ]: 47 : g_clear_object (&self->clock);
423 : :
424 [ - + ]: 47 : g_assert (!self->in_reschedule);
425 : :
426 : : /* Chain up to the parent class */
427 : 47 : G_OBJECT_CLASS (mws_scheduler_parent_class)->dispose (object);
428 : 47 : }
429 : :
430 : : static void
431 : 10 : mws_scheduler_get_property (GObject *object,
432 : : guint property_id,
433 : : GValue *value,
434 : : GParamSpec *pspec)
435 : : {
436 : 10 : MwsScheduler *self = MWS_SCHEDULER (object);
437 : :
438 [ + + + + : 10 : switch ((MwsSchedulerProperty) property_id)
+ + + - ]
439 : : {
440 : 1 : case PROP_ENTRIES:
441 : 1 : g_value_set_boxed (value, self->entries);
442 : 1 : break;
443 : 2 : case PROP_MAX_ENTRIES:
444 : 2 : g_value_set_uint (value, self->max_entries);
445 : 2 : break;
446 : 1 : case PROP_CONNECTION_MONITOR:
447 : 1 : g_value_set_object (value, self->connection_monitor);
448 : 1 : break;
449 : 3 : case PROP_MAX_ACTIVE_ENTRIES:
450 : 3 : g_value_set_uint (value, self->max_active_entries);
451 : 3 : break;
452 : 1 : case PROP_PEER_MANAGER:
453 : 1 : g_value_set_object (value, self->peer_manager);
454 : 1 : break;
455 : 1 : case PROP_ALLOW_DOWNLOADS:
456 : 1 : g_value_set_boolean (value, mws_scheduler_get_allow_downloads (self));
457 : 1 : break;
458 : 1 : case PROP_CLOCK:
459 : 1 : g_value_set_object (value, self->clock);
460 : 1 : break;
461 : 0 : default:
462 : 0 : g_assert_not_reached ();
463 : : }
464 : 10 : }
465 : :
466 : : static void
467 : 240 : mws_scheduler_set_property (GObject *object,
468 : : guint property_id,
469 : : const GValue *value,
470 : : GParamSpec *pspec)
471 : : {
472 : 240 : MwsScheduler *self = MWS_SCHEDULER (object);
473 : :
474 [ - + + + : 240 : switch ((MwsSchedulerProperty) property_id)
+ + - ]
475 : : {
476 : 0 : case PROP_ENTRIES:
477 : : case PROP_ALLOW_DOWNLOADS:
478 : : /* Read only. */
479 : 0 : g_assert_not_reached ();
480 : : break;
481 : 48 : case PROP_MAX_ENTRIES:
482 : : /* Construct only. */
483 : 48 : self->max_entries = g_value_get_uint (value);
484 : 48 : break;
485 : 48 : case PROP_CONNECTION_MONITOR:
486 : : /* Construct only. */
487 [ - + ]: 48 : g_assert (self->connection_monitor == NULL);
488 : 48 : self->connection_monitor = g_value_dup_object (value);
489 : 48 : break;
490 : 48 : case PROP_MAX_ACTIVE_ENTRIES:
491 : : /* Construct only. */
492 : 48 : self->max_active_entries = g_value_get_uint (value);
493 : 48 : break;
494 : 48 : case PROP_PEER_MANAGER:
495 : : /* Construct only. */
496 [ - + ]: 48 : g_assert (self->peer_manager == NULL);
497 : 48 : self->peer_manager = g_value_dup_object (value);
498 : 48 : break;
499 : 48 : case PROP_CLOCK:
500 : : /* Construct only. */
501 [ - + ]: 48 : g_assert (self->clock == NULL);
502 : 48 : self->clock = g_value_dup_object (value);
503 : 48 : break;
504 : 0 : default:
505 : 0 : g_assert_not_reached ();
506 : : }
507 : 240 : }
508 : :
509 : : static void
510 : 27 : connection_monitor_connections_changed_cb (MwsConnectionMonitor *connection_monitor,
511 : : GPtrArray *added,
512 : : GPtrArray *removed,
513 : : gpointer user_data)
514 : : {
515 : 27 : MwsScheduler *self = MWS_SCHEDULER (user_data);
516 : :
517 : : /* This needs to update self->cached_allow_downloads too. */
518 [ + - + - ]: 27 : g_debug ("%s: Connections changed (%u added, %u removed)",
519 : : G_STRFUNC, (added != NULL) ? added->len : 0,
520 : : (removed != NULL) ? removed->len : 0);
521 : 27 : mws_scheduler_reschedule (self);
522 : 27 : }
523 : :
524 : : static void
525 : 70 : connection_monitor_connection_details_changed_cb (MwsConnectionMonitor *connection_monitor,
526 : : const gchar *connection_id,
527 : : gpointer user_data)
528 : : {
529 : 70 : MwsScheduler *self = MWS_SCHEDULER (user_data);
530 : :
531 : : /* This needs to update self->cached_allow_downloads too. */
532 : 70 : g_debug ("%s: Connection ‘%s’ changed details", G_STRFUNC, connection_id);
533 : 70 : mws_scheduler_reschedule (self);
534 : 70 : }
535 : :
536 : : static void
537 : 2 : peer_manager_peer_vanished_cb (MwsPeerManager *manager,
538 : : const gchar *name,
539 : : gpointer user_data)
540 : : {
541 : 2 : MwsScheduler *self = MWS_SCHEDULER (user_data);
542 : 2 : g_autoptr(GError) local_error = NULL;
543 : :
544 : : /* Remove the schedule entries for this peer. */
545 [ - + ]: 2 : if (!mws_scheduler_remove_entries_for_owner (self, name, &local_error))
546 : : {
547 : 0 : g_debug ("Failed to remove schedule entries for owner ‘%s’: %s",
548 : : name, local_error->message);
549 : : }
550 : 2 : }
551 : :
552 : : static void
553 : 21 : clock_offset_changed_cb (MwsClock *clock,
554 : : gpointer user_data)
555 : : {
556 : 21 : MwsScheduler *self = MWS_SCHEDULER (user_data);
557 : :
558 : 42 : g_autoptr(GDateTime) now = mws_clock_get_now_local (clock);
559 : 42 : g_autofree gchar *now_str = g_date_time_format (now, "%FT%T%:::z");
560 : :
561 : 21 : g_debug ("%s: Clock offset changed; time is now %s", G_STRFUNC, now_str);
562 : 21 : mws_scheduler_reschedule (self);
563 : 21 : }
564 : :
565 : : /**
566 : : * mws_scheduler_new:
567 : : * @connection_monitor: (transfer none): a #MwsConnectionMonitor to provide
568 : : * information about network connections to the scheduler
569 : : * @peer_manager: (transfer none): a #MwsPeerManager to provide information
570 : : * about peers which are adding schedule entries to the scheduler
571 : : * @clock: (transfer none): a #MwsClock to provide timing information
572 : : *
573 : : * Create a new #MwsScheduler instance, with no schedule entries to begin
574 : : * with.
575 : : *
576 : : * Returns: (transfer full): a new #MwsScheduler
577 : : * Since: 0.1.0
578 : : */
579 : : MwsScheduler *
580 : 1 : mws_scheduler_new (MwsConnectionMonitor *connection_monitor,
581 : : MwsPeerManager *peer_manager,
582 : : MwsClock *clock)
583 : : {
584 [ - + ]: 1 : g_return_val_if_fail (MWS_IS_CONNECTION_MONITOR (connection_monitor), NULL);
585 [ - + ]: 1 : g_return_val_if_fail (MWS_IS_PEER_MANAGER (peer_manager), NULL);
586 [ - + ]: 1 : g_return_val_if_fail (MWS_IS_CLOCK (clock), NULL);
587 : :
588 : 1 : return g_object_new (MWS_TYPE_SCHEDULER,
589 : : "connection-monitor", connection_monitor,
590 : : "peer-manager", peer_manager,
591 : : "clock", clock,
592 : : NULL);
593 : : }
594 : :
595 : : /**
596 : : * mws_scheduler_get_peer_manager:
597 : : * @self: a #MwsScheduler
598 : : *
599 : : * Get the value of #MwsScheduler:peer-manager.
600 : : *
601 : : * Returns: (transfer none): the peer manager for the scheduler
602 : : * Since: 0.1.0
603 : : */
604 : : MwsPeerManager *
605 : 31 : mws_scheduler_get_peer_manager (MwsScheduler *self)
606 : : {
607 [ - + ]: 31 : g_return_val_if_fail (MWS_IS_SCHEDULER (self), NULL);
608 : :
609 : 31 : return self->peer_manager;
610 : : }
611 : :
612 : : /**
613 : : * mws_scheduler_update_entries:
614 : : * @self: a #MwsScheduler
615 : : * @added: (nullable) (transfer none) (element-type MwsScheduleEntry): set of
616 : : * #MwsScheduleEntry instances to add to the scheduler
617 : : * @removed: (nullable) (transfer none) (element-type utf8): set of entry IDs
618 : : * to remove from the scheduler
619 : : * @error: return location for a #GError, or %NULL
620 : : *
621 : : * Update the set of schedule entries in the scheduler, adding all entries in
622 : : * @added, and removing all those in @removed.
623 : : *
624 : : * Entries in @added which are already in the scheduler, and entries in @removed
625 : : * which are not in the scheduler, are ignored.
626 : : *
627 : : * If adding any of @added to the scheduler would cause it to exceed
628 : : * #MwsScheduler:max-entries, %MWS_SCHEDULER_ERROR_FULL will be returned and
629 : : * the scheduler will not be modified to add or remove any of @added or
630 : : * @removed.
631 : : *
632 : : * Returns: %TRUE on success, %FALSE otherwise
633 : : * Since: 0.1.0
634 : : */
635 : : gboolean
636 : 64 : mws_scheduler_update_entries (MwsScheduler *self,
637 : : GPtrArray *added,
638 : : GPtrArray *removed,
639 : : GError **error)
640 : : {
641 [ - + ]: 64 : g_return_val_if_fail (MWS_IS_SCHEDULER (self), FALSE);
642 [ + - - + ]: 64 : g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
643 : :
644 : 64 : g_autoptr(GPtrArray) actually_added = NULL; /* (element-type MwsScheduleEntry) */
645 : 64 : actually_added = g_ptr_array_new_with_free_func (g_object_unref);
646 : 64 : g_autoptr(GPtrArray) actually_removed = NULL; /* (element-type MwsScheduleEntry) */
647 : 64 : actually_removed = g_ptr_array_new_with_free_func (g_object_unref);
648 : 64 : g_autoptr(GPtrArray) actually_removed_active = NULL; /* (element-type MwsScheduleEntry) */
649 : 64 : actually_removed_active = g_ptr_array_new_with_free_func (g_object_unref);
650 : :
651 : : /* Check resource limits. */
652 [ + + ]: 64 : if (added != NULL &&
653 [ + + ]: 40 : added->len > self->max_entries - g_hash_table_size (self->entries))
654 : : {
655 : 1 : g_set_error (error, MWS_SCHEDULER_ERROR, MWS_SCHEDULER_ERROR_FULL,
656 : : _("Too many ongoing downloads already."));
657 : 1 : return FALSE;
658 : : }
659 : :
660 : : /* Remove and add entries. Throughout, we need to ensure that @entries and
661 : : * @entries_data always have identical sets of keys; that reduces the number
662 : : * of checks needed in other places in the code. */
663 [ + + + + ]: 88 : for (gsize i = 0; removed != NULL && i < removed->len; i++)
664 : : {
665 : 25 : const gchar *entry_id = removed->pdata[i];
666 [ - + ]: 25 : g_return_val_if_fail (mws_schedule_entry_id_is_valid (entry_id), FALSE);
667 : :
668 : 25 : g_debug ("Removing schedule entry ‘%s’.", entry_id);
669 : :
670 : : /* FIXME: Upstream a g_hash_table_steal_extended() function which combines these two.
671 : : * See: https://bugzilla.gnome.org/show_bug.cgi?id=795302 */
672 : : gpointer value;
673 [ + + ]: 25 : if (g_hash_table_lookup_extended (self->entries, entry_id, NULL, &value))
674 : : {
675 : 24 : gboolean was_active = mws_scheduler_is_entry_active (self, MWS_SCHEDULE_ENTRY (value));
676 : :
677 : 24 : g_autoptr(MwsScheduleEntry) entry = value;
678 : 24 : g_hash_table_steal (self->entries, entry_id);
679 [ - + ]: 24 : g_assert (g_hash_table_remove (self->entries_data, entry_id));
680 : :
681 [ + + ]: 24 : if (was_active)
682 : 19 : g_ptr_array_add (actually_removed_active, g_object_ref (entry));
683 : 24 : g_ptr_array_add (actually_removed, g_steal_pointer (&entry));
684 : : }
685 : : else
686 : : {
687 : 1 : g_debug ("Schedule entry ‘%s’ did not exist in MwsScheduler %p.",
688 : : entry_id, self);
689 [ - + ]: 1 : g_assert (g_hash_table_lookup (self->entries_data, entry_id) == NULL);
690 : : }
691 : : }
692 : :
693 [ + + + + ]: 122 : for (gsize i = 0; added != NULL && i < added->len; i++)
694 : : {
695 : 59 : MwsScheduleEntry *entry = added->pdata[i];
696 : 59 : const gchar *entry_id = mws_schedule_entry_get_id (entry);
697 [ - + ]: 59 : g_return_val_if_fail (MWS_IS_SCHEDULE_ENTRY (entry), FALSE);
698 : :
699 : 59 : g_debug ("Adding schedule entry ‘%s’.", entry_id);
700 : :
701 [ + + ]: 59 : if (g_hash_table_replace (self->entries,
702 : : (gpointer) entry_id, g_object_ref (entry)))
703 : : {
704 : 58 : g_hash_table_replace (self->entries_data,
705 : 58 : (gpointer) entry_id, entry_data_new ());
706 : 58 : g_ptr_array_add (actually_added, g_object_ref (entry));
707 : : }
708 : : else
709 : : {
710 : 1 : g_debug ("Schedule entry ‘%s’ already existed in MwsScheduler %p.",
711 : : entry_id, self);
712 [ - + ]: 1 : g_assert (g_hash_table_lookup (self->entries_data, entry_id) != NULL);
713 : : }
714 : : }
715 : :
716 [ + + ]: 63 : if (actually_removed_active->len > 0)
717 : : {
718 : 19 : g_debug ("%s: Emitting active-entries-changed with %u added, %u removed",
719 : : G_STRFUNC, (guint) 0, actually_removed_active->len);
720 : 19 : g_signal_emit_by_name (G_OBJECT (self), "active-entries-changed",
721 : : NULL, actually_removed_active);
722 : : }
723 : :
724 [ + + + + ]: 63 : if (actually_added->len > 0 || actually_removed->len > 0)
725 : : {
726 : 58 : g_debug ("%s: Emitting entries-changed with %u added, %u removed",
727 : : G_STRFUNC, actually_added->len, actually_removed->len);
728 : 58 : g_object_notify (G_OBJECT (self), "entries");
729 : 58 : g_signal_emit_by_name (G_OBJECT (self), "entries-changed",
730 : : actually_added, actually_removed);
731 : :
732 : : /* Trigger a reschedule due to the new or removed entries. */
733 : 58 : mws_scheduler_reschedule (self);
734 : : }
735 : :
736 : 63 : return TRUE;
737 : : }
738 : :
739 : : /**
740 : : * mws_scheduler_remove_entries_for_owner:
741 : : * @self: a #MwsScheduler
742 : : * @owner: the D-Bus unique name of the peer to remove entries for
743 : : * @error: return location for a #GError, or %NULL
744 : : *
745 : : * Remove all schedule entries from the #MwsScheduler whose owner is @owner.
746 : : * Possible errors are the same as for mws_scheduler_update_entries().
747 : : *
748 : : * Returns: %TRUE on success, %FALSE otherwise
749 : : * Since: 0.1.0
750 : : */
751 : : gboolean
752 : 5 : mws_scheduler_remove_entries_for_owner (MwsScheduler *self,
753 : : const gchar *owner,
754 : : GError **error)
755 : : {
756 [ - + ]: 5 : g_return_val_if_fail (MWS_IS_SCHEDULER (self), FALSE);
757 [ - + ]: 5 : g_return_val_if_fail (g_dbus_is_unique_name (owner), FALSE);
758 [ + - - + ]: 5 : g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
759 : :
760 : 10 : g_autoptr(GPtrArray) entries_to_remove = g_ptr_array_new_with_free_func (NULL);
761 : :
762 : : GHashTableIter iter;
763 : : gpointer value;
764 : :
765 : 5 : g_hash_table_iter_init (&iter, self->entries);
766 [ + + ]: 14 : while (g_hash_table_iter_next (&iter, NULL, &value))
767 : : {
768 : 9 : MwsScheduleEntry *entry = MWS_SCHEDULE_ENTRY (value);
769 : :
770 [ + + ]: 9 : if (g_str_equal (mws_schedule_entry_get_owner (entry), owner))
771 : 6 : g_ptr_array_add (entries_to_remove,
772 : 6 : (gpointer) mws_schedule_entry_get_id (entry));
773 : : }
774 : :
775 : 5 : return mws_scheduler_update_entries (self, NULL, entries_to_remove, error);
776 : : }
777 : :
778 : : /**
779 : : * mws_scheduler_get_entry:
780 : : * @self: a #MwsScheduler
781 : : * @entry_id: ID of the schedule entry to look up
782 : : *
783 : : * Look up the given @entry_id in the scheduler and return it if found;
784 : : * otherwise return %NULL.
785 : : *
786 : : * Returns: (transfer none) (nullable): the found entry, or %NULL
787 : : * Since: 0.1.0
788 : : */
789 : : MwsScheduleEntry *
790 : 1 : mws_scheduler_get_entry (MwsScheduler *self,
791 : : const gchar *entry_id)
792 : : {
793 [ - + ]: 1 : g_return_val_if_fail (MWS_IS_SCHEDULER (self), NULL);
794 [ - + ]: 1 : g_return_val_if_fail (mws_schedule_entry_id_is_valid (entry_id), NULL);
795 : :
796 : 1 : return g_hash_table_lookup (self->entries, entry_id);
797 : : }
798 : :
799 : : /**
800 : : * mws_scheduler_get_entries:
801 : : * @self: a #MwsScheduler
802 : : *
803 : : * Get the complete set of schedule entries known to the scheduler, as a map of
804 : : * #MwsScheduleEntry instances indexed by entry ID.
805 : : *
806 : : * Returns: (transfer none) (element-type utf8 MwsScheduleEntry): mapping of
807 : : * entry IDs to entries
808 : : * Since: 0.1.0
809 : : */
810 : : GHashTable *
811 : 70 : mws_scheduler_get_entries (MwsScheduler *self)
812 : : {
813 [ - + ]: 70 : g_return_val_if_fail (MWS_IS_SCHEDULER (self), NULL);
814 : :
815 : 70 : return self->entries;
816 : : }
817 : :
818 : : /**
819 : : * mws_scheduler_is_entry_active:
820 : : * @self: a #MwsScheduler
821 : : * @entry: the entry
822 : : *
823 : : * Checks whether the given entry is currently allowed to be downloaded. This
824 : : * only checks cached state: it does not recalculate the scheduler state.
825 : : *
826 : : * It is an error to call this on an @entry which is not currently in the
827 : : * scheduler.
828 : : *
829 : : * Returns: %TRUE if entry can be downloaded now, %FALSE otherwise
830 : : * Since: 0.1.0
831 : : */
832 : : gboolean
833 : 40 : mws_scheduler_is_entry_active (MwsScheduler *self,
834 : : MwsScheduleEntry *entry)
835 : : {
836 [ - + ]: 40 : g_return_val_if_fail (MWS_IS_SCHEDULER (self), FALSE);
837 [ - + ]: 40 : g_return_val_if_fail (MWS_IS_SCHEDULE_ENTRY (entry), FALSE);
838 : :
839 : 40 : const gchar *entry_id = mws_schedule_entry_get_id (entry);
840 : 40 : const EntryData *data = g_hash_table_lookup (self->entries_data, entry_id);
841 [ - + ]: 40 : g_return_val_if_fail (data != NULL, FALSE);
842 : :
843 [ + + ]: 40 : g_debug ("%s: Entry ‘%s’, active: %s",
844 : : G_STRFUNC, entry_id, data->is_active ? "yes" : "no");
845 : :
846 : 40 : return data->is_active;
847 : : }
848 : :
849 : : static gboolean
850 : 11 : reschedule_cb (gpointer user_data)
851 : : {
852 : 11 : MwsScheduler *self = MWS_SCHEDULER (user_data);
853 : 11 : mws_scheduler_reschedule (self);
854 : 11 : return G_SOURCE_REMOVE;
855 : : }
856 : :
857 : : /* Get the priority of a given peer. Higher returned numbers indicate more
858 : : * important peers. */
859 : : static gint
860 : 146 : get_peer_priority (MwsScheduler *self,
861 : : MwsScheduleEntry *entry)
862 : : {
863 : 146 : const gchar *owner = mws_schedule_entry_get_owner (entry);
864 : : const gchar *owner_path =
865 : 146 : mws_peer_manager_get_peer_credentials (self->peer_manager, owner);
866 : :
867 : : /* If we haven’t got credentials for this peer (which would be unexpected and
868 : : * indicate a serious problem), give it a low priority. */
869 [ + + ]: 146 : if (owner_path == NULL)
870 : 53 : return G_MININT;
871 : :
872 : : /* The OS and app updaters are equally as important as each other. The actual
873 : : * priority numbers chosen here are fairly arbitrary; it’s the partial order
874 : : * over them which is important. */
875 [ + + + + ]: 174 : if (g_str_equal (owner_path, "/usr/libexec/eos-updater") ||
876 : 81 : g_str_equal (owner_path, "/usr/bin/gnome-software"))
877 : 21 : return G_MAXINT;
878 : :
879 : : /* Anything else goes in the range (G_MININT, G_MAXINT). */
880 : 72 : gint priority = g_str_hash (owner_path) + G_MININT;
881 [ - + ]: 72 : if (priority == G_MININT)
882 : 0 : priority += 1;
883 [ - + ]: 72 : if (priority == G_MAXINT)
884 : 0 : priority -= 1;
885 : 72 : return priority;
886 : : }
887 : :
888 : : /* Compare entries to give a total order by scheduling priority, with the most
889 : : * important entries for scheduling listed first. i.e. Given entries @a and @b,
890 : : * this will return a negative number if @a should be scheduled before @b.
891 : : * This is one of the core parts of the scheduling algorithm; earlier stages
892 : : * trim any entries which can’t be scheduled (for example, due to wanting to use
893 : : * a metered connection when the user has disallowed it); later stages select
894 : : * the most important N entries to actually schedule, according to
895 : : * parallelisation limits. */
896 : : static gint
897 : 73 : entry_compare_cb (gconstpointer a_,
898 : : gconstpointer b_,
899 : : gpointer user_data)
900 : : {
901 : 73 : MwsScheduler *self = MWS_SCHEDULER (user_data);
902 : 73 : MwsScheduleEntry *a = MWS_SCHEDULE_ENTRY (*((MwsScheduleEntry **) a_));
903 : 73 : MwsScheduleEntry *b = MWS_SCHEDULE_ENTRY (*((MwsScheduleEntry **) b_));
904 : :
905 : : /* As per https://phabricator.endlessm.com/T21327, we want the following
906 : : * priority order (most important first):
907 : : * 1. App extensions to com.endlessm.* apps
908 : : * 2. OS updates
909 : : * 3. App updates and app extensions to non-Endless apps
910 : : * 4. Anything else
911 : : *
912 : : * We currently have two inputs to base this on: the identity of the peer who
913 : : * owns a #MwsScheduleEntry, and the priority they set for the entry.
914 : : * Priorities are scoped to an owner, not global.
915 : : *
916 : : * We can implement what we want by hard-coding it so that the scopes for
917 : : * gnome-software and eos-updater are merged, and both given priority over any
918 : : * other peer. Then getting the ordering we want is a matter of coordinating
919 : : * the priorities set by gnome-software and eos-updater on their schedule
920 : : * entries, which is possible since we control all of that code. */
921 : :
922 : : /* Sort by peer first. */
923 : 73 : gint a_peer_priority = get_peer_priority (self, a);
924 : 73 : gint b_peer_priority = get_peer_priority (self, b);
925 : :
926 [ + + ]: 73 : if (a_peer_priority != b_peer_priority)
927 : : {
928 : 29 : g_debug ("%s: Comparing schedule entries ‘%s’ and ‘%s’ by peer priority: %d vs %d",
929 : : G_STRFUNC, mws_schedule_entry_get_id (a),
930 : : mws_schedule_entry_get_id (b), a_peer_priority, b_peer_priority);
931 [ + + ]: 29 : return (b_peer_priority > a_peer_priority) ? 1 : -1;
932 : : }
933 : :
934 : : /* Within the peer, sort by the priority assigned by that peer to the entry. */
935 : 44 : guint32 a_entry_priority = mws_schedule_entry_get_priority (a);
936 : 44 : guint32 b_entry_priority = mws_schedule_entry_get_priority (b);
937 : :
938 [ + + ]: 44 : if (a_entry_priority != b_entry_priority)
939 : : {
940 : 36 : g_debug ("%s: Comparing schedule entries ‘%s’ and ‘%s’ by entry priority: %u vs %u",
941 : : G_STRFUNC, mws_schedule_entry_get_id (a),
942 : : mws_schedule_entry_get_id (b), a_entry_priority,
943 : : b_entry_priority);
944 [ + + ]: 36 : return (b_entry_priority > a_entry_priority) ? 1 : -1;
945 : : }
946 : :
947 : : /* Arbitrarily break ties using the entries’ IDs, which should always be
948 : : * different. */
949 : 8 : g_debug ("%s: Comparing schedule entries ‘%s’ and ‘%s’ by entry ID",
950 : : G_STRFUNC, mws_schedule_entry_get_id (a),
951 : : mws_schedule_entry_get_id (b));
952 : 8 : return g_strcmp0 (mws_schedule_entry_get_id (a),
953 : 8 : mws_schedule_entry_get_id (b));
954 : : }
955 : :
956 : : /**
957 : : * mws_scheduler_reschedule:
958 : : * @self: a #MwsScheduler
959 : : *
960 : : * Calculate an updated download schedule for all currently active entries, and
961 : : * update the set of active entries if necessary. Changes to the set of active
962 : : * entries will be signalled using #MwsScheduler::active-entries-changed.
963 : : *
964 : : * This is called automatically when the set of entries in the scheduler
965 : : * changes, or when any relevant input to the scheduler changes; so should not
966 : : * normally need to be called manually. It is exposed mainly for unit testing.
967 : : *
968 : : * Since: 0.1.0
969 : : */
970 : : void
971 : 235 : mws_scheduler_reschedule (MwsScheduler *self)
972 : : {
973 [ - + ]: 323 : g_return_if_fail (MWS_IS_SCHEDULER (self));
974 : :
975 [ - + ]: 235 : g_assert (!self->in_reschedule);
976 : 235 : self->in_reschedule = TRUE;
977 : :
978 : 235 : g_debug ("%s: Rescheduling %u entries",
979 : : G_STRFUNC, g_hash_table_size (self->entries));
980 : :
981 : : /* Sanity checks. */
982 [ - + ]: 235 : g_assert (g_hash_table_size (self->entries) ==
983 : : g_hash_table_size (self->entries_data));
984 : :
985 : : /* Clear any pending reschedule. */
986 [ - + ]: 235 : if (self->reschedule_alarm_id != 0)
987 : : {
988 : 0 : mws_clock_remove_alarm (self->clock, self->reschedule_alarm_id);
989 : 0 : self->reschedule_alarm_id = 0;
990 : : }
991 : :
992 : : /* Preload information from the connection monitor. */
993 : 235 : const gchar * const *all_connection_ids = NULL;
994 : 235 : all_connection_ids = mws_connection_monitor_get_connection_ids (self->connection_monitor);
995 : 235 : gsize n_connections = g_strv_length ((gchar **) all_connection_ids);
996 : :
997 [ + + ]: 470 : g_autoptr(GArray) all_connection_details = g_array_sized_new (FALSE, FALSE,
998 : : sizeof (MwsConnectionDetails),
999 : : n_connections);
1000 : 235 : g_array_set_clear_func (all_connection_details,
1001 : : (GDestroyNotify) mws_connection_details_clear);
1002 : 235 : g_array_set_size (all_connection_details, n_connections);
1003 : :
1004 : 235 : gboolean cached_allow_downloads = TRUE;
1005 : :
1006 [ + + ]: 437 : for (gsize i = 0; all_connection_ids[i] != NULL; i++)
1007 : : {
1008 : 202 : MwsConnectionDetails *out_details = &g_array_index (all_connection_details,
1009 : : MwsConnectionDetails, i);
1010 : :
1011 [ - + ]: 202 : if (!mws_connection_monitor_get_connection_details (self->connection_monitor,
1012 : 202 : all_connection_ids[i],
1013 : : out_details))
1014 : : {
1015 : : /* Fill the details with dummy values. */
1016 : 0 : g_debug ("%s: Failed to get details for connection ‘%s’.",
1017 : : G_STRFUNC, all_connection_ids[i]);
1018 : 0 : mws_connection_details_clear (out_details);
1019 : 0 : continue;
1020 : : }
1021 : :
1022 : : /* FIXME: See FIXME below by `can_be_active` about allowing clients to
1023 : : * specify whether they support downloading from selective connections.
1024 : : * If that logic changes, so does this. */
1025 [ + + + + ]: 202 : cached_allow_downloads = cached_allow_downloads && out_details->allow_downloads;
1026 : : }
1027 : :
1028 [ + + ]: 235 : if (self->cached_allow_downloads != cached_allow_downloads)
1029 : : {
1030 : 51 : g_debug ("%s: Updating cached_allow_downloads from %u to %u",
1031 : : G_STRFUNC, (guint) self->cached_allow_downloads,
1032 : : (guint) cached_allow_downloads);
1033 : 51 : self->cached_allow_downloads = cached_allow_downloads;
1034 : 51 : g_object_notify (G_OBJECT (self), "allow-downloads");
1035 : : }
1036 : :
1037 : : /* Fast path. We still have to load the connection monitor information above,
1038 : : * though, so that we can update self->cached_allow_downloads. */
1039 [ + + ]: 235 : if (g_hash_table_size (self->entries) == 0)
1040 : : {
1041 : 88 : self->in_reschedule = FALSE;
1042 : 88 : return;
1043 : : }
1044 : :
1045 : : /* As we iterate over all the entries, see when the earliest time we next need
1046 : : * to reschedule is. */
1047 : 294 : g_autoptr(GDateTime) now = mws_clock_get_now_local (self->clock);
1048 : 147 : g_autoptr(GDateTime) next_reschedule = NULL;
1049 : :
1050 : 294 : g_autofree gchar *now_str = g_date_time_format (now, "%FT%T%:::z");
1051 : 147 : g_debug ("%s: Considering now = %s", G_STRFUNC, now_str);
1052 : :
1053 : : /* For each entry, see if it’s permissible to start downloading it. For the
1054 : : * moment, we only use whether the network is metered as a basis for this
1055 : : * calculation. In future, we can factor in the tariff on each connection,
1056 : : * bandwidth usage, capacity limits, etc. */
1057 : 294 : g_autoptr(GPtrArray) entries_now_active = g_ptr_array_new_with_free_func (NULL);
1058 : 294 : g_autoptr(GPtrArray) entries_were_active = g_ptr_array_new_with_free_func (NULL);
1059 : :
1060 : : /* An array of the entries which can be active within the user’s preferences
1061 : : * for cost. Not necessarily all of them will be chosen to be active, though,
1062 : : * based on parallelisation limits and prioritisation. */
1063 : 294 : g_autoptr(GPtrArray) entries_can_be_active = g_ptr_array_new_with_free_func (NULL);
1064 : :
1065 : 147 : g_autoptr(GPtrArray) safe_connections = g_ptr_array_new_full (n_connections, NULL);
1066 : :
1067 : : GHashTableIter iter;
1068 : : gpointer key, value;
1069 : 147 : g_hash_table_iter_init (&iter, self->entries);
1070 : :
1071 [ + + ]: 340 : while (g_hash_table_iter_next (&iter, &key, &value))
1072 : : {
1073 : 193 : const gchar *entry_id = key;
1074 : 193 : MwsScheduleEntry *entry = value;
1075 : 193 : EntryData *data = g_hash_table_lookup (self->entries_data, entry_id);
1076 [ - + ]: 193 : g_assert (data != NULL);
1077 : :
1078 : 193 : g_debug ("%s: Scheduling entry ‘%s’", G_STRFUNC, entry_id);
1079 : :
1080 : : /* Work out which connections this entry could be downloaded on safely. */
1081 : 193 : g_ptr_array_set_size (safe_connections, 0);
1082 : :
1083 [ + + ]: 360 : for (gsize i = 0; all_connection_ids[i] != NULL; i++)
1084 : : {
1085 : : /* FIXME: Support multi-path properly by allowing each
1086 : : * #MwsScheduleEntry to specify which connections it might download
1087 : : * over. Currently we assume the client might download over any
1088 : : * active connection. (Typically only one connection will ever be
1089 : : * active anyway.) */
1090 : 167 : const MwsConnectionDetails *details = &g_array_index (all_connection_details,
1091 : : MwsConnectionDetails, i);
1092 : :
1093 : : /* If this connection has a tariff specified, work out whether we’ve
1094 : : * hit any of the limits for the current tariff period. */
1095 : 167 : MwtPeriod *tariff_period = NULL;
1096 : 167 : gboolean tariff_period_reached_capacity_limit = FALSE;
1097 : :
1098 [ + + ]: 167 : if (details->tariff != NULL)
1099 : : {
1100 : 98 : tariff_period = mwt_tariff_lookup_period (details->tariff, now);
1101 : : }
1102 : :
1103 [ + + ]: 167 : if (tariff_period != NULL)
1104 : : {
1105 : 94 : g_autofree gchar *tariff_period_start_str =
1106 : 94 : g_date_time_format (mwt_period_get_start (tariff_period), "%FT%T%:::z");
1107 : 94 : g_autofree gchar *tariff_period_end_str =
1108 : 94 : g_date_time_format (mwt_period_get_end (tariff_period), "%FT%T%:::z");
1109 : 94 : g_debug ("%s: Considering tariff period %p: %s to %s",
1110 : : G_STRFUNC, tariff_period, tariff_period_start_str,
1111 : : tariff_period_end_str);
1112 : : }
1113 : : else
1114 : : {
1115 : 73 : g_debug ("%s: No tariff period found", G_STRFUNC);
1116 : : }
1117 : :
1118 [ + + ]: 167 : if (tariff_period != NULL)
1119 : : {
1120 : : /* FIXME: For the moment, we can only see if the capacity limit is
1121 : : * hard-coded to zero to indicate a period when downloads are
1122 : : * banned. In future, we will need to query the amount of data
1123 : : * downloaded in the current period and check it against the
1124 : : * limit (plus do a reschedule when the amount of data downloaded
1125 : : * does reach the limit). */
1126 : 94 : tariff_period_reached_capacity_limit =
1127 : 94 : (mwt_period_get_capacity_limit (tariff_period) == 0);
1128 : : }
1129 : :
1130 : : /* Is it safe to schedule this entry on this connection now? */
1131 : 369 : gboolean is_safe = ((details->metered == MWS_METERED_NO ||
1132 [ + - ]: 35 : details->metered == MWS_METERED_GUESS_NO ||
1133 [ + + ]: 35 : details->allow_downloads_when_metered) &&
1134 [ + + + + : 202 : details->allow_downloads &&
+ + ]
1135 : : !tariff_period_reached_capacity_limit);
1136 [ + + + + : 167 : g_debug ("%s: Connection ‘%s’ is %s to download entry ‘%s’ on "
+ + + + ]
1137 : : "(metered: %s, allow-downloads-when-metered: %s, "
1138 : : "allow-downloads: %s, tariff-period-reached-capacity-limit: %s).",
1139 : : G_STRFUNC, all_connection_ids[i],
1140 : : is_safe ? "safe" : "not safe", entry_id,
1141 : : mws_metered_to_string (details->metered),
1142 : : details->allow_downloads_when_metered ? "yes" : "no",
1143 : : details->allow_downloads ? "yes" : "no",
1144 : : tariff_period_reached_capacity_limit ? "yes" : "no");
1145 : :
1146 [ + + ]: 167 : if (is_safe)
1147 : 85 : g_ptr_array_add (safe_connections, (gpointer) all_connection_ids[i]);
1148 : :
1149 : : /* Work out when to do the next reschedule due to this tariff changing
1150 : : * periods. */
1151 [ + + ]: 167 : if (details->tariff != NULL)
1152 : : {
1153 : 98 : g_autoptr(GDateTime) next_transition = NULL;
1154 : 98 : next_transition = mwt_tariff_get_next_transition (details->tariff, now,
1155 : : NULL, NULL);
1156 : :
1157 : 98 : g_autofree gchar *next_transition_str = NULL;
1158 [ + + ]: 98 : if (next_transition != NULL)
1159 : 96 : next_transition_str = g_date_time_format (next_transition, "%FT%T%:::z");
1160 : : else
1161 : 2 : next_transition_str = g_strdup ("never");
1162 : 98 : g_debug ("%s: Connection ‘%s’ next transition is %s",
1163 : : G_STRFUNC, all_connection_ids[i], next_transition_str);
1164 : :
1165 [ + + + - ]: 194 : if (next_transition != NULL &&
1166 : 96 : g_date_time_compare (now, next_transition) < 0 &&
1167 [ + + + + ]: 112 : (next_reschedule == NULL ||
1168 : 16 : g_date_time_compare (next_transition, next_reschedule) < 0))
1169 : : {
1170 [ + + ]: 88 : g_clear_pointer (&next_reschedule, g_date_time_unref);
1171 : 88 : next_reschedule = g_date_time_ref (next_transition);
1172 : : }
1173 : : }
1174 : : }
1175 : :
1176 : : /* If all the active connections are safe for this entry, it can be made
1177 : : * active. We assume that the client cannot support downloading over a
1178 : : * particular connection and ignoring another: all active connections have
1179 : : * to be safe to start a download.
1180 : : * FIXME: Allow clients to specify whether they support downloading from
1181 : : * selective connections. If so, their downloads could be made active
1182 : : * without all active connections having to be safe. */
1183 : 193 : gboolean can_be_active = (safe_connections->len == n_connections);
1184 [ + + ]: 193 : g_debug ("%s: Entry ‘%s’ %s (%u of %" G_GSIZE_FORMAT " connections are safe)",
1185 : : G_STRFUNC, entry_id,
1186 : : can_be_active ? "can be active" : "cannot be active",
1187 : : safe_connections->len, n_connections);
1188 : :
1189 [ + + ]: 193 : if (can_be_active)
1190 : : {
1191 : 125 : g_ptr_array_add (entries_can_be_active, entry);
1192 : : }
1193 : : else
1194 : : {
1195 : : /* Accounting for the signal emission at the end of the function. */
1196 [ + + ]: 68 : if (data->is_active)
1197 : 12 : g_ptr_array_add (entries_were_active, entry);
1198 : :
1199 : : /* Update this entry’s status. */
1200 : 68 : data->is_active = FALSE;
1201 : : }
1202 : : }
1203 : :
1204 : : /* Order the potentially-active entries by priority. */
1205 : 147 : g_ptr_array_sort_with_data (entries_can_be_active, entry_compare_cb, self);
1206 : :
1207 : : /* Take the most important N potentially-active entries and actually mark them
1208 : : * as active; mark the rest as not active. N is the maximum number of active
1209 : : * entries set at construction time for the scheduler. */
1210 [ + + ]: 272 : for (gsize i = 0; i < entries_can_be_active->len; i++)
1211 : : {
1212 : 125 : MwsScheduleEntry *entry = MWS_SCHEDULE_ENTRY (g_ptr_array_index (entries_can_be_active, i));
1213 : 125 : const gchar *entry_id = mws_schedule_entry_get_id (entry);
1214 : 125 : EntryData *data = g_hash_table_lookup (self->entries_data, entry_id);
1215 [ - + ]: 125 : g_assert (data != NULL);
1216 : :
1217 : 125 : gboolean active = (i < self->max_active_entries);
1218 [ + + ]: 125 : g_debug ("%s: Entry ‘%s’ %s (index %" G_GSIZE_FORMAT " of %u sorted "
1219 : : "entries which can be active; limit of %u which will be active)",
1220 : : G_STRFUNC, entry_id,
1221 : : active ? "will be active" : "will not be active",
1222 : : i, entries_can_be_active->len, self->max_active_entries);
1223 : :
1224 : : /* Accounting for the signal emission at the end of the function. */
1225 [ + + + + ]: 125 : if (data->is_active && !active)
1226 : 1 : g_ptr_array_add (entries_were_active, entry);
1227 [ + + + + ]: 124 : else if (!data->is_active && active)
1228 : 48 : g_ptr_array_add (entries_now_active, entry);
1229 : :
1230 : : /* Update this entry’s status. */
1231 : 125 : data->is_active = active;
1232 : : }
1233 : :
1234 : : /* Signal the changes. */
1235 [ + + + + ]: 147 : if (entries_now_active->len > 0 || entries_were_active->len > 0)
1236 : : {
1237 : 59 : g_debug ("%s: Emitting active-entries-changed with %u now active, %u no longer active",
1238 : : G_STRFUNC, entries_now_active->len, entries_were_active->len);
1239 : 59 : g_signal_emit_by_name (G_OBJECT (self), "active-entries-changed",
1240 : : entries_now_active, entries_were_active);
1241 : : }
1242 : :
1243 : : /* Set up the next scheduling run. */
1244 [ + + ]: 147 : if (next_reschedule != NULL)
1245 : : {
1246 : : /* FIXME: This doesn’t take into account the difference between monotonic
1247 : : * and wall clock time: if the computer suspends, or the timezone changes,
1248 : : * the reschedule will happen at the wrong time. We probably want to split
1249 : : * this out into a separate interface, with an implementation which can
1250 : : * monitor org.freedesktop.timedate1. */
1251 : 80 : GTimeSpan interval = g_date_time_difference (next_reschedule, now);
1252 [ - + ]: 80 : g_assert (interval >= 0);
1253 : :
1254 : 80 : mws_clock_add_alarm (self->clock, next_reschedule, reschedule_cb, self, NULL);
1255 : :
1256 : 80 : g_autofree gchar *next_reschedule_str = NULL;
1257 : 80 : next_reschedule_str = g_date_time_format (next_reschedule, "%FT%T%:::z");
1258 : 80 : g_debug ("%s: Setting next reschedule for %s (in %" G_GUINT64_FORMAT " seconds)",
1259 : : G_STRFUNC, next_reschedule_str, (guint64) interval / G_USEC_PER_SEC);
1260 : : }
1261 : : else
1262 : : {
1263 : 67 : g_debug ("%s: Setting next reschedule to never", G_STRFUNC);
1264 : : }
1265 : :
1266 : 147 : self->in_reschedule = FALSE;
1267 : : }
1268 : :
1269 : : /**
1270 : : * mws_scheduler_get_allow_downloads:
1271 : : * @self: a #MwsScheduler
1272 : : *
1273 : : * Get the value of #MwsScheduler:allow-downloads.
1274 : : *
1275 : : * Returns: %TRUE if the user has indicated that at least one of the active
1276 : : * network connections should be used for large downloads, %FALSE otherwise
1277 : : * Since: 0.1.0
1278 : : */
1279 : : gboolean
1280 : 28 : mws_scheduler_get_allow_downloads (MwsScheduler *self)
1281 : : {
1282 [ - + ]: 28 : g_return_val_if_fail (MWS_IS_SCHEDULER (self), TRUE);
1283 : :
1284 : 28 : return self->cached_allow_downloads;
1285 : : }
|