-
Notifications
You must be signed in to change notification settings - Fork 39
/
wsuiserver.py
executable file
·235 lines (206 loc) · 11.1 KB
/
wsuiserver.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# -*- coding: utf-8 -*-
""" This file is part of B{Domogik} project (U{http://www.domogik.org}$
License
=======
B{Domogik} is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
B{Domogik} is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Domogik. If not, see U{http://www.gnu.org/licenses}.
Plugin purpose
==============
WebSocket server for plugins
Implements
==========
Manage a server for dialog between a plugin and UI using websocket HTML5 functionnality
All message have an header JSON with keys :
- 'type': give the type of message, values can be:
-'req' : a simple client resquest, this type is passed to callback.
-'req-ack' : a client resquest with confirmation requested , this type is passed to callback.
-'pub' : Automaticly select with server.broadcastMessage function.
Next two keys help to secure and validate the client connection.
-'confirm-connect' : Internal server type for confirm to client connection accepted. See UI client part.
This message type is send automaticly to client when he ask for connection. Add 2 keys for identity :
-'id' : 'ws_serverUI' : constante string
-'idws' : peer_adresss, id that the client keeps for identification.
-'ack-connect' : Internal server type to confirm that client have recept first confirmation ('confirm-connect').
The client must send this message type to finalized connection. See UI client part.
-'server-hbeat' : Internal server type for client check server running.
server send automaticly an confirmation message. See UI client part if you implement it.
- 'idws' : Identify resquesting client. See UI client part.
- 'idmsg: Identify of individual resquet. See UI client part.
- 'ip' : Identify resquesting IP client.
- 'timestamp' : A reference time when message was sent. See UI client part.
@author: Nico <nico84dev@gmail.com>
@copyright: (C) 2007-2013 Domogik projectiport
@license: GPL(v3)
@organization: Domogik
"""
import logging
import logging.handlers
from wsgiref.simple_server import make_server
from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler
from ws4py.server.wsgiutils import WebSocketWSGIApplication
from ws4py.websocket import WebSocket
from ws4py import configure_logger as wsServer_logger
import threading
import json
import time
import os
__ctrlServer__ = [] # tab servers for websockets recover
class WsUIServerException(Exception):
""""Websocket server generic exception class.
"""
def __init__(self, value):
"""Initialisation"""
Exception.__init__(self)
self.msg = "Websocket server generic exception:"
self.value = value
def __str__(self):
"""String format objet"""
return repr(self.msg + ' ' + self.value)
class BroadcastServer(object):
"""Class de gestion du server websocket pour dialogue plugin UI"""
def __init__(self, port=5570, cb_recept = None, log = None ):
fName = "//var//log/domogik//wsuiserver.log"
if log :
for h in log.__dict__['handlers']:
if h.__class__.__name__ in ['FileHandler', 'TimedRotatingFileHandler','RotatingFileHandler', 'WatchedFileHandler']:
fName = os.path.dirname(h.baseFilename) + "/wsuiserver.log"
break
log.debug('Log WS Server on : {0}'.format(fName))
self._wsLogws = wsServer_logger(level = logging.DEBUG)
logfmt = logging.Formatter("[%(asctime)s] %(levelname)s %(message)s")
handler = logging.handlers.RotatingFileHandler(fName, maxBytes=10485760, backupCount=5)
handler.setLevel(logging.DEBUG)
handler.setFormatter(logfmt)
self._wsLogws.addHandler(handler)
global __ctrlServer__
# check free port
for s in __ctrlServer__ :
if s.port == port :
if log : log.error("Creating WS server error, port %d allready used." % port)
raise WsUIServerException ("Creating WS server error, port %d allready used." % port)
self.buffer = []
self.clients = set()
self.fail_clients = set()
self.running = False
self.port = port
self.cb_recept = cb_recept
self.log = log
self.server = None
self.server = make_server('', port, server_class=WSGIServer,
handler_class=WebSocketWSGIRequestHandler,
app=WebSocketWSGIApplication(handler_cls=WebSocketsHandler))
if not self.server : raise WsUIServerException('Error websocket server creation, check if there is any other running plugin instance.')
self.server.initialize_websockets_manager()
__ctrlServer__.append(self)
servUI = threading.Thread(None, self.run, "th_WSserv_msg_to_ui", (), {} )
servUI.start()
time.sleep(0.1)
if self.log : self.log.info('WebSocket server started on port : %d' %self.port)
print "**************** WebSocket server is started on port %d ********************" %self.port
def run(self):
"""Starting server in forever mode , calling by thread start."""
# TODO : Ajouter la gestion d'une exception en cas d'erreur sur le server
print "**************** Starting WebSocket server forever **************"
if self.log : self.log.info('Starting WebSocket server forever on port : %d' %self.port)
self.running = True
self.server.serve_forever()
def close(self):
"""Closing server"""
self.server.server_close()
if self.log : self.log.info('WebSocket server forever on port : %d closed' %self.port)
def __del__(self):
"""Close server and Destroy class."""
print ('server stopped')
self.running = False
if self.server : self.server.server_close()
if __ctrlServer__ : __ctrlServer__.remove(self)
if self.log : self.log.info('WebSocket server forever on port : %d Destroyed' %self.port)
def broadcastMessage(self, msg):
"""broadcast Message to all clients"""
message = msg.copy() # copy dict to ensure a memory change during process
header = {'type':'pub', 'idws' : 'for each' , 'ip' : '0.0.0.0', 'timestamp' : long(time.time()*100)}
message['header'] = header
info = 'Websocket server sending for %d client(s) : %s' % (len(self.server.manager.websockets), str(message))
# print info
if self.log : self.log.debug(info)
for ws in self.server.manager.websockets.itervalues():
try:
message['header'] = {'type':'pub', 'idws' : ws.peer_address[1] , 'ip' : ws.peer_address[0], 'timestamp' : long(time.time()*100)}
print "Server broadcasting send to : ", message['header']['idws']
print message
ws.send(json.dumps(message))
except Exception:
self.server.manager.remove(ws)
print "Failed sockets"
continue
def sendAck(self, ackMessage):
"""Send a confirmation message 'Ack' to client"""
ackMsg = ackMessage.copy() # copy dict to ensure a memory change during process
if ackMsg['header'] :
for ws in self.server.manager.websockets.itervalues():
if ws.peer_address[1] == ackMsg['header']['idws'] :
print 'Send Ack on WebSocket client', ackMsg['header']['idws']
ws.send(json.dumps(ackMsg))
class WebSocketsHandler(WebSocket):
"""One Client Class par client, create by server, inherited from WebSocket class ."""
def opened(self):
"""Call at client openning."""
global __ctrlServer__
port = self.sock.getsockname()[1]
self.server = None
for s in __ctrlServer__ : # Find conresponding server object
if s.port == port : self.server = s
if not self.server :
raise WsUIServerException ("Openning WS client error, port %d not find in server list." % port)
print 'New WebSocket client detected ', self.peer_address
if self.server.log : self.server.log.info('A new WebSocket client connected : %s : %s' % (self.peer_address[0], self.peer_address[1]))
self.send(json.dumps({'header': {'type' : 'confirm-connect', 'id' : 'ws_serverUI', 'idws': self.peer_address[1]}}))
print 'WebSockect Message confirmation send from open to client', self.peer_address[1]
self.confirmed = False
def closed(self, code, status):
"""Call at client closing or lost"""
print 'Client WebSocket supprimer', code, status, self.connection
if self.server.log : self.server.log.info('WebSocket client disconnected : %s' % self.connection )
def received_message(self, message):
"""Callback from recept client message, handle return confirmation message (Ack)."""
print ' Recept from client, transfer to handler: ', message
# session = self.environ.get('REMOTE_ADDR')
# print ' Recept websocket message, session on : ', session, ' client : ', self.peer_address[1]
try :
msg = json.loads(str(message))
header = msg['header']
except TypeError as e :
print ' Error parsing websocket msg :', e
if self.server.log : self.server.log.debug('WebSocket client error parsing msg : %s , Message : %s' % (e, str(message)))
else :
if header['type'] == 'ack-connect':
self.confirmed = True
self.send(json.dumps({'header': {'type' : 'confirm-connect', 'id' : 'ws_serverUI', 'idws': self.peer_address[1]}}))
print 'WebSockect client connection confirmed by received, send client identity : ', self.peer_address[1]
elif header['type'] == 'server-hbeat':
self.sendAck(msg)
elif self.confirmed == True :
print ' Send to callback'
if self.server.cb_recept : self.server.cb_recept(msg)
if header['type'] == "ack" : self.sendAck({'msg':msg})
print '++++++++++ End Websocket reception +++++++++++'
def sendAck(self, msg):
"""Send return confirmation message (Ack)."""
print 'WebSocket send Ack'
msg.update({'header': {'type':'ack', 'idws' : self.peer_address[1] , 'ip' : self.peer_address[0], 'timestamp' : long(time.time()*100)}})
self.send(json.dumps(msg))
def getServerOnPort(port):
server = None
for s in __ctrlServer__ : # Find conresponding server object
if s.port == port :
server = s
break
return server