vorschlag: (optionale) asynchrone ausgabe von get kommandos

Begonnen von justme1968, 08 November 2015, 22:29:06

Vorheriges Thema - Nächstes Thema

justme1968

mit dem hier vorgestellten vorschlag wird es möglich die ausgabe von potentiell länger dauernden get kommandos asynchron zu machen und so fhem nicht auf das fertige kommando warten zu lassen.

die idee ist:

  • in den frontend modulen (telnet, fhemweb, ...) eine FW_DelayedOutput routine hinzuzufügen die dem anwender eine ausgabe zeigen kann,
  • das jeweilige device ($cl) über das ein kommando abgesetzt wurde in die GetFn zu übergeben
  • und dann asynchron sobald eine antwort vorliegt über eine zentrale delayedOutput funktion die meldung an dasjenige eingabe device zu verteilen von dem die anfrage ursprünglich gekommen ist.

ein konkretes beispiel an dem ich gerade arbeite ist ein modul das per HttpUtils_NonblockingGet bestimmte werte abfragt. sobald diese abfrage durch ein get angestossen wird muss ich bis jetzt auf HttpUtils_BlockingGet ausweichen um dem anwender die antwort auch zeigen zu können. mit der hier vorgeschlagenen änderung kann jetzt in meiner GetFn $cl an den non-blocking callback durchreichen und dann die ausgabe asynchron anzeigen sobald sie da ist. ich habe alleine in mindestens 3-4 anderen meiner module dafür verwendung.

die änderungen um die funktionalität einzubauen sind recht klein.

fhem.pl:
hier wird $cl an die GetFn durchgereicht und die delayedOutput funktion eingeführt.Index: fhem.pl
===================================================================
--- fhem.pl (revision 9780)
+++ fhem.pl (working copy)
@@ -1599,12 +1601,32 @@
     }

     $a[0] = $sdev;
+    $defs{$sdev}->{CL} = $cl;
     my $ret = CallFn($sdev, "GetFn", $defs{$sdev}, @a);
+    delete $defs{$sdev}->{CL};
     push @rets, $ret if(defined($ret) && $ret ne "");
   }
   return join("\n", @rets);
}
+sub
+delayedOutput($$)
+{
+  my ($cl,$ret) = @_;

+  return undef if( !$cl );
+
+  if( !$defs{$cl->{NAME}}
+      || $defs{$cl->{NAME}}->{NR} != $cl->{NR}
+      || $defs{$cl->{NAME}}->{NAME} ne $cl->{NAME} ) {
+
+    Log3 $cl->{NAME}, 3, "$cl->{NAME} delayedOutput: device gone, output was: $ret";
+   
+    return undef;
+  }
+
+  $ret = CallFn($cl->{NAME}, "DelayedOutputFn", $defs{$cl->{NAME}}, $ret);
+
+  return $ret;
+}
+


98_telnet.pm:
hier wird die DelayedOutputFn eingeführt die einfach auf das socket schreibt (und auch gleich intern verwendet).Index: 98_telnet.pm
===================================================================
--- 98_telnet.pm (revision 9565)
+++ 98_telnet.pm (working copy)
@@ -17,6 +17,7 @@

   $hash->{DefFn}   = "telnet_Define";
   $hash->{ReadFn}  = "telnet_Read";
+  $hash->{DelayedOutputFn}  = "telnet_Output";
   $hash->{UndefFn} = "telnet_Undef";
   $hash->{AttrFn}  = "telnet_Attr";
   $hash->{NotifyFn}= "telnet_SecurityCheck";
@@ -284,9 +285,26 @@
   $ret .= (join("\n", @ret) . "\n") if(@ret);
   $ret .= ($hash->{prevlines} ? "> " : $hash->{prompt}." ")
           if($gotCmd && $hash->{showPrompt} && !$hash->{rcvdQuit});
