tunnelgo-client 2.20.0__tar.gz
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.
- tunnelgo_client-2.20.0/PKG-INFO +97 -0
- tunnelgo_client-2.20.0/README.md +70 -0
- tunnelgo_client-2.20.0/pyproject.toml +42 -0
- tunnelgo_client-2.20.0/setup.cfg +4 -0
- tunnelgo_client-2.20.0/src/tunnelgo_client/__init__.py +10 -0
- tunnelgo_client-2.20.0/src/tunnelgo_client/__main__.py +5 -0
- tunnelgo_client-2.20.0/src/tunnelgo_client/cli.py +130 -0
- tunnelgo_client-2.20.0/src/tunnelgo_client/daemon.py +212 -0
- tunnelgo_client-2.20.0/src/tunnelgo_client/downloader.py +177 -0
- tunnelgo_client-2.20.0/src/tunnelgo_client.egg-info/PKG-INFO +97 -0
- tunnelgo_client-2.20.0/src/tunnelgo_client.egg-info/SOURCES.txt +12 -0
- tunnelgo_client-2.20.0/src/tunnelgo_client.egg-info/dependency_links.txt +1 -0
- tunnelgo_client-2.20.0/src/tunnelgo_client.egg-info/entry_points.txt +2 -0
- tunnelgo_client-2.20.0/src/tunnelgo_client.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tunnelgo-client
|
|
3
|
+
Version: 2.20.0
|
|
4
|
+
Summary: TunnelGo 内网穿透客户端 — 通过 pip 一键安装,支持 double-fork 守护进程模式
|
|
5
|
+
Author: TunnelGo Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ctz168/tunnelgo
|
|
8
|
+
Project-URL: Repository, https://github.com/ctz168/tunnelgo
|
|
9
|
+
Project-URL: Issues, https://github.com/ctz168/tunnelgo/issues
|
|
10
|
+
Keywords: tunnel,ngrok,nat-traversal,reverse-proxy,daemon
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Internet :: Proxy Servers
|
|
24
|
+
Classifier: Topic :: System :: Networking
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# TunnelGo Client (Python)
|
|
29
|
+
|
|
30
|
+
TunnelGo 内网穿透客户端的 Python 封装包。通过 `pip` 一键安装,自动下载对应平台的 Go 二进制文件,支持 double-fork 守护进程模式。
|
|
31
|
+
|
|
32
|
+
## 安装
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install tunnelgo-client
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
安装后会自动下载对应平台(Linux/macOS/Windows,amd64/arm64)的 Go 二进制文件到 `~/.tunnelgo/bin/`。
|
|
39
|
+
|
|
40
|
+
## 使用
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# 基本模式
|
|
44
|
+
tunnelgo-client --key YOUR_TOKEN --port 8080
|
|
45
|
+
|
|
46
|
+
# 子域名模式(推荐)
|
|
47
|
+
tunnelgo-client -k YOUR_TOKEN -p 8080 --subdomain myapp
|
|
48
|
+
|
|
49
|
+
# 守护进程模式(Python 原生 double-fork,脱离终端)
|
|
50
|
+
tunnelgo-client -k YOUR_TOKEN -p 8080 --daemon
|
|
51
|
+
|
|
52
|
+
# 停止守护进程
|
|
53
|
+
tunnelgo-client --stop
|
|
54
|
+
|
|
55
|
+
# 子域名 + TCP 转发
|
|
56
|
+
tunnelgo-client -k YOUR_TOKEN -p 8080 --subdomain myapp --tcp-ports 22,3306
|
|
57
|
+
|
|
58
|
+
# 禁用 P2P 直连
|
|
59
|
+
tunnelgo-client -k YOUR_TOKEN -p 8080 --no-p2p
|
|
60
|
+
|
|
61
|
+
# 强制更新客户端二进制
|
|
62
|
+
tunnelgo-client --update
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Double-Fork 守护进程模式
|
|
66
|
+
|
|
67
|
+
`--daemon` 参数启动 Python 原生的 double-fork 守护进程模式:
|
|
68
|
+
|
|
69
|
+
1. 第一次 fork → 创建中间子进程
|
|
70
|
+
2. 中间子进程调用 `setsid()` 成为新会话组长
|
|
71
|
+
3. 第二次 fork → 创建孙进程(实际守护进程)
|
|
72
|
+
4. 中间子进程退出
|
|
73
|
+
5. 孙进程被 init (PID 1) 收养,完全脱离终端
|
|
74
|
+
|
|
75
|
+
守护进程的日志输出到 `/tmp/tunnelgo-client.log`(Windows 为 `%TEMP%\tunnelgo-client.log`)。
|
|
76
|
+
|
|
77
|
+
## 参数
|
|
78
|
+
|
|
79
|
+
| 参数 | 说明 | 默认值 |
|
|
80
|
+
|------|------|--------|
|
|
81
|
+
| `-k, --key` | 认证令牌(必填) | - |
|
|
82
|
+
| `-p, --port` | 本地服务端口 | 8080 |
|
|
83
|
+
| `-s, --server` | 服务器地址 | aicq.online:6639 |
|
|
84
|
+
| `--host` | 本地服务地址 | localhost |
|
|
85
|
+
| `--p2p-port` | P2P 监听端口 | 与 --port 相同 |
|
|
86
|
+
| `--no-p2p` | 禁用 P2P 直连 | false |
|
|
87
|
+
| `--tcp-ports` | TCP 转发端口,逗号分隔 | - |
|
|
88
|
+
| `--tcp-default` | 设为非TLS同端口默认路由 | false |
|
|
89
|
+
| `--http-port` | HTTP 独立端口模式 | false |
|
|
90
|
+
| `--subdomain` | 子域名前缀 | - |
|
|
91
|
+
| `--daemon` | 以守护进程模式运行 (double-fork) | false |
|
|
92
|
+
| `--stop` | 停止守护进程 | false |
|
|
93
|
+
| `--update` | 强制更新客户端二进制 | false |
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# TunnelGo Client (Python)
|
|
2
|
+
|
|
3
|
+
TunnelGo 内网穿透客户端的 Python 封装包。通过 `pip` 一键安装,自动下载对应平台的 Go 二进制文件,支持 double-fork 守护进程模式。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install tunnelgo-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
安装后会自动下载对应平台(Linux/macOS/Windows,amd64/arm64)的 Go 二进制文件到 `~/.tunnelgo/bin/`。
|
|
12
|
+
|
|
13
|
+
## 使用
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# 基本模式
|
|
17
|
+
tunnelgo-client --key YOUR_TOKEN --port 8080
|
|
18
|
+
|
|
19
|
+
# 子域名模式(推荐)
|
|
20
|
+
tunnelgo-client -k YOUR_TOKEN -p 8080 --subdomain myapp
|
|
21
|
+
|
|
22
|
+
# 守护进程模式(Python 原生 double-fork,脱离终端)
|
|
23
|
+
tunnelgo-client -k YOUR_TOKEN -p 8080 --daemon
|
|
24
|
+
|
|
25
|
+
# 停止守护进程
|
|
26
|
+
tunnelgo-client --stop
|
|
27
|
+
|
|
28
|
+
# 子域名 + TCP 转发
|
|
29
|
+
tunnelgo-client -k YOUR_TOKEN -p 8080 --subdomain myapp --tcp-ports 22,3306
|
|
30
|
+
|
|
31
|
+
# 禁用 P2P 直连
|
|
32
|
+
tunnelgo-client -k YOUR_TOKEN -p 8080 --no-p2p
|
|
33
|
+
|
|
34
|
+
# 强制更新客户端二进制
|
|
35
|
+
tunnelgo-client --update
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Double-Fork 守护进程模式
|
|
39
|
+
|
|
40
|
+
`--daemon` 参数启动 Python 原生的 double-fork 守护进程模式:
|
|
41
|
+
|
|
42
|
+
1. 第一次 fork → 创建中间子进程
|
|
43
|
+
2. 中间子进程调用 `setsid()` 成为新会话组长
|
|
44
|
+
3. 第二次 fork → 创建孙进程(实际守护进程)
|
|
45
|
+
4. 中间子进程退出
|
|
46
|
+
5. 孙进程被 init (PID 1) 收养,完全脱离终端
|
|
47
|
+
|
|
48
|
+
守护进程的日志输出到 `/tmp/tunnelgo-client.log`(Windows 为 `%TEMP%\tunnelgo-client.log`)。
|
|
49
|
+
|
|
50
|
+
## 参数
|
|
51
|
+
|
|
52
|
+
| 参数 | 说明 | 默认值 |
|
|
53
|
+
|------|------|--------|
|
|
54
|
+
| `-k, --key` | 认证令牌(必填) | - |
|
|
55
|
+
| `-p, --port` | 本地服务端口 | 8080 |
|
|
56
|
+
| `-s, --server` | 服务器地址 | aicq.online:6639 |
|
|
57
|
+
| `--host` | 本地服务地址 | localhost |
|
|
58
|
+
| `--p2p-port` | P2P 监听端口 | 与 --port 相同 |
|
|
59
|
+
| `--no-p2p` | 禁用 P2P 直连 | false |
|
|
60
|
+
| `--tcp-ports` | TCP 转发端口,逗号分隔 | - |
|
|
61
|
+
| `--tcp-default` | 设为非TLS同端口默认路由 | false |
|
|
62
|
+
| `--http-port` | HTTP 独立端口模式 | false |
|
|
63
|
+
| `--subdomain` | 子域名前缀 | - |
|
|
64
|
+
| `--daemon` | 以守护进程模式运行 (double-fork) | false |
|
|
65
|
+
| `--stop` | 停止守护进程 | false |
|
|
66
|
+
| `--update` | 强制更新客户端二进制 | false |
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tunnelgo-client"
|
|
7
|
+
version = "2.20.0"
|
|
8
|
+
description = "TunnelGo 内网穿透客户端 — 通过 pip 一键安装,支持 double-fork 守护进程模式"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "TunnelGo Contributors"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["tunnel", "ngrok", "nat-traversal", "reverse-proxy", "daemon"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Intended Audience :: System Administrators",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.8",
|
|
25
|
+
"Programming Language :: Python :: 3.9",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Topic :: Internet :: Proxy Servers",
|
|
30
|
+
"Topic :: System :: Networking",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
tunnelgo-client = "tunnelgo_client.cli:main"
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/ctz168/tunnelgo"
|
|
38
|
+
Repository = "https://github.com/ctz168/tunnelgo"
|
|
39
|
+
Issues = "https://github.com/ctz168/tunnelgo/issues"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI entry point for the tunnelgo-client Python package.
|
|
3
|
+
|
|
4
|
+
Provides the same interface as the Go binary, plus automatic
|
|
5
|
+
binary download and Python-native double-fork daemon mode.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
from .downloader import ensure_binary
|
|
15
|
+
from .daemon import double_fork_daemon, stop_daemon
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
"""Main CLI entry point."""
|
|
20
|
+
parser = argparse.ArgumentParser(
|
|
21
|
+
prog="tunnelgo-client",
|
|
22
|
+
description=f"TunnelGo 客户端 v{__version__} (Python 封装,自动下载 Go 二进制)",
|
|
23
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
24
|
+
epilog="""
|
|
25
|
+
示例:
|
|
26
|
+
tunnelgo-client --key YOUR_TOKEN --port 8080
|
|
27
|
+
tunnelgo-client -k YOUR_TOKEN -p 3000 -s aicq.online:6639
|
|
28
|
+
tunnelgo-client -k YOUR_TOKEN -p 80 --no-p2p
|
|
29
|
+
tunnelgo-client -k TOKEN -p 80 --daemon 后台守护进程运行 (double-fork)
|
|
30
|
+
tunnelgo-client --stop 停止守护进程
|
|
31
|
+
|
|
32
|
+
P2P 模式 (默认启用):
|
|
33
|
+
优先级: IPv6 直连 > UPnP IPv4 > 服务器中转
|
|
34
|
+
""",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
parser.add_argument("-k", "--key", default="", help="认证令牌 (必须)")
|
|
38
|
+
parser.add_argument("-p", "--port", type=int, default=8080, help="本地服务端口 (默认: 8080)")
|
|
39
|
+
parser.add_argument("-s", "--server", default="aicq.online:6639", help="服务器地址 (默认: aicq.online:6639)")
|
|
40
|
+
parser.add_argument("--host", default="localhost", help="本地服务地址 (默认: localhost)")
|
|
41
|
+
parser.add_argument("--p2p-port", type=int, default=0, help="P2P 监听端口 (默认: 与 --port 相同)")
|
|
42
|
+
parser.add_argument("--no-p2p", action="store_true", help="禁用 P2P 直连,强制使用服务器中转模式")
|
|
43
|
+
parser.add_argument("--tcp-ports", default="", help="TCP 转发端口,逗号分隔 (如: 22,3306)")
|
|
44
|
+
parser.add_argument("--http-port", action="store_true", help="启用 HTTP 独立端口模式")
|
|
45
|
+
parser.add_argument("--subdomain", default="", help="子域名前缀 (如: myapp)")
|
|
46
|
+
parser.add_argument("--tcp-default", action="store_true", help="设为非TLS同端口默认路由")
|
|
47
|
+
parser.add_argument("--daemon", action="store_true", help="以守护进程模式运行 (double-fork,脱离终端)")
|
|
48
|
+
parser.add_argument("--stop", action="store_true", help="停止守护进程 (通过 PID 文件)")
|
|
49
|
+
parser.add_argument("--update", action="store_true", help="强制更新到最新版本的客户端二进制")
|
|
50
|
+
parser.add_argument("--version", action="version", version=f"tunnelgo-client v{__version__} (Python)")
|
|
51
|
+
|
|
52
|
+
args = parser.parse_args()
|
|
53
|
+
|
|
54
|
+
# Handle --stop
|
|
55
|
+
if args.stop:
|
|
56
|
+
stop_daemon()
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# Validate required arguments
|
|
60
|
+
if not args.key:
|
|
61
|
+
parser.error("必须提供认证令牌 (--key)")
|
|
62
|
+
|
|
63
|
+
# Ensure the Go binary is available
|
|
64
|
+
try:
|
|
65
|
+
binary_path = ensure_binary()
|
|
66
|
+
except RuntimeError as e:
|
|
67
|
+
print(f"错误: {e}", file=sys.stderr)
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
|
|
70
|
+
# Handle --update
|
|
71
|
+
if args.update:
|
|
72
|
+
from .downloader import download_binary
|
|
73
|
+
try:
|
|
74
|
+
binary_path = download_binary(force=True)
|
|
75
|
+
print(f" 客户端已更新: {binary_path}")
|
|
76
|
+
except RuntimeError as e:
|
|
77
|
+
print(f"更新失败: {e}", file=sys.stderr)
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# Build Go binary arguments
|
|
82
|
+
go_args = []
|
|
83
|
+
go_args.extend(["-k", args.key])
|
|
84
|
+
go_args.extend(["-p", str(args.port)])
|
|
85
|
+
go_args.extend(["-s", args.server])
|
|
86
|
+
go_args.extend(["--host", args.host])
|
|
87
|
+
|
|
88
|
+
if args.p2p_port:
|
|
89
|
+
go_args.extend(["--p2p-port", str(args.p2p_port)])
|
|
90
|
+
|
|
91
|
+
if args.no_p2p:
|
|
92
|
+
go_args.append("--no-p2p")
|
|
93
|
+
|
|
94
|
+
if args.tcp_ports:
|
|
95
|
+
go_args.extend(["--tcp-ports", args.tcp_ports])
|
|
96
|
+
|
|
97
|
+
if args.http_port:
|
|
98
|
+
go_args.append("--http-port")
|
|
99
|
+
|
|
100
|
+
if args.subdomain:
|
|
101
|
+
go_args.extend(["--subdomain", args.subdomain])
|
|
102
|
+
|
|
103
|
+
if args.tcp_default:
|
|
104
|
+
go_args.append("--tcp-default")
|
|
105
|
+
|
|
106
|
+
# Handle --daemon: use Python-native double-fork
|
|
107
|
+
if args.daemon:
|
|
108
|
+
# Do NOT pass --daemon to Go binary — Python handles the double-fork
|
|
109
|
+
# The Go binary will run in the foreground inside the daemon process
|
|
110
|
+
double_fork_daemon(binary_path, go_args)
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# Normal mode: run the Go binary in the foreground
|
|
114
|
+
try:
|
|
115
|
+
result = subprocess.run(
|
|
116
|
+
[binary_path] + go_args,
|
|
117
|
+
stdin=sys.stdin,
|
|
118
|
+
stdout=sys.stdout,
|
|
119
|
+
stderr=sys.stderr,
|
|
120
|
+
)
|
|
121
|
+
sys.exit(result.returncode)
|
|
122
|
+
except KeyboardInterrupt:
|
|
123
|
+
sys.exit(0)
|
|
124
|
+
except FileNotFoundError:
|
|
125
|
+
print(f"错误: 找不到客户端二进制: {binary_path}", file=sys.stderr)
|
|
126
|
+
sys.exit(1)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == "__main__":
|
|
130
|
+
main()
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Double-fork daemon mode for TunnelGo client.
|
|
3
|
+
|
|
4
|
+
Implements the classic Unix double-fork pattern in Python:
|
|
5
|
+
1. First fork → child_1 (intermediate)
|
|
6
|
+
2. Child_1: setsid() to become session leader
|
|
7
|
+
3. Second fork → child_2 (grandchild, the actual daemon)
|
|
8
|
+
4. Child_1 exits
|
|
9
|
+
5. Grandchild: redirect stdio, write PID file, run the client
|
|
10
|
+
|
|
11
|
+
This ensures the daemon is fully detached from the terminal
|
|
12
|
+
and cannot accidentally acquire a controlling terminal.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import signal
|
|
18
|
+
import subprocess
|
|
19
|
+
import time
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def pid_file_path():
|
|
23
|
+
"""Return the path to the PID file."""
|
|
24
|
+
return "/tmp/tunnelgo-client.pid"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def log_file_path():
|
|
28
|
+
"""Return the path to the daemon log file."""
|
|
29
|
+
if sys.platform == "win32":
|
|
30
|
+
return os.path.join(os.environ.get("TEMP", os.environ.get("TMP", "C:\\Temp")), "tunnelgo-client.log")
|
|
31
|
+
return "/tmp/tunnelgo-client.log"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def write_pid_file():
|
|
35
|
+
"""Write the current PID to the PID file."""
|
|
36
|
+
with open(pid_file_path(), "w") as f:
|
|
37
|
+
f.write(str(os.getpid()))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def remove_pid_file():
|
|
41
|
+
"""Remove the PID file."""
|
|
42
|
+
try:
|
|
43
|
+
os.remove(pid_file_path())
|
|
44
|
+
except FileNotFoundError:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def stop_daemon():
|
|
49
|
+
"""Stop the daemon process via PID file."""
|
|
50
|
+
try:
|
|
51
|
+
with open(pid_file_path(), "r") as f:
|
|
52
|
+
pid = int(f.read().strip())
|
|
53
|
+
except (FileNotFoundError, ValueError):
|
|
54
|
+
print("守护进程未运行 (PID 文件不存在)", file=sys.stderr)
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
os.kill(pid, signal.SIGTERM)
|
|
59
|
+
except ProcessLookupError:
|
|
60
|
+
print(f"进程 {pid} 不存在", file=sys.stderr)
|
|
61
|
+
except PermissionError:
|
|
62
|
+
print(f"无权限终止进程 {pid}", file=sys.stderr)
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
except OSError as e:
|
|
65
|
+
print(f"终止进程 {pid} 失败: {e}", file=sys.stderr)
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
|
|
68
|
+
remove_pid_file()
|
|
69
|
+
print(f" 守护进程已停止 (PID: {pid})")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def double_fork_daemon(binary_path, args):
|
|
73
|
+
"""
|
|
74
|
+
Start the TunnelGo client as a double-fork daemon.
|
|
75
|
+
|
|
76
|
+
This implements the classic Unix double-fork pattern:
|
|
77
|
+
1. Fork → child (intermediate process)
|
|
78
|
+
2. Child: setsid() to become session leader
|
|
79
|
+
3. Fork again → grandchild (actual daemon)
|
|
80
|
+
4. Child exits
|
|
81
|
+
5. Grandchild: redirect stdio, write PID, exec the Go binary
|
|
82
|
+
|
|
83
|
+
On Windows, this falls back to a simple detached subprocess.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
binary_path: Path to the Go client binary.
|
|
87
|
+
args: List of command-line arguments to pass to the binary.
|
|
88
|
+
"""
|
|
89
|
+
if sys.platform == "win32":
|
|
90
|
+
_daemon_windows(binary_path, args)
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# Unix: true double-fork
|
|
94
|
+
_daemon_unix(binary_path, args)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _daemon_unix(binary_path, args):
|
|
98
|
+
"""Double-fork daemon implementation for Unix systems."""
|
|
99
|
+
# Build the full command
|
|
100
|
+
cmd = [binary_path] + args
|
|
101
|
+
|
|
102
|
+
# First fork
|
|
103
|
+
try:
|
|
104
|
+
pid = os.fork()
|
|
105
|
+
except OSError as e:
|
|
106
|
+
print(f"守护进程错误: 第一次 fork 失败: {e}", file=sys.stderr)
|
|
107
|
+
sys.exit(1)
|
|
108
|
+
|
|
109
|
+
if pid > 0:
|
|
110
|
+
# Parent process: wait briefly for daemon to start, then return
|
|
111
|
+
# The child will fork again and the grandchild will be the actual daemon
|
|
112
|
+
os.waitpid(pid, 0) # Wait for intermediate child to exit
|
|
113
|
+
|
|
114
|
+
# Wait for PID file to appear
|
|
115
|
+
for _ in range(20):
|
|
116
|
+
try:
|
|
117
|
+
with open(pid_file_path(), "r") as f:
|
|
118
|
+
daemon_pid = int(f.read().strip())
|
|
119
|
+
if daemon_pid > 0:
|
|
120
|
+
print(f" 守护进程已启动 (PID: {daemon_pid})")
|
|
121
|
+
break
|
|
122
|
+
except (FileNotFoundError, ValueError):
|
|
123
|
+
pass
|
|
124
|
+
time.sleep(0.1)
|
|
125
|
+
else:
|
|
126
|
+
print(" 守护进程已启动 (PID 文件尚未写入,请查看日志)")
|
|
127
|
+
|
|
128
|
+
print(f" 日志文件: {log_file_path()}")
|
|
129
|
+
print(" 停止命令: tunnelgo-client --stop")
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# First fork child (intermediate): create new session, then fork again
|
|
133
|
+
os.setsid() # Become session leader, detach from controlling terminal
|
|
134
|
+
|
|
135
|
+
# Second fork
|
|
136
|
+
try:
|
|
137
|
+
pid2 = os.fork()
|
|
138
|
+
except OSError as e:
|
|
139
|
+
print(f"守护进程错误: 第二次 fork 失败: {e}", file=sys.stderr)
|
|
140
|
+
os._exit(1)
|
|
141
|
+
|
|
142
|
+
if pid2 > 0:
|
|
143
|
+
# Intermediate child exits — grandchild is adopted by init (PID 1)
|
|
144
|
+
os._exit(0)
|
|
145
|
+
|
|
146
|
+
# Grandchild: the actual daemon process
|
|
147
|
+
# Redirect standard file descriptors
|
|
148
|
+
_redirect_stdio()
|
|
149
|
+
|
|
150
|
+
# Write PID file
|
|
151
|
+
write_pid_file()
|
|
152
|
+
|
|
153
|
+
# Execute the Go binary (replaces the Python process)
|
|
154
|
+
try:
|
|
155
|
+
os.execv(binary_path, cmd)
|
|
156
|
+
except OSError as e:
|
|
157
|
+
print(f"守护进程错误: 无法执行 {binary_path}: {e}", file=sys.stderr)
|
|
158
|
+
remove_pid_file()
|
|
159
|
+
os._exit(1)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _daemon_windows(binary_path, args):
|
|
163
|
+
"""Daemon implementation for Windows (detached subprocess)."""
|
|
164
|
+
cmd = [binary_path] + args
|
|
165
|
+
|
|
166
|
+
log_file = log_file_path()
|
|
167
|
+
try:
|
|
168
|
+
lf = open(log_file, "a")
|
|
169
|
+
except IOError:
|
|
170
|
+
lf = subprocess.DEVNULL
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
proc = subprocess.Popen(
|
|
174
|
+
cmd,
|
|
175
|
+
stdin=subprocess.DEVNULL,
|
|
176
|
+
stdout=lf if lf != subprocess.DEVNULL else subprocess.DEVNULL,
|
|
177
|
+
stderr=subprocess.STDOUT if lf != subprocess.DEVNULL else subprocess.DEVNULL,
|
|
178
|
+
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS,
|
|
179
|
+
close_fds=True,
|
|
180
|
+
)
|
|
181
|
+
print(f" 守护进程已启动 (PID: {proc.pid})")
|
|
182
|
+
print(f" 日志文件: {log_file}")
|
|
183
|
+
print(" 停止命令: tunnelgo-client --stop")
|
|
184
|
+
except Exception as e:
|
|
185
|
+
print(f"守护进程错误: 启动失败: {e}", file=sys.stderr)
|
|
186
|
+
sys.exit(1)
|
|
187
|
+
finally:
|
|
188
|
+
if lf != subprocess.DEVNULL:
|
|
189
|
+
lf.close()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _redirect_stdio():
|
|
193
|
+
"""Redirect stdin/stdout/stderr for the daemon process."""
|
|
194
|
+
# stdin → /dev/null
|
|
195
|
+
devnull = os.open(os.devnull, os.O_RDWR)
|
|
196
|
+
os.dup2(devnull, 0) # stdin
|
|
197
|
+
|
|
198
|
+
# stdout/stderr → log file
|
|
199
|
+
log_path = log_file_path()
|
|
200
|
+
try:
|
|
201
|
+
log_fd = os.open(log_path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
|
|
202
|
+
except OSError:
|
|
203
|
+
log_fd = devnull
|
|
204
|
+
|
|
205
|
+
os.dup2(log_fd, 1) # stdout
|
|
206
|
+
os.dup2(log_fd, 2) # stderr
|
|
207
|
+
|
|
208
|
+
# Close original fds
|
|
209
|
+
if devnull > 2:
|
|
210
|
+
os.close(devnull)
|
|
211
|
+
if log_fd > 2 and log_fd != devnull:
|
|
212
|
+
os.close(log_fd)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Binary downloader for TunnelGo client.
|
|
3
|
+
|
|
4
|
+
Downloads the platform-appropriate Go binary from GitHub Releases
|
|
5
|
+
and caches it in ~/.tunnelgo/bin/.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import io
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import platform
|
|
13
|
+
import stat
|
|
14
|
+
import sys
|
|
15
|
+
import urllib.request
|
|
16
|
+
import urllib.error
|
|
17
|
+
import zipfile
|
|
18
|
+
import tarfile
|
|
19
|
+
|
|
20
|
+
# GitHub repo info
|
|
21
|
+
GITHUB_OWNER = "ctz168"
|
|
22
|
+
GITHUB_REPO = "tunnelgo"
|
|
23
|
+
|
|
24
|
+
# Cache directory
|
|
25
|
+
CACHE_DIR = os.path.expanduser("~/.tunnelgo/bin")
|
|
26
|
+
VERSION_FILE = os.path.join(CACHE_DIR, ".installed_version")
|
|
27
|
+
|
|
28
|
+
# Platform mapping: (system, machine) → binary name suffix
|
|
29
|
+
PLATFORM_MAP = {
|
|
30
|
+
("Linux", "x86_64"): "tunnelgo-client-linux-amd64",
|
|
31
|
+
("Linux", "aarch64"): "tunnelgo-client-linux-arm64",
|
|
32
|
+
("Linux", "armv7l"): "tunnelgo-client-linux-arm64",
|
|
33
|
+
("Darwin", "x86_64"): "tunnelgo-client-darwin-amd64",
|
|
34
|
+
("Darwin", "arm64"): "tunnelgo-client-darwin-arm64",
|
|
35
|
+
("Windows", "AMD64"): "tunnelgo-client-windows-amd64.exe",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_platform_binary_name():
|
|
40
|
+
"""Return the binary filename for the current platform."""
|
|
41
|
+
system = platform.system()
|
|
42
|
+
machine = platform.machine()
|
|
43
|
+
|
|
44
|
+
# Normalize machine names
|
|
45
|
+
machine_lower = machine.lower()
|
|
46
|
+
if machine_lower in ("x86_64", "amd64"):
|
|
47
|
+
machine_key = "x86_64" if system != "Windows" else "AMD64"
|
|
48
|
+
elif machine_lower in ("aarch64", "arm64", "armv7l"):
|
|
49
|
+
machine_key = machine
|
|
50
|
+
else:
|
|
51
|
+
machine_key = machine
|
|
52
|
+
|
|
53
|
+
key = (system, machine_key)
|
|
54
|
+
if key in PLATFORM_MAP:
|
|
55
|
+
return PLATFORM_MAP[key]
|
|
56
|
+
|
|
57
|
+
# Fallback: try common combinations
|
|
58
|
+
for k, v in PLATFORM_MAP.items():
|
|
59
|
+
if k[0] == system:
|
|
60
|
+
return v
|
|
61
|
+
|
|
62
|
+
raise RuntimeError(
|
|
63
|
+
f"Unsupported platform: {system} {machine}. "
|
|
64
|
+
f"Please download the binary manually from "
|
|
65
|
+
f"https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}/releases"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_latest_version():
|
|
70
|
+
"""Fetch the latest release version from GitHub API."""
|
|
71
|
+
api_url = f"https://api.github.com/repos/{GITHUB_OWNER}/{GITHUB_REPO}/releases/latest"
|
|
72
|
+
try:
|
|
73
|
+
req = urllib.request.Request(api_url, headers={"User-Agent": "tunnelgo-client-python"})
|
|
74
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
75
|
+
data = json.loads(resp.read().decode())
|
|
76
|
+
return data.get("tag_name", "").lstrip("v")
|
|
77
|
+
except Exception:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_installed_version():
|
|
82
|
+
"""Read the currently installed version from the version file."""
|
|
83
|
+
try:
|
|
84
|
+
with open(VERSION_FILE, "r") as f:
|
|
85
|
+
return f.read().strip()
|
|
86
|
+
except (FileNotFoundError, IOError):
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_binary_path():
|
|
91
|
+
"""Return the full path to the cached binary."""
|
|
92
|
+
binary_name = get_platform_binary_name()
|
|
93
|
+
# Normalize: strip .exe for the local copy on non-Windows
|
|
94
|
+
if platform.system() != "Windows" and binary_name.endswith(".exe"):
|
|
95
|
+
binary_name = binary_name[:-4]
|
|
96
|
+
return os.path.join(CACHE_DIR, binary_name)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def download_binary(version=None, force=False):
|
|
100
|
+
"""
|
|
101
|
+
Download the TunnelGo client binary from GitHub Releases.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
version: Specific version to download (e.g. "2.20.0"). If None, uses latest.
|
|
105
|
+
force: Force re-download even if binary exists.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Path to the downloaded binary.
|
|
109
|
+
"""
|
|
110
|
+
binary_path = get_binary_path()
|
|
111
|
+
|
|
112
|
+
# Check if binary already exists and is up-to-date
|
|
113
|
+
if not force and os.path.isfile(binary_path) and os.access(binary_path, os.X_OK):
|
|
114
|
+
installed = get_installed_version()
|
|
115
|
+
if installed and (version is None or installed == version):
|
|
116
|
+
return binary_path
|
|
117
|
+
|
|
118
|
+
# Determine version
|
|
119
|
+
if version is None:
|
|
120
|
+
version = get_latest_version()
|
|
121
|
+
if version is None:
|
|
122
|
+
version = "latest"
|
|
123
|
+
|
|
124
|
+
# Build download URL
|
|
125
|
+
binary_name = get_platform_binary_name()
|
|
126
|
+
if version == "latest":
|
|
127
|
+
download_url = (
|
|
128
|
+
f"https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}/releases/latest/download/{binary_name}"
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
download_url = (
|
|
132
|
+
f"https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}/releases/download/v{version}/{binary_name}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Create cache directory
|
|
136
|
+
os.makedirs(CACHE_DIR, exist_ok=True)
|
|
137
|
+
|
|
138
|
+
# Download
|
|
139
|
+
print(f" 正在下载 TunnelGo 客户端 v{version} ({binary_name})...")
|
|
140
|
+
try:
|
|
141
|
+
req = urllib.request.Request(download_url, headers={"User-Agent": "tunnelgo-client-python"})
|
|
142
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
143
|
+
data = resp.read()
|
|
144
|
+
except urllib.error.HTTPError as e:
|
|
145
|
+
raise RuntimeError(
|
|
146
|
+
f"下载失败 (HTTP {e.code}): {download_url}\n"
|
|
147
|
+
f"请手动从 https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}/releases 下载"
|
|
148
|
+
)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
raise RuntimeError(f"下载失败: {e}")
|
|
151
|
+
|
|
152
|
+
# Write binary
|
|
153
|
+
with open(binary_path, "wb") as f:
|
|
154
|
+
f.write(data)
|
|
155
|
+
|
|
156
|
+
# Make executable
|
|
157
|
+
os.chmod(binary_path, os.stat(binary_path).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
|
158
|
+
|
|
159
|
+
# Write version file
|
|
160
|
+
if version != "latest":
|
|
161
|
+
with open(VERSION_FILE, "w") as f:
|
|
162
|
+
f.write(version)
|
|
163
|
+
|
|
164
|
+
size_mb = len(data) / (1024 * 1024)
|
|
165
|
+
print(f" 下载完成 ({size_mb:.1f} MB) → {binary_path}")
|
|
166
|
+
|
|
167
|
+
return binary_path
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def ensure_binary(version=None):
|
|
171
|
+
"""
|
|
172
|
+
Ensure the binary is available, downloading if necessary.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Path to the binary.
|
|
176
|
+
"""
|
|
177
|
+
return download_binary(version=version, force=False)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tunnelgo-client
|
|
3
|
+
Version: 2.20.0
|
|
4
|
+
Summary: TunnelGo 内网穿透客户端 — 通过 pip 一键安装,支持 double-fork 守护进程模式
|
|
5
|
+
Author: TunnelGo Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ctz168/tunnelgo
|
|
8
|
+
Project-URL: Repository, https://github.com/ctz168/tunnelgo
|
|
9
|
+
Project-URL: Issues, https://github.com/ctz168/tunnelgo/issues
|
|
10
|
+
Keywords: tunnel,ngrok,nat-traversal,reverse-proxy,daemon
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Internet :: Proxy Servers
|
|
24
|
+
Classifier: Topic :: System :: Networking
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# TunnelGo Client (Python)
|
|
29
|
+
|
|
30
|
+
TunnelGo 内网穿透客户端的 Python 封装包。通过 `pip` 一键安装,自动下载对应平台的 Go 二进制文件,支持 double-fork 守护进程模式。
|
|
31
|
+
|
|
32
|
+
## 安装
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install tunnelgo-client
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
安装后会自动下载对应平台(Linux/macOS/Windows,amd64/arm64)的 Go 二进制文件到 `~/.tunnelgo/bin/`。
|
|
39
|
+
|
|
40
|
+
## 使用
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# 基本模式
|
|
44
|
+
tunnelgo-client --key YOUR_TOKEN --port 8080
|
|
45
|
+
|
|
46
|
+
# 子域名模式(推荐)
|
|
47
|
+
tunnelgo-client -k YOUR_TOKEN -p 8080 --subdomain myapp
|
|
48
|
+
|
|
49
|
+
# 守护进程模式(Python 原生 double-fork,脱离终端)
|
|
50
|
+
tunnelgo-client -k YOUR_TOKEN -p 8080 --daemon
|
|
51
|
+
|
|
52
|
+
# 停止守护进程
|
|
53
|
+
tunnelgo-client --stop
|
|
54
|
+
|
|
55
|
+
# 子域名 + TCP 转发
|
|
56
|
+
tunnelgo-client -k YOUR_TOKEN -p 8080 --subdomain myapp --tcp-ports 22,3306
|
|
57
|
+
|
|
58
|
+
# 禁用 P2P 直连
|
|
59
|
+
tunnelgo-client -k YOUR_TOKEN -p 8080 --no-p2p
|
|
60
|
+
|
|
61
|
+
# 强制更新客户端二进制
|
|
62
|
+
tunnelgo-client --update
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Double-Fork 守护进程模式
|
|
66
|
+
|
|
67
|
+
`--daemon` 参数启动 Python 原生的 double-fork 守护进程模式:
|
|
68
|
+
|
|
69
|
+
1. 第一次 fork → 创建中间子进程
|
|
70
|
+
2. 中间子进程调用 `setsid()` 成为新会话组长
|
|
71
|
+
3. 第二次 fork → 创建孙进程(实际守护进程)
|
|
72
|
+
4. 中间子进程退出
|
|
73
|
+
5. 孙进程被 init (PID 1) 收养,完全脱离终端
|
|
74
|
+
|
|
75
|
+
守护进程的日志输出到 `/tmp/tunnelgo-client.log`(Windows 为 `%TEMP%\tunnelgo-client.log`)。
|
|
76
|
+
|
|
77
|
+
## 参数
|
|
78
|
+
|
|
79
|
+
| 参数 | 说明 | 默认值 |
|
|
80
|
+
|------|------|--------|
|
|
81
|
+
| `-k, --key` | 认证令牌(必填) | - |
|
|
82
|
+
| `-p, --port` | 本地服务端口 | 8080 |
|
|
83
|
+
| `-s, --server` | 服务器地址 | aicq.online:6639 |
|
|
84
|
+
| `--host` | 本地服务地址 | localhost |
|
|
85
|
+
| `--p2p-port` | P2P 监听端口 | 与 --port 相同 |
|
|
86
|
+
| `--no-p2p` | 禁用 P2P 直连 | false |
|
|
87
|
+
| `--tcp-ports` | TCP 转发端口,逗号分隔 | - |
|
|
88
|
+
| `--tcp-default` | 设为非TLS同端口默认路由 | false |
|
|
89
|
+
| `--http-port` | HTTP 独立端口模式 | false |
|
|
90
|
+
| `--subdomain` | 子域名前缀 | - |
|
|
91
|
+
| `--daemon` | 以守护进程模式运行 (double-fork) | false |
|
|
92
|
+
| `--stop` | 停止守护进程 | false |
|
|
93
|
+
| `--update` | 强制更新客户端二进制 | false |
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/tunnelgo_client/__init__.py
|
|
4
|
+
src/tunnelgo_client/__main__.py
|
|
5
|
+
src/tunnelgo_client/cli.py
|
|
6
|
+
src/tunnelgo_client/daemon.py
|
|
7
|
+
src/tunnelgo_client/downloader.py
|
|
8
|
+
src/tunnelgo_client.egg-info/PKG-INFO
|
|
9
|
+
src/tunnelgo_client.egg-info/SOURCES.txt
|
|
10
|
+
src/tunnelgo_client.egg-info/dependency_links.txt
|
|
11
|
+
src/tunnelgo_client.egg-info/entry_points.txt
|
|
12
|
+
src/tunnelgo_client.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tunnelgo_client
|