Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add interactive stack trace #233

Open
wants to merge 22 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1bb0480
Formatted stack trace and save to file
ryan-doan Sep 27, 2022
2285d9d
code formatting changes
ryan-doan Sep 30, 2022
8581cae
Delete stack_trace.py
ryan-doan Sep 30, 2022
c5d5eb1
Update pros/serial/terminal/terminal.py
ryan-doan Oct 4, 2022
466503e
Update pros/cli/terminal.py
ryan-doan Oct 4, 2022
221ae03
update correct path, prevent addr2line from running if elf file doesn…
ryan-doan Oct 4, 2022
2304312
Merge branch 'add-interactive-stack-trace' of https://github.com/purd…
ryan-doan Oct 4, 2022
3e8c63c
remove project folder
ryan-doan Oct 4, 2022
059b077
added css styling to upload button
ryan-doan Oct 5, 2022
33124e1
Revert "added css styling to upload button"
ryan-doan Oct 5, 2022
efc6095
remove stack_trace.txt
ryan-doan Oct 18, 2022
3125d4a
fix errors with path
ryan-doan Oct 18, 2022
1b9167c
rewrote code to make it functional
ryan-doan Oct 19, 2022
02e98c0
Remove sqlalchemy, add options, string formatting, add colored output.
BennyBot Oct 19, 2022
9e5fcb7
Merge branch 'develop' into add-interactive-stack-trace
ryan-doan Oct 25, 2022
283712e
add shell argument to subprocess open
Andrewtho5942 Feb 24, 2023
f4ae10c
Merging in develop
Ghurfa Mar 1, 2024
7f2ad9d
Changed the addr2line path to use the toolchain
Ghurfa Mar 1, 2024
30f83ab
Merge branch 'develop' into add-interactive-stack-trace
Rocky14683 Jun 11, 2024
5a15963
Finish data abort realtime stack trace functionality
Rocky14683 Jun 11, 2024
afcd1e7
Merge remote-tracking branch 'origin/add-interactive-stack-trace' int…
Rocky14683 Jun 11, 2024
de60210
Work on formatting and color
Rocky14683 Jul 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pip_version
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
3.5.3
3.5.3

1 change: 0 additions & 1 deletion pros/cli/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,6 @@ def resolve_v5_port(port: Optional[str], type: str, quiet: bool = False) -> Tupl
show_default=False,
type=click.Choice([p.description.split(' ')[-1] for p in ports]))
port = [p.device for p in ports if p.description.split(' ')[-1] == brain_id][0]

assert port in [p.device for p in ports]
else:
return None, False
Expand Down
1 change: 1 addition & 0 deletions pros/cli/conductor.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,3 +392,4 @@ def reset(force: bool):
os.remove(file)

ui.echo("Conductor was reset")

329 changes: 165 additions & 164 deletions pros/cli/misc_commands.py
Original file line number Diff line number Diff line change
@@ -1,164 +1,165 @@
import os
from pathlib import Path
import subprocess

from click.shell_completion import CompletionItem, add_completion_class, ZshComplete

import pros.common.ui as ui
from pros.cli.common import *
from pros.ga.analytics import analytics

@pros_root
def misc_commands_cli():
pass


@misc_commands_cli.command()
@click.option('--force-check', default=False, is_flag=True,
help='Force check for updates, disregarding auto-check frequency')
@click.option('--no-install', default=False, is_flag=True,
help='Only check if a new version is available, do not attempt to install')
@default_options
def upgrade(force_check, no_install):
"""
Check for updates to the PROS CLI
"""
with ui.Notification():
ui.echo('The "pros upgrade" command is currently non-functioning. Did you mean to run "pros c upgrade"?', color='yellow')

return # Dead code below

analytics.send("upgrade")
from pros.upgrade import UpgradeManager
manager = UpgradeManager()
manifest = manager.get_manifest(force_check)
ui.logger(__name__).debug(repr(manifest))
if manager.has_stale_manifest:
ui.logger(__name__).error('Failed to get latest upgrade information. '
'Try running with --debug for more information')
return -1
if not manager.needs_upgrade:
ui.finalize('upgradeInfo', 'PROS CLI is up to date')
else:
ui.finalize('upgradeInfo', manifest)
if not no_install:
if not manager.can_perform_upgrade:
ui.logger(__name__).error(f'This manifest cannot perform the upgrade.')
return -3
ui.finalize('upgradeComplete', manager.perform_upgrade())


