def upload(local_path): peers = [i for i in dispatchers.all_instances() if i.enabled] if not peers: console_output('No other remote shell to replicate files to\n') return if len(peers) == 1: # We wouldn't be able to show the progress indicator with only one # destination. We need one remote connection in blocking mode to send # the base64 data to. We also need one remote connection in non blocking # mode for polysh to display the progress indicator via the main select # loop. console_output('Uploading to only one remote shell is not supported, ' 'use scp instead\n') return def should_print_bw(node, already_chosen=[False]): if not node.children and not already_chosen[0]: already_chosen[0] = True return True return False tree = file_transfer_tree_node(None, peers[0], peers[1:], 0, should_print_bw, path=local_path, is_upload=True)
def dispatch_write(self, buf): """Augment the buffer with stuff to write when possible""" self.write_buffer += buf if len(self.write_buffer) > self.MAX_BUFFER_SIZE: console_output('Buffer too big ({:d}) for {}\n'.format( len(self.write_buffer), str(self)).encode()) raise asyncore.ExitNow(1) return True
def dispatch_write(self, buf): """Augment the buffer with stuff to write when possible""" assert self.allow_write self.write_buffer += buf if len(self.write_buffer) > buffered_dispatcher.MAX_BUFFER_SIZE: console_output('Buffer too big (%d) for %s\n' % (len(self.write_buffer), str(self))) raise asyncore.ExitNow(1)
def handle_expt(self): pid, status = os.waitpid(self.pid, 0) exit_code = os.WEXITSTATUS(status) options.exit_code = max(options.exit_code, exit_code) if exit_code and options.interactive: console_output('Error talking to %s\n' % self.display_name) self.disconnect() if self.temporary: self.close()
def do_chdir(command): """ Usage: :chdir LOCAL_PATH Change the current directory of polysh (not the remote shells). """ try: os.chdir(expand_local_path(command.strip())) except OSError, e: console_output('%s\n' % str(e))
def do_upload(command): """ Usage: :upload LOCAL_PATH Upload the specified local path to enabled remote shells. """ if command: file_transfer.upload(command.strip()) else: console_output('No local path given\n')
def do_list(command): """ Usage: :list [SHELLS...] List remote shells and their states. The output consists of: <hostname> <enabled?> <state>: <last printed line>. The special characters * ? and [] work as expected. """ instances = [i.get_info() for i in selected_shells(command)] dispatchers.format_info(instances) console_output(''.join(instances))
def handle_control_command(line): if not line: return cmd_name = line.split()[0] try: cmd_func = get_control_command(cmd_name) except AttributeError: console_output('Unknown control command: %s\n' % cmd_name) else: parameters = line[len(cmd_name) + 1:] cmd_func(parameters)
def do_set_log(command: str) -> None: command = command.strip() if command: try: remote_dispatcher.options.log_file = open(command, 'a') except IOError as e: console_output('{}\n'.format(str(e)).encode()) command = None if not command: remote_dispatcher.options.log_file = None console_output(b'Logging disabled\n')
def handle_control_command(line): if not line: return cmd_name = line.split()[0] try: cmd_func = get_control_command(cmd_name) except AttributeError: console_output( 'Unknown control command: {}\n'.format(cmd_name).encode()) else: parameters = line[len(cmd_name) + 1:] cmd_func(parameters)
def do_set_log(command): """ Usage: :set_log [LOCAL_PATH] Duplicate every console I/O into the given local file. If LOCAL_PATH is not given, restore the default behaviour of not logging. """ command = command.strip() if command: try: remote_dispatcher.options.log_file = file(command, 'a') except IOError, e: console_output('%s\n' % str(e)) command = None
def do_set_debug(command: str) -> None: split = command.split() if not split: console_output(b'Expected at least a letter\n') return letter = split[0].lower() if letter not in ('y', 'n'): console_output("Expected 'y' or 'n', got: {}\n".format( split[0]).encode()) return debug = letter == 'y' for i in selected_shells(' '.join(split[1:])): i.debug = debug
def do_help(command): """ Usage: :help [COMMAND] List control commands or show their documentations. """ command = command.strip() if command: texts = [] for name in command.split(): try: cmd = get_control_command(name.lstrip(':')) except AttributeError: console_output('Unknown control command: %s\n' % name) else: doc = [d.strip() for d in cmd.__doc__.split('\n') if d.strip()] texts.append('\n'.join(doc)) if texts: console_output('\n\n'.join(texts)) console_output('\n') else: names = list_control_commands() max_name_len = max(map(len, names)) for i in xrange(len(names)): name = names[i] txt = ':' + name + (max_name_len - len(name) + 2) * ' ' doc = get_control_command(name).__doc__ txt += doc.split('\n')[2].strip() + '\n' console_output(txt)
def do_hide_password(command: str) -> None: warned = False for i in dispatchers.all_instances(): if i.enabled and i.debug: i.debug = False if not warned: console_output(b'Debugging disabled to avoid displaying ' b'passwords\n') warned = True stdin.set_echo(False) if remote_dispatcher.options.log_file: console_output(b'Logging disabled to avoid writing passwords\n') remote_dispatcher.options.log_file = None
def do_send_ctrl(command: str) -> None: split = command.split() if not split: console_output(b'Expected at least a letter\n') return letter = split[0] if len(letter) != 1: console_output( 'Expected a single letter, got: {}\n'.format(letter).encode()) return control_letter = chr(ord(letter.lower()) - ord('a') + 1) for i in selected_shells(' '.join(split[1:])): if i.enabled: i.dispatch_write(control_letter.encode())
def handle_close(self): if self.state is STATE_DEAD: # This connection has already been killed. Asyncore has probably # called handle_close() or handle_expt() on this connection twice. return pid, status = os.waitpid(self.pid, 0) exit_code = os.WEXITSTATUS(status) options.exit_code = max(options.exit_code, exit_code) if exit_code and options.interactive: console_output('Error talking to {}\n'.format( self.display_name).encode()) self.disconnect() if self.temporary: self.close()
def process_input_buffer() -> None: """Send the content of the input buffer to all remote processes, this must be called in the main thread""" from polysh.control_commands_helpers import handle_control_command data = the_stdin_thread.input_buffer.get() remote_dispatcher.log(b'> ' + data) if data.startswith(b':'): try: handle_control_command(data[1:-1].decode()) except UnicodeDecodeError as e: console_output(b'Could not decode command.') return if data.startswith(b'!'): try: retcode = subprocess.call(data[1:], shell=True) except OSError as e: if e.errno == errno.EINTR: console_output(b'Child was interrupted\n') retcode = 0 else: raise if retcode > 128 and retcode <= 192: retcode = 128 - retcode if retcode > 0: console_output('Child returned {:d}\n'.format(retcode).encode()) elif retcode < 0: console_output('Child was terminated by signal {:d}\n'.format( -retcode).encode()) return for r in dispatchers.all_instances(): try: r.dispatch_command(data) except asyncore.ExitNow as e: raise e except Exception as msg: raise msg console_output('{} for {}, disconnecting\n'.format( str(msg), r.display_name).encode()) r.disconnect() else: if r.enabled and r.state is remote_dispatcher.STATE_IDLE: r.change_state(remote_dispatcher.STATE_RUNNING)
def do_set_debug(command): """ Usage: :set_debug y|n [SHELLS...] Enable or disable debugging output for remote shells. The first argument is 'y' to enable the debugging output, 'n' to disable it. The remaining optional arguments are the selected shells. The special characters * ? and [] work as expected. """ split = command.split() if not split: console_output('Expected at least a letter\n') return letter = split[0].lower() if letter not in ('y', 'n'): console_output("Expected 'y' or 'n', got: %s\n" % split[0]) return debug = letter == 'y' for i in selected_shells(' '.join(split[1:])): i.debug = debug
def replicate(shell, path): peers = [i for i in dispatchers.all_instances() if i.enabled] if len(peers) <= 1: console_output('No other remote shell to replicate files to\n') return def should_print_bw(node, already_chosen=[False]): if not node.children and not already_chosen[0] and not node.is_upload: already_chosen[0] = True return True return False sender_index = peers.index(shell) destinations = peers[:sender_index] + peers[sender_index+1:] tree = file_transfer_tree_node(None, shell, destinations, 0, should_print_bw, path=path)
def do_hide_password(command): """ Usage: :hide_password Do not echo the next typed line. This is useful when entering password. If debugging or logging is enabled, it will be disabled to avoid displaying a password. Therefore, you will have to reenable logging or debugging afterwards if need be. """ warned = False for i in dispatchers.all_instances(): if i.enabled and i.debug: i.debug = False if not warned: console_output('Debugging disabled to avoid displaying ' 'passwords\n') warned = True stdin.set_echo(False) if remote_dispatcher.options.log_file: console_output('Logging disabled to avoid writing passwords\n') remote_dispatcher.options.log_file = None
def selected_shells(command): """Iterator over the shells with names matching the patterns. An empty patterns matches all the shells""" if not command or command == '*': for i in dispatchers.all_instances(): yield i return selected = set() instance_found = False for pattern in command.split(): found = False for expanded_pattern in expand_syntax(pattern): for i in dispatchers.all_instances(): instance_found = True if fnmatch(i.display_name, expanded_pattern): found = True if i not in selected: selected.add(i) yield i if instance_found and not found: console_output('%s not found\n' % pattern)
def selected_shells(command): """Iterator over the shells with names matching the patterns. An empty patterns matches all the shells""" if not command or command == '*': for i in dispatchers.all_instances(): yield i return selected = set() instance_found = False for pattern in command.split(): found = False for expanded_pattern in expand_syntax(pattern): for i in dispatchers.all_instances(): instance_found = True if fnmatch(i.display_name, expanded_pattern): found = True if i not in selected: selected.add(i) yield i if instance_found and not found: console_output('{} not found\n'.format(pattern).encode())
def process_input_buffer(): """Send the content of the input buffer to all remote processes, this must be called in the main thread""" from polysh.control_commands_helpers import handle_control_command data = the_stdin_thread.input_buffer.get() remote_dispatcher.log('> ' + data) if data.startswith(':'): handle_control_command(data[1:-1]) return if data.startswith('!'): try: retcode = subprocess.call(data[1:], shell=True) except OSError, e: if e.errno == errno.EINTR: console_output('Child was interrupted\n') retcode = 0 else: raise if retcode > 128 and retcode <= 192: retcode = 128 - retcode if retcode > 0: console_output('Child returned %d\n' % retcode) elif retcode < 0: console_output('Child was terminated by signal %d\n' % -retcode) return
def print_lines(self, lines): from polysh.display_names import max_display_name_length lines = lines.strip('\n') while True: no_empty_lines = lines.replace('\n\n', '\n') if len(no_empty_lines) == len(lines): break lines = no_empty_lines if not lines: return indent = max_display_name_length - len(self.display_name) log_prefix = self.display_name + indent * ' ' + ' : ' if self.color_code is None: console_prefix = log_prefix else: console_prefix = '\033[1;%dm%s\033[1;m' % (self.color_code, log_prefix) console_data = (console_prefix + lines.replace('\n', '\n' + console_prefix) + '\n') log_data = log_prefix + lines.replace('\n', '\n' + log_prefix) + '\n' console_output(console_data, logging_msg=log_data) self.last_printed_line = lines[lines.rfind('\n') + 1:]
def do_send_ctrl(command): """ Usage: :send_ctrl LETTER [SHELLS...] Send a control character to remote shells. The first argument is the control character to send like c, d or z. Note that these three control characters can be sent simply by typing them into polysh. The remaining optional arguments are the destination shells. The special characters * ? and [] work as expected. """ split = command.split() if not split: console_output('Expected at least a letter\n') return letter = split[0] if len(letter) != 1: console_output('Expected a single letter, got: %s\n' % letter) return control_letter = chr(ord(letter.lower()) - ord('a') + 1) for i in selected_shells(' '.join(split[1:])): if i.enabled: i.dispatch_write(control_letter)
def selected_shells( command: str) -> Iterator[remote_dispatcher.RemoteDispatcher]: """Iterator over the shells with names matching the patterns. An empty patterns matches all the shells""" if not command or command == '*': for i in dispatchers.all_instances(): yield i return selected = set() # type: Set[remote_dispatcher.RemoteDispatcher] instance_found = False for pattern in command.split(): found = False for expanded_pattern in expand_syntax(pattern): for i in dispatchers.all_instances(): instance_found = True if (fnmatch(i.display_name, expanded_pattern) or fnmatch( str(i.last_printed_line), expanded_pattern)): found = True if i not in selected: selected.add(i) yield i if instance_found and not found: console_output('{} not found\n'.format(pattern).encode())
def print_lines(self, lines: bytes) -> None: from polysh.display_names import max_display_name_length lines = lines.strip(b'\n') while True: no_empty_lines = lines.replace(b'\n\n', b'\n') if len(no_empty_lines) == len(lines): break lines = no_empty_lines if not lines: return indent = max_display_name_length - len(self.display_name) log_prefix = self.display_name.encode() + indent * b' ' + b' : ' if self.color_code is None: console_prefix = log_prefix else: console_prefix = (b'\033[1;' + str(self.color_code).encode() + b'm' + log_prefix + b'\033[1;m') console_data = (console_prefix + lines.replace(b'\n', b'\n' + console_prefix) + b'\n') log_data = (log_prefix + lines.replace(b'\n', b'\n' + log_prefix) + b'\n') console_output(console_data, logging_msg=log_data) self.last_printed_line = lines[lines.rfind(b'\n') + 1:]
def loop(interactive): histfile = os.path.expanduser("~/.polysh_history") init_history(histfile) next_signal = None last_status = None while True: try: if next_signal: current_signal = next_signal next_signal = None sig2chr = {signal.SIGINT: 'C', signal.SIGTSTP: 'Z'} ctrl = sig2chr[current_signal] remote_dispatcher.log('> ^{}\n'.format(ctrl).encode()) control_commands.do_send_ctrl(ctrl) console_output(b'') stdin.the_stdin_thread.prepend_text = None while dispatchers.count_awaited_processes()[0] and \ remote_dispatcher.main_loop_iteration(timeout=0.2): pass # Now it's quiet for r in dispatchers.all_instances(): r.print_unfinished_line() current_status = dispatchers.count_awaited_processes() if current_status != last_status: console_output(b'') if remote_dispatcher.options.interactive: stdin.the_stdin_thread.want_raw_input() last_status = current_status if dispatchers.all_terminated(): # Clear the prompt console_output(b'') raise asyncore.ExitNow(remote_dispatcher.options.exit_code) if not next_signal: # possible race here with the signal handler remote_dispatcher.main_loop_iteration() except KeyboardInterrupt: if interactive: next_signal = signal.SIGINT else: kill_all() os.kill(0, signal.SIGINT) except asyncore.ExitNow as e: console_output(b'') save_history(histfile) sys.exit(e.args[0])
def do_replicate(command): """ Usage: :replicate SHELL:REMOTE_PATH Copy a path from one remote shell to all others """ if ':' not in command: console_output('Usage: :replicate SHELL:REMOTE_PATH\n') return shell_name, path = command.strip().split(':', 1) if not path: console_output('No remote path given\n') return for shell in dispatchers.all_instances(): if shell.display_name == shell_name: if not shell.enabled: console_output('%s is not enabled\n' % shell_name) return break else: console_output('%s not found\n' % shell_name) return file_transfer.replicate(shell, path)
def main_loop(): global next_signal last_status = None while True: try: if next_signal: current_signal = next_signal next_signal = None sig2chr = {signal.SIGINT: 'c', signal.SIGTSTP: 'z'} ctrl = sig2chr[current_signal] remote_dispatcher.log('> ^%c\n' % ctrl.upper()) control_commands.do_send_ctrl(ctrl) console_output('') the_stdin_thread.prepend_text = None while dispatchers.count_awaited_processes()[0] and \ remote_dispatcher.main_loop_iteration(timeout=0.2): pass # Now it's quiet for r in dispatchers.all_instances(): r.print_unfinished_line() current_status = dispatchers.count_awaited_processes() if current_status != last_status: console_output('') if remote_dispatcher.options.interactive: the_stdin_thread.want_raw_input() last_status = current_status if dispatchers.all_terminated(): # Clear the prompt console_output('') raise asyncore.ExitNow(remote_dispatcher.options.exit_code) if not next_signal: # possible race here with the signal handler remote_dispatcher.main_loop_iteration() except asyncore.ExitNow, e: console_output('') sys.exit(e.args[0])
raise if retcode > 128 and retcode <= 192: retcode = 128 - retcode if retcode > 0: console_output('Child returned %d\n' % retcode) elif retcode < 0: console_output('Child was terminated by signal %d\n' % -retcode) return for r in dispatchers.all_instances(): try: r.dispatch_command(data) except asyncore.ExitNow, e: raise e except Exception, msg: console_output('%s for %s, disconnecting\n' % (msg, r.display_name)) r.disconnect() else: if r.enabled and r.state is remote_dispatcher.STATE_IDLE: r.change_state(remote_dispatcher.STATE_RUNNING) # The stdin thread uses a synchronous (with ACK) socket to communicate with the # main thread, which is most of the time waiting in the poll() loop. # Socket character protocol: # d: there is new data to send # A: ACK, same reply for every message, communications are synchronous, so the # stdin thread sends a character to the socket, the main thread processes it, # sends the ACK, and the stdin thread can go on. class socket_notification_reader(asyncore.dispatcher): """The socket reader in the main thread"""
def do_chdir(command: str) -> None: try: os.chdir(expand_local_path(command.strip())) except OSError as e: console_output('{}\n'.format(str(e)).encode())
def do_list(command: str) -> None: instances = [i.get_info() for i in selected_shells(command)] flat_instances = dispatchers.format_info(instances) console_output(b''.join(flat_instances))
def do_set_log(command): """ Usage: :set_log [LOCAL_PATH] Duplicate every console I/O into the given local file. If LOCAL_PATH is not given, restore the default behaviour of not logging. """ command = command.strip() if command: try: remote_dispatcher.options.log_file = file(command, 'a') except IOError, e: console_output('%s\n' % str(e)) command = None if not command: remote_dispatcher.options.log_file = None console_output('Logging disabled\n') def complete_show_read_buffer(line, text): return complete_shells(line, text, lambda i: i.read_buffer or i.read_in_state_not_started) def do_show_read_buffer(command): """ Usage: :show_read_buffer [SHELLS...] Print the data read by remote shells. The special characters * ? and [] work as expected. """ for i in selected_shells(command): if i.read_in_state_not_started: i.print_lines(i.read_in_state_not_started) i.read_in_state_not_started = ''
def print_debug(self, msg): """Log some debugging information to the console""" state = STATE_NAMES[self.state] msg = msg.encode('string_escape') console_output('[dbg] %s[%s]: %s\n' % (self.display_name, state, msg))