fm-weck 1.4.7__py3-none-any.whl → 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fm_weck/__init__.py +1 -1
- fm_weck/capture.py +31 -0
- fm_weck/cli.py +218 -28
- fm_weck/config.py +4 -3
- fm_weck/engine.py +150 -17
- fm_weck/exceptions.py +58 -0
- fm_weck/file_util.py +28 -0
- fm_weck/grpc_service/__init__.py +9 -0
- fm_weck/grpc_service/fm_weck_client.py +148 -0
- fm_weck/grpc_service/fm_weck_server.py +175 -0
- fm_weck/grpc_service/proto/__init__.py +0 -0
- fm_weck/grpc_service/proto/fm_weck_service.proto +139 -0
- fm_weck/grpc_service/proto/fm_weck_service_pb2.py +73 -0
- fm_weck/grpc_service/proto/fm_weck_service_pb2.pyi +151 -0
- fm_weck/grpc_service/proto/fm_weck_service_pb2_grpc.py +331 -0
- fm_weck/grpc_service/proto/generate_protocol_files.sh +18 -0
- fm_weck/grpc_service/request_handling.py +332 -0
- fm_weck/grpc_service/run_store.py +38 -0
- fm_weck/grpc_service/server_utils.py +27 -0
- fm_weck/image_mgr.py +3 -1
- fm_weck/resources/Containerfile +1 -2
- fm_weck/resources/__init__.py +26 -8
- fm_weck/resources/c_program_example.c +627 -0
- fm_weck/resources/fm_tools/ltsmin.yml +20 -2
- fm_weck/run_result.py +1 -1
- fm_weck/serve.py +64 -18
- fm_weck/smoke_test_mode.py +82 -0
- fm_weck/tmp_file.py +61 -0
- {fm_weck-1.4.7.dist-info → fm_weck-1.5.0.dist-info}/METADATA +13 -5
- {fm_weck-1.4.7.dist-info → fm_weck-1.5.0.dist-info}/RECORD +32 -15
- {fm_weck-1.4.7.dist-info → fm_weck-1.5.0.dist-info}/WHEEL +0 -0
- {fm_weck-1.4.7.dist-info → fm_weck-1.5.0.dist-info}/entry_points.txt +0 -0
fm_weck/__init__.py
CHANGED
fm_weck/capture.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# This file is part of fm-weck: executing fm-tools in containerized environments.
|
|
2
|
+
# https://gitlab.com/sosy-lab/software/fm-weck
|
|
3
|
+
#
|
|
4
|
+
# SPDX-FileCopyrightText: 2024 Dirk Beyer <https://www.sosy-lab.org>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
7
|
+
|
|
8
|
+
import io
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Capture:
|
|
15
|
+
def __init__(self, target: io.TextIOBase, stream: Literal["stdout, stderr, stdin"] = "stdout"):
|
|
16
|
+
self.io_stream = getattr(sys, stream)
|
|
17
|
+
self._copy = os.dup(self.io_stream.fileno())
|
|
18
|
+
os.dup2(target.fileno(), self.io_stream.fileno())
|
|
19
|
+
|
|
20
|
+
def __del__(self):
|
|
21
|
+
# Ensure cleanup in case the context manager isn't used
|
|
22
|
+
self._cleanup()
|
|
23
|
+
|
|
24
|
+
def _cleanup(self):
|
|
25
|
+
os.dup2(self._copy, self.io_stream.fileno())
|
|
26
|
+
|
|
27
|
+
def __enter__(self):
|
|
28
|
+
return self # Return the instance for use within the context
|
|
29
|
+
|
|
30
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
31
|
+
self._cleanup() # Clean up resources upon leaving the context
|
fm_weck/cli.py
CHANGED
|
@@ -11,7 +11,6 @@ import logging
|
|
|
11
11
|
import os
|
|
12
12
|
from argparse import Namespace
|
|
13
13
|
from dataclasses import dataclass
|
|
14
|
-
from functools import cache
|
|
15
14
|
from pathlib import Path
|
|
16
15
|
from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple, Union
|
|
17
16
|
|
|
@@ -39,7 +38,7 @@ import contextlib
|
|
|
39
38
|
from fm_weck import Config
|
|
40
39
|
from fm_weck.cache_mgr import ask_and_clear, clear_cache
|
|
41
40
|
from fm_weck.config import _SEARCH_ORDER
|
|
42
|
-
from fm_weck.resources import iter_fm_data,
|
|
41
|
+
from fm_weck.resources import fm_tools_choice_map, iter_fm_data, property_choice_map
|
|
43
42
|
|
|
44
43
|
from . import __version__
|
|
45
44
|
from .exceptions import NoImageError
|
|
@@ -112,9 +111,9 @@ class ShellCompletion:
|
|
|
112
111
|
def add_tool_arg(parser, nargs: str | None = "?"):
|
|
113
112
|
parser.add_argument(
|
|
114
113
|
"TOOL",
|
|
115
|
-
help="The tool to obtain the container from. Can be the form <tool>:<version>. "
|
|
114
|
+
help="The tool to obtain the container from. Can be in the form <tool>:<version>. "
|
|
116
115
|
"The TOOL is either the name of a bundled tool (c.f. fm-weck --list) or "
|
|
117
|
-
"the path to
|
|
116
|
+
"the path to an fm-tools YAML file.",
|
|
118
117
|
type=ToolQualifier,
|
|
119
118
|
nargs=nargs,
|
|
120
119
|
).completer = ShellCompletion.tool_completer
|
|
@@ -131,6 +130,27 @@ def add_shared_args_for_run_modes(parser):
|
|
|
131
130
|
add_tool_arg(parser, nargs=None)
|
|
132
131
|
|
|
133
132
|
|
|
133
|
+
def add_shared_args_for_client(parser):
|
|
134
|
+
parser.add_argument(
|
|
135
|
+
"--host",
|
|
136
|
+
action="store",
|
|
137
|
+
dest="host",
|
|
138
|
+
type=str,
|
|
139
|
+
help=("Specifies the IP address and the port of the server."),
|
|
140
|
+
required=True,
|
|
141
|
+
default=None,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
parser.add_argument(
|
|
145
|
+
"--timelimit",
|
|
146
|
+
action="store",
|
|
147
|
+
dest="timelimit",
|
|
148
|
+
type=str,
|
|
149
|
+
help=("Specifies the maximum amount of time to wait for the server to finish a run, in seconds."),
|
|
150
|
+
default=10,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
134
154
|
def parse(raw_args: list[str]) -> Tuple[Callable[[], None], Namespace]:
|
|
135
155
|
parser = argparse.ArgumentParser(description="fm-weck")
|
|
136
156
|
|
|
@@ -192,9 +212,9 @@ def parse(raw_args: list[str]) -> Tuple[Callable[[], None], Namespace]:
|
|
|
192
212
|
"--spec",
|
|
193
213
|
action="store",
|
|
194
214
|
help=(
|
|
195
|
-
"Property
|
|
215
|
+
"Property that is forwarded to the fm-tool."
|
|
196
216
|
" Either a path to a property file or a property name from SV-COMP or Test-Comp."
|
|
197
|
-
" Use fm-weck serve --list to
|
|
217
|
+
" Use fm-weck serve --list to view all properties that can be called by name."
|
|
198
218
|
),
|
|
199
219
|
required=False,
|
|
200
220
|
default=None,
|
|
@@ -318,6 +338,98 @@ def parse(raw_args: list[str]) -> Tuple[Callable[[], None], Namespace]:
|
|
|
318
338
|
).completer = ShellCompletion.versions_completer # type: ignore[assignment]
|
|
319
339
|
versions.set_defaults(main=main_versions)
|
|
320
340
|
|
|
341
|
+
server = subparsers.add_parser("server", aliases=["s"], help="Run fm-weck remotely on a server.")
|
|
342
|
+
server.add_argument(
|
|
343
|
+
"--port",
|
|
344
|
+
action="store",
|
|
345
|
+
dest="port",
|
|
346
|
+
type=str,
|
|
347
|
+
help=("Specifies the port number on which the server will listen."),
|
|
348
|
+
required=True,
|
|
349
|
+
default=None,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
server.add_argument(
|
|
353
|
+
"--listen",
|
|
354
|
+
action="store",
|
|
355
|
+
dest="ipaddr",
|
|
356
|
+
type=str,
|
|
357
|
+
help=("Specifies the IP address on which the server will listen."),
|
|
358
|
+
required=True,
|
|
359
|
+
default=None,
|
|
360
|
+
)
|
|
361
|
+
server.set_defaults(main=main_server)
|
|
362
|
+
|
|
363
|
+
client = subparsers.add_parser("remote-run", aliases=["rr"], help="Execute tasks remotely.")
|
|
364
|
+
client.add_argument(
|
|
365
|
+
"-p",
|
|
366
|
+
"--property",
|
|
367
|
+
"--spec",
|
|
368
|
+
action="store",
|
|
369
|
+
help=(
|
|
370
|
+
"Property that is forwarded to the fm-tool."
|
|
371
|
+
" Either a path to a property file or a property name from SV-COMP or Test-Comp."
|
|
372
|
+
" Use fm-weck serve --list to view all properties that can be called by name."
|
|
373
|
+
),
|
|
374
|
+
required=True,
|
|
375
|
+
default=None,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
add_shared_args_for_client(client)
|
|
379
|
+
add_tool_arg(client, nargs=None)
|
|
380
|
+
client.add_argument("files", metavar="FILES", nargs="+", help="Files to pass to the tool.")
|
|
381
|
+
client.set_defaults(main=main_client)
|
|
382
|
+
|
|
383
|
+
client_expert = subparsers.add_parser(
|
|
384
|
+
"remote-expert", aliases=["re"], help="Execute tasks remotely in expert mode."
|
|
385
|
+
)
|
|
386
|
+
add_shared_args_for_client(client_expert)
|
|
387
|
+
add_tool_arg(client_expert, nargs=None)
|
|
388
|
+
client_expert.add_argument("argument_list", metavar="args", nargs="*", help="Arguments for the fm-tool.")
|
|
389
|
+
client_expert.set_defaults(main=main_client_expert)
|
|
390
|
+
|
|
391
|
+
client_get_run = subparsers.add_parser(
|
|
392
|
+
"get-run", aliases=["gr"], help="Get the result for a remotely executed task."
|
|
393
|
+
)
|
|
394
|
+
add_shared_args_for_client(client_get_run)
|
|
395
|
+
client_get_run.add_argument("run_id", metavar="RUN-ID", nargs=1, help="The run ID for which to get the result.")
|
|
396
|
+
client_get_run.set_defaults(main=main_client_get_run)
|
|
397
|
+
|
|
398
|
+
client_query_files = subparsers.add_parser(
|
|
399
|
+
"query-files", aliases=["qf"], help="Get the resulting files for a remotely executed task."
|
|
400
|
+
)
|
|
401
|
+
client_query_files.add_argument("run_id", metavar="RUN-ID", nargs=1, help="The run ID for which to get the result.")
|
|
402
|
+
client_query_files.add_argument("file_names", metavar="files", nargs="*", help="Files to query for.")
|
|
403
|
+
add_shared_args_for_client(client_query_files)
|
|
404
|
+
client_query_files.add_argument(
|
|
405
|
+
"--output-path",
|
|
406
|
+
action="store",
|
|
407
|
+
dest="output_path",
|
|
408
|
+
type=Path,
|
|
409
|
+
help=(
|
|
410
|
+
"Specifies the location where the incoming files from the server will be stored, "
|
|
411
|
+
"relative to the current working directory."
|
|
412
|
+
),
|
|
413
|
+
default=Path.cwd(),
|
|
414
|
+
)
|
|
415
|
+
client_query_files.set_defaults(main=main_client_query_files)
|
|
416
|
+
|
|
417
|
+
smoke_test = subparsers.add_parser("smoke-test", help="Run a smoke test on the tool.")
|
|
418
|
+
smoke_test.add_argument(
|
|
419
|
+
"TOOL",
|
|
420
|
+
help="The tool for which to run the smoke test.",
|
|
421
|
+
type=ToolQualifier,
|
|
422
|
+
nargs=1,
|
|
423
|
+
).completer = ShellCompletion.versions_completer
|
|
424
|
+
smoke_test.add_argument(
|
|
425
|
+
"--gitlab-ci-mode",
|
|
426
|
+
action="store_true",
|
|
427
|
+
help="Run in GitLab CI mode: directly install required packages with apt instead of building/pulling images.",
|
|
428
|
+
required=False,
|
|
429
|
+
default=False,
|
|
430
|
+
)
|
|
431
|
+
smoke_test.set_defaults(main=main_smoke_test)
|
|
432
|
+
|
|
321
433
|
with contextlib.suppress(ImportError):
|
|
322
434
|
import argcomplete
|
|
323
435
|
|
|
@@ -341,22 +453,6 @@ def parse(raw_args: list[str]) -> Tuple[Callable[[], None], Namespace]:
|
|
|
341
453
|
return help_callback, parser.parse_args(raw_args)
|
|
342
454
|
|
|
343
455
|
|
|
344
|
-
@cache
|
|
345
|
-
def fm_tools_choice_map():
|
|
346
|
-
ignore = {
|
|
347
|
-
"schema.yml",
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
actors = {actor_def.stem: actor_def for actor_def in iter_fm_data() if (actor_def.name not in ignore)}
|
|
351
|
-
|
|
352
|
-
return actors
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
@cache
|
|
356
|
-
def property_choice_map():
|
|
357
|
-
return {spec_path.stem: spec_path for spec_path in iter_properties() if spec_path.suffix != ".license"}
|
|
358
|
-
|
|
359
|
-
|
|
360
456
|
def list_known_tools():
|
|
361
457
|
return fm_tools_choice_map().keys()
|
|
362
458
|
|
|
@@ -380,6 +476,13 @@ def resolve_property(prop_name: str) -> Path:
|
|
|
380
476
|
return property_choice_map()[prop_name]
|
|
381
477
|
|
|
382
478
|
|
|
479
|
+
def resolve_property_for_server(prop_name: str) -> Union[Path, str]:
|
|
480
|
+
if (as_path := Path(prop_name)).exists() and as_path.is_file():
|
|
481
|
+
return as_path
|
|
482
|
+
|
|
483
|
+
return prop_name
|
|
484
|
+
|
|
485
|
+
|
|
383
486
|
def set_log_options(loglevel: Optional[str], logfile: Optional[str], config: dict[str, Any]):
|
|
384
487
|
level = "WARNING"
|
|
385
488
|
level = loglevel.upper() if loglevel else config.get("logging", {}).get("level", level)
|
|
@@ -470,12 +573,7 @@ def main_install(args: argparse.Namespace):
|
|
|
470
573
|
logger.error("Unknown tool %s. Skipping installation...", tool)
|
|
471
574
|
continue
|
|
472
575
|
|
|
473
|
-
install_fm_tool(
|
|
474
|
-
fm_tool=fm_data,
|
|
475
|
-
version=tool.version,
|
|
476
|
-
configuration=Config(),
|
|
477
|
-
install_path=args.destination
|
|
478
|
-
)
|
|
576
|
+
install_fm_tool(fm_tool=fm_data, version=tool.version, configuration=Config(), install_path=args.destination)
|
|
479
577
|
|
|
480
578
|
return 0
|
|
481
579
|
|
|
@@ -524,6 +622,98 @@ def main_versions(args: argparse.Namespace):
|
|
|
524
622
|
VersionListing(tool_paths).print_versions()
|
|
525
623
|
|
|
526
624
|
|
|
625
|
+
def main_server(args: argparse.Namespace):
|
|
626
|
+
from .grpc_service import serve
|
|
627
|
+
|
|
628
|
+
serve(ipaddr=args.ipaddr, port=args.port)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def main_client(args: argparse.Namespace):
|
|
632
|
+
from .grpc_service import client_run
|
|
633
|
+
|
|
634
|
+
tool = resolve_tool(args.TOOL)
|
|
635
|
+
property = resolve_property(args.property)
|
|
636
|
+
timelimit, _ = check_client_options(args.timelimit)
|
|
637
|
+
|
|
638
|
+
client_run((tool, args.TOOL.version), args.host, property, args.files, timelimit)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def main_client_expert(args: argparse.Namespace):
|
|
642
|
+
from .grpc_service import client_expert_run
|
|
643
|
+
|
|
644
|
+
tool = resolve_tool(args.TOOL)
|
|
645
|
+
command = args.argument_list
|
|
646
|
+
client_expert_run((tool, args.TOOL.version), args.host, command, args.timelimit)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def main_client_get_run(args: argparse.Namespace):
|
|
650
|
+
from .grpc_service import client_get_run
|
|
651
|
+
|
|
652
|
+
timelimit, _ = check_client_options(args.timelimit)
|
|
653
|
+
client_get_run(args.host, args.run_id[0], timelimit)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def main_client_query_files(args: argparse.Namespace):
|
|
657
|
+
from .grpc_service import query_files
|
|
658
|
+
|
|
659
|
+
timelimit, _ = check_client_options(args.timelimit)
|
|
660
|
+
file_names = args.file_names
|
|
661
|
+
|
|
662
|
+
query_files(args.host, args.run_id[0], file_names, timelimit, args.output_path)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def check_client_options(timelimit_arg, output_path=None):
|
|
666
|
+
try:
|
|
667
|
+
timelimit = int(timelimit_arg)
|
|
668
|
+
except ValueError:
|
|
669
|
+
logger.error("Invalid timelimit value: %s. It must be an integer.", timelimit_arg)
|
|
670
|
+
exit(1)
|
|
671
|
+
|
|
672
|
+
if output_path:
|
|
673
|
+
output_path = Path.cwd() / Path(output_path)
|
|
674
|
+
if not output_path.exists():
|
|
675
|
+
try:
|
|
676
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
677
|
+
except Exception as e:
|
|
678
|
+
logger.error("Failed to create output path %s: %s", output_path, e)
|
|
679
|
+
exit(1)
|
|
680
|
+
|
|
681
|
+
return timelimit, output_path
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def main_smoke_test(args: argparse.Namespace):
|
|
685
|
+
from .serve import setup_fm_tool
|
|
686
|
+
from .smoke_test_mode import run_smoke_test, run_smoke_test_gitlab_ci
|
|
687
|
+
|
|
688
|
+
try:
|
|
689
|
+
tool = resolve_tool(args.TOOL[0])
|
|
690
|
+
except KeyError:
|
|
691
|
+
logger.error("Unknown tool: %s", args.TOOL[0].tool)
|
|
692
|
+
return 1
|
|
693
|
+
|
|
694
|
+
fm_data, shelve_space = setup_fm_tool(
|
|
695
|
+
fm_tool=tool,
|
|
696
|
+
version=args.TOOL[0].version,
|
|
697
|
+
configuration=Config(),
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
if args.gitlab_ci_mode:
|
|
701
|
+
from subprocess import CalledProcessError
|
|
702
|
+
|
|
703
|
+
try:
|
|
704
|
+
run_smoke_test_gitlab_ci(fm_data, shelve_space)
|
|
705
|
+
except CalledProcessError as e:
|
|
706
|
+
return e.returncode
|
|
707
|
+
|
|
708
|
+
return 0
|
|
709
|
+
|
|
710
|
+
try:
|
|
711
|
+
run_smoke_test(fm_data, shelve_space, Config())
|
|
712
|
+
except ValueError as e:
|
|
713
|
+
logger.error(e)
|
|
714
|
+
return 1
|
|
715
|
+
|
|
716
|
+
|
|
527
717
|
def log_no_image_error(tool, config):
|
|
528
718
|
order = []
|
|
529
719
|
for path in _SEARCH_ORDER:
|
fm_weck/config.py
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
import importlib.resources as pkg_resources
|
|
9
9
|
import logging
|
|
10
10
|
import os
|
|
11
|
-
import shutil
|
|
12
11
|
import stat
|
|
13
12
|
import sys
|
|
14
13
|
from functools import cache
|
|
@@ -40,6 +39,8 @@ except ImportError:
|
|
|
40
39
|
|
|
41
40
|
from fm_weck.resources import RUN_WITH_OVERLAY, RUNEXEC_SCRIPT
|
|
42
41
|
|
|
42
|
+
from .file_util import copy_ensuring_unix_line_endings
|
|
43
|
+
|
|
43
44
|
try:
|
|
44
45
|
import tomllib as toml
|
|
45
46
|
except ImportError:
|
|
@@ -206,7 +207,7 @@ class Config(object):
|
|
|
206
207
|
# Try to copy from package resources
|
|
207
208
|
try:
|
|
208
209
|
with pkg_resources.path("fm_weck.resources", target_name) as source_path:
|
|
209
|
-
|
|
210
|
+
copy_ensuring_unix_line_endings(source_path, target)
|
|
210
211
|
except FileNotFoundError:
|
|
211
212
|
logging.error(f"Resource {target_name} not found in package.")
|
|
212
213
|
return None
|
|
@@ -214,7 +215,7 @@ class Config(object):
|
|
|
214
215
|
# Compare modification time if the file exists
|
|
215
216
|
with pkg_resources.path("fm_weck.resources", target_name) as source_path:
|
|
216
217
|
if source_path.stat().st_mtime > target.stat().st_mtime:
|
|
217
|
-
|
|
218
|
+
copy_ensuring_unix_line_endings(source_path, target)
|
|
218
219
|
else:
|
|
219
220
|
logging.debug(f"Using existing {target_name} script")
|
|
220
221
|
return target
|
fm_weck/engine.py
CHANGED
|
@@ -7,16 +7,23 @@
|
|
|
7
7
|
|
|
8
8
|
import io
|
|
9
9
|
import logging
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
import shlex
|
|
10
13
|
import shutil
|
|
11
14
|
import signal
|
|
12
15
|
import subprocess
|
|
13
16
|
import sys
|
|
17
|
+
import threading
|
|
14
18
|
from abc import ABC, abstractmethod
|
|
15
19
|
from functools import cached_property, singledispatchmethod
|
|
16
20
|
from pathlib import Path
|
|
17
21
|
from tempfile import mkdtemp
|
|
22
|
+
from threading import Thread
|
|
18
23
|
from typing import TYPE_CHECKING, Callable, Iterable, List, Optional, Union
|
|
19
24
|
|
|
25
|
+
from fm_weck.capture import Capture
|
|
26
|
+
from fm_weck.file_util import ensure_linux_style
|
|
20
27
|
from fm_weck.run_result import RunResult
|
|
21
28
|
|
|
22
29
|
try:
|
|
@@ -55,8 +62,11 @@ class Engine(ABC):
|
|
|
55
62
|
print_output_to_stdout: bool = True
|
|
56
63
|
add_benchexec_capabilities: bool = False
|
|
57
64
|
add_mounting_capabilities: bool = True
|
|
65
|
+
_use_overlay: bool = False
|
|
66
|
+
overlay_tool_dir: Optional[str] = None
|
|
58
67
|
image: Optional[str] = None
|
|
59
68
|
dry_run: bool = False
|
|
69
|
+
work_dir: Optional[Path] = Path(CWD_MOUNT_LOCATION)
|
|
60
70
|
|
|
61
71
|
def __init__(self, image: Union[str, FmImageConfig]):
|
|
62
72
|
self._tmp_output_dir = Path(mkdtemp("fm_weck_output")).resolve()
|
|
@@ -73,14 +83,16 @@ class Engine(ABC):
|
|
|
73
83
|
if self._tmp_output_dir.exists():
|
|
74
84
|
shutil.rmtree(self._tmp_output_dir)
|
|
75
85
|
|
|
76
|
-
def get_workdir(self):
|
|
77
|
-
return Path(CWD_MOUNT_LOCATION)
|
|
86
|
+
def get_workdir(self) -> str:
|
|
87
|
+
return Path(CWD_MOUNT_LOCATION).as_posix()
|
|
78
88
|
|
|
79
|
-
def
|
|
80
|
-
self.log_file =
|
|
89
|
+
def set_log_file(self, log_file: Path):
|
|
90
|
+
self.log_file = log_file
|
|
91
|
+
self.log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
81
92
|
|
|
82
|
-
def
|
|
83
|
-
self.output_dir =
|
|
93
|
+
def set_output_dir(self, output_dir: Path):
|
|
94
|
+
self.output_dir = output_dir
|
|
95
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
84
96
|
|
|
85
97
|
def mount(self, source: str, target: str):
|
|
86
98
|
self.extra_args["mounts"] = self.extra_args.get("mounts", []) + [
|
|
@@ -119,6 +131,10 @@ class Engine(ABC):
|
|
|
119
131
|
def add_environment(self):
|
|
120
132
|
return sum([["-e", f"{key}={value}"] for key, value in self.env.items()], [])
|
|
121
133
|
|
|
134
|
+
def use_overlay(self, overlay_dir: str):
|
|
135
|
+
self._use_overlay = True
|
|
136
|
+
self.overlay_tool_dir = overlay_dir
|
|
137
|
+
|
|
122
138
|
def setup_command(self):
|
|
123
139
|
return [
|
|
124
140
|
"--security-opt",
|
|
@@ -126,7 +142,7 @@ class Engine(ABC):
|
|
|
126
142
|
"--entrypoint",
|
|
127
143
|
'[""]',
|
|
128
144
|
"-v",
|
|
129
|
-
f"{Path.cwd().absolute()}:{
|
|
145
|
+
f"{Path.cwd().absolute()}:{self.get_workdir()}",
|
|
130
146
|
"-v",
|
|
131
147
|
f"{Config().cache_location}:{CACHE_MOUNT_LOCATION}",
|
|
132
148
|
"-v",
|
|
@@ -183,9 +199,32 @@ class Engine(ABC):
|
|
|
183
199
|
base.append(value)
|
|
184
200
|
|
|
185
201
|
_command = self._prep_command(command)
|
|
202
|
+
|
|
203
|
+
if self._use_overlay:
|
|
204
|
+
_command = (f"{CACHE_MOUNT_LOCATION}/.scripts/run_with_overlay.sh", self.overlay_tool_dir, *_command)
|
|
205
|
+
|
|
186
206
|
return base + [self.image, *_command]
|
|
187
207
|
|
|
188
|
-
def
|
|
208
|
+
def assemble_smoke_test_command(self, command: tuple[str, ...]):
|
|
209
|
+
base = self.base_command()
|
|
210
|
+
if self.add_benchexec_capabilities:
|
|
211
|
+
base += self.benchexec_capabilities()
|
|
212
|
+
|
|
213
|
+
if self.add_mounting_capabilities:
|
|
214
|
+
base += self.mounting_capabilities()
|
|
215
|
+
|
|
216
|
+
if self.interactive:
|
|
217
|
+
base += self.interactive_command()
|
|
218
|
+
|
|
219
|
+
for value in self.extra_args.values():
|
|
220
|
+
if isinstance(value, list) and not isinstance(value, str):
|
|
221
|
+
base += value
|
|
222
|
+
else:
|
|
223
|
+
base.append(value)
|
|
224
|
+
|
|
225
|
+
return base + command
|
|
226
|
+
|
|
227
|
+
def _prep_command(self, command: tuple[str, ...]) -> tuple[str, ...]:
|
|
189
228
|
"""We want to map absolute paths of the current working directory to the
|
|
190
229
|
working directory of the container."""
|
|
191
230
|
|
|
@@ -203,9 +242,9 @@ class Engine(ABC):
|
|
|
203
242
|
return p
|
|
204
243
|
mapped = _map_path(Path(p))
|
|
205
244
|
if Path(p) == mapped:
|
|
206
|
-
return p
|
|
245
|
+
return ensure_linux_style(p)
|
|
207
246
|
else:
|
|
208
|
-
return mapped
|
|
247
|
+
return Path(mapped).as_posix()
|
|
209
248
|
|
|
210
249
|
return tuple(map(_map_path, command))
|
|
211
250
|
|
|
@@ -227,12 +266,14 @@ class Engine(ABC):
|
|
|
227
266
|
|
|
228
267
|
@staticmethod
|
|
229
268
|
def _base_engine_class(config: Config):
|
|
230
|
-
engine = config.defaults().get("engine", "podman").lower()
|
|
269
|
+
engine = "docker" if platform.system() != "Linux" else config.defaults().get("engine", "podman").lower()
|
|
231
270
|
|
|
232
271
|
if engine == "docker":
|
|
233
272
|
return Docker
|
|
234
273
|
if engine == "podman":
|
|
235
274
|
return Podman
|
|
275
|
+
if engine == "runexec" or engine == "benchexec":
|
|
276
|
+
return Runexec
|
|
236
277
|
|
|
237
278
|
raise ValueError(f"Unknown engine {engine}")
|
|
238
279
|
|
|
@@ -359,9 +400,13 @@ class Engine(ABC):
|
|
|
359
400
|
logging.info("Received signal %s. Terminating container process.", signal_received)
|
|
360
401
|
process.send_signal(signal.SIGTERM)
|
|
361
402
|
|
|
403
|
+
def register_signal(signum, handler) -> None:
|
|
404
|
+
if threading.current_thread() is threading.main_thread():
|
|
405
|
+
signal.signal(signum, handler)
|
|
406
|
+
|
|
362
407
|
# Register signal handler
|
|
363
|
-
|
|
364
|
-
|
|
408
|
+
register_signal(signal.SIGINT, terminate_process_group)
|
|
409
|
+
register_signal(signal.SIGTERM, terminate_process_group)
|
|
365
410
|
|
|
366
411
|
logger.debug("\n\nRunning command:\n%s\n\n", " ".join(map(str, command)))
|
|
367
412
|
|
|
@@ -377,11 +422,14 @@ class Engine(ABC):
|
|
|
377
422
|
if self.print_output_to_stdout:
|
|
378
423
|
sys.stdout.write(line)
|
|
379
424
|
|
|
425
|
+
file_handle = None
|
|
380
426
|
if self.log_file is None:
|
|
381
|
-
run_and_poll(full_stdout.write)
|
|
427
|
+
polling_thread = Thread(target=run_and_poll, args=(full_stdout.write,))
|
|
382
428
|
else:
|
|
383
|
-
|
|
384
|
-
|
|
429
|
+
file_handle = self.log_file.open("w")
|
|
430
|
+
polling_thread = Thread(target=run_and_poll, args=(file_handle.write,))
|
|
431
|
+
|
|
432
|
+
polling_thread.start()
|
|
385
433
|
|
|
386
434
|
assert process is not None, "Process should be defined at this point."
|
|
387
435
|
try:
|
|
@@ -390,6 +438,10 @@ class Engine(ABC):
|
|
|
390
438
|
process.terminate()
|
|
391
439
|
process.wait()
|
|
392
440
|
|
|
441
|
+
polling_thread.join()
|
|
442
|
+
if file_handle is not None:
|
|
443
|
+
file_handle.close()
|
|
444
|
+
|
|
393
445
|
if self.log_file is not None:
|
|
394
446
|
with self.log_file.open("r") as log:
|
|
395
447
|
return RunResult(command, process.returncode, log.read())
|
|
@@ -404,7 +456,7 @@ class Engine(ABC):
|
|
|
404
456
|
logger.debug("Running: %s", command)
|
|
405
457
|
if self.dry_run:
|
|
406
458
|
print("Command to be executed:")
|
|
407
|
-
print(
|
|
459
|
+
print(shlex.join(command))
|
|
408
460
|
return RunResult(command, 0, "Dry run: no command executed.")
|
|
409
461
|
|
|
410
462
|
if self.interactive or not self.handle_io:
|
|
@@ -505,3 +557,84 @@ class Docker(Engine):
|
|
|
505
557
|
"label=disable",
|
|
506
558
|
"-v /sys/fs/cgroup:/sys/fs/cgroup",
|
|
507
559
|
]
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
class Runexec(Engine):
|
|
563
|
+
def __init__(self, image: Union[str, FmImageConfig]):
|
|
564
|
+
super().__init__("unused")
|
|
565
|
+
self._engine = "runexec"
|
|
566
|
+
|
|
567
|
+
def image_from(self, containerfile: Path):
|
|
568
|
+
logging.info("Engine 'runexec' does not support building images. Continuing without image.")
|
|
569
|
+
raise NotImplementedError("runexec does not support building images.")
|
|
570
|
+
|
|
571
|
+
def benchexec_capabilities(self):
|
|
572
|
+
return []
|
|
573
|
+
|
|
574
|
+
def base_command(self):
|
|
575
|
+
return ["runexec"]
|
|
576
|
+
|
|
577
|
+
def setup_command(self):
|
|
578
|
+
return []
|
|
579
|
+
|
|
580
|
+
def _get_dir_modes(self):
|
|
581
|
+
from benchexec.container import DIR_HIDDEN, DIR_OVERLAY, DIR_READ_ONLY
|
|
582
|
+
|
|
583
|
+
dir_modes = {
|
|
584
|
+
"/": DIR_READ_ONLY,
|
|
585
|
+
"/home": DIR_HIDDEN,
|
|
586
|
+
os.getcwd(): DIR_OVERLAY,
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if not Config().cache_location.resolve().is_relative_to(Path.cwd().resolve()):
|
|
590
|
+
dir_modes[str(Config().cache_location.resolve())] = DIR_OVERLAY
|
|
591
|
+
|
|
592
|
+
return dir_modes
|
|
593
|
+
|
|
594
|
+
def run(self, *command, timeout_sec: Optional[float] = None) -> RunResult:
|
|
595
|
+
import threading
|
|
596
|
+
from tempfile import TemporaryFile
|
|
597
|
+
|
|
598
|
+
from benchexec.containerexecutor import ContainerExecutor
|
|
599
|
+
|
|
600
|
+
executor = ContainerExecutor(
|
|
601
|
+
network_access=True, container_system_config=False, cgroup_access=True, dir_modes=self._get_dir_modes()
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
def signal_handler_kill(signum, frame):
|
|
605
|
+
executor.stop()
|
|
606
|
+
|
|
607
|
+
signal.signal(signal.SIGTERM, signal_handler_kill)
|
|
608
|
+
signal.signal(signal.SIGQUIT, signal_handler_kill)
|
|
609
|
+
signal.signal(signal.SIGINT, signal_handler_kill)
|
|
610
|
+
|
|
611
|
+
working_dir = Config().cache_location / self.overlay_tool_dir if self.overlay_tool_dir else Path.cwd()
|
|
612
|
+
|
|
613
|
+
log_creator = TemporaryFile
|
|
614
|
+
if self.log_file:
|
|
615
|
+
log_creator = self.log_file.open
|
|
616
|
+
|
|
617
|
+
log_str = ""
|
|
618
|
+
|
|
619
|
+
def stop_executor_after_timeout():
|
|
620
|
+
# If timeout is None, this waits indefinitely
|
|
621
|
+
threading.Event().wait(timeout_sec)
|
|
622
|
+
executor.stop()
|
|
623
|
+
|
|
624
|
+
eventual_stopping_thread = threading.Thread(target=stop_executor_after_timeout, daemon=True)
|
|
625
|
+
|
|
626
|
+
with log_creator("w+") as log, Capture(log), Capture(log, "stderr"):
|
|
627
|
+
eventual_stopping_thread.start()
|
|
628
|
+
exit_code = executor.execute_run(
|
|
629
|
+
args=command,
|
|
630
|
+
output_dir=self.output_dir,
|
|
631
|
+
workingDir=str(working_dir.absolute()),
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
log.seek(0)
|
|
635
|
+
log_str = log.read()
|
|
636
|
+
|
|
637
|
+
if self.print_output_to_stdout:
|
|
638
|
+
print(log_str)
|
|
639
|
+
|
|
640
|
+
return RunResult(command, exit_code.raw, log_str)
|