File size: 11,467 Bytes
84c65b6
042d8bf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84c65b6
042d8bf
 
 
 
84c65b6
 
042d8bf
 
 
 
 
84c65b6
 
042d8bf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84c65b6
042d8bf
84c65b6
 
042d8bf
 
 
 
 
 
 
 
84c65b6
042d8bf
 
84c65b6
 
 
 
 
 
042d8bf
 
 
 
 
 
 
84c65b6
 
042d8bf
84c65b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
042d8bf
 
84c65b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
042d8bf
 
 
 
 
 
84c65b6
 
 
 
042d8bf
84c65b6
 
 
 
 
 
 
 
 
 
 
 
042d8bf
 
 
 
 
84c65b6
 
042d8bf
 
 
84c65b6
 
 
042d8bf
 
 
 
 
 
 
84c65b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
042d8bf
 
 
 
 
 
 
 
84c65b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
#!/usr/bin/env python3
# ================================================================
# node_client.py – عميل تسجيل العُقدة في نظام AmalOffload
# ---------------------------------------------------------------
# • يختار منفذًا (من ENV أو من مجموعة PORTS).
# • يجلب عنوان الـ IP المحلي.
# • يحاول التسجيل في خادم سجلٍّ مركزي واحد تِلو الآخر،
#   وعلى كل المنافذ، حتى ينجح.
# • عند النجاح يُرجع قائمة الأقران (Peers) من الخادم.
# ================================================================

import os
import socket
import time
import logging
import random
import requests
from typing import Iterable, Tuple, List

# ⬇️ منافذ مقترحة؛ يمكنك التعديل أو توليدها ديناميكيًا
DEFAULT_PORTS = {
    7520, 7384, 9021, 6998, 5810, 9274,
    8645, 7329, 7734, 8456, 6173, 7860,
    8080, 8000, 5000, 3000, 8888, 9999
}

# ⬇️ خوادم السجل الاحتياطية بالترتيب المفضَّل
DEFAULT_REGISTRY_SERVERS = [
    "http://localhost:8888",  # خادم محلي أولاً
    "http://127.0.0.1:8888",  # خادم محلي بديل
    "https://cv4790811.regru.cloud",
    "https://amaloffload.onrender.com",
    "https://osamabyc86-offload.hf.space",
    "http://10.229.36.125",
    "http://10.229.228.178",
    "http://192.168.1.1:8888",  # راوتر محلي
    "http://192.168.0.1:8888",  # راوتر محلي بديل
]

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%H:%M:%S",
)


