mitmproxy/docs/scripts/clirecording/clidirector.py

191 lines
6.0 KiB
Python
Raw Normal View History

2020-09-03 15:14:54 +00:00
import json
2022-04-26 11:53:23 +00:00
from typing import NamedTuple, Optional
2020-09-03 15:14:54 +00:00
import libtmux
import random
import subprocess
import threading
import time
2022-04-26 11:53:23 +00:00
class InstructionSpec(NamedTuple):
2020-09-04 13:22:04 +00:00
instruction: str
time_from: float
time_to: float
2020-09-03 15:14:54 +00:00
class CliDirector:
def __init__(self):
self.record_start = None
2020-09-04 13:02:58 +00:00
self.pause_between_keys = 0.2
2022-04-26 11:51:11 +00:00
self.instructions: list[InstructionSpec] = []
2020-09-03 15:14:54 +00:00
def start(self, filename: str, width: int = 0, height: int = 0) -> libtmux.Session:
self.start_session(width, height)
self.start_recording(filename)
return self.tmux_session
def start_session(self, width: int = 0, height: int = 0) -> libtmux.Session:
self.tmux_server = libtmux.Server()
2022-04-26 11:53:35 +00:00
self.tmux_session = self.tmux_server.new_session(
session_name="asciinema_recorder", kill_session=True
)
2020-09-03 15:14:54 +00:00
self.tmux_pane = self.tmux_session.attached_window.attached_pane
self.tmux_version = self.tmux_pane.display_message("#{version}", True)
if width and height:
self.resize_window(width, height)
self.pause(3)
return self.tmux_session
def start_recording(self, filename: str) -> None:
2022-04-26 11:53:35 +00:00
self.asciinema_proc = subprocess.Popen(
[
"asciinema",
"rec",
"-y",
"--overwrite",
"-c",
"tmux attach -t asciinema_recorder",
filename,
]
)
2020-09-03 15:14:54 +00:00
self.pause(1.5)
2020-09-04 13:02:58 +00:00
self.record_start = time.time()
2020-09-03 15:14:54 +00:00
def resize_window(self, width: int, height: int) -> None:
2022-04-26 11:53:35 +00:00
subprocess.Popen(
["resize", "-s", str(height), str(width)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
2020-09-03 15:14:54 +00:00
def end(self) -> None:
self.end_recording()
self.end_session()
def end_recording(self) -> None:
self.asciinema_proc.terminate()
self.asciinema_proc.wait(timeout=5)
2020-09-04 13:02:58 +00:00
self.record_start = None
2020-09-04 13:22:04 +00:00
self.instructions = []
2020-09-03 15:14:54 +00:00
def end_session(self) -> None:
self.tmux_session.kill_session()
2022-04-26 11:53:35 +00:00
def press_key(
self, keys: str, count=1, pause: Optional[float] = None, target=None
) -> None:
2020-09-03 15:14:54 +00:00
if pause is None:
pause = self.pause_between_keys
if target is None:
target = self.tmux_pane
for i in range(count):
if keys == " ":
keys = "Space"
target.send_keys(cmd=keys, enter=False, suppress_history=False)
2020-09-04 13:02:58 +00:00
# inspired by https://github.com/dmotz/TuringType
2020-09-04 16:39:08 +00:00
real_pause = random.uniform(0, pause) + 0.4 * pause
if keys == "Space":
real_pause += 1.5 * pause
elif keys == ".":
2020-09-04 13:02:58 +00:00
real_pause += pause
2020-09-04 16:39:08 +00:00
elif random.random() > 0.75:
real_pause += pause
elif random.random() > 0.95:
2020-09-04 13:02:58 +00:00
real_pause += 2 * pause
self.pause(real_pause)
2020-09-03 15:14:54 +00:00
2022-04-26 11:53:35 +00:00
def type(self, keys: str, pause: Optional[float] = None, target=None) -> None:
2020-09-03 15:14:54 +00:00
if pause is None:
pause = self.pause_between_keys
if target is None:
target = self.tmux_pane
target.select_pane()
for key in keys:
self.press_key(key, pause=pause, target=target)
2022-04-26 11:53:35 +00:00
def exec(self, keys: str, target=None) -> None:
2020-09-03 15:14:54 +00:00
if target is None:
target = self.tmux_pane
self.type(keys, target=target)
self.pause(1.25)
self.press_key("Enter", target=target)
self.pause(0.5)
def focus_pane(self, pane: libtmux.Pane, set_active_pane: bool = True) -> None:
pane.select_pane()
if set_active_pane:
self.tmux_pane = pane
def pause(self, seconds: float) -> None:
time.sleep(seconds)
def run_external(self, command: str) -> None:
subprocess.run(command, shell=True)
2022-04-26 11:53:35 +00:00
def message(
self,
msg: str,
duration: Optional[int] = None,
add_instruction: bool = True,
instruction_html: str = "",
) -> None:
2020-09-03 15:14:54 +00:00
if duration is None:
2020-09-04 16:39:08 +00:00
duration = len(msg) * 0.08 # seconds
2022-04-26 11:53:35 +00:00
self.tmux_session.set_option(
"display-time", int(duration * 1000)
) # milliseconds
2020-09-03 15:14:54 +00:00
self.tmux_pane.display_message(" " + msg)
if add_instruction or instruction_html:
if not instruction_html:
instruction_html = msg
self.instruction(instruction=instruction_html, duration=duration)
self.pause(duration + 0.5)
def popup(self, content: str, duration: int = 4) -> None:
# todo: check if installed tmux version supports display-popup
# tmux's display-popup is blocking, so we close it in a separate thread
2020-09-04 16:51:02 +00:00
t = threading.Thread(target=self.close_popup, args=[duration])
2020-09-03 15:14:54 +00:00
t.start()
lines = content.splitlines()
self.tmux_pane.cmd("display-popup", "", *lines)
t.join()
def close_popup(self, duration: float = 0) -> None:
self.pause(duration)
self.tmux_pane.cmd("display-popup", "-C")
2022-04-26 11:53:35 +00:00
def instruction(
self, instruction: str, duration: float = 3, time_from: Optional[float] = None
) -> None:
2020-09-03 15:14:54 +00:00
if time_from is None:
time_from = self.current_time
2022-04-26 11:53:35 +00:00
self.instructions.append(
InstructionSpec(
instruction=str(len(self.instructions) + 1) + ". " + instruction,
time_from=round(time_from, 1),
time_to=round(time_from + duration, 1),
)
)
2020-09-03 15:14:54 +00:00
def save_instructions(self, output_path: str) -> None:
instr_as_dicts = []
for instr in self.instructions:
instr_as_dicts.append(instr._asdict())
2022-04-26 11:53:35 +00:00
with open(output_path, "w", encoding="utf-8") as f:
2020-09-03 15:14:54 +00:00
json.dump(instr_as_dicts, f, ensure_ascii=False, indent=4)
2020-09-04 13:22:04 +00:00
@property
def current_time(self) -> float:
now = time.time()
return round(now - self.record_start, 1)
2020-09-03 15:14:54 +00:00
2020-09-04 13:22:04 +00:00
@property
def current_pane(self) -> libtmux.Pane:
return self.tmux_pane