1
+ import os
2
+ import subprocess
3
+ import time
4
+ import argparse
5
+ import logging
6
+ import requests
7
+ import threading
8
+ import json
9
+ import urllib .parse
10
+
11
+ # Assuming speed.py is in the same directory or accessible in PYTHONPATH
12
+ from speed import get_top_proxies
13
+
14
+ # --- Configuration ---
15
+ CLASH_CONTROLLER_HOST = "127.0.0.1"
16
+ CLASH_CONTROLLER_PORT = 9090
17
+ CLASH_API_BASE_URL = f"http://{ CLASH_CONTROLLER_HOST } :{ CLASH_CONTROLLER_PORT } "
18
+ # The group proxy name to which the best individual proxy will be assigned.
19
+ # Make sure this group exists in your Clash configuration.
20
+ TARGET_PROXY_GROUP = "🚧Proxy"
21
+
22
+ def setup_logging ():
23
+ """Configures basic logging for the script. Clears previous log."""
24
+ if os .path .exists ('clash.log' ):
25
+ with open ('clash.log' , 'w' ): # clears the log file
26
+ pass
27
+ logging .basicConfig (
28
+ filename = 'clash.log' ,
29
+ level = logging .INFO ,
30
+ format = '%(asctime)s - %(message)s' ,
31
+ datefmt = '%Y-%m-%d %H:%M:%S'
32
+ )
33
+
34
+ def start_system_proxy (global_proxy_address ):
35
+ """Sets system-wide proxy environment variables."""
36
+ os .environ ["GLOBAL_PROXY" ] = global_proxy_address # Set for consistency if needed elsewhere
37
+ os .environ ["HTTP_PROXY" ] = f"http://{ global_proxy_address } "
38
+ os .environ ["HTTPS_PROXY" ] = f"http://{ global_proxy_address } "
39
+ os .environ ["http_proxy" ] = f"http://{ global_proxy_address } "
40
+ os .environ ["https_proxy" ] = f"http://{ global_proxy_address } "
41
+ # These typically don't need to be explicitly set to "false" with modern tools,
42
+ # but keeping for compatibility with your original script's intent.
43
+ os .environ ["HTTP_PROXY_REQUEST_FULLURI" ] = "false"
44
+ os .environ ["HTTPS_PROXY_REQUEST_FULLURI" ] = "false"
45
+ os .environ ["ALL_PROXY" ] = os .environ ["http_proxy" ]
46
+ logging .info (f"System-wide proxy set to: { global_proxy_address } " )
47
+
48
+ def stop_system_proxy ():
49
+ """Clears system-wide proxy environment variables."""
50
+ os .environ ["http_proxy" ] = ""
51
+ os .environ ["HTTP_PROXY" ] = ""
52
+ os .environ ["https_proxy" ] = ""
53
+ os .environ ["HTTPS_PROXY" ] = ""
54
+ os .environ ["HTTP_PROXY_REQUEST_FULLURI" ] = "true" # Revert to default
55
+ os .environ ["HTTPS_PROXY_REQUEST_FULLURI" ] = "true"
56
+ os .environ ["ALL_PROXY" ] = ""
57
+ logging .info ("System-wide proxy stopped (environment variables cleared)." )
58
+
59
+ def switch_clash_proxy_group (group_name , proxy_name ):
60
+ """
61
+ Switches the active proxy in a specified Clash proxy group to a new proxy.
62
+ """
63
+ encoded_group_name = urllib .parse .quote (group_name )
64
+ url = f"{ CLASH_API_BASE_URL } /proxies/{ encoded_group_name } "
65
+ headers = {"Content-Type" : "application/json" }
66
+ payload = {"name" : proxy_name }
67
+
68
+ try :
69
+ response = requests .put (url , headers = headers , data = json .dumps (payload ), timeout = 5 )
70
+ response .raise_for_status ()
71
+ logging .info (f"Successfully switched '{ group_name } ' to '{ proxy_name } '." )
72
+ return True
73
+ except requests .exceptions .ConnectionError :
74
+ logging .error (f"Error: Could not connect to Clash API at { CLASH_API_BASE_URL } to switch proxy." )
75
+ logging .error ("Ensure Clash is running and its external-controller is configured." )
76
+ return False
77
+ except requests .exceptions .Timeout :
78
+ logging .error (f"Error: Connection to Clash API timed out while switching proxy for '{ group_name } '." )
79
+ return False
80
+ except requests .exceptions .RequestException as e :
81
+ logging .error (f"An unexpected error occurred while switching proxy for '{ group_name } ': { e } " )
82
+ return False
83
+
84
+ def main ():
85
+ """Main function to start Clash, then periodically select and switch to best proxy."""
86
+ setup_logging ()
87
+
88
+ parser = argparse .ArgumentParser (description = "Clash management script for periodic proxy switching." )
89
+ parser .add_argument ("--minutes" , type = int , default = 10 , help = "Minutes between updates (default: 10)" )
90
+ parser .add_argument ("--iterations" , type = int , default = 1000 , help = "Number of iterations (default: 1000)" )
91
+ parser .add_argument (
92
+ "--clash-executable" ,
93
+ type = str ,
94
+ default = os .getenv ("CLASH_EXECUTABLE" ),
95
+ help = "Path to the Clash executable. Defaults to CLASH_EXECUTABLE environment variable if set."
96
+ )
97
+ parser .add_argument (
98
+ "--hk" ,
99
+ action = "store_true" ,
100
+ help = "Include HK proxies in selection (not just SG/TW)"
101
+ )
102
+ args = parser .parse_args ()
103
+
104
+ ITERATIONS = args .iterations
105
+ SLEEP_SECONDS = args .minutes * 60
106
+ clash_executable_path = args .clash_executable
107
+
108
+ if not clash_executable_path :
109
+ logging .critical ("Error: No Clash executable path provided. Please set CLASH_EXECUTABLE environment variable or use --clash-executable argument." )
110
+ return # Exit if no executable path is available
111
+
112
+ # Step 1: Stop any existing system proxy settings
113
+ stop_system_proxy ()
114
+
115
+ # Step 2: Start Clash in the background
116
+ clash_process = None
117
+ try :
118
+ # Start Clash and redirect its output to a logging function instead of a file
119
+ def log_clash_output (pipe , level = logging .INFO ):
120
+ for line in iter (pipe .readline , b'' ):
121
+ logging .log (level , f"[Clash] { line .decode (errors = 'replace' ).rstrip ()} " )
122
+ pipe .close ()
123
+
124
+ clash_process = subprocess .Popen (
125
+ [clash_executable_path ],
126
+ stdout = subprocess .PIPE ,
127
+ stderr = subprocess .PIPE
128
+ )
129
+ logging .info (f"Clash started with PID { clash_process .pid } " )
130
+
131
+ # Start threads to capture and log stdout and stderr
132
+ threading .Thread (target = log_clash_output , args = (clash_process .stdout , logging .INFO ), daemon = True ).start ()
133
+ threading .Thread (target = log_clash_output , args = (clash_process .stderr , logging .ERROR ), daemon = True ).start ()
134
+
135
+ # Give Clash a moment to fully initialize and open its API port
136
+ time .sleep (5 )
137
+ except FileNotFoundError :
138
+ logging .critical (f"Clash executable not found at: { clash_executable_path } " )
139
+ logging .critical ("Please ensure the path is correct and Clash is installed." )
140
+ return # Critical error, exit script
141
+ except Exception as e :
142
+ logging .error (f"Failed to start Clash: { e } " )
143
+ return
144
+
145
+ # Set the system-wide proxy to point to Clash's local HTTP proxy.
146
+ # Clash typically runs its HTTP proxy on port 7890 (or similar, check your config).
147
+ clash_local_proxy_address = f"{ CLASH_CONTROLLER_HOST } :7890" # Adjust if your Clash HTTP port is different
148
+ start_system_proxy (clash_local_proxy_address )
149
+
150
+ for i in range (1 , ITERATIONS + 1 ):
151
+ logging .info (f"--- Starting Iteration { i } of { ITERATIONS } ---" )
152
+
153
+ # Step 3: Test proxy speeds and select the best one
154
+ best_proxy_name = None
155
+ try :
156
+ logging .info ("Testing proxy speeds to find the best one..." )
157
+ top_proxies = get_top_proxies (num_results = 20 ) # Get top 20 proxies
158
+ if top_proxies :
159
+ # Check for SG or TW in proxy names (or HK if --hk is set)
160
+ for proxy in top_proxies :
161
+ proxy_name = proxy ['name' ]
162
+ if args .hk :
163
+ if any (x in proxy_name for x in ['HK' , 'SG' , 'TW' ]):
164
+ best_proxy_name = proxy_name
165
+ logging .info (f"Selected proxy '{ best_proxy_name } ' (contains HK/SG/TW) with latency { proxy ['latency' ]} ms" )
166
+ break
167
+ else :
168
+ if any (x in proxy_name for x in ['SG' , 'TW' ]):
169
+ best_proxy_name = proxy_name
170
+ logging .info (f"Selected proxy '{ best_proxy_name } ' (contains SG/TW) with latency { proxy ['latency' ]} ms" )
171
+ break
172
+ # If no matching proxy is found, use the first one
173
+ if not best_proxy_name :
174
+ best_proxy_name = top_proxies [0 ]['name' ]
175
+ logging .info (f"No preferred proxy found. Selected first proxy '{ best_proxy_name } ' with latency { top_proxies [0 ]['latency' ]} ms" )
176
+ else :
177
+ logging .warning ("No successful proxy tests. Cannot select a best proxy for this iteration." )
178
+ except Exception as e :
179
+ logging .error (f"Error during proxy speed testing: { e } " )
180
+
181
+ # Step 4: Switch Clash's proxy group to the best proxy (if found)
182
+ if best_proxy_name :
183
+ if not switch_clash_proxy_group (TARGET_PROXY_GROUP , best_proxy_name ):
184
+ logging .error (f"Failed to switch Clash group '{ TARGET_PROXY_GROUP } ' to '{ best_proxy_name } '." )
185
+ else :
186
+ logging .warning ("No best proxy found, skipping proxy group switch for this iteration." )
187
+
188
+ # Step 5: Wait for the specified duration
189
+ logging .info (f"Waiting for { args .minutes } minutes before next iteration..." )
190
+ time .sleep (SLEEP_SECONDS )
191
+
192
+ logging .info (f"--- Iteration { i } completed ---" )
193
+
194
+ # Step 6: Stop Clash process
195
+ if clash_process :
196
+ logging .info ("Terminating Clash process..." )
197
+ logging .info ("Terminating Clash process..." )
198
+ clash_process .terminate ()
199
+ try :
200
+ clash_process .wait (timeout = 10 ) # Give Clash a bit more time to shut down gracefully
201
+ logging .info ("Clash stopped successfully." )
202
+ except subprocess .TimeoutExpired :
203
+ logging .warning ("Clash did not terminate gracefully, killing process." )
204
+ clash_process .kill ()
205
+ clash_process .wait () # Ensure process is fully killed
206
+ except Exception as e :
207
+ logging .error (f"Error while waiting for Clash to stop: { e } " )
208
+
209
+ stop_system_proxy ()
210
+
211
+ logging .info (f"Completed { ITERATIONS } iterations. Script finished." )
212
+
213
+ if __name__ == "__main__" :
214
+ main ()
0 commit comments