diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 633bc237..4ee492b9 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -381,7 +381,7 @@ def service_configuration(session: Session, config: core_pb2.ServiceConfig) -> N if config.files: service.configs = tuple(config.files) if config.directories: - service.directories = tuple(config.directories) + service.dirs = tuple(config.directories) if config.startup: service.startup = tuple(config.startup) if config.validate: diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 7d8e832c..e20f1506 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -500,7 +500,6 @@ class CoreClient: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: emane_config = None - response = core_pb2.StartSessionResponse(result=False) try: response = self.client.start_session( @@ -521,7 +520,6 @@ class CoreClient: logging.info( "start session(%s), result: %s", self.session_id, response.result ) - if response.result: self.set_metadata() except grpc.RpcError as e: @@ -620,6 +618,8 @@ class CoreClient: self, node_id: int, service_name: str, + dirs: List[str], + files: List[str], startups: List[str], validations: List[str], shutdowns: List[str], @@ -628,14 +628,17 @@ class CoreClient: self.session_id, node_id, service_name, + directories=dirs, + files=files, startup=startups, validate=validations, shutdown=shutdowns, ) logging.info( - "Set %s service for node(%s), Startup: %s, Validation: %s, Shutdown: %s, Result: %s", + "Set %s service for node(%s), files: %s, Startup: %s, Validation: %s, Shutdown: %s, Result: %s", service_name, node_id, + files, startups, validations, shutdowns, @@ -933,6 +936,8 @@ class CoreClient: config_proto = core_pb2.ServiceConfig( node_id=node_id, service=name, + directories=config.dirs, + files=config.configs, startup=config.startup, validate=config.validate, shutdown=config.shutdown, diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index b528e32a..e610cf94 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -1,8 +1,7 @@ -""" -Service configuration dialog -""" +import logging +import os import tkinter as tk -from tkinter import ttk +from tkinter import filedialog, ttk from typing import TYPE_CHECKING, Any, List import grpc @@ -48,12 +47,18 @@ class ServiceConfigDialog(Dialog): self.validation_mode = None self.validation_time = None self.validation_period = None - self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW, 16) - self.editdelete_img = Images.get(ImageEnum.EDITDELETE, 16) + self.directory_entry = None + self.default_directories = [] + self.temp_directories = [] + self.documentnew_img = Images.get( + ImageEnum.DOCUMENTNEW, int(16 * app.app_scale) + ) + self.editdelete_img = Images.get(ImageEnum.EDITDELETE, int(16 * app.app_scale)) self.notebook = None self.metadata_entry = None self.filename_combobox = None + self.dir_list = None self.startup_commands_listbox = None self.shutdown_commands_listbox = None self.validate_commands_listbox = None @@ -62,6 +67,7 @@ class ServiceConfigDialog(Dialog): self.service_file_data = None self.validation_period_entry = None self.original_service_files = {} + self.default_config = None self.temp_service_files = {} self.modified_files = set() @@ -71,7 +77,7 @@ class ServiceConfigDialog(Dialog): if not self.has_error: self.draw() - def load(self) -> bool: + def load(self): try: self.app.core.create_nodes_and_links() default_config = self.app.core.get_node_service( @@ -80,15 +86,14 @@ class ServiceConfigDialog(Dialog): self.default_startup = default_config.startup[:] self.default_validate = default_config.validate[:] self.default_shutdown = default_config.shutdown[:] - custom_configs = self.service_configs - if ( - self.node_id in custom_configs - and self.service_name in custom_configs[self.node_id] - ): - service_config = custom_configs[self.node_id][self.service_name] - else: - service_config = default_config - + self.default_directories = default_config.dirs[:] + custom_service_config = self.service_configs.get(self.node_id, {}).get( + self.service_name, None + ) + self.default_config = default_config + service_config = ( + custom_service_config if custom_service_config else default_config + ) self.dependencies = service_config.dependencies[:] self.executables = service_config.executables[:] self.metadata = service_config.meta @@ -98,20 +103,19 @@ class ServiceConfigDialog(Dialog): self.shutdown_commands = service_config.shutdown[:] self.validation_mode = service_config.validation_mode self.validation_time = service_config.validation_timer + self.temp_directories = service_config.dirs[:] self.original_service_files = { x: self.app.core.get_node_service_file( self.node_id, self.service_name, x ) - for x in self.filenames + for x in default_config.configs } self.temp_service_files = dict(self.original_service_files) - file_configs = self.file_configs - if ( - self.node_id in file_configs - and self.service_name in file_configs[self.node_id] - ): - for file, data in file_configs[self.node_id][self.service_name].items(): - self.temp_service_files[file] = data + file_config = self.file_configs.get(self.node_id, {}).get( + self.service_name, {} + ) + for file, data in file_config.items(): + self.temp_service_files[file] = data except grpc.RpcError as e: self.has_error = True show_grpc_error(e, self.master, self.app) @@ -155,18 +159,18 @@ class ServiceConfigDialog(Dialog): frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="File Name") label.grid(row=0, column=0, padx=PADX, sticky="w") - self.filename_combobox = ttk.Combobox( - frame, values=self.filenames, state="readonly" - ) + self.filename_combobox = ttk.Combobox(frame, values=self.filenames) self.filename_combobox.bind( "<>", self.display_service_file_data ) self.filename_combobox.grid(row=0, column=1, sticky="ew", padx=PADX) - button = ttk.Button(frame, image=self.documentnew_img, state="disabled") - button.bind("", self.add_filename) + button = ttk.Button( + frame, image=self.documentnew_img, command=self.add_filename + ) button.grid(row=0, column=2, padx=PADX) - button = ttk.Button(frame, image=self.editdelete_img, state="disabled") - button.bind("", self.delete_filename) + button = ttk.Button( + frame, image=self.editdelete_img, command=self.delete_filename + ) button.grid(row=0, column=3) frame = ttk.Frame(tab) @@ -229,7 +233,30 @@ class ServiceConfigDialog(Dialog): tab, text="Directories required by this service that are unique for each node.", ) - label.grid() + label.grid(row=0, column=0, sticky="ew") + frame = ttk.Frame(tab, padding=FRAME_PAD) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.grid(row=1, column=0, sticky="nsew") + var = tk.StringVar(value="") + self.directory_entry = ttk.Entry(frame, textvariable=var) + self.directory_entry.grid(row=0, column=0, sticky="ew") + button = ttk.Button(frame, text="...", command=self.find_directory_button) + button.grid(row=0, column=1, sticky="ew") + self.dir_list = ListboxScroll(tab) + self.dir_list.grid(row=2, column=0, sticky="nsew") + self.dir_list.listbox.bind("<>", self.directory_select) + for d in self.temp_directories: + self.dir_list.listbox.insert("end", d) + + frame = ttk.Frame(tab) + frame.grid(row=3, column=0, sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + button = ttk.Button(frame, text="Add", command=self.add_directory) + button.grid(row=0, column=0, sticky="ew") + button = ttk.Button(frame, text="Remove", command=self.remove_directory) + button.grid(row=0, column=1, sticky="ew") def draw_tab_startstop(self): tab = ttk.Frame(self.notebook, padding=FRAME_PAD) @@ -358,26 +385,30 @@ class ServiceConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=3, sticky="ew") - def add_filename(self, event: tk.Event): - # not worry about it for now - return - frame_contains_button = event.widget.master - combobox = frame_contains_button.grid_slaves(row=0, column=1)[0] - filename = combobox.get() - if filename not in combobox["values"]: - combobox["values"] += (filename,) + def add_filename(self): + filename = self.filename_combobox.get() + if filename not in self.filename_combobox["values"]: + self.filename_combobox["values"] += (filename,) + self.filename_combobox.set(filename) + self.temp_service_files[filename] = self.service_file_data.text.get( + 1.0, "end" + ) + else: + logging.debug("file already existed") - def delete_filename(self, event: tk.Event): - # not worry about it for now - return - frame_comntains_button = event.widget.master - combobox = frame_comntains_button.grid_slaves(row=0, column=1)[0] - filename = combobox.get() - if filename in combobox["values"]: - combobox["values"] = tuple([x for x in combobox["values"] if x != filename]) - combobox.set("") + def delete_filename(self): + cbb = self.filename_combobox + filename = cbb.get() + if filename in cbb["values"]: + cbb["values"] = tuple([x for x in cbb["values"] if x != filename]) + cbb.set("") + self.service_file_data.text.delete(1.0, "end") + self.temp_service_files.pop(filename, None) + if filename in self.modified_files: + self.modified_files.remove(filename) - def add_command(self, event: tk.Event): + @classmethod + def add_command(cls, event: tk.Event): frame_contains_button = event.widget.master listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox command_to_add = frame_contains_button.grid_slaves(row=0, column=0)[0].get() @@ -388,7 +419,8 @@ class ServiceConfigDialog(Dialog): return listbox.insert(tk.END, command_to_add) - def update_entry(self, event: tk.Event): + @classmethod + def update_entry(cls, event: tk.Event): listbox = event.widget current_selection = listbox.curselection() if len(current_selection) > 0: @@ -399,7 +431,8 @@ class ServiceConfigDialog(Dialog): entry.delete(0, "end") entry.insert(0, cmd) - def delete_command(self, event: tk.Event): + @classmethod + def delete_command(cls, event: tk.Event): button = event.widget frame_contains_button = button.master listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox @@ -410,30 +443,36 @@ class ServiceConfigDialog(Dialog): entry.delete(0, tk.END) def click_apply(self): - current_listbox = self.master.current.listbox - if not self.is_custom_service_config() and not self.is_custom_service_file(): - if self.node_id in self.service_configs: - self.service_configs[self.node_id].pop(self.service_name, None) - current_listbox.itemconfig(current_listbox.curselection()[0], bg="") + if ( + not self.is_custom_command() + and not self.is_custom_service_file() + and not self.has_new_files() + and not self.is_custom_directory() + ): + self.service_configs.get(self.node_id, {}).pop(self.service_name, None) + self.current_service_color("") self.destroy() return try: - if self.is_custom_service_config(): - startup_commands = self.startup_commands_listbox.get(0, "end") - shutdown_commands = self.shutdown_commands_listbox.get(0, "end") - validate_commands = self.validate_commands_listbox.get(0, "end") + if ( + self.is_custom_command() + or self.has_new_files() + or self.is_custom_directory() + ): + startup, validate, shutdown = self.get_commands() config = self.core.set_node_service( self.node_id, self.service_name, - startups=startup_commands, - validations=validate_commands, - shutdowns=shutdown_commands, + dirs=self.temp_directories, + files=list(self.filename_combobox["values"]), + startups=startup, + validations=validate, + shutdowns=shutdown, ) if self.node_id not in self.service_configs: self.service_configs[self.node_id] = {} self.service_configs[self.node_id][self.service_name] = config - for file in self.modified_files: if self.node_id not in self.file_configs: self.file_configs[self.node_id] = {} @@ -442,53 +481,67 @@ class ServiceConfigDialog(Dialog): self.file_configs[self.node_id][self.service_name][ file ] = self.temp_service_files[file] - self.app.core.set_node_service_file( self.node_id, self.service_name, file, self.temp_service_files[file] ) - all_current = current_listbox.get(0, tk.END) - current_listbox.itemconfig(all_current.index(self.service_name), bg="green") + self.current_service_color("green") except grpc.RpcError as e: show_grpc_error(e, self.top, self.app) self.destroy() def display_service_file_data(self, event: tk.Event): - combobox = event.widget - filename = combobox.get() + filename = self.filename_combobox.get() self.service_file_data.text.delete(1.0, "end") self.service_file_data.text.insert("end", self.temp_service_files[filename]) def update_temp_service_file_data(self, event: tk.Event): - scrolledtext = event.widget filename = self.filename_combobox.get() - self.temp_service_files[filename] = scrolledtext.get(1.0, "end") - if self.temp_service_files[filename] != self.original_service_files[filename]: + self.temp_service_files[filename] = self.service_file_data.text.get(1.0, "end") + if self.temp_service_files[filename] != self.original_service_files.get( + filename, "" + ): self.modified_files.add(filename) else: self.modified_files.discard(filename) - def is_custom_service_config(self): - startup_commands = self.startup_commands_listbox.get(0, "end") - shutdown_commands = self.shutdown_commands_listbox.get(0, "end") - validate_commands = self.validate_commands_listbox.get(0, "end") + def is_custom_command(self): + startup, validate, shutdown = self.get_commands() return ( - set(self.default_startup) != set(startup_commands) - or set(self.default_validate) != set(validate_commands) - or set(self.default_shutdown) != set(shutdown_commands) + set(self.default_startup) != set(startup) + or set(self.default_validate) != set(validate) + or set(self.default_shutdown) != set(shutdown) ) + def has_new_files(self): + return set(self.filenames) != set(self.filename_combobox["values"]) + def is_custom_service_file(self): return len(self.modified_files) > 0 + def is_custom_directory(self): + return set(self.default_directories) != set(self.dir_list.listbox.get(0, "end")) + def click_defaults(self): - if self.node_id in self.service_configs: - self.service_configs[self.node_id].pop(self.service_name, None) - if self.node_id in self.file_configs: - self.file_configs[self.node_id].pop(self.service_name, None) + """ + clears out any custom configuration permanently + """ + # clear coreclient data + self.service_configs.get(self.node_id, {}).pop(self.service_name, None) + self.file_configs.get(self.node_id, {}).pop(self.service_name, None) self.temp_service_files = dict(self.original_service_files) - filename = self.filename_combobox.get() + self.modified_files.clear() + + # reset files tab + files = list(self.default_config.configs[:]) + self.filenames = files + self.filename_combobox.config(values=files) self.service_file_data.text.delete(1.0, "end") - self.service_file_data.text.insert("end", self.temp_service_files[filename]) + if len(files) > 0: + filename = files[0] + self.filename_combobox.set(filename) + self.service_file_data.text.insert("end", self.temp_service_files[filename]) + + # reset commands self.startup_commands_listbox.delete(0, tk.END) self.validate_commands_listbox.delete(0, tk.END) self.shutdown_commands_listbox.delete(0, tk.END) @@ -499,13 +552,68 @@ class ServiceConfigDialog(Dialog): for cmd in self.default_shutdown: self.shutdown_commands_listbox.insert(tk.END, cmd) + # reset directories + self.directory_entry.delete(0, "end") + self.dir_list.listbox.delete(0, "end") + self.temp_directories = list(self.default_directories) + for d in self.default_directories: + self.dir_list.listbox.insert("end", d) + + self.current_service_color("") + def click_copy(self): dialog = CopyServiceConfigDialog(self, self.app, self.node_id) dialog.show() + @classmethod def append_commands( - self, commands: List[str], listbox: tk.Listbox, to_add: List[str] + cls, commands: List[str], listbox: tk.Listbox, to_add: List[str] ): for cmd in to_add: commands.append(cmd) listbox.insert(tk.END, cmd) + + def get_commands(self): + startup = self.startup_commands_listbox.get(0, "end") + shutdown = self.shutdown_commands_listbox.get(0, "end") + validate = self.validate_commands_listbox.get(0, "end") + return startup, validate, shutdown + + def find_directory_button(self): + d = filedialog.askdirectory(initialdir="/") + self.directory_entry.delete(0, "end") + self.directory_entry.insert("end", d) + + def add_directory(self): + d = self.directory_entry.get() + if os.path.isdir(d): + if d not in self.temp_directories: + self.dir_list.listbox.insert("end", d) + self.temp_directories.append(d) + + def remove_directory(self): + d = self.directory_entry.get() + dirs = self.dir_list.listbox.get(0, "end") + if d and d in self.temp_directories: + self.temp_directories.remove(d) + try: + i = dirs.index(d) + self.dir_list.listbox.delete(i) + except ValueError: + logging.debug("directory is not in the list") + self.directory_entry.delete(0, "end") + + def directory_select(self, event): + i = self.dir_list.listbox.curselection() + if i: + d = self.dir_list.listbox.get(i) + self.directory_entry.delete(0, "end") + self.directory_entry.insert("end", d) + + def current_service_color(self, color=""): + """ + change the current service label color + """ + listbox = self.master.current.listbox + services = listbox.get(0, tk.END) + listbox.itemconfig(services.index(self.service_name), bg=color) diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 7905ed8c..5652fa40 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -656,8 +656,11 @@ class CanvasGraph(tk.Canvas): delete selected nodes and any data that relates to it """ logging.debug("press delete key") - nodes = self.delete_selection_objects() - self.core.delete_graph_nodes(nodes) + if not self.app.core.is_runtime(): + nodes = self.delete_selection_objects() + self.core.delete_graph_nodes(nodes) + else: + logging.info("node deletion is disabled during runtime state") def double_click(self, event: tk.Event): selected = self.get_selected(event) @@ -850,11 +853,17 @@ class CanvasGraph(tk.Canvas): self.core.create_link(edge, source, dest) def copy(self): + if self.app.core.is_runtime(): + logging.info("copy is disabled during runtime state") + return if self.selection: logging.debug("to copy %s nodes", len(self.selection)) self.to_copy = self.selection.keys() def paste(self): + if self.app.core.is_runtime(): + logging.info("paste is disabled during runtime state") + return # maps original node canvas id to copy node canvas id copy_map = {} # the edges that will be copy over diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 7b4ccf31..3ed5b1d9 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -17,7 +17,7 @@ from core.gui.errors import show_grpc_error from core.gui.graph import tags from core.gui.graph.tooltip import CanvasTooltip from core.gui.images import ImageEnum, Images -from core.gui.nodeutils import ANTENNA_SIZE, EdgeUtils, NodeUtils +from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils if TYPE_CHECKING: from core.gui.app import Application @@ -66,48 +66,9 @@ class CanvasNode: def delete(self): logging.debug("Delete canvas node for %s", self.core_node) - - # if node is wlan, EMANE type, remove any existing wireless links between nodes connetect to this node - if NodeUtils.is_wireless_node(self.core_node.type): - nodes = [] - for edge in self.edges: - token = edge.token - if self.id == token[0]: - nodes.append(token[1]) - else: - nodes.append(token[0]) - for i in range(len(nodes)): - for j in range(i + 1, len(nodes)): - token = EdgeUtils.get_token(nodes[i], nodes[j]) - wireless_edge = self.canvas.wireless_edges.pop(token, None) - if wireless_edge: - - self.canvas.nodes[nodes[i]].wireless_edges.remove(wireless_edge) - self.canvas.nodes[nodes[j]].wireless_edges.remove(wireless_edge) - self.canvas.delete(wireless_edge.id) - else: - logging.debug("%s is not a wireless edge", token) - # if node is MDR, remove wireless links to other MDRs - elif NodeUtils.is_mdr_node(self.core_node.type, self.core_node.model): - for wireless_edge in self.wireless_edges: - token = wireless_edge.token - other = token[0] - if other == self.id: - other = token[1] - self.canvas.nodes[other].wireless_edges.discard(wireless_edge) - try: - wlan_edge = self.canvas.wireless_edges.pop(token) - self.canvas.delete(wlan_edge.id) - except KeyError: - logging.error( - "wireless link not found, potentially multiple wireless link issue" - ) - self.delete_antennas() - - self.wireless_edges.clear() - self.canvas.delete(self.id) self.canvas.delete(self.text_id) + self.delete_antennas() def add_antenna(self): x, y = self.canvas.coords(self.id) diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 935e0b92..f4c12014 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -25,6 +25,7 @@ class Menubar(tk.Menu): self.app = app self.menuaction = action.MenuAction(app, master) self.recent_menu = None + self.edit_menu = None self.draw() def draw(self): @@ -110,6 +111,7 @@ class Menubar(tk.Menu): self.app.master.bind_all("", self.menuaction.copy) self.app.master.bind_all("", self.menuaction.paste) + self.edit_menu = menu def draw_canvas_menu(self): """ @@ -439,3 +441,15 @@ class Menubar(tk.Menu): self.app.core.save_xml(xml_file) else: self.menuaction.file_save_as_xml() + + def change_menubar_item_state(self, is_runtime: bool): + for i in range(self.edit_menu.index("end")): + try: + label_name = self.edit_menu.entrycget(i, "label") + if label_name in ["Copy", "Paste"]: + if is_runtime: + self.edit_menu.entryconfig(i, state="disabled") + else: + self.edit_menu.entryconfig(i, state="normal") + except tk.TclError: + logging.debug("Ignore separators") diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index 870ac8bf..81aa2cba 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -90,10 +90,6 @@ class NodeUtils: def is_rj45_node(cls, node_type: NodeType) -> bool: return node_type in cls.RJ45_NODES - @classmethod - def is_mdr_node(cls, node_type: NodeType, model: str) -> bool: - return cls.is_container_node(node_type) and model == "mdr" - @classmethod def node_icon( cls, diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index eff37257..3b4828d0 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -280,6 +280,7 @@ class Toolbar(ttk.Frame): server. """ self.app.canvas.hide_context() + self.app.menubar.change_menubar_item_state(is_runtime=True) self.app.statusbar.progress_bar.start(5) self.app.canvas.mode = GraphMode.SELECT self.time = time.perf_counter() @@ -469,6 +470,7 @@ class Toolbar(ttk.Frame): """ logging.info("Click stop button") self.app.canvas.hide_context() + self.app.menubar.change_menubar_item_state(is_runtime=False) self.app.statusbar.progress_bar.start(5) self.time = time.perf_counter() task = BackgroundTask(self, self.app.core.stop_session, self.stop_callback)