async def run_package_cb(req: rpc.RunPackage.Request, ui: WsClient) -> None: async def _update_executed(package_id: str) -> None: meta = await run_in_executor(read_package_meta, package_id) meta.executed = datetime.now(tz=timezone.utc) await run_in_executor(write_package_meta, package_id, meta) global PROCESS global TASK global RUNNING_PACKAGE_ID if PACKAGE_STATE_EVENT.data.state not in PackageState.RUNNABLE_STATES: raise Arcor2Exception("Package not stopped!") assert not process_running() package_path = os.path.join(PROJECT_PATH, req.args.id) try: await run_in_executor(os.chdir, package_path, propagate=[FileNotFoundError]) except FileNotFoundError: raise Arcor2Exception("Not found.") script_path = os.path.join(package_path, MAIN_SCRIPT_NAME) await check_script(script_path) # this is necessary in order to make PEX embedded modules available to subprocess pypath = ":".join(sys.path) # create a temp copy of the env variables myenv = os.environ.copy() # set PYTHONPATH to match this scripts sys.path myenv["PYTHONPATH"] = pypath args = [script_path] if req.args.start_paused: args.append("-p") if req.args.breakpoints: args.append(f"-b \"{','.join(req.args.breakpoints)}\"") logger.info(f"Starting script: {script_path}") PROCESS = await asyncio.create_subprocess_exec( "python3.9", *args, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, env=myenv, ) if PROCESS.returncode is not None: raise Arcor2Exception("Failed to start package.") RUNNING_PACKAGE_ID = req.args.id await package_state(PackageState(PackageState.Data(PackageState.Data.StateEnum.RUNNING, RUNNING_PACKAGE_ID))) TASK = asyncio.create_task(read_proc_stdout()) # run task in background asyncio.create_task(_update_executed(req.args.id))
def handle_stdin_commands(*, before: bool, breakpoint: bool = False) -> None: """Reads stdin and checks for commands from parent script (e.g. Execution unit). Prints events to stdout. p == pause script r == resume script :return: """ global _pause_on_next_action global start_paused if read_stdin() == "p" or ( before and _pause_on_next_action) or start_paused or breakpoint: start_paused = False print_event( PackageState(PackageState.Data( PackageState.Data.StateEnum.PAUSED))) while True: cmd = read_stdin(0.1) if cmd not in ("s", "r"): continue _pause_on_next_action = cmd == "s" print_event( PackageState( PackageState.Data(PackageState.Data.StateEnum.RUNNING))) break
async def pause_package_cb(req: rpc.PausePackage.Request, ui: WsClient) -> None: if not process_running(): raise Arcor2Exception("Project not running.") assert PROCESS is not None assert PROCESS.stdin is not None if PACKAGE_STATE_EVENT.data.state != PackageState.Data.StateEnum.RUNNING: raise Arcor2Exception("Cannot pause.") await package_state(PackageState(PackageState.Data(PackageState.Data.StateEnum.PAUSING, RUNNING_PACKAGE_ID))) PROCESS.stdin.write("p\n".encode()) await PROCESS.stdin.drain() return None
async def step_action_cb(req: rpc.StepAction.Request, ui: WsClient) -> None: async def _step() -> None: assert PROCESS is not None assert PROCESS.stdin is not None PROCESS.stdin.write("s\n".encode()) await PROCESS.stdin.drain() logger.info("Stepping to a next action.") if PACKAGE_STATE_EVENT.data.state != PackageState.Data.StateEnum.PAUSED: raise Arcor2Exception("Can't step, execution is not paused.") await package_state(PackageState(PackageState.Data(PackageState.Data.StateEnum.RESUMING, RUNNING_PACKAGE_ID))) assert process_running() asyncio.create_task(_step())
async def pause_package_cb(req: rpc.PausePackage.Request, ui: WsClient) -> None: async def _pause() -> None: assert PROCESS is not None assert PROCESS.stdin is not None PROCESS.stdin.write("p\n".encode()) await PROCESS.stdin.drain() logger.info("Package paused.") if PACKAGE_STATE_EVENT.data.state != PackageState.Data.StateEnum.RUNNING: raise Arcor2Exception("Cannot pause.") await package_state(PackageState(PackageState.Data(PackageState.Data.StateEnum.PAUSING, RUNNING_PACKAGE_ID))) assert process_running() asyncio.create_task(_pause())
async def stop_package_cb(req: rpc.StopPackage.Request, ui: WsClient) -> None: global PACKAGE_INFO_EVENT global RUNNING_PACKAGE_ID if not process_running(): raise Arcor2Exception("Project not running.") assert PROCESS is not None assert TASK is not None await package_state(PackageState(PackageState.Data(PackageState.Data.StateEnum.STOPPING, RUNNING_PACKAGE_ID))) logger.info("Terminating process") PROCESS.send_signal(signal.SIGINT) # the same as when a user presses ctrl+c logger.info("Waiting for process to finish...") await asyncio.wait([TASK]) PACKAGE_INFO_EVENT = None RUNNING_PACKAGE_ID = None
def handle_stdin_commands() -> None: """Reads stdin and checks for commands from parent script (e.g. Execution unit). Prints events to stdout. p == pause script r == resume script :return: """ ctrl_cmd = read_stdin() if ctrl_cmd == "p": print_event(PackageState(PackageState.Data(PackageState.Data.StateEnum.PAUSED))) while True: ctrl_cmd = read_stdin(0.1) if ctrl_cmd == "r": print_event(PackageState(PackageState.Data(PackageState.Data.StateEnum.RUNNING))) break
async def stop_package_cb(req: rpc.StopPackage.Request, ui: WsClient) -> None: async def _terminate_task() -> None: global PACKAGE_INFO_EVENT global RUNNING_PACKAGE_ID assert PROCESS assert TASK logger.info("Terminating process") PROCESS.send_signal(signal.SIGINT) # the same as when a user presses ctrl+c logger.info("Waiting for process to finish...") await asyncio.wait([TASK]) PACKAGE_INFO_EVENT = None RUNNING_PACKAGE_ID = None if PACKAGE_STATE_EVENT.data.state not in PackageState.RUN_STATES: raise Arcor2Exception("Package not running.") assert process_running() await package_state(PackageState(PackageState.Data(PackageState.Data.StateEnum.STOPPING, RUNNING_PACKAGE_ID))) asyncio.create_task(_terminate_task())
app = create_app(__name__) ws: Optional[websocket.WebSocket] = None if TYPE_CHECKING: ReqQueue = Queue[ arcor2_rpc.common.RPC.Request] # this is only processed by mypy RespQueue = Queue[arcor2_rpc.common.RPC.Response] else: ReqQueue = Queue # this is not seen by mypy but will be executed at runtime. RespQueue = Queue rpc_request_queue: ReqQueue = ReqQueue() rpc_responses: dict[int, RespQueue] = {} package_state: PackageState.Data = PackageState.Data() package_info: Optional[PackageInfo.Data] = None exception_messages: list[str] = [] # hold last action state for AP visualization # do not unset ActionStateBefore event, this information might be used even after ActionStateAfter event action_state_before: Optional[ActionStateBefore.Data] = None action_state_after: Optional[ActionStateAfter.Data] = None breakpoints: dict[str, set[str]] = {} @contextmanager def tokens_db(): """This is a wrapper for SqliteDict enabling one to change all settings at one place."""
async def read_proc_stdout() -> None: global PACKAGE_STATE_EVENT global ACTION_EVENT global ACTION_ARGS_EVENT global PACKAGE_INFO_EVENT global RUNNING_PACKAGE_ID logger.info("Reading script stdout...") assert PROCESS is not None assert PROCESS.stdout is not None assert RUNNING_PACKAGE_ID is not None await package_state(PackageState(PackageState.Data(PackageState.Data.StateEnum.RUNNING, RUNNING_PACKAGE_ID))) printed_out: List[str] = [] while process_running(): try: stdout = await PROCESS.stdout.readuntil() except asyncio.exceptions.IncompleteReadError: break decoded = stdout.decode("utf-8") stripped = decoded.strip() try: data = json.loads(stripped) except json.JsonException: printed_out.append(decoded) logger.error(decoded.strip()) continue if not isinstance(data, dict) or "event" not in data: logger.error("Strange data from script: {}".format(data)) continue try: evt = EVENT_MAPPING[data["event"]].from_dict(data) except ValidationError as e: logger.error("Invalid event: {}, error: {}".format(data, e)) continue if isinstance(evt, PackageState): evt.data.package_id = RUNNING_PACKAGE_ID await package_state(evt) continue elif isinstance(evt, PackageInfo): PACKAGE_INFO_EVENT = evt await send_to_clients(evt) PACKAGE_INFO_EVENT = None if PROCESS.returncode: if printed_out: # TODO remember this (until another package is started) and send it to new clients? last_line = printed_out[-1].strip() try: exception_type, message = last_line.split(":", 1) except ValueError: exception_type, message = "Unknown", last_line await send_to_clients(ProjectException(ProjectException.Data(message, exception_type))) with open("traceback-{}.txt".format(time.strftime("%Y%m%d-%H%M%S")), "w") as tb_file: tb_file.write("".join(printed_out)) else: logger.warn( f"Process ended with non-zero return code ({PROCESS.returncode}), but didn't printed out anything." ) await package_state(PackageState(PackageState.Data(PackageState.Data.StateEnum.STOPPED, RUNNING_PACKAGE_ID))) logger.info(f"Process finished with returncode {PROCESS.returncode}.") RUNNING_PACKAGE_ID = None
import arcor2_execution_data from arcor2 import json, ws_server from arcor2.data import common, compile_json_schemas from arcor2.data import rpc as arcor2_rpc from arcor2.data.events import Event, PackageInfo, PackageState, ProjectException from arcor2.exceptions import Arcor2Exception from arcor2.helpers import port_from_url from arcor2.logging import get_aiologger from arcor2_execution_data import EVENTS, URL, events, rpc from arcor2_execution_data.common import PackageSummary, ProjectMeta from arcor2_execution_data.package import PROJECT_PATH, read_package_meta, write_package_meta logger = get_aiologger("Execution") PROCESS: Union[asyncio.subprocess.Process, None] = None PACKAGE_STATE_EVENT: PackageState = PackageState(PackageState.Data()) # undefined state RUNNING_PACKAGE_ID: Optional[str] = None # in case of man. written scripts, this might not be sent PACKAGE_INFO_EVENT: Optional[PackageInfo] = None TASK = None CLIENTS: Set = set() MAIN_SCRIPT_NAME = "script.py" EVENT_MAPPING = {evt.__name__: evt for evt in EVENTS} def process_running() -> bool: