diff --git a/menu-ip-change.py b/menu-ip-change.py index 1151efb..9bee1ba 100644 --- a/menu-ip-change.py +++ b/menu-ip-change.py @@ -173,6 +173,9 @@ class IPChangerApp: self.continuous_ping = tk.BooleanVar(value=False) self.ping_running = False self.ping_stop_event = threading.Event() + self.last_ping_time = tk.StringVar( + value="N/A" + ) # Add variable for last ping time # Variables for IP scanning self.scan_results = [] @@ -263,7 +266,32 @@ class IPChangerApp: self.log_frame = ttk.LabelFrame(self.master, text="Operation Log", padding="5") self.log_frame.grid(row=1, column=0, sticky="nsew", padx=10, pady=(0, 10)) - self.log_text = tk.Text(self.log_frame, wrap="none", height=10) + # Use a better font and enable tag configuration + log_font = ( + "Consolas", + 10, + ) # Use a monospaced font like Consolas or Courier New + self.log_text = tk.Text(self.log_frame, wrap="none", height=10, font=log_font) + + # Create tags for different message types with specific formatting + self.log_text.tag_configure("INFO", foreground="blue") + self.log_text.tag_configure( + "ERROR", foreground="red", font=(log_font[0], log_font[1], "bold") + ) + self.log_text.tag_configure( + "SUCCESS", foreground="green", font=(log_font[0], log_font[1], "bold") + ) + self.log_text.tag_configure("WARN", foreground="orange") + self.log_text.tag_configure("PING", foreground="purple") + self.log_text.tag_configure( + "DISCOVERY", foreground="navy", font=(log_font[0], log_font[1], "bold") + ) + self.log_text.tag_configure("HOSTNAME", foreground="teal") + self.log_text.tag_configure("MAC", foreground="maroon") + self.log_text.tag_configure("COMMAND", foreground="sienna") + self.log_text.tag_configure("VENDOR", foreground="#8B008B") # Dark magenta + + # Continue with existing scrollbar setup self.log_scrollbar_y = ttk.Scrollbar( self.log_frame, orient="vertical", command=self.log_text.yview ) @@ -459,7 +487,7 @@ class IPChangerApp: 1, weight=1 ) # Make the scan frame expandable - # Network Tools section (moved from original layout) + # Network Tools section (ping tool) self.ping_frame = ttk.LabelFrame( self.tools_control_frame, text="Ping Tool", padding="5" ) @@ -479,17 +507,27 @@ class IPChangerApp: # Bind the FocusOut event to save the ping target self.ping_entry.bind("", self.save_current_ping_target) + # Add last ping time display field + ttk.Label(self.ping_frame, text="Last RTT:").grid( + row=0, column=2, sticky="w", padx=5 + ) + self.last_ping_time = tk.StringVar(value="N/A") + ping_time_display = ttk.Entry( + self.ping_frame, textvariable=self.last_ping_time, width=8, state="readonly" + ) + ping_time_display.grid(row=0, column=3, padx=5) + # Add continuous ping checkbox self.continuous_ping_check = ttk.Checkbutton( self.ping_frame, text="Continuous Ping", variable=self.continuous_ping ) - self.continuous_ping_check.grid(row=0, column=2, padx=5) + self.continuous_ping_check.grid(row=0, column=4, padx=5) ttk.Button(self.ping_frame, text="Ping", command=self.do_ping).grid( - row=0, column=3, padx=5 + row=0, column=5, padx=5 ) ttk.Button(self.ping_frame, text="Stop", command=self.stop_ping).grid( - row=0, column=4, padx=5 + row=0, column=6, padx=5 ) # Network Scan section - make it expand both horizontally and vertically @@ -586,29 +624,33 @@ class IPChangerApp: # Replace Listbox with Treeview that has columns - make sure it expands self.scan_results_tree = ttk.Treeview( self.results_frame, - columns=("ip", "hostname", "mac", "vendor"), # Add vendor column + columns=("ip", "delay", "hostname", "mac", "vendor"), # Added delay column show="headings", height=10, ) # Define columns self.scan_results_tree.heading("ip", text="IP Address") + self.scan_results_tree.heading( + "delay", text="Ping (ms)" + ) # New column for ping delay self.scan_results_tree.heading("hostname", text="Hostname") self.scan_results_tree.heading("mac", text="MAC Address") - self.scan_results_tree.heading( - "vendor", text="MAC Vendor" - ) # Add vendor heading + self.scan_results_tree.heading("vendor", text="MAC Vendor") - # Set column widths + # Set column widths with adjusted proportions total_width = 650 # Base total width for the columns self.scan_results_tree.column( - "ip", width=int(total_width * 0.18), anchor="w", stretch=True + "ip", width=int(total_width * 0.15), anchor="w", stretch=True ) self.scan_results_tree.column( - "hostname", width=int(total_width * 0.30), anchor="w", stretch=True + "delay", width=int(total_width * 0.08), anchor="center", stretch=True ) self.scan_results_tree.column( - "mac", width=int(total_width * 0.22), anchor="w", stretch=True + "hostname", width=int(total_width * 0.27), anchor="w", stretch=True + ) + self.scan_results_tree.column( + "mac", width=int(total_width * 0.20), anchor="w", stretch=True ) self.scan_results_tree.column( "vendor", width=int(total_width * 0.30), anchor="w", stretch=True @@ -627,8 +669,52 @@ class IPChangerApp: def clear_log(self): self.log_text.delete("1.0", tk.END) - def log_message(self, message: str): - self.log_text.insert(tk.END, f"{message}\n") + def log_message(self, message: str, tag: str = None): + """Enhanced log function with tags for color and formatting""" + # Try to automatically detect message type if tag not specified + if tag is None: + if "ERROR:" in message or "Error" in message or "failed" in message.lower(): + tag = "ERROR" + elif ( + "SUCCESS" in message + or "Successfully" in message + or "completed" in message + ): + tag = "SUCCESS" + elif "WARNING" in message or "Warn" in message: + tag = "WARN" + elif "Host discovered:" in message: + tag = "DISCOVERY" + elif ( + "Reply from" in message + or "ping" in message.lower() + or "Request timed out" in message + ): + tag = "PING" + elif "Hostname:" in message: + tag = "HOSTNAME" + elif "MAC Address:" in message: + tag = "MAC" + elif "MAC Vendor:" in message or "Vendor:" in message: + tag = "VENDOR" + elif "Executing" in message and "command" in message.lower(): + tag = "COMMAND" + elif "INFO:" in message: + tag = "INFO" + else: + tag = None + + # Insert the timestamp and message + timestamp = time.strftime("%H:%M:%S", time.localtime()) + self.log_text.insert(tk.END, f"[{timestamp}] ", "timestamp") + + # Insert the message with appropriate tag + if tag: + self.log_text.insert(tk.END, f"{message}\n", tag) + else: + self.log_text.insert(tk.END, f"{message}\n") + + # Scroll to see the latest message self.log_text.see(tk.END) self.log_text.update_idletasks() @@ -1017,6 +1103,7 @@ class IPChangerApp: self.log_message("Stopping continuous ping...") self.ping_stop_event.set() self.ping_running = False + self.last_ping_time.set("N/A") # Reset when stopped def _execute_continuous_ping(self, target: str): """Execute continuous ping until stopped""" @@ -1039,13 +1126,17 @@ class IPChangerApp: for reply in response: if reply.success: rtt = reply.time_elapsed_ms + # Update the last ping time field + self.last_ping_time.set(f"{rtt:.1f} ms") self.log_message( f"Reply from {target}: time={rtt:.2f}ms (seq={count})" ) break else: + self.last_ping_time.set("Timeout") self.log_message(f"Request timed out (seq={count})") except Exception as ping_error: + self.last_ping_time.set("Error") self.log_message(f"Ping error: {str(ping_error)} (seq={count})") else: # Fallback to socket @@ -1058,14 +1149,18 @@ class IPChangerApp: s.close() if result == 0: + # Update the last ping time field + self.last_ping_time.set(f"{elapsed_time:.1f} ms") self.log_message( f"Connected to {target}:80 in {elapsed_time:.2f}ms (seq={count})" ) else: + self.last_ping_time.set("Failed") self.log_message( f"Failed to connect to {target}:80 (seq={count})" ) except Exception as socket_error: + self.last_ping_time.set("Error") self.log_message( f"Connection error: {str(socket_error)} (seq={count})" ) @@ -1086,23 +1181,40 @@ class IPChangerApp: self.log_message(f"Error in continuous ping: {str(e)}") finally: self.ping_running = False + self.last_ping_time.set("N/A") # Reset on stop def _execute_ping(self, target: str): """Execute a single ping and display the results""" try: - self.log_message(f"Pinging {target} with 4 echo requests...") + # Reset the last ping time to "Testing..." + self.last_ping_time.set("Testing...") + + self.log_message(f"Pinging {target} with 4 echo requests...", "PING") if PING_LIBRARY_AVAILABLE: # Use PythonPing library response = pythonping(target, count=4, timeout=1) - # Display individual replies + # Display individual replies with PING tag + total_rtt = 0 + successful_pings = 0 + for i, reply in enumerate(response): if reply.success: rtt = reply.time_elapsed_ms - self.log_message(f"Reply from {target}: time={rtt:.2f}ms") + total_rtt += rtt + successful_pings += 1 + self.log_message( + f"Reply from {target}: time={rtt:.2f}ms", "PING" + ) + # Update the last ping time with the most recent successful ping + self.last_ping_time.set(f"{rtt:.1f} ms") else: - self.log_message(f"Request timed out (seq={i+1})") + self.log_message(f"Request timed out (seq={i+1})", "PING") + + # If all pings failed, update the last ping time appropriately + if successful_pings == 0: + self.last_ping_time.set("Timeout") # Display statistics success_count = sum(1 for r in response if r.success) @@ -1179,14 +1291,15 @@ class IPChangerApp: vendor = self.get_mac_vendor(mac_address) if hostname: - self.log_message(f"Hostname: {hostname}") + self.log_message(f"Hostname: {hostname}", "HOSTNAME") if mac_address: - self.log_message(f"MAC Address: {mac_address}") + self.log_message(f"MAC Address: {mac_address}", "MAC") if vendor: - self.log_message(f"MAC Vendor: {vendor}") + self.log_message(f"MAC Vendor: {vendor}", "VENDOR") except Exception as e: - self.log_message(f"Error executing ping: {str(e)}") + self.last_ping_time.set("Error") + self.log_message(f"Error executing ping: {str(e)}", "ERROR") def start_scan(self): """Start network scanning between IP range""" @@ -1291,7 +1404,7 @@ class IPChangerApp: self.scan_running = False def _scan_worker(self): - """Worker thread function to scan IPs from the queue - now only performs ping scan""" + """Worker thread function to scan IPs from the queue - now with ping time recording""" while not self.scan_stop_event.is_set(): try: # Get next IP from queue with timeout @@ -1301,18 +1414,24 @@ class IPChangerApp: # No more IPs to scan break - # Ping the IP - is_active = self._ping_host(str(ip_address)) + # Ping the IP and measure the time + ping_time = 0 + ip_str = str(ip_address) + is_active, ping_delay = self._ping_host_with_time(ip_str) # If active, add to results - but don't get host info yet if is_active: - ip_str = str(ip_address) - self.log_message(f"Host discovered: {ip_str}") + self.log_message( + f"Host discovered: {ip_str} ({ping_delay:.1f}ms)", "DISCOVERY" + ) self.scan_results.append(ip_str) - # Add to results tree with placeholder values + # Add to results tree with placeholder values and ping time self.master.after( - 0, lambda ip=ip_str: self.add_scan_result(ip, "", "", "") + 0, + lambda ip=ip_str, delay=ping_delay: self.add_scan_result( + ip, delay, "", "", "" + ), ) # Update progress @@ -1338,7 +1457,7 @@ class IPChangerApp: """Perform the actual host information gathering""" try: self.log_message( - f"Gathering information for {len(self.scan_results)} hosts..." + f"Gathering information for {len(self.scan_results)} hosts...", "INFO" ) # Reset progress bar for this operation @@ -1349,7 +1468,7 @@ class IPChangerApp: # Get information for each host for i, ip in enumerate(self.scan_results): if self.scan_stop_event.is_set(): - self.log_message("Host information gathering stopped.") + self.log_message("Host information gathering stopped.", "WARN") break self.log_message( @@ -1364,21 +1483,20 @@ class IPChangerApp: # Look up vendor information if we have a MAC address if mac_address and MAC_LOOKUP_AVAILABLE: vendor = self.get_mac_vendor(mac_address) - if vendor: - self.log_message(f" MAC Vendor: {vendor}") - else: - self.log_message(f" MAC Vendor: Unknown") - # Log what we found + # Log the results in a cleaner format + result_parts = [] if hostname: - self.log_message(f" Hostname: {hostname}") - else: - self.log_message(f" Hostname: Not resolved") - + result_parts.append(f"Hostname: {hostname}") if mac_address: - self.log_message(f" MAC Address: {mac_address}") + result_parts.append(f"MAC: {mac_address}") + if vendor: + result_parts.append(f"Vendor: {vendor}") + + if result_parts: + self.log_message(f" {ip} → " + " | ".join(result_parts)) else: - self.log_message(f" MAC Address: Not found") + self.log_message(f" {ip} → No additional information found") # Update the tree with vendor information self.update_host_in_tree(ip, hostname, mac_address, vendor) @@ -1386,10 +1504,10 @@ class IPChangerApp: # Update progress self.scan_progress_var.set(i + 1) - self.log_message("Host information gathering completed.") + self.log_message("Host information gathering completed.", "SUCCESS") except Exception as e: - self.log_message(f"Error gathering host information: {str(e)}") + self.log_message(f"Error gathering host information: {str(e)}", "ERROR") def update_host_in_tree(self, ip, hostname, mac, vendor=""): """Update an existing host in the treeview with obtained information""" @@ -1398,46 +1516,66 @@ class IPChangerApp: for item_id in self.scan_results_tree.get_children(): values = self.scan_results_tree.item(item_id, "values") if values and values[0] == ip: - # Update the values + # Get the current delay value (to preserve it) + delay = values[1] if len(values) > 1 else "" + + # Update the values, preserving the delay value hostname_str = hostname if hostname else "Not resolved" - mac_str = mac if mac else "Not found" + mac_str = ( + mac.upper() if mac else "Not found" + ) # Ensure uppercase MAC vendor_str = vendor if vendor else "Unknown" self.scan_results_tree.item( - item_id, values=(ip, hostname_str, mac_str, vendor_str) + item_id, values=(ip, delay, hostname_str, mac_str, vendor_str) ) break except Exception as e: self.log_message(f"Error updating host in treeview: {str(e)}") - def add_scan_result(self, ip, hostname, mac, vendor=""): + def add_scan_result(self, ip, delay=0, hostname="", mac="", vendor=""): """Helper method to add scan result to the treeview""" try: - # Insert the item with all values, even if some are empty + # Insert the item with all values hostname = hostname if hostname else "Not resolved" mac = mac if mac else "Not found" vendor = vendor if vendor else "Unknown" + delay_str = f"{delay:.1f}" if delay > 0 else "" - self.scan_results_tree.insert("", "end", values=(ip, hostname, mac, vendor)) + self.scan_results_tree.insert( + "", "end", values=(ip, delay_str, hostname, mac, vendor) + ) except Exception as e: self.log_message(f"Error adding scan result to UI: {str(e)}") - def _ping_host(self, host: str) -> bool: - """Ping a host to check if it's active, returns True if host responds""" + def _ping_host_with_time(self, host: str) -> tuple[bool, float]: + """Ping a host to check if it's active, returns (success, delay_in_ms)""" try: if PING_LIBRARY_AVAILABLE: # Use PythonPing with shorter timeout for scanning + start_time = time.time() response = pythonping(host, count=1, timeout=0.5) - return response.success() + ping_time = 0 + + if response.success(): + # Get the actual ping time from the response + for reply in response: + if reply.success: + ping_time = reply.time_elapsed_ms + break + return True, ping_time + return False, 0 else: - # Fallback using socket + # Fallback using socket with timing + start_time = time.time() s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(0.5) result = s.connect_ex((host, 80)) # Try common port + ping_time = (time.time() - start_time) * 1000 # ms s.close() - return result == 0 + return result == 0, ping_time if result == 0 else 0 except: - return False + return False, 0 def set_static_ip(self): interface = self.if_var.get() @@ -1553,11 +1691,11 @@ class IPChangerApp: self.show_error("Failed to elevate privileges") def show_error(self, message: str): - self.log_message(f"ERROR: {message}") + self.log_message(f"ERROR: {message}", "ERROR") messagebox.showerror("Error", message) def show_info(self, message: str): - self.log_message(f"INFO: {message}") + self.log_message(f"INFO: {message}", "INFO") messagebox.showinfo("Information", message) # Add new methods for subnet mask conversion @@ -1782,22 +1920,17 @@ class IPChangerApp: def get_hostname(self, ip_address): """Try to resolve hostname for an IP address with better error handling""" try: - self.log_message(f"Resolving hostname for {ip_address}...") + # Silently try to resolve without logging the attempt hostname, _, _ = socket.gethostbyaddr(ip_address) return hostname - except socket.herror as e: - self.log_message(f"Hostname resolution error for {ip_address}: {e}") + except socket.herror: + # Don't log the error details - just return empty return "" - except socket.gaierror as e: - self.log_message(f"Address-related error for {ip_address}: {e}") + except socket.gaierror: return "" except socket.timeout: - self.log_message(f"Timeout resolving hostname for {ip_address}") return "" - except Exception as e: - self.log_message( - f"Unknown error resolving hostname for {ip_address}: {str(e)}" - ) + except Exception: return "" def get_mac_address(self, ip_address): @@ -1813,10 +1946,7 @@ class IPChangerApp: ) if result.returncode == 0 and result.stdout: - # Log the raw output for debugging - self.log_message( - f"ARP output for {ip_address}: {result.stdout.strip()}" - ) + # Don't log the raw ARP output anymore # Parse the output to find the MAC address # First try the typical format with hyphens or colons @@ -1826,7 +1956,8 @@ class IPChangerApp: re.IGNORECASE, ) if mac_match: - return mac_match.group(0) + # Convert to uppercase for consistent display + return mac_match.group(0).upper() # Try an alternative format with spaces (Windows format) mac_match = re.search( @@ -1835,7 +1966,8 @@ class IPChangerApp: re.IGNORECASE, ) if mac_match: - return mac_match.group(0) + # Convert to uppercase for consistent display + return mac_match.group(0).upper() # Try yet another approach - look for any string of hex digits with separators lines = result.stdout.strip().split("\n") @@ -1850,15 +1982,14 @@ class IPChangerApp: parts[1], re.IGNORECASE, ): - return parts[1] + # Convert to uppercase for consistent display + return parts[1].upper() - self.log_message(f"No MAC address found for {ip_address}") + # Only return empty string if MAC not found - don't log the failure return "" except subprocess.TimeoutExpired: - self.log_message(f"Timeout running ARP command for {ip_address}") return "" - except Exception as e: - self.log_message(f"Error getting MAC address for {ip_address}: {str(e)}") + except Exception: return "" def get_mac_vendor(self, mac_address):