outbuff.py 37.1 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077
###############################################################################
# Name: outbuff.py                                                            #
# Purpose: Gui and helper classes for running processes and displaying output #
# Author: Cody Precord <cprecord@editra.org>                                  #
# Copyright: (c) 2008 Cody Precord <staff@editra.org>                         #
# License: wxWindows License                                                  #
###############################################################################

"""
Editra Control Library: OutputBuffer

This module contains classes that are useful for displaying output from running
tasks and processes. The classes are divided into three main categories, gui
classes, mixins, and thread classes. All the classes can be used together to
easily create multithreaded gui display classes without needing to worry about
the details and thread safety of the gui.

For example usage of these classes see ed_log and the Editra's Launch plugin

Class OutputBuffer:
This is the main class exported by this module. It provides a readonly output
display buffer that when used with the other classes in this module provides an
easy way to display continuous output from other processes and threads. It
provides two methods for subclasses to override if they wish to perform custom
handling.

  - Override the ApplyStyles method to do any processing and coloring of the
    text as it is put in the buffer.
  - Override the DoHotSpotClicked method to handle any actions to take when a
    hotspot has been clicked in the buffer.
  - Override the DoUpdatesEmpty method to perform any idle processing when no
    new text is waiting to be processed.

Class ProcessBufferMixin:
Mixin class for the L{OutputBuffer} class that provides handling for when an
OutputBuffer is used with a L{ProcessThread}. It provides three methods that can
be overridden in subclasses to perform extra processing.

  - DoProcessStart: Called as the process is being started in the ProcessThread,
                    it receives the process command string as an argument.
  - DoFilterInput: Called as each chunk of output comes from the running process
                   use it to filter the results before displaying them in the
                   buffer.
  - DoProcessExit: Called when the running process has exited. It receives the
                   processes exit code as a parameter.

Class ProcessThread:
Thread class for running subprocesses and posting the output to an
L{OutputBuffer} via events.

Class TaskThread:
Thread class for running a callable. For optimal performance and responsiveness
the callable should be a generator object. All results are directed to an
L{OutputBuffer} through its AppendUpdate method.

Requirements:
  * wxPython 2.8
  * Macintosh/Linux/Unix Python 2.4+
  * Windows Python 2.5+ (ctypes is needed)

"""

__author__ = "Cody Precord <cprecord@editra.org>"
__svnid__ = "$Id: outbuff.py 69743 2011-11-12 20:22:48Z CJP $"
__revision__ = "$Revision: 69743 $"

__all__ = ["OutputBuffer", "OutputBufferEvent", "ProcessBufferMixin",
           "ProcessThreadBase", "ProcessThread", "TaskThread", "TaskObject",
           "OPB_STYLE_DEFAULT", "OPB_STYLE_INFO",
           "OPB_STYLE_WARN", "OPB_STYLE_ERROR", "OPB_STYLE_MAX",

           "OPB_ERROR_NONE", "OPB_ERROR_INVALID_COMMAND",

           "edEVT_PROCESS_START", "EVT_PROCESS_START", "edEVT_TASK_START",
           "EVT_TASK_START", "edEVT_UPDATE_TEXT", "EVT_UPDATE_TEXT",
           "edEVT_PROCESS_EXIT", "EVT_PROCESS_EXIT", "edEVT_TASK_COMPLETE",
           "EVT_TASK_COMPLETE", "edEVT_PROCESS_ERROR", "EVT_PROCESS_ERROR"]

#--------------------------------------------------------------------------#
# Imports
import os
import sys
import time
import errno
import signal
import threading
import types
import subprocess
import wx
import wx.stc

# Platform specific modules needed for killing processes
if subprocess.mswindows:
    import msvcrt
    import ctypes
else:
    import shlex
    import select
    import fcntl

#--------------------------------------------------------------------------#
# Globals
OUTPUTBUFF_NAME_STR = u'EditraOutputBuffer'
THREADEDBUFF_NAME_STR = u'EditraThreadedBuffer'

# Style Codes
OPB_STYLE_DEFAULT = 0  # Default Black text styling
OPB_STYLE_INFO    = 1  # Default Blue text styling
OPB_STYLE_WARN    = 2  # Default Red text styling
OPB_STYLE_ERROR   = 3  # Default Red/Hotspot text styling
OPB_STYLE_MAX     = 3  # Highest style byte used by outputbuffer

