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