class NodeClient:
    """

    عميل خفيف يعتني بالتسجيل المتكرِّر في خادم سجل مركزي.

    يمكن استيراده في أي سكربت وتشغيله في خيط منفصل.

    """

    def __init__(

        self,

        PORTs: Iterable[int] | None = None,

        registry_servers: List[str] | None = None,

        node_id: str | None = None,

    ):
        self.PORTs = set(PORTs) if PORTs else DEFAULT_PORTS
        self.registry_servers = list(registry_servers) if registry_servers else DEFAULT_REGISTRY_SERVERS
        self.node_id = node_id or os.getenv("NODE_ID", socket.gethostname())

        # مبدئيًّا اختَر منفذًا (أولوية للمتغيّر البيئي إن وُجد)
        self.port: int = int(os.getenv("CPU_PORT", random.choice(list(self.PORTs))))  # 🔧 تصحيح: PORTs بدلاً من PORTS
        self.current_server_index: int | None = None
        self.session = requests.Session()
        self.session.timeout = 10

    # -------------------------------------------------------------------------
    @staticmethod
    def get_local_ip() -> str:
        """يحاول معرفة أفضل عنوان IP محلي لاستخدامه في الشبكة."""
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
                # لا يهم أن ينجح الاتصال الفعلي، الهدف كشف IP واجهة الخروج
                s.connect(("8.8.8.8", 53))
                return s.getsockname()[0]
        except Exception:
            try:
                # محاولة بديلة
                hostname = socket.gethostname()
                return socket.gethostbyname(hostname)
            except Exception:
                return "127.0.0.1"

    def _register_once(self, server: str, port: int) -> List[str]:
        """مُحاولة واحدة للتسجيل؛ تُعيد peers أو ترفع استثناءً."""
        payload = {
            "node_id": self.node_id,
            "ip": self.get_local_ip(),
            "port": port,
            "hostname": socket.gethostname(),
            "timestamp": time.time()
        }
        
        # جرب مسارات مختلفة للتسجيل
        endpoints = ["/register", "/api/register", "/nodes/register", "/peer/register"]
        
        for endpoint in endpoints:
            try:
                url = f"{server.rstrip('/')}{endpoint}"
                logging.info(f"🔗 محاولة التسجيل في: {url}")
                resp = self.session.post(url, json=payload, timeout=8)
                
                if resp.status_code == 200:
                    data = resp.json()
                    logging.info(f"✅ تسجيل ناجح في {server}")
                    return data.get("peers", []) if isinstance(data, dict) else data
                elif resp.status_code == 404:
                    continue  # جرب endpoint التالي
                else:
                    resp.raise_for_status()
                    
            except requests.exceptions.Timeout:
                logging.warning(f"⏰ انتهت المهلة مع {server}{endpoint}")
                continue
            except requests.exceptions.ConnectionError:
                logging.warning(f"🔌 تعذر الاتصال بـ {server}{endpoint}")
                continue
            except Exception as e:
                logging.warning(f"❌ خطأ مع {server}{endpoint}: {e}")
                continue
        
        # إذا وصلنا هنا، فكل المحاولات فشلت
        raise Exception(f"فشل التسجيل في {server} بعد تجربة جميع المسارات")

    # -------------------------------------------------------------------------
    def discover_local_servers(self) -> List[str]:
        """اكتشاف خوادم محلية على الشبكة."""
        local_servers = []
        base_ip = ".".join(self.get_local_ip().split(".")[:-1])
        
        # فحص نطاق IPs المحلي
        for i in range(1, 50):  # فحص أول 50 عنوان فقط للسرعة
            if i == int(self.get_local_ip().split(".")[-1]):
                continue  # تخطي الذات
            
            ip = f"{base_ip}.{i}"
            for port in [8888, 8000, 5000, 3000]:
                server_url = f"http://{ip}:{port}"
                if self.check_server_availability(server_url):
                    local_servers.append(server_url)
                    logging.info(f"🔍 تم اكتشاف خادم محلي: {server_url}")
        
        return local_servers

    def check_server_availability(self, server_url: str) -> bool:
        """فحص توفر الخادم."""
        try:
            resp = self.session.get(f"{server_url}/status", timeout=2)
            return resp.status_code == 200
        except:
            return False

    def connect_until_success(self, retry_delay: int = 10) -> Tuple[str, List[str]]:
        """

        يدور على جميع المنافذ والخوادم حتى ينجح التسجيل.

        • عند النجاح يُرجع: (عنوان الخادم، قائمة الأقران)

        • لا يرفع استثناءات؛ إمّا ينجح أو يستمر في المحاولة إلى ما لا نهاية.

        """
        logging.info("🔄 بدء محاولات التسجيل للعقدة '%s'...", self.node_id)
        logging.info("🌐 عنوان IP المحلي: %s", self.get_local_ip())
        logging.info("📋 عدد الخوادم المتاحة: %d", len(self.registry_servers))
        
        attempt = 0
        while True:
            attempt += 1
            logging.info("🔄 محاولة التسجيل رقم %d", attempt)
            
            # اكتشاف خوادم محلية أولاً
            if attempt % 3 == 1:  # كل 3 محاولات، اكتشف خوادم محلية
                local_servers = self.discover_local_servers()
                all_servers = local_servers + self.registry_servers
            else:
                all_servers = self.registry_servers
            
            for port in self.PORTs:  # 🔧 تصحيح: PORTs بدلاً من PORTS
                for idx, server in enumerate(all_servers):
                    try:
                        peers = self._register_once(server, port)
                        # سجّل النجاح واحفظ المعلومات
                        self.port = port
                        self.current_server_index = idx
                        logging.info("✅ تسجيل ناجح: %s على المنفذ %s", server, port)
                        logging.info("👥 عدد الأقران المكتشفين: %d", len(peers))
                        return server, peers
                    except Exception as e:
                        logging.debug("❌ %s:%s -> %s", server, port, e)
            
            logging.warning("❌ فشلت جميع محاولات التسجيل، إعادة المحاولة بعد %d ثواني", retry_delay)
            time.sleep(retry_delay)

    # -------------------------------------------------------------------------
    def run_background(self) -> None:
        """

        إطلاق التسجيل في خيط منفصل؛ مفيد إذا كنت تريد

        إبقاء Main Thread للمهام الأخرى.

        """
        import threading
        def background_connect():
            try:
                server, peers = self.connect_until_success()
                logging.info("🎯 التسجيل الخلفي ناجح مع %s", server)
            except Exception as e:
                logging.error("💥 خطأ في التسجيل الخلفي: %s", e)
        
        threading.Thread(target=background_connect, daemon=True).start()

    def get_current_info(self) -> dict:
        """الحصول على معلومات العقدة الحالية."""
        return {
            "node_id": self.node_id,
            "ip": self.get_local_ip(),
            "port": self.port,
            "hostname": socket.gethostname(),
            "current_server": self.registry_servers[self.current_server_index] if self.current_server_index is not None else None
        }


# -----------------------------------------------------------------------------  
if __name__ == "__main__":
    """

    للتجربة المباشرة:

    $ python node_client.py

    """
    try:
        client = NodeClient()
        print("🔍 جاري اكتشاف الخوادم المحلية...")
        local_servers = client.discover_local_servers()
        if local_servers:
            print(f"✅ تم اكتشاف {len(local_servers)} خادم محلي")
        
        print("🚀 بدء عملية التسجيل...")
        server, peer_list = client.connect_until_success()
        print(f"✅ تسجيل ناجح مع الخادم: {server}")
        print(f"👥 عدد الأقران: {len(peer_list)}")
        if peer_list:
            print("📋 قائمة الأقران:")
            for i, peer in enumerate(peer_list[:10]):  # عرض أول 10 أقران فقط
                print(f"  {i+1}. {peer}")
            if len(peer_list) > 10:
                print(f"  ... و{len(peer_list) - 10} آخرين")
    except KeyboardInterrupt:
        print("\n🛑 تم إيقاف العميل بواسطة المستخدم")
    except Exception as e:
        print(f"💥 خطأ غير متوقع: {e}")