Sichere Erkennung von seriellen Ports

4. März 2015

Bei der Realisierung hardwarenaher Funktionen ist des Öfteren die Kommunikation mit Geräten über einen seriellen Port zu implementieren. Im Zusammenhang mit virtuellen USB Ports, die jederzeit angesteckt oder abgezogen werden können, ist eine sichere Erkennung der im System vorhandenen seriellen Schnittstellen essentiell. Informationen zum seriellen Port und USB lassen sich z.B. auf Wikipedia unter Serielle Schnittstelle – Wikipedia und Universal Serial Bus – Wikipedia finden.

Auch im 21. Jahrhundert sind serielle Schnittstellen kein Relikt längst vergangener Tage, sondern sind häufig als Steuerungsmedium verschiedener Hardwaretypen gegenwärtig. So lassen sich viele UMTS-Sticks oder UMTS-Router nicht nur über eine Weboberfläche oder durch eine sonstige vom Hersteller bereitgestellte Software konfigurieren. Sie stellen zusätzlich Steuerungs- und Monitorfunktionen über eine virtuelle serielle USB Schnittstelle bereit. Oft ist dies die einzige Möglichkeit, über einen erweiterten AT-Befehlssatz Funktionen außerhalb der bereitgestellten UI zu erreichen. Eine Beschreibung des ursprünglich zur Steuerung von Modems geschaffenen AT-Befehlssatzes befindet sich auf AT-Befehlssatz – Wikipedia. Soll beispielsweise die eigene Applikation Änderungen der Signalstärke oder Verbindungsklasse (LTE, HSPA, etc.) eines UTMS-Sticks  visualisieren, ist die Einbindung der seriellen Ports in den Code gefordert.

