2024-08-26 14:55:36 -04:00
|
|
|
#!/usr/bin/env python3
|
2026-03-11 23:15:07 -04:00
|
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
2024-08-26 14:55:36 -04:00
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
|
2023-02-18 23:20:42 -05:00
|
|
|
import asyncio
|
|
|
|
|
from collections import deque
|
|
|
|
|
|
2025-11-20 02:44:44 -05:00
|
|
|
try:
|
|
|
|
|
import uvloop
|
|
|
|
|
except ImportError:
|
|
|
|
|
uvloop = None
|
2023-02-18 23:20:42 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def run_in_serial(task_list):
|
|
|
|
|
q = asyncio.Queue()
|
|
|
|
|
for task in task_list:
|
|
|
|
|
await q.put(task)
|
|
|
|
|
lst_result = []
|
|
|
|
|
for i in range(len(task_list)):
|
|
|
|
|
co = await q.get()
|
|
|
|
|
result = await co
|
|
|
|
|
lst_result.append(result)
|
|
|
|
|
return lst_result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def run_command_get_output(*args, cwd=None):
|
|
|
|
|
if cwd is not None:
|
|
|
|
|
process = await asyncio.create_subprocess_exec(
|
|
|
|
|
*args,
|
|
|
|
|
# stdout must a pipe to be accessible as process.stdout
|
|
|
|
|
stdout=asyncio.subprocess.PIPE,
|
|
|
|
|
stderr=asyncio.subprocess.PIPE,
|
|
|
|
|
cwd=cwd,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
process = await asyncio.create_subprocess_exec(
|
|
|
|
|
*args,
|
|
|
|
|
# stdout must a pipe to be accessible as process.stdout
|
|
|
|
|
stdout=asyncio.subprocess.PIPE,
|
|
|
|
|
stderr=asyncio.subprocess.PIPE,
|
|
|
|
|
)
|
|
|
|
|
# Wait for the subprocess to finish
|
|
|
|
|
stdout, stderr = await process.communicate()
|
|
|
|
|
return stdout.decode()
|
|
|
|
|
|
|
|
|
|
|
2024-01-23 22:04:15 -05:00
|
|
|
async def run_shell_get_output(cmd, cwd=None):
|
|
|
|
|
if cwd is not None:
|
|
|
|
|
process = await asyncio.create_subprocess_shell(
|
|
|
|
|
cmd,
|
|
|
|
|
# stdout must a pipe to be accessible as process.stdout
|
|
|
|
|
stdout=asyncio.subprocess.PIPE,
|
|
|
|
|
stderr=asyncio.subprocess.PIPE,
|
|
|
|
|
cwd=cwd,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
process = await asyncio.create_subprocess_shell(
|
|
|
|
|
cmd,
|
|
|
|
|
# stdout must a pipe to be accessible as process.stdout
|
|
|
|
|
stdout=asyncio.subprocess.PIPE,
|
|
|
|
|
stderr=asyncio.subprocess.PIPE,
|
|
|
|
|
)
|
|
|
|
|
# Wait for the subprocess to finish
|
|
|
|
|
stdout, stderr = await process.communicate()
|
|
|
|
|
return stdout.decode()
|
|
|
|
|
|
|
|
|
|
|
2023-02-18 23:20:42 -05:00
|
|
|
async def run_command_get_output_and_status(*args, cwd=None):
|
|
|
|
|
if cwd is not None:
|
|
|
|
|
process = await asyncio.create_subprocess_exec(
|
|
|
|
|
*args,
|
|
|
|
|
# stdout must a pipe to be accessible as process.stdout
|
|
|
|
|
stdout=asyncio.subprocess.PIPE,
|
|
|
|
|
stderr=asyncio.subprocess.PIPE,
|
|
|
|
|
cwd=cwd,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
process = await asyncio.create_subprocess_exec(
|
|
|
|
|
*args,
|
|
|
|
|
# stdout must a pipe to be accessible as process.stdout
|
|
|
|
|
stdout=asyncio.subprocess.PIPE,
|
|
|
|
|
stderr=asyncio.subprocess.PIPE,
|
|
|
|
|
)
|
|
|
|
|
# Wait for the subprocess to finish
|
|
|
|
|
stdout, stderr = await process.communicate()
|
|
|
|
|
return stdout.decode(), stderr.decode(), process.returncode
|
|
|
|
|
|
|
|
|
|
|
2024-01-23 22:04:15 -05:00
|
|
|
def print_summary_task(task_list, lst_task_name=None):
|
|
|
|
|
for i, task in enumerate(task_list):
|
|
|
|
|
if lst_task_name:
|
|
|
|
|
task_name = lst_task_name[i]
|
|
|
|
|
print(f"{i} - {task.cr_code.co_name} - {task_name}")
|
|
|
|
|
else:
|
|
|
|
|
print(f"{i} - {task.cr_code.co_name}")
|
2023-02-18 23:20:42 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def execute(config, lst_task, use_uvloop=False):
|
2024-01-23 22:04:15 -05:00
|
|
|
error_detected = False
|
|
|
|
|
return_value = None
|
2023-02-18 23:20:42 -05:00
|
|
|
|
2025-11-20 02:44:44 -05:00
|
|
|
# --- Mode 1 : pas de parallélisme → déjà OK avec asyncio.run ---
|
2023-02-18 23:20:42 -05:00
|
|
|
if config.no_parallel:
|
2025-11-20 02:44:44 -05:00
|
|
|
# run_in_serial doit être une coroutine (async def)
|
|
|
|
|
try:
|
|
|
|
|
if use_uvloop and uvloop is not None:
|
|
|
|
|
# À faire AVANT la création de la loop
|
|
|
|
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
|
|
|
|
return_value = asyncio.run(run_in_serial(lst_task))
|
|
|
|
|
except Exception as e:
|
|
|
|
|
error_detected = True
|
|
|
|
|
print(e)
|
|
|
|
|
return return_value, error_detected
|
|
|
|
|
|
|
|
|
|
# --- Mode 2 : AsyncioPool (on suppose qu'il gère sa loop lui-même) ---
|
|
|
|
|
if config.max_process:
|
2023-02-18 23:20:42 -05:00
|
|
|
pool = AsyncioPool(config.max_process)
|
|
|
|
|
for task in lst_task:
|
|
|
|
|
pool.add_coro(task)
|
|
|
|
|
try:
|
2025-11-20 02:44:44 -05:00
|
|
|
# API existante, on ne touche pas
|
2023-02-18 23:20:42 -05:00
|
|
|
return_value = pool.run_until_complete()
|
2025-11-20 02:44:44 -05:00
|
|
|
except Exception as e:
|
2024-01-23 22:04:15 -05:00
|
|
|
error_detected = True
|
|
|
|
|
print(e)
|
2023-02-18 23:20:42 -05:00
|
|
|
finally:
|
2025-11-20 02:44:44 -05:00
|
|
|
pool.close()
|
|
|
|
|
return return_value, error_detected
|
|
|
|
|
|
|
|
|
|
# --- Mode 3 : utilisation maximale des ressources avec asyncio.gather ---
|
|
|
|
|
async def _run_max_resources(tasks, debug: bool):
|
|
|
|
|
if debug:
|
|
|
|
|
loop = asyncio.get_running_loop()
|
|
|
|
|
loop.set_debug(True)
|
|
|
|
|
# tasks doit être une liste de coroutines
|
|
|
|
|
results = await asyncio.gather(*tasks)
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
if use_uvloop and uvloop is not None:
|
|
|
|
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
|
|
|
|
return_value = asyncio.run(_run_max_resources(lst_task, config.debug))
|
|
|
|
|
except RuntimeError as e:
|
|
|
|
|
# par ex. si tu appelles ça depuis un environnement où il y a
|
|
|
|
|
# déjà une loop (Jupyter, certains frameworks)
|
|
|
|
|
error_detected = True
|
|
|
|
|
print(e)
|
|
|
|
|
|
2024-01-23 22:04:15 -05:00
|
|
|
return return_value, error_detected
|
2023-02-18 23:20:42 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class AsyncioPool:
|
|
|
|
|
# TODO check to replace this pool by https://github.com/dano/aioprocessing
|
|
|
|
|
def __init__(self, concurrency, loop=None):
|
|
|
|
|
"""
|
|
|
|
|
@param loop: asyncio loop
|
|
|
|
|
@param concurrency: Maximum number of concurrently running tasks
|
|
|
|
|
"""
|
|
|
|
|
self._loop = loop or asyncio.get_event_loop()
|
|
|
|
|
self._concurrency = concurrency
|
|
|
|
|
self._coros = deque([]) # All coroutines queued for execution
|
|
|
|
|
self._futures = [] # All currently running coroutines
|
|
|
|
|
self._lst_result = []
|
|
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
|
self._loop.close()
|
|
|
|
|
|
|
|
|
|
def add_coro(self, coro):
|
|
|
|
|
"""
|
|
|
|
|
@param coro: coroutine to add
|
|
|
|
|
"""
|
|
|
|
|
self._coros.append(coro)
|
|
|
|
|
self.print_status()
|
|
|
|
|
|
|
|
|
|
def run_until_complete(self):
|
|
|
|
|
self._loop.run_until_complete(self._wait_for_futures())
|
|
|
|
|
return self._lst_result
|
|
|
|
|
|
|
|
|
|
def print_status(self):
|
|
|
|
|
print(
|
|
|
|
|
" Status: coros:%s - futures:%s"
|
|
|
|
|
% (len(self._coros), len(self._futures))
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _start_futures(self):
|
|
|
|
|
num_to_start = self._concurrency - len(self._futures)
|
|
|
|
|
num_to_start = min(num_to_start, len(self._coros))
|
|
|
|
|
for _ in range(num_to_start):
|
|
|
|
|
coro = self._coros.popleft()
|
|
|
|
|
future = asyncio.ensure_future(coro, loop=self._loop)
|
|
|
|
|
self._futures.append(future)
|
|
|
|
|
self.print_status()
|
|
|
|
|
|
|
|
|
|
async def _wait_for_futures(self):
|
|
|
|
|
while len(self._coros) > 0 or len(self._futures) > 0:
|
|
|
|
|
self._start_futures()
|
|
|
|
|
futures_completed, futures_pending = await asyncio.wait(
|
|
|
|
|
self._futures,
|
|
|
|
|
loop=self._loop,
|
|
|
|
|
return_when=asyncio.FIRST_COMPLETED,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for future in futures_completed:
|
|
|
|
|
self._lst_result.append(future.result())
|
|
|
|
|
self._futures.remove(future)
|
|
|
|
|
self._start_futures()
|