pswitch 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pswitch/__init__.py +3 -0
- pswitch/__main__.py +6 -0
- pswitch/cli.py +650 -0
- pswitch/config.py +98 -0
- pswitch/direct.py +299 -0
- pswitch/launchd.py +69 -0
- pswitch/proxy.py +397 -0
- pswitch/stream.py +40 -0
- pswitch-0.1.5.dist-info/METADATA +184 -0
- pswitch-0.1.5.dist-info/RECORD +13 -0
- pswitch-0.1.5.dist-info/WHEEL +4 -0
- pswitch-0.1.5.dist-info/entry_points.txt +2 -0
- pswitch-0.1.5.dist-info/licenses/LICENSE +21 -0
pswitch/__init__.py
ADDED
pswitch/__main__.py
ADDED
pswitch/cli.py
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
"""pswitch 命令行接口。"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import signal
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
import urllib.request
|
|
12
|
+
|
|
13
|
+
from . import __version__, launchd
|
|
14
|
+
from .config import (
|
|
15
|
+
CONFIG_PATH,
|
|
16
|
+
HOME,
|
|
17
|
+
LOG_PATH,
|
|
18
|
+
STATE_PATH,
|
|
19
|
+
config_hint,
|
|
20
|
+
enabled_upstreams,
|
|
21
|
+
ensure_config,
|
|
22
|
+
find_upstream,
|
|
23
|
+
has_upstreams,
|
|
24
|
+
load_config,
|
|
25
|
+
read_json,
|
|
26
|
+
save_config,
|
|
27
|
+
split_hostport,
|
|
28
|
+
)
|
|
29
|
+
from .proxy import daemon_running, port_open, probe_full, serve_main
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# --------------------------------------------------------------------------
|
|
33
|
+
# 小工具
|
|
34
|
+
# --------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
def signal_daemon(sig):
|
|
37
|
+
pid = daemon_running()
|
|
38
|
+
if pid:
|
|
39
|
+
try:
|
|
40
|
+
os.kill(pid, sig)
|
|
41
|
+
except OSError:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def require_upstreams(cfg, action):
|
|
46
|
+
if has_upstreams(cfg):
|
|
47
|
+
return True
|
|
48
|
+
print(f"还没有配置任何上游,拒绝{action}。")
|
|
49
|
+
print("先添加一个,例如:")
|
|
50
|
+
print(" pswitch add local 127.0.0.1:17890")
|
|
51
|
+
print(config_hint())
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _live_probe(cfg):
|
|
56
|
+
"""只探活启用中的上游,返回值与 enabled_upstreams(cfg) 一一对应。"""
|
|
57
|
+
async def _all():
|
|
58
|
+
url = cfg.get("probe_url")
|
|
59
|
+
timeout = cfg.get("probe_timeout", 3)
|
|
60
|
+
return await asyncio.gather(*[probe_full(u["addr"], url, timeout)
|
|
61
|
+
for u in enabled_upstreams(cfg)])
|
|
62
|
+
return asyncio.run(_all())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _auto_pick(ups, probes):
|
|
66
|
+
"""auto 选路:先 HTTP 通,再 TCP 通,最后退回第一个。"""
|
|
67
|
+
for key in ("http", "tcp"):
|
|
68
|
+
for u, h in zip(ups, probes):
|
|
69
|
+
if h[key]:
|
|
70
|
+
return u["name"]
|
|
71
|
+
return ups[0]["name"] if ups else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def require_enabled_upstreams(cfg, action):
|
|
75
|
+
if enabled_upstreams(cfg):
|
|
76
|
+
return True
|
|
77
|
+
print(f"所有上游都被禁用了,拒绝{action}。")
|
|
78
|
+
print("先启用一个,例如: pswitch enable <名字>")
|
|
79
|
+
print(config_hint())
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# --------------------------------------------------------------------------
|
|
84
|
+
# 进程管理
|
|
85
|
+
# --------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def pid_alive(pid):
|
|
88
|
+
try:
|
|
89
|
+
os.kill(pid, 0)
|
|
90
|
+
return True
|
|
91
|
+
except OSError:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def cmd_start(args):
|
|
96
|
+
cfg = load_config()
|
|
97
|
+
if cfg.get("mode") != "direct":
|
|
98
|
+
if not require_upstreams(cfg, "启动"):
|
|
99
|
+
return 1
|
|
100
|
+
if not require_enabled_upstreams(cfg, "启动"):
|
|
101
|
+
return 1
|
|
102
|
+
pid = daemon_running()
|
|
103
|
+
if pid:
|
|
104
|
+
print(f"已经在运行 (pid {pid})")
|
|
105
|
+
return 0
|
|
106
|
+
HOME.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
host, port = split_hostport(cfg["listen"])
|
|
108
|
+
|
|
109
|
+
if launchd.installed():
|
|
110
|
+
r = launchd.bootstrap()
|
|
111
|
+
if r.returncode != 0 and "already bootstrapped" not in (r.stderr or ""):
|
|
112
|
+
print(f"launchctl 启动失败: {(r.stderr or '').strip()}")
|
|
113
|
+
return 1
|
|
114
|
+
else:
|
|
115
|
+
out = open(LOG_PATH, "a", encoding="utf-8")
|
|
116
|
+
subprocess.Popen(
|
|
117
|
+
[sys.executable, "-m", "pswitch", "serve"],
|
|
118
|
+
stdin=subprocess.DEVNULL, stdout=out, stderr=out,
|
|
119
|
+
start_new_session=True, close_fds=True)
|
|
120
|
+
|
|
121
|
+
for _ in range(60):
|
|
122
|
+
time.sleep(0.1)
|
|
123
|
+
pid = daemon_running()
|
|
124
|
+
if pid and port_open(host, port):
|
|
125
|
+
print(f"已启动 (pid {pid}),监听 {host}:{port}")
|
|
126
|
+
return 0
|
|
127
|
+
print(f"启动失败或超时,看日志: {LOG_PATH}")
|
|
128
|
+
return 1
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def cmd_stop(args):
|
|
132
|
+
pid = daemon_running()
|
|
133
|
+
if launchd.installed():
|
|
134
|
+
# 先摘掉 launchd 作业,否则 KeepAlive 会立刻把它拉起来
|
|
135
|
+
launchd.bootout()
|
|
136
|
+
if not pid:
|
|
137
|
+
print("没有在运行")
|
|
138
|
+
return 0
|
|
139
|
+
for _ in range(50):
|
|
140
|
+
if not pid_alive(pid):
|
|
141
|
+
print("已停止")
|
|
142
|
+
return 0
|
|
143
|
+
try:
|
|
144
|
+
os.kill(pid, signal.SIGTERM)
|
|
145
|
+
except OSError:
|
|
146
|
+
pass
|
|
147
|
+
time.sleep(0.1)
|
|
148
|
+
try:
|
|
149
|
+
os.kill(pid, signal.SIGKILL)
|
|
150
|
+
print("已强制停止")
|
|
151
|
+
except OSError:
|
|
152
|
+
print("进程已退出")
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def cmd_restart(args):
|
|
157
|
+
cmd_stop(args)
|
|
158
|
+
return cmd_start(args)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# --------------------------------------------------------------------------
|
|
162
|
+
# 状态 / 上游管理
|
|
163
|
+
# --------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
def cmd_status(args):
|
|
166
|
+
cfg = load_config()
|
|
167
|
+
st = read_json(STATE_PATH) or {}
|
|
168
|
+
pid = daemon_running()
|
|
169
|
+
host, port = split_hostport(cfg["listen"])
|
|
170
|
+
listening = port_open(host, port) if pid else False
|
|
171
|
+
|
|
172
|
+
print("pswitch")
|
|
173
|
+
print(f" 配置文件 {CONFIG_PATH}")
|
|
174
|
+
if pid:
|
|
175
|
+
print(f" 守护进程 运行中 (pid {pid})")
|
|
176
|
+
else:
|
|
177
|
+
print(" 守护进程 未运行 → pswitch start")
|
|
178
|
+
print(f" 监听 {cfg['listen']} {'可连接' if listening else '不可连接'}")
|
|
179
|
+
|
|
180
|
+
mode = cfg.get("mode")
|
|
181
|
+
direct = mode == "direct"
|
|
182
|
+
|
|
183
|
+
if not has_upstreams(cfg):
|
|
184
|
+
if direct:
|
|
185
|
+
print(" 模式 direct 全部直连")
|
|
186
|
+
print(" 当前出口 direct(直接连接目标,不使用上游)")
|
|
187
|
+
print(" 上游 未配置 → pswitch add <名字> <host:port>")
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
enabled = enabled_upstreams(cfg)
|
|
191
|
+
probes = _live_probe(cfg)
|
|
192
|
+
health = {u["name"]: h for u, h in zip(enabled, probes)}
|
|
193
|
+
|
|
194
|
+
if direct:
|
|
195
|
+
print(" 模式 direct 全部直连")
|
|
196
|
+
print(" 当前出口 direct(直接连接目标,不使用上游)")
|
|
197
|
+
active = None
|
|
198
|
+
else:
|
|
199
|
+
interval = max(2, int(cfg.get("health_check_interval", 10) or 10))
|
|
200
|
+
fresh = pid and (time.time() - st.get("updated", 0)) < max(15, 3 * interval)
|
|
201
|
+
# 守护进程的 active 若指向刚被禁用的上游,就不算新鲜
|
|
202
|
+
if fresh and st.get("mode") == mode and st.get("active") in health:
|
|
203
|
+
active = st.get("active")
|
|
204
|
+
elif mode == "manual" and cfg.get("active") in health:
|
|
205
|
+
active = cfg["active"]
|
|
206
|
+
else:
|
|
207
|
+
active = _auto_pick(enabled, probes)
|
|
208
|
+
|
|
209
|
+
print(f" 模式 {'manual 固定' if mode == 'manual' else 'auto 自动选择'}")
|
|
210
|
+
if not enabled:
|
|
211
|
+
print(" 当前上游 -(所有上游都已禁用)")
|
|
212
|
+
else:
|
|
213
|
+
print(f" 当前上游 {active or '-'}")
|
|
214
|
+
manual_target = cfg.get("active")
|
|
215
|
+
if mode == "manual" and manual_target and manual_target not in health:
|
|
216
|
+
print(f" 说明 manual 目标 {manual_target} 已禁用或不存在,按 auto 顺序选择")
|
|
217
|
+
print(" 上游列表")
|
|
218
|
+
for u in cfg["upstreams"]:
|
|
219
|
+
mark = "*" if u["name"] == active else " "
|
|
220
|
+
if not u["enabled"]:
|
|
221
|
+
print(f" {mark} {u['name']:<12} {u['addr']:<24} 已禁用")
|
|
222
|
+
continue
|
|
223
|
+
h = health.get(u["name"], {})
|
|
224
|
+
if h.get("http"):
|
|
225
|
+
state = f"OK {h.get('ms') or 0:>3}ms"
|
|
226
|
+
elif h.get("tcp"):
|
|
227
|
+
state = "TCP only"
|
|
228
|
+
else:
|
|
229
|
+
state = "DOWN"
|
|
230
|
+
print(f" {mark} {u['name']:<12} {u['addr']:<24} {state}")
|
|
231
|
+
return 0
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def cmd_use(args):
|
|
235
|
+
cfg = load_config()
|
|
236
|
+
key = args.target
|
|
237
|
+
if key == "direct":
|
|
238
|
+
cfg["mode"] = "direct"
|
|
239
|
+
save_config(cfg)
|
|
240
|
+
signal_daemon(signal.SIGHUP)
|
|
241
|
+
print("已切换为 direct:所有请求直接连目标,不经过任何上游")
|
|
242
|
+
return 0
|
|
243
|
+
if not has_upstreams(cfg):
|
|
244
|
+
print("还没有配置任何上游。先 pswitch add <名字> <host:port>")
|
|
245
|
+
return 1
|
|
246
|
+
if key == "auto":
|
|
247
|
+
cfg["mode"] = "auto"
|
|
248
|
+
save_config(cfg)
|
|
249
|
+
signal_daemon(signal.SIGHUP)
|
|
250
|
+
print("已切换为 auto:按顺序自动选择第一个可用的上游")
|
|
251
|
+
return 0
|
|
252
|
+
u = find_upstream(cfg, key)
|
|
253
|
+
if not u:
|
|
254
|
+
names = ", ".join(x["name"] for x in enabled_upstreams(cfg)) or "无(都被禁用了)"
|
|
255
|
+
print(f"没有这个上游: {key}\n可选: {names}, auto, direct")
|
|
256
|
+
return 1
|
|
257
|
+
if not u["enabled"]:
|
|
258
|
+
print(f"{u['name']} 已被禁用,先 pswitch enable {u['name']}")
|
|
259
|
+
return 1
|
|
260
|
+
cfg["mode"] = "manual"
|
|
261
|
+
cfg["active"] = u["name"]
|
|
262
|
+
save_config(cfg)
|
|
263
|
+
signal_daemon(signal.SIGHUP)
|
|
264
|
+
print(f"已切换: {u['name']} -> {u['addr']}")
|
|
265
|
+
return 0
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def cmd_list(args):
|
|
269
|
+
cfg = load_config()
|
|
270
|
+
if not has_upstreams(cfg):
|
|
271
|
+
print("还没有配置任何上游。用 pswitch add <名字> <host:port> 添加。")
|
|
272
|
+
return 0
|
|
273
|
+
active = (read_json(STATE_PATH) or {}).get("active")
|
|
274
|
+
for i, u in enumerate(cfg["upstreams"], 1):
|
|
275
|
+
mark = "*" if u["name"] == active else " "
|
|
276
|
+
tag = "" if u["enabled"] else " 已禁用"
|
|
277
|
+
print(f" {mark} {i}. {u['name']:<12} {u['addr']}{tag}")
|
|
278
|
+
return 0
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def cmd_add(args):
|
|
282
|
+
cfg = load_config()
|
|
283
|
+
try:
|
|
284
|
+
split_hostport(args.addr)
|
|
285
|
+
except ValueError as e:
|
|
286
|
+
print(e)
|
|
287
|
+
return 1
|
|
288
|
+
if find_upstream(cfg, args.name):
|
|
289
|
+
print(f"名字已存在: {args.name}")
|
|
290
|
+
return 1
|
|
291
|
+
cfg["upstreams"].append({"name": args.name, "addr": args.addr, "enabled": True})
|
|
292
|
+
if not cfg.get("active"):
|
|
293
|
+
cfg["active"] = args.name
|
|
294
|
+
save_config(cfg)
|
|
295
|
+
signal_daemon(signal.SIGHUP)
|
|
296
|
+
print(f"已添加: {args.name} -> {args.addr}")
|
|
297
|
+
if len(cfg["upstreams"]) == 1:
|
|
298
|
+
print("现在可以启动了: pswitch start")
|
|
299
|
+
return 0
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def cmd_remove(args):
|
|
303
|
+
cfg = load_config()
|
|
304
|
+
before = len(cfg["upstreams"])
|
|
305
|
+
cfg["upstreams"] = [u for u in cfg["upstreams"] if u["name"] != args.name]
|
|
306
|
+
if len(cfg["upstreams"]) == before:
|
|
307
|
+
print(f"没有这个上游: {args.name}")
|
|
308
|
+
return 1
|
|
309
|
+
if cfg.get("active") == args.name:
|
|
310
|
+
cfg["mode"] = "auto"
|
|
311
|
+
remaining = enabled_upstreams(cfg) or cfg["upstreams"]
|
|
312
|
+
cfg["active"] = remaining[0]["name"] if remaining else None
|
|
313
|
+
save_config(cfg)
|
|
314
|
+
signal_daemon(signal.SIGHUP)
|
|
315
|
+
print(f"已删除: {args.name}")
|
|
316
|
+
return 0
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def cmd_priority(args):
|
|
320
|
+
cfg = load_config()
|
|
321
|
+
ups = cfg["upstreams"]
|
|
322
|
+
if not ups:
|
|
323
|
+
print("还没有配置任何上游。用 pswitch add <名字> <host:port> 添加。")
|
|
324
|
+
return 0
|
|
325
|
+
if not args.name:
|
|
326
|
+
print("优先级顺序(auto 从上到下取第一个可用的,禁用的跳过):")
|
|
327
|
+
for i, u in enumerate(ups, 1):
|
|
328
|
+
tag = "" if u["enabled"] else " 已禁用"
|
|
329
|
+
print(f" {i}. {u['name']:<12} {u['addr']:<24}{tag}".rstrip())
|
|
330
|
+
return 0
|
|
331
|
+
|
|
332
|
+
u = find_upstream(cfg, args.name)
|
|
333
|
+
if not u:
|
|
334
|
+
names = ", ".join(x["name"] for x in ups)
|
|
335
|
+
print(f"没有这个上游: {args.name}\n可选: {names}")
|
|
336
|
+
return 1
|
|
337
|
+
cur = ups.index(u)
|
|
338
|
+
if args.pos is None:
|
|
339
|
+
print(f"{u['name']} 当前优先级 {cur + 1}/{len(ups)}")
|
|
340
|
+
return 0
|
|
341
|
+
if not 1 <= args.pos <= len(ups):
|
|
342
|
+
print(f"位置要在 1..{len(ups)} 之间")
|
|
343
|
+
return 1
|
|
344
|
+
if args.pos - 1 == cur:
|
|
345
|
+
print(f"{u['name']} 已经在第 {args.pos} 位")
|
|
346
|
+
return 0
|
|
347
|
+
ups.insert(args.pos - 1, ups.pop(cur))
|
|
348
|
+
save_config(cfg)
|
|
349
|
+
signal_daemon(signal.SIGHUP)
|
|
350
|
+
print(f"已把 {u['name']} 排到第 {args.pos} 位: "
|
|
351
|
+
+ " > ".join(x["name"] for x in ups))
|
|
352
|
+
if not u["enabled"]:
|
|
353
|
+
print(f"注意:它当前是禁用状态,顺序不参与选路,先 pswitch enable {u['name']}")
|
|
354
|
+
return 0
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _set_enabled(args, enabled):
|
|
358
|
+
cfg = load_config()
|
|
359
|
+
u = find_upstream(cfg, args.name)
|
|
360
|
+
if not u:
|
|
361
|
+
names = ", ".join(x["name"] for x in cfg["upstreams"]) or "无"
|
|
362
|
+
print(f"没有这个上游: {args.name}\n可选: {names}")
|
|
363
|
+
return 1
|
|
364
|
+
if u["enabled"] == enabled:
|
|
365
|
+
print(f"{u['name']} 已经是{'启用' if enabled else '禁用'}状态")
|
|
366
|
+
return 0
|
|
367
|
+
u["enabled"] = enabled
|
|
368
|
+
save_config(cfg)
|
|
369
|
+
signal_daemon(signal.SIGHUP)
|
|
370
|
+
if enabled:
|
|
371
|
+
print(f"已启用: {u['name']} -> {u['addr']}")
|
|
372
|
+
return 0
|
|
373
|
+
print(f"已禁用: {u['name']} -> {u['addr']}(配置保留,不参与 use / auto / 故障转移)")
|
|
374
|
+
if not enabled_upstreams(cfg):
|
|
375
|
+
if cfg.get("mode") == "direct":
|
|
376
|
+
print("警告:所有上游都已禁用;direct 模式不受影响,切回 auto/manual 前需启用一个。")
|
|
377
|
+
else:
|
|
378
|
+
print("警告:所有上游都已禁用,转发会全部失败。启用一个: pswitch enable <名字>")
|
|
379
|
+
elif cfg.get("mode") == "manual" and cfg.get("active") == u["name"]:
|
|
380
|
+
print("注意:它正是当前 manual 固定的上游,守护进程会暂时改用其它可用上游。")
|
|
381
|
+
return 0
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def cmd_enable(args):
|
|
385
|
+
return _set_enabled(args, True)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def cmd_disable(args):
|
|
389
|
+
return _set_enabled(args, False)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# --------------------------------------------------------------------------
|
|
393
|
+
# 其他命令
|
|
394
|
+
# --------------------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
def cmd_version(args):
|
|
397
|
+
print(f"pswitch {__version__}")
|
|
398
|
+
return 0
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def cmd_env(args):
|
|
402
|
+
cfg = load_config()
|
|
403
|
+
proxy = f"http://{cfg['listen']}"
|
|
404
|
+
socks = f"socks5://{cfg['listen']}"
|
|
405
|
+
for line in (
|
|
406
|
+
f'export http_proxy="{proxy}"',
|
|
407
|
+
f'export https_proxy="{proxy}"',
|
|
408
|
+
f'export HTTP_PROXY="{proxy}"',
|
|
409
|
+
f'export HTTPS_PROXY="{proxy}"',
|
|
410
|
+
f'export all_proxy="{socks}"',
|
|
411
|
+
f'export ALL_PROXY="{socks}"',
|
|
412
|
+
'export no_proxy="127.0.0.1,localhost"',
|
|
413
|
+
'export NO_PROXY="127.0.0.1,localhost"',
|
|
414
|
+
):
|
|
415
|
+
print(line)
|
|
416
|
+
return 0
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def cmd_test(args):
|
|
420
|
+
cfg = load_config()
|
|
421
|
+
if cfg.get("mode") != "direct" and not require_upstreams(cfg, "测试"):
|
|
422
|
+
return 1
|
|
423
|
+
host, port = split_hostport(cfg["listen"])
|
|
424
|
+
proxy = f"http://{host}:{port}"
|
|
425
|
+
opener = urllib.request.build_opener(
|
|
426
|
+
urllib.request.ProxyHandler({"http": proxy, "https": proxy}))
|
|
427
|
+
ok_all = True
|
|
428
|
+
for url in ("http://connectivitycheck.gstatic.com/generate_204",
|
|
429
|
+
"https://www.gstatic.com/generate_204"):
|
|
430
|
+
t0 = time.monotonic()
|
|
431
|
+
try:
|
|
432
|
+
with opener.open(url, timeout=args.timeout) as r:
|
|
433
|
+
code = r.status
|
|
434
|
+
print(f"OK {url} {code} {round((time.monotonic()-t0)*1000)}ms")
|
|
435
|
+
except Exception as e:
|
|
436
|
+
ok_all = False
|
|
437
|
+
print(f"FAIL {url} {e}")
|
|
438
|
+
st = read_json(STATE_PATH) or {}
|
|
439
|
+
if st.get("active"):
|
|
440
|
+
print(f"实际出口: {st['active']} ({st.get('active_addr')})")
|
|
441
|
+
return 0 if ok_all else 1
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def cmd_logs(args):
|
|
445
|
+
if not LOG_PATH.exists():
|
|
446
|
+
print(f"暂无日志: {LOG_PATH}")
|
|
447
|
+
return 0
|
|
448
|
+
lines = LOG_PATH.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
449
|
+
for line in lines[-args.lines:]:
|
|
450
|
+
print(line)
|
|
451
|
+
print(f"\n(日志文件: {LOG_PATH},实时跟踪: tail -f {LOG_PATH})")
|
|
452
|
+
return 0
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def cmd_install(args):
|
|
456
|
+
cfg = load_config()
|
|
457
|
+
if cfg.get("mode") != "direct":
|
|
458
|
+
if not require_upstreams(cfg, "安装开机自启"):
|
|
459
|
+
return 1
|
|
460
|
+
if not require_enabled_upstreams(cfg, "安装开机自启"):
|
|
461
|
+
return 1
|
|
462
|
+
if sys.platform != "darwin":
|
|
463
|
+
print("仅支持 macOS 的 launchd")
|
|
464
|
+
return 1
|
|
465
|
+
launchd.write_plist()
|
|
466
|
+
launchd.bootout()
|
|
467
|
+
r = launchd.bootstrap()
|
|
468
|
+
if r.returncode != 0 and "already bootstrapped" not in (r.stderr or ""):
|
|
469
|
+
print(f"launchctl 失败: {(r.stderr or '').strip()}")
|
|
470
|
+
return 1
|
|
471
|
+
host, port = split_hostport(cfg["listen"])
|
|
472
|
+
for _ in range(100):
|
|
473
|
+
pid = daemon_running()
|
|
474
|
+
if pid and port_open(host, port):
|
|
475
|
+
print(f"已安装开机自启并启动 (pid {pid}),监听 {host}:{port}")
|
|
476
|
+
print(f"plist: {launchd.PLIST}")
|
|
477
|
+
print("停止并禁用: pswitch uninstall")
|
|
478
|
+
return 0
|
|
479
|
+
time.sleep(0.1)
|
|
480
|
+
print(f"已安装开机自启: {launchd.PLIST}(进程尚未就绪,看日志 {LOG_PATH})")
|
|
481
|
+
print("停止并禁用: pswitch uninstall")
|
|
482
|
+
return 0
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def cmd_uninstall(args):
|
|
486
|
+
launchd.bootout()
|
|
487
|
+
launchd.remove_plist()
|
|
488
|
+
cmd_stop(args)
|
|
489
|
+
print("已卸载开机自启")
|
|
490
|
+
return 0
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
# -- macOS 系统代理 ---------------------------------------------------------
|
|
494
|
+
|
|
495
|
+
def active_network_service():
|
|
496
|
+
try:
|
|
497
|
+
out = subprocess.run(["route", "-n", "get", "default"],
|
|
498
|
+
capture_output=True, text=True, timeout=5).stdout
|
|
499
|
+
except Exception:
|
|
500
|
+
return None
|
|
501
|
+
iface = None
|
|
502
|
+
for line in out.splitlines():
|
|
503
|
+
if "interface:" in line:
|
|
504
|
+
iface = line.split("interface:")[1].strip()
|
|
505
|
+
break
|
|
506
|
+
if not iface:
|
|
507
|
+
return None
|
|
508
|
+
try:
|
|
509
|
+
order = subprocess.run(["networksetup", "-listnetworkserviceorder"],
|
|
510
|
+
capture_output=True, text=True, timeout=5).stdout
|
|
511
|
+
except Exception:
|
|
512
|
+
return None
|
|
513
|
+
svc = None
|
|
514
|
+
for line in order.splitlines():
|
|
515
|
+
m = re.match(r"^\((?:\*)?\d+\)\s+(.+)$", line.strip())
|
|
516
|
+
if m:
|
|
517
|
+
svc = m.group(1).strip()
|
|
518
|
+
continue
|
|
519
|
+
if svc and "Device:" in line:
|
|
520
|
+
dev = line.split("Device:")[1].split(")")[0].strip()
|
|
521
|
+
if dev == iface:
|
|
522
|
+
return svc
|
|
523
|
+
return None
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def cmd_system(args):
|
|
527
|
+
cfg = load_config()
|
|
528
|
+
if cfg.get("mode") != "direct" and not require_upstreams(cfg, "修改系统代理"):
|
|
529
|
+
return 1
|
|
530
|
+
if args.action == "on" and not daemon_running():
|
|
531
|
+
print("守护进程未运行,先 pswitch start 或 pswitch install")
|
|
532
|
+
return 1
|
|
533
|
+
if (args.action == "on" and cfg.get("mode") != "direct"
|
|
534
|
+
and not require_enabled_upstreams(cfg, "开启系统代理")):
|
|
535
|
+
return 1
|
|
536
|
+
svc = active_network_service()
|
|
537
|
+
if not svc:
|
|
538
|
+
print("找不到当前活动的网络服务")
|
|
539
|
+
return 1
|
|
540
|
+
host, port = split_hostport(cfg["listen"])
|
|
541
|
+
if host == "0.0.0.0":
|
|
542
|
+
host = "127.0.0.1"
|
|
543
|
+
sets = [
|
|
544
|
+
["-setwebproxy", svc, host, str(port)],
|
|
545
|
+
["-setsecurewebproxy", svc, host, str(port)],
|
|
546
|
+
["-setsocksfirewallproxy", svc, host, str(port)],
|
|
547
|
+
["-setproxybypassdomains", svc, "127.0.0.1", "localhost"],
|
|
548
|
+
]
|
|
549
|
+
offs = [
|
|
550
|
+
["-setwebproxystate", svc, "off"],
|
|
551
|
+
["-setsecurewebproxystate", svc, "off"],
|
|
552
|
+
["-setsocksfirewallproxystate", svc, "off"],
|
|
553
|
+
]
|
|
554
|
+
if args.action == "on":
|
|
555
|
+
for cmd in sets:
|
|
556
|
+
subprocess.run(["networksetup", *cmd], capture_output=True)
|
|
557
|
+
print(f"系统代理({svc})已指向 {host}:{port}")
|
|
558
|
+
elif args.action == "off":
|
|
559
|
+
for cmd in offs:
|
|
560
|
+
subprocess.run(["networksetup", *cmd], capture_output=True)
|
|
561
|
+
print(f"系统代理({svc})已关闭")
|
|
562
|
+
else:
|
|
563
|
+
for kind, flag in (("HTTP", "-getwebproxy"),
|
|
564
|
+
("HTTPS", "-getsecurewebproxy"),
|
|
565
|
+
("SOCKS", "-getsocksfirewallproxy")):
|
|
566
|
+
r = subprocess.run(["networksetup", flag, svc],
|
|
567
|
+
capture_output=True, text=True)
|
|
568
|
+
data = dict(line.split(":", 1) for line in r.stdout.splitlines() if ":" in line)
|
|
569
|
+
state = "开" if data.get("Enabled", "").strip() == "Yes" else "关"
|
|
570
|
+
print(f" {kind:<6}{state} {data.get('Server','').strip()}:{data.get('Port','').strip()}")
|
|
571
|
+
return 0
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
# --------------------------------------------------------------------------
|
|
575
|
+
# 参数解析
|
|
576
|
+
# --------------------------------------------------------------------------
|
|
577
|
+
|
|
578
|
+
def build_parser():
|
|
579
|
+
p = argparse.ArgumentParser(prog="pswitch", description="一个本地端口,多个上游代理")
|
|
580
|
+
p.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
581
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
582
|
+
|
|
583
|
+
s = sub.add_parser("serve", help="前台运行转发进程(调试用,通常由 start / launchd 调用)")
|
|
584
|
+
s.add_argument("--foreground", action="store_true", help=argparse.SUPPRESS)
|
|
585
|
+
s.add_argument("--debug", action="store_true", help=argparse.SUPPRESS)
|
|
586
|
+
|
|
587
|
+
sub.add_parser("start", help="启动转发守护进程")
|
|
588
|
+
sub.add_parser("stop", help="停止守护进程")
|
|
589
|
+
sub.add_parser("restart", help="重启守护进程")
|
|
590
|
+
sub.add_parser("status", help="查看状态(含实时可达性)")
|
|
591
|
+
sub.add_parser("list", help="列出上游")
|
|
592
|
+
sub.add_parser("env", help="打印应设置的代理环境变量")
|
|
593
|
+
sub.add_parser("install", help="安装为 launchd 开机自启")
|
|
594
|
+
sub.add_parser("uninstall", help="卸载开机自启")
|
|
595
|
+
sub.add_parser("version", help="查看版本号")
|
|
596
|
+
|
|
597
|
+
u = sub.add_parser("use", help="切换出口:use <名字> | use auto | use direct")
|
|
598
|
+
u.add_argument("target")
|
|
599
|
+
|
|
600
|
+
a = sub.add_parser("add", help="添加上游")
|
|
601
|
+
a.add_argument("name")
|
|
602
|
+
a.add_argument("addr")
|
|
603
|
+
|
|
604
|
+
r = sub.add_parser("remove", help="删除上游")
|
|
605
|
+
r.add_argument("name")
|
|
606
|
+
|
|
607
|
+
pr = sub.add_parser("priority", help="查看/调整优先级(auto 按此顺序选)")
|
|
608
|
+
pr.add_argument("name", nargs="?", help="要调整的上游,省略则只列出顺序")
|
|
609
|
+
pr.add_argument("pos", nargs="?", type=int, help="目标位置,1 = 最高优先级")
|
|
610
|
+
|
|
611
|
+
en = sub.add_parser("enable", help="启用上游(保留配置,重新参与 use / auto 选路)")
|
|
612
|
+
en.add_argument("name")
|
|
613
|
+
|
|
614
|
+
dis = sub.add_parser("disable", help="禁用上游(保留配置,不参与 use / auto 选路)")
|
|
615
|
+
dis.add_argument("name")
|
|
616
|
+
|
|
617
|
+
t = sub.add_parser("test", help="经本地端口做一次真实请求")
|
|
618
|
+
t.add_argument("--timeout", type=float, default=8)
|
|
619
|
+
|
|
620
|
+
lg = sub.add_parser("logs", help="查看最近日志")
|
|
621
|
+
lg.add_argument("-n", "--lines", type=int, default=30)
|
|
622
|
+
|
|
623
|
+
sy = sub.add_parser("system", help="管理 macOS 系统代理指向")
|
|
624
|
+
sy.add_argument("action", choices=["on", "off", "status"])
|
|
625
|
+
|
|
626
|
+
return p
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
HANDLERS = {
|
|
630
|
+
"start": cmd_start, "stop": cmd_stop, "restart": cmd_restart,
|
|
631
|
+
"status": cmd_status, "use": cmd_use, "list": cmd_list,
|
|
632
|
+
"add": cmd_add, "remove": cmd_remove,
|
|
633
|
+
"enable": cmd_enable, "disable": cmd_disable, "priority": cmd_priority,
|
|
634
|
+
"env": cmd_env,
|
|
635
|
+
"test": cmd_test, "logs": cmd_logs,
|
|
636
|
+
"install": cmd_install, "uninstall": cmd_uninstall,
|
|
637
|
+
"system": cmd_system, "version": cmd_version,
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def main(argv=None):
|
|
642
|
+
args = build_parser().parse_args(argv)
|
|
643
|
+
if args.cmd == "serve":
|
|
644
|
+
return serve_main(args.foreground, args.debug)
|
|
645
|
+
if args.cmd == "version":
|
|
646
|
+
# 纯查询,不应该顺手创建配置文件
|
|
647
|
+
return cmd_version(args)
|
|
648
|
+
if ensure_config():
|
|
649
|
+
print(f"已创建配置文件: {CONFIG_PATH}(还没有上游,用 pswitch add 添加)")
|
|
650
|
+
return HANDLERS[args.cmd](args)
|