# Script files for each shell
_SCRIPT_FILES = {
'bash': 'pros-complete.bash',
'zsh': 'pros-complete.zsh',
'fish': 'pros.fish',
'pwsh': 'pros-complete.ps1',
'powershell': 'pros-complete.ps1',
}


def _get_shell_script(shell: str) -> str:
"""Get the shell script for the specified shell."""
script_file = Path(__file__).parent.parent / 'autocomplete' / _SCRIPT_FILES[shell]
with script_file.open('r') as f:
return f.read()


@add_completion_class
class PowerShellComplete(ZshComplete): # Identical to ZshComplete except comma delimited instead of newline
"""Shell completion for PowerShell and Windows PowerShell."""

name = "powershell"
source_template = _get_shell_script("powershell")

def format_completion(self, item: CompletionItem) -> str:
return super().format_completion(item).replace("\n", ",")


@misc_commands_cli.command()
@click.argument('shell', type=click.Choice(['bash', 'zsh', 'fish', 'pwsh', 'powershell']), required=True)
@click.argument('config_path', type=click.Path(resolve_path=True), default=None, required=False)
@click.option('--force', '-f', is_flag=True, default=False, help='Skip confirmation prompts')
@default_options
def setup_autocomplete(shell, config_path, force):
"""
Set up autocomplete for PROS CLI

SHELL: The shell to set up autocomplete for

CONFIG_PATH: The configuration path to add the autocomplete script to. If not specified, the default configuration
file for the shell will be used.

Example: pros setup-autocomplete bash ~/.bashrc
"""

# https://click.palletsprojects.com/en/8.1.x/shell-completion/

default_config_paths = { # Default config paths for each shell
'bash': '~/.bashrc',
'zsh': '~/.zshrc',
'fish': '~/.config/fish/completions/',
'pwsh': None,
'powershell': None,
}

# Get the powershell profile path if not specified
if shell in ('pwsh', 'powershell') and config_path is None:
try:
profile_command = f'{shell} -NoLogo -NoProfile -Command "Write-Output $PROFILE"' if os.name == 'nt' else f"{shell} -NoLogo -NoProfile -Command 'Write-Output $PROFILE'"
default_config_paths[shell] = subprocess.run(profile_command, shell=True, capture_output=True, check=True, text=True).stdout.strip()
except subprocess.CalledProcessError as exc:
raise click.UsageError("Failed to determine the PowerShell profile path. Please specify a valid config file.") from exc

# Use default config path if not specified
if config_path is None:
config_path = default_config_paths[shell]
ui.echo(f"Using default config path {config_path}. To specify a different config path, run 'pros setup-autocomplete {shell} [CONFIG_PATH]'.\n")
config_path = Path(config_path).expanduser().resolve()

if shell in ('bash', 'zsh', 'pwsh', 'powershell'):
if config_path.is_dir():
raise click.UsageError(f"Config file {config_path} is a directory. Please specify a valid config file.")
if not config_path.exists():
raise click.UsageError(f"Config file {config_path} does not exist. Please specify a valid config file.")

# Write the autocomplete script to a shell script file
script_file = Path(click.get_app_dir("PROS")) / "autocomplete" / _SCRIPT_FILES[shell]
script_file.parent.mkdir(exist_ok=True)
with script_file.open('w') as f:
f.write(_get_shell_script(shell))

# Source the autocomplete script in the config file
if shell in ('bash', 'zsh'):
source_autocomplete = f'. "{script_file.as_posix()}"\n'
elif shell in ('pwsh', 'powershell'):
source_autocomplete = f'"{script_file}" | Invoke-Expression\n'
if force or ui.confirm(f"Add the autocomplete script to {config_path}?", default=True):
with config_path.open('r+') as f:
# Only append if the source command is not already in the file
if source_autocomplete not in f.readlines():
f.write("\n# PROS CLI autocomplete\n")
f.write(source_autocomplete)
else:
ui.echo(f"Autocomplete script written to {script_file}.")
ui.echo(f"Add the following line to {config_path} then restart your shell to enable autocomplete:\n")
ui.echo(source_autocomplete)
return
elif shell == 'fish':
# Check if the config path is a directory or file and set the script directory and file accordingly
if config_path.is_file():
script_dir = config_path.parent
script_file = config_path
else:
script_dir = config_path
script_file = config_path / _SCRIPT_FILES[shell]

