1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-05-01 04:03:59 -04:00

Merge pull request #214 from craigerl/fix/cli-command-issues

Fix CLI command inconsistencies
This commit is contained in:
Walter A. Boring IV 2026-02-28 09:33:31 -05:00 committed by GitHub
commit bbc2ccd302
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 65 additions and 117 deletions

View File

@ -1,16 +1,19 @@
import click import click
import click.shell_completion import click.shell_completion
from aprsd import cli_helper
from aprsd.main import cli from aprsd.main import cli
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
@cli.command() @cli.command()
@cli_helper.add_options(cli_helper.common_options)
@click.argument( @click.argument(
'shell', type=click.Choice(list(click.shell_completion._available_shells)) 'shell', type=click.Choice(list(click.shell_completion._available_shells))
) )
def completion(shell): @cli_helper.process_standard_options_no_config
def completion(ctx, shell):
"""Show the shell completion code""" """Show the shell completion code"""
from click.utils import _detect_program_name from click.utils import _detect_program_name

View File

@ -7,53 +7,11 @@ import click
from aprsd import cli_helper from aprsd import cli_helper
from aprsd import plugin as aprsd_plugin from aprsd import plugin as aprsd_plugin
from aprsd.main import cli from aprsd.main import cli
from aprsd.plugins import fortune, notify, ping, time, version, weather
from aprsd.utils import package as aprsd_package from aprsd.utils import package as aprsd_package
LOG = logging.getLogger('APRSD') LOG = logging.getLogger('APRSD')
def get_built_in_plugins():
"""Discover all built-in APRSD plugins."""
modules = [fortune, notify, ping, time, version, weather]
plugins = []
for module in modules:
entries = inspect.getmembers(module, inspect.isclass)
for entry in entries:
cls = entry[1]
if issubclass(cls, aprsd_plugin.APRSDPluginBase):
plugin_info = {
'package': 'aprsd',
'class_name': cls.__qualname__,
'path': f'{cls.__module__}.{cls.__qualname__}',
'version': cls.version,
'base_class_type': aprsd_package.plugin_type(cls),
}
# If it's a regex command plugin, include the command_regex
if issubclass(cls, aprsd_plugin.APRSDRegexCommandPluginBase):
# Try to get command_regex from the class
# It's typically defined as a class attribute in plugin implementations
try:
# Check the MRO to find where command_regex is actually defined
cmd_regex = None
for base_cls in inspect.getmro(cls):
if 'command_regex' in base_cls.__dict__:
attr = base_cls.__dict__['command_regex']
# If it's not a property descriptor, use it
if not isinstance(attr, property):
cmd_regex = attr
break
plugin_info['command_regex'] = cmd_regex
except Exception:
plugin_info['command_regex'] = None
plugins.append(plugin_info)
return plugins
def get_installed_plugin_classes(): def get_installed_plugin_classes():
"""Discover all installed 3rd party plugin classes, grouped by package.""" """Discover all installed 3rd party plugin classes, grouped by package."""
installed_plugins = aprsd_package.get_installed_plugins() installed_plugins = aprsd_package.get_installed_plugins()
@ -117,7 +75,7 @@ def export_plugins(ctx):
} }
# Get built-in plugins # Get built-in plugins
built_in = get_built_in_plugins() built_in = aprsd_package.get_built_in_plugins()
output['built_in_plugins'] = built_in output['built_in_plugins'] = built_in
# Get installed 3rd party plugins (grouped by package) # Get installed 3rd party plugins (grouped by package)

View File

@ -85,7 +85,7 @@ def healthcheck(ctx, timeout):
client_stats = stats.get('APRSClientStats') client_stats = stats.get('APRSClientStats')
if not client_stats: if not client_stats:
console.log('No APRSClientStats') console.log('No APRSClientStats - Is the aprsd server running?')
sys.exit(-1) sys.exit(-1)
else: else:
aprsis_last_update = client_stats['connection_keepalive'] aprsis_last_update = client_stats['connection_keepalive']

View File

@ -1,4 +1,3 @@
import inspect
import logging import logging
import click import click
@ -7,39 +6,33 @@ from rich.table import Table
from rich.text import Text from rich.text import Text
from aprsd import cli_helper from aprsd import cli_helper
from aprsd import plugin as aprsd_plugin
from aprsd.main import cli from aprsd.main import cli
from aprsd.plugins import fortune, notify, ping, time, version, weather
from aprsd.utils import package as aprsd_package from aprsd.utils import package as aprsd_package
LOG = logging.getLogger('APRSD') LOG = logging.getLogger('APRSD')
def show_built_in_plugins(console): def show_built_in_plugins(console):
modules = [fortune, notify, ping, time, version, weather] built_in = aprsd_package.get_built_in_plugins()
plugins = [] plugins = []
for module in modules: for plugin in built_in:
entries = inspect.getmembers(module, inspect.isclass) info = {
for entry in entries: 'name': plugin['class_name'],
cls = entry[1] 'path': plugin['path'],
if issubclass(cls, aprsd_plugin.APRSDPluginBase): 'version': plugin['version'],
info = { 'short_desc': '',
'name': cls.__qualname__, }
'path': f'{cls.__module__}.{cls.__qualname__}',
'version': cls.version,
'docstring': cls.__doc__,
'short_desc': cls.short_description,
}
if issubclass(cls, aprsd_plugin.APRSDRegexCommandPluginBase): if plugin.get('command_regex'):
info['command_regex'] = cls.command_regex info['command_regex'] = plugin['command_regex']
info['type'] = 'RegexCommand' info['type'] = 'RegexCommand'
elif plugin['base_class_type'] == 'WatchList':
info['type'] = 'WatchList'
else:
info['type'] = plugin['base_class_type']
if issubclass(cls, aprsd_plugin.APRSDWatchListPluginBase): plugins.append(info)
info['type'] = 'WatchList'
plugins.append(info)
plugins = sorted(plugins, key=lambda i: i['name']) plugins = sorted(plugins, key=lambda i: i['name'])

