diff --git a/pip_version b/pip_version index 678fd88a..2f279c1b 100644 --- a/pip_version +++ b/pip_version @@ -1 +1,2 @@ -3.5.3 \ No newline at end of file +3.5.3 + diff --git a/pros/cli/common.py b/pros/cli/common.py index 6c12fa06..6deb92d4 100644 --- a/pros/cli/common.py +++ b/pros/cli/common.py @@ -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 diff --git a/pros/cli/conductor.py b/pros/cli/conductor.py index 38d43235..481584fb 100644 --- a/pros/cli/conductor.py +++ b/pros/cli/conductor.py @@ -392,3 +392,4 @@ def reset(force: bool): os.remove(file) ui.echo("Conductor was reset") + diff --git a/pros/cli/misc_commands.py b/pros/cli/misc_commands.py index d00fbfd3..7c529437 100644 --- a/pros/cli/misc_commands.py +++ b/pros/cli/misc_commands.py @@ -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.") + diff --git a/pros/cli/terminal.py b/pros/cli/terminal.py index 2f05f2fe..b5fac7c0 100644 --- a/pros/cli/terminal.py +++ b/pros/cli/terminal.py @@ -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): @@ -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): diff --git a/pros/conductor/conductor.py b/pros/conductor/conductor.py index 53129cf3..c102963d 100644 --- a/pros/conductor/conductor.py +++ b/pros/conductor/conductor.py @@ -129,6 +129,12 @@ def __init__(self, file=None): if 'cortex' not in self.pros_4_default_libraries: self.pros_4_default_libraries['cortex'] = [] needs_saving = True + if 'v5' not in self.early_access_libraries: + self.early_access_libraries['v5'] = [] + needs_saving = True + if 'cortex' not in self.early_access_libraries: + self.early_access_libraries['cortex'] = [] + needs_saving = True if needs_saving: self.save() from pros.common.sentry import add_context @@ -221,7 +227,6 @@ def resolve_templates(self, identifier: Union[str, BaseTemplate], allow_online: results.extend(online_results) logger(__name__).debug('Saving Conductor config after checking for remote updates') self.save() # Save self since there may have been some updates from the depots - if len(results) == 0 and not use_early_access: raise dont_send( InvalidTemplateException(f'{identifier.name} does not support kernel version {kernel_version}')) @@ -281,9 +286,9 @@ def apply_template(self, project: Project, identifier: Union[str, BaseTemplate], if template is None: raise dont_send( InvalidTemplateException(f'Could not find a template satisfying {identifier} for {project.target}')) - + apply_liblvgl = False # flag to apply liblvgl if upgrading to PROS 4 - + # warn and prompt user if upgrading to PROS 4 or downgrading to PROS 3 if template.name == 'kernel': isProject = Project.find_project("") @@ -303,7 +308,7 @@ def apply_template(self, project: Project, identifier: Union[str, BaseTemplate], if not confirm: raise dont_send( InvalidTemplateException(f'Not downgrading')) - + if not isinstance(template, LocalTemplate): with ui.Notification(): template = self.fetch_template(self.get_depot(template.metadata['origin']), template, **kwargs) @@ -333,6 +338,7 @@ def apply_template(self, project: Project, identifier: Union[str, BaseTemplate], assert isinstance(template, LocalTemplate) project.apply_template(template) ui.finalize('apply', f'Finished applying {template.identifier} to {project.location}') + elif valid_action != TemplateAction.AlreadyInstalled: raise dont_send( InvalidTemplateException(f'Could not install {template.identifier} because it is {valid_action.name},' @@ -368,6 +374,7 @@ def new_project(self, path: str, no_default_libs: bool = False, **kwargs) -> Pro raise dont_send(ValueError('Will not create a project in user home directory')) proj = Project(path=path, create=True, early_access=use_early_access) + if 'target' in kwargs: proj.target = kwargs['target'] if 'project_name' in kwargs and kwargs['project_name'] and not kwargs['project_name'].isspace(): @@ -383,6 +390,7 @@ def new_project(self, path: str, no_default_libs: bool = False, **kwargs) -> Pro if not no_default_libs: major_version = proj.kernel[0] libraries = self.pros_4_default_libraries if major_version == '4' else self.pros_3_default_libraries + for library in libraries[proj.target]: try: # remove kernel version so that latest template satisfying query is correctly selected diff --git a/pros/serial/terminal/terminal.py b/pros/serial/terminal/terminal.py index a0c78264..d5e6f589 100644 --- a/pros/serial/terminal/terminal.py +++ b/pros/serial/terminal/terminal.py @@ -1,6 +1,7 @@ import codecs import os import signal +import subprocess import sys import threading @@ -168,9 +169,12 @@ def cleanup(self): class Terminal(object): """This class is loosely based off of the pyserial miniterm""" + beginStackTrace = False + stackTraceFile = None def __init__(self, port_instance: StreamDevice, transformations=(), - output_raw: bool = False, request_banner: bool = True): + output_raw: bool = False, request_banner: bool = True, + auto_stack_trace: bool = True, stack_trace_file: str = None): self.device = port_instance self.device.subscribe(b'sout') self.device.subscribe(b'serr') @@ -183,6 +187,8 @@ def __init__(self, port_instance: StreamDevice, transformations=(), self.output_raw = output_raw self.request_banner = request_banner self.no_sigint = True # SIGINT flag + self.convert_stack_traces = auto_stack_trace + self.stack_trace_file = stack_trace_file signal.signal(signal.SIGINT, self.catch_sigint) # SIGINT handler self.console = Console() self.console.output = colorama.AnsiToWin32(self.console.output).stream @@ -211,6 +217,7 @@ def _stop_tx(self): self.transmitter_thread.join() def reader(self): + SEPARATOR = "\n--------------------------------\n" if self.request_banner: try: self.device.write(b'pRb') @@ -224,7 +231,75 @@ def reader(self): if data[0] == b'sout': text = decode_bytes_to_str(data[1]) elif data[0] == b'serr': - text = '{}{}{}'.format(colorama.Fore.RED, decode_bytes_to_str(data[1]), colorama.Style.RESET_ALL) + # print(len(text)) + addr = "0x" + decode_bytes_to_str(data[1])[:7] + + convert_trace = (self.beginStackTrace and + addr.isalnum() and + addr[3] != 'x' and + self.convert_stack_traces and + ((os.name != 'nt') or os.environ.get('PROS_TOOLCHAIN'))) + + if convert_trace: + if os.name == 'nt' and os.environ.get('PROS_TOOLCHAIN'): + addr2line_path = os.path.join(os.environ.get('PROS_TOOLCHAIN'), 'bin', + 'arm-none-eabi-addr2line') + else: + addr2line_path = 'arm-none-eabi-addr2line' + + def getTrace(s, path): + if not os.path.exists(path): + return '' + temp = subprocess.check_output([addr2line_path, '-faps', '-e', path, s]).decode('utf-8') + if (temp.find('?') != -1): + return '' + else: + return temp[12: len(temp) - 2] + + trace = ' : {}{}{}'.format( + getTrace(addr, "./bin/hot.package.elf"), + getTrace(addr, "./bin/cold.package.elf"), + getTrace(addr, "./bin/monolith.elf")) + text = '{}{}{}{}{}{}'.format(colorama.Fore.RED, '\n' + addr, + colorama.Style.RESET_ALL, colorama.Fore.YELLOW, trace, + colorama.Style.RESET_ALL) + + if self.stack_trace_file: + file.write(addr + trace + '\n') + elif "DATA ABORT EXCEPTION" in decode_bytes_to_str(data[1]): + DATA_ABORT_ANNON_STRING = "================================\n" \ + " DATA ABORT EXCEPTION!!! \n" \ + "================================\n" \ + "\n" + text = '{}{}{}{}'.format(colorama.Style.BRIGHT, colorama.Fore.RED, DATA_ABORT_ANNON_STRING, + colorama.Style.RESET_ALL) + elif "CURRENT TASK:" in decode_bytes_to_str(data[1]): + text = '{}{}{}{}'.format(colorama.Style.BRIGHT, colorama.Fore.RED, + decode_bytes_to_str(data[1]) + SEPARATOR, colorama.Style.RESET_ALL) + elif "REGISTERS AT ABORT" in decode_bytes_to_str(data[1]): + REGISTERS_ANNON_STRING = "Registers At Exception:" + SEPARATOR + text = '{}{}{}{}'.format(colorama.Style.BRIGHT, colorama.Fore.RED, + '\n' + REGISTERS_ANNON_STRING, colorama.Style.RESET_ALL) + elif not "0x" == decode_bytes_to_str(data[1]): + text = '{}{}{}'.format(colorama.Fore.LIGHTWHITE_EX, decode_bytes_to_str(data[1]), + colorama.Style.RESET_ALL) + if "BEGIN STACK TRACE" in text: + self.beginStackTrace = True + if self.convert_stack_traces: + text = '{}{}{}'.format(colorama.Style.BRIGHT, colorama.Fore.RED, + "BEGINNING STACK TRACE" + SEPARATOR, colorama.Style.RESET_ALL) + if self.stack_trace_file: + file = open(self.stack_trace_file, "w") + + if "END OF TRACE" in text: + self.beginStackTrace = False + if self.convert_stack_traces: + text = '{}{}{}'.format(colorama.Style.BRIGHT, colorama.Fore.RED, + SEPARATOR + "END OF STACK TRACE\n\n", colorama.Style.RESET_ALL) + if self.stack_trace_file: + file.close() + file = None + elif data[0] == b'kdbg': text = '{}\n\nKERNEL DEBUG:\t{}{}\n'.format(colorama.Back.GREEN + colorama.Style.BRIGHT, decode_bytes_to_str(data[1]), diff --git a/version b/version index 678fd88a..444877d4 100644 --- a/version +++ b/version @@ -1 +1 @@ -3.5.3 \ No newline at end of file +3.5.3 diff --git a/win_version b/win_version index 1067a669..f98c3412 100644 --- a/win_version +++ b/win_version @@ -1 +1 @@ -3.5.3.0 \ No newline at end of file +3.5.3.0