waits for the process to complete, handles the exit code

def wait(self):
        """ waits for the process to complete, handles the exit code """

        self.log.debug("acquiring wait lock to wait for completion")
        # using the lock in a with-context blocks, which is what we want if
        # we're running wait()
        with self._wait_lock:
            self.log.debug("got wait lock")
            witnessed_end = False

            if self.exit_code is None:
                self.log.debug("exit code not set, waiting on pid")
                pid, exit_code = no_interrupt(os.waitpid, self.pid, 0) # blocks
                self.exit_code = handle_process_exit_code(exit_code)
                witnessed_end = True

            else:
                self.log.debug("exit code already set (%d), no need to wait",
                        self.exit_code)

            self._quit_threads.set()

            # we may not have a thread for stdin, if the pipe has been connected
            # via _piped="direct"
            if self._input_thread:
                self._input_thread.join()

            # wait, then signal to our output thread that the child process is
            # done, and we should have finished reading all the stdout/stderr
            # data that we can by now
            timer = threading.Timer(2.0, self._stop_output_event.set)
            timer.start()

            # wait for our stdout and stderr streamreaders to finish reading and
            # aggregating the process output
            self._output_thread.join()
            timer.cancel()

            self._background_thread.join()

            if witnessed_end:
                self._process_just_ended()

            return self.exit_code