View File

@ -1,10 +1,6 @@
import cProfile
import datetime
import logging import logging
import pstats
import signal import signal
import sys import sys
import time
import click import click
from loguru import logger from loguru import logger
@ -34,16 +30,9 @@ console = Console()
def signal_handler(sig, frame): def signal_handler(sig, frame):
threads.APRSDThreadList().stop_all() from aprsd import main as aprsd_main
if 'subprocess' not in str(frame):
LOG.info( aprsd_main.signal_handler(sig, frame)
'Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}'.format(
datetime.datetime.now(),
),
)
time.sleep(5)
# Last save to disk
collector.Collector().collect()
class APRSDListenProcessThread(rx.APRSDFilterThread): class APRSDListenProcessThread(rx.APRSDFilterThread):
@ -154,12 +143,6 @@ class APRSDListenProcessThread(rx.APRSDFilterThread):
default='http://localhost:8081', default='http://localhost:8081',
help='URL of the aprsd-exporter API to send stats to.', help='URL of the aprsd-exporter API to send stats to.',
) )
@click.option(
'--profile',
default=False,
is_flag=True,
help='Enable Python cProfile profiling to identify performance bottlenecks.',
)
@click.pass_context @click.pass_context
@cli_helper.process_standard_options @cli_helper.process_standard_options
def listen( def listen(
@ -174,7 +157,6 @@ def listen(
enable_packet_stats, enable_packet_stats,
export_stats, export_stats,
exporter_url, exporter_url,
profile,
): ):
"""Listen to packets on the APRS-IS Network based on FILTER. """Listen to packets on the APRS-IS Network based on FILTER.
@ -186,12 +168,6 @@ def listen(
o/obj1/obj2... - Object Filter Pass all objects with the exact name of obj1, obj2, ... (* wild card allowed)\n o/obj1/obj2... - Object Filter Pass all objects with the exact name of obj1, obj2, ... (* wild card allowed)\n
""" """
# Initialize profiler if enabled
profiler = None
if profile:
LOG.info('Starting Python cProfile profiling')
profiler = cProfile.Profile()
profiler.enable()
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGTERM, signal_handler)
@ -315,25 +291,3 @@ def listen(
stats.join() stats.join()
if stats_export: if stats_export:
stats_export.join() stats_export.join()
# Save profiling results if enabled
if profiler:
profiler.disable()
profile_file = 'aprsd_listen_profile.prof'
profiler.dump_stats(profile_file)
LOG.info(f'Profile saved to {profile_file}')
# Print profiling summary
LOG.info('Profile Summary (top 50 functions by cumulative time):')
stats = pstats.Stats(profiler)
stats.sort_stats('cumulative')
# Log the top functions
LOG.info('-' * 80)
for item in stats.get_stats().items()[:50]:
func_info, stats_tuple = item
cumulative = stats_tuple[3]
total_calls = stats_tuple[0]
LOG.info(
f'{func_info} - Calls: {total_calls}, Cumulative: {cumulative:.4f}s'
)

View File

@ -2,6 +2,7 @@ import datetime
import decimal import decimal
import json import json
import sys import sys
from dataclasses import asdict, is_dataclass
from aprsd.packets import core from aprsd.packets import core
@ -63,6 +64,8 @@ class SimpleJSONEncoder(json.JSONEncoder):
return str(obj) return str(obj)
elif isinstance(obj, core.Packet): elif isinstance(obj, core.Packet):
return obj.to_dict() return obj.to_dict()
elif is_dataclass(obj):
return asdict(obj)
else: else:
return super().default(obj) return super().default(obj)

View File

@ -11,6 +11,7 @@ import requests
from thesmuggler import smuggle from thesmuggler import smuggle
from aprsd import plugin as aprsd_plugin from aprsd import plugin as aprsd_plugin
from aprsd.plugins import fortune, notify, ping, time, version, weather
# Handle importlib.metadata compatibility # Handle importlib.metadata compatibility
try: try:
@ -245,3 +246,39 @@ def log_installed_extensions_and_plugins():
for plugin in plugins: for plugin in plugins:
LOG.info(f'Plugin: {plugin} version: {plugins[plugin][0]["version"]}') LOG.info(f'Plugin: {plugin} version: {plugins[plugin][0]["version"]}')
def get_built_in_plugins():
"""Discover all built-in APRSD plugins."""
modules = [fortune, notify, ping, time, version, weather]
plugins = []
for module in modules:
entries = inspect.getmembers(module, inspect.isclass)
for entry in entries:
cls = entry[1]
if issubclass(cls, aprsd_plugin.APRSDPluginBase):
plugin_info = {
'package': 'aprsd',
'class_name': cls.__qualname__,
'path': f'{cls.__module__}.{cls.__qualname__}',
'version': cls.version,
'base_class_type': plugin_type(cls),
}
if issubclass(cls, aprsd_plugin.APRSDRegexCommandPluginBase):
try:
cmd_regex = None
for base_cls in inspect.getmro(cls):
if 'command_regex' in base_cls.__dict__:
attr = base_cls.__dict__['command_regex']
if not isinstance(attr, property):
cmd_regex = attr
break
plugin_info['command_regex'] = cmd_regex
except Exception:
plugin_info['command_regex'] = None
plugins.append(plugin_info)
return plugins