# All Styles
OPB_ALL_STYLES = (wx.stc.STC_STYLE_DEFAULT, wx.stc.STC_STYLE_CONTROLCHAR,
                  OPB_STYLE_DEFAULT, OPB_STYLE_ERROR, OPB_STYLE_INFO,
                  OPB_STYLE_WARN)

# Error Codes
OPB_ERROR_NONE            = 0
OPB_ERROR_INVALID_COMMAND = -1

#--------------------------------------------------------------------------#

# Event for notifying that the process has started running
# GetValue will return the command line string that started the process
edEVT_PROCESS_START = wx.NewEventType()
EVT_PROCESS_START = wx.PyEventBinder(edEVT_PROCESS_START, 1)

# Event for notifying that a task is starting to run
edEVT_TASK_START = wx.NewEventType()
EVT_TASK_START = wx.PyEventBinder(edEVT_TASK_START, 1)

# Event for passing output data to buffer
# GetValue returns the output text retrieved from the process
edEVT_UPDATE_TEXT = wx.NewEventType()
EVT_UPDATE_TEXT = wx.PyEventBinder(edEVT_UPDATE_TEXT, 1)

# Event for notifying that the the process has finished and no more update
# events will be sent. GetValue will return the processes exit code
edEVT_PROCESS_EXIT = wx.NewEventType()
EVT_PROCESS_EXIT = wx.PyEventBinder(edEVT_PROCESS_EXIT, 1)

# Event to notify that a process has completed
edEVT_TASK_COMPLETE = wx.NewEventType()
EVT_TASK_COMPLETE = wx.PyEventBinder(edEVT_TASK_COMPLETE, 1)

# Event to notify that an error occurred in the process
edEVT_PROCESS_ERROR = wx.NewEventType()
EVT_PROCESS_ERROR = wx.PyEventBinder(edEVT_PROCESS_ERROR, 1)

class OutputBufferEvent(wx.PyCommandEvent):
    """Event for data transfer and signaling actions in the L{OutputBuffer}"""
    def __init__(self, etype, eid=wx.ID_ANY, value=''):
        """Creates the event object"""
        super(OutputBufferEvent, self).__init__(etype, eid)

        # Attributes
        self._value = value
        self._errmsg = None

    #---- Properties ----#
    Value = property(lambda self: self.GetValue(),
                     lambda self, v: setattr(self, '_value', v))
    ErrorMessage = property(lambda self: self.GetErrorMessage(),
                            lambda self, msg: self.SetErrorMessage(msg))

    def GetValue(self):
        """Returns the value from the event.
        @return: the value of this event

        """
        return self._value

    def GetErrorMessage(self):
        """Get the error message value
        @return: Exception traceback string or None

        """
        return self._errmsg

    def SetErrorMessage(self, msg):
        """Set the error message value
        @param msg: Exception traceback string

        """
        try:
            tmsg = unicode(msg)
        except:
            tmsg = None
        self._errmsg = msg

#--------------------------------------------------------------------------#

