use saxutils escape for escaping xml (maybe it solves our problems with &...)
[vuplus_dvbapp-plugin] / webinterface / src / webif.py
1 Version = '$Header$';
2
3 # OK, this is more than a proof of concept
4 # things to improve:
5 #  - nicer code
6 #  - screens need to be defined somehow else. 
7 #    I don't know how, yet. Probably each in an own file.
8 #  - more components, like the channellist
9 #  - better error handling
10 #  - use namespace parser
11 from enigma import eServiceReference
12
13 from Screens.Screen import Screen
14 from Tools.Import import my_import
15
16 from Screens.InfoBarGenerics import InfoBarServiceName, InfoBarEvent, InfoBarTuner
17
18 from Components.Sources.Source import ObsoleteSource
19
20 from Components.Sources.Clock import Clock
21 from Components.Sources.ServiceList import ServiceList
22
23 from WebComponents.Sources.ServiceListRecursive import ServiceListRecursive
24 from WebComponents.Sources.Volume import Volume
25 from WebComponents.Sources.EPG import EPG
26 from WebComponents.Sources.Timer import Timer
27 from WebComponents.Sources.Movie import Movie
28 from WebComponents.Sources.Message import Message
29 from WebComponents.Sources.PowerState import PowerState
30 from WebComponents.Sources.RemoteControl import RemoteControl
31 from WebComponents.Sources.Settings import Settings
32 from WebComponents.Sources.SubServices import SubServices
33 from WebComponents.Sources.ParentControl import ParentControl
34 from WebComponents.Sources.About import About
35 from WebComponents.Sources.RequestData import RequestData
36 from WebComponents.Sources.AudioTracks import AudioTracks
37 from WebComponents.Sources.WAPfunctions import WAPfunctions
38 from WebComponents.Sources.MP import MP
39 from WebComponents.Sources.Files import Files
40
41 from Components.Sources.FrontendStatus import FrontendStatus
42
43 from Components.Converter.Converter import Converter
44
45 from Components.Element import Element
46
47 from xml.sax import make_parser
48 from xml.sax.handler import ContentHandler, feature_namespaces
49 from xml.sax.saxutils import escape as escape_xml
50 from twisted.python import util
51
52 # prototype of the new web frontend template system.
53
54 class WebScreen(Screen):
55         def __init__(self, session, request):
56                 Screen.__init__(self, session)
57                 self.stand_alone = True
58                 self.request = request
59                 self.instance = None
60
61 class DummyWebScreen(WebScreen):
62         #use it, if you dont need any source, just to can do a static file with an xml-file
63         def __init__(self, session,request):
64                 WebScreen.__init__(self, session,request)
65
66 class UpdateWebScreen(InfoBarServiceName, InfoBarEvent,InfoBarTuner,WebScreen):
67         def __init__(self, session,request):
68                 WebScreen.__init__(self, session,request)
69                 InfoBarServiceName.__init__(self)
70                 InfoBarEvent.__init__(self)
71                 InfoBarTuner.__init__(self)
72                 self["CurrentTime"] = Clock()
73                 fav = eServiceReference('1:7:1:0:0:0:0:0:0:0:(type == 1) || (type == 17) || (type == 195) || (type == 25) FROM BOUQUET "bouquets.tv" ORDER BY bouquet')
74                 #CurrentService
75                 #Event_Now
76                 #Event_Next
77                 #FrontendStatus
78
79 class MessageWebScreen(WebScreen):
80         def __init__(self, session,request):
81                 WebScreen.__init__(self, session,request)
82                 self["Message"] = Message(session,func = Message.PRINT)
83                 self["GetAnswer"] = Message(session,func = Message.ANSWER)
84
85 class AudioWebScreen(WebScreen):
86         def __init__(self, session,request):
87                 WebScreen.__init__(self, session,request)
88                 self["AudioTracks"] = AudioTracks(session, func=AudioTracks.GET)
89                 self["SelectAudioTrack"] = AudioTracks(session, func=AudioTracks.SET)   
90
91 class AboutWebScreen(WebScreen):
92         def __init__(self, session,request):
93                 WebScreen.__init__(self, session,request)
94                 self["About"] = About(session)
95                 
96 class VolumeWebScreen(WebScreen):
97         def __init__(self, session,request):
98                 WebScreen.__init__(self, session,request)
99                 self["Volume"] = Volume(session)
100
101 class SettingsWebScreen(WebScreen):
102         def __init__(self, session,request):
103                 WebScreen.__init__(self, session,request)
104                 self["Settings"] = Settings(session)
105
106 class SubServiceWebScreen(WebScreen):
107         def __init__(self, session,request):
108                 WebScreen.__init__(self, session,request)
109                 self["SubServices"] = SubServices(session)
110
111 class ServiceWebScreen(WebScreen):
112         def __init__(self, session,request):
113                 WebScreen.__init__(self, session,request)
114                 fav = eServiceReference('1:7:1:0:0:0:0:0:0:0:(type == 1) || (type == 17) || (type == 195) || (type == 25) FROM BOUQUET "bouquets.tv" ORDER BY bouquet')
115                 self["SwitchService"] = ServiceList(fav, command_func = self.zapTo, validate_commands=False)
116                 self["ServiceList"] = ServiceList(fav, command_func = self.getServiceList, validate_commands=False)
117                 self["ServiceListRecursive"] = ServiceListRecursive(session, func=ServiceListRecursive.FETCH)
118
119         def getServiceList(self, sRef):
120                 self["ServiceList"].root = sRef
121
122         def zapTo(self, reftozap):
123                 from Components.config import config
124                 pc = config.ParentalControl.configured.value
125                 if pc:
126                         config.ParentalControl.configured.value = False
127                 self.session.nav.playService(reftozap)
128                 if pc:
129                         config.ParentalControl.configured.value = pc
130                 """
131                 switching config.ParentalControl.configured.value
132                 ugly, but necessary :(
133                 """
134
135 class EPGWebScreen(WebScreen):
136         def __init__(self, session,request):
137                 WebScreen.__init__(self, session,request)
138                 self["EPGTITLE"] = EPG(session,func=EPG.TITLE)
139                 self["EPGSERVICE"] = EPG(session,func=EPG.SERVICE)
140                 self["EPGNOW"] = EPG(session,func=EPG.NOW)
141                 self["EPGNEXT"] = EPG(session,func=EPG.NEXT)
142
143 class MovieWebScreen(WebScreen):
144         def __init__(self, session,request):
145                 WebScreen.__init__(self, session,request)
146                 from Components.MovieList import MovieList
147                 from Tools.Directories import resolveFilename,SCOPE_HDD
148                 movielist = MovieList(eServiceReference("2:0:1:0:0:0:0:0:0:0:" + resolveFilename(SCOPE_HDD)))
149                 self["MovieList"] = Movie(session,movielist,func = Movie.LIST)
150                 self["MovieFileDel"] = Movie(session,movielist,func = Movie.DEL)
151                 self["MovieTags"] = Movie(session,movielist,func = Movie.TAGS)
152
153 class MediaPlayerWebScreen(WebScreen):
154         def __init__(self, session,request):
155                 WebScreen.__init__(self, session,request)
156                 self["FileList"] = MP(session,func = MP.LIST)
157                 self["PlayFile"] = MP(session,func = MP.PLAY)
158                 self["Command"] = MP(session,func = MP.COMMAND)
159                 self["WritePlaylist"] = MP(session,func = MP.WRITEPLAYLIST)
160                 
161 class FilesWebScreen(WebScreen):
162         def __init__(self, session,request):
163                 WebScreen.__init__(self, session,request)
164                 self["DelFile"] = Files(session,func = Files.DEL)
165                 
166 class TimerWebScreen(WebScreen):
167         def __init__(self, session,request):
168                 WebScreen.__init__(self, session,request)
169                 self["TimerList"] = Timer(session,func = Timer.LIST)
170                 self["TimerAddEventID"] = Timer(session,func = Timer.ADDBYID)
171                 self["TimerAdd"] = Timer(session,func = Timer.ADD)
172                 self["TimerDel"] = Timer(session,func = Timer.DEL)
173                 self["TimerChange"] = Timer(session,func = Timer.CHANGE)
174                 self["TimerListWrite"] = Timer(session,func = Timer.WRITE)
175                 self["TVBrowser"] = Timer(session,func = Timer.TVBROWSER)
176                 self["RecordNow"] = Timer(session,func = Timer.RECNOW)
177
178 class RemoteWebScreen(WebScreen):
179         def __init__(self, session,request):
180                 WebScreen.__init__(self, session,request)
181                 self["RemoteControl"] = RemoteControl(session)
182
183 class PowerWebScreen(WebScreen):
184         def __init__(self, session,request):
185                 WebScreen.__init__(self, session,request)
186                 self["PowerState"] = PowerState(session)
187
188 class ParentControlWebScreen(WebScreen):
189         def __init__(self, session,request):
190                 WebScreen.__init__(self, session,request)
191                 self["ParentControlList"] = ParentControl(session)
192                                 
193 class WAPWebScreen(WebScreen):
194         def __init__(self, session,request):
195                 WebScreen.__init__(self, session,request)
196                 self["WAPFillOptionListSyear"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
197                 self["WAPFillOptionListSday"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
198                 self["WAPFillOptionListSmonth"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
199                 self["WAPFillOptionListShour"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
200                 self["WAPFillOptionListSmin"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
201                 
202                 self["WAPFillOptionListEyear"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
203                 self["WAPFillOptionListEday"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
204                 self["WAPFillOptionListEmonth"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
205                 self["WAPFillOptionListEhour"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
206                 self["WAPFillOptionListEmin"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
207                 
208                 self["WAPFillOptionListRecord"] = WAPfunctions(session,func = WAPfunctions.OPTIONLIST)
209                 self["WAPFillOptionListAfterEvent"] = WAPfunctions(session,func = WAPfunctions.OPTIONLIST)
210                 
211                 self["WAPFillValueName"] = WAPfunctions(session,func = WAPfunctions.FILLVALUE)
212                 self["WAPFillValueDescr"] = WAPfunctions(session,func = WAPfunctions.FILLVALUE)
213
214                 self["WAPFillOptionListRepeated"] = WAPfunctions(session,func = WAPfunctions.REPEATED)
215                 self["WAPServiceList"] = WAPfunctions(session, func = WAPfunctions.SERVICELIST)
216
217                 self["WAPdeleteOldOnSave"] = WAPfunctions(session,func = WAPfunctions.DELETEOLD)
218         
219 class StreamingWebScreen(WebScreen):
220         def __init__(self, session,request):
221                 WebScreen.__init__(self, session,request)
222                 from Components.Sources.StreamService import StreamService
223                 self["StreamService"] = StreamService(self.session.nav)
224
225 class M3UStreamingWebScreen(WebScreen):
226         def __init__(self, session,request):
227                 WebScreen.__init__(self, session,request)
228                 from Components.Sources.StaticText import StaticText
229                 from Components.Sources.Config import Config
230                 from Components.config import config
231                 self["ref"] = StaticText()
232                 self["localip"] = RequestData(request,what=RequestData.HOST)
233
234 class TsM3U(WebScreen):
235         def __init__(self, session,request):
236                 WebScreen.__init__(self, session,request)
237                 from Components.Sources.StaticText import StaticText
238                 from Components.Sources.Config import Config
239                 from Components.config import config
240                 self["file"] = StaticText()
241                 self["localip"] = RequestData(request,what=RequestData.HOST)
242
243 class RestartWebScreen(WebScreen):
244         def __init__(self, session,request):
245                 WebScreen.__init__(self, session,request)
246                 import plugin
247                 plugin.restartWebserver()
248                 
249 class GetPid(WebScreen):
250       def __init__(self, session,request):
251          WebScreen.__init__(self, session,request)
252          from Components.Sources.StaticText import StaticText
253          from enigma import iServiceInformation
254          pids = self.session.nav.getCurrentService()
255          if pids is not None:
256                  pidinfo = pids.info()
257                  VPID = hex(pidinfo.getInfo(iServiceInformation.sVideoPID))
258                  APID = hex(pidinfo.getInfo(iServiceInformation.sAudioPID))
259                  PPID = hex(pidinfo.getInfo(iServiceInformation.sPMTPID))
260          self["pids"] = StaticText("%s,%s,%s"%(PPID.lstrip("0x"),VPID.lstrip("0x"),APID.lstrip("0x")))
261          self["localip"] = RequestData(request,what=RequestData.HOST)
262
263
264 # implements the 'render'-call.
265 # this will act as a downstream_element, like a renderer.
266 class OneTimeElement(Element):
267         def __init__(self, id):
268                 Element.__init__(self)
269                 self.source_id = id
270
271         # CHECKME: is this ok performance-wise?
272         def handleCommand(self, args):
273                 if self.source_id.find(",") >=0:
274                         paramlist = self.source_id.split(",")
275                         list={}
276                         for key in paramlist:
277                                 arg = args.get(key, [])
278                                 if len(arg) == 0:
279                                         list[key] = None        
280                                 elif len(arg) == 1:
281                                         list[key] = "".join(arg)        
282                                 elif len(arg) == 2:
283                                         list[key] = arg[0]
284                         self.source.handleCommand(list)
285                 else:
286                         for c in args.get(self.source_id, []):
287                                 self.source.handleCommand(c)
288
289         def render(self, stream):
290                 t = self.source.getHTML(self.source_id)
291                 stream.write(t)
292
293         def execBegin(self):
294                 pass
295
296         def execEnd(self):
297                 pass
298
299         def onShow(self):
300                 pass
301
302         def onHide(self):
303                 pass
304
305         def destroy(self):
306                 pass
307
308 class MacroElement(OneTimeElement):
309         def __init__(self, id, macro_dict, macro_name):
310                 OneTimeElement.__init__(self, id)
311                 self.macro_dict = macro_dict
312                 self.macro_name = macro_name
313
314         def render(self, stream):
315                 self.macro_dict[self.macro_name] = self.source.getHTML(self.source_id)
316
317 class StreamingElement(OneTimeElement):
318         def __init__(self, id):
319                 OneTimeElement.__init__(self, id)
320                 self.stream = None
321
322         def changed(self, what):
323                 if self.stream:
324                         self.render(self.stream)
325
326         def setStream(self, stream):
327                 self.stream = stream
328
329 # a to-be-filled list item
330 class ListItem:
331         def __init__(self, name, filternum):
332                 self.name = name
333                 self.filternum = filternum
334
335 class ListMacroItem:
336         def __init__(self, macrodict, macroname):
337                 self.macrodict = macrodict
338                 self.macroname = macroname
339
340 class TextToHTML(Converter):
341         def __init__(self, arg):
342                 Converter.__init__(self, arg)
343
344         def getHTML(self, id):
345                 return self.source.text # encode & etc. here!
346
347 class TextToURL(Converter):
348         def __init__(self, arg):
349                 Converter.__init__(self, arg)
350
351         def getHTML(self, id):
352                 return self.source.text.replace(" ","%20")
353
354 class ReturnEmptyXML(Converter):
355         def __init__(self, arg):
356                 Converter.__init__(self, arg)
357
358         def getHTML(self, id):
359                 return "<rootElement></rootElement>"
360
361 # a null-output. Useful if you only want to issue a command.
362 class Null(Converter):
363         def __init__(self, arg):
364                 Converter.__init__(self, arg)
365
366         def getHTML(self, id):
367                 return ""
368
369 class JavascriptUpdate(Converter):
370         def __init__(self, arg):
371                 Converter.__init__(self, arg)
372
373         def getHTML(self, id):
374                 # 3c5x9, added parent. , this is because the ie loads this in a iframe. an the set is in index.html.xml
375                 #                all other will replace this in JS
376                 return '<script>parent.set("%s", "%s");</script>\n'%(id, self.source.text.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"').replace('\xb0', '&deg;'))
377
378 # the performant 'listfiller'-engine (plfe)
379 class ListFiller(Converter):
380         def __init__(self, arg):
381                 Converter.__init__(self, arg)
382 #               print "ListFiller-arg: ",arg
383
384         def getText(self):
385                 l = self.source.list
386                 lut = self.source.lut
387                 conv_args = self.converter_arguments
388
389                 # now build a ["string", 1, "string", 2]-styled list, with indices into the
390                 # list to avoid lookup of item name for each entry
391                 lutlist = [ ]
392                 for element in conv_args:
393                         if isinstance(element, basestring):
394                                 lutlist.append((element, None))
395                         elif isinstance(element, ListItem):
396                                 lutlist.append((lut[element.name], element.filternum))
397                         elif isinstance(element, ListMacroItem):
398                                 lutlist.append((element.macrodict[element.macroname], None))
399                         else:
400                                 raise "neither string, ListItem nor ListMacroItem"
401
402                 # now, for the huge list, do:
403                 strlist = [ ]
404                 append = strlist.append
405                 for item in l:
406                         for (element, filternum) in lutlist:
407                                 if not filternum:
408                                         append(element)
409                                 elif filternum == 2:
410                                         append(str(item[element]).replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"'))
411                                 elif filternum == 3:
412                                         #append(str(item[element]).replace("&", "&amp;").replace("<", "&lt;").replace('"', '&quot;').replace(">", "&gt;"))
413                                         append(escape_xml(str(item[element])))
414                                 elif filternum == 4:
415                                         append(str(item[element]).replace("%", "%25").replace("+", "%2B").replace('&', '%26').replace('?', '%3f').replace(' ', '+'))
416                                 else:
417                                         append(str(item[element]))
418                 # (this will be done in c++ later!)
419                 return ''.join(strlist)
420
421         text = property(getText)
422
423 class webifHandler(ContentHandler):
424         def __init__(self, session, request):
425                 self.res = [ ]
426                 self.mode = 0
427                 self.screen = None
428                 self.session = session
429                 self.screens = [ ]
430                 self.request = request
431                 self.macros = { }
432
433         def start_element(self, attrs):
434                 scr = self.screen
435
436                 wsource = attrs["source"]
437
438                 path = wsource.split('.')
439                 while len(path) > 1:
440                         scr = self.screen.getRelatedScreen(path[0])
441                         if scr is None:
442                                 print "[webif.py] Parent Screen not found!"
443                                 print wsource
444                         path = path[1:]
445
446                 source = scr.get(path[0])
447
448                 if isinstance(source, ObsoleteSource):
449                         # however, if we found an "obsolete source", issue warning, and resolve the real source.
450                         print "WARNING: WEBIF '%s' USES OBSOLETE SOURCE '%s', USE '%s' INSTEAD!" % (name, wsource, source.new_source)
451                         print "OBSOLETE SOURCE WILL BE REMOVED %s, PLEASE UPDATE!" % (source.removal_date)
452                         if source.description:
453                                 print source.description
454
455                         wsource = source.new_source
456                 else:
457                         pass
458                         # otherwise, use that source.
459
460                 self.source = source            
461                 self.source_id = str(attrs.get("id", wsource))
462                 self.is_streaming = "streaming" in attrs
463                 self.macro_name = attrs.get("macro") or None
464
465         def end_element(self):
466                 # instatiate either a StreamingElement or a OneTimeElement, depending on what's required.
467                 if not self.is_streaming:
468                         if self.macro_name is None:
469                                 c = OneTimeElement(self.source_id)
470                         else:
471                                 c = MacroElement(self.source_id, self.macros, self.macro_name)
472                 else:
473                         assert self.macro_name is None
474                         c = StreamingElement(self.source_id)
475
476                 c.connect(self.source)
477                 self.res.append(c)
478                 self.screen.renderer.append(c)
479                 del self.source
480
481         def start_convert(self, attrs):
482                 ctype = attrs["type"]
483
484                         # TODO: we need something better here
485                 if ctype[:4] == "web:": # for now
486                         self.converter = eval(ctype[4:])
487                 else:
488                         try:
489                                 self.converter = my_import('.'.join(["Components", "Converter", ctype])).__dict__.get(ctype)
490                         except ImportError:
491                                 self.converter = my_import('.'.join(["Plugins", "Extensions", "WebInterface", "WebComponents", "Converter", ctype])).__dict__.get(ctype)
492                 self.sub = [ ]
493
494         def end_convert(self):
495                 if len(self.sub) == 1:
496                         self.sub = self.sub[0]
497                 c = self.converter(self.sub)
498                 c.connect(self.source)
499                 self.source = c
500                 del self.sub
501
502         def parse_item(self, attrs):
503                 if "name" in attrs:
504                         filter = {"": 1, "javascript_escape": 2, "xml": 3, "uri": 4}[attrs.get("filter", "")]
505                         self.sub.append(ListItem(attrs["name"], filter))
506                 else:
507                         assert "macro" in attrs, "e2:item must have a name= or macro= attribute!"
508                         self.sub.append(ListMacroItem(self.macros, attrs["macro"]))
509
510         def startElement(self, name, attrs):
511                 if name == "e2:screen":
512                         self.screen = eval(attrs["name"])(self.session,self.request) # fixme
513                         self.screens.append(self.screen)
514                         return
515
516                 if name[:3] == "e2:":
517                         self.mode += 1
518
519                 tag = [' %s="%s"' %(key,val) for (key, val) in attrs.items()]
520                 tag.insert(0, name)
521                 tag.insert(0, '<')
522                 tag.append('>')
523                 tag = ''.join(tag)#.encode('utf-8')
524
525                 if self.mode == 0:
526                         self.res.append(tag)
527                 elif self.mode == 1: # expect "<e2:element>"
528                         assert name == "e2:element", "found %s instead of e2:element" % name
529                         self.start_element(attrs)
530                 elif self.mode == 2: # expect "<e2:convert>"
531                         if name[:3] == "e2:":
532                                 assert name == "e2:convert"
533                                 self.start_convert(attrs)
534                         else:
535                                 self.sub.append(tag)
536                 elif self.mode == 3:
537                         assert name == "e2:item", "found %s instead of e2:item!" % name
538                         self.parse_item(attrs)
539
540         def endElement(self, name):
541                 if name == "e2:screen":
542                         self.screen = None
543                         return
544
545                 tag = "</" + name + ">"
546                 if self.mode == 0:
547                         self.res.append(tag)
548                 elif self.mode == 2 and name[:3] != "e2:":
549                         self.sub.append(tag)
550                 elif self.mode == 2: # closed 'convert' -> sub
551                         self.end_convert()
552                 elif self.mode == 1: # closed 'element'
553                         self.end_element()
554                 if name[:3] == "e2:":
555                         self.mode -= 1
556
557         def processingInstruction(self, target, data):
558                 self.res.append('<?' + target + ' ' + data + '>')
559
560         def characters(self, ch):
561                 ch = ch.encode('utf-8')
562                 if self.mode == 0:
563                         self.res.append(ch)
564                 elif self.mode == 2:
565                         self.sub.append(ch)
566
567         def startEntity(self, name):
568                 self.res.append('&' + name + ';');
569
570         def execBegin(self):
571                 for screen in self.screens:
572                         screen.execBegin()
573
574         def cleanup(self):
575                 print "screen cleanup!"
576                 for screen in self.screens:
577                         screen.execEnd()
578                         screen.doClose()
579                 self.screens = [ ]
580
581 def renderPage(stream, path, req, session):
582         
583         # read in the template, create required screens
584         # we don't have persistense yet.
585         # if we had, this first part would only be done once.
586         handler = webifHandler(session,req)
587         parser = make_parser()
588         parser.setFeature(feature_namespaces, 0)
589         parser.setContentHandler(handler)
590         parser.parse(open(util.sibpath(__file__, path)))
591         
592         # by default, we have non-streaming pages
593         finish = True
594         
595         # first, apply "commands" (aka. URL argument)
596         for x in handler.res:
597                 if isinstance(x, Element):
598                         x.handleCommand(req.args)
599
600         handler.execBegin()
601
602         # now, we have a list with static texts mixed
603         # with non-static Elements.
604         # flatten this list, write into the stream.
605         for x in handler.res:
606                 if isinstance(x, Element):
607                         if isinstance(x, StreamingElement):
608                                 finish = False
609                                 x.setStream(stream)
610                         x.render(stream)
611                 else:
612                         stream.write(str(x))
613
614         def ping(s):
615                 from twisted.internet import reactor
616                 s.write("\n");
617                 reactor.callLater(3, ping, s)
618         
619         # if we met a "StreamingElement", there is at least one
620         # element which wants to output data more than once,
621         # i.e. on host-originated changes.
622         # in this case, don't finish yet, don't cleanup yet,
623         # but instead do that when the client disconnects.
624         if finish:
625                 handler.cleanup()
626                 stream.finish()
627         else:
628                 # ok.
629                 # you *need* something which constantly sends something in a regular interval,
630                 # in order to detect disconnected clients.
631                 # i agree that this "ping" sucks terrible, so better be sure to have something 
632                 # similar. A "CurrentTime" is fine. Or anything that creates *some* output.
633                 ping(stream)
634                 stream.closed_callback = lambda: handler.cleanup()