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