class OutputBuffer(wx.stc.StyledTextCtrl):
    """OutputBuffer is a general purpose output display for showing text. It
    provides an easy interface for the buffer to interact with multiple threads
    that may all be sending updates to the buffer at the same time. Methods for
    styling and filtering output are also available.

    """
    def __init__(self, parent, id=wx.ID_ANY,
                 pos=wx.DefaultPosition,
                 size=wx.DefaultSize,
                 style=wx.BORDER_SUNKEN,
                 name=OUTPUTBUFF_NAME_STR):
        super(OutputBuffer, self).__init__(parent, id, pos,
                                           size, style, name)

        # Attributes
        self._mutex = threading.Lock()
        self._updating = threading.Condition(self._mutex)
        self._updates = list()
        self._timer = wx.Timer(self)
        self._line_buffer = -1                
        self._colors = dict(defaultb=(255, 255, 255), defaultf=(0, 0, 0),
                            errorb=(255, 255, 255), errorf=(255, 0, 0),
                            infob=(255, 255, 255), infof=(0, 0, 255),
                            warnb=(255, 255, 255), warnf=(255, 0, 0))

        # Setup
        self.__ConfigureSTC()

        # Event Handlers
        self.Bind(wx.EVT_TIMER, self.OnTimer)
        self.Bind(wx.stc.EVT_STC_HOTSPOT_CLICK, self._OnHotSpot)

    def __del__(self):
        """Ensure timer is cleaned up when we are deleted"""
        if self._timer.IsRunning():
            self._timer.Stop()

    def __ConfigureSTC(self):
        """Setup the stc to behave/appear as we want it to
        and define all styles used for giving the output context.
        @todo: make more of this configurable

        """
        self.SetMargins(3, 3)
        self.SetMarginWidth(0, 0)
        self.SetMarginWidth(1, 0)

        # To improve performance at cost of memory cache the document layout
        self.SetLayoutCache(wx.stc.STC_CACHE_DOCUMENT)
        self.SetUndoCollection(False) # Don't keep undo history
        self.SetReadOnly(True)
        self.SetCaretWidth(0)

        if wx.Platform == '__WXMSW__':
            self.SetEOLMode(wx.stc.STC_EOL_CRLF)
        else:
            self.SetEOLMode(wx.stc.STC_EOL_LF)

        #self.SetEndAtLastLine(False)
        self.SetVisiblePolicy(1, wx.stc.STC_VISIBLE_STRICT)

        # Define Styles
        highlight = wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT)
        self.SetSelBackground(True, highlight)
        if sum(highlight.Get()) < 384:
            self.SetSelForeground(True, wx.WHITE)
        else:
            self.SetSelForeground(True, wx.BLACK)
        self.__SetupStyles()

    def FlushBuffer(self):
        """Flush the update buffer
        @postcondition: The update buffer is empty

        """
        self._updating.acquire()
        self.SetReadOnly(False)
        txt = u''.join(self._updates[:])
        start = self.GetLength()
        if u'\0' in txt:
            # HACK: handle displaying NULLs in the STC
            self.AddStyledText('\0'.join(txt.encode('utf-8'))+'\0')
        else:
            self.AppendText(txt)
        self.GotoPos(self.GetLength())
        self._updates = list()
        self.ApplyStyles(start, txt)
        self.SetReadOnly(True)
        self.RefreshBufferedLines()
        self._updating.release()

    def __SetupStyles(self, font=None):
        """Setup the default styles of the text in the buffer
        @keyword font: wx.Font to use or None to use default

        """
        if font is None:
            if wx.Platform == '__WXMAC__':
                fsize = 11
            else:
                fsize = 10

            font = wx.Font(fsize, wx.FONTFAMILY_MODERN,
                           wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
        style = (font.GetFaceName(), font.GetPointSize(), "#FFFFFF")
        wx.stc.StyledTextCtrl.SetFont(self, font)

        # Custom Styles
        self.StyleSetSpec(OPB_STYLE_DEFAULT,
                          "face:%s,size:%d,fore:#000000,back:%s" % style)
        self.StyleSetSpec(OPB_STYLE_INFO,
                          "face:%s,size:%d,fore:#0000FF,back:%s" % style)
        self.StyleSetSpec(OPB_STYLE_WARN,
                          "face:%s,size:%d,fore:#FF0000,back:%s" % style)
        self.StyleSetSpec(OPB_STYLE_ERROR,
                          "face:%s,size:%d,fore:#FF0000,back:%s" % style)
        self.StyleSetHotSpot(OPB_STYLE_ERROR, True)

        # Default Styles
        self.StyleSetSpec(wx.stc.STC_STYLE_DEFAULT, \
                          "face:%s,size:%d,fore:#000000,back:%s" % style)
        self.StyleSetSpec(wx.stc.STC_STYLE_CONTROLCHAR, \
                          "face:%s,size:%d,fore:#000000,back:%s" % style)
        self.Colourise(0, -1)

    def _OnHotSpot(self, evt):
        """Handle hotspot clicks"""
        pos = evt.GetPosition()
        self.DoHotSpotClicked(pos, self.LineFromPosition(pos))

    #---- Public Member Functions ----#

    def AppendUpdate(self, value):
        """Buffer output before adding to window. This method can safely be
        called from non gui threads to add updates to the buffer, that will
        be displayed during the next idle period.
        @param value: update string to append to stack

        """
        self._updating.acquire()
        if not (type(value) is types.UnicodeType):
            value = value.decode(sys.getfilesystemencoding())
        self._updates.append(value)
        self._updating.release()

    def ApplyStyles(self, start, txt):
        """Apply coloring to text starting at start position.
        Override this function to do perform any styling that you want
        done on the text.
        @param start: Start position of text that needs styling in the buffer
        @param txt: The string of text that starts at the start position in the
                    buffer.

        """
        pass

    def CanCopy(self):
        """Is it possible to copy text right now
        @return: bool

        """
        sel = self.GetSelection()
        return sel[0] != sel[1]

    def CanCut(self):
        """Is it possible to Cut
        @return: bool

        """
        return not self.GetReadOnly()

    def Clear(self):
        """Clear the Buffer"""
        self.SetReadOnly(False)
        self.ClearAll()
        self.EmptyUndoBuffer()
        self.SetReadOnly(True)

    def DoHotSpotClicked(self, pos, line):
        """Action to perform when a hotspot region is clicked in the buffer.
        Override this function to provide handling of hotspots.
        @param pos: Position in buffer of where the click occurred.
        @param line: Line in which the click occurred (zero based index)

        """
        pass

    def DoUpdatesEmpty(self):
        """Called when update stack is empty
        Override this function to perform actions when there are no updates
        to process. It can be used for things such as temporarily stopping
        the timer or performing idle processing.

        """
        pass

    def GetDefaultBackground(self):
        """Get the default text style background color
        @return: wx.Colour

        """
        return wx.Colour(*self._colors['defaultb'])

    def GetDefaultForeground(self):
        """Get the default text style foreground color
        @return: wx.Colour

        """
        return wx.Colour(*self._colors['defaultf'])

    def GetErrorBackground(self):
        """Get the error text style background color
        @return: wx.Colour

        """
        return wx.Colour(*self._colors['errorb'])

    def GetErrorForeground(self):
        """Get the error text style foreground color
        @return: wx.Colour

        """
        return wx.Colour(*self._colors['errorf'])

    def GetInfoBackground(self):
        """Get the info text style background color
        @return: wx.Colour

        """
        return wx.Colour(*self._colors['infob'])

    def GetInfoForeground(self):
        """Get the info text style foreground color
        @return: wx.Colour

        """
        return wx.Colour(*self._colors['infof'])

    def GetWarningBackground(self):
        """Get the warning text style background color
        @return: wx.Colour

        """
        return wx.Colour(*self._colors['warnb'])

    def GetWarningForeground(self):
        """Get the warning text style foreground color
        @return: wx.Colour

        """
        return wx.Colour(*self._colors['warnf'])

    def GetUpdateQueue(self):
        """Gets a copy of the current update queue"""
        self._updating.acquire()
        val = list(self._updates)
        self._updating.release()
        return val

    def IsRunning(self):
        """Return whether the buffer is running and ready for output
        @return: bool

        """
        return self._timer.IsRunning()

    def OnTimer(self, evt):
        """Process and display text from the update buffer
        @note: this gets called many times while running thus needs to
               return quickly to avoid blocking the ui.

        """
        if len(self._updates):
            self.FlushBuffer()
        elif evt is not None:
            self.DoUpdatesEmpty()
        else:
            pass

    def RefreshBufferedLines(self):
        """Refresh and readjust the lines in the buffer to fit the current
        line buffering limits.
        @postcondition: Oldest lines are removed until we are back within the
                        buffer limit bounds.

        """
        if self._line_buffer < 0:
            return

        self.SetReadOnly(False)
        while self.GetLineCount() > self._line_buffer:
            self.SetCurrentPos(0)
            self.LineDelete()
        self.SetReadOnly(True)
        self.SetCurrentPos(self.GetLength())

    def SetDefaultColor(self, fore=None, back=None):
        """Set the colors for the default text style
        @keyword fore: Foreground Color
        @keyword back: Background Color

        """
        if fore is not None:
            self.StyleSetForeground(wx.stc.STC_STYLE_DEFAULT, fore)
            self.StyleSetForeground(wx.stc.STC_STYLE_CONTROLCHAR, fore)
            self.StyleSetForeground(OPB_STYLE_DEFAULT, fore)
            self._colors['defaultf'] = fore.Get()

        if back is not None:
            self.StyleSetBackground(wx.stc.STC_STYLE_DEFAULT, back)
            self.StyleSetBackground(wx.stc.STC_STYLE_CONTROLCHAR, back)
            self.StyleSetBackground(OPB_STYLE_DEFAULT, back)
            self._colors['defaultb'] = back.Get()

    def SetErrorColor(self, fore=None, back=None):
        """Set color for error text
        @keyword fore: Foreground Color
        @keyword back: Background Color

        """
        if fore is not None:
            self.StyleSetForeground(OPB_STYLE_ERROR, fore)
            self._colors['errorf'] = fore.Get()

        if back is not None:
            self.StyleSetBackground(OPB_STYLE_ERROR, back)
            self._colors['errorb'] = back.Get()

    def SetInfoColor(self, fore=None, back=None):
        """Set color for info text
        @keyword fore: Foreground Color
        @keyword back: Background Color

        """
        if fore is not None:
            self.StyleSetForeground(OPB_STYLE_INFO, fore)
            self._colors['infof'] = fore.Get()

        if back is not None:
            self.StyleSetBackground(OPB_STYLE_INFO, back)
            self._colors['infob'] = back.Get()

    def SetWarningColor(self, fore=None, back=None):
        """Set color for warning text
        @keyword fore: Foreground Color
        @keyword back: Background Color

        """
        if fore is not None:
            self.StyleSetForeground(OPB_STYLE_WARN, fore)
            self._colors['warnf'] = fore.Get()

        if back is not None:
            self.StyleSetBackground(OPB_STYLE_WARN, back)
            self._colors['warnb'] = back.Get()

    def SetFont(self, font):
        """Set the font used by all text in the buffer
        @param font: wxFont

        """
        for style in OPB_ALL_STYLES:
            self.StyleSetFont(style, font)

    def SetLineBuffering(self, num):
        """Set how many lines the buffer should keep for display.
        @param num: int (-1 == unlimited)

        """
        self._line_buffer = num
        self.RefreshBufferedLines()

    def SetText(self, text):
        """Set the text that is shown in the buffer
        @param text: text string to set as buffers current value

        """
        self.SetReadOnly(False)
        wx.stc.StyledTextCtrl.SetText(self, text)
        self.SetReadOnly(True)

    def Start(self, interval):
        """Start the window's timer to check for updates
        @param interval: interval in milliseconds to do updates

        """
        self._timer.Start(interval)

    def Stop(self):
        """Stop the update process of the buffer"""
        # Dump any output still left in tmp buffer before stopping
        self.OnTimer(None)
        self._timer.Stop()
        self.SetReadOnly(True)

#-----------------------------------------------------------------------------#

class ProcessBufferMixin:
    """Mixin class for L{OutputBuffer} to handle events
    generated by a L{ProcessThread}.

    """
    def __init__(self, update=100):
        """Initialize the mixin
        @keyword update: The update interval speed in msec

        """
        # Attributes
        self._rate = update

        # Event Handlers
        self.Bind(EVT_PROCESS_START, self._OnProcessStart)
        self.Bind(EVT_UPDATE_TEXT, self._OnProcessUpdate)
        self.Bind(EVT_PROCESS_EXIT, self._OnProcessExit)
        self.Bind(EVT_PROCESS_ERROR, self._OnProcessError)

    def _OnProcessError(self, evt):
        """Handle EVT_PROCESS_ERROR"""
        self.DoProcessError(evt.GetValue(), evt.GetErrorMessage())

    def _OnProcessExit(self, evt):
        """Handles EVT_PROCESS_EXIT"""
        self.DoProcessExit(evt.GetValue())

    def _OnProcessStart(self, evt):
        """Handles EVT_PROCESS_START"""
        self.DoProcessStart(evt.GetValue())
        self.Start(self._rate)

    def _OnProcessUpdate(self, evt):
        """Handles EVT_UPDATE_TEXT"""
        txt = self.DoFilterInput(evt.GetValue())
        self.AppendUpdate(txt)

    def DoFilterInput(self, txt):
        """Override this method to do an filtering on input that is sent to
        the buffer from the process text. The return text is what is put in
        the buffer.
        @param txt: incoming update text
        @return: string

        """
        return txt

    def DoProcessError(self, code, excdata=None):
        """Override this method to do any ui notification of when errors happen
        in running the process.
        @param code: an OBP error code
        @keyword excdata: Exception Data from process error
        @return: None

        """
        pass

    def DoProcessExit(self, code=0):
        """Override this method to do any post processing after the running
        task has exited. Typically this is a good place to call
        L{OutputBuffer.Stop} to stop the buffers timer.
        @keyword code: Exit code of program
        @return: None

        """
        self.Stop()

    def DoProcessStart(self, cmd=''):
        """Override this method to do any pre-processing before starting
        a processes output.
        @keyword cmd: Command used to start program
        @return: None

        """
        pass

    def SetUpdateInterval(self, value):
        """Set the rate at which the buffer outputs update messages. Set to
        a higher number if the process outputs large amounts of text at a very
        high rate.
        @param value: rate in milliseconds to do updates on

        """
        self._rate = value

#-----------------------------------------------------------------------------#

class ProcessThreadBase(threading.Thread):
    """Base Process Thread
    Override DoPopen in subclasses.

    """
    def __init__(self, parent):
        super(ProcessThreadBase, self).__init__()

        # Attributes
        self.abort = False          # Abort Process
        self._proc = None
        self._parent = parent       # Parent Window/Event Handler
        self._sig_abort = signal.SIGTERM    # default signal to kill process
        self._last_cmd = u""        # Last run command

    #---- Properties ----#
    LastCommand = property(lambda self: self._last_cmd,
                           lambda self, val: setattr(self, '_last_cmd', val)) 
    Parent = property(lambda self: self._parent)
    Process = property(lambda self: self._proc)

    def __DoOneRead(self):
        """Read one line of output and post results.
        @return: bool (True if more), (False if not)

        """
        if subprocess.mswindows:
            # Windows nonblocking pipe read implementation
            read = u''
            try:
                handle = msvcrt.get_osfhandle(self._proc.stdout.fileno())
                avail = ctypes.c_long()
                ctypes.windll.kernel32.PeekNamedPipe(handle, None, 0, 0,
                                                     ctypes.byref(avail), None)
                if avail.value > 0:
                    read = self._proc.stdout.read(avail.value)
                    if read.endswith(os.linesep):
                        read = read[:-1 * len(os.linesep)]
                else:
                    if self._proc.poll() is None:
                        time.sleep(1)
                        return True
                    else:
                        # Process has Exited
                        return False
            except ValueError, msg:
                return False
            except (subprocess.pywintypes.error, Exception), msg:
                if msg[0] in (109, errno.ESHUTDOWN):
                    return False
        else:
            # OSX and Unix nonblocking pipe read implementation
            if self._proc.stdout is None:
                return False

            flags = fcntl.fcntl(self._proc.stdout, fcntl.F_GETFL)
            if not self._proc.stdout.closed:
                fcntl.fcntl(self._proc.stdout,
                            fcntl.F_SETFL,
                            flags|os.O_NONBLOCK)

            try:
                try:
                    if not select.select([self._proc.stdout], [], [], 1)[0]:
                        return True

                    read = self._proc.stdout.read(4096)
                    if read == '':
                        return False
                except IOError, msg:
                    return False
            finally:
                if not self._proc.stdout.closed:
                    fcntl.fcntl(self._proc.stdout, fcntl.F_SETFL, flags)

        # Ignore encoding errors and return an empty line instead
        try:
            result = read.decode(sys.getfilesystemencoding())
        except UnicodeDecodeError:
            result = os.linesep

        if self.Parent:
            evt = OutputBufferEvent(edEVT_UPDATE_TEXT, self.Parent.GetId(), result)
            wx.PostEvent(self.Parent, evt)
            return True
        else:
            return False # Parent is dead no need to keep running

    def __KillPid(self, pid):
        """Kill a process by process id, causing the run loop to exit
        @param pid: Id of process to kill

        """
        # Dont kill if the process if it is the same one we
        # are running under (i.e we are running a shell command)
        if pid == os.getpid():
            return

        if wx.Platform != '__WXMSW__':
            # Close output pipe(s)
            try:
                try:
                    self._proc.stdout.close()
                except Exception, msg:
                    pass
            finally:
                self._proc.stdout = None

            # Try to kill the group
            try:
                os.kill(pid, self._sig_abort)
            except OSError, msg:
                pass

            # If still alive shoot it again
            if self._proc.poll() is not None:
                try:
                    os.kill(-pid, signal.SIGKILL)
                except OSError, msg:
                    pass

            # Try and wait for it to cleanup
            try:
                os.waitpid(pid, os.WNOHANG)
            except OSError, msg:
                pass

        else:
            # 1 == PROCESS_TERMINATE
            handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
            ctypes.windll.kernel32.TerminateProcess(handle, -1)
            ctypes.windll.kernel32.CloseHandle(handle)

    #---- Public Member Functions ----#
    def Abort(self, sig=signal.SIGTERM):
        """Abort the running process and return control to the main thread"""
        self._sig_abort = sig
        self.abort = True

    def DoPopen(self):
        """Open the process
        Override in a subclass to implement custom process opening
        @return: subprocess.Popen instance

        """
        raise NotImplementedError("Must implement DoPopen in subclasses!")

    def run(self):
        """Run the process until finished or aborted. Don't call this
        directly instead call self.start() to start the thread else this will
        run in the context of the current thread.
        @note: overridden from Thread

        """
        err = None
        try:
            self._proc = self.DoPopen()
        except OSError, msg:
            # NOTE: throws WindowsError on Windows which is a subclass of
            #       OSError, so it will still get caught here.
            if self.Parent:
                err =  OutputBufferEvent(edEVT_PROCESS_ERROR,
                                         self.Parent.GetId(),
                                         OPB_ERROR_INVALID_COMMAND)
                err.SetErrorMessage(msg)

        if self.Parent:
            evt = OutputBufferEvent(edEVT_PROCESS_START,
                                    self.Parent.GetId(),
                                    self.LastCommand)
            wx.PostEvent(self.Parent, evt)

        # Read from stdout while there is output from process
        while not err and True:
            if self.abort:
                self.__KillPid(self.Process.pid)
                self.__DoOneRead()
                more = False
                break
            else:
                more = False
                try:
                    more = self.__DoOneRead()
                except wx.PyDeadObjectError:
                    # Our parent window is dead so kill process and return
                    self.__KillPid(self.Process.pid)
                    return

                if not more:
                    break

        # Notify of error in running the process
        if err is not None:
            if self.Parent:
                wx.PostEvent(self.Parent, err)
            result = -1
        else:
            try:
                result = self.Process.wait()
            except OSError:
                result = -1

        # Notify that process has exited
        # Pack the exit code as the events value
        if self.Parent:
            evt = OutputBufferEvent(edEVT_PROCESS_EXIT, self.Parent.GetId(), result)
            wx.PostEvent(self.Parent, evt)

class ProcessThread(ProcessThreadBase):
    """Run a subprocess in a separate thread. Thread posts events back
    to parent object on main thread for processing in the ui.
    @see: EVT_PROCESS_START, EVT_PROCESS_END, EVT_UPDATE_TEXT

    """
    def __init__(self, parent, command, fname='',
                 args=list(), cwd=None, env=dict(),
                 use_shell=True):
        """Initialize the ProcessThread object
        Example:
          >>> myproc = ProcessThread(myframe, '/usr/local/bin/python',
                                     'hello.py', '--version', '/Users/me/home/')
          >>> myproc.start()

        @param parent: Parent Window/EventHandler to receive the events
                       generated by the process.
        @param command: Command string to execute as a subprocess.
        @keyword fname: Filename or path to file to run command on.
        @keyword args: Argument list or string to pass to use with fname arg.
        @keyword cwd: Directory to execute process from or None to use current
        @keyword env: Environment to run the process in (dictionary) or None to
                      use default.
        @keyword use_shell: Specify whether a shell should be used to launch 
                            program or run directly

        """
        super(ProcessThread, self).__init__(parent)

        if isinstance(args, list):
            args = u' '.join([arg.strip() for arg in args])

        # Attributes
        self._cwd = cwd             # Path at which to run from
        self._cmd = dict(cmd=command, file=fname, args=args)
        self._use_shell = use_shell

        # Make sure the environment is sane it must be all strings
        nenv = dict(env) # make a copy to manipulate
        for k, v in env.iteritems():
            if isinstance(v, types.UnicodeType):
                nenv[k] = v.encode(sys.getfilesystemencoding())
            elif not isinstance(v, basestring):
                nenv.pop(k)
        self._env = nenv

        # Setup
        self.setDaemon(True)

    def DoPopen(self):
        """Open the process
        @return: subprocess.Popen instance

        """
        # using shell, Popen will need a string, else it must be a sequence
        # use shlex for complex command line tokenization/parsing
        command = u' '.join([item.strip() for item in [self._cmd['cmd'],
                                                       self._cmd['file'],
                                                       self._cmd['args']]])
        command = command.strip()
        # TODO: exception handling and notification to main thread
        #       when encoding fails.
        command = command.encode(sys.getfilesystemencoding())
        if not self._use_shell and not subprocess.mswindows:
            # Note: shlex does not support Unicode
            command = shlex.split(command)

        # TODO: if a file path to the exe has any spaces in it on Windows
        #       and use_shell is True then the command will fail. Must force
        #       to False under this condition.
        use_shell = self._use_shell
        # TODO: See about supporting use_shell on Windows it causes lots of
        #       issues with gui apps and killing processes when it is True.
        if use_shell and subprocess.mswindows:
            suinfo = subprocess.STARTUPINFO()
            # Don't set this flag if we are not using the shell on
            # Windows as it will cause any gui app to not show on the
            # display!
            #TODO: move this into common library as it is needed
            #      by most code that uses subprocess
            if hasattr(subprocess, 'STARTF_USESHOWWINDOW'):
                suinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
            else:
                try:
                    from win32process import STARTF_USESHOWWINDOW
                    suinfo.dwFlags |= STARTF_USESHOWWINDOW
                except ImportError:
                    # Give up and try hard coded value from Windows.h
                    suinfo.dwFlags |= 0x00000001
        else:
            suinfo = None
        
        proc = subprocess.Popen(command,
                                stdout=subprocess.PIPE,
                                stderr=subprocess.STDOUT,
                                shell=use_shell,
                                cwd=self._cwd,
                                env=self._env,
                                startupinfo=suinfo)
        self.LastCommand = command # Set last run command
        return proc

    def SetArgs(self, args):
        """Set the args to pass to the command
        @param args: list or string of program arguments

        """
        if isinstance(args, list):
            u' '.join(item.strip() for item in args)
        self._cmd['args'] = args.strip()

    def SetCommand(self, cmd):
        """Set the command to execute
        @param cmd: Command string

        """
        self._cmd['cmd'] = cmd

    def SetFilename(self, fname):
        """Set the filename to run the command on
        @param fname: string or Unicode

        """
        self._cmd['file'] = fname

#-----------------------------------------------------------------------------#

class TaskThread(threading.Thread):
    """Run a task in its own thread."""
    def __init__(self, parent, task, *args, **kwargs):
        """Initialize the TaskThread. All *args and **kwargs are passed
        to the task.

        @param parent: Parent Window/EventHandler to receive the events
                       generated by the process.
        @param task: callable should be a generator object and must be iterable

        """
        super(TaskThread, self).__init__()
        assert isinstance(parent, OutputBuffer)

        self._task = TaskObject(parent, task, *args, **kwargs)

    def run(self):
        self._task.DoTask()

    def Cancel(self):
        self._task.Cancel()

class TaskObject(object):
    """Run a task in its own thread."""
    def __init__(self, parent, task, *args, **kwargs):
        """Initialize the TaskObject. All *args and **kwargs are passed
        to the task.

        @param parent: Parent Window/EventHandler to receive the events
                       generated by the process.
        @param task: callable should be a generator object and must be iterable

        """
        super(TaskObject, self).__init__()

        assert isinstance(parent, OutputBuffer)

        # Attributes
        self.cancel = False         # Abort task
        self._parent = parent       # Parent Window/Event Handler
        self.task = task            # Task method to run
        self._args = args
        self._kwargs = kwargs

    def DoTask(self):
        """Start running the task"""
        # Notify that task is beginning
        evt = OutputBufferEvent(edEVT_TASK_START, self._parent.GetId())
        wx.PostEvent(self._parent, evt)
        time.sleep(.5) # Give the event a chance to be processed

        # Run the task and post the results
        for result in self.task(*self._args, **self._kwargs):
            self._parent.AppendUpdate(result)
            if self.cancel:
                break

        # Notify that the task is finished
        evt = OutputBufferEvent(edEVT_TASK_COMPLETE, self._parent.GetId())
        wx.PostEvent(self._parent, evt)

    def Cancel(self):
        """Cancel the running task"""
        self.cancel = True