import sys
import psutil
import subprocess
import threading
import re
import time
import socket  # Import socket module
import locale  # To get system's preferred encoding
from collections import defaultdict
from PySide6.QtWidgets import (
    QApplication, QMainWindow, QVBoxLayout, QWidget, QTableWidget,
    QTableWidgetItem, QHeaderView, QComboBox, QHBoxLayout, QLabel, QLineEdit
)
from PySide6.QtCore import QThread, Signal, Qt, QSize
from PySide6.QtGui import QIcon, QPixmap
from PIL import Image
import win32gui
import win32ui
from scapy.all import sniff, IP, TCP, UDP
import os
import ctypes


def make_transparent(image):
    # Convert image to RGBA
    image = image.convert("RGBA")
    datas = image.getdata()
    new_data = []
    for item in datas:
        if item[0] == 0 and item[1] == 0 and item[2] == 0:  # Black
            new_data.append((255, 255, 255, 0))  # Transparent
        else:
            new_data.append(item)
    image.putdata(new_data)
    return image


class NetworkMonitorThread(QThread):
    update_signal = Signal(list)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.running = True
        self.process_connection_map = {}  # key: (local_ip, local_port, remote_ip, remote_port), value: pid
        self.network_usage = defaultdict(lambda: {'bytes_recv': 0, 'bytes_sent': 0, 'name': ''})
        self.lock = threading.Lock()
        self.update_interval = 5  # Interval to update process-connection mapping

        # Start a thread to periodically update the connection map
        self.connection_thread = threading.Thread(target=self.update_connection_map, daemon=True)
        self.connection_thread.start()

        # Start a thread to capture packets
        self.sniff_thread = threading.Thread(target=self.packet_sniffer, daemon=True)
        self.sniff_thread.start()

    def run(self):
        while self.running:
            time.sleep(1)  # Main thread loop can be empty or used for other tasks

    def stop(self):
        self.running = False

    def update_connection_map(self):
        """
        Periodically parse 'netstat -nbo' to map active connections to process IDs.
        """
        while self.running:
            try:
                # Determine system's preferred encoding
                preferred_encoding = locale.getpreferredencoding(False)

                # Execute netstat command
                cmd = ['netstat', '-nbo']
                result = subprocess.check_output(cmd, encoding=preferred_encoding, stderr=subprocess.STDOUT, shell=True)

                # Parse netstat output
                self.process_connection_map.clear()
                lines = result.splitlines()
                # Regex to match lines with TCP connections
                conn_re_tcp = re.compile(r'^\s*TCP\s+(\S+):(\d+)\s+(\S+):(\d+)\s+\S+\s+(\d+)\s+(\d+)')
                # Regex to match lines with UDP connections
                conn_re_udp = re.compile(r'^\s*UDP\s+(\S+):(\d+)\s+(\S+):(\d+)\s+\S+\s+(\d+)')

                for line in lines:
                    line = line.strip()
                    tcp_match = conn_re_tcp.match(line)
                    udp_match = conn_re_udp.match(line)

                    if tcp_match:
                        local_ip, local_port, remote_ip, remote_port, state, pid = tcp_match.groups()
                        if pid.isdigit():
                            pid = int(pid)
                            key = (local_ip, int(local_port), remote_ip, int(remote_port))
                            self.process_connection_map[key] = pid
                    elif udp_match:
                        local_ip, local_port, remote_ip, remote_port, pid = udp_match.groups()
                        if pid.isdigit():
                            pid = int(pid)
                            key = (local_ip, int(local_port), remote_ip, int(remote_port))
                            self.process_connection_map[key] = pid
                    # Ignore lines that don't match

                time.sleep(self.update_interval)
            except subprocess.CalledProcessError as e:
                print(f"Error executing netstat: {e}")
                print("Command Output:")
                print(e.output)
                time.sleep(self.update_interval)
            except UnicodeDecodeError as e:
                print(f"Unicode decoding error in update_connection_map: {e}")
                time.sleep(self.update_interval)
            except Exception as e:
                print(f"Unexpected error in update_connection_map: {e}")
                time.sleep(self.update_interval)

    def parse_address(self, address):
        """
        Parse address in the form IP:Port
        """
        if ':' in address:
            ip, port = address.rsplit(':', 1)
            return ip, int(port)
        else:
            return address, 0

    def packet_sniffer(self):
        """
        Sniff network packets and associate them with processes.
        """
        # Define the callback for packet processing
        def process_packet(packet):
            if IP in packet:
                ip_layer = packet[IP]
                src_ip = ip_layer.src
                dst_ip = ip_layer.dst
                if TCP in packet:
                    l4_layer = packet[TCP]
                elif UDP in packet:
                    l4_layer = packet[UDP]
                else:
                    return  # Non-TCP/UDP packet

                src_port = l4_layer.sport
                dst_port = l4_layer.dport
                packet_len = len(packet)

                # Determine direction: incoming or outgoing
                # For simplicity, assume packets with local_ip as source are outgoing
                # and incoming otherwise
                # Alternatively, you can specify your local interfaces

                # Get local IPs
                local_interfaces = psutil.net_if_addrs()
                local_ips = set()
                for iface in local_interfaces.values():
                    for addr in iface:
                        if addr.family == socket.AF_INET:  # Corrected to use socket.AF_INET
                            local_ips.add(addr.address)

                if src_ip in local_ips:
                    direction = 'out'  # Outgoing
                    local_ip, local_port = src_ip, src_port
                    remote_ip, remote_port = dst_ip, dst_port
                else:
                    direction = 'in'  # Incoming
                    local_ip, local_port = dst_ip, dst_port
                    remote_ip, remote_port = src_ip, src_port

                # Lookup PID
                key = (local_ip, local_port, remote_ip, remote_port)
                with self.lock:
                    pid = self.process_connection_map.get(key)
                    if not pid:
                        # Try reverse lookup
                        reverse_key = (remote_ip, remote_port, local_ip, local_port)
                        pid = self.process_connection_map.get(reverse_key)
                    if pid:
                        try:
                            proc = psutil.Process(pid)
                            proc_name = proc.name()
                        except psutil.NoSuchProcess:
                            proc_name = "Unknown"
                        if direction == 'in':
                            self.network_usage[pid]['bytes_recv'] += packet_len
                        else:
                            self.network_usage[pid]['bytes_sent'] += packet_len
                        self.network_usage[pid]['name'] = proc_name

        # Start sniffing with Scapy
        sniff(
            prn=process_packet,
            store=False,
            iface=None,  # None listens on all interfaces
            filter="tcp or udp",  # Capture only TCP and UDP packets
            timeout=None
        )

    def get_network_usage(self):
        """
        Retrieve the current network usage data.
        """
        with self.lock:
            data = []
            for pid, info in self.network_usage.items():
                name = info['name']
                bytes_recv = info['bytes_recv'] / (1024)  # KB
                bytes_sent = info['bytes_sent'] / (1024)  # KB
                total_recv = info['bytes_recv'] / (1024 * 1024)  # MB
                total_sent = info['bytes_sent'] / (1024 * 1024)  # MB
                data.append((name, bytes_recv, bytes_sent, total_recv, total_sent))
            # Reset the counters after capturing
            self.network_usage.clear()
            return data

    def emit_updates(self):
        """
        Periodically emit updates to the GUI.
        """
        while self.running:
            data = self.get_network_usage()
            if data:
                # Sorting can be handled in the GUI thread
                self.update_signal.emit(data)
            time.sleep(self.update_interval)

    def start(self):
        """
        Override start to initiate the emit_updates thread.
        """
        super().start()
        self.emit_thread = threading.Thread(target=self.emit_updates, daemon=True)
        self.emit_thread.start()