+
+  $ret =~ s/\n/\r\n/g if($pw);  # only for DOS telnet
+  telnet_Output($hash,$ret);
+
+  if($hash->{rcvdQuit}) {
+    if($hash->{isClient}) {
+      delete($hash->{rcvdQuit});
+      telnet_ClientDisconnect($hash, 0);
+    } else {
+      CommandDelete(undef, $name);
+    }
+  }
+}
+sub
+telnet_Output($$)
+{
+  my ($hash,$ret) = @_;
+
   if($ret) {
     $ret = utf8ToLatin1($ret) if( $hash->{encoding} eq "latin1" );
-    $ret =~ s/\n/\r\n/g if($pw);  # only for DOS telnet
     for(;;) {
       my $l = syswrite($hash->{CD}, $ret);
       last if(!$l || $l == length($ret));
@@ -295,14 +313,8 @@
     $hash->{CD}->flush();

   }
-  if($hash->{rcvdQuit}) {
-    if($hash->{isClient}) {
-      delete($hash->{rcvdQuit});
-      telnet_ClientDisconnect($hash, 0);
-    } else {
-      CommandDelete(undef, $name);
-    }
-  }
+
+  return undef;
}


01_FHEMWEB.pl:
hier wird eine DelayedOutputFn eingeführt die die ausgabe in einem jQuery popup anzeigt.Index: 01_FHEMWEB.pm
===================================================================
--- 01_FHEMWEB.pm (revision 9780)
+++ 01_FHEMWEB.pm (working copy)
@@ -119,6 +119,7 @@
   my ($hash) = @_;

   $hash->{ReadFn}  = "FW_Read";
+  $hash->{DelayedOutputFn}  = "FW_DelayedOutput";
   $hash->{GetFn}   = "FW_Get";
   $hash->{SetFn}   = "FW_Set";
   $hash->{AttrFn}  = "FW_Attr";
@@ -476,6 +477,19 @@
}

sub
+FW_DelayedOutput($$)
+{
+  my ($hash, $ret) = @_;
+
+  $ret = "<pre>$ret</pre>" if($ret =~ m/\n/ );
+  $ret =~ s/\n/<br>/g;
+  my $data = FW_longpollInfo('JSON', "#FHEMWEB:$hash->{NAME}","FW_okDialog('$ret')","");
+  addToWritebuffer($hash, $data."\n");
+
+  return undef;
+}
+
+sub
FW_closeConn($)
{
   my ($hash) = @_;


das ganze lässt sich auch auf andere frontends ausdehnen und im prinzip auch für asynchrone, modul übergreifende GetFn aufrufe verwenden, funktioniert aber natürlich nur wenn es auch in einem modul verwendet wird das die Abarbeitung des kommandos selber asynchron erledigt.

diskussionspunkte:
- für das durchreichen von $cl ist mir keine elegantere methode eingefallen die funktioniert ohne die signatur zu ändern.
- ist es sinnvoll das auch für set einzubauen?
- man könnte noch eine überschrift einblenden zu welchem kommando die ausgabe gehört und wie lange es gedauert hat.

gruss
  andre

edit: es gibt noch ein unstimmigkeit mit fhemweb: beim absetzen des get über das drop down menü und klick auf 'get' erscheint das popup mit der antwort nicht, nur bei eingabe eines get kommandos in maininput text feld. ich bin mir noch nicht sicher woran es liegt aber vermutlich wird statt dem fhemweb device das zur aktuellen seite gehört das wenige verwendet das zum ihr get kommando gehört. ideen wie das zu lösen ist?

edit2: eine mögliche lösung ist wie beim trigger <web> JS:... die ausgabe an alle devices mit $FW_wname als $hash->{SNAME} zu senden. das hat aber den nachteil das die ausgabe an allen offenen fenstern erscheint.

vorschlag: in den <body> tag jeder seite wird eine eindeutige id generiert die die seite identifiziert und die über FW_cmd und FW_answerCall wieder zurück gegeben wird so das die ausgabe des get eindeutig einer seite zugeordnet werden kann.

edit3: die idee jedes offene fhemweb fenster mit einer eindeutigen id zu versehen funktioniert gut.

01_FHEMWEB.pl:
im body tag jeder fhemweb seite wird ein eindeutiges fw_id attribut erzeugt. dieses wird im XHR header wieder zurück gegeben und in der zur longpoll verbindung gehörenden fhemweb device gespeichert. in FW_DelayedOutput wird dann die longpoll verbindung gesucht die zum fhemweb fenster gehört in dem das get kommando erzeugt wurde.Index: 01_FHEMWEB.pm
===================================================================
--- 01_FHEMWEB.pm       (revision 9780)
+++ 01_FHEMWEB.pm       (working copy)
@@ -106,6 +106,7 @@
my %FW_hiddengroup;# hash of hidden groups
my $FW_inform;
my $FW_XHR;        # Data only answer, no HTML
+my $FW_id;         # id of current page
my $FW_jsonp;      # jasonp answer (sending function calls to the client)
my $FW_headercors; #
my $FW_chash;      # client fhem hash
@@ -119,6 +120,7 @@
   my ($hash) = @_;

   $hash->{ReadFn}  = "FW_Read";
+  $hash->{DelayedOutputFn}  = "FW_DelayedOutput";
   $hash->{GetFn}   = "FW_Get";
   $hash->{SetFn}   = "FW_Set";
   $hash->{AttrFn}  = "FW_Attr";
@@ -278,6 +280,7 @@

   $FW_chash = $hash;
   $FW_wname = $hash->{SNAME};
+  $FW_id = $hash->{NR};
   $FW_cname = $name;
   $FW_subdir = "";

@@ -476,6 +479,26 @@
}

sub
+FW_DelayedOutput($$)
+{
+  my ($hash, $ret) = @_;
+
+  $ret = "<pre>$ret</pre>" if($ret =~ m/\n/ );
+  $ret =~ s/\n/<br>/g;
+  foreach my $d (keys %defs ) {
+    my $chash = $defs{$d};
+    next if( $chash->{TYPE} ne 'FHEMWEB' );
+    #next if( $chash->{SNAME} ne $FW_wname );
+    next if( !$chash->{FW_ID} || $chash->{FW_ID} ne $FW_id );
+    my $data = FW_longpollInfo('JSON', "#FHEMWEB:$FW_wname","FW_okDialog('$ret')","");
+    addToWritebuffer($chash, $data."\n");
+    last;
+  }
+
+  return undef;
+}
+
+sub
FW_closeConn($)
{
   my ($hash) = @_;
@@ -603,6 +626,7 @@

   if($FW_inform) {      # Longpoll header
     if($FW_inform =~ /type=/) {
+      $me->{FW_ID} = $FW_id;
       foreach my $kv (split(";", $FW_inform)) {
         my ($key,$value) = split("=", $kv, 2);
         $me->{inform}{$key} = $value;
@@ -782,7 +806,7 @@
   my $csrf= ($FW_CSRF ? "fwcsrf='$defs{$FW_wname}{CSRFTOKEN}'" : "");
   my $gen = 'generated="'.(time()-1).'"';
   my $lp  = 'longpoll="'.AttrVal($FW_wname,"longpoll",1).'"';
-  FW_pO "</head>\n<body name=\"$t\" $gen $lp $csrf>";
+  FW_pO "</head>\n<body name=\"$t\" fw_id=\"$FW_id\" $gen $lp $csrf>";

   if($FW_activateInform) {
     $cmd = "style eventMonitor $FW_activateInform";
@@ -902,6 +926,7 @@
     if($p eq "XHR")          { $FW_XHR = 1; }
     if($p eq "jsonp")        { $FW_jsonp = $v; }
     if($p eq "inform")       { $FW_inform = $v; }
+    if($p eq "fw_id")        { $FW_id = $v; }

   }
   $cmd.=" $dev{$c}" if(defined($dev{$c}));


fhemweb.js:
der inhalt des fw_id attributes aus dem body tag wird im FW_cmd header übergeben.
Index: fhemweb.js
===================================================================
--- fhemweb.js (revision 9035)
+++ fhemweb.js (working copy)
@@ -223,6 +223,7 @@
{
   log("FW_cmd:"+arg);
   arg = addcsrf(arg);
+  arg += '&fw_id='+document.body.getAttribute('fw_id');
   var req = new XMLHttpRequest();
   req.open("POST", arg, true);
   req.send(null);


ich vermute die eindeutige zuordnung von auslösendem fhemweb fenster zur zugehörigen longpoll verbindung kann auch noch an anderer stelle nützlich sein.
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

rudolfkoenig

Die grundsaetzliche Idee finde ich gut, und ich werde es auch einbauen.
Was mir noch unklar ist, wie man zwischen Benutzereingabe und Programmaufruf unterscheidet, z.Bsp. wenn jemand get per HTTP Link aus einem externen Programm aufruft.

justme1968

in der url die für den externen aufruf erzeugt wird fehlt der fw_id parameter. diesen gibt es nur wenn die url über FW_cmd aus einer interaktiv verwendeten web seite erzeugt wird.
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

justme1968

#3
vorschlag: im $cl das an die GetFn durchgereicht wird könnten wir noch ein $hash->{canDelayedUpdate} einbauen. das wäre bei telnet immer true, bei fhrmweb nur wenn es interaktiv durch klick zustande gekommen ist.

das kann das modul in der GetFn entscheiden ob es im nicht interaktiven fall auf blockieren zurück fällt oder ob die rückgabe dann ins leere läuft.
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

justme1968

ich bin gerade dabei den patch zu überarbeiten und vollständig zu machen. von meiner seite aus gibt es erst mal nur noch eine stelle die eine frage aufwirft:

das verhalten bei eingabe eines get kommandos in das maininput textfeld: bisher erfolgt nach der eingabe ein redirect auf die hauptweite und es wird eine neue longpoll verbindung aufgemacht. das zuordnen der neuen verbindung zum kommando habe ich zwar inzwischen im griff aber es gibt eine racecondition wenn das kommando schneller abgearbeitet wird als die longpoll verbindung aufgebaut wird.

mir fallen zwei mögliche lösungen ein. fhem merkt das es noch keine longpoll verbindung gibt und verzögert die ausgabe so lange bis die verbindung wieder steht oder das get wird wie die 'anklickbaren' get in der detail ansicht per xhr abgewickelt ohne redirect und neuaufbau der seite.

ersteres habe ich noch nicht vollständig probiert, es versucht aber nur das symptom zu umgehen und es ist unschön die seiten id durch den redirect zu retten.

letzteres hat den zusätzlichen vorteil das durch den wegfall des seitenaufbaus alles effizienter ist und sogar besser aussieht.

welche variante bevorzugst du ?
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

justme1968

#5
anbei die aktuelle (und umbenannte) version des vorschlags.

für das redirect problem habe ich doch noch eine ziemlich einfache lösung gefunden und eingebaut.

den umbau des get auf xhr habe ich in fhemweb.js aber trotzdem drin gelassen. ich finde die bedienung ist sehr viel angenehmer ohne das seiten zappeln und die umleitung auf die haupt seite.

der patch umfasst (wie schon oben beschrieben) folgendes:

  • in die GetFn wird über $hash->{CL} das eingabedevice übergeben von dem aus das get kommando abgesetzt wurde

  • es gibt eine asyncOutput routine mit der asynchron eine ausgabe auf ein solches (gemerktes) client device erfolgen kann

  • in telnet und fhemweb ist jeweils eine passende AsyncOutputFn implementiert

  • der GetFn wird in $hash->{canAsyncOutput} übergeben ob das eingabedevice die asynchrone ausgabe potentiell unterstützt.
    ein fhem modul das die asynchrone ausgabe für get kommandos unterstützen will hat so die möglichkeit zu entscheiden ob es im nicht-interaktiven fall auf blocking zurück fällt oder die rückgabe dann verworfen wird.

für telnet ist das ganze straightforward, da die verbindung auf der ein get rein kommt auch für die ausgabe zuständig ist.

in fhemweb ist es etwas anders da die ausgabe nicht auf der gleichen verbindung erfolgt. die zuordnung geschieht hier anhand eines fw_id parameters der für zusammengehörende anfragen jeweils gleich ist und im seiten body und allen relevanten verbindungen enthalten ist.

frontends die den bestehenden longpoll mechanismus verwenden können mit passend gesetztem fw_id parameter im request der longpoll verbindung und im abgesetzten get kommando die zuordnung erreichen, andere frontends können bei bedarf recht einfach nachziehen und anhand der beiden beispiele eine passende AsyncOutputFn implementieren.


unabhängig von der asynchrone ausgabe hat die übergabe von $hash->{CL} übrigens den netten nebeneffekt das es in der GetFn nun möglich ist gezielt text oder html rückgabe zu erzeugen je nach dem ob die rückgabe für telnet oder web sein soll. im neuen plex modul das ich gerade baue kann ich per get so z.b. die info zu titeln oder playlisten inklusive cover bilden anzeigen wenn die ausgabe im browser erfolgt. 
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

rudolfkoenig

Zitatbisher erfolgt nach der eingabe ein redirect auf die hauptweite und es wird eine neue longpoll verbindung aufgemacht.

Das kann ich nicht unterschreiben, und per Test gerade geprueft: bei get bleibt man auf der Detailseite, und man bekommt das Ergebnis in einem Dialog.

justme1968

der dialog kommt nur wenn man das get über die per makeSelect erzeugte get zeile mit get button, drop down menü und das text feld daneben auslöst. die hatten wir bei der großen fhemweb umstellung auf xhr umgebaut.

wenn man das get über freitext eingabe in der maininput zeile auslöst erfolgt bis jetzt ein normales form post auf das in fhemweb mit einem redirect auf die hauptweite geantwortet wird in der dann die rückgabe angezeigt wird. (ich habe in der rückgabe noch < durch &lt; ersetzt damit das auch für rückgaben wie 'usage: get <device> detail <key>' funktioniert.) hier wird bis jetzt nur shutdown abgefangen und nicht per post sondern per xhr weitergeleitet.

mit dem patch wird jetzt auch get abgefangen, per xhr weitergeleitet und das ergebniss entweder in einem dialog angezeigt oder direkt auf der hauptseite falls man sich noch dort befindet.
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

rudolfkoenig

Du bist so aktiv bei den Aenderungen. Soll ich noch ein/zwei Tage warten, bis es "abgehangen" ist?
:)

justme1968

hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

justme1968

hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

justme1968

in der ersten version war noch ein tippfehler. der ist inzwischen korrigiert: http://forum.fhem.de/index.php/topic,43771.msg357870.html#msg357870.

ansonsten funktioniert das ganze so stabil das ich mich interaktiv durch eine komplette plex mediathek suchen/klicken kann ohne das fhem dabei auch nur im geringsten blockiert. auch wenn zum teil abfragen an den plex server dabei sind die mehrere sekunden bis zur antwort brauchen.
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

rudolfkoenig

Ich habe dein Patch durchgelesen.
Da ich mich noch mit Testen schwer tue: stimmt, dass bei einem get im telnet erst der Prompt kommt, und dann die Antwort? Und gibt es eine elegante Moeglichkeit im Modul beide Varianten Varianten zu unterstuetzen? Das eine muesste vom globalen select die Daten holen, der andere vom lokalen.

justme1968

bei telnet funktioniert es wie auf einer unix shell wenn du ein kommando in den hintergrund schickst. du bekommst sofort ein prompt und die ausgabe erscheint einfach irgendwann später. bei der interaktiven verwendung ist das völlig unproblematisch und funktioniert wirklich gut.

ich verwende fhem per telnet übrigens immer ohne prompt (ich vermute zwar das ist eigentlich ein bug :), bin aber dagegen das zu ändern). da fällt es nicht auf.

im vordergrund könnte/kann man sogar weiterarbeiten. im gegensatz zur unix konsole werden die ausgaben dabei auch nicht vermischt da es nicht mehrere prozesse sind. es erscheint immer eine ausgabe komplett.

wenn es dir um den prompt an sich und das aussehen geht: man könnte einfach am ende der AsyncOutputFn im telnet modul noch mal einen prompt ausgeben. dann muss man nur das gemeinsame verwenden von telnet_Output in telnet_Read und AsyncOutputFn ändern oder rückbauen. das kann ich gerne machen.

ich hatte anfangs geplant das ein modul über die initiale get rückgabe signalisieren kann das die ausgabe später kommt so das man etwas anzeigen oder in dem einen telnet fenster warten kann. das zieht aber diverse probleme nach sich falls die ausgabe dann aus irgend einem grund doch unterbleibt. ich bin inzwischen der meinung das es gar nicht so kompliziert sein muss. die aktuelle version ist einfach und transparent.

ich bin mir nicht sicher was du mit lokalem select meinst. es gibt doch nur ein einziges select in fhem. ein zusätzliches lokales select würde nichts gutes bewirken.

ich verwende den aktuellen stand jetzt seit einiger zeit und es funktioniert perfekt für get ausgaben die im bereich von fast sofort bis hin zu 10-20 sekunden kommen. als benutzer wartet man in der regel auf eine solche ausgabe. und selbst wenn nicht funktioniert es gut.

probier mal das hier um ein gefühl dafür zu bekommen:
- ein readingsProxy:define rp readingsProxy rp
attr rp getFn { my $delay = (ord($CMD)-97)*3;;\
\   
  if( $hash->{CL} && $hash->{CL}->{canAsyncOutput} ) {\   
    $hash->{cl} = $hash->{CL};;\
    InternalTimer(gettimeofday()+$delay, "delayed", $hash, 0);;\
    return undef;;\
  }\   
\   
  return ("would wait $delay", 1 );\
}     
attr rp getList a b c
attr rp room rp

- für 99_myUtils.pm:sub                                           
delayed($)                                   
{                                             
  my ($hash) = @_;                           
                                             
  asyncOutput( $hash->{cl}, "test\ntest\ntest\n" );
}                                             


get rp a liefert sofort etwas zurück
get rp b nach etwa 3 sekunden
get rp c nach etwa 6 sekunden
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

rudolfkoenig

Danke fuers Beispiel, habs eingecheckt.

Zitatdu bekommst sofort ein prompt und die ausgabe erscheint einfach irgendwann später.
Das habe ich ja befuerchtet, ist nicht wirklich mein Ding. Da ich mit prompt arbeite, bringt das meine Ausgabe durcheinander. Ist aber zunaechst egal.

Zitatich verwende fhem per telnet übrigens immer ohne prompt
Ich vermute, du tippst kein <return> in einer leeren Zeile.

Zitatdie aktuelle version ist einfach und transparent.
Ist mir auch lieber.

Zitates gibt doch nur ein einziges select in fhem.
Traeumer :)
Ueberall, wo ich meine Finger drin hatte (00_CUL.pm, 00_FBAHA, 00_ZWDongle) wird get mit einem lokalen select realisiert. Das es nicht gut ist, daemmert mir auch :) , deswegen ja die Frage. Im ZWave ist es am schlimmsten, weil es inzischen auch programmatisch verwendet wird, um von der Gegenseite secNonce zu holen, und mein Co-Autor weigert sich vom get Abschied zu nehmen. Aber auch im CUL ist das stoerend (get RFR ccconf), ist nur bisher nicht so stark aufgefallen.