linksocks 1.7.14__py3-none-win_amd64.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.
- linksocks/__init__.py +9 -0
- linksocks/_base.py +509 -0
- linksocks/_cli.py +306 -0
- linksocks/_client.py +289 -0
- linksocks/_logging_config.py +182 -0
- linksocks/_server.py +353 -0
- linksocks/_utils.py +144 -0
- linksocks-1.7.14.dist-info/METADATA +494 -0
- linksocks-1.7.14.dist-info/RECORD +16 -0
- linksocks-1.7.14.dist-info/WHEEL +5 -0
- linksocks-1.7.14.dist-info/entry_points.txt +2 -0
- linksocks-1.7.14.dist-info/top_level.txt +2 -0
- linksocks_ffi/__init__.py +23 -0
- linksocks_ffi/_lib.py +221 -0
- linksocks_ffi/linksocks_ffi.dll +0 -0
- linksocks_ffi/linksocks_ffi.h +109 -0
linksocks/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""linksocks: SOCKS5 over WebSocket proxy library."""
|
|
2
|
+
|
|
3
|
+
__version__ = "1.7.14"
|
|
4
|
+
|
|
5
|
+
from ._server import Server
|
|
6
|
+
from ._client import Client
|
|
7
|
+
from ._base import ReverseTokenResult, set_log_level
|
|
8
|
+
|
|
9
|
+
__all__ = ["Server", "Client", "ReverseTokenResult", "set_log_level"]
|
linksocks/_base.py
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
"""Base classes and utilities for linksocks.
|
|
2
|
+
|
|
3
|
+
This module contains shared functionality used by Server and Client classes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import timedelta
|
|
14
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
15
|
+
|
|
16
|
+
_BACKEND: str
|
|
17
|
+
|
|
18
|
+
from linksocks_ffi import cancel_log_waiters as _ffi_cancel_log_waiters
|
|
19
|
+
from linksocks_ffi import parse_duration as _ffi_parse_duration
|
|
20
|
+
from linksocks_ffi import seconds as _ffi_seconds
|
|
21
|
+
from linksocks_ffi import wait_for_log_entries as _ffi_wait_for_log_entries
|
|
22
|
+
|
|
23
|
+
_BACKEND = "ffi"
|
|
24
|
+
|
|
25
|
+
_logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# Type aliases
|
|
28
|
+
DurationLike = Union[int, float, timedelta, str]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _snake_to_camel(name: str) -> str:
|
|
32
|
+
"""Convert snake_case to CamelCase."""
|
|
33
|
+
parts = name.split("_")
|
|
34
|
+
return "".join(p.capitalize() for p in parts if p)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _camel_to_snake(name: str) -> str:
|
|
38
|
+
"""Convert CamelCase to snake_case."""
|
|
39
|
+
out: List[str] = []
|
|
40
|
+
for ch in name:
|
|
41
|
+
if ch.isupper() and out:
|
|
42
|
+
out.append("_")
|
|
43
|
+
out.append(ch.lower())
|
|
44
|
+
return "".join(out)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _to_duration(value: Optional[DurationLike]) -> Any:
|
|
48
|
+
"""Convert seconds/str/timedelta to Go time.Duration via bindings.
|
|
49
|
+
|
|
50
|
+
- None -> 0
|
|
51
|
+
- int/float -> seconds (supports fractions)
|
|
52
|
+
- timedelta -> total seconds
|
|
53
|
+
- str -> parsed by Go (e.g., "1.5s", "300ms")
|
|
54
|
+
|
|
55
|
+
Returns an int since Go's time.Duration is int64.
|
|
56
|
+
"""
|
|
57
|
+
if value is None:
|
|
58
|
+
return 0
|
|
59
|
+
if isinstance(value, timedelta):
|
|
60
|
+
seconds = value.total_seconds()
|
|
61
|
+
return int(seconds * int(_ffi_seconds()))
|
|
62
|
+
if isinstance(value, (int, float)):
|
|
63
|
+
return int(value * int(_ffi_seconds()))
|
|
64
|
+
if isinstance(value, str):
|
|
65
|
+
try:
|
|
66
|
+
return int(_ffi_parse_duration(value))
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
raise ValueError(f"Invalid duration string: {value}") from exc
|
|
69
|
+
raise TypeError(f"Unsupported duration type: {type(value)!r}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class _NoopLogger:
|
|
73
|
+
def __call__(self, *args: Any, **kwargs: Any) -> None:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _DummyManagedLogger:
|
|
78
|
+
def __init__(self, py_logger: logging.Logger, logger_id: str):
|
|
79
|
+
self.py_logger = py_logger
|
|
80
|
+
self.logger_id = logger_id
|
|
81
|
+
self.go_logger = _NoopLogger()
|
|
82
|
+
|
|
83
|
+
def cleanup(self) -> None:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _json_key(snake: str) -> str:
|
|
88
|
+
return snake
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class _FFIReverseTokenOptions:
|
|
92
|
+
def __init__(self) -> None:
|
|
93
|
+
self.Token = ""
|
|
94
|
+
self.Port: Optional[int] = None
|
|
95
|
+
self.Username = ""
|
|
96
|
+
self.Password = ""
|
|
97
|
+
self.AllowManageConnector: Optional[bool] = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class _FFIServerOption:
|
|
101
|
+
def __init__(self) -> None:
|
|
102
|
+
self._cfg: Dict[str, Any] = {}
|
|
103
|
+
|
|
104
|
+
def WithLogger(self, logger: Any) -> None:
|
|
105
|
+
logger_id: Optional[str] = None
|
|
106
|
+
if isinstance(logger, str):
|
|
107
|
+
logger_id = logger
|
|
108
|
+
else:
|
|
109
|
+
logger_id = getattr(logger, "logger_id", None)
|
|
110
|
+
if logger_id:
|
|
111
|
+
self._cfg[_json_key("logger_id")] = str(logger_id)
|
|
112
|
+
|
|
113
|
+
def WithWSHost(self, v: str) -> None:
|
|
114
|
+
self._cfg[_json_key("ws_host")] = v
|
|
115
|
+
|
|
116
|
+
def WithWSPort(self, v: int) -> None:
|
|
117
|
+
self._cfg[_json_key("ws_port")] = int(v)
|
|
118
|
+
|
|
119
|
+
def WithSocksHost(self, v: str) -> None:
|
|
120
|
+
self._cfg[_json_key("socks_host")] = v
|
|
121
|
+
|
|
122
|
+
def WithSocksWaitClient(self, v: bool) -> None:
|
|
123
|
+
self._cfg[_json_key("socks_wait_client")] = bool(v)
|
|
124
|
+
|
|
125
|
+
def WithPortPool(self, _v: Any) -> None:
|
|
126
|
+
raise NotImplementedError("port_pool is not supported by the cffi backend")
|
|
127
|
+
|
|
128
|
+
def WithBufferSize(self, v: int) -> None:
|
|
129
|
+
self._cfg[_json_key("buffer_size")] = int(v)
|
|
130
|
+
|
|
131
|
+
def WithAPI(self, v: str) -> None:
|
|
132
|
+
self._cfg[_json_key("api_key")] = v
|
|
133
|
+
|
|
134
|
+
def WithChannelTimeout(self, v: int) -> None:
|
|
135
|
+
self._cfg[_json_key("channel_timeout_ns")] = int(v)
|
|
136
|
+
|
|
137
|
+
def WithConnectTimeout(self, v: int) -> None:
|
|
138
|
+
self._cfg[_json_key("connect_timeout_ns")] = int(v)
|
|
139
|
+
|
|
140
|
+
def WithFastOpen(self, v: bool) -> None:
|
|
141
|
+
self._cfg[_json_key("fast_open")] = bool(v)
|
|
142
|
+
|
|
143
|
+
def WithUpstreamProxy(self, v: str) -> None:
|
|
144
|
+
self._cfg[_json_key("upstream_proxy")] = v
|
|
145
|
+
|
|
146
|
+
def WithUpstreamAuth(self, username: str, password: str) -> None:
|
|
147
|
+
self._cfg[_json_key("upstream_username")] = username
|
|
148
|
+
self._cfg[_json_key("upstream_password")] = password
|
|
149
|
+
|
|
150
|
+
def to_cfg(self) -> Dict[str, Any]:
|
|
151
|
+
return dict(self._cfg)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class _FFIClientOption:
|
|
155
|
+
def __init__(self) -> None:
|
|
156
|
+
self._cfg: Dict[str, Any] = {}
|
|
157
|
+
|
|
158
|
+
def WithLogger(self, logger: Any) -> None:
|
|
159
|
+
logger_id: Optional[str] = None
|
|
160
|
+
if isinstance(logger, str):
|
|
161
|
+
logger_id = logger
|
|
162
|
+
else:
|
|
163
|
+
logger_id = getattr(logger, "logger_id", None)
|
|
164
|
+
if logger_id:
|
|
165
|
+
self._cfg[_json_key("logger_id")] = str(logger_id)
|
|
166
|
+
|
|
167
|
+
def WithWSURL(self, v: str) -> None:
|
|
168
|
+
self._cfg[_json_key("ws_url")] = v
|
|
169
|
+
|
|
170
|
+
def WithReverse(self, v: bool) -> None:
|
|
171
|
+
self._cfg[_json_key("reverse")] = bool(v)
|
|
172
|
+
|
|
173
|
+
def WithSocksHost(self, v: str) -> None:
|
|
174
|
+
self._cfg[_json_key("socks_host")] = v
|
|
175
|
+
|
|
176
|
+
def WithSocksPort(self, v: int) -> None:
|
|
177
|
+
self._cfg[_json_key("socks_port")] = int(v)
|
|
178
|
+
|
|
179
|
+
def WithSocksUsername(self, v: str) -> None:
|
|
180
|
+
self._cfg[_json_key("socks_username")] = v
|
|
181
|
+
|
|
182
|
+
def WithSocksPassword(self, v: str) -> None:
|
|
183
|
+
self._cfg[_json_key("socks_password")] = v
|
|
184
|
+
|
|
185
|
+
def WithSocksWaitServer(self, v: bool) -> None:
|
|
186
|
+
self._cfg[_json_key("socks_wait_server")] = bool(v)
|
|
187
|
+
|
|
188
|
+
def WithReconnect(self, v: bool) -> None:
|
|
189
|
+
self._cfg[_json_key("reconnect")] = bool(v)
|
|
190
|
+
|
|
191
|
+
def WithReconnectDelay(self, v: int) -> None:
|
|
192
|
+
self._cfg[_json_key("reconnect_delay_ns")] = int(v)
|
|
193
|
+
|
|
194
|
+
def WithBufferSize(self, v: int) -> None:
|
|
195
|
+
self._cfg[_json_key("buffer_size")] = int(v)
|
|
196
|
+
|
|
197
|
+
def WithChannelTimeout(self, v: int) -> None:
|
|
198
|
+
self._cfg[_json_key("channel_timeout_ns")] = int(v)
|
|
199
|
+
|
|
200
|
+
def WithConnectTimeout(self, v: int) -> None:
|
|
201
|
+
self._cfg[_json_key("connect_timeout_ns")] = int(v)
|
|
202
|
+
|
|
203
|
+
def WithThreads(self, v: int) -> None:
|
|
204
|
+
self._cfg[_json_key("threads")] = int(v)
|
|
205
|
+
|
|
206
|
+
def WithFastOpen(self, v: bool) -> None:
|
|
207
|
+
self._cfg[_json_key("fast_open")] = bool(v)
|
|
208
|
+
|
|
209
|
+
def WithUpstreamProxy(self, v: str) -> None:
|
|
210
|
+
self._cfg[_json_key("upstream_proxy")] = v
|
|
211
|
+
|
|
212
|
+
def WithUpstreamAuth(self, username: str, password: str) -> None:
|
|
213
|
+
self._cfg[_json_key("upstream_username")] = username
|
|
214
|
+
self._cfg[_json_key("upstream_password")] = password
|
|
215
|
+
|
|
216
|
+
def WithNoEnvProxy(self, v: bool) -> None:
|
|
217
|
+
self._cfg[_json_key("no_env_proxy")] = bool(v)
|
|
218
|
+
|
|
219
|
+
def to_cfg(self) -> Dict[str, Any]:
|
|
220
|
+
return dict(self._cfg)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class _FFIRawServer:
|
|
224
|
+
def __init__(self, cfg: Dict[str, Any]):
|
|
225
|
+
from linksocks_ffi import Server as FFIServer
|
|
226
|
+
|
|
227
|
+
self._srv = FFIServer(cfg)
|
|
228
|
+
|
|
229
|
+
def WaitReady(self, *, ctx: Any = None, timeout: int = 0) -> None:
|
|
230
|
+
self._srv.wait_ready(int(timeout))
|
|
231
|
+
|
|
232
|
+
def Close(self) -> None:
|
|
233
|
+
self._srv.close()
|
|
234
|
+
|
|
235
|
+
def AddForwardToken(self, token: str = "") -> str:
|
|
236
|
+
return self._srv.add_forward_token(token or "")
|
|
237
|
+
|
|
238
|
+
def AddReverseToken(self, opts: Any) -> Any:
|
|
239
|
+
payload: Dict[str, Any] = {}
|
|
240
|
+
token = getattr(opts, "Token", "")
|
|
241
|
+
if token:
|
|
242
|
+
payload["token"] = token
|
|
243
|
+
port = getattr(opts, "Port", None)
|
|
244
|
+
if port is not None:
|
|
245
|
+
payload["port"] = int(port)
|
|
246
|
+
username = getattr(opts, "Username", "")
|
|
247
|
+
if username:
|
|
248
|
+
payload["username"] = username
|
|
249
|
+
password = getattr(opts, "Password", "")
|
|
250
|
+
if password:
|
|
251
|
+
payload["password"] = password
|
|
252
|
+
allow = getattr(opts, "AllowManageConnector", None)
|
|
253
|
+
if allow is not None:
|
|
254
|
+
payload["allow_manage_connector"] = bool(allow)
|
|
255
|
+
return self._srv.add_reverse_token(payload)
|
|
256
|
+
|
|
257
|
+
def RemoveToken(self, token: str) -> bool:
|
|
258
|
+
return bool(self._srv.remove_token(token))
|
|
259
|
+
|
|
260
|
+
def AddConnectorToken(self, connector: str, reverse_token: str) -> str:
|
|
261
|
+
return self._srv.add_connector_token(connector, reverse_token)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class _FFIRawClient:
|
|
265
|
+
def __init__(self, token: str, cfg: Dict[str, Any]):
|
|
266
|
+
from linksocks_ffi import Client as FFIClient
|
|
267
|
+
|
|
268
|
+
self.SocksPort = cfg.get("socks_port")
|
|
269
|
+
self._cli = FFIClient(token, cfg)
|
|
270
|
+
|
|
271
|
+
def WaitReady(self, *, ctx: Any = None, timeout: int = 0) -> None:
|
|
272
|
+
self._cli.wait_ready(int(timeout))
|
|
273
|
+
|
|
274
|
+
def Close(self) -> None:
|
|
275
|
+
self._cli.close()
|
|
276
|
+
|
|
277
|
+
def AddConnector(self, token: str) -> str:
|
|
278
|
+
return self._cli.add_connector(token)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class _FFIContextWithCancel:
|
|
282
|
+
def __init__(self) -> None:
|
|
283
|
+
self._cancelled = False
|
|
284
|
+
|
|
285
|
+
def Context(self) -> None:
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
def Cancel(self) -> None:
|
|
289
|
+
self._cancelled = True
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class _FFIBackend:
|
|
293
|
+
def DefaultServerOption(self) -> Any:
|
|
294
|
+
return _FFIServerOption()
|
|
295
|
+
|
|
296
|
+
def DefaultClientOption(self) -> Any:
|
|
297
|
+
return _FFIClientOption()
|
|
298
|
+
|
|
299
|
+
def DefaultReverseTokenOptions(self) -> Any:
|
|
300
|
+
return _FFIReverseTokenOptions()
|
|
301
|
+
|
|
302
|
+
def NewLinkSocksServer(self, opt: Any) -> Any:
|
|
303
|
+
return _FFIRawServer(opt.to_cfg())
|
|
304
|
+
|
|
305
|
+
def NewLinkSocksClient(self, token: str, opt: Any) -> Any:
|
|
306
|
+
return _FFIRawClient(token, opt.to_cfg())
|
|
307
|
+
|
|
308
|
+
def NewContextWithCancel(self) -> Any:
|
|
309
|
+
return _FFIContextWithCancel()
|
|
310
|
+
|
|
311
|
+
def NewLoggerWithID(self, _logger_id: str) -> Any:
|
|
312
|
+
return str(_logger_id)
|
|
313
|
+
|
|
314
|
+
def NewLogger(self, _cb: Any) -> Any:
|
|
315
|
+
return f"logger_{id(_cb)}"
|
|
316
|
+
|
|
317
|
+
def WaitForLogEntries(self, ms: int) -> list[Any]:
|
|
318
|
+
try:
|
|
319
|
+
entries = _ffi_wait_for_log_entries(int(ms)) # type: ignore[misc]
|
|
320
|
+
if not entries:
|
|
321
|
+
return []
|
|
322
|
+
out: List[Any] = []
|
|
323
|
+
for e in entries:
|
|
324
|
+
if isinstance(e, dict):
|
|
325
|
+
out.append({
|
|
326
|
+
"LoggerID": e.get("LoggerID") or e.get("logger_id") or "",
|
|
327
|
+
"Message": e.get("Message") or e.get("message") or "",
|
|
328
|
+
"Time": e.get("Time") or e.get("time") or 0,
|
|
329
|
+
})
|
|
330
|
+
return out
|
|
331
|
+
except Exception:
|
|
332
|
+
time.sleep(max(int(ms), 1) / 1000.0)
|
|
333
|
+
return []
|
|
334
|
+
|
|
335
|
+
def CancelLogWaiters(self) -> None:
|
|
336
|
+
try:
|
|
337
|
+
_ffi_cancel_log_waiters() # type: ignore[misc]
|
|
338
|
+
except Exception:
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
def Second(self) -> int:
|
|
342
|
+
return int(_ffi_seconds()) # type: ignore[misc]
|
|
343
|
+
|
|
344
|
+
def ParseDuration(self, s: str) -> int:
|
|
345
|
+
return int(_ffi_parse_duration(s)) # type: ignore[misc]
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
backend = _FFIBackend()
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# Shared Go->Python log dispatcher
|
|
352
|
+
_def_level_map = {
|
|
353
|
+
"trace": logging.DEBUG,
|
|
354
|
+
"debug": logging.DEBUG,
|
|
355
|
+
"info": logging.INFO,
|
|
356
|
+
"warn": logging.WARNING,
|
|
357
|
+
"warning": logging.WARNING,
|
|
358
|
+
"error": logging.ERROR,
|
|
359
|
+
"fatal": logging.CRITICAL,
|
|
360
|
+
"panic": logging.CRITICAL,
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _emit_go_log(py_logger: logging.Logger, line: str) -> None:
|
|
365
|
+
"""Process a Go log line and emit it to the Python logger."""
|
|
366
|
+
try:
|
|
367
|
+
obj = json.loads(line)
|
|
368
|
+
except Exception:
|
|
369
|
+
py_logger.info(line)
|
|
370
|
+
return
|
|
371
|
+
level = str(obj.get("level", "")).lower()
|
|
372
|
+
message = obj.get("message") or obj.get("msg") or ""
|
|
373
|
+
extras: Dict[str, Any] = {}
|
|
374
|
+
for k, v in obj.items():
|
|
375
|
+
if k in ("level", "time", "message", "msg"):
|
|
376
|
+
continue
|
|
377
|
+
extras[k] = v
|
|
378
|
+
py_logger.log(_def_level_map.get(level, logging.INFO), message, extra={"go": extras})
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# Global registry for logger instances
|
|
382
|
+
_logger_registry: Dict[str, logging.Logger] = {}
|
|
383
|
+
|
|
384
|
+
# Event-driven log monitoring system
|
|
385
|
+
_log_listeners: List[Callable[[List], None]] = []
|
|
386
|
+
_listener_thread: Optional[threading.Thread] = None
|
|
387
|
+
_listener_active: bool = False
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _start_log_listener() -> None:
|
|
391
|
+
"""Start background thread to drain Go log buffer and forward to Python loggers."""
|
|
392
|
+
global _listener_thread, _listener_active
|
|
393
|
+
if _listener_active and _listener_thread and _listener_thread.is_alive():
|
|
394
|
+
return
|
|
395
|
+
_listener_active = True
|
|
396
|
+
|
|
397
|
+
def _run() -> None:
|
|
398
|
+
# Drain loop: wait for entries with timeout to allow graceful shutdown
|
|
399
|
+
while _listener_active:
|
|
400
|
+
entries = backend.WaitForLogEntries(2000)
|
|
401
|
+
|
|
402
|
+
if not entries:
|
|
403
|
+
continue
|
|
404
|
+
|
|
405
|
+
# Iterate returned entries; handle both attr and dict styles
|
|
406
|
+
for entry in entries:
|
|
407
|
+
try:
|
|
408
|
+
logger_id = getattr(entry, "LoggerID", None)
|
|
409
|
+
if logger_id is None and isinstance(entry, dict):
|
|
410
|
+
logger_id = entry.get("LoggerID")
|
|
411
|
+
|
|
412
|
+
message = getattr(entry, "Message", None)
|
|
413
|
+
if message is None and isinstance(entry, dict):
|
|
414
|
+
message = entry.get("Message")
|
|
415
|
+
|
|
416
|
+
if not message:
|
|
417
|
+
continue
|
|
418
|
+
|
|
419
|
+
py_logger = _logger_registry.get(str(logger_id)) or _logger
|
|
420
|
+
_emit_go_log(py_logger, str(message))
|
|
421
|
+
except Exception:
|
|
422
|
+
# Never let logging path crash the listener
|
|
423
|
+
continue
|
|
424
|
+
|
|
425
|
+
_listener_thread = threading.Thread(target=_run, name="linksocks-go-log-listener", daemon=True)
|
|
426
|
+
_listener_thread.start()
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _stop_log_listener() -> None:
|
|
430
|
+
"""Stop the background log listener thread."""
|
|
431
|
+
global _listener_active
|
|
432
|
+
_listener_active = False
|
|
433
|
+
try:
|
|
434
|
+
backend.CancelLogWaiters()
|
|
435
|
+
except Exception:
|
|
436
|
+
pass
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class BufferZerologLogger:
|
|
440
|
+
"""Buffer-based logger system for Go bindings."""
|
|
441
|
+
|
|
442
|
+
def __init__(self, py_logger: logging.Logger, logger_id: str):
|
|
443
|
+
self.py_logger = py_logger
|
|
444
|
+
self.logger_id = logger_id
|
|
445
|
+
# Ensure background listener is running
|
|
446
|
+
_start_log_listener()
|
|
447
|
+
|
|
448
|
+
# Prefer Go logger with explicit ID so we can map entries back
|
|
449
|
+
try:
|
|
450
|
+
# Newer binding that tags entries with our provided ID
|
|
451
|
+
self.go_logger = backend.NewLoggerWithID(self.logger_id)
|
|
452
|
+
except Exception:
|
|
453
|
+
# Fallback to older API; if present, still try callback path
|
|
454
|
+
try:
|
|
455
|
+
def log_callback(line: str) -> None:
|
|
456
|
+
_emit_go_log(py_logger, line)
|
|
457
|
+
|
|
458
|
+
self.go_logger = backend.NewLogger(log_callback)
|
|
459
|
+
except Exception:
|
|
460
|
+
# As a last resort, create a default Go logger
|
|
461
|
+
self.go_logger = backend.NewLoggerWithID(self.logger_id) # may still raise; surface to caller
|
|
462
|
+
_logger_registry[logger_id] = py_logger
|
|
463
|
+
|
|
464
|
+
def cleanup(self):
|
|
465
|
+
"""Clean up logger resources."""
|
|
466
|
+
if self.logger_id in _logger_registry:
|
|
467
|
+
del _logger_registry[self.logger_id]
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@dataclass
|
|
471
|
+
class ReverseTokenResult:
|
|
472
|
+
"""Result of adding a reverse token."""
|
|
473
|
+
token: str
|
|
474
|
+
port: int
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class _SnakePassthrough:
|
|
478
|
+
"""Mixin to map snake_case attribute access to underlying CamelCase.
|
|
479
|
+
|
|
480
|
+
Only used when an explicit Pythonic method/attribute is not defined.
|
|
481
|
+
"""
|
|
482
|
+
|
|
483
|
+
def __getattr__(self, name: str) -> Any:
|
|
484
|
+
raw = super().__getattribute__("_raw") # type: ignore[attr-defined]
|
|
485
|
+
camel = _snake_to_camel(name)
|
|
486
|
+
try:
|
|
487
|
+
return getattr(raw, camel)
|
|
488
|
+
except AttributeError:
|
|
489
|
+
raise
|
|
490
|
+
|
|
491
|
+
def __dir__(self) -> List[str]:
|
|
492
|
+
# Expose snake_case versions of underlying CamelCase for IDEs
|
|
493
|
+
names = set(super().__dir__())
|
|
494
|
+
try:
|
|
495
|
+
raw = super().__getattribute__("_raw") # type: ignore[attr-defined]
|
|
496
|
+
for attr in dir(raw):
|
|
497
|
+
if not attr or attr.startswith("_"):
|
|
498
|
+
continue
|
|
499
|
+
names.add(_camel_to_snake(attr))
|
|
500
|
+
except Exception:
|
|
501
|
+
pass
|
|
502
|
+
return sorted(names)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def set_log_level(level: Union[int, str]) -> None:
|
|
506
|
+
"""Set the global log level for linksocks."""
|
|
507
|
+
if isinstance(level, str):
|
|
508
|
+
level = getattr(logging, level.upper())
|
|
509
|
+
_logger.setLevel(level)
|