class NetworkMonitor(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("Network Traffic Monitor")
        self.setGeometry(100, 100, 900, 600)

        self.table = QTableWidget(self)
        self.table.setColumnCount(6)
        self.table.setHorizontalHeaderLabels([
            "Icon", "Process", "Download Speed (KB/s)",
            "Upload Speed (KB/s)", "Downloaded (MB)", "Uploaded (MB)"
        ])
        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        self.table.setIconSize(QSize(32, 32))  # Corrected usage

        layout = QVBoxLayout()
        layout.addWidget(self.table)

        self.sort_combo = QComboBox()
        self.sort_combo.addItem("Sort by Download Speed")
        self.sort_combo.addItem("Sort by Process Name")
        self.sort_combo.currentIndexChanged.connect(self.refresh_table)

        self.filter_input = QLineEdit(self)
        self.filter_input.setPlaceholderText("Filter by process name")
        self.filter_input.textChanged.connect(self.refresh_table)

        sort_layout = QHBoxLayout()
        sort_layout.addWidget(QLabel("Sort by:"))
        sort_layout.addWidget(self.sort_combo)
        sort_layout.addWidget(QLabel("Filter:"))
        sort_layout.addWidget(self.filter_input)
        layout.addLayout(sort_layout)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

        self.network_thread = NetworkMonitorThread(self)
        self.network_thread.update_signal.connect(self.update_table)
        self.network_thread.start()

        self.icon_cache = {}
        # Use a default icon path or create a simple placeholder
        # Ensure 'default_icon.png' exists or replace with a valid path
        self.default_icon = QIcon("default_icon.png") if os.path.exists("default_icon.png") else QIcon()

    def update_table(self, data):
        """
        Receive data from the network thread and update the table.
        """
        self.table.setRowCount(len(data))
        for row, (process_name, download_speed, upload_speed, total_downloaded, total_uploaded) in enumerate(data):
            # Icon Item
            icon_item = QTableWidgetItem()
            icon = self.get_exe_icon(process_name)
            if icon:
                icon_item.setIcon(icon)
            else:
                icon_item.setIcon(self.default_icon)
            self.table.setItem(row, 0, icon_item)

            # Process Name
            process_item = QTableWidgetItem(process_name)
            self.table.setItem(row, 1, process_item)

            # Download Speed
            download_item = QTableWidgetItem(f"{download_speed:.2f}")
            self.table.setItem(row, 2, download_item)

            # Upload Speed
            upload_item = QTableWidgetItem(f"{upload_speed:.2f}")
            self.table.setItem(row, 3, upload_item)

            # Total Downloaded
            total_downloaded_item = QTableWidgetItem(f"{total_downloaded:.2f}")
            self.table.setItem(row, 4, total_downloaded_item)

            # Total Uploaded
            total_uploaded_item = QTableWidgetItem(f"{total_uploaded:.2f}")
            self.table.setItem(row, 5, total_uploaded_item)

        self.refresh_table()

    def get_exe_icon(self, process_name):
        """
        Retrieve the executable icon for a given process name.
        """
        if process_name in self.icon_cache:
            return self.icon_cache[process_name]

        try:
            # Find the process by name
            pid = None
            exe_path = None  # Initialize exe_path
            for proc in psutil.process_iter(['pid', 'name', 'exe']):
                if proc.info['name'].lower() == process_name.lower():
                    pid = proc.info['pid']
                    exe_path = proc.info['exe']
                    break

            if not pid or not exe_path:
                return None

            # Extract icon
            large, _ = win32gui.ExtractIconEx(exe_path, 0)
            if not large:
                return None

            # Create a device context
            hdc = win32gui.GetDC(0)
            hdc = win32ui.CreateDCFromHandle(hdc)
            hbmp = win32ui.CreateBitmap()
            hbmp.CreateCompatibleBitmap(hdc, 32, 32)

            # Draw the icon onto the bitmap
            hdc = hdc.CreateCompatibleDC()
            hdc.SelectObject(hbmp)
            hdc.DrawIcon((0, 0), large[0])

            # Convert bitmap to PIL Image
            bmpinfo = hbmp.GetInfo()
            bmpstr = hbmp.GetBitmapBits(True)
            img = Image.frombuffer(
                'RGB',
                (bmpinfo['bmWidth'], bmpinfo['bmHeight']),
                bmpstr, 'raw', 'BGRX', 0, 1
            )

            # Make black transparent
            img = make_transparent(img)

            # Save to temporary file
            img_path = f'temp_icon_{pid}.png'
            img.save(img_path)

            # Load into QPixmap
            pixmap = QPixmap(img_path)
            icon = QIcon(pixmap)
            self.icon_cache[process_name] = icon

            # Clean up temporary file
            os.remove(img_path)

            return icon
        except Exception as e:
            print(f"Error extracting icon for {process_name}: {e}")
            return None

    def refresh_table(self):
        """
        Apply sorting and filtering to the table.
        """
        sort_by = self.sort_combo.currentIndex()
        filter_text = self.filter_input.text().lower()

        # Collect visible items
        visible_rows = []
        for row in range(self.table.rowCount()):
            process_item = self.table.item(row, 1)
            if process_item:
                process_name = process_item.text().lower()
                if filter_text in process_name or not filter_text:
                    visible_rows.append(row)

        # Sorting
        if sort_by == 0:
            # Sort by Download Speed
            visible_rows.sort(key=lambda r: float(self.table.item(r, 2).text()), reverse=True)
        else:
            # Sort by Process Name
            visible_rows.sort(key=lambda r: self.table.item(r, 1).text().lower())

        # Reorder rows
        for idx, row in enumerate(visible_rows):
            for col in range(self.table.columnCount()):
                item = self.table.takeItem(row, col)
                self.table.setItem(idx, col, item)

    def closeEvent(self, event):
        self.network_thread.stop()
        self.network_thread.wait()
        super().closeEvent(event)


if __name__ == "__main__":
    # 检查是否以管理员身份运行
    try:
        is_admin = ctypes.windll.shell32.IsUserAnAdmin()
    except:
        is_admin = False

    if not is_admin:
        print("请以管理员身份运行此程序以获取完整的网络监控功能。")
        ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, __file__, None, 1)
        sys.exit()

    app = QApplication(sys.argv)
    window = NetworkMonitor()
    window.show()
    sys.exit(app.exec())
。。。。。。。。。运行后界面上显示没有监控到任何网络活动，怎么办