enablel/disable zapping via config
[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 Screens.ChannelSelection import service_types_tv, service_types_radio
15 from Tools.Import import my_import
16
17 from Components.Sources.Source import ObsoleteSource
18
19 from Components.Sources.Clock import Clock
20 from Components.Sources.ServiceList import ServiceList
21
22 from WebComponents.Sources.ServiceListRecursive import ServiceListRecursive
23 from WebComponents.Sources.Volume import Volume
24 from WebComponents.Sources.EPG import EPG
25 from WebComponents.Sources.Timer import Timer
26 from WebComponents.Sources.Movie import Movie
27 from WebComponents.Sources.Message import Message
28 from WebComponents.Sources.PowerState import PowerState
29 from WebComponents.Sources.RemoteControl import RemoteControl
30 from WebComponents.Sources.Settings import Settings
31 from WebComponents.Sources.SubServices import SubServices
32 from WebComponents.Sources.ParentControl import ParentControl
33 from WebComponents.Sources.About import About
34 from WebComponents.Sources.RequestData import RequestData
35 from WebComponents.Sources.AudioTracks import AudioTracks
36 from WebComponents.Sources.WAPfunctions import WAPfunctions
37 from WebComponents.Sources.MP import MP
38 from WebComponents.Sources.Files import Files
39 from WebComponents.Sources.ServiceListReload import ServiceListReload
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(WebScreen):
67         def __init__(self, session,request):
68                 WebScreen.__init__(self, session,request)
69                 self["CurrentTime"] = Clock()
70                 fav = eServiceReference(service_types_tv + ' FROM BOUQUET "bouquets.tv" ORDER BY bouquet')
71
72 class MessageWebScreen(WebScreen):
73         def __init__(self, session,request):
74                 WebScreen.__init__(self, session,request)
75                 self["Message"] = Message(session,func = Message.PRINT)
76                 self["GetAnswer"] = Message(session,func = Message.ANSWER)
77                 
78 class ServiceListReloadWebScreen(WebScreen):
79         def __init__(self, session,request):
80                 WebScreen.__init__(self, session,request)
81                 self["ServiceListReload"] = ServiceListReload(session)
82                 
83 class AudioWebScreen(WebScreen):
84         def __init__(self, session,request):
85                 WebScreen.__init__(self, session,request)
86                 self["AudioTracks"] = AudioTracks(session, func=AudioTracks.GET)
87                 self["SelectAudioTrack"] = AudioTracks(session, func=AudioTracks.SET)   
88
89 class AboutWebScreen(WebScreen):
90         def __init__(self, session,request):
91                 WebScreen.__init__(self, session,request)
92                 self["About"] = About(session)
93                 
94 class VolumeWebScreen(WebScreen):
95         def __init__(self, session,request):
96                 WebScreen.__init__(self, session,request)
97                 self["Volume"] = Volume(session)
98
99 class SettingsWebScreen(WebScreen):
100         def __init__(self, session,request):
101                 WebScreen.__init__(self, session,request)
102                 self["Settings"] = Settings(session)
103
104 class SubServiceWebScreen(WebScreen):
105         def __init__(self, session,request):
106                 WebScreen.__init__(self, session,request)
107                 self["SubServices"] = SubServices(session)
108
109 class ServiceWebScreen(WebScreen):
110         def __init__(self, session,request):
111                 WebScreen.__init__(self, session,request)
112
113                 fav = eServiceReference(service_types_tv + ' FROM BOUQUET "bouquets.tv" ORDER BY bouquet')
114                 self["SwitchService"] = ServiceList(fav, command_func = self.zapTo, validate_commands=False)
115                 self["ServiceList"] = ServiceList(fav, command_func = self.getServiceList, validate_commands=False)
116                 self["ServiceListRecursive"] = ServiceListRecursive(session, func=ServiceListRecursive.FETCH)
117
118         def getServiceList(self, sRef):
119                 self["ServiceList"].root = sRef
120
121         def zapTo(self, reftozap):
122                 from Components.config import config
123                 pc = config.ParentalControl.configured.value
124                 if pc:
125                         config.ParentalControl.configured.value = False
126                 if config.plugins.Webinterface.allowzapping.value:
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 TextToXML(Converter):
348         def __init__(self, arg):
349                 Converter.__init__(self, arg)
350         
351         def getHTML(self, id):
352                 return escape_xml(self.source.text).replace("\x19", "").replace("\x1c", "").replace("\x1e", "")
353
354 class TextToURL(Converter):
355         def __init__(self, arg):
356                 Converter.__init__(self, arg)
357
358         def getHTML(self, id):
359                 return self.source.text.replace(" ","%20")
360
361 class ReturnEmptyXML(Converter):
362         def __init__(self, arg):
363                 Converter.__init__(self, arg)
364
365         def getHTML(self, id):
366                 return "<rootElement></rootElement>"
367
368 # a null-output. Useful if you only want to issue a command.
369 class Null(Converter):
370         def __init__(self, arg):
371                 Converter.__init__(self, arg)
372
373         def getHTML(self, id):
374                 return ""
375
376 class JavascriptUpdate(Converter):
377         def __init__(self, arg):
378                 Converter.__init__(self, arg)
379
380         def getHTML(self, id):
381                 # 3c5x9, added parent. , this is because the ie loads this in a iframe. an the set is in index.html.xml
382                 #                all other will replace this in JS
383                 return '<script>parent.set("%s", "%s");</script>\n'%(id, self.source.text.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"').replace('\xb0', '&deg;'))
384
385 # the performant 'listfiller'-engine (plfe)
386 class ListFiller(Converter):
387         def __init__(self, arg):
388                 Converter.__init__(self, arg)
389 #               print "ListFiller-arg: ",arg
390
391         def getText(self):
392                 l = self.source.list
393                 lut = self.source.lut
394                 conv_args = self.converter_arguments
395
396                 # now build a ["string", 1, "string", 2]-styled list, with indices into the
397                 # list to avoid lookup of item name for each entry
398                 lutlist = [ ]
399                 for element in conv_args:
400                         if isinstance(element, basestring):
401                                 lutlist.append((element, None))
402                         elif isinstance(element, ListItem):
403                                 lutlist.append((lut[element.name], element.filternum))
404                         elif isinstance(element, ListMacroItem):
405                                 lutlist.append((element.macrodict[element.macroname], None))
406                         else:
407                                 raise "neither string, ListItem nor ListMacroItem"
408
409                 # now, for the huge list, do:
410                 strlist = [ ]
411                 append = strlist.append
412                 for item in l:
413                         for (element, filternum) in lutlist:
414                                 if not filternum:
415                                         append(element)
416                                 elif filternum == 2:
417                                         append(str(item[element]).replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"'))
418                                 elif filternum == 3:
419                                         #append(str(item[element]).replace("&", "&amp;").replace("<", "&lt;").replace('"', '&quot;').replace(">", "&gt;"))
420                                         append(escape_xml(str(item[element])))
421                                 elif filternum == 4:
422                                         append(str(item[element]).replace("%", "%25").replace("+", "%2B").replace('&', '%26').replace('?', '%3f').replace(' ', '+'))
423                                 else:
424                                         append(str(item[element]))
425                 # (this will be done in c++ later!)
426                 return ''.join(strlist)
427
428         text = property(getText)
429
430 class webifHandler(ContentHandler):
431         def __init__(self, session, request):
432                 self.res = [ ]
433                 self.mode = 0
434                 self.screen = None
435                 self.session = session
436                 self.screens = [ ]
437                 self.request = request
438                 self.macros = { }
439
440         def start_element(self, attrs):
441                 scr = self.screen
442
443                 wsource = attrs["source"]
444
445                 path = wsource.split('.')
446                 while len(path) > 1:
447                         scr = self.screen.getRelatedScreen(path[0])
448                         if scr is None:
449                                 print "[webif.py] Parent Screen not found!"
450                                 print wsource
451                         path = path[1:]
452
453                 source = scr.get(path[0])
454
455                 if isinstance(source, ObsoleteSource):
456                         # however, if we found an "obsolete source", issue warning, and resolve the real source.
457                         print "WARNING: WEBIF '%s' USES OBSOLETE SOURCE '%s', USE '%s' INSTEAD!" % (name, wsource, source.new_source)
458                         print "OBSOLETE SOURCE WILL BE REMOVED %s, PLEASE UPDATE!" % (source.removal_date)
459                         if source.description:
460                                 print source.description
461
462                         wsource = source.new_source
463                 else:
464                         pass
465                         # otherwise, use that source.
466
467                 self.source = source
468                 self.source_id = str(attrs.get("id", wsource))
469                 self.is_streaming = "streaming" in attrs
470                 self.macro_name = attrs.get("macro") or None
471
472         def end_element(self):
473                 # instatiate either a StreamingElement or a OneTimeElement, depending on what's required.
474                 if not self.is_streaming:
475                         if self.macro_name is None:
476                                 c = OneTimeElement(self.source_id)
477                         else:
478                                 c = MacroElement(self.source_id, self.macros, self.macro_name)
479                 else:
480                         assert self.macro_name is None
481                         c = StreamingElement(self.source_id)
482
483                 c.connect(self.source)
484                 self.res.append(c)
485                 self.screen.renderer.append(c)
486                 del self.source
487
488         def start_convert(self, attrs):
489                 ctype = attrs["type"]
490
491                         # TODO: we need something better here
492                 if ctype[:4] == "web:": # for now
493                         self.converter = eval(ctype[4:])
494                 else:
495                         try:
496                                 self.converter = my_import('.'.join(["Components", "Converter", ctype])).__dict__.get(ctype)
497                         except ImportError:
498                                 self.converter = my_import('.'.join(["Plugins", "Extensions", "WebInterface", "WebComponents", "Converter", ctype])).__dict__.get(ctype)
499                 self.sub = [ ]
500
501         def end_convert(self):
502                 if len(self.sub) == 1:
503                         self.sub = self.sub[0]
504                 c = self.converter(self.sub)
505                 c.connect(self.source)
506                 self.source = c
507                 del self.sub
508
509         def parse_item(self, attrs):
510                 if "name" in attrs:
511                         filter = {"": 1, "javascript_escape": 2, "xml": 3, "uri": 4}[attrs.get("filter", "")]
512                         self.sub.append(ListItem(attrs["name"], filter))
513                 else:
514                         assert "macro" in attrs, "e2:item must have a name= or macro= attribute!"
515                         self.sub.append(ListMacroItem(self.macros, attrs["macro"]))
516
517         def startElement(self, name, attrs):
518                 if name == "e2:screen":
519                         self.screen = eval(attrs["name"])(self.session,self.request) # fixme
520                         self.screens.append(self.screen)
521                         return
522
523                 if name[:3] == "e2:":
524                         self.mode += 1
525
526                 tag = [' %s="%s"' %(key,val) for (key, val) in attrs.items()]
527                 tag.insert(0, name)
528                 tag.insert(0, '<')
529                 tag.append('>')
530                 tag = ''.join(tag)#.encode('utf-8')
531                 
532                 if self.mode == 0:
533                         self.res.append(tag)
534                 elif self.mode == 1: # expect "<e2:element>"
535                         assert name == "e2:element", "found %s instead of e2:element" % name
536                         self.start_element(attrs)
537                 elif self.mode == 2: # expect "<e2:convert>"
538                         if name[:3] == "e2:":
539                                 assert name == "e2:convert"
540                                 self.start_convert(attrs)
541                         else:
542                                 self.sub.append(tag)
543                 elif self.mode == 3:
544                         assert name == "e2:item", "found %s instead of e2:item!" % name
545                         
546                         self.parse_item(attrs)
547
548         def endElement(self, name):
549                 if name == "e2:screen":
550                         self.screen = None
551                         return
552
553                 tag = "</" + name + ">"
554                 if self.mode == 0:
555                         self.res.append(tag)
556                 elif self.mode == 2 and name[:3] != "e2:":
557                         self.sub.append(tag)
558                 elif self.mode == 2: # closed 'convert' -> sub
559                         self.end_convert()
560                 elif self.mode == 1: # closed 'element'
561                         self.end_element()
562                 if name[:3] == "e2:":
563                         self.mode -= 1
564
565         def processingInstruction(self, target, data):
566                 self.res.append('<?' + target + ' ' + data + '>')
567
568         def characters(self, ch):
569                 ch = ch.encode('utf-8')
570                 if self.mode == 0:
571                         self.res.append(ch)
572                 elif self.mode == 2:
573                         self.sub.append(ch)
574
575         def startEntity(self, name):
576                 self.res.append('&' + name + ';');
577
578         def execBegin(self):
579                 for screen in self.screens:
580                         screen.execBegin()
581
582         def cleanup(self):
583                 print "screen cleanup!"
584                 for screen in self.screens:
585                         screen.execEnd()
586                         screen.doClose()
587                 self.screens = [ ]
588
589 def renderPage(stream, path, req, session):
590         
591         # read in the template, create required screens
592         # we don't have persistense yet.
593         # if we had, this first part would only be done once.
594         handler = webifHandler(session,req)
595         parser = make_parser()
596         parser.setFeature(feature_namespaces, 0)
597         parser.setContentHandler(handler)
598         parser.parse(open(util.sibpath(__file__, path)))
599         
600         # by default, we have non-streaming pages
601         finish = True
602         
603         # first, apply "commands" (aka. URL argument)
604         for x in handler.res:
605                 if isinstance(x, Element):
606                         x.handleCommand(req.args)
607
608         handler.execBegin()
609
610         # now, we have a list with static texts mixed
611         # with non-static Elements.
612         # flatten this list, write into the stream.
613         for x in handler.res:
614                 if isinstance(x, Element):
615                         if isinstance(x, StreamingElement):
616                                 finish = False
617                                 x.setStream(stream)
618                         x.render(stream)
619                 else:
620                         stream.write(str(x))
621
622         def ping(s):
623                 from twisted.internet import reactor
624                 s.write("\n");
625                 reactor.callLater(3, ping, s)
626         
627         # if we met a "StreamingElement", there is at least one
628         # element which wants to output data more than once,
629         # i.e. on host-originated changes.
630         # in this case, don't finish yet, don't cleanup yet,
631         # but instead do that when the client disconnects.
632         if finish:
633                 streamFinish(handler, stream)
634         else:
635                 # ok.
636                 # you *need* something which constantly sends something in a regular interval,
637                 # in order to detect disconnected clients.
638                 # i agree that this "ping" sucks terrible, so better be sure to have something 
639                 # similar. A "CurrentTime" is fine. Or anything that creates *some* output.
640                 ping(stream)
641                 stream.closed_callback = lambda : streamFinish(handler, stream)
642
643 def streamFinish(handler, stream):
644         handler.cleanup()
645         stream.finish()
646         del handler
647         del stream