Merge request !1290 from 'port-scheduler'

port-serial: add command scheduler infrastructure and use it

https://gitlab.freedesktop.org/mobile-broadband/ModemManager/-/merge_requests/1290
This commit is contained in:
Dan Williams
2025-05-23 18:58:30 -05:00
12 changed files with 1361 additions and 11 deletions
+4
View File
@@ -134,6 +134,8 @@ headers = files(
'mm-iface-port-at.h',
'mm-port.h',
'mm-port-serial-at.h',
'mm-port-scheduler.h',
'mm-port-scheduler-rr.h',
)
sources = files(
@@ -146,6 +148,8 @@ sources = files(
'mm-port-serial-gps.c',
'mm-port-serial-qcdm.c',
'mm-serial-parsers.c',
'mm-port-scheduler.c',
'mm-port-scheduler-rr.c',
)
deps = [libkerneldevice_dep]
+12 -1
View File
@@ -63,7 +63,7 @@ mm_log_object_get_id (MMLogObject *self)
gchar *self_id;
self_id = MM_LOG_OBJECT_GET_IFACE (self)->build_id (self);
if (priv->owner_id) {
if (self_id && priv->owner_id) {
priv->id = g_strdup_printf ("%s/%s", priv->owner_id, self_id);
g_free (self_id);
} else
@@ -81,6 +81,17 @@ mm_log_object_set_owner_id (MMLogObject *self,
priv = get_private (self);
g_free (priv->owner_id);
priv->owner_id = g_strdup (owner_id);
mm_log_object_reset_id (self);
}
void
mm_log_object_reset_id (MMLogObject *self)
{
Private *priv;
priv = get_private (self);
g_clear_pointer (&priv->id, g_free);
}
static void
+1
View File
@@ -34,5 +34,6 @@ struct _MMLogObjectInterface
const gchar *mm_log_object_get_id (MMLogObject *self);
void mm_log_object_set_owner_id (MMLogObject *self,
const gchar *owner_id);
void mm_log_object_reset_id (MMLogObject *self);
#endif /* MM_LOG_OBJECT_H */
+393
View File
@@ -0,0 +1,393 @@
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details:
*
* Copyright (C) 2025 Dan Williams <dan@ioncontrol.co>
*/
#include "mm-port-scheduler-rr.h"
#include "mm-log-object.h"
/* Theory of operation:
*
* Sources (e.g. MMPort subclasses) register themselves with the scheduler.
*
* Each source notifies the scheduler whenever its command queue depth changes,
* for example when new commands are submitted, when commands are completed,
* or when commands are canceled.
*
* The scheduler will round-robin between all sources with pending commands,
* sleeping when there are no pending commands from any source.
*
* For each source with a pending command the scheduler will emit the
* 'send-command' signal with that source's ID. The given source should
* send the next command in its queue to the modem.
*
* When that command is finished (either successfully or with an error/timeout)
* the source must call mm_port_scheduler_notify_command_done() to notify the
* scheduler that it may advance to the next source with a pending command, if
* any. If the 'send-command' signal and the notify_command_done() call are not
* balanced the scheduler may stall.
*/
static void mm_port_scheduler_iface_init (MMPortSchedulerInterface *iface);
static void log_object_iface_init (MMLogObjectInterface *iface);
struct _MMPortSchedulerRRPrivate {
guint instance_id;
GPtrArray *sources;
guint cur_source;
gboolean in_command;
guint next_pending_id;
/* Delay between allowing ports to send commands, in ms */
guint inter_port_delay;
};
enum {
PROP_0,
PROP_INTER_PORT_DELAY,
LAST_PROP
};
static guint send_command_signal = 0;
static guint instance_id_last = 0;
G_DEFINE_TYPE_WITH_CODE (MMPortSchedulerRR, mm_port_scheduler_rr, G_TYPE_OBJECT,
G_ADD_PRIVATE (MMPortSchedulerRR)
G_IMPLEMENT_INTERFACE (MM_TYPE_PORT_SCHEDULER,
mm_port_scheduler_iface_init)
G_IMPLEMENT_INTERFACE (MM_TYPE_LOG_OBJECT,
log_object_iface_init))
/*****************************************************************************/
typedef struct {
gpointer id;
gchar *tag; /* e.g. port name */
guint num_pending;
} Source;
static void
source_free (Source *s)
{
g_free (s->tag);
g_slice_free (Source, s);
}
static Source *
find_source (MMPortSchedulerRR *self,
gpointer source_id,
guint *out_idx)
{
guint i;
for (i = 0; i < self->priv->sources->len; i++) {
Source *s;
s = g_ptr_array_index (self->priv->sources, i);
if (s->id == source_id) {
if (out_idx)
*out_idx = i;
return s;
}
}
return NULL;
}
static Source *
find_next_source (MMPortSchedulerRR *self,
guint *out_idx)
{
guint i, idx;
/* Starting at the source *after* the current source, advance through
* the entire array and back to the current source (in case only the
* current source has pending commands) to find the next source with
* a pending command.
*/
for (i = 0, idx = self->priv->cur_source + 1;
i < self->priv->sources->len;
i++, idx++) {
Source *s;
/* Wrap around */
if (idx >= self->priv->sources->len)
idx = 0;
s = g_ptr_array_index (self->priv->sources, idx);
if (s->num_pending > 0) {
if (out_idx)
*out_idx = idx;
return s;
}
}
return NULL;
}
static void schedule_next_command (MMPortSchedulerRR *self);
static gboolean
run_next_command (MMPortSchedulerRR *self)
{
self->priv->next_pending_id = 0;
if (find_next_source (self, &self->priv->cur_source)) {
Source *s;
s = g_ptr_array_index (self->priv->sources, self->priv->cur_source);
/* If this source has a pending command, run it. */
self->priv->in_command = TRUE;
g_signal_emit (MM_PORT_SCHEDULER (self),
send_command_signal,
0,
s->id);
}
return G_SOURCE_REMOVE;
}
static void
schedule_next_command (MMPortSchedulerRR *self)
{
guint next_idx = 0;
guint delay = 0;
if (self->priv->next_pending_id || self->priv->in_command || !find_next_source (self, &next_idx))
return;
/* Only delay next command if we change sources and this isn't the
* first time we're running a command.
*/
if (next_idx != self->priv->cur_source && self->priv->cur_source < self->priv->sources->len)
delay = self->priv->inter_port_delay;
self->priv->next_pending_id = g_timeout_add (delay, (GSourceFunc) run_next_command, self);
}
static void
register_source (MMPortScheduler *scheduler,
gpointer source_id,
const gchar *tag)
{
MMPortSchedulerRR *self = MM_PORT_SCHEDULER_RR (scheduler);
Source *s;
g_assert (source_id != NULL);
s = find_source (self, source_id, NULL);
if (!s) {
s = g_slice_new0 (Source);
s->id = source_id;
s->tag = g_strdup (tag);
g_ptr_array_add (self->priv->sources, s);
g_assert_cmpint (self->priv->sources->len, <, UINT_MAX);
mm_obj_dbg (self, "[%s] source id %p registered", tag, source_id);
mm_log_object_reset_id (MM_LOG_OBJECT (self));
}
}
static void
unregister_source (MMPortScheduler *scheduler, gpointer source_id)
{
MMPortSchedulerRR *self = MM_PORT_SCHEDULER_RR (scheduler);
Source *s;
guint idx = 0;
g_assert (source_id != NULL);
s = find_source (self, source_id, &idx);
if (s) {
mm_obj_dbg (self, "[%s] source id %p unregistered", s->tag, s->id);
g_ptr_array_remove_index (self->priv->sources, idx);
mm_log_object_reset_id (MM_LOG_OBJECT (self));
/* If we just removed the current source, advance to the next one */
if (self->priv->cur_source == idx)
schedule_next_command (self);
}
}
static void
notify_num_pending (MMPortScheduler *scheduler,
gpointer source_id,
guint num_pending)
{
MMPortSchedulerRR *self = MM_PORT_SCHEDULER_RR (scheduler);
Source *s;
g_assert (source_id != NULL);
s = find_source (self, source_id, NULL);
if (s && s->num_pending != num_pending) {
s->num_pending = num_pending;
schedule_next_command (self);
}
}
static void
notify_command_done (MMPortScheduler *scheduler,
gpointer source_id,
guint num_pending)
{
MMPortSchedulerRR *self = MM_PORT_SCHEDULER_RR (scheduler);
Source *s;
guint idx = 0;
g_assert (source_id != NULL);
s = find_source (self, source_id, &idx);
if (!s) {
mm_obj_warn (self, "unknown source %p notified command-done", source_id);
return;
}
/* Only the current source gets to call this function */
if (self->priv->cur_source != idx) {
mm_obj_warn (self, "[%s] notified command-done but not active source", s->tag);
return;
}
self->priv->in_command = FALSE;
s->num_pending = num_pending;
schedule_next_command (self);
}
/*****************************************************************************/
MMPortSchedulerRR *
mm_port_scheduler_rr_new (void)
{
return MM_PORT_SCHEDULER_RR (g_object_new (MM_TYPE_PORT_SCHEDULER_RR, NULL));
}
static void
get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
MMPortSchedulerRR *self = MM_PORT_SCHEDULER_RR (object);
switch (prop_id) {
case PROP_INTER_PORT_DELAY:
g_value_set_uint (value, self->priv->inter_port_delay);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
MMPortSchedulerRR *self = MM_PORT_SCHEDULER_RR (object);
switch (prop_id) {
case PROP_INTER_PORT_DELAY:
self->priv->inter_port_delay = g_value_get_uint (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
mm_port_scheduler_iface_init (MMPortSchedulerInterface *scheduler_iface)
{
scheduler_iface->register_source = register_source;
scheduler_iface->unregister_source = unregister_source;
scheduler_iface->notify_num_pending = notify_num_pending;
scheduler_iface->notify_command_done = notify_command_done;
send_command_signal = g_signal_lookup (MM_PORT_SCHEDULER_SIGNAL_SEND_COMMAND,
MM_TYPE_PORT_SCHEDULER);
}
static gchar *
log_object_build_id (MMLogObject *_self)
{
MMPortSchedulerRR *self = MM_PORT_SCHEDULER_RR (_self);
g_autoptr(GString) str;
guint i;
str = g_string_sized_new (16);
for (i = 0; i < self->priv->sources->len; i++) {
Source *s;
s = g_ptr_array_index (self->priv->sources, i);
if (str->len)
g_string_append_c (str, ',');
g_string_append (str, s->tag);
}
return g_strdup_printf ("scheduler-%u (%s)", self->priv->instance_id, str->str);
}
static void
log_object_iface_init (MMLogObjectInterface *iface)
{
iface->build_id = log_object_build_id;
}
static void
mm_port_scheduler_rr_init (MMPortSchedulerRR *self)
{
self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
MM_TYPE_PORT_SCHEDULER_RR,
MMPortSchedulerRRPrivate);
self->priv->sources = g_ptr_array_new_full (2, (GDestroyNotify) source_free);
self->priv->cur_source = G_MAXUINT32;
self->priv->instance_id = instance_id_last++;
}
static void
dispose (GObject *object)
{
MMPortSchedulerRR *self = MM_PORT_SCHEDULER_RR (object);
if (self->priv->next_pending_id) {
g_source_remove (self->priv->next_pending_id);
self->priv->next_pending_id = 0;
}
g_assert (self->priv->sources->len == 0);
g_ptr_array_free (self->priv->sources, TRUE);
G_OBJECT_CLASS (mm_port_scheduler_rr_parent_class)->dispose (object);
}
static void
mm_port_scheduler_rr_class_init (MMPortSchedulerRRClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
/* Virtual methods */
object_class->set_property = set_property;
object_class->get_property = get_property;
object_class->dispose = dispose;
g_object_class_install_property
(object_class, PROP_INTER_PORT_DELAY,
g_param_spec_uint (MM_PORT_SCHEDULER_RR_INTER_PORT_DELAY,
"Inter-port Delay",
"Inter-port delay in ms",
0, G_MAXUINT, 0,
G_PARAM_READWRITE));
}
+53
View File
@@ -0,0 +1,53 @@
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details:
*
* Copyright (C) 2025 Dan Williams <dan@ioncontrol.co>
*/
#ifndef _MM_PORT_SCHEDULER_RR_H_
#define _MM_PORT_SCHEDULER_RR_H_
#include <glib-object.h>
#include <gio/gio.h>
#include "mm-port-scheduler.h"
#define MM_TYPE_PORT_SCHEDULER_RR (mm_port_scheduler_rr_get_type ())
#define MM_PORT_SCHEDULER_RR(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_PORT_SCHEDULER_RR, MMPortSchedulerRR))
#define MM_PORT_SCHEDULER_RR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), MM_TYPE_PORT_SCHEDULER_RR, MMPortSchedulerRRClass))
#define MM_IS_PORT_SCHEDULER_RR(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_PORT_SCHEDULER_RR))
#define MM_IS_PORT_SCHEDULER_RR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), MM_TYPE_PORT_SCHEDULER_RR))
#define MM_PORT_SCHEDULER_RR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), MM_TYPE_PORT_SCHEDULER_RR, MMPortSchedulerRRClass))
#define MM_PORT_SCHEDULER_RR_INTER_PORT_DELAY "inter-port-delay"
typedef struct _MMPortSchedulerRR MMPortSchedulerRR;
typedef struct _MMPortSchedulerRRClass MMPortSchedulerRRClass;
typedef struct _MMPortSchedulerRRPrivate MMPortSchedulerRRPrivate;
struct _MMPortSchedulerRR {
/*< private >*/
GObject parent;
MMPortSchedulerRRPrivate *priv;
};
struct _MMPortSchedulerRRClass {
/*< private >*/
GObjectClass parent;
};
GType mm_port_scheduler_rr_get_type (void);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (MMPortSchedulerRR, g_object_unref)
MMPortSchedulerRR *mm_port_scheduler_rr_new (void);
#endif /* _MM_PORT_SCHEDULER_RR_H_ */
+74
View File
@@ -0,0 +1,74 @@
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details:
*
* Copyright (C) 2025 Dan Williams <dan@ioncontrol.co>
*/
#include "mm-port-scheduler.h"
G_DEFINE_INTERFACE (MMPortScheduler, mm_port_scheduler, G_TYPE_OBJECT)
enum {
SIGNAL_SEND_COMMAND,
SIGNAL_LAST
};
static guint signals[SIGNAL_LAST] = { 0 };
/*****************************************************************************/
void
mm_port_scheduler_register_source (MMPortScheduler *self,
gpointer source,
const gchar *tag)
{
MM_PORT_SCHEDULER_GET_INTERFACE (self)->register_source (self, source, tag);
}
void
mm_port_scheduler_unregister_source (MMPortScheduler *self,
gpointer source)
{
MM_PORT_SCHEDULER_GET_INTERFACE (self)->unregister_source (self, source);
}
void
mm_port_scheduler_notify_num_pending (MMPortScheduler *self,
gpointer source,
guint num_pending)
{
MM_PORT_SCHEDULER_GET_INTERFACE (self)->notify_num_pending (self, source, num_pending);
}
void
mm_port_scheduler_notify_command_done (MMPortScheduler *self,
gpointer source,
guint num_pending)
{
MM_PORT_SCHEDULER_GET_INTERFACE (self)->notify_command_done (self, source, num_pending);
}
/*****************************************************************************/
static void
mm_port_scheduler_default_init (MMPortSchedulerInterface *iface)
{
signals[SIGNAL_SEND_COMMAND] =
g_signal_new (MM_PORT_SCHEDULER_SIGNAL_SEND_COMMAND,
MM_TYPE_PORT_SCHEDULER,
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (MMPortSchedulerInterface, send_command),
NULL, NULL,
NULL,
G_TYPE_NONE, 1,
G_TYPE_POINTER);
}
+71
View File
@@ -0,0 +1,71 @@
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details:
*
* Copyright (C) 2025 Dan Williams <dan@ioncontrol.co>
*/
#ifndef MM_PORT_SCHEDULER_H
#define MM_PORT_SCHEDULER_H
#include <glib.h>
#include <glib-object.h>
#define MM_TYPE_PORT_SCHEDULER mm_port_scheduler_get_type ()
#define MM_PORT_SCHEDULER_GET_INTERFACE(o) (G_TYPE_INSTANCE_GET_INTERFACE ((o), MM_TYPE_PORT_SCHEDULER, MMPortSchedulerInterface))
G_DECLARE_INTERFACE (MMPortScheduler, mm_port_scheduler, MM, PORT_SCHEDULER, GObject)
#define MM_PORT_SCHEDULER_SIGNAL_SEND_COMMAND "send-command"
typedef struct _MMPortSchedulerInterface MMPortSchedulerInterface;
struct _MMPortSchedulerInterface
{
GTypeInterface g_iface;
/* Signals */
void (*send_command) (MMPortScheduler *self,
gpointer source);
/* Methods */
void (*register_source) (MMPortScheduler *self,
gpointer source,
const gchar *tag);
void (*unregister_source) (MMPortScheduler *self,
gpointer source);
void (*notify_num_pending) (MMPortScheduler *self,
gpointer source,
guint num_pending);
void (*notify_command_done) (MMPortScheduler *self,
gpointer source,
guint num_pending);
};
void mm_port_scheduler_register_source (MMPortScheduler *self,
gpointer source,
const gchar *tag);
void mm_port_scheduler_unregister_source (MMPortScheduler *self,
gpointer source);
void mm_port_scheduler_notify_num_pending (MMPortScheduler *self,
gpointer source,
guint num_pending);
void mm_port_scheduler_notify_command_done (MMPortScheduler *self,
gpointer source,
guint num_pending);
#endif /* MM_PORT_SCHEDULER_H */
+108 -10
View File
@@ -36,6 +36,8 @@
#include "mm-port-serial.h"
#include "mm-log-object.h"
#include "mm-helper-enums-types.h"
#include "mm-port-scheduler.h"
#include "mm-port-scheduler-rr.h"
static gboolean port_serial_queue_process (gpointer data);
static void port_serial_schedule_queue_process (MMPortSerial *self,
@@ -59,6 +61,7 @@ enum {
PROP_FD,
PROP_SPEW_CONTROL,
PROP_FLASH_OK,
PROP_SCHEDULER,
LAST_PROP
};
@@ -81,6 +84,10 @@ struct _MMPortSerialPrivate {
GQueue *queue;
GByteArray *response;
/* Command scheduler */
MMPortScheduler *scheduler;
guint scheduler_send_id;
/* For real ports, iochannel, and we implement the eagain limit */
GIOChannel *iochannel;
guint iochannel_id;
@@ -89,7 +96,6 @@ struct _MMPortSerialPrivate {
GSocket *socket;
GSource *socket_source;
guint baud;
guint bits;
char parity;
@@ -194,8 +200,9 @@ mm_port_serial_command (MMPortSerial *self,
else
g_queue_push_tail (self->priv->queue, task);
if (g_queue_get_length (self->priv->queue) == 1)
port_serial_schedule_queue_process (self, 0);
mm_port_scheduler_notify_num_pending (self->priv->scheduler,
self,
g_queue_get_length (self->priv->queue));
}
/*****************************************************************************/
@@ -737,10 +744,11 @@ port_serial_got_response (MMPortSerial *self,
}
g_object_unref (task);
}
if (!g_queue_is_empty (self->priv->queue))
port_serial_schedule_queue_process (self, 0);
mm_port_scheduler_notify_command_done (self->priv->scheduler,
self,
g_queue_get_length (self->priv->queue));
}
}
g_object_unref (self);
g_clear_error (&error);
@@ -805,6 +813,7 @@ port_serial_queue_process (gpointer data)
GCancellable *cancellable;
GError *error = NULL;
g_assert (self->priv->timeout_id == 0);
self->priv->queue_id = 0;
task = g_queue_peek_head (self->priv->queue);
@@ -877,6 +886,21 @@ port_serial_queue_process (gpointer data)
return G_SOURCE_REMOVE;
}
static void
scheduler_send_command (MMPortScheduler *scheduler,
gpointer source,
gpointer user_data)
{
MMPortSerial *self = MM_PORT_SERIAL (user_data);
/* Must be for us */
if (source == self) {
g_assert (self->priv->queue_id == 0);
g_assert (self->priv->timeout_id == 0);
port_serial_queue_process (self);
}
}
static void
parse_response_buffer (MMPortSerial *self)
{
@@ -1444,6 +1468,8 @@ _close_internal (MMPortSerial *self, gboolean force)
}
g_queue_clear (self->priv->queue);
mm_port_scheduler_notify_num_pending (self->priv->scheduler, self, 0);
if (self->priv->timeout_id) {
g_source_remove (self->priv->timeout_id);
self->priv->timeout_id = 0;
@@ -1889,6 +1915,35 @@ mm_port_serial_get_flow_control (MMPortSerial *self)
/*****************************************************************************/
static void
scheduler_setup (MMPortSerial *self, MMPortScheduler *scheduler)
{
self->priv->scheduler = scheduler;
if (self->priv->scheduler) {
const gchar *port_name;
port_name = mm_port_get_device (MM_PORT (self));
mm_port_scheduler_register_source (self->priv->scheduler, self, port_name);
self->priv->scheduler_send_id = g_signal_connect (self->priv->scheduler,
MM_PORT_SCHEDULER_SIGNAL_SEND_COMMAND,
G_CALLBACK (scheduler_send_command),
self);
}
}
static void
scheduler_cleanup (MMPortSerial *self)
{
if (self->priv->scheduler) {
mm_port_scheduler_unregister_source (self->priv->scheduler, self);
g_signal_handler_disconnect (self->priv->scheduler, self->priv->scheduler_send_id);
self->priv->scheduler_send_id = 0;
}
g_clear_object (&self->priv->scheduler);
}
/*****************************************************************************/
MMPortSerial *
mm_port_serial_new (const char *name, MMPortType ptype)
{
@@ -1961,6 +2016,16 @@ mm_port_serial_init (MMPortSerial *self)
self->priv->response = g_byte_array_sized_new (500);
}
static void
constructed (GObject *object)
{
MMPortSerial *self = MM_PORT_SERIAL (object);
/* Add a default scheduler if none was set via GObject properties */
if (!self->priv->scheduler)
scheduler_setup (self, MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ()));
}
static void
set_property (GObject *object,
guint prop_id,
@@ -1997,6 +2062,10 @@ set_property (GObject *object,
case PROP_FLASH_OK:
self->priv->flash_ok = g_value_get_boolean (value);
break;
case PROP_SCHEDULER:
scheduler_cleanup (self);
scheduler_setup (self, g_value_dup_object (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -2039,6 +2108,9 @@ get_property (GObject *object,
case PROP_FLASH_OK:
g_value_set_boolean (value, self->priv->flash_ok);
break;
case PROP_SCHEDULER:
g_value_set_object (value, self->priv->scheduler);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -2046,7 +2118,7 @@ get_property (GObject *object,
}
static void
finalize (GObject *object)
dispose (GObject *object)
{
MMPortSerial *self = MM_PORT_SERIAL (object);
@@ -2059,14 +2131,30 @@ finalize (GObject *object)
g_assert (self->priv->socket == NULL);
g_assert (self->priv->socket_source == NULL);
if (self->priv->timeout_id)
if (self->priv->timeout_id) {
g_source_remove (self->priv->timeout_id);
self->priv->timeout_id = 0;
}
if (self->priv->queue_id)
if (self->priv->queue_id) {
g_source_remove (self->priv->queue_id);
self->priv->queue_id = 0;
}
g_byte_array_unref (self->priv->response);
self->priv->response = NULL;
scheduler_cleanup (self);
G_OBJECT_CLASS (mm_port_serial_parent_class)->dispose (object);
}
static void
finalize (GObject *object)
{
MMPortSerial *self = MM_PORT_SERIAL (object);
g_hash_table_destroy (self->priv->reply_cache);
g_byte_array_unref (self->priv->response);
g_queue_free (self->priv->queue);
G_OBJECT_CLASS (mm_port_serial_parent_class)->finalize (object);
@@ -2080,8 +2168,10 @@ mm_port_serial_class_init (MMPortSerialClass *klass)
g_type_class_add_private (object_class, sizeof (MMPortSerialPrivate));
/* Virtual methods */
object_class->constructed = constructed;
object_class->set_property = set_property;
object_class->get_property = get_property;
object_class->dispose = dispose;
object_class->finalize = finalize;
klass->config_fd = real_config_fd;
@@ -2161,6 +2251,14 @@ mm_port_serial_class_init (MMPortSerialClass *klass)
TRUE,
G_PARAM_READWRITE));
g_object_class_install_property
(object_class, PROP_SCHEDULER,
g_param_spec_object (MM_PORT_SERIAL_SCHEDULER,
"Scheduler",
"Command scheduler object (optional)",
MM_TYPE_PORT_SCHEDULER,
G_PARAM_READWRITE));
/* Signals */
signals[BUFFER_FULL] =
g_signal_new ("buffer-full",
+1
View File
@@ -40,6 +40,7 @@
#define MM_PORT_SERIAL_FD "fd" /* Construct-only */
#define MM_PORT_SERIAL_SPEW_CONTROL "spew-control"
#define MM_PORT_SERIAL_FLASH_OK "flash-ok"
#define MM_PORT_SERIAL_SCHEDULER "scheduler"
typedef enum {
MM_PORT_SERIAL_RESPONSE_NONE,
+3
View File
@@ -132,6 +132,9 @@ log_object_build_id (MMLogObject *_self)
MMPort *self;
self = MM_PORT (_self);
if (!self->priv->device || !self->priv->ptype)
return NULL;
return g_strdup_printf ("%s/%s",
mm_port_get_device (self),
mm_port_type_get_string (mm_port_get_port_type (self)));
+1
View File
@@ -8,6 +8,7 @@ test_units = {
'error-helpers': libhelpers_dep,
'kernel-device-helpers': libkerneldevice_dep,
'modem-helpers': libhelpers_dep,
'port-scheduler': libport_dep,
'sms-part-3gpp': libhelpers_dep,
'sms-part-cdma': libhelpers_dep,
'sms-list': libsms_dep,
+640
View File
@@ -0,0 +1,640 @@
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details:
*
* Copyright (C) 2025 Dan Williams <dan@ioncontrol.co>
*/
#include <glib.h>
#include <string.h>
#include <locale.h>
#include <stdio.h>
#include "mm-port-scheduler-rr.h"
#include "mm-log-test.h"
static GMainLoop *loop;
/*****************************************************************************/
typedef struct {
MMPortScheduler *sched;
guint sig_id;
gpointer source_id;
gint num_pending;
gboolean immediate;
guint idle_id;
gpointer data;
gpointer data2;
} TestSourceCtx;
static void
test_source_setup (TestSourceCtx *ctx,
MMPortScheduler *sched,
GCallback send_cmd_func)
{
ctx->sched = g_object_ref (sched);
mm_port_scheduler_register_source (sched, ctx->source_id, "test");
ctx->sig_id = g_signal_connect (sched,
MM_PORT_SCHEDULER_SIGNAL_SEND_COMMAND,
send_cmd_func,
ctx);
mm_port_scheduler_notify_num_pending (ctx->sched, ctx->source_id, ctx->num_pending);
}
static void
test_source_cleanup (TestSourceCtx *ctx)
{
g_assert_cmpint (ctx->num_pending, ==, 0);
g_signal_handler_disconnect (ctx->sched, ctx->sig_id);
mm_port_scheduler_unregister_source (ctx->sched, ctx->source_id);
g_object_unref (ctx->sched);
}
/*****************************************************************************/
static gboolean
test_ss_command_done (TestSourceCtx *ctx)
{
ctx->idle_id = 0;
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
if (ctx->num_pending == 0)
g_main_loop_quit (loop);
return G_SOURCE_REMOVE;
}
static void
test_ss_send_command (MMPortScheduler *scheduler,
gpointer source,
TestSourceCtx *ctx)
{
g_assert (scheduler == ctx->sched);
g_assert (source == ctx->source_id);
ctx->num_pending--;
g_assert_cmpint (ctx->num_pending, >=, 0);
if (ctx->immediate) {
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
if (ctx->num_pending == 0)
g_main_loop_quit (loop);
} else
ctx->idle_id = g_idle_add ((GSourceFunc)test_ss_command_done, ctx);
}
static void
test_ss_done (gboolean immediate)
{
MMPortScheduler *sched;
TestSourceCtx ctx = {
.source_id = GUINT_TO_POINTER (0x1),
.num_pending = 10,
.immediate = immediate,
};
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
test_source_setup (&ctx, sched, G_CALLBACK (test_ss_send_command));
g_main_loop_run (loop);
test_source_cleanup (&ctx);
g_object_unref (sched);
}
/*****************************************************************************/
static void
test_ds_send_command (MMPortScheduler *scheduler,
gpointer source,
TestSourceCtx *ctx)
{
guint *counter = ctx->data;
g_assert (scheduler == ctx->sched);
if (source != ctx->source_id)
return;
ctx->num_pending--;
g_assert_cmpuint (ctx->num_pending, >=, 0);
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
/* assert that the scheduler alternates between the two sources */
g_assert_cmpuint ((*counter) % 2, ==, ctx->idle_id);
(*counter)--;
if (*counter == 0)
g_main_loop_quit (loop);
}
static void
test_ds_ordering (void)
{
MMPortScheduler *sched;
guint counter;
TestSourceCtx ctx1 = {
.source_id = GUINT_TO_POINTER (0x1),
.num_pending = 10,
.data = &counter,
.idle_id = 0, /* expected value of (counter % 2) */
};
TestSourceCtx ctx2 = {
.source_id = GUINT_TO_POINTER (0x2),
.num_pending = 10,
.data = &counter,
.idle_id = 1, /* expected value of (counter % 2) */
};
counter = ctx1.num_pending + ctx2.num_pending;
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_send_command));
test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_send_command));
g_main_loop_run (loop);
test_source_cleanup (&ctx1);
test_source_cleanup (&ctx2);
g_object_unref (sched);
}
/*****************************************************************************/
static void
test_ds_uneven_send_command (MMPortScheduler *scheduler,
gpointer source,
TestSourceCtx *ctx)
{
guint *counter = ctx->data;
g_assert (scheduler == ctx->sched);
if (source != ctx->source_id)
return;
ctx->num_pending--;
/* Test that the scheduler only calls each source for the number of pending
* commands it has notified the scheduler are in its queue.
*/
g_assert_cmpint (ctx->num_pending, >=, 0);
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
(*counter)--;
if (*counter == 0)
g_main_loop_quit (loop);
}
static void
test_ds_uneven_num_pending (void)
{
MMPortScheduler *sched;
guint counter;
TestSourceCtx ctx1 = {
.source_id = GUINT_TO_POINTER (0x1),
.num_pending = 10,
.data = &counter,
};
TestSourceCtx ctx2 = {
.source_id = GUINT_TO_POINTER (0x2),
.num_pending = 5,
.data = &counter,
};
counter = ctx1.num_pending + ctx2.num_pending;
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_uneven_send_command));
test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_uneven_send_command));
g_main_loop_run (loop);
test_source_cleanup (&ctx1);
test_source_cleanup (&ctx2);
g_object_unref (sched);
}
/*****************************************************************************/
static gboolean
ds_later_notify_pending (TestSourceCtx *ctx)
{
mm_port_scheduler_notify_num_pending (ctx->sched, ctx->source_id, ctx->num_pending);
return G_SOURCE_REMOVE;
}
static void
test_ds_later_send_command (MMPortScheduler *scheduler,
gpointer source,
TestSourceCtx *ctx)
{
guint *counter = ctx->data;
g_assert (scheduler == ctx->sched);
if (source != ctx->source_id)
return;
ctx->num_pending--;
g_assert_cmpint (ctx->num_pending, >=, 0);
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
/* After we've reached zero pending commands wait a short time and then
* add more to make sure the scheduler wakes up and processes the new pending
* requests */
if (ctx->num_pending == 0 && ctx->idle_id > 0) {
ctx->num_pending = ctx->idle_id;
ctx->idle_id = 0;
g_timeout_add_seconds (GPOINTER_TO_UINT (ctx->source_id),
(GSourceFunc)ds_later_notify_pending,
ctx);
}
(*counter)--;
if (*counter == 0)
g_main_loop_quit (loop);
}
static void
test_ds_num_pending_later (void)
{
MMPortScheduler *sched;
guint counter;
TestSourceCtx ctx1 = {
.source_id = GUINT_TO_POINTER (0x1),
.num_pending = 5,
.data = &counter,
.idle_id = 2, /* num pending to add after original num_pending reaches 0 */
};
TestSourceCtx ctx2 = {
.source_id = GUINT_TO_POINTER (0x2),
.num_pending = 4,
.data = &counter,
.idle_id = 1, /* num pending to add after original num_pending reaches 0 */
};
counter = ctx1.num_pending + ctx2.num_pending + ctx1.idle_id + ctx2.idle_id;
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_later_send_command));
test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_later_send_command));
g_main_loop_run (loop);
test_source_cleanup (&ctx1);
test_source_cleanup (&ctx2);
g_object_unref (sched);
}
/*****************************************************************************/
static gboolean
quit_loop (void)
{
g_main_loop_quit (loop);
return G_SOURCE_REMOVE;
}
static void
test_ds_bad_notify_send_command (MMPortScheduler *scheduler,
gpointer source,
TestSourceCtx *ctx)
{
g_assert (scheduler == ctx->sched);
if (source != ctx->source_id)
return;
if (GPOINTER_TO_UINT (source) == 0x2) {
/* Second source without any pending commands tries to call
* notify_command_done but this should have no effect; the scheduler
* should ignore the num_pending given here.
*/
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, 15);
return;
}
g_assert_cmpuint (GPOINTER_TO_UINT (source), ==, 0x1);
ctx->num_pending--;
g_assert_cmpint (ctx->num_pending, >=, 0);
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
if (ctx->num_pending == 0) {
/* Schedule a timeout to quit the mainloop to give enough time for
* the scheduler to mess up (which we don't expect).
*/
g_timeout_add_seconds (1, (GSourceFunc)quit_loop, NULL);
}
}
static gboolean
assert_not_reached (void)
{
g_assert_not_reached ();
return G_SOURCE_REMOVE;
}
static void
test_ds_bad_notify_done (void)
{
MMPortScheduler *sched;
guint timeout_id;
TestSourceCtx ctx1 = {
.source_id = GUINT_TO_POINTER (0x1),
.num_pending = 5,
};
TestSourceCtx ctx2 = {
.source_id = GUINT_TO_POINTER (0x2),
.num_pending = 0,
/* This source just hammers notify_done even though it never has any
* pending commands.
*/
};
timeout_id = g_timeout_add_seconds (3, (GSourceFunc)assert_not_reached, NULL);
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_bad_notify_send_command));
test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_bad_notify_send_command));
g_main_loop_run (loop);
g_source_remove (timeout_id);
test_source_cleanup (&ctx1);
test_source_cleanup (&ctx2);
g_object_unref (sched);
}
/*****************************************************************************/
static void
test_ds_delay_notify_send_command (MMPortScheduler *scheduler,
gpointer source,
TestSourceCtx *ctx)
{
guint *counter = ctx->data;
gint64 *last_call = ctx->data2;
gint64 now;
g_assert (scheduler == ctx->sched);
if (source != ctx->source_id)
return;
/* Ensure there was at least the inter-port delay time since the last call */
now = g_get_monotonic_time ();
g_assert_cmpint (now - *last_call, >=, 500);
*last_call = now;
ctx->num_pending--;
g_assert_cmpuint (ctx->num_pending, >=, 0);
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
(*counter)--;
if (*counter == 0)
g_main_loop_quit (loop);
}
static void
test_ds_inter_port_delay (void)
{
MMPortScheduler *sched;
guint counter;
gint64 last_call = 0;
TestSourceCtx ctx1 = {
.source_id = GUINT_TO_POINTER (0x1),
.data = &counter,
.data2 = &last_call,
.num_pending = 5,
};
TestSourceCtx ctx2 = {
.source_id = GUINT_TO_POINTER (0x2),
.data = &counter,
.data2 = &last_call,
.num_pending = 5,
};
counter = ctx1.num_pending + ctx2.num_pending;
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
g_object_set (sched, MM_PORT_SCHEDULER_RR_INTER_PORT_DELAY, 500, NULL);
test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_delay_notify_send_command));
test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_delay_notify_send_command));
g_main_loop_run (loop);
test_source_cleanup (&ctx1);
test_source_cleanup (&ctx2);
g_object_unref (sched);
}
/*****************************************************************************/
static void
test_ds_no_delay_notify_send_command (MMPortScheduler *scheduler,
gpointer source,
TestSourceCtx *ctx)
{
guint *counter = ctx->data;
gint64 *last_call = ctx->data2;
gint64 now;
g_assert (scheduler == ctx->sched);
if (source != ctx->source_id)
return;
/* Since the second source has no pending commands, there should be
* no delay between calls since only one source is executing.
*/
now = g_get_monotonic_time ();
g_assert_cmpint (now - *last_call, <, 1000);
*last_call = now;
ctx->num_pending--;
g_assert_cmpuint (ctx->num_pending, >=, 0);
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
(*counter)--;
if (*counter == 0)
g_main_loop_quit (loop);
}
static void
test_ds_inter_port_no_delay (void)
{
MMPortScheduler *sched;
guint counter;
gint64 last_call;
TestSourceCtx ctx1 = {
.source_id = GUINT_TO_POINTER (0x1),
.data = &counter,
.data2 = &last_call,
.num_pending = 5,
};
TestSourceCtx ctx2 = {
.source_id = GUINT_TO_POINTER (0x2),
.data = &counter,
.data2 = &last_call,
.num_pending = 0,
};
counter = ctx1.num_pending + ctx2.num_pending;
last_call = g_get_monotonic_time ();
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_no_delay_notify_send_command));
test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_no_delay_notify_send_command));
g_main_loop_run (loop);
test_source_cleanup (&ctx1);
test_source_cleanup (&ctx2);
g_object_unref (sched);
}
/*****************************************************************************/
static void
test_ds_pending_during_done_notify_send_command (MMPortScheduler *scheduler,
gpointer source,
TestSourceCtx *ctx)
{
guint *counter = ctx->data;
g_assert (scheduler == ctx->sched);
if (source != ctx->source_id)
return;
ctx->num_pending--;
g_assert_cmpuint (ctx->num_pending, >=, 0);
/* Simulate command completion adding more pending commands before calling command-done */
if (ctx->idle_id > 0) {
ctx->idle_id--;
ctx->num_pending++; /* increase length of fake source's command queue */
mm_port_scheduler_notify_num_pending (ctx->sched, ctx->source_id, ctx->num_pending);
}
mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending);
(*counter)--;
if (*counter == 0)
g_main_loop_quit (loop);
}
static void
test_ds_pending_during_done (void)
{
MMPortScheduler *sched;
guint counter;
TestSourceCtx ctx1 = {
.source_id = GUINT_TO_POINTER (0x1),
.data = &counter,
.idle_id = 5, /* additional to add during notify-command-done */
.num_pending = 5,
};
TestSourceCtx ctx2 = {
.source_id = GUINT_TO_POINTER (0x2),
.data = &counter,
.idle_id = 5, /* additional to add during notify-command-done */
.num_pending = 5,
};
counter = ctx1.num_pending + ctx2.num_pending + ctx1.idle_id + ctx2.idle_id;
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_pending_during_done_notify_send_command));
test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_pending_during_done_notify_send_command));
g_main_loop_run (loop);
test_source_cleanup (&ctx1);
test_source_cleanup (&ctx2);
g_object_unref (sched);
}
/*****************************************************************************/
static void
test_errors_bad_source_done (void)
{
MMPortScheduler *sched;
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
mm_port_scheduler_notify_command_done (sched, GUINT_TO_POINTER (0x1), 5);
g_object_unref (sched);
}
/*****************************************************************************/
static void
test_errors_source_done_before_loop (void)
{
MMPortScheduler *sched;
TestSourceCtx ctx = {
.source_id = GUINT_TO_POINTER (0x1),
};
sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ());
test_source_setup (&ctx, sched, G_CALLBACK (assert_not_reached));
mm_port_scheduler_notify_command_done (sched, ctx.source_id, 5);
test_source_cleanup (&ctx);
g_object_unref (sched);
}
/*****************************************************************************/
int main (int argc, char **argv)
{
int ret;
setlocale (LC_ALL, "");
g_test_init (&argc, &argv, NULL);
loop = g_main_loop_new (NULL, FALSE);
g_test_add_data_func ("/MM/port-scheduler/single-source/done-immediate", GUINT_TO_POINTER (TRUE), (GTestDataFunc)test_ss_done);
g_test_add_data_func ("/MM/port-scheduler/single-source/done-idle", GUINT_TO_POINTER (FALSE), (GTestDataFunc)test_ss_done);
g_test_add_data_func ("/MM/port-scheduler/dual-source/ordering", NULL, (GTestDataFunc)test_ds_ordering);
g_test_add_data_func ("/MM/port-scheduler/dual-source/uneven-num-pending", NULL, (GTestDataFunc)test_ds_uneven_num_pending);
g_test_add_data_func ("/MM/port-scheduler/dual-source/num-pending-later", NULL, (GTestDataFunc)test_ds_num_pending_later);
g_test_add_data_func ("/MM/port-scheduler/dual-source/bad-notify-done", NULL, (GTestDataFunc)test_ds_bad_notify_done);
g_test_add_data_func ("/MM/port-scheduler/dual-source/inter-port-delay", NULL, (GTestDataFunc)test_ds_inter_port_delay);
g_test_add_data_func ("/MM/port-scheduler/dual-source/inter-port-no-delay", NULL, (GTestDataFunc)test_ds_inter_port_no_delay);
g_test_add_data_func ("/MM/port-scheduler/dual-source/pending-during-done", NULL, (GTestDataFunc)test_ds_pending_during_done);
g_test_add_data_func ("/MM/port-scheduler/errors/bad-source-done", NULL, (GTestDataFunc)test_errors_bad_source_done);
g_test_add_data_func ("/MM/port-scheduler/errors/source-done-before-loop", NULL, (GTestDataFunc)test_errors_source_done_before_loop);
ret = g_test_run();
g_main_loop_unref (loop);
return ret;
}