Das .NET Framework gibt dem Entwickler für die Kommunikation über serielle Schnittstellen die Klasse System.IO.Ports,SerialPort an die Hand. Vorsichtig ausgedrückt ist diese Klasse nicht optimal implementiert und enthält eine Reihe von Fehlern oder Schwächen (siehe z.B. If you *must* use .NET System.IO.Ports.SerialPort – Sparx Blog). Bekanntermaßen sind z.B. das DataReceived Event(vgl. c# – SerialPort not receiving any data – Stack Overflow), die BytesToRead Property (SerialPort.BytesToRead returns 0 even though data in the buffer?) oder die ReadExisting (z.B. Serial Ports in C# ReadExisting() issue? – CodeProject) Methode problematisch. Die Close Methode zum Schließen des Ports erzeugt ein Deadlock, falls auf dem SerialPort Objekt noch ein Eventhandler registriert ist.

Die eigene Erfahrung zeigt, dass insbesondere im Zusammenhang mit virtuellen seriellen USB Ports, auch die statische Methode GetPortNames zur Abfrage der verfügbaren seriellen Schnittstellen teilweise keine korrekten Ergebnisse liefert. Wird ein Gerät bei aktivem geöffnetem Port vom USB abgezogen, erscheint es dennoch weiterhin bei Aufruf von GetPortNames als vorhandene Schnittstelle. Erneutes Einstecken führt dazu, dass es zweimal gemeldet wird. Wird es in einen anderen USB Anschluss eingesteckt, wird möglicherweise eine neue Schnittstelle (korrekt) gemeldet, wobei aber die vorher vorhandene weiterhin in der Auflistung von GetPortNames zu finden ist. Eventuell wird dieses Verhalten durch mangelhafte Treiber der virtuellen COM Ports hervorgerufen. Fakt ist jedenfalls, dass das String Array von COM Portnamen als Resultat dieser Methode nicht ungeprüft benutzt werden kann.

Besonders fatal ist dieses Verhalten von GetPortNames, wenn die Applikation das An- oder Abstecken von Hardware überwachen und bei neu erkannten Schnittstellen eine automatische Erkennung daran angebundener Geräte vornehmen soll. An der von GetPortNames erhaltenen Schnittstellenliste ist erfahrungsgemäß nur verlässlich, dass sie höchstens mehr, aber niemals weniger Portnamen enthält, als tatsächlich vorhanden sind.

Die Erkennung neuer Geräte bzw. ihr Verschwinden kann in Windows durch eine WMI Event Query überwacht werden:

   1: WqlEventQuery deviceArrivalQuery = new WqlEventQuery(

   2:     "SELECT * FROM Win32_DeviceChangeEvent WHERE EventType = 2");

   3: WqlEventQuery deviceRemovalQuery = new WqlEventQuery(

   4:     "SELECT * FROM Win32_DeviceChangeEvent WHERE EventType = 3");

   5:  

   6: this.arrival = new ManagementEventWatcher(deviceArrivalQuery);

   7: this.removal = new ManagementEventWatcher(deviceRemovalQuery);

   8:  

   9: this.arrival.EventArrived += this.ArrivalOnEventArrived;

  10: this.removal.EventArrived += this.RemovalOnEventArrived;

  11:  

  12: // Start listening for events

  13: this.arrival.Start();

  14: this.removal.Start();

In den Eventhandlern ArrivalOnEventArrived (Neues Gerät) und RemovalOnEventArrived (Gerät verschwunden) soll nun zuverlässig die Liste der verfügbaren seriellen Ports ermittelt werden. Nachgelagert kann die Applikation dann auf neu hinzugekommenen Ports eine Geräteerkennung ausführen. Bei entfernten Ports kann sie z.B. die Überwachung eines Gerätes beenden oder sonst geeignet reagieren. Dabei hat sich die folgende statische Methode zum Ermitteln der tatsächlich aktuell vorhandenen COM Ports bewährt:

   1: /// <summary>

   2: /// Detect system's valid serial ports

   3: /// </summary>

   4: /// <returns>

   5: /// String array of names of system's available serial ports

   6: /// </returns>

   7: public static string[] DetectSerialPorts()

   8: {

   9:     // NOTE: Due to bugs in the .NET serial port implementation it is necessary

  10:     // to check all ports availability by trying to open them.

  11:     string[] availableSerialPorts = SerialPort.GetPortNames();

  12:     List<string> testSerialPorts = availableSerialPorts.ToList();

  13:     List<string> checkedSerialPorts = new List<string>();

  14:     foreach (string checkPort in testSerialPorts.Distinct()) {

  15:         try {

  16:             Logger.Trace("Trying to detect serial port {0}", checkPort);

  17:             using (SerialPort testPort = new SerialPort(checkPort)) {

  18:                 testPort.Open();

  19:                 testPort.Close();

  20:             }

  21:             // If this line of code is reached, th serial port exists and 

  22:             // is not in use by any instance

  23:             checkedSerialPorts.Add(checkPort);

  24:         }

  25:         catch (InvalidOperationException) {

  26:             // Port exists:

  27:             // From MSDN documentation: The specified port on the current 

  28:             // instance of the SerialPort is already open.

  29:             Logger.Trace("Got InvalidOperationException on port {0}", checkPort);

  30:             checkedSerialPorts.Add(checkPort);

  31:         }

  32:         catch (UnauthorizedAccessException) {

  33:             // Port exists:

  34:             // From MSDN documentation: Access is denied to the port or the current

  35:             // process, or another process on the system, already has the 

  36:             // specified COM port open either by a SerialPort instance or in 

  37:             // unmanaged code.

  38:             Logger.Trace("Got UnauthorizedAccessException on port {0}", checkPort);

  39:             checkedSerialPorts.Add(checkPort);

  40:         }

  41:         catch (ArgumentOutOfRangeException) {

  42:             // Port exists:

  43:             // From MSDN documentation: One or more of the properties for this 

  44:             // instance are invalid. For example, the Parity, DataBits, or 

  45:             // handshake properties are not valid values; the BaudRate is less 

  46:             // than or equal to zero; the ReadTimeout or WriteTimeout property 

  47:             // is less than zero and is not InfiniteTimeout. 

  48:             Logger.Trace("Got ArgumentOutOfRangeException on port {0}", checkPort);

  49:             checkedSerialPorts.Add(checkPort);

  50:         }

  51:         catch (IOException) {

  52:             // Port does not exist and will not be added to the checked serial 

  53:             // ports list: From MSDN documentation: The port is in an invalid state

  54:             // or an attempt to set the state of the underlying port failed. 

  55:             Logger.Trace("Got IOException on port {0}", checkPort);

  56:         }

  57:         catch (Exception exc) {

  58:             Logger.ErrorException("Unexpected error caught: ", exc);

  59:         }

  60:     }

  61:     availableSerialPorts = checkedSerialPorts

  62:         .Distinct()

  63:         .OrderByDescending(p => p)

  64:         .ToArray();

  65:  

  66:     return availableSerialPorts;

  67: }

Dazu wird jeder Port geöffnet und sofort wieder geschlossen (Zeilen 21-23). Funktioniert das fehlerfrei ist der Port vorhanden und wird der Liste der geprüften Ports hinzugefügt (Zeilen 19-22). Im Fehlerfall ist die geworfene Exception auszuwerten: Nach MSDN Dokumentation werden InvalidOperationException, UnauthorizedAccessException und ArgumentOutOfRangeException unter verschiedenen Umständen geworfen. Alle drei treten jedoch nur bei Existenz des Ports auf. Daher wird im jeweiligen Catch Block die Liste der geprüften Ports um den aktuellen Testkandidaten erweitert (Zeilen 25-50). Tritt hingegen eine IOException auf, ist der Port nicht vorhanden oder verfügbar und wird nicht der Liste der geprüften Ports hinzugefügt (Zeilen 51-59).

Fazit: Die sichere Kommunikation mit Geräten über den seriellen Port ist mit .NET Mitteln möglich. Der Entwickler sollte aber mit Überraschungen rechnen und die implementierten Funktionen am besten mit einer Vielzahl an Gerätekombinationen ausgiebig testen.