leakscope 0.2.0rc1__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.
- leakscope-0.2.0rc1.dist-info/METADATA +40 -0
- leakscope-0.2.0rc1.dist-info/RECORD +32 -0
- leakscope-0.2.0rc1.dist-info/WHEEL +5 -0
- leakscope-0.2.0rc1.dist-info/entry_points.txt +2 -0
- leakscope-0.2.0rc1.dist-info/licenses/LICENSE +21 -0
- leakscope-0.2.0rc1.dist-info/top_level.txt +1 -0
- leakscope_api/__init__.py +3 -0
- leakscope_api/agent.py +477 -0
- leakscope_api/analysis.py +90 -0
- leakscope_api/app.py +1558 -0
- leakscope_api/capture.py +162 -0
- leakscope_api/cli.py +513 -0
- leakscope_api/config.py +96 -0
- leakscope_api/context_capture.py +51 -0
- leakscope_api/contracts.py +223 -0
- leakscope_api/db.py +61 -0
- leakscope_api/deployment.py +60 -0
- leakscope_api/healthcheck.py +71 -0
- leakscope_api/investigation.py +493 -0
- leakscope_api/limits.py +46 -0
- leakscope_api/migrations/env.py +14 -0
- leakscope_api/migrations/versions/0001_initial.py +17 -0
- leakscope_api/migrations/versions/0002_agent_evidence.py +19 -0
- leakscope_api/migrations/versions/0003_monitoring.py +21 -0
- leakscope_api/models.py +213 -0
- leakscope_api/monitoring.py +633 -0
- leakscope_api/native.py +132 -0
- leakscope_api/readiness.py +236 -0
- leakscope_api/reports.py +123 -0
- leakscope_api/sensor.py +70 -0
- leakscope_api/service.py +644 -0
- leakscope_api/storage.py +106 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: leakscope
|
|
3
|
+
Version: 0.2.0rc1
|
|
4
|
+
Summary: LeakScope Python collector and CLI; native executables and web assets are installed separately
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/polsala/LeakScope
|
|
7
|
+
Project-URL: Documentation, https://github.com/polsala/LeakScope/tree/main/docs
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: fastapi==0.143.1
|
|
12
|
+
Requires-Dist: uvicorn==0.34.3
|
|
13
|
+
Requires-Dist: pydantic==2.11.7
|
|
14
|
+
Requires-Dist: sqlalchemy==2.0.41
|
|
15
|
+
Requires-Dist: alembic==1.16.2
|
|
16
|
+
Requires-Dist: zstandard==0.23.0
|
|
17
|
+
Requires-Dist: protobuf==6.33.5
|
|
18
|
+
Requires-Dist: PyYAML==6.0.2
|
|
19
|
+
Requires-Dist: httpx==0.28.1
|
|
20
|
+
Requires-Dist: starlette==1.8.0
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# LeakScope Python component
|
|
24
|
+
|
|
25
|
+
This distribution installs the Python collector, FastAPI backend, CLI and Alembic
|
|
26
|
+
migrations. It requires Python 3.11 or newer. It does **not** bundle the Rust sensor,
|
|
27
|
+
Rust analyzer, web assets or CPython 2.7 compatibility extension, and performs no
|
|
28
|
+
executable downloads during installation.
|
|
29
|
+
|
|
30
|
+
`leakscope --version`, offline capture inspection and database backup work through
|
|
31
|
+
this package. Recording and native allocation analysis require separately installed
|
|
32
|
+
matching `leakscope-sensor` and `leakscope-analyzer` executables on PATH (or the
|
|
33
|
+
explicit LEAKSCOPE_SENSOR_BIN / LEAKSCOPE_ANALYZER_BIN settings). Serving the UI
|
|
34
|
+
requires LEAKSCOPE_WEB_DIST pointing to matching built assets. For the complete
|
|
35
|
+
application, use the verified offline standalone tarball or Debian distribution
|
|
36
|
+
from https://github.com/polsala/LeakScope/releases.
|
|
37
|
+
|
|
38
|
+
Engineering previews do not claim production readiness. Native tracing is opt-in;
|
|
39
|
+
metrics remain available without BPF. Python 2.7 is used only by the separate
|
|
40
|
+
compatibility agent and optional laboratory interpreter.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
leakscope-0.2.0rc1.dist-info/licenses/LICENSE,sha256=crLYarT66hziCSc788sNGeGffBWd2JhNLFI3kmHgJF8,1062
|
|
2
|
+
leakscope_api/__init__.py,sha256=W0B2d4Yb04BxsclJNHF-NtDsaubMhwcwrrYeRkH7dZg,72
|
|
3
|
+
leakscope_api/agent.py,sha256=e-r9a8xWiBnL6z9XF4O6H0gMWLBf9Fbfr-fL_-PeXHk,21648
|
|
4
|
+
leakscope_api/analysis.py,sha256=VfYsOJjDSIy2etB3opqPt0CKQ48eYRwmNo_Od6Roagc,3743
|
|
5
|
+
leakscope_api/app.py,sha256=GFqfWvhj76q5TsypyrJPeHqh7QiySGNFwfJ9I2Ko3Hs,67566
|
|
6
|
+
leakscope_api/capture.py,sha256=d5Onla3Mm5mCzk_QySSxa5z6-3Aw7gNUB2EaVifLMcc,6312
|
|
7
|
+
leakscope_api/cli.py,sha256=l99_fEPJRHLN-gEBijvA2ZSS25d0qa9WJPbQnOf-voE,22636
|
|
8
|
+
leakscope_api/config.py,sha256=n1dSSjPSkx6G6YujtnZ2-ai69FwtFO8NSa-GwNIRxA8,3598
|
|
9
|
+
leakscope_api/context_capture.py,sha256=meKvR2n-dvYx03qiZJYifx-b6--BoNi8W_5aaX1KVuw,2238
|
|
10
|
+
leakscope_api/contracts.py,sha256=qAmn7x6cG4vIsPepkUwqwFHMEOJxkrTLeC8glKhrarI,6421
|
|
11
|
+
leakscope_api/db.py,sha256=SJPIxqHNUXTXlbkeLUF6DqX46mvM7NdWhtEO5Oh_9FE,2438
|
|
12
|
+
leakscope_api/deployment.py,sha256=Z1WvZGGG3JCq4pr41qyswZa1tnIDKfEnUOYxU1kKQlQ,1534
|
|
13
|
+
leakscope_api/healthcheck.py,sha256=1UGD4vLEEwYqiicsYZm3SxoJs1EAGgFMQ5gpFMqu3UQ,3009
|
|
14
|
+
leakscope_api/investigation.py,sha256=XE6ez5_bpOy6pK6ujJZq7BewutzoqN3B9rOFk2AsB3Q,20790
|
|
15
|
+
leakscope_api/limits.py,sha256=DzyG6Sb1dr0E02qNlm76aKL3EDUbLjfxml8TWiuA9yY,1514
|
|
16
|
+
leakscope_api/models.py,sha256=ecboN_OdY-RA-wFLpS4P_JIp4qTubRnztUnXZQBq_Xg,8404
|
|
17
|
+
leakscope_api/monitoring.py,sha256=Da_-0JrFw5HeA1YoI9t1steSHeBpOVDR7C0UL-Mp4s0,25421
|
|
18
|
+
leakscope_api/native.py,sha256=zwAzItRjO3ytUqnyQEY_SZ5FOkLxJW1dgzsLcFVGNJ0,5203
|
|
19
|
+
leakscope_api/readiness.py,sha256=uYkrtwOiygYae0bLVMp0iqTMUq-DPlKzZ8RRv1E4DsQ,8385
|
|
20
|
+
leakscope_api/reports.py,sha256=VOgB6lroG7i6qzQUcPvaaLR1CjTnNoDx1gYnspxR3N0,6186
|
|
21
|
+
leakscope_api/sensor.py,sha256=TsNLlGECLtBoAyZlW8YYf3wHlxwYYFADrEJU33gViQc,2357
|
|
22
|
+
leakscope_api/service.py,sha256=8rrLqXrRjSnl0q_SI4TJJybGxN65A8uQUjZH5M9dHPM,29722
|
|
23
|
+
leakscope_api/storage.py,sha256=6HVCVoYKAj9U8tH3FDJQQ6v9j17bNVhJNmAVpAiJx0U,4639
|
|
24
|
+
leakscope_api/migrations/env.py,sha256=JulslKQ1Wm5YIMqrhaGHU_rMoUrtuPEFXQJWsFqP_Gw,466
|
|
25
|
+
leakscope_api/migrations/versions/0001_initial.py,sha256=cMX2Hu9eH7BvOP1HNAWDBlahjTvIjKuhpBhy1dj5ot8,347
|
|
26
|
+
leakscope_api/migrations/versions/0002_agent_evidence.py,sha256=7ZFobSRz3HlDuFpPzwSyfciny1g5E94MB-EZXrPEHJk,447
|
|
27
|
+
leakscope_api/migrations/versions/0003_monitoring.py,sha256=3sBKEVVUN9N4ryFZxZsu7NFlHB34TbYFWEWoj1S6EGQ,678
|
|
28
|
+
leakscope-0.2.0rc1.dist-info/METADATA,sha256=t_SR0MgRtlEqT9uH25kG7bslkgafJzAG3RPoRClqQpQ,1832
|
|
29
|
+
leakscope-0.2.0rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
30
|
+
leakscope-0.2.0rc1.dist-info/entry_points.txt,sha256=0HOaZN1Iyyw_YoW7uepQx852ft60dk5Ey31f2YoTQmE,53
|
|
31
|
+
leakscope-0.2.0rc1.dist-info/top_level.txt,sha256=QEQSR0sZ5p2cA5GEVkdXbbI4prCISwD5qU4rWcj2m4k,14
|
|
32
|
+
leakscope-0.2.0rc1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PSala
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
leakscope_api
|
leakscope_api/agent.py
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"""Receive opt-in cooperative safe-point diagnostics, authenticated by Linux peer credentials."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import os
|
|
6
|
+
import socket
|
|
7
|
+
import struct
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
from collections import deque
|
|
11
|
+
from typing import Literal
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
from sqlalchemy import select, func
|
|
14
|
+
from .config import Strict
|
|
15
|
+
from .contracts import MarkerCreate
|
|
16
|
+
from . import models as m
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Packet(Strict):
|
|
20
|
+
version: Literal[1]
|
|
21
|
+
pid: int = Field(ge=1, le=2**31 - 1)
|
|
22
|
+
start_ticks: int = Field(ge=0)
|
|
23
|
+
sequence: int = Field(ge=1, le=2**63 - 1)
|
|
24
|
+
thread: int = Field(ge=0)
|
|
25
|
+
timestamp: float = Field(ge=0, allow_inf_nan=False)
|
|
26
|
+
monotonic_ns: int | None = Field(None, ge=0, le=2**63 - 1)
|
|
27
|
+
dropped: int = Field(0, ge=0)
|
|
28
|
+
kind: Literal[
|
|
29
|
+
"hello",
|
|
30
|
+
"marker",
|
|
31
|
+
"gc_snapshot",
|
|
32
|
+
"operation",
|
|
33
|
+
"python_codes",
|
|
34
|
+
"python_profile",
|
|
35
|
+
"lab_stack",
|
|
36
|
+
"lab_stacks",
|
|
37
|
+
"lab_events",
|
|
38
|
+
"lab_end",
|
|
39
|
+
]
|
|
40
|
+
payload: dict
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TypeCount(Strict):
|
|
44
|
+
count: int = Field(ge=0, le=50000)
|
|
45
|
+
shallow_bytes_estimate: int = Field(ge=0, le=2**63 - 1)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Snapshot(Strict):
|
|
49
|
+
name: str = Field(min_length=1, max_length=80)
|
|
50
|
+
visibility: Literal["gc_generation_zero_only"]
|
|
51
|
+
visited: int = Field(ge=0, le=50000)
|
|
52
|
+
duration_ms: float = Field(ge=0, le=1000, allow_inf_nan=False)
|
|
53
|
+
partial: bool
|
|
54
|
+
types: dict[str, TypeCount] = Field(max_length=128)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Operation(Strict):
|
|
58
|
+
id: str = Field(pattern=r"^[a-f0-9-]{1,100}$")
|
|
59
|
+
parent: str | None = Field(None, pattern=r"^[a-f0-9-]{1,100}$")
|
|
60
|
+
operation: str = Field(pattern=r"^[A-Za-z_][A-Za-z0-9_.: -]{0,79}$")
|
|
61
|
+
module: str | None = Field(None, pattern=r"^[A-Za-z_][A-Za-z0-9_.: -]{0,79}$")
|
|
62
|
+
model: str | None = Field(None, pattern=r"^[A-Za-z_][A-Za-z0-9_.: -]{0,79}$")
|
|
63
|
+
method: str | None = Field(None, pattern=r"^[A-Za-z_][A-Za-z0-9_.: -]{0,79}$")
|
|
64
|
+
depth: int = Field(ge=0, le=31)
|
|
65
|
+
thread: int = Field(ge=1, le=2**31 - 1)
|
|
66
|
+
start_ns: int = Field(ge=0, le=2**63 - 1)
|
|
67
|
+
end_ns: int | None = Field(None, ge=0, le=2**63 - 1)
|
|
68
|
+
status: Literal["running", "completed", "error", "cancelled"]
|
|
69
|
+
evidence: Literal["exact_operation_boundary"]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class PythonCode(Strict):
|
|
73
|
+
id: int = Field(ge=1, le=512)
|
|
74
|
+
file: str = Field(max_length=256)
|
|
75
|
+
function: str = Field(max_length=96)
|
|
76
|
+
first_line: int = Field(ge=0)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class PythonCodes(Strict):
|
|
80
|
+
epoch: int = Field(ge=0, le=2**63 - 1)
|
|
81
|
+
codes: list[PythonCode] = Field(max_length=16)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class PythonBoundary(Strict):
|
|
85
|
+
activation: int = Field(0, ge=0, le=2**63 - 1)
|
|
86
|
+
sequence: int = Field(ge=1, le=2**63 - 1)
|
|
87
|
+
monotonic_ns: int = Field(ge=0, le=2**63 - 1)
|
|
88
|
+
thread: int = Field(ge=1, le=2**31 - 1)
|
|
89
|
+
event: Literal[0, 3, 4, 5, 6, 7, 8]
|
|
90
|
+
frames: list[tuple[int, int]] = Field(max_length=16)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class PythonProfile(Strict):
|
|
94
|
+
expired: int = Field(0, ge=0)
|
|
95
|
+
epoch: int = Field(ge=0, le=2**63 - 1)
|
|
96
|
+
lost: int = Field(ge=0, le=2**63 - 1)
|
|
97
|
+
evidence: Literal["function_boundary"]
|
|
98
|
+
events: list[PythonBoundary] = Field(max_length=64)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class LabFrame(Strict):
|
|
102
|
+
file: str = Field(max_length=192)
|
|
103
|
+
module: str = Field(max_length=192)
|
|
104
|
+
function: str = Field(max_length=64)
|
|
105
|
+
line: int = Field(ge=0)
|
|
106
|
+
first_line: int = Field(ge=0)
|
|
107
|
+
evidence: Literal["verified_lab_allocation_frame"]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class LabStack(Strict):
|
|
111
|
+
epoch: int = Field(0, ge=0, le=2**63 - 1)
|
|
112
|
+
id: int = Field(ge=1, le=512)
|
|
113
|
+
flags: int = Field(ge=0, le=7)
|
|
114
|
+
frames: list[LabFrame] = Field(max_length=8)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class LabStacks(Strict):
|
|
118
|
+
epoch: int = Field(ge=1, le=2**63 - 1)
|
|
119
|
+
stacks: list[LabStack] = Field(max_length=8)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class LabEvents(Strict):
|
|
123
|
+
unknown_frees: int = Field(0, ge=0, le=2**63 - 1)
|
|
124
|
+
stop_reason: Literal[0, 1, 2] = 0
|
|
125
|
+
epoch: int = Field(ge=1, le=2**63 - 1)
|
|
126
|
+
events: list[tuple[int, int, int, int, int, int, int, int, int, int, int, int]] = Field(
|
|
127
|
+
max_length=64
|
|
128
|
+
)
|
|
129
|
+
lost: int = Field(ge=0, le=2**63 - 1)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class LabEnd(Strict):
|
|
133
|
+
epoch: int = Field(ge=1, le=2**63 - 1)
|
|
134
|
+
last_sequence: int = Field(ge=0, le=2**63 - 1)
|
|
135
|
+
delivered: bool
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def same_clock_namespace(pid):
|
|
139
|
+
try:
|
|
140
|
+
own = os.stat("/proc/self/ns/time")
|
|
141
|
+
target = os.stat(f"/proc/{pid}/ns/time")
|
|
142
|
+
return (own.st_dev, own.st_ino) == (target.st_dev, target.st_ino)
|
|
143
|
+
except FileNotFoundError:
|
|
144
|
+
return not os.path.exists("/proc/self/ns/time")
|
|
145
|
+
except PermissionError:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def validate_evidence(kind, payload):
|
|
150
|
+
if kind == "operation":
|
|
151
|
+
return Operation.model_validate(payload).model_dump()
|
|
152
|
+
if kind == "python_codes":
|
|
153
|
+
return PythonCodes.model_validate(payload).model_dump()
|
|
154
|
+
if kind == "python_profile":
|
|
155
|
+
return PythonProfile.model_validate(payload).model_dump()
|
|
156
|
+
if kind == "lab_stack":
|
|
157
|
+
return LabStack.model_validate(payload).model_dump()
|
|
158
|
+
if kind == "lab_stacks":
|
|
159
|
+
return LabStacks.model_validate(payload).model_dump()
|
|
160
|
+
if kind == "lab_end":
|
|
161
|
+
return LabEnd.model_validate(payload).model_dump()
|
|
162
|
+
if kind == "lab_events":
|
|
163
|
+
result = LabEvents.model_validate(payload).model_dump()
|
|
164
|
+
for row in result["events"]:
|
|
165
|
+
if (
|
|
166
|
+
any(v < 0 or v > 2**64 - 1 for v in row)
|
|
167
|
+
or row[6] not in {1, 2, 3}
|
|
168
|
+
or row[8] > 512
|
|
169
|
+
or row[10] > 7
|
|
170
|
+
):
|
|
171
|
+
raise ValueError("Invalid Lab native record")
|
|
172
|
+
return result
|
|
173
|
+
raise ValueError("Unsupported cooperative evidence kind")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
async def receive_agents(collector, path):
|
|
177
|
+
if path.exists():
|
|
178
|
+
path.unlink()
|
|
179
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
|
180
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED, 1)
|
|
181
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 256 * 1024)
|
|
182
|
+
sock.bind(str(path))
|
|
183
|
+
path.chmod(0o600)
|
|
184
|
+
sock.settimeout(0.25)
|
|
185
|
+
rates: dict[int, deque] = {}
|
|
186
|
+
inbox: asyncio.Queue = asyncio.Queue(maxsize=128) # <=4 MiB of datagrams
|
|
187
|
+
|
|
188
|
+
async def read_packets():
|
|
189
|
+
while True:
|
|
190
|
+
try:
|
|
191
|
+
packet = await asyncio.to_thread(sock.recvmsg, 32768, socket.CMSG_SPACE(12))
|
|
192
|
+
try:
|
|
193
|
+
inbox.put_nowait(packet)
|
|
194
|
+
except asyncio.QueueFull:
|
|
195
|
+
collector.agent_dropped += 1
|
|
196
|
+
except socket.timeout:
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
collector.agent_inbox = inbox
|
|
200
|
+
reader = asyncio.create_task(read_packets())
|
|
201
|
+
try:
|
|
202
|
+
while True:
|
|
203
|
+
raw, ancillary, flags, _ = await inbox.get()
|
|
204
|
+
try:
|
|
205
|
+
peer = next(
|
|
206
|
+
struct.unpack("3i", value)
|
|
207
|
+
for level, kind, value in ancillary
|
|
208
|
+
if level == socket.SOL_SOCKET and kind == socket.SCM_CREDENTIALS
|
|
209
|
+
)
|
|
210
|
+
packet = Packet.model_validate_json(raw)
|
|
211
|
+
if flags & socket.MSG_TRUNC or peer[0] != packet.pid or peer[1] != os.getuid():
|
|
212
|
+
continue
|
|
213
|
+
now = time.monotonic()
|
|
214
|
+
if packet.pid not in rates:
|
|
215
|
+
if len(rates) >= 128:
|
|
216
|
+
rates.pop(next(iter(rates)))
|
|
217
|
+
rates[packet.pid] = deque()
|
|
218
|
+
rate = rates[packet.pid]
|
|
219
|
+
while rate and rate[0] < now - 1:
|
|
220
|
+
rate.popleft()
|
|
221
|
+
if len(rate) >= 100:
|
|
222
|
+
collector.agent_dropped += 1
|
|
223
|
+
continue
|
|
224
|
+
rate.append(now)
|
|
225
|
+
process = await collector.call("sample", pid=packet.pid)
|
|
226
|
+
if process["identity"]["start_ticks"] != packet.start_ticks:
|
|
227
|
+
continue
|
|
228
|
+
key = process["key"]
|
|
229
|
+
clock_compatible = same_clock_namespace(packet.pid)
|
|
230
|
+
if key not in collector.agents and len(collector.agents) >= 128:
|
|
231
|
+
collector.agents.pop(next(iter(collector.agents)))
|
|
232
|
+
old = collector.agents.get(key)
|
|
233
|
+
if old and packet.sequence <= old["sequence"]:
|
|
234
|
+
continue
|
|
235
|
+
collector.agents[key] = {
|
|
236
|
+
"operations_completed": (old or {}).get("operations_completed", 0),
|
|
237
|
+
"context_incomplete": (old or {}).get("context_incomplete", False)
|
|
238
|
+
or packet.dropped > 0
|
|
239
|
+
or bool(old and packet.sequence != old["sequence"] + 1),
|
|
240
|
+
"last_seen": time.time(),
|
|
241
|
+
"sequence": packet.sequence,
|
|
242
|
+
"visibility": "gc_generation_zero_only",
|
|
243
|
+
"operation_context": True,
|
|
244
|
+
"native_clock_correlation": clock_compatible,
|
|
245
|
+
"lab": packet.payload.get("lab_object_blocks", False)
|
|
246
|
+
if packet.kind == "hello"
|
|
247
|
+
else (old or {}).get("lab", False),
|
|
248
|
+
"python_function_boundaries": packet.kind
|
|
249
|
+
in {"python_codes", "python_profile", "lab_stack", "lab_events"}
|
|
250
|
+
or (old or {}).get("python_function_boundaries", False),
|
|
251
|
+
}
|
|
252
|
+
if packet.kind == "operation":
|
|
253
|
+
operation = Operation.model_validate(packet.payload)
|
|
254
|
+
if operation.end_ns is not None:
|
|
255
|
+
collector.agents[key]["operations_completed"] += 1
|
|
256
|
+
with collector.db.begin() as db:
|
|
257
|
+
sessions = list(
|
|
258
|
+
db.scalars(
|
|
259
|
+
select(m.Session).where(
|
|
260
|
+
m.Session.process_key == key,
|
|
261
|
+
m.Session.state.in_(["recording", "stopping"]),
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
for s in sessions:
|
|
266
|
+
if (
|
|
267
|
+
s.quality.get("coverage") == "native_observed"
|
|
268
|
+
and not clock_compatible
|
|
269
|
+
and packet.kind in {"operation", "python_codes", "python_profile"}
|
|
270
|
+
):
|
|
271
|
+
s.quality = {
|
|
272
|
+
**s.quality,
|
|
273
|
+
"context_incomplete": True,
|
|
274
|
+
"context_failure": "Different or inaccessible Linux time namespaces; native/Python temporal correlation disabled",
|
|
275
|
+
}
|
|
276
|
+
continue
|
|
277
|
+
# Independent caps on opt-in diagnostics. A faulty agent cannot fill storage.
|
|
278
|
+
if packet.kind in {
|
|
279
|
+
"operation",
|
|
280
|
+
"python_codes",
|
|
281
|
+
"python_profile",
|
|
282
|
+
"lab_stack",
|
|
283
|
+
"lab_stacks",
|
|
284
|
+
"lab_events",
|
|
285
|
+
"lab_end",
|
|
286
|
+
}:
|
|
287
|
+
if packet.kind.startswith("lab_") and s.config["mode"] != "lab":
|
|
288
|
+
continue
|
|
289
|
+
payload = validate_evidence(packet.kind, packet.payload)
|
|
290
|
+
if packet.kind == "operation" and (
|
|
291
|
+
payload["thread"] != packet.thread
|
|
292
|
+
or (
|
|
293
|
+
payload["end_ns"] is not None
|
|
294
|
+
and payload["end_ns"] < payload["start_ns"]
|
|
295
|
+
)
|
|
296
|
+
):
|
|
297
|
+
continue
|
|
298
|
+
count, size = db.execute(
|
|
299
|
+
select(
|
|
300
|
+
func.count(), func.coalesce(func.sum(m.AgentRecord.size), 0)
|
|
301
|
+
).where(m.AgentRecord.session_id == s.id)
|
|
302
|
+
).one()
|
|
303
|
+
quota = min(
|
|
304
|
+
16 * 1024 * 1024, s.config["max_capture_mb"] * 1024 * 1024 // 4
|
|
305
|
+
)
|
|
306
|
+
if (
|
|
307
|
+
count >= 2048
|
|
308
|
+
or size + len(raw) > quota
|
|
309
|
+
or collector.disk_usage() + len(raw)
|
|
310
|
+
> collector.config.max_storage_mb * 1024**2
|
|
311
|
+
):
|
|
312
|
+
collector.agent_dropped += 1
|
|
313
|
+
s.quality = {
|
|
314
|
+
**s.quality,
|
|
315
|
+
"context_incomplete": True,
|
|
316
|
+
"context_limit": "cooperative evidence quota",
|
|
317
|
+
}
|
|
318
|
+
continue
|
|
319
|
+
if packet.kind in {"lab_stacks", "lab_events", "lab_end"}:
|
|
320
|
+
epoch = payload["epoch"]
|
|
321
|
+
epochs = s.quality.get("lab_epochs", [])
|
|
322
|
+
if epoch not in epochs:
|
|
323
|
+
epochs = epochs + [epoch]
|
|
324
|
+
s.quality = {
|
|
325
|
+
**s.quality,
|
|
326
|
+
"lab_epochs": epochs[:16],
|
|
327
|
+
"lab_last_sequence": 0,
|
|
328
|
+
"lab_complete": False,
|
|
329
|
+
"incomplete": s.quality.get("incomplete", False)
|
|
330
|
+
or len(epochs) > 1,
|
|
331
|
+
}
|
|
332
|
+
if len(epochs) > 1:
|
|
333
|
+
s.quality = {
|
|
334
|
+
**s.quality,
|
|
335
|
+
"lab_boundary": "Multiple Lab activations; unobserved gaps suppress exact end survivors",
|
|
336
|
+
}
|
|
337
|
+
gap = bool(old and packet.sequence != old["sequence"] + 1)
|
|
338
|
+
s.quality = {
|
|
339
|
+
**s.quality,
|
|
340
|
+
"python_context": True,
|
|
341
|
+
"context_incomplete": s.quality.get("context_incomplete", False)
|
|
342
|
+
or gap
|
|
343
|
+
or packet.dropped > 0
|
|
344
|
+
or (packet.kind == "python_profile" and payload["lost"] > 0),
|
|
345
|
+
"agent_dropped": packet.dropped,
|
|
346
|
+
"python_profile_expired": max(
|
|
347
|
+
s.quality.get("python_profile_expired", 0),
|
|
348
|
+
payload.get("expired", 0)
|
|
349
|
+
if packet.kind == "python_profile"
|
|
350
|
+
else 0,
|
|
351
|
+
),
|
|
352
|
+
"lab_complete": s.quality.get("lab_complete", False)
|
|
353
|
+
or (
|
|
354
|
+
packet.kind == "lab_end"
|
|
355
|
+
and payload["delivered"]
|
|
356
|
+
and packet.dropped == 0
|
|
357
|
+
and payload["last_sequence"]
|
|
358
|
+
== s.quality.get("lab_last_sequence", 0)
|
|
359
|
+
),
|
|
360
|
+
"lab_last_sequence": max(
|
|
361
|
+
s.quality.get("lab_last_sequence", 0),
|
|
362
|
+
max((row[0] for row in payload["events"]), default=0)
|
|
363
|
+
if packet.kind == "lab_events"
|
|
364
|
+
else 0,
|
|
365
|
+
),
|
|
366
|
+
"lab_stop_reason": payload.get("stop_reason", 0)
|
|
367
|
+
if packet.kind == "lab_events"
|
|
368
|
+
else s.quality.get("lab_stop_reason", 0),
|
|
369
|
+
"lab_unknown_frees": max(
|
|
370
|
+
s.quality.get("lab_unknown_frees", 0),
|
|
371
|
+
payload.get("unknown_frees", 0),
|
|
372
|
+
),
|
|
373
|
+
"python_profile_lost": max(
|
|
374
|
+
s.quality.get("python_profile_lost", 0),
|
|
375
|
+
payload.get("lost", 0)
|
|
376
|
+
if packet.kind == "python_profile"
|
|
377
|
+
else 0,
|
|
378
|
+
),
|
|
379
|
+
"lost_events": max(
|
|
380
|
+
s.quality.get("lost_events", 0),
|
|
381
|
+
payload.get("lost", 0) if packet.kind == "lab_events" else 0,
|
|
382
|
+
),
|
|
383
|
+
"lab_unattributed_events": s.quality.get(
|
|
384
|
+
"lab_unattributed_events", 0
|
|
385
|
+
)
|
|
386
|
+
+ sum(bool(row[10] & 5) for row in payload.get("events", []))
|
|
387
|
+
if packet.kind == "lab_events"
|
|
388
|
+
else s.quality.get("lab_unattributed_events", 0),
|
|
389
|
+
"lab_events": s.quality.get("lab_events", 0)
|
|
390
|
+
+ (len(payload["events"]) if packet.kind == "lab_events" else 0),
|
|
391
|
+
"incomplete": s.quality.get("incomplete", False)
|
|
392
|
+
or (packet.kind == "lab_events" and payload["lost"] > 0),
|
|
393
|
+
}
|
|
394
|
+
db.add(
|
|
395
|
+
m.AgentRecord(
|
|
396
|
+
session_id=s.id,
|
|
397
|
+
agent_sequence=packet.sequence,
|
|
398
|
+
timestamp=packet.timestamp,
|
|
399
|
+
size=len(raw),
|
|
400
|
+
data={
|
|
401
|
+
"kind": packet.kind,
|
|
402
|
+
"process_key": key,
|
|
403
|
+
"thread": packet.thread,
|
|
404
|
+
"monotonic_ns": packet.monotonic_ns,
|
|
405
|
+
"timestamp": packet.timestamp,
|
|
406
|
+
"payload": payload,
|
|
407
|
+
},
|
|
408
|
+
)
|
|
409
|
+
)
|
|
410
|
+
elif packet.kind == "gc_snapshot":
|
|
411
|
+
snapshot = Snapshot.model_validate(packet.payload)
|
|
412
|
+
if any(
|
|
413
|
+
len(name) > 96 or not name.isprintable() for name in snapshot.types
|
|
414
|
+
):
|
|
415
|
+
continue
|
|
416
|
+
count = db.scalar(
|
|
417
|
+
select(func.count())
|
|
418
|
+
.select_from(m.GCTypeSnapshot)
|
|
419
|
+
.where(m.GCTypeSnapshot.session_id == s.id)
|
|
420
|
+
)
|
|
421
|
+
if count >= 100:
|
|
422
|
+
collector.agent_dropped += 1
|
|
423
|
+
continue
|
|
424
|
+
db.add(
|
|
425
|
+
m.GCTypeSnapshot(
|
|
426
|
+
id=uuid.uuid4().hex,
|
|
427
|
+
session_id=s.id,
|
|
428
|
+
timestamp=packet.timestamp,
|
|
429
|
+
snapshot=snapshot.model_dump(),
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
elif packet.kind == "marker":
|
|
433
|
+
allowed = {
|
|
434
|
+
"operation",
|
|
435
|
+
"module",
|
|
436
|
+
"model",
|
|
437
|
+
"method",
|
|
438
|
+
"phase",
|
|
439
|
+
"worker_type",
|
|
440
|
+
}
|
|
441
|
+
metadata = packet.payload.get("metadata", {})
|
|
442
|
+
if not isinstance(metadata, dict) or set(metadata) - allowed:
|
|
443
|
+
continue
|
|
444
|
+
marker = MarkerCreate(
|
|
445
|
+
name=packet.payload["name"], operation=metadata.get("operation")
|
|
446
|
+
)
|
|
447
|
+
count = db.scalar(
|
|
448
|
+
select(func.count())
|
|
449
|
+
.select_from(m.Marker)
|
|
450
|
+
.where(m.Marker.session_id == s.id)
|
|
451
|
+
)
|
|
452
|
+
if count >= 1000:
|
|
453
|
+
collector.agent_dropped += 1
|
|
454
|
+
continue
|
|
455
|
+
db.add(
|
|
456
|
+
m.Marker(
|
|
457
|
+
id=uuid.uuid4().hex,
|
|
458
|
+
session_id=s.id,
|
|
459
|
+
timestamp=packet.timestamp,
|
|
460
|
+
name=marker.name,
|
|
461
|
+
metadata_json={
|
|
462
|
+
"operation": marker.operation,
|
|
463
|
+
"source": "cooperative_agent",
|
|
464
|
+
"thread": packet.thread,
|
|
465
|
+
},
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
except (ValueError, KeyError, StopIteration, RuntimeError, OSError):
|
|
469
|
+
collector.agent_dropped += 1
|
|
470
|
+
finally:
|
|
471
|
+
inbox.task_done()
|
|
472
|
+
finally:
|
|
473
|
+
reader.cancel()
|
|
474
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
475
|
+
await reader
|
|
476
|
+
sock.close()
|
|
477
|
+
path.unlink(missing_ok=True)
|