Commit 266378a125 for asterisk.org

commit 266378a125bf8b0c72b066503b66ff3796aae175
Author: Joshua C. Colp <jcolp@sangoma.com>
Date:   Mon Oct 26 10:30:23 2026 -0300

    extstate: Handle lack of or late loading of device state.

    The extension state implementation is stateful resulting in state
    not being updated if a device state provider does not publish an
    update after loading. To handle providers which don't do this we
    query for updated state if we are asked for it and the existing
    state is invalid. This ensures that if the device state provider
    is loaded later we will get the updated state when asked.

    The "core show hints" and "core show hint" CLI commands have also
    been changed so that they will update invalid sources for both
    device and presence before displaying the information.

    Fixes: #2249

diff --git a/main/extension_state.c b/main/extension_state.c
index 9ca4883bf5..f623de4968 100644
--- a/main/extension_state.c
+++ b/main/extension_state.c
@@ -1313,6 +1313,96 @@ struct stasis_topic *ast_extension_state_topic(const char *exten, const char *co
 	return topic;
 }

+/*!
+ * \internal
+ * \brief Refresh the device snapshot of an extension state if it contains invalid device states
+ *
+ * \param state The extension state to refresh
+ *
+ * \pre The extension state must be locked by the caller before calling this function.
+ *
+ * This function checks whether any of the cached device states of the extension state are invalid
+ * and if so attempts to retrieve the latest device state. If a valid device state is retrieved then
+ * the source is updated and an update message is published to any subscribers.
+ */
+static void extension_state_device_snapshot_refresh(struct extension_state *state)
+{
+	struct ast_devstate_aggregate agg;
+	enum ast_extension_states new_device_state;
+	int i;
+	int invalid = 0;
+
+	/* If no cached device state is invalid then there is nothing to refresh */
+	for (i = 0; i < AST_VECTOR_SIZE(&state->device_state_sources); i++) {
+		struct extension_state_device_source *source = AST_VECTOR_GET(&state->device_state_sources, i);
+
+		if (source->info->state == AST_DEVICE_INVALID) {
+			invalid = 1;
+			break;
+		}
+	}
+	if (!invalid) {
+		return;
+	}
+
+	ast_devstate_aggregate_init(&agg);
+
+	for (i = 0; i < AST_VECTOR_SIZE(&state->device_state_sources); i++) {
+		struct extension_state_device_source *source = AST_VECTOR_GET(&state->device_state_sources, i);
+
+		/*
+		 * An invalid state may be because the device state provider was not available when the
+		 * state was last queried, such as when the hint was created before the provider was
+		 * loaded. Since a caller is explicitly requesting the state we make another attempt to
+		 * retrieve it and if it has changed update it and notify any subscribers.
+		 */
+		if (source->info->state == AST_DEVICE_INVALID) {
+			enum ast_device_state device_state = ast_device_state(source->info->device);
+
+			if (device_state != source->info->state) {
+				struct ast_extension_state_device_state_info *info;
+
+				info = extension_state_device_state_info_alloc(source->info->device, device_state);
+				if (info) {
+					ao2_replace(source->info, info);
+					ao2_ref(info, -1);
+				}
+			}
+		}
+
+		ast_devstate_aggregate_add(&agg, source->info->state);
+	}
+
+	new_device_state = ast_devstate_to_extenstate(ast_devstate_aggregate_result(&agg));
+	if (new_device_state != state->device_snapshot->state) {
+		struct ast_extension_state_device_snapshot *device_snapshot;
+		struct ast_extension_state_update_message *update_message;
+
+		device_snapshot = extension_state_device_snapshot_create(new_device_state, &state->device_state_sources, NULL);
+		if (!device_snapshot) {
+			return;
+		}
+
+		update_message = extension_state_update_message_create(state->dialplan_context, state->dialplan_extension,
+			state->device_snapshot, device_snapshot, state->presence_snapshot, state->presence_snapshot);
+
+		/* Even if we can't publish an update message we still ensure the local cached snapshot is up to date */
+		ao2_replace(state->device_snapshot, device_snapshot);
+		ao2_ref(device_snapshot, -1);
+
+		if (update_message) {
+			struct stasis_message *message = stasis_message_create(ast_extension_state_update_message_type(), update_message);
+
+			if (message) {
+				stasis_publish(state->extension_state_topic, message);
+				ao2_ref(message, -1);
+			}
+
+			ao2_ref(update_message, -1);
+		}
+	}
+}
+
 struct ast_extension_state_device_snapshot *ast_extension_state_get_latest_device_snapshot(struct ast_channel *chan,
 	const char *exten, const char *context)
 {
@@ -1325,6 +1415,7 @@ struct ast_extension_state_device_snapshot *ast_extension_state_get_latest_devic
 	}

 	ao2_lock(state);
+	extension_state_device_snapshot_refresh(state);
 	device_snapshot = ao2_bump(state->device_snapshot);
 	ao2_unlock(state);
 	ao2_ref(state, -1);
@@ -1454,6 +1545,8 @@ static char *handle_show_hints(struct ast_cli_entry *e, int cmd, struct ast_cli_
 	i = ao2_iterator_init(extension_states, 0);
 	for (; (state = ao2_iterator_next(&i)); ao2_ref(state, -1)) {
 		ao2_lock(state);
+		extension_state_device_snapshot_refresh(state);
+		extension_state_presence_snapshot_refresh(state);
 		ast_cli(a->fd, "%-30.30s: %-60.60s  State:%-15.15s Presence:%-15.15s Watchers %2zd\n",
 			state->extension,
 			state->hint_extension ? ast_get_extension_app(state->hint_extension) : "None",
@@ -1558,6 +1651,8 @@ static char *handle_show_hint(struct ast_cli_entry *e, int cmd, struct ast_cli_a
 	for (; (extstate = ao2_iterator_next(&i)); ao2_ref(extstate, -1)) {
 		ao2_lock(extstate);
 		if (!strncasecmp(extstate->extension, a->argv[3], extenlen)) {
+			extension_state_device_snapshot_refresh(extstate);
+			extension_state_presence_snapshot_refresh(extstate);
 			ast_cli(a->fd, "%-30.30s: %-60.60s  State:%-15.15s Presence:%-15.15s Watchers %2zd\n",
 				extstate->extension,
 				extstate->hint_extension ? ast_get_extension_app(extstate->hint_extension) : "None",