Instrument Neutral Distributed Interface INDI 2.2.5
hotplugmanager.cpp
Go to the documentation of this file.
1/*
2 Hot Plug Manager Class Source File
3
4 Copyright (C) 2026 Jasem Mutlaq ([email protected])
5
6 This library is free software; you can redistribute it and/or
7 modify it under the terms of the GNU Lesser General Public
8 License as published by the Free Software Foundation; either
9 version 2.1 of the License, or (at your option) any later version.
10
11 This library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Lesser General Public License for more details.
15
16 You should have received a copy of the GNU Lesser General Public
17 License along with this library; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19*/
20
21#include "hotplugmanager.h"
22#include "indilogger.h" // For LOG_DEBUG, LOG_DEBUG, etc.
23#include <algorithm> // For std::remove_if
24#include <set> // For std::set to easily find differences
25#include <errno.h> // For program_invocation_short_name
26#include <chrono> // For std::chrono::steady_clock
27
28#ifdef HAVE_UDEV
29#include <libudev.h> // For udev
30#endif
31
32// Event loop functions (C API)
33extern "C" {
34 int IEAddCallback(int readfiledes, void (*fp)(int, void *), void *p);
35 void IERmCallback(int callbackid);
36}
37
38namespace INDI
39{
40
41[[maybe_unused]] const int MAX_INITIAL_POLL = 5; // Maximum number of initial polls when udev is available
42const int MAX_NON_UDEV_POLL_DURATION_SECONDS = 60; // Maximum duration for non-udev polling in seconds
43[[maybe_unused]] const int NON_UDEV_POLL_INTERVAL_MS =
44 1000; // Default interval for non-udev polling if not specified by driver
45
46
47HotPlugManager::HotPlugManager() : pollingCount(0), oneShotMode(false),
48 nonUdevPollingDurationSeconds(-1), initialPollingDurationSeconds(-1),
49 udevEventReceived(false) // Initialize with -1 (use defaults)
50{
51#ifdef HAVE_UDEV
52 udevContext = nullptr;
53 udevMonitor = nullptr;
54 udevCallbackId = -1;
55#endif
56 hotPlugTimer.setSingleShot(false); // Timer runs periodically
57 hotPlugTimer.callOnTimeout(std::bind(&HotPlugManager::checkHotPlugEvents, this));
58
59 // Initialize mainThreadDebounceTimer for debouncing udev events in the main thread
60 mainThreadDebounceTimer.setSingleShot(true);
61 mainThreadDebounceTimer.setInterval(100);
62 mainThreadDebounceTimer.callOnTimeout(std::bind(&HotPlugManager::checkHotPlugEvents, this));
63
64 LOG_DEBUG("HotPlugManager initialized.");
65#ifdef HAVE_UDEV
66 initUdev(); // Attempt to initialize udev
67#endif
68}
69
70HotPlugManager::~HotPlugManager()
71{
72 stop();
73#ifdef HAVE_UDEV
74 deinitUdev(); // Deinitialize udev
75#endif
76 LOG_DEBUG("HotPlugManager shut down.");
77}
78
79HotPlugManager &HotPlugManager::getInstance()
80{
81 static HotPlugManager instance;
82 return instance;
83}
84
85void HotPlugManager::registerHandler(std::shared_ptr<HotPlugCapableDevice> handler)
86{
87 std::unique_lock<std::recursive_mutex> lock(handlersMutex);
88 // Check if handler is already registered
89 for (const auto& existingHandler : registeredHandlers)
90 {
91 if (existingHandler == handler)
92 {
93 LOG_DEBUG("Attempted to register an already registered HotPlugCapableDevice handler.");
94 return;
95 }
96 }
97 registeredHandlers.push_back(handler);
98 LOG_DEBUG("HotPlugCapableDevice handler registered.");
99}
100
101void HotPlugManager::unregisterHandler(std::shared_ptr<HotPlugCapableDevice> handler)
102{
103 std::unique_lock<std::recursive_mutex> lock(handlersMutex);
104 auto it = std::remove_if(registeredHandlers.begin(), registeredHandlers.end(),
105 [&](const std::shared_ptr<HotPlugCapableDevice> &h)
106 {
107 return h == handler;
108 });
109
110 if (it != registeredHandlers.end())
111 {
112 registeredHandlers.erase(it, registeredHandlers.end());
113 LOG_DEBUG("HotPlugCapableDevice handler unregistered.");
114 }
115 else
116 {
117 LOG_DEBUG("Attempted to unregister a HotPlugCapableDevice handler that was not registered.");
118 }
119}
120
121void HotPlugManager::start(uint32_t intervalMs, bool oneShot)
122{
123 if (hotPlugTimer.isActive())
124 {
125 LOG_DEBUG("HotPlugManager already running.");
126 return;
127 }
128
129 this->oneShotMode.store(oneShot);
130 pollingCount.store(0); // Reset polling count on start
131
132#ifdef HAVE_UDEV
133 if (udevMonitor && udevContext && udevCallbackId >= 0)
134 {
135 // Event-driven mode: udev events handled by event loop callback
136 // Initial polling to detect already connected devices
137
138 // Determine the maximum number of initial polls based on configuration
139 int maxInitialPolls = initialPollingDurationSeconds.load();
140 if (maxInitialPolls == -1)
141 {
142 maxInitialPolls = MAX_INITIAL_POLL; // Use default
143 }
144
145 hotPlugTimer.setSingleShot(false);
146 hotPlugTimer.setInterval(1000); // 1 second interval
147 hotPlugTimer.callOnTimeout([this, maxInitialPolls]()
148 {
149 if (pollingCount.load() < maxInitialPolls)
150 {
151 checkHotPlugEvents(); // Perform a hotplug check
152 pollingCount++;
153 LOGF_DEBUG("HotPlugManager: Initial polling count: %d/%d", pollingCount.load(), maxInitialPolls);
154
155 if (pollingCount.load() >= maxInitialPolls)
156 {
157 LOGF_DEBUG("HotPlugManager: Initial polling finished (%d times).", maxInitialPolls);
158 hotPlugTimer.stop();
159
160 if (this->oneShotMode.load())
161 {
162 LOG_DEBUG("HotPlugManager: Hotplugging disabled (one-shot mode) after initial polling.");
163 }
164 else
165 {
166 LOG_DEBUG("HotPlugManager: Now monitoring udev events via event loop callback.");
167 // The event loop callback will trigger handleUdevEvent() when devices change
168 // No more timer needed - completely event-driven!
169 }
170 }
171 }
172 });
173 hotPlugTimer.start();
174
175 if (maxInitialPolls == MAX_INITIAL_POLL)
176 {
177 LOGF_DEBUG("HotPlugManager started with initial polling (1000ms interval, %d times - default)%s.", maxInitialPolls,
178 this->oneShotMode.load() ? ", then disabled" : ", then event-driven via callback");
179 }
180 else
181 {
182 LOGF_DEBUG("HotPlugManager started with initial polling (1000ms interval, %d times)%s.", maxInitialPolls,
183 this->oneShotMode.load() ? ", then disabled" : ", then event-driven via callback");
184 }
185 }
186 else
187#endif
188 {
189 // Fallback to continuous polling if udev is not available or HAVE_UDEV is not defined
190 hotPlugTimer.setSingleShot(false);
191 hotPlugTimer.setInterval(intervalMs); // Use the interval provided by the driver
192 nonUdevPollingStartTime = std::chrono::steady_clock::now(); // Record start time
193
194 hotPlugTimer.callOnTimeout([this]()
195 {
196 checkHotPlugEvents(); // Perform a hotplug check
197
198 // Determine the maximum duration based on configuration
199 int maxDuration = nonUdevPollingDurationSeconds.load();
200 if (maxDuration == -1)
201 {
202 maxDuration = MAX_NON_UDEV_POLL_DURATION_SECONDS; // Use default
203 }
204
205 // Check if the maximum non-udev polling duration has been reached (0 = unlimited)
206 if (maxDuration > 0)
207 {
208 auto now = std::chrono::steady_clock::now();
209 auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - nonUdevPollingStartTime).count();
210
211 if (elapsed >= maxDuration)
212 {
213 hotPlugTimer.stop();
214 LOGF_DEBUG("HotPlugManager: Non-udev polling stopped after %d seconds (max %d seconds reached).",
215 elapsed, maxDuration);
216 }
217 }
218 });
219 hotPlugTimer.start();
220
221 // Log the configured duration
222 int maxDuration = nonUdevPollingDurationSeconds.load();
223 if (maxDuration == -1)
224 {
225 LOGF_DEBUG("HotPlugManager started with continuous polling interval: %u ms (udev not available). Max duration: %d seconds (default).",
226 intervalMs, MAX_NON_UDEV_POLL_DURATION_SECONDS);
227 }
228 else if (maxDuration == 0)
229 {
230 LOGF_DEBUG("HotPlugManager started with continuous polling interval: %u ms (udev not available). Max duration: unlimited.",
231 intervalMs);
232 }
233 else
234 {
235 LOGF_DEBUG("HotPlugManager started with continuous polling interval: %u ms (udev not available). Max duration: %d seconds.",
236 intervalMs, maxDuration);
237 }
238 }
239}
240
241void HotPlugManager::stop()
242{
243 if (hotPlugTimer.isActive())
244 {
245 hotPlugTimer.stop();
246 LOG_DEBUG("HotPlugManager stopped polling timer.");
247 }
248
249 // Stop the debounce timer if active
250 if (mainThreadDebounceTimer.isActive())
251 {
252 mainThreadDebounceTimer.stop();
253 LOG_DEBUG("HotPlugManager stopped debounce timer.");
254 }
255
256 // Note: The event loop callback remains registered and will be cleaned up
257 // when deinitUdev() is called in the destructor
258}
259
260void HotPlugManager::setNonUdevPollingDuration(int seconds)
261{
262 nonUdevPollingDurationSeconds.store(seconds);
263
264 if (seconds == -1)
265 {
266 LOGF_DEBUG("HotPlugManager: Non-udev polling duration set to default (%d seconds).", MAX_NON_UDEV_POLL_DURATION_SECONDS);
267 }
268 else if (seconds == 0)
269 {
270 LOG_DEBUG("HotPlugManager: Non-udev polling duration set to unlimited.");
271 }
272 else
273 {
274 LOGF_DEBUG("HotPlugManager: Non-udev polling duration set to %d seconds.", seconds);
275 }
276}
277
278void HotPlugManager::setInitialPollingDuration(int seconds)
279{
280 // Clamp the value between 1 and MAX_NON_UDEV_POLL_DURATION_SECONDS, or use -1 for default
281 if (seconds != -1 && seconds > 0)
282 {
283 seconds = std::min(seconds, MAX_NON_UDEV_POLL_DURATION_SECONDS);
284 }
285
286 initialPollingDurationSeconds.store(seconds);
287
288 if (seconds == -1)
289 {
290 LOGF_DEBUG("HotPlugManager: Initial polling duration set to default (%d seconds).", MAX_INITIAL_POLL);
291 }
292 else
293 {
294 LOGF_DEBUG("HotPlugManager: Initial polling duration set to %d seconds.", seconds);
295 }
296}
297
298void HotPlugManager::checkHotPlugEvents()
299{
300 std::unique_lock<std::recursive_mutex> lock(handlersMutex);
301 LOG_DEBUG("HotPlugManager: Checking for hot-plug events...");
302
303 for (const auto& handler : registeredHandlers)
304 {
305 LOG_DEBUG("HotPlugManager: Checking handler for a device type.");
306
307 // 1. Get devices currently managed by the handler
308 std::map<std::string, std::shared_ptr<DefaultDevice>> managedDevices = handler->getManagedDevices(); // This is a copy now
309 std::set<std::string> currentlyManaged;
310 for (const auto& pair : managedDevices)
311 {
312 currentlyManaged.insert(pair.first);
313 }
314
315 // 2. Discover currently connected devices
316 std::vector<std::string> discoveredIdentifiers = handler->discoverConnectedDeviceIdentifiers();
317 std::set<std::string> currentConnected(discoveredIdentifiers.begin(), discoveredIdentifiers.end());
318
319 // 3. Reconcile differences
320
321 // Devices to remove (managed but not currently connected)
322 for (const std::string& identifier : currentlyManaged)
323 {
324 if (currentConnected.find(identifier) == currentConnected.end())
325 {
326 // Device disconnected
327 LOGF_DEBUG("HotPlugManager: Device disconnected: %s", identifier.c_str());
328 std::shared_ptr<DefaultDevice> deviceToRemove = managedDevices.at(identifier);
329 handler->destroyDevice(deviceToRemove);
330 // Note: The handler's internal map should be updated by destroyDevice or subsequent createDevice calls
331 // For std::deque, this means removing the element.
332 }
333 }
334
335 // Devices to add (connected but not currently managed)
336 for (const std::string& identifier : currentConnected)
337 {
338 if (currentlyManaged.find(identifier) == currentlyManaged.end())
339 {
340 // New device connected
341 LOGF_DEBUG("HotPlugManager: New device connected: %s", identifier.c_str());
342 std::shared_ptr<DefaultDevice> newDevice = handler->createDevice(identifier);
343 if (newDevice)
344 {
345 // Define properties for the new device
346 newDevice->ISGetProperties(nullptr);
347 }
348 else
349 {
350 LOGF_ERROR("HotPlugManager: Failed to create device for identifier: %s", identifier.c_str());
351 }
352 }
353 }
354 }
355}
356
357#ifdef HAVE_UDEV
358bool HotPlugManager::initUdev()
359{
360 udevContext = udev_new();
361 if (!udevContext)
362 {
363 LOG_ERROR("HotPlugManager: Failed to create udev context.");
364 return false;
365 }
366
367 udevMonitor = udev_monitor_new_from_netlink(udevContext, "udev");
368 if (!udevMonitor)
369 {
370 LOG_ERROR("HotPlugManager: Failed to create udev monitor.");
371 udev_unref(udevContext);
372 udevContext = nullptr;
373 return false;
374 }
375
376 // Filter for USB devices
377 udev_monitor_filter_add_match_subsystem_devtype(udevMonitor, "usb", nullptr);
378 udev_monitor_filter_add_match_subsystem_devtype(udevMonitor, "usb_device", nullptr);
379
380 if (udev_monitor_enable_receiving(udevMonitor) < 0)
381 {
382 LOG_ERROR("HotPlugManager: Failed to enable udev monitor receiving.");
383 udev_monitor_unref(udevMonitor);
384 udevMonitor = nullptr;
385 udev_unref(udevContext);
386 udevContext = nullptr;
387 return false;
388 }
389
390 // Register the udev file descriptor with the event loop
391 int fd = udev_monitor_get_fd(udevMonitor);
392 if (fd < 0)
393 {
394 LOG_ERROR("HotPlugManager: Failed to get udev monitor file descriptor.");
395 udev_monitor_unref(udevMonitor);
396 udevMonitor = nullptr;
397 udev_unref(udevContext);
398 udevContext = nullptr;
399 return false;
400 }
401
402 udevCallbackId = IEAddCallback(fd, udevCallbackWrapper, this);
403 if (udevCallbackId < 0)
404 {
405 LOG_ERROR("HotPlugManager: Failed to register udev callback with event loop.");
406 udev_monitor_unref(udevMonitor);
407 udevMonitor = nullptr;
408 udev_unref(udevContext);
409 udevContext = nullptr;
410 return false;
411 }
412
413 LOGF_DEBUG("HotPlugManager: udev monitor initialized successfully (callback ID: %d).", udevCallbackId);
414 return true;
415}
416
417void HotPlugManager::deinitUdev()
418{
419 // Unregister the event loop callback
420 if (udevCallbackId >= 0)
421 {
422 IERmCallback(udevCallbackId);
423 udevCallbackId = -1;
424 LOG_DEBUG("HotPlugManager: udev callback unregistered from event loop.");
425 }
426
427 if (udevMonitor)
428 {
429 udev_monitor_unref(udevMonitor);
430 udevMonitor = nullptr;
431 LOG_DEBUG("HotPlugManager: udev monitor deinitialized.");
432 }
433 if (udevContext)
434 {
435 udev_unref(udevContext);
436 udevContext = nullptr;
437 LOG_DEBUG("HotPlugManager: udev context deinitialized.");
438 }
439}
440#endif
441
442#ifdef HAVE_UDEV
443// Static callback wrapper for the event loop
444void HotPlugManager::udevCallbackWrapper(int fd, void* userdata)
445{
447 HotPlugManager* manager = static_cast<HotPlugManager*>(userdata);
448 if (manager)
449 {
450 manager->handleUdevEvent(fd);
451 }
452}
453
454// Handle udev events when the file descriptor becomes readable
455void HotPlugManager::handleUdevEvent(int fd)
456{
458
459 udev_device* dev = udev_monitor_receive_device(udevMonitor);
460 if (dev)
461 {
462 const char* action = udev_device_get_action(dev);
463 const char* subsystem = udev_device_get_subsystem(dev);
464 const char* devnode = udev_device_get_devnode(dev);
465
466 LOGF_DEBUG("HotPlugManager: udev event: %s %s %s", action ? action : "N/A",
467 subsystem ? subsystem : "N/A", devnode ? devnode : "N/A");
468
469 // Signal that a udev event has been received
470 udevEventReceived.store(true);
471
472 // Start/restart the debounce timer
473 mainThreadDebounceTimer.start();
474
475 udev_device_unref(dev);
476 }
477 else
478 {
479 LOG_ERROR("HotPlugManager: udev_monitor_receive_device returned null.");
480 }
481}
482#endif
483
484} // namespace INDI
void IERmCallback(int callbackid)
Definition eventloop.c:581
int IEAddCallback(int readfiledes, void(*fp)(int, void *), void *p)
#define INDI_UNUSED(x)
Definition indidevapi.h:132
#define LOG_DEBUG(txt)
Definition indilogger.h:75
#define LOGF_DEBUG(fmt,...)
Definition indilogger.h:83
#define LOG_ERROR(txt)
Shorter logging macros. In order to use these macros, the function (or method) "getDeviceName()" must...
Definition indilogger.h:72
#define LOGF_ERROR(fmt,...)
Definition indilogger.h:80
int fd
Namespace to encapsulate INDI client, drivers, and mediator classes.
const int MAX_INITIAL_POLL
const int NON_UDEV_POLL_INTERVAL_MS
const int MAX_NON_UDEV_POLL_DURATION_SECONDS