diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 11030eda..f99147be 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -335,7 +335,7 @@ class CoreClient: self.parse_metadata(response.config) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) # update ui to represent current state self.app.after(0, self.app.joined_session_update) @@ -423,16 +423,20 @@ class CoreClient: ) self.join_session(response.session_id, query_location=False) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) - def delete_session(self, session_id: int = None): + def delete_session(self, session_id: int = None, parent_frame=None): if session_id is None: session_id = self.session_id try: response = self.client.delete_session(session_id) logging.info("deleted session(%s), Result: %s", session_id, response) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + # use the right master widget so the error dialog displays right on top of it + master = self.app + if parent_frame: + master = parent_frame + self.app.after(0, show_grpc_error, e, master, self.app) def set_up(self): """ @@ -468,7 +472,7 @@ class CoreClient: x.node_type: set(x.services) for x in response.defaults } except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) self.app.close() def edit_node(self, core_node: core_pb2.Node): @@ -477,7 +481,7 @@ class CoreClient: self.session_id, core_node.id, core_node.position, source=GUI_SOURCE ) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) def start_session(self) -> core_pb2.StartSessionResponse: nodes = [x.core_node for x in self.canvas_nodes.values()] @@ -521,7 +525,7 @@ class CoreClient: if response.result: self.set_metadata() except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) return response def stop_session(self, session_id: int = None) -> core_pb2.StartSessionResponse: @@ -532,7 +536,7 @@ class CoreClient: response = self.client.stop_session(session_id) logging.info("stopped session(%s), result: %s", session_id, response) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) return response def show_mobility_players(self): @@ -577,7 +581,7 @@ class CoreClient: logging.info("launching terminal %s", cmd) os.system(cmd) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) def save_xml(self, file_path: str): """ @@ -590,7 +594,7 @@ class CoreClient: response = self.client.save_xml(self.session_id, file_path) logging.info("saved xml file %s, result: %s", file_path, response) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) def open_xml(self, file_path: str): """ @@ -601,7 +605,7 @@ class CoreClient: logging.info("open xml file %s, response: %s", file_path, response) self.join_session(response.session_id) except grpc.RpcError as e: - self.app.after(0, show_grpc_error, e) + self.app.after(0, show_grpc_error, e, self.app, self.app) def get_node_service( self, node_id: int, service_name: str diff --git a/daemon/core/gui/data/icons/error.png b/daemon/core/gui/data/icons/error.png new file mode 100644 index 00000000..d73d1dd4 Binary files /dev/null and b/daemon/core/gui/data/icons/error.png differ diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py index f92d23bb..3aaac1a4 100644 --- a/daemon/core/gui/dialogs/configserviceconfig.py +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -65,8 +65,13 @@ class ConfigServiceConfigDialog(Dialog): self.config_frame = None self.default_config = None self.config = None + + self.has_error = False + self.load() - self.draw() + + if not self.has_error: + self.draw() def load(self): try: @@ -106,7 +111,8 @@ class ConfigServiceConfigDialog(Dialog): self.modified_files.add(file) self.temp_service_files[file] = data except grpc.RpcError as e: - show_grpc_error(e) + self.has_error = True + show_grpc_error(e, self.app, self.app) def draw(self): self.top.columnconfigure(0, weight=1) @@ -327,7 +333,7 @@ class ConfigServiceConfigDialog(Dialog): all_current = current_listbox.get(0, tk.END) current_listbox.itemconfig(all_current.index(self.service_name), bg="green") except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.top, self.app) self.destroy() def handle_template_changed(self, event: tk.Event): diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index 1bcdd78b..e5f403fe 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -65,14 +65,16 @@ class EmaneModelDialog(Dialog): self.model = f"emane_{model}" self.interface = interface self.config_frame = None + self.has_error = False try: self.config = self.app.core.get_emane_model_config( self.node.id, self.model, self.interface ) + self.draw() except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.app, self.app) + self.has_error = True self.destroy() - self.draw() def draw(self): self.top.columnconfigure(0, weight=1) @@ -225,7 +227,8 @@ class EmaneConfigDialog(Dialog): dialog = EmaneModelDialog( self, self.app, self.canvas_node.core_node, model_name ) - dialog.show() + if not dialog.has_error: + dialog.show() def emane_model_change(self, event: tk.Event): """ diff --git a/daemon/core/gui/dialogs/mobilityconfig.py b/daemon/core/gui/dialogs/mobilityconfig.py index 18e62a17..f7192ca4 100644 --- a/daemon/core/gui/dialogs/mobilityconfig.py +++ b/daemon/core/gui/dialogs/mobilityconfig.py @@ -29,12 +29,14 @@ class MobilityConfigDialog(Dialog): self.canvas_node = canvas_node self.node = canvas_node.core_node self.config_frame = None + self.has_error = False try: self.config = self.app.core.get_mobility_config(self.node.id) + self.draw() except grpc.RpcError as e: - show_grpc_error(e) + self.has_error = True + show_grpc_error(e, self.app, self.app) self.destroy() - self.draw() def draw(self): self.top.columnconfigure(0, weight=1) diff --git a/daemon/core/gui/dialogs/mobilityplayer.py b/daemon/core/gui/dialogs/mobilityplayer.py index 873a2b37..dd2e9f9a 100644 --- a/daemon/core/gui/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -153,7 +153,7 @@ class MobilityPlayerDialog(Dialog): session_id, self.node.id, MobilityAction.START ) except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.top, self.app) def click_pause(self): self.set_pause() @@ -163,7 +163,7 @@ class MobilityPlayerDialog(Dialog): session_id, self.node.id, MobilityAction.PAUSE ) except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.top, self.app) def click_stop(self): self.set_stop() @@ -173,4 +173,4 @@ class MobilityPlayerDialog(Dialog): session_id, self.node.id, MobilityAction.STOP ) except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.top, self.app) diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py index 1230cede..e41290ed 100644 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -124,7 +124,8 @@ class NodeConfigServiceDialog(Dialog): service_name=self.current.listbox.get(current_selection[0]), node_id=self.node_id, ) - dialog.show() + if not dialog.has_error: + dialog.show() else: messagebox.showinfo( "Node service configuration", "Select a service to configure" diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index c61983f7..f2fe1db2 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -140,7 +140,12 @@ class NodeServiceDialog(Dialog): service_name=self.current.listbox.get(current_selection[0]), node_id=self.node_id, ) - dialog.show() + + # if error occurred when creating ServiceConfigDialog, don't show the dialog + if not dialog.has_error: + dialog.show() + else: + dialog.destroy() else: messagebox.showinfo( "Node service configuration", "Select a service to configure" diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index 804e7e3f..e8c13019 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -64,10 +64,14 @@ class ServiceConfigDialog(Dialog): self.original_service_files = {} self.temp_service_files = {} self.modified_files = set() - self.load() - self.draw() - def load(self): + self.has_error = False + + self.load() + if not self.has_error: + self.draw() + + def load(self) -> bool: try: self.app.core.create_nodes_and_links() default_config = self.app.core.get_node_service( @@ -109,7 +113,8 @@ class ServiceConfigDialog(Dialog): for file, data in file_configs[self.node_id][self.service_name].items(): self.temp_service_files[file] = data except grpc.RpcError as e: - show_grpc_error(e) + self.has_error = True + show_grpc_error(e, self.master, self.app) def draw(self): self.top.columnconfigure(0, weight=1) @@ -444,7 +449,7 @@ class ServiceConfigDialog(Dialog): all_current = current_listbox.get(0, tk.END) current_listbox.itemconfig(all_current.index(self.service_name), bg="green") except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.top, self.app) self.destroy() def display_service_file_data(self, event: tk.Event): diff --git a/daemon/core/gui/dialogs/sessionoptions.py b/daemon/core/gui/dialogs/sessionoptions.py index ffd61340..a3f738a7 100644 --- a/daemon/core/gui/dialogs/sessionoptions.py +++ b/daemon/core/gui/dialogs/sessionoptions.py @@ -17,8 +17,10 @@ class SessionOptionsDialog(Dialog): def __init__(self, master: "Application", app: "Application"): super().__init__(master, app, "Session Options", modal=True) self.config_frame = None + self.has_error = False self.config = self.get_config() - self.draw() + if not self.has_error: + self.draw() def get_config(self): try: @@ -26,7 +28,8 @@ class SessionOptionsDialog(Dialog): response = self.app.core.client.get_session_options(session_id) return response.config except grpc.RpcError as e: - show_grpc_error(e) + self.has_error = True + show_grpc_error(e, self.app, self.app) self.destroy() def draw(self): @@ -53,5 +56,5 @@ class SessionOptionsDialog(Dialog): response = self.app.core.client.set_session_options(session_id, config) logging.info("saved session config: %s", response) except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.top, self.app) self.destroy() diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index 3af76c52..e540a3ca 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -25,8 +25,10 @@ class SessionsDialog(Dialog): self.selected = False self.selected_id = None self.tree = None + self.has_error = False self.sessions = self.get_sessions() - self.draw() + if not self.has_error: + self.draw() def get_sessions(self) -> Iterable[core_pb2.SessionSummary]: try: @@ -34,7 +36,8 @@ class SessionsDialog(Dialog): logging.info("sessions: %s", response) return response.sessions except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.app, self.app) + self.has_error = True self.destroy() def draw(self): @@ -211,7 +214,7 @@ class SessionsDialog(Dialog): item = self.tree.selection() if item: sid = int(self.tree.item(item, "text")) - self.app.core.delete_session(sid) + self.app.core.delete_session(sid, self.top) self.tree.delete(item[0]) if sid == self.app.core.session_id: self.click_new() diff --git a/daemon/core/gui/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py index 264b9e2e..d6da667e 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -27,12 +27,14 @@ class WlanConfigDialog(Dialog): self.canvas_node = canvas_node self.node = canvas_node.core_node self.config_frame = None + self.has_error = False try: self.config = self.app.core.get_wlan_config(self.node.id) + self.draw() except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.app, self.app) + self.has_error = True self.destroy() - self.draw() def draw(self): self.top.columnconfigure(0, weight=1) diff --git a/daemon/core/gui/errors.py b/daemon/core/gui/errors.py index 18e025db..b6152489 100644 --- a/daemon/core/gui/errors.py +++ b/daemon/core/gui/errors.py @@ -1,12 +1,38 @@ -from tkinter import messagebox +from tkinter import ttk from typing import TYPE_CHECKING +from core.gui.dialogs.dialog import Dialog +from core.gui.images import ImageEnum, Images +from core.gui.widgets import CodeText + if TYPE_CHECKING: import grpc + from core.gui.app import Application -def show_grpc_error(e: "grpc.RpcError"): +class ErrorDialog(Dialog): + def __init__(self, master, app: "Application", title: str, details: str): + super().__init__(master, app, title, modal=True) + self.error_message = None + self.details = details + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + image = Images.get(ImageEnum.ERROR, 36) + label = ttk.Label(self.top, image=image) + label.image = image + label.grid(row=0, column=0) + self.error_message = CodeText(self.top) + self.error_message.text.insert("1.0", self.details) + self.error_message.text.config(state="disabled") + self.error_message.grid(row=1, column=0, sticky="nsew") + + +def show_grpc_error(e: "grpc.RpcError", master, app: "Application"): title = [x.capitalize() for x in e.code().name.lower().split("_")] title = " ".join(title) title = f"GRPC {title}" - messagebox.showerror(title, e.details()) + dialog = ErrorDialog(master, app, title, e.details()) + dialog.show() diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index ff33d7e5..dbd8827b 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -163,7 +163,7 @@ class CanvasNode: output = self.app.core.run(self.core_node.id) self.tooltip.text.set(output) except grpc.RpcError as e: - show_grpc_error(e) + show_grpc_error(e, self.app, self.app) def on_leave(self, event: tk.Event): self.tooltip.on_leave(event) @@ -238,12 +238,14 @@ class CanvasNode: def show_wlan_config(self): self.canvas.context = None dialog = WlanConfigDialog(self.app, self.app, self) - dialog.show() + if not dialog.has_error: + dialog.show() def show_mobility_config(self): self.canvas.context = None dialog = MobilityConfigDialog(self.app, self.app, self) - dialog.show() + if not dialog.has_error: + dialog.show() def show_mobility_player(self): self.canvas.context = None diff --git a/daemon/core/gui/images.py b/daemon/core/gui/images.py index 6bf23e24..cd472764 100644 --- a/daemon/core/gui/images.py +++ b/daemon/core/gui/images.py @@ -89,3 +89,4 @@ class ImageEnum(Enum): DELETE = "delete" SHUTDOWN = "shutdown" CANCEL = "cancel" + ERROR = "error" diff --git a/daemon/core/gui/menuaction.py b/daemon/core/gui/menuaction.py index 92657cc5..95699d4c 100644 --- a/daemon/core/gui/menuaction.py +++ b/daemon/core/gui/menuaction.py @@ -9,6 +9,8 @@ import webbrowser from tkinter import filedialog, messagebox from typing import TYPE_CHECKING +import grpc + from core.gui.appconfig import XMLS_PATH from core.gui.dialogs.about import AboutDialog from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog @@ -34,12 +36,18 @@ class MenuAction: self.app = app self.canvas = app.canvas - def cleanup_old_session(self, session_id): - response = self.app.core.stop_session() - self.app.core.delete_session(session_id) - logging.info( - "Stop session(%s) and delete it, result: %s", session_id, response.result - ) + def cleanup_old_session(self, session_id: int): + try: + res = self.app.core.client.get_session(session_id) + logging.debug("retrieve session(%s), %s", session_id, res) + stop_response = self.app.core.stop_session() + logging.debug("stop session(%s), result: %s", session_id, stop_response) + delete_response = self.app.core.delete_session(session_id) + logging.debug( + "deleted session(%s), result: %s", session_id, delete_response + ) + except grpc.RpcError: + logging.debug("session is not alive") def prompt_save_running_session(self, quitapp: bool = False): """ @@ -125,7 +133,8 @@ class MenuAction: def session_options(self): logging.debug("Click options") dialog = SessionOptionsDialog(self.app, self.app) - dialog.show() + if not dialog.has_error: + dialog.show() def session_change_sessions(self): logging.debug("Click change sessions") diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 8298cafb..1df24993 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -3,7 +3,7 @@ import time import tkinter as tk from enum import Enum from functools import partial -from tkinter import messagebox, ttk +from tkinter import ttk from typing import TYPE_CHECKING, Callable from core.api.grpc import core_pb2 @@ -302,9 +302,6 @@ class Toolbar(ttk.Frame): self.set_runtime() self.app.core.set_metadata() self.app.core.show_mobility_players() - else: - message = "\n".join(response.exceptions) - messagebox.showerror("Start Error", message) def set_runtime(self): self.runtime_frame.tkraise() @@ -490,8 +487,6 @@ class Toolbar(ttk.Frame): message = f"Stopped in {total:.3f} seconds" self.app.statusbar.set_status(message) self.app.canvas.stopped_session() - if not response.result: - messagebox.showerror("Stop Error", "Errors stopping session") def update_annotation( self, image: "ImageTk.PhotoImage", shape_type: ShapeType, image_enum diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index 472eae51..52429b26 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -208,6 +208,9 @@ bootfrr() fi bootdaemon "zebra" + if grep -q "^ip route " $FRR_CONF; then + bootdaemon "staticd" + fi for r in rip ripng ospf6 ospf bgp babel; do if grep -q "^router \\<${r}\\>" $FRR_CONF; then bootdaemon "${r}d"