Exemple #1
0
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))
Exemple #2
0
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
Exemple #3
0
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
Exemple #4
0
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())
Exemple #5
0
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())
Exemple #6
0
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
Exemple #7
0
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
Exemple #8
0
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."""
Exemple #10
0
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
Exemple #11
0
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: