1 | // Copyright (C) 2011-2012 Denis Shienkov <denis.shienkov@gmail.com> |
2 | // Copyright (C) 2011 Sergey Belyashov <Sergey.Belyashov@gmail.com> |
3 | // Copyright (C) 2012 Laszlo Papp <lpapp@kde.org> |
4 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
5 | |
6 | #include "qserialportinfo.h" |
7 | #include "qserialportinfo_p.h" |
8 | #include "qserialport_p.h" |
9 | |
10 | #include <QtCore/qlockfile.h> |
11 | #include <QtCore/qfile.h> |
12 | #include <QtCore/qdir.h> |
13 | #include <QtCore/qtenvironmentvariables.h> |
14 | |
15 | #include <private/qcore_unix_p.h> |
16 | |
17 | #include <memory> |
18 | |
19 | #include <errno.h> |
20 | #include <sys/types.h> // kill |
21 | #include <signal.h> // kill |
22 | |
23 | #include "qtudev_p.h" |
24 | |
25 | QT_BEGIN_NAMESPACE |
26 | |
27 | constexpr char SkipUdevEnvVarName[] = "QT_SERIALPORT_SKIP_UDEV_LOOKUP" ; |
28 | |
29 | static QStringList filteredDeviceFilePaths() |
30 | { |
31 | static const QStringList deviceFileNameFilterList = QStringList() |
32 | |
33 | #ifdef Q_OS_LINUX |
34 | << QStringLiteral("ttyS*" ) // Standard UART 8250 and etc. |
35 | << QStringLiteral("ttyO*" ) // OMAP UART 8250 and etc. |
36 | << QStringLiteral("ttyUSB*" ) // Usb/serial converters PL2303 and etc. |
37 | << QStringLiteral("ttyACM*" ) // CDC_ACM converters (i.e. Mobile Phones). |
38 | << QStringLiteral("ttyGS*" ) // Gadget serial device (i.e. Mobile Phones with gadget serial driver). |
39 | << QStringLiteral("ttyMI*" ) // MOXA pci/serial converters. |
40 | << QStringLiteral("ttymxc*" ) // Motorola IMX serial ports (i.e. Freescale i.MX). |
41 | << QStringLiteral("ttyAMA*" ) // AMBA serial device for embedded platform on ARM (i.e. Raspberry Pi). |
42 | << QStringLiteral("ttyTHS*" ) // Serial device for embedded platform on ARM (i.e. Tegra Jetson TK1). |
43 | << QStringLiteral("rfcomm*" ) // Bluetooth serial device. |
44 | << QStringLiteral("ircomm*" ) // IrDA serial device. |
45 | << QStringLiteral("tnt*" ); // Virtual tty0tty serial device. |
46 | #elif defined(Q_OS_FREEBSD) |
47 | << QStringLiteral("cu*" ); |
48 | #elif defined(Q_OS_QNX) |
49 | << QStringLiteral("ser*" ); |
50 | #else |
51 | ; |
52 | #endif |
53 | |
54 | QStringList result; |
55 | |
56 | QDir deviceDir(QStringLiteral("/dev" )); |
57 | if (deviceDir.exists()) { |
58 | deviceDir.setNameFilters(deviceFileNameFilterList); |
59 | deviceDir.setFilter(QDir::Files | QDir::System | QDir::NoSymLinks); |
60 | QStringList deviceFilePaths; |
61 | const auto deviceFileInfos = deviceDir.entryInfoList(); |
62 | for (const QFileInfo &deviceFileInfo : deviceFileInfos) { |
63 | const QString deviceAbsoluteFilePath = deviceFileInfo.absoluteFilePath(); |
64 | |
65 | #ifdef Q_OS_FREEBSD |
66 | // it is a quick workaround to skip the non-serial devices |
67 | if (deviceAbsoluteFilePath.endsWith(QLatin1String(".init" )) |
68 | || deviceAbsoluteFilePath.endsWith(QLatin1String(".lock" ))) { |
69 | continue; |
70 | } |
71 | #endif |
72 | |
73 | if (!deviceFilePaths.contains(str: deviceAbsoluteFilePath)) { |
74 | deviceFilePaths.append(t: deviceAbsoluteFilePath); |
75 | result.append(t: deviceAbsoluteFilePath); |
76 | } |
77 | } |
78 | } |
79 | |
80 | return result; |
81 | } |
82 | |
83 | QList<QSerialPortInfo> availablePortsByFiltersOfDevices(bool &ok) |
84 | { |
85 | QList<QSerialPortInfo> serialPortInfoList; |
86 | |
87 | const auto deviceFilePaths = filteredDeviceFilePaths(); |
88 | for (const QString &deviceFilePath : deviceFilePaths) { |
89 | QSerialPortInfoPrivate priv; |
90 | priv.device = deviceFilePath; |
91 | priv.portName = QSerialPortInfoPrivate::portNameFromSystemLocation(source: deviceFilePath); |
92 | serialPortInfoList.append(t: priv); |
93 | } |
94 | |
95 | ok = true; |
96 | return serialPortInfoList; |
97 | } |
98 | |
99 | static bool isSerial8250Driver(const QString &driverName) |
100 | { |
101 | return (driverName == QLatin1String("serial8250" )); |
102 | } |
103 | |
104 | static bool isValidSerial8250(const QString &systemLocation) |
105 | { |
106 | #ifdef Q_OS_LINUX |
107 | const mode_t flags = O_RDWR | O_NONBLOCK | O_NOCTTY; |
108 | const int fd = qt_safe_open(pathname: systemLocation.toLocal8Bit().constData(), flags); |
109 | if (fd != -1) { |
110 | struct serial_struct serinfo; |
111 | const int retval = ::ioctl(fd: fd, TIOCGSERIAL, &serinfo); |
112 | qt_safe_close(fd); |
113 | if (retval != -1 && serinfo.type != PORT_UNKNOWN) |
114 | return true; |
115 | } |
116 | #else |
117 | Q_UNUSED(systemLocation); |
118 | #endif |
119 | return false; |
120 | } |
121 | |
122 | static bool isRfcommDevice(QStringView portName) |
123 | { |
124 | if (!portName.startsWith(s: QLatin1String("rfcomm" ))) |
125 | return false; |
126 | |
127 | bool ok; |
128 | const int portNumber = portName.mid(pos: 6).toInt(ok: &ok); |
129 | if (!ok || (portNumber < 0) || (portNumber > 255)) |
130 | return false; |
131 | return true; |
132 | } |
133 | |
134 | // provided by the tty0tty driver |
135 | static bool isVirtualNullModemDevice(const QString &portName) |
136 | { |
137 | return portName.startsWith(s: QLatin1String("tnt" )); |
138 | } |
139 | |
140 | // provided by the g_serial driver |
141 | static bool isGadgetDevice(const QString &portName) |
142 | { |
143 | return portName.startsWith(s: QLatin1String("ttyGS" )); |
144 | } |
145 | |
146 | static QString ueventProperty(const QDir &targetDir, const QByteArray &pattern) |
147 | { |
148 | QFile f(QFileInfo(targetDir, QStringLiteral("uevent" )).absoluteFilePath()); |
149 | if (!f.open(flags: QIODevice::ReadOnly | QIODevice::Text)) |
150 | return QString(); |
151 | |
152 | const QByteArray content = f.readAll(); |
153 | |
154 | const int firstbound = content.indexOf(bv: pattern); |
155 | if (firstbound == -1) |
156 | return QString(); |
157 | |
158 | const int lastbound = content.indexOf(ch: '\n', from: firstbound); |
159 | return QString::fromLatin1( |
160 | ba: content.mid(index: firstbound + pattern.size(), |
161 | len: lastbound - firstbound - pattern.size())) |
162 | .simplified(); |
163 | } |
164 | |
165 | static QString deviceName(const QDir &targetDir) |
166 | { |
167 | return ueventProperty(targetDir, pattern: "DEVNAME=" ); |
168 | } |
169 | |
170 | static QString deviceDriver(const QDir &targetDir) |
171 | { |
172 | const QDir deviceDir(targetDir.absolutePath() + QLatin1String("/device" )); |
173 | return ueventProperty(targetDir: deviceDir, pattern: "DRIVER=" ); |
174 | } |
175 | |
176 | static QString deviceProperty(const QString &targetFilePath) |
177 | { |
178 | QFile f(targetFilePath); |
179 | if (!f.open(flags: QIODevice::ReadOnly | QIODevice::Text)) |
180 | return QString(); |
181 | return QString::fromLatin1(ba: f.readAll()).simplified(); |
182 | } |
183 | |
184 | static QString deviceDescription(const QDir &targetDir) |
185 | { |
186 | return deviceProperty(targetFilePath: QFileInfo(targetDir, QStringLiteral("product" )).absoluteFilePath()); |
187 | } |
188 | |
189 | static QString deviceManufacturer(const QDir &targetDir) |
190 | { |
191 | return deviceProperty(targetFilePath: QFileInfo(targetDir, QStringLiteral("manufacturer" )).absoluteFilePath()); |
192 | } |
193 | |
194 | static quint16 deviceProductIdentifier(const QDir &targetDir, bool &hasIdentifier) |
195 | { |
196 | QString result = deviceProperty(targetFilePath: QFileInfo(targetDir, QStringLiteral("idProduct" )).absoluteFilePath()); |
197 | if (result.isEmpty()) |
198 | result = deviceProperty(targetFilePath: QFileInfo(targetDir, QStringLiteral("device" )).absoluteFilePath()); |
199 | return result.toInt(ok: &hasIdentifier, base: 16); |
200 | } |
201 | |
202 | static quint16 deviceVendorIdentifier(const QDir &targetDir, bool &hasIdentifier) |
203 | { |
204 | QString result = deviceProperty(targetFilePath: QFileInfo(targetDir, QStringLiteral("idVendor" )).absoluteFilePath()); |
205 | if (result.isEmpty()) |
206 | result = deviceProperty(targetFilePath: QFileInfo(targetDir, QStringLiteral("vendor" )).absoluteFilePath()); |
207 | return result.toInt(ok: &hasIdentifier, base: 16); |
208 | } |
209 | |
210 | static QString deviceSerialNumber(const QDir &targetDir) |
211 | { |
212 | return deviceProperty(targetFilePath: QFileInfo(targetDir, QStringLiteral("serial" )).absoluteFilePath()); |
213 | } |
214 | |
215 | QList<QSerialPortInfo> availablePortsBySysfs(bool &ok) |
216 | { |
217 | QDir ttySysClassDir(QStringLiteral("/sys/class/tty" )); |
218 | |
219 | if (!(ttySysClassDir.exists() && ttySysClassDir.isReadable())) { |
220 | ok = false; |
221 | return QList<QSerialPortInfo>(); |
222 | } |
223 | |
224 | QList<QSerialPortInfo> serialPortInfoList; |
225 | ttySysClassDir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); |
226 | const auto fileInfos = ttySysClassDir.entryInfoList(); |
227 | for (const QFileInfo &fileInfo : fileInfos) { |
228 | if (!fileInfo.isSymLink()) |
229 | continue; |
230 | |
231 | QDir targetDir(fileInfo.symLinkTarget()); |
232 | |
233 | QSerialPortInfoPrivate priv; |
234 | |
235 | priv.portName = deviceName(targetDir); |
236 | if (priv.portName.isEmpty()) |
237 | continue; |
238 | |
239 | const QString driverName = deviceDriver(targetDir); |
240 | if (driverName.isEmpty()) { |
241 | if (!isRfcommDevice(portName: priv.portName) |
242 | && !isVirtualNullModemDevice(portName: priv.portName) |
243 | && !isGadgetDevice(portName: priv.portName)) { |
244 | continue; |
245 | } |
246 | } |
247 | |
248 | priv.device = QSerialPortInfoPrivate::portNameToSystemLocation(source: priv.portName); |
249 | if (isSerial8250Driver(driverName) && !isValidSerial8250(systemLocation: priv.device)) |
250 | continue; |
251 | |
252 | do { |
253 | if (priv.description.isEmpty()) |
254 | priv.description = deviceDescription(targetDir); |
255 | |
256 | if (priv.manufacturer.isEmpty()) |
257 | priv.manufacturer = deviceManufacturer(targetDir); |
258 | |
259 | if (priv.serialNumber.isEmpty()) |
260 | priv.serialNumber = deviceSerialNumber(targetDir); |
261 | |
262 | if (!priv.hasVendorIdentifier) |
263 | priv.vendorIdentifier = deviceVendorIdentifier(targetDir, hasIdentifier&: priv.hasVendorIdentifier); |
264 | |
265 | if (!priv.hasProductIdentifier) |
266 | priv.productIdentifier = deviceProductIdentifier(targetDir, hasIdentifier&: priv.hasProductIdentifier); |
267 | |
268 | if (!priv.description.isEmpty() |
269 | || !priv.manufacturer.isEmpty() |
270 | || !priv.serialNumber.isEmpty() |
271 | || priv.hasVendorIdentifier |
272 | || priv.hasProductIdentifier) { |
273 | break; |
274 | } |
275 | } while (targetDir.cdUp()); |
276 | |
277 | serialPortInfoList.append(t: priv); |
278 | } |
279 | |
280 | ok = true; |
281 | return serialPortInfoList; |
282 | } |
283 | |
284 | struct udev_deleter { |
285 | void operator()(struct ::udev *pointer) const |
286 | { |
287 | ::udev_unref(udev: pointer); |
288 | } |
289 | void operator()(struct ::udev_enumerate *pointer) const |
290 | { |
291 | ::udev_enumerate_unref(udev_enumerate: pointer); |
292 | } |
293 | void operator()(struct ::udev_device *pointer) const |
294 | { |
295 | ::udev_device_unref(udev_device: pointer); |
296 | } |
297 | }; |
298 | template <typename T> |
299 | using udev_ptr = std::unique_ptr<T, udev_deleter>; |
300 | |
301 | #ifndef LINK_LIBUDEV |
302 | Q_GLOBAL_STATIC(QLibrary, udevLibrary) |
303 | #endif |
304 | |
305 | static QString deviceProperty(struct ::udev_device *dev, const char *name) |
306 | { |
307 | return QString::fromLatin1(ba: ::udev_device_get_property_value(udev_device: dev, key: name)); |
308 | } |
309 | |
310 | static QString deviceDriver(struct ::udev_device *dev) |
311 | { |
312 | return QString::fromLatin1(ba: ::udev_device_get_driver(udev_device: dev)); |
313 | } |
314 | |
315 | static QString deviceDescription(struct ::udev_device *dev) |
316 | { |
317 | return deviceProperty(dev, name: "ID_MODEL" ).replace(before: QLatin1Char('_'), after: QLatin1Char(' ')); |
318 | } |
319 | |
320 | static QString deviceManufacturer(struct ::udev_device *dev) |
321 | { |
322 | return deviceProperty(dev, name: "ID_VENDOR" ).replace(before: QLatin1Char('_'), after: QLatin1Char(' ')); |
323 | } |
324 | |
325 | static quint16 deviceProductIdentifier(struct ::udev_device *dev, bool &hasIdentifier) |
326 | { |
327 | return deviceProperty(dev, name: "ID_MODEL_ID" ).toInt(ok: &hasIdentifier, base: 16); |
328 | } |
329 | |
330 | static quint16 deviceVendorIdentifier(struct ::udev_device *dev, bool &hasIdentifier) |
331 | { |
332 | return deviceProperty(dev, name: "ID_VENDOR_ID" ).toInt(ok: &hasIdentifier, base: 16); |
333 | } |
334 | |
335 | static QString deviceSerialNumber(struct ::udev_device *dev) |
336 | { |
337 | return deviceProperty(dev,name: "ID_SERIAL_SHORT" ); |
338 | } |
339 | |
340 | static QString deviceName(struct ::udev_device *dev) |
341 | { |
342 | return QString::fromLatin1(ba: ::udev_device_get_sysname(udev_device: dev)); |
343 | } |
344 | |
345 | static QString deviceLocation(struct ::udev_device *dev) |
346 | { |
347 | return QString::fromLatin1(ba: ::udev_device_get_devnode(udev_device: dev)); |
348 | } |
349 | |
350 | QList<QSerialPortInfo> availablePortsByUdev(bool &ok) |
351 | { |
352 | ok = false; |
353 | |
354 | #ifndef LINK_LIBUDEV |
355 | static bool symbolsResolved = resolveSymbols(udevLibrary()); |
356 | if (!symbolsResolved) |
357 | return QList<QSerialPortInfo>(); |
358 | #endif |
359 | |
360 | const udev_ptr<struct ::udev> udev(::udev_new()); |
361 | |
362 | if (!udev) |
363 | return QList<QSerialPortInfo>(); |
364 | |
365 | const udev_ptr<udev_enumerate> enumerate(::udev_enumerate_new(udev: udev.get())); |
366 | |
367 | if (!enumerate) |
368 | return QList<QSerialPortInfo>(); |
369 | |
370 | ::udev_enumerate_add_match_subsystem(udev_enumerate: enumerate.get(), subsystem: "tty" ); |
371 | ::udev_enumerate_scan_devices(udev_enumerate: enumerate.get()); |
372 | |
373 | udev_list_entry *devices = ::udev_enumerate_get_list_entry(udev_enumerate: enumerate.get()); |
374 | |
375 | QList<QSerialPortInfo> serialPortInfoList; |
376 | udev_list_entry *dev_list_entry; |
377 | udev_list_entry_foreach(dev_list_entry, devices) { |
378 | |
379 | ok = true; |
380 | |
381 | const udev_ptr<udev_device> |
382 | dev(::udev_device_new_from_syspath( |
383 | udev: udev.get(), syspath: ::udev_list_entry_get_name(list_entry: dev_list_entry))); |
384 | |
385 | if (!dev) |
386 | return serialPortInfoList; |
387 | |
388 | QSerialPortInfoPrivate priv; |
389 | |
390 | priv.device = deviceLocation(dev: dev.get()); |
391 | priv.portName = deviceName(dev: dev.get()); |
392 | |
393 | udev_device *parentdev = ::udev_device_get_parent(udev_device: dev.get()); |
394 | |
395 | if (parentdev) { |
396 | const QString driverName = deviceDriver(dev: parentdev); |
397 | if (isSerial8250Driver(driverName) && !isValidSerial8250(systemLocation: priv.device)) |
398 | continue; |
399 | priv.description = deviceDescription(dev: dev.get()); |
400 | priv.manufacturer = deviceManufacturer(dev: dev.get()); |
401 | priv.serialNumber = deviceSerialNumber(dev: dev.get()); |
402 | priv.vendorIdentifier = deviceVendorIdentifier(dev: dev.get(), hasIdentifier&: priv.hasVendorIdentifier); |
403 | priv.productIdentifier = deviceProductIdentifier(dev: dev.get(), hasIdentifier&: priv.hasProductIdentifier); |
404 | } else { |
405 | if (!isRfcommDevice(portName: priv.portName) |
406 | && !isVirtualNullModemDevice(portName: priv.portName) |
407 | && !isGadgetDevice(portName: priv.portName)) { |
408 | continue; |
409 | } |
410 | } |
411 | |
412 | serialPortInfoList.append(t: priv); |
413 | } |
414 | |
415 | return serialPortInfoList; |
416 | } |
417 | |
418 | QList<QSerialPortInfo> QSerialPortInfo::availablePorts() |
419 | { |
420 | bool ok = false; |
421 | QList<QSerialPortInfo> serialPortInfoList; |
422 | |
423 | const bool skipUdevLookup = qEnvironmentVariableIsSet(varName: SkipUdevEnvVarName); |
424 | if (!skipUdevLookup) |
425 | serialPortInfoList = availablePortsByUdev(ok); |
426 | |
427 | #ifdef Q_OS_LINUX |
428 | if (!ok) |
429 | serialPortInfoList = availablePortsBySysfs(ok); |
430 | #endif |
431 | |
432 | if (!ok) |
433 | serialPortInfoList = availablePortsByFiltersOfDevices(ok); |
434 | |
435 | return serialPortInfoList; |
436 | } |
437 | |
438 | QString QSerialPortInfoPrivate::portNameToSystemLocation(const QString &source) |
439 | { |
440 | return (source.startsWith(c: QLatin1Char('/')) |
441 | || source.startsWith(s: QLatin1String("./" )) |
442 | || source.startsWith(s: QLatin1String("../" ))) |
443 | ? source : (QLatin1String("/dev/" ) + source); |
444 | } |
445 | |
446 | QString QSerialPortInfoPrivate::portNameFromSystemLocation(const QString &source) |
447 | { |
448 | return source.startsWith(s: QLatin1String("/dev/" )) |
449 | ? source.mid(position: 5) : source; |
450 | } |
451 | |
452 | QT_END_NAMESPACE |
453 | |