def poll(command: str) -> Deletion: if command.lower() == Deletion.HARD.name.lower(): if show_confirmation: print(Style.BRIGHT + "Permanently deleted {}".format(path)) if dry: logger.debug("Operating in dry mode. Would otherwise have deleted {}".format(path)) else: delete_fn(path) logger.debug("Permanently deleted {}".format(path)) return Deletion.HARD elif command.lower() == Deletion.TRASH.name.lower(): if dry: logger.debug("Operating in dry mode. Would otherwise have trashed {} to {}".format(path, trash_dir)) else: shutil.move(path, trash_dir) logger.debug("Trashed {} to {}".format(path, trash_dir)) if show_confirmation: print(Style.BRIGHT + "Trashed {} to {}".format(path, trash_dir)) return Deletion.TRASH elif command.lower() == Deletion.NO.name.lower() or len(command) == 0 and allow_ignore: logger.debug("Will not delete {}".format(path)) return Deletion.NO else: print(Fore.RED + "Enter {}".format(' or '.join(choices))) return None
def wrap_cmd_call(cmd: List[str], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell_cmd: str = None, cwd: Optional[str] = None, timeout_secs: Optional[float] = None) -> (str, str): """Calls an external command, waits, and throws a ExternalCommandFailed for nonzero exit codes. Returns (stdout, stderr). The user can optionally provide a shell to run the command with, e.g. "powershell.exe" """ cmd = [str(p) for p in cmd] if shell_cmd: cmd = [shell_cmd] + cmd logger.debug("Calling '{}'".format(' '.join(cmd))) p = subprocess.Popen(cmd, stdout=stdout, stderr=stderr, cwd=cwd) out, err, exit_code = None, None, None try: (out, err) = p.communicate(timeout=timeout_secs) out = out.decode('utf-8') err = err.decode('utf-8') exit_code = p.wait(timeout=timeout_secs) except Exception as e: _log(out, err, logger.warning) raise e finally: p.kill() if exit_code != 0: _log(out, err, logger.warning) raise ExternalCommandFailed( "Got nonzero exit code {} from '{}'".format( exit_code, ' '.join(cmd)), cmd, exit_code, out, err) _log(out, err, logger.debug) return out, err
def run(self, args: List[str]) -> None: full_args = self.parser.parse_args(args[1:2]) subcommand = full_args.subcommand.replace('-', '_') if not hasattr(self.target, subcommand) and not subcommand.startswith('_'): print(Fore.RED + 'Unrecognized subcommand {}'.format(subcommand)) self.parser.print_help() return # clever; from Chase Seibert: https://chase-seibert.github.io/blog/2014/03/21/python-multilevel-argparse.html # use dispatch pattern to invoke method with same name try: if self.temp_dir is not None: if pexists(self.temp_dir) and pdir(self.temp_dir): shutil.rmtree(self.temp_dir) elif pexists(self.temp_dir): raise InvalidDirectoryException(self.temp_dir) remake_dirs(self.temp_dir) logger.debug("Created temp dir at {}".format(self.temp_dir)) getattr(self.target, subcommand)() except NaturalExpectedException as e: pass # ignore totally except KeyboardInterrupt as e: try: logger.fatal("Received cancellation signal", exc_info=True) self.cancel_handler(e) except BaseException: pass raise e except SystemExit as e: try: logger.fatal("Received system exit signal", exc_info=True) self.cancel_handler(e) except BaseException: pass raise e except BaseException as e: try: logger.fatal("{} failed!".format(self.parser.prog), exc_info=True) self.error_handler(e) except BaseException: pass raise e finally: if self.temp_dir is not None: if pexists(self.temp_dir): logger.debug("Deleted temp dir at {}".format( self.temp_dir)) shutil.rmtree(self.temp_dir) try: os.remove(self.temp_dir) except IOError: pass
def smart_log_callback(source, line, prefix: str = '') -> None: line = line.decode('utf-8') if line.startswith('FATAL:'): logger.fatal(prefix + line) elif line.startswith('ERROR:'): logger.error(prefix + line) elif line.startswith('WARNING:'): logger.warning(prefix + line) elif line.startswith('INFO:'): logger.info(prefix + line) elif line.startswith('DEBUG:'): logger.debug(prefix + line) else: logger.debug(prefix + line)
def slow_delete(path: str, wait: int = 5, delete_fn: Callable[[str], None] = deletion_fn): logger.debug("Deleting directory tree {} ...".format(path)) print(Fore.BLUE + "Waiting for {}s before deleting {}: ".format(wait, path), end='') for i in range(0, wait): time.sleep(1) print(Fore.BLUE + str(wait-i) + ' ', end='') time.sleep(1) print(Fore.BLUE + '...', end='') chmod_err = delete_fn(path) print(Fore.BLUE + ' deleted.') #if chmod_err is not None: # try: # raise chmod_err # except: # logger.warning("Couldn't chmod {}".format(path), exc_info=True) logger.debug("Deleted directory tree {}".format(path))
def stream_cmd_call(cmd: List[str], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell_cmd: str = None, cwd: Optional[str] = None, timeout_secs: Optional[float] = None, log_callback: Callable[[PipeType, bytes], None] = None, bufsize: int = 1) -> None: """Calls an external command, waits, and throws a ExternalCommandFailed for nonzero exit codes. Returns (stdout, stderr). The user can optionally provide a shell to run the command with, e.g. "powershell.exe" """ if log_callback is None: log_callback = smart_log_callback cmd = [str(p) for p in cmd] if shell_cmd: cmd = [shell_cmd] + cmd logger.debug("Streaming '{}'".format(' '.join(cmd))) p = subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE, cwd=cwd, bufsize=bufsize) try: q = Queue() Thread(target=_reader, args=[PipeType.STDOUT, p.stdout, q]).start() Thread(target=_reader, args=[PipeType.STDERR, p.stderr, q]).start() for _ in range(2): for source, line in iter(q.get, None): log_callback(source, line) exit_code = p.wait(timeout=timeout_secs) finally: p.kill() if exit_code != 0: raise ExternalCommandFailed( "Got nonzero exit code {} from '{}'".format( exit_code, ' '.join(cmd)), cmd, exit_code, '<<unknown>>', '<<unknown>>')
def deletion_fn(path) -> Optional[Exception]: """ Deletes files or directories, which should work even in Windows. :return Returns None, or an Exception for minor warnings """ # we need this because of Windows chmod_err = None try: os.chmod(path, stat.S_IRWXU) except Exception as e: chmod_err = e # another reason for returning exception: # We don't want to interrupt the current line being printed like in slow_delete if os.path.isdir(path): shutil.rmtree(path, ignore_errors=True) # ignore_errors because of Windows try: os.remove(path) # again, because of Windows except IOError: pass # almost definitely because it doesn't exist else: os.remove(path) logger.debug("Permanently deleted {}".format(path)) return chmod_err