fluidattacks-agent 0.1.1__tar.gz → 0.1.2__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.
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/.gitignore +1 -1
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/PKG-INFO +1 -1
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/fluidattacks_agent/deliver.py +23 -3
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/fluidattacks_agent/executions.py +3 -1
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/fluidattacks_agent/startup.py +50 -24
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/pyproject.toml +1 -1
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/README.md +0 -0
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/fluidattacks_agent/__init__.py +0 -0
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/fluidattacks_agent/batch.py +0 -0
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/fluidattacks_agent/distributions.py +0 -0
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/fluidattacks_agent/gate.py +0 -0
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/fluidattacks_agent/loads.py +0 -0
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/fluidattacks_agent/observer.py +0 -0
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/fluidattacks_agent/outbox.py +0 -0
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/fluidattacks_agent/patience.py +0 -0
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/fluidattacks_agent/post.py +0 -0
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/fluidattacks_agent/report.py +0 -0
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/fluidattacks_agent/settings.py +0 -0
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/fluidattacks_agent/sink.py +0 -0
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/fluidattacks_agent/switch.py +0 -0
- {fluidattacks_agent-0.1.1 → fluidattacks_agent-0.1.2}/fluidattacks_agent.pth +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fluidattacks-agent
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: In-process probe reporting what a Python workload imports and runs
|
|
5
5
|
Project-URL: Homepage, https://fluidattacks.com
|
|
6
6
|
Project-URL: Source, https://gitlab.com/fluidattacks/universe/-/tree/trunk/watches/agents/python
|
|
@@ -46,6 +46,12 @@ class Deliverer:
|
|
|
46
46
|
guard: threading.Lock = field(default_factory=threading.Lock, repr=False)
|
|
47
47
|
# one pass at a time: what is taken but not delivered lives in one place
|
|
48
48
|
passing: threading.Lock = field(default_factory=threading.Lock, repr=False)
|
|
49
|
+
# told once a period went by with the far end in reach and nothing left to
|
|
50
|
+
# carry, so what the probe still holds goes out before it is any older
|
|
51
|
+
remind: Callable[[], None] = field(default=lambda: None, repr=False)
|
|
52
|
+
# when the probe was last reminded: a far end that refuses and then takes
|
|
53
|
+
# ends a wait every standoff, and a wait ending is not a period going by
|
|
54
|
+
reminded: float = 0.0
|
|
49
55
|
leaving: bool = False
|
|
50
56
|
delivered: int = 0
|
|
51
57
|
|
|
@@ -75,10 +81,23 @@ class Deliverer:
|
|
|
75
81
|
while not self.leaving:
|
|
76
82
|
# a thread that dies takes delivery with it and tells nobody
|
|
77
83
|
with contextlib.suppress(Exception):
|
|
78
|
-
self.woken.wait(self._waiting())
|
|
84
|
+
roused = self.woken.wait(self._waiting())
|
|
79
85
|
self.woken.clear()
|
|
80
|
-
if time.monotonic()
|
|
81
|
-
|
|
86
|
+
if time.monotonic() < self.due:
|
|
87
|
+
continue
|
|
88
|
+
self.offering()
|
|
89
|
+
# a period, never a wake: a wake says there is something to
|
|
90
|
+
# carry, and asking for more on one made each flush bring the
|
|
91
|
+
# next. Nothing in hand, or the far end is still refusing
|
|
92
|
+
now = time.monotonic()
|
|
93
|
+
if (
|
|
94
|
+
not roused
|
|
95
|
+
and self.holding is None
|
|
96
|
+
and not self.leaving
|
|
97
|
+
and now - self.reminded >= self.interval
|
|
98
|
+
):
|
|
99
|
+
self.reminded = now
|
|
100
|
+
self.remind()
|
|
82
101
|
|
|
83
102
|
def parting(self) -> None:
|
|
84
103
|
"""Give whatever is held one bounded chance to travel, then give up."""
|
|
@@ -109,6 +128,7 @@ class Deliverer:
|
|
|
109
128
|
# every worker of a pre-forking server was born in the same second
|
|
110
129
|
self.due = 0.0
|
|
111
130
|
self.tries = 0
|
|
131
|
+
self.reminded = 0.0
|
|
112
132
|
# a child is not on its way out merely because its parent was
|
|
113
133
|
self.leaving = False
|
|
114
134
|
self.batcher.forked()
|
|
@@ -13,7 +13,9 @@ from fluidattacks_agent.report import Ecosystem, Evidence, Granularity, Record,
|
|
|
13
13
|
# ignores it, so nothing changes until the floor moves
|
|
14
14
|
__lazy_modules__ = ["pathlib"]
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# a real application runs some 26,000 distinct functions while it starts, and
|
|
17
|
+
# what filled the ceiling is what never gets named again for the process's life
|
|
18
|
+
MAX_FUNCTIONS: Final = 32768
|
|
17
19
|
|
|
18
20
|
OURS: Final = "fluidattacks_agent"
|
|
19
21
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Start the probe inside a workload and leave reports where the agent drains."""
|
|
2
2
|
|
|
3
|
+
import _thread
|
|
3
4
|
import atexit
|
|
4
5
|
import contextlib
|
|
5
6
|
import os
|
|
@@ -19,7 +20,8 @@ from fluidattacks_agent.settings import Delivery, asked, delivery
|
|
|
19
20
|
from fluidattacks_agent.sink import FileSink, Holding, Sink, Stalled
|
|
20
21
|
from fluidattacks_agent.switch import switched_on
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
# first sightings between flushes. The carrier's clock brings the rest
|
|
24
|
+
FLUSH_EVERY: Final = 128
|
|
23
25
|
|
|
24
26
|
# what a site directory is given so that installing the package starts the
|
|
25
27
|
# probe. The agent writes the same line when it injects one it did not install
|
|
@@ -74,6 +76,13 @@ class Probe:
|
|
|
74
76
|
# sampled when the events were armed, not when the report is written: the
|
|
75
77
|
# later reading governs the next window rather than the one reported
|
|
76
78
|
solitary: bool = False
|
|
79
|
+
# set off the workload's threads, acted on by the next sighting on one of
|
|
80
|
+
# them, since what is held is walked nowhere else
|
|
81
|
+
overdue: bool = False
|
|
82
|
+
# every thread sees overdue at once, so the flush is gated by a lock nothing
|
|
83
|
+
# waits on: a second caller finds it taken and returns. From _thread, which
|
|
84
|
+
# every interpreter start has already paid for, and threading is not
|
|
85
|
+
gate: _thread.LockType = field(default_factory=_thread.allocate_lock, repr=False)
|
|
77
86
|
|
|
78
87
|
def find_spec(
|
|
79
88
|
self,
|
|
@@ -93,10 +102,8 @@ class Probe:
|
|
|
93
102
|
def _looked(self, fullname: str) -> None:
|
|
94
103
|
if self.flushing:
|
|
95
104
|
return
|
|
96
|
-
held = self.observer.suppressed
|
|
97
105
|
module = self.observer.note(fullname)
|
|
98
|
-
|
|
99
|
-
self._pile(fresh or int(self.observer.suppressed != held))
|
|
106
|
+
self._pile(0 if module is None else self._mark(Evidence.IMPORTED, module))
|
|
100
107
|
|
|
101
108
|
def read(self, path: str) -> None:
|
|
102
109
|
"""Note a file the workload opened, and report as those pile up too."""
|
|
@@ -104,21 +111,17 @@ class Probe:
|
|
|
104
111
|
# is the workload using anything
|
|
105
112
|
if self.flushing:
|
|
106
113
|
return
|
|
107
|
-
held = self.loads.suppressed
|
|
108
114
|
package = self.loads.note(path)
|
|
109
115
|
# tested here and not inside: the hook runs on every open the host
|
|
110
116
|
# makes, and all but a few are not ours to stamp
|
|
111
|
-
|
|
112
|
-
self._pile(fresh or int(self.loads.suppressed != held))
|
|
117
|
+
self._pile(0 if package is None else self._mark(Evidence.READ, package))
|
|
113
118
|
|
|
114
119
|
def ran(self, filename: str, qualname: str) -> None:
|
|
115
120
|
"""Note code starting, and report as first sightings pile up too."""
|
|
116
121
|
if self.flushing:
|
|
117
122
|
return
|
|
118
|
-
held = self.runs.suppressed
|
|
119
123
|
symbol = self.runs.note(filename, qualname)
|
|
120
|
-
|
|
121
|
-
self._pile(fresh or int(self.runs.suppressed != held))
|
|
124
|
+
self._pile(0 if symbol is None else self._mark(Evidence.EXECUTED, symbol))
|
|
122
125
|
|
|
123
126
|
def _mark(self, evidence: Evidence, symbol: str) -> int:
|
|
124
127
|
"""Stamp a sighting, saying whether it was the first of its symbol."""
|
|
@@ -130,11 +133,17 @@ class Probe:
|
|
|
130
133
|
self.first[key] = seen
|
|
131
134
|
return 1
|
|
132
135
|
|
|
136
|
+
def remind(self) -> None:
|
|
137
|
+
"""Have the next sighting bring a flush, however few have piled up."""
|
|
138
|
+
# a flag and never a flush: the carrier's clock calls this from a
|
|
139
|
+
# thread of its own, and a flush runs on the workload's alone
|
|
140
|
+
self.overdue = True
|
|
141
|
+
|
|
133
142
|
def _pile(self, added: int) -> None:
|
|
134
|
-
#
|
|
135
|
-
#
|
|
143
|
+
# first sightings only: a flush re-arms every code object, so counting
|
|
144
|
+
# one seen again, or one with no room, made each flush bring the next
|
|
136
145
|
self.pending += added
|
|
137
|
-
if self.pending >= FLUSH_EVERY:
|
|
146
|
+
if self.overdue or self.pending >= FLUSH_EVERY:
|
|
138
147
|
# cleared before the call, because monitoring reports the entry of
|
|
139
148
|
# flush itself and would otherwise pile up and call it again
|
|
140
149
|
self.pending = 0
|
|
@@ -142,9 +151,11 @@ class Probe:
|
|
|
142
151
|
|
|
143
152
|
def flush(self) -> None:
|
|
144
153
|
"""Write what has not been reported, without ever raising at a caller."""
|
|
145
|
-
if self.
|
|
154
|
+
if not self.gate.acquire(blocking=False):
|
|
146
155
|
return
|
|
147
156
|
self.flushing = True
|
|
157
|
+
# spent here, so a reminder given while this runs brings one more flush
|
|
158
|
+
self.overdue = False
|
|
148
159
|
try:
|
|
149
160
|
with contextlib.suppress(Exception):
|
|
150
161
|
if self._sighted():
|
|
@@ -152,8 +163,18 @@ class Probe:
|
|
|
152
163
|
finally:
|
|
153
164
|
self.pending = 0
|
|
154
165
|
self.flushing = False
|
|
166
|
+
self.gate.release()
|
|
155
167
|
self._rearm()
|
|
156
168
|
|
|
169
|
+
def forked(self) -> None:
|
|
170
|
+
"""Leave a child able to flush, whatever its parent was doing at the fork."""
|
|
171
|
+
# a lock another thread held at the fork is held in the child forever,
|
|
172
|
+
# and a flag it set stays set: the child has no thread left to clear it
|
|
173
|
+
self.gate = _thread.allocate_lock()
|
|
174
|
+
self.flushing = False
|
|
175
|
+
self.overdue = False
|
|
176
|
+
self.pending = 0
|
|
177
|
+
|
|
157
178
|
def _owed(self) -> bool:
|
|
158
179
|
"""Say whether a count is waiting for a report that can state it."""
|
|
159
180
|
return bool(self.refused or self.undated)
|
|
@@ -371,23 +392,28 @@ def start(sink: Sink | None = None) -> Probe | None:
|
|
|
371
392
|
return None
|
|
372
393
|
|
|
373
394
|
|
|
374
|
-
def chosen() -> Sink:
|
|
375
|
-
"""
|
|
395
|
+
def chosen(remind: Callable[[], None]) -> Sink:
|
|
396
|
+
"""
|
|
397
|
+
Give a workload what it asked for, and something loud if it asked badly.
|
|
398
|
+
|
|
399
|
+
``remind`` is what a carrier calls, off the workload's threads, once a
|
|
400
|
+
period has gone by with the far end in reach and nothing left to carry.
|
|
401
|
+
"""
|
|
376
402
|
if not asked():
|
|
377
403
|
return FileSink()
|
|
378
404
|
told = delivery()
|
|
379
405
|
if told is None:
|
|
380
406
|
# half configured must be loud, not indistinguishable from quiet
|
|
381
407
|
return Holding()
|
|
382
|
-
return carrier(told)
|
|
408
|
+
return carrier(told, remind)
|
|
383
409
|
|
|
384
410
|
|
|
385
|
-
def carrier(told: Delivery) -> Sink:
|
|
411
|
+
def carrier(told: Delivery, remind: Callable[[], None]) -> Sink:
|
|
386
412
|
"""Build the one thing that carries reports out, and wire it to this process."""
|
|
387
413
|
# imported here, so a workload that never asked pays for none of it
|
|
388
414
|
from fluidattacks_agent.deliver import Deliverer # noqa: PLC0415
|
|
389
415
|
|
|
390
|
-
held = Deliverer(told=told)
|
|
416
|
+
held = Deliverer(told=told, remind=remind)
|
|
391
417
|
# before the flush handler, since exit handlers run in reverse order
|
|
392
418
|
atexit.register(held.parting)
|
|
393
419
|
held.registered()
|
|
@@ -401,11 +427,11 @@ def _start(sink: Sink | None) -> Probe | None:
|
|
|
401
427
|
if not switched_on():
|
|
402
428
|
return None
|
|
403
429
|
sites = site_dirs()
|
|
404
|
-
probe = Probe(
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
430
|
+
probe = Probe(loads=LoadObserver(sites=sites), runs=RunObserver(sites=sites))
|
|
431
|
+
# after the probe, because what carries its reports is told what to remind
|
|
432
|
+
probe.sink = chosen(probe.remind) if sink is None else sink
|
|
433
|
+
if hasattr(os, "register_at_fork"):
|
|
434
|
+
os.register_at_fork(after_in_child=probe.forked)
|
|
409
435
|
install(probe)
|
|
410
436
|
sys.addaudithook(watch(probe))
|
|
411
437
|
probe.monitoring = observe_runs(probe)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|