def execute(self): if len(self.actions) == 0: ui.logger(__name__).warning('No actions necessary.') return location = self.project.location tfd, tfn = tempfile.mkstemp(prefix='pros-project-', suffix=f'-{self.project.name}.zip', text='w+b') with os.fdopen(tfd, 'w+b') as tf: with zipfile.ZipFile(tf, mode='w') as zf: files, length = it.tee(location.glob('**/*'), 2) length = len(list(length)) with ui.progressbar(files, length=length, label=f'Backing up {self.project.name} to {tfn}') as pb: for file in pb: zf.write(file, arcname=file.relative_to(location)) try: with ui.Notification(): for action in self.actions: ui.logger(__name__).debug(action.describe(self.conductor, self.project)) rv = action.execute(self.conductor, self.project) ui.logger(__name__).debug(f'{action} returned {rv}') if rv is not None and not rv: raise ValueError('Action did not complete successfully') ui.echo('All actions performed successfully') except Exception as e: ui.logger(__name__).warning(f'Failed to perform transaction, restoring project to previous state') with zipfile.ZipFile(tfn) as zf: with ui.progressbar(zf.namelist(), label=f'Restoring {self.project.name} from {tfn}') as pb: for file in pb: zf.extract(file, path=location) ui.logger(__name__).exception(e) finally: ui.echo(f'Removing {tfn}') os.remove(tfn)
def wake_me(self): """ Hack to wake up input thread to know to shut down """ ui.logger(__name__).debug(f'Broadcasting WAKEME for {self.app}') if ui.ismachineoutput(): ui._machineoutput({'type': 'wakeme'}) else: ui.echo('Wake up the renderer!')
def version(ctx: click.Context, param, value): if not value: return ctx.ensure_object(dict) if ctx.obj.get('machine_output', False): ui.echo(get_version()) else: ui.echo('pros, version {}'.format(get_version())) ctx.exit(0)
def user_script(script_file): """ Run a script file with the PROS CLI package """ import os.path import importlib.util package_name = os.path.splitext(os.path.split(script_file)[0])[0] package_path = os.path.abspath(script_file) ui.echo(f'Loading {package_name} from {package_path}') spec = importlib.util.spec_from_file_location(package_name, package_path) spec.loader.load_module()
def create_query(cls, name: str = None, **kwargs) -> 'BaseTemplate': if not isinstance(name, str): return cls(**kwargs) if name.count('@') > 1: raise ValueError(f'Malformed identifier: {name}') if '@' in name: name, kwargs['version'] = name.split('@') if name == 'kernal': ui.echo("Assuming 'kernal' is the British spelling of kernel.") name = 'kernel' return cls(name=name, **kwargs)
def confirm(self, *args, **kwargs): assert self.can_confirm self.exit() project = self.conductor.new_project( path=self.directory.value, target=self.targets.value, version=self.kernel_versions.value, no_default_libs=not self.install_default_libraries.value, project_name=self.project_name.value) from pros.conductor.project import ProjectReport report = ProjectReport(project) ui.finalize('project-report', report) with ui.Notification(): ui.echo('Building project...') project.compile([])
def write_program(self, file: typing.BinaryIO, **kwargs): action_string = '' if hasattr(file, 'name'): action_string += f' {Path(file.name).name}' action_string += f' to Cortex on {self.port}' ui.echo(f'Uploading {action_string}') logger(__name__).info('Writing program to Cortex') status = self.query_system() logger(__name__).info(status) if not status.flags | self.SystemStatusFlags.TETH_USB and not status.flags | self.SystemStatusFlags.DL_MODE: self.send_to_download_channel() bootloader = self.expose_bootloader() rv = bootloader.write_program(file, **kwargs) ui.finalize('upload', f'Finished uploading {action_string}') return rv
def new_project(ctx: click.Context, path: str, target: str, version: str, force_user: bool = False, force_system: bool = False, no_default_libs: bool = False, compile_after: bool = True, build_cache: bool = None, **kwargs): """ Create a new PROS project Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more """ if version.lower() == 'latest' or not version: version = '>0' if not force_system and c.Project.find_project(path) is not None: logger(__name__).error( 'A project already exists in this location! Delete it first', extra={'sentry': False}) ctx.exit(-1) try: _conductor = c.Conductor() if target is None: target = _conductor.default_target project = _conductor.new_project(path, target=target, version=version, force_user=force_user, force_system=force_system, no_default_libs=no_default_libs, **kwargs) ui.echo('New PROS Project was created:', output_machine=False) ctx.invoke(info_project, project=project) if compile_after or build_cache: with ui.Notification(): ui.echo('Building project...') ctx.exit(project.compile([], scan_build=build_cache)) except Exception as e: pros.common.logger(__name__).exception(e) ctx.exit(-1)
def get_remote_templates(self, auto_check_freq: Optional[timedelta] = None, force_check: bool = False, **kwargs): if auto_check_freq is None: auto_check_freq = getattr(self, 'update_frequency', cli_config().update_frequency) logger(__name__).info( f'Last check of {self.name} was {self.last_remote_update} ' f'({datetime.now() - self.last_remote_update} vs {auto_check_freq}).' ) if force_check or datetime.now( ) - self.last_remote_update > auto_check_freq: with ui.Notification(): ui.echo(f'Updating {self.name}... ', nl=False) self.update_remote_templates(**kwargs) ui.echo('Done', color='green') for t in self.remote_templates: t.metadata['origin'] = self.name return self.remote_templates
def get_manifest(self, force: bool = False) -> UpgradeManifestV1: if not force and not self.has_stale_manifest: return self._manifest ui.echo('Fetching upgrade manifest...') import requests import jsonpickle import json channel_url = f'https://purduesigbots.github.io/pros-mainline/{self.release_channel.value}' self._manifest = None manifest_urls = [ f"{channel_url}/{manifest.__name__}.json" for manifest in manifests ] for manifest_url in manifest_urls: resp = requests.get(manifest_url) if resp.status_code == 200: try: self._manifest = jsonpickle.decode(resp.text, keys=True) logger(__name__).debug(self._manifest) self._last_check = datetime.now() self.save() break except json.decoder.JSONDecodeError as e: logger(__name__).warning( f'Failed to decode {manifest_url}') logger(__name__).debug(e) else: logger(__name__).debug( f'Failed to get {manifest_url} ({resp.status_code})') if not self._manifest: manifest_list = "\n".join(manifest_urls) logger(__name__).warning( f'Could not access any upgrade manifests from any of:\n{manifest_list}' ) return self._manifest
def prompt_to_send(event: Dict[str, Any], hint: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: """ Asks the user for permission to send data to Sentry """ global cli_config with ui.Notification(): if cli_config is None or (cli_config.offer_sentry is not None and not cli_config.offer_sentry): return if 'extra' in event and not event['extra'].get('sentry', True): ui.logger(__name__).debug('Not sending candidate event because event was tagged with extra.sentry = False') return if 'exc_info' in hint and (not getattr(hint['exc_info'][1], 'sentry', True) or any(isinstance(hint['exc_info'][1], t) for t in SUPPRESSED_EXCEPTIONS)): ui.logger(__name__).debug('Not sending candidate event because exception was tagged with sentry = False') return if not event['tags']: event['tags'] = dict() extra_text = '' if 'message' in event: extra_text += event['message'] + '\n' if 'culprit' in event: extra_text += event['culprit'] + '\n' if 'logentry' in event and 'message' in event['logentry']: extra_text += event['logentry']['message'] + '\n' if 'exc_info' in hint: import traceback extra_text += ''.join(traceback.format_exception(*hint['exc_info'], limit=4)) event['tags']['confirmed'] = ui.confirm('We detected something went wrong! Do you want to send a report?', log=extra_text) if event['tags']['confirmed']: ui.echo('Sending bug report.') ui.echo(f'Want to get updates? Visit https://pros.cs.purdue.edu/report.html?event={event["event_id"]}') return event else: ui.echo('Not sending bug report.')
def _output(data: dict): data['type'] = 'input/interactive' if ui.ismachineoutput(): ui._machineoutput(data) else: ui.echo(str(data))
def upload(path: str, project: Optional[c.Project], port: str, **kwargs): """ Upload a binary to a microcontroller. [PATH] may be a directory or file. If a directory, finds a PROS project root and uploads the binary for the correct target automatically. If a file, then the file is uploaded. Note that --target must be specified in this case. [PORT] may be any valid communication port file, such as COM1 or /dev/ttyACM0. If left blank, then a port is automatically detected based on the target (or as supplied by the PROS project) """ import pros.serial.devices.vex as vex from pros.serial.ports import DirectPort args = [] if path is None or os.path.isdir(path): if project is None: project_path = c.Project.find_project(path or os.getcwd()) if project_path is None: raise click.UsageError( 'Specify a file to upload or set the cwd inside a PROS project' ) project = c.Project(project_path) path = os.path.join(project.location, project.output) if project.target == 'v5' and not kwargs['name']: kwargs['name'] = project.name # apply upload_options as a template options = dict(**project.upload_options) options.update(kwargs) kwargs = options kwargs[ 'target'] = project.target # enforce target because uploading to the wrong uC is VERY bad if 'program-version' in kwargs: kwargs['version'] = kwargs['program-version'] if 'name' not in kwargs: kwargs['name'] = project.name if 'target' not in kwargs: logger(__name__).debug( f'Target not specified. Arguments provided: {kwargs}') raise click.UsageError( 'Target not specified. specify a project (using the file argument) or target manually' ) if kwargs['target'] == 'v5': port = resolve_v5_port(port, 'system') elif kwargs['target'] == 'cortex': port = resolve_cortex_port(port) else: logger(__name__).debug(f"Invalid target provided: {kwargs['target']}") logger(__name__).debug('Target should be one of ("v5" or "cortex").') if not port: return -1 if kwargs['target'] == 'v5': if kwargs['name'] is None: kwargs['name'] = os.path.splitext(os.path.basename(path))[0] args.append(kwargs.pop('name').replace('@', '_')) kwargs['slot'] -= 1 if kwargs['run_after'] and kwargs['run_screen']: kwargs['run_after'] = vex.V5Device.FTCompleteOptions.RUN_SCREEN elif kwargs['run_after'] and not kwargs['run_screen']: kwargs[ 'run_after'] = vex.V5Device.FTCompleteOptions.RUN_IMMEDIATELY else: kwargs['run_after'] = vex.V5Device.FTCompleteOptions.DONT_RUN elif kwargs['target'] == 'cortex': pass # print what was decided ui.echo('Uploading {} to {} device on {}'.format(path, kwargs['target'], port), nl=False) if kwargs['target'] == 'v5': ui.echo(f' as {args[0]} to slot {kwargs["slot"] + 1}', nl=False) ui.echo('') logger(__name__).debug('Arguments: {}'.format(str(kwargs))) if not os.path.isfile(path) and path is not '-': logger(__name__).error( '{} is not a valid file! Make sure it exists (e.g. by building your project)' .format(path)) return -1 # Do the actual uploading! try: ser = DirectPort(port) device = None if kwargs['target'] == 'v5': device = vex.V5Device(ser) elif kwargs['target'] == 'cortex': device = vex.CortexDevice(ser).get_connected_device() with click.open_file(path, mode='rb') as pf: device.write_program(pf, *args, **kwargs) except Exception as e: logger(__name__).exception(e, exc_info=True) exit(1) ui.finalize('upload', f'Finished uploading {path} to {kwargs["target"]} on {port}')
def create_template(ctx, path: str, destination: str, do_zip: bool, **kwargs): """ Create a template to be used in other projects Templates primarily consist of the following fields: name, version, and files to install. Templates have two types of files: system files and user files. User files are files in a template intended to be modified by users - they are not replaced during upgrades or removed by default when a library is uninstalled. System files are files that are for the "system." They get replaced every time the template is upgraded. The default PROS project is a template. The user files are files like src/opcontrol.c and src/initialize.c, and the system files are files like firmware/libpros.a and include/api.h. You should specify the --system and --user options multiple times to include more than one file. Both flags also accept glob patterns. When a glob pattern is provided and inside a PROS project, then all files that match the pattern that are NOT supplied by another template are included. Example usage: pros conduct create-template . libblrs 2.0.1 --system "firmware/*.a" --system "include/*.h" """ project = c.Project.find_project(path, recurse_times=1) if project: project = c.Project(project) path = project.location if not kwargs['supported_kernels'] and kwargs['name'] != 'kernel': kwargs['supported_kernels'] = f'^{project.kernel}' kwargs['target'] = project.target if not destination: if os.path.isdir(path): destination = path else: destination = os.path.dirname(path) kwargs['system_files'] = list(kwargs['system_files']) kwargs['user_files'] = list(kwargs['user_files']) kwargs['metadata'] = { ctx.args[i][2:]: ctx.args[i + 1] for i in range(0, int(len(ctx.args) / 2) * 2, 2) } def get_matching_files(globs: List[str]) -> Set[str]: matching_files: List[str] = [] _path = os.path.normpath(path) + os.path.sep for g in [g for g in globs if glob.has_magic(g)]: files = glob.glob(f'{path}/{g}', recursive=True) files = filter(lambda f: os.path.isfile(f), files) files = [ os.path.normpath(os.path.normpath(f).split(_path)[-1]) for f in files ] matching_files.extend(files) # matches things like src/opcontrol.{c,cpp} so that we can expand to src/opcontrol.c and src/opcontrol.cpp pattern = re.compile(r'^([\w{}]+.){{((?:\w+,)*\w+)}}$'.format( os.path.sep.replace('\\', '\\\\'))) for f in [os.path.normpath(f) for f in globs if not glob.has_magic(f)]: if re.match(pattern, f): matches = re.split(pattern, f) logger(__name__).debug(f'Matches on {f}: {matches}') matching_files.extend( [f'{matches[1]}{ext}' for ext in matches[2].split(',')]) else: matching_files.append(f) matching_files: Set[str] = set(matching_files) return matching_files matching_system_files: Set[str] = get_matching_files( kwargs['system_files']) matching_user_files: Set[str] = get_matching_files(kwargs['user_files']) matching_system_files: Set[ str] = matching_system_files - matching_user_files # exclude existing project.pros and template.pros from the template, # and name@*.zip so that we don't redundantly include ZIPs exclude_files = { 'project.pros', 'template.pros', *get_matching_files([f"{kwargs['name']}@*.zip"]) } if project: exclude_files = exclude_files.union(project.list_template_files()) matching_system_files = matching_system_files - exclude_files matching_user_files = matching_user_files - exclude_files def filename_remap(file_path: str) -> str: if os.path.dirname(file_path) == 'bin': return file_path.replace('bin', 'firmware', 1) return file_path kwargs['system_files'] = list(map(filename_remap, matching_system_files)) kwargs['user_files'] = list(map(filename_remap, matching_user_files)) if do_zip: if not os.path.isdir( destination) and os.path.splitext(destination)[-1] != '.zip': logger(__name__).error( f'{destination} must be a zip file or an existing directory.') return -1 with tempfile.TemporaryDirectory() as td: template = ExternalTemplate(file=os.path.join(td, 'template.pros'), **kwargs) template.save() if os.path.isdir(destination): destination = os.path.join(destination, f'{template.identifier}.zip') with zipfile.ZipFile(destination, mode='w') as z: z.write(template.save_file, arcname='template.pros') for file in matching_user_files: source_path = os.path.join(path, file) dest_file = filename_remap(file) if os.path.exists(source_path): ui.echo(f'U: {file}' + ( f' -> {dest_file}' if file != dest_file else '')) z.write(f'{path}/{file}', arcname=dest_file) for file in matching_system_files: source_path = os.path.join(path, file) dest_file = filename_remap(file) if os.path.exists(source_path): ui.echo(f'S: {file}' + ( f' -> {dest_file}' if file != dest_file else '')) z.write(f'{path}/{file}', arcname=dest_file) else: if os.path.isdir(destination): destination = os.path.join(destination, 'template.pros') template = ExternalTemplate(file=destination, **kwargs) template.save()
def test(): ui.echo('Hello World!') with ui.Notification(): ui.echo('Hello from another box') ui.echo('Back on the other one', nl=False) ui.echo('Whoops I missed a newline') with ui.Notification(): ui.echo('Yet another box') with ui.progressbar(range(20)) as bar: for _ in bar: time.sleep(0.1) ui.echo('more below the ', nl=False) ui.echo('progressbar') ui.echo('Back in the other notification') logger(__name__).warning('Hello') try: raise Exception('Hey') except Exception as e: logger(__name__).exception(e) ui.finalize('test', {'hello': 'world'}, human_prefix='Created ')