From 3597e6cc0e2797c5a92f45f8844c455025f516f0 Mon Sep 17 00:00:00 2001 From: Robin Date: Sat, 4 Oct 2025 20:32:22 +0200 Subject: [PATCH] +Update --- .gitignore | 4 + LICENSE.md | 22 ++++ README.md | 58 +++++++++++ build.bat | 17 +++ build.sh | 16 +++ main.py | 261 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + 7 files changed, 380 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 build.bat create mode 100644 build.sh create mode 100644 main.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c469d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + + +#Ignore vscode AI rules +.github\instructions\codacy.instructions.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..d5e8879 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,22 @@ + +MIT License + +Copyright (c) 2025 Robin Oliver Lucas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8ddc20 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ + +# Open Port Killer + +A cross-platform GUI utility to find and kill processes with open network ports. Built with Python, tkinter, and psutil. + +## Features + +- **Cross-Platform:** Works on Windows, Linux, and macOS. +- **Process Listing:** Scans and lists all local processes with open TCP ports. +- **Kill Processes:** Easily terminate one or more selected processes from the GUI. +- **Sortable List:** Click any column header to sort the process list. +- **Multi-Language:** Switch between English and German on-the-fly. + +## Installation & Usage + +1. **Prerequisites:** + - Python 3.6+ must be installed. + +2. **Clone the repository (or download the source): + ```sh + git clone + cd OpenPortKiller + ``` + +3. **Install dependencies:** + ```sh + pip install -r requirements.txt + ``` + +4. **Run the application:** + ```sh + python main.py + ``` + +## Building from Source + +You can create a standalone executable for your operating system using the provided build scripts. This requires `pyinstaller`, which is included in `requirements.txt`. + +The executable will be placed in the `dist` directory. + +### Windows + +Run the batch file: +```cmd +.\build.bat +``` + +### Linux / macOS + +First, make the script executable, then run it: +```sh +chmod +x build.sh +./build.sh +``` + +## License + +This project is licensed under the MIT License. See the [LICENSE.md](LICENSE.md) file for details. diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..e025f57 --- /dev/null +++ b/build.bat @@ -0,0 +1,17 @@ + +@echo off +echo Building executable for Windows... + +REM Check if pyinstaller is installed +python -c "import pyinstaller" 2>nul +if %errorlevel% neq 0 ( + echo PyInstaller not found. Please install it first using: + echo pip install -r requirements.txt + exit /b 1 +) + +pyinstaller --name OpenPortKiller --onefile --windowed --icon=NONE main.py + +echo. +echo Build complete. Executable is located in the 'dist' folder. +pause diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..eadf1b3 --- /dev/null +++ b/build.sh @@ -0,0 +1,16 @@ + +#!/bin/bash + +echo "Building executable for Linux/macOS..." + +# Check if pyinstaller is installed +if ! python -c "import pyinstaller" &> /dev/null; then + echo "PyInstaller not found. Please install it first using:" + echo "pip install -r requirements.txt" + exit 1 +fi + +pyinstaller --name OpenPortKiller --onefile --windowed --icon=NONE main.py + +echo "" +echo "Build complete. Executable is located in the 'dist' folder." diff --git a/main.py b/main.py new file mode 100644 index 0000000..ea75a6c --- /dev/null +++ b/main.py @@ -0,0 +1,261 @@ + +import tkinter as tk +from tkinter import ttk, messagebox, font +import psutil +import threading +import locale +import webbrowser + +# --- Internationalization (i18n) --- +STRINGS = { + "en": { + "title": "Open Port Killer", + "pid": "PID", + "name": "Process Name", + "port": "Port", + "ip": "IP Address", + "refresh": "Refresh", + "kill": "Kill Selected Process", + "loading": "Loading process list...", + "searching": "Searching for processes...", + "found": "{count} processes with open ports found.", + "error_fetch": "Error fetching processes: {error}", + "no_selection_title": "No Selection", + "no_selection_msg": "Please select a process from the list.", + "confirm_kill_title": "Confirm Kill", + "confirm_kill_msg": "Do you really want to kill the selected processes?", + "error_title": "Error", + "no_such_process": "Process with PID {pid} not found anymore.", + "access_denied": "Permission denied to kill process with PID {pid}. Try running the script as an administrator/root.", + "unexpected_error": "An unexpected error occurred: {error}", + "help": "Help", + "about": "About", + "language": "Language", + "about_title": "About Open Port Killer", + "about_copyright": "Copyright © 2025 by Robin Oliver Lucas", + "about_homepage": "Homepage:", + "about_email": "Email:", + }, + "de": { + "title": "Open Port Killer", + "pid": "PID", + "name": "Programmname", + "port": "Port", + "ip": "IP-Adresse", + "refresh": "Aktualisieren", + "kill": "Ausgewählten Prozess beenden", + "loading": "Lade Prozessliste...", + "searching": "Suche nach Prozessen...", + "found": "{count} Prozesse mit offenen Ports gefunden.", + "error_fetch": "Fehler beim Abrufen der Prozesse: {error}", + "no_selection_title": "Keine Auswahl", + "no_selection_msg": "Bitte wählen Sie einen Prozess aus der Liste aus.", + "confirm_kill_title": "Bestätigen", + "confirm_kill_msg": "Möchten Sie die ausgewählten Prozesse wirklich beenden?", + "error_title": "Fehler", + "no_such_process": "Prozess mit PID {pid} nicht mehr gefunden.", + "access_denied": "Keine Berechtigung, den Prozess mit PID {pid} zu beenden. Versuchen Sie, das Skript als Administrator/root auszuführen.", + "unexpected_error": "Ein unerwarteter Fehler ist aufgetreten: {error}", + "help": "Hilfe", + "about": "Über", + "language": "Sprache", + "about_title": "Über Open Port Killer", + "about_copyright": "Copyright © 2025 by Robin Oliver Lucas", + "about_homepage": "Homepage:", + "about_email": "E-Mail:", + } +} + +class PortKillerApp: + def __init__(self, root): + self.root = root + self.process_data = [] + self.sort_column = "port" + self.sort_reverse = False + self.strings = STRINGS["en"] # Default to English + + self.root.geometry("650x450") + + # --- Style & Fonts --- + self.style = ttk.Style() + self.style.theme_use("clam") + self.link_font = font.Font(family="Helvetica", size=10, underline=True) + + # --- Menu --- + self.menu = tk.Menu(self.root) + self.root.config(menu=self.menu) + + # Language Menu + self.lang_menu = tk.Menu(self.menu, tearoff=0) + self.menu.add_cascade(label=self.strings["language"], menu=self.lang_menu) + self.lang_menu.add_command(label="English", command=lambda: self.switch_language("en")) + self.lang_menu.add_command(label="Deutsch", command=lambda: self.switch_language("de")) + + # Help Menu + self.help_menu = tk.Menu(self.menu, tearoff=0) + self.menu.add_cascade(label=self.strings["help"], menu=self.help_menu) + self.help_menu.add_command(label=self.strings["about"], command=self.show_about_window) + + # --- Widgets --- + self.tree_frame = ttk.Frame(self.root) + self.tree_frame.pack(pady=10, padx=10, fill="both", expand=True) + + self.columns = ("pid", "name", "port", "ip") + self.tree = ttk.Treeview(self.tree_frame, columns=self.columns, show="headings") + + self.tree.column("pid", width=80, anchor="e") + self.tree.column("name", width=220) + self.tree.column("port", width=80, anchor="e") + self.tree.column("ip", width=150) + + self.tree.pack(side="left", fill="both", expand=True) + + self.scrollbar = ttk.Scrollbar(self.tree_frame, orient="vertical", command=self.tree.yview) + self.tree.configure(yscroll=self.scrollbar.set) + self.scrollbar.pack(side="right", fill="y") + + self.button_frame = ttk.Frame(self.root) + self.button_frame.pack(pady=10) + + self.refresh_button = ttk.Button(self.button_frame, command=self.start_refresh_thread) + self.refresh_button.pack(side="left", padx=5) + + self.kill_button = ttk.Button(self.button_frame, command=self.kill_selected_process) + self.kill_button.pack(side="left", padx=5) + + self.status_label = ttk.Label(self.root) + self.status_label.pack(pady=5) + + self.retranslate_ui() + self.start_refresh_thread() + + def switch_language(self, lang_code): + self.strings = STRINGS.get(lang_code, STRINGS["en"]) + self.retranslate_ui() + + def retranslate_ui(self): + self.root.title(self.strings["title"]) + + # --- Recreate Menu on language change --- + self.menu.delete(0, "end") # Clear existing menu + + # Language Menu + self.lang_menu = tk.Menu(self.menu, tearoff=0) + self.menu.add_cascade(label=self.strings["language"], menu=self.lang_menu) + self.lang_menu.add_command(label="English", command=lambda: self.switch_language("en")) + self.lang_menu.add_command(label="Deutsch", command=lambda: self.switch_language("de")) + + # Help Menu + self.help_menu = tk.Menu(self.menu, tearoff=0) + self.menu.add_cascade(label=self.strings["help"], menu=self.help_menu) + self.help_menu.add_command(label=self.strings["about"], command=self.show_about_window) + + # --- Update other widgets --- + for col in self.columns: + self.tree.heading(col, text=self.strings[col], command=lambda _col=col: self.sort_by_column(_col)) + + self.refresh_button.config(text=self.strings["refresh"]) + self.kill_button.config(text=self.strings["kill"]) + self.status_label.config(text=self.strings["loading"]) + self.update_ui() + + def show_about_window(self): + about_win = tk.Toplevel(self.root) + about_win.title(self.strings["about_title"]) + about_win.geometry("350x150") + about_win.resizable(False, False) + about_win.transient(self.root) + + ttk.Label(about_win, text=self.strings["about_copyright"], font=("Helvetica", 10, "bold")).pack(pady=(15, 5)) + + hp_frame = ttk.Frame(about_win) + hp_frame.pack() + ttk.Label(hp_frame, text=self.strings["about_homepage"]).pack(side="left") + hp_link = tk.Label(hp_frame, text="https://rl-dev.de", fg="blue", cursor="hand2", font=self.link_font) + hp_link.pack(side="left") + hp_link.bind("", lambda e: webbrowser.open_new_tab("https://rl-dev.de")) + + email_frame = ttk.Frame(about_win) + email_frame.pack() + ttk.Label(email_frame, text=self.strings["about_email"]).pack(side="left") + email_link = tk.Label(email_frame, text="admin@rl-dev.de", fg="blue", cursor="hand2", font=self.link_font) + email_link.pack(side="left") + email_link.bind("", lambda e: webbrowser.open_new_tab("mailto:admin@rl-dev.de")) + + ttk.Button(about_win, text="OK", command=about_win.destroy).pack(pady=15) + + def sort_by_column(self, col): + if self.sort_column == col: + self.sort_reverse = not self.sort_reverse + else: + self.sort_column = col + self.sort_reverse = False + self.update_ui() + + def start_refresh_thread(self): + self.refresh_button.config(state="disabled") + self.kill_button.config(state="disabled") + self.status_label.config(text=self.strings["searching"]) + + thread = threading.Thread(target=self.refresh_processes) + thread.daemon = True + thread.start() + + def refresh_processes(self): + processes = [] + try: + connections = psutil.net_connections(kind='inet') + for conn in connections: + if conn.status == 'LISTEN' and conn.pid is not None: + try: + proc = psutil.Process(conn.pid) + processes.append({"pid": conn.pid, "name": proc.name(), "port": conn.laddr.port, "ip": conn.laddr.ip}) + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + self.process_data = processes + except Exception as e: + self.process_data = [] + self.root.after(0, lambda: messagebox.showerror(self.strings["error_title"], self.strings["error_fetch"].format(error=e))) + self.root.after(0, self.update_ui) + + def update_ui(self): + for i in self.tree.get_children(): + self.tree.delete(i) + + if self.process_data: + key_func = lambda p: p.get(self.sort_column, 0) if self.sort_column in ["pid", "port"] else p.get(self.sort_column, "").lower() + sorted_data = sorted(self.process_data, key=key_func, reverse=self.sort_reverse) + for proc in sorted_data: + self.tree.insert("", "end", values=(proc["pid"], proc["name"], proc["port"], proc["ip"])) + + self.refresh_button.config(state="normal") + self.kill_button.config(state="normal") + self.status_label.config(text=self.strings["found"].format(count=len(self.process_data))) + + def kill_selected_process(self): + selected_items = self.tree.selection() + if not selected_items: + messagebox.showwarning(self.strings["no_selection_title"], self.strings["no_selection_msg"]) + return + + if not messagebox.askyesno(self.strings["confirm_kill_title"], self.strings["confirm_kill_msg"]): + return + + for selected_item in selected_items: + item = self.tree.item(selected_item) + pid = item["values"][0] + try: + psutil.Process(pid).kill() + except psutil.NoSuchProcess: + messagebox.showerror(self.strings["error_title"], self.strings["no_such_process"].format(pid=pid)) + except psutil.AccessDenied: + messagebox.showerror(self.strings["error_title"], self.strings["access_denied"].format(pid=pid)) + except Exception as e: + messagebox.showerror(self.strings["error_title"], self.strings["unexpected_error"].format(error=e)) + + self.start_refresh_thread() + +if __name__ == "__main__": + root = tk.Tk() + app = PortKillerApp(root) + root.mainloop() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..60a075e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +psutil +pyinstaller \ No newline at end of file