rnmon 0.1.0__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.
- rnmon/Databases.py +37 -0
- rnmon/Logging.py +6 -0
- rnmon/MP.py +5 -0
- rnmon/RNSUtils.py +77 -0
- rnmon/Remotes.py +138 -0
- rnmon/__init__.py +0 -0
- rnmon/__main__.py +3 -0
- rnmon/rnmon.py +84 -0
- rnmon-0.1.0.dist-info/METADATA +28 -0
- rnmon-0.1.0.dist-info/RECORD +12 -0
- rnmon-0.1.0.dist-info/WHEEL +4 -0
- rnmon-0.1.0.dist-info/entry_points.txt +2 -0
rnmon/Databases.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import requests
|
|
3
|
+
from random import randrange
|
|
4
|
+
from collections import deque
|
|
5
|
+
|
|
6
|
+
import RNS
|
|
7
|
+
from . import MP
|
|
8
|
+
|
|
9
|
+
class InfluxWriter:
|
|
10
|
+
def __init__(self, address: str, batch_size: int = 1000, flush_interval: int = 5, **kwargs):
|
|
11
|
+
self.maxlen = batch_size
|
|
12
|
+
self.flush_interval = flush_interval
|
|
13
|
+
self.flush_jitter = kwargs.setdefault('flush_jitter', 0)
|
|
14
|
+
self.address = address
|
|
15
|
+
self.http_headers = kwargs.setdefault('http_headers', None)
|
|
16
|
+
self.run()
|
|
17
|
+
|
|
18
|
+
def run(self):
|
|
19
|
+
last_push = time.time()
|
|
20
|
+
jitter = 0
|
|
21
|
+
RNS.log("[RNMon] Started InfluxWriter", RNS.LOG_INFO)
|
|
22
|
+
while not MP.terminate.is_set():
|
|
23
|
+
if len(MP.metric_queue) >= self.maxlen or (time.time() - last_push) > (self.flush_interval + jitter):
|
|
24
|
+
data = []
|
|
25
|
+
try:
|
|
26
|
+
for _ in range(self.maxlen):
|
|
27
|
+
data.append(MP.metric_queue.pop())
|
|
28
|
+
except IndexError:
|
|
29
|
+
pass
|
|
30
|
+
if data:
|
|
31
|
+
RNS.log(f"[RNMon] Pushing metrics - Count: {len(data)} Time: {int(time.time() - last_push)}s", RNS.LOG_DEBUG)
|
|
32
|
+
r = requests.post(self.address, headers=self.http_headers, data="\n".join(data))
|
|
33
|
+
last_push = time.time()
|
|
34
|
+
jitter = randrange(-self.flush_jitter, self.flush_jitter+1)
|
|
35
|
+
time.sleep(0.2)
|
|
36
|
+
|
|
37
|
+
RNS.log(f"[RNMon] Stopped InfluxWriter", RNS.LOG_INFO)
|
rnmon/Logging.py
ADDED
rnmon/MP.py
ADDED
rnmon/RNSUtils.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
import RNS
|
|
5
|
+
from . import MP
|
|
6
|
+
|
|
7
|
+
path_request_timeout = 30
|
|
8
|
+
link_est_timeout = 30
|
|
9
|
+
|
|
10
|
+
def establish_link(dest_identity: str, rpc_identity: os.PathLike, dest_type) -> RNS.Link:
|
|
11
|
+
mgmt_identity = read_rpc_identity(rpc_identity)
|
|
12
|
+
dest_hash = RNS.Destination.hash_from_name_and_identity( \
|
|
13
|
+
'.'.join(dest_type.ASPECTS), bytes.fromhex(dest_identity))
|
|
14
|
+
ensure_path(dest_hash)
|
|
15
|
+
RNS.log(f"[RNMon] Setting up Destination: {RNS.prettyhexrep(dest_hash)}", RNS.LOG_INFO)
|
|
16
|
+
remote_dest = RNS.Destination(
|
|
17
|
+
RNS.Identity.recall(dest_hash),
|
|
18
|
+
RNS.Destination.OUT,
|
|
19
|
+
RNS.Destination.SINGLE,
|
|
20
|
+
*dest_type.ASPECTS
|
|
21
|
+
)
|
|
22
|
+
RNS.log(f"[RNMon] Establishing a new link with {RNS.prettyhexrep(dest_hash)}", RNS.LOG_DEBUG)
|
|
23
|
+
link = RNS.Link(remote_dest)
|
|
24
|
+
link.set_link_established_callback(on_link_established(link, mgmt_identity))
|
|
25
|
+
link.set_link_closed_callback(on_link_closed)
|
|
26
|
+
|
|
27
|
+
start = time.time()
|
|
28
|
+
while link.status != RNS.Link.ACTIVE:
|
|
29
|
+
# Sometimes with multiple clients in a shared instance, it is possible that
|
|
30
|
+
# the establishment timeout is reset to PATHFINDER_M due to some edge cases
|
|
31
|
+
if time.time() - start > link_est_timeout:
|
|
32
|
+
RNS.log("[RNMon] Timed out waiting for link establishment", RNS.LOG_ERROR)
|
|
33
|
+
raise RuntimeError
|
|
34
|
+
RNS.log(f"[RNMon] Link to {dest_identity} Status: {link.status}", RNS.LOG_DEBUG)
|
|
35
|
+
time.sleep(1)
|
|
36
|
+
link.identify(mgmt_identity)
|
|
37
|
+
return link
|
|
38
|
+
|
|
39
|
+
def on_link_established(link: RNS.Link, rpc_identity: RNS.Identity) -> None:
|
|
40
|
+
RNS.log("[RNMon] Link established with server", RNS.LOG_DEBUG)
|
|
41
|
+
RNS.log(f"[RNMon] KEEPALIVE interval: {link.KEEPALIVE}s, Stale time: {link.stale_time}s", RNS.LOG_DEBUG)
|
|
42
|
+
|
|
43
|
+
def on_link_closed(link: RNS.Link) -> None:
|
|
44
|
+
reason = link.teardown_reason
|
|
45
|
+
if reason == RNS.Link.TIMEOUT:
|
|
46
|
+
RNS.log("[RNMon] The link timed out", RNS.LOG_WARNING)
|
|
47
|
+
elif reason == RNS.Link.DESTINATION_CLOSED:
|
|
48
|
+
RNS.log("[RNMon] The link was closed by the server", RNS.LOG_WARNING)
|
|
49
|
+
elif reason == RNS.Link.INITIATOR_CLOSED:
|
|
50
|
+
RNS.log("[RNMon] Closing link", RNS.LOG_ERROR)
|
|
51
|
+
|
|
52
|
+
def ensure_path(dest_hash: bytes) -> None:
|
|
53
|
+
if not RNS.Transport.has_path(dest_hash):
|
|
54
|
+
RNS.log(f"[RNMon] No path to destination known. Requesting path and waiting for announce to arrive...")
|
|
55
|
+
RNS.Transport.request_path(dest_hash)
|
|
56
|
+
start = time.time()
|
|
57
|
+
while not RNS.Transport.has_path(dest_hash):
|
|
58
|
+
time.sleep(0.2)
|
|
59
|
+
# Abort if the path request is taking too long, for example if the next transport_node
|
|
60
|
+
# is not in a mode that does path discovery for us, it might take a while to discover the path
|
|
61
|
+
if time.time() - start > path_request_timeout:
|
|
62
|
+
RNS.log("[RNMon] Timed out waiting for path announcement", RNS.LOG_ERROR)
|
|
63
|
+
raise RuntimeError
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def read_rpc_identity(rpc_identity: os.PathLike) -> RNS.Identity:
|
|
67
|
+
mgmt_identity = RNS.Identity.from_file(os.path.expanduser(rpc_identity))
|
|
68
|
+
if not mgmt_identity:
|
|
69
|
+
raise FileNotFoundError(f"Failed to load identity from {rpc_identity}, check path and permissions.")
|
|
70
|
+
RNS.log(f"[RNMon] Loaded identity from '{rpc_identity}'", RNS.LOG_INFO)
|
|
71
|
+
return mgmt_identity
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def validate_hexhash(hexhash: str) -> None:
|
|
75
|
+
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
|
76
|
+
if len(hexhash) != dest_len:
|
|
77
|
+
raise TypeError(f"Destination length is invalid, must be {dest_len} hexadecimal characters ({dest_len//2} bytes)")
|
rnmon/Remotes.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from random import randrange
|
|
4
|
+
|
|
5
|
+
import RNS
|
|
6
|
+
from . import MP, RNSUtils
|
|
7
|
+
|
|
8
|
+
class RNSTransportNode:
|
|
9
|
+
ASPECTS = ("rnstransport", "remote", "management")
|
|
10
|
+
|
|
11
|
+
NODE_METRICS = {
|
|
12
|
+
"rxb": "rns_transport_node_rx_bytes_total",
|
|
13
|
+
"txb": "rns_transport_node_tx_bytes_total",
|
|
14
|
+
"transport_uptime": "rns_transport_node_uptime_s",
|
|
15
|
+
"link_count": "rns_transport_node_link_count", # returned as second array element, unlabelled...
|
|
16
|
+
}
|
|
17
|
+
NODE_LABELS = {
|
|
18
|
+
"transport_id": "transport_id",
|
|
19
|
+
}
|
|
20
|
+
IFACE_METRICS = {
|
|
21
|
+
"clients": "rns_iface_client_count",
|
|
22
|
+
"bitrate": "rns_iface_bitrate",
|
|
23
|
+
"status": "rns_iface_up",
|
|
24
|
+
"mode": "rns_iface_mode",
|
|
25
|
+
"rxb": "rns_iface_rx_bytes_total",
|
|
26
|
+
"txb": "rns_iface_tx_bytes_total",
|
|
27
|
+
"held_announces": "rns_iface_announces_held_count",
|
|
28
|
+
"announce_queue": "rns_iface_announces_queue_count",
|
|
29
|
+
"incoming_announce_frequency": "rns_iface_announces_rx_rate",
|
|
30
|
+
"outgoing_announce_frequency": "rns_iface_announces_tx_rate",
|
|
31
|
+
}
|
|
32
|
+
IFACE_LABELS = {
|
|
33
|
+
"type": "type",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
LPROTO_LABEL_TTABLE = str.maketrans({
|
|
37
|
+
" ": "\\ ",
|
|
38
|
+
",": "\\,"
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
def __init__(self, interval: int, dest_identity: str, rpc_identity: os.PathLike, name: str, **kwargs) -> None:
|
|
42
|
+
self.link = RNSUtils.establish_link(dest_identity, rpc_identity, self)
|
|
43
|
+
self.interval = interval
|
|
44
|
+
self.collection_jitter = kwargs.setdefault('collection_jitter', 0)
|
|
45
|
+
|
|
46
|
+
# Used for metric labeling
|
|
47
|
+
self.node_name = name
|
|
48
|
+
self.dest_identity = dest_identity
|
|
49
|
+
|
|
50
|
+
self.request_timeout = self.link.rtt * self.link.traffic_timeout_factor + RNS.Resource.RESPONSE_MAX_GRACE_TIME*1.125
|
|
51
|
+
if self.request_timeout >= interval:
|
|
52
|
+
self.request_timeout = interval
|
|
53
|
+
RNS.log(f"[RNMon] Set Request timeout for '{self.node_name}': {self.request_timeout}s", RNS.LOG_EXTREME)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
self.run()
|
|
58
|
+
|
|
59
|
+
def run(self) -> bool:
|
|
60
|
+
RNS.log(f"[RNMon] Starting RNSTransportNode scraper for '{self.node_name}'", RNS.LOG_INFO)
|
|
61
|
+
last_request_time = time.time()
|
|
62
|
+
jitter = 0
|
|
63
|
+
while not MP.terminate.is_set():
|
|
64
|
+
if self.link.status != RNS.Link.ACTIVE:
|
|
65
|
+
RNS.log(f"[RNMon] Link no longer active, stopping scraper for '{self.node_name}", RNS.LOG_DEBUG)
|
|
66
|
+
break
|
|
67
|
+
try:
|
|
68
|
+
# No point in spamming requests if the last one hasnt timed out yet, save local and network resources
|
|
69
|
+
if not self.link.pending_requests and ((time.time() - last_request_time) > (self.interval + jitter)):
|
|
70
|
+
req = self.link.request(
|
|
71
|
+
"/status",
|
|
72
|
+
data = [True],
|
|
73
|
+
response_callback = self._on_response,
|
|
74
|
+
failed_callback = self._on_request_fail,
|
|
75
|
+
timeout = self.request_timeout
|
|
76
|
+
)
|
|
77
|
+
last_request_time = time.time()
|
|
78
|
+
jitter = randrange(-self.collection_jitter, self.collection_jitter+1)
|
|
79
|
+
RNS.log(f"[RNMon] Sending request {RNS.prettyhexrep(req.request_id)} to '{self.node_name}'", RNS.LOG_EXTREME)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
RNS.log(f"[RNMon] Error while sending request to '{self.node_name}': {str(e)}")
|
|
82
|
+
|
|
83
|
+
time.sleep(0.2)
|
|
84
|
+
|
|
85
|
+
RNS.log(f"[RNMon] Stopping RNSTransportNode scraper for '{self.node_name}'", RNS.LOG_INFO)
|
|
86
|
+
self.link.teardown()
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def _on_response(self, response) -> None:
|
|
90
|
+
self._parse_metrics(response.response)
|
|
91
|
+
|
|
92
|
+
def _on_request_fail(self, response) -> None:
|
|
93
|
+
RNS.log(f"[RNMon] The request {RNS.prettyhexrep(response.request_id)} to '{self.node_name}' failed.", RNS.LOG_DEBUG)
|
|
94
|
+
|
|
95
|
+
def _parse_metrics(self, data: list) -> None:
|
|
96
|
+
iface_labels = {}
|
|
97
|
+
iface_metrics = {}
|
|
98
|
+
node_labels = {}
|
|
99
|
+
node_metrics = {}
|
|
100
|
+
t = time.time_ns()
|
|
101
|
+
|
|
102
|
+
# link_count isnt labeled >.>
|
|
103
|
+
node_metrics[RNSTransportNode.NODE_METRICS['link_count']] = data[1]
|
|
104
|
+
|
|
105
|
+
for mk, mv in data[0].items():
|
|
106
|
+
if mk == 'interfaces':
|
|
107
|
+
for iface in mv:
|
|
108
|
+
if iface['short_name'].startswith('Client on'):
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
for k, v in iface.items():
|
|
112
|
+
if k in RNSTransportNode.IFACE_METRICS:
|
|
113
|
+
iface_metrics[RNSTransportNode.IFACE_METRICS[k]] = v
|
|
114
|
+
if k in RNSTransportNode.IFACE_LABELS:
|
|
115
|
+
iface_labels[RNSTransportNode.IFACE_LABELS[k]] = v
|
|
116
|
+
|
|
117
|
+
iface_labels['name'] = iface['short_name']
|
|
118
|
+
iface_labels['identity'] = self.dest_identity
|
|
119
|
+
iface_labels['node_name'] = self.node_name
|
|
120
|
+
|
|
121
|
+
# convert to influx line format
|
|
122
|
+
labels = ",".join(f"{k}={v.translate(RNSTransportNode.LPROTO_LABEL_TTABLE)}" for k, v in iface_labels.items())
|
|
123
|
+
for k, v in iface_metrics.items():
|
|
124
|
+
metric = f"{k},{labels} value={v} {t}"
|
|
125
|
+
MP.metric_queue.append(metric)
|
|
126
|
+
|
|
127
|
+
else:
|
|
128
|
+
if mk in RNSTransportNode.NODE_METRICS:
|
|
129
|
+
node_metrics[RNSTransportNode.NODE_METRICS[mk]] = mv
|
|
130
|
+
|
|
131
|
+
node_labels['identity'] = self.dest_identity
|
|
132
|
+
node_labels['node_name'] = self.node_name
|
|
133
|
+
|
|
134
|
+
#convert to influx line format
|
|
135
|
+
labels = ",".join(f"{k}={v.translate(RNSTransportNode.LPROTO_LABEL_TTABLE)}" for k, v in node_labels.items())
|
|
136
|
+
for k, v in node_metrics.items():
|
|
137
|
+
metric = f"{k},{labels} value={v} {t}"
|
|
138
|
+
MP.metric_queue.append(metric)
|
rnmon/__init__.py
ADDED
|
File without changes
|
rnmon/__main__.py
ADDED
rnmon/rnmon.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
import signal
|
|
4
|
+
import argparse
|
|
5
|
+
import concurrent.futures
|
|
6
|
+
|
|
7
|
+
from yaml import safe_load
|
|
8
|
+
|
|
9
|
+
import RNS
|
|
10
|
+
# RNS.Link.KEEPALIVE=10
|
|
11
|
+
# RNS.Link.STALE_TIME=2*RNS.Link.KEEPALIVE
|
|
12
|
+
# TODO: PR to have these be configurable as arguments to RNS.Link.__init__
|
|
13
|
+
|
|
14
|
+
from .Databases import InfluxWriter
|
|
15
|
+
from .Remotes import RNSTransportNode
|
|
16
|
+
from . import MP, RNSUtils
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main():
|
|
21
|
+
parser = argparse.ArgumentParser(description="Simple request/response example")
|
|
22
|
+
parser.add_argument('-v', '--verbose', action='count', default=0)
|
|
23
|
+
parser.add_argument("--rns-config", type=str, default=None, \
|
|
24
|
+
help="path to Reticulum config directory")
|
|
25
|
+
parser.add_argument("config", nargs='?', type=argparse.FileType('r'), default="scraping.yaml", \
|
|
26
|
+
help="path to target list file")
|
|
27
|
+
args = parser.parse_args()
|
|
28
|
+
|
|
29
|
+
config = safe_load(args.config)
|
|
30
|
+
|
|
31
|
+
JOB_TYPES = {
|
|
32
|
+
"transport_node": RNSTransportNode,
|
|
33
|
+
"influx": InfluxWriter
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Init Reticulum instance
|
|
37
|
+
RNS.Reticulum(configdir=args.rns_config, verbosity=args.verbose)
|
|
38
|
+
|
|
39
|
+
def sig_handler(signum, frame):
|
|
40
|
+
MP.terminate.set()
|
|
41
|
+
signal.signal(signal.SIGINT, sig_handler)
|
|
42
|
+
signal.signal(signal.SIGTERM, sig_handler)
|
|
43
|
+
|
|
44
|
+
jobs = []
|
|
45
|
+
# Setup InfluxWriter push job
|
|
46
|
+
jobs.append({"type": "influx"} | config['influxdb'])
|
|
47
|
+
# Setup Scraping Jobs
|
|
48
|
+
for target in config['targets']:
|
|
49
|
+
RNSUtils.validate_hexhash(target['dest_identity'])
|
|
50
|
+
jobs.append({"verbosity": args.verbose} | target)
|
|
51
|
+
|
|
52
|
+
futures = {}
|
|
53
|
+
with concurrent.futures.ThreadPoolExecutor(len(jobs)) as executor:
|
|
54
|
+
|
|
55
|
+
for job in jobs:
|
|
56
|
+
futures[executor.submit(JOB_TYPES[job['type']], **job )] = job
|
|
57
|
+
|
|
58
|
+
while len(futures) > 0:
|
|
59
|
+
new_jobs = {}
|
|
60
|
+
done, not_done = concurrent.futures.wait(futures, return_when=concurrent.futures.FIRST_COMPLETED)
|
|
61
|
+
if MP.terminate.is_set():
|
|
62
|
+
break
|
|
63
|
+
for future in done:
|
|
64
|
+
job = futures[future]
|
|
65
|
+
if future.exception():
|
|
66
|
+
RNS.log(f"[RNMon] Job exited with exception: \"{future.exception()}\"", RNS.LOG_WARNING)
|
|
67
|
+
RNS.log(f"[RNMon] Job Exception Restart: {JOB_TYPES[job['type']]}", RNS.LOG_WARNING)
|
|
68
|
+
new_jobs[executor.submit(JOB_TYPES[job['type']], **job)] = job
|
|
69
|
+
for future in not_done:
|
|
70
|
+
job = futures[future]
|
|
71
|
+
new_jobs[future] = job
|
|
72
|
+
futures = new_jobs
|
|
73
|
+
time.sleep(1)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# RNS.exit() calls os._exit(), It does not clean things up properly
|
|
77
|
+
# and triggers semaphore_tracker:UserWarning since multiprocessing.resource_tracker
|
|
78
|
+
# is running, as intended, until it is terminated on main thread exit
|
|
79
|
+
# RNS.exit()
|
|
80
|
+
RNS.Reticulum.exit_handler()
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
|
|
83
|
+
if __name__ == '__main__':
|
|
84
|
+
main()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rnmon
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: RNS Monitoring Agent
|
|
5
|
+
Author: lbatalha
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
8
|
+
Requires-Dist: requests>=2.32.3
|
|
9
|
+
Requires-Dist: rns>=0.9.6
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# RNMon - Reticulum Application Monitoring Daemon
|
|
13
|
+
|
|
14
|
+
RNMon is a simple monitoring daemon designed to monitor the status of multiple RNS applications and push the metrics over http using the influx line protocol.
|
|
15
|
+
|
|
16
|
+
## Installing
|
|
17
|
+
|
|
18
|
+
## Configuration
|
|
19
|
+
|
|
20
|
+
Configure the daemon via `scraping.yaml`, the example config has comments explaining the options.
|
|
21
|
+
|
|
22
|
+
The configuration for reticulum is auto-discovered, but you can specify the location of the configuration directory using the `--rns-config` argument.
|
|
23
|
+
|
|
24
|
+
## Operational principles
|
|
25
|
+
|
|
26
|
+
The metric pusher and all targets are executed in their own thread. The main thread starts a new RNS instance, and closes it on exit.
|
|
27
|
+
|
|
28
|
+
A link is established for each scrape target to reduce network overhead. If a link is broken for any reason, the thread is terminated and restarted - this avoids having to deal with the built-in RNS link retry mechanisms, their associated timeouts and any edge cases caused by using shared RNS intances. This might be changed in the future if RNS fixes the issues particular to this use case.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
rnmon/Databases.py,sha256=yUnGYjQDGlYdCYTaIKz0JQob7rN3FcqeoxpdRlYulUc,1463
|
|
2
|
+
rnmon/Logging.py,sha256=eEtf_vyo--rgqXS8cePtIIPEU1jUWp6JbbFbpiNwqWM,79
|
|
3
|
+
rnmon/MP.py,sha256=nub7xJQUYkG3aO6T42mZSHGJO5mII42R0eMxp9srU_c,125
|
|
4
|
+
rnmon/RNSUtils.py,sha256=c9JEMP2yi9FmyDGf96I7O7kh-7Uf4biMSODdKX74i58,3552
|
|
5
|
+
rnmon/Remotes.py,sha256=xzAfvTLeoiADkiY90VE0NCJEEhU-5J9cCto60UZc4Ps,5798
|
|
6
|
+
rnmon/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
rnmon/__main__.py,sha256=xTyj40x3M7fIPfZbQ7xS4VvA8pRn6hw4dcMplsibdWY,34
|
|
8
|
+
rnmon/rnmon.py,sha256=K7F2T06fStbm3UIg7CPsVdqurC5tnnxQlWzPisjDQ00,2866
|
|
9
|
+
rnmon-0.1.0.dist-info/METADATA,sha256=MdsW3fIFCPPlubLflzGIMUj9FclFt66AaS77CbXkl2k,1297
|
|
10
|
+
rnmon-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
rnmon-0.1.0.dist-info/entry_points.txt,sha256=pKY8NEHRhxa8E9Q3H9TlkphkDFoaPwzrHkT4v8mPGsU,43
|
|
12
|
+
rnmon-0.1.0.dist-info/RECORD,,
|