if not script_dir.exists():
raise click.UsageError(f"Completions directory {script_dir} does not exist. Please specify a valid completions file or directory.")

# Write the autocomplete script to a shell script file
with script_file.open('w') as f:
f.write(_get_shell_script(shell))

ui.echo(f"Succesfully set up autocomplete for {shell} in {config_path}. Restart your shell to apply changes.")
import os
from pathlib import Path
import subprocess

from click.shell_completion import CompletionItem, add_completion_class, ZshComplete

import pros.common.ui as ui
from pros.cli.common import *
from pros.ga.analytics import analytics

@pros_root
def misc_commands_cli():
pass


@misc_commands_cli.command()
@click.option('--force-check', default=False, is_flag=True,
help='Force check for updates, disregarding auto-check frequency')
@click.option('--no-install', default=False, is_flag=True,
help='Only check if a new version is available, do not attempt to install')
@default_options
def upgrade(force_check, no_install):
"""
Check for updates to the PROS CLI
"""
with ui.Notification():
ui.echo('The "pros upgrade" command is currently non-functioning. Did you mean to run "pros c upgrade"?', color='yellow')

return # Dead code below

analytics.send("upgrade")
from pros.upgrade import UpgradeManager
manager = UpgradeManager()
manifest = manager.get_manifest(force_check)
ui.logger(__name__).debug(repr(manifest))
if manager.has_stale_manifest:
ui.logger(__name__).error('Failed to get latest upgrade information. '
'Try running with --debug for more information')
return -1
if not manager.needs_upgrade:
ui.finalize('upgradeInfo', 'PROS CLI is up to date')
else:
ui.finalize('upgradeInfo', manifest)
if not no_install:
if not manager.can_perform_upgrade:
ui.logger(__name__).error(f'This manifest cannot perform the upgrade.')
return -3
ui.finalize('upgradeComplete', manager.perform_upgrade())


# Script files for each shell
_SCRIPT_FILES = {
'bash': 'pros-complete.bash',
'zsh': 'pros-complete.zsh',
'fish': 'pros.fish',
'pwsh': 'pros-complete.ps1',
'powershell': 'pros-complete.ps1',
}


def _get_shell_script(shell: str) -> str:
"""Get the shell script for the specified shell."""
script_file = Path(__file__).parent.parent / 'autocomplete' / _SCRIPT_FILES[shell]
with script_file.open('r') as f:
return f.read()


@add_completion_class
class PowerShellComplete(ZshComplete): # Identical to ZshComplete except comma delimited instead of newline
"""Shell completion for PowerShell and Windows PowerShell."""

name = "powershell"
source_template = _get_shell_script("powershell")

def format_completion(self, item: CompletionItem) -> str:
return super().format_completion(item).replace("\n", ",")


@misc_commands_cli.command()
@click.argument('shell', type=click.Choice(['bash', 'zsh', 'fish', 'pwsh', 'powershell']), required=True)
@click.argument('config_path', type=click.Path(resolve_path=True), default=None, required=False)
@click.option('--force', '-f', is_flag=True, default=False, help='Skip confirmation prompts')
@default_options
def setup_autocomplete(shell, config_path, force):
"""
Set up autocomplete for PROS CLI

SHELL: The shell to set up autocomplete for

CONFIG_PATH: The configuration path to add the autocomplete script to. If not specified, the default configuration
file for the shell will be used.

Example: pros setup-autocomplete bash ~/.bashrc
"""

# https://click.palletsprojects.com/en/8.1.x/shell-completion/

default_config_paths = { # Default config paths for each shell
'bash': '~/.bashrc',
'zsh': '~/.zshrc',
'fish': '~/.config/fish/completions/',
'pwsh': None,
'powershell': None,
}

# Get the powershell profile path if not specified
if shell in ('pwsh', 'powershell') and config_path is None:
try:
profile_command = f'{shell} -NoLogo -NoProfile -Command "Write-Output $PROFILE"' if os.name == 'nt' else f"{shell} -NoLogo -NoProfile -Command 'Write-Output $PROFILE'"
default_config_paths[shell] = subprocess.run(profile_command, shell=True, capture_output=True, check=True, text=True).stdout.strip()
except subprocess.CalledProcessError as exc:
raise click.UsageError("Failed to determine the PowerShell profile path. Please specify a valid config file.") from exc

