|
4 | 4 | # -------------------------------------------------------------------------------------------- |
5 | 5 | # pylint: disable=line-too-long |
6 | 6 |
|
7 | | -__version__ = "2.82.0" |
| 7 | +__version__ = "2.83.0" |
8 | 8 |
|
9 | 9 | import os |
10 | 10 | import sys |
11 | 11 | import timeit |
12 | | -import concurrent.futures |
13 | | -from concurrent.futures import ThreadPoolExecutor |
14 | 12 |
|
15 | 13 | from knack.cli import CLI |
16 | 14 | from knack.commands import CLICommandsLoader |
|
36 | 34 | ALWAYS_LOADED_MODULES = [] |
37 | 35 | # Extensions that will always be loaded if installed. They don't expose commands but hook into CLI core. |
38 | 36 | ALWAYS_LOADED_EXTENSIONS = ['azext_ai_examples', 'azext_next'] |
39 | | -# Timeout (in seconds) for loading a single module. Acts as a safety valve to prevent indefinite hangs |
40 | | -MODULE_LOAD_TIMEOUT_SECONDS = 30 |
41 | | -# Maximum number of worker threads for parallel module loading. |
42 | | -MAX_WORKER_THREAD_COUNT = 4 |
43 | 37 |
|
44 | 38 |
|
45 | 39 | def _configure_knack(): |
@@ -203,17 +197,6 @@ def _configure_style(self): |
203 | 197 | format_styled_text.theme = theme |
204 | 198 |
|
205 | 199 |
|
206 | | -class ModuleLoadResult: # pylint: disable=too-few-public-methods |
207 | | - def __init__(self, module_name, command_table, group_table, elapsed_time, error=None, traceback_str=None, command_loader=None): |
208 | | - self.module_name = module_name |
209 | | - self.command_table = command_table |
210 | | - self.group_table = group_table |
211 | | - self.elapsed_time = elapsed_time |
212 | | - self.error = error |
213 | | - self.traceback_str = traceback_str |
214 | | - self.command_loader = command_loader |
215 | | - |
216 | | - |
217 | 200 | class MainCommandsLoader(CLICommandsLoader): |
218 | 201 |
|
219 | 202 | # Format string for pretty-print the command module table |
@@ -258,11 +241,11 @@ def load_command_table(self, args): |
258 | 241 | import pkgutil |
259 | 242 | import traceback |
260 | 243 | from azure.cli.core.commands import ( |
261 | | - _load_extension_command_loader, ExtensionCommandSource) |
| 244 | + _load_module_command_loader, _load_extension_command_loader, BLOCKED_MODS, ExtensionCommandSource) |
262 | 245 | from azure.cli.core.extension import ( |
263 | 246 | get_extensions, get_extension_path, get_extension_modname) |
264 | 247 | from azure.cli.core.breaking_change import ( |
265 | | - import_core_breaking_changes, import_extension_breaking_changes) |
| 248 | + import_core_breaking_changes, import_module_breaking_changes, import_extension_breaking_changes) |
266 | 249 |
|
267 | 250 | def _update_command_table_from_modules(args, command_modules=None): |
268 | 251 | """Loads command tables from modules and merge into the main command table. |
@@ -290,10 +273,38 @@ def _update_command_table_from_modules(args, command_modules=None): |
290 | 273 | except ImportError as e: |
291 | 274 | logger.warning(e) |
292 | 275 |
|
293 | | - results = self._load_modules(args, command_modules) |
| 276 | + count = 0 |
| 277 | + cumulative_elapsed_time = 0 |
| 278 | + cumulative_group_count = 0 |
| 279 | + cumulative_command_count = 0 |
| 280 | + logger.debug("Loading command modules:") |
| 281 | + logger.debug(self.header_mod) |
294 | 282 |
|
295 | | - count, cumulative_elapsed_time, cumulative_group_count, cumulative_command_count = \ |
296 | | - self._process_results_with_timing(results) |
| 283 | + for mod in [m for m in command_modules if m not in BLOCKED_MODS]: |
| 284 | + try: |
| 285 | + start_time = timeit.default_timer() |
| 286 | + module_command_table, module_group_table = _load_module_command_loader(self, args, mod) |
| 287 | + import_module_breaking_changes(mod) |
| 288 | + for cmd in module_command_table.values(): |
| 289 | + cmd.command_source = mod |
| 290 | + self.command_table.update(module_command_table) |
| 291 | + self.command_group_table.update(module_group_table) |
| 292 | + |
| 293 | + elapsed_time = timeit.default_timer() - start_time |
| 294 | + logger.debug(self.item_format_string, mod, elapsed_time, |
| 295 | + len(module_group_table), len(module_command_table)) |
| 296 | + count += 1 |
| 297 | + cumulative_elapsed_time += elapsed_time |
| 298 | + cumulative_group_count += len(module_group_table) |
| 299 | + cumulative_command_count += len(module_command_table) |
| 300 | + except Exception as ex: # pylint: disable=broad-except |
| 301 | + # Changing this error message requires updating CI script that checks for failed |
| 302 | + # module loading. |
| 303 | + from azure.cli.core import telemetry |
| 304 | + logger.error("Error loading command module '%s': %s", mod, ex) |
| 305 | + telemetry.set_exception(exception=ex, fault_type='module-load-error-' + mod, |
| 306 | + summary='Error loading module: {}'.format(mod)) |
| 307 | + logger.debug(traceback.format_exc()) |
297 | 308 | # Summary line |
298 | 309 | logger.debug(self.item_format_string, |
299 | 310 | "Total ({})".format(count), cumulative_elapsed_time, |
@@ -367,7 +378,7 @@ def _filter_modname(extensions): |
367 | 378 | # from an extension requires this map to be up-to-date. |
368 | 379 | # self._mod_to_ext_map[ext_mod] = ext_name |
369 | 380 | start_time = timeit.default_timer() |
370 | | - extension_command_table, extension_group_table, _ = \ |
| 381 | + extension_command_table, extension_group_table = \ |
371 | 382 | _load_extension_command_loader(self, args, ext_mod) |
372 | 383 | import_extension_breaking_changes(ext_mod) |
373 | 384 |
|
@@ -576,99 +587,6 @@ def load_arguments(self, command=None): |
576 | 587 | self.extra_argument_registry.update(loader.extra_argument_registry) |
577 | 588 | loader._update_command_definitions() # pylint: disable=protected-access |
578 | 589 |
|
579 | | - def _load_modules(self, args, command_modules): |
580 | | - """Load command modules using ThreadPoolExecutor with timeout protection.""" |
581 | | - from azure.cli.core.commands import BLOCKED_MODS |
582 | | - |
583 | | - results = [] |
584 | | - with ThreadPoolExecutor(max_workers=MAX_WORKER_THREAD_COUNT) as executor: |
585 | | - future_to_module = {executor.submit(self._load_single_module, mod, args): mod |
586 | | - for mod in command_modules if mod not in BLOCKED_MODS} |
587 | | - |
588 | | - for future in concurrent.futures.as_completed(future_to_module): |
589 | | - try: |
590 | | - result = future.result(timeout=MODULE_LOAD_TIMEOUT_SECONDS) |
591 | | - results.append(result) |
592 | | - except concurrent.futures.TimeoutError: |
593 | | - mod = future_to_module[future] |
594 | | - logger.warning("Module '%s' load timeout after %s seconds", mod, MODULE_LOAD_TIMEOUT_SECONDS) |
595 | | - results.append(ModuleLoadResult(mod, {}, {}, 0, |
596 | | - Exception(f"Module '{mod}' load timeout"))) |
597 | | - except (ImportError, AttributeError, TypeError, ValueError) as ex: |
598 | | - mod = future_to_module[future] |
599 | | - logger.warning("Module '%s' load failed: %s", mod, ex) |
600 | | - results.append(ModuleLoadResult(mod, {}, {}, 0, ex)) |
601 | | - except Exception as ex: # pylint: disable=broad-exception-caught |
602 | | - mod = future_to_module[future] |
603 | | - logger.warning("Module '%s' load failed with unexpected exception: %s", mod, ex) |
604 | | - results.append(ModuleLoadResult(mod, {}, {}, 0, ex)) |
605 | | - |
606 | | - return results |
607 | | - |
608 | | - def _load_single_module(self, mod, args): |
609 | | - from azure.cli.core.breaking_change import import_module_breaking_changes |
610 | | - from azure.cli.core.commands import _load_module_command_loader |
611 | | - import traceback |
612 | | - try: |
613 | | - start_time = timeit.default_timer() |
614 | | - module_command_table, module_group_table, command_loader = _load_module_command_loader(self, args, mod) |
615 | | - import_module_breaking_changes(mod) |
616 | | - elapsed_time = timeit.default_timer() - start_time |
617 | | - return ModuleLoadResult(mod, module_command_table, module_group_table, elapsed_time, command_loader=command_loader) |
618 | | - except Exception as ex: # pylint: disable=broad-except |
619 | | - tb_str = traceback.format_exc() |
620 | | - return ModuleLoadResult(mod, {}, {}, 0, ex, tb_str) |
621 | | - |
622 | | - def _handle_module_load_error(self, result): |
623 | | - """Handle errors that occurred during module loading.""" |
624 | | - from azure.cli.core import telemetry |
625 | | - |
626 | | - logger.error("Error loading command module '%s': %s", result.module_name, result.error) |
627 | | - telemetry.set_exception(exception=result.error, |
628 | | - fault_type='module-load-error-' + result.module_name, |
629 | | - summary='Error loading module: {}'.format(result.module_name)) |
630 | | - if result.traceback_str: |
631 | | - logger.debug(result.traceback_str) |
632 | | - |
633 | | - def _process_successful_load(self, result): |
634 | | - """Process successfully loaded module results.""" |
635 | | - if result.command_loader: |
636 | | - self.loaders.append(result.command_loader) |
637 | | - |
638 | | - for cmd in result.command_table: |
639 | | - self.cmd_to_loader_map[cmd] = [result.command_loader] |
640 | | - |
641 | | - for cmd in result.command_table.values(): |
642 | | - cmd.command_source = result.module_name |
643 | | - |
644 | | - self.command_table.update(result.command_table) |
645 | | - self.command_group_table.update(result.group_table) |
646 | | - |
647 | | - logger.debug(self.item_format_string, result.module_name, result.elapsed_time, |
648 | | - len(result.group_table), len(result.command_table)) |
649 | | - |
650 | | - def _process_results_with_timing(self, results): |
651 | | - """Process pre-loaded module results with timing and progress reporting.""" |
652 | | - logger.debug("Loading command modules:") |
653 | | - logger.debug(self.header_mod) |
654 | | - |
655 | | - count = 0 |
656 | | - cumulative_elapsed_time = 0 |
657 | | - cumulative_group_count = 0 |
658 | | - cumulative_command_count = 0 |
659 | | - |
660 | | - for result in results: |
661 | | - if result.error: |
662 | | - self._handle_module_load_error(result) |
663 | | - else: |
664 | | - self._process_successful_load(result) |
665 | | - count += 1 |
666 | | - cumulative_elapsed_time += result.elapsed_time |
667 | | - cumulative_group_count += len(result.group_table) |
668 | | - cumulative_command_count += len(result.command_table) |
669 | | - |
670 | | - return count, cumulative_elapsed_time, cumulative_group_count, cumulative_command_count |
671 | | - |
672 | 590 |
|
673 | 591 | class CommandIndex: |
674 | 592 |
|
|
0 commit comments