Hallo,
ich habe seit einiger Zeit meinen zweiten AVRNETIO im Betrieb, der mit der Original-Pollin-Firmware läuft. Ich kommuniziere mit dem AVRNETIO über (mein) ECMD-Modul via telnet. Es kommt nun vor, daß die telnet-Verbindung nicht mehr steht, wenn ich einen Befehl sende. Gibt es eine Möglichkeit, vor dem write auf den socket herauszufinden, ob die Verbindung noch existiert, um diese dann ggf. zunächst neu aufzubauen? Ist das in DevIO.pm ggf. sogar bereits umgesetzt (muß ECMD noch auf Verwendung von DevIO migrieren)?
Viele Grüße
Boris
ZitatGibt es eine Möglichkeit, vor dem write auf den socket herauszufinden, ob die Verbindung noch existiert
Mir ist keins bekannt (und wuerde mich auch ueberraschen), keepalive wuerde auch nur begrenzt helfen. DevIo_SimpleWrite prueft z.Zt nicht mal den Rueckgabewert von syswrite, die Verbindung optional neu zu oeffnen finde ich aber sinnvoll.
so wie ein tcp-socket funktioniert gibt es keine Möglichkeit vor dem Schreiben zuverlässig herauszufinden, ob die Verbindung noch steht ohne irgendwas zu schreiben. Die Verbindung existiert ja nur virtuell und besteht ausschließlich darin, dass sich beide Seiten jeweils das Tupel aus lokaler und remote ip-addresse und port gemerkt haben, auf der Leitung passiert nur dann was, wenn etwas zu übertragen ist. Und selbst dann, wenn man schreibt, merkt es der Socket potientiell auch nur am Ausbleiben der Antwort (mit großzügigen Timeouts), dass die Gegenstelle nicht mehr erreichbar ist. Wenn das OS keepalive unterstützt und der Socket dafür konfiguriert ist, dann schickt er regelmäßig Packete an die Gegenseite um die Erreichbarkeit zu testen. Aber auch das ist keine Garantie - nach einer Antwort auf ein Keep-alive-packet kann die Gegenstelle ja weiterhin verschwinden ohne die Verbindung sauber abzubauen. Um eine Logik, die bei Nicht-antwort der Gegenseite die Verbindung neu aufbaut, kommt man daher prinzipiell nicht herum.
Gruß,
Norbert
Am gleichen Problem scheitert übrigens regelmäßig 93_DbLog, wenn das Loggen auf einen MySQL Server erfolgt, der nicht auf localhost läuft. Und das Log-Modul bekommt nichts davon mit, dass das Socket nicht mehr existiert und schreibt einfach munter weiter ins Nirvana.
Wollen wir das Problem grundsätzlich in DevIO.pm lösen oder bleibt es jedem Modul selbst überlassen?
Ich denke an eine Funktion, die ähnlich wie expect einen String sendet, dann auf einen String wartet (regex). Wenn ein Timeout kommt, wird die Verbindung neu aufgebaut und es nochmal versucht. Im Fehlerfall dann Fehlermeldung und Abbruch.
Andere Idee?
Viele Grüße
Boris
Gerne in DevIo.pm, aber nur mit Timeout (siehe DevIo_TimeoutRead)
Fuer das erwaehnte Problem (Verbindung tot) ist das Warten auf einem String nur dann notwendig, wenn das Modul sonst nicht vom FD liest.
fhem.pl wuerde sonst kurz nach dem Schreiben das Modul-eigene ReadFn aufrufen, der dann mit einem sysread feststellen kann, dass die Verbindung tot ist.
Zitat von: rudolfkoenig am 06 März 2014, 10:33:21...kurz nach dem Schreiben[...]mit einem sysread feststellen kann, dass die Verbindung tot ist.
Genau das geht bei einer TCP-verbindung eben nicht (zuverlässig) sondern nur dann, wenn die Verbindung (von der Gegenseite) sauber geschlossen wurde oder die Gegenseite ankommende Pakete zu einer lokal schon geschlossenen Verbindung mit einem RST abweist um zu melden, dass die Verbindung nicht mehr gültig ist. Ist die Gegenseite z.B. ein NetIO der sich einfach aufgehängt hat, so dass die Pakete ins Leere gehen, dann muss erst mal der TCP-timeout vergehen, bevor der FD ungültig wird. In diesem Fall ist das explizite Warten auf eine bestimmte Response sinnvoller als sich auf den Fehler beim Sysread zu verlassen.
Wenn man nicht synchron (a la DevIO_TiumeoutRead) auf das Ende des Timeouts warten will, dann könnte eine neue Schreib-methode im DevIO im Device-hash einen Eintrag ablegen, bis wann spätestens die Antwort erwartet wird und per InternalTimer auf kurz nach diesem Zeitpunkt eine Methode schedulen, die überprüft, ob dieser Eintrag noch da bzw. schon abgelaufen ist. Beim Lesen der Antwort über die ReadFn wird der Eintrag wieder entfernt. Immer wenn die Antwort zu spät kommt und der InternalTimer vorher aufgerufen wird hat man einen Timeout (asynchron) erkannt.
Klingt sinnvoll. Weisst Du wie lang der TCP-Timeout ist?
Der Timeout ist nicht fest und nicht für alle Systeme gleich. Er hängt üblicherweise (wenn der Stack nach rfc2988 (http://tools.ietf.org/html/rfc2988) implementiert ist) von der Verbindungsqualität (genauer gesagt der Roundtriptime) ab. Nach meiner eigenen Erfahrung dauert es auf Linux typischerweise eine meist zweistellige Zahl an Minuten (bei Mobilfunkverbindungen kann das aber auch mal eine Stunde sein) bis die Verbindung per Timeout geschlossen wird.
Hallo,
ich schlage folgendes vor (nicht getestet!).
########################
# Write something, then read something
# reopen device if timeout occurs and write again, then read again
sub DevIo_Expect($$$)
{
my ($hash, $msg, $timeout) = @_;
# write something
return undef unless defined(DevIo_SimpleWrite($hash, $msg, 0));
# read answer
my $answer= DevIo_TimeoutRead($hash, $timeout);
return $answer unless($answer eq ""):
# reopen device
# unclear how to know whether the following succeeded
DevIo_OpenDev($hash, 1, undef); # where to get the initfn from?
# write something again
return undef unless defined(DevIo_SimpleWrite($hash, $msg, 0));
# read answer again
$answer= DevIo_TimeoutRead($hash, $timeout);
# success
return $answer unless($answer eq ""):
# ultimate failure
return undef; # undef means ultimate failure
}
Viele Grüße
Boris
Probleme mit der vorgeschlagenen DevIO_Expect:
- FHEM wartet exklusiv auf diesem Geraet.
- Falls ein Modul mit dieser Funktion von einem lokalen USB-Stick liest, und waehrend des Aufrufs der Stick entfernt wird, dann wird das zu SIGSEGV/Blockieren/reboot-Notwendigkeit fuehren, weil in solchen Faellen das Oeffnen nicht sofort erfolgen darf. Das wird in DevIO_Disconnected beruecksichtigt.
Ich finde den (leider komplizierteren) Vorschlag von ntruchsess den richtigen Weg.
Seufz...
Ich mache mich mal dran, ECMD auf DevIO zu migrieren und erledige das dann gleich mit. Ich stelle den Code dann erstmal im Forum zum testen ein.
Viele Grüße
Boris
Nachdem ich mich jetzt an die Sache drangegeben habe, kommen auch schon die Verständnisprobleme.
Norberts Vorschlag beschreibt eine asynchrone Kommunikation.
Bei get oder set würde man aber das Ergebnis synchron benötigen: ich sende einen Befehl an das Gerät und bekomme die Antwort oder eine Fehlermeldung zurück. Wie funktioniert das, wenn FHEM während des Wartens auf die Antwort etwas anderes machen darf?
Viele Grüße
Boris
Synchron funktioniert es nicht bzw. mir faellt nichts einfaches dafuer ein.
Zitat von: Dr. Boris Neubert am 10 März 2014, 19:40:23Wie funktioniert das, wenn FHEM während des Wartens auf die Antwort etwas anderes machen darf?
Das betreffende Modul braucht eine ReadFn, die aus der Fhem-mainloop aufgerufen wird, wenn auf der Socketverbindung Daten eingetroffen sind. Dazu noch eine Funktion, die prüfen kann, ob der gewünschte Timeout abgelaufen ist. Anstelle eines Aufrufs von 'Set/get ...' mit Warten auf die Antwort schickt das betreffende Modul einfach den Request raus, setzt ein 'timeoutflag' und startet den InternalTimer mit der TimeoutFunktion. Anschließend tut es erst mal nichts mehr. Kommen Daten über die ReadFn rein, werden die ausgewertet, die Readings geupdatet und das TimeoutFlag gelöscht. Wird vorher die Timeoutfunktion aufgerufen, schreibt diese etwas dazu passendes in die Readings. Reagieren tut man auf das Ganze, indem man die passenden Notifys auf die gewünschten Readings setzt. (Eines für erfolgreichen Empfang der Antwort, eines für Timeout).
Alternativ könntest Du auch die Perl-protothreads-library (https://github.com/ntruchsess/perl-protothreads/blob/master/ProtoThreadsTest.pl) benutzen. Dann sieht der code etwas 'linearer' aus, so als ob Du in einem Hintergrundthread erst den Request abschickst und dann auf den Timeout wartest. Das Updaten der Readings für Erfolg oder Timeout würde dann aus dem gleichen Protothread heraus erfolgen. Aber letztendlich ist so ein Protothread auch nur eine Statemachine, die man immer wieder aufrufen muss, und die bei jedem Aufruf gegebenenfalls ein Stückchen weiterläuft. D.h. man kann auch nicht synchron zu einem Get oder Set darauf warten, sondern startet den Protothread und tut dann erst mal nix mehr (Außer dass man Protothread z.B. aus einer ReadyFn zyklisch aufrufen läßt oder dafür einen InternalTimer zyklisch immer wieder neu startet). Reagieren tut man dann genauso über Notifys.
Zitat von: ntruchsess am 10 März 2014, 23:06:18
Das betreffende Modul braucht eine ReadFn, die aus der Fhem-mainloop aufgerufen wird, wenn auf der Socketverbindung Daten eingetroffen sind. Dazu noch eine Funktion, die prüfen kann, ob der gewünschte Timeout abgelaufen ist. Anstelle eines Aufrufs von 'Set/get ...' mit Warten auf die Antwort schickt das betreffende Modul einfach den Request raus, setzt ein 'timeoutflag' und startet den InternalTimer mit der TimeoutFunktion. Anschließend tut es erst mal nichts mehr. Kommen Daten über die ReadFn rein, werden die ausgewertet, die Readings geupdatet und das TimeoutFlag gelöscht. Wird vorher die Timeoutfunktion aufgerufen, schreibt diese etwas dazu passendes in die Readings. Reagieren tut man auf das Ganze, indem man die passenden Notifys auf die gewünschten Readings setzt. (Eines für erfolgreichen Empfang der Antwort, eines für Timeout).
OK, das verstehe ich. Im Prinzip schießt man ein Kommando an das Gerät ab und tut dann so, als ob die darauf vom Gerät erstellte Antwort spontan gesendet wurde (wie bei einer Wetterstation, die ab und an ungefragt die Temperatur schickt). Zusätzlich überwacht man über einen Timer, ob wirklich was zurückkam, und geht in eine Fehlerbehandlung, wenn das nicht innerhalb der festgelegten Zeit geschah. Das ist clever.
Ich beschäftige mich aber mit einer viel trivialeren Frage: ich will eine Lampe mit
set Lampe on anknipsen. Und dann soll die Lampe angehen und ich kann das nächste Kommando senden. Und wenn die Verbindung zum Schalter kaputt ist, soll diese erst noch versucht werden, neu aufzubauen. Und wenn das alles nicht klappt, dann bekomme ich eine Fehlermeldung.
Also doch meine (nur im Fehlerfall maximal 2x Timeout) blockierende DevIo_Expect aus einem vorigen Post von mir in diesem Thread? In der Regel antwortet so ein Mikrocontroller ja innerhalb einer Sekunde und der Timeout kann kurz eingestellt sein. Ist das eigentlich wirklich problematisch, zumal der Fall ja nur eintritt, wenn die Verbindung zum Gerät abgerissen ist?
Ich würde dann aber dafür sorgen, daß Geräte, die DISCONNECTED sind, überhaupt erst nicht angesprochen werden. DISCONNECTED ist ein Gerät dann, wenn DevIo_Expect trotz reopen und erneuter Versendung innerhalb des Timeouts keine Antwort zurückliefert. Es braucht dann wohl noch einen Zwischenzustand FAILING.
Viele Grüße
Boris
Zitat von: Dr. Boris Neubert am 11 März 2014, 19:51:12Ist das eigentlich wirklich problematisch, zumal der Fall ja nur eintritt, wenn die Verbindung zum Gerät abgerissen ist?
Wenn der Timeout akzeptabel kurz, das Kommando im Regelfall schnell erledigt und der Fehlerfall eher selten ist, ist es natürlich nicht problematisch. Im Allgemeinen Fall, bei dem die 3 Kriterien nicht (sicher) zutreffen, aber schon.
So, jetzt habe ich zwei Funktionen in DevIo.pm nachgerüstet.
DevIo_SimpleReadWithTimeout() liest Daten vom Gerät, wartet aber höchstens bis zum Timeout, daß Daten vom Gerät zum Lesen bereitgestellt werden.
########################
# wait at most timeout seconds until the file handle gets ready
# for reading; returns undef on timeout
sub
DevIo_SimpleReadWithTimeout($$)
{
my ($hash, $timeout) = @_;
my $rin = "";
vec($rin, $hash->{FD}, 1) = 1;
my $nfound = select($rin, undef, undef, $timeout);
return DevIo_DoSimpleRead($hash) if($nfound> 0);
return undef;
}
DevIo_Expect() sendet eine Nachricht an das Gerät. Dann wird die Antwort vom Gerät gelesen und zurückgeliefert. Kommt es zum Timeout, wird die Verbindung zum Gerät geschlossen und neu aufgebaut. Danach wird die Nachricht erneut an das Gerät gesendet, die Antwort vom Gerät gelesen und zurückgeliefert. Scheitert auch dies, wird die Verbindung zum Gerät endgültig geschlossen. Das Gerät ist dann in einem Fehlerzustand.
########################
# Write something, then read something
# reopen device if timeout occurs and write again, then read again
sub DevIo_Expect($$$)
{
my ($hash, $msg, $timeout) = @_;
my $name= $hash->{NAME};
my $state= $hash->{STATE};
if($state ne "opened") {
Log3 $name, 2, "Attempt to write to $state device.";
return undef;
}
# write something
return undef unless defined(DevIo_SimpleWrite($hash, $msg, 0));
# read answer
my $answer= DevIo_SimpleReadWithTimeout($hash, $timeout);
return $answer unless($answer eq "");
# the device has failed to deliver a result
$hash->{STATE}= "failed";
DoTrigger($name, "FAILED");
# reopen device
# unclear how to know whether the following succeeded
Log3 $name, 2, "$name: first attempt to read timed out, trying to close and open the device.";
# The next two lines are required to avoid a deadlock when the remote end closes the connection
# upon DevIo_OpenDev, as e.g. netcat -l <port> does.
DevIo_CloseDev($hash);
sleep(5) if($hash->{USBDEV});
DevIo_OpenDev($hash, 0, undef); # where to get the initfn from?
# write something again
return undef unless defined(DevIo_SimpleWrite($hash, $msg, 0));
# read answer again
$answer= DevIo_SimpleReadWithTimeout($hash, $timeout);
# success
if($answer ne "") {
$hash->{STATE}= "opened";
DoTrigger($name, "CONNECTED");
return $answer;
}
# ultimate failure
Log3 $name, 2, "$name: second attempt to read timed out, this is an unrecoverable error.";
DoTrigger($name, "DISCONNECTED");
return undef; # undef means ultimate failure
}
Unschön ist die Erfordernis zum Schließen im Mittelteil, da ein einfaches Reopen zum Deadlock führen kann.
Ich habe DevIo_Expect() mit ECMD intensiv getestet, und zwar mit der NetServer- und der Ethersex-Firmware auf einem AVRNETIO und mit einem netcat -l <port> auf der Gegenseite.
Ich bitte um Review und idealerweise Einbau in DevIO.pm. Patch anbei (Achtung: im Patch ist das Logging in DevIo_SimpleWrite() mit einer Begründung auskommentiert).
Viele Grüße
Boris
Habs eingecheckt, bis auf den Ausbau der Log-Zeile. Falls es nicht geloggt werden soll, muss das Modul temporaer das Logging abschalten (verbose fuer das Geraet auf 0 setzen)
Danke!
Viele Grüße
Boris
Währe es nicht auch gut für TCP Verbindungen Keep-Alives zu aktivieren um eine höhere Chance zu erreichen TCP Verbindungen als geschlossen zu erkennen, wo die Gegenseite aufgrund eines Stromausfalls o.ä. die Session nicht mehr kennt?.
DevIo.pm - Zeile 146: $conn->setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1) if(defined($conn));
Gruß
Markus
Habs hinzugefuegt, auch zu TcpServerUtils.pm.
Scheint keine negativen Auswirkungen zu haben, habs aber nicht getestet, wie es im Fehlerfall auswirkt.