mirror of
https://github.com/GNS3/gns3-server
synced 2024-12-26 00:38:10 +00:00
Fix long-polling request for project notifications.
This commit is contained in:
parent
10702f87bc
commit
7fe8f7e716
@ -460,10 +460,7 @@ class BaseManager:
|
|||||||
if not data:
|
if not data:
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
continue
|
continue
|
||||||
try:
|
|
||||||
await response.write(data)
|
await response.write(data)
|
||||||
except ConnectionError:
|
|
||||||
break
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise aiohttp.web.HTTPNotFound()
|
raise aiohttp.web.HTTPNotFound()
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
|
@ -446,13 +446,14 @@ class Compute:
|
|||||||
msg = json.loads(response.data)
|
msg = json.loads(response.data)
|
||||||
action = msg.pop("action")
|
action = msg.pop("action")
|
||||||
event = msg.pop("event")
|
event = msg.pop("event")
|
||||||
|
project_id = msg.pop("project_id", None)
|
||||||
if action == "ping":
|
if action == "ping":
|
||||||
self._cpu_usage_percent = event["cpu_usage_percent"]
|
self._cpu_usage_percent = event["cpu_usage_percent"]
|
||||||
self._memory_usage_percent = event["memory_usage_percent"]
|
self._memory_usage_percent = event["memory_usage_percent"]
|
||||||
#FIXME: slow down number of compute events
|
#FIXME: slow down number of compute events
|
||||||
self._controller.notification.controller_emit("compute.updated", self.__json__())
|
self._controller.notification.controller_emit("compute.updated", self.__json__())
|
||||||
else:
|
else:
|
||||||
await self._controller.notification.dispatch(action, event, compute_id=self.id)
|
await self._controller.notification.dispatch(action, event, project_id=project_id, compute_id=self.id)
|
||||||
elif response.type == aiohttp.WSMsgType.CLOSED or response.type == aiohttp.WSMsgType.ERROR or response.data is None:
|
elif response.type == aiohttp.WSMsgType.CLOSED or response.type == aiohttp.WSMsgType.ERROR or response.data is None:
|
||||||
self._connected = False
|
self._connected = False
|
||||||
break
|
break
|
||||||
|
@ -185,7 +185,7 @@ class Drawing:
|
|||||||
data = self.__json__()
|
data = self.__json__()
|
||||||
if not svg_changed:
|
if not svg_changed:
|
||||||
del data["svg"]
|
del data["svg"]
|
||||||
self._project.controller.notification.project_emit("drawing.updated", data)
|
self._project.emit_notification("drawing.updated", data)
|
||||||
self._project.dump()
|
self._project.dump()
|
||||||
|
|
||||||
def __json__(self, topology_dump=False):
|
def __json__(self, topology_dump=False):
|
||||||
|
@ -74,7 +74,7 @@ async def export_project(project, temporary_dir, include_images=False, keep_comp
|
|||||||
except OSError as e:
|
except OSError as e:
|
||||||
msg = "Could not export file {}: {}".format(path, e)
|
msg = "Could not export file {}: {}".format(path, e)
|
||||||
log.warning(msg)
|
log.warning(msg)
|
||||||
project.controller.notification.project_emit("log.warning", {"message": msg})
|
project.emit_notification("log.warning", {"message": msg})
|
||||||
continue
|
continue
|
||||||
# ignore the .gns3 file
|
# ignore the .gns3 file
|
||||||
if file.endswith(".gns3"):
|
if file.endswith(".gns3"):
|
||||||
|
@ -199,14 +199,14 @@ class Link:
|
|||||||
self._filters = new_filters
|
self._filters = new_filters
|
||||||
if self._created:
|
if self._created:
|
||||||
await self.update()
|
await self.update()
|
||||||
self._project.controller.notification.project_emit("link.updated", self.__json__())
|
self._project.emit_notification("link.updated", self.__json__())
|
||||||
self._project.dump()
|
self._project.dump()
|
||||||
|
|
||||||
async def update_suspend(self, value):
|
async def update_suspend(self, value):
|
||||||
if value != self._suspended:
|
if value != self._suspended:
|
||||||
self._suspended = value
|
self._suspended = value
|
||||||
await self.update()
|
await self.update()
|
||||||
self._project.controller.notification.project_emit("link.updated", self.__json__())
|
self._project.emit_notification("link.updated", self.__json__())
|
||||||
self._project.dump()
|
self._project.dump()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -269,7 +269,7 @@ class Link:
|
|||||||
n["node"].add_link(self)
|
n["node"].add_link(self)
|
||||||
n["port"].link = self
|
n["port"].link = self
|
||||||
self._created = True
|
self._created = True
|
||||||
self._project.controller.notification.project_emit("link.created", self.__json__())
|
self._project.emit_notification("link.created", self.__json__())
|
||||||
|
|
||||||
if dump:
|
if dump:
|
||||||
self._project.dump()
|
self._project.dump()
|
||||||
@ -282,7 +282,7 @@ class Link:
|
|||||||
label = node_data.get("label")
|
label = node_data.get("label")
|
||||||
if label:
|
if label:
|
||||||
port["label"] = label
|
port["label"] = label
|
||||||
self._project.controller.notification.project_emit("link.updated", self.__json__())
|
self._project.emit_notification("link.updated", self.__json__())
|
||||||
self._project.dump()
|
self._project.dump()
|
||||||
|
|
||||||
async def create(self):
|
async def create(self):
|
||||||
@ -317,7 +317,7 @@ class Link:
|
|||||||
|
|
||||||
self._capturing = True
|
self._capturing = True
|
||||||
self._capture_file_name = capture_file_name
|
self._capture_file_name = capture_file_name
|
||||||
self._project.controller.notification.project_emit("link.updated", self.__json__())
|
self._project.emit_notification("link.updated", self.__json__())
|
||||||
|
|
||||||
async def stop_capture(self):
|
async def stop_capture(self):
|
||||||
"""
|
"""
|
||||||
@ -325,7 +325,7 @@ class Link:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
self._capturing = False
|
self._capturing = False
|
||||||
self._project.controller.notification.project_emit("link.updated", self.__json__())
|
self._project.emit_notification("link.updated", self.__json__())
|
||||||
|
|
||||||
def pcap_streaming_url(self):
|
def pcap_streaming_url(self):
|
||||||
"""
|
"""
|
||||||
|
@ -405,7 +405,7 @@ class Node:
|
|||||||
await self.parse_node_response(response.json)
|
await self.parse_node_response(response.json)
|
||||||
elif old_json != self.__json__():
|
elif old_json != self.__json__():
|
||||||
# We send notif only if object has changed
|
# We send notif only if object has changed
|
||||||
self.project.controller.notification.project_emit("node.updated", self.__json__())
|
self.project.emit_notification("node.updated", self.__json__())
|
||||||
self.project.dump()
|
self.project.dump()
|
||||||
|
|
||||||
async def parse_node_response(self, response):
|
async def parse_node_response(self, response):
|
||||||
@ -563,13 +563,13 @@ class Node:
|
|||||||
for directory in images_directories(type):
|
for directory in images_directories(type):
|
||||||
image = os.path.join(directory, img)
|
image = os.path.join(directory, img)
|
||||||
if os.path.exists(image):
|
if os.path.exists(image):
|
||||||
self.project.controller.notification.project_emit("log.info", {"message": "Uploading missing image {}".format(img)})
|
self.project.emit_notification("log.info", {"message": "Uploading missing image {}".format(img)})
|
||||||
try:
|
try:
|
||||||
with open(image, 'rb') as f:
|
with open(image, 'rb') as f:
|
||||||
await self._compute.post("/{}/images/{}".format(self._node_type, os.path.basename(img)), data=f, timeout=None)
|
await self._compute.post("/{}/images/{}".format(self._node_type, os.path.basename(img)), data=f, timeout=None)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise aiohttp.web.HTTPConflict(text="Can't upload {}: {}".format(image, str(e)))
|
raise aiohttp.web.HTTPConflict(text="Can't upload {}: {}".format(image, str(e)))
|
||||||
self.project.controller.notification.project_emit("log.info", {"message": "Upload finished for {}".format(img)})
|
self.project.emit_notification("log.info", {"message": "Upload finished for {}".format(img)})
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -33,19 +33,19 @@ class Notification:
|
|||||||
self._controller_listeners = []
|
self._controller_listeners = []
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def project_queue(self, project):
|
def project_queue(self, project_id):
|
||||||
"""
|
"""
|
||||||
Get a queue of notifications
|
Get a queue of notifications
|
||||||
|
|
||||||
Use it with Python with
|
Use it with Python with
|
||||||
"""
|
"""
|
||||||
queue = NotificationQueue()
|
queue = NotificationQueue()
|
||||||
self._project_listeners.setdefault(project.id, set())
|
self._project_listeners.setdefault(project_id, set())
|
||||||
self._project_listeners[project.id].add(queue)
|
self._project_listeners[project_id].add(queue)
|
||||||
try:
|
try:
|
||||||
yield queue
|
yield queue
|
||||||
finally:
|
finally:
|
||||||
self._project_listeners[project.id].remove(queue)
|
self._project_listeners[project_id].remove(queue)
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def controller_queue(self):
|
def controller_queue(self):
|
||||||
@ -84,14 +84,14 @@ class Notification:
|
|||||||
for controller_listener in self._controller_listeners:
|
for controller_listener in self._controller_listeners:
|
||||||
controller_listener.put_nowait((action, event, {}))
|
controller_listener.put_nowait((action, event, {}))
|
||||||
|
|
||||||
def project_has_listeners(self, project):
|
def project_has_listeners(self, project_id):
|
||||||
"""
|
"""
|
||||||
:param project_id: Project object
|
:param project_id: Project object
|
||||||
:returns: True if client listen this project
|
:returns: True if client listen this project
|
||||||
"""
|
"""
|
||||||
return project.id in self._project_listeners and len(self._project_listeners[project.id]) > 0
|
return project_id in self._project_listeners and len(self._project_listeners[project_id]) > 0
|
||||||
|
|
||||||
async def dispatch(self, action, event, compute_id):
|
async def dispatch(self, action, event, project_id, compute_id):
|
||||||
"""
|
"""
|
||||||
Notification received from compute node. Send it directly
|
Notification received from compute node. Send it directly
|
||||||
to clients or process it
|
to clients or process it
|
||||||
@ -110,13 +110,13 @@ class Notification:
|
|||||||
self.project_emit("node.updated", node.__json__())
|
self.project_emit("node.updated", node.__json__())
|
||||||
except (aiohttp.web.HTTPNotFound, aiohttp.web.HTTPForbidden): # Project closing
|
except (aiohttp.web.HTTPNotFound, aiohttp.web.HTTPForbidden): # Project closing
|
||||||
return
|
return
|
||||||
elif action == "ping":
|
# elif action == "ping":
|
||||||
event["compute_id"] = compute_id
|
# event["compute_id"] = compute_id
|
||||||
self.project_emit(action, event)
|
# self.project_emit(action, event)
|
||||||
else:
|
else:
|
||||||
self.project_emit(action, event)
|
self.project_emit(action, event, project_id)
|
||||||
|
|
||||||
def project_emit(self, action, event):
|
def project_emit(self, action, event, project_id=None):
|
||||||
"""
|
"""
|
||||||
Send a notification to clients scoped by projects
|
Send a notification to clients scoped by projects
|
||||||
|
|
||||||
@ -136,8 +136,8 @@ class Notification:
|
|||||||
except TypeError: # If we receive a mock as an event it will raise TypeError when using json dump
|
except TypeError: # If we receive a mock as an event it will raise TypeError when using json dump
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if "project_id" in event:
|
if "project_id" in event or project_id:
|
||||||
self._send_event_to_project(event["project_id"], action, event)
|
self._send_event_to_project(event.get("project_id", project_id), action, event)
|
||||||
else:
|
else:
|
||||||
self._send_event_to_all_projects(action, event)
|
self._send_event_to_all_projects(action, event)
|
||||||
|
|
||||||
|
@ -122,6 +122,16 @@ class Project:
|
|||||||
assert self._status != "closed"
|
assert self._status != "closed"
|
||||||
self.dump()
|
self.dump()
|
||||||
|
|
||||||
|
def emit_notification(self, action, event):
|
||||||
|
"""
|
||||||
|
Emit a notification to all clients using this project.
|
||||||
|
|
||||||
|
:param action: Action name
|
||||||
|
:param event: Event to send
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.controller.notification.project_emit(action, event, project_id=self.id)
|
||||||
|
|
||||||
async def update(self, **kwargs):
|
async def update(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Update the project
|
Update the project
|
||||||
@ -135,7 +145,7 @@ class Project:
|
|||||||
|
|
||||||
# We send notif only if object has changed
|
# We send notif only if object has changed
|
||||||
if old_json != self.__json__():
|
if old_json != self.__json__():
|
||||||
self.controller.notification.project_emit("project.updated", self.__json__())
|
self.emit_notification("project.updated", self.__json__())
|
||||||
self.dump()
|
self.dump()
|
||||||
|
|
||||||
# update on computes
|
# update on computes
|
||||||
@ -533,7 +543,7 @@ class Project:
|
|||||||
self._project_created_on_compute.add(compute)
|
self._project_created_on_compute.add(compute)
|
||||||
await node.create()
|
await node.create()
|
||||||
self._nodes[node.id] = node
|
self._nodes[node.id] = node
|
||||||
self.controller.notification.project_emit("node.created", node.__json__())
|
self.emit_notification("node.created", node.__json__())
|
||||||
if dump:
|
if dump:
|
||||||
self.dump()
|
self.dump()
|
||||||
return node
|
return node
|
||||||
@ -558,7 +568,7 @@ class Project:
|
|||||||
del self._nodes[node.id]
|
del self._nodes[node.id]
|
||||||
await node.destroy()
|
await node.destroy()
|
||||||
self.dump()
|
self.dump()
|
||||||
self.controller.notification.project_emit("node.deleted", node.__json__())
|
self.emit_notification("node.deleted", node.__json__())
|
||||||
|
|
||||||
@open_required
|
@open_required
|
||||||
def get_node(self, node_id):
|
def get_node(self, node_id):
|
||||||
@ -623,7 +633,7 @@ class Project:
|
|||||||
if drawing_id not in self._drawings:
|
if drawing_id not in self._drawings:
|
||||||
drawing = Drawing(self, drawing_id=drawing_id, **kwargs)
|
drawing = Drawing(self, drawing_id=drawing_id, **kwargs)
|
||||||
self._drawings[drawing.id] = drawing
|
self._drawings[drawing.id] = drawing
|
||||||
self.controller.notification.project_emit("drawing.created", drawing.__json__())
|
self.emit_notification("drawing.created", drawing.__json__())
|
||||||
if dump:
|
if dump:
|
||||||
self.dump()
|
self.dump()
|
||||||
return drawing
|
return drawing
|
||||||
@ -644,7 +654,7 @@ class Project:
|
|||||||
drawing = self.get_drawing(drawing_id)
|
drawing = self.get_drawing(drawing_id)
|
||||||
del self._drawings[drawing.id]
|
del self._drawings[drawing.id]
|
||||||
self.dump()
|
self.dump()
|
||||||
self.controller.notification.project_emit("drawing.deleted", drawing.__json__())
|
self.emit_notification("drawing.deleted", drawing.__json__())
|
||||||
|
|
||||||
@open_required
|
@open_required
|
||||||
async def add_link(self, link_id=None, dump=True):
|
async def add_link(self, link_id=None, dump=True):
|
||||||
@ -671,7 +681,7 @@ class Project:
|
|||||||
if force_delete is False:
|
if force_delete is False:
|
||||||
raise
|
raise
|
||||||
self.dump()
|
self.dump()
|
||||||
self.controller.notification.project_emit("link.deleted", link.__json__())
|
self.emit_notification("link.deleted", link.__json__())
|
||||||
|
|
||||||
@open_required
|
@open_required
|
||||||
def get_link(self, link_id):
|
def get_link(self, link_id):
|
||||||
@ -743,7 +753,7 @@ class Project:
|
|||||||
self._clean_pictures()
|
self._clean_pictures()
|
||||||
self._status = "closed"
|
self._status = "closed"
|
||||||
if not ignore_notification:
|
if not ignore_notification:
|
||||||
self.controller.notification.project_emit("project.closed", self.__json__())
|
self.emit_notification("project.closed", self.__json__())
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def _clean_pictures(self):
|
def _clean_pictures(self):
|
||||||
|
@ -122,7 +122,7 @@ class Snapshot:
|
|||||||
except (OSError, PermissionError) as e:
|
except (OSError, PermissionError) as e:
|
||||||
raise aiohttp.web.HTTPConflict(text=str(e))
|
raise aiohttp.web.HTTPConflict(text=str(e))
|
||||||
await project.open()
|
await project.open()
|
||||||
self._project.controller.notification.project_emit("snapshot.restored", self.__json__())
|
self._project.emit_notification("snapshot.restored", self.__json__())
|
||||||
return self._project
|
return self._project
|
||||||
|
|
||||||
def __json__(self):
|
def __json__(self):
|
||||||
|
@ -220,24 +220,31 @@ class ProjectHandler:
|
|||||||
async def notification(request, response):
|
async def notification(request, response):
|
||||||
|
|
||||||
controller = Controller.instance()
|
controller = Controller.instance()
|
||||||
project = controller.get_project(request.match_info["project_id"])
|
project_id = request.match_info["project_id"]
|
||||||
|
|
||||||
response.content_type = "application/json"
|
response.content_type = "application/json"
|
||||||
response.set_status(200)
|
response.set_status(200)
|
||||||
response.enable_chunked_encoding()
|
response.enable_chunked_encoding()
|
||||||
|
|
||||||
await response.prepare(request)
|
await response.prepare(request)
|
||||||
with controller.notification.project_queue(project) as queue:
|
log.info("New client has connected to the notification stream for project ID '{}' (HTTP long-polling method)".format(project_id))
|
||||||
|
|
||||||
|
try:
|
||||||
|
with controller.notification.project_queue(project_id) as queue:
|
||||||
while True:
|
while True:
|
||||||
msg = await queue.get_json(5)
|
msg = await queue.get_json(5)
|
||||||
await response.write(("{}\n".format(msg)).encode("utf-8"))
|
await response.write(("{}\n".format(msg)).encode("utf-8"))
|
||||||
|
finally:
|
||||||
|
log.info("Client has disconnected from notification for project ID '{}' (HTTP long-polling method)".format(project_id))
|
||||||
|
try:
|
||||||
|
project = controller.get_project(project_id)
|
||||||
if project.auto_close:
|
if project.auto_close:
|
||||||
# To avoid trouble with client connecting disconnecting we sleep few seconds before checking
|
# To avoid trouble with client connecting disconnecting we sleep few seconds before checking
|
||||||
# if someone else is not connected
|
# if someone else is not connected
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
if not controller.notification.project_has_listeners(project):
|
if not controller.notification.project_has_listeners(project.id):
|
||||||
|
log.info("Project '{}' is automatically closing due to no client listening".format(project.id))
|
||||||
await project.close()
|
await project.close()
|
||||||
|
except aiohttp.web.HTTPNotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
@Route.get(
|
@Route.get(
|
||||||
r"/projects/{project_id}/notifications/ws",
|
r"/projects/{project_id}/notifications/ws",
|
||||||
@ -252,31 +259,36 @@ class ProjectHandler:
|
|||||||
async def notification_ws(request, response):
|
async def notification_ws(request, response):
|
||||||
|
|
||||||
controller = Controller.instance()
|
controller = Controller.instance()
|
||||||
project = controller.get_project(request.match_info["project_id"])
|
project_id = request.match_info["project_id"]
|
||||||
|
|
||||||
ws = aiohttp.web.WebSocketResponse()
|
ws = aiohttp.web.WebSocketResponse()
|
||||||
await ws.prepare(request)
|
await ws.prepare(request)
|
||||||
|
|
||||||
request.app['websockets'].add(ws)
|
request.app['websockets'].add(ws)
|
||||||
asyncio.ensure_future(process_websocket(ws))
|
asyncio.ensure_future(process_websocket(ws))
|
||||||
|
log.info("New client has connected to the notification stream for project ID '{}' (WebSocket method)".format(project_id))
|
||||||
try:
|
try:
|
||||||
with controller.notification.project_queue(project) as queue:
|
with controller.notification.project_queue(project_id) as queue:
|
||||||
while True:
|
while True:
|
||||||
notification = await queue.get_json(5)
|
notification = await queue.get_json(5)
|
||||||
if ws.closed:
|
if ws.closed:
|
||||||
break
|
break
|
||||||
await ws.send_str(notification)
|
await ws.send_str(notification)
|
||||||
finally:
|
finally:
|
||||||
|
log.info("Client has disconnected from notification stream for project ID '{}' (WebSocket method)".format(project_id))
|
||||||
if not ws.closed:
|
if not ws.closed:
|
||||||
await ws.close()
|
await ws.close()
|
||||||
request.app['websockets'].discard(ws)
|
request.app['websockets'].discard(ws)
|
||||||
|
try:
|
||||||
|
project = controller.get_project(project_id)
|
||||||
if project.auto_close:
|
if project.auto_close:
|
||||||
# To avoid trouble with client connecting disconnecting we sleep few seconds before checking
|
# To avoid trouble with client connecting disconnecting we sleep few seconds before checking
|
||||||
# if someone else is not connected
|
# if someone else is not connected
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
if not controller.notification.project_has_listeners(project):
|
if not controller.notification.project_has_listeners(project_id):
|
||||||
|
log.info("Project '{}' is automatically closing due to no client listening".format(project.id))
|
||||||
await project.close()
|
await project.close()
|
||||||
|
except aiohttp.web.HTTPNotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
return ws
|
return ws
|
||||||
|
|
||||||
@ -298,9 +310,7 @@ class ProjectHandler:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
stream = await export_project(project,
|
stream = await export_project(project, tmp_dir, include_images=bool(int(request.query.get("include_images", "0"))))
|
||||||
tmp_dir,
|
|
||||||
include_images=bool(int(request.query.get("include_images", "0"))))
|
|
||||||
# We need to do that now because export could failed and raise an HTTP error
|
# We need to do that now because export could failed and raise an HTTP error
|
||||||
# that why response start need to be the later possible
|
# that why response start need to be the later possible
|
||||||
response.content_type = 'application/gns3project'
|
response.content_type = 'application/gns3project'
|
||||||
|
Loading…
Reference in New Issue
Block a user