# Use default config path if not specified
if config_path is None:
config_path = default_config_paths[shell]
ui.echo(f"Using default config path {config_path}. To specify a different config path, run 'pros setup-autocomplete {shell} [CONFIG_PATH]'.\n")
config_path = Path(config_path).expanduser().resolve()

if shell in ('bash', 'zsh', 'pwsh', 'powershell'):
if config_path.is_dir():
raise click.UsageError(f"Config file {config_path} is a directory. Please specify a valid config file.")
if not config_path.exists():
raise click.UsageError(f"Config file {config_path} does not exist. Please specify a valid config file.")

# Write the autocomplete script to a shell script file
script_file = Path(click.get_app_dir("PROS")) / "autocomplete" / _SCRIPT_FILES[shell]
script_file.parent.mkdir(exist_ok=True)
with script_file.open('w') as f:
f.write(_get_shell_script(shell))

# Source the autocomplete script in the config file
if shell in ('bash', 'zsh'):
source_autocomplete = f'. "{script_file.as_posix()}"\n'
elif shell in ('pwsh', 'powershell'):
source_autocomplete = f'"{script_file}" | Invoke-Expression\n'
if force or ui.confirm(f"Add the autocomplete script to {config_path}?", default=True):
with config_path.open('r+') as f:
# Only append if the source command is not already in the file
if source_autocomplete not in f.readlines():
f.write("\n# PROS CLI autocomplete\n")
f.write(source_autocomplete)
else:
ui.echo(f"Autocomplete script written to {script_file}.")
ui.echo(f"Add the following line to {config_path} then restart your shell to enable autocomplete:\n")
ui.echo(source_autocomplete)
return
elif shell == 'fish':
# Check if the config path is a directory or file and set the script directory and file accordingly
if config_path.is_file():
script_dir = config_path.parent
script_file = config_path
else:
script_dir = config_path
script_file = config_path / _SCRIPT_FILES[shell]

if not script_dir.exists():
raise click.UsageError(f"Completions directory {script_dir} does not exist. Please specify a valid completions file or directory.")

# Write the autocomplete script to a shell script file
with script_file.open('w') as f:
f.write(_get_shell_script(shell))

ui.echo(f"Succesfully set up autocomplete for {shell} in {config_path}. Restart your shell to apply changes.")

26 changes: 25 additions & 1 deletion pros/cli/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ def terminal_cli():
help='Specify 2 ports for the "share" backend. The default option deterministically selects ports '
'based on the serial port name')
@click.option('--banner/--no-banner', 'request_banner', default=True)
@click.option('--raw-stack-trace', is_flag=True, default=True, help='Display stack traces as raw data. By default, the terminal will attempt to parse stack traces')
@click.option('--stack-trace-file', type=str, default=None, help='Output stack traces to a file')
@click.option('--output', nargs = 1, type=str, is_eager = True, help='Redirect terminal output to a file', default=None)

def terminal(port: str, backend: str, **kwargs):
Expand Down Expand Up @@ -83,7 +85,29 @@ def terminal(port: str, backend: str, **kwargs):
device = devices.RawStreamDevice(ser)
else:
device = devices.vex.V5UserDevice(ser)
term = Terminal(device, request_banner=kwargs.pop('request_banner', True))
term = Terminal(device, request_banner=kwargs.pop('request_banner', True), auto_stack_trace=(kwargs.pop('raw_stack_trace', False) or kwargs.pop('raw', False)), stack_trace_file=kwargs.pop('stack_trace_file', None))

class TerminalOutput(object):
def __init__(self, file):
self.terminal = sys.stdout
self.log = open(file, 'a')
def write(self, data):
self.terminal.write(data)
self.log.write(data)
def flush(self):
pass
def end(self):
self.log.close()

output = None
if kwargs.get('output', None):
output_file = kwargs['output']
output = TerminalOutput(f'{output_file}')
term.console.output = output
sys.stdout = output
logger(__name__).info(f'Redirecting Terminal Output to File: {output_file}')
else:
sys.stdout = sys.__stdout__

class TerminalOutput(object):
def __init__(self, file):
Expand Down
Loading
Loading