mirror of
https://github.com/revng/revng
synced 2026-06-21 14:07:57 +00:00
b8bd2fe868
Drop autogenerated queries from the GraphQL API and switch `revng.api` to use `PipelineDescriptor`.
199 lines
7.0 KiB
Python
199 lines
7.0 KiB
Python
#
|
|
# This file is distributed under the MIT License. See LICENSE.md for details.
|
|
#
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
from base64 import b64decode
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from functools import partial
|
|
from pathlib import Path
|
|
from typing import AsyncGenerator, Awaitable, Callable, Optional, ParamSpec, TypeVar
|
|
|
|
from starlette.datastructures import UploadFile
|
|
|
|
from ariadne import MutationType, QueryType, SubscriptionType, make_executable_schema
|
|
from ariadne import upload_scalar
|
|
|
|
from revng.api.manager import Manager
|
|
from revng.api.target import Target
|
|
|
|
from .event_manager import EventType, emit_event
|
|
from .multiqueue import MultiQueue
|
|
from .util import produce_serializer
|
|
|
|
executor = ThreadPoolExecutor(1)
|
|
invalidation_queue: MultiQueue[str] = MultiQueue()
|
|
|
|
T = TypeVar("T")
|
|
P = ParamSpec("P")
|
|
|
|
|
|
# Python runs all coroutines in the same event loop (which is handled by a single thread)
|
|
# The scheduling is done cooperatively, so once a coroutine starts executing it will run until
|
|
# the first call to `await` or `return`.
|
|
# This can work poorly if there are long-running sync function that are executed, since those block
|
|
# the event loop. To remedy this we use a separate thread to run these functions so that the event
|
|
# loop can run other coroutines in the meantime.
|
|
def run_in_executor(function: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> Awaitable[T]:
|
|
loop = asyncio.get_event_loop()
|
|
return loop.run_in_executor(executor, partial(function, *args, **kwargs))
|
|
|
|
|
|
query = QueryType()
|
|
mutation = MutationType()
|
|
subscription = SubscriptionType()
|
|
|
|
|
|
@query.field("produce")
|
|
async def resolve_produce(
|
|
obj, info, *, step: str, container: str, targetList: str, onlyIfReady=False # noqa: N803
|
|
):
|
|
manager: Manager = info.context["manager"]
|
|
targets = targetList.split(",")
|
|
result = await run_in_executor(manager.produce_target, step, targets, container, onlyIfReady)
|
|
return produce_serializer(result)
|
|
|
|
|
|
@query.field("produceArtifacts")
|
|
async def resolve_produce_artifacts(
|
|
obj, info, *, step: str, paths: Optional[str] = None, onlyIfReady=False # noqa: N803
|
|
):
|
|
manager: Manager = info.context["manager"]
|
|
target_paths = paths.split(",") if paths is not None else None
|
|
result = await run_in_executor(
|
|
manager.produce_target, step, target_paths, only_if_ready=onlyIfReady
|
|
)
|
|
return produce_serializer(result)
|
|
|
|
|
|
@query.field("targets")
|
|
async def resolve_targets(_, info, *, step: str, container: str):
|
|
manager: Manager = info.context["manager"]
|
|
targets = await run_in_executor(manager.get_targets, step, container)
|
|
return await run_in_executor(lambda: [t.as_dict() for t in targets])
|
|
|
|
|
|
@query.field("target")
|
|
async def resolve_target(_, info, *, step: str, container: str, target: str) -> Optional[Target]:
|
|
manager: Manager = info.context["manager"]
|
|
result = await run_in_executor(manager.deserialize_target, f"{step}/{container}/{target}")
|
|
return result.as_dict() if result is not None else None
|
|
|
|
|
|
@query.field("getGlobal")
|
|
async def resolve_get_global(_, info, *, name: str) -> str:
|
|
manager: Manager = info.context["manager"]
|
|
return await run_in_executor(manager.get_global, name)
|
|
|
|
|
|
@query.field("verifyGlobal")
|
|
async def resolve_verify_global(_, info, *, name: str, content: str) -> bool:
|
|
manager: Manager = info.context["manager"]
|
|
result = await run_in_executor(manager.verify_global, name, content)
|
|
return result.unwrap()
|
|
|
|
|
|
@query.field("verifyDiff")
|
|
async def resolve_verify_diff(_, info, *, globalName: str, content: str) -> bool: # noqa: N803
|
|
manager: Manager = info.context["manager"]
|
|
result = await run_in_executor(manager.verify_diff, globalName, content)
|
|
return result.unwrap()
|
|
|
|
|
|
@query.field("pipelineDescription")
|
|
async def resolve_pipeline_description(_, info) -> str:
|
|
manager: Manager = info.context["manager"]
|
|
return await run_in_executor(manager.get_pipeline_description)
|
|
|
|
|
|
@mutation.field("uploadB64")
|
|
@emit_event(EventType.BEGIN)
|
|
async def resolve_upload_b64(_, info, *, input: str, container: str): # noqa: A002
|
|
manager: Manager = info.context["manager"]
|
|
await run_in_executor(manager.set_input, container, b64decode(input))
|
|
await invalidation_queue.send("begin/input/:Binary")
|
|
logging.info(f"Saved file for container {container}")
|
|
return True
|
|
|
|
|
|
@mutation.field("uploadFile")
|
|
@emit_event(EventType.BEGIN)
|
|
async def resolve_upload_file(_, info, *, file: UploadFile, container: str):
|
|
manager: Manager = info.context["manager"]
|
|
contents = await file.read()
|
|
await run_in_executor(manager.set_input, container, contents)
|
|
await invalidation_queue.send("begin/input/:Binary")
|
|
logging.info(f"Saved file for container {container}")
|
|
return True
|
|
|
|
|
|
@mutation.field("runAnalysis")
|
|
@emit_event(EventType.CONTEXT)
|
|
async def resolve_run_analysis(
|
|
_,
|
|
info,
|
|
*,
|
|
step: str,
|
|
analysis: str,
|
|
containerToTargets: str | None = None, # noqa: N803
|
|
options: str | None = None,
|
|
):
|
|
manager: Manager = info.context["manager"]
|
|
target_map = json.loads(containerToTargets) if containerToTargets is not None else {}
|
|
parse_options = json.loads(options) if options is not None else {}
|
|
result = await run_in_executor(manager.run_analysis, step, analysis, target_map, parse_options)
|
|
await invalidation_queue.send(str(result.invalidations))
|
|
return json.dumps(result.result)
|
|
|
|
|
|
@mutation.field("runAnalysesList")
|
|
async def resolve_run_analyses_list(_, info, *, name: str, options: str | None = None):
|
|
manager: Manager = info.context["manager"]
|
|
parse_options = json.loads(options) if options is not None else {}
|
|
result = await run_in_executor(manager.run_analyses_list, name, parse_options)
|
|
await invalidation_queue.send(str(result.invalidations))
|
|
return json.dumps(result.result)
|
|
|
|
|
|
@mutation.field("setGlobal")
|
|
@emit_event(EventType.CONTEXT)
|
|
async def mutation_set_global(_, info, *, name: str, content: str) -> bool:
|
|
manager: Manager = info.context["manager"]
|
|
result = await run_in_executor(manager.set_global, name, content)
|
|
await invalidation_queue.send(str(result.invalidations))
|
|
return result.result.unwrap()
|
|
|
|
|
|
@mutation.field("applyDiff")
|
|
@emit_event(EventType.CONTEXT)
|
|
async def mutation_apply_diff(_, info, *, globalName: str, content: str) -> bool: # noqa: N803
|
|
manager: Manager = info.context["manager"]
|
|
result = await run_in_executor(manager.apply_diff, globalName, content)
|
|
await invalidation_queue.send(str(result.invalidations))
|
|
return result.result.unwrap()
|
|
|
|
|
|
@subscription.source("invalidations")
|
|
async def invalidations_generator(_, info) -> AsyncGenerator[str, None]:
|
|
with invalidation_queue.stream() as stream:
|
|
async for message in stream:
|
|
yield message
|
|
|
|
|
|
@subscription.field("invalidations")
|
|
async def invalidations(message: str, info):
|
|
return message
|
|
|
|
|
|
def get_schema():
|
|
schema_file = (Path(__file__).parent.resolve()) / "schema.graphql"
|
|
return make_executable_schema(
|
|
schema_file.read_text(),
|
|
query,
|
|
mutation,
|
|
subscription,
|
|
upload_scalar,
|
|
)
|