Beispiel #1
0
def newmux():
    mux = Tmux(binascii.hexlify(os.urandom(8)))
    muxes.append(mux)
    return mux
Beispiel #2
0
    def __init__(
        self,
        *command,
        width=80, height=24,
        wait_interval=0.01, default_timeout=1
    ):
        """
        Hecate will run the command line arguments specified by command (
        note: These may not contain spaces. If you want to pass this to a
        shell, invoke the shell explicitly).

        Additional parameters:

            width and height specify the height of the console to run in in
                characters. Note that you cannot currently resize the console
                once started.
            wait_interval is the polling frequency for functions like
                await_text. A smaller value will be more CPU intensive but
                may be slightly faster.
            default_timeout specifies the default timeout for all await
                functions.
        """
        self.wait_interval = wait_interval
        self.default_timeout = default_timeout
        self.has_shutdown = False
        self.tmux_id = binascii.hexlify(os.urandom(8)).decode('ascii')
        self.exit_seen = False
        self.tmux = Tmux(self.tmux_id)
        try:
            self.report_file = os.path.join(
                hecate_temp_dir(), self.tmux_id
            )
            with open(self.report_file, "w"):
                pass
            self.shutdown_called = False
            self.launched = False
            self.tmux.new_session(
                width=width, height=height, name=HECATE_SESSION_NAME,
                command=' '.join(
                    map(
                        shlex.quote, [
                            sys.executable,
                            RUNNER_PROGRAM, self.report_file
                        ] + list(command)))
            )
            sessions = [
                l.strip() for l in self.tmux.execute_command(
                    "list-sessions", "-F", "#{session_name}").splitlines()
            ]
            sessions.remove(HECATE_SESSION_NAME)
            for s in sessions:
                self.tmux.kill_session(s)
            windows = [
                l.strip() for l in self.tmux.execute_command(
                    "list-windows", "-F", "#{window_name}").splitlines()
            ]
            assert len(windows) == 1
            self.target_window = windows[0]
            assert len(self.tmux.panes()) == 1
            self.screenshot()
            for _ in self.poll_until_timeout(self.default_timeout):
                report = self.report_variables()
                if runner.CHILD in report:
                    self.ready = True
                    self.screenshot()
                    break
            else:
                raise Timeout(
                    "Process failed to start"
                )
            report = self.report_variables()
            os.kill(report[runner.CONTROLLER], signal.SIGUSR1)
            self.child_pid = report[runner.CHILD]
        except:
            self.shutdown_called = True
            self.tmux.shutdown()
            raise
Beispiel #3
0
    def __init__(self,
                 *command,
                 width=80,
                 height=24,
                 wait_interval=0.01,
                 default_timeout=1):
        """
        Hecate will run the command line arguments specified by command (
        note: These may not contain spaces. If you want to pass this to a
        shell, invoke the shell explicitly).

        Additional parameters:

            width and height specify the height of the console to run in in
                characters. Note that you cannot currently resize the console
                once started.
            wait_interval is the polling frequency for functions like
                await_text. A smaller value will be more CPU intensive but
                may be slightly faster.
            default_timeout specifies the default timeout for all await
                functions.
        """
        self.wait_interval = wait_interval
        self.default_timeout = default_timeout
        self.has_shutdown = False
        self.tmux_id = binascii.hexlify(os.urandom(8)).decode('ascii')
        self.exit_seen = False
        self.tmux = Tmux(self.tmux_id)
        try:
            self.report_file = os.path.join(hecate_temp_dir(), self.tmux_id)
            with open(self.report_file, "w"):
                pass
            self.shutdown_called = False
            self.launched = False
            self.tmux.new_session(
                width=width,
                height=height,
                name=HECATE_SESSION_NAME,
                command=' '.join(
                    map(shlex.quote,
                        [sys.executable, RUNNER_PROGRAM, self.report_file] +
                        list(command))))
            sessions = [
                l.strip() for l in self.tmux.execute_command(
                    "list-sessions", "-F", "#{session_name}").splitlines()
            ]
            sessions.remove(HECATE_SESSION_NAME)
            for s in sessions:
                self.tmux.kill_session(s)
            windows = [
                l.strip() for l in self.tmux.execute_command(
                    "list-windows", "-F", "#{window_name}").splitlines()
            ]
            assert len(windows) == 1
            self.target_window = windows[0]
            assert len(self.tmux.panes()) == 1
            self.screenshot()
            for _ in self.poll_until_timeout(self.default_timeout):
                report = self.report_variables()
                if runner.CHILD in report:
                    self.ready = True
                    self.screenshot()
                    break
            else:
                raise Timeout("Process failed to start")
            report = self.report_variables()
            os.kill(report[runner.CONTROLLER], signal.SIGUSR1)
            self.child_pid = report[runner.CHILD]
        except:
            self.shutdown_called = True
            self.tmux.shutdown()
            raise
