def _repl_serial(self, serial_ok): """Thread, copies bytes from serial to out""" term = Terminal() try: with serial_ok, term.raw(): save_timeout = self._serial.timeout # Set a timeout so that the read returns periodically with no data # and allows us to check whether the main thread wants us to quit. self._serial.timeout = 0.4 while not self._quit_serial_reader: char = self._serial.read(1) if char.decode('utf-8') != '': print(char.decode('utf-8'), end='', flush=True) self._serial.timeout = save_timeout except ConnectionError as e: self.disconnect() print('\r') eprint(str(e).replace('\n', '\r')) except Exception: # catchall, print error traceback from io import StringIO s = StringIO() print('\r', printing.ERR_COLOR) traceback.print_exc(file=s) eprint(s.getvalue().replace('\n', '\r'))
def do_rm(self, line): """rm [-f|--force] FILE... Remove one or more files rm [-f|--force] PATTERN Remove multiple files rm -r [-f|--force] [FILE|DIRECTORY]... Files and/or directories rm -r [-f|--force] PATTERN Multiple files and/or directories Removes files or directories. To remove directories (and any contents) -r must be specified. """ args = self.line_to_args(line) filenames = args.filename # Process PATTERN sfn = filenames[0] if is_pattern(sfn): if len(filenames) > 1: eprint("Usage: rm [-r] [-f] PATTERN") return filenames = process_pattern(self.boards, self.cur_dir, sfn) if filenames is None: return for filename in filenames: filename = resolve_path(self.cur_dir, filename) if not rm( self.boards, filename, recursive=args.recursive, force=args.force): if not args.force: eprint( "Unable to remove '{}' (try -rf if you are sure)".format( filename)) break
def do_run(self, line): """run [FILE] Send file to remote for execution and print results on console. If FILE is not specified, executes the file from the last invocation. """ global LAST_RUN_FILE args = line.split() if len(args) > 1: eprint("*** Only one file, please!") return if len(args) == 0: file = LAST_RUN_FILE qprint("run '{}' on micropython board".format(file)) else: file = os.path.join(self.cur_dir, args[0]) LAST_RUN_FILE = file print(printing.MPY_COLOR, end='') try: self.boards.default.execfile(file, data_consumer=putch, timeout=None) except FileNotFoundError: eprint("*** File not found on host, '{}'".format(file)) except KeyboardInterrupt: print()
def do_repl(self, line): """repl [board-name] [~ line [~]] Enters into the regular REPL (read-eval-print-loop) with the MicroPython board. Use Control-X to exit REPL mode and return the shell. It may take a couple of seconds before the REPL exits. If you provide a line to the repl command, then that will be executed. If you want the REPL to exit, end the line with the ~ character. """ args = self.line_to_args(line) if len(args) > 0 and line[0] != '~': name = args[0] line = ' '.join(args[1:]) board = self.boards.find_board(name) if not board: eprint("No board '{}'".format(name)) return else: board = self.boards.default if line[0:2] == '~ ': line = line[2:] mprint('Entering REPL. Control-%c to exit.' % QUIT_REPL_CHAR) mprint(' Soft reset: Control-D or sys.exit()') mprint(' Hard reset: Reset button on board or machine.reset()') print(printing.MPY_COLOR, end='') board.repl(getch, putch) print()
def do_mkdir(self, line): """mkdir DIRECTORY... Creates one or more directories. """ args = self.line_to_args(line) for filename in args: filename = resolve_path(self.cur_dir, filename) if not mkdir(self.boards, filename): eprint('Unable to create %s' % filename)
def process_pattern(devs, cur_dir, fn): """Return a list of paths matching a pattern (or None on error). """ directory, pattern = validate_pattern(devs, cur_dir, fn) if directory is not None: filenames = fnmatch.filter(auto(devs, listdir, directory), pattern) if filenames: return [directory + '/' + sfn for sfn in filenames] else: eprint("cannot access '{}': No such file or directory".format(fn))
def remote_eval(self, func, *args, **kwargs): """Calls func with the indicated args on the micropython board, and converts the response back into python by using eval. """ res = self.remote(func, *args, **kwargs) try: return eval(res) except (SyntaxError, ValueError) as e: eprint("*** remote_eval({}, {}, {}) -> \n{} is not valid python code".format( func.__name__, args, kwargs, res.decode('utf-8'))) return None
def __init__(self, port=None, baudrate=115200): self.is_circuitpy = False try: # check which ports are available if not port: for p in comports(): if p.vid == ADAFRUIT_VID: port = p.device self.is_circuitpy = True break elif p.vid == ESP32_VID: port = p.device break elif p.vid: qprint(f"Unknown board {p} with vid '{p.vid}' skipped") # did we find a valid board? if not port: eprint("No board found") sys.exit(1) # wait for port to come online for wait in range(3): if os.path.exists(port): break qprint("Waiting for port '{}' to come online".format(port)) time.sleep(1) # try to connect for attempt in range(5): try: self._serial = Serial(port, baudrate, parity='N', inter_byte_timeout=1) break except IOError as e: qprint( "Waiting for serial connection at '{}'".format(port)) qprint(e) time.sleep(1) # send Control-C to put MicroPython in known state for attempt in range(20): try: self._serial.write(b'\x03') break except SerialException: time.sleep(0.5) qprint("Trying to talk to the MicroPython interpreter") self._port = port qprint(f"SerialConnection to {port} established") except AttributeError: raise ConnectionError( "Failed connecting to board at '{}'".format(port)) except KeyboardInterrupt: self._serial = None
def do_ls(self, line): """ls [-a] [-l] [FILE|DIRECTORY|PATTERN]... PATTERN supports * ? [seq] [!seq] Unix filename matching List directory contents. """ args = self.line_to_args(line) if len(args.filenames) == 0: args.filenames = ['.'] for idx, fn in enumerate(args.filenames): if not is_pattern(fn): filename = resolve_path(self.cur_dir, fn) stat = auto(self.boards, get_stat, filename) mode = stat_mode(stat) if not mode_exists(mode): err = "Cannot access '{}': No such file or directory" eprint(err.format(filename)) continue if not mode_isdir(mode): if args.long: print_long(filename, stat, oprint) else: oprint(filename) continue if len(args.filenames) > 1: if idx > 0: oprint('') oprint("%s:" % filename) pattern = '*' else: # A pattern was specified filename, pattern = validate_pattern(self.boards, self.cur_dir, fn) if filename is None: # An error was printed continue files = [] ldir_stat = auto(self.boards, listdir_stat, filename) if ldir_stat is None: err = "Cannot access '{}': No such file or directory" eprint(err.format(filename)) else: for filename, stat in sorted(ldir_stat, key=lambda entry: entry[0]): if is_visible(filename) or args.all: if fnmatch(filename, pattern): if args.long: print_long(filename, stat, oprint) else: files.append(decorated_filename(filename, stat)) if len(files) > 0: print_cols(sorted(files), oprint, shutil.get_terminal_size().columns)
def _load(self): qprint("Loading configuration '{}'".format(self._config_file)) try: with open(self._config_file) as f: self._config = literal_eval(f.read()) except FileNotFoundError: oprint("WARNING: configuration '{}' does not exist, creating default".format(self._config_file)) self._create_default() self._modified = True except SyntaxError as e: eprint("Syntax error in {}: {}".format(self._config_file, e)) eprint("If the problem persists, manually check the file for " "invalid Python syntax or delete it and re-enter the configuration information.") sys.exit()
def do_connect(self, line): """ connect TYPE TYPE_PARAMS Connect boards to shell49. connect serial [port [baud]] Wired connection. Uses defaults from config file. connect telnet [url [user [pwd]]] Wireless connection. If no url/ip address is specified, connects to all known boards advertising repl service via mDNS. Optional user (default: 'micro') and password (default: 'python'). Note: do not connect to the same board via serial AND telnet connections. Doing so may block communication with the board. """ args = self.line_to_args(line) if len(args) < 1: eprint('Missing connection TYPE') return connect_type = args[0] if connect_type == 'serial': port = args[1] if len(args) > 1 else self.config.get( 0, 'port', '/dev/cu.SLAB_USBtoUART') baud = args[2] if len(args) > 2 else self.config.get( 0, 'baudrate', '115200') try: baud = int(baud) except ValueError: eprint("Not a valid baudrate, '{}'".format(baud)) return # Note: board may be connected over telnet, but we don't know ... # in this case, connect blocks if self.boards.find_board(port): eprint("board already connected on '{}'".format(port)) return self.boards.connect_serial(port, baud) elif connect_type == 'telnet': if len(args) > 1: user = args[2] if len(args) > 2 else 'micro' pwd = args[3] if len(args) > 3 else 'python' self.boards.connect_telnet(args[1], user, pwd) else: listener = MdnsListener() adv = list(listener.listen(seconds=1)) if len(adv) == 0: qprint("No boards detected via mDNS.") for b in adv: qprint("Heard from '{}' ({})".format(b.url, b.ip)) # connect only to boards in the config database board_id = self.config.get_board_from_name(b.hostname) if not board_id: qprint(" not in db, skip!") continue # we are not already connected to if self.boards.connected(b.hostname): qprint(" already connected") continue # let's connect! user = self.config.get(board_id, 'user', 'micro') pwd = self.config.get(board_id, 'password', 'python') self.boards.connect_telnet(b.url, user, pwd) else: eprint('Unrecognized connection TYPE: {}'.format(connect_type))
def do_flash(self, line): """flash [-l|--list] [-e|--erase] [-v|--version VERSION] [-b|--board BOARD] Flash firmware to microcontroller. """ args = self.line_to_args(line) firmware_url = "https://people.eecs.berkeley.edu/~boser/iot49/firmware" flash_options = "--chip esp32 " \ "--before default_reset --after hard_reset " \ "write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect" id = 0 try: id = self.boards.default.id except BoardError: pass firmware_url = self.config.get(id, "firmware_url", firmware_url) flash_options = self.config.get(id, "flash_options", flash_options) port = self.config.get(id, "port", "/dev/cu.SLAB_USBtoUART") baudrate = self.config.get(id, "flash_baudrate", 921600) board = self.config.get(id, "board", "HUZZAH32") if args.board: board = args.board dprint("firmware url: ", firmware_url) dprint("flash options:", flash_options) dprint("port: ", port) dprint("baudrate: ", baudrate) dprint("board: ", board) try: f = Flasher(board=board, url=firmware_url) if args.list: print("available firmware versions:") print('\n'.join( [" {:8s} {}".format(v, d) for v, d in f.versions()])) return dev = self.boards.find_board(port) if dev: dev.disconnect() if args.erase: f.erase_flash(port) f.flash(args.version, flash_options=flash_options, port=port, baudrate=baudrate) except FlasherError as e: eprint(e)
def make_dir(devs, dst_dir, dry_run, recursed): """Creates a directory. Produces information in case of dry run. Isues error where necessary. """ parent = os.path.split(dst_dir.rstrip( '/'))[0] # Check for nonexistent parent parent_files = auto(devs, listdir_stat, parent) if parent else True # Relative dir if dry_run: if recursed: # Assume success: parent not actually created yet qprint("Creating directory {}".format(dst_dir)) elif parent_files is None: qprint("Unable to create {}".format(dst_dir)) return True if not mkdir(devs, dst_dir): eprint("Unable to create {}".format(dst_dir)) return False return True
def do_cat(self, line): """cat FILENAME... Concatenates files and sends to stdout. """ # note: when we get around to supporting cat from stdin, we'll need # to write stdin to a temp file, and then copy the file # since we need to know the filesize when copying to the pyboard. args = self.line_to_args(line) for filename in args: filename = resolve_path(self.cur_dir, filename) mode = auto(self.boards, get_mode, filename) if not mode_exists(mode): eprint("Cannot access '%s': No such file" % filename) continue if not mode_isfile(mode): eprint("'%s': is not a file" % filename) continue cat(self.boards, filename, self.stdout)
def do_rsync(self, line): """rsync [-m|--mirror] [-n|--dry-run] [SRC_DIR [DST_DIR]] Synchronize destination directory tree to source directory tree. """ db = self.boards.default args = self.line_to_args(line) sd = args.src_dst_dir if len(sd) > 2: eprint("*** More than one destination directory given") return src_dir = sd[0] if len(sd) > 0 else db.get_config('host_dir', '~/iot49') dst_dir = sd[1] if len(sd) > 1 else db.get_config('remote_dir', '/flash') src_dir = resolve_path(self.cur_dir, src_dir) dst_dir = resolve_path(self.cur_dir, dst_dir) if len(sd) < 2: qprint("synchronizing {} --> {}".format(src_dir, dst_dir)) rsync(self.boards, src_dir, dst_dir, mirror=not args.mirror, dry_run=args.dry_run, recursed=True)
def do_cd(self, line): """cd DIRECTORY Changes the current directory. ~ expansion is supported, and cd - goes to the previous directory. """ args = self.line_to_args(line) if len(args) == 0: dirname = '~' else: if args[0] == '-': dirname = self.prev_dir else: dirname = args[0] dirname = resolve_path(self.cur_dir, dirname) mode = auto(self.boards, get_mode, dirname) if mode_isdir(mode): self.prev_dir = self.cur_dir self.cur_dir = dirname auto(self.boards, chdir, dirname) else: eprint("Directory '%s' does not exist" % dirname)
def do_edit(self, line): """edit FILE Copies the file locally, launches an editor to edit the file. When the editor exits, if the file was modified then its copied back. You can specify the editor used with the --editor command line option when you start shell49, or by using the SHELL49_EDITOR or VISUAL or EDITOR environment variable. If none of those are set, then vi will be used. """ if len(line) == 0: eprint("Must provide a filename") return filename = resolve_path(self.cur_dir, line) dev, dev_filename = self.boards.get_dev_and_path(filename) mode = auto(self.boards, get_mode, filename) if mode_exists(mode) and mode_isdir(mode): eprint("Unable to edit directory '{}'".format(filename)) return if dev is None: # File is local os.system("{} '{}'".format(self.editor, filename)) else: # File is remote with tempfile.TemporaryDirectory() as temp_dir: local_filename = os.path.join(temp_dir, os.path.basename(filename)) if mode_exists(mode): print('Retrieving {} ...'.format(filename)) cp(filename, local_filename) old_stat = get_stat(local_filename) os.system("{} '{}'".format(self.editor, local_filename)) new_stat = get_stat(local_filename) if old_stat != new_stat: print('Updating {} ...'.format(filename)) cp(local_filename, filename)
def onecmd(self, line): """Global error catcher""" try: res = cmd.Cmd.onecmd(self, line) if self.interactive: self.set_prompt() return res except (BoardError, ConnectionError) as e: eprint("***", e) except UnicodeDecodeError as e: eprint("***", e) except KeyboardInterrupt: eprint("Command aborted") except Exception as e: # catchall, to prevent shell from quitting print(printing.ERR_COLOR) traceback.print_exc(file=sys.stdout)
def validate_pattern(devs, cur_dir, fn): """On success return an absolute path and a pattern. Otherwise print a message and return None, None """ directory, pattern = parse_pattern(fn) if directory is None: eprint("Invalid pattern {}.".format(fn)) return None, None target = resolve_path(cur_dir, directory) mode = auto(devs, get_mode, target) if not mode_exists(mode): eprint("cannot access '{}': No such file or directory".format(fn)) return None, None if not mode_isdir(mode): eprint("cannot access '{}': Not a directory".format(fn)) return None, None return directory, pattern
def do_cp(self, line): """cp SOURCE DEST Copy a single SOURCE file to DEST file. cp SOURCE... DIRECTORY Copy multiple SOURCE files to a directory. cp [-r] PATTERN DIRECTORY Copy matching files to DIRECTORY. cp [-r|--recursive] [SOURCE|SOURCE_DIR]... DIRECTORY The destination must be a directory except in the case of copying a single file. To copy directories -r must be specified. This will cause directories and their contents to be recursively copied. """ args = self.line_to_args(line) if len(args.filenames) < 2: eprint('Missing destination file') return dst_dirname = resolve_path(self.cur_dir, args.filenames[-1]) dst_mode = auto(self.boards, get_mode, dst_dirname) d_dst = {} # Destination directory: lookup stat by basename if args.recursive: dst_files = auto(self.boards, listdir_stat, dst_dirname) if dst_files is None: err = "cp: target {} is not a directory" eprint(err.format(dst_dirname)) return for name, stat in dst_files: d_dst[name] = stat src_filenames = args.filenames[:-1] # Process PATTERN sfn = src_filenames[0] if is_pattern(sfn): if len(src_filenames) > 1: eprint("Usage: cp [-r] PATTERN DIRECTORY") return src_filenames = process_pattern(self.boards, self.cur_dir, sfn) if src_filenames is None: return for src_filename in src_filenames: if is_pattern(src_filename): eprint("Only one pattern permitted.") return src_filename = resolve_path(self.cur_dir, src_filename) src_mode = auto(self.boards, get_mode, src_filename) if not mode_exists(src_mode): eprint("File '{}' doesn't exist".format(src_filename)) return if mode_isdir(src_mode): if args.recursive: # Copying a directory src_basename = os.path.basename(src_filename) dst_filename = os.path.join(dst_dirname, src_basename) if src_basename in d_dst: dst_stat = d_dst[src_basename] dst_mode = stat_mode(dst_stat) if not mode_isdir(dst_mode): err = "Destination {} is not a directory" eprint(err.format(dst_filename)) return else: if not mkdir(dst_filename): err = "Unable to create directory {}" eprint(err.format(dst_filename)) return rsync(src_filename, dst_filename, mirror=False, dry_run=False, print_func=lambda *args: None, recursed=False) else: eprint("Omitting directory {}".format(src_filename)) continue if mode_isdir(dst_mode): dst_filename = os.path.join( dst_dirname, os.path.basename(src_filename)) else: dst_filename = dst_dirname if not cp(self.boards, src_filename, dst_filename): err = "Unable to copy '{}' to '{}'" eprint(err.format(src_filename, dst_filename)) break
def main(): """The main program.""" if sys.version_info.major < 3: v = sys.version_info eprint("Shell49 requires Python 3.6 (not {}.{}.{})".format( v.major, v.minor, v.micro)) return default_config = os.getenv('SHELL49_CONFIG_FILE') or '~/.shell49_rc.py' default_editor = os.getenv('SHELL49_EDITOR') or os.getenv( 'VISUAL') or os.getenv('EDITOR') or 'vi' default_nocolor = 'win32' in sys.platform default_debug = False default_quiet = False parser = argparse.ArgumentParser( prog="shell49", usage="%(prog)s [options] [cmd]", description="Remote Shell for MicroPython boards.", epilog=(""" Environment variables: SHELL49_CONFIG_FILE configuration file (Default: '{}') SHELL49_EDITOR editor (Default: {}) """.format(default_config, default_editor)), formatter_class=argparse.RawTextHelpFormatter) parser.add_argument( "-c", "--config", dest="config", help="Set path of the configuration file (default: '%s')" % default_config, default=default_config) parser.add_argument("-e", "--editor", dest="editor", help="Set the editor to use (default: '%s')" % default_editor, default=default_editor) parser.add_argument("-d", "--debug", dest="debug", action="store_true", help="Enable debug features (default %s)" % default_debug, default=default_debug) parser.add_argument("-n", "--nocolor", dest="nocolor", action="store_true", help="Turn off colorized output (default: %s)" % default_nocolor, default=default_nocolor) parser.add_argument("--quiet", dest="quiet", action="store_true", help="Turn off some output (default: %s)" % default_quiet, default=False) parser.add_argument( "-a", "--no_auto_connect", dest="auto_connect", action="store_false", help="Do not automatically connect to board connected to serial port", default=True) parser.add_argument('-V', '--version', dest='version', action='store_true', help='Report the version and exit.', default=False) parser.add_argument("-f", "--file", dest="filename", help="File of commands to process (non-interactive).") parser.add_argument("cmd", nargs=argparse.REMAINDER, help="Optional command to execute and quit.") args = parser.parse_args(sys.argv[1:]) debug(args.debug) quiet(args.quiet or args.cmd or args.filename) if args.nocolor: nocolor() dprint("config = %s" % args.config) dprint("editor = %s" % args.editor) dprint("debug = %s" % args.debug) dprint("quiet = %s" % args.quiet) dprint("nocolor = %s" % args.nocolor) dprint("auto_connect = %s" % args.auto_connect) dprint("version = %s" % __version__) dprint("cmd = [%s]" % ', '.join(args.cmd)) if args.version: print(__version__) return cmd_line = ' '.join(args.cmd) if not args.filename and cmd_line == '': oprint( "Welcome to shell49 version {}. Type 'help' for information; Control-D to exit." .format(__version__)) args.config = os.path.expanduser(args.config) args.config = os.path.normpath(args.config) with Config(args.config) as config: boards = ActiveBoards(config) # connect to board ... try: if args.auto_connect: boards.connect_serial(config.get('default', 'port')) except (ConnectionError, BoardError) as err: eprint(err) except KeyboardInterrupt: pass # start command shell attach_commands() if args.filename: with open(args.filename) as cmd_file: shell = Shell(boards, args.editor, stdin=cmd_file) shell.cmdloop('') else: if boards.num_boards() == 0: eprint( "No MicroPython boards connected - use the connect command to add one." ) shell = Shell(boards, args.editor) try: shell.cmdloop(cmd_line) except KeyboardInterrupt: qprint("Bye") print(printing.NO_COLOR)
def do_config(self, line): """config Print option values. config [-u] [-d] [--default] OPTION [VALUE] Set/delete OPTION to VALUE. """ default_board = None board_id = 'default' try: default_board = self.boards.default board_id = default_board.id except BoardError: pass if line == '': # print configuration print_config(self.config, board_id, color=printing.MPY_COLOR) if default_board: oprint("Defaults:") keys = self.config.options(board_id) print_config(self.config, 'default', exc=keys) return # parse arguments args = self.line_to_args(line) if args.default: board_id = 'default' default_board = None value = ' '.join(args.value) try: # try to convert value to Python object (e.g. for numbers) value = literal_eval(value) except: pass if not args.default and not default_board: eprint( "*** No board connected, use --default to change default configuration" ) return # delete / set option value if args.delete: # delete option self.config.remove(board_id, args.option) else: # set option try: self.config.set(board_id, args.option, value) except ConfigError as e: eprint("*** {}".format(e)) # upload if args.upload: if not default_board: eprint("*** No board connected, cannot upload configuration") return with NamedTemporaryFile() as temp: temp.close() f = open(temp.name, 'w') now = datetime.now().strftime("%Y-%b-%d %H:%M:%S") print("# config.py, created on {}".format(now), file=f) for key in default_board.config_options(): print("{} = {}".format(key, repr(default_board.get_config(key))), file=f) print("mac_table = {}".format(repr(self.config.mac_table())), file=f) f.close() dst = os.path.join( default_board.get_config('remote_dir', '/flash'), 'config.py') cp(self.boards, temp.name, dst) os.unlink(temp.name)
def default(self, line): eprint("Unrecognized command:", line)
# Linux: no install needed # This module is usually unnecessary on Linux and other Unix systems with # default readline support. # Win: pyreadline # If you are using Windows, which also ships without GNU readline, you might # want to consider using the pyreadline module instead, which is a readline # replacement written in pure Python that interacts with the Windows clipboard. try: if sys.platform == 'darwin': import gnureadline as readline elif sys.platform == 'win32': # import pyreadline as readline import readline sys.modules['readline'] = readline except ImportError: eprint("could not load platform specific readline, using default instead") import readline import cmd import inspect import importlib import argparse import traceback import os class Shell(cmd.Cmd): def __init__(self, boards, editor): self.boards = boards self.config = boards.config self.editor = editor
def rsync(devs, src_dir, dst_dir, mirror, dry_run, recursed): """Synchronizes 2 directory trees.""" # This test is a hack to avoid errors when accessing /flash. When the # cache synchronisation issue is solved it should be removed if not isinstance(src_dir, str) or not len(src_dir): return # check that source is a directory sstat = auto(devs, get_stat, src_dir) if not is_dir(sstat): eprint("*** Source {} is not a directory".format(src_dir)) return # create destination directory if it does not exist sstat = auto(devs, get_stat, dst_dir) if not file_exists(sstat): qprint("Create {} on remote".format(dst_dir)) if not dry_run: if recursed and not make_dir(devs, dst_dir, dry_run, recursed): eprint("*** Unable to create directory", dst_dir) elif not is_dir(sstat): eprint("*** Destination {} is not a directory".format(src_dir)) return # get list of src & dst files and stats qprint(" checking {}".format(dst_dir)) d_src = file_dir(devs, src_dir) d_dst = file_dir(devs, dst_dir) # determine what needs to be copied or deleted set_dst = set(d_dst.keys()) set_src = set(d_src.keys()) to_add = set_src - set_dst # Files to copy to dest to_del = set_dst - set_src # To delete from dest to_upd = set_dst.intersection(set_src) # In both: may need updating if False: eprint("rsync {} -> {}".format(src_dir, dst_dir)) eprint(" sources", set_src) eprint(" dest ", set_dst) eprint(" add ", to_add) eprint(" delete ", to_del) eprint(" update ", to_upd) # add ... for f in to_add: src = os.path.join(src_dir, f) dst = os.path.join(dst_dir, f) qprint("Adding {}".format(dst)) if is_dir(d_src[f]): if recursed: rsync(devs, src, dst, mirror, dry_run, recursed) else: if not dry_run: if not cp(devs, src, dst): eprint("*** Unable to add {} --> {}".format(src, dst)) # delete ... for f in to_del: if not mirror: break dst = os.path.join(dst_dir, f) qprint("Removing {}".format(dst)) if not dry_run: res = rm(devs, dst, recursive=True, force=True) if not res: eprint("Cannot remove {}", dst) # update ... for f in to_upd: src = os.path.join(src_dir, f) dst = os.path.join(dst_dir, f) if is_dir(d_src[f]): if is_dir(d_dst[f]): # src and dst are directories if recursed: rsync(devs, src, dst, mirror, dry_run, recursed) else: msg = "Source '{}' is a directory and destination " \ "'{}' is a file. Ignoring" eprint(msg.format(src, dst)) else: if is_dir(d_dst[f]): msg = "Source '{}' is a file and destination " \ "'{}' is a directory. Ignoring" eprint(msg.format(src, dst)) else: if False: eprint("BEB src {} > dst {} delta={}".format( stat_mtime(d_src[f]), stat_mtime(d_dst[f]), stat_mtime(d_src[f]) - stat_mtime(d_dst[f]))) if stat_size(d_src[f]) != stat_size(d_dst[f]) or \ stat_mtime(d_src[f]) > stat_mtime(d_dst[f]): msg = "Copying {} (newer than {})" qprint(msg.format(src, dst)) if not dry_run: if not cp(devs, src, dst): eprint( "*** Unable to update {} --> {}".format(src, dst)) else: dprint(f, "NO update src time:", stat_mtime(d_src[f]), "dst time", stat_mtime( d_dst[f]), "delta", stat_mtime(d_src[f]) - stat_mtime(d_dst[f]))