mrok 0.2.1__tar.gz → 0.2.3__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.
- {mrok-0.2.1 → mrok-0.2.3}/PKG-INFO +1 -1
- {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/app.py +8 -2
- {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/routes/extensions.py +21 -7
- {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/routes/instances.py +5 -1
- {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/schemas.py +13 -1
- mrok-0.2.3/mrok/http/master.py +132 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/ziti/api.py +1 -1
- {mrok-0.2.1 → mrok-0.2.3}/pyproject.toml +1 -1
- {mrok-0.2.1 → mrok-0.2.3}/tests/controller/test_extensions.py +60 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/controller/test_instances.py +10 -2
- {mrok-0.2.1 → mrok-0.2.3}/tests/http/test_master.py +61 -2
- mrok-0.2.1/mrok/http/master.py +0 -90
- {mrok-0.2.1 → mrok-0.2.3}/.github/actions/setup-python-env/action.yml +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/.github/workflows/assets/turing_team_pr_bot.png +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/.github/workflows/notify-pr-closed.yaml +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/.github/workflows/notify-pr-reviewed.yml +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/.github/workflows/pr-build-merge.yaml +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/.github/workflows/release.yml +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/.gitignore +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/.pre-commit-config.yaml +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/.python-version +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/LICENSE.txt +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/README.md +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/dev.Dockerfile +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/docker-compose.yaml +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/entrypoint.sh +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/agent/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/agent/sidecar/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/agent/sidecar/app.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/agent/sidecar/main.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/agent/ziticorn.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/bootstrap.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/list/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/list/extensions.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/list/instances.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/register/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/register/extensions.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/register/instances.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/unregister/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/unregister/extensions.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/unregister/instances.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/admin/utils.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/agent/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/agent/run/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/agent/run/asgi.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/agent/run/sidecar.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/controller/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/controller/openapi.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/commands/controller/run.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/main.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/cli/rich.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/conf.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/auth.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/dependencies/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/dependencies/conf.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/dependencies/ziti.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/openapi/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/openapi/examples.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/openapi/utils.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/pagination.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/controller/routes/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/errors.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/http/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/http/config.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/http/forwarder.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/http/lifespan.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/http/protocol.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/http/server.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/logging.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/ziti/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/ziti/bootstrap.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/ziti/constants.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/ziti/errors.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/ziti/identities.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/ziti/pki.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/mrok/ziti/services.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/prod.Dockerfile +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/scripts/ziti.sh +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/settings.yaml +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/sonar-project.properties +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/agent/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/agent/sidecar/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/agent/sidecar/test_app.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/agent/sidecar/test_main.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/agent/test_ziticorn.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/cli/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/cli/admin/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/cli/admin/test_bootstrap.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/cli/admin/test_list.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/cli/admin/test_register.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/cli/admin/test_unregister.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/cli/admin/test_utils.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/cli/agent/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/cli/agent/test_run.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/cli/controller/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/cli/controller/test_openapi.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/cli/controller/test_run.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/cli/test_main.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/conftest.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/controller/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/controller/test_auth.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/controller/test_openapi.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/http/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/http/test_config.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/http/test_forwarder.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/http/test_lifespan.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/http/test_protocol.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/http/test_server.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/ziti/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/ziti/test_api.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/ziti/test_bootstrap.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/ziti/test_identities.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/ziti/test_pki.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/tests/ziti/test_services.py +0 -0
- {mrok-0.2.1 → mrok-0.2.3}/uv.lock +0 -0
|
@@ -51,9 +51,15 @@ def setup_app():
|
|
|
51
51
|
|
|
52
52
|
# TODO: Add healthcheck
|
|
53
53
|
app.include_router(
|
|
54
|
-
extensions_router,
|
|
54
|
+
extensions_router,
|
|
55
|
+
prefix="/extensions",
|
|
56
|
+
dependencies=[Depends(authenticate)],
|
|
57
|
+
)
|
|
58
|
+
app.include_router(
|
|
59
|
+
instances_router,
|
|
60
|
+
prefix="/instances",
|
|
61
|
+
dependencies=[Depends(authenticate)],
|
|
55
62
|
)
|
|
56
|
-
app.include_router(instances_router, prefix="/instances", dependencies=[Depends(authenticate)])
|
|
57
63
|
|
|
58
64
|
settings = get_settings()
|
|
59
65
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Annotated
|
|
2
|
+
from typing import Annotated, Literal
|
|
3
3
|
|
|
4
4
|
from fastapi import APIRouter, Body, HTTPException, status
|
|
5
5
|
|
|
@@ -116,8 +116,26 @@ async def create_extension(
|
|
|
116
116
|
async def get_extension_by_id_or_extension_id(
|
|
117
117
|
mgmt_api: ZitiManagementAPI,
|
|
118
118
|
id_or_extension_id: str,
|
|
119
|
+
with_instances: Literal["none", "online", "offline"] = "none",
|
|
119
120
|
):
|
|
120
|
-
|
|
121
|
+
extension = await fetch_extension_or_404(mgmt_api, id_or_extension_id)
|
|
122
|
+
|
|
123
|
+
if with_instances == "none":
|
|
124
|
+
return ExtensionRead(**extension)
|
|
125
|
+
|
|
126
|
+
instances = list(
|
|
127
|
+
filter(
|
|
128
|
+
lambda ir: ir.status == with_instances,
|
|
129
|
+
[
|
|
130
|
+
InstanceRead(**identity)
|
|
131
|
+
async for identity in mgmt_api.identities(
|
|
132
|
+
{"filter": f'tags.{MROK_SERVICE_TAG_NAME} = "{extension["name"]}"'}
|
|
133
|
+
)
|
|
134
|
+
],
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return ExtensionRead(**extension, instances=instances)
|
|
121
139
|
|
|
122
140
|
|
|
123
141
|
@router.delete(
|
|
@@ -272,11 +290,7 @@ async def get_instance_by_id_or_instance_id(
|
|
|
272
290
|
id_or_instance_id: str,
|
|
273
291
|
):
|
|
274
292
|
identity = await fetch_instance_or_404(mgmt_api, id_or_extension_id, id_or_instance_id)
|
|
275
|
-
return InstanceRead(
|
|
276
|
-
id=identity["id"],
|
|
277
|
-
name=identity["name"],
|
|
278
|
-
tags=identity["tags"],
|
|
279
|
-
)
|
|
293
|
+
return InstanceRead(**identity)
|
|
280
294
|
|
|
281
295
|
|
|
282
296
|
@router.delete(
|
|
@@ -6,6 +6,7 @@ from mrok.controller.dependencies import ZitiManagementAPI
|
|
|
6
6
|
from mrok.controller.openapi import examples
|
|
7
7
|
from mrok.controller.pagination import LimitOffsetPage, paginate
|
|
8
8
|
from mrok.controller.schemas import InstanceRead
|
|
9
|
+
from mrok.ziti.constants import MROK_IDENTITY_TYPE_TAG_NAME, MROK_IDENTITY_TYPE_TAG_VALUE_INSTANCE
|
|
9
10
|
|
|
10
11
|
logger = logging.getLogger("mrok.controller")
|
|
11
12
|
|
|
@@ -68,4 +69,7 @@ async def get_instance_by_id_or_instance_id(
|
|
|
68
69
|
async def get_instances(
|
|
69
70
|
mgmt_api: ZitiManagementAPI,
|
|
70
71
|
):
|
|
71
|
-
|
|
72
|
+
params = {
|
|
73
|
+
"filter": f'tags.{MROK_IDENTITY_TYPE_TAG_NAME}="{MROK_IDENTITY_TYPE_TAG_VALUE_INSTANCE}"'
|
|
74
|
+
}
|
|
75
|
+
return await paginate(mgmt_api, "/identities", InstanceRead, extra_params=params)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any, Literal
|
|
2
4
|
|
|
3
5
|
from pydantic import (
|
|
4
6
|
BaseModel,
|
|
@@ -34,6 +36,7 @@ class ExtensionBase(BaseSchema):
|
|
|
34
36
|
|
|
35
37
|
class ExtensionRead(BaseSchema, IdSchema):
|
|
36
38
|
name: str
|
|
39
|
+
instances: list[InstanceRead] | None = None
|
|
37
40
|
|
|
38
41
|
@computed_field
|
|
39
42
|
def extension(self) -> dict:
|
|
@@ -51,6 +54,11 @@ class InstanceBase(BaseSchema):
|
|
|
51
54
|
class InstanceRead(BaseSchema, IdSchema):
|
|
52
55
|
name: str
|
|
53
56
|
identity: dict[str, Any] | None = None
|
|
57
|
+
has_edge_router_connection: bool | None = Field(
|
|
58
|
+
False,
|
|
59
|
+
alias="hasEdgeRouterConnection",
|
|
60
|
+
exclude=True,
|
|
61
|
+
)
|
|
54
62
|
|
|
55
63
|
@computed_field
|
|
56
64
|
def instance(self) -> dict:
|
|
@@ -62,6 +70,10 @@ class InstanceRead(BaseSchema, IdSchema):
|
|
|
62
70
|
_, extension_id = self.name.split(".", 1)
|
|
63
71
|
return {"id": extension_id.upper()}
|
|
64
72
|
|
|
73
|
+
@computed_field
|
|
74
|
+
def status(self) -> Literal["online", "offline"]:
|
|
75
|
+
return "online" if bool(self.has_edge_router_connection) else "offline"
|
|
76
|
+
|
|
65
77
|
|
|
66
78
|
class InstanceCreate(InstanceBase):
|
|
67
79
|
pass
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import signal
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from watchfiles import watch
|
|
10
|
+
from watchfiles.filters import PythonFilter
|
|
11
|
+
from watchfiles.run import CombinedProcess, start_process
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("mrok.agent")
|
|
14
|
+
|
|
15
|
+
MONITOR_THREAD_JOIN_TIMEOUT = 5
|
|
16
|
+
MONITOR_THREAD_CHECK_DELAY = 1
|
|
17
|
+
MONITOR_THREAD_ERROR_DELAY = 3
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def print_path(path):
|
|
21
|
+
try:
|
|
22
|
+
return f'"{path.relative_to(Path.cwd())}"'
|
|
23
|
+
except ValueError:
|
|
24
|
+
return f'"{path}"'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Master:
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
start_fn: Callable,
|
|
31
|
+
workers: int,
|
|
32
|
+
reload: bool,
|
|
33
|
+
):
|
|
34
|
+
self.start_fn = start_fn
|
|
35
|
+
self.workers = workers
|
|
36
|
+
self.reload = reload
|
|
37
|
+
self.worker_processes: dict[int, CombinedProcess] = {}
|
|
38
|
+
self.stop_event = threading.Event()
|
|
39
|
+
self.watch_filter = PythonFilter(ignore_paths=None)
|
|
40
|
+
self.watcher = watch(
|
|
41
|
+
Path.cwd(),
|
|
42
|
+
watch_filter=self.watch_filter,
|
|
43
|
+
stop_event=self.stop_event,
|
|
44
|
+
yield_on_timeout=True,
|
|
45
|
+
)
|
|
46
|
+
self.setup_signals_handler()
|
|
47
|
+
self.monitor_thread = None
|
|
48
|
+
|
|
49
|
+
def setup_signals_handler(self):
|
|
50
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
51
|
+
signal.signal(sig, self.handle_signal)
|
|
52
|
+
|
|
53
|
+
def handle_signal(self, *args, **kwargs):
|
|
54
|
+
self.stop_event.set()
|
|
55
|
+
|
|
56
|
+
def start_worker(self, worker_id: int):
|
|
57
|
+
"""Start a single worker process"""
|
|
58
|
+
p = start_process(
|
|
59
|
+
self.start_fn,
|
|
60
|
+
"function",
|
|
61
|
+
(),
|
|
62
|
+
None,
|
|
63
|
+
)
|
|
64
|
+
logger.info(f"Worker {worker_id} [{p.pid}] started")
|
|
65
|
+
return p
|
|
66
|
+
|
|
67
|
+
def start(self):
|
|
68
|
+
for i in range(self.workers):
|
|
69
|
+
p = self.start_worker(i)
|
|
70
|
+
self.worker_processes[i] = p
|
|
71
|
+
|
|
72
|
+
def stop(self):
|
|
73
|
+
for process in self.worker_processes.values():
|
|
74
|
+
process.stop(sigint_timeout=5, sigkill_timeout=1)
|
|
75
|
+
self.worker_processes.clear()
|
|
76
|
+
|
|
77
|
+
def restart(self):
|
|
78
|
+
self.stop()
|
|
79
|
+
self.start()
|
|
80
|
+
|
|
81
|
+
def monitor_workers(self):
|
|
82
|
+
while not self.stop_event.is_set():
|
|
83
|
+
try:
|
|
84
|
+
for worker_id, process in self.worker_processes.items():
|
|
85
|
+
if not process.is_alive():
|
|
86
|
+
logger.warning(f"Worker {worker_id} [{process.pid}] died unexpectedly")
|
|
87
|
+
process.stop(sigint_timeout=1, sigkill_timeout=1)
|
|
88
|
+
new_process = self.start_worker(worker_id)
|
|
89
|
+
self.worker_processes[worker_id] = new_process
|
|
90
|
+
logger.info(
|
|
91
|
+
f"Restarted worker {worker_id} [{process.pid}] -> [{new_process.pid}]"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
time.sleep(MONITOR_THREAD_CHECK_DELAY)
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error(f"Error in worker monitoring: {e}")
|
|
98
|
+
time.sleep(MONITOR_THREAD_ERROR_DELAY)
|
|
99
|
+
|
|
100
|
+
def __iter__(self):
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def __next__(self):
|
|
104
|
+
changes = next(self.watcher)
|
|
105
|
+
if changes:
|
|
106
|
+
return list({Path(change[1]) for change in changes})
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def run(self):
|
|
110
|
+
self.start()
|
|
111
|
+
logger.info(f"Master process started: {os.getpid()}")
|
|
112
|
+
|
|
113
|
+
# Start worker monitoring thread
|
|
114
|
+
self.monitor_thread = threading.Thread(target=self.monitor_workers, daemon=True)
|
|
115
|
+
self.monitor_thread.start()
|
|
116
|
+
logger.debug("Worker monitoring thread started")
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
if self.reload:
|
|
120
|
+
for files_changed in self:
|
|
121
|
+
if files_changed:
|
|
122
|
+
logger.warning(
|
|
123
|
+
f"{', '.join(map(print_path, files_changed))} changed, reloading...",
|
|
124
|
+
)
|
|
125
|
+
self.restart()
|
|
126
|
+
else:
|
|
127
|
+
self.stop_event.wait()
|
|
128
|
+
finally:
|
|
129
|
+
if self.monitor_thread and self.monitor_thread.is_alive(): # pragma: no cover
|
|
130
|
+
logger.debug("Wait for monitor worker to exit")
|
|
131
|
+
self.monitor_thread.join(timeout=MONITOR_THREAD_JOIN_TIMEOUT)
|
|
132
|
+
self.stop()
|
|
@@ -397,7 +397,7 @@ class ZitiManagementAPI(BaseZitiAPI):
|
|
|
397
397
|
async def search_config_type(self, id_or_name: str) -> dict[str, Any] | None:
|
|
398
398
|
return await self.search_by_id_or_name("/config-types", id_or_name)
|
|
399
399
|
|
|
400
|
-
async def delete_config_type(self, config_type_id: str) ->
|
|
400
|
+
async def delete_config_type(self, config_type_id: str) -> None:
|
|
401
401
|
return await self.delete("/config-types", config_type_id)
|
|
402
402
|
|
|
403
403
|
async def get_identity(self, identity_id: str) -> dict[str, Any]:
|
|
@@ -305,6 +305,7 @@ async def test_register_instance(mocker: MockerFixture, api_client: AsyncClient)
|
|
|
305
305
|
"extension": {"id": "EXT-1234-5678"},
|
|
306
306
|
"instance": {"id": "INS-1234-5678-0001"},
|
|
307
307
|
"name": "ins-1234-5678-0001.ext-1234-5678",
|
|
308
|
+
"status": "offline",
|
|
308
309
|
"tags": {
|
|
309
310
|
MROK_VERSION_TAG_NAME: "0.0.0.dev0",
|
|
310
311
|
MROK_SERVICE_TAG_NAME: "ext-1234-5678",
|
|
@@ -321,11 +322,13 @@ async def test_register_instance(mocker: MockerFixture, api_client: AsyncClient)
|
|
|
321
322
|
|
|
322
323
|
|
|
323
324
|
@pytest.mark.asyncio
|
|
325
|
+
@pytest.mark.parametrize("status", ["online", "offline"])
|
|
324
326
|
async def test_get_instance(
|
|
325
327
|
mocker: MockerFixture,
|
|
326
328
|
settings_factory: SettingsFactory,
|
|
327
329
|
api_client: AsyncClient,
|
|
328
330
|
httpx_mock: HTTPXMock,
|
|
331
|
+
status: str,
|
|
329
332
|
):
|
|
330
333
|
mocker.patch(
|
|
331
334
|
"mrok.controller.routes.extensions.fetch_extension_or_404",
|
|
@@ -349,6 +352,7 @@ async def test_get_instance(
|
|
|
349
352
|
MROK_VERSION_TAG_NAME: "0.0.0.dev0",
|
|
350
353
|
MROK_SERVICE_TAG_NAME: "ext-1234-5678",
|
|
351
354
|
},
|
|
355
|
+
"hasEdgeRouterConnection": status == "online",
|
|
352
356
|
}
|
|
353
357
|
],
|
|
354
358
|
},
|
|
@@ -361,6 +365,7 @@ async def test_get_instance(
|
|
|
361
365
|
"extension": {"id": "EXT-1234-5678"},
|
|
362
366
|
"instance": {"id": "INS-1234-5678-0001"},
|
|
363
367
|
"name": "ins-1234-5678-0001.ext-1234-5678",
|
|
368
|
+
"status": status,
|
|
364
369
|
"tags": {
|
|
365
370
|
MROK_VERSION_TAG_NAME: "0.0.0.dev0",
|
|
366
371
|
MROK_SERVICE_TAG_NAME: "ext-1234-5678",
|
|
@@ -369,11 +374,13 @@ async def test_get_instance(
|
|
|
369
374
|
|
|
370
375
|
|
|
371
376
|
@pytest.mark.asyncio
|
|
377
|
+
@pytest.mark.parametrize("status", ["online", "offline"])
|
|
372
378
|
async def test_get_instance_by_instance_id(
|
|
373
379
|
mocker: MockerFixture,
|
|
374
380
|
settings_factory: SettingsFactory,
|
|
375
381
|
api_client: AsyncClient,
|
|
376
382
|
httpx_mock: HTTPXMock,
|
|
383
|
+
status: str,
|
|
377
384
|
):
|
|
378
385
|
mocker.patch(
|
|
379
386
|
"mrok.controller.routes.extensions.fetch_extension_or_404",
|
|
@@ -390,6 +397,7 @@ async def test_get_instance_by_instance_id(
|
|
|
390
397
|
{
|
|
391
398
|
"id": "ins1",
|
|
392
399
|
"name": "ins-1234-5678-0001.ext-1234-5678",
|
|
400
|
+
"hasEdgeRouterConnection": status == "online",
|
|
393
401
|
"tags": {
|
|
394
402
|
MROK_VERSION_TAG_NAME: "0.0.0.dev0",
|
|
395
403
|
MROK_SERVICE_TAG_NAME: "ext-1234-5678",
|
|
@@ -406,6 +414,7 @@ async def test_get_instance_by_instance_id(
|
|
|
406
414
|
"extension": {"id": "EXT-1234-5678"},
|
|
407
415
|
"instance": {"id": "INS-1234-5678-0001"},
|
|
408
416
|
"name": "ins-1234-5678-0001.ext-1234-5678",
|
|
417
|
+
"status": status,
|
|
409
418
|
"tags": {
|
|
410
419
|
MROK_VERSION_TAG_NAME: "0.0.0.dev0",
|
|
411
420
|
MROK_SERVICE_TAG_NAME: "ext-1234-5678",
|
|
@@ -487,3 +496,54 @@ async def test_delete_instance_extension_not_found(
|
|
|
487
496
|
|
|
488
497
|
response = await api_client.delete("/extensions/EXT-1234-5678/instances/INS-1234-5678-0001")
|
|
489
498
|
assert response.status_code == 404
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@pytest.mark.asyncio
|
|
502
|
+
@pytest.mark.parametrize(
|
|
503
|
+
("status", "expected_instance"),
|
|
504
|
+
[("online", "ins1.svc"), ("offline", "ins2.svc")],
|
|
505
|
+
)
|
|
506
|
+
async def test_get_extension_with_instances(
|
|
507
|
+
settings_factory: SettingsFactory,
|
|
508
|
+
api_client: AsyncClient,
|
|
509
|
+
httpx_mock: HTTPXMock,
|
|
510
|
+
status: str,
|
|
511
|
+
expected_instance: str,
|
|
512
|
+
):
|
|
513
|
+
settings = settings_factory()
|
|
514
|
+
query = quote(
|
|
515
|
+
f'(id="EXT-1234-5678" or name="ext-1234-5678") and tags.{MROK_VERSION_TAG_NAME} != null'
|
|
516
|
+
)
|
|
517
|
+
httpx_mock.add_response(
|
|
518
|
+
method="GET",
|
|
519
|
+
url=f"{settings.ziti.api.management}/edge/management/v1/services?filter={query}",
|
|
520
|
+
json={
|
|
521
|
+
"meta": {"pagination": {"totalCount": 1}},
|
|
522
|
+
"data": [
|
|
523
|
+
{
|
|
524
|
+
"id": "svc1",
|
|
525
|
+
"name": "ext-1234-5678",
|
|
526
|
+
"tags": {MROK_VERSION_TAG_NAME: "0.0.0.dev0"},
|
|
527
|
+
}
|
|
528
|
+
],
|
|
529
|
+
},
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
query = quote(f'tags.{MROK_SERVICE_TAG_NAME} = "ext-1234-5678"')
|
|
533
|
+
httpx_mock.add_response(
|
|
534
|
+
method="GET",
|
|
535
|
+
url=f"{settings.ziti.api.management}/edge/management/v1/identities?filter={query}&limit=5&offset=0",
|
|
536
|
+
json={
|
|
537
|
+
"meta": {"pagination": {"totalCount": 2, "limit": 5, "offset": 0}},
|
|
538
|
+
"data": [
|
|
539
|
+
{"id": "ins1", "name": "ins1.svc", "hasEdgeRouterConnection": True},
|
|
540
|
+
{"id": "ins2", "name": "ins2.svc", "hasEdgeRouterConnection": False},
|
|
541
|
+
],
|
|
542
|
+
},
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
response = await api_client.get(f"/extensions/EXT-1234-5678?with_instances={status}")
|
|
546
|
+
assert response.status_code == 200
|
|
547
|
+
ext = response.json()
|
|
548
|
+
assert len(ext["instances"]) == 1
|
|
549
|
+
assert ext["instances"][0]["name"] == expected_instance
|
|
@@ -17,7 +17,7 @@ async def test_list_instances(
|
|
|
17
17
|
settings = settings_factory()
|
|
18
18
|
httpx_mock.add_response(
|
|
19
19
|
method="GET",
|
|
20
|
-
url=f"{settings.ziti.api.management}/edge/management/v1/identities
|
|
20
|
+
url=f"{settings.ziti.api.management}/edge/management/v1/identities?filter=tags.mrok-identity-type%3D%22instance%22&limit=10&offset=0",
|
|
21
21
|
json={
|
|
22
22
|
"meta": {"pagination": {"totalCount": 15, "limit": 10, "offset": 0}},
|
|
23
23
|
"data": [{"id": f"ins{i}", "name": "ins.svc"} for i in range(10)],
|
|
@@ -25,7 +25,7 @@ async def test_list_instances(
|
|
|
25
25
|
)
|
|
26
26
|
httpx_mock.add_response(
|
|
27
27
|
method="GET",
|
|
28
|
-
url=f"{settings.ziti.api.management}/edge/management/v1/identities
|
|
28
|
+
url=f"{settings.ziti.api.management}/edge/management/v1/identities?filter=tags.mrok-identity-type%3D%22instance%22&limit=10&offset=10",
|
|
29
29
|
json={
|
|
30
30
|
"meta": {"pagination": {"totalCount": 15, "limit": 10, "offset": 10}},
|
|
31
31
|
"data": [{"id": f"ins{i}", "name": "ins.svc"} for i in range(11, 16)],
|
|
@@ -49,10 +49,12 @@ async def test_list_instances(
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
@pytest.mark.asyncio
|
|
52
|
+
@pytest.mark.parametrize("status", ["online", "offline"])
|
|
52
53
|
async def test_get_instance(
|
|
53
54
|
settings_factory: SettingsFactory,
|
|
54
55
|
api_client: AsyncClient,
|
|
55
56
|
httpx_mock: HTTPXMock,
|
|
57
|
+
status: str,
|
|
56
58
|
):
|
|
57
59
|
settings = settings_factory()
|
|
58
60
|
query = quote(
|
|
@@ -68,6 +70,7 @@ async def test_get_instance(
|
|
|
68
70
|
{
|
|
69
71
|
"id": "ins1",
|
|
70
72
|
"name": "ins-1234-1234-0001.ext-1234-1234",
|
|
73
|
+
"hasEdgeRouterConnection": status == "online",
|
|
71
74
|
"tags": {
|
|
72
75
|
MROK_VERSION_TAG_NAME: "0.0.0.dev0",
|
|
73
76
|
MROK_SERVICE_TAG_NAME: "ext-1234-1234",
|
|
@@ -85,6 +88,7 @@ async def test_get_instance(
|
|
|
85
88
|
"extension": {"id": "EXT-1234-1234"},
|
|
86
89
|
"instance": {"id": "INS-1234-1234-0001"},
|
|
87
90
|
"name": "ins-1234-1234-0001.ext-1234-1234",
|
|
91
|
+
"status": status,
|
|
88
92
|
"tags": {
|
|
89
93
|
MROK_VERSION_TAG_NAME: "0.0.0.dev0",
|
|
90
94
|
MROK_SERVICE_TAG_NAME: "ext-1234-1234",
|
|
@@ -93,10 +97,12 @@ async def test_get_instance(
|
|
|
93
97
|
|
|
94
98
|
|
|
95
99
|
@pytest.mark.asyncio
|
|
100
|
+
@pytest.mark.parametrize("status", ["online", "offline"])
|
|
96
101
|
async def test_get_instance_by_instance_id(
|
|
97
102
|
settings_factory: SettingsFactory,
|
|
98
103
|
api_client: AsyncClient,
|
|
99
104
|
httpx_mock: HTTPXMock,
|
|
105
|
+
status: str,
|
|
100
106
|
):
|
|
101
107
|
settings = settings_factory()
|
|
102
108
|
query = quote(f'(id="ins1" or name="ins1") and tags.{MROK_VERSION_TAG_NAME} != null')
|
|
@@ -109,6 +115,7 @@ async def test_get_instance_by_instance_id(
|
|
|
109
115
|
{
|
|
110
116
|
"id": "ins1",
|
|
111
117
|
"name": "ins-1234-1234-0001.ext-1234-1234",
|
|
118
|
+
"hasEdgeRouterConnection": status == "online",
|
|
112
119
|
"tags": {
|
|
113
120
|
MROK_VERSION_TAG_NAME: "0.0.0.dev0",
|
|
114
121
|
MROK_SERVICE_TAG_NAME: "ext-1234-1234",
|
|
@@ -125,6 +132,7 @@ async def test_get_instance_by_instance_id(
|
|
|
125
132
|
"extension": {"id": "EXT-1234-1234"},
|
|
126
133
|
"instance": {"id": "INS-1234-1234-0001"},
|
|
127
134
|
"name": "ins-1234-1234-0001.ext-1234-1234",
|
|
135
|
+
"status": status,
|
|
128
136
|
"tags": {
|
|
129
137
|
MROK_VERSION_TAG_NAME: "0.0.0.dev0",
|
|
130
138
|
MROK_SERVICE_TAG_NAME: "ext-1234-1234",
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import signal
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
2
4
|
from collections.abc import Generator
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
|
|
@@ -42,7 +44,7 @@ def test_start(mocker: MockerFixture):
|
|
|
42
44
|
start_fn = mocker.MagicMock()
|
|
43
45
|
master = Master(start_fn, 3, False)
|
|
44
46
|
master.start()
|
|
45
|
-
assert master.worker_processes == processes
|
|
47
|
+
assert master.worker_processes == {0: processes[0], 1: processes[1], 2: processes[2]}
|
|
46
48
|
for i in range(3):
|
|
47
49
|
assert mocked_start_process.mock_calls[i].args == (start_fn, "function", (), None)
|
|
48
50
|
|
|
@@ -51,7 +53,7 @@ def test_stop(mocker: MockerFixture):
|
|
|
51
53
|
master = Master(mocker.MagicMock(), 3, False)
|
|
52
54
|
p1 = mocker.MagicMock()
|
|
53
55
|
p2 = mocker.MagicMock()
|
|
54
|
-
master.worker_processes =
|
|
56
|
+
master.worker_processes = {0: p1, 2: p2}
|
|
55
57
|
master.stop()
|
|
56
58
|
p1.stop.assert_called_once_with(sigint_timeout=5, sigkill_timeout=1)
|
|
57
59
|
p2.stop.assert_called_once_with(sigint_timeout=5, sigkill_timeout=1)
|
|
@@ -84,17 +86,20 @@ def test_next(mocker: MockerFixture):
|
|
|
84
86
|
|
|
85
87
|
def test_run(mocker: MockerFixture):
|
|
86
88
|
mocked_start = mocker.patch.object(Master, "start")
|
|
89
|
+
mocked_monitor_fn = mocker.patch.object(Master, "monitor_workers")
|
|
87
90
|
master = Master(mocker.MagicMock(), 3, False)
|
|
88
91
|
mocked_stop_event = mocker.MagicMock()
|
|
89
92
|
master.stop_event = mocked_stop_event
|
|
90
93
|
master.run()
|
|
91
94
|
mocked_start.assert_called_once()
|
|
92
95
|
mocked_stop_event.wait.assert_called_once()
|
|
96
|
+
mocked_monitor_fn.assert_called_once()
|
|
93
97
|
|
|
94
98
|
|
|
95
99
|
def test_run_with_reload(mocker: MockerFixture):
|
|
96
100
|
mocker.patch.object(Master, "start")
|
|
97
101
|
mocked_restart = mocker.patch.object(Master, "restart")
|
|
102
|
+
mocked_monitor_fn = mocker.patch.object(Master, "monitor_workers")
|
|
98
103
|
|
|
99
104
|
def watcher() -> Generator:
|
|
100
105
|
yield {(Change.modified, "/file1.py")}
|
|
@@ -105,3 +110,57 @@ def test_run_with_reload(mocker: MockerFixture):
|
|
|
105
110
|
master.run()
|
|
106
111
|
|
|
107
112
|
mocked_restart.assert_called_once()
|
|
113
|
+
mocked_monitor_fn.assert_called_once()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_monitor_workers_restarts_dead_process(mocker: MockerFixture):
|
|
117
|
+
mocker.patch("mrok.http.master.MONITOR_THREAD_CHECK_DELAY", 0.1)
|
|
118
|
+
|
|
119
|
+
mock_start_process = mocker.patch("mrok.http.master.start_process")
|
|
120
|
+
master = Master(mocker.MagicMock(), 3, False)
|
|
121
|
+
|
|
122
|
+
dead_process = mocker.Mock()
|
|
123
|
+
dead_process.is_alive.return_value = False
|
|
124
|
+
dead_process.pid = 12345
|
|
125
|
+
|
|
126
|
+
alive_process = mocker.Mock()
|
|
127
|
+
alive_process.is_alive.return_value = True
|
|
128
|
+
alive_process.pid = 12346
|
|
129
|
+
|
|
130
|
+
new_process = mocker.Mock()
|
|
131
|
+
new_process.pid = 12347
|
|
132
|
+
|
|
133
|
+
master.worker_processes = {0: dead_process, 1: alive_process}
|
|
134
|
+
|
|
135
|
+
mock_start_process.return_value = new_process
|
|
136
|
+
|
|
137
|
+
monitor_thread = threading.Thread(target=master.monitor_workers)
|
|
138
|
+
monitor_thread.start()
|
|
139
|
+
time.sleep(0.1)
|
|
140
|
+
master.stop_event.set()
|
|
141
|
+
monitor_thread.join()
|
|
142
|
+
|
|
143
|
+
dead_process.stop.assert_called_once_with(sigint_timeout=1, sigkill_timeout=1)
|
|
144
|
+
mock_start_process.assert_called_once_with(master.start_fn, "function", (), None)
|
|
145
|
+
assert master.worker_processes[0] == new_process
|
|
146
|
+
assert master.worker_processes[1] == alive_process
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_monitor_workers_handles_is_alive_exception(mocker: MockerFixture):
|
|
150
|
+
mocker.patch("mrok.http.master.MONITOR_THREAD_ERROR_DELAY", 0.1)
|
|
151
|
+
mock_logger = mocker.patch("mrok.http.master.logger.error")
|
|
152
|
+
master = Master(mocker.MagicMock(), 3, False)
|
|
153
|
+
|
|
154
|
+
problematic_process = mocker.Mock()
|
|
155
|
+
problematic_process.is_alive.side_effect = Exception("Test exception")
|
|
156
|
+
|
|
157
|
+
master.worker_processes = {0: problematic_process}
|
|
158
|
+
|
|
159
|
+
monitor_thread = threading.Thread(target=master.monitor_workers)
|
|
160
|
+
monitor_thread.start()
|
|
161
|
+
|
|
162
|
+
time.sleep(0.1)
|
|
163
|
+
master.stop_event.set()
|
|
164
|
+
monitor_thread.join()
|
|
165
|
+
|
|
166
|
+
assert mock_logger.mock_calls[0].args[0] == "Error in worker monitoring: Test exception"
|
mrok-0.2.1/mrok/http/master.py
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import os
|
|
3
|
-
import signal
|
|
4
|
-
import threading
|
|
5
|
-
from collections.abc import Callable
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
from watchfiles import watch
|
|
9
|
-
from watchfiles.filters import PythonFilter
|
|
10
|
-
from watchfiles.run import CombinedProcess, start_process
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger("mrok.agent")
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def print_path(path):
|
|
16
|
-
try:
|
|
17
|
-
return f'"{path.relative_to(Path.cwd())}"'
|
|
18
|
-
except ValueError:
|
|
19
|
-
return f'"{path}"'
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class Master:
|
|
23
|
-
def __init__(
|
|
24
|
-
self,
|
|
25
|
-
start_fn: Callable,
|
|
26
|
-
workers: int,
|
|
27
|
-
reload: bool,
|
|
28
|
-
):
|
|
29
|
-
self.start_fn = start_fn
|
|
30
|
-
self.workers = workers
|
|
31
|
-
self.reload = reload
|
|
32
|
-
self.worker_processes: list[CombinedProcess] = []
|
|
33
|
-
self.stop_event = threading.Event()
|
|
34
|
-
self.watch_filter = PythonFilter(ignore_paths=None)
|
|
35
|
-
self.watcher = watch(
|
|
36
|
-
Path.cwd(),
|
|
37
|
-
watch_filter=self.watch_filter,
|
|
38
|
-
stop_event=self.stop_event,
|
|
39
|
-
yield_on_timeout=True,
|
|
40
|
-
)
|
|
41
|
-
self.setup_signals_handler()
|
|
42
|
-
|
|
43
|
-
def setup_signals_handler(self):
|
|
44
|
-
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
45
|
-
signal.signal(sig, self.handle_signal)
|
|
46
|
-
|
|
47
|
-
def handle_signal(self, *args, **kwargs):
|
|
48
|
-
self.stop_event.set()
|
|
49
|
-
|
|
50
|
-
def start(self):
|
|
51
|
-
for _ in range(self.workers):
|
|
52
|
-
p = start_process(
|
|
53
|
-
self.start_fn,
|
|
54
|
-
"function",
|
|
55
|
-
(),
|
|
56
|
-
None,
|
|
57
|
-
)
|
|
58
|
-
logger.info(f"Worker [{p.pid}] started")
|
|
59
|
-
self.worker_processes.append(p)
|
|
60
|
-
|
|
61
|
-
def stop(self):
|
|
62
|
-
for process in self.worker_processes:
|
|
63
|
-
process.stop(sigint_timeout=5, sigkill_timeout=1)
|
|
64
|
-
self.worker_processes = []
|
|
65
|
-
|
|
66
|
-
def restart(self):
|
|
67
|
-
self.stop()
|
|
68
|
-
self.start()
|
|
69
|
-
|
|
70
|
-
def __iter__(self):
|
|
71
|
-
return self
|
|
72
|
-
|
|
73
|
-
def __next__(self):
|
|
74
|
-
changes = next(self.watcher)
|
|
75
|
-
if changes:
|
|
76
|
-
return list({Path(change[1]) for change in changes})
|
|
77
|
-
return None
|
|
78
|
-
|
|
79
|
-
def run(self):
|
|
80
|
-
self.start()
|
|
81
|
-
logger.info(f"Master process started: {os.getpid()}")
|
|
82
|
-
if self.reload:
|
|
83
|
-
for files_changed in self:
|
|
84
|
-
if files_changed:
|
|
85
|
-
logger.warning(
|
|
86
|
-
f"{', '.join(map(print_path, files_changed))} changed, reloading...",
|
|
87
|
-
)
|
|
88
|
-
self.restart()
|
|
89
|
-
else:
|
|
90
|
-
self.stop_event.wait()
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|