Beispiel #4
0
class Runner(object):
    """
    A Runner manages a running console app. It is started in a
    virtual terminal of a specified size. You may then interact with it and
    get screenhots of the current state of the screen. Once you are done,
    you must call shutdown on the instance. You can also use it as a context
    manager for automatic resource cleanup.
    """
    print_on_exit = False

    def __init__(
        self,
        *command,
        width=80, height=24,
        wait_interval=0.01, default_timeout=1
    ):
        """
        Hecate will run the command line arguments specified by command (
        note: These may not contain spaces. If you want to pass this to a
        shell, invoke the shell explicitly).

        Additional parameters:

            width and height specify the height of the console to run in in
                characters. Note that you cannot currently resize the console
                once started.
            wait_interval is the polling frequency for functions like
                await_text. A smaller value will be more CPU intensive but
                may be slightly faster.
            default_timeout specifies the default timeout for all await
                functions.
        """
        self.wait_interval = wait_interval
        self.default_timeout = default_timeout
        self.has_shutdown = False
        self.tmux_id = binascii.hexlify(os.urandom(8)).decode('ascii')
        self.exit_seen = False
        self.tmux = Tmux(self.tmux_id)
        try:
            self.report_file = os.path.join(
                hecate_temp_dir(), self.tmux_id
            )
            with open(self.report_file, "w"):
                pass
            self.shutdown_called = False
            self.launched = False
            self.tmux.new_session(
                width=width, height=height, name=HECATE_SESSION_NAME,
                command=' '.join(
                    map(
                        shlex.quote, [
                            sys.executable,
                            RUNNER_PROGRAM, self.report_file
                        ] + list(command)))
            )
            sessions = [
                l.strip() for l in self.tmux.execute_command(
                    "list-sessions", "-F", "#{session_name}").splitlines()
            ]
            sessions.remove(HECATE_SESSION_NAME)
            for s in sessions:
                self.tmux.kill_session(s)
            windows = [
                l.strip() for l in self.tmux.execute_command(
                    "list-windows", "-F", "#{window_name}").splitlines()
            ]
            assert len(windows) == 1
            self.target_window = windows[0]
            assert len(self.tmux.panes()) == 1
            self.screenshot()
            for _ in self.poll_until_timeout(self.default_timeout):
                report = self.report_variables()
                if runner.CHILD in report:
                    self.ready = True
                    self.screenshot()
                    break
            else:
                raise Timeout(
                    "Process failed to start"
                )
            report = self.report_variables()
            os.kill(report[runner.CONTROLLER], signal.SIGUSR1)
            self.child_pid = report[runner.CHILD]
        except:
            self.shutdown_called = True
            self.tmux.shutdown()
            raise

    def shutdown(self):
        """
        Kill this Hecate instance and free all resources associated with it.

        This is safe to call multiple times but is a no-op the second time. It
        will be automatically called if you are using this as a context
        manager.
        """
        if self.shutdown_called:
            return
        self.shutdown_called = True
        try:
            if not self.exit_seen:
                try:
                    self.await_exit()
                except Timeout:
                    pass
            if self.print_on_exit:
                print(self.last_screenshot)
        finally:
            report = self.report_variables()
            for c in [runner.CHILD, runner.CONTROLLER]:
                try:
                    must_die(report[c])
                except KeyError:
                    pass
                except:
                    traceback.print_exc()
            self.tmux.shutdown()

    def __del__(self):
        if not self.shutdown_called:
            warn(HecateWillHauntYou(
                "Garbage collecting Hecate instance which has not been shut "
                "down properly. This is a really bad idea. Always call "
                "shutdown on your Hecate instances, ideally by using them as "
                "context managers."))
            self.shutdown()

    def screenshot(self):
        """
        Return a string representing the current state of the screen.
        """
        try:
            result = self.tmux.capture_pane(0)
            self.last_screenshot = result
            return result
        except DeadServer:
            return self.last_screenshot

    def press(self, key):
        """
        Press the key identified by key-press. This will currently be passed
        uninterpreted to tmux, which will assign its own meaning to it. So e.g
        Enter will be the enter key, C-d will send EOF, etc.
        """
        self.tmux.send_key(0, key)

    def write(self, text):
        """
        Write this as text to the console as it is. Will not be interpreted as
        a special character, so e.g. Enter is the literal string Enter and not
        a return character.
        """
        self.tmux.new_buffer(text)
        self.tmux.execute_command("paste-buffer")

    def await_text(self, text, timeout=None):
        """
        Wait for 'text' to appear on the screen, accounting for line wrapping.
        If timeout (or default timeout if not set) seconds elapse first, raise
        a Timeout error.
        """
        for _ in self.poll_until_timeout(timeout):
            screen = self.screenshot()
            munged = screen.replace('\n', '')
            if text in munged:
                return
        raise Timeout("Timeout while waiting for text %r to appear" % (text,))

    def await_exit(self, timeout=None):
        """
        Wait for the process to exit. If it exits with a non-zero status code,
        AbnormalExit will be raised. If timeout or default_timeout seconds pass
        without it exiting, raise a Timeout.
        """
        for _ in self.poll_until_timeout(timeout):
            report = self.report_variables()
            if runner.EXIT_STATUS in report:
                self.exit_seen = True
                status = report[runner.EXIT_STATUS]
                if status != 0:
                    raise AbnormalExit(
                        "Process exited with status %d" % (status,))
                else:
                    return
        self.screenshot()
        raise Timeout("Timeout while waiting for process to exit")

    def kill(self, sig):
        """
        Send a signal to the running process. sig is either an integer signal
        number or the name of the signal.
        """
        if isinstance(sig, str):
            sig = getattr(signal, sig)
        os.kill(self.child_pid, sig)

    def poll_until_timeout(self, timeout=None):
        if timeout is None:
            timeout = self.default_timeout
        start = time.time()
        while time.time() <= start + timeout:
            yield
            time.sleep(self.wait_interval)

    def report_variables(self):
        with open(self.report_file) as r:
            result = {}
            for line in r:
                k, v = line.split(":", 2)
                result[k] = int(v)
            return result

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.shutdown()
Beispiel #5
0
class Runner(object):
    """
    A Runner manages a running console app. It is started in a
    virtual terminal of a specified size. You may then interact with it and
    get screenhots of the current state of the screen. Once you are done,
    you must call shutdown on the instance. You can also use it as a context
    manager for automatic resource cleanup.
    """
    print_on_exit = False

    def __init__(self,
                 *command,
                 width=80,
                 height=24,
                 wait_interval=0.01,
                 default_timeout=1):
        """
        Hecate will run the command line arguments specified by command (
        note: These may not contain spaces. If you want to pass this to a
        shell, invoke the shell explicitly).

        Additional parameters:

            width and height specify the height of the console to run in in
                characters. Note that you cannot currently resize the console
                once started.
            wait_interval is the polling frequency for functions like
                await_text. A smaller value will be more CPU intensive but
                may be slightly faster.
            default_timeout specifies the default timeout for all await
                functions.
        """
        self.wait_interval = wait_interval
        self.default_timeout = default_timeout
        self.has_shutdown = False
        self.tmux_id = binascii.hexlify(os.urandom(8)).decode('ascii')
        self.exit_seen = False
        self.tmux = Tmux(self.tmux_id)
        try:
            self.report_file = os.path.join(hecate_temp_dir(), self.tmux_id)
            with open(self.report_file, "w"):
                pass
            self.shutdown_called = False
            self.launched = False
            self.tmux.new_session(
                width=width,
                height=height,
                name=HECATE_SESSION_NAME,
                command=' '.join(
                    map(shlex.quote,
                        [sys.executable, RUNNER_PROGRAM, self.report_file] +
                        list(command))))
            sessions = [
                l.strip() for l in self.tmux.execute_command(
                    "list-sessions", "-F", "#{session_name}").splitlines()
            ]
            sessions.remove(HECATE_SESSION_NAME)
            for s in sessions:
                self.tmux.kill_session(s)
            windows = [
                l.strip() for l in self.tmux.execute_command(
                    "list-windows", "-F", "#{window_name}").splitlines()
            ]
            assert len(windows) == 1
            self.target_window = windows[0]
            assert len(self.tmux.panes()) == 1
            self.screenshot()
            for _ in self.poll_until_timeout(self.default_timeout):
                report = self.report_variables()
                if runner.CHILD in report:
                    self.ready = True
                    self.screenshot()
                    break
            else:
                raise Timeout("Process failed to start")
            report = self.report_variables()
            os.kill(report[runner.CONTROLLER], signal.SIGUSR1)
            self.child_pid = report[runner.CHILD]
        except:
            self.shutdown_called = True
            self.tmux.shutdown()
            raise

    def shutdown(self):
        """
        Kill this Hecate instance and free all resources associated with it.

        This is safe to call multiple times but is a no-op the second time. It
        will be automatically called if you are using this as a context
        manager.
        """
        if self.shutdown_called:
            return
        self.shutdown_called = True
        try:
            if not self.exit_seen:
                try:
                    self.await_exit()
                except Timeout:
                    pass
            if self.print_on_exit:
                print(self.last_screenshot)
        finally:
            report = self.report_variables()
            for c in [runner.CHILD, runner.CONTROLLER]:
                try:
                    must_die(report[c])
                except KeyError:
                    pass
                except:
                    traceback.print_exc()
            self.tmux.shutdown()

    def __del__(self):
        if not self.shutdown_called:
            warn(
                HecateWillHauntYou(
                    "Garbage collecting Hecate instance which has not been shut "
                    "down properly. This is a really bad idea. Always call "
                    "shutdown on your Hecate instances, ideally by using them as "
                    "context managers."))
            self.shutdown()

    def screenshot(self):
        """
        Return a string representing the current state of the screen.
        """
        try:
            result = self.tmux.capture_pane(0)
            self.last_screenshot = result
            return result
        except DeadServer:
            return self.last_screenshot

    def press(self, key):
        """
        Press the key identified by key-press. This will currently be passed
        uninterpreted to tmux, which will assign its own meaning to it. So e.g
        Enter will be the enter key, C-d will send EOF, etc.
        """
        self.tmux.send_key(0, key)

    def write(self, text):
        """
        Write this as text to the console as it is. Will not be interpreted as
        a special character, so e.g. Enter is the literal string Enter and not
        a return character.
        """
        self.tmux.new_buffer(text)
        self.tmux.execute_command("paste-buffer")

    def await_text(self, text, timeout=None):
        """
        Wait for 'text' to appear on the screen, accounting for line wrapping.
        If timeout (or default timeout if not set) seconds elapse first, raise
        a Timeout error.
        """
        for _ in self.poll_until_timeout(timeout):
            screen = self.screenshot()
            munged = screen.replace('\n', '')
            if text in munged:
                return
        raise Timeout("Timeout while waiting for text %r to appear" % (text, ))

    def await_exit(self, timeout=None):
        """
        Wait for the process to exit. If it exits with a non-zero status code,
        AbnormalExit will be raised. If timeout or default_timeout seconds pass
        without it exiting, raise a Timeout.
        """
        for _ in self.poll_until_timeout(timeout):
            report = self.report_variables()
            if runner.EXIT_STATUS in report:
                self.exit_seen = True
                status = report[runner.EXIT_STATUS]
                if status != 0:
                    raise AbnormalExit("Process exited with status %d" %
                                       (status, ))
                else:
                    return
        self.screenshot()
        raise Timeout("Timeout while waiting for process to exit")

    def kill(self, sig):
        """
        Send a signal to the running process. sig is either an integer signal
        number or the name of the signal.
        """
        if isinstance(sig, str):
            sig = getattr(signal, sig)
        os.kill(self.child_pid, sig)

    def poll_until_timeout(self, timeout=None):
        if timeout is None:
            timeout = self.default_timeout
        start = time.time()
        while time.time() <= start + timeout:
            yield
            time.sleep(self.wait_interval)

    def report_variables(self):
        with open(self.report_file) as r:
            result = {}
            for line in r:
                k, v = line.split(":", 2)
                result[k] = int(v)
            return result

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.shutdown()