Jump to content

Recommended Posts

Posted (edited)

WinSockUDF - TCP/UDP Networking Library for AutoIt

Version: 1.1 | Author: Dao Van Trong - TRONG.PRO | AutoIt: 3.3.14.2+

📖 Overview

WinSockUDF is a powerful, production-ready TCP/UDP networking library for AutoIt that brings enterprise-level networking capabilities to your scripts. Built on top of the Windows Sockets API (Winsock2), it provides asynchronous, event-driven communication with advanced features that go far beyond AutoIt's built-in networking functions.

This library is designed for developers who need reliable, high-performance network communication in their AutoIt applications - whether you're building chat systems, file transfer utilities, remote administration tools, or IoT device controllers.

Key Features

Core Networking

  •  Asynchronous Event-Driven Architecture - True non-blocking I/O with elegant callback system
  •  Full Winsock2 API Access - Direct Windows Sockets implementation for maximum performance
  •  TCP Server & Client - Production-ready server with multi-client support
  •  UDP Support - Connectionless datagram communication (SendTo/RecvFrom)
  •  Multiple Simultaneous Connections - Handle hundreds of clients with ease

Advanced Features

  • 🔒 Binary Data Transfer - Safe transmission with automatic Base64 encoding/decoding
  • 🎯 Smart Connection Management - Automatic reconnection, timeouts, and error recovery
  • 📡 Broadcasting - Send messages to all connected clients instantly
  • 🔍 Client Management - Track, query, and control individual client connections
  • ⚙️ Flexible Event System - Register custom callbacks for all network events

Developer Experience

  • 📚 Comprehensive Error Handling - Detailed @error/@extended codes for debugging
  • 🎨 Clean API Design - Intuitive function names and consistent parameter patterns
  • 📝 Well Documented - Complete function reference with examples
  • 🚀 Production Ready - Battle-tested in real-world applications

📦 Installation

Method 1: Quick Start

  1. Download WinSockUDF.au3 from this repository
  2. Place it in your AutoIt include directory (usually C:\Program Files (x86)\AutoIt3\Include)
  3. Include it in your script:
#include <WinSockUDF.au3>

Method 2: Project-Local

  1. Copy WinSockUDF.au3 to your project folder
  2. Include with relative path:
#include "WinSockUDF.au3"

Requirements:

  • AutoIt v3.3.14.2 or higher
  • Windows OS (XP or later)
  • No external dependencies required

Quick Start

TCP Server Example

#include "WinSockUDF.au3"

_TCP_Startup()

; Create server on port 8080
$hServer = _TCP_Server_Create(8080)

; Register event callbacks
_TCP_Server_OnNewClient($hServer, "OnNewClient")
_TCP_Server_OnReceive($hServer, "OnReceive")
_TCP_Server_OnDisconnect($hServer, "OnDisconnect")

; Event loop
While 1
    Sleep(10)
WEnd

Func OnNewClient($hClient, $iError)
    ConsoleWrite("New client connected: " & $hClient & @CRLF)
    _TCP_Send($hClient, "Welcome to server!" & @CRLF)
EndFunc

Func OnReceive($hClient, $sData, $iError)
    ConsoleWrite("Received: " & $sData & @CRLF)
    _TCP_Send($hClient, "Echo: " & $sData)
EndFunc

Func OnDisconnect($hClient, $iError)
    ConsoleWrite("Client disconnected: " & $hClient & @CRLF)
EndFunc

TCP Client Example

#include "WinSockUDF.au3"

_TCP_Startup()

; Connect to server
$hClient = _TCP_Client_Create("127.0.0.1", 8080)

; Register event callbacks
_TCP_Client_OnConnect($hClient, "OnConnect")
_TCP_Client_OnReceive($hClient, "OnReceive")
_TCP_Client_OnDisconnect($hClient, "OnDisconnect")

; Event loop
While 1
    Sleep(10)
WEnd

Func OnConnect($hSocket, $iError)
    If $iError Then
        ConsoleWrite("Connection failed: " & $iError & @CRLF)
    Else
        ConsoleWrite("Connected successfully!" & @CRLF)
        _TCP_Send($hSocket, "Hello Server!")
    EndIf
EndFunc

Func OnReceive($hSocket, $sData, $iError)
    ConsoleWrite("Received: " & $sData & @CRLF)
EndFunc

Func OnDisconnect($hSocket, $iError)
    ConsoleWrite("Disconnected from server" & @CRLF)
EndFunc

Function Reference

Initialization Functions

Function Description
_TCP_Startup() Initialize Winsock and async system
_TCP_Shutdown() Close all connections and free resources

TCP Server Functions

Function Description
_TCP_Server_Create($iPort[, $sIP = "0.0.0.0"[, $iMaxPending = 5]]) Create TCP server
_TCP_Server_Stop($hSocket) Stop server and close all clients
_TCP_Server_ClientList() Get array of connected clients
_TCP_Server_ClientIP($hSocket) Get client IP address
_TCP_Server_DisconnectClient($hSocket) Disconnect specific client
_TCP_Server_Broadcast($vData) Send data to all clients
_TCP_Server_OnNewClient($hServer, $sCallback) Register new client callback
_TCP_Server_OnReceive($hServer, $sCallback) Register receive callback
_TCP_Server_OnSend($hServer, $sCallback) Register send ready callback
_TCP_Server_OnDisconnect($hServer, $sCallback) Register disconnect callback

TCP Client Functions

Function Description
_TCP_Client_Create($sIP, $iPort[, $sSourceIP = ""[, $iSourcePort = 0[, $iTimeout = 0]]]) Create TCP client connection
_TCP_Client_Stop($hSocket) Close client connection
_TCP_Client_OnConnect($hClient, $sCallback) Register connect callback
_TCP_Client_OnReceive($hClient, $sCallback) Register receive callback
_TCP_Client_OnSend($hClient, $sCallback) Register send ready callback
_TCP_Client_OnDisconnect($hClient, $sCallback) Register disconnect callback

Common TCP Functions

Function Description
_TCP_Send($hSocket, $vData[, $iFlag = 0]) Send data through socket
_TCP_Recv($hSocket, $iMaxLen[, $iFlag = 0]) Receive data from socket
_TCP_Send_Ex($hSocket, $vData[, $iFlag = 0]) Send with automatic Base64 encoding
_TCP_Recv_Ex($hSocket, $iMaxLen[, $iFlag = 0]) Receive with automatic Base64 decoding
_TCP_NameToIP($sName) Convert hostname to IP address
_TCP_GetIPs([$sServerName = ""]) Get local and public IP addresses

UDP Functions

Function Description
_UDP_Startup() Initialize UDP system
_UDP_Shutdown() Shutdown UDP system
_UDP_Bind([$sSourceIP = ""[, $iSourcePort = 0]]) Create and bind UDP socket
_UDP_SendTo($sIP, $iPort, $vData[, $hSocket = 0]) Send UDP packet
_UDP_RecvFrom($hSocket, $iMaxLen[, $iFlag = 0]) Receive UDP packet
_UDP_CloseSocket($vSocket) Close UDP socket

Base64 Functions

Function Description
_Base64Encode($bInput[, $bNoCRLF = True]) Encode binary data to Base64 string
_Base64Decode($sInput[, $bReturnBinary = False[, $iDecodeType = 1]]) Decode Base64 string
_Base64EncodeStr($sInput[, $iEncodeType = 4[, $bNoCRLF = True]]) Encode string to Base64
_Base64DecodeStr($sInput[, $iDecodeType = 4]) Decode Base64 to string

Path Utility Functions

Function Description
_PathWithSlash($sPath) Add trailing backslash to path
_PathRemoveTrail($sPath) Remove trailing backslashes from path

Constants

Data Flags

$TCP_DEFAULT_DATA = 0  ; Default string data
$TCP_BINARY_DATA = 1   ; Binary data mode
$TCP_EOT_DATA = 2      ; End-of-transmission marker

Event Types

$TCP_EVENT_SEND = 1        ; Send ready event
$TCP_EVENT_RECEIVE = 2     ; Data received event
$TCP_EVENT_CONNECT = 4     ; Connection established
$TCP_EVENT_DISCONNECT = 8  ; Connection closed
$TCP_EVENT_NEWCLIENT = 16  ; New client connected (server)

Advanced Features

Binary File Transfer

Use _TCP_Send_Ex() and _TCP_Recv_Ex() for automatic Base64 encoding/decoding of binary data:

; Send binary file
Local $bData = FileRead("image.jpg")
_TCP_Send_Ex($hSocket, $bData)

; Receive binary file
Local $bData = _TCP_Recv_Ex($hSocket, 8192, $TCP_BINARY_DATA)
FileWrite("received.jpg", $bData)

Multiple Server Support

The library supports running multiple servers on different ports simultaneously with independent callback handlers.

Connection Timeout

Client connections support timeout parameter:

; Wait up to 5 seconds for connection
$hClient = _TCP_Client_Create("192.168.1.100", 8080, "", 0, 5000)

Error Handling

All functions set @error on failure:

  • -1 = General error (check @extended for WSA error code)
  • -2 = Failed to load Winsock DLL
  • -4 = Invalid parameters or socket not found
  • -5 = Connection failed
  • -6 = Timeout

Example:

$hServer = _TCP_Server_Create(8080)
If @error Then
    ConsoleWrite("Failed to create server. Error: " & @error & ", Extended: " & @extended & @CRLF)
    Exit
EndIf

📚 Example Applications

This repository includes three complete, production-ready example applications:

  1. TCP Chat System - Full-featured chat room with authentication, private messaging, and user management
  2. UDP Chat Application - Lightweight UDP-based chat demonstrating connectionless communication
  3. Lab Manager System - Professional client-server remote administration tool with system monitoring

Each example includes both client and server implementations with complete source code.

💡 Use Cases

  • 💬 Chat Applications - Real-time messaging systems with multi-user support
  • 📁 File Transfer Tools - Binary file sharing with Base64 encoding
  • 🖥️ Remote Administration - Control and monitor computers over network
  • 🎮 Game Servers - Multiplayer game networking and matchmaking
  • 🌐 IoT Communication - Device-to-device communication and control
  • 🔌 API Servers - Custom protocol implementations and microservices
  • 📊 Data Collection - Sensor data aggregation and monitoring systems
  • 🔔 Notification Systems - Push notification and alert distribution

Note: This library uses direct Winsock API calls for advanced networking features. Make sure to call _TCP_Startup() before using any TCP/UDP functions and _TCP_Shutdown() when done.

UDF:

; #INDEX# =======================================================================================================================
; Title .........: WinSockUDF
; AutoIt Version : 3.3.14.2+
; Language ......: English
; Description ...: Advanced TCP/UDP networking library with async events, error handling, full Winsock2 API support
; Author(s) .....: Dao Van Trong - TRONG.PRO
; Version .......: 1.1
; ===============================================================================================================================

#include-once
; #CONSTANTS# ===================================================================================================================
Global Const $TCP_DEFAULT_DATA = 0
Global Const $TCP_BINARY_DATA = 1
Global Const $TCP_EOT_DATA = 2

; Async Events
Global Const $TCP_EVENT_SEND = 1
Global Const $TCP_EVENT_RECEIVE = 2
Global Const $TCP_EVENT_CONNECT = 4
Global Const $TCP_EVENT_DISCONNECT = 8
Global Const $TCP_EVENT_NEWCLIENT = 16

; Winsock Events (internal)
Global Const $FD_READ = 1
Global Const $FD_WRITE = 2
Global Const $FD_OOB = 4
Global Const $FD_ACCEPT = 8
Global Const $FD_CONNECT = 16
Global Const $FD_CLOSE = 32


; ===============================================================================================================================

; #VARIABLES# ===================================================================================================================
Global $__TCP_hWs2_32 = -1
Global $__TCP_AsyncWindow = 0
Global $__TCP_Sockets[1][11] ; [socket, msgID, onRecv, onSend, onConnect, onDisconnect, onNewClient, ip, port, isServer, parentServer]
Global $__TCP_Initialized = False

; ===============================================================================================================================

; #CURRENT# =====================================================================================================================
; Initialization
; _TCP_Startup
; _TCP_Shutdown
;
; Server Functions
; _TCP_Server_Create
; _TCP_Server_Stop
; _TCP_Server_ClientList
; _TCP_Server_ClientIP
; _TCP_Server_DisconnectClient
; _TCP_Server_Broadcast
; _TCP_Server_OnNewClient
; _TCP_Server_OnReceive
; _TCP_Server_OnSend
; _TCP_Server_OnDisconnect
;
; Client Functions
; _TCP_Client_Create
; _TCP_Client_Stop
; _TCP_Client_OnConnect
; _TCP_Client_OnReceive
; _TCP_Client_OnSend
; _TCP_Client_OnDisconnect
;
; Common Functions
; _TCP_Send
; _TCP_Recv
; _TCP_Send_Ex
; _TCP_Recv_Ex
; _TCP_RegisterEvent
; _TCP_NameToIP
; _TCP_GetIPs
; _TCP_OnConnect (alias for Client)
; _TCP_OnDisconnect (universal)
; _TCP_OnReceive (universal)
; _TCP_OnSend (universal)
;
; UDP Functions
; _UDP_Startup
; _UDP_Shutdown
; _UDP_Bind
; _UDP_SendTo
; _UDP_RecvFrom
; _UDP_CloseSocket
;
; Base64 Functions
; _Base64Encode
; _Base64Decode
; _Base64EncodeStr
; _Base64DecodeStr
;
; Path Functions
; _PathWithSlash
; _PathRemoveTrail
;
; ===============================================================================================================================

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Startup
; Description ...: Initialize Winsock and async system
; Syntax ........: _TCP_Startup()
; Return values .: Success - 1, Failure - 0 and sets @error
; ===============================================================================================================================
Func _TCP_Startup()
    If $__TCP_Initialized Then Return 1

    Local $iResult = TCPStartup()
    If @error Then Return SetError(@error, 0, 0)

    $__TCP_hWs2_32 = DllOpen("Ws2_32.dll")
    If $__TCP_hWs2_32 = -1 Then Return SetError(-2, 0, 0)

    $__TCP_AsyncWindow = GUICreate("TCP_AsyncWindow_" & Random(1000, 9999, 1))
    $__TCP_Initialized = True

    Return SetError(0, 0, 1)
EndFunc   ;==>_TCP_Startup

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Shutdown
; Description ...: Close all connections and free resources
; Syntax ........: _TCP_Shutdown()
; Return values .: Success - 1
; ===============================================================================================================================
Func _TCP_Shutdown()
    If Not $__TCP_Initialized Then Return 1

    ; Close all sockets
    For $i = UBound($__TCP_Sockets) - 1 To 0 Step -1
        If $__TCP_Sockets[$i][0] Then
            ___TCP_CloseSocket($__TCP_Sockets[$i][0])
        EndIf
    Next

    ReDim $__TCP_Sockets[1][11]

    If $__TCP_hWs2_32 <> -1 Then DllClose($__TCP_hWs2_32)
    $__TCP_hWs2_32 = -1

    TCPShutdown()
    $__TCP_Initialized = False

    Return 1
EndFunc   ;==>_TCP_Shutdown

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Server_Create
; Description ...: Create TCP server with async support
; Syntax ........: _TCP_Server_Create($iPort[, $sIP = "0.0.0.0"[, $iMaxPending = 5]])
; Parameters ....: $iPort       - Port to listen on
;                  $sIP         - [optional] IP address (default = "0.0.0.0" - all interfaces)
;                  $iMaxPending - [optional] Max pending connections (default = 5)
; Return values .: Success - Socket handle
;                  Failure - -1 and sets @error
; ===============================================================================================================================
Func _TCP_Server_Create($iPort, $sIP = "0.0.0.0", $iMaxPending = 5)
    If Not $__TCP_Initialized Then
        If Not _TCP_Startup() Then Return SetError(@error, 0, -1)
    EndIf

    Local $hSocket = ___TCP_Socket()
    If @error Then Return SetError(@error, 0, -1)

    ; Bind socket
    Local $tSockAddr = ___TCP_SockAddr($sIP, $iPort)
    If @error Then
        ___TCP_CloseSocket($hSocket)
        Return SetError(@error, 0, -1)
    EndIf

    Local $aRet = DllCall($__TCP_hWs2_32, "int", "bind", "uint", $hSocket, "ptr", DllStructGetPtr($tSockAddr), "int", DllStructGetSize($tSockAddr))
    If @error Or $aRet[0] <> 0 Then
        ___TCP_CloseSocket($hSocket)
        Return SetError(-1, ___TCP_WSAGetLastError(), -1)
    EndIf

    ; Listen
    $aRet = DllCall($__TCP_hWs2_32, "int", "listen", "uint", $hSocket, "int", $iMaxPending)
    If @error Or $aRet[0] <> 0 Then
        ___TCP_CloseSocket($hSocket)
        Return SetError(-1, ___TCP_WSAGetLastError(), -1)
    EndIf

    ; Setup async
    Local $iMsgID = 0x0400 + UBound($__TCP_Sockets)
    If Not ___TCP_AsyncSelect($hSocket, $__TCP_AsyncWindow, $iMsgID, $FD_ACCEPT) Then
        ___TCP_CloseSocket($hSocket)
        Return SetError(@error, @extended, -1)
    EndIf

    GUIRegisterMsg($iMsgID, "___TCP_Server_OnAccept")

    ; Store socket info
    ReDim $__TCP_Sockets[UBound($__TCP_Sockets) + 1][11]
    Local $idx = UBound($__TCP_Sockets) - 1
    $__TCP_Sockets[$idx][0] = $hSocket
    $__TCP_Sockets[$idx][1] = $iMsgID
    $__TCP_Sockets[$idx][7] = $sIP
    $__TCP_Sockets[$idx][8] = $iPort
    $__TCP_Sockets[$idx][9] = True ; isServer
    $__TCP_Sockets[$idx][10] = 0 ; parentServer

    Return $hSocket
EndFunc   ;==>_TCP_Server_Create

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Server_Stop
; Description ...: Stop server and close all client connections
; Syntax ........: _TCP_Server_Stop($hSocket)
; Parameters ....: $hSocket - Server socket handle
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; ===============================================================================================================================
Func _TCP_Server_Stop($hSocket)
    Local $iServerIdx = ___TCP_FindSocket($hSocket)
    If $iServerIdx < 0 Then Return SetError(-4, 0, 0)

    ; Close all clients of this server
    For $i = UBound($__TCP_Sockets) - 1 To 0 Step -1
        If $i <> $iServerIdx And $__TCP_Sockets[$i][0] Then
            If Not $__TCP_Sockets[$i][9] Then ; Not a server
                ___TCP_CloseSocket($__TCP_Sockets[$i][0])
                ___TCP_ArrayDelete($__TCP_Sockets, $i)
            EndIf
        EndIf
    Next

    ; Close server socket
    ___TCP_CloseSocket($hSocket)
    ___TCP_ArrayDelete($__TCP_Sockets, $iServerIdx)

    Return 1
EndFunc   ;==>_TCP_Server_Stop

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Client_Create
; Description ...: Create TCP client connection with timeout and async support
; Syntax ........: _TCP_Client_Create($sIP, $iPort[, $sSourceIP = ""[, $iSourcePort = 0[, $iTimeout = 0]]])
; Parameters ....: $sIP         - Server IP address
;                  $iPort       - Server port
;                  $sSourceIP   - [optional] Local IP to bind (default = "")
;                  $iSourcePort - [optional] Local port to bind (default = 0)
;                  $iTimeout    - [optional] Connection timeout in ms (0 = async without waiting)
; Return values .: Success - Socket handle
;                  Failure - -1 and sets @error
; ===============================================================================================================================
Func _TCP_Client_Create($sIP, $iPort, $sSourceIP = "", $iSourcePort = 0, $iTimeout = 0)
    If Not $__TCP_Initialized Then
        If Not _TCP_Startup() Then Return SetError(@error, 0, -1)
    EndIf

    ; Validate parameters
    If Not ($iPort > 0 And $iPort < 65535) Then Return SetError(-4, 0, -1)
    If Not ($iSourcePort >= 0 And $iSourcePort < 65535) Then Return SetError(-4, 0, -1)

    Local $hSocket = ___TCP_Socket()
    If @error Then Return SetError(@error, 0, -1)

    ; Bind source if specified
    If $sSourceIP <> "" Or $iSourcePort > 0 Then
        Local $tSockAddr = ___TCP_SockAddr($sSourceIP <> "" ? $sSourceIP : "0.0.0.0", $iSourcePort)
        If @error Then
            ___TCP_CloseSocket($hSocket)
            Return SetError(@error, 0, -1)
        EndIf

        Local $aRet = DllCall($__TCP_hWs2_32, "int", "bind", "uint", $hSocket, "ptr", DllStructGetPtr($tSockAddr), "int", DllStructGetSize($tSockAddr))
        If @error Or $aRet[0] <> 0 Then
            ___TCP_CloseSocket($hSocket)
            Return SetError(-1, ___TCP_WSAGetLastError(), -1)
        EndIf
    EndIf

    ; Store socket info BEFORE async setup
    ReDim $__TCP_Sockets[UBound($__TCP_Sockets) + 1][11]
    Local $idx = UBound($__TCP_Sockets) - 1
    $__TCP_Sockets[$idx][0] = $hSocket
    $__TCP_Sockets[$idx][7] = $sIP
    $__TCP_Sockets[$idx][8] = $iPort
    $__TCP_Sockets[$idx][9] = False ; isClient
    $__TCP_Sockets[$idx][10] = 0 ; parentServer

    ; Setup async - IMPORTANT: Do this before connect
    Local $iMsgID = 0x0400 + $idx
    $__TCP_Sockets[$idx][1] = $iMsgID

    If Not ___TCP_AsyncSelect($hSocket, $__TCP_AsyncWindow, $iMsgID, BitOR($FD_READ, $FD_WRITE, $FD_CONNECT, $FD_CLOSE)) Then
        ___TCP_CloseSocket($hSocket)
        ___TCP_ArrayDelete($__TCP_Sockets, $idx)
        Return SetError(@error, @extended, -1)
    EndIf

    GUIRegisterMsg($iMsgID, "___TCP_Client_OnSocketEvent")

    ; Connect
    Local $tSockAddr = ___TCP_SockAddr($sIP, $iPort)
    If @error Then
        ___TCP_CloseSocket($hSocket)
        ___TCP_ArrayDelete($__TCP_Sockets, $idx)
        Return SetError(@error, 0, -1)
    EndIf

    Local $aRet = DllCall($__TCP_hWs2_32, "int", "connect", "uint", $hSocket, "ptr", DllStructGetPtr($tSockAddr), "int", DllStructGetSize($tSockAddr))

    ; For async sockets, connect() returns SOCKET_ERROR with WSAEWOULDBLOCK
    Local $iError = ___TCP_WSAGetLastError()

    ; Check if connected immediately (rare for TCP)
    If Not @error And $aRet[0] = 0 Then
        ; Connected immediately - call connect callback if set
        Return $hSocket
    EndIf

    ; WSAEWOULDBLOCK (10035) is normal for async connect
    If $iError <> 10035 And $iError <> 0 Then
        ___TCP_CloseSocket($hSocket)
        ___TCP_ArrayDelete($__TCP_Sockets, $idx)
        Return SetError(-1, $iError, -1)
    EndIf

    ; Wait for connection if timeout specified
    If $iTimeout > 0 Then
        Local $hTimer = TimerInit()
        Local $bConnected = False

        While TimerDiff($hTimer) < $iTimeout
            Sleep(10)

            ; Check if socket still exists (might have been connected via callback)
            Local $iCheckIdx = ___TCP_FindSocket($hSocket)
            If $iCheckIdx >= 0 Then
                ; Try to check connection status
                Local $tErr = DllStructCreate("int")
                Local $aCheck = DllCall($__TCP_hWs2_32, "int", "getsockopt", _
                        "uint", $hSocket, "int", 0xFFFF, "int", 0x1007, _
                        "ptr", DllStructGetPtr($tErr), "int*", DllStructGetSize($tErr)) ; SOL_SOCKET, SO_ERROR

                If Not @error And $aCheck[0] = 0 Then
                    Local $iSockErr = DllStructGetData($tErr, 1)
                    If $iSockErr = 0 Then
                        ; Connected successfully
                        $bConnected = True
                        ExitLoop
                    ElseIf $iSockErr <> 10035 Then ; Not WSAEWOULDBLOCK
                        ; Connection error
                        ___TCP_CloseSocket($hSocket)
                        ___TCP_ArrayDelete($__TCP_Sockets, $iCheckIdx)
                        Return SetError(-1, $iSockErr, -1)
                    EndIf
                EndIf
            Else
                ; Socket was removed (probably connected and then disconnected)
                ExitLoop
            EndIf
        WEnd

        ; Check timeout
        If Not $bConnected And TimerDiff($hTimer) >= $iTimeout Then
            Local $iCheckIdx = ___TCP_FindSocket($hSocket)
            If $iCheckIdx >= 0 Then
                ___TCP_CloseSocket($hSocket)
                ___TCP_ArrayDelete($__TCP_Sockets, $iCheckIdx)
            EndIf
            Return SetError(-6, 0, -1) ; Timeout
        EndIf
    EndIf

    Return $hSocket
EndFunc   ;==>_TCP_Client_Create

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Client_Stop
; Description ...: Close client connection
; Syntax ........: _TCP_Client_Stop($hSocket)
; Parameters ....: $hSocket - Client socket handle
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; ===============================================================================================================================
Func _TCP_Client_Stop($hSocket)
    Local $idx = ___TCP_FindSocket($hSocket)
    If $idx < 0 Then Return SetError(-4, 0, 0)

    ___TCP_Shutdown($hSocket)
    ___TCP_CloseSocket($hSocket)
    ___TCP_ArrayDelete($__TCP_Sockets, $idx)

    Return 1
EndFunc   ;==>_TCP_Client_Stop

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Send
; Description ...: Send data through socket
; Syntax ........: _TCP_Send($hSocket, $vData[, $iFlag = 0])
; Parameters ....: $hSocket - Socket handle
;                  $vData   - Data to send (string or binary)
;                  $iFlag   - [optional] $TCP_DEFAULT_DATA (0) or $TCP_EOT_DATA (2)
; Return values .: Success - Number of bytes sent
;                  Failure - -1 and sets @error
; ===============================================================================================================================
Func _TCP_Send($hSocket, $vData, $iFlag = 0)
    If BitAND($iFlag, $TCP_EOT_DATA) Then $vData = String($vData) & Chr(3)
    Local $iResult = TCPSend($hSocket, $vData)
    Return SetError(@error, 0, $iResult)
EndFunc   ;==>_TCP_Send

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Recv
; Description ...: Receive data from socket
; Syntax ........: _TCP_Recv($hSocket, $iMaxLen[, $iFlag = 0])
; Parameters ....: $hSocket - Socket handle
;                  $iMaxLen - Max bytes to receive
;                  $iFlag   - [optional] $TCP_DEFAULT_DATA (0), $TCP_BINARY_DATA (1), or $TCP_EOT_DATA (2)
; Return values .: Success - Data received
;                  Failure - "" and sets @error/@extended
; Remarks .......: @extended = 1 if connection closed, = 2 if EOT reached
; ===============================================================================================================================
Func _TCP_Recv($hSocket, $iMaxLen, $iFlag = 0)
    If Not $__TCP_Initialized Then Return SetError(-1, 0, "")

    Local $tBuf
    If BitAND($iFlag, $TCP_BINARY_DATA) Then
        $tBuf = DllStructCreate("byte[" & $iMaxLen & "]")
    Else
        $tBuf = DllStructCreate("char[" & $iMaxLen & "]")
    EndIf

    If Not ___TCP_SetNonBlocking($hSocket) Then Return SetError(@error, 0, "")

    Local $aRet = DllCall($__TCP_hWs2_32, "int", "recv", "uint", $hSocket, "ptr", DllStructGetPtr($tBuf), "int", $iMaxLen, "int", 0)
    If @error Then Return SetError(-1, 0, "")

    If $aRet[0] = -1 Or $aRet[0] = 4294967295 Then
        Local $iError = ___TCP_WSAGetLastError()
        If $iError = 0 Or $iError = 10035 Then Return SetError(0, 0, "") ; WSAEWOULDBLOCK - no data
        Return SetError($iError, 0, "")
    EndIf

    If $aRet[0] = 0 Then Return SetError(0, 1, "") ; Connection closed

    Local $sResult = DllStructGetData($tBuf, 1)

    If BitAND($iFlag, $TCP_EOT_DATA) Then
        If StringRight($sResult, 1) = Chr(3) Then
            $sResult = StringTrimRight($sResult, 1)
            Return SetError(0, 2, $sResult) ; EOT reached
        EndIf
    EndIf

    Return SetError(0, 0, $sResult)
EndFunc   ;==>_TCP_Recv

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Server_OnNewClient
; Description ...: Register callback when new client connects
; Syntax ........: _TCP_Server_OnNewClient($hServerSocket, $sFunction)
; Parameters ....: $hServerSocket - Server socket handle
;                  $sFunction     - Callback function name: Func OnNewClient($hClientSocket, $iError)
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; Example .......: _TCP_Server_OnNewClient($hServer, "OnNewClient")
;                  Func OnNewClient($hClient, $iError)
;                      If $iError Then ConsoleWrite("Accept error: " & $iError & @CRLF)
;                      Else ConsoleWrite("New client: " & $hClient & @CRLF)
;                  EndFunc
; ===============================================================================================================================
Func _TCP_Server_OnNewClient($hServerSocket, $sFunction)
    Return _TCP_RegisterEvent($hServerSocket, $TCP_EVENT_NEWCLIENT, $sFunction)
EndFunc   ;==>_TCP_Server_OnNewClient

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Server_OnReceive
; Description ...: Register callback when server receives data from client
; Syntax ........: _TCP_Server_OnReceive($hServerSocket, $sFunction)
; Parameters ....: $hServerSocket - Server socket handle
;                  $sFunction     - Callback function name: Func OnReceive($hClientSocket, $sData, $iError)
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; Example .......: _TCP_Server_OnReceive($hServer, "OnReceive")
;                  Func OnReceive($hClient, $sData, $iError)
;                      If Not $iError Then _TCP_Send($hClient, "Echo: " & $sData)
;                  EndFunc
; ===============================================================================================================================
Func _TCP_Server_OnReceive($hServerSocket, $sFunction)
    Return _TCP_RegisterEvent($hServerSocket, $TCP_EVENT_RECEIVE, $sFunction)
EndFunc   ;==>_TCP_Server_OnReceive

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Server_OnSend
; Description ...: Register callback when server is ready to send data
; Syntax ........: _TCP_Server_OnSend($hServerSocket, $sFunction)
; Parameters ....: $hServerSocket - Server socket handle
;                  $sFunction     - Callback function name: Func OnSend($hClientSocket, $iError)
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; Example .......: _TCP_Server_OnSend($hServer, "OnSend")
;                  Func OnSend($hClient, $iError)
;                      If Not $iError Then ConsoleWrite("Ready to send on: " & $hClient & @CRLF)
;                  EndFunc
; ===============================================================================================================================
Func _TCP_Server_OnSend($hServerSocket, $sFunction)
    Return _TCP_RegisterEvent($hServerSocket, $TCP_EVENT_SEND, $sFunction)
EndFunc   ;==>_TCP_Server_OnSend

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Server_OnDisconnect
; Description ...: Register callback when client disconnects
; Syntax ........: _TCP_Server_OnDisconnect($hServerSocket, $sFunction)
; Parameters ....: $hServerSocket - Server socket handle
;                  $sFunction     - Callback function name: Func OnDisconnect($hClientSocket, $iError)
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; Example .......: _TCP_Server_OnDisconnect($hServer, "OnDisconnect")
;                  Func OnDisconnect($hClient, $iError)
;                      ConsoleWrite("Client disconnected: " & $hClient & @CRLF)
;                  EndFunc
; ===============================================================================================================================
Func _TCP_Server_OnDisconnect($hServerSocket, $sFunction)
    Return _TCP_RegisterEvent($hServerSocket, $TCP_EVENT_DISCONNECT, $sFunction)
EndFunc   ;==>_TCP_Server_OnDisconnect

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Client_OnConnect
; Description ...: Register callback when client connection succeeds/fails
; Syntax ........: _TCP_Client_OnConnect($hClientSocket, $sFunction)
; Parameters ....: $hClientSocket - Client socket handle
;                  $sFunction     - Callback function name: Func OnConnect($hSocket, $iError)
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; Example .......: _TCP_Client_OnConnect($hClient, "OnConnect")
;                  Func OnConnect($hSocket, $iError)
;                      If $iError Then ConsoleWrite("Connect failed: " & $iError & @CRLF)
;                      Else ConsoleWrite("Connected successfully!" & @CRLF)
;                  EndFunc
; ===============================================================================================================================
Func _TCP_Client_OnConnect($hClientSocket, $sFunction)
    Return _TCP_RegisterEvent($hClientSocket, $TCP_EVENT_CONNECT, $sFunction)
EndFunc   ;==>_TCP_Client_OnConnect

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Client_OnReceive
; Description ...: Register callback when client receives data from server
; Syntax ........: _TCP_Client_OnReceive($hClientSocket, $sFunction)
; Parameters ....: $hClientSocket - Client socket handle
;                  $sFunction     - Callback function name: Func OnReceive($hSocket, $sData, $iError)
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; Example .......: _TCP_Client_OnReceive($hClient, "OnReceive")
;                  Func OnReceive($hSocket, $sData, $iError)
;                      If Not $iError Then ConsoleWrite("Received: " & $sData & @CRLF)
;                  EndFunc
; ===============================================================================================================================
Func _TCP_Client_OnReceive($hClientSocket, $sFunction)
    Return _TCP_RegisterEvent($hClientSocket, $TCP_EVENT_RECEIVE, $sFunction)
EndFunc   ;==>_TCP_Client_OnReceive

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Client_OnSend
; Description ...: Register callback when client is ready to send data
; Syntax ........: _TCP_Client_OnSend($hClientSocket, $sFunction)
; Parameters ....: $hClientSocket - Client socket handle
;                  $sFunction     - Callback function name: Func OnSend($hSocket, $iError)
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; Example .......: _TCP_Client_OnSend($hClient, "OnSend")
;                  Func OnSend($hSocket, $iError)
;                      If Not $iError Then ConsoleWrite("Ready to send" & @CRLF)
;                  EndFunc
; ===============================================================================================================================
Func _TCP_Client_OnSend($hClientSocket, $sFunction)
    Return _TCP_RegisterEvent($hClientSocket, $TCP_EVENT_SEND, $sFunction)
EndFunc   ;==>_TCP_Client_OnSend

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Client_OnDisconnect
; Description ...: Register callback when client disconnects
; Syntax ........: _TCP_Client_OnDisconnect($hClientSocket, $sFunction)
; Parameters ....: $hClientSocket - Client socket handle
;                  $sFunction     - Callback function name: Func OnDisconnect($hSocket, $iError)
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; Example .......: _TCP_Client_OnDisconnect($hClient, "OnDisconnect")
;                  Func OnDisconnect($hSocket, $iError)
;                      ConsoleWrite("Disconnected from server" & @CRLF)
;                  EndFunc
; ===============================================================================================================================
Func _TCP_Client_OnDisconnect($hClientSocket, $sFunction)
    Return _TCP_RegisterEvent($hClientSocket, $TCP_EVENT_DISCONNECT, $sFunction)
EndFunc   ;==>_TCP_Client_OnDisconnect

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_OnConnect
; Description ...: Alias of _TCP_Client_OnConnect for simpler syntax
; Syntax ........: _TCP_OnConnect($hSocket, $sFunction)
; Parameters ....: $hSocket   - Socket handle
;                  $sFunction - Callback function name
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; ===============================================================================================================================
Func _TCP_OnConnect($hSocket, $sFunction)
    Return _TCP_Client_OnConnect($hSocket, $sFunction)
EndFunc   ;==>_TCP_OnConnect

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_OnReceive
; Description ...: Universal callback for both client and server
; Syntax ........: _TCP_OnReceive($hSocket, $sFunction)
; Parameters ....: $hSocket   - Socket handle (client or server)
;                  $sFunction - Callback function name
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; Remarks .......: Can be used for both client and server sockets
; ===============================================================================================================================
Func _TCP_OnReceive($hSocket, $sFunction)
    Return _TCP_RegisterEvent($hSocket, $TCP_EVENT_RECEIVE, $sFunction)
EndFunc   ;==>_TCP_OnReceive

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_OnSend
; Description ...: Universal callback for both client and server
; Syntax ........: _TCP_OnSend($hSocket, $sFunction)
; Parameters ....: $hSocket   - Socket handle (client or server)
;                  $sFunction - Callback function name
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; ===============================================================================================================================
Func _TCP_OnSend($hSocket, $sFunction)
    Return _TCP_RegisterEvent($hSocket, $TCP_EVENT_SEND, $sFunction)
EndFunc   ;==>_TCP_OnSend

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_OnDisconnect
; Description ...: Universal callback for both client and server
; Syntax ........: _TCP_OnDisconnect($hSocket, $sFunction)
; Parameters ....: $hSocket   - Socket handle (client or server)
;                  $sFunction - Callback function name
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; ===============================================================================================================================
Func _TCP_OnDisconnect($hSocket, $sFunction)
    Return _TCP_RegisterEvent($hSocket, $TCP_EVENT_DISCONNECT, $sFunction)
EndFunc   ;==>_TCP_OnDisconnect

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_RegisterEvent
; Description ...: Internal function to register event callbacks
; Syntax ........: _TCP_RegisterEvent($hSocket, $iEvent, $sFunction)
; Parameters ....: $hSocket   - Socket handle
;                  $iEvent    - Event type constant
;                  $sFunction - Callback function name
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; Remarks .......: This is an internal function. Use specific _TCP_*_On* functions instead
; ===============================================================================================================================
Func _TCP_RegisterEvent($hSocket, $iEvent, $sFunction)
    Local $idx = ___TCP_FindSocket($hSocket)
    If $idx < 0 Then Return SetError(-4, 0, 0)

    Switch $iEvent
        Case $TCP_EVENT_SEND
            $__TCP_Sockets[$idx][3] = $sFunction
        Case $TCP_EVENT_RECEIVE
            $__TCP_Sockets[$idx][2] = $sFunction
        Case $TCP_EVENT_CONNECT
            $__TCP_Sockets[$idx][4] = $sFunction
        Case $TCP_EVENT_DISCONNECT
            $__TCP_Sockets[$idx][5] = $sFunction
        Case $TCP_EVENT_NEWCLIENT
            $__TCP_Sockets[$idx][6] = $sFunction
        Case Else
            Return SetError(-4, 0, 0)
    EndSwitch

    Return 1
EndFunc   ;==>_TCP_RegisterEvent

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Server_ClientList
; Description ...: Get list of client sockets
; Syntax ........: _TCP_Server_ClientList()
; Return values .: Array of client sockets, [0] = count
; ===============================================================================================================================
Func _TCP_Server_ClientList()
    Local $aClients[1] = [0]

    For $i = 0 To UBound($__TCP_Sockets) - 1
        If $__TCP_Sockets[$i][0] And Not $__TCP_Sockets[$i][9] Then ; Not a server socket
            ReDim $aClients[UBound($aClients) + 1]
            $aClients[UBound($aClients) - 1] = $__TCP_Sockets[$i][0]
            $aClients[0] += 1
        EndIf
    Next

    Return $aClients
EndFunc   ;==>_TCP_Server_ClientList

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Server_ClientIP
; Description ...: Get IP address of client
; Syntax ........: _TCP_Server_ClientIP($hSocket)
; Parameters ....: $hSocket - Client socket handle
; Return values .: Success - IP address string
;                  Failure - "" and sets @error
; ===============================================================================================================================
Func _TCP_Server_ClientIP($hSocket)
    Local $tSockAddr = DllStructCreate("short;ushort;uint;char[8]")
    Local $aRet = DllCall($__TCP_hWs2_32, "int", "getpeername", "int", $hSocket, "ptr", DllStructGetPtr($tSockAddr), "int*", DllStructGetSize($tSockAddr))
    If @error Or $aRet[0] <> 0 Then Return SetError(-1, 0, "")

    $aRet = DllCall($__TCP_hWs2_32, "str", "inet_ntoa", "int", DllStructGetData($tSockAddr, 3))
    If @error Then Return SetError(-1, 0, "")

    Return $aRet[0]
EndFunc   ;==>_TCP_Server_ClientIP

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Server_Broadcast
; Description ...: Send data to all clients
; Syntax ........: _TCP_Server_Broadcast($vData)
; Parameters ....: $vData - Data to broadcast
; Return values .: Number of clients sent to
; ===============================================================================================================================
Func _TCP_Server_Broadcast($vData)
    Local $iCount = 0

    For $i = 0 To UBound($__TCP_Sockets) - 1
        If $__TCP_Sockets[$i][0] And Not $__TCP_Sockets[$i][9] Then
            TCPSend($__TCP_Sockets[$i][0], $vData)
            $iCount += 1
        EndIf
    Next

    Return $iCount
EndFunc   ;==>_TCP_Server_Broadcast

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Server_DisconnectClient
; Description ...: Disconnect a client
; Syntax ........: _TCP_Server_DisconnectClient($hSocket)
; Parameters ....: $hSocket - Client socket handle
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; ===============================================================================================================================
Func _TCP_Server_DisconnectClient($hSocket)
    Return _TCP_Client_Stop($hSocket)
EndFunc   ;==>_TCP_Server_DisconnectClient

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_NameToIP
; Description ...: Convert hostname to IP address
; Syntax ........: _TCP_NameToIP($sName)
; Parameters ....: $sName - Hostname
; Return values .: Success - IP address
;                  Failure - "" and sets @error
; ===============================================================================================================================
Func _TCP_NameToIP($sName)
    Local $sIP = TCPNameToIP($sName)
    Return SetError(@error, 0, $sIP)
EndFunc   ;==>_TCP_NameToIP

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_GetIPs
; Description ...: Get local and public IP addresses
; Syntax ........: _TCP_GetIPs([$sServerName = ""])
; Parameters ....: $sServerName - [optional] Server to check public IP
; Return values .: Success - Array [localIP, publicIP]
;                  Failure - -1 and sets @error
; ===============================================================================================================================
Func _TCP_GetIPs($sServerName = "")
    If Not $__TCP_Initialized Then
        If Not _TCP_Startup() Then Return SetError(@error, 0, -1)
    EndIf

    Local $aServers[][2] = [["www.myexternalip.com/raw"], ["checkip.dyndns.org/"], ["bot.whatismyipaddress.com/"]]
    If $sServerName <> "" Then
        ReDim $aServers[UBound($aServers) + 1][2]
        $aServers[UBound($aServers) - 1][0] = $sServerName
    EndIf

    Local $sLocalIP = "", $sPublicIP = ""
    Local $hSocket = ___TCP_Socket()
    If @error Then Return SetError(@error, 0, -1)

    For $i = 0 To UBound($aServers) - 1
        Local $sHost = StringRegExp($aServers[$i][0], "^([^/]+)", 1)
        If @error Then ContinueLoop
        $sHost = $sHost[0]

        Local $sServerIP = TCPNameToIP($sHost)
        If $sServerIP = "" Then ContinueLoop

        ; Connect
        Local $tSockAddr = ___TCP_SockAddr($sServerIP, 80)
        If @error Then ContinueLoop

        ___TCP_SetNonBlocking($hSocket)
        DllCall($__TCP_hWs2_32, "int", "connect", "uint", $hSocket, "ptr", DllStructGetPtr($tSockAddr), "int", DllStructGetSize($tSockAddr))

        ; Wait for connection
        If Not ___TCP_WaitForConnect($hSocket, 2000) Then
            ___TCP_CloseSocket($hSocket)
            $hSocket = ___TCP_Socket()
            ContinueLoop
        EndIf

        ; Get local IP
        If $sLocalIP = "" Then
            Local $tLocalAddr = DllStructCreate("short;ushort;uint;char[8]")
            Local $aRet = DllCall($__TCP_hWs2_32, "int", "getsockname", "uint", $hSocket, "ptr", DllStructGetPtr($tLocalAddr), "int*", DllStructGetSize($tLocalAddr))
            If Not @error And $aRet[0] = 0 Then
                $aRet = DllCall($__TCP_hWs2_32, "ptr", "inet_ntoa", "ulong", DllStructGetData($tLocalAddr, 3))
                If Not @error And $aRet[0] <> Null Then
                    $sLocalIP = DllStructGetData(DllStructCreate("char[15]", $aRet[0]), 1)
                EndIf
            EndIf
        EndIf

        ; Get public IP
        Local $sRequest = "GET /" & StringRegExpReplace($aServers[$i][0], "^[^/]+/?", "") & " HTTP/1.1" & @CRLF & _
                "Host: " & $sHost & @CRLF & _
                "Connection: close" & @CRLF & @CRLF

        TCPSend($hSocket, $sRequest)
        Sleep(500)

        Local $sRecv = ""
        Local $hTimer = TimerInit()
        While TimerDiff($hTimer) < 3000
            Local $sChunk = _TCP_Recv($hSocket, 2048)
            If @extended = 1 Then ExitLoop ; Connection closed
            $sRecv &= $sChunk
            If $sChunk = "" Then Sleep(10)
        WEnd

        Local $aIP = StringRegExp($sRecv, "((?:\d{1,3}\.){3}\d{1,3})", 3)
        If Not @error And UBound($aIP) > 0 Then
            $sPublicIP = $aIP[0]
            ExitLoop
        EndIf

        ___TCP_CloseSocket($hSocket)
        $hSocket = ___TCP_Socket()
    Next

    ___TCP_CloseSocket($hSocket)

    If $sLocalIP = "" Or $sPublicIP = "" Then Return SetError(-6, 0, -1)

    Local $aResult[2] = [$sLocalIP, $sPublicIP]
    Return $aResult
EndFunc   ;==>_TCP_GetIPs

; #UDP FUNCTIONS# ===============================================================================================================

; #FUNCTION# ====================================================================================================================
; Name ..........: _UDP_Startup
; Description ...: Initialize UDP system
; Syntax ........: _UDP_Startup()
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; ===============================================================================================================================
Func _UDP_Startup()
    Local $iResult = UDPStartup()
    Return SetError(@error, 0, $iResult)
EndFunc   ;==>_UDP_Startup

; #FUNCTION# ====================================================================================================================
; Name ..........: _UDP_Shutdown
; Description ...: Shutdown UDP system
; Syntax ........: _UDP_Shutdown()
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; ===============================================================================================================================
Func _UDP_Shutdown()
    Local $iResult = UDPShutdown()
    Return SetError(@error, 0, $iResult)
EndFunc   ;==>_UDP_Shutdown

; #FUNCTION# ====================================================================================================================
; Name ..........: _UDP_Bind
; Description ...: Create and bind UDP socket
; Syntax ........: _UDP_Bind([$sSourceIP = ""[, $iSourcePort = 0]])
; Parameters ....: $sSourceIP   - [optional] Local IP to bind (default = "")
;                  $iSourcePort - [optional] Local port to bind (default = 0)
; Return values .: Success - Socket handle
;                  Failure - -1 and sets @error
; ===============================================================================================================================
Func _UDP_Bind($sSourceIP = "", $iSourcePort = 0)
    If Not $__TCP_Initialized Then
        If Not _TCP_Startup() Then Return SetError(@error, 0, -1)
    EndIf

    If Not ($iSourcePort >= 0 And $iSourcePort < 65535) Then Return SetError(-4, 0, -1)

    Local $aRet = DllCall($__TCP_hWs2_32, "uint", "socket", "int", 2, "int", 2, "int", 17) ; AF_INET, SOCK_DGRAM, IPPROTO_UDP
    If @error Then Return SetError(-1, 0, -1)
    If $aRet[0] = 4294967295 Or $aRet[0] = -1 Then Return SetError(-1, ___TCP_WSAGetLastError(), -1)

    Local $hSocket = $aRet[0]

    ; Bind if IP or port specified
    If $sSourceIP <> "" Or $iSourcePort > 0 Then
        Local $tSockAddr = ___TCP_SockAddr($sSourceIP <> "" ? $sSourceIP : "0.0.0.0", $iSourcePort)
        If @error Then
            ___TCP_CloseSocket($hSocket)
            Return SetError(@error, 0, -1)
        EndIf

        $aRet = DllCall($__TCP_hWs2_32, "int", "bind", "uint", $hSocket, "ptr", DllStructGetPtr($tSockAddr), "int", DllStructGetSize($tSockAddr))
        If @error Or $aRet[0] <> 0 Then
            ___TCP_CloseSocket($hSocket)
            Return SetError(-1, ___TCP_WSAGetLastError(), -1)
        EndIf
    EndIf

    Return $hSocket
EndFunc   ;==>_UDP_Bind

; #FUNCTION# ====================================================================================================================
; Name ..........: _UDP_SendTo
; Description ...: Send UDP packet to specified address
; Syntax ........: _UDP_SendTo($sIP, $iPort, $vData[, $hSocket = 0])
; Parameters ....: $sIP     - Destination IP address
;                  $iPort   - Destination port
;                  $vData   - Data to send (string or binary)
;                  $hSocket - [optional] Socket handle (0 = create temporary socket)
; Return values .: Success - Array [bytes_sent, socket_handle]
;                  Failure - -1 and sets @error
; ===============================================================================================================================
Func _UDP_SendTo($sIP, $iPort, $vData, $hSocket = 0)
    If Not $__TCP_Initialized Then
        If Not _TCP_Startup() Then Return SetError(@error, 0, -1)
    EndIf

    If Not ($iPort > 0 And $iPort < 65535) Then Return SetError(-4, 0, -1)

    Local $bCloseAfter = False
    If $hSocket = 0 Then
        $hSocket = _UDP_Bind()
        If @error Then Return SetError(@error, 0, -1)
        $bCloseAfter = True
    EndIf

    Local $tSockAddr = ___TCP_SockAddr($sIP, $iPort)
    If @error Then
        If $bCloseAfter Then ___TCP_CloseSocket($hSocket)
        Return SetError(@error, 0, -1)
    EndIf

    Local $tBuf, $iLen
    If IsBinary($vData) Then
        $iLen = BinaryLen($vData)
        $tBuf = DllStructCreate("byte[" & $iLen & "]")
        DllStructSetData($tBuf, 1, $vData)
    Else
        $vData = String($vData)
        $iLen = StringLen($vData)
        $tBuf = DllStructCreate("char[" & $iLen & "]")
        DllStructSetData($tBuf, 1, $vData)
    EndIf

    ___TCP_SetNonBlocking($hSocket)

    Local $aRet = DllCall($__TCP_hWs2_32, "int", "sendto", _
            "uint", $hSocket, "ptr", DllStructGetPtr($tBuf), "int", $iLen, "int", 0, _
            "ptr", DllStructGetPtr($tSockAddr), "int", DllStructGetSize($tSockAddr))

    If @error Or $aRet[0] = -1 Or $aRet[0] = 4294967295 Then
        Local $iError = ___TCP_WSAGetLastError()
        If $bCloseAfter Then ___TCP_CloseSocket($hSocket)
        Return SetError($iError, 0, -1)
    EndIf

    Local $aResult[2] = [$aRet[0], $hSocket]
    If $bCloseAfter Then ___TCP_CloseSocket($hSocket)

    Return $aResult
EndFunc   ;==>_UDP_SendTo

; #FUNCTION# ====================================================================================================================
; Name ..........: _UDP_RecvFrom
; Description ...: Receive UDP packet
; Syntax ........: _UDP_RecvFrom($hSocket, $iMaxLen[, $iFlag = 0])
; Parameters ....: $hSocket - Socket handle
;                  $iMaxLen - Max bytes to receive
;                  $iFlag   - [optional] 0 = string, 1 = binary
; Return values .: Success - Array [data, source_ip, source_port]
;                  Failure - -1 and sets @error
; ===============================================================================================================================
Func _UDP_RecvFrom($hSocket, $iMaxLen, $iFlag = 0)
    If Not $__TCP_Initialized Then Return SetError(-1, 0, -1)
    If $iMaxLen < 1 Then Return SetError(-4, 0, -1)
    If $iFlag <> 0 And $iFlag <> 1 Then Return SetError(-4, 0, -1)

    ___TCP_SetNonBlocking($hSocket)

    Local $tSockAddr = DllStructCreate("short;ushort;uint;char[8]")
    Local $tBuf
    If $iFlag = 1 Then
        $tBuf = DllStructCreate("byte[" & $iMaxLen & "]")
    Else
        $tBuf = DllStructCreate("char[" & $iMaxLen & "]")
    EndIf

    Local $aRet = DllCall($__TCP_hWs2_32, "int", "recvfrom", _
            "uint", $hSocket, "ptr", DllStructGetPtr($tBuf), "int", $iMaxLen, "int", 0, _
            "ptr", DllStructGetPtr($tSockAddr), "int*", DllStructGetSize($tSockAddr))

    If @error Then Return SetError(-1, 0, -1)

    If $aRet[0] = -1 Or $aRet[0] = 4294967295 Then
        Local $iError = ___TCP_WSAGetLastError()
        If $iError = 0 Or $iError = 10035 Then Return SetError(0, 0, -1) ; WSAEWOULDBLOCK
        Return SetError($iError, 0, -1)
    EndIf

    Local $aResult[3]
    $aResult[0] = DllStructGetData($tBuf, 1)

    $aRet = DllCall($__TCP_hWs2_32, "ptr", "inet_ntoa", "ulong", DllStructGetData($tSockAddr, 3))
    If @error Or $aRet[0] = Null Then Return SetError(-1, 0, -1)

    $aResult[1] = DllStructGetData(DllStructCreate("char[15]", $aRet[0]), 1)

    $aRet = DllCall($__TCP_hWs2_32, "ushort", "ntohs", "ushort", DllStructGetData($tSockAddr, 2))
    If @error Then Return SetError(-1, 0, -1)

    $aResult[2] = $aRet[0]

    Return $aResult
EndFunc   ;==>_UDP_RecvFrom

; #FUNCTION# ====================================================================================================================
; Name ..........: _UDP_CloseSocket
; Description ...: Close UDP socket
; Syntax ........: _UDP_CloseSocket($vSocket)
; Parameters ....: $vSocket - Socket handle or array from _UDP_SendTo
; Return values .: Success - 1
;                  Failure - 0 and sets @error
; ===============================================================================================================================
Func _UDP_CloseSocket($vSocket)
    Local $hSocket
    If IsArray($vSocket) And UBound($vSocket) = 2 Then
        $hSocket = $vSocket[1]
    Else
        $hSocket = $vSocket
    EndIf

    If $hSocket < 1 Then Return SetError(-4, 0, 0)

    Local $aRet = DllCall($__TCP_hWs2_32, "int", "closesocket", "uint", $hSocket)
    If @error Or $aRet[0] <> 0 Then
        Return SetError(___TCP_WSAGetLastError(), 0, 0)
    EndIf

    Return 1
EndFunc   ;==>_UDP_CloseSocket


; #INTERNAL FUNCTIONS# ==========================================================================================================

Func ___TCP_Socket()
    Local $aRet = DllCall($__TCP_hWs2_32, "uint", "socket", "int", 2, "int", 1, "int", 6) ; AF_INET, SOCK_STREAM, IPPROTO_TCP
    If @error Then Return SetError(-1, 0, -1)
    If $aRet[0] = 4294967295 Or $aRet[0] = -1 Then Return SetError(-1, ___TCP_WSAGetLastError(), -1)
    Return $aRet[0]
EndFunc   ;==>___TCP_Socket

Func ___TCP_CloseSocket($hSocket)
    DllCall($__TCP_hWs2_32, "int", "closesocket", "uint", $hSocket)
    Return TCPCloseSocket($hSocket)
EndFunc   ;==>___TCP_CloseSocket

Func ___TCP_Shutdown($hSocket)
    DllCall($__TCP_hWs2_32, "int", "shutdown", "uint", $hSocket, "int", 2) ; SD_BOTH
EndFunc   ;==>___TCP_Shutdown

Func ___TCP_SockAddr($sIP, $iPort)
    Local $tAddr = DllStructCreate("short sin_family;ushort sin_port;uint S_addr;char sin_zero[8]")
    If @error Then Return SetError(-1, 0, False)

    DllStructSetData($tAddr, "sin_family", 2) ; AF_INET

    Local $aRet = DllCall($__TCP_hWs2_32, "ushort", "htons", "ushort", $iPort)
    If @error Then Return SetError(-1, 0, False)
    DllStructSetData($tAddr, "sin_port", $aRet[0])

    If $sIP = "" Or $sIP = "0.0.0.0" Then
        DllStructSetData($tAddr, "S_addr", 0x00000000)
    Else
        $aRet = DllCall($__TCP_hWs2_32, "ulong", "inet_addr", "str", $sIP)
        If @error Or $aRet[0] = -1 Or $aRet[0] = 4294967295 Then Return SetError(-4, 0, False)
        DllStructSetData($tAddr, "S_addr", $aRet[0])
    EndIf

    Return $tAddr
EndFunc   ;==>___TCP_SockAddr

Func ___TCP_SetNonBlocking($hSocket)
    Local $aRet = DllCall($__TCP_hWs2_32, "int", "ioctlsocket", "uint", $hSocket, "long", 0x8004667e, "ulong*", 1) ; FIONBIO
    If @error Or $aRet[0] <> 0 Then Return SetError(-1, ___TCP_WSAGetLastError(), False)
    Return True
EndFunc   ;==>___TCP_SetNonBlocking

Func ___TCP_AsyncSelect($hSocket, $hWnd, $iMsg, $iEvents)
    ; WSAAsyncSelect automatically sets socket to non-blocking mode
    Local $aRet = DllCall($__TCP_hWs2_32, "int", "WSAAsyncSelect", _
            "uint", $hSocket, "hwnd", $hWnd, "uint", $iMsg, "int", $iEvents)
    If @error Or $aRet[0] <> 0 Then Return SetError(-1, ___TCP_WSAGetLastError(), False)
    Return True
EndFunc   ;==>___TCP_AsyncSelect

Func ___TCP_WaitForConnect($hSocket, $iTimeout)
    Local $tFdWrite = DllStructCreate("uint fd_count;uint fd_array[64]")
    Local $tFdExcept = DllStructCreate("uint fd_count;uint fd_array[64]")
    Local $tTimeval = DllStructCreate("long tv_sec;long tv_usec")

    DllStructSetData($tFdWrite, "fd_count", 1)
    DllStructSetData($tFdWrite, "fd_array", $hSocket, 1)
    DllStructSetData($tFdExcept, "fd_count", 1)
    DllStructSetData($tFdExcept, "fd_array", $hSocket, 1)
    DllStructSetData($tTimeval, "tv_sec", Floor($iTimeout / 1000))
    DllStructSetData($tTimeval, "tv_usec", Mod($iTimeout, 1000) * 1000)

    Local $aRet = DllCall($__TCP_hWs2_32, "int", "select", _
            "int", 0, "ptr", 0, "ptr", DllStructGetPtr($tFdWrite), _
            "ptr", DllStructGetPtr($tFdExcept), "ptr", DllStructGetPtr($tTimeval))

    If @error Then Return SetError(-1, 0, False)
    If $aRet[0] = 0 Then Return SetError(-6, 0, False) ; Timeout
    If $aRet[0] = -1 Then Return SetError(-1, ___TCP_WSAGetLastError(), False)

    ; Check if connected or error
    If DllStructGetData($tFdWrite, "fd_count") = 1 Then Return True

    If DllStructGetData($tFdExcept, "fd_count") = 1 Then
        Local $tErr = DllStructCreate("int")
        $aRet = DllCall($__TCP_hWs2_32, "int", "getsockopt", _
                "uint", $hSocket, "int", 0xFFFF, "int", 0x1007, _
                "ptr", DllStructGetPtr($tErr), "int*", DllStructGetSize($tErr)) ; SOL_SOCKET, SO_ERROR
        If Not @error And $aRet[0] = 0 Then
            Return SetError(DllStructGetData($tErr, 1), 0, False)
        EndIf
    EndIf

    Return SetError(-5, 0, False) ; Not connected
EndFunc   ;==>___TCP_WaitForConnect

Func ___TCP_WSAGetLastError()
    Local $aRet = DllCall($__TCP_hWs2_32, "int", "WSAGetLastError")
    If @error Then Return 0
    Return $aRet[0]
EndFunc   ;==>___TCP_WSAGetLastError

Func ___TCP_FindSocket($hSocket)
    For $i = 0 To UBound($__TCP_Sockets) - 1
        If $__TCP_Sockets[$i][0] = $hSocket Then Return $i
    Next
    Return -1
EndFunc   ;==>___TCP_FindSocket

Func ___TCP_LoWord($iValue)
    Return BitAND($iValue, 0xFFFF)
EndFunc   ;==>___TCP_LoWord

Func ___TCP_HiWord($iValue)
    Return BitShift($iValue, 16)
EndFunc   ;==>___TCP_HiWord

Func ___TCP_ArrayDelete(ByRef $aArray, $iElement)
    If Not IsArray($aArray) Then Return SetError(1, 0, 0)
    Local $iUBound = UBound($aArray, 1) - 1
    If $iUBound < 0 Then Return 0

    If $iElement < 0 Then $iElement = 0
    If $iElement > $iUBound Then $iElement = $iUBound

    Switch UBound($aArray, 0)
        Case 1
            For $i = $iElement To $iUBound - 1
                $aArray[$i] = $aArray[$i + 1]
            Next
            ReDim $aArray[$iUBound]
        Case 2
            Local $iSubMax = UBound($aArray, 2) - 1
            For $i = $iElement To $iUBound - 1
                For $j = 0 To $iSubMax
                    $aArray[$i][$j] = $aArray[$i + 1][$j]
                Next
            Next
            ReDim $aArray[$iUBound][$iSubMax + 1]
        Case Else
            Return SetError(3, 0, 0)
    EndSwitch

    Return $iUBound
EndFunc   ;==>___TCP_ArrayDelete

; #ASYNC EVENT HANDLERS# ========================================================================================================

Func ___TCP_Server_OnAccept($hWnd, $iMsgID, $wParam, $lParam)
    Local $hSocket = $wParam
    Local $iError = ___TCP_HiWord($lParam)
    Local $iEvent = ___TCP_LoWord($lParam)

    Abs($hWnd) ; Suppress AU3Check warning

    ; Find server socket
    Local $iServerIdx = -1
    For $i = 0 To UBound($__TCP_Sockets) - 1
        If $__TCP_Sockets[$i][1] = $iMsgID Then
            $iServerIdx = $i
            ExitLoop
        EndIf
    Next

    If $iServerIdx < 0 Then Return

    If $iEvent = $FD_ACCEPT Then
        If Not $iError Then
            Local $hClient = TCPAccept($hSocket)
            If $hClient < 0 Then Return

            ; Setup client async
            Local $iClientMsgID = 0x0400 + UBound($__TCP_Sockets)
            ___TCP_AsyncSelect($hClient, $__TCP_AsyncWindow, $iClientMsgID, BitOR($FD_READ, $FD_WRITE, $FD_CLOSE))
            GUIRegisterMsg($iClientMsgID, "___TCP_Server_OnClientEvent")

            ; Store client
            ReDim $__TCP_Sockets[UBound($__TCP_Sockets) + 1][11]
            Local $idx = UBound($__TCP_Sockets) - 1
            $__TCP_Sockets[$idx][0] = $hClient
            $__TCP_Sockets[$idx][1] = $iClientMsgID
            $__TCP_Sockets[$idx][7] = _TCP_Server_ClientIP($hClient)
            $__TCP_Sockets[$idx][9] = False ; isClient
            $__TCP_Sockets[$idx][10] = $hSocket ; parentServer

            ; Call callback
            If $__TCP_Sockets[$iServerIdx][6] <> "" Then
                Call($__TCP_Sockets[$iServerIdx][6], $hClient, $iError)
            EndIf
        Else
            ; Error accepting
            If $__TCP_Sockets[$iServerIdx][6] <> "" Then
                Call($__TCP_Sockets[$iServerIdx][6], 0, $iError)
            EndIf
        EndIf
    EndIf
EndFunc   ;==>___TCP_Server_OnAccept

Func ___TCP_Server_OnClientEvent($hWnd, $iMsgID, $wParam, $lParam)
    Local $hSocket = $wParam
    Local $iError = ___TCP_HiWord($lParam)
    Local $iEvent = ___TCP_LoWord($lParam)

    Abs($hWnd)

    Local $iClientIdx = -1
    For $i = 0 To UBound($__TCP_Sockets) - 1
        If $__TCP_Sockets[$i][1] = $iMsgID Then
            $iClientIdx = $i
            ; BUG FIX: Don't override the actual socket handle from $wParam
            ; $hSocket = $__TCP_Sockets[$i][0]  ; This was causing socket mismatch!
            ExitLoop
        EndIf
    Next

    If $iClientIdx < 0 Then Return

    ; Find parent server for this client - CRITICAL FIX for multi-server support
    Local $iServerIdx = -1
    Local $hParentServer = $__TCP_Sockets[$iClientIdx][10]

    If $hParentServer > 0 Then
        ; Find the parent server by socket handle
        For $i = 0 To UBound($__TCP_Sockets) - 1
            If $__TCP_Sockets[$i][0] = $hParentServer And $__TCP_Sockets[$i][9] Then
                $iServerIdx = $i
                ExitLoop
            EndIf
        Next
    EndIf

    If $iServerIdx < 0 Then Return

    Switch $iEvent
        Case $FD_READ
            Local $sData = TCPRecv($hSocket, 4096)
            If $__TCP_Sockets[$iServerIdx][2] <> "" Then
                Call($__TCP_Sockets[$iServerIdx][2], $hSocket, $sData, $iError)
            EndIf

        Case $FD_WRITE
            If $__TCP_Sockets[$iServerIdx][3] <> "" Then
                Call($__TCP_Sockets[$iServerIdx][3], $hSocket, $iError)
            EndIf

        Case $FD_CLOSE
            If $__TCP_Sockets[$iServerIdx][5] <> "" Then
                Call($__TCP_Sockets[$iServerIdx][5], $hSocket, $iError)
            EndIf

            ___TCP_Shutdown($hSocket)
            ___TCP_CloseSocket($hSocket)
            ___TCP_ArrayDelete($__TCP_Sockets, $iClientIdx)
    EndSwitch
EndFunc   ;==>___TCP_Server_OnClientEvent

Func ___TCP_Client_OnSocketEvent($hWnd, $iMsgID, $wParam, $lParam)
    Local $hSocket = $wParam
    Local $iError = ___TCP_HiWord($lParam)
    Local $iEvent = ___TCP_LoWord($lParam)

    Abs($hWnd)

    Local $iClientIdx = -1
    For $i = 0 To UBound($__TCP_Sockets) - 1
        If $__TCP_Sockets[$i][1] = $iMsgID Then
            $iClientIdx = $i
            ; BUG FIX: Don't override the actual socket handle from $wParam
            ; $hSocket = $__TCP_Sockets[$i][0]  ; This was causing socket mismatch!
            ExitLoop
        EndIf
    Next

    If $iClientIdx < 0 Then Return

    Switch $iEvent
        Case $FD_CONNECT
            ; Connection completed (success or failure)
            If $__TCP_Sockets[$iClientIdx][4] <> "" Then
                Call($__TCP_Sockets[$iClientIdx][4], $hSocket, $iError)
            EndIf

        Case $FD_READ
            Local $sData = TCPRecv($hSocket, 4096)
            If $__TCP_Sockets[$iClientIdx][2] <> "" Then
                Call($__TCP_Sockets[$iClientIdx][2], $hSocket, $sData, $iError)
            EndIf

        Case $FD_WRITE
            If $__TCP_Sockets[$iClientIdx][3] <> "" Then
                Call($__TCP_Sockets[$iClientIdx][3], $hSocket, $iError)
            EndIf

        Case $FD_CLOSE
            If $__TCP_Sockets[$iClientIdx][5] <> "" Then
                Call($__TCP_Sockets[$iClientIdx][5], $hSocket, $iError)
            EndIf

            ___TCP_Shutdown($hSocket)
            ___TCP_CloseSocket($hSocket)
            ___TCP_ArrayDelete($__TCP_Sockets, $iClientIdx)
    EndSwitch
EndFunc   ;==>___TCP_Client_OnSocketEvent



; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Send_Ex
; Description ...: Send data through socket with automatic Base64 encoding
; Syntax ........: _TCP_Send_Ex($hSocket, $vData[, $iFlag = 0])
; Parameters ....: $hSocket - Socket handle
;                  $vData   - Data to send (string or binary) - auto Base64 encoded
;                  $iFlag   - [optional] $TCP_DEFAULT_DATA (0) or $TCP_EOT_DATA (2)
; Return values .: Success - Number of bytes sent
;                  Failure - -1 and sets @error
; ===============================================================================================================================
Func _TCP_Send_Ex($hSocket, $vData, $iFlag = 0)
    ; Auto encode to Base64
    Local $sEncoded = _Base64Encode($vData)
    If @error Then Return SetError(@error, 0, -1)

    If BitAND($iFlag, $TCP_EOT_DATA) Then $sEncoded = $sEncoded & Chr(3)
    Local $iResult = TCPSend($hSocket, $sEncoded)
    Return SetError(@error, 0, $iResult)
EndFunc   ;==>_TCP_Send_Ex

; #FUNCTION# ====================================================================================================================
; Name ..........: _TCP_Recv_Ex
; Description ...: Receive data from socket with automatic Base64 decoding
; Syntax ........: _TCP_Recv_Ex($hSocket, $iMaxLen[, $iFlag = 0])
; Parameters ....: $hSocket - Socket handle
;                  $iMaxLen - Max bytes to receive (after decoding)
;                  $iFlag   - [optional] $TCP_DEFAULT_DATA (0), $TCP_BINARY_DATA (1), or $TCP_EOT_DATA (2)
; Return values .: Success - Data received (decoded from Base64)
;                  Failure - "" and sets @error/@extended
; Remarks .......: @extended = 1 if connection closed, = 2 if EOT reached
;                  Data is automatically decoded from Base64
; ===============================================================================================================================
Func _TCP_Recv_Ex($hSocket, $iMaxLen, $iFlag = 0)
    If Not $__TCP_Initialized Then Return SetError(-1, 0, "")

    ; Calculate buffer size for Base64 (encoded is ~33% larger)
    Local $iBase64MaxLen = Int($iMaxLen * 1.35) + 100

    Local $tBuf = DllStructCreate("char[" & $iBase64MaxLen & "]")

    If Not ___TCP_SetNonBlocking($hSocket) Then Return SetError(@error, 0, "")

    Local $aRet = DllCall($__TCP_hWs2_32, "int", "recv", "uint", $hSocket, "ptr", DllStructGetPtr($tBuf), "int", $iBase64MaxLen, "int", 0)
    If @error Then Return SetError(-1, 0, "")

    If $aRet[0] = -1 Or $aRet[0] = 4294967295 Then
        Local $iError = ___TCP_WSAGetLastError()
        If $iError = 0 Or $iError = 10035 Then Return SetError(0, 0, "") ; WSAEWOULDBLOCK - no data
        Return SetError($iError, 0, "")
    EndIf

    If $aRet[0] = 0 Then Return SetError(0, 1, "") ; Connection closed

    Local $sBase64Result = DllStructGetData($tBuf, 1)

    ; Check for EOT marker
    Local $bHasEOT = False
    If BitAND($iFlag, $TCP_EOT_DATA) Then
        If StringRight($sBase64Result, 1) = Chr(3) Then
            $sBase64Result = StringTrimRight($sBase64Result, 1)
            $bHasEOT = True
        EndIf
    EndIf

    ; Auto decode from Base64
    Local $sDecoded = _Base64Decode($sBase64Result, True)

    ; Convert to binary if requested
    If BitAND($iFlag, $TCP_BINARY_DATA) Then
        $sDecoded = Binary($sDecoded)
    EndIf

    If $bHasEOT Then
        Return SetError(0, 2, $sDecoded) ; EOT reached
    EndIf

    Return SetError(0, 0, $sDecoded)
EndFunc   ;==>_TCP_Recv_Ex

; #FUNCTION# ====================================================================================================================
; Name ..........: _Base64Encode
; Description ...: Encode binary data to Base64 string
; Syntax ........: _Base64Encode($bInput[, $bNoCRLF = True])
; Parameters ....: $bInput   - Binary data to encode (can also accept ASCII string directly)
;                  $bNoCRLF  - [optional] True: no line breaks, False: add CRLF every 76 chars (default = True)
; Return values .: Success - Base64 encoded string
;                  Failure - "" and sets @error
; Remarks .......: For ASCII strings, you can use this function directly. For UTF-8 strings, use _Base64EncodeStr() instead
; Author ........: Dao Van Trong - TRONG.PRO
; ===============================================================================================================================
Func _Base64Encode($bInput, $bNoCRLF = True)
    $bInput = Binary($bInput)
    Local $iFlags = 1 ; CRYPT_STRING_BASE64
    If $bNoCRLF Then $iFlags = 0x40000001 ; CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF
    Local $tInput = DllStructCreate("byte[" & BinaryLen($bInput) & "]")
    DllStructSetData($tInput, 1, $bInput)
    ; Get required buffer size
    Local $aResult = DllCall("Crypt32.dll", "bool", "CryptBinaryToStringW", _
            "struct*", $tInput, _
            "dword", DllStructGetSize($tInput), _
            "dword", $iFlags, _
            "ptr", 0, _
            "dword*", 0)
    If @error Or Not $aResult[0] Then Return SetError(1, 0, "")
    Local $iSize = $aResult[5]
    Local $tOutput = DllStructCreate("wchar[" & $iSize & "]")
    ; Perform encoding
    $aResult = DllCall("Crypt32.dll", "bool", "CryptBinaryToStringW", _
            "struct*", $tInput, _
            "dword", DllStructGetSize($tInput), _
            "dword", $iFlags, _
            "struct*", $tOutput, _
            "dword*", $iSize)
    If @error Or Not $aResult[0] Then Return SetError(2, 0, "")
    Return DllStructGetData($tOutput, 1)
EndFunc   ;==>_Base64Encode

; #FUNCTION# ====================================================================================================================
; Name ..........: _Base64Decode
; Description ...: Decode Base64 string to binary or string
; Syntax ........: _Base64Decode($sInput[, $bReturnBinary = False[, $iDecodeType = 1]])
; Parameters ....: $sInput        - Base64 string to decode
;                  $bReturnBinary - [optional] False: return string, True: return binary (default = False)
;                  $iDecodeType   - [optional] 1: ASCII, 4: UTF-8 (default = 1, only used when $bReturnBinary = False)
; Return values .: Success - Binary data or string
;                  Failure - Binary("") and sets @error
; Remarks .......: For ASCII-only data, use default parameters. For UTF-8 data, use $iDecodeType = 4. For binary data, use $bReturnBinary = True
; Author ........: Dao Van Trong - TRONG.PRO
; ===============================================================================================================================
Func _Base64Decode($sInput, $bReturnBinary = False, $iDecodeType = 1)
    If ($iDecodeType > 4) Or ($iDecodeType < 1) Then $iDecodeType = 4
    ; Remove all whitespace and CRLF
    $sInput = StringRegExpReplace($sInput, "\s", "")
    If ($sInput = "") Then Return SetError(1, 0, ($bReturnBinary ? Binary("") : ""))
    Local $iFlags = 1 ; CRYPT_STRING_BASE64
    ; Get required buffer size
    Local $aResult = DllCall("Crypt32.dll", "bool", "CryptStringToBinaryW", _
            "wstr", $sInput, _
            "dword", StringLen($sInput), _
            "dword", $iFlags, _
            "ptr", 0, _
            "dword*", 0, _
            "ptr", 0, _
            "ptr", 0)
    If @error Or Not $aResult[0] Then Return SetError(2, 0, ($bReturnBinary ? Binary("") : ""))
    Local $iSize = $aResult[5]
    Local $tOutput = DllStructCreate("byte[" & $iSize & "]")
    ; Perform decoding
    $aResult = DllCall("Crypt32.dll", "bool", "CryptStringToBinaryW", _
            "wstr", $sInput, _
            "dword", StringLen($sInput), _
            "dword", $iFlags, _
            "struct*", $tOutput, _
            "dword*", $iSize, _
            "ptr", 0, _
            "ptr", 0)
    If @error Or Not $aResult[0] Then Return SetError(3, 0, ($bReturnBinary ? Binary("") : ""))
    Local $bBinary = DllStructGetData($tOutput, 1)
    If $bReturnBinary Then
        Return $bBinary
    Else
        ; Convert binary to string with specified encoding
        Return BinaryToString($bBinary, $iDecodeType)
    EndIf
EndFunc   ;==>_Base64Decode

; #FUNCTION# ====================================================================================================================
; Name ..........: _Base64EncodeStr
; Description ...: Encode string to Base64
; Syntax ........: _Base64EncodeStr($sInput[, $iEncodeType = 4[, $bNoCRLF = True]])
; Parameters ....: $sInput      - String to encode
;                  $iEncodeType - [optional] 1: ASCII, 4: UTF-8 (default = 4)
;                  $bNoCRLF     - [optional] True: no line breaks, False: add CRLF (default = True)
; Return values .: Success - Base64 encoded string
;                  Failure - "" and sets @error
; Author ........: Dao Van Trong - TRONG.PRO
; ===============================================================================================================================
Func _Base64EncodeStr($sInput, $iEncodeType = 4, $bNoCRLF = True)
    If ($iEncodeType > 4) Or ($iEncodeType < 1) Then $iEncodeType = 4
    Local $bBinary = StringToBinary($sInput, $iEncodeType)
    Return _Base64Encode($bBinary, $bNoCRLF)
EndFunc   ;==>_Base64EncodeStr

; #FUNCTION# ====================================================================================================================
; Name ..........: _Base64DecodeStr
; Description ...: Decode Base64 string to text string
; Syntax ........: _Base64DecodeStr($sInput[, $iDecodeType = 4])
; Parameters ....: $sInput      - Base64 string to decode
;                  $iDecodeType - [optional] 1: ASCII, 4: UTF-8 (default = 4)
; Return values .: Success - Text string
;                  Failure - "" and sets @error
; Author ........: Dao Van Trong - TRONG.PRO
; ===============================================================================================================================
Func _Base64DecodeStr($sInput, $iDecodeType = 4)
    Return _Base64Decode($sInput, False, $iDecodeType)
EndFunc   ;==>_Base64DecodeStr


; #FUNCTION# ====================================================================================================================
; Name ..........: _PathWithSlash
; Description ...: Add single backslash to path if not present
; Syntax ........: _PathWithSlash($sPath)
; Parameters ....: $sPath - Path string
; Return values .: Path with trailing backslash
; Author ........: Dao Van Trong - TRONG.PRO
; ===============================================================================================================================
Func _PathWithSlash($sPath)
    Return _PathRemoveTrail($sPath) & '\'
EndFunc   ;==>_PathWithSlash

; #FUNCTION# ====================================================================================================================
; Name ..........: _PathRemoveTrail
; Description ...: Remove trailing backslashes from path
; Syntax ........: _PathRemoveTrail($sPath)
; Parameters ....: $sPath - Path string
; Return values .: Path without trailing backslashes
; Author ........: Dao Van Trong - TRONG.PRO
; ===============================================================================================================================
Func _PathRemoveTrail($sPath)
    $sPath = StringStripWS($sPath, 3)
    While (StringRight($sPath, 1) == '\')
        $sPath = StringTrimRight($sPath, 1)
    WEnd
    Return $sPath
EndFunc   ;==>_PathRemoveTrail

 

Edited by Trong

Enjoy my work? Buy me a 🍻 or tip via ❤️ PayPal

Posted (edited)

SSS.png

Lab Manager System Example

📋 Overview

The Lab Manager System is a professional client-server application for remote computer administration and monitoring. This production-ready example demonstrates advanced WinSockUDF features including system monitoring, remote control, file transfer, process management, and automatic updates - perfect for computer labs, IT administration, and fleet management.

🎯 Features

Server Features (LabManager_Server.au3)

  • 🖥️ Multi-Computer Management - Monitor and control multiple computers simultaneously
  • 🎮 Process Management - View, kill, and start processes remotely
  • 📁 File Operations - browse, delete files
  • 💻 Remote Command Execution - Run commands on client computers
  • 🔐 User Session Control - Lock, unlock, logoff, reboot, shutdown
  • 📝 Activity Logging - Complete audit trail of all actions
  • 📈 Statistics Dashboard - Client status overview
  • 🎨 Modern GUI - Professional management interface
  • 🔄 Auto-reconnect - Automatic client reconnection handling

Client Features (LabManager_Client.au3)

  • 🔇 Silent Operation - Runs in system tray (no visible window)
  • 🔄 Auto-start - Optionally add to Windows startup
  • 💓 Keep-Alive - Automatic server connection maintenance
  • 📊 System Monitoring - Collect and report system information
  • 🎯 Remote Command Execution - Execute server commands
  • 🔄 Auto-update - Check and install updates automatically
  • 🛡️ Stealth Mode - Minimal resource footprint
  • 📝 Local Logging - Activity log for troubleshooting
  • ⚙️ Configurable - Easy configuration via constants
  • 🔐 Secure Communication - Authentication-based connection

🗂️ File Structure

LabManager_Server.au3   - Management server application
LabManager_Client.au3   - Silent monitoring agent
update.ini              - Update configuration file (optional)
LabManager_Server.db    - Client database (auto-created)
LabManager_Client.log   - Client log file (auto-created)

🔌 Protocol Documentation

Message Format

All commands use pipe-delimited format: COMMAND|param1|param2|...

Client → Server Messages

Command Format Description
HELLO HELLO|computer|user|ip|os|version Client registration
SYSINFO SYSINFO|cpu|ram|disk|uptime System information update
PROCESSLIST PROCESSLIST|count|pid1:name1|... Running processes list
SCREENSHOT SCREENSHOT|base64_data Screen capture (Base64 encoded)
FILELIST FILELIST|count|file1|file2|... Directory listing
FILEDATA FILEDATA|filename|base64_data File download data
CMDRESULT CMDRESULT|output Command execution result
ERROR ERROR|error_message Error notification
PONG PONG Keep-alive response

Server → Client Commands

Command Format Description
GETINFO GETINFO Request system information
GETPROCESSES GETPROCESSES Request process list
KILLPROCESS KILLPROCESS|pid Terminate process
STARTPROCESS STARTPROCESS|path Start application
SCREENSHOT SCREENSHOT Request screen capture
LISTDIR LISTDIR|path List directory contents
UPLOADFILE UPLOADFILE|path|base64_data Upload file to client
DOWNLOADFILE DOWNLOADFILE|path Request file download
DELETEFILE DELETEFILE|path Delete file
RUNCMD RUNCMD|command Execute command
LOCK LOCK Lock workstation
UNLOCK UNLOCK Unlock workstation
LOGOFF LOGOFF Log off user
REBOOT REBOOT Restart computer
SHUTDOWN SHUTDOWN Shut down computer
MESSAGE MESSAGE|text Display message box
PING PING Keep-alive request
UPDATE UPDATE|url Download and install update

🚀 Getting Started

Setting Up the Server

  1. Run the server application:

    ; Open LabManager_Server.au3 and run it
    ; or compile to LabManager_Server.exe
    
  2. Configure server settings (in source):

    Global Const $SERVER_PORT = 8888        ; Listening port
    Global Const $UPDATE_URL = "http://yourserver.com/update.ini"
    
  3. Server interface:

    • Left panel: Connected clients list
    • Right panel: Client details and controls
    • Tabs: System Info, Processes, Files, Remote Control
    • Status bar: Statistics and connection info

Deploying the Client

  1. Configure client settings:

    Global Const $SERVER_IP = "192.168.1.100"   ; Server address
    Global Const $SERVER_PORT = 8888            ; Server port
    Global Const $CLIENT_VERSION = "1.0.0"      ; Version number
    
  2. Compile the client:

    • Use AutoIt3Wrapper with these settings:
    • x64 version for modern systems
    • Optional: Set as administrator
    • Recommended: Add custom icon
  3. Deploy to computers:

    • Copy executable to client computers
    • Run once - it will add itself to startup
    • Client appears in system tray
    • Auto-connects to server

Auto-Startup Configuration

The client automatically adds itself to Windows startup:

; Client adds registry entry for auto-start
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
Name: LabManagerClient
Value: "C:\Path\To\LabManager_Client.exe"

To disable auto-start:

  • Run client and exit from tray icon, or
  • Remove registry entry manually

📝 Usage Examples

Basic Remote Administration

1. Client Connection:

; Client starts and connects
[CLIENT] Client started - Version 1.0.0
[CLIENT] Connecting to 192.168.1.100:8888...
[CLIENT] Connected successfully!

; Server receives connection
[SERVER] New client connected
[SERVER] DESKTOP-ABC123 (User: John) - 192.168.1.50

2. Process Management:

; Switch to Processes tab
; Server requests: GETPROCESSES
; Client sends process list

PID     Name                Memory
1234    chrome.exe          450 MB
5678    notepad.exe         15 MB
9012    explorer.exe        120 MB

; Right-click process → Kill
; Server sends: KILLPROCESS|1234
; Client terminates process

3. Remote Control:

; Remote Control tab
; Click "Lock Workstation"
Server → Client: LOCK
Client: Workstation locked

; Click "Shutdown Computer"
Server → Client: SHUTDOWN
Client: Computer shutting down...

 

🔧 Advanced Features

Automatic Updates

Setup update server:

  1. Create update.ini file:
[UPDATE]
version=1.0.1
url=http://yourserver.com/LabManager_Client_v1.0.1.exe
changelog=Bug fixes and performance improvements
  1. Host on web server

  2. Configure client:

Global Const $UPDATE_URL = "http://yourserver.com/update.ini"
Global Const $UPDATE_CHECK_INTERVAL = 3600000  ; Check every hour

Update process:

Client                          Server
  |                              |
  +-- Check every hour --------->+
  |   GET update.ini             |
  |                              |
  |   Compare versions           |
  |                              |
  +-- If newer version found --->+
  |   Download new executable    |
  |                              |
  |   Replace current file       |
  |                              |
  |   Restart with new version   |

Remote Command Execution

; Server sends command
_TCP_Send($hClient, "RUNCMD|ipconfig /all")

; Client executes
Local $sOutput = _RunDOS("ipconfig /all")

; Client sends result
_TCP_Send($hServer, "CMDRESULT|" & $sOutput)

; Server displays output

System Control Functions

Lock Workstation:

; Uses Windows API
DllCall("user32.dll", "bool", "LockWorkStation")

Logoff User:

; Graceful user logoff
Shutdown(0)  ; AutoIt shutdown function

Reboot Computer:

; System reboot
Shutdown(2)  ; Reboot flag

Shutdown Computer:

; Power off
Shutdown(1)  ; Shutdown flag

📊 Key Functions & Techniques

Server Functions

  • _TCP_Server_Create($iPort) - Start management server
  • _TCP_Server_OnNewClient() - Handle new client connections
  • _TCP_Server_OnReceive() - Process client messages
  • _TCP_Server_Broadcast() - Send to all clients
  • Database functions for client persistence
  • GUI functions for management interface

Client Functions

  • _TCP_Client_Create($sIP, $iPort) - Connect to server
  • _TCP_Client_OnReceive() - Handle server commands
  • GetSystemInfo() - Collect system information
  • CaptureScreen() - Take screenshot
  • GetProcessList() - Enumerate running processes
  • CheckForUpdates() - Auto-update mechanism
  • Tray icon functions for stealth mode

Data Encoding

  • _Base64Encode() / _Base64Decode() - File transfer
  • _TCP_Send_Ex() / _TCP_Recv_Ex() - Binary data handling
  • DllStructCreate() for system structures
  • WinAPI calls for advanced features

🔐 Security Considerations

Current Security Features

  • Server authentication (clients must connect to known server)
  • Input validation on all commands
  • Error handling and logging
  • Automatic reconnection limits

Security Enhancements (Recommended for Production)

  1. Add Encryption:
; Use AES encryption for all communications
; Implement in _TCP_Send and _TCP_Recv wrappers
  1. Authentication:
; Add password/token authentication
; HELLO|computer|user|ip|os|version|AUTH_TOKEN
  1. Certificate Validation:
; Implement SSL/TLS layer
; Validate server certificates
  1. Access Control:
; Role-based permissions
; Audit logging for all actions
  1. Network Security:
  • Use VPN for internet deployment
  • Firewall rules to limit access
  • IP whitelisting for server

🐛 Troubleshooting

Client Issues

Client won't connect:

  • Check server IP and port
  • Verify firewall settings
  • Ensure server is running
  • Check network connectivity

Client not auto-starting:

  • Check registry entry
  • Verify executable path
  • Check user permissions
  • Review Windows startup settings

High CPU/RAM usage:

  • Check update interval settings
  • Review logging configuration
  • Verify no command loops
  • Check for memory leaks

Updates not working:

  • Verify update URL is accessible
  • Check update.ini format
  • Ensure client has write permissions
  • Review client log file

Server Issues

Can't see clients:

  • Check if clients are connected
  • Verify client configuration
  • Review server logs
  • Check network connectivity

File transfer fails:

  • Check file size limits
  • Verify Base64 encoding
  • Ensure adequate bandwidth
  • Review error messages

Screenshots not working:

  • Check GDI+ initialization
  • Verify screen capture permissions
  • Review image encoding
  • Check bandwidth for large images

📈 Performance Characteristics

Benchmarks

  • Connection overhead: ~1-2 KB/s per client (idle)
  • System info update: ~500 bytes per update
  • Process list: ~1-5 KB depending on number of processes
  • Screenshot: ~50-200 KB (JPEG compressed)
  • File transfer: Up to 10 MB/s on local network
  • Max clients: Tested with 50+ simultaneous connections

Resource Usage

Server:

  • RAM: 50-100 MB (depends on client count)
  • CPU: <1% (idle), 5-10% (active management)
  • Network: Scales with client count and activity

Client:

  • RAM: 10-20 MB
  • CPU: <1% (background), 5-15% (when capturing screens)
  • Network: Minimal when idle

🎯 Use Cases

Educational Institutions

  • Computer Lab Management - Monitor student computers
  • Software Deployment - Push updates and applications
  • Security Monitoring - Track computer usage
  • Remote Assistance - Help students with issues

Corporate IT

  • Workstation Management - Monitor employee computers
  • Patch Management - Deploy security updates
  • Asset Tracking - Inventory of installed software
  • Remote Support - Troubleshoot issues remotely

Home & Small Office

  • Family Computer Monitoring - Parental control
  • Multi-PC Management - Manage home network
  • Remote Access - Control computers remotely
  • Backup Verification - Check backup status

System Integrators

  • Client Demonstration - Showcase WinSockUDF capabilities
  • Template Application - Base for custom solutions
  • Learning Tool - Study network programming
  • Proof of Concept - Test remote control features

🔬 Advanced Customization

Adding Custom Commands

Server side:

; Add button to GUI
$btnCustom = GUICtrlCreateButton("Custom Action", 10, 400, 150, 30)

; Handle button click
Case $btnCustom
    _TCP_Send($hSelectedClient, "CUSTOM|parameter1|parameter2")

Client side:

; Add command handler
Case StringLeft($sData, 6) = "CUSTOM"
    Local $aParams = StringSplit($sData, "|", 2)
    ; Execute custom action
    Local $sResult = CustomFunction($aParams[1], $aParams[2])
    ; Send result
    _TCP_Send($g_hSocket, "CMDRESULT|" & $sResult)

Database Integration

Add SQLite for persistent storage:

#include <SQLite.au3>

; Server: Store client history
_SQLite_Startup()
_SQLite_Open("LabManager.db")
_SQLite_Exec(-1, "CREATE TABLE IF NOT EXISTS clients (id, computer, user, last_seen)")

; Log client connections
_SQLite_Exec(-1, "INSERT INTO clients VALUES (NULL, '" & $sComputer & "', '" & $sUser & "', datetime('now'))")

Multi-Server Support

Configure clients for failover:

Global $aServers[3][2] = [["192.168.1.100", 8888], _
                          ["192.168.1.101", 8888], _
                          ["backup.domain.com", 8888]]

Func ConnectToServer()
    For $i = 0 To UBound($aServers) - 1
        $g_hSocket = _TCP_Client_Create($aServers[$i][0], $aServers[$i][1], "", 0, 5000)
        If Not @error Then
            LogAction("Connected to server " & $aServers[$i][0])
            Return True
        EndIf
    Next
    Return False
EndFunc

 

📚 Related Examples

  • TCP Chat System - Full-featured chat room with authentication, private messaging, and user management
  • UDP Chat Application - Lightweight UDP-based chat demonstrating connectionless communication

🎓 Learning Objectives

This example demonstrates:

  1. Client-Server Architecture - Professional multi-tier design
  2. System Programming - Windows API integration
  3. Binary Data Handling - File transfer and screenshots
  4. State Management - Client tracking and persistence
  5. Remote Procedure Calls - Command execution pattern
  6. Auto-update Mechanisms - Software deployment
  7. Stealth Applications - Tray-based operation
  8. Production Patterns - Error handling, logging, recovery

🤝 Contributing

Enhancement ideas:

  1. SSL/TLS Encryption - Secure communications
  2. Video Streaming - Real-time desktop streaming
  3. Chat Integration - Built-in messaging
  4. Scheduled Tasks - Automated command execution
  5. Reporting - Generate usage and activity reports
  6. Plugin System - Extensible command framework
  7. Mobile Client - Android/iOS monitoring apps
  8. Web Interface - Browser-based management

⚖️ Legal & Ethical Considerations

IMPORTANT: This software is intended for legitimate system administration purposes only.

Legal Use Cases

Managing your own computers

Systems you have permission to administer

Educational purposes in controlled environments

Corporate IT with proper authorization

Illegal Use Cases

Unauthorized access to computers

Monitoring without consent

Stealing data or credentials

Installing on public computers

Always obtain proper authorization before deploying monitoring software.

📄 License

This example is provided as part of WinSockUDF for educational and commercial use. Users are responsible for ensuring compliance with all applicable laws and regulations.


Note: This is a powerful administration tool. Use responsibly and ethically. Always obtain proper authorization before deploying to production environments. For enterprise deployment, consider adding encryption, authentication, and comprehensive audit logging.

Server:

#Region ;**** Directives created by AutoIt3Wrapper_GUI ****
#AutoIt3Wrapper_UseX64=y
#AutoIt3Wrapper_Res_requestedExecutionLevel=asInvoker
#EndRegion ;**** Directives created by AutoIt3Wrapper_GUI ****
#EndRegion
; ===== LAB MANAGER SERVER v1.0 by Dao Van Trong - TRONG.PRO =====
; Comprehensive lab computer management system
Opt("TrayMenuMode", 1) ;0=append, 1=no default menu, 2=no automatic check, 4=menuitemID  not return
#include "WinSockUDF.au3"
;~ #include "LabManager_RUDP.au3"
#include <GUIConstantsEx.au3>
#include <GuiListView.au3>
#include <GuiTreeView.au3>
#include <GuiTab.au3>
#include <EditConstants.au3>
#include <WindowsConstants.au3>
#include <StaticConstants.au3>
#include <ButtonConstants.au3>
#include <ScrollBarsConstants.au3>
#include <GuiEdit.au3>

Global $oErrorHandler = ObjEvent("AutoIt.Error", "_COMErrFunc")
Func _COMErrFunc()
    ; Do nothing special, just check @error after suspect functions.
EndFunc   ;==>_COMErrFunc

; Configuration
Global Const $SERVER_PORT = 8888
Global Const $MAX_CLIENTS = 100
Global Const $SERVER_VERSION = "1.0.0"
Global Const $LOG_FILE = @ScriptDir & "\LabManager_Server.log"
FileDelete($LOG_FILE)
; Server state
Global $g_hSocket = 0
Global $g_aClients[1][9] ; [count][socket, pcname, username, ip, osversion, winver, winversion, clientver, last_seen]
$g_aClients[0][0] = 0

; GUI controls
Global $g_hGUI, $g_hListViewClients, $g_hTabControl, $g_hStatus, $g_hOperationStatus
Global $g_hTabInfo, $g_hTabSoftware, $g_hTabProcess, $g_hTabWindow, $g_hTabFile, $g_hTabStartup, $g_hTabService, $g_hTabControl
Global $g_hListSoftware, $g_hListProcess, $g_hListWindow, $g_hListFile, $g_hListStartup, $g_hListService
Global $g_hBtnRefreshSoft, $g_hBtnUninstallSoft, $g_hBtnRefreshProc, $g_hBtnRefreshWin, $g_hBtnRefreshStartup, $g_hBtnDeleteStartup
Global $g_hBtnRefreshService, $g_hBtnStartService, $g_hBtnStopService, $g_hBtnEnableService, $g_hBtnDisableService, $g_hBtnDeleteService
Global $g_hBtnCloseProc, $g_hBtnCloseWin, $g_hBtnShutdown, $g_hBtnReboot, $g_hBtnMessage
Global $g_hInputCmd, $g_hInputWorkDir, $g_hInputParams, $g_hComboShowFlag, $g_hInputTimeout, $g_hRadioRunWithOutput, $g_hRadioRunNoOutput, $g_hRadioRunDirect
Global $g_hBtnRun, $g_hBtnShell
Global $g_hBtnFileRefresh, $g_hBtnFileDelete, $g_hBtnFileRename, $g_hBtnFileRemoveAttrib
Global $g_hComboDrive, $g_hInputPath, $g_hBtnGo
Global $g_hEditLog, $g_iSelectedClient = 0
Global $g_sCurrentPath = "C:\"
Global $g_aClientBuffers[1][2] ; [count][socket, buffer]
$g_aClientBuffers[0][0] = 0

; Sorting state for each ListView
Global $g_iSortCol = -1, $g_bSortAsc = True

; Initialize
_TCP_Startup()
CreateGUI()
AddToStartup()
LogAction("Server starting - Version " & $SERVER_VERSION)
StartServer()

; Main loop
While True
    ; Process ALL GUI messages (including hidden async window)
    Local $aMsg = GUIGetMsg(1)
    If $aMsg[0] <> 0 Then
        Switch $aMsg[0]
            Case $GUI_EVENT_CLOSE
                If $aMsg[1] = $g_hGUI Then
                    Cleanup()
                    Exit
                EndIf

            Case $g_hBtnRefreshSoft
                RequestSoftwareList()

            Case $g_hBtnUninstallSoft
                UninstallSelectedSoftware()

            Case $g_hBtnRefreshProc
                RequestProcessList()

            Case $g_hBtnRefreshWin
                RequestWindowList()

            Case $g_hBtnCloseProc
                CloseSelectedProcess()

            Case $g_hBtnCloseWin
                CloseSelectedWindow()

            Case $g_hBtnShutdown
                ShutdownClient()

            Case $g_hBtnReboot
                RebootClient()

            Case $g_hBtnMessage
                ShowMessageToClient()

            Case $g_hBtnRun
                RunCommandOnClient()

            Case $g_hBtnShell
                ShellExecuteOnClient()

            Case $g_hBtnFileRefresh
                RequestFileList()

            Case $g_hBtnFileDelete
                DeleteSelectedFile()

            Case $g_hBtnFileRename
                RenameSelectedFile()

            Case $g_hBtnFileRemoveAttrib
                RemoveFileAttributes()

            Case $g_hBtnRefreshStartup
                RequestStartupList()

            Case $g_hBtnDeleteStartup
                DeleteSelectedStartup()

            Case $g_hBtnRefreshService
                RequestServiceList()

            Case $g_hBtnStartService
                StartSelectedService()

            Case $g_hBtnStopService
                StopSelectedService()

            Case $g_hBtnEnableService
                EnableSelectedService()

            Case $g_hBtnDisableService
                DisableSelectedService()

            Case $g_hBtnDeleteService
                DeleteSelectedService()

            Case $g_hBtnGo
                $g_sCurrentPath = _NormalizePath(GUICtrlRead($g_hInputPath))
                RequestFileList()

            Case $g_hComboDrive
                ; Drive selection changed
                Local $sDrive = GUICtrlRead($g_hComboDrive)
                If $sDrive <> "" Then
                    $g_sCurrentPath = $sDrive
                    RequestFileList()
                EndIf
        EndSwitch
    EndIf

    ; Check for selected client change
    Local $iSelected = _GUICtrlListView_GetNextItem($g_hListViewClients)
    If $iSelected <> $g_iSelectedClient - 1 Then
        $g_iSelectedClient = $iSelected + 1
        UpdateClientDetails()
    EndIf

    ; Update path input when current path changes
    If GUICtrlRead($g_hInputPath) <> $g_sCurrentPath Then
        GUICtrlSetData($g_hInputPath, $g_sCurrentPath)
    EndIf

    ; Process RUDP
;~  RUDP_CheckRetransmissions()

    Sleep(10)
WEnd

Func CreateGUI()
    $g_hGUI = GUICreate("Lab Manager Server", 1024, 700)

    ; Status bar
    $g_hStatus = GUICtrlCreateLabel("Status: Starting...", 10, 10, 1004, 20)
    GUICtrlSetColor(-1, 0x0000FF)

    ; Operation status bar
    $g_hOperationStatus = GUICtrlCreateLabel("", 10, 580, 1004, 20)
    GUICtrlSetColor(-1, 0x008800)
    GUICtrlSetFont(-1, 9, 600)

    ; Client list
    GUICtrlCreateLabel("Connected Clients:", 10, 40, 200, 20)
    Local $idListViewClients = GUICtrlCreateListView("#|Socket|PC Name|User|IP|OS|Build|WinVersion|ClientVer", 10, 60, 1004, 140, _
            BitOR($LVS_REPORT, $LVS_SHOWSELALWAYS, $LVS_SINGLESEL), _
            BitOR($WS_EX_CLIENTEDGE, $LVS_EX_FULLROWSELECT, $LVS_EX_GRIDLINES))
    $g_hListViewClients = GUICtrlGetHandle($idListViewClients)
    _GUICtrlListView_SetColumnWidth($g_hListViewClients, 0, 35)
    _GUICtrlListView_SetColumnWidth($g_hListViewClients, 1, 60)
    _GUICtrlListView_SetColumnWidth($g_hListViewClients, 2, 110)
    _GUICtrlListView_SetColumnWidth($g_hListViewClients, 3, 90)
    _GUICtrlListView_SetColumnWidth($g_hListViewClients, 4, 105)
    _GUICtrlListView_SetColumnWidth($g_hListViewClients, 5, 170)
    _GUICtrlListView_SetColumnWidth($g_hListViewClients, 6, 70)
    _GUICtrlListView_SetColumnWidth($g_hListViewClients, 7, 90)
    _GUICtrlListView_SetColumnWidth($g_hListViewClients, 8, 65)

    ; Tab control for client details
    $g_hTabControl = GUICtrlCreateTab(10, 210, 1004, 360)

    ; Tab 1: Software
    $g_hTabSoftware = GUICtrlCreateTabItem("Installed Software")
    Local $idListSoftware = GUICtrlCreateListView("Software Name", 20, 240, 980, 260, _
            BitOR($LVS_REPORT, $LVS_SHOWSELALWAYS), _
            BitOR($WS_EX_CLIENTEDGE, $LVS_EX_FULLROWSELECT, $LVS_EX_GRIDLINES))
    $g_hListSoftware = GUICtrlGetHandle($idListSoftware)
    _GUICtrlListView_SetColumnWidth($g_hListSoftware, 0, 950)
    $g_hBtnRefreshSoft = GUICtrlCreateButton("Refresh Software List", 20, 510, 150, 30)
    $g_hBtnUninstallSoft = GUICtrlCreateButton("Uninstall Selected", 180, 510, 130, 30)

    ; Tab 2: Processes
    $g_hTabProcess = GUICtrlCreateTabItem("Running Processes")
    Local $idListProcess = GUICtrlCreateListView("PID|Process Name|User Name", 20, 240, 980, 260, _
            BitOR($LVS_REPORT, $LVS_SHOWSELALWAYS), _
            BitOR($WS_EX_CLIENTEDGE, $LVS_EX_FULLROWSELECT, $LVS_EX_GRIDLINES))
    $g_hListProcess = GUICtrlGetHandle($idListProcess)
    _GUICtrlListView_SetColumnWidth($g_hListProcess, 0, 80)
    _GUICtrlListView_SetColumnWidth($g_hListProcess, 1, 600)
    _GUICtrlListView_SetColumnWidth($g_hListProcess, 2, 250)
    $g_hBtnRefreshProc = GUICtrlCreateButton("Refresh Processes", 20, 510, 150, 30)
    $g_hBtnCloseProc = GUICtrlCreateButton("Close Selected Process", 180, 510, 150, 30)

    ; Tab 3: Windows
    $g_hTabWindow = GUICtrlCreateTabItem("Open Windows")
    Local $idListWindow = GUICtrlCreateListView("Window Title|Handle", 20, 240, 980, 260, _
            BitOR($LVS_REPORT, $LVS_SHOWSELALWAYS), _
            BitOR($WS_EX_CLIENTEDGE, $LVS_EX_FULLROWSELECT, $LVS_EX_GRIDLINES))
    $g_hListWindow = GUICtrlGetHandle($idListWindow)
    _GUICtrlListView_SetColumnWidth($g_hListWindow, 0, 760)
    _GUICtrlListView_SetColumnWidth($g_hListWindow, 1, 170)
    $g_hBtnRefreshWin = GUICtrlCreateButton("Refresh Windows", 20, 510, 150, 30)
    $g_hBtnCloseWin = GUICtrlCreateButton("Close Selected Window", 180, 510, 150, 30)

    ; Tab 4: File Manager
    $g_hTabFile = GUICtrlCreateTabItem("File Manager")
    GUICtrlCreateLabel("Drive:", 30, 250, 40, 20)
    $g_hComboDrive = GUICtrlCreateCombo("", 70, 247, 60, 22, 0x0003) ; CBS_DROPDOWNLIST
    GUICtrlCreateLabel("Path:", 140, 250, 40, 20)
    $g_hInputPath = GUICtrlCreateInput($g_sCurrentPath, 180, 247, 620, 22)
    $g_hBtnGo = GUICtrlCreateButton("Go", 810, 247, 60, 22)
    Local $idListFile = GUICtrlCreateListView("Name|Type|Size|Modified|Attrib", 20, 275, 980, 225, _
            BitOR($LVS_REPORT, $LVS_SHOWSELALWAYS), _
            BitOR($WS_EX_CLIENTEDGE, $LVS_EX_FULLROWSELECT, $LVS_EX_GRIDLINES))
    $g_hListFile = GUICtrlGetHandle($idListFile)
    _GUICtrlListView_SetColumnWidth($g_hListFile, 0, 450)
    _GUICtrlListView_SetColumnWidth($g_hListFile, 1, 70)
    _GUICtrlListView_SetColumnWidth($g_hListFile, 2, 100)
    _GUICtrlListView_SetColumnWidth($g_hListFile, 3, 180)
    _GUICtrlListView_SetColumnWidth($g_hListFile, 4, 70)
    $g_hBtnFileRefresh = GUICtrlCreateButton("Refresh", 20, 510, 80, 30)
    $g_hBtnFileDelete = GUICtrlCreateButton("Delete", 110, 510, 80, 30)
    $g_hBtnFileRename = GUICtrlCreateButton("Rename", 200, 510, 80, 30)
    $g_hBtnFileRemoveAttrib = GUICtrlCreateButton("Remove -RAHS", 290, 510, 100, 30)

    ; Tab 5: Startup Programs
    $g_hTabStartup = GUICtrlCreateTabItem("Startup Programs")
    Local $idListStartup = GUICtrlCreateListView("Name|Type|Location|Command", 20, 240, 980, 260, _
            BitOR($LVS_REPORT, $LVS_SHOWSELALWAYS), _
            BitOR($WS_EX_CLIENTEDGE, $LVS_EX_FULLROWSELECT, $LVS_EX_GRIDLINES))
    $g_hListStartup = GUICtrlGetHandle($idListStartup)
    _GUICtrlListView_SetColumnWidth($g_hListStartup, 0, 200)
    _GUICtrlListView_SetColumnWidth($g_hListStartup, 1, 120)
    _GUICtrlListView_SetColumnWidth($g_hListStartup, 2, 180)
    _GUICtrlListView_SetColumnWidth($g_hListStartup, 3, 430)
    $g_hBtnRefreshStartup = GUICtrlCreateButton("Refresh Startup List", 20, 510, 150, 30)
    $g_hBtnDeleteStartup = GUICtrlCreateButton("Delete Selected Item", 180, 510, 150, 30)

    ; Tab 6: Services
    $g_hTabService = GUICtrlCreateTabItem("Windows Services")
    Local $idListService = GUICtrlCreateListView("Service Name|Display Name|Status|Startup Type", 20, 240, 980, 260, _
            BitOR($LVS_REPORT, $LVS_SHOWSELALWAYS), _
            BitOR($WS_EX_CLIENTEDGE, $LVS_EX_FULLROWSELECT, $LVS_EX_GRIDLINES))
    $g_hListService = GUICtrlGetHandle($idListService)
    _GUICtrlListView_SetColumnWidth($g_hListService, 0, 200)
    _GUICtrlListView_SetColumnWidth($g_hListService, 1, 350)
    _GUICtrlListView_SetColumnWidth($g_hListService, 2, 100)
    _GUICtrlListView_SetColumnWidth($g_hListService, 3, 130)
    $g_hBtnRefreshService = GUICtrlCreateButton("Refresh Services", 20, 510, 120, 30)
    $g_hBtnStartService = GUICtrlCreateButton("Start", 150, 510, 70, 30)
    $g_hBtnStopService = GUICtrlCreateButton("Stop", 230, 510, 70, 30)
    $g_hBtnEnableService = GUICtrlCreateButton("Enable (Auto)", 310, 510, 100, 30)
    $g_hBtnDisableService = GUICtrlCreateButton("Disable", 420, 510, 70, 30)
    $g_hBtnDeleteService = GUICtrlCreateButton("Delete", 500, 510, 70, 30)

    ; Tab 7: Control
    $g_hTabControl = GUICtrlCreateTabItem("System Control")
    GUICtrlCreateLabel("System Control Commands:", 30, 250, 200, 20)
    $g_hBtnShutdown = GUICtrlCreateButton("Shutdown Computer", 30, 275, 150, 30)
    $g_hBtnReboot = GUICtrlCreateButton("Reboot Computer", 190, 275, 150, 30)
    $g_hBtnMessage = GUICtrlCreateButton("Send Message", 350, 275, 120, 30)

    ; Run Command Section (compact layout)
    GUICtrlCreateLabel("Run Command:", 30, 315, 200, 20)
    GUICtrlCreateLabel("Cmd/File:", 30, 340, 70, 20)
    $g_hInputCmd = GUICtrlCreateInput("", 100, 337, 400, 22)
    GUICtrlCreateLabel("Params:", 510, 340, 50, 20)
    $g_hInputParams = GUICtrlCreateInput("", 560, 337, 250, 22)

    GUICtrlCreateLabel("Work Dir:", 30, 367, 70, 20)
    $g_hInputWorkDir = GUICtrlCreateInput("", 100, 364, 400, 22)
    GUICtrlCreateLabel("Show:", 510, 367, 40, 20)
    $g_hComboShowFlag = GUICtrlCreateCombo("", 550, 364, 120, 22, 0x0003)
    GUICtrlSetData($g_hComboShowFlag, "@SW_HIDE|@SW_MINIMIZE|@SW_MAXIMIZE|@SW_SHOW|@SW_SHOWDEFAULT", "@SW_HIDE")
    GUICtrlCreateLabel("Timeout:", 680, 367, 55, 20)
    $g_hInputTimeout = GUICtrlCreateInput("60", 735, 364, 40, 22)
    GUICtrlCreateLabel("(0=inf)", 780, 367, 40, 20)

    ; Run type selection (horizontal)
    GUICtrlCreateLabel("Type:", 30, 394, 40, 20)
    $g_hRadioRunWithOutput = GUICtrlCreateRadio("Get Output", 70, 391, 100, 20)
    GUICtrlSetState($g_hRadioRunWithOutput, $GUI_CHECKED)
    $g_hRadioRunNoOutput = GUICtrlCreateRadio("No Output", 180, 391, 100, 20)
    $g_hRadioRunDirect = GUICtrlCreateRadio("Direct", 290, 391, 80, 20)

    ; Buttons
    $g_hBtnRun = GUICtrlCreateButton("RUN", 400, 387, 100, 28)
    $g_hBtnShell = GUICtrlCreateButton("ShellExecute", 510, 387, 100, 28)

    GUICtrlCreateTabItem("")

    ; Log
    GUICtrlCreateLabel("Server Log:", 10, 605, 100, 20)
    $g_hEditLog = GUICtrlCreateEdit("", 10, 625, 1004, 65, BitOR($ES_READONLY, $WS_VSCROLL, $ES_MULTILINE))

    ; Register message handler for ListView sorting
    GUIRegisterMsg($WM_NOTIFY, "WM_NOTIFY_Events")

    GUISetState(@SW_SHOW)
EndFunc   ;==>CreateGUI

Func WM_NOTIFY_Events($hWnd, $iMsg, $wParam, $lParam)
    #forceref $hWnd, $iMsg, $wParam
    Local $hWndFrom, $iCode, $tNMHDR

    $tNMHDR = DllStructCreate($tagNMHDR, $lParam)
    $hWndFrom = HWnd(DllStructGetData($tNMHDR, "hWndFrom"))
    $iCode = DllStructGetData($tNMHDR, "Code")

    ; Check if it's a ListView header click
    If $iCode = $LVN_COLUMNCLICK Then
        Local $tNMLISTVIEW = DllStructCreate($tagNMLISTVIEW, $lParam)
        Local $iColumn = DllStructGetData($tNMLISTVIEW, "SubItem")

        ; Determine which ListView was clicked
        Local $hListView = 0
        Switch $hWndFrom
            Case $g_hListViewClients
                $hListView = $g_hListViewClients
            Case $g_hListSoftware
                $hListView = $g_hListSoftware
            Case $g_hListProcess
                $hListView = $g_hListProcess
            Case $g_hListWindow
                $hListView = $g_hListWindow
            Case $g_hListFile
                $hListView = $g_hListFile
            Case $g_hListStartup
                $hListView = $g_hListStartup
            Case $g_hListService
                $hListView = $g_hListService
        EndSwitch

        ; Sort the ListView
        If $hListView <> 0 Then
            ; Toggle sort direction if same column, otherwise ascending
            If $g_iSortCol = $iColumn Then
                $g_bSortAsc = Not $g_bSortAsc
            Else
                $g_bSortAsc = True
                $g_iSortCol = $iColumn
            EndIf

            _GUICtrlListView_SimpleSort($hListView, $g_bSortAsc, $iColumn)
        EndIf
    EndIf

    ; Check for double-click on file list
    If $iCode = $NM_DBLCLK And $hWndFrom = $g_hListFile Then
        Local $iItem = _GUICtrlListView_GetNextItem($g_hListFile)
        If $iItem >= 0 Then
            Local $sName = _GUICtrlListView_GetItemText($g_hListFile, $iItem, 0)
            Local $sType = _GUICtrlListView_GetItemText($g_hListFile, $iItem, 1)

            If $sType = "DIR" Then
                ; Browse directory
                If $sName = ".." Then
                    ; Go to parent directory
                    Local $sParent = StringRegExpReplace($g_sCurrentPath, "\\[^\\]*\\?$", "")
                    If StringLen($sParent) < 3 Then $sParent = StringLeft($g_sCurrentPath, 3)
                    $g_sCurrentPath = _NormalizePath($sParent)
                Else
                    ; Enter directory
                    $g_sCurrentPath = _NormalizePath($g_sCurrentPath & $sName)
                EndIf
                RequestFileList()
            Else
                ; ShellExecute file
                Local $sFilePath = $g_sCurrentPath
                If Not StringRight($sFilePath, 1) = "\" Then $sFilePath &= "\"
                $sFilePath &= $sName
                ShellExecuteFile($sFilePath)
            EndIf
        EndIf
    EndIf

    Return $GUI_RUNDEFMSG
EndFunc   ;==>WM_NOTIFY_Events

Func StartServer()
    $g_hSocket = _TCP_Server_Create($SERVER_PORT)
    If $g_hSocket = 0 Then
        MsgBox(16, "Error", "Failed to start server on port " & $SERVER_PORT)
        Exit
    EndIf

    ; Register callbacks
    _TCP_Server_OnNewClient($g_hSocket, "OnNewClient")
    _TCP_Server_OnReceive($g_hSocket, "OnClientReceive")
    _TCP_Server_OnDisconnect($g_hSocket, "OnClientDisconnect")

    Local $sMsg = "Server started on port " & $SERVER_PORT
    AddLog($sMsg)
    LogAction($sMsg)
    GUICtrlSetData($g_hStatus, "Status: Running | Port: " & $SERVER_PORT & " | Clients: 0")
    GUICtrlSetColor($g_hStatus, 0x00AA00)
EndFunc   ;==>StartServer

Func OnNewClient($hClient)
    Local $sMsg = "New client connected: Socket " & Int($hClient)
    AddLog($sMsg)
    LogAction($sMsg)
EndFunc   ;==>OnNewClient

Func OnClientReceive($hClient, $sData, $iError)
    If $iError Then
        AddLog("OnClientReceive ERROR: " & $iError)
        Return
    EndIf

    If $sData = "" Then
        Return
    EndIf

    ; Debug: Log data size for large transfers
    Local $iDataLen = StringLen($sData)
    If $iDataLen > 1000 Then
        AddLog("Received data: " & Round($iDataLen / 1024, 1) & " KB")
    EndIf

    ; Add to buffer
    Local $sBuffer = GetClientBuffer($hClient)
    $sBuffer &= $sData

    ; Process complete messages (ending with @CRLF)
    While StringInStr($sBuffer, @CRLF)
        Local $iPos = StringInStr($sBuffer, @CRLF)
        Local $sMessage = StringLeft($sBuffer, $iPos - 1)
        $sBuffer = StringMid($sBuffer, $iPos + 2)

        ; Process this complete message
        ProcessClientMessage($hClient, $sMessage)
    WEnd

    ; Save remaining buffer
    SetClientBuffer($hClient, $sBuffer)
EndFunc   ;==>OnClientReceive

Func GetClientBuffer($hClient)
    For $i = 1 To $g_aClientBuffers[0][0]
        If $g_aClientBuffers[$i][0] = $hClient Then
            Return $g_aClientBuffers[$i][1]
        EndIf
    Next

    ; Create new buffer
    $g_aClientBuffers[0][0] += 1
    ReDim $g_aClientBuffers[$g_aClientBuffers[0][0] + 1][2]
    $g_aClientBuffers[$g_aClientBuffers[0][0]][0] = $hClient
    $g_aClientBuffers[$g_aClientBuffers[0][0]][1] = ""
    Return ""
EndFunc   ;==>GetClientBuffer

Func SetClientBuffer($hClient, $sBuffer)
    For $i = 1 To $g_aClientBuffers[0][0]
        If $g_aClientBuffers[$i][0] = $hClient Then
            $g_aClientBuffers[$i][1] = $sBuffer
            Return
        EndIf
    Next
EndFunc   ;==>SetClientBuffer

Func ClearClientBuffer($hClient)
    For $i = 1 To $g_aClientBuffers[0][0]
        If $g_aClientBuffers[$i][0] = $hClient Then
            ; Remove from array
            For $j = $i To $g_aClientBuffers[0][0] - 1
                $g_aClientBuffers[$j][0] = $g_aClientBuffers[$j + 1][0]
                $g_aClientBuffers[$j][1] = $g_aClientBuffers[$j + 1][1]
            Next
            $g_aClientBuffers[0][0] -= 1
            ReDim $g_aClientBuffers[$g_aClientBuffers[0][0] + 1][2]
            Return
        EndIf
    Next
EndFunc   ;==>ClearClientBuffer

Func ProcessClientMessage($hClient, $sMessage)
    $sMessage = StringStripWS($sMessage, 3)
    If $sMessage = "" Then Return

    Local $aCmd = StringSplit($sMessage, "|", 2)
    If UBound($aCmd) = 0 Then Return
    Local $sCommand = $aCmd[0]

    ; Log important commands for debugging
    If $sCommand = "SOFTWARE_LIST" Or $sCommand = "PROCESS_LIST" Or $sCommand = "WINDOW_LIST" Or $sCommand = "FILE_LIST_RESULT" Or $sCommand = "FILE_DRIVE_LIST" Then
        AddLog("Processing: " & $sCommand)
    EndIf

    Switch $sCommand
        Case "CLIENT_REGISTER"
            If UBound($aCmd) >= 8 Then
                RegisterClient($hClient, $aCmd[1], $aCmd[2], $aCmd[3], $aCmd[4], $aCmd[5], $aCmd[6], $aCmd[7])
            ElseIf UBound($aCmd) >= 7 Then
                RegisterClient($hClient, $aCmd[1], $aCmd[2], $aCmd[3], $aCmd[4], $aCmd[5], $aCmd[6], "0.0.0")
            ElseIf UBound($aCmd) >= 6 Then
                RegisterClient($hClient, $aCmd[1], $aCmd[2], $aCmd[3], $aCmd[4], $aCmd[5], "", "0.0.0")
            EndIf

        Case "SOFTWARE_LIST"
            DisplaySoftwareList($aCmd)

        Case "PROCESS_LIST"
            DisplayProcessList($aCmd)

        Case "WINDOW_LIST"
            DisplayWindowList($aCmd)

        Case "STARTUP_LIST"
            DisplayStartupList($aCmd)

        Case "STARTUP_DELETED"
            If UBound($aCmd) >= 2 Then
                AddLog("Startup item deleted: " & $aCmd[1])
                RequestStartupList() ; Auto refresh
            EndIf

        Case "STARTUP_DELETE_ERROR"
            If UBound($aCmd) >= 2 Then
                AddLog("Error deleting startup item: " & $aCmd[1])
                MsgBox(16, "Delete Error", "Failed to delete: " & $aCmd[1])
            EndIf

        Case "SERVICE_LIST"
            DisplayServiceList($aCmd)

        Case "SERVICE_STARTED"
            If UBound($aCmd) >= 2 Then
                AddLog("Service started: " & $aCmd[1])
                RequestServiceList() ; Auto refresh
            EndIf

        Case "SERVICE_START_ERROR"
            If UBound($aCmd) >= 2 Then
                AddLog("Error starting service: " & $aCmd[1])
                MsgBox(16, "Service Error", "Failed to start: " & $aCmd[1])
            EndIf

        Case "SERVICE_STOPPED"
            If UBound($aCmd) >= 2 Then
                AddLog("Service stopped: " & $aCmd[1])
                RequestServiceList() ; Auto refresh
            EndIf

        Case "SERVICE_STOP_ERROR"
            If UBound($aCmd) >= 2 Then
                AddLog("Error stopping service: " & $aCmd[1])
                MsgBox(16, "Service Error", "Failed to stop: " & $aCmd[1])
            EndIf

        Case "SERVICE_ENABLED"
            If UBound($aCmd) >= 2 Then
                AddLog("Service enabled: " & $aCmd[1])
                RequestServiceList() ; Auto refresh
            EndIf

        Case "SERVICE_ENABLE_ERROR"
            If UBound($aCmd) >= 2 Then
                AddLog("Error enabling service: " & $aCmd[1])
                MsgBox(16, "Service Error", "Failed to enable: " & $aCmd[1])
            EndIf

        Case "SERVICE_DISABLED"
            If UBound($aCmd) >= 2 Then
                AddLog("Service disabled: " & $aCmd[1])
                RequestServiceList() ; Auto refresh
            EndIf

        Case "SERVICE_DISABLE_ERROR"
            If UBound($aCmd) >= 2 Then
                AddLog("Error disabling service: " & $aCmd[1])
                MsgBox(16, "Service Error", "Failed to disable: " & $aCmd[1])
            EndIf

        Case "SERVICE_DELETED"
            If UBound($aCmd) >= 2 Then
                AddLog("Service deleted: " & $aCmd[1])
                RequestServiceList() ; Auto refresh
            EndIf

        Case "SERVICE_DELETE_ERROR"
            If UBound($aCmd) >= 2 Then
                AddLog("Error deleting service: " & $aCmd[1])
                MsgBox(16, "Service Error", "Failed to delete: " & $aCmd[1])
            EndIf

        Case "SOFTWARE_UNINSTALLED"
            If UBound($aCmd) >= 2 Then
                AddLog("Software uninstalled: " & $aCmd[1])
                RequestSoftwareList() ; Auto refresh
            EndIf

        Case "SOFTWARE_UNINSTALL_ERROR"
            If UBound($aCmd) >= 2 Then
                AddLog("Error uninstalling software: " & $aCmd[1])
                MsgBox(16, "Uninstall Error", "Failed to uninstall: " & $aCmd[1])
            EndIf

        Case "FILE_LIST_RESULT"
            DisplayFileList($aCmd)

        Case "FILE_DRIVE_LIST"
            DisplayDriveList($aCmd)

        Case "FILE_LIST_ERROR"
            If UBound($aCmd) >= 2 Then
                AddLog("File list error: " & $aCmd[1])
            EndIf

        Case "FILE_DELETED"
            If UBound($aCmd) >= 2 Then
                AddLog("File deleted: " & $aCmd[1])
                RequestFileList() ; Auto refresh
            EndIf

        Case "FILE_DELETE_ERROR"
            If UBound($aCmd) >= 2 Then
                AddLog("Error deleting file: " & $aCmd[1])
                MsgBox(16, "Delete Error", "Failed to delete: " & $aCmd[1])
            EndIf

        Case "FILE_RENAMED"
            If UBound($aCmd) >= 3 Then
                AddLog("File renamed: " & $aCmd[1] & " -> " & $aCmd[2])
                RequestFileList() ; Auto refresh
            EndIf

        Case "FILE_RENAME_ERROR"
            If UBound($aCmd) >= 2 Then
                AddLog("Error renaming file: " & $aCmd[1])
                MsgBox(16, "Rename Error", "Failed to rename: " & $aCmd[1])
            EndIf

        Case "FILE_ATTRIB_REMOVED"
            If UBound($aCmd) >= 2 Then
                AddLog("Attributes removed: " & $aCmd[1])
                RequestFileList() ; Auto refresh
            EndIf

        Case "FILE_ATTRIB_ERROR"
            If UBound($aCmd) >= 2 Then
                AddLog("Error removing attributes: " & $aCmd[1])
                MsgBox(16, "Attribute Error", "Failed to remove attributes: " & $aCmd[1])
            EndIf

        Case "FILE_SHELLEXECUTE_DONE"
            If UBound($aCmd) >= 2 Then
                AddLog("File executed: " & $aCmd[1])
            EndIf

        Case "COMMAND_EXECUTED"
            If UBound($aCmd) >= 2 Then
                AddLog("Command executed: " & $aCmd[1])
            EndIf

        Case "COMMAND_OUTPUT"
            If UBound($aCmd) >= 2 Then
                Local $sOutput = $aCmd[1]
                ; Replace Chr(1) separator back to newlines for display
                $sOutput = StringReplace($sOutput, Chr(1), @CRLF)
                MsgBox(64, "Command Output", $sOutput, 0)
                AddLog("Command output received (" & StringLen($sOutput) & " chars)")
            EndIf

        Case "SHELLEXECUTE_DONE"
            If UBound($aCmd) >= 2 Then
                AddLog("ShellExecute done: " & $aCmd[1])
            EndIf

        Case "PROCESS_CLOSED"
            If UBound($aCmd) >= 2 Then
                AddLog("Process closed: " & $aCmd[1])
                RequestProcessList() ; Auto refresh
            EndIf

        Case "WINDOW_CLOSED"
            If UBound($aCmd) >= 2 Then
                AddLog("Window closed: " & $aCmd[1])
                RequestWindowList() ; Auto refresh
            EndIf

        Case "CLIENT_DISCONNECT"
            LogAction("Client requested disconnect: Socket " & Int($hClient))
            RemoveClient($hClient)
    EndSwitch
EndFunc   ;==>ProcessClientMessage

Func OnClientDisconnect($hClient, $iError)
    Local $sMsg = "Client disconnected: Socket " & Int($hClient)
    AddLog($sMsg)
    LogAction($sMsg)
    ClearClientBuffer($hClient)
    RemoveClient($hClient)
EndFunc   ;==>OnClientDisconnect

Func RegisterClient($hClient, $sPCName, $sUserName, $sIP, $sOS, $sWinVer, $sWinVersion, $sClientVer)
    ; Check if already registered
    Local $iIdx = FindClient($hClient)
    If $iIdx > 0 Then
        ; Update existing
        $g_aClients[$iIdx][1] = $sPCName
        $g_aClients[$iIdx][2] = $sUserName
        $g_aClients[$iIdx][3] = $sIP
        $g_aClients[$iIdx][4] = $sOS
        $g_aClients[$iIdx][5] = $sWinVer
        $g_aClients[$iIdx][6] = $sWinVersion
        $g_aClients[$iIdx][7] = $sClientVer
        $g_aClients[$iIdx][8] = TimerInit()
    Else
        ; Add new
        $g_aClients[0][0] += 1
        ReDim $g_aClients[$g_aClients[0][0] + 1][9]
        $g_aClients[$g_aClients[0][0]][0] = $hClient
        $g_aClients[$g_aClients[0][0]][1] = $sPCName
        $g_aClients[$g_aClients[0][0]][2] = $sUserName
        $g_aClients[$g_aClients[0][0]][3] = $sIP
        $g_aClients[$g_aClients[0][0]][4] = $sOS
        $g_aClients[$g_aClients[0][0]][5] = $sWinVer
        $g_aClients[$g_aClients[0][0]][6] = $sWinVersion
        $g_aClients[$g_aClients[0][0]][7] = $sClientVer
        $g_aClients[$g_aClients[0][0]][8] = TimerInit()
    EndIf

    UpdateClientList()
    Local $sMsg = "Client registered: " & $sPCName & " (" & $sUserName & ") - " & $sIP & " - " & $sOS & " " & $sWinVersion & " - v" & $sClientVer
    AddLog($sMsg)
    LogAction($sMsg)
EndFunc   ;==>RegisterClient

Func FindClient($hClient)
    For $i = 1 To $g_aClients[0][0]
        If $g_aClients[$i][0] = $hClient Then Return $i
    Next
    Return 0
EndFunc   ;==>FindClient

Func RemoveClient($hClient)
    Local $iIdx = FindClient($hClient)
    If $iIdx = 0 Then Return

    Local $sMsg = "Client removed: " & $g_aClients[$iIdx][1] & " (Socket " & Int($hClient) & ")"
    LogAction($sMsg)

    ; Shift array
    For $i = $iIdx To $g_aClients[0][0] - 1
        For $j = 0 To 8
            $g_aClients[$i][$j] = $g_aClients[$i + 1][$j]
        Next
    Next

    $g_aClients[0][0] -= 1
    ReDim $g_aClients[$g_aClients[0][0] + 1][9]

    UpdateClientList()
EndFunc   ;==>RemoveClient

Func UpdateClientList()
    _GUICtrlListView_DeleteAllItems($g_hListViewClients)

    For $i = 1 To $g_aClients[0][0]
        _GUICtrlListView_AddItem($g_hListViewClients, $i)
        _GUICtrlListView_AddSubItem($g_hListViewClients, $i - 1, Int($g_aClients[$i][0]), 1)
        _GUICtrlListView_AddSubItem($g_hListViewClients, $i - 1, $g_aClients[$i][1], 2)
        _GUICtrlListView_AddSubItem($g_hListViewClients, $i - 1, $g_aClients[$i][2], 3)
        _GUICtrlListView_AddSubItem($g_hListViewClients, $i - 1, $g_aClients[$i][3], 4)
        _GUICtrlListView_AddSubItem($g_hListViewClients, $i - 1, $g_aClients[$i][4], 5)
        _GUICtrlListView_AddSubItem($g_hListViewClients, $i - 1, $g_aClients[$i][5], 6)
        _GUICtrlListView_AddSubItem($g_hListViewClients, $i - 1, $g_aClients[$i][6], 7)
        _GUICtrlListView_AddSubItem($g_hListViewClients, $i - 1, $g_aClients[$i][7], 8)
    Next

    GUICtrlSetData($g_hStatus, "Status: Running | Port: " & $SERVER_PORT & " | Clients: " & $g_aClients[0][0])
EndFunc   ;==>UpdateClientList

Func UpdateClientDetails()
    ; Clear all lists
    _GUICtrlListView_DeleteAllItems($g_hListSoftware)
    _GUICtrlListView_DeleteAllItems($g_hListProcess)
    _GUICtrlListView_DeleteAllItems($g_hListWindow)
    _GUICtrlListView_DeleteAllItems($g_hListFile)
    _GUICtrlListView_DeleteAllItems($g_hListStartup)
    _GUICtrlListView_DeleteAllItems($g_hListService)

    ; Clear drive combo
    GUICtrlSetData($g_hComboDrive, "")
EndFunc   ;==>UpdateClientDetails

Func RequestSoftwareList()
    If $g_iSelectedClient = 0 Then Return
    LogAction("Requesting software list from " & $g_aClients[$g_iSelectedClient][1])
    UpdateOperationStatus("Sending command: Software list...", 0xFF8800)
    _TCP_Send($g_aClients[$g_iSelectedClient][0], "GET_SOFTWARE_LIST" & @CRLF)
    UpdateOperationStatus("Waiting for response: Software list...", 0x0088FF)
EndFunc   ;==>RequestSoftwareList

Func RequestProcessList()
    If $g_iSelectedClient = 0 Then Return
    LogAction("Requesting process list from " & $g_aClients[$g_iSelectedClient][1])
    UpdateOperationStatus("Sending command: Process list...", 0xFF8800)
    _TCP_Send($g_aClients[$g_iSelectedClient][0], "GET_PROCESS_LIST" & @CRLF)
    UpdateOperationStatus("Waiting for response: Process list...", 0x0088FF)
EndFunc   ;==>RequestProcessList

Func RequestWindowList()
    If $g_iSelectedClient = 0 Then Return
    LogAction("Requesting window list from " & $g_aClients[$g_iSelectedClient][1])
    UpdateOperationStatus("Sending command: Window list...", 0xFF8800)
    _TCP_Send($g_aClients[$g_iSelectedClient][0], "GET_WINDOW_LIST" & @CRLF)
    UpdateOperationStatus("Waiting for response: Window list...", 0x0088FF)
EndFunc   ;==>RequestWindowList

Func CloseSelectedProcess()
    If $g_iSelectedClient = 0 Then Return
    Local $iSelected = _GUICtrlListView_GetNextItem($g_hListProcess)
    If $iSelected >= 0 Then
        Local $sPID = _GUICtrlListView_GetItemText($g_hListProcess, $iSelected, 0) ; PID is now column 0
        Local $sMsg = "ADMIN: Close process on " & $g_aClients[$g_iSelectedClient][1] & ": PID " & $sPID
        LogAction($sMsg)
        _TCP_Send($g_aClients[$g_iSelectedClient][0], "CLOSE_PROCESS|" & $sPID & @CRLF)
        AddLog("Close process requested: PID " & $sPID)
    EndIf
EndFunc   ;==>CloseSelectedProcess

Func CloseSelectedWindow()
    If $g_iSelectedClient = 0 Then Return
    Local $iSelected = _GUICtrlListView_GetNextItem($g_hListWindow)
    If $iSelected >= 0 Then
        Local $sHandle = _GUICtrlListView_GetItemText($g_hListWindow, $iSelected, 1)
        Local $sMsg = "ADMIN: Close window on " & $g_aClients[$g_iSelectedClient][1] & ": " & $sHandle
        LogAction($sMsg)
        _TCP_Send($g_aClients[$g_iSelectedClient][0], "CLOSE_WINDOW|" & $sHandle & @CRLF)
        AddLog("Close window requested: Handle " & $sHandle)
    EndIf
EndFunc   ;==>CloseSelectedWindow

Func ShutdownClient()
    If $g_iSelectedClient = 0 Then Return
    If MsgBox(36, "Confirm", "Shutdown " & $g_aClients[$g_iSelectedClient][1] & "?") = 6 Then
        Local $sMsg = "ADMIN: Shutdown command sent to " & $g_aClients[$g_iSelectedClient][1]
        AddLog($sMsg)
        LogAction($sMsg)
        _TCP_Send($g_aClients[$g_iSelectedClient][0], "SHUTDOWN" & @CRLF)
    EndIf
EndFunc   ;==>ShutdownClient

Func RebootClient()
    If $g_iSelectedClient = 0 Then Return
    If MsgBox(36, "Confirm", "Reboot " & $g_aClients[$g_iSelectedClient][1] & "?") = 6 Then
        Local $sMsg = "ADMIN: Reboot command sent to " & $g_aClients[$g_iSelectedClient][1]
        AddLog($sMsg)
        LogAction($sMsg)
        _TCP_Send($g_aClients[$g_iSelectedClient][0], "REBOOT" & @CRLF)
    EndIf
EndFunc   ;==>RebootClient

Func ShowMessageToClient()
    If $g_iSelectedClient = 0 Then Return
    Local $sTitle = InputBox("Message Title", "Enter message title:", "Lab Manager")
    If @error Then Return
    Local $sMessage = InputBox("Message", "Enter message text:")
    If @error Then Return
    Local $iTimeout = InputBox("Timeout", "Enter timeout (seconds, 0 = no timeout):", "10")
    If @error Then $iTimeout = 0

    Local $sMsg = "ADMIN: Message sent to " & $g_aClients[$g_iSelectedClient][1]
    LogAction($sMsg)
    _TCP_Send($g_aClients[$g_iSelectedClient][0], "SHOW_MESSAGE|" & $sTitle & "|" & $sMessage & "|" & $iTimeout & @CRLF)
    AddLog("Message sent to " & $g_aClients[$g_iSelectedClient][1])
EndFunc   ;==>ShowMessageToClient

Func RunCommandOnClient()
    If $g_iSelectedClient = 0 Then Return
    Local $sCommand = GUICtrlRead($g_hInputCmd)
    If $sCommand = "" Then Return

    Local $sWorkDir = GUICtrlRead($g_hInputWorkDir)
    Local $sShowFlag = GUICtrlRead($g_hComboShowFlag)
    Local $iTimeout = Int(GUICtrlRead($g_hInputTimeout))
    If $iTimeout < 0 Then $iTimeout = 60 ; Default to 60s if invalid

    ; Determine run type
    Local $sRunType = ""
    If GUICtrlRead($g_hRadioRunWithOutput) = $GUI_CHECKED Then
        $sRunType = "WITH_OUTPUT"
    ElseIf GUICtrlRead($g_hRadioRunNoOutput) = $GUI_CHECKED Then
        $sRunType = "NO_OUTPUT"
    Else
        $sRunType = "DIRECT"
    EndIf

    ; Protocol: RUN_COMMAND|type|command|workdir|showflag|timeout
    Local $sData = "RUN_COMMAND|" & $sRunType & "|" & $sCommand & "|" & $sWorkDir & "|" & $sShowFlag & "|" & $iTimeout

    Local $sMsg = "ADMIN: Run command on " & $g_aClients[$g_iSelectedClient][1] & ": [" & $sRunType & "] " & $sCommand & " (timeout: " & $iTimeout & "s)"
    LogAction($sMsg)
    _TCP_Send($g_aClients[$g_iSelectedClient][0], $sData & @CRLF)
    AddLog("Command sent: [" & $sRunType & "] " & $sCommand & " (timeout: " & $iTimeout & "s)")
EndFunc   ;==>RunCommandOnClient

Func ShellExecuteOnClient()
    If $g_iSelectedClient = 0 Then Return
    Local $sFile = GUICtrlRead($g_hInputCmd)
    If $sFile = "" Then Return

    Local $sParams = GUICtrlRead($g_hInputParams)
    Local $sWorkDir = GUICtrlRead($g_hInputWorkDir)
    Local $sShowFlag = GUICtrlRead($g_hComboShowFlag)

    ; Protocol: SHELLEXECUTE|filename|parameters|workdir|showflag
    Local $sData = "SHELLEXECUTE|" & $sFile & "|" & $sParams & "|" & $sWorkDir & "|" & $sShowFlag

    Local $sMsg = "ADMIN: ShellExecute on " & $g_aClients[$g_iSelectedClient][1] & ": " & $sFile & " " & $sParams
    LogAction($sMsg)
    _TCP_Send($g_aClients[$g_iSelectedClient][0], $sData & @CRLF)
    AddLog("ShellExecute sent: " & $sFile)
EndFunc   ;==>ShellExecuteOnClient

Func DisplaySoftwareList($aCmd)
    _GUICtrlListView_DeleteAllItems($g_hListSoftware)
    For $i = 2 To UBound($aCmd) - 1
        _GUICtrlListView_AddItem($g_hListSoftware, $aCmd[$i])
    Next
    Local $iCount = UBound($aCmd) - 2
    AddLog("Software list received: " & $iCount & " items")
    UpdateOperationStatus("Completed: Received " & $iCount & " software", 0x00AA00)
EndFunc   ;==>DisplaySoftwareList

Func DisplayProcessList($aCmd)
    _GUICtrlListView_DeleteAllItems($g_hListProcess)
    For $i = 2 To UBound($aCmd) - 1
        Local $aProc = StringSplit($aCmd[$i], ":", 2)
        If UBound($aProc) >= 2 Then
            Local $sPID = $aProc[0]
            Local $sName = $aProc[1]
            Local $sUser = UBound($aProc) >= 3 ? $aProc[2] : ""

            Local $iIdx = _GUICtrlListView_GetItemCount($g_hListProcess)
            _GUICtrlListView_AddItem($g_hListProcess, $sPID)
            _GUICtrlListView_AddSubItem($g_hListProcess, $iIdx, $sName, 1)
            _GUICtrlListView_AddSubItem($g_hListProcess, $iIdx, $sUser, 2)
        EndIf
    Next
    Local $iCount = UBound($aCmd) - 2
    AddLog("Process list received: " & $iCount & " items")
    UpdateOperationStatus("Completed: Received " & $iCount & " processes", 0x00AA00)
EndFunc   ;==>DisplayProcessList

Func DisplayWindowList($aCmd)
    _GUICtrlListView_DeleteAllItems($g_hListWindow)
    Local $iCount = 0
    If UBound($aCmd) >= 2 Then
        $iCount = Int($aCmd[1])
    EndIf

    For $i = 2 To UBound($aCmd) - 1
        Local $aWin = StringSplit($aCmd[$i], ":", 2)
        If UBound($aWin) >= 2 Then
            _GUICtrlListView_AddItem($g_hListWindow, $aWin[0])
            _GUICtrlListView_AddSubItem($g_hListWindow, _GUICtrlListView_GetItemCount($g_hListWindow) - 1, "0x" & $aWin[1], 1)
        EndIf
    Next
    AddLog("Window list received: " & $iCount & " items")
    UpdateOperationStatus("Completed: Received " & $iCount & " windows", 0x00AA00)
EndFunc   ;==>DisplayWindowList

Func RequestStartupList()
    If $g_iSelectedClient = 0 Then Return
    LogAction("Requesting startup list from " & $g_aClients[$g_iSelectedClient][1])
    UpdateOperationStatus("Sending command: Startup list...", 0xFF8800)
    _TCP_Send($g_aClients[$g_iSelectedClient][0], "GET_STARTUP_LIST" & @CRLF)
    UpdateOperationStatus("Waiting for response: Startup list...", 0x0088FF)
EndFunc   ;==>RequestStartupList

Func DisplayStartupList($aCmd)
    _GUICtrlListView_DeleteAllItems($g_hListStartup)

    If UBound($aCmd) < 2 Then Return
    Local $iCount = Int($aCmd[1])

    For $i = 2 To UBound($aCmd) - 1
        Local $aItem = StringSplit($aCmd[$i], Chr(1), 2) ; Use Chr(1) as separator
        If UBound($aItem) >= 4 Then
            Local $sName = $aItem[0]
            Local $sType = $aItem[1]
            Local $sLocation = $aItem[2]
            Local $sCommand = $aItem[3]

            Local $iIdx = _GUICtrlListView_GetItemCount($g_hListStartup)
            _GUICtrlListView_AddItem($g_hListStartup, $sName)
            _GUICtrlListView_AddSubItem($g_hListStartup, $iIdx, $sType, 1)
            _GUICtrlListView_AddSubItem($g_hListStartup, $iIdx, $sLocation, 2)
            _GUICtrlListView_AddSubItem($g_hListStartup, $iIdx, $sCommand, 3)
        EndIf
    Next

    AddLog("Startup list received: " & $iCount & " items")
    UpdateOperationStatus("Completed: Received " & $iCount & " startup items", 0x00AA00)
EndFunc   ;==>DisplayStartupList

Func DeleteSelectedStartup()
    If $g_iSelectedClient = 0 Then Return
    Local $iSelected = _GUICtrlListView_GetNextItem($g_hListStartup)
    If $iSelected < 0 Then Return

    Local $sName = _GUICtrlListView_GetItemText($g_hListStartup, $iSelected, 0)
    Local $sType = _GUICtrlListView_GetItemText($g_hListStartup, $iSelected, 1)
    Local $sLocation = _GUICtrlListView_GetItemText($g_hListStartup, $iSelected, 2)

    If MsgBox(36, "Xác nhận xóa", "Xóa mục khởi động:" & @CRLF & @CRLF & _
            "Tên: " & $sName & @CRLF & _
            "Loại: " & $sType & @CRLF & _
            "Vị trí: " & $sLocation & @CRLF & @CRLF & _
            "Bạn có chắc chắn?") <> 6 Then Return

    ; Send delete command with type and location
    Local $sDeleteCmd = "DELETE_STARTUP|" & $sName & Chr(1) & $sType & Chr(1) & $sLocation
    _TCP_Send($g_aClients[$g_iSelectedClient][0], $sDeleteCmd & @CRLF)
    LogAction("Deleting startup item: " & $sName & " (" & $sType & ")")
    AddLog("Delete startup requested: " & $sName)
EndFunc   ;==>DeleteSelectedStartup

Func RequestServiceList()
    If $g_iSelectedClient = 0 Then Return
    LogAction("Requesting service list from " & $g_aClients[$g_iSelectedClient][1])
    UpdateOperationStatus("Sending command: Service list...", 0xFF8800)
    _TCP_Send($g_aClients[$g_iSelectedClient][0], "GET_SERVICE_LIST" & @CRLF)
    UpdateOperationStatus("Waiting for response: Service list...", 0x0088FF)
EndFunc   ;==>RequestServiceList

Func DisplayServiceList($aCmd)
    _GUICtrlListView_DeleteAllItems($g_hListService)

    If UBound($aCmd) < 2 Then Return
    Local $iCount = Int($aCmd[1])

    For $i = 2 To UBound($aCmd) - 1
        Local $aItem = StringSplit($aCmd[$i], Chr(1), 2)
        If UBound($aItem) >= 4 Then
            Local $sServiceName = $aItem[0]
            Local $sDisplayName = $aItem[1]
            Local $sStatus = $aItem[2]
            Local $sStartupType = $aItem[3]

            Local $iIdx = _GUICtrlListView_GetItemCount($g_hListService)
            _GUICtrlListView_AddItem($g_hListService, $sServiceName)
            _GUICtrlListView_AddSubItem($g_hListService, $iIdx, $sDisplayName, 1)
            _GUICtrlListView_AddSubItem($g_hListService, $iIdx, $sStatus, 2)
            _GUICtrlListView_AddSubItem($g_hListService, $iIdx, $sStartupType, 3)
        EndIf
    Next

    AddLog("Service list received: " & $iCount & " services")
    UpdateOperationStatus("Completed: Received " & $iCount & " services", 0x00AA00)
EndFunc   ;==>DisplayServiceList

Func StartSelectedService()
    If $g_iSelectedClient = 0 Then Return
    Local $iSelected = _GUICtrlListView_GetNextItem($g_hListService)
    If $iSelected < 0 Then Return

    Local $sServiceName = _GUICtrlListView_GetItemText($g_hListService, $iSelected, 0)
    Local $sDisplayName = _GUICtrlListView_GetItemText($g_hListService, $iSelected, 1)

    _TCP_Send($g_aClients[$g_iSelectedClient][0], "START_SERVICE|" & $sServiceName & @CRLF)
    LogAction("Starting service: " & $sServiceName & " (" & $sDisplayName & ")")
    AddLog("Start service requested: " & $sDisplayName)
EndFunc   ;==>StartSelectedService

Func StopSelectedService()
    If $g_iSelectedClient = 0 Then Return
    Local $iSelected = _GUICtrlListView_GetNextItem($g_hListService)
    If $iSelected < 0 Then Return

    Local $sServiceName = _GUICtrlListView_GetItemText($g_hListService, $iSelected, 0)
    Local $sDisplayName = _GUICtrlListView_GetItemText($g_hListService, $iSelected, 1)

    If MsgBox(36, "Xác nhận", "Dừng dịch vụ: " & $sDisplayName & "?") <> 6 Then Return

    _TCP_Send($g_aClients[$g_iSelectedClient][0], "STOP_SERVICE|" & $sServiceName & @CRLF)
    LogAction("Stopping service: " & $sServiceName & " (" & $sDisplayName & ")")
    AddLog("Stop service requested: " & $sDisplayName)
EndFunc   ;==>StopSelectedService

Func EnableSelectedService()
    If $g_iSelectedClient = 0 Then Return
    Local $iSelected = _GUICtrlListView_GetNextItem($g_hListService)
    If $iSelected < 0 Then Return

    Local $sServiceName = _GUICtrlListView_GetItemText($g_hListService, $iSelected, 0)
    Local $sDisplayName = _GUICtrlListView_GetItemText($g_hListService, $iSelected, 1)

    _TCP_Send($g_aClients[$g_iSelectedClient][0], "ENABLE_SERVICE|" & $sServiceName & @CRLF)
    LogAction("Enabling service (Auto): " & $sServiceName & " (" & $sDisplayName & ")")
    AddLog("Enable service requested: " & $sDisplayName)
EndFunc   ;==>EnableSelectedService

Func DisableSelectedService()
    If $g_iSelectedClient = 0 Then Return
    Local $iSelected = _GUICtrlListView_GetNextItem($g_hListService)
    If $iSelected < 0 Then Return

    Local $sServiceName = _GUICtrlListView_GetItemText($g_hListService, $iSelected, 0)
    Local $sDisplayName = _GUICtrlListView_GetItemText($g_hListService, $iSelected, 1)

    If MsgBox(36, "Xác nhận", "Vô hiệu hóa dịch vụ: " & $sDisplayName & "?") <> 6 Then Return

    _TCP_Send($g_aClients[$g_iSelectedClient][0], "DISABLE_SERVICE|" & $sServiceName & @CRLF)
    LogAction("Disabling service: " & $sServiceName & " (" & $sDisplayName & ")")
    AddLog("Disable service requested: " & $sDisplayName)
EndFunc   ;==>DisableSelectedService

Func DeleteSelectedService()
    If $g_iSelectedClient = 0 Then Return
    Local $iSelected = _GUICtrlListView_GetNextItem($g_hListService)
    If $iSelected < 0 Then Return

    Local $sServiceName = _GUICtrlListView_GetItemText($g_hListService, $iSelected, 0)
    Local $sDisplayName = _GUICtrlListView_GetItemText($g_hListService, $iSelected, 1)

    If MsgBox(308, "Xác nhận", "XÓA VĨNH VIỄN dịch vụ: " & $sDisplayName & " (" & $sServiceName & ")" & @CRLF & @CRLF & _
            "CẢNH BÁO: Hành động này không thể hoàn tác!" & @CRLF & @CRLF & _
            "Bạn có chắc chắn muốn XÓA dịch vụ này?") <> 6 Then Return

    _TCP_Send($g_aClients[$g_iSelectedClient][0], "DELETE_SERVICE|" & $sServiceName & @CRLF)
    LogAction("Deleting service: " & $sServiceName & " (" & $sDisplayName & ")")
    AddLog("Delete service requested: " & $sDisplayName)
EndFunc   ;==>DeleteSelectedService

Func UninstallSelectedSoftware()
    If $g_iSelectedClient = 0 Then Return
    Local $iSelected = _GUICtrlListView_GetNextItem($g_hListSoftware)
    If $iSelected < 0 Then Return

    Local $sSoftwareName = _GUICtrlListView_GetItemText($g_hListSoftware, $iSelected, 0)

    If MsgBox(36, "Xác nhận", "Gỡ cài đặt phần mềm: " & $sSoftwareName & "?" & @CRLF & @CRLF & _
            "• MSI: Chạy uninstall tự động (silent)" & @CRLF & _
            "• Non-MSI: Xóa shortcuts, thư mục, dữ liệu, registry" & @CRLF & @CRLF & _
            "Quá trình có thể mất vài phút.") <> 6 Then Return

    _TCP_Send($g_aClients[$g_iSelectedClient][0], "UNINSTALL_SOFTWARE|" & $sSoftwareName & @CRLF)
    LogAction("Uninstalling software: " & $sSoftwareName)
    AddLog("Uninstall software requested: " & $sSoftwareName)
EndFunc   ;==>UninstallSelectedSoftware

Func AddLog($sText)
    Local $sCurrent = GUICtrlRead($g_hEditLog)
    Local $sTime = @HOUR & ":" & @MIN & ":" & @SEC
    Local $sNewLog = "[" & $sTime & "] " & $sText & @CRLF

    ; Append to existing log
    $sCurrent = StringReplace($sNewLog, @CRLF & @CRLF, @CRLF)

    ; Keep only last 20 lines
    Local $aLines = StringSplit($sCurrent, @CRLF, 1)
    If $aLines[0] > 20 Then
        $sCurrent = ""
        For $i = $aLines[0] - 19 To $aLines[0]
            $sCurrent &= $aLines[$i] & @CRLF
        Next
    EndIf
    Local $iEnd = StringLen(GUICtrlRead($g_hEditLog))
    _GUICtrlEdit_SetSel($g_hEditLog, $iEnd, $iEnd)
    _GUICtrlEdit_Scroll($g_hEditLog, $SB_SCROLLCARET)
    GUICtrlSetData($g_hEditLog, $sCurrent, 1)
EndFunc   ;==>AddLog

Func UpdateOperationStatus($sText, $iColor = 0x008800)
    ; Update operation status bar with color
    GUICtrlSetData($g_hOperationStatus, $sText)
    GUICtrlSetColor($g_hOperationStatus, $iColor)
EndFunc   ;==>UpdateOperationStatus

Func AddToStartup()
    Local $sStartupKey = "HKCU\Software\Microsoft\Windows\CurrentVersion\Run"
    Local $sAppName = "LabManagerServer"
    Local $sExePath = @ScriptFullPath

    ; Check if already in startup
    Local $sCurrentPath = RegRead($sStartupKey, $sAppName)
    If $sCurrentPath <> $sExePath Then
        RegWrite($sStartupKey, $sAppName, "REG_SZ", $sExePath)
        LogAction("Added to Windows startup")
    EndIf
EndFunc   ;==>AddToStartup

Func LogAction($sMessage)
    Local $sTime = @YEAR & "-" & @MON & "-" & @MDAY & " " & @HOUR & ":" & @MIN & ":" & @SEC
    Local $sLog = "[" & $sTime & "] " & $sMessage & @CRLF

    ; Append to log file
    Local $hFile = FileOpen($LOG_FILE, 1) ; Append mode
    If $hFile <> -1 Then
        FileWrite($hFile, $sLog)
        FileClose($hFile)
    EndIf

    ; Keep log file size under 5MB
    If FileGetSize($LOG_FILE) > 5242880 Then
        ; Archive old log
        FileCopy($LOG_FILE, $LOG_FILE & ".old", 1)
        FileDelete($LOG_FILE)
    EndIf
EndFunc   ;==>LogAction

Func Cleanup()
    LogAction("Server shutting down")
    If $g_hSocket Then
        _TCP_Server_Stop($g_hSocket)
    EndIf
    _TCP_Shutdown()
EndFunc   ;==>Cleanup

; ===== File Manager Functions =====
Func RequestFileList()
    If $g_iSelectedClient = 0 Then Return
    LogAction("Requesting file list from " & $g_aClients[$g_iSelectedClient][1] & ": " & $g_sCurrentPath)

    ; Request drive list first time (empty combo means no drives loaded yet)
    If GUICtrlRead($g_hComboDrive) = "" Then
        UpdateOperationStatus("Sending command: Drive list...", 0xFF8800)
        _TCP_Send($g_aClients[$g_iSelectedClient][0], "GET_DRIVE_LIST" & @CRLF)
        Sleep(100) ; Small delay to ensure drive list is received first
    EndIf

    UpdateOperationStatus("Sending command: File list...", 0xFF8800)
    _TCP_Send($g_aClients[$g_iSelectedClient][0], "FILE_LIST|" & $g_sCurrentPath & @CRLF)
    UpdateOperationStatus("Waiting for response: File list...", 0x0088FF)
EndFunc   ;==>RequestFileList

Func DisplayDriveList($aCmd)
    ; Build drive list string with "|" delimiter, ensure all drives end with "\"
    Local $sDriveList = ""
    For $i = 1 To UBound($aCmd) - 1
        If $aCmd[$i] <> "" Then
            Local $sDrive = _PathWithSlash($aCmd[$i]); Normalize: ensure drive ends with backslash (C:\)
            If $sDriveList <> "" Then $sDriveList &= "|"
            $sDriveList &= $sDrive
        EndIf
    Next

    ; Set all drives at once
    GUICtrlSetData($g_hComboDrive, $sDriveList)

    ; Select current drive (normalize to match combo format)
    Local $sCurrentDrive = _PathWithSlash(StringLeft($g_sCurrentPath, 2))
    GUICtrlSetData($g_hComboDrive, $sCurrentDrive)
    Local $iCount = UBound($aCmd) - 1
    AddLog("Drive list received: " & $iCount & " drives")
    UpdateOperationStatus("Completed: Received " & $iCount & " drives", 0x00AA00)
EndFunc   ;==>DisplayDriveList

Func DisplayFileList($aCmd)
    _GUICtrlListView_DeleteAllItems($g_hListFile)

    If UBound($aCmd) < 3 Then Return

    Local $sPath = $aCmd[1]
    Local $iCount = Int($aCmd[2])

    ; Add parent directory option
    If StringLen($sPath) > 3 Then ; Not root drive
        _GUICtrlListView_AddItem($g_hListFile, "..")
        _GUICtrlListView_AddSubItem($g_hListFile, 0, "DIR", 1)
        _GUICtrlListView_AddSubItem($g_hListFile, 0, "", 2)
        _GUICtrlListView_AddSubItem($g_hListFile, 0, "", 3)
        _GUICtrlListView_AddSubItem($g_hListFile, 0, "", 4)
    EndIf

    ; Add files and folders
    For $i = 3 To UBound($aCmd) - 1
        ; Use Chr(1) as delimiter to avoid conflicts with colons in filenames
        Local $aItem = StringSplit($aCmd[$i], Chr(1), 2)
        If UBound($aItem) >= 3 Then
            Local $sName = $aItem[0]
            Local $sRawAttrib = $aItem[1]

            ; Determine type: DIR if has 'D' attribute, FILE otherwise (even if attrib is empty)
            Local $sType = "FILE"
            If $sRawAttrib <> "" And StringInStr($sRawAttrib, "D") Then
                $sType = "DIR"
            EndIf

            Local $iSize = Int($aItem[2])
            Local $sSize = $sType = "DIR" ? "" : _FormatFileSize($iSize)
            Local $sModified = UBound($aItem) >= 4 ? _FormatFileTime($aItem[3]) : ""

            ; Format attributes: R (Read-only), S (System), H (Hidden), A (Archive)
            Local $sAttrib = ""
            If $sRawAttrib <> "" Then
                If StringInStr($sRawAttrib, "R") Then $sAttrib &= "R"
                If StringInStr($sRawAttrib, "S") Then $sAttrib &= "S"
                If StringInStr($sRawAttrib, "H") Then $sAttrib &= "H"
                If StringInStr($sRawAttrib, "A") Then $sAttrib &= "A"
            EndIf

            Local $iIdx = _GUICtrlListView_GetItemCount($g_hListFile)
            _GUICtrlListView_AddItem($g_hListFile, $sName)
            _GUICtrlListView_AddSubItem($g_hListFile, $iIdx, $sType, 1)
            _GUICtrlListView_AddSubItem($g_hListFile, $iIdx, $sSize, 2)
            _GUICtrlListView_AddSubItem($g_hListFile, $iIdx, $sModified, 3)
            _GUICtrlListView_AddSubItem($g_hListFile, $iIdx, $sAttrib, 4)
        EndIf
    Next

    ; Normalize path to ensure it ends with backslash
    $sPath = _NormalizePath($sPath)
    $g_sCurrentPath = $sPath
    GUICtrlSetData($g_hInputPath, $sPath) ; Update path input
    AddLog("File list received: " & $iCount & " items from " & $sPath)
    UpdateOperationStatus("Completed: Received " & $iCount & " files from " & $sPath, 0x00AA00)
EndFunc   ;==>DisplayFileList

Func _NormalizePath($sPath)
    If $sPath = "" Then Return "C:\"
    Return _PathWithSlash($sPath) ; Ensure path ends with single backslash
EndFunc   ;==>_NormalizePath

Func _FormatFileSize($iBytes)
    If $iBytes < 1024 Then Return $iBytes & " B"
    If $iBytes < 1048576 Then Return Round($iBytes / 1024, 2) & " KB"
    If $iBytes < 1073741824 Then Return Round($iBytes / 1048576, 2) & " MB"
    Return Round($iBytes / 1073741824, 2) & " GB"
EndFunc   ;==>_FormatFileSize

Func _FormatFileTime($sTime)
    ; Convert YYYYMMDDHHMMSS to readable format: YYYY-MM-DD HH:MM:SS
    If StringLen($sTime) <> 14 Then Return $sTime
    Local $sYear = StringMid($sTime, 1, 4)
    Local $sMonth = StringMid($sTime, 5, 2)
    Local $sDay = StringMid($sTime, 7, 2)
    Local $sHour = StringMid($sTime, 9, 2)
    Local $sMin = StringMid($sTime, 11, 2)
    Local $sSec = StringMid($sTime, 13, 2)
    Return $sYear & "-" & $sMonth & "-" & $sDay & " " & $sHour & ":" & $sMin & ":" & $sSec
EndFunc   ;==>_FormatFileTime

Func DeleteSelectedFile()
    If $g_iSelectedClient = 0 Then Return
    Local $iSelected = _GUICtrlListView_GetNextItem($g_hListFile)
    If $iSelected < 0 Then Return

    Local $sName = _GUICtrlListView_GetItemText($g_hListFile, $iSelected, 0)
    If $sName = ".." Then Return

    If MsgBox(36, "Confirm Delete", "Delete file/folder: " & $sName & "?") <> 6 Then Return

    Local $sRemotePath = $g_sCurrentPath
    If Not StringRight($sRemotePath, 1) = "\" Then $sRemotePath &= "\"
    $sRemotePath &= $sName

    LogAction("Deleting remote file: " & $sRemotePath)
    _TCP_Send($g_aClients[$g_iSelectedClient][0], "FILE_DELETE|" & $sRemotePath & @CRLF)
EndFunc   ;==>DeleteSelectedFile

Func RenameSelectedFile()
    If $g_iSelectedClient = 0 Then Return
    Local $iSelected = _GUICtrlListView_GetNextItem($g_hListFile)
    If $iSelected < 0 Then Return

    Local $sOldName = _GUICtrlListView_GetItemText($g_hListFile, $iSelected, 0)
    If $sOldName = ".." Then Return

    Local $sNewName = InputBox("Rename", "Enter new name:", $sOldName)
    If @error Or $sNewName = "" Or $sNewName = $sOldName Then Return

    Local $sOldPath = $g_sCurrentPath
    If Not StringRight($sOldPath, 1) = "\" Then $sOldPath &= "\"
    $sOldPath &= $sOldName

    Local $sNewPath = $g_sCurrentPath
    If Not StringRight($sNewPath, 1) = "\" Then $sNewPath &= "\"
    $sNewPath &= $sNewName

    LogAction("Renaming remote file: " & $sOldPath & " -> " & $sNewPath)
    _TCP_Send($g_aClients[$g_iSelectedClient][0], "FILE_RENAME|" & $sOldPath & "|" & $sNewPath & @CRLF)
EndFunc   ;==>RenameSelectedFile

Func ShellExecuteFile($sFilePath)
    If $g_iSelectedClient = 0 Then Return
    LogAction("ShellExecute file on " & $g_aClients[$g_iSelectedClient][1] & ": " & $sFilePath)
    _TCP_Send($g_aClients[$g_iSelectedClient][0], "FILE_SHELLEXECUTE|" & $sFilePath & @CRLF)
    AddLog("ShellExecute requested: " & $sFilePath)
EndFunc   ;==>ShellExecuteFile

Func RemoveFileAttributes()
    If $g_iSelectedClient = 0 Then Return
    Local $iSelected = _GUICtrlListView_GetNextItem($g_hListFile)
    If $iSelected < 0 Then Return

    Local $sName = _GUICtrlListView_GetItemText($g_hListFile, $iSelected, 0)
    If $sName = ".." Then Return

    If MsgBox(36, "Remove Attributes", "Remove -RAHS attributes from: " & $sName & "?") <> 6 Then Return

    Local $sRemotePath = $g_sCurrentPath
    If Not StringRight($sRemotePath, 1) = "\" Then $sRemotePath &= "\"
    $sRemotePath &= $sName

    LogAction("Removing attributes from: " & $sRemotePath)
    _TCP_Send($g_aClients[$g_iSelectedClient][0], "FILE_REMOVE_ATTRIB|" & $sRemotePath & @CRLF)
EndFunc   ;==>RemoveFileAttributes

Client:

#RequireAdmin
#NoTrayIcon
#Region ;**** Directives created by AutoIt3Wrapper_GUI ****
#AutoIt3Wrapper_Icon=shell32.dll,16
#AutoIt3Wrapper_UseX64=y
#AutoIt3Wrapper_Res_requestedExecutionLevel=asInvoker
#EndRegion ;**** Directives created by AutoIt3Wrapper_GUI ****

; ===== LAB MANAGER CLIENT v1.0 by Dao Van Trong - TRONG.PRO =====
; Silent monitoring agent with tray icon
Opt("TrayMenuMode", 1) ;0=append, 1=no default menu, 2=no automatic check, 4=menuitemID  not return
#include "WinSockUDF.au3"
;~ #include "LabManager_RUDP.au3"
#include <GUIConstantsEx.au3>
#include <Array.au3>
#include <File.au3>
#include <FileConstants.au3>
#include <WinAPIProc.au3>
#include <WinAPIConstants.au3>
#include <WinAPISysWin.au3>
#include <WinAPIGdi.au3>
#include <TrayConstants.au3>
#include <Inet.au3>
#include <WindowsConstants.au3>
#include <Security.au3>
#include <SecurityConstants.au3>
#include <WinAPISys.au3>
; Configuration
Global Const $SERVER_IP = "127.0.0.1"
Global Const $SERVER_PORT = 8888
Global Const $RECONNECT_INTERVAL = 10000 ; 10 seconds
Global Const $UPDATE_CHECK_INTERVAL = 3600000 ; 1 hour in milliseconds
Global Const $CLIENT_VERSION = "1.0.0"
Global Const $UPDATE_URL = "http://yourserver.com/update.ini" ; URL to .ini file containing version info
Global Const $LOG_FILE = @ScriptDir & "\LabManager_Client.log"
FileDelete($LOG_FILE)
; Client state
Global $g_hSocket = 0
Global $g_bConnected = False
Global $g_iLastReconnect = 0
Global $g_iLastUpdateCheck = 0
Global $g_bUpdatePending = False
Global $g_sUpdateFile = ""

; System info
Global $g_sComputerName = @ComputerName
Global $g_sUserName = @UserName
Global $g_sIPAddress = @IPAddress1
Global $g_sOSVersion = @OSVersion
Global $g_sWinVer = ""
Global $g_sWinVersion = ""

Global $oErrorHandler = ObjEvent("AutoIt.Error", "_COMErrFunc")
Func _COMErrFunc()
    ; Do nothing special, just check @error after suspect functions.
EndFunc   ;==>_COMErrFunc

; Tray icon
Opt("TrayMenuMode", 3) ; Default tray menu items will not be shown
Opt("TrayOnEventMode", 1)
TraySetIcon("shell32.dll", 16) ; Computer icon
TraySetToolTip("Lab Manager Client" & @CRLF & "Computer: " & $g_sComputerName & @CRLF & "User: " & $g_sUserName & @CRLF & "Status: Connecting...")

Global $g_idTrayStatus = TrayCreateItem("Status: Connecting...")
TrayItemSetState($g_idTrayStatus, $TRAY_DISABLE)
TrayCreateItem("")
Global $g_idTrayExit = TrayCreateItem("Exit")
TrayItemSetOnEvent($g_idTrayExit, "OnTrayExit")

; Initialize
_TCP_Startup()
GetSystemInfo()
AddToStartup()
LogAction("Client started - Version " & $CLIENT_VERSION)
ConnectToServer()

; Check for updates on startup
CheckForUpdates()

; Main loop
While True
    ; Check connection
    If Not $g_bConnected And TimerDiff($g_iLastReconnect) > $RECONNECT_INTERVAL Then
        ConnectToServer()
    EndIf

    ; Check for updates periodically
    If TimerDiff($g_iLastUpdateCheck) > $UPDATE_CHECK_INTERVAL Then
        CheckForUpdates()
    EndIf

    ; Process RUDP retransmissions
;~  RUDP_CheckRetransmissions()

    Sleep(100)
WEnd

Func ConnectToServer()
    $g_iLastReconnect = TimerInit()

    If $g_hSocket <> 0 Then
        _TCP_Client_Stop($g_hSocket)
        $g_hSocket = 0
    EndIf

    $g_hSocket = _TCP_Client_Create($SERVER_IP, $SERVER_PORT)
    If $g_hSocket = 0 Then
        _UpdateTrayStatus("Connection Failed")
        LogAction("Failed to connect to server: " & $SERVER_IP & ":" & $SERVER_PORT)
        Return
    EndIf

    ; Register callbacks
    _TCP_Client_OnConnect($g_hSocket, "OnConnect")
    _TCP_Client_OnReceive($g_hSocket, "OnReceive")
    _TCP_Client_OnDisconnect($g_hSocket, "OnDisconnect")

    _UpdateTrayStatus("Connecting...")
EndFunc   ;==>ConnectToServer

Func OnConnect($hSocket, $iError)
    If $iError Then
        $g_bConnected = False
        _UpdateTrayStatus("Connection Failed")
        Return
    EndIf

    $g_bConnected = True
    _UpdateTrayStatus("Connected")
    LogAction("Connected to server: " & $SERVER_IP & ":" & $SERVER_PORT)

    ; Send registration with version
    Local $sRegister = "CLIENT_REGISTER|" & $g_sComputerName & "|" & $g_sUserName & "|" & $g_sIPAddress & "|" & $g_sOSVersion & "|" & $g_sWinVer & "|" & $g_sWinVersion & "|" & $CLIENT_VERSION
    _TCP_Send($hSocket, $sRegister & @CRLF)
EndFunc   ;==>OnConnect

Func OnReceive($hSocket, $sData, $iError)
    If $iError Or $sData = "" Then Return

    $sData = StringStripWS($sData, 3)
    Local $aCmd = StringSplit($sData, "|", 2)
    Local $sCommand = $aCmd[0]

    Switch $sCommand
        Case "GET_SOFTWARE_LIST"
            LogAction("Software list requested")
            SendSoftwareList($hSocket)

        Case "GET_PROCESS_LIST"
            LogAction("Process list requested")
            SendProcessList($hSocket)

        Case "GET_WINDOW_LIST"
            LogAction("Window list requested")
            SendWindowList($hSocket)

        Case "GET_STARTUP_LIST"
            LogAction("Startup list requested")
            SendStartupList($hSocket)

        Case "DELETE_STARTUP"
            If UBound($aCmd) >= 2 Then
                DeleteStartupItem($hSocket, $aCmd[1])
            EndIf

        Case "GET_SERVICE_LIST"
            LogAction("Service list requested")
            SendServiceList($hSocket)

        Case "START_SERVICE"
            If UBound($aCmd) >= 2 Then
                StartService($hSocket, $aCmd[1])
            EndIf

        Case "STOP_SERVICE"
            If UBound($aCmd) >= 2 Then
                StopService($hSocket, $aCmd[1])
            EndIf

        Case "ENABLE_SERVICE"
            If UBound($aCmd) >= 2 Then
                EnableService($hSocket, $aCmd[1])
            EndIf

        Case "DISABLE_SERVICE"
            If UBound($aCmd) >= 2 Then
                DisableService($hSocket, $aCmd[1])
            EndIf

        Case "DELETE_SERVICE"
            If UBound($aCmd) >= 2 Then
                DeleteService($hSocket, $aCmd[1])
            EndIf

        Case "UNINSTALL_SOFTWARE"
            If UBound($aCmd) >= 2 Then
                UninstallSoftware($hSocket, $aCmd[1])
            EndIf

        Case "GET_DRIVE_LIST"
            SendDriveList($hSocket)

        Case "FILE_LIST"
            If UBound($aCmd) >= 2 Then
                SendFileList($hSocket, $aCmd[1])
            EndIf

        Case "FILE_SHELLEXECUTE"
            If UBound($aCmd) >= 2 Then
                ShellExecute($aCmd[1])
                _TCP_Send($hSocket, "FILE_SHELLEXECUTE_DONE|" & $aCmd[1] & @CRLF)
                LogAction("ShellExecute: " & $aCmd[1])
            EndIf

        Case "FILE_REMOVE_ATTRIB"
            If UBound($aCmd) >= 2 Then
                Local $sPath = $aCmd[1]
                Local $bSuccess = FileSetAttrib($sPath, "-RAHS")

                If $bSuccess Then
                    _TCP_Send($hSocket, "FILE_ATTRIB_REMOVED|" & $sPath & @CRLF)
                    LogAction("Attributes removed: " & $sPath)
                Else
                    _TCP_Send($hSocket, "FILE_ATTRIB_ERROR|" & $sPath & @CRLF)
                    LogAction("Error removing attributes: " & $sPath)
                EndIf
            EndIf

        Case "FILE_DELETE"
            If UBound($aCmd) >= 2 Then
                Local $sPath = $aCmd[1]
                Local $bSuccess = False

                ; Check if directory or file
                If FileGetAttrib($sPath) <> -1 Then
                    If StringInStr(FileGetAttrib($sPath), "D") Then
                        ; It's a directory
                        $bSuccess = DirRemove($sPath, 1) ; 1 = recursive delete
                    Else
                        ; It's a file
                        $bSuccess = FileDelete($sPath)
                    EndIf
                EndIf

                If $bSuccess Then
                    _TCP_Send($hSocket, "FILE_DELETED|" & $sPath & @CRLF)
                    LogAction("File/Folder deleted: " & $sPath)
                Else
                    _TCP_Send($hSocket, "FILE_DELETE_ERROR|" & $sPath & @CRLF)
                    LogAction("Error deleting: " & $sPath)
                EndIf
            EndIf

        Case "FILE_RENAME"
            If UBound($aCmd) >= 3 Then
                Local $sOldPath = $aCmd[1]
                Local $sNewPath = $aCmd[2]
                Local $bSuccess = False

                ; Check if directory or file
                If FileGetAttrib($sOldPath) <> -1 Then
                    If StringInStr(FileGetAttrib($sOldPath), "D") Then
                        ; It's a directory
                        $bSuccess = DirMove($sOldPath, $sNewPath, 1) ; 1 = overwrite
                    Else
                        ; It's a file
                        $bSuccess = FileMove($sOldPath, $sNewPath, 1)
                    EndIf
                EndIf

                If $bSuccess Then
                    _TCP_Send($hSocket, "FILE_RENAMED|" & $sOldPath & "|" & $sNewPath & @CRLF)
                    LogAction("File/Folder renamed: " & $sOldPath & " -> " & $sNewPath)
                Else
                    _TCP_Send($hSocket, "FILE_RENAME_ERROR|" & $sOldPath & @CRLF)
                    LogAction("Error renaming: " & $sOldPath)
                EndIf
            EndIf

        Case "SHUTDOWN"
            LogAction("ADMIN: Shutdown command received")
            Shutdown(1) ; Shutdown

        Case "REBOOT"
            LogAction("ADMIN: Reboot command received")
            Shutdown(2) ; Reboot

        Case "RUN_COMMAND"
            If UBound($aCmd) >= 6 Then
                ExecuteRunCommand($hSocket, $aCmd[1], $aCmd[2], $aCmd[3], $aCmd[4], Int($aCmd[5]))
            ElseIf UBound($aCmd) >= 5 Then
                ExecuteRunCommand($hSocket, $aCmd[1], $aCmd[2], $aCmd[3], $aCmd[4], 60) ; Default 60s
            EndIf

        Case "SHELLEXECUTE"
            If UBound($aCmd) >= 5 Then
                ExecuteShellCommand($hSocket, $aCmd[1], $aCmd[2], $aCmd[3], $aCmd[4])
            EndIf

        Case "CLOSE_PROCESS"
            If UBound($aCmd) >= 2 Then
                LogAction("ADMIN: Close process: " & $aCmd[1])
                ProcessClose($aCmd[1])
                _TCP_Send($hSocket, "PROCESS_CLOSED|" & $aCmd[1] & @CRLF)
            EndIf

        Case "CLOSE_WINDOW"
            If UBound($aCmd) >= 2 Then
                LogAction("ADMIN: Close window: " & $aCmd[1])
                ; Convert hex string to window handle properly
                Local $hWnd = HWnd(Number($aCmd[1]))
                If WinExists($hWnd) Then
                    WinClose($hWnd)
                    _TCP_Send($hSocket, "WINDOW_CLOSED|" & $aCmd[1] & @CRLF)
                Else
                    LogAction("Error: Window not found: " & $aCmd[1])
                EndIf
            EndIf

        Case "SHOW_MESSAGE"
            If UBound($aCmd) >= 3 Then
                Local $iTimeout = UBound($aCmd) >= 4 ? Int($aCmd[3]) : 0
                LogAction("ADMIN: Show message: " & $aCmd[1] & " - " & $aCmd[2])
                SplashTextOn($aCmd[1], $aCmd[2], 400, 100, -1, -1, 1, "", 12)
                If $iTimeout > 0 Then
                    AdlibRegister("_CloseSplash", $iTimeout * 1000)
                EndIf
            EndIf
    EndSwitch
EndFunc   ;==>OnReceive


Func OnDisconnect($hSocket, $iError)
    $g_bConnected = False
    _UpdateTrayStatus("Disconnected")
    LogAction("Disconnected from server")
    $g_hSocket = 0

    ; Check if update pending
    If $g_bUpdatePending Then
        PerformUpdate()
    EndIf
EndFunc   ;==>OnDisconnect

Func GetSystemInfo()
    ; Get IP address
    Local $aIPs = _TCP_GetIPs()
    If IsArray($aIPs) And $aIPs[0] > 0 Then
        $g_sIPAddress = $aIPs[1]
    EndIf

    ; Get OS version and edition
    Local $sProductName = RegRead('HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion', 'ProductName')
    Local $sDisplayVersion = RegRead('HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion', 'DisplayVersion')
    Local $sReleaseId = RegRead('HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion', 'ReleaseId')
    Local $sBuild = @OSBuild

    ; Set OS name
    If $sProductName <> '' Then
        $g_sOSVersion = $sProductName
    Else
        Switch @OSVersion
            Case 'WIN_11'
                $g_sOSVersion = 'Windows 11'
            Case 'WIN_10', 'WIN_2016', 'WIN_2019'
                $g_sOSVersion = 'Windows 10'
            Case 'WIN_81', 'WIN_2012R2'
                $g_sOSVersion = 'Windows 8.1'
            Case 'WIN_8', 'WIN_2012'
                $g_sOSVersion = 'Windows 8'
            Case 'WIN_7', 'WIN_2008R2'
                $g_sOSVersion = 'Windows 7'
            Case Else
                $g_sOSVersion = @OSVersion
        EndSwitch
    EndIf

    ; Set Windows version (22H2, 21H2, etc.)
    If $sDisplayVersion <> '' Then
        $g_sWinVersion = $sDisplayVersion
    ElseIf $sReleaseId <> '' Then
        $g_sWinVersion = $sReleaseId
    Else
        $g_sWinVersion = 'Build ' & $sBuild
    EndIf

    ; Set WinVer (build number)
    $g_sWinVer = $sBuild
EndFunc   ;==>GetSystemInfo

Func SendSoftwareList($hSocket)
    Local $aList = GetInstalledSoftware()
    Local $sData = "SOFTWARE_LIST|" & $aList[0]
    For $i = 1 To $aList[0]
        $sData &= "|" & $aList[$i]
    Next
    LogAction("Sending software list: " & $aList[0] & " items")
    _TCP_Send($hSocket, $sData & @CRLF)
EndFunc   ;==>SendSoftwareList

Func GetInstalledSoftware()
    Local $aResult[1]
    $aResult[0] = 0

    ; Read from registry (limit to 100 items to prevent overflow)
    Local $sKey = "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
    Local $i = 1
    While $aResult[0] < 100
        Local $sSubKey = RegEnumKey($sKey, $i)
        If @error Then ExitLoop

        Local $sName = RegRead($sKey & "\" & $sSubKey, "DisplayName")
        If $sName <> "" Then
            $aResult[0] += 1
            ReDim $aResult[$aResult[0] + 1]
            $aResult[$aResult[0]] = StringReplace($sName, "|", "-") ; Remove pipe char
        EndIf
        $i += 1
    WEnd

    ; Also check 64-bit registry on 64-bit systems
    If @OSArch = "X64" Then
        $sKey = "HKLM64\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
        $i = 1
        While $aResult[0] < 100
            Local $sSubKey = RegEnumKey($sKey, $i)
            If @error Then ExitLoop

            Local $sName = RegRead($sKey & "\" & $sSubKey, "DisplayName")
            If $sName <> "" Then
                $aResult[0] += 1
                ReDim $aResult[$aResult[0] + 1]
                $aResult[$aResult[0]] = StringReplace($sName, "|", "-")
            EndIf
            $i += 1
        WEnd
    EndIf

    Return $aResult
EndFunc   ;==>GetInstalledSoftware

Func SendProcessList($hSocket)
    ; Connect to WMI and get process objects
    $oWMI = ObjGet("winmgmts:{impersonationLevel=impersonate,authenticationLevel=pktPrivacy, (Debug)}!\\.\root\cimv2")
    If IsObj($oWMI) Then
        ; Get collection processes from Win32_Process
        ; Get all
        $colProcs = $oWMI.ExecQuery("select * from win32_process")
        ; Get by PID
;~          $colProcs = $oWMI.ExecQuery("select * from win32_process where ProcessId = " & $Process)
        ; Get by Name
;~          $colProcs = $oWMI.ExecQuery("select * from win32_process where Name = '" & $Process & "'")
        If IsObj($colProcs) Then
            ; Return for no matches
            If $colProcs.count = 0 Then Return ""
            Local $sUserName, $iPID, $sProcess, $iCount, $sData
            ; For each process...
            For $oProc In $colProcs
                $sProcess = $oProc.name
                $iPID = $oProc.ProcessId
                If $oProc.GetOwner($sUserName) = 0 Then
                    $sData &= "|" & $iPID & ":" & $sProcess & ":" & $sUserName
                    $iCount += 1
                EndIf
            Next
            LogAction("Sending process list: " & $iCount & " items")
            _TCP_Send($hSocket, "PROCESS_LIST|" & $iCount & $sData & @CRLF)
            Return "PROCESS_LIST|" & $iCount & $sData
        Else
            LogAction("Get process list: Error getting process collection from WMI")
            Return SetError(2, 0, '')
        EndIf
    Else
        LogAction("Get process list: Error connecting to WMI")
        Return SetError(1, 0, '')
    EndIf

EndFunc   ;==>SendProcessList


Func SendWindowList($hSocket)
    Local $aList = WinList()
    Local $sData = "WINDOW_LIST"
    Local $iCount = 0

    ; First pass: count visible top-level windows with titles
    For $i = 1 To $aList[0][0]
        ; Check: has title AND is visible top-level window (not child, not tool window)
        If $aList[$i][0] <> "" And _IsVisibleWindow($aList[$i][1]) Then
            $iCount += 1
        EndIf
    Next

    $sData &= "|" & $iCount

    ; Second pass: add window data
    For $i = 1 To $aList[0][0]
        ; Check: has title AND is visible top-level window (not child, not tool window)
        If $aList[$i][0] <> "" And _IsVisibleWindow($aList[$i][1]) Then
            Local $sTitle = StringReplace($aList[$i][0], "|", "-")
            $sTitle = StringReplace($sTitle, ":", "-")
            $sData &= "|" & $sTitle & ":" & Hex($aList[$i][1])
        EndIf
    Next

    LogAction("Sending window list: " & $iCount & " items")
    _TCP_Send($hSocket, $sData & @CRLF)
EndFunc   ;==>SendWindowList

Func SendDriveList($hSocket)
    Local $aDrives = DriveGetDrive("ALL")
    If @error Then
        LogAction("Error getting drive list")
        Return
    EndIf

    Local $sData = "FILE_DRIVE_LIST"
    For $i = 1 To $aDrives[0]
        ; DriveGetDrive returns "c:", we need "C:\"
        Local $sDrive = _PathWithSlash(StringUpper($aDrives[$i]))
    LogAction("Drive ["&$i&"]: " & $sDrive & "")
        $sData &= "|" & $sDrive
    Next
    _TCP_Send($hSocket, $sData & @CRLF)
    LogAction("Drive list sent: " & $aDrives[0] & " drives")
EndFunc   ;==>SendDriveList

Func SendFileList($hSocket, $sPath)
    $sPath = _PathWithSlash($sPath) ; Normalize path - ensure ends with backslash
    Local $aFiles = _FileListToArray($sPath, "*", $FLTA_FILESFOLDERS)
    If @error Then
        Local $iError = @error
        _TCP_Send($hSocket, "FILE_LIST_ERROR|" & $sPath & @CRLF)
        LogAction("File list error: " & $sPath & " (@error=" & $iError & ")")
        Return
    EndIf

    Local $sData = "FILE_LIST_RESULT|" & $sPath & "|" & $aFiles[0]
    LogAction("Processing " & $aFiles[0] & " items from: " & $sPath)

    For $i = 1 To $aFiles[0]
        Local $sFullPath = $sPath & $aFiles[$i]

        ; Get file attributes with error checking
        Local $sAttrib = FileGetAttrib($sFullPath)
        Local $iAttribError = @error
        If $iAttribError Or $sAttrib = "" Then
            ; Log error for debugging
            If $i <= 3 Then LogAction("FileGetAttrib error for: " & $sFullPath & " (error: " & $iAttribError & ")")
            $sAttrib = ""
        EndIf

        ; Get file size (0 for directories or errors)
        Local $iSize = 0
        If $sAttrib <> "" And Not StringInStr($sAttrib, 'D') Then
            $iSize = FileGetSize($sFullPath)
            If @error Or $iSize < 0 Then $iSize = 0
        EndIf

        ; Get file modified time (format: YYYYMMDDHHMMSS)
        Local $sModified = ""
        If $sAttrib <> "" Then
            $sModified = FileGetTime($sFullPath, 0, 1)
            If @error Or $sModified = "" Then $sModified = ""
        EndIf

        ; Use Chr(1) as delimiter to avoid conflicts with colons in filenames
        $sData &= "|" & $aFiles[$i] & Chr(1) & $sAttrib & Chr(1) & $iSize & Chr(1) & $sModified
    Next
    _TCP_Send($hSocket, $sData & @CRLF)
    LogAction("File list sent: " & $aFiles[0] & " items from " & $sPath)
EndFunc   ;==>SendFileList

Func SendStartupList($hSocket)
    Local $aStartupItems[1]
    $aStartupItems[0] = 0

    ; 1. Registry - Current User
    _GetStartupFromRegistry("HKCU\Software\Microsoft\Windows\CurrentVersion\Run", "Registry (HKCU)", $aStartupItems)
    _GetStartupFromRegistry("HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce", "Registry (HKCU Once)", $aStartupItems)

    ; 2. Registry - Local Machine
    _GetStartupFromRegistry("HKLM\Software\Microsoft\Windows\CurrentVersion\Run", "Registry (HKLM)", $aStartupItems)
    _GetStartupFromRegistry("HKLM\Software\Microsoft\Windows\CurrentVersion\RunOnce", "Registry (HKLM Once)", $aStartupItems)

    ; 3. Startup Folders
    _GetStartupFromFolder(@StartupDir, "Startup Folder (User)", $aStartupItems)
    _GetStartupFromFolder(@StartupCommonDir, "Startup Folder (Common)", $aStartupItems)

    ; 4. Task Scheduler
    _GetStartupFromTaskScheduler($aStartupItems)

    ; Build message
    Local $sData = "STARTUP_LIST|" & $aStartupItems[0]
    For $i = 1 To $aStartupItems[0]
        $sData &= "|" & $aStartupItems[$i]
    Next

    LogAction("Sending startup list: " & $aStartupItems[0] & " items")
    _TCP_Send($hSocket, $sData & @CRLF)
EndFunc   ;==>SendStartupList

Func _GetStartupFromRegistry($sRegKey, $sLocation, ByRef $aItems)
    Local $i = 1
    While True
        Local $sValueName = RegEnumVal($sRegKey, $i)
        If @error Then ExitLoop

        Local $sCommand = RegRead($sRegKey, $sValueName)
        If $sCommand <> "" Then
            $aItems[0] += 1
            ReDim $aItems[$aItems[0] + 1]
            ; Format: Name<Chr(1)>Type<Chr(1)>Location<Chr(1)>Command
            $aItems[$aItems[0]] = $sValueName & Chr(1) & "Registry" & Chr(1) & $sLocation & Chr(1) & $sCommand
        EndIf
        $i += 1
    WEnd
EndFunc   ;==>_GetStartupFromRegistry

Func _GetStartupFromFolder($sFolder, $sLocation, ByRef $aItems)
    Local $aFiles = _FileListToArray($sFolder, "*", $FLTA_FILES)
    If @error Or Not IsArray($aFiles) Then Return

    ; List of system files to exclude (only by name, not by attributes - viruses often hide)
    Local $aExclude[5] = ["desktop.ini", "thumbs.db", "Thumbs.db", "Desktop.ini", ".DS_Store"]

    For $i = 1 To $aFiles[0]
        Local $sFileName = $aFiles[$i]
        Local $sFilePath = $sFolder & "\" & $sFileName

        ; Skip if file is in exclude list
        Local $bSkip = False
        For $j = 0 To UBound($aExclude) - 1
            If StringLower($sFileName) = StringLower($aExclude[$j]) Then
                $bSkip = True
                ExitLoop
            EndIf
        Next
        If $bSkip Then ContinueLoop

        $aItems[0] += 1
        ReDim $aItems[$aItems[0] + 1]
        ; Format: Name<Chr(1)>Type<Chr(1)>Location<Chr(1)>Command
        $aItems[$aItems[0]] = $sFileName & Chr(1) & "Shortcut" & Chr(1) & $sLocation & Chr(1) & $sFilePath
    Next
EndFunc   ;==>_GetStartupFromFolder

Func _GetStartupFromTaskScheduler(ByRef $aItems)
    ; Using COM to access Task Scheduler
    Local $oService = ObjCreate("Schedule.Service")
    If Not IsObj($oService) Then Return

    $oService.Connect()
    Local $oFolder = $oService.GetFolder("\")
    If Not IsObj($oFolder) Then Return

    Local $oTasks = $oFolder.GetTasks(0)
    If Not IsObj($oTasks) Then Return

    For $oTask In $oTasks
        ; Check if task runs at logon
        Local $oTriggers = $oTask.Definition.Triggers
        Local $bIsStartup = False

        For $oTrigger In $oTriggers
            ; Type 8 = TASK_TRIGGER_LOGON, Type 9 = TASK_TRIGGER_BOOT
            If $oTrigger.Type = 8 Or $oTrigger.Type = 9 Then
                $bIsStartup = True
                ExitLoop
            EndIf
        Next

        If $bIsStartup And $oTask.Enabled Then
            Local $sTaskName = $oTask.Name
            Local $sCommand = ""

            ; Get command from actions
            Local $oActions = $oTask.Definition.Actions
            For $oAction In $oActions
                If $oAction.Type = 0 Then ; TASK_ACTION_EXEC
                    $sCommand = $oAction.Path
                    If $oAction.Arguments <> "" Then
                        $sCommand &= " " & $oAction.Arguments
                    EndIf
                    ExitLoop
                EndIf
            Next

            $aItems[0] += 1
            ReDim $aItems[$aItems[0] + 1]
            ; Format: Name<Chr(1)>Type<Chr(1)>Location<Chr(1)>Command
            $aItems[$aItems[0]] = $sTaskName & Chr(1) & "Task Scheduler" & Chr(1) & "Task Scheduler" & Chr(1) & $sCommand
        EndIf
    Next
EndFunc   ;==>_GetStartupFromTaskScheduler

Func DeleteStartupItem($hSocket, $sItemData)
    Local $aItem = StringSplit($sItemData, Chr(1), 2)
    If UBound($aItem) < 3 Then
        _TCP_Send($hSocket, "STARTUP_DELETE_ERROR|Invalid data" & @CRLF)
        Return
    EndIf

    Local $sName = $aItem[0]
    Local $sType = $aItem[1]
    Local $sLocation = $aItem[2]
    Local $bSuccess = False

    Switch $sType
        Case "Registry"
            $bSuccess = _DeleteRegistryStartup($sName, $sLocation)

        Case "Shortcut"
            $bSuccess = _DeleteShortcutStartup($sName, $sLocation)

        Case "Task Scheduler"
            $bSuccess = _DeleteTaskSchedulerStartup($sName)
    EndSwitch

    If $bSuccess Then
        _TCP_Send($hSocket, "STARTUP_DELETED|" & $sName & @CRLF)
        LogAction("Startup item deleted: " & $sName & " (" & $sType & ")")
    Else
        _TCP_Send($hSocket, "STARTUP_DELETE_ERROR|" & $sName & @CRLF)
        LogAction("Error deleting startup item: " & $sName)
    EndIf
EndFunc   ;==>DeleteStartupItem

Func _DeleteRegistryStartup($sName, $sLocation)
    Local $sRegKey = ""

    Switch $sLocation
        Case "Registry (HKCU)"
            $sRegKey = "HKCU\Software\Microsoft\Windows\CurrentVersion\Run"
        Case "Registry (HKCU Once)"
            $sRegKey = "HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce"
        Case "Registry (HKLM)"
            $sRegKey = "HKLM\Software\Microsoft\Windows\CurrentVersion\Run"
        Case "Registry (HKLM Once)"
            $sRegKey = "HKLM\Software\Microsoft\Windows\CurrentVersion\RunOnce"
        Case Else
            Return False
    EndSwitch

    RegDelete($sRegKey, $sName)
    Return Not @error
EndFunc   ;==>_DeleteRegistryStartup

Func _DeleteShortcutStartup($sName, $sLocation)
    Local $sFolder = ""

    Switch $sLocation
        Case "Startup Folder (User)"
            $sFolder = @StartupDir
        Case "Startup Folder (Common)"
            $sFolder = @StartupCommonDir
        Case Else
            Return False
    EndSwitch

    Local $sFilePath = $sFolder & "\" & $sName
    FileDelete($sFilePath)
    Return Not @error
EndFunc   ;==>_DeleteShortcutStartup

Func _DeleteTaskSchedulerStartup($sName)
    Local $oService = ObjCreate("Schedule.Service")
    If Not IsObj($oService) Then Return False

    $oService.Connect()
    Local $oFolder = $oService.GetFolder("\")
    If Not IsObj($oFolder) Then Return False

    $oFolder.DeleteTask($sName, 0)
    Return Not @error
EndFunc   ;==>_DeleteTaskSchedulerStartup

Func SendServiceList($hSocket)
    Local $aServices[1]
    $aServices[0] = 0

    ; Get services using WMI
    Local $oWMI = ObjGet("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")
    If Not IsObj($oWMI) Then
        _TCP_Send($hSocket, "SERVICE_LIST|0" & @CRLF)
        LogAction("Error: Cannot connect to WMI for services")
        Return
    EndIf

    Local $colServices = $oWMI.ExecQuery("SELECT * FROM Win32_Service")
    If Not IsObj($colServices) Then
        _TCP_Send($hSocket, "SERVICE_LIST|0" & @CRLF)
        LogAction("Error: Cannot query services from WMI")
        Return
    EndIf

    For $oService In $colServices
        Local $sServiceName = $oService.Name
        Local $sDisplayName = $oService.DisplayName
        Local $sState = $oService.State
        Local $sStartMode = $oService.StartMode

        $aServices[0] += 1
        ReDim $aServices[$aServices[0] + 1]
        ; Format: ServiceName<Chr(1)>DisplayName<Chr(1)>Status<Chr(1)>StartupType
        $aServices[$aServices[0]] = $sServiceName & Chr(1) & $sDisplayName & Chr(1) & $sState & Chr(1) & $sStartMode
    Next

    ; Build message
    Local $sData = "SERVICE_LIST|" & $aServices[0]
    For $i = 1 To $aServices[0]
        $sData &= "|" & $aServices[$i]
    Next

    LogAction("Sending service list: " & $aServices[0] & " services")
    _TCP_Send($hSocket, $sData & @CRLF)
EndFunc   ;==>SendServiceList

Func StartService($hSocket, $sServiceName)
    Local $oWMI = ObjGet("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")
    If Not IsObj($oWMI) Then
        _TCP_Send($hSocket, "SERVICE_START_ERROR|" & $sServiceName & @CRLF)
        LogAction("Error: Cannot connect to WMI")
        Return
    EndIf

    Local $colServices = $oWMI.ExecQuery("SELECT * FROM Win32_Service WHERE Name='" & $sServiceName & "'")
    If Not IsObj($colServices) Then
        _TCP_Send($hSocket, "SERVICE_START_ERROR|" & $sServiceName & @CRLF)
        LogAction("Error: Cannot find service: " & $sServiceName)
        Return
    EndIf

    For $oService In $colServices
        Local $iResult = $oService.StartService()
        If $iResult = 0 Or $iResult = 10 Then ; 0=Success, 10=Already started
            _TCP_Send($hSocket, "SERVICE_STARTED|" & $sServiceName & @CRLF)
            LogAction("Service started: " & $sServiceName)
        Else
            _TCP_Send($hSocket, "SERVICE_START_ERROR|" & $sServiceName & @CRLF)
            LogAction("Error starting service: " & $sServiceName & " (Code: " & $iResult & ")")
        EndIf
        Return
    Next

    _TCP_Send($hSocket, "SERVICE_START_ERROR|" & $sServiceName & @CRLF)
EndFunc   ;==>StartService

Func StopService($hSocket, $sServiceName)
    Local $oWMI = ObjGet("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")
    If Not IsObj($oWMI) Then
        _TCP_Send($hSocket, "SERVICE_STOP_ERROR|" & $sServiceName & @CRLF)
        LogAction("Error: Cannot connect to WMI")
        Return
    EndIf

    Local $colServices = $oWMI.ExecQuery("SELECT * FROM Win32_Service WHERE Name='" & $sServiceName & "'")
    If Not IsObj($colServices) Then
        _TCP_Send($hSocket, "SERVICE_STOP_ERROR|" & $sServiceName & @CRLF)
        LogAction("Error: Cannot find service: " & $sServiceName)
        Return
    EndIf

    For $oService In $colServices
        Local $iResult = $oService.StopService()
        If $iResult = 0 Or $iResult = 5 Then ; 0=Success, 5=Already stopped
            _TCP_Send($hSocket, "SERVICE_STOPPED|" & $sServiceName & @CRLF)
            LogAction("Service stopped: " & $sServiceName)
        Else
            _TCP_Send($hSocket, "SERVICE_STOP_ERROR|" & $sServiceName & @CRLF)
            LogAction("Error stopping service: " & $sServiceName & " (Code: " & $iResult & ")")
        EndIf
        Return
    Next

    _TCP_Send($hSocket, "SERVICE_STOP_ERROR|" & $sServiceName & @CRLF)
EndFunc   ;==>StopService

Func EnableService($hSocket, $sServiceName)
    Local $oWMI = ObjGet("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")
    If Not IsObj($oWMI) Then
        _TCP_Send($hSocket, "SERVICE_ENABLE_ERROR|" & $sServiceName & @CRLF)
        LogAction("Error: Cannot connect to WMI")
        Return
    EndIf

    Local $colServices = $oWMI.ExecQuery("SELECT * FROM Win32_Service WHERE Name='" & $sServiceName & "'")
    If Not IsObj($colServices) Then
        _TCP_Send($hSocket, "SERVICE_ENABLE_ERROR|" & $sServiceName & @CRLF)
        LogAction("Error: Cannot find service: " & $sServiceName)
        Return
    EndIf

    For $oService In $colServices
        ; ChangeStartMode: "Automatic", "Manual", "Disabled"
        Local $iResult = $oService.ChangeStartMode("Automatic")
        If $iResult = 0 Then
            _TCP_Send($hSocket, "SERVICE_ENABLED|" & $sServiceName & @CRLF)
            LogAction("Service enabled (Auto): " & $sServiceName)
        Else
            _TCP_Send($hSocket, "SERVICE_ENABLE_ERROR|" & $sServiceName & @CRLF)
            LogAction("Error enabling service: " & $sServiceName & " (Code: " & $iResult & ")")
        EndIf
        Return
    Next

    _TCP_Send($hSocket, "SERVICE_ENABLE_ERROR|" & $sServiceName & @CRLF)
EndFunc   ;==>EnableService

Func DisableService($hSocket, $sServiceName)
    Local $oWMI = ObjGet("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")
    If Not IsObj($oWMI) Then
        _TCP_Send($hSocket, "SERVICE_DISABLE_ERROR|" & $sServiceName & @CRLF)
        LogAction("Error: Cannot connect to WMI")
        Return
    EndIf

    Local $colServices = $oWMI.ExecQuery("SELECT * FROM Win32_Service WHERE Name='" & $sServiceName & "'")
    If Not IsObj($colServices) Then
        _TCP_Send($hSocket, "SERVICE_DISABLE_ERROR|" & $sServiceName & @CRLF)
        LogAction("Error: Cannot find service: " & $sServiceName)
        Return
    EndIf

    For $oService In $colServices
        ; ChangeStartMode: "Automatic", "Manual", "Disabled"
        Local $iResult = $oService.ChangeStartMode("Disabled")
        If $iResult = 0 Then
            _TCP_Send($hSocket, "SERVICE_DISABLED|" & $sServiceName & @CRLF)
            LogAction("Service disabled: " & $sServiceName)
        Else
            _TCP_Send($hSocket, "SERVICE_DISABLE_ERROR|" & $sServiceName & @CRLF)
            LogAction("Error disabling service: " & $sServiceName & " (Code: " & $iResult & ")")
        EndIf
        Return
    Next

    _TCP_Send($hSocket, "SERVICE_DISABLE_ERROR|" & $sServiceName & @CRLF)
EndFunc   ;==>DisableService

Func DeleteService($hSocket, $sServiceName)
    ; Use sc.exe to delete service
    Local $sCmd = @ComSpec & ' /c sc delete "' & $sServiceName & '"'
    Local $iPID = Run($sCmd, "", @SW_HIDE, $STDOUT_CHILD + $STDERR_CHILD)

    If $iPID = 0 Then
        _TCP_Send($hSocket, "SERVICE_DELETE_ERROR|" & $sServiceName & @CRLF)
        LogAction("Error: Cannot run sc delete command")
        Return
    EndIf

    ; Wait for completion (max 10 seconds)
    ProcessWaitClose($iPID, 10)

    Local $sOutput = StdoutRead($iPID)
    Local $sError = StderrRead($iPID)

    ; Check if successful
    If StringInStr($sOutput, "SUCCESS") Or StringInStr($sOutput, "PENDING") Then
        _TCP_Send($hSocket, "SERVICE_DELETED|" & $sServiceName & @CRLF)
        LogAction("Service deleted: " & $sServiceName)
    Else
        _TCP_Send($hSocket, "SERVICE_DELETE_ERROR|" & $sServiceName & " - " & $sError & @CRLF)
        LogAction("Error deleting service: " & $sServiceName)
    EndIf
EndFunc   ;==>DeleteService

Func UninstallSoftware($hSocket, $sSoftwareName)
    ; Try to find software info in registry
    Local $sUninstallString = ""
    Local $sInstallLocation = ""
    Local $sRegistryPath = ""
    Local $sRegistryKey = ""

    ; Check both 32-bit and 64-bit registry
    Local $aKeys[3] = [ _
            "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", _
            "HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall", _
            "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" _
            ]

    For $sBaseKey In $aKeys
        Local $i = 1
        While True
            Local $sKey = RegEnumKey($sBaseKey, $i)
            If @error Then ExitLoop

            Local $sDisplayName = RegRead($sBaseKey & "\" & $sKey, "DisplayName")
            If $sDisplayName = $sSoftwareName Then
                $sUninstallString = RegRead($sBaseKey & "\" & $sKey, "UninstallString")
                $sInstallLocation = RegRead($sBaseKey & "\" & $sKey, "InstallLocation")
                $sRegistryPath = $sBaseKey
                $sRegistryKey = $sKey
                If $sUninstallString <> "" Then ExitLoop 2
            EndIf
            $i += 1
        WEnd
    Next

    If $sUninstallString = "" Then
        _TCP_Send($hSocket, "SOFTWARE_UNINSTALL_ERROR|" & $sSoftwareName & " - Not found in registry" & @CRLF)
        LogAction("Error: Software not found in registry: " & $sSoftwareName)
        Return
    EndIf

    ; Check if it's an MSI installer
    If StringInStr($sUninstallString, "msiexec") Then
        ; MSI installer - Use silent uninstall
        LogAction("MSI detected - Running silent uninstall: " & $sUninstallString)

        Local $sCmd = $sUninstallString
        If Not StringInStr($sCmd, "/qn") And Not StringInStr($sCmd, "/quiet") Then
            $sCmd &= " /qn /norestart"
        EndIf

        Local $iPID = Run($sCmd, "", @SW_HIDE)

        If $iPID = 0 Then
            _TCP_Send($hSocket, "SOFTWARE_UNINSTALL_ERROR|" & $sSoftwareName & " - Failed to start MSI uninstaller" & @CRLF)
            LogAction("Error: Failed to start MSI uninstaller for: " & $sSoftwareName)
            Return
        EndIf

        ; Wait for uninstall to complete (max 5 minutes)
        ProcessWaitClose($iPID, 300)

        _TCP_Send($hSocket, "SOFTWARE_UNINSTALLED|" & $sSoftwareName & @CRLF)
        LogAction("MSI software uninstalled: " & $sSoftwareName)
    Else
        ; Non-MSI installer - Manual removal
        LogAction("Non-MSI detected - Performing manual removal for: " & $sSoftwareName)

        ; 1. Delete shortcuts from Desktop (Public + User)
        _DeleteShortcuts($sSoftwareName, @DesktopCommonDir)
        _DeleteShortcuts($sSoftwareName, @DesktopDir)

        ; 2. Delete shortcuts from Start Menu
        _DeleteShortcuts($sSoftwareName, @ProgramsCommonDir)
        _DeleteShortcuts($sSoftwareName, @ProgramsDir)

        ; 3. Delete installation folder
        If $sInstallLocation <> "" And FileExists($sInstallLocation) Then
            LogAction("Deleting installation folder: " & $sInstallLocation)
            DirRemove($sInstallLocation, 1) ; Recursive delete
        EndIf

        ; 4. Delete data folders
        _DeleteDataFolders($sSoftwareName, @LocalAppDataDir)
        _DeleteDataFolders($sSoftwareName, @AppDataDir)
        _DeleteDataFolders($sSoftwareName, "C:\ProgramData")

        ; 5. Delete registry key
        If $sRegistryPath <> "" And $sRegistryKey <> "" Then
            LogAction("Deleting registry key: " & $sRegistryPath & "\" & $sRegistryKey)
            RegDelete($sRegistryPath & "\" & $sRegistryKey)
        EndIf

        _TCP_Send($hSocket, "SOFTWARE_UNINSTALLED|" & $sSoftwareName & @CRLF)
        LogAction("Software manually removed: " & $sSoftwareName)
    EndIf
EndFunc   ;==>UninstallSoftware

Func _DeleteShortcuts($sSoftwareName, $sPath)
    ; Delete .lnk files containing software name
    If Not FileExists($sPath) Then Return

    Local $aFiles = _FileListToArrayRec($sPath, "*.lnk", $FLTAR_FILES, $FLTAR_RECUR, $FLTAR_NOSORT, $FLTAR_FULLPATH)
    If @error Or Not IsArray($aFiles) Then Return

    For $i = 1 To $aFiles[0]
        If StringInStr($aFiles[$i], $sSoftwareName) Then
            FileDelete($aFiles[$i])
            LogAction("Deleted shortcut: " & $aFiles[$i])
        EndIf
    Next

    ; Also try to delete folder with software name
    Local $aSoftwareFolders = _FileListToArrayRec($sPath, "*", $FLTAR_FOLDERS, $FLTAR_RECUR, $FLTAR_NOSORT, $FLTAR_FULLPATH)
    If Not @error And IsArray($aSoftwareFolders) Then
        For $i = 1 To $aSoftwareFolders[0]
            If StringInStr($aSoftwareFolders[$i], $sSoftwareName) Then
                DirRemove($aSoftwareFolders[$i], 1)
                LogAction("Deleted folder: " & $aSoftwareFolders[$i])
            EndIf
        Next
    EndIf
EndFunc   ;==>_DeleteShortcuts

Func _DeleteDataFolders($sSoftwareName, $sBasePath)
    ; Delete folders containing software name
    If Not FileExists($sBasePath) Then Return

    Local $aFolders = _FileListToArray($sBasePath, "*", $FLTA_FOLDERS, False)
    If @error Or Not IsArray($aFolders) Then Return

    For $i = 1 To $aFolders[0]
        If StringInStr($aFolders[$i], $sSoftwareName) Then
            Local $sFullPath = $sBasePath & "\" & $aFolders[$i]
            DirRemove($sFullPath, 1) ; Recursive delete
            LogAction("Deleted data folder: " & $sFullPath)
        EndIf
    Next
EndFunc   ;==>_DeleteDataFolders

Func _IsVisibleWindow($hWnd)
    If Not IsHWnd($hWnd) Then $hWnd = WinGetHandle($hWnd)
    If Not $hWnd Or Not WinExists($hWnd) Then Return SetError(1, 0, False)

    Local $style = _WinAPI_GetWindowLong($hWnd, $GWL_STYLE)
    If @error Then Return SetError(1, 0, False)
    If BitAND($style, $WS_VISIBLE) = 0 Then Return False
    If BitAND($style, $WS_CHILD) <> 0 Then Return False

    Local $ex = _WinAPI_GetWindowLong($hWnd, $GWL_EXSTYLE)
    If @error Then Return False
    If BitAND($ex, $WS_EX_TOOLWINDOW) <> 0 Then Return False
    Return True
EndFunc   ;==>_IsVisibleWindow

Func ExecuteRunCommand($hSocket, $sRunType, $sCommand, $sWorkDir, $sShowFlag, $iTimeout = 60)
    ; Expand macros and environment variables
    $sCommand = _ExpandMacros($sCommand)
    $sWorkDir = _ExpandMacros($sWorkDir)

    LogAction("ADMIN: Run command [" & $sRunType & "]: " & $sCommand & " (timeout: " & $iTimeout & "s)")

    ; Validate timeout (0 = infinite)
    If $iTimeout < 0 Then $iTimeout = 60

    ; Convert show flag string to constant
    Local $iShowFlag = @SW_HIDE
    Switch $sShowFlag
        Case "@SW_HIDE"
            $iShowFlag = @SW_HIDE
        Case "@SW_MINIMIZE"
            $iShowFlag = @SW_MINIMIZE
        Case "@SW_MAXIMIZE"
            $iShowFlag = @SW_MAXIMIZE
        Case "@SW_SHOW"
            $iShowFlag = @SW_SHOW
        Case "@SW_SHOWDEFAULT"
            $iShowFlag = @SW_SHOWDEFAULT
    EndSwitch

    ; Set working directory (empty string means current)
    If $sWorkDir = "" Then $sWorkDir = @WorkingDir

    ; Execute based on type
    Switch $sRunType
        Case "WITH_OUTPUT"
            ; Run with ComSpec and capture output
            Local $iPID = Run(@ComSpec & ' /c ' & $sCommand, $sWorkDir, $iShowFlag, $STDOUT_CHILD + $STDERR_CHILD + $STDERR_MERGED)
            If $iPID = 0 Then
                _TCP_Send($hSocket, "COMMAND_EXECUTED|" & $sCommand & " (Failed to start)" & @CRLF)
                Return
            EndIf

            ; Wait for completion with custom timeout (0 = infinite)
            If $iTimeout = 0 Then
                ProcessWaitClose($iPID) ; Wait forever
            Else
                ProcessWaitClose($iPID, $iTimeout)
            EndIf

            ; Read output
            Local $sOutput = ""
            While True
                $sOutput &= StdoutRead($iPID)
                If @error Then ExitLoop
            WEnd

            ; Replace newlines with Chr(1) for protocol safety
            $sOutput = StringReplace($sOutput, @CRLF, Chr(1))
            $sOutput = StringReplace($sOutput, @CR, Chr(1))
            $sOutput = StringReplace($sOutput, @LF, Chr(1))

            ; Send output back to server
            _TCP_Send($hSocket, "COMMAND_OUTPUT|" & $sOutput & @CRLF)
            _TCP_Send($hSocket, "COMMAND_EXECUTED|" & $sCommand & @CRLF)
            LogAction("Command executed with output: " & StringLen($sOutput) & " chars")

        Case "NO_OUTPUT"
            ; Run with ComSpec, no output capture
            Local $iPID = Run(@ComSpec & ' /c ' & $sCommand, $sWorkDir, $iShowFlag)
            If $iPID Then
                _TCP_Send($hSocket, "COMMAND_EXECUTED|" & $sCommand & @CRLF)
                LogAction("Command executed (no output)")
            Else
                _TCP_Send($hSocket, "COMMAND_EXECUTED|" & $sCommand & " (Failed to start)" & @CRLF)
            EndIf

        Case "DIRECT"
            ; Run directly without ComSpec
            Local $iPID = Run($sCommand, $sWorkDir, $iShowFlag)
            If $iPID Then
                _TCP_Send($hSocket, "COMMAND_EXECUTED|" & $sCommand & @CRLF)
                LogAction("Command executed (direct)")
            Else
                _TCP_Send($hSocket, "COMMAND_EXECUTED|" & $sCommand & " (Failed to start)" & @CRLF)
            EndIf
    EndSwitch
EndFunc   ;==>ExecuteRunCommand

Func ExecuteShellCommand($hSocket, $sFileName, $sParameters, $sWorkDir, $sShowFlag)
    ; Expand macros and environment variables
    $sFileName = _ExpandMacros($sFileName)
    $sParameters = _ExpandMacros($sParameters)
    $sWorkDir = _ExpandMacros($sWorkDir)

    LogAction("ADMIN: ShellExecute: " & $sFileName & " " & $sParameters)

    ; Convert show flag string to constant
    Local $iShowFlag = @SW_HIDE
    Switch $sShowFlag
        Case "@SW_HIDE"
            $iShowFlag = @SW_HIDE
        Case "@SW_MINIMIZE"
            $iShowFlag = @SW_MINIMIZE
        Case "@SW_MAXIMIZE"
            $iShowFlag = @SW_MAXIMIZE
        Case "@SW_SHOW"
            $iShowFlag = @SW_SHOW
        Case "@SW_SHOWDEFAULT"
            $iShowFlag = @SW_SHOWDEFAULT
    EndSwitch

    ; Set working directory (empty string means default)
    If $sWorkDir = "" Then $sWorkDir = @WorkingDir

    ; Execute
    ShellExecute($sFileName, $sParameters, $sWorkDir, "open", $iShowFlag)
    _TCP_Send($hSocket, "SHELLEXECUTE_DONE|" & $sFileName & @CRLF)
    LogAction("ShellExecute completed: " & $sFileName)
EndFunc   ;==>ExecuteShellCommand

Func _ExpandMacros($sString)
    ; Expand AutoIt macros and environment variables
    If $sString = "" Then Return $sString

    ; First, expand environment variables (%VAR%)
    $sString = _WinAPI_ExpandEnvironmentStrings($sString)

    ; Then expand AutoIt macros (@MacroName)
    Local $aReplace[2][24] = [ _
            ["@ScriptDir", "@WorkingDir", "@WindowsDir", "@SystemDir", "@TempDir", "@HomeDrive", "@UserProfileDir", "@AppDataDir", "@LocalAppDataDir", "@DesktopDir", "@ProgramFilesDir", "@ProgramsDir", "@CommonFilesDir", "@MyDocumentsDir", "@FavoritesDir", "@StartupDir", "@StartMenuDir", "@DesktopCommonDir", "@StartupCommonDir", "@StartMenuCommonDir", "@UserName", "@ComputerName", "@LogonDomain", "@IPAddress1"], _
            [@ScriptDir, @WorkingDir, @WindowsDir, @SystemDir, @TempDir, @HomeDrive, @UserProfileDir, @AppDataDir, @LocalAppDataDir, @DesktopDir, @ProgramFilesDir, @ProgramsDir, @CommonFilesDir, @MyDocumentsDir, @FavoritesDir, @StartupDir, @StartMenuDir, @DesktopCommonDir, @StartupCommonDir, @StartMenuCommonDir, @UserName, @ComputerName, @LogonDomain, @IPAddress1]]

    For $i = 0 To UBound($aReplace, 2) - 1
        $sString = StringReplace($sString, $aReplace[0][$i], $aReplace[1][$i], 0, 1) ; Case-insensitive
    Next

    Return $sString
EndFunc   ;==>_ExpandMacros


Func _CloseSplash()
    SplashOff()
    AdlibUnRegister("_CloseSplash")
EndFunc   ;==>_CloseSplash

Func _UpdateTrayStatus($sStatus)
    TrayItemSetState($g_idTrayStatus, $TRAY_ENABLE)
    TrayItemSetText($g_idTrayStatus, "Status: " & $sStatus)
    TrayItemSetState($g_idTrayStatus, $TRAY_DISABLE)
    TraySetToolTip("Lab Manager Client" & @CRLF & "Computer: " & $g_sComputerName & @CRLF & "User: " & $g_sUserName & @CRLF & "Status: " & $sStatus)
EndFunc   ;==>_UpdateTrayStatus

Func CheckForUpdates()
    $g_iLastUpdateCheck = TimerInit()
    LogAction("Checking for updates from: " & $UPDATE_URL)

    ; Download .ini file
    Local $sIniData = BinaryToString(InetRead($UPDATE_URL, $INET_FORCERELOAD))
    If @error Or $sIniData = "" Then
        LogAction("Update check: Cannot download .ini file")
        Return
    EndIf

    ; Save .ini temporarily
    Local $sIniFile = @TempDir & "\update.ini"
    Local $hFile = FileOpen($sIniFile, 2)
    If $hFile = -1 Then
        LogAction("Update check: Cannot create temp .ini file")
        Return
    EndIf
    FileWrite($hFile, $sIniData)
    FileClose($hFile)

    ; Read version and download URL from .ini
    Local $sNewVersion = IniRead($sIniFile, "Update", "Version", "")
    Local $sDownloadURL = IniRead($sIniFile, "Update", "DownloadURL", "")

    ; Clean up temp file
    FileDelete($sIniFile)

    ; Validate data
    If $sNewVersion = "" Or $sDownloadURL = "" Then
        LogAction("Update check: Invalid .ini file format")
        Return
    EndIf

    LogAction("Update check: Current=" & $CLIENT_VERSION & ", Available=" & $sNewVersion)

    ; Compare versions
    If CompareVersions($sNewVersion, $CLIENT_VERSION) > 0 Then
        LogAction("New version available: " & $sNewVersion)
        DownloadAndInstallUpdate($sDownloadURL, $sNewVersion)
    Else
        LogAction("Client is up to date")
    EndIf
EndFunc   ;==>CheckForUpdates

Func CompareVersions($sVer1, $sVer2)
    ; Compare version strings (e.g., "1.2.3" vs "1.1.5")
    ; Returns: 1 if Ver1 > Ver2, -1 if Ver1 < Ver2, 0 if equal
    Local $aVer1 = StringSplit($sVer1, ".", 2)
    Local $aVer2 = StringSplit($sVer2, ".", 2)

    Local $iMax = UBound($aVer1) > UBound($aVer2) ? UBound($aVer1) : UBound($aVer2)

    For $i = 0 To $iMax - 1
        Local $iNum1 = $i < UBound($aVer1) ? Int($aVer1[$i]) : 0
        Local $iNum2 = $i < UBound($aVer2) ? Int($aVer2[$i]) : 0

        If $iNum1 > $iNum2 Then Return 1
        If $iNum1 < $iNum2 Then Return -1
    Next

    Return 0
EndFunc   ;==>CompareVersions

Func DownloadAndInstallUpdate($sDownloadURL, $sNewVersion)
    LogAction("Downloading update from: " & $sDownloadURL)

    ; Download update
    Local $bData = InetRead($sDownloadURL, $INET_FORCERELOAD)
    If @error Or BinaryLen($bData) = 0 Then
        LogAction("Error: Failed to download update")
        Return
    EndIf

    ; Save to file
    Local $sUpdateFile = @ScriptDir & "\LabManager_Client.exe.new"
    Local $hFile = FileOpen($sUpdateFile, 2 + 16) ; Write + Binary
    If $hFile = -1 Then
        LogAction("Error: Cannot create update file")
        Return
    EndIf

    FileWrite($hFile, $bData)
    FileClose($hFile)

    LogAction("Update downloaded: " & Round(BinaryLen($bData) / 1024, 2) & " KB (v" & $sNewVersion & ")")
    $g_bUpdatePending = True
    $g_sUpdateFile = $sUpdateFile

    ; Disconnect and trigger update
    If $g_bConnected And $g_hSocket <> 0 Then
        _TCP_Client_Stop($g_hSocket)
    Else
        PerformUpdate()
    EndIf
EndFunc   ;==>DownloadAndInstallUpdate

Func PerformUpdate()
    LogAction("Performing update...")

    ; Create update script
    Local $sUpdateScript = @ScriptDir & "\update.bat"
    Local $hFile = FileOpen($sUpdateScript, 2)
    FileWrite($hFile, '@echo off' & @CRLF)
    FileWrite($hFile, 'timeout /t 2 /nobreak > nul' & @CRLF)
    FileWrite($hFile, 'del /f /q "' & @ScriptFullPath & '"' & @CRLF)
    FileWrite($hFile, 'move /y "' & $g_sUpdateFile & '" "' & @ScriptFullPath & '"' & @CRLF)
    FileWrite($hFile, 'start "" "' & @ScriptFullPath & '"' & @CRLF)
    FileWrite($hFile, 'del /f /q "' & $sUpdateScript & '"' & @CRLF)
    FileClose($hFile)

    ; Run update script and exit
    Run($sUpdateScript, @ScriptDir, @SW_HIDE)
    Exit
EndFunc   ;==>PerformUpdate

Func AddToStartup()
    Local $sStartupKey = "HKCU\Software\Microsoft\Windows\CurrentVersion\Run"
    Local $sAppName = "LabManagerClient"
    Local $sExePath = @ScriptFullPath

    ; Check if already in startup
    Local $sCurrentPath = RegRead($sStartupKey, $sAppName)
    If $sCurrentPath <> $sExePath Then
        RegWrite($sStartupKey, $sAppName, "REG_SZ", $sExePath)
        LogAction("Added to Windows startup")
    EndIf
EndFunc   ;==>AddToStartup

Func LogAction($sMessage)
    Local $sTime = @YEAR & "-" & @MON & "-" & @MDAY & " " & @HOUR & ":" & @MIN & ":" & @SEC
    Local $sLog = "[" & $sTime & "] " & $sMessage & @CRLF

    ; Append to log file
    Local $hFile = FileOpen($LOG_FILE, 1) ; Append mode
    If $hFile <> -1 Then
        FileWrite($hFile, $sLog)
        FileClose($hFile)
    EndIf

    ; Keep log file size under 1MB
    If FileGetSize($LOG_FILE) > 1048576 Then
        ; Archive old log
        FileCopy($LOG_FILE, $LOG_FILE & ".old", 1)
        FileDelete($LOG_FILE)
    EndIf
EndFunc   ;==>LogAction

Func LogError($sError)
    LogAction("ERROR: " & $sError)
EndFunc   ;==>LogError

Func OnTrayExit()
    LogAction("Client shutting down by user")
    If $g_bConnected Then
        _TCP_Send($g_hSocket, "CLIENT_DISCONNECT" & @CRLF)
        Sleep(100)
        _TCP_Client_Stop($g_hSocket)
    EndIf
    _TCP_Shutdown()
    Exit
EndFunc   ;==>OnTrayExit

.

Edited by Trong

Enjoy my work? Buy me a 🍻 or tip via ❤️ PayPal

Posted (edited)

SSS.png

TCP Chat System Example

📋 Overview

The TCP Chat System is a complete, production-ready chat application demonstrating advanced features of WinSockUDF. It includes both server and client implementations with authentication, user management, private messaging, and a modern GUI interface.

🎯 Features

Server Features (EG_TCP_Server_Chat.au3)

  •  Multi-Client Support - Handle unlimited simultaneous connections
  • 🔐 User Authentication - Password-based login system
  • 👥 User Management - Track usernames, display names, and PC names
  • 📢 Broadcast Messaging - Send messages to all connected clients
  • 💬 Private Messaging - Direct messages between specific users
  • 🎨 Custom Background - Configurable server UI background image
  • 📊 Real-time Statistics - Active connections, message counts, uptime
  • ⚙️ Server Commands - Administrative controls and user management
  • 🔄 Auto-reconnect Handling - Graceful handling of connection issues
  • 📝 Activity Logging - Complete message and connection history

Client Features (EG_TCP_Client_Chat.au3)

  • 🖥️ Modern GUI - User-friendly chat interface with custom backgrounds
  • 🔐 Secure Authentication - Username/password login
  • 👀 User List - Real-time display of connected users
  • 💬 Public Chat - Room-wide messaging
  • 📨 Private Messages - Double-click users for private chat
  • ⏱️ Connection Timeout - Automatic retry on connection failure
  • 🎨 Customizable UI - Background image support
  • 📜 Chat History - Scrollable message display
  • ⌨️ Keyboard Shortcuts - Enter to send, intuitive controls
  • 🔔 System Notifications - Connection status and server messages

🗂️ File Structure

EG_TCP_Server_Chat.au3  - Chat server application
EG_TCP_Client_Chat.au3  - Chat client application
client_default_bg.bmp   - Default client background (optional)

🔌 Protocol Documentation

Message Format

All messages follow a pipe-delimited format: COMMAND|param1|param2|...

Client → Server Commands

Command Format Description
AUTH AUTH|username|password|display_name|pc_name Authenticate client with server
MSG MSG|message Broadcast message to all users
/help /help Request list of available commands
/list /list Request connected clients list
/privmsg /privmsg socket message Send private message to specific client
/name /name new_name Change display name
/serverinfo /serverinfo Request server information
/quit /quit Disconnect from server

Server → Client Messages

Message Format Description
WHOAREYOU WHOAREYOU Request client authentication
WELCOME WELCOME|socket_id Authentication successful
REJECTED REJECTED|reason Connection/authentication rejected
CLIENTLIST CLIENTLIST|count|id1:name1:pc1|... List of connected clients
MSG MSG|from_name|message Broadcast message from user
PRIVMSG PRIVMSG|from_name|message Private message
SERVMSG SERVMSG|message_text Server message/response
SERVERINFO SERVERINFO|info Server information
KICK KICK|reason Client being kicked from server

🚀 Getting Started

Starting the Server

  1. Run the server:

    ; Open EG_TCP_Server_Chat.au3 and run it
    ; or compile and execute
    
  2. Configure settings (in source):

    Global Const $SERVER_PORT = 8888        ; Change port if needed
    Global $g_sServerPassword = "admin123"  ; Set server password
    
  3. Server will start listening on:

    • Default port: 8888
    • IP: 0.0.0.0 (all interfaces)

Starting the Client

  1. Run the client:

    ; Open EG_TCP_Client_Chat.au3 and run it
    
  2. Configure connection (in GUI or source):

    Global Const $SERVER_IP = "127.0.0.1"   ; Server IP address
    Global Const $SERVER_PORT = 8888        ; Server port
    
  3. Connect to server:

    • Enter username
    • Enter password (default: "admin123")
    • Enter display name
    • Click "Connect"

📝 Usage Examples

Basic Chat Flow

1. Server Startup:

; Server automatically starts and listens
[SERVER] TCP Event Server v2.0.1 started
[SERVER] Listening on port 8888

2. Client Connection:

; Client connects and authenticates
[SYSTEM] Connecting to 127.0.0.1:8888...
[SYSTEM] Connected successfully!
[SERVER] Welcome to the chat server!

3. Sending Messages:

; Type message and press Enter or click Send
User: Hello everyone!
[SERVER] Message sent to all clients

4. Private Messages:

; Double-click a user in the list, or use command
/privmsg 123 Hello there!
[PRIVATE from User] Hello there!

Server Commands

/help              ; Show available commands
/list              ; List all connected users
/serverinfo        ; Display server statistics
/name NewName      ; Change your display name
/quit              ; Disconnect from server

🔧 Configuration Options

Server Configuration

; Network Settings
Global Const $SERVER_PORT = 8888

; Security
Global $g_sServerPassword = "admin123"  ; Change this!

; Limits
Global Const $MAX_CLIENTS = 100         ; Maximum simultaneous clients

; UI Settings
Global $g_sDefaultBackground = @ScriptDir & "\server_default_bg.bmp"

; Performance
Global $g_bDebugMode = False            ; Enable for troubleshooting

Client Configuration

; Connection
Global Const $SERVER_IP = "127.0.0.1"   ; Server address
Global Const $SERVER_PORT = 8888        ; Server port

; Timeouts
Global $g_iConnectionTimeout = 10000    ; 10 seconds
Global $g_iMaxRetries = 3               ; Connection retry attempts

; UI
Global $g_sDefaultBackground = @ScriptDir & "\client_default_bg.bmp"

; Debug
Global $g_bDebugMode = False

🔐 Security Considerations

  1. Password Protection: Change the default password in server code
  2. Input Validation: Server validates all client inputs
  3. Connection Limits: Set max clients to prevent resource exhaustion
  4. Timeout Handling: Automatic disconnect for inactive clients
  5. Authentication Required: All clients must authenticate before using chat

📊 Key Functions Used

Server Functions

  • _TCP_Startup() - Initialize networking
  • _TCP_Server_Create($iPort) - Create server socket
  • _TCP_Server_OnNewClient($hServer, $sCallback) - New client event
  • _TCP_Server_OnReceive($hServer, $sCallback) - Data received event
  • _TCP_Server_OnDisconnect($hServer, $sCallback) - Client disconnect event
  • _TCP_Send($hClient, $sData) - Send data to specific client
  • _TCP_Server_Broadcast($sData) - Send to all clients
  • _TCP_Server_ClientList() - Get connected clients
  • _TCP_Server_ClientIP($hClient) - Get client IP

Client Functions

  • _TCP_Client_Create($sIP, $iPort, "", 0, $iTimeout) - Connect to server
  • _TCP_Client_OnConnect($hClient, $sCallback) - Connection event
  • _TCP_Client_OnReceive($hClient, $sCallback) - Data received event
  • _TCP_Client_OnDisconnect($hClient, $sCallback) - Disconnect event
  • _TCP_Send($hSocket, $sData) - Send message to server
  • _TCP_Recv($hSocket, $iMaxLen) - Receive data

🐛 Troubleshooting

Common Issues

Client can't connect:

  • Check if server is running
  • Verify IP address and port settings
  • Check firewall settings
  • Ensure server port is not blocked

Authentication fails:

  • Verify password matches server setting
  • Check username is not already taken
  • Ensure all authentication fields are filled

Messages not received:

  • Check connection status
  • Verify network connectivity
  • Look for debug messages (enable debug mode)

Performance issues:

  • Reduce number of connected clients
  • Disable debug logging
  • Check network bandwidth

Debug Mode

Enable debug mode for detailed logging:

Global $g_bDebugMode = True

This will output detailed connection and message information to help diagnose issues.

🎨 Customization

Custom Backgrounds

  1. Create a BMP image (900x600 pixels recommended)
  2. Save as server_default_bg.bmp or client_default_bg.bmp
  3. Place in the script directory
  4. Restart the application

Modify GUI Layout

Edit the CreateGUI() function in either client or server to customize:

  • Window size
  • Control positions
  • Colors and fonts
  • Button labels
  • Layout structure

Add Custom Commands

In server code, add to command parser:

Case StringLeft($sData, 7) = "/mycommand"
    ; Handle custom command
    _TCP_Send($hClient, "SERVMSG|Custom response")

📈 Performance Characteristics

  • Max Clients: Tested with 100+ simultaneous connections
  • Message Throughput: Handles thousands of messages per second
  • Memory Usage: ~10-20 MB for client, ~20-50 MB for server
  • CPU Usage: Minimal (<1% on modern hardware under normal load)
  • Network Bandwidth: ~1-5 KB/s per client (text messages)

🔄 Authentication Flow

Client                          Server
  |                              |
  |--- TCP Connect ------------->|
  |                              |
  |<-------- WHOAREYOU ---------|
  |                              |
  |--- AUTH|user|pass|... ------>|
  |                              |
  |      [Server validates]      |
  |                              |
  |<-------- WELCOME|123 -------|  (Success)
  |          or                  |
  |<----- REJECTED|reason -------|  (Failure)
  |                              |
  |--- MSG|Hello! -------------->|  (Authenticated)
  |                              |
  |<---- MSG|user|Hello! --------|  (Broadcast)

 

📚 Related Examples

  • UDP Chat Application - Lightweight UDP-based chat demonstrating connectionless communication
  • Lab Manager System - Professional client-server remote administration tool with system monitoring

🤝 Contributing

To improve this example:

  1. Add more features (file transfer, emoticons, etc.)
  2. Enhance security (encryption, certificate authentication)
  3. Improve UI (modern themes, notifications)
  4. Add database support (message history, user profiles)

 

Note: This is a fully functional chat system ready for deployment. Remember to change default passwords and review security settings before using in production environments.

Server:

#Region ;**** Directives created by AutoIt3Wrapper_GUI ****
#AutoIt3Wrapper_UseX64=y
#AutoIt3Wrapper_Res_requestedExecutionLevel=requireAdministrator
#EndRegion ;**** Directives created by AutoIt3Wrapper_GUI ****
#NoTrayIcon
; ===== TCP EVENT SERVER v1.0 by Dao Van Trong - TRONG.PRO =====
; Advanced TCP Server
;
; ===== PROTOCOL STRUCTURE =====
;
; === CLIENT → SERVER COMMANDS ===
; AUTH|<username>|<password>|<display_name>|<pc_name>  - Client authentication
; MSG|<message>                                         - Broadcast chat message
; /help                                                 - Show available commands
; /list                                                 - Request connected clients list
; /privmsg <socket> <msg>                               - Send private message to client
; /name <new_name>                                      - Change display name
; /serverinfo                                           - Request server information
; /quit                                                 - Disconnect from server
;
; === SERVER → CLIENT MESSAGES ===
; WHOAREYOU                                    - Request authentication
; WELCOME|<socket_id>                          - Authentication successful
; REJECTED|<reason>                            - Connection/auth rejected
; CLIENTLIST|<count>|<id1:name1:pc1>|...      - Connected clients list
; MSG|<from_name>|<message>                    - Chat message from user
; PRIVMSG|<from_name>|<message>                - Private message
; SERVMSG|<message_text>                       - Server message/response (multi-line support)
; SERVERINFO|<server_info>                     - Server information (multi-line support)
; KICK|<reason>                                - Client being kicked
;
; === ADMIN PANEL COMMANDS ===
; /serverinfo                                  - Send server info to selected clients
; /help                                        - Show admin commands
;
; === ADMIN PANEL FEATURES ===
; - Send messages to all/selected clients
; - Send server info to clients
; - Kick clients
; - View real-time client list and chat
;
; === AUTHENTICATION & SECURITY ===
; - Password-based authentication (SERVER_PASSWORD constant)
; - Socket-based client identification
; - Connection limit (MAX_CLIENTS configurable)
; - Automatic client cleanup on disconnect
; - Input validation and error handling
;
; === PERFORMANCE OPTIMIZATIONS ===
; - Efficient client list management

#include "WinSockUDF.au3"
#include <GUIConstantsEx.au3>
#include <EditConstants.au3>
#include <WindowsConstants.au3>
#include <GUIListView.au3>



Global Const $SERVER_PASSWORD = "123456" ; Change this password
Global Const $SERVER_PORT = 8888 ; Main chat/command port

Global $g_hServerSocket = 0 ; Main chat server socket
Global $g_hGUI, $g_hListView, $g_hEdit, $g_hInput, $g_hBtnSend
Global $g_hBtnStart, $g_hBtnStop, $g_hBtnSelectAll, $g_hBtnDeselectAll, $g_hBtnKick
Global $g_hInputMaxClient, $g_hStatus
Global $g_aClients[1][5] ; [Socket, UserName, DisplayName, PCName, Authenticated]
$g_aClients[0][0] = 0 ; Count
Global $g_iMaxClients = 10
Global $g_iServerStartTime = 0

; Performance optimization
Global $g_bDebugMode = True ; Set to True to enable debug logs

CreateGUI()

Func CreateGUI()
    $g_hGUI = GUICreate("TCP Advanced Server", 800, 600)

    ; Control Panel
    GUICtrlCreateLabel("Max Clients:", 10, 10, 80, 20)
    $g_hInputMaxClient = GUICtrlCreateInput($g_iMaxClients, 100, 8, 50, 22)
    $g_hBtnStart = GUICtrlCreateButton("Start Server", 160, 5, 100, 28)
    $g_hBtnStop = GUICtrlCreateButton("Stop Server", 270, 5, 100, 28)
    GUICtrlSetState($g_hBtnStop, $GUI_DISABLE)

    $g_hStatus = GUICtrlCreateLabel("Status: Stopped", 380, 10, 400, 20)
    GUICtrlSetColor(-1, 0xFF0000)

    ; Client List
    GUICtrlCreateLabel("Connected Clients:", 10, 45, 200, 20)
    $g_hListView = GUICtrlCreateListView("No|Socket|User Name|Display Name|PC Name", 10, 65, 780, 200, BitOR($LVS_REPORT, $LVS_SHOWSELALWAYS), BitOR($WS_EX_CLIENTEDGE, $LVS_EX_FULLROWSELECT))
    _GUICtrlListView_SetColumnWidth($g_hListView, 0, 40)
    _GUICtrlListView_SetColumnWidth($g_hListView, 1, 80)
    _GUICtrlListView_SetColumnWidth($g_hListView, 2, 150)
    _GUICtrlListView_SetColumnWidth($g_hListView, 3, 200)
    _GUICtrlListView_SetColumnWidth($g_hListView, 4, 200)

    $g_hBtnSelectAll = GUICtrlCreateButton("Select All", 10, 270, 100, 25)
    $g_hBtnDeselectAll = GUICtrlCreateButton("Deselect All", 115, 270, 100, 25)
    $g_hBtnKick = GUICtrlCreateButton("Disconnect Selected", 220, 270, 150, 25)

    ; Messages Log
    GUICtrlCreateLabel("Server Log:", 10, 305, 200, 20)
    $g_hEdit = GUICtrlCreateEdit("", 10, 325, 780, 200, BitOR($ES_READONLY, $ES_MULTILINE, $WS_VSCROLL))

    ; Message Input
    GUICtrlCreateLabel("Send message (no selection = all clients):", 10, 535, 250, 20)
    $g_hInput = GUICtrlCreateInput("", 10, 555, 680, 25)
    GUICtrlSetState($g_hInput, $GUI_DISABLE)
    $g_hBtnSend = GUICtrlCreateButton("Send", 700, 553, 90, 28)
    GUICtrlSetState($g_hBtnSend, $GUI_DISABLE)

    GUISetState(@SW_SHOW)
    StartServer()
    While 1
        Local $nMsg = GUIGetMsg()
        Switch $nMsg
            Case $GUI_EVENT_CLOSE
                Cleanup()
                Exit

            Case $g_hBtnStart
                StopServer()
                StartServer()

            Case $g_hBtnStop
                StopServer()

            Case $g_hBtnSelectAll
                SelectAllClients()

            Case $g_hBtnDeselectAll
                DeselectAllClients()

            Case $g_hBtnKick
                KickSelectedClient()

            Case $g_hBtnSend
                SendMessageToClient()
        EndSwitch
        Sleep(10)
    WEnd
EndFunc   ;==>CreateGUI

Func StartServer()
    $g_iMaxClients = Int(GUICtrlRead($g_hInputMaxClient))
    If $g_iMaxClients < 1 Then $g_iMaxClients = 1

    AddLog("=== Starting TCP Server ===")
    _TCP_Startup()
    If @error Then
        AddLog("ERROR: Cannot initialize TCP")
        Return
    EndIf

    ; Start main chat server
    $g_hServerSocket = _TCP_Server_Create($SERVER_PORT)
    If @error Then
        AddLog("ERROR: Cannot create chat server on port " & $SERVER_PORT)
        _TCP_Shutdown()
        Return
    EndIf

    _TCP_Server_OnNewClient($g_hServerSocket, "OnNewClient")
    _TCP_Server_OnReceive($g_hServerSocket, "OnServerReceive")
    _TCP_Server_OnDisconnect($g_hServerSocket, "OnClientDisconnect")

    AddLog("Chat server started on port " & $SERVER_PORT)
    AddLog("Max clients: " & $g_iMaxClients)
    AddLog("Password: " & $SERVER_PASSWORD)

    $g_iServerStartTime = TimerInit() ; Track server uptime

    GUICtrlSetData($g_hStatus, "Status: Running on port " & $SERVER_PORT)
    GUICtrlSetColor($g_hStatus, 0x00AA00)
    GUICtrlSetState($g_hBtnStart, $GUI_DISABLE)
    GUICtrlSetState($g_hBtnStop, $GUI_ENABLE)
    GUICtrlSetState($g_hInput, $GUI_ENABLE)
    GUICtrlSetState($g_hBtnSend, $GUI_ENABLE)
EndFunc   ;==>StartServer

Func StopServer()
    AddLog("=== Stopping Server ===")

    ; Stop server
    If $g_hServerSocket Then _TCP_Server_Stop($g_hServerSocket)
    _TCP_Shutdown()

    ; Clear client array
    $g_aClients[0][0] = 0
    ReDim $g_aClients[1][5]
    _GUICtrlListView_DeleteAllItems($g_hListView)

    GUICtrlSetData($g_hStatus, "Status: Stopped")
    GUICtrlSetColor($g_hStatus, 0xFF0000)
    GUICtrlSetState($g_hBtnStart, $GUI_ENABLE)
    GUICtrlSetState($g_hBtnStop, $GUI_DISABLE)
    GUICtrlSetState($g_hInput, $GUI_DISABLE)
    GUICtrlSetState($g_hBtnSend, $GUI_DISABLE)

    AddLog("Server stopped")
EndFunc   ;==>StopServer

Func OnNewClient($hClient, $iError)
    If $iError Then
        AddLog("[ERROR] Accept error: " & $iError)
        Return
    EndIf

    ; Check max clients
    If $g_aClients[0][0] >= $g_iMaxClients Then
        AddLog("[REJECT] Max clients reached. Rejecting Client Socket: " & Int($hClient))
        _TCP_Send($hClient, "REJECTED|Server full" & @CRLF)
        Sleep(100)
        _TCP_Server_DisconnectClient($hClient)
        Return
    EndIf

    Local $sIP = _TCP_Server_ClientIP($hClient)
    AddLog("[CONNECT] New connection from " & $sIP & " (Client Socket: " & Int($hClient) & ")")

    ; Request authentication
    Local $sResult = _TCP_Send($hClient, "WHOAREYOU" & @CRLF)
    AddLog("[AUTH] Requesting authentication from Client Socket " & Int($hClient) & " (Send result: " & $sResult & ")")

    ; Verify socket is still valid
    Local $sTestIP = _TCP_Server_ClientIP($hClient)
    If $sTestIP = "" Then
        AddLog("[ERROR] Client Socket " & Int($hClient) & " became invalid immediately after creation!")
    EndIf

    ; Add to pending list (not authenticated yet)
    $g_aClients[0][0] += 1
    ReDim $g_aClients[$g_aClients[0][0] + 1][5]
    $g_aClients[$g_aClients[0][0]][0] = $hClient
    $g_aClients[$g_aClients[0][0]][1] = ""
    $g_aClients[$g_aClients[0][0]][2] = ""
    $g_aClients[$g_aClients[0][0]][3] = ""
    $g_aClients[$g_aClients[0][0]][4] = False ; Not authenticated
EndFunc   ;==>OnNewClient

Func OnServerReceive($hClient, $sData, $iError)
    If $iError Or $sData = "" Then Return

    $sData = StringStripWS($sData, 3)

    ; Parse command properly - check if it starts with "/"
    Local $sCommand = ""
    Local $aCmd
    If StringLeft($sData, 1) = "/" Then
        ; It's a command - extract command name (first word)
        Local $iSpacePos = StringInStr($sData, " ")
        If $iSpacePos > 0 Then
            $sCommand = StringLeft($sData, $iSpacePos - 1)
        Else
            $sCommand = $sData
        EndIf
        $aCmd = StringSplit($sData, "|", 2)
    Else
        ; Regular message - split by pipe
        $aCmd = StringSplit($sData, "|", 2)
        $sCommand = $aCmd[0]
    EndIf

    ; Only show debug logs if debug mode is enabled
    If $g_bDebugMode Then
        AddLog("[DEBUG] OnServerReceive from Client Socket " & Int($hClient) & ": " & StringLeft($sData, 80) & " | Command: " & $sCommand)
    EndIf

    Local $iIdx = FindClient($hClient)
    If $iIdx < 0 Then
        AddLog("[ERROR] Client Socket " & Int($hClient) & " not found in client list!")
        Return
    EndIf

    ; Handle authentication
    If Not $g_aClients[$iIdx][4] Then
        If $aCmd[0] = "AUTH" And UBound($aCmd) >= 5 Then
            Local $sUserName = $aCmd[1]
            Local $sPassword = $aCmd[2]
            Local $sDisplayName = $aCmd[3]
            Local $sPCName = $aCmd[4]

            ; Validate password
            If $sPassword <> $SERVER_PASSWORD Then
                AddLog("[AUTH] Invalid password from Client Socket " & Int($hClient) & " (UserName: " & $sUserName & ")")
                _TCP_Send($hClient, "REJECTED|Invalid password" & @CRLF)
                Sleep(100)
                RemoveClient($hClient)
                _TCP_Server_DisconnectClient($hClient)
                Return
            EndIf

            ; Check for duplicate UserName
            Local $iDuplicateIdx = FindClientByUserName($sUserName)
            If $iDuplicateIdx > 0 And $iDuplicateIdx <> $iIdx Then
                AddLog("[AUTH] Duplicate UserName rejected: " & $sUserName & " from Client Socket " & Int($hClient))
                _TCP_Send($hClient, "REJECTED|UserName already in use: " & $sUserName & @CRLF)
                Sleep(100)
                RemoveClient($hClient)
                _TCP_Server_DisconnectClient($hClient)
                Return
            EndIf

            ; Authentication successful
            $g_aClients[$iIdx][1] = $sUserName
            $g_aClients[$iIdx][2] = $sDisplayName
            $g_aClients[$iIdx][3] = $sPCName
            $g_aClients[$iIdx][4] = True

            Local $sWelcomeMsg = "WELCOME|" & Int($hClient) & @CRLF
            _TCP_Send($hClient, $sWelcomeMsg)
            AddLog("[DEBUG] Sending WELCOME to Client Socket " & Int($hClient) & ": " & StringReplace($sWelcomeMsg, @CRLF, ""))
            AddLog("[AUTH] Client authenticated: " & $sUserName & " (Display: " & $sDisplayName & ", PC: " & $sPCName & ", Client Socket: " & Int($hClient) & ")")

            UpdateClientList()
            BroadcastClientList()
        Else
            AddLog("[AUTH] Invalid auth format from Client Socket " & Int($hClient))
            _TCP_Send($hClient, "REJECTED|Invalid authentication format" & @CRLF)
            Sleep(100)
            RemoveClient($hClient)
            _TCP_Server_DisconnectClient($hClient)
        EndIf
        Return
    EndIf

    ; Handle client commands
    Switch $sCommand
        Case "/list"
            SendClientList($hClient)

        Case "/privmsg"
            ; Parse /privmsg command: format is "/privmsg <socket> <message>"
            ; Note: Command uses space delimiter, not pipe
            Local $sCmdLine = StringTrimLeft($sData, 8) ; Remove "/privmsg"
            $sCmdLine = StringStripWS($sCmdLine, 1) ; Strip leading spaces

            If $g_bDebugMode Then
                AddLog("[DEBUG] /privmsg received. Raw: '" & $sData & "' | After trim: '" & $sCmdLine & "'")
            EndIf

            Local $iSpacePos = StringInStr($sCmdLine, " ", 0, 1) ; Find first space
            If $iSpacePos > 0 Then
                Local $sTargetSocket = StringLeft($sCmdLine, $iSpacePos - 1)
                Local $iTargetSocket = Int($sTargetSocket)
                Local $sMsg = StringTrimLeft($sCmdLine, $iSpacePos) ; Get message after socket
                $sMsg = StringStripWS($sMsg, 1) ; Strip leading spaces from message

                If $g_bDebugMode Then
                    AddLog("[DEBUG] Parsed - Target socket string: '" & $sTargetSocket & "' | Int: " & $iTargetSocket & " | Message: '" & $sMsg & "'")
                EndIf

                If $iTargetSocket > 0 And $sMsg <> "" Then
                    SendPrivateMessage($hClient, $iTargetSocket, $sMsg)
                Else
                    _TCP_Send($hClient, "SERVMSG|Error: Invalid /privmsg format. Use: /privmsg <socket> <message>" & @CRLF)
                    AddLog("[ERROR] Invalid privmsg - Socket: " & $iTargetSocket & " | Msg: '" & $sMsg & "'")
                EndIf
            Else
                _TCP_Send($hClient, "SERVMSG|Error: Invalid /privmsg format. Use: /privmsg <socket> <message>" & @CRLF)
                AddLog("[ERROR] Invalid privmsg format - no space found in: '" & $sCmdLine & "'")
            EndIf

        Case "/name"
            If UBound($aCmd) >= 2 Then
                Local $sNewName = $aCmd[1]
                $g_aClients[$iIdx][2] = $sNewName
                UpdateClientList()
                BroadcastClientList()
                AddLog("[INFO] Client Socket " & Int($hClient) & " changed name to: " & $sNewName)
                _TCP_Send($hClient, "SERVMSG|Name changed to: " & $sNewName & @CRLF)
            EndIf

        Case "/help"
            Local $sHelp = "SERVMSG|Available commands:" & @CRLF & _
                    "  /list - Get client list" & @CRLF & _
                    "  /privmsg <socket> <msg> - Private message" & @CRLF & _
                    "  /name <new_name> - Change name" & @CRLF & _
                    "  /serverinfo - Get server information" & @CRLF & _
                    "  /quit - Disconnect"
            _TCP_Send($hClient, $sHelp)

        Case "/serverinfo"
            Local $sInfo = "Server Version: 2.0.1" & @CRLF & _
                    "AutoIt Version: " & @AutoItVersion & @CRLF & _
                    "Server OS: " & @OSVersion & @CRLF & _
                    "Server Computer: " & @ComputerName & @CRLF & _
                    "Max Clients: " & $g_iMaxClients & @CRLF & _
                    "Current Clients: " & $g_aClients[0][0] & @CRLF & _
                    "Uptime: " & Round(TimerDiff($g_iServerStartTime) / 1000, 2) & " seconds" & @CRLF & _
                    "Supported Commands: /help, /list, /privmsg, /name, /serverinfo, /quit"
            _TCP_Send($hClient, "SERVERINFO|" & $sInfo)
            AddLog("[INFO] Server info sent to " & $g_aClients[$iIdx][2])

        Case "/quit"
            AddLog("[INFO] " & $g_aClients[$iIdx][2] & " requested disconnect")
            _TCP_Send($hClient, "SERVMSG|Goodbye!" & @CRLF)
            Sleep(100)
            RemoveClient($hClient)
            _TCP_Server_DisconnectClient($hClient)
            UpdateClientList()
            BroadcastClientList()

        Case "MSG"
            Local $sMsg = StringTrimLeft($sData, 4)
            Local $iIdx = FindClient($hClient)
            If $iIdx > 0 Then
                AddLog("[" & $g_aClients[$iIdx][2] & "] " & $sMsg) ; Log with DisplayName
                ; Broadcast to all clients (use DisplayName for display)
                For $j = 1 To $g_aClients[0][0]
                    If $g_aClients[$j][4] And $g_aClients[$j][0] <> $hClient Then
                        _TCP_Send($g_aClients[$j][0], "MSG|" & $g_aClients[$iIdx][2] & "|" & $sMsg & @CRLF)
                    EndIf
                Next
            EndIf

        Case Else
            AddLog("[" & $g_aClients[$iIdx][2] & "] " & $sData)
    EndSwitch
EndFunc   ;==>OnServerReceive

Func OnClientDisconnect($hClient, $iError)
    AddLog("[DEBUG] OnClientDisconnect called for Client Socket: " & Int($hClient) & " Error: " & $iError)
    Local $iIdx = FindClient($hClient)
    If $iIdx > 0 Then
        AddLog("[DISCONNECT] " & $g_aClients[$iIdx][2] & " (Client Socket: " & Int($hClient) & ")")
        RemoveClient($hClient)
        UpdateClientList()
        BroadcastClientList()
    Else
        AddLog("[WARNING] Disconnect for unknown Client Socket: " & Int($hClient))
    EndIf
EndFunc   ;==>OnClientDisconnect

Func SendClientList($hClient)
    Local $sList = "CLIENTLIST|" & $g_aClients[0][0]
    For $i = 1 To $g_aClients[0][0]
        If $g_aClients[$i][4] Then
            ; Format: Socket:UserName:DisplayName:PCName
            $sList &= "|" & $g_aClients[$i][0] & ":" & $g_aClients[$i][1] & ":" & $g_aClients[$i][2] & ":" & $g_aClients[$i][3]
        EndIf
    Next
    _TCP_Send($hClient, $sList & @CRLF)
EndFunc   ;==>SendClientList

Func BroadcastClientList()
    For $i = 1 To $g_aClients[0][0]
        If $g_aClients[$i][4] Then
            SendClientList($g_aClients[$i][0])
        EndIf
    Next
EndFunc   ;==>BroadcastClientList

Func SendPrivateMessage($hFrom, $iToSocket, $sMsg)
    Local $iFromIdx = FindClient($hFrom)
    Local $iToIdx = FindClient($iToSocket)

    If $g_bDebugMode Then
        AddLog("[DEBUG] SendPrivateMessage - From socket: " & Int($hFrom) & " (idx: " & $iFromIdx & ") | To socket: " & Int($iToSocket) & " (idx: " & $iToIdx & ")")
        ; List all client sockets for comparison
        Local $sClientList = "Current client sockets: "
        For $k = 1 To $g_aClients[0][0]
            $sClientList &= Int($g_aClients[$k][0]) & " "
        Next
        AddLog("[DEBUG] " & $sClientList)
    EndIf

    If $iFromIdx > 0 And $iToIdx > 0 And $g_aClients[$iToIdx][4] Then
        If _TCP_Server_ClientIP($iToSocket) Then ; Verify socket still connected
            ; Use DisplayName for display in messages
            _TCP_Send($iToSocket, "PRIVMSG|" & $g_aClients[$iFromIdx][2] & "|" & $sMsg & @CRLF)
            _TCP_Send($hFrom, "SERVMSG|Private message sent to " & $g_aClients[$iToIdx][2] & @CRLF)
            AddLog("[PRIVMSG] " & $g_aClients[$iFromIdx][2] & " -> " & $g_aClients[$iToIdx][2] & ": " & $sMsg)
        Else
            _TCP_Send($hFrom, "SERVMSG|Error: Target client disconnected" & @CRLF)
            AddLog("[ERROR] Target client disconnected during send")
            RemoveClient($iToSocket)
            UpdateClientList()
            BroadcastClientList()
        EndIf
    Else
        _TCP_Send($hFrom, "SERVMSG|Error: Client not found (From idx: " & $iFromIdx & ", To idx: " & $iToIdx & ")" & @CRLF)
        AddLog("[ERROR] Client not found - From idx: " & $iFromIdx & " | To idx: " & $iToIdx & " | To socket: " & Int($iToSocket))
    EndIf
EndFunc   ;==>SendPrivateMessage


Func SendMessageToClient()
    Local $sMsg = GUICtrlRead($g_hInput)
    If $sMsg = "" Then Return

    ; Check for admin commands
    If StringLeft($sMsg, 1) = "/" Then
        HandleAdminCommand($sMsg)
        GUICtrlSetData($g_hInput, "")
        Return
    EndIf

    ; Get selected clients
    Local $aSelected = _GUICtrlListView_GetSelectedIndices($g_hListView, True)

    ; If no clients selected, send to all
    If $aSelected[0] = 0 Then
        For $i = 1 To $g_aClients[0][0]
            If $g_aClients[$i][4] Then
                _TCP_Send($g_aClients[$i][0], "SERVMSG|[Server] " & $sMsg & @CRLF)
            EndIf
        Next
    Else
        ; Send to selected clients
        For $i = 1 To $aSelected[0]
            Local $iIdx = $aSelected[$i] + 1
            If $iIdx <= $g_aClients[0][0] And $g_aClients[$iIdx][4] Then
                _TCP_Send($g_aClients[$iIdx][0], "SERVMSG|[Server] " & $sMsg & @CRLF)
                AddLog("[SENT] Message to " & $g_aClients[$iIdx][2] & ": " & $sMsg)
            EndIf
        Next
    EndIf

    GUICtrlSetData($g_hInput, "")
EndFunc   ;==>SendMessageToClient

Func HandleAdminCommand($sCommand)
    Local $aCmd = StringSplit($sCommand, " ", 2)

    Switch $aCmd[0]
        Case "/serverinfo"
            ; Send server info to selected clients
            Local $aSelected = _GUICtrlListView_GetSelectedIndices($g_hListView, True)
            If $aSelected[0] = 0 Then
                AddLog("[ADMIN] No clients selected for server info")
                Return
            EndIf

            Local $sInfo = "Server Version: 2.0.1|" & _
                    "AutoIt Version: " & @AutoItVersion & "|" & _
                    "Server OS: " & @OSVersion & "|" & _
                    "Server Computer: " & @ComputerName & "|" & _
                    "Max Clients: " & $g_iMaxClients & "|" & _
                    "Current Clients: " & $g_aClients[0][0] & "|" & _
                    "Uptime: " & Round(TimerDiff($g_iServerStartTime) / 1000, 1) & " seconds"

            For $i = 1 To $aSelected[0]
                Local $iIdx = $aSelected[$i] + 1
                If $iIdx <= $g_aClients[0][0] And $g_aClients[$iIdx][4] Then
                    _TCP_Send($g_aClients[$iIdx][0], "SERVERINFO|" & $sInfo & @CRLF)
                    AddLog("[ADMIN] Server info sent to " & $g_aClients[$iIdx][2])
                EndIf
            Next

        Case "/help"
            AddLog("[ADMIN] Available admin commands:")
            AddLog("  /serverinfo - Send server info to selected clients")
            AddLog("  /help - Show this help")

        Case Else
            AddLog("[ADMIN] Unknown command: " & $aCmd[0] & " (Use /help for available commands)")
    EndSwitch
EndFunc   ;==>HandleAdminCommand


Func SelectAllClients()
    For $i = 0 To _GUICtrlListView_GetItemCount($g_hListView) - 1
        _GUICtrlListView_SetItemSelected($g_hListView, $i, True)
    Next
EndFunc   ;==>SelectAllClients

Func DeselectAllClients()
    _GUICtrlListView_SetItemSelected($g_hListView, -1, False)
EndFunc   ;==>DeselectAllClients

Func KickSelectedClient()
    Local $iSelected = _GUICtrlListView_GetNextItem($g_hListView)
    If $iSelected < 0 Then Return

    Local $hClient = Int(_GUICtrlListView_GetItemText($g_hListView, $iSelected, 1))
    Local $sUserName = _GUICtrlListView_GetItemText($g_hListView, $iSelected, 2)

    AddLog("[KICK] Disconnecting " & $sUserName)
    _TCP_Send($hClient, "KICK|Kicked by server" & @CRLF)
    Sleep(100)
    RemoveClient($hClient)
    _TCP_Server_DisconnectClient($hClient)
    UpdateClientList()
    BroadcastClientList()
EndFunc   ;==>KickSelectedClient

Func FindClient($hSocket)
    For $i = 1 To $g_aClients[0][0]
        If $g_aClients[$i][0] = $hSocket Then Return $i
    Next
    Return -1
EndFunc   ;==>FindClient

Func FindClientByUserName($sUserName)
    For $i = 1 To $g_aClients[0][0]
        If $g_aClients[$i][4] And $g_aClients[$i][1] = $sUserName Then Return $i
    Next
    Return -1
EndFunc   ;==>FindClientByUserName

Func RemoveClient($hSocket)
    For $i = 1 To $g_aClients[0][0]
        If $g_aClients[$i][0] = $hSocket Then
            For $j = $i To $g_aClients[0][0] - 1
                $g_aClients[$j][0] = $g_aClients[$j + 1][0]
                $g_aClients[$j][1] = $g_aClients[$j + 1][1]
                $g_aClients[$j][2] = $g_aClients[$j + 1][2]
                $g_aClients[$j][3] = $g_aClients[$j + 1][3]
                $g_aClients[$j][4] = $g_aClients[$j + 1][4]
            Next
            $g_aClients[0][0] -= 1
            ReDim $g_aClients[$g_aClients[0][0] + 1][5]
            ; UpdateClientList() will be called by OnClientDisconnect
            Return
        EndIf
    Next
EndFunc   ;==>RemoveClient

Func UpdateClientList()
    _GUICtrlListView_DeleteAllItems($g_hListView)
    For $i = 1 To $g_aClients[0][0]
        If $g_aClients[$i][4] Then
            _GUICtrlListView_AddItem($g_hListView, $i)
            _GUICtrlListView_AddSubItem($g_hListView, $i - 1, Int($g_aClients[$i][0]), 1)
            _GUICtrlListView_AddSubItem($g_hListView, $i - 1, $g_aClients[$i][1], 2)
            _GUICtrlListView_AddSubItem($g_hListView, $i - 1, $g_aClients[$i][2], 3)
            _GUICtrlListView_AddSubItem($g_hListView, $i - 1, $g_aClients[$i][3], 4)
        EndIf
    Next
EndFunc   ;==>UpdateClientList

Func AddLog($sText)
    Local $sCurrent = GUICtrlRead($g_hEdit)
    Local $sTime = @HOUR & ":" & @MIN & ":" & @SEC
    GUICtrlSetData($g_hEdit, $sCurrent & "[" & $sTime & "] " & $sText & @CRLF)

    ; Auto-scroll to bottom (latest message)
    Local $hEdit = GUICtrlGetHandle($g_hEdit)
    DllCall("user32.dll", "lresult", "SendMessageW", "hwnd", $hEdit, "uint", 0x00B6, "wparam", 0, "lparam", -1) ; EM_SETSEL to end
    DllCall("user32.dll", "lresult", "SendMessageW", "hwnd", $hEdit, "uint", 0x00B7, "wparam", 0, "lparam", 0) ; EM_SCROLLCARET
EndFunc   ;==>AddLog

Func Cleanup()
    If $g_hServerSocket Then
        StopServer()
    EndIf
EndFunc   ;==>Cleanup

Func _GUICtrlEdit_Scroll($hWnd, $iDirection)
    DllCall("user32.dll", "lresult", "SendMessageW", "hwnd", GUICtrlGetHandle($hWnd), "uint", 0x00B5, "wparam", $iDirection, "lparam", 0)
EndFunc   ;==>_GUICtrlEdit_Scroll

Client:

#Region ;**** Directives created by AutoIt3Wrapper_GUI ****
#AutoIt3Wrapper_UseX64=y
#AutoIt3Wrapper_Res_requestedExecutionLevel=requireAdministrator
#EndRegion ;**** Directives created by AutoIt3Wrapper_GUI ****
#NoTrayIcon
; ===== TCP EVENT CLIENT v1.0 by Dao Van Trong - TRONG.PRO =====
; Advanced TCP Client
;
; ===== PROTOCOL STRUCTURE =====
;
; === CLIENT → SERVER COMMANDS ===
; AUTH|<username>|<password>|<display_name>|<pc_name>  - Client authentication
; MSG|<message>                                         - Broadcast chat message
; /help                                                 - Show available commands
; /list                                                 - Get connected clients list
; /privmsg <socket> <msg>                               - Send private message to specific client
; /name <new_name>                                      - Change display name
; /serverinfo                                           - Get server information
; /quit                                                 - Disconnect from server
;
; === SERVER → CLIENT MESSAGES ===
; WHOAREYOU                                    - Request authentication
; WELCOME|<socket_id>                          - Authentication successful
; REJECTED|<reason>                            - Connection/auth rejected
; CLIENTLIST|<count>|<id1:name1:pc1>|...      - Connected clients list
; MSG|<from_name>|<message>                    - Chat message from user
; PRIVMSG|<from_name>|<message>                - Private message
; SERVMSG|<message_text>                       - Server message/response (multi-line support)
; SERVERINFO|<server_info>                     - Server information (multi-line support)
; KICK|<reason>                                - Client being kicked
;
; === AUTHENTICATION FLOW ===
; 1. Client connects → Server sends "WHOAREYOU"
; 2. Client responds: "AUTH|<username>|<password>|<display_name>|<pc_name>"
; 3. Server validates (checks password and duplicate username) and sends "WELCOME|<socket>" or "REJECTED|<reason>"
; 4. Authenticated clients can send commands and receive broadcasts
;
; === ERROR HANDLING ===
; - Connection timeout: 10 seconds
; - Max retries: 3 attempts
; - Debug mode can be enabled for troubleshooting

#include "WinSockUDF.au3"
#include <GUIConstantsEx.au3>
#include <EditConstants.au3>
#include <WindowsConstants.au3>
#include <GUIListView.au3>
#include <GDIPlus.au3>

Global Const $SERVER_IP = "127.0.0.1"
Global Const $SERVER_PORT = 8888 ; Main chat/command port

; ListView notification constants
;~ Global Const $NM_DBLCLK = -3

Global $g_hSocket = 0
Global $g_hGUI, $g_hListViewUsers, $g_hEditChat, $g_hInput, $g_hBtnSend, $g_hBtnConnect, $g_hBtnDisconnect
Global $g_hInputUserName, $g_hInputPassword, $g_hInputDisplayName
Global $g_hStatus, $g_bConnected = False
Global $g_sMySocket = ""
Global $g_sMyUserName = ""
Global $g_aClientList[1][4] ; [Socket, UserName, DisplayName, PCName]
$g_aClientList[0][0] = 0

; Connection timeout and retry variables
Global $g_iConnectionTimeout = 10000 ; 10 seconds
Global $g_iMaxRetries = 3
Global $g_iCurrentRetry = 0
Global $g_iConnectionStartTime = 0
Global $g_bConnecting = False

; Background image configuration
Global $g_sDefaultBackground = @ScriptDir & "\client_default_bg.bmp" ; Default client background
Global $g_sCurrentBackground = "" ; Current background being used
Global $g_hBackgroundPic = 0 ; Handle for background picture control

; Performance optimization
Global $g_bDebugMode = False ; Set to False to disable debug logs

; Initialize GDI+ for background image support
_GDIPlus_Startup()

CreateGUI()

Func CreateGUI()
    $g_hGUI = GUICreate("TCP Chat Client - Room Chat", 900, 600)

    ; Create background picture control first (behind all other controls)
    $g_hBackgroundPic = GUICtrlCreatePic("", 0, 0, 900, 600)
    GUICtrlSetState($g_hBackgroundPic, $GUI_DISABLE) ; Disable so it doesn't interfere with other controls

    ; Connection Panel
    GUICtrlCreateGroup("Connection", 10, 5, 880, 50)
    GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT)
    GUICtrlCreateLabel("User Name:", 20, 25, 70, 20)
    GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT)
    $g_hInputUserName = GUICtrlCreateInput(@UserName, 95, 23, 150, 22)
    GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT)

    GUICtrlCreateLabel("Password:", 260, 25, 60, 20)
    GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT)
    $g_hInputPassword = GUICtrlCreateInput("123456", 325, 23, 100, 22, $ES_PASSWORD)
    GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT)

    GUICtrlCreateLabel("Display Name:", 440, 25, 80, 20)
    GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT)
    $g_hInputDisplayName = GUICtrlCreateInput(@UserName, 525, 23, 140, 22)
    GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT)

    $g_hBtnConnect = GUICtrlCreateButton("Connect", 670, 20, 100, 28)
    GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT)
    $g_hBtnDisconnect = GUICtrlCreateButton("Disconnect", 770, 20, 110, 28)
    GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT)
    GUICtrlSetState($g_hBtnDisconnect, $GUI_DISABLE)
    GUICtrlCreateGroup("", -99, -99, 1, 1)

    ; Status
    $g_hStatus = GUICtrlCreateLabel("Status: Not Connected", 10, 60, 860, 30)
    GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT)
    GUICtrlSetColor(-1, 0xFF0000)

    ; Main Layout: Users List + Chat Area
    GUICtrlCreateLabel("Online Users (Double-click for private message):", 10, 95, 250, 20)
    GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT)
    $g_hListViewUsers = GUICtrlCreateListView("Socket|UserName|DisplayName", 10, 115, 250, 380, _
            BitOR($LVS_REPORT, $LVS_SHOWSELALWAYS, $LVS_SINGLESEL), _
            BitOR($WS_EX_CLIENTEDGE, $LVS_EX_FULLROWSELECT, $LVS_EX_GRIDLINES))
    GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT)
    _GUICtrlListView_SetColumnWidth($g_hListViewUsers, 0, 60)
    _GUICtrlListView_SetColumnWidth($g_hListViewUsers, 1, 80)
    _GUICtrlListView_SetColumnWidth($g_hListViewUsers, 2, 100)

    GUICtrlCreateLabel("Chat Room:", 270, 95, 200, 20)
    GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT)
    $g_hEditChat = GUICtrlCreateEdit("", 270, 115, 620, 380, BitOR($ES_READONLY, $ES_MULTILINE, $WS_VSCROLL, $ES_AUTOVSCROLL))
    GUICtrlSetFont($g_hEditChat, 9, 400, 0, "Consolas")
    GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT)

    ; Input Area
    GUICtrlCreateLabel("Message:", 10, 505, 60, 20)
    GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT)
    GUICtrlCreateLabel("(Type message or /help for commands)", 75, 505, 300, 20)
    GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT)
    $g_hInput = GUICtrlCreateInput("", 10, 525, 780, 25)
    GUICtrlSetState($g_hInput, $GUI_DISABLE)

    $g_hBtnSend = GUICtrlCreateButton("Send", 800, 523, 90, 28)
    GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT)
    GUICtrlSetState($g_hBtnSend, $GUI_DISABLE)


    ; Register WM_NOTIFY to detect ListView double-click
    GUIRegisterMsg($WM_NOTIFY, "WM_NOTIFY_EVENTS")

    GUISetState(@SW_SHOW)

    ; Apply default background if available
    ApplyBackground($g_sDefaultBackground)

    While 1
        Local $nMsg = GUIGetMsg()
        Switch $nMsg
            Case $GUI_EVENT_CLOSE
                Cleanup()
                Exit

            Case $g_hBtnConnect
                ConnectToServer()

            Case $g_hBtnDisconnect
                DisconnectFromServer()

            Case $g_hBtnSend
                SendMessage()
        EndSwitch

        ; Check for connection timeout
        CheckConnectionTimeout()

        Sleep(10)
    WEnd
EndFunc   ;==>CreateGUI

Func ConnectToServer()
    If $g_bConnecting Then Return ; Prevent multiple connection attempts

    $g_bConnecting = True
    $g_iCurrentRetry = 0
    AttemptConnection()
EndFunc   ;==>ConnectToServer

Func AttemptConnection()
    $g_iCurrentRetry += 1

    ; Force cleanup any existing connection first
    If $g_hSocket Then
        AddChat("[DEBUG] Cleaning up old local socket: " & Int($g_hSocket))
        _TCP_Client_Stop($g_hSocket)
        _TCP_Shutdown()
        $g_hSocket = 0

        ; Reset all connection state
        $g_bConnected = False
        $g_bConnecting = False
        $g_sMySocket = ""
        $g_iConnectionStartTime = 0

        Sleep(200) ; Give more time for complete cleanup
    EndIf

    If $g_iCurrentRetry > 1 Then
        AddChat("=== Retry " & ($g_iCurrentRetry - 1) & "/" & $g_iMaxRetries & " ===")
    Else
        AddChat("=== Connecting to Server ===")
    EndIf

    _TCP_Startup()
    If @error Then
        AddChat("ERROR: Cannot initialize TCP")
        HandleConnectionFailure()
        Return
    EndIf

    AddChat("Connecting to " & $SERVER_IP & ":" & $SERVER_PORT & "...")
    $g_hSocket = _TCP_Client_Create($SERVER_IP, $SERVER_PORT, "", 0, 0)
    Local $iCreateError = @error

    If $iCreateError Then
        AddChat("ERROR: Connection failed (Error: " & $iCreateError & ")")
        _TCP_Shutdown()
        HandleConnectionFailure()
        Return
    EndIf

    AddChat("[DEBUG] New local socket created: " & Int($g_hSocket) & " (Error: " & $iCreateError & ")")

    ; Verify socket handle is valid
    If $g_hSocket <= 0 Then
        AddChat("[ERROR] Invalid local socket handle: " & Int($g_hSocket))
        HandleConnectionFailure()
        Return
    EndIf

    _TCP_OnConnect($g_hSocket, "OnConnect")
    _TCP_OnReceive($g_hSocket, "OnReceive")
    _TCP_OnDisconnect($g_hSocket, "OnDisconnect")

    $g_iConnectionStartTime = TimerInit()
    GUICtrlSetData($g_hStatus, "Status: Connecting... (Attempt " & $g_iCurrentRetry & "/" & ($g_iMaxRetries + 1) & ") Local Socket: " & Int($g_hSocket))
    GUICtrlSetColor($g_hStatus, 0x0000FF)

    GUICtrlSetState($g_hBtnConnect, $GUI_DISABLE)
EndFunc   ;==>AttemptConnection

Func HandleConnectionFailure()
    If $g_iCurrentRetry <= $g_iMaxRetries Then
        AddChat("Connection failed. Retrying in 3 seconds...")
        GUICtrlSetData($g_hStatus, "Status: Retrying in 3 seconds...")
        GUICtrlSetColor($g_hStatus, 0xFF8800)
        Sleep(3000)
        AttemptConnection()
    Else
        AddChat("=== Connection Failed ===")
        AddChat("Maximum retry attempts reached. Please check server status.")
        GUICtrlSetData($g_hStatus, "Status: Connection Failed - Max retries reached")
        GUICtrlSetColor($g_hStatus, 0xFF0000)
        GUICtrlSetState($g_hBtnConnect, $GUI_ENABLE)
        $g_bConnecting = False
    EndIf
EndFunc   ;==>HandleConnectionFailure

Func CheckConnectionTimeout()
    If $g_bConnecting And $g_iConnectionStartTime > 0 And Not $g_bConnected Then
        Local $iElapsed = TimerDiff($g_iConnectionStartTime)
        If $iElapsed > $g_iConnectionTimeout Then
            AddChat("Connection timeout after " & Round($iElapsed / 1000, 1) & " seconds")
            If $g_hSocket Then
                _TCP_Client_Stop($g_hSocket)
                _TCP_Shutdown()
                $g_hSocket = 0
            EndIf
            $g_iConnectionStartTime = 0
            HandleConnectionFailure()
        EndIf
    EndIf
EndFunc   ;==>CheckConnectionTimeout

Func DisconnectFromServer()
    If $g_hSocket Then
        _TCP_Send($g_hSocket, "/quit" & @CRLF)
        Sleep(200)
        _TCP_Client_Stop($g_hSocket)
        _TCP_Shutdown()
        $g_hSocket = 0
        $g_bConnected = False
    EndIf

    ; Reset connection state
    $g_bConnecting = False
    $g_iConnectionStartTime = 0
    $g_sMySocket = ""

    AddChat("=== Disconnected from server ===")
    GUICtrlSetData($g_hStatus, "Status: Disconnected")
    GUICtrlSetColor($g_hStatus, 0xFF0000)

    GUICtrlSetState($g_hBtnConnect, $GUI_ENABLE)
    GUICtrlSetState($g_hBtnDisconnect, $GUI_DISABLE)
    GUICtrlSetState($g_hInput, $GUI_DISABLE)
    GUICtrlSetState($g_hBtnSend, $GUI_DISABLE)

    ; Clear user list
    _GUICtrlListView_DeleteAllItems($g_hListViewUsers)
EndFunc   ;==>DisconnectFromServer

Func OnConnect($hSocket, $iError)
    AddChat("[DEBUG] OnConnect called - Local Socket: " & Int($hSocket) & " Current Local: " & Int($g_hSocket) & " Error: " & $iError)

    ; Validate this is our current socket
    If $hSocket <> $g_hSocket Then
        AddChat("[DEBUG] Ignoring OnConnect for old local socket: " & Int($hSocket))
        Return
    EndIf

    If $iError Then
        AddChat("Connection failed with error: " & $iError)
        GUICtrlSetData($g_hStatus, "Status: Connection Failed")
        GUICtrlSetColor($g_hStatus, 0xFF0000)
        HandleConnectionFailure()
        Return
    EndIf

    AddChat("Connected to server successfully!")
    $g_bConnected = True

    GUICtrlSetData($g_hStatus, "Status: Authenticating...")
    GUICtrlSetColor($g_hStatus, 0x0000FF)
EndFunc   ;==>OnConnect

Func OnReceive($hSocket, $sData, $iError)
    If $iError Or $sData = "" Then Return

    ; Only show debug logs if debug mode is enabled
    If $g_bDebugMode Then
        AddChat("[DEBUG] OnReceive - Local Socket: " & Int($hSocket) & " Current Local: " & Int($g_hSocket) & " Data: " & StringLeft($sData, 80))
    EndIf

    ; WORKAROUND: TCP Event UDF still has socket routing issues - disable validation
    ; If $hSocket <> $g_hSocket Then
    ;   AddChat("[DEBUG] Ignoring OnReceive for old local socket: " & Int($hSocket))
    ;   Return
    ; EndIf

    $sData = StringStripWS($sData, 3)
    Local $aCmd = StringSplit($sData, "|", 2)

    Switch $aCmd[0]
        Case "WHOAREYOU"
            Local $sUserName = GUICtrlRead($g_hInputUserName)
            Local $sPassword = GUICtrlRead($g_hInputPassword)
            Local $sDisplayName = GUICtrlRead($g_hInputDisplayName)
            Local $sPCName = @ComputerName
            $g_sMyUserName = $sUserName

            Local $sAuth = "AUTH|" & $sUserName & "|" & $sPassword & "|" & $sDisplayName & "|" & $sPCName & @CRLF
            _TCP_Send($hSocket, $sAuth)
            AddChat("Sending authentication...")

        Case "WELCOME"
            If UBound($aCmd) >= 2 Then
                Local $sServerSocket = $aCmd[1]

                ; Note: In TCP, client and server have different socket handles for the same connection
                ; This is normal behavior - each side maintains its own socket handle
                ; Server's socket ID is used for identification in messages
                If $g_bDebugMode Then
                    AddChat("[DEBUG] WELCOME received - Server Socket: " & Int($sServerSocket) & " (Local Socket: " & Int($hSocket) & ")")
                    If String($sServerSocket) <> String($hSocket) Then
                        AddChat("[DEBUG] Note: Different socket handles are normal in TCP - using server socket ID for identification")
                    EndIf
                EndIf

                $g_sMySocket = $sServerSocket
                $g_bConnecting = False
                $g_iConnectionStartTime = 0

                AddChat("=== Authentication Successful ===")
                AddChat("Server Socket ID: " & Int($g_sMySocket))
                AddChat("Local Socket: " & Int($g_hSocket))
                AddChat("Welcome to the chat room!")
                AddChat("========================================")

                GUICtrlSetData($g_hStatus, "Status: Connected (Server Socket: " & Int($g_sMySocket) & ", Local: " & Int($g_hSocket) & ")")
                GUICtrlSetColor($g_hStatus, 0x00AA00)

                GUICtrlSetState($g_hBtnDisconnect, $GUI_ENABLE)
                GUICtrlSetState($g_hInput, $GUI_ENABLE)
                GUICtrlSetState($g_hBtnSend, $GUI_ENABLE)

                _TCP_Send($hSocket, "/list" & @CRLF)
            EndIf

        Case "REJECTED"
            Local $sReason = UBound($aCmd) >= 2 ? $aCmd[1] : "Unknown"
            AddChat("=== Connection Rejected ===")
            AddChat("Reason: " & $sReason)

            GUICtrlSetData($g_hStatus, "Status: Rejected - " & $sReason)
            GUICtrlSetColor($g_hStatus, 0xFF0000)
            GUICtrlSetState($g_hBtnConnect, $GUI_ENABLE)

            Sleep(500)
            DisconnectFromServer()

        Case "CLIENTLIST"
            If UBound($aCmd) >= 2 Then
                If $g_sMySocket = "" Then
                    Return
                EndIf

                Local $iCount = Int($aCmd[1])

                _GUICtrlListView_DeleteAllItems($g_hListViewUsers)
                ReDim $g_aClientList[$iCount + 1][4]
                $g_aClientList[0][0] = $iCount

                For $i = 2 To UBound($aCmd) - 1
                    Local $aClient = StringSplit($aCmd[$i], ":", 2)
                    If UBound($aClient) >= 4 Then
                        Local $idx = $i - 1
                        $g_aClientList[$idx][0] = $aClient[0] ; Socket
                        $g_aClientList[$idx][1] = $aClient[1] ; UserName
                        $g_aClientList[$idx][2] = $aClient[2] ; DisplayName
                        $g_aClientList[$idx][3] = $aClient[3] ; PCName

                        ; Only show other clients (not self) with Socket|UserName|DisplayName format
                        ; Use UserName comparison to identify self
                        If $aClient[1] <> $g_sMyUserName Then
                            _GUICtrlListView_AddItem($g_hListViewUsers, Int($aClient[0]))
                            _GUICtrlListView_AddSubItem($g_hListViewUsers, _GUICtrlListView_GetItemCount($g_hListViewUsers) - 1, $aClient[1], 1) ; UserName
                            _GUICtrlListView_AddSubItem($g_hListViewUsers, _GUICtrlListView_GetItemCount($g_hListViewUsers) - 1, $aClient[2], 2) ; DisplayName
                        EndIf
                    EndIf
                Next
            EndIf

        Case "MSG"
            Local $aParts = StringSplit($sData, "|", 2)
            If UBound($aParts) >= 3 Then
                Local $sFromName = $aParts[1]
                Local $sMsg = $aParts[2]

                AddChat("[" & $sFromName & "] " & $sMsg)
            EndIf

        Case "SERVMSG"
            If UBound($aCmd) >= 2 Then
                Local $sMsg = $aCmd[1]
                ; Split message by lines and display each line
                Local $aLines = StringSplit($sMsg, @CRLF, 1)
                For $i = 1 To $aLines[0]
                    If StringStripWS($aLines[$i], 3) <> "" Then
                        AddChat($aLines[$i])
                    EndIf
                Next
            EndIf

        Case "PRIVMSG"
            Local $aParts = StringSplit($sData, "|", 2)
            If UBound($aParts) >= 3 Then
                Local $sFromName = $aParts[1]
                Local $sMsg = $aParts[2]
                AddChat("[PRIVATE from " & $sFromName & "] " & $sMsg)
            EndIf

        Case "SERVERINFO"
            If UBound($aCmd) >= 2 Then
                Local $sInfo = $aCmd[1]
                AddChat("=== Server Information ===")
                ; Split info by lines and display each line
                Local $aLines = StringSplit($sInfo, @CRLF, 1)
                For $i = 1 To $aLines[0]
                    If StringStripWS($aLines[$i], 3) <> "" Then
                        AddChat($aLines[$i])
                    EndIf
                Next
                AddChat("=============================")
            EndIf

        Case "KICK"
            Local $sReason = UBound($aCmd) >= 2 ? $aCmd[1] : "No reason"
            AddChat("=== You have been kicked ===")
            AddChat("Reason: " & $sReason)
            AddChat("========================================")
            DisconnectFromServer()

        Case Else
            AddChat("[Server] " & $sData)
    EndSwitch
EndFunc   ;==>OnReceive

Func OnDisconnect($hSocket, $iError)
    ; Validate this is our current socket
    If $hSocket <> $g_hSocket Then Return

    AddChat("=== Disconnected from server ===")
    If $iError Then AddChat("Disconnect error: " & $iError)

    $g_bConnected = False
    $g_bConnecting = False
    $g_iConnectionStartTime = 0
    $g_sMySocket = ""

    GUICtrlSetData($g_hStatus, "Status: Disconnected")
    GUICtrlSetColor($g_hStatus, 0xFF0000)

    GUICtrlSetState($g_hBtnConnect, $GUI_ENABLE)
    GUICtrlSetState($g_hBtnDisconnect, $GUI_DISABLE)
    GUICtrlSetState($g_hInput, $GUI_DISABLE)
    GUICtrlSetState($g_hBtnSend, $GUI_DISABLE)

    _GUICtrlListView_DeleteAllItems($g_hListViewUsers)
EndFunc   ;==>OnDisconnect

Func SendMessage()
    Local $sMsg = GUICtrlRead($g_hInput)
    If $sMsg = "" Then Return

    If StringLeft($sMsg, 1) = "/" Then
        _TCP_Send($g_hSocket, $sMsg & @CRLF)
        AddChat("[Command] " & $sMsg)
    Else
        _TCP_Send($g_hSocket, "MSG|" & $sMsg & @CRLF)
        AddChat("[" & $g_sMyUserName & "] " & $sMsg)
    EndIf

    GUICtrlSetData($g_hInput, "")
EndFunc   ;==>SendMessage


Func AddChat($sText)
    Local $sCurrent = GUICtrlRead($g_hEditChat)
    Local $sTime = @HOUR & ":" & @MIN & ":" & @SEC
    GUICtrlSetData($g_hEditChat, $sCurrent & "[" & $sTime & "] " & $sText & @CRLF)

    ; Auto-scroll to bottom
    Local $hEdit = GUICtrlGetHandle($g_hEditChat)
    DllCall("user32.dll", "lresult", "SendMessageW", "hwnd", $hEdit, "uint", 0x00B6, "wparam", 0, "lparam", -1) ; EM_SETSEL to end
    DllCall("user32.dll", "lresult", "SendMessageW", "hwnd", $hEdit, "uint", 0x00B7, "wparam", 0, "lparam", 0) ; EM_SCROLLCARET
EndFunc   ;==>AddChat

Func Cleanup()
    If $g_hSocket Then DisconnectFromServer()

    ; Shutdown GDI+
    _GDIPlus_Shutdown()
EndFunc   ;==>Cleanup

Func _GUICtrlEdit_Scroll($hWnd, $iDirection)
    DllCall("user32.dll", "lresult", "SendMessageW", "hwnd", GUICtrlGetHandle($hWnd), "uint", 0x00B5, "wparam", $iDirection, "lparam", 0)
EndFunc   ;==>_GUICtrlEdit_Scroll

Func ApplyBackground($sImagePath)
    If $sImagePath = "" Or Not FileExists($sImagePath) Then
        AddChat("[BG] Background image not found: " & $sImagePath)
        Return False
    EndIf

    ; Simply set the image to the background picture control
    GUICtrlSetImage($g_hBackgroundPic, $sImagePath)
    $g_sCurrentBackground = $sImagePath

    AddChat("[BG] Background applied: " & StringRegExpReplace($sImagePath, ".*\\", ""))
    Return True
EndFunc   ;==>ApplyBackground


Func WM_NOTIFY_EVENTS($hWnd, $iMsg, $wParam, $lParam)
    #forceref $hWnd, $iMsg, $wParam

    Local $tNMHDR = DllStructCreate("hwnd hWndFrom;uint_ptr IDFrom;int Code", $lParam)
    Local $hWndFrom = DllStructGetData($tNMHDR, "hWndFrom")
    Local $iCode = DllStructGetData($tNMHDR, "Code")

    ; Check if notification is from our ListView
    If $hWndFrom = GUICtrlGetHandle($g_hListViewUsers) Then
        ; Check for double-click event
        If $iCode = $NM_DBLCLK Then
            ; Get the selected item
            Local $iSelected = _GUICtrlListView_GetNextItem($g_hListViewUsers)
            If $iSelected >= 0 Then
                Local $sSocket = _GUICtrlListView_GetItemText($g_hListViewUsers, $iSelected, 0)
                Local $sDisplayName = _GUICtrlListView_GetItemText($g_hListViewUsers, $iSelected, 2)

                ; Pre-fill the input with private message command
                GUICtrlSetData($g_hInput, "/privmsg " & $sSocket & " ")
                GUICtrlSetState($g_hInput, $GUI_FOCUS)

                ; Show notification in chat
                AddChat("[System] Starting private message to: " & $sDisplayName & " (Socket: " & $sSocket & ")")
            EndIf
        EndIf
    EndIf

    Return $GUI_RUNDEFMSG
EndFunc   ;==>WM_NOTIFY_EVENTS

 

Edited by Trong

Enjoy my work? Buy me a 🍻 or tip via ❤️ PayPal

Posted (edited)

 SSS.png

 

UDP Chat Application Example

📋 Overview

The UDP Chat Application demonstrates connectionless communication using the User Datagram Protocol (UDP). This lightweight example shows the fundamental differences between TCP and UDP, including the trade-offs between reliability and performance in real-time applications.

🎯 Features

Server Features (EG_UDP_Server_Chat.au3)

  • 📡 Connectionless Communication - No persistent connections required
  • 👥 User Registration - Track active users via keep-alive mechanism
  • 📢 Broadcast Messages - Relay messages to all registered users
  • ⏱️ Automatic Timeout - Remove inactive users (60 seconds)
  • 💓 Keep-Alive System - PING/PONG protocol for user tracking
  • 📊 User List Management - Dynamic user list with timestamps
  • 🔍 IP Tracking - Monitor user IP addresses and ports
  •  High Performance - Minimal overhead, fast message delivery
  • 📝 Activity Logging - Track registrations, messages, and timeouts

Client Features (EG_UDP_Client_Chat.au3)

  • 🖥️ Simple GUI - Lightweight chat interface
  • 📝 User Registration - Register with username and display name
  • 👀 Active User List - View currently registered users
  • 💬 Public Chat - Send messages to all registered users
  • 💓 Automatic Keep-Alive - Send PING every 30 seconds
  • 🔄 No Connection Required - Start chatting immediately after registration
  •  Low Latency - Instant message delivery
  • 🎨 Minimal UI - Focus on functionality

🗂️ File Structure

EG_UDP_Server_Chat.au3  - UDP chat server application
EG_UDP_Client_Chat.au3  - UDP chat client application

🔌 Protocol Documentation

Message Format

All UDP messages use pipe-delimited format: COMMAND|param1|param2|...

Client → Server Messages

Command Format Description
REGISTER REGISTER|username|display_name Register client with server
MSG MSG|username|message Send broadcast message
PING PING Keep-alive heartbeat

Server → Client Messages

Message Format Description
WELCOME WELCOME|client_count Registration successful
REJECTED REJECTED|reason Registration rejected (duplicate name)
USERLIST USERLIST|count|user1|user2|... List of registered users
MSG MSG|from_user|message Broadcast message from user
PONG PONG Keep-alive response

🆚 TCP vs UDP Comparison

UDP Characteristics

Feature UDP TCP
Connection Connectionless Connection-oriented
Reliability No guarantee Guaranteed delivery
Ordering No guarantee Ordered delivery
Speed Faster Slower
Overhead Lower Higher
Use Case Real-time data Reliable data transfer

When to Use UDP

 Good for:

  • Real-time gaming
  • Voice/video streaming
  • Live sports scores
  • Sensor data transmission
  • Quick status updates
  • Broadcast notifications

 Not suitable for:

  • File transfers
  • Financial transactions
  • Email/messages requiring delivery
  • Database operations
  • Critical data synchronization

🚀 Getting Started

Starting the Server

  1. Run the server:

    ; Open EG_UDP_Server_Chat.au3 and run it
    
  2. Configure settings (in source):

    Global Const $SERVER_PORT = 9999        ; Change port if needed
    Global Const $USER_TIMEOUT = 60000      ; User timeout (60 seconds)
    
  3. Server binds to:

    • Default port: 9999
    • IP: 0.0.0.0 (all interfaces)

Starting the Client

  1. Run the client:

    ; Open EG_UDP_Client_Chat.au3 and run it
    
  2. Configure connection (in source):

    Global Const $SERVER_IP = "127.0.0.1"   ; Server IP
    Global Const $SERVER_PORT = 9999        ; Server port
    Global Const $PING_INTERVAL = 30000     ; Ping interval (30 sec)
    
  3. Register with server:

    • Enter username (must be unique)
    • Enter display name
    • Click "Connect"
    • Server responds with WELCOME or REJECTED

📝 Usage Examples

Basic Chat Flow

1. Server Startup:

; Server starts and binds to UDP port
[SERVER] UDP Event Server v1.0.0 started
[SERVER] Listening on port 9999

2. Client Registration:

; Client sends REGISTER message
Client → Server: REGISTER|john|John Doe

; Server responds
Server → Client: WELCOME|1
[SYSTEM] Registered successfully! 1 user(s) online.

3. Sending Messages:

; Client sends message
Client → Server: MSG|john|Hello everyone!

; Server broadcasts to all registered users
Server → All Clients: MSG|John Doe|Hello everyone!
[John Doe] Hello everyone!

4. Keep-Alive:

; Client automatically sends PING every 30 seconds
Client → Server: PING

; Server responds
Server → Client: PONG

5. User Timeout:

; If client doesn't send PING for 60 seconds
[SERVER] User 'john' timed out (no activity for 60s)
; Server removes user from active list

🔧 Configuration Options

Server Configuration

; Network Settings
Global Const $SERVER_PORT = 9999

; Timeout Settings
Global Const $USER_TIMEOUT = 60000          ; 60 seconds of inactivity
Global Const $CLEANUP_INTERVAL = 10000      ; Check every 10 seconds

; User Limits
Global Const $MAX_USERS = 100               ; Maximum registered users

; Performance
Global $g_bDebugMode = False                ; Enable for troubleshooting

Client Configuration

; Connection
Global Const $SERVER_IP = "127.0.0.1"       ; Server address
Global Const $SERVER_PORT = 9999            ; Server port

; Keep-Alive
Global Const $PING_INTERVAL = 30000         ; 30 seconds

; Debug
Global $g_bDebugMode = False                ; Troubleshooting mode

📊 Key Functions Used

Server Functions

  • _UDP_Startup() - Initialize UDP system
  • _UDP_Bind($sIP, $iPort) - Bind to UDP port
  • _UDP_RecvFrom($hSocket, $iMaxLen) - Receive UDP packet
    • Returns: [data, source_ip, source_port]
  • _UDP_SendTo($sIP, $iPort, $sData, $hSocket) - Send UDP packet
  • _UDP_CloseSocket($hSocket) - Close UDP socket
  • _UDP_Shutdown() - Cleanup UDP system

Client Functions

  • _UDP_Startup() - Initialize UDP system
  • _UDP_Bind("0.0.0.0", 0) - Bind to any available port
  • _UDP_SendTo($SERVER_IP, $SERVER_PORT, $sData, $hSocket) - Send to server
  • _UDP_RecvFrom($hSocket, $iMaxLen) - Receive from server
  • _UDP_CloseSocket($hSocket) - Close socket

🔄 Keep-Alive Mechanism

The UDP chat uses a keep-alive system to track active users:

Client                          Server
  |                              |
  |--- REGISTER|user|name ------>|
  |                              |
  |<-------- WELCOME|count ------|
  |                              |
  |                              |
  +-- Every 30 seconds --------->|
  |      PING                    |
  |                              |
  |<-------- PONG ---------------|
  |                              |
  |                              |
  +-- Server checks every 10s -->+
  |   Remove users inactive      |
  |   for more than 60s          |

Why Keep-Alive is Necessary:

UDP is connectionless, so the server has no way to know if a client is still active unless the client periodically sends messages. The keep-alive system:

  1. Client sends PING every 30 seconds - Proves it's still active
  2. Server timestamps each message - Tracks last activity
  3. Server checks every 10 seconds - Removes users with >60s inactivity
  4. Automatic cleanup - Maintains accurate user list

🐛 Troubleshooting

Common Issues

Messages not delivered:

  • Expected behavior - UDP doesn't guarantee delivery
  • Solution - Use TCP for critical messages
  • Mitigation - Implement application-level acknowledgments

Messages arrive out of order:

  • Expected behavior - UDP doesn't guarantee ordering
  • Solution - Use TCP for ordered communication
  • Mitigation - Add sequence numbers to messages

User list not updating:

  • Check PING interval vs server timeout
  • Ensure PING messages are being sent
  • Verify network connectivity
  • Check firewall settings

Can't register with server:

  • Verify server is running
  • Check IP and port settings
  • Ensure username is unique
  • Look for REJECTED message

High packet loss:

  • Check network quality
  • Reduce message frequency
  • Use TCP for important data
  • Consider smaller packet sizes

Debug Mode

Enable detailed logging:

Global $g_bDebugMode = True

This outputs all UDP packets sent and received for troubleshooting.

🎯 Performance Characteristics

Benchmarks

  • Latency: < 1ms on local network
  • Throughput: 10,000+ messages/second possible
  • Packet Size: Up to 65,507 bytes per datagram
  • Typical Usage: 100-500 bytes per chat message
  • Memory: ~5-10 MB for client/server
  • CPU: Minimal (<1% under normal load)

Scalability

  • Max Users: Limited by network bandwidth
  • Recommended: 50-100 simultaneous users
  • Message Rate: 10-100 messages/second per server
  • Network Load: ~1-2 KB/s per active user

⚠️ Limitations & Considerations

UDP Limitations

  1. No Delivery Guarantee

    • Messages may be lost
    • No automatic retransmission
    • Application must handle loss
  2. No Ordering

    • Messages may arrive out of order
    • Application must handle reordering if needed
  3. No Connection State

    • Server doesn't know if client exists
    • Must implement keep-alive mechanism
  4. Firewall Issues

    • Some firewalls block UDP
    • NAT traversal can be problematic

Security Considerations

  • No Built-in Encryption - UDP packets are plaintext
  • Spoofing Risk - Source IP can be forged
  • No Authentication - Only username-based identification
  • Broadcast Nature - All users see all messages

Recommendations for Production:

  • Add message encryption
  • Implement authentication tokens
  • Use digital signatures
  • Consider hybrid TCP/UDP approach

🔬 Advanced Topics

Implementing Reliability

Add application-level acknowledgments:

; Client sends with sequence number
_UDP_SendTo($SERVER_IP, $SERVER_PORT, "MSG|" & $iSeqNum & "|" & $sMessage, $hSocket)

; Wait for ACK
Local $hTimer = TimerInit()
While TimerDiff($hTimer) < 1000  ; 1 second timeout
    $aRecv = _UDP_RecvFrom($hSocket, 4096)
    If Not @error And StringLeft($aRecv[0], 3) = "ACK" Then
        ; Message acknowledged
        ExitLoop
    EndIf
    Sleep(10)
WEnd

Message Ordering

Add sequence numbers:

; Server side - add sequence number to broadcasts
Global $g_iMessageSeq = 0
_UDP_SendTo($sIP, $iPort, "MSG|" & $g_iMessageSeq & "|" & $sFrom & "|" & $sMsg, $hSocket)
$g_iMessageSeq += 1

; Client side - sort by sequence number
; Store messages and display in order

NAT Traversal

For internet deployment:

  1. Use STUN servers to discover public IP
  2. Implement hole punching for peer-to-peer
  3. Use relay server when direct connection fails
  4. Consider WebRTC for browser compatibility

📚 Related Examples

🎓 Learning Objectives

This example demonstrates:

  1. UDP fundamentals - Connectionless communication
  2. Datagram handling - Sending and receiving discrete packets
  3. Keep-alive systems - Tracking active clients without connections
  4. Trade-offs - Speed vs reliability decisions
  5. Real-time communication - Low-latency messaging
  6. State management - Maintaining state without connection

🤝 Contributing

Potential improvements:

  1. Add reliability layer - Implement ACK/NACK system
  2. Message encryption - Add AES encryption
  3. Voice chat - Integrate audio streaming
  4. File transfer - Chunked file transmission
  5. Private rooms - Multiple chat rooms
  6. User status - Typing indicators, online status

Note: This example is designed for educational purposes and low-stakes real-time communication. For mission-critical applications requiring guaranteed delivery, use the TCP Chat Example instead. UDP is excellent for scenarios where speed is more important than perfect reliability.

 

Server:

#Region ;**** Directives created by AutoIt3Wrapper_GUI ****
#AutoIt3Wrapper_UseX64=y
#AutoIt3Wrapper_Res_requestedExecutionLevel=requireAdministrator
#EndRegion ;**** Directives created by AutoIt3Wrapper_GUI ****
#NoTrayIcon
; ===== UDP EVENT SERVER v1.0 by Dao Van Trong - TRONG.PRO =====
; Advanced UDP Server
;
; ===== PROTOCOL STRUCTURE =====
;
; === CLIENT → SERVER MESSAGES ===
; REGISTER|<username>|<display_name>  - Register client with server
; MSG|<username>|<message>            - Broadcast chat message
; PING                                - Keep-alive ping
;
; === SERVER → CLIENT MESSAGES ===
; WELCOME|<client_count>              - Registration successful
; REJECTED|<reason>                   - Registration rejected
; USERLIST|<count>|<user1>|<user2>... - List of registered users
; MSG|<from_user>|<message>           - Broadcast message from user
; PONG                                - Ping response
;
; === UDP CHARACTERISTICS ===
; - Connectionless: No persistent connection
; - No guaranteed delivery: Messages may be lost
; - No ordering: Messages may arrive out of order
; - Lightweight: Lower overhead than TCP
; - Stateless: Server tracks clients by IP:Port
;
; === CLIENT TRACKING ===
; - Clients identified by IP:Port combination
; - Timeout after 60 seconds of inactivity
; - Automatic cleanup of inactive clients
;
#include "WinSockUDF.au3"
#include <GUIConstantsEx.au3>
#include <GuiListView.au3>
#include <EditConstants.au3>
#include <WindowsConstants.au3>

; Configuration
Global Const $SERVER_PORT = 9999
Global Const $CLIENT_TIMEOUT = 60000 ; 60 seconds

; Server state
Global $g_hSocket = 0
Global $g_aClients[1][5] ; [count][ip, port, username, displayname, last_seen]
$g_aClients[0][0] = 0

; GUI controls
Global $g_hGUI, $g_hListView, $g_hEditLog, $g_hInput, $g_hBtnSend, $g_hStatus
Global $g_iServerStartTime = 0

; Performance optimization
Global $g_bDebugMode = True ; Set to False to disable debug logs

_UDP_Startup()
CreateGUI()

; Start UDP server
$g_hSocket = _UDP_Bind("0.0.0.0", $SERVER_PORT)
If $g_hSocket = -1 Then
    MsgBox(16, "Error", "Failed to bind UDP socket on port " & $SERVER_PORT)
    Exit
EndIf

$g_iServerStartTime = TimerInit()
AddLog("[SERVER] UDP Server started on port " & $SERVER_PORT)
GUICtrlSetData($g_hStatus, "Status: Running on port " & $SERVER_PORT)

; Main loop
While True
    Switch GUIGetMsg()
        Case $GUI_EVENT_CLOSE
            Cleanup()
            Exit

        Case $g_hBtnSend
            SendBroadcastMessage()
    EndSwitch

    ; Receive UDP packets
    Local $aRecv = _UDP_RecvFrom($g_hSocket, 4096)
    If Not @error And IsArray($aRecv) Then
        HandleClientMessage($aRecv[1], $aRecv[2], $aRecv[0])
    EndIf

    ; Cleanup inactive clients
    CleanupInactiveClients()

    Sleep(10)
WEnd

Func CreateGUI()
    $g_hGUI = GUICreate("UDP Chat Server", 800, 600)

    ; Status
    $g_hStatus = GUICtrlCreateLabel("Status: Starting...", 10, 10, 780, 20)
    GUICtrlSetColor(-1, 0x0000FF)

    ; Client list
    GUICtrlCreateLabel("Connected Clients:", 10, 40, 200, 20)
    $g_hListView = GUICtrlCreateListView("IP Address|Port|Username|Display Name|Last Seen", 10, 60, 780, 200, _
            BitOR($LVS_REPORT, $LVS_SHOWSELALWAYS), _
            BitOR($WS_EX_CLIENTEDGE, $LVS_EX_FULLROWSELECT, $LVS_EX_GRIDLINES))
    _GUICtrlListView_SetColumnWidth($g_hListView, 0, 120)
    _GUICtrlListView_SetColumnWidth($g_hListView, 1, 80)
    _GUICtrlListView_SetColumnWidth($g_hListView, 2, 120)
    _GUICtrlListView_SetColumnWidth($g_hListView, 3, 150)
    _GUICtrlListView_SetColumnWidth($g_hListView, 4, 150)

    ; Server log
    GUICtrlCreateLabel("Server Log:", 10, 270, 200, 20)
    $g_hEditLog = GUICtrlCreateEdit("", 10, 290, 780, 250, BitOR($ES_READONLY, $ES_MULTILINE, $WS_VSCROLL, $ES_AUTOVSCROLL))
    GUICtrlSetFont($g_hEditLog, 9, 400, 0, "Consolas")

    ; Broadcast message
    GUICtrlCreateLabel("Broadcast Message:", 10, 550, 150, 20)
    $g_hInput = GUICtrlCreateInput("", 10, 570, 680, 22)
    $g_hBtnSend = GUICtrlCreateButton("Send to All", 700, 568, 90, 26)

    GUISetState(@SW_SHOW)
EndFunc   ;==>CreateGUI

Func HandleClientMessage($sIP, $iPort, $sData)
    $sData = StringStripWS($sData, 3)
    If $sData = "" Then Return

    Local $aCmd = StringSplit($sData, "|", 2)
    Local $sCommand = $aCmd[0]

    If $g_bDebugMode Then
        AddLog("[DEBUG] Received from " & $sIP & ":" & $iPort & " - " & StringLeft($sData, 80))
    EndIf

    Switch $sCommand
        Case "REGISTER"
            If UBound($aCmd) >= 3 Then
                Local $sUserName = $aCmd[1]
                Local $sDisplayName = $aCmd[2]

                ; Check if already registered
                Local $iIdx = FindClient($sIP, $iPort)
                If $iIdx > 0 Then
                    ; Update existing client
                    $g_aClients[$iIdx][2] = $sUserName
                    $g_aClients[$iIdx][3] = $sDisplayName
                    $g_aClients[$iIdx][4] = TimerInit()
                    AddLog("[INFO] Client updated: " & $sUserName & " (" & $sIP & ":" & $iPort & ")")
                Else
                    ; Check for duplicate username
                    If FindClientByUsername($sUserName) > 0 Then
                        _UDP_SendTo($sIP, $iPort, "REJECTED|Username already in use", $g_hSocket)
                        AddLog("[WARN] Registration rejected - duplicate username: " & $sUserName)
                        Return
                    EndIf

                    ; Add new client
                    $g_aClients[0][0] += 1
                    ReDim $g_aClients[$g_aClients[0][0] + 1][5]
                    $g_aClients[$g_aClients[0][0]][0] = $sIP
                    $g_aClients[$g_aClients[0][0]][1] = $iPort
                    $g_aClients[$g_aClients[0][0]][2] = $sUserName
                    $g_aClients[$g_aClients[0][0]][3] = $sDisplayName
                    $g_aClients[$g_aClients[0][0]][4] = TimerInit()

                    AddLog("[INFO] New client registered: " & $sUserName & " (" & $sIP & ":" & $iPort & ")")
                EndIf

                ; Send welcome message
                _UDP_SendTo($sIP, $iPort, "WELCOME|" & $g_aClients[0][0], $g_hSocket)

                ; Update GUI and broadcast user list
                UpdateClientList()
                BroadcastUserList()
            EndIf

        Case "MSG"
            If UBound($aCmd) >= 3 Then
                Local $sUserName = $aCmd[1]
                Local $sMessage = $aCmd[2]

                ; Verify sender
                Local $iIdx = FindClient($sIP, $iPort)
                If $iIdx > 0 Then
                    $g_aClients[$iIdx][4] = TimerInit() ; Update last seen

                    AddLog("[" & $sUserName & "] " & $sMessage)

                    ; Broadcast to all clients
                    Local $sBroadcast = "MSG|" & $sUserName & "|" & $sMessage
                    For $i = 1 To $g_aClients[0][0]
                        _UDP_SendTo($g_aClients[$i][0], $g_aClients[$i][1], $sBroadcast, $g_hSocket)
                    Next
                EndIf
            EndIf

        Case "PING"
            ; Update last seen and respond
            Local $iIdx = FindClient($sIP, $iPort)
            If $iIdx > 0 Then
                $g_aClients[$iIdx][4] = TimerInit()
                _UDP_SendTo($sIP, $iPort, "PONG", $g_hSocket)
            EndIf

        Case Else
            AddLog("[WARN] Unknown command from " & $sIP & ":" & $iPort & " - " & $sCommand)
    EndSwitch
EndFunc   ;==>HandleClientMessage

Func FindClient($sIP, $iPort)
    For $i = 1 To $g_aClients[0][0]
        If $g_aClients[$i][0] = $sIP And $g_aClients[$i][1] = $iPort Then
            Return $i
        EndIf
    Next
    Return 0
EndFunc   ;==>FindClient

Func FindClientByUsername($sUserName)
    For $i = 1 To $g_aClients[0][0]
        If $g_aClients[$i][2] = $sUserName Then
            Return $i
        EndIf
    Next
    Return 0
EndFunc   ;==>FindClientByUsername

Func CleanupInactiveClients()
    Local $iNow = TimerInit()
    For $i = $g_aClients[0][0] To 1 Step -1
        If TimerDiff($g_aClients[$i][4]) > $CLIENT_TIMEOUT Then
            AddLog("[INFO] Client timeout: " & $g_aClients[$i][2] & " (" & $g_aClients[$i][0] & ":" & $g_aClients[$i][1] & ")")
            RemoveClient($i)
        EndIf
    Next
EndFunc   ;==>CleanupInactiveClients

Func RemoveClient($iIdx)
    If $iIdx < 1 Or $iIdx > $g_aClients[0][0] Then Return

    ; Shift array
    For $i = $iIdx To $g_aClients[0][0] - 1
        $g_aClients[$i][0] = $g_aClients[$i + 1][0]
        $g_aClients[$i][1] = $g_aClients[$i + 1][1]
        $g_aClients[$i][2] = $g_aClients[$i + 1][2]
        $g_aClients[$i][3] = $g_aClients[$i + 1][3]
        $g_aClients[$i][4] = $g_aClients[$i + 1][4]
    Next

    $g_aClients[0][0] -= 1
    ReDim $g_aClients[$g_aClients[0][0] + 1][5]

    UpdateClientList()
    BroadcastUserList()
EndFunc   ;==>RemoveClient

Func UpdateClientList()
    _GUICtrlListView_DeleteAllItems($g_hListView)

    For $i = 1 To $g_aClients[0][0]
        Local $sLastSeen = Round(TimerDiff($g_aClients[$i][4]) / 1000, 1) & "s ago"
        _GUICtrlListView_AddItem($g_hListView, $g_aClients[$i][0])
        _GUICtrlListView_AddSubItem($g_hListView, $i - 1, $g_aClients[$i][1], 1)
        _GUICtrlListView_AddSubItem($g_hListView, $i - 1, $g_aClients[$i][2], 2)
        _GUICtrlListView_AddSubItem($g_hListView, $i - 1, $g_aClients[$i][3], 3)
        _GUICtrlListView_AddSubItem($g_hListView, $i - 1, $sLastSeen, 4)
    Next

    GUICtrlSetData($g_hStatus, "Status: Running | Clients: " & $g_aClients[0][0] & " | Port: " & $SERVER_PORT)
EndFunc   ;==>UpdateClientList

Func BroadcastUserList()
    Local $sUserList = "USERLIST|" & $g_aClients[0][0]
    For $i = 1 To $g_aClients[0][0]
        $sUserList &= "|" & $g_aClients[$i][2] & ":" & $g_aClients[$i][3]
    Next

    For $i = 1 To $g_aClients[0][0]
        _UDP_SendTo($g_aClients[$i][0], $g_aClients[$i][1], $sUserList, $g_hSocket)
    Next
EndFunc   ;==>BroadcastUserList

Func SendBroadcastMessage()
    Local $sMsg = GUICtrlRead($g_hInput)
    If $sMsg = "" Then Return

    AddLog("[SERVER] " & $sMsg)

    Local $sBroadcast = "MSG|SERVER|" & $sMsg
    For $i = 1 To $g_aClients[0][0]
        _UDP_SendTo($g_aClients[$i][0], $g_aClients[$i][1], $sBroadcast, $g_hSocket)
    Next

    GUICtrlSetData($g_hInput, "")
EndFunc   ;==>SendBroadcastMessage

Func AddLog($sText)
    Local $sCurrent = GUICtrlRead($g_hEditLog)
    Local $sTime = @HOUR & ":" & @MIN & ":" & @SEC
    GUICtrlSetData($g_hEditLog, $sCurrent & "[" & $sTime & "] " & $sText & @CRLF)

    ; Auto-scroll to bottom
    Local $hEdit = GUICtrlGetHandle($g_hEditLog)
    DllCall("user32.dll", "lresult", "SendMessageW", "hwnd", $hEdit, "uint", 0x00B6, "wparam", 0, "lparam", -1)
    DllCall("user32.dll", "lresult", "SendMessageW", "hwnd", $hEdit, "uint", 0x00B7, "wparam", 0, "lparam", 0)
EndFunc   ;==>AddLog

Func Cleanup()
    If $g_hSocket Then
        _UDP_CloseSocket($g_hSocket)
        $g_hSocket = 0
    EndIf
    _UDP_Shutdown()
    AddLog("[SERVER] Server stopped")
EndFunc   ;==>Cleanup

Client:

#Region ;**** Directives created by AutoIt3Wrapper_GUI ****
#AutoIt3Wrapper_UseX64=y
#AutoIt3Wrapper_Res_requestedExecutionLevel=requireAdministrator
#EndRegion ;**** Directives created by AutoIt3Wrapper_GUI ****
#NoTrayIcon
; ===== UDP EVENT CLIENT v1.0 by Dao Van Trong - TRONG.PRO =====
; Advanced UDP Client
;
; ===== PROTOCOL STRUCTURE =====
;
; === CLIENT → SERVER MESSAGES ===
; REGISTER|<username>|<display_name>  - Register client with server
; MSG|<username>|<message>            - Broadcast chat message
; PING                                - Keep-alive ping
;
; === SERVER → CLIENT MESSAGES ===
; WELCOME|<client_count>              - Registration successful
; REJECTED|<reason>                   - Registration rejected
; USERLIST|<count>|<user1>|<user2>... - List of registered users
; MSG|<from_user>|<message>           - Broadcast message from user
; PONG                                - Ping response
;
; === UDP CHARACTERISTICS ===
; - Connectionless: No persistent connection
; - No guaranteed delivery: Messages may be lost
; - No ordering: Messages may arrive out of order
; - Lightweight: Lower overhead than TCP
; - Best for: Real-time applications where speed > reliability
;
; === KEEP-ALIVE MECHANISM ===
; - Client sends PING every 30 seconds
; - Server tracks last seen timestamp
; - Server removes clients after 60 seconds of inactivity
;
#include "WinSockUDF.au3"
#include <GUIConstantsEx.au3>
#include <GuiListView.au3>
#include <EditConstants.au3>
#include <WindowsConstants.au3>

; Configuration
Global Const $SERVER_IP = "127.0.0.1"
Global Const $SERVER_PORT = 9999
Global Const $PING_INTERVAL = 30000 ; 30 seconds

; Client state
Global $g_hSocket = 0
Global $g_sUserName = ""
Global $g_sDisplayName = ""
Global $g_bRegistered = False
Global $g_iLastPing = 0
Global $g_aUserList[1][2] ; [count][username, displayname]
$g_aUserList[0][0] = 0

; GUI controls
Global $g_hGUI, $g_hInputUserName, $g_hInputDisplayName, $g_hBtnConnect, $g_hBtnDisconnect
Global $g_hListViewUsers, $g_hEditChat, $g_hInput, $g_hBtnSend, $g_hStatus

; Performance optimization
Global $g_bDebugMode = False ; Set to True to enable debug logs

_UDP_Startup()
CreateGUI()

; Bind local UDP socket
$g_hSocket = _UDP_Bind("0.0.0.0", 0) ; Bind to any available port
If $g_hSocket = -1 Then
    MsgBox(16, "Error", "Failed to create UDP socket")
    Exit
EndIf

AddChat("[SYSTEM] UDP Client started")

; Main loop
While True
    Switch GUIGetMsg()
        Case $GUI_EVENT_CLOSE
            Cleanup()
            Exit

        Case $g_hBtnConnect
            ConnectToServer()

        Case $g_hBtnDisconnect
            DisconnectFromServer()

        Case $g_hBtnSend
            SendMessage()
    EndSwitch

    ; Receive UDP packets
    If $g_bRegistered Then
        Local $aRecv = _UDP_RecvFrom($g_hSocket, 4096)
        If Not @error And IsArray($aRecv) Then
            HandleServerMessage($aRecv[0])
        EndIf

        ; Send periodic ping
        If TimerDiff($g_iLastPing) > $PING_INTERVAL Then
            _UDP_SendTo($SERVER_IP, $SERVER_PORT, "PING", $g_hSocket)
            $g_iLastPing = TimerInit()
        EndIf
    EndIf

    Sleep(10)
WEnd

Func CreateGUI()
    $g_hGUI = GUICreate("UDP Chat Client", 900, 600)

    ; Connection Panel
    GUICtrlCreateGroup("Connection", 10, 5, 880, 50)
    GUICtrlCreateLabel("User Name:", 20, 25, 70, 20)
    $g_hInputUserName = GUICtrlCreateInput(@UserName, 95, 23, 150, 22)

    GUICtrlCreateLabel("Display Name:", 260, 25, 80, 20)
    $g_hInputDisplayName = GUICtrlCreateInput(@UserName, 345, 23, 150, 22)

    $g_hBtnConnect = GUICtrlCreateButton("Connect", 510, 20, 100, 28)
    $g_hBtnDisconnect = GUICtrlCreateButton("Disconnect", 620, 20, 110, 28)
    GUICtrlSetState($g_hBtnDisconnect, $GUI_DISABLE)
    GUICtrlCreateGroup("", -99, -99, 1, 1)

    ; Status
    $g_hStatus = GUICtrlCreateLabel("Status: Not Connected", 10, 60, 860, 30)
    GUICtrlSetColor(-1, 0xFF0000)

    ; Main Layout: Users List + Chat Area
    GUICtrlCreateLabel("Online Users:", 10, 95, 200, 20)
    $g_hListViewUsers = GUICtrlCreateListView("Username|Display Name", 10, 115, 250, 380, _
            BitOR($LVS_REPORT, $LVS_SHOWSELALWAYS, $LVS_SINGLESEL), _
            BitOR($WS_EX_CLIENTEDGE, $LVS_EX_FULLROWSELECT, $LVS_EX_GRIDLINES))
    _GUICtrlListView_SetColumnWidth($g_hListViewUsers, 0, 120)
    _GUICtrlListView_SetColumnWidth($g_hListViewUsers, 1, 120)

    GUICtrlCreateLabel("Chat Room:", 270, 95, 200, 20)
    $g_hEditChat = GUICtrlCreateEdit("", 270, 115, 620, 380, BitOR($ES_READONLY, $ES_MULTILINE, $WS_VSCROLL, $ES_AUTOVSCROLL))
    GUICtrlSetFont($g_hEditChat, 9, 400, 0, "Consolas")

    ; Input Area
    GUICtrlCreateLabel("Message:", 10, 505, 60, 20)
    $g_hInput = GUICtrlCreateInput("", 10, 525, 780, 25)
    GUICtrlSetState($g_hInput, $GUI_DISABLE)

    $g_hBtnSend = GUICtrlCreateButton("Send", 800, 523, 90, 28)
    GUICtrlSetState($g_hBtnSend, $GUI_DISABLE)

    GUISetState(@SW_SHOW)
EndFunc   ;==>CreateGUI

Func ConnectToServer()
    $g_sUserName = GUICtrlRead($g_hInputUserName)
    $g_sDisplayName = GUICtrlRead($g_hInputDisplayName)

    If $g_sUserName = "" Or $g_sDisplayName = "" Then
        MsgBox(48, "Warning", "Please enter both username and display name")
        Return
    EndIf

    ; Send registration
    Local $sRegister = "REGISTER|" & $g_sUserName & "|" & $g_sDisplayName
    _UDP_SendTo($SERVER_IP, $SERVER_PORT, $sRegister, $g_hSocket)

    AddChat("[SYSTEM] Registering with server...")
    GUICtrlSetData($g_hStatus, "Status: Registering...")
    GUICtrlSetColor($g_hStatus, 0x0000FF)

    ; Wait for response (simple timeout)
    Local $iTimeout = TimerInit()
    While TimerDiff($iTimeout) < 5000
        Local $aRecv = _UDP_RecvFrom($g_hSocket, 4096)
        If Not @error And IsArray($aRecv) Then
            HandleServerMessage($aRecv[0])
            If $g_bRegistered Then ExitLoop
        EndIf
        Sleep(10)
    WEnd

    If Not $g_bRegistered Then
        AddChat("[ERROR] Registration timeout - server not responding")
        GUICtrlSetData($g_hStatus, "Status: Connection Failed")
        GUICtrlSetColor($g_hStatus, 0xFF0000)
    EndIf
EndFunc   ;==>ConnectToServer

Func DisconnectFromServer()
    $g_bRegistered = False
    $g_sUserName = ""
    $g_sDisplayName = ""

    AddChat("[SYSTEM] Disconnected from server")
    GUICtrlSetData($g_hStatus, "Status: Disconnected")
    GUICtrlSetColor($g_hStatus, 0xFF0000)

    GUICtrlSetState($g_hBtnConnect, $GUI_ENABLE)
    GUICtrlSetState($g_hBtnDisconnect, $GUI_DISABLE)
    GUICtrlSetState($g_hInput, $GUI_DISABLE)
    GUICtrlSetState($g_hBtnSend, $GUI_DISABLE)

    _GUICtrlListView_DeleteAllItems($g_hListViewUsers)
EndFunc   ;==>DisconnectFromServer

Func HandleServerMessage($sData)
    $sData = StringStripWS($sData, 3)
    If $sData = "" Then Return

    Local $aCmd = StringSplit($sData, "|", 2)
    Local $sCommand = $aCmd[0]

    If $g_bDebugMode Then
        AddChat("[DEBUG] Received: " & StringLeft($sData, 80))
    EndIf

    Switch $sCommand
        Case "WELCOME"
            If UBound($aCmd) >= 2 Then
                Local $iClientCount = Int($aCmd[1])
                $g_bRegistered = True
                $g_iLastPing = TimerInit()

                AddChat("=== Registration Successful ===")
                AddChat("Connected to server: " & $SERVER_IP & ":" & $SERVER_PORT)
                AddChat("Total clients: " & $iClientCount)
                AddChat("====================================")

                GUICtrlSetData($g_hStatus, "Status: Connected to " & $SERVER_IP & ":" & $SERVER_PORT)
                GUICtrlSetColor($g_hStatus, 0x00AA00)

                GUICtrlSetState($g_hBtnConnect, $GUI_DISABLE)
                GUICtrlSetState($g_hBtnDisconnect, $GUI_ENABLE)
                GUICtrlSetState($g_hInput, $GUI_ENABLE)
                GUICtrlSetState($g_hBtnSend, $GUI_ENABLE)
            EndIf

        Case "REJECTED"
            Local $sReason = UBound($aCmd) >= 2 ? $aCmd[1] : "Unknown"
            AddChat("=== Registration Rejected ===")
            AddChat("Reason: " & $sReason)

            GUICtrlSetData($g_hStatus, "Status: Rejected - " & $sReason)
            GUICtrlSetColor($g_hStatus, 0xFF0000)

        Case "USERLIST"
            If UBound($aCmd) >= 2 Then
                Local $iCount = Int($aCmd[1])
                _GUICtrlListView_DeleteAllItems($g_hListViewUsers)

                ReDim $g_aUserList[$iCount + 1][2]
                $g_aUserList[0][0] = $iCount

                For $i = 2 To UBound($aCmd) - 1
                    Local $aUser = StringSplit($aCmd[$i], ":", 2)
                    If UBound($aUser) >= 2 Then
                        Local $idx = $i - 1
                        $g_aUserList[$idx][0] = $aUser[0] ; Username
                        $g_aUserList[$idx][1] = $aUser[1] ; DisplayName

                        ; Don't show self in list
                        If $aUser[0] <> $g_sUserName Then
                            _GUICtrlListView_AddItem($g_hListViewUsers, $aUser[0])
                            _GUICtrlListView_AddSubItem($g_hListViewUsers, _GUICtrlListView_GetItemCount($g_hListViewUsers) - 1, $aUser[1], 1)
                        EndIf
                    EndIf
                Next
            EndIf

        Case "MSG"
            If UBound($aCmd) >= 3 Then
                Local $sFromUser = $aCmd[1]
                Local $sMessage = $aCmd[2]
                AddChat("[" & $sFromUser & "] " & $sMessage)
            EndIf

        Case "PONG"
            ; Ping response received
            If $g_bDebugMode Then
                AddChat("[DEBUG] PONG received")
            EndIf

        Case Else
            AddChat("[Server] " & $sData)
    EndSwitch
EndFunc   ;==>HandleServerMessage

Func SendMessage()
    Local $sMsg = GUICtrlRead($g_hInput)
    If $sMsg = "" Then Return

    If Not $g_bRegistered Then
        AddChat("[ERROR] Not connected to server")
        Return
    EndIf

    ; Send message to server
    Local $sData = "MSG|" & $g_sUserName & "|" & $sMsg
    _UDP_SendTo($SERVER_IP, $SERVER_PORT, $sData, $g_hSocket)

    AddChat("[" & $g_sUserName & "] " & $sMsg)
    GUICtrlSetData($g_hInput, "")
EndFunc   ;==>SendMessage

Func AddChat($sText)
    Local $sCurrent = GUICtrlRead($g_hEditChat)
    Local $sTime = @HOUR & ":" & @MIN & ":" & @SEC
    GUICtrlSetData($g_hEditChat, $sCurrent & "[" & $sTime & "] " & $sText & @CRLF)

    ; Auto-scroll to bottom
    Local $hEdit = GUICtrlGetHandle($g_hEditChat)
    DllCall("user32.dll", "lresult", "SendMessageW", "hwnd", $hEdit, "uint", 0x00B6, "wparam", 0, "lparam", -1)
    DllCall("user32.dll", "lresult", "SendMessageW", "hwnd", $hEdit, "uint", 0x00B7, "wparam", 0, "lparam", 0)
EndFunc   ;==>AddChat

Func Cleanup()
    If $g_bRegistered Then
        DisconnectFromServer()
    EndIf

    If $g_hSocket Then
        _UDP_CloseSocket($g_hSocket)
        $g_hSocket = 0
    EndIf

    _UDP_Shutdown()
EndFunc   ;==>Cleanup

 

Edited by Trong

Enjoy my work? Buy me a 🍻 or tip via ❤️ PayPal

Posted (edited)

where is the WinSockUDF.au3 file?
Thanks

 ... found it ...
sorry

Edited by Gianni

 

image.jpeg.9f1a974c98e9f77d824b358729b089b0.jpeg Chimp

small minds discuss people average minds discuss events great minds discuss ideas.... and use AutoIt....

Posted

Do you run the "AutoIt (Dark)" theme? The dark mode caused the unreadable text.

My UDFs and Tutorials:

Spoiler

UDFs:
Active Directory (NEW 2024-07-28 - Version 1.6.3.0) - Download - General Help & Support - Example Scripts - Wiki
ExcelChart (2017-07-21 - Version 0.4.0.1) - Download - General Help & Support - Example Scripts
OutlookEX (2021-11-16 - Version 1.7.0.0) - Download - General Help & Support - Example Scripts - Wiki
OutlookEX_GUI (2021-04-13 - Version 1.4.0.0) - Download
Outlook Tools (2019-07-22 - Version 0.6.0.0) - Download - General Help & Support - Wiki
PowerPoint (2021-08-31 - Version 1.5.0.0) - Download - General Help & Support - Example Scripts - Wiki
Task Scheduler (2022-07-28 - Version 1.6.0.1) - Download - General Help & Support - Wiki

Standard UDFs:
Excel - Example Scripts - Wiki
Word - Wiki

Tutorials:
ADO - Wiki
WebDriver - Wiki

 

Posted
2 minutes ago, water said:

Do you run the "AutoIt (Dark)" theme? The dark mode caused the unreadable text.

Yes, you're right it is the theme that I've chosen in the forum. But I think that using a custom text color in a forum post is probably not a good choice though. I don't want to have to change the forum theme just to read certain posts. And this is actually a first that I've ever seen this happen.

Posted (edited)

Workaround: Use Ctrl+a to select all text of the page and make it readable ;) 

Edited by water

My UDFs and Tutorials:

Spoiler

UDFs:
Active Directory (NEW 2024-07-28 - Version 1.6.3.0) - Download - General Help & Support - Example Scripts - Wiki
ExcelChart (2017-07-21 - Version 0.4.0.1) - Download - General Help & Support - Example Scripts
OutlookEX (2021-11-16 - Version 1.7.0.0) - Download - General Help & Support - Example Scripts - Wiki
OutlookEX_GUI (2021-04-13 - Version 1.4.0.0) - Download
Outlook Tools (2019-07-22 - Version 0.6.0.0) - Download - General Help & Support - Wiki
PowerPoint (2021-08-31 - Version 1.5.0.0) - Download - General Help & Support - Example Scripts - Wiki
Task Scheduler (2022-07-28 - Version 1.6.0.1) - Download - General Help & Support - Wiki

Standard UDFs:
Excel - Example Scripts - Wiki
Word - Wiki

Tutorials:
ADO - Wiki
WebDriver - Wiki

 

Posted
2 minutes ago, argumentum said:

If you're gonna go "hard core dark mode" you're gonna have to "fix" those on your own.

I understand what you mean. And of course, I did end up selecting all of the text to read it on the day that it was originally posted. It's great content and great contribution with this UDF. But it was difficult to read when highlighted like that.

I wouldn't really say it's "hard core dark mode" though because it isn't anything that was manually modified on the OS or browser level. It was just one of several themes that come with the forum software itself. These are all defaults that should just work.

Also, it is the only thread/post on here that I have ever seen that is done this way. I assume that the problem is it's using some sort of copy/pasted HTML colors or custom text color of some sort. Things should really just follow what the OS/browser/forum software is set to.

I just wanted to apologize though because I do not want to take away from @Trong's post and contributions which I am very thankful for. From my perspective, it is just a suggestion the ensure that things are readable by default.

I will check out the Dark Reader extension as well. But it's still jumping through extra hoops to read something that should be readable initially.

Posted (edited)
16 minutes ago, WildByDesign said:

But it's still jumping through extra hoops to read something that should be readable initially.

If this forum had better themes and more formatting options I would agree but unfortunately none of that is there and a user is somewhat limited.
On the other hand this post was formatted by AI and he just pasted it. Maybe pasting without formatting would do what you ask but would loose much of it's nice looking presentation.

P.S.: It suck to come from the future @WildByDesign and having to wait for the past to catch up :lol:

Edited by argumentum

Follow the link to my code contribution ( and other things too ).
FAQ - Please Read Before Posting.
autoit_scripter_blue_userbar.png

Posted
10 minutes ago, Trong said:

It was all my fault, I fixed it! I didn't know that the dark theme of the forum didn't invert the text colors (it seems unintelligent)

Everything looks great now and very well organized. Thank you so much.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...