#!/usr/bin/env python # coding: utf-8 # In[ ]: # Console related imports. from subprocess import Popen, PIPE import os from IPython.utils.py3compat import bytes_to_str, string_types # Widget related imports. from IPython.html import widgets from IPython.display import display # Define function to run a process without blocking the input. # In[ ]: def read_process(process, append_output): """ Try to read the stdout and stderr of a process and render it using the append_output method provided Parameters ---------- process: Popen handle append_output: method handle Callback to render output. Signature of append_output(output, [prefix=])""" try: stdout = process.stdout.read() if stdout is not None and len(stdout) > 0: append_output(stdout, prefix=' ') except: pass try: stderr = process.stderr.read() if stderr is not None and len(stderr) > 0: append_output(stderr, prefix='ERR ') except: pass def set_pipe_nonblocking(pipe): """Set a pipe as non-blocking""" try: import fcntl fl = fcntl.fcntl(pipe, fcntl.F_GETFL) fcntl.fcntl(pipe, fcntl.F_SETFL, fl | os.O_NONBLOCK) except: pass kernel = get_ipython().kernel def run_command(command, append_output, has_user_exited=None): """Run a command asyncronously Parameters ---------- command: str Shell command to launch a process with. append_output: method handle Callback to render output. Signature of append_output(output, [prefix=]) has_user_exited: method handle Check to see if the user wants to stop the command. Must return a boolean.""" # Echo input. append_output(command, prefix='>>> ') # Create the process. Make sure the pipes are set as non-blocking. process = Popen(command, shell=True, stdout=PIPE, stderr=PIPE) set_pipe_nonblocking(process.stdout) set_pipe_nonblocking(process.stderr) # Only continue to read from the command while (has_user_exited is None or not has_user_exited()) and process.poll() is None: read_process(process, append_output) kernel.do_one_iteration() # Run IPython iteration. This is the code that # makes this operation non-blocking. This will # allow widget messages and callbacks to be # processed. # If the process is still running, the user must have exited. if process.poll() is None: process.kill() else: read_process(process, append_output) # Read remainer # Create the console widgets without displaying them. # In[ ]: console_container = widgets.VBox(visible=False) console_container.padding = '10px' output_box = widgets.Textarea() output_box.height = '400px' output_box.font_family = 'monospace' output_box.color = '#AAAAAA' output_box.background_color = 'black' output_box.width = '800px' input_box = widgets.Text() input_box.font_family = 'monospace' input_box.color = '#AAAAAA' input_box.background_color = 'black' input_box.width = '800px' console_container.children = [output_box, input_box] # Hook the process execution methods up to our console widgets. # In[ ]: def append_output(output, prefix): if isinstance(output, string_types): output_str = output else: output_str = bytes_to_str(output) output_lines = output_str.split('\n') formatted_output = '\n'.join([prefix + line for line in output_lines if len(line) > 0]) + '\n' output_box.value += formatted_output output_box.scroll_to_bottom() def has_user_exited(): return not console_container.visible def handle_input(sender): sender.disabled = True try: command = sender.value sender.value = '' run_command(command, append_output=append_output, has_user_exited=has_user_exited) finally: sender.disabled = False input_box.on_submit(handle_input) # Create the button that will be used to display and hide the console. Display both the console container and the new button used to toggle it. # In[ ]: toggle_button = widgets.Button(description="Start Console") def toggle_console(sender): console_container.visible = not console_container.visible if console_container.visible: toggle_button.description="Stop Console" input_box.disabled = False else: toggle_button.description="Start Console" toggle_button.on_click(toggle_console) display(toggle_button) display(console_container)