fluidattacks-agent 0.2.0__tar.gz → 0.3.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.
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/PKG-INFO +1 -1
- fluidattacks_agent-0.3.0/fluidattacks_agent/executions.py +162 -0
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/fluidattacks_agent/loads.py +57 -27
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/fluidattacks_agent/observer.py +63 -35
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/fluidattacks_agent/report.py +58 -4
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/fluidattacks_agent/startup.py +92 -88
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/pyproject.toml +1 -1
- fluidattacks_agent-0.2.0/fluidattacks_agent/executions.py +0 -124
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/.gitignore +0 -0
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/README.md +0 -0
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/fluidattacks_agent/__init__.py +0 -0
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/fluidattacks_agent/batch.py +0 -0
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/fluidattacks_agent/deliver.py +0 -0
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/fluidattacks_agent/distributions.py +0 -0
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/fluidattacks_agent/gate.py +0 -0
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/fluidattacks_agent/outbox.py +0 -0
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/fluidattacks_agent/patience.py +0 -0
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/fluidattacks_agent/post.py +0 -0
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/fluidattacks_agent/settings.py +0 -0
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/fluidattacks_agent/sink.py +0 -0
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/fluidattacks_agent/switch.py +0 -0
- {fluidattacks_agent-0.2.0 → fluidattacks_agent-0.3.0}/fluidattacks_agent.pth +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fluidattacks-agent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Watch code inside a package start running, where the interpreter allows it."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import PurePath
|
|
7
|
+
from typing import Final
|
|
8
|
+
|
|
9
|
+
from fluidattacks_agent.distributions import Dist, distribution_of
|
|
10
|
+
from fluidattacks_agent.report import (
|
|
11
|
+
Ecosystem,
|
|
12
|
+
Evidence,
|
|
13
|
+
Granularity,
|
|
14
|
+
Record,
|
|
15
|
+
Sighting,
|
|
16
|
+
dated,
|
|
17
|
+
merged,
|
|
18
|
+
reached,
|
|
19
|
+
sighted,
|
|
20
|
+
trimmed,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# 3.15 makes the import below lazy from this; every version we support
|
|
24
|
+
# ignores it, so nothing changes until the floor moves
|
|
25
|
+
__lazy_modules__ = ["pathlib"]
|
|
26
|
+
|
|
27
|
+
# what one window holds at most: a real application runs some 26,000 distinct
|
|
28
|
+
# functions while it starts, and the start is the busiest window there is. A
|
|
29
|
+
# name refused past it is counted, and named again the next window it runs in
|
|
30
|
+
MAX_FUNCTIONS: Final = 32768
|
|
31
|
+
|
|
32
|
+
OURS: Final = "fluidattacks_agent"
|
|
33
|
+
|
|
34
|
+
SOURCE: Final = ".py"
|
|
35
|
+
|
|
36
|
+
INITIALIZER: Final = "__init__"
|
|
37
|
+
PARENT: Final = ".."
|
|
38
|
+
|
|
39
|
+
TOOL: Final = 3
|
|
40
|
+
TOOL_NAME: Final = "fluidattacks-agent"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
TOOLS: Final = 6
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def available() -> bool:
|
|
47
|
+
"""Say whether this interpreter can report code starting at all."""
|
|
48
|
+
return hasattr(sys, "monitoring")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def alone(tool: int = TOOL) -> bool:
|
|
52
|
+
"""Say whether this is the only tool watching: restart_events takes none."""
|
|
53
|
+
held = (sys.monitoring.get_tool(other) for other in range(TOOLS) if other != tool)
|
|
54
|
+
return not any(name is not None for name in held)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def module_of(filename: str, sites: tuple[str, ...]) -> str | None:
|
|
58
|
+
"""Name the module whose source a running code object came from."""
|
|
59
|
+
for site in sites:
|
|
60
|
+
prefix = site + "/"
|
|
61
|
+
if not filename.startswith(prefix) or not filename.endswith(SOURCE):
|
|
62
|
+
continue
|
|
63
|
+
held = PurePath(filename[len(prefix) :])
|
|
64
|
+
parts = list(held.parts)
|
|
65
|
+
if not parts or held.is_absolute() or PARENT in parts:
|
|
66
|
+
return None
|
|
67
|
+
stem = parts.pop()[: -len(SOURCE)]
|
|
68
|
+
if not stem:
|
|
69
|
+
return None
|
|
70
|
+
if stem != INITIALIZER:
|
|
71
|
+
parts.append(stem)
|
|
72
|
+
return ".".join(parts) or None
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class RunObserver:
|
|
78
|
+
"""Functions that started in the window under way, and in no window before it."""
|
|
79
|
+
|
|
80
|
+
sites: tuple[str, ...] = ()
|
|
81
|
+
# the window's own: a fold takes the whole map, and the reader adds the
|
|
82
|
+
# windows up. So the bound is on a window and not on the process's life
|
|
83
|
+
seen: dict[str, Sighting] = field(default_factory=dict)
|
|
84
|
+
# what a fold took and did not report, for the next fold to offer again.
|
|
85
|
+
# The fold's own: no thread of the workload writes beside it. Bounded by
|
|
86
|
+
# the same ceiling, and what finds no room is counted with the refusals
|
|
87
|
+
owed: dict[str, Sighting] = field(default_factory=dict)
|
|
88
|
+
suppressed: int = 0
|
|
89
|
+
unkept: int = 0
|
|
90
|
+
|
|
91
|
+
def note(
|
|
92
|
+
self,
|
|
93
|
+
filename: str,
|
|
94
|
+
qualname: str,
|
|
95
|
+
clock: Callable[[], int] = reached,
|
|
96
|
+
) -> str | None:
|
|
97
|
+
"""Take note of code starting, naming the function whenever the window has room."""
|
|
98
|
+
module = module_of(filename, self.sites)
|
|
99
|
+
if module is None:
|
|
100
|
+
return None
|
|
101
|
+
if module.split(".", maxsplit=1)[0] == OURS:
|
|
102
|
+
return None
|
|
103
|
+
qualified = f"{module}.{qualname}"
|
|
104
|
+
# bound once: a fold may swap the window out meanwhile, and a sighting
|
|
105
|
+
# must land in the window it was counted against, never in the next
|
|
106
|
+
seen = self.seen
|
|
107
|
+
held = seen.get(qualified)
|
|
108
|
+
if held is None and len(seen) >= MAX_FUNCTIONS:
|
|
109
|
+
self.suppressed += 1
|
|
110
|
+
return None
|
|
111
|
+
seen[qualified] = sighted(held, clock())
|
|
112
|
+
return qualified
|
|
113
|
+
|
|
114
|
+
def drain(self) -> dict[str, Sighting]:
|
|
115
|
+
"""Take the window whole, and what the last fold owed, leaving the next one empty."""
|
|
116
|
+
taken, self.seen = self.seen, {}
|
|
117
|
+
# into a map of the fold's own, so what a late sighting lands in the
|
|
118
|
+
# window taken is at most lost, and never written over by the fold
|
|
119
|
+
offered = self.owed
|
|
120
|
+
self.owed = {}
|
|
121
|
+
merged(offered, taken)
|
|
122
|
+
return offered
|
|
123
|
+
|
|
124
|
+
def restore(self, taken: Mapping[str, Sighting]) -> None:
|
|
125
|
+
"""Keep for the next fold what this one took and did not report, as far as there is room."""
|
|
126
|
+
merged(self.owed, taken)
|
|
127
|
+
self.unkept += trimmed(self.owed, MAX_FUNCTIONS)
|
|
128
|
+
|
|
129
|
+
def records(
|
|
130
|
+
self,
|
|
131
|
+
taken: Mapping[str, Sighting],
|
|
132
|
+
installed: dict[str, Dist],
|
|
133
|
+
origin: int,
|
|
134
|
+
) -> tuple[list[Record], dict[str, Sighting]]:
|
|
135
|
+
"""Report every function a distribution owns, and say what the next window keeps."""
|
|
136
|
+
found: list[Record] = []
|
|
137
|
+
kept: dict[str, Sighting] = {}
|
|
138
|
+
scanned = bool(installed)
|
|
139
|
+
for qualified in sorted(taken):
|
|
140
|
+
held = taken[qualified]
|
|
141
|
+
dist = distribution_of(qualified, installed)
|
|
142
|
+
if dist is None:
|
|
143
|
+
# no distribution will own what a settled scan does not, and a
|
|
144
|
+
# scan that has found nothing yet may still own all of these
|
|
145
|
+
if not scanned:
|
|
146
|
+
kept[qualified] = held
|
|
147
|
+
continue
|
|
148
|
+
found.append(
|
|
149
|
+
Record(
|
|
150
|
+
ecosystem=Ecosystem.PYPI,
|
|
151
|
+
name=dist.name,
|
|
152
|
+
version=dist.version,
|
|
153
|
+
confidence=dist.confidence,
|
|
154
|
+
symbol=qualified,
|
|
155
|
+
granularity=Granularity.FUNCTION,
|
|
156
|
+
evidence=Evidence.EXECUTED,
|
|
157
|
+
frequency=held.times,
|
|
158
|
+
when=held.first,
|
|
159
|
+
used=dated(origin, held.last),
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
return found, kept
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
"""Account for package code the interpreter read without importing it."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
from collections.abc import Mapping
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from types import ModuleType
|
|
7
7
|
|
|
8
8
|
from fluidattacks_agent.distributions import Dist, distribution_of
|
|
9
|
-
from fluidattacks_agent.report import
|
|
9
|
+
from fluidattacks_agent.report import (
|
|
10
|
+
Ecosystem,
|
|
11
|
+
Evidence,
|
|
12
|
+
Granularity,
|
|
13
|
+
Record,
|
|
14
|
+
Sighting,
|
|
15
|
+
dated,
|
|
16
|
+
merged,
|
|
17
|
+
reached,
|
|
18
|
+
sighted,
|
|
19
|
+
trimmed,
|
|
20
|
+
)
|
|
10
21
|
|
|
22
|
+
# what one window holds at most, and not the process's life
|
|
11
23
|
MAX_PACKAGES = 4096
|
|
12
24
|
|
|
13
25
|
OURS = "fluidattacks_agent"
|
|
@@ -46,51 +58,67 @@ def package_of(path: str, sites: tuple[str, ...]) -> str | None:
|
|
|
46
58
|
|
|
47
59
|
@dataclass
|
|
48
60
|
class LoadObserver:
|
|
49
|
-
"""Packages whose code was read
|
|
61
|
+
"""Packages whose code was read in the window under way."""
|
|
50
62
|
|
|
51
63
|
sites: tuple[str, ...] = ()
|
|
52
64
|
# how many files of each package were read, not merely whether any was
|
|
53
|
-
seen: dict[str,
|
|
54
|
-
|
|
65
|
+
seen: dict[str, Sighting] = field(default_factory=dict)
|
|
66
|
+
# what a fold took and did not report, for the next fold to offer again.
|
|
67
|
+
# The fold's own: no thread of the workload writes beside it. Bounded by
|
|
68
|
+
# the same ceiling, and what finds no room is counted with the refusals
|
|
69
|
+
owed: dict[str, Sighting] = field(default_factory=dict)
|
|
55
70
|
suppressed: int = 0
|
|
71
|
+
unkept: int = 0
|
|
56
72
|
|
|
57
|
-
def note(self, path: str) -> str | None:
|
|
58
|
-
"""Take note of a file opened, naming the package whenever
|
|
73
|
+
def note(self, path: str, clock: Callable[[], int] = reached) -> str | None:
|
|
74
|
+
"""Take note of a file opened, naming the package whenever the window has room."""
|
|
59
75
|
package = package_of(path, self.sites)
|
|
60
76
|
if package is None or package == OURS:
|
|
61
77
|
return None
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return package
|
|
66
|
-
if len(self.seen) >= MAX_PACKAGES:
|
|
78
|
+
seen = self.seen
|
|
79
|
+
held = seen.get(package)
|
|
80
|
+
if held is None and len(seen) >= MAX_PACKAGES:
|
|
67
81
|
self.suppressed += 1
|
|
68
82
|
return None
|
|
69
|
-
|
|
70
|
-
self.moved.add(package)
|
|
83
|
+
seen[package] = sighted(held, clock())
|
|
71
84
|
return package
|
|
72
85
|
|
|
86
|
+
def drain(self) -> dict[str, Sighting]:
|
|
87
|
+
"""Take the window whole, and what the last fold owed, leaving the next one empty."""
|
|
88
|
+
taken, self.seen = self.seen, {}
|
|
89
|
+
# into a map of the fold's own, so what a late sighting lands in the
|
|
90
|
+
# window taken is at most lost, and never written over by the fold
|
|
91
|
+
offered = self.owed
|
|
92
|
+
self.owed = {}
|
|
93
|
+
merged(offered, taken)
|
|
94
|
+
return offered
|
|
95
|
+
|
|
96
|
+
def restore(self, taken: Mapping[str, Sighting]) -> None:
|
|
97
|
+
"""Keep for the next fold what this one took and did not report, as far as there is room."""
|
|
98
|
+
merged(self.owed, taken)
|
|
99
|
+
self.unkept += trimmed(self.owed, MAX_PACKAGES)
|
|
100
|
+
|
|
73
101
|
def records(
|
|
74
102
|
self,
|
|
103
|
+
taken: Mapping[str, Sighting],
|
|
75
104
|
modules: dict[str, ModuleType],
|
|
76
105
|
installed: dict[str, Dist],
|
|
77
|
-
|
|
78
|
-
) -> list[Record]:
|
|
79
|
-
"""Report the packages that
|
|
106
|
+
origin: int,
|
|
107
|
+
) -> tuple[list[Record], dict[str, Sighting]]:
|
|
108
|
+
"""Report the packages read that became no module, and say what the next window keeps."""
|
|
80
109
|
found: list[Record] = []
|
|
110
|
+
kept: dict[str, Sighting] = {}
|
|
81
111
|
scanned = bool(installed)
|
|
82
|
-
for package in sorted(
|
|
83
|
-
|
|
84
|
-
if times <= carried.get(keyed(Evidence.READ, package), 0):
|
|
85
|
-
self.moved.discard(package)
|
|
86
|
-
continue
|
|
112
|
+
for package in sorted(taken):
|
|
113
|
+
held = taken[package]
|
|
87
114
|
if package in modules:
|
|
88
|
-
#
|
|
115
|
+
# the stronger evidence has it. A name that leaves sys.modules
|
|
116
|
+
# and is read again is a sighting of the window that reads it
|
|
89
117
|
continue
|
|
90
118
|
dist = distribution_of(package, installed)
|
|
91
119
|
if dist is None:
|
|
92
|
-
if scanned:
|
|
93
|
-
|
|
120
|
+
if not scanned:
|
|
121
|
+
kept[package] = held
|
|
94
122
|
continue
|
|
95
123
|
found.append(
|
|
96
124
|
Record(
|
|
@@ -103,7 +131,9 @@ class LoadObserver:
|
|
|
103
131
|
# module it is would claim the interpreter reached it
|
|
104
132
|
granularity=Granularity.PACKAGE,
|
|
105
133
|
evidence=Evidence.READ,
|
|
106
|
-
frequency=times,
|
|
134
|
+
frequency=held.times,
|
|
135
|
+
when=held.first,
|
|
136
|
+
used=dated(origin, held.last),
|
|
107
137
|
),
|
|
108
138
|
)
|
|
109
|
-
return found
|
|
139
|
+
return found, kept
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
"""Watch what a workload imports, without taking part in importing it."""
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
-
from collections.abc import Mapping
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from types import ModuleType
|
|
7
7
|
from typing import Protocol
|
|
8
8
|
|
|
9
9
|
from fluidattacks_agent.distributions import Dist, distribution_of
|
|
10
|
-
from fluidattacks_agent.report import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
from fluidattacks_agent.report import (
|
|
11
|
+
Ecosystem,
|
|
12
|
+
Evidence,
|
|
13
|
+
Granularity,
|
|
14
|
+
Record,
|
|
15
|
+
Sighting,
|
|
16
|
+
dated,
|
|
17
|
+
merged,
|
|
18
|
+
reached,
|
|
19
|
+
sighted,
|
|
20
|
+
trimmed,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# the probe rides inside a workload we do not own, so what one window holds is
|
|
24
|
+
# bounded, and the bound is on the window rather than on the process's life
|
|
14
25
|
MAX_MODULES = 4096
|
|
15
26
|
|
|
16
27
|
# our own package would otherwise land in the workload's inventory
|
|
@@ -27,29 +38,29 @@ class ImportObserver:
|
|
|
27
38
|
finders behind it to decide the outcome.
|
|
28
39
|
"""
|
|
29
40
|
|
|
30
|
-
# how many times each module was looked up
|
|
31
|
-
# record can name the module that was seen and not
|
|
32
|
-
# it. The name is what bounds the ceiling below
|
|
33
|
-
seen: dict[str,
|
|
34
|
-
|
|
35
|
-
#
|
|
41
|
+
# how many times each module was looked up in the window under way, by its
|
|
42
|
+
# whole dotted name, so a record can name the module that was seen and not
|
|
43
|
+
# only the package holding it. The name is what bounds the ceiling below
|
|
44
|
+
seen: dict[str, Sighting] = field(default_factory=dict)
|
|
45
|
+
# what a fold took and did not report, for the next fold to offer again.
|
|
46
|
+
# The fold's own: no thread of the workload writes beside it. Bounded by
|
|
47
|
+
# the same ceiling, and what finds no room is counted with the refusals
|
|
48
|
+
owed: dict[str, Sighting] = field(default_factory=dict)
|
|
36
49
|
suppressed: int = 0
|
|
50
|
+
unkept: int = 0
|
|
37
51
|
|
|
38
|
-
def note(self, fullname: str) -> str | None:
|
|
39
|
-
"""Take note of a module looked up, naming it whenever
|
|
52
|
+
def note(self, fullname: str, clock: Callable[[], int] = reached) -> str | None:
|
|
53
|
+
"""Take note of a module looked up, naming it whenever the window has room."""
|
|
40
54
|
if fullname.split(".", maxsplit=1)[0] == OURS:
|
|
41
55
|
return None
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# name already held is counted
|
|
48
|
-
if len(self.seen) >= MAX_MODULES:
|
|
56
|
+
seen = self.seen
|
|
57
|
+
held = seen.get(fullname)
|
|
58
|
+
# the ceiling bounds how many distinct names a window holds, not how
|
|
59
|
+
# often a name already held is counted
|
|
60
|
+
if held is None and len(seen) >= MAX_MODULES:
|
|
49
61
|
self.suppressed += 1
|
|
50
62
|
return None
|
|
51
|
-
|
|
52
|
-
self.moved.add(fullname)
|
|
63
|
+
seen[fullname] = sighted(held, clock())
|
|
53
64
|
return fullname
|
|
54
65
|
|
|
55
66
|
def find_spec(
|
|
@@ -61,38 +72,53 @@ class ImportObserver:
|
|
|
61
72
|
"""Note the module being looked up, then defer to the finders behind."""
|
|
62
73
|
self.note(fullname)
|
|
63
74
|
|
|
75
|
+
def drain(self) -> dict[str, Sighting]:
|
|
76
|
+
"""Take the window whole, and what the last fold owed, leaving the next one empty."""
|
|
77
|
+
taken, self.seen = self.seen, {}
|
|
78
|
+
# into a map of the fold's own, so what a late sighting lands in the
|
|
79
|
+
# window taken is at most lost, and never written over by the fold
|
|
80
|
+
offered = self.owed
|
|
81
|
+
self.owed = {}
|
|
82
|
+
merged(offered, taken)
|
|
83
|
+
return offered
|
|
84
|
+
|
|
85
|
+
def restore(self, taken: Mapping[str, Sighting]) -> None:
|
|
86
|
+
"""Keep for the next fold what this one took and did not report, as far as there is room."""
|
|
87
|
+
merged(self.owed, taken)
|
|
88
|
+
self.unkept += trimmed(self.owed, MAX_MODULES)
|
|
89
|
+
|
|
64
90
|
def records(
|
|
65
91
|
self,
|
|
92
|
+
taken: Mapping[str, Sighting],
|
|
66
93
|
modules: dict[str, ModuleType],
|
|
67
94
|
installed: dict[str, Dist],
|
|
68
|
-
|
|
69
|
-
) -> list[Record]:
|
|
95
|
+
origin: int,
|
|
96
|
+
) -> tuple[list[Record], dict[str, Sighting]]:
|
|
70
97
|
"""
|
|
71
98
|
Name the distribution behind every module that finished importing.
|
|
72
99
|
|
|
73
100
|
A lookup is only an attempt: an import that raised never reaches
|
|
74
101
|
``modules``, and reporting it would claim a package loaded when it did
|
|
75
102
|
not. Intersecting the two is what makes this evidence rather than
|
|
76
|
-
intent.
|
|
103
|
+
intent. What has not landed yet is what the next window keeps.
|
|
77
104
|
|
|
78
105
|
Both maps are taken by the caller so this stays a pure fold, and so the
|
|
79
106
|
distribution scan happens once per report instead of once per module.
|
|
80
107
|
"""
|
|
81
108
|
found: list[Record] = []
|
|
109
|
+
kept: dict[str, Sighting] = {}
|
|
82
110
|
scanned = bool(installed)
|
|
83
|
-
for name in sorted(
|
|
84
|
-
|
|
85
|
-
if times <= carried.get(keyed(Evidence.IMPORTED, name), 0):
|
|
86
|
-
self.moved.discard(name)
|
|
87
|
-
continue
|
|
111
|
+
for name in sorted(taken):
|
|
112
|
+
held = taken[name]
|
|
88
113
|
module = modules.get(name)
|
|
89
114
|
if module is None:
|
|
90
|
-
# kept, because a lookup mid-import finishes after this
|
|
115
|
+
# kept, because a lookup mid-import finishes after this fold
|
|
116
|
+
kept[name] = held
|
|
91
117
|
continue
|
|
92
118
|
dist = distribution_of(name, installed)
|
|
93
119
|
if dist is None:
|
|
94
|
-
if scanned:
|
|
95
|
-
|
|
120
|
+
if not scanned:
|
|
121
|
+
kept[name] = held
|
|
96
122
|
continue
|
|
97
123
|
found.append(
|
|
98
124
|
Record(
|
|
@@ -103,10 +129,12 @@ class ImportObserver:
|
|
|
103
129
|
symbol=name,
|
|
104
130
|
granularity=granularity_of(module),
|
|
105
131
|
evidence=Evidence.IMPORTED,
|
|
106
|
-
frequency=times,
|
|
132
|
+
frequency=held.times,
|
|
133
|
+
when=held.first,
|
|
134
|
+
used=dated(origin, held.last),
|
|
107
135
|
),
|
|
108
136
|
)
|
|
109
|
-
return found
|
|
137
|
+
return found, kept
|
|
110
138
|
|
|
111
139
|
|
|
112
140
|
def granularity_of(module: ModuleType) -> Granularity:
|
|
@@ -7,9 +7,11 @@ a signal about the wire rather than about us.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import re
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Mapping
|
|
10
12
|
from dataclasses import dataclass
|
|
11
13
|
from enum import Enum
|
|
12
|
-
from typing import Final
|
|
14
|
+
from typing import Final, NamedTuple
|
|
13
15
|
|
|
14
16
|
REPORT_TAG: Final = "watches-exec/1"
|
|
15
17
|
ABSENT: Final = "-"
|
|
@@ -35,6 +37,15 @@ USED: Final = "u"
|
|
|
35
37
|
MAX_WHEN: Final = 3_153_600_000_000
|
|
36
38
|
MAX_USED: Final = 4_102_444_800_000
|
|
37
39
|
|
|
40
|
+
MS: Final = 1_000_000
|
|
41
|
+
SECOND: Final = 1000
|
|
42
|
+
|
|
43
|
+
START: Final = time.monotonic_ns()
|
|
44
|
+
|
|
45
|
+
# the reader reads u= as milliseconds since the epoch. Taken once and advanced
|
|
46
|
+
# by the monotonic clock, so a stepped clock cannot walk a stamp backwards
|
|
47
|
+
ORIGIN: Final = time.time_ns() // MS
|
|
48
|
+
|
|
38
49
|
# a kind outside the three rungs: the six fixed columns describe a symbol, and
|
|
39
50
|
# this describes the window the symbols were seen in
|
|
40
51
|
WINDOW: Final = "s"
|
|
@@ -162,9 +173,52 @@ class Report:
|
|
|
162
173
|
held: tuple[Record, ...] = ()
|
|
163
174
|
|
|
164
175
|
|
|
165
|
-
def
|
|
166
|
-
"""
|
|
167
|
-
return (
|
|
176
|
+
def reached() -> int:
|
|
177
|
+
"""Say how far into the probe's own life this moment is, in milliseconds."""
|
|
178
|
+
return (time.monotonic_ns() - START) // MS
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def dated(origin: int, at: int) -> int:
|
|
182
|
+
"""Date a moment of the probe's life by the epoch, to the second."""
|
|
183
|
+
# what drains a report holds its own clock to whole seconds and drops any
|
|
184
|
+
# date past it, so a finer one here is one it can only throw away
|
|
185
|
+
return (origin + at) // SECOND * SECOND
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class Sighting(NamedTuple):
|
|
189
|
+
"""What a window holds of one symbol: how often, and when it was first and last seen."""
|
|
190
|
+
|
|
191
|
+
times: int
|
|
192
|
+
first: int
|
|
193
|
+
last: int
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def sighted(held: Sighting | None, now: int) -> Sighting:
|
|
197
|
+
"""Fold one more sighting into what the window holds of its symbol."""
|
|
198
|
+
return Sighting(1, now, now) if held is None else Sighting(held.times + 1, held.first, now)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def merged(into: dict[str, Sighting], taken: Mapping[str, Sighting]) -> None:
|
|
202
|
+
"""Fold what one window holds into another."""
|
|
203
|
+
# a copy of the items, taken in one step: the window taken may still be
|
|
204
|
+
# written by a thread that bound it before the fold swapped it out
|
|
205
|
+
for symbol, held in list(taken.items()):
|
|
206
|
+
kept = into.get(symbol)
|
|
207
|
+
into[symbol] = (
|
|
208
|
+
held
|
|
209
|
+
if kept is None
|
|
210
|
+
else Sighting(
|
|
211
|
+
kept.times + held.times,
|
|
212
|
+
min(kept.first, held.first),
|
|
213
|
+
max(kept.last, held.last),
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def trimmed(held: dict[str, Sighting], ceiling: int) -> int:
|
|
219
|
+
"""Drop what a window holds past its ceiling, and say how many sightings that was."""
|
|
220
|
+
beyond = list(held)[ceiling:]
|
|
221
|
+
return sum(held.pop(symbol).times for symbol in beyond)
|
|
168
222
|
|
|
169
223
|
|
|
170
224
|
def _writable(value: str, limit: int) -> bool:
|
|
@@ -5,9 +5,8 @@ import atexit
|
|
|
5
5
|
import contextlib
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
|
-
import time
|
|
9
8
|
from collections.abc import Callable
|
|
10
|
-
from dataclasses import dataclass, field
|
|
9
|
+
from dataclasses import dataclass, field
|
|
11
10
|
from types import CodeType, ModuleType
|
|
12
11
|
from typing import TYPE_CHECKING, Final
|
|
13
12
|
|
|
@@ -15,7 +14,15 @@ from fluidattacks_agent.distributions import Dist, distribution_map
|
|
|
15
14
|
from fluidattacks_agent.executions import TOOL, TOOL_NAME, RunObserver, alone, available
|
|
16
15
|
from fluidattacks_agent.loads import LoadObserver
|
|
17
16
|
from fluidattacks_agent.observer import ImportObserver, install
|
|
18
|
-
from fluidattacks_agent.report import
|
|
17
|
+
from fluidattacks_agent.report import (
|
|
18
|
+
ORIGIN,
|
|
19
|
+
Evidence,
|
|
20
|
+
Record,
|
|
21
|
+
Sighting,
|
|
22
|
+
Window,
|
|
23
|
+
reached,
|
|
24
|
+
render,
|
|
25
|
+
)
|
|
19
26
|
from fluidattacks_agent.settings import Delivery, delivery
|
|
20
27
|
from fluidattacks_agent.sink import PARTING, Sink, Stalled
|
|
21
28
|
from fluidattacks_agent.switch import switched_on
|
|
@@ -30,24 +37,10 @@ PTH_NAME: Final = "fluidattacks_agent.pth"
|
|
|
30
37
|
# reading the switch and for nothing above it
|
|
31
38
|
PTH_LINE: Final = "import fluidattacks_agent.gate\n"
|
|
32
39
|
|
|
33
|
-
MS: Final = 1_000_000
|
|
34
|
-
SECOND: Final = 1000
|
|
35
|
-
|
|
36
40
|
# what a workload's fork may be delayed by while a fold under way ends: a fold
|
|
37
41
|
# imports, and a lock held across a fork is held in the child forever
|
|
38
42
|
FORKING: Final = 0.5
|
|
39
43
|
|
|
40
|
-
START: Final = time.monotonic_ns()
|
|
41
|
-
|
|
42
|
-
# the reader reads u= as milliseconds since the epoch. Taken once and advanced
|
|
43
|
-
# by the monotonic clock, so a stepped clock cannot walk a stamp backwards
|
|
44
|
-
ORIGIN: Final = time.time_ns() // MS
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def reached() -> int:
|
|
48
|
-
"""Say how far into the probe's own life this moment is, in milliseconds."""
|
|
49
|
-
return (time.monotonic_ns() - START) // MS
|
|
50
|
-
|
|
51
44
|
|
|
52
45
|
@dataclass
|
|
53
46
|
class Probe:
|
|
@@ -60,12 +53,10 @@ class Probe:
|
|
|
60
53
|
distributions: Callable[[], dict[str, Dist]] = distribution_map
|
|
61
54
|
clock: Callable[[], int] = reached
|
|
62
55
|
origin: int = ORIGIN
|
|
63
|
-
# what each
|
|
64
|
-
|
|
65
|
-
# the
|
|
56
|
+
# what the sightings each observer found no room for were last stated as, so
|
|
57
|
+
# a report carries the increment. The sightings themselves are never held
|
|
58
|
+
# past the window that saw them: the reader adds the windows up
|
|
66
59
|
stated: dict[str, int] = field(default_factory=dict)
|
|
67
|
-
first: dict[tuple[str, str], int] = field(default_factory=dict)
|
|
68
|
-
last: dict[tuple[str, str], int] = field(default_factory=dict)
|
|
69
60
|
scanned: dict[str, Dist] | None = None
|
|
70
61
|
# set while a fold runs, with the thread running it: that thread's own
|
|
71
62
|
# reads and imports are the probe's, and every other thread's are the
|
|
@@ -108,34 +99,21 @@ class Probe:
|
|
|
108
99
|
def _looked(self, fullname: str) -> None:
|
|
109
100
|
if self._folding():
|
|
110
101
|
return
|
|
111
|
-
|
|
112
|
-
if module is not None:
|
|
113
|
-
self._mark(Evidence.IMPORTED, module)
|
|
102
|
+
self.observer.note(fullname, self.clock)
|
|
114
103
|
|
|
115
104
|
def read(self, path: str) -> None:
|
|
116
105
|
"""Note a file the workload opened."""
|
|
117
106
|
if self._folding():
|
|
118
107
|
return
|
|
119
|
-
|
|
120
|
-
#
|
|
121
|
-
|
|
122
|
-
if package is not None:
|
|
123
|
-
self._mark(Evidence.READ, package)
|
|
108
|
+
# the hook runs on every open the host makes, and all but a few are ours
|
|
109
|
+
# to stamp, so the clock is read inside and only for those
|
|
110
|
+
self.loads.note(path, self.clock)
|
|
124
111
|
|
|
125
112
|
def ran(self, filename: str, qualname: str) -> None:
|
|
126
113
|
"""Note code starting."""
|
|
127
114
|
if self._folding():
|
|
128
115
|
return
|
|
129
|
-
|
|
130
|
-
if symbol is not None:
|
|
131
|
-
self._mark(Evidence.EXECUTED, symbol)
|
|
132
|
-
|
|
133
|
-
def _mark(self, evidence: Evidence, symbol: str) -> None:
|
|
134
|
-
"""Stamp a sighting: when its symbol was first reached, and this once."""
|
|
135
|
-
key = keyed(evidence, symbol)
|
|
136
|
-
seen = self.clock()
|
|
137
|
-
self.last[key] = seen
|
|
138
|
-
self.first.setdefault(key, seen)
|
|
116
|
+
self.runs.note(filename, qualname, self.clock)
|
|
139
117
|
|
|
140
118
|
def flush(self) -> None:
|
|
141
119
|
"""Write what has not been reported, waiting out a fold under way."""
|
|
@@ -188,15 +166,16 @@ class Probe:
|
|
|
188
166
|
|
|
189
167
|
def _sighted(self) -> bool:
|
|
190
168
|
"""Say whether this window holds anything a report could carry."""
|
|
191
|
-
#
|
|
192
|
-
# a copy of it, since the workload's threads add to it meanwhile
|
|
169
|
+
# a copy of the lookups, since the workload's threads add to them meanwhile
|
|
193
170
|
return bool(
|
|
194
171
|
self._owed()
|
|
195
|
-
or self.loads.
|
|
196
|
-
or self.
|
|
197
|
-
|
|
172
|
+
or self.loads.seen
|
|
173
|
+
or self.loads.owed
|
|
174
|
+
or self.runs.seen
|
|
175
|
+
or self.runs.owed
|
|
176
|
+
# a lookup that never became a module is kept when the records are
|
|
198
177
|
# folded, so on its own it is not cause to walk any metadata
|
|
199
|
-
or any(name in sys.modules for name in list(self.observer.
|
|
178
|
+
or any(name in sys.modules for name in [*list(self.observer.seen), *self.observer.owed])
|
|
200
179
|
or any(self._beyond().values())
|
|
201
180
|
or self.stalled != self.charged,
|
|
202
181
|
)
|
|
@@ -227,16 +206,66 @@ class Probe:
|
|
|
227
206
|
return scanned
|
|
228
207
|
|
|
229
208
|
def _write_fresh(self, installed: dict[str, Dist]) -> None:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
209
|
+
# every window is taken whole before any is read, so a sighting the
|
|
210
|
+
# workload makes meanwhile lands in the next one and in nothing older
|
|
211
|
+
taken = {
|
|
212
|
+
Evidence.READ: self.loads.drain(),
|
|
213
|
+
Evidence.IMPORTED: self.observer.drain(),
|
|
214
|
+
Evidence.EXECUTED: self.runs.drain(),
|
|
215
|
+
}
|
|
216
|
+
try:
|
|
217
|
+
read, keep_read = self.loads.records(
|
|
218
|
+
taken[Evidence.READ],
|
|
219
|
+
sys.modules,
|
|
220
|
+
installed,
|
|
221
|
+
self.origin,
|
|
222
|
+
)
|
|
223
|
+
imported, keep_imported = self.observer.records(
|
|
224
|
+
taken[Evidence.IMPORTED],
|
|
225
|
+
sys.modules,
|
|
226
|
+
installed,
|
|
227
|
+
self.origin,
|
|
228
|
+
)
|
|
229
|
+
ran, keep_ran = self.runs.records(taken[Evidence.EXECUTED], installed, self.origin)
|
|
230
|
+
except Exception:
|
|
231
|
+
# nothing taken is lost to a fold that could not even read it
|
|
232
|
+
self._restore(taken)
|
|
233
|
+
raise
|
|
234
|
+
# given back at once: the next window keeps these whatever this fold's fate
|
|
235
|
+
self._restore(
|
|
236
|
+
{
|
|
237
|
+
Evidence.READ: keep_read,
|
|
238
|
+
Evidence.IMPORTED: keep_imported,
|
|
239
|
+
Evidence.EXECUTED: keep_ran,
|
|
240
|
+
},
|
|
241
|
+
)
|
|
242
|
+
fresh = [*read, *imported, *ran]
|
|
236
243
|
counted = self._beyond()
|
|
237
244
|
beyond = {tag: n - self.stated.get(tag, 0) for tag, n in counted.items()}
|
|
238
245
|
if not fresh and not any(beyond.values()) and not self._owed():
|
|
239
246
|
return
|
|
247
|
+
# what this fold offers and has not landed yet, by rung: a record a
|
|
248
|
+
# report never carried goes back to its window rather than being lost
|
|
249
|
+
offered = {
|
|
250
|
+
evidence: {record.symbol: taken[evidence][record.symbol] for record in records}
|
|
251
|
+
for evidence, records in (
|
|
252
|
+
(Evidence.READ, read),
|
|
253
|
+
(Evidence.IMPORTED, imported),
|
|
254
|
+
(Evidence.EXECUTED, ran),
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
try:
|
|
258
|
+
self._land(fresh, beyond, counted, offered)
|
|
259
|
+
finally:
|
|
260
|
+
self._restore(offered)
|
|
261
|
+
|
|
262
|
+
def _land(
|
|
263
|
+
self,
|
|
264
|
+
fresh: list[Record],
|
|
265
|
+
beyond: dict[str, int],
|
|
266
|
+
counted: dict[str, int],
|
|
267
|
+
offered: dict[Evidence, dict[str, Sighting]],
|
|
268
|
+
) -> None:
|
|
240
269
|
# the reader sums what every report states, so this is the increment
|
|
241
270
|
stalled = self.stalled
|
|
242
271
|
window: Window | None = self._window(beyond, stalled - self.charged)
|
|
@@ -244,7 +273,9 @@ class Probe:
|
|
|
244
273
|
while True:
|
|
245
274
|
report = render(pending, window, self.refused, self.undated)
|
|
246
275
|
self._write(report.text)
|
|
247
|
-
|
|
276
|
+
# landed, or dropped and counted: neither is the window's any more
|
|
277
|
+
for record in pending[: len(pending) - len(report.held)]:
|
|
278
|
+
offered[record.evidence].pop(record.symbol, None)
|
|
248
279
|
held = list(report.held)
|
|
249
280
|
self.refused = report.dropped if held else 0
|
|
250
281
|
self.undated = report.unstamped if held else 0
|
|
@@ -258,16 +289,17 @@ class Probe:
|
|
|
258
289
|
return
|
|
259
290
|
pending = held
|
|
260
291
|
|
|
261
|
-
def
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
292
|
+
def _restore(self, kept: dict[Evidence, dict[str, Sighting]]) -> None:
|
|
293
|
+
self.loads.restore(kept[Evidence.READ])
|
|
294
|
+
self.observer.restore(kept[Evidence.IMPORTED])
|
|
295
|
+
self.runs.restore(kept[Evidence.EXECUTED])
|
|
265
296
|
|
|
266
297
|
def _beyond(self) -> dict[str, int]:
|
|
298
|
+
# what a window had no room for, and what a fold had no room to keep
|
|
267
299
|
return {
|
|
268
|
-
Evidence.READ.value: self.loads.suppressed,
|
|
269
|
-
Evidence.IMPORTED.value: self.observer.suppressed,
|
|
270
|
-
Evidence.EXECUTED.value: self.runs.suppressed,
|
|
300
|
+
Evidence.READ.value: self.loads.suppressed + self.loads.unkept,
|
|
301
|
+
Evidence.IMPORTED.value: self.observer.suppressed + self.observer.unkept,
|
|
302
|
+
Evidence.EXECUTED.value: self.runs.suppressed + self.runs.unkept,
|
|
271
303
|
}
|
|
272
304
|
|
|
273
305
|
def _window(self, beyond: dict[str, int], backlog: int) -> Window:
|
|
@@ -280,30 +312,6 @@ class Probe:
|
|
|
280
312
|
alone=self.monitoring and self.solitary,
|
|
281
313
|
)
|
|
282
314
|
|
|
283
|
-
def _since(self, record: Record) -> Record | None:
|
|
284
|
-
# the reader adds up what every report of a record says, so a report
|
|
285
|
-
# carries what has happened since the last one and never the running
|
|
286
|
-
# total, which would count each sighting once per flush that follows it
|
|
287
|
-
key = _key(record)
|
|
288
|
-
since = record.frequency - self.reported.get(key, 0)
|
|
289
|
-
if since < 1:
|
|
290
|
-
return None
|
|
291
|
-
return replace(
|
|
292
|
-
record,
|
|
293
|
-
frequency=since,
|
|
294
|
-
when=self.first.get(key),
|
|
295
|
-
used=self._used(key),
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
def _used(self, key: tuple[str, str]) -> int | None:
|
|
299
|
-
"""Date the last sighting of a symbol as of this report, to the second."""
|
|
300
|
-
seen = self.last.get(key)
|
|
301
|
-
if seen is None:
|
|
302
|
-
return None
|
|
303
|
-
# what drains a report holds its own clock to whole seconds and drops
|
|
304
|
-
# any date past it, so a finer one here is one it can only throw away
|
|
305
|
-
return (self.origin + seen) // SECOND * SECOND
|
|
306
|
-
|
|
307
315
|
def _write(self, text: str) -> None:
|
|
308
316
|
try:
|
|
309
317
|
self.sink.write(text)
|
|
@@ -314,10 +322,6 @@ class Probe:
|
|
|
314
322
|
raise
|
|
315
323
|
|
|
316
324
|
|
|
317
|
-
def _key(record: Record) -> tuple[str, str]:
|
|
318
|
-
return keyed(record.evidence, record.symbol)
|
|
319
|
-
|
|
320
|
-
|
|
321
325
|
def site_dirs() -> tuple[str, ...]:
|
|
322
326
|
"""Name the directories the workload installs its distributions into."""
|
|
323
327
|
found: list[str] = []
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
"""Watch code inside a package start running, where the interpreter allows it."""
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
from collections.abc import Mapping
|
|
5
|
-
from dataclasses import dataclass, field
|
|
6
|
-
from pathlib import PurePath
|
|
7
|
-
from typing import Final
|
|
8
|
-
|
|
9
|
-
from fluidattacks_agent.distributions import Dist, distribution_of
|
|
10
|
-
from fluidattacks_agent.report import Ecosystem, Evidence, Granularity, Record, keyed
|
|
11
|
-
|
|
12
|
-
# 3.15 makes the import below lazy from this; every version we support
|
|
13
|
-
# ignores it, so nothing changes until the floor moves
|
|
14
|
-
__lazy_modules__ = ["pathlib"]
|
|
15
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
OURS: Final = "fluidattacks_agent"
|
|
21
|
-
|
|
22
|
-
SOURCE: Final = ".py"
|
|
23
|
-
|
|
24
|
-
INITIALIZER: Final = "__init__"
|
|
25
|
-
PARENT: Final = ".."
|
|
26
|
-
|
|
27
|
-
TOOL: Final = 3
|
|
28
|
-
TOOL_NAME: Final = "fluidattacks-agent"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
TOOLS: Final = 6
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def available() -> bool:
|
|
35
|
-
"""Say whether this interpreter can report code starting at all."""
|
|
36
|
-
return hasattr(sys, "monitoring")
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def alone(tool: int = TOOL) -> bool:
|
|
40
|
-
"""Say whether this is the only tool watching: restart_events takes none."""
|
|
41
|
-
held = (sys.monitoring.get_tool(other) for other in range(TOOLS) if other != tool)
|
|
42
|
-
return not any(name is not None for name in held)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def module_of(filename: str, sites: tuple[str, ...]) -> str | None:
|
|
46
|
-
"""Name the module whose source a running code object came from."""
|
|
47
|
-
for site in sites:
|
|
48
|
-
prefix = site + "/"
|
|
49
|
-
if not filename.startswith(prefix) or not filename.endswith(SOURCE):
|
|
50
|
-
continue
|
|
51
|
-
held = PurePath(filename[len(prefix) :])
|
|
52
|
-
parts = list(held.parts)
|
|
53
|
-
if not parts or held.is_absolute() or PARENT in parts:
|
|
54
|
-
return None
|
|
55
|
-
stem = parts.pop()[: -len(SOURCE)]
|
|
56
|
-
if not stem:
|
|
57
|
-
return None
|
|
58
|
-
if stem != INITIALIZER:
|
|
59
|
-
parts.append(stem)
|
|
60
|
-
return ".".join(parts) or None
|
|
61
|
-
return None
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@dataclass
|
|
65
|
-
class RunObserver:
|
|
66
|
-
"""Functions that started, bounded the way the reader's own map is."""
|
|
67
|
-
|
|
68
|
-
sites: tuple[str, ...] = ()
|
|
69
|
-
seen: dict[str, int] = field(default_factory=dict)
|
|
70
|
-
moved: set[str] = field(default_factory=set)
|
|
71
|
-
suppressed: int = 0
|
|
72
|
-
|
|
73
|
-
def note(self, filename: str, qualname: str) -> str | None:
|
|
74
|
-
"""Take note of code starting, naming the function whenever there is room."""
|
|
75
|
-
module = module_of(filename, self.sites)
|
|
76
|
-
if module is None:
|
|
77
|
-
return None
|
|
78
|
-
if module.split(".", maxsplit=1)[0] == OURS:
|
|
79
|
-
return None
|
|
80
|
-
qualified = f"{module}.{qualname}"
|
|
81
|
-
if qualified in self.seen:
|
|
82
|
-
self.seen[qualified] += 1
|
|
83
|
-
self.moved.add(qualified)
|
|
84
|
-
return qualified
|
|
85
|
-
if len(self.seen) >= MAX_FUNCTIONS:
|
|
86
|
-
self.suppressed += 1
|
|
87
|
-
return None
|
|
88
|
-
self.seen[qualified] = 1
|
|
89
|
-
self.moved.add(qualified)
|
|
90
|
-
return qualified
|
|
91
|
-
|
|
92
|
-
def records(
|
|
93
|
-
self,
|
|
94
|
-
installed: dict[str, Dist],
|
|
95
|
-
carried: Mapping[tuple[str, str], int],
|
|
96
|
-
) -> list[Record]:
|
|
97
|
-
"""Report every function whose package a distribution installed."""
|
|
98
|
-
found: list[Record] = []
|
|
99
|
-
scanned = bool(installed)
|
|
100
|
-
for qualified in sorted(self.moved):
|
|
101
|
-
times = self.seen[qualified]
|
|
102
|
-
# what a report carried comes back only if note sees it again,
|
|
103
|
-
# and no distribution will own what a settled scan does not
|
|
104
|
-
if times <= carried.get(keyed(Evidence.EXECUTED, qualified), 0):
|
|
105
|
-
self.moved.discard(qualified)
|
|
106
|
-
continue
|
|
107
|
-
dist = distribution_of(qualified, installed)
|
|
108
|
-
if dist is None:
|
|
109
|
-
if scanned:
|
|
110
|
-
self.moved.discard(qualified)
|
|
111
|
-
continue
|
|
112
|
-
found.append(
|
|
113
|
-
Record(
|
|
114
|
-
ecosystem=Ecosystem.PYPI,
|
|
115
|
-
name=dist.name,
|
|
116
|
-
version=dist.version,
|
|
117
|
-
confidence=dist.confidence,
|
|
118
|
-
symbol=qualified,
|
|
119
|
-
granularity=Granularity.FUNCTION,
|
|
120
|
-
evidence=Evidence.EXECUTED,
|
|
121
|
-
frequency=times,
|
|
122
|
-
),
|
|
123
|
-
)
|
|
124
|
-
return found
|
|
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
|