iotsploit-core 0.0.6__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.
- iotsploit_core/__init__.py +7 -0
- iotsploit_core/context.py +27 -0
- iotsploit_core/core/__init__.py +0 -0
- iotsploit_core/core/base_plugin.py +290 -0
- iotsploit_core/core/device_config.py +105 -0
- iotsploit_core/core/device_manager.py +942 -0
- iotsploit_core/core/device_registry.py +118 -0
- iotsploit_core/core/device_scanner.py +74 -0
- iotsploit_core/core/device_spec.py +105 -0
- iotsploit_core/core/device_store.py +86 -0
- iotsploit_core/core/execution_backend.py +464 -0
- iotsploit_core/core/execution_queue.py +456 -0
- iotsploit_core/core/exploit_manager.py +728 -0
- iotsploit_core/core/exploit_spec.py +73 -0
- iotsploit_core/core/stream_manager.py +157 -0
- iotsploit_core/core/tool_config.py +430 -0
- iotsploit_core/core/tool_manager.py +1166 -0
- iotsploit_core/core/tool_service.py +936 -0
- iotsploit_core/domain/__init__.py +3 -0
- iotsploit_core/domain/device.py +70 -0
- iotsploit_core/domain/execution_plan.py +29 -0
- iotsploit_core/domain/plugin.py +17 -0
- iotsploit_core/domain/stream.py +63 -0
- iotsploit_core/domain/target.py +243 -0
- iotsploit_core/platforms/__init__.py +19 -0
- iotsploit_core/platforms/consts.py +25 -0
- iotsploit_core/ports/__init__.py +6 -0
- iotsploit_core/ports/driver_state_repo.py +15 -0
- iotsploit_core/ports/plugin_repo.py +26 -0
- iotsploit_core/ports/stream_backend.py +28 -0
- iotsploit_core/ports/task_runner.py +22 -0
- iotsploit_core/ports/wifi_backend.py +148 -0
- iotsploit_core/py.typed +2 -0
- iotsploit_core/utils/__init__.py +53 -0
- iotsploit_core/utils/exceptions.py +152 -0
- iotsploit_core/utils/helpers.py +65 -0
- iotsploit_core/utils/iots_logger.py +164 -0
- iotsploit_core/utils/result.py +41 -0
- iotsploit_core-0.0.6.dist-info/METADATA +117 -0
- iotsploit_core-0.0.6.dist-info/RECORD +41 -0
- iotsploit_core-0.0.6.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Execution queue and scheduler for tool execution."""
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
import threading
|
|
6
|
+
import queue
|
|
7
|
+
import logging
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Dict, List, Optional, Any, Callable, Union
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum, IntEnum
|
|
12
|
+
from concurrent.futures import ThreadPoolExecutor, Future
|
|
13
|
+
import uuid
|
|
14
|
+
|
|
15
|
+
from .execution_backend import ExecutionResult, get_execution_backend_manager
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
class TaskPriority(IntEnum):
|
|
20
|
+
"""Task priority levels (lower number = higher priority)"""
|
|
21
|
+
CRITICAL = 0
|
|
22
|
+
HIGH = 1
|
|
23
|
+
NORMAL = 2
|
|
24
|
+
LOW = 3
|
|
25
|
+
BACKGROUND = 4
|
|
26
|
+
|
|
27
|
+
class TaskStatus(Enum):
|
|
28
|
+
"""Task execution status"""
|
|
29
|
+
PENDING = "pending"
|
|
30
|
+
QUEUED = "queued"
|
|
31
|
+
RUNNING = "running"
|
|
32
|
+
COMPLETED = "completed"
|
|
33
|
+
FAILED = "failed"
|
|
34
|
+
CANCELLED = "cancelled"
|
|
35
|
+
TIMEOUT = "timeout"
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ExecutionTask:
|
|
39
|
+
"""Represents a tool execution task"""
|
|
40
|
+
task_id: str
|
|
41
|
+
tool_name: str
|
|
42
|
+
tool_path: str
|
|
43
|
+
args: List[str]
|
|
44
|
+
priority: TaskPriority = TaskPriority.NORMAL
|
|
45
|
+
timeout: Optional[int] = None
|
|
46
|
+
working_dir: Optional[str] = None
|
|
47
|
+
env: Optional[Dict[str, str]] = None
|
|
48
|
+
backend: Optional[str] = None
|
|
49
|
+
callback: Optional[Callable] = None
|
|
50
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
# Status tracking
|
|
53
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
54
|
+
created_at: float = field(default_factory=time.time)
|
|
55
|
+
started_at: Optional[float] = None
|
|
56
|
+
completed_at: Optional[float] = None
|
|
57
|
+
result: Optional[ExecutionResult] = None
|
|
58
|
+
error: Optional[str] = None
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class QueueStats:
|
|
62
|
+
"""Execution queue statistics"""
|
|
63
|
+
total_tasks: int = 0
|
|
64
|
+
pending_tasks: int = 0
|
|
65
|
+
running_tasks: int = 0
|
|
66
|
+
completed_tasks: int = 0
|
|
67
|
+
failed_tasks: int = 0
|
|
68
|
+
cancelled_tasks: int = 0
|
|
69
|
+
average_execution_time: float = 0.0
|
|
70
|
+
queue_size: int = 0
|
|
71
|
+
|
|
72
|
+
class ExecutionQueue:
|
|
73
|
+
"""Priority-based execution queue with scheduling"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, max_workers: int = 4, max_queue_size: int = 1000):
|
|
76
|
+
self.max_workers = max_workers
|
|
77
|
+
self.max_queue_size = max_queue_size
|
|
78
|
+
|
|
79
|
+
# Task storage
|
|
80
|
+
self.tasks: Dict[str, ExecutionTask] = {}
|
|
81
|
+
self.task_queue = queue.PriorityQueue(maxsize=max_queue_size)
|
|
82
|
+
|
|
83
|
+
# Execution management
|
|
84
|
+
self.executor = ThreadPoolExecutor(max_workers=max_workers)
|
|
85
|
+
self.running_tasks: Dict[str, Future] = {}
|
|
86
|
+
|
|
87
|
+
# Synchronization
|
|
88
|
+
self._lock = threading.RLock()
|
|
89
|
+
self._shutdown = False
|
|
90
|
+
|
|
91
|
+
# Statistics
|
|
92
|
+
self.stats = QueueStats()
|
|
93
|
+
|
|
94
|
+
# Backend manager
|
|
95
|
+
self.backend_manager = get_execution_backend_manager()
|
|
96
|
+
|
|
97
|
+
logger.info(f"Execution queue initialized with {max_workers} workers")
|
|
98
|
+
|
|
99
|
+
def submit_task(self, tool_name: str, tool_path: str, args: List[str],
|
|
100
|
+
priority: TaskPriority = TaskPriority.NORMAL,
|
|
101
|
+
timeout: Optional[int] = None,
|
|
102
|
+
working_dir: Optional[str] = None,
|
|
103
|
+
env: Optional[Dict[str, str]] = None,
|
|
104
|
+
backend: Optional[str] = None,
|
|
105
|
+
callback: Optional[Callable] = None,
|
|
106
|
+
metadata: Optional[Dict[str, Any]] = None) -> str:
|
|
107
|
+
"""Submit a task for execution"""
|
|
108
|
+
|
|
109
|
+
if self._shutdown:
|
|
110
|
+
raise RuntimeError("Execution queue is shutdown")
|
|
111
|
+
|
|
112
|
+
if self.task_queue.qsize() >= self.max_queue_size:
|
|
113
|
+
raise RuntimeError("Execution queue is full")
|
|
114
|
+
|
|
115
|
+
# Create task
|
|
116
|
+
task_id = str(uuid.uuid4())
|
|
117
|
+
task = ExecutionTask(
|
|
118
|
+
task_id=task_id,
|
|
119
|
+
tool_name=tool_name,
|
|
120
|
+
tool_path=tool_path,
|
|
121
|
+
args=args,
|
|
122
|
+
priority=priority,
|
|
123
|
+
timeout=timeout,
|
|
124
|
+
working_dir=working_dir,
|
|
125
|
+
env=env,
|
|
126
|
+
backend=backend,
|
|
127
|
+
callback=callback,
|
|
128
|
+
metadata=metadata or {}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
with self._lock:
|
|
132
|
+
# Store task
|
|
133
|
+
self.tasks[task_id] = task
|
|
134
|
+
task.status = TaskStatus.QUEUED
|
|
135
|
+
|
|
136
|
+
# Add to priority queue (priority, creation_time, task_id)
|
|
137
|
+
self.task_queue.put((priority.value, task.created_at, task_id))
|
|
138
|
+
|
|
139
|
+
# Update stats
|
|
140
|
+
self.stats.total_tasks += 1
|
|
141
|
+
self.stats.pending_tasks += 1
|
|
142
|
+
self.stats.queue_size = self.task_queue.qsize()
|
|
143
|
+
|
|
144
|
+
logger.debug(f"Task {task_id} queued with priority {priority.name}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
self._process_queue()
|
|
148
|
+
|
|
149
|
+
return task_id
|
|
150
|
+
|
|
151
|
+
def _process_queue(self):
|
|
152
|
+
"""Process queued tasks"""
|
|
153
|
+
with self._lock:
|
|
154
|
+
# Check if we can start more tasks
|
|
155
|
+
while (len(self.running_tasks) < self.max_workers and
|
|
156
|
+
not self.task_queue.empty() and
|
|
157
|
+
not self._shutdown):
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
# Get highest priority task
|
|
161
|
+
priority, created_at, task_id = self.task_queue.get_nowait()
|
|
162
|
+
|
|
163
|
+
if task_id not in self.tasks:
|
|
164
|
+
continue # Task was cancelled
|
|
165
|
+
|
|
166
|
+
task = self.tasks[task_id]
|
|
167
|
+
|
|
168
|
+
# Submit for execution
|
|
169
|
+
future = self.executor.submit(self._execute_task, task)
|
|
170
|
+
self.running_tasks[task_id] = future
|
|
171
|
+
|
|
172
|
+
# Update task status
|
|
173
|
+
task.status = TaskStatus.RUNNING
|
|
174
|
+
task.started_at = time.time()
|
|
175
|
+
|
|
176
|
+
# Update stats
|
|
177
|
+
self.stats.pending_tasks -= 1
|
|
178
|
+
self.stats.running_tasks += 1
|
|
179
|
+
self.stats.queue_size = self.task_queue.qsize()
|
|
180
|
+
|
|
181
|
+
logger.debug(f"Started execution of task {task_id}")
|
|
182
|
+
|
|
183
|
+
except queue.Empty:
|
|
184
|
+
break
|
|
185
|
+
|
|
186
|
+
def _execute_task(self, task: ExecutionTask) -> ExecutionResult:
|
|
187
|
+
"""Execute a single task"""
|
|
188
|
+
try:
|
|
189
|
+
logger.info(f"Executing task {task.task_id}: {task.tool_name}")
|
|
190
|
+
|
|
191
|
+
# Execute using backend manager
|
|
192
|
+
result = self.backend_manager.execute(
|
|
193
|
+
tool_path=task.tool_path,
|
|
194
|
+
args=task.args,
|
|
195
|
+
backend=task.backend,
|
|
196
|
+
working_dir=task.working_dir,
|
|
197
|
+
env=task.env,
|
|
198
|
+
timeout=task.timeout
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Update task
|
|
202
|
+
with self._lock:
|
|
203
|
+
task.result = result
|
|
204
|
+
task.completed_at = time.time()
|
|
205
|
+
task.status = TaskStatus.COMPLETED if result.success else TaskStatus.FAILED
|
|
206
|
+
|
|
207
|
+
# Remove from running tasks
|
|
208
|
+
if task.task_id in self.running_tasks:
|
|
209
|
+
del self.running_tasks[task.task_id]
|
|
210
|
+
|
|
211
|
+
# Update stats
|
|
212
|
+
self.stats.running_tasks -= 1
|
|
213
|
+
if result.success:
|
|
214
|
+
self.stats.completed_tasks += 1
|
|
215
|
+
else:
|
|
216
|
+
self.stats.failed_tasks += 1
|
|
217
|
+
|
|
218
|
+
# Update average execution time
|
|
219
|
+
self._update_average_execution_time(result.execution_time)
|
|
220
|
+
|
|
221
|
+
# Call callback if provided
|
|
222
|
+
if task.callback:
|
|
223
|
+
try:
|
|
224
|
+
task.callback(task, result)
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error(f"Task callback failed: {e}")
|
|
227
|
+
|
|
228
|
+
logger.info(f"Task {task.task_id} completed: {result.success}")
|
|
229
|
+
|
|
230
|
+
# Process more tasks
|
|
231
|
+
self._process_queue()
|
|
232
|
+
|
|
233
|
+
return result
|
|
234
|
+
|
|
235
|
+
except Exception as e:
|
|
236
|
+
logger.error(f"Task {task.task_id} execution failed: {e}")
|
|
237
|
+
|
|
238
|
+
# Update task
|
|
239
|
+
with self._lock:
|
|
240
|
+
task.error = str(e)
|
|
241
|
+
task.completed_at = time.time()
|
|
242
|
+
task.status = TaskStatus.FAILED
|
|
243
|
+
|
|
244
|
+
# Remove from running tasks
|
|
245
|
+
if task.task_id in self.running_tasks:
|
|
246
|
+
del self.running_tasks[task.task_id]
|
|
247
|
+
|
|
248
|
+
# Update stats
|
|
249
|
+
self.stats.running_tasks -= 1
|
|
250
|
+
self.stats.failed_tasks += 1
|
|
251
|
+
|
|
252
|
+
# Process more tasks
|
|
253
|
+
self._process_queue()
|
|
254
|
+
|
|
255
|
+
raise
|
|
256
|
+
|
|
257
|
+
def _update_average_execution_time(self, execution_time: float):
|
|
258
|
+
"""Update average execution time"""
|
|
259
|
+
completed = self.stats.completed_tasks
|
|
260
|
+
if completed == 1:
|
|
261
|
+
self.stats.average_execution_time = execution_time
|
|
262
|
+
else:
|
|
263
|
+
# Running average
|
|
264
|
+
current_avg = self.stats.average_execution_time
|
|
265
|
+
self.stats.average_execution_time = (
|
|
266
|
+
(current_avg * (completed - 1) + execution_time) / completed
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def get_task(self, task_id: str) -> Optional[ExecutionTask]:
|
|
270
|
+
"""Get task by ID"""
|
|
271
|
+
with self._lock:
|
|
272
|
+
return self.tasks.get(task_id)
|
|
273
|
+
|
|
274
|
+
def cancel_task(self, task_id: str) -> bool:
|
|
275
|
+
"""Cancel a task"""
|
|
276
|
+
with self._lock:
|
|
277
|
+
task = self.tasks.get(task_id)
|
|
278
|
+
if not task:
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
if task.status == TaskStatus.RUNNING:
|
|
282
|
+
# Try to cancel running task
|
|
283
|
+
future = self.running_tasks.get(task_id)
|
|
284
|
+
if future and future.cancel():
|
|
285
|
+
task.status = TaskStatus.CANCELLED
|
|
286
|
+
task.completed_at = time.time()
|
|
287
|
+
del self.running_tasks[task_id]
|
|
288
|
+
self.stats.running_tasks -= 1
|
|
289
|
+
self.stats.cancelled_tasks += 1
|
|
290
|
+
return True
|
|
291
|
+
return False
|
|
292
|
+
|
|
293
|
+
elif task.status in [TaskStatus.PENDING, TaskStatus.QUEUED]:
|
|
294
|
+
# Cancel queued task
|
|
295
|
+
task.status = TaskStatus.CANCELLED
|
|
296
|
+
task.completed_at = time.time()
|
|
297
|
+
self.stats.pending_tasks -= 1
|
|
298
|
+
self.stats.cancelled_tasks += 1
|
|
299
|
+
return True
|
|
300
|
+
|
|
301
|
+
return False
|
|
302
|
+
|
|
303
|
+
def wait_for_task(self, task_id: str, timeout: Optional[float] = None) -> Optional[ExecutionResult]:
|
|
304
|
+
"""Wait for task completion"""
|
|
305
|
+
task = self.get_task(task_id)
|
|
306
|
+
if not task:
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
start_time = time.time()
|
|
310
|
+
while task.status in [TaskStatus.PENDING, TaskStatus.QUEUED, TaskStatus.RUNNING]:
|
|
311
|
+
if timeout and (time.time() - start_time) > timeout:
|
|
312
|
+
return None
|
|
313
|
+
time.sleep(0.1)
|
|
314
|
+
|
|
315
|
+
return task.result
|
|
316
|
+
|
|
317
|
+
def list_tasks(self, status: Optional[TaskStatus] = None) -> List[ExecutionTask]:
|
|
318
|
+
"""List tasks, optionally filtered by status"""
|
|
319
|
+
with self._lock:
|
|
320
|
+
tasks = list(self.tasks.values())
|
|
321
|
+
if status:
|
|
322
|
+
tasks = [t for t in tasks if t.status == status]
|
|
323
|
+
return sorted(tasks, key=lambda t: t.created_at)
|
|
324
|
+
|
|
325
|
+
def get_stats(self) -> QueueStats:
|
|
326
|
+
"""Get queue statistics"""
|
|
327
|
+
with self._lock:
|
|
328
|
+
stats = QueueStats(
|
|
329
|
+
total_tasks=self.stats.total_tasks,
|
|
330
|
+
pending_tasks=self.stats.pending_tasks,
|
|
331
|
+
running_tasks=self.stats.running_tasks,
|
|
332
|
+
completed_tasks=self.stats.completed_tasks,
|
|
333
|
+
failed_tasks=self.stats.failed_tasks,
|
|
334
|
+
cancelled_tasks=self.stats.cancelled_tasks,
|
|
335
|
+
average_execution_time=self.stats.average_execution_time,
|
|
336
|
+
queue_size=self.task_queue.qsize()
|
|
337
|
+
)
|
|
338
|
+
return stats
|
|
339
|
+
|
|
340
|
+
def clear_completed_tasks(self, older_than: Optional[float] = None):
|
|
341
|
+
"""Clear completed tasks"""
|
|
342
|
+
with self._lock:
|
|
343
|
+
current_time = time.time()
|
|
344
|
+
to_remove = []
|
|
345
|
+
|
|
346
|
+
for task_id, task in self.tasks.items():
|
|
347
|
+
if task.status in [TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED]:
|
|
348
|
+
if older_than is None or (task.completed_at and
|
|
349
|
+
(current_time - task.completed_at) > older_than):
|
|
350
|
+
to_remove.append(task_id)
|
|
351
|
+
|
|
352
|
+
for task_id in to_remove:
|
|
353
|
+
del self.tasks[task_id]
|
|
354
|
+
|
|
355
|
+
logger.info(f"Cleared {len(to_remove)} completed tasks")
|
|
356
|
+
|
|
357
|
+
def shutdown(self, wait: bool = True):
|
|
358
|
+
"""Shutdown the execution queue"""
|
|
359
|
+
logger.info("Shutting down execution queue...")
|
|
360
|
+
|
|
361
|
+
with self._lock:
|
|
362
|
+
self._shutdown = True
|
|
363
|
+
|
|
364
|
+
if wait:
|
|
365
|
+
# Cancel all pending tasks
|
|
366
|
+
for task_id, task in list(self.tasks.items()):
|
|
367
|
+
if task.status in [TaskStatus.PENDING, TaskStatus.QUEUED]:
|
|
368
|
+
self.cancel_task(task_id)
|
|
369
|
+
|
|
370
|
+
# Shutdown executor
|
|
371
|
+
self.executor.shutdown(wait=wait)
|
|
372
|
+
logger.info("Execution queue shutdown complete")
|
|
373
|
+
|
|
374
|
+
class TaskScheduler:
|
|
375
|
+
"""Task scheduler with simple cron-like capabilities"""
|
|
376
|
+
|
|
377
|
+
def __init__(self, execution_queue: ExecutionQueue):
|
|
378
|
+
self.execution_queue = execution_queue
|
|
379
|
+
self.scheduled_tasks: Dict[str, Dict] = {}
|
|
380
|
+
self._scheduler_thread = None
|
|
381
|
+
self._shutdown = False
|
|
382
|
+
self._lock = threading.Lock()
|
|
383
|
+
|
|
384
|
+
def schedule_task(self, schedule_id: str, tool_name: str, tool_path: str,
|
|
385
|
+
args: List[str], cron_expression: str, **kwargs) -> str:
|
|
386
|
+
"""Schedule a recurring task"""
|
|
387
|
+
with self._lock:
|
|
388
|
+
self.scheduled_tasks[schedule_id] = {
|
|
389
|
+
'tool_name': tool_name,
|
|
390
|
+
'tool_path': tool_path,
|
|
391
|
+
'args': args,
|
|
392
|
+
'cron_expression': cron_expression,
|
|
393
|
+
'kwargs': kwargs,
|
|
394
|
+
'last_run': None,
|
|
395
|
+
'next_run': self._calculate_next_run(cron_expression)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if not self._scheduler_thread:
|
|
399
|
+
self._start_scheduler()
|
|
400
|
+
|
|
401
|
+
return schedule_id
|
|
402
|
+
|
|
403
|
+
def _calculate_next_run(self, cron_expression: str) -> float:
|
|
404
|
+
"""Calculate next run time (simplified)"""
|
|
405
|
+
return time.time() + 60
|
|
406
|
+
|
|
407
|
+
def _start_scheduler(self):
|
|
408
|
+
"""Start the scheduler thread"""
|
|
409
|
+
self._scheduler_thread = threading.Thread(target=self._scheduler_loop, daemon=True)
|
|
410
|
+
self._scheduler_thread.start()
|
|
411
|
+
|
|
412
|
+
def _scheduler_loop(self):
|
|
413
|
+
"""Main scheduler loop"""
|
|
414
|
+
while not self._shutdown:
|
|
415
|
+
current_time = time.time()
|
|
416
|
+
|
|
417
|
+
with self._lock:
|
|
418
|
+
for schedule_id, task_info in self.scheduled_tasks.items():
|
|
419
|
+
if current_time >= task_info['next_run']:
|
|
420
|
+
self.execution_queue.submit_task(
|
|
421
|
+
tool_name=task_info['tool_name'],
|
|
422
|
+
tool_path=task_info['tool_path'],
|
|
423
|
+
args=task_info['args'],
|
|
424
|
+
**task_info['kwargs']
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
# Update next run
|
|
428
|
+
task_info['last_run'] = current_time
|
|
429
|
+
task_info['next_run'] = self._calculate_next_run(
|
|
430
|
+
task_info['cron_expression']
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
time.sleep(1)
|
|
434
|
+
|
|
435
|
+
def shutdown(self):
|
|
436
|
+
"""Shutdown the scheduler"""
|
|
437
|
+
self._shutdown = True
|
|
438
|
+
if self._scheduler_thread:
|
|
439
|
+
self._scheduler_thread.join()
|
|
440
|
+
|
|
441
|
+
_execution_queue = None
|
|
442
|
+
_task_scheduler = None
|
|
443
|
+
|
|
444
|
+
def get_execution_queue() -> ExecutionQueue:
|
|
445
|
+
"""Get singleton execution queue"""
|
|
446
|
+
global _execution_queue
|
|
447
|
+
if _execution_queue is None:
|
|
448
|
+
_execution_queue = ExecutionQueue()
|
|
449
|
+
return _execution_queue
|
|
450
|
+
|
|
451
|
+
def get_task_scheduler() -> TaskScheduler:
|
|
452
|
+
"""Get singleton task scheduler"""
|
|
453
|
+
global _task_scheduler
|
|
454
|
+
if _task_scheduler is None:
|
|
455
|
+
_task_scheduler = TaskScheduler(get_execution_queue())
|
|
456
|
+
return _task_scheduler
|