instrumentation: add next-share/
[cs-p2p-next.git] / instrumentation / next-share / BaseLib / UPnP / upnpserver / upnpservice.py
1 # Written by Ingar Arntzen, Norut
2 # see LICENSE.txt for license information
3
4 """
5 This module implements a UPnP Service. 
6
7 This involves a base class intended for development of new services.
8 The baseclass hides complexity related to producing UPnP Service
9 description in both XML and HTML format. It also hides complexity 
10 related to the placement of a service within a device hierarchy.
11 """
12 import types
13 import uuid
14 import exceptions
15 import BaseLib.UPnP.common.upnpmarshal as upnpmarshal
16
17 class ActionError (exceptions.Exception): 
18     """Error associated with invoking actions on a UPnP Server. """
19     pass
20
21 ##############################################
22 # XML FMT
23 ##############################################
24
25 _SERVICE_DESCRIPTION_FMT = """<?xml version="1.0"?>
26 <scpd xmlns="urn:schemas-upnp-org:service-1-0">
27 <specVersion>
28 <major>1</major>
29 <minor>0</minor>
30 </specVersion>
31 <actionList>
32 %s</actionList>
33 <serviceStateTable>
34 %s</serviceStateTable>
35 </scpd>"""
36
37 _ACTION_FMT = """<action>
38 <name>%s</name>
39 <argumentList>
40 %s
41 </argumentList>
42 </action>
43 """
44
45 _ARGUMENT_FMT = """<argument>
46 <name>%s</name>
47 <relatedStateVariable>%s</relatedStateVariable>
48 <direction>%s</direction>
49 </argument>"""
50
51 _EVENTED_VARIABLE_FMT = """<stateVariable sendEvents="yes">
52 <name>%s</name>
53 <dataType>%s</dataType>
54 <defaultValue>%s</defaultValue>
55 </stateVariable>
56 """
57
58 _ARG_VARIABLE_FMT = """<stateVariable sendEvents="no">
59 <name>%s</name>
60 <dataType>%s</dataType>
61 </stateVariable>
62 """
63
64 def _service_description_toxml(service):  
65     """This function produces the UPnP XML service description."""
66
67     svs_str = ""
68     # Evented Variables
69     for evar in service.get_evented_variables():
70         data_type = upnpmarshal.dumps_data_type(evar.the_type)
71         default_value = upnpmarshal.dumps(evar.default_value)
72         args = (evar.the_name, data_type, default_value)
73         svs_str += _EVENTED_VARIABLE_FMT % args
74
75     actions_str = ""
76     arg_counter = 0
77
78     # One state variable per type (event variables of arguments)
79     unique_variables = {} # type : variable name
80     for evar in service.get_evented_variables():
81         if not unique_variables.has_key(evar.the_type):
82             unique_variables[evar.the_type] = evar.the_name
83         
84     # Arguments
85     for action in service.get_actions():
86         args_str = ""
87         for arg in action.in_arg_list + action.out_arg_list:            
88
89             # Check if argument can be related to event variable
90             if unique_variables.has_key(arg.the_type):
91                 related_variable_name = unique_variables[arg.the_type]
92             else:
93                 arg_counter += 1
94                 related_variable_name = "A_ARG_TYPE_%d" % arg_counter
95                 unique_variables[arg.the_type] = related_variable_name
96                 # New State Variable
97                 data_type = upnpmarshal.dumps_data_type(arg.the_type)
98                 svs_str += _ARG_VARIABLE_FMT % (related_variable_name, 
99                                                 data_type)
100
101             # New Argument 
102             direction = 'in' if isinstance(arg, _InArg) else 'out'
103             args_str += _ARGUMENT_FMT % (arg.the_name, 
104                                          related_variable_name, direction)
105         # Action
106         actions_str += _ACTION_FMT % (action.name, args_str)
107
108     return _SERVICE_DESCRIPTION_FMT % (actions_str, svs_str)
109
110     
111
112 ##############################################
113 # UPNP SERVICE
114 ##############################################
115
116 class UPnPService:
117
118     """
119     This implements a base class for all UPnP Services.
120
121     New services should extend this class.
122     The base class hides complexity related to production
123     of XML service descriptions as well as HTTP descriptions.
124     The base class also hides complexity related to placement
125     in the UPnP device hierarchy.
126     """
127
128     def __init__(self, service_id, service_type, service_version=1):
129         self.service_manager = None
130
131         self._actions = {} # actionName : Action
132         self._events = {} # eventName : Event
133         self._subs = {} # callbackURL : Subscriptions
134         
135         # Initialise
136         self.service_type = service_type
137         self.service_version = service_version
138         self.service_id =  service_id
139
140         self.base_url = ""
141         self.description_path = ""
142         self.control_path = ""
143         self.event_path = ""
144         self._logger = None
145
146     def set_service_manager(self, service_manager):
147         """Initialise UPnP service with reference to service manager."""
148         self.service_manager = service_manager
149         self.base_url = self.service_manager.get_base_url()
150         self.description_path = "services/%s/description.xml" % self.service_id
151         self.control_path = "services/%s/control" % self.service_id
152         self.event_path = "services/%s/events" % self.service_id
153         # Logging
154         self._logger = self.service_manager.get_logger()
155         
156     def is_valid(self):
157         """Check if service is valid."""
158         return (self.service_type != None and self.service_id != None
159                 and self.base_url != None and self.service_manager != None)
160
161     def get_short_service_id(self):
162         """Return short service id."""
163         return self.service_id
164
165     def get_service_id(self):
166         """Return full service id."""
167         return "urn:upnp-org:serviceId:%s" % self.service_id
168
169     def get_service_type(self):
170         """Return service type."""
171         fmt = "urn:schemas-upnp-org:service:%s:%s"
172         return  fmt % (self.service_type, self.service_version)
173
174     def get_xml_description(self):
175         """Returns xml description of service."""
176         return _service_description_toxml(self)
177
178     def close(self):
179         """Close UPnP service safely."""
180         for sub in self._subs.values():
181             sub.close()
182
183     ##############################################
184     # LOG API
185     ##############################################
186
187     def log(self, msg):
188         """Logger."""
189         if self._logger:
190             self._logger.log("SERVICE", "%s %s" % (self.service_id, msg))
191
192     ##############################################
193     # SUBSCRIBE / NOTIFY API
194     ##############################################
195
196     def notify(self, evented_variables):
197         """Notify all subscribers of updated event variables."""
198         self._remove_expired_subscriptions()
199         # Dispatch Event Messages to all subscribers  
200         # of the given serviceid.
201         # Make sure all stateVariables are evented variables.
202         for sub in self._subs.values():
203             sub.notify(evented_variables)
204
205     def subscribe(self, callback_urls, requested_duration):
206         """Process new subscription request."""
207         # requested duration == 0 => infinite
208         self._remove_expired_subscriptions()
209         # For the moment, just accept a single callbackUrl
210         # Subscriber defined by callbackUrl
211         callback_url = callback_urls[0]        
212         if self._subs.has_key(callback_url):
213             # Subscriber already exists
214             return (None, None)
215         else:
216             # Add new Subscriber
217             sub = _Subscription(self, callback_url, requested_duration)
218             self._subs[callback_url] = sub
219             # Dispatch Initial Event Message
220             sub.initial_notify()
221             return (sub.sid, sub.duration)
222     
223     def renew(self, sid_str, requested_duration):
224         """Request to renew an existing subscription."""
225         # requested duration == 0 => infinite
226         for sub in self._subs.values():
227             if str(sub.sid) == sid_str:
228                 return sub.renew(requested_duration)
229         else: return None
230         
231     def unsubscribe(self, sid_str):
232         """Request to unsubscribe an existing subscription."""
233         sub = None
234         for sub in self._subs.values():
235             if str(sub.sid) == sid_str: 
236                 break 
237         if sub:
238             sub.cancel()
239             del self._subs[sub.callback_url]
240             return True
241         else: 
242             return False
243
244     def _remove_expired_subscriptions(self):
245         """Scans subscriptions and removes invalidated."""
246         for url, sub in self._subs.items()[:]:
247             if sub.is_expired: 
248                 del self._subs[url]
249
250
251     ##############################################
252     # ACTION API
253     ##############################################
254     
255     def define_action(self, method, in_args=None, out_args=None, 
256                       name=None):
257         """Define an action that the service implements. 
258         Used by subclass."""
259         if not in_args:
260             in_args = []
261         if not out_args:
262             out_args = []
263         if not name:
264             action_name = method.__name__
265         else:
266             action_name = name
267         # In/Out Args must be tuples of (name, type<?>)        
268         in_args = [_InArg(t[0], t[1]) for t in in_args]
269         out_args = [_OutArg(t[0], t[1]) for t in out_args]
270         action = _Action(action_name, method, in_args, out_args)
271         self._actions[action_name] = action
272
273     def invoke_action(self, action_name, in_args):
274         """Invoke and action that the service implements.
275         Used by httpserver as part of UPnP control interface."""
276         # in_args is assumed to be tuple of (name, data) all unicode string.
277         try:
278             if not self._actions.has_key(action_name): 
279                 raise ActionError, "Action Not Supported"
280             else:
281                 action = self._actions[action_name]            
282                 return action.execute(in_args)
283         except ActionError, why:
284             print why
285
286     def get_actions(self):
287         """Returns all actions that the service implements."""
288         return self._actions.values()
289
290
291     ##############################################
292     # EVENTED VARIABLE API
293     ##############################################
294
295     def define_evented_variable(self, event_name, the_type, default_value):
296         """Define an evented variable for the service. Used by subclass."""
297         evar = _EventedVariable(self, event_name, the_type, default_value)
298         self._events[event_name] = evar
299         return evar
300
301     def get_evented_variable(self, event_name):
302         """Return evented variable given name."""
303         return self._events.get(event_name, None)
304
305     def get_evented_variables(self):
306         """Return all evented variables defined by the service."""
307         return self._events.values()
308
309     def set_evented_variables(self, list_):
310         """
311         Update a list of state variables at once.
312         Input will be a list of tuples [(eventedVariable, newValue)]
313         The method avoids sending one notification to every subscriber,
314         for each state variable. Instead, a single subscriber receives
315         one eventMessage containing all the updated state Variables 
316         in this list.
317         """
318         # Update Values
319         changed_variables = []
320         for evar, new_value in list_:
321             changed = evar.set(new_value, notify_ok=False)
322             if changed: 
323                 changed_variables.append(evar)
324         # notify all in one batch
325         self.notify(changed_variables)
326
327
328 ##############################################
329 # EVENTED VARIABLE
330 ##############################################
331
332 class _EventedVariable:
333
334     """This class defines an evented variable. The class hides
335     complexity related to event notification."""
336
337     def __init__(self, service, the_name, the_type, default_value):
338         self._service = service
339         self.the_name = the_name
340         if type(the_type) == types.TypeType: 
341             self.the_type = the_type
342         else: 
343             msg = "Argument 'the_type' is not actually a python type."
344             raise TypeError,  msg
345         self._value = default_value
346         self.default_value = default_value
347
348     def set(self, new_value, notify_ok=True):
349         """Set a new value for the evented variable. If the value
350         is different from the old value, notifications will be generated."""
351         if type(new_value) != self.the_type:
352             msg = "Argument 'the_type' is not actually a python type."
353             raise TypeError, msg
354         if new_value != self._value:
355             # Update Value
356             self._value = new_value
357             # Notify
358             if notify_ok:
359                 self._service.notify([self])
360             return True
361         else : return False
362
363     def get(self):
364         """Get the value of an evented variable."""
365         return self._value
366
367
368 ##############################################
369 # ARGUMENT
370 ##############################################
371
372 class _Argument :
373
374     """The class defines an argument by holding a type and
375     and argument name."""
376     def __init__(self, the_name, the_type):
377         self.the_name = the_name
378         self.the_type = the_type
379         
380 class _InArg(_Argument): 
381     """The class defines an input argument by holding a type and
382     and argument name."""
383     pass
384
385 class _OutArg(_Argument): 
386     """The class defines an output argument (result value) by 
387     holding a type and and argument name."""
388     pass
389
390 ##############################################
391 # ACTION
392 ##############################################
393
394 class _Action:
395
396     """This class represents an action implemented by the 
397     service."""
398     
399     def __init__(self, name, method, in_arg_list, out_arg_list):
400         self.name = name
401         self.method = method
402         self.in_arg_list = in_arg_list 
403         self.out_arg_list = out_arg_list 
404
405     def execute(self, in_args):
406         """Execute the action."""
407         # in_args is assumed to be tuple of (name, data) all unicode string.
408         # the tuple is supposed to be ordered according to in_arg_list
409         if len(in_args) != len(self.in_arg_list): 
410             raise ActionError, "Wrong number of input arguments"
411         typed_args = []
412         for i in range(len(in_args)):
413             name, data = in_args[i]
414             in_arg = self.in_arg_list[i]
415             if name != in_arg.the_name: 
416                 raise ActionError, "Wrong name/order for input argument"
417             try:
418                 value = upnpmarshal.loads(in_arg.the_type, data)
419             except upnpmarshal.MarshalError, why:
420                 raise ActionError, why
421             typed_args.append(value)
422
423         # Execute
424         try:
425             result = self.method(*typed_args)
426         except TypeError, why:
427             raise ActionError, "Method Execution Failed (%s)" % why
428         
429         # Result is eiter a single value (incl. None) or a tuple of values.
430         # Make it into a list in both cases.
431         if result == None:
432             result = []
433         elif result == types.TupleType:
434             result = list(result)
435         else:
436             result = [result]
437
438         # Check that result holds the correct number of values
439         if len(result) != len(self.out_arg_list): 
440             raise ActionError, "Wrong number of Results"
441         # Check that each value has the correct type
442         # Also convert python type objects to string representations. 
443         # Construct out_args list of tuples [(name, data), ...]
444         out_args = []
445         for i in range(len(result)):
446             out_arg = self.out_arg_list[i]
447             value = result[i]
448             if not isinstance(value, out_arg.the_type):
449                 raise ActionError, "Result is wrong type."
450             else:
451                 try:
452                     data = upnpmarshal.dumps(value)
453                 except upnpmarshal.MarshalError, why:
454                     raise ActionError, why
455                 out_args.append((out_arg.the_name, data))
456         return out_args
457
458
459 ##############################################
460 # SUBSCRIPTION
461 ##############################################
462
463 class NotifyError (exceptions.Exception): 
464     """Error associated with event notification."""
465     pass
466
467 class _Subscription:
468     
469     """This class represents a subscription made to the service,
470     for notification whenever one of its evented variables is updated."""
471     
472     def __init__(self, service, callback_url, requested_duration):
473         # requested_duration == 0 implies INFINITE
474         # requested_duration > 0 implies FINITE
475         # requested_duration < 0 not legal
476         self.service = service
477         self.sid = uuid.uuid1()
478         self.event_key = 0
479         self.callback_url = callback_url
480         self.duration = 1800 # ignore requested_duration
481         self.is_expired = False
482
483     def notify(self, evented_variables):
484         """Notify this subscriber that given evented variables 
485         have been updated."""
486         if self.is_expired : 
487             return False # should not be neccesary
488         else:
489             self.event_key += 1
490             # Construct list of tuples [(name, value), ...]
491             variables = []
492             for evar in evented_variables:
493                 try:
494                     data = upnpmarshal.dumps(evar.get())
495                 except upnpmarshal.MarshalError, why:
496                     raise NotifyError, why
497                 variables.append((evar.the_name, data))
498
499             # Dispatch Notification
500             edp = self.service.service_manager.get_event_dispatcher()
501             edp.dispatch(self.sid, self.event_key, self.callback_url, variables)
502             return True
503
504     def initial_notify(self):
505         """Notify this subscriber of all evented state 
506         variables and their values"""
507         if self.is_expired: 
508             return False
509         # Event Key must be 0
510         if self.event_key != 0: 
511             return False
512         # All Evented Variables
513         evented_variables = self.service.get_evented_variables()
514         variables = []
515         for evar in evented_variables:
516             try:
517                 data = upnpmarshal.dumps(evar.get())
518             except upnpmarshal.MarshalError, why:
519                 raise NotifyError, why
520             variables.append((evar.the_name, data))
521         
522         # Dispatch Notification
523         edp = self.service.service_manager.get_event_dispatcher()
524         edp.dispatch(self.sid, 0, self.callback_url, variables)
525         return True
526
527     def renew(self, requested_duration):
528         """Renew subscription for this subscriber."""
529         self.duration = requested_duration
530         self.is_expired = False
531         return self.duration
532     
533     def cancel(self):
534         """Cancel subscription for this subscriber."""
535         self.is_expired = True
536         return True
537
538     def close(self):
539         """Close this subscription safely."""
540         pass
541
542 ##############################################
543 # MAIN
544 ##############################################
545
546 if __name__ == '__main__':
547
548     class MockEventDispatcher:
549         """Mock Event Dispatcher."""
550         def __init__(self):
551             pass
552         def dispatch(self, sid, event_key, callback_url, variables):
553             """Mock method."""
554             print "Notify", sid, event_key, callback_url, variables
555
556     class MockServiceManager:
557         """Mock Service Manager."""
558         def __init__(self):
559             self._ed = MockEventDispatcher()
560         def get_event_dispatcher(self):
561             """Mock method."""
562             return self._ed
563         def get_base_url(self):
564             """Mock method."""
565             return "http://myhost:44444"
566         def get_logger(self):
567             """Mock method."""
568             return None
569
570     SM = MockServiceManager()
571     from BaseLib.UPnP.services import SwitchPower
572     SERVICE = SwitchPower('SwitchPower')
573     SERVICE.set_service_manager(SM)
574     print SERVICE.get_xml_description()
575