argussight 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.
- argussight/__init__.py +0 -0
- argussight/__main__.py +0 -0
- argussight/core/config.py +20 -0
- argussight/core/configurations/config.yaml +40 -0
- argussight/core/configurations/processes/savers/stream_buffer.yaml +15 -0
- argussight/core/configurations/processes/savers/video_recorder.yaml +22 -0
- argussight/core/configurations/processes/savers/video_saver.yaml +16 -0
- argussight/core/configurations/processes/streamer/flow_detection.yaml +21 -0
- argussight/core/configurations/processes/streamer/optical_flow_detection.yaml +16 -0
- argussight/core/configurations/processes/streamer/streamer.yaml +3 -0
- argussight/core/configurations/processes/vprocess.yaml +3 -0
- argussight/core/helper_functions.py +35 -0
- argussight/core/manager.py +71 -0
- argussight/core/spawner.py +321 -0
- argussight/core/video_processes/savers/stream_buffer.py +32 -0
- argussight/core/video_processes/savers/video_recorder.py +122 -0
- argussight/core/video_processes/savers/video_saver.py +164 -0
- argussight/core/video_processes/streamer/flow_detection.py +185 -0
- argussight/core/video_processes/streamer/optical_flow_detection.py +136 -0
- argussight/core/video_processes/streamer/streamer.py +87 -0
- argussight/core/video_processes/vprocess.py +245 -0
- argussight/grpc/__init__.py +0 -0
- argussight/grpc/argus_service.proto +78 -0
- argussight/grpc/argus_service_pb2.py +74 -0
- argussight/grpc/argus_service_pb2_grpc.py +341 -0
- argussight/grpc/helper_functions.py +55 -0
- argussight/grpc/server.py +112 -0
- argussight/grpc/test_client.py +63 -0
- argussight/grpc/test_concurrent_requests.py +48 -0
- argussight/main.py +62 -0
- argussight/streamsproxy.py +62 -0
- argussight-0.1.0.dist-info/LICENSE +21 -0
- argussight-0.1.0.dist-info/METADATA +151 -0
- argussight-0.1.0.dist-info/RECORD +36 -0
- argussight-0.1.0.dist-info/WHEEL +4 -0
- argussight-0.1.0.dist-info/entry_points.txt +4 -0
argussight/__init__.py
ADDED
|
File without changes
|
argussight/__main__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RedisConfiguration(BaseModel):
|
|
7
|
+
host: str = Field("localhost", description="redis host")
|
|
8
|
+
port: int = Field(6379, description="redis port")
|
|
9
|
+
channel: str = Field(
|
|
10
|
+
"video-streamer", description="redis-channel to publish stream"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CollectorConfiguration(BaseModel):
|
|
15
|
+
redis: RedisConfiguration
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_config_from_dict(config_data: dict) -> Union[CollectorConfiguration, None]:
|
|
19
|
+
data = CollectorConfiguration(**config_data)
|
|
20
|
+
return data
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
# This indicates the path to the files of the process classes
|
|
4
|
+
modules_path: argussight.core.video_processes
|
|
5
|
+
|
|
6
|
+
# List of available processes and their file locations from modules_path
|
|
7
|
+
# If accessible=true then process can be started via grpc call
|
|
8
|
+
worker_classes:
|
|
9
|
+
test:
|
|
10
|
+
location: vprocess.Test
|
|
11
|
+
accessible: true
|
|
12
|
+
flow_detection:
|
|
13
|
+
location: streamer.optical_flow_detection.OpticalFlowDetection
|
|
14
|
+
accessible: true
|
|
15
|
+
stream_buffer:
|
|
16
|
+
location: savers.stream_buffer.StreamBuffer
|
|
17
|
+
accessible: false
|
|
18
|
+
video_recorder:
|
|
19
|
+
location: savers.video_recorder.Recorder
|
|
20
|
+
accessible: false
|
|
21
|
+
|
|
22
|
+
# This indicates the processes that are started by argussight,
|
|
23
|
+
# if killed due to no response, they are restarted automatically
|
|
24
|
+
processes:
|
|
25
|
+
- name: "Saver"
|
|
26
|
+
type: stream_buffer
|
|
27
|
+
- name: "Recorder"
|
|
28
|
+
type: video_recorder
|
|
29
|
+
|
|
30
|
+
# This indicates the maximal waiting time (s),
|
|
31
|
+
# the spawner waits on responds from processes, before killing them
|
|
32
|
+
wait_time: 5
|
|
33
|
+
|
|
34
|
+
# This is the port for the rerouting of the streams
|
|
35
|
+
streams_layer_port: 7000
|
|
36
|
+
|
|
37
|
+
# Argussight is configured to run streams on different ports
|
|
38
|
+
# It will check for free ports, whenever a new streaming process starts
|
|
39
|
+
# The value below indicates at which port the search for a free port begins,
|
|
40
|
+
streams_starting_port: 9000
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
parameters:
|
|
4
|
+
main_save_folder:
|
|
5
|
+
value: "logs/recordings"
|
|
6
|
+
exposed: false
|
|
7
|
+
# pay attention to the setting of the temp folder, as the folder will
|
|
8
|
+
# be cleared when the recorder is initialised,
|
|
9
|
+
# IMPORTANT: do NOT use an existing folder!!!
|
|
10
|
+
temp_folder:
|
|
11
|
+
value: "temp"
|
|
12
|
+
exposed: false
|
|
13
|
+
save_format:
|
|
14
|
+
value: "both"
|
|
15
|
+
exposed: true
|
|
16
|
+
personnal_folder:
|
|
17
|
+
value: "test"
|
|
18
|
+
exposed: true
|
|
19
|
+
max_recording_time:
|
|
20
|
+
# unit: seconds, 0 means not set
|
|
21
|
+
value: 0
|
|
22
|
+
exposed: false
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
parameters:
|
|
4
|
+
main_save_folder:
|
|
5
|
+
value: "logs/"
|
|
6
|
+
exposed: false
|
|
7
|
+
save_format:
|
|
8
|
+
value: "both"
|
|
9
|
+
exposed: true
|
|
10
|
+
personnal_folder:
|
|
11
|
+
value: "test"
|
|
12
|
+
exposed: true
|
|
13
|
+
max_recording_time:
|
|
14
|
+
# unit: seconds, 0 means not set
|
|
15
|
+
value: 0
|
|
16
|
+
exposed: false
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
parameters:
|
|
4
|
+
feature_params:
|
|
5
|
+
value:
|
|
6
|
+
maxCorners: 5
|
|
7
|
+
qualityLevel: 0.7
|
|
8
|
+
minDistance: 50
|
|
9
|
+
blockSize: 7
|
|
10
|
+
exposed: false
|
|
11
|
+
|
|
12
|
+
lk_params:
|
|
13
|
+
value:
|
|
14
|
+
winSize: [15, 15]
|
|
15
|
+
maxLevel: 2
|
|
16
|
+
criteria: [3, 10, 0.03]
|
|
17
|
+
exposed: false
|
|
18
|
+
|
|
19
|
+
roi:
|
|
20
|
+
value: [550, 450, 200, 500]
|
|
21
|
+
exposed: true
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
import Levenshtein
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def find_close_key(d: dict, key: str, max_distance: int = 3) -> Union[str, None]:
|
|
8
|
+
"""Return first key in dict whose Levenshtein distance to key is <= max_distance"""
|
|
9
|
+
min_distance = float("inf")
|
|
10
|
+
closest_key = None
|
|
11
|
+
|
|
12
|
+
for dict_key in d:
|
|
13
|
+
distance = Levenshtein.distance(key, dict_key)
|
|
14
|
+
if distance < min_distance:
|
|
15
|
+
min_distance = distance
|
|
16
|
+
closest_key = dict_key
|
|
17
|
+
|
|
18
|
+
if min_distance <= max_distance:
|
|
19
|
+
return closest_key
|
|
20
|
+
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_port_free(port):
|
|
25
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
26
|
+
s.settimeout(1) # Optional: set a short timeout for connection attempt
|
|
27
|
+
result = s.connect_ex(("localhost", port))
|
|
28
|
+
return result != 0 # If result is non-zero, port is free
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def find_free_port(start_port=9000):
|
|
32
|
+
port = start_port
|
|
33
|
+
while not is_port_free(port):
|
|
34
|
+
port += 1
|
|
35
|
+
return port
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import queue
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from multiprocessing import Queue
|
|
4
|
+
from threading import Event
|
|
5
|
+
|
|
6
|
+
from argussight.core.video_processes.vprocess import ProcessError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Manager:
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
command_queue: Queue,
|
|
13
|
+
response_queue: Queue,
|
|
14
|
+
finished_event: Event,
|
|
15
|
+
failed_event: Event,
|
|
16
|
+
) -> None:
|
|
17
|
+
self._commands_list = queue.Queue(
|
|
18
|
+
maxsize=20
|
|
19
|
+
) # commands that are waiting to be processed by Manager
|
|
20
|
+
self._command_queue = (
|
|
21
|
+
command_queue # commands that are sent to process for being processed
|
|
22
|
+
)
|
|
23
|
+
self._response_queue = response_queue
|
|
24
|
+
self._finished_event = finished_event
|
|
25
|
+
self._failed_event = failed_event
|
|
26
|
+
|
|
27
|
+
def receive_command(
|
|
28
|
+
self,
|
|
29
|
+
command: str,
|
|
30
|
+
wait_time: int,
|
|
31
|
+
processed_event: Event,
|
|
32
|
+
result_queue: queue.Queue,
|
|
33
|
+
args,
|
|
34
|
+
) -> None:
|
|
35
|
+
if self._commands_list.full():
|
|
36
|
+
raise ProcessError(
|
|
37
|
+
f"Cannot execute command {command}: too many commands in waiting list"
|
|
38
|
+
)
|
|
39
|
+
self._commands_list.put(
|
|
40
|
+
{
|
|
41
|
+
"command": command,
|
|
42
|
+
"max_wait_time": wait_time,
|
|
43
|
+
"time_stamp": datetime.now(),
|
|
44
|
+
"args": args,
|
|
45
|
+
"processed_event": processed_event,
|
|
46
|
+
"result_queue": result_queue,
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def handle_commands(self) -> None:
|
|
51
|
+
while not self._commands_list.empty():
|
|
52
|
+
command = self._commands_list.get()
|
|
53
|
+
|
|
54
|
+
# Check if command is alive
|
|
55
|
+
if (datetime.now() - command["time_stamp"]).total_seconds() > command[
|
|
56
|
+
"max_wait_time"
|
|
57
|
+
]:
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
self._command_queue.put((command["command"], command["args"]))
|
|
61
|
+
command["processed_event"].set()
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
result = self._response_queue.get(timeout=command["max_wait_time"])
|
|
65
|
+
command["result_queue"].put(result)
|
|
66
|
+
|
|
67
|
+
except queue.Empty:
|
|
68
|
+
self._failed_event.set()
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
self._finished_event.set()
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import inspect
|
|
3
|
+
import multiprocessing
|
|
4
|
+
import os
|
|
5
|
+
import queue
|
|
6
|
+
import threading
|
|
7
|
+
from typing import Any, Dict, List, Tuple
|
|
8
|
+
|
|
9
|
+
import psutil
|
|
10
|
+
import requests
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
import argussight.streamsproxy as StreamsProxy
|
|
14
|
+
from argussight.core.helper_functions import find_close_key, find_free_port
|
|
15
|
+
from argussight.core.manager import Manager
|
|
16
|
+
from argussight.core.video_processes.streamer.streamer import Streamer
|
|
17
|
+
from argussight.core.video_processes.vprocess import ProcessError, Vprocess
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Spawner:
|
|
21
|
+
def __init__(self, collector_config) -> None:
|
|
22
|
+
self._processes = {}
|
|
23
|
+
self._worker_classes = {}
|
|
24
|
+
self._managers_dict = {}
|
|
25
|
+
self._restricted_classes = []
|
|
26
|
+
self._streamer_types = []
|
|
27
|
+
self.collector_config = collector_config
|
|
28
|
+
self._settings_manager = multiprocessing.Manager()
|
|
29
|
+
self._streams = set([])
|
|
30
|
+
|
|
31
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
32
|
+
self.load_config(os.path.join(current_dir, "configurations/config.yaml"))
|
|
33
|
+
|
|
34
|
+
print(f"running stream_layer on port {self.config['streams_layer_port']}")
|
|
35
|
+
streams_layer_process = multiprocessing.Process(
|
|
36
|
+
target=StreamsProxy.run, args=(self.config["streams_layer_port"],)
|
|
37
|
+
)
|
|
38
|
+
streams_layer_process.start()
|
|
39
|
+
|
|
40
|
+
def load_config(self, path_config_file: str) -> None:
|
|
41
|
+
with open(path_config_file, "r") as f:
|
|
42
|
+
self.config = yaml.safe_load(f)
|
|
43
|
+
self.load_worker_classes()
|
|
44
|
+
|
|
45
|
+
for process in self.config["processes"]:
|
|
46
|
+
self.start_process(process["name"], process["type"])
|
|
47
|
+
|
|
48
|
+
def load_worker_classes(self):
|
|
49
|
+
worker_classes_config = self.config["worker_classes"]
|
|
50
|
+
modules_path = self.config["modules_path"]
|
|
51
|
+
for key, worker_class in worker_classes_config.items():
|
|
52
|
+
class_path = worker_class["location"]
|
|
53
|
+
module_name, class_name = class_path.rsplit(".", 1)
|
|
54
|
+
module = importlib.import_module(modules_path + "." + module_name)
|
|
55
|
+
self._worker_classes[key] = getattr(module, class_name)
|
|
56
|
+
if not worker_class["accessible"]:
|
|
57
|
+
self._restricted_classes.append(key)
|
|
58
|
+
if issubclass(self._worker_classes[key], Streamer):
|
|
59
|
+
self._streamer_types.append(key)
|
|
60
|
+
|
|
61
|
+
def create_worker(
|
|
62
|
+
self, worker_type: str, free_port, settings: Dict[str, Any]
|
|
63
|
+
) -> Vprocess:
|
|
64
|
+
if worker_type in self._streamer_types:
|
|
65
|
+
return self._worker_classes.get(worker_type)(
|
|
66
|
+
self.collector_config, free_port, settings
|
|
67
|
+
)
|
|
68
|
+
return self._worker_classes.get(worker_type)(self.collector_config, settings)
|
|
69
|
+
|
|
70
|
+
def add_process(
|
|
71
|
+
self,
|
|
72
|
+
name: str,
|
|
73
|
+
worker_type: str,
|
|
74
|
+
process: multiprocessing.Process,
|
|
75
|
+
command_queue: multiprocessing.Queue,
|
|
76
|
+
response_queue: multiprocessing.Queue,
|
|
77
|
+
settings: Dict[str, Any],
|
|
78
|
+
) -> None:
|
|
79
|
+
self._processes[name] = {
|
|
80
|
+
"process_instance": process,
|
|
81
|
+
"command_queue": command_queue,
|
|
82
|
+
"response_queue": response_queue,
|
|
83
|
+
"type": worker_type,
|
|
84
|
+
"settings": settings,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# This function checks if worker_type can be accessed
|
|
88
|
+
def check_restricted_access(self, worker_type: str) -> bool:
|
|
89
|
+
# First check if access is restricted
|
|
90
|
+
if worker_type not in self._restricted_classes:
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
# If access is restricted, check if the caller is the server
|
|
94
|
+
stack = inspect.stack()
|
|
95
|
+
|
|
96
|
+
# The caller's frame will be two levels up in the stack:
|
|
97
|
+
# - The current frame
|
|
98
|
+
# - The frame of the function that called check_restricted_access
|
|
99
|
+
# - The frame of the function that called that function (the original caller)
|
|
100
|
+
caller_frame = stack[2]
|
|
101
|
+
|
|
102
|
+
# Get the calling function's name and module
|
|
103
|
+
caller_module = inspect.getmodule(caller_frame.frame)
|
|
104
|
+
|
|
105
|
+
# Check if the caller is a method in the same class
|
|
106
|
+
if caller_module and caller_module.__name__ == self.__class__.__module__:
|
|
107
|
+
caller_class = caller_frame.frame.f_locals.get("self")
|
|
108
|
+
if isinstance(caller_class, self.__class__):
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
def add_stream(self, name, port, stream_id) -> None:
|
|
114
|
+
requests.post(
|
|
115
|
+
f"http://localhost:{str(self.config['streams_layer_port'])}/add-stream",
|
|
116
|
+
params={
|
|
117
|
+
"path": name,
|
|
118
|
+
"port": port,
|
|
119
|
+
"id": stream_id,
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
self._streams.add(name)
|
|
123
|
+
|
|
124
|
+
def start_process(self, name, type) -> None:
|
|
125
|
+
if name in self._processes:
|
|
126
|
+
raise ProcessError(
|
|
127
|
+
f"Process names must be unique. '{name}' already exists. Either terminate '{name}' or choose a different unique name"
|
|
128
|
+
)
|
|
129
|
+
if type not in self._worker_classes:
|
|
130
|
+
raise ProcessError(f"Type {type} does not exist")
|
|
131
|
+
# check if somebody tries to start a restricted worker type from outside this class
|
|
132
|
+
if not self.check_restricted_access(type):
|
|
133
|
+
raise ProcessError(f"Worker of type {type} can only be started by server.")
|
|
134
|
+
|
|
135
|
+
free_port = find_free_port(self.config["streams_starting_port"])
|
|
136
|
+
settings = self._settings_manager.dict()
|
|
137
|
+
worker_instance = self.create_worker(type, free_port, settings)
|
|
138
|
+
command_queue = multiprocessing.Queue()
|
|
139
|
+
response_queue = multiprocessing.Queue()
|
|
140
|
+
p = multiprocessing.Process(
|
|
141
|
+
target=worker_instance.run, args=(command_queue, response_queue)
|
|
142
|
+
)
|
|
143
|
+
print(f"started {name} of type {type}")
|
|
144
|
+
p.start()
|
|
145
|
+
if isinstance(worker_instance, Streamer):
|
|
146
|
+
self.add_stream(name, free_port, worker_instance.get_stream_id())
|
|
147
|
+
self.add_process(name, type, p, command_queue, response_queue, settings)
|
|
148
|
+
|
|
149
|
+
# check if process is running otherwise throw ProcessError
|
|
150
|
+
def check_for_running_process(self, name: str) -> None:
|
|
151
|
+
if name not in self._processes:
|
|
152
|
+
errorMessage = f"{name} is not a running process."
|
|
153
|
+
closest_key = find_close_key(self._processes, name)
|
|
154
|
+
if closest_key:
|
|
155
|
+
errorMessage += f" Did you mean: {closest_key}"
|
|
156
|
+
|
|
157
|
+
raise ProcessError(errorMessage)
|
|
158
|
+
|
|
159
|
+
def find_process_in_config_by_name(self, name: str) -> Dict:
|
|
160
|
+
for process in self.config["processes"]:
|
|
161
|
+
if process["name"] == name:
|
|
162
|
+
return process
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
def terminate_processes(self, names: List[str]) -> None:
|
|
166
|
+
for name in names:
|
|
167
|
+
self.check_for_running_process(name)
|
|
168
|
+
worker_type = self._processes[name]["type"]
|
|
169
|
+
# check if somebody tries to kill a restricted worker type from outside this class
|
|
170
|
+
if not self.check_restricted_access(worker_type):
|
|
171
|
+
raise ProcessError(
|
|
172
|
+
f"Worker of type {worker_type} can only be terminated by server."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
for name in names:
|
|
176
|
+
p = self._processes[name]["process_instance"]
|
|
177
|
+
worker_type = self._processes[name]["type"]
|
|
178
|
+
# first kill all possible children
|
|
179
|
+
parent = psutil.Process(p.pid)
|
|
180
|
+
children = parent.children(recursive=True)
|
|
181
|
+
for child in children:
|
|
182
|
+
print(f"Terminating child process: {child.pid}")
|
|
183
|
+
child.terminate()
|
|
184
|
+
# now kill the process itself and clean up
|
|
185
|
+
p.terminate()
|
|
186
|
+
p.join()
|
|
187
|
+
del self._processes[name]["settings"]
|
|
188
|
+
del self._processes[name]
|
|
189
|
+
|
|
190
|
+
if worker_type in self._streamer_types:
|
|
191
|
+
requests.post(
|
|
192
|
+
f"http://localhost:{str(self.config['streams_layer_port'])}/remove-stream",
|
|
193
|
+
params={"path": name},
|
|
194
|
+
)
|
|
195
|
+
self._streams.discard(name)
|
|
196
|
+
|
|
197
|
+
print(f"terminated {name} of type {worker_type}")
|
|
198
|
+
if (
|
|
199
|
+
worker_type in self._restricted_classes
|
|
200
|
+
and self.check_restricted_access(worker_type)
|
|
201
|
+
):
|
|
202
|
+
process = self.find_process_in_config_by_name(name)
|
|
203
|
+
self.start_process(process["name"], process["type"])
|
|
204
|
+
|
|
205
|
+
def wait_for_manager(
|
|
206
|
+
self,
|
|
207
|
+
finished_event: threading.Event,
|
|
208
|
+
failed_event: threading.Event,
|
|
209
|
+
name: str,
|
|
210
|
+
manager: threading.Thread,
|
|
211
|
+
) -> None:
|
|
212
|
+
while manager.is_alive():
|
|
213
|
+
finished_event.wait(timeout=1000)
|
|
214
|
+
if finished_event.is_set():
|
|
215
|
+
del self._managers_dict[name]
|
|
216
|
+
if failed_event.is_set():
|
|
217
|
+
self.terminate_processes([name])
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
if name in self._managers_dict:
|
|
221
|
+
del self._managers_dict[name]
|
|
222
|
+
print("manager is not alive anymore but hasn't finished")
|
|
223
|
+
|
|
224
|
+
def send_command_to_manager(
|
|
225
|
+
self, name: str, command: str, args
|
|
226
|
+
) -> Tuple[threading.Event, queue.Queue]:
|
|
227
|
+
processed_event = threading.Event()
|
|
228
|
+
response_queue = queue.Queue()
|
|
229
|
+
|
|
230
|
+
self._managers_dict[name]["manager"].receive_command(
|
|
231
|
+
command, self.config["wait_time"], processed_event, response_queue, args
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return processed_event, response_queue
|
|
235
|
+
|
|
236
|
+
# this function should only be called by the spawner service and used as Thread
|
|
237
|
+
def manage_process(self, name: str, command: str, args) -> None:
|
|
238
|
+
self.check_for_running_process(name)
|
|
239
|
+
|
|
240
|
+
# Check if manager already exists
|
|
241
|
+
if name not in self._managers_dict:
|
|
242
|
+
finished_event = threading.Event()
|
|
243
|
+
failed_event = threading.Event()
|
|
244
|
+
self._managers_dict[name] = {
|
|
245
|
+
"manager": Manager(
|
|
246
|
+
self._processes[name]["command_queue"],
|
|
247
|
+
self._processes[name]["response_queue"],
|
|
248
|
+
finished_event,
|
|
249
|
+
failed_event,
|
|
250
|
+
),
|
|
251
|
+
"failed_event": failed_event,
|
|
252
|
+
}
|
|
253
|
+
processed_event, result_queue = self.send_command_to_manager(
|
|
254
|
+
name, command, args
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
manager_thread = threading.Thread(
|
|
258
|
+
target=self._managers_dict[name]["manager"].handle_commands
|
|
259
|
+
)
|
|
260
|
+
waiter_thread = threading.Thread(
|
|
261
|
+
target=self.wait_for_manager,
|
|
262
|
+
args=(finished_event, failed_event, name, manager_thread),
|
|
263
|
+
)
|
|
264
|
+
manager_thread.start()
|
|
265
|
+
waiter_thread.start()
|
|
266
|
+
else:
|
|
267
|
+
try:
|
|
268
|
+
processed_event, result_queue = self.send_command_to_manager(
|
|
269
|
+
name, command, args
|
|
270
|
+
)
|
|
271
|
+
failed_event = self._managers_dict[name]["failed_event"]
|
|
272
|
+
except ProcessError as e:
|
|
273
|
+
e.message += f" for {name}. Try again later"
|
|
274
|
+
|
|
275
|
+
is_processing = processed_event.wait(timeout=self.config["wait_time"])
|
|
276
|
+
if is_processing:
|
|
277
|
+
try:
|
|
278
|
+
result = result_queue.get(timeout=self.config["wait_time"])
|
|
279
|
+
if isinstance(result, Exception):
|
|
280
|
+
raise result
|
|
281
|
+
|
|
282
|
+
return
|
|
283
|
+
except queue.Empty:
|
|
284
|
+
wait_time = self.config["wait_time"]
|
|
285
|
+
raise ProcessError(
|
|
286
|
+
f"Command {command} could not be executed in time {wait_time}. Hence process {name} is getting terminated"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
elif failed_event.is_set():
|
|
290
|
+
raise ProcessError(
|
|
291
|
+
f"An error occured in process {name}. Process is no longer alive."
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
raise ProcessError(
|
|
295
|
+
f"Process {name} is busy and could not start command in time. Try again later."
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
def get_processes(self):
|
|
299
|
+
running_processes = {}
|
|
300
|
+
for uname, process in self._processes.items():
|
|
301
|
+
type = process["type"]
|
|
302
|
+
current_class = self._worker_classes[type]
|
|
303
|
+
commands = [
|
|
304
|
+
command
|
|
305
|
+
for command in current_class.create_commands_dict().keys()
|
|
306
|
+
if command != "settings" and command != "default_settings"
|
|
307
|
+
]
|
|
308
|
+
settings = dict(process["settings"])
|
|
309
|
+
running_processes[uname] = {
|
|
310
|
+
"type": type,
|
|
311
|
+
"commands": commands,
|
|
312
|
+
"settings": settings,
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
available_types = [
|
|
316
|
+
type
|
|
317
|
+
for type in self._worker_classes
|
|
318
|
+
if type not in self._restricted_classes
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
return running_processes, available_types, self._streams
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from collections import deque
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
from argussight.core.video_processes.savers.video_saver import VideoSaver
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StreamBuffer(VideoSaver):
|
|
8
|
+
def __init__(self, collector_config, exposed_parameters: Dict[str, Any]) -> None:
|
|
9
|
+
super().__init__(collector_config, exposed_parameters)
|
|
10
|
+
self._queue = deque(maxlen=self._parameters["max_queue_len"])
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def create_commands_dict(cls) -> Dict[str, Any]:
|
|
14
|
+
result = super().create_commands_dict()
|
|
15
|
+
result.update({"save": cls.save_queue})
|
|
16
|
+
return result
|
|
17
|
+
|
|
18
|
+
def save_queue(self) -> None:
|
|
19
|
+
queue = self._queue.copy()
|
|
20
|
+
self.executor.submit(
|
|
21
|
+
self.save_iterable,
|
|
22
|
+
queue,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def add_to_iterable(self, frame: Dict) -> None:
|
|
26
|
+
self._queue.append(frame)
|
|
27
|
+
|
|
28
|
+
def _max_recording_callback(self) -> None:
|
|
29
|
+
self.save_queue()
|
|
30
|
+
# this should normally not be called but if it is,
|
|
31
|
+
# there is no way to reset recording except by restarting the server
|
|
32
|
+
self._parameters["recording"] = False
|