+Update
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#Ignore vscode AI rules
|
||||||
|
.github\instructions\codacy.instructions.md
|
22
LICENSE.md
Normal file
22
LICENSE.md
Normal file
@@ -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.
|
58
README.md
Normal file
58
README.md
Normal file
@@ -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 <repository_url>
|
||||||
|
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.
|
17
build.bat
Normal file
17
build.bat
Normal file
@@ -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
|
16
build.sh
Normal file
16
build.sh
Normal file
@@ -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."
|
261
main.py
Normal file
261
main.py
Normal file
@@ -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("<Button-1>", 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("<Button-1>", 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()
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
psutil
|
||||||
|
pyinstaller
|
Reference in New Issue
Block a user