fm-weck 1.4.8__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.
@@ -0,0 +1,331 @@
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
+ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
9
+ """Client and server classes corresponding to protobuf-defined services."""
10
+ import grpc
11
+ import warnings
12
+
13
+ from . import fm_weck_service_pb2 as fm__weck__service__pb2
14
+
15
+ GRPC_GENERATED_VERSION = '1.71.0'
16
+ GRPC_VERSION = grpc.__version__
17
+ _version_not_supported = False
18
+
19
+ try:
20
+ from grpc._utilities import first_version_is_lower
21
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
22
+ except ImportError:
23
+ _version_not_supported = True
24
+
25
+ if _version_not_supported:
26
+ raise RuntimeError(
27
+ f'The grpc package installed is at version {GRPC_VERSION},'
28
+ + f' but the generated code in fm_weck_service_pb2_grpc.py depends on'
29
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
30
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
31
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
32
+ )
33
+
34
+
35
+ class FmWeckRemoteStub(object):
36
+ """This service runs fm-weck remotely.
37
+
38
+ """
39
+
40
+ def __init__(self, channel):
41
+ """Constructor.
42
+
43
+ Args:
44
+ channel: A grpc.Channel.
45
+ """
46
+ self.startRun = channel.unary_unary(
47
+ '/FmWeckRemote/startRun',
48
+ request_serializer=fm__weck__service__pb2.RunRequest.SerializeToString,
49
+ response_deserializer=fm__weck__service__pb2.RunID.FromString,
50
+ _registered_method=True)
51
+ self.startExpertRun = channel.unary_unary(
52
+ '/FmWeckRemote/startExpertRun',
53
+ request_serializer=fm__weck__service__pb2.ExpertRunRequest.SerializeToString,
54
+ response_deserializer=fm__weck__service__pb2.RunID.FromString,
55
+ _registered_method=True)
56
+ self.cancelRun = channel.unary_unary(
57
+ '/FmWeckRemote/cancelRun',
58
+ request_serializer=fm__weck__service__pb2.CancelRunRequest.SerializeToString,
59
+ response_deserializer=fm__weck__service__pb2.CancelRunResult.FromString,
60
+ _registered_method=True)
61
+ self.cleanupRun = channel.unary_unary(
62
+ '/FmWeckRemote/cleanupRun',
63
+ request_serializer=fm__weck__service__pb2.RunID.SerializeToString,
64
+ response_deserializer=fm__weck__service__pb2.CleanUpResponse.FromString,
65
+ _registered_method=True)
66
+ self.waitOnRun = channel.unary_unary(
67
+ '/FmWeckRemote/waitOnRun',
68
+ request_serializer=fm__weck__service__pb2.WaitParameters.SerializeToString,
69
+ response_deserializer=fm__weck__service__pb2.WaitRunResult.FromString,
70
+ _registered_method=True)
71
+ self.queryFiles = channel.unary_stream(
72
+ '/FmWeckRemote/queryFiles',
73
+ request_serializer=fm__weck__service__pb2.FileQuery.SerializeToString,
74
+ response_deserializer=fm__weck__service__pb2.File.FromString,
75
+ _registered_method=True)
76
+
77
+
78
+ class FmWeckRemoteServicer(object):
79
+ """This service runs fm-weck remotely.
80
+
81
+ """
82
+
83
+ def startRun(self, request, context):
84
+ """Runs a verification task for a given C program.
85
+ """
86
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
87
+ context.set_details('Method not implemented!')
88
+ raise NotImplementedError('Method not implemented!')
89
+
90
+ def startExpertRun(self, request, context):
91
+ """Runs a tool in expert mode
92
+ """
93
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
94
+ context.set_details('Method not implemented!')
95
+ raise NotImplementedError('Method not implemented!')
96
+
97
+ def cancelRun(self, request, context):
98
+ """Cancels a previously started run.
99
+ """
100
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
101
+ context.set_details('Method not implemented!')
102
+ raise NotImplementedError('Method not implemented!')
103
+
104
+ def cleanupRun(self, request, context):
105
+ """Cleans up files of a finished run.
106
+ """
107
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
108
+ context.set_details('Method not implemented!')
109
+ raise NotImplementedError('Method not implemented!')
110
+
111
+ def waitOnRun(self, request, context):
112
+ """Gets the result of a previously started run using its unique ID.
113
+ """
114
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
115
+ context.set_details('Method not implemented!')
116
+ raise NotImplementedError('Method not implemented!')
117
+
118
+ def queryFiles(self, request, context):
119
+ """Query for a number of result files.
120
+ """
121
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
122
+ context.set_details('Method not implemented!')
123
+ raise NotImplementedError('Method not implemented!')
124
+
125
+
126
+ def add_FmWeckRemoteServicer_to_server(servicer, server):
127
+ rpc_method_handlers = {
128
+ 'startRun': grpc.unary_unary_rpc_method_handler(
129
+ servicer.startRun,
130
+ request_deserializer=fm__weck__service__pb2.RunRequest.FromString,
131
+ response_serializer=fm__weck__service__pb2.RunID.SerializeToString,
132
+ ),
133
+ 'startExpertRun': grpc.unary_unary_rpc_method_handler(
134
+ servicer.startExpertRun,
135
+ request_deserializer=fm__weck__service__pb2.ExpertRunRequest.FromString,
136
+ response_serializer=fm__weck__service__pb2.RunID.SerializeToString,
137
+ ),
138
+ 'cancelRun': grpc.unary_unary_rpc_method_handler(
139
+ servicer.cancelRun,
140
+ request_deserializer=fm__weck__service__pb2.CancelRunRequest.FromString,
141
+ response_serializer=fm__weck__service__pb2.CancelRunResult.SerializeToString,
142
+ ),
143
+ 'cleanupRun': grpc.unary_unary_rpc_method_handler(
144
+ servicer.cleanupRun,
145
+ request_deserializer=fm__weck__service__pb2.RunID.FromString,
146
+ response_serializer=fm__weck__service__pb2.CleanUpResponse.SerializeToString,
147
+ ),
148
+ 'waitOnRun': grpc.unary_unary_rpc_method_handler(
149
+ servicer.waitOnRun,
150
+ request_deserializer=fm__weck__service__pb2.WaitParameters.FromString,
151
+ response_serializer=fm__weck__service__pb2.WaitRunResult.SerializeToString,
152
+ ),
153
+ 'queryFiles': grpc.unary_stream_rpc_method_handler(
154
+ servicer.queryFiles,
155
+ request_deserializer=fm__weck__service__pb2.FileQuery.FromString,
156
+ response_serializer=fm__weck__service__pb2.File.SerializeToString,
157
+ ),
158
+ }
159
+ generic_handler = grpc.method_handlers_generic_handler(
160
+ 'FmWeckRemote', rpc_method_handlers)
161
+ server.add_generic_rpc_handlers((generic_handler,))
162
+ server.add_registered_method_handlers('FmWeckRemote', rpc_method_handlers)
163
+
164
+
165
+ # This class is part of an EXPERIMENTAL API.
166
+ class FmWeckRemote(object):
167
+ """This service runs fm-weck remotely.
168
+
169
+ """
170
+
171
+ @staticmethod
172
+ def startRun(request,
173
+ target,
174
+ options=(),
175
+ channel_credentials=None,
176
+ call_credentials=None,
177
+ insecure=False,
178
+ compression=None,
179
+ wait_for_ready=None,
180
+ timeout=None,
181
+ metadata=None):
182
+ return grpc.experimental.unary_unary(
183
+ request,
184
+ target,
185
+ '/FmWeckRemote/startRun',
186
+ fm__weck__service__pb2.RunRequest.SerializeToString,
187
+ fm__weck__service__pb2.RunID.FromString,
188
+ options,
189
+ channel_credentials,
190
+ insecure,
191
+ call_credentials,
192
+ compression,
193
+ wait_for_ready,
194
+ timeout,
195
+ metadata,
196
+ _registered_method=True)
197
+
198
+ @staticmethod
199
+ def startExpertRun(request,
200
+ target,
201
+ options=(),
202
+ channel_credentials=None,
203
+ call_credentials=None,
204
+ insecure=False,
205
+ compression=None,
206
+ wait_for_ready=None,
207
+ timeout=None,
208
+ metadata=None):
209
+ return grpc.experimental.unary_unary(
210
+ request,
211
+ target,
212
+ '/FmWeckRemote/startExpertRun',
213
+ fm__weck__service__pb2.ExpertRunRequest.SerializeToString,
214
+ fm__weck__service__pb2.RunID.FromString,
215
+ options,
216
+ channel_credentials,
217
+ insecure,
218
+ call_credentials,
219
+ compression,
220
+ wait_for_ready,
221
+ timeout,
222
+ metadata,
223
+ _registered_method=True)
224
+
225
+ @staticmethod
226
+ def cancelRun(request,
227
+ target,
228
+ options=(),
229
+ channel_credentials=None,
230
+ call_credentials=None,
231
+ insecure=False,
232
+ compression=None,
233
+ wait_for_ready=None,
234
+ timeout=None,
235
+ metadata=None):
236
+ return grpc.experimental.unary_unary(
237
+ request,
238
+ target,
239
+ '/FmWeckRemote/cancelRun',
240
+ fm__weck__service__pb2.CancelRunRequest.SerializeToString,
241
+ fm__weck__service__pb2.CancelRunResult.FromString,
242
+ options,
243
+ channel_credentials,
244
+ insecure,
245
+ call_credentials,
246
+ compression,
247
+ wait_for_ready,
248
+ timeout,
249
+ metadata,
250
+ _registered_method=True)
251
+
252
+ @staticmethod
253
+ def cleanupRun(request,
254
+ target,
255
+ options=(),
256
+ channel_credentials=None,
257
+ call_credentials=None,
258
+ insecure=False,
259
+ compression=None,
260
+ wait_for_ready=None,
261
+ timeout=None,
262
+ metadata=None):
263
+ return grpc.experimental.unary_unary(
264
+ request,
265
+ target,
266
+ '/FmWeckRemote/cleanupRun',
267
+ fm__weck__service__pb2.RunID.SerializeToString,
268
+ fm__weck__service__pb2.CleanUpResponse.FromString,
269
+ options,
270
+ channel_credentials,
271
+ insecure,
272
+ call_credentials,
273
+ compression,
274
+ wait_for_ready,
275
+ timeout,
276
+ metadata,
277
+ _registered_method=True)
278
+
279
+ @staticmethod
280
+ def waitOnRun(request,
281
+ target,
282
+ options=(),
283
+ channel_credentials=None,
284
+ call_credentials=None,
285
+ insecure=False,
286
+ compression=None,
287
+ wait_for_ready=None,
288
+ timeout=None,
289
+ metadata=None):
290
+ return grpc.experimental.unary_unary(
291
+ request,
292
+ target,
293
+ '/FmWeckRemote/waitOnRun',
294
+ fm__weck__service__pb2.WaitParameters.SerializeToString,
295
+ fm__weck__service__pb2.WaitRunResult.FromString,
296
+ options,
297
+ channel_credentials,
298
+ insecure,
299
+ call_credentials,
300
+ compression,
301
+ wait_for_ready,
302
+ timeout,
303
+ metadata,
304
+ _registered_method=True)
305
+
306
+ @staticmethod
307
+ def queryFiles(request,
308
+ target,
309
+ options=(),
310
+ channel_credentials=None,
311
+ call_credentials=None,
312
+ insecure=False,
313
+ compression=None,
314
+ wait_for_ready=None,
315
+ timeout=None,
316
+ metadata=None):
317
+ return grpc.experimental.unary_stream(
318
+ request,
319
+ target,
320
+ '/FmWeckRemote/queryFiles',
321
+ fm__weck__service__pb2.FileQuery.SerializeToString,
322
+ fm__weck__service__pb2.File.FromString,
323
+ options,
324
+ channel_credentials,
325
+ insecure,
326
+ call_credentials,
327
+ compression,
328
+ wait_for_ready,
329
+ timeout,
330
+ metadata,
331
+ _registered_method=True)
@@ -0,0 +1,18 @@
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
+ #!/bin/bash
9
+
10
+ script_dir="$(dirname "$(realpath "$0")")"
11
+
12
+ cd "$script_dir" || exit
13
+
14
+ python -m grpc_tools.protoc -I./ --python_out=./ --pyi_out=./ --grpc_python_out=./ ./fm_weck_service.proto
15
+
16
+ sed -i 's/^import fm_weck_service_pb2 as fm__weck__service__pb2/from . import fm_weck_service_pb2 as fm__weck__service__pb2/' fm_weck_service_pb2_grpc.py
17
+
18
+ reuse annotate -y 2024 -l Apache-2.0 -c "Dirk Beyer <https://www.sosy-lab.org>" --template header --skip-existing --skip-unrecognised ./fm_weck_service_pb2_grpc.py ./fm_weck_service_pb2.pyi ./fm_weck_service_pb2.py
@@ -0,0 +1,332 @@
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 contextlib
9
+ import multiprocessing
10
+ import multiprocessing.synchronize
11
+ import os
12
+ import threading
13
+ import uuid
14
+ from pathlib import Path
15
+ from shutil import rmtree
16
+ from typing import TYPE_CHECKING, Generator, Optional, Tuple, Union
17
+
18
+ from fm_tools.benchexec_helper import DataModel
19
+
20
+ from fm_weck.config import Config
21
+ from fm_weck.exceptions import Failure, RunFailedError, failure_from_exception
22
+ from fm_weck.resources import fm_tools_choice_map, property_choice_map
23
+ from fm_weck.serve import run_guided, run_manual
24
+
25
+ from .server_utils import TMP_DIR
26
+
27
+ if TYPE_CHECKING:
28
+ from fm_weck.grpc_service.proto.fm_weck_service_pb2 import File, RunRequest
29
+ from fm_weck.run_result import RunResult
30
+
31
+
32
+ def _get_unique_id() -> str:
33
+ return str(uuid.uuid4())
34
+
35
+
36
+ class StillRunningError(Exception):
37
+ pass
38
+
39
+
40
+ def worker(setup_complete_flag, queue: multiprocessing.SimpleQueue, initializer, initargs, target, args, kwargs):
41
+ import signal
42
+ import sys
43
+
44
+ # In the rare case that a cancel happens before the target function overrides the
45
+ # signal handler, we need to make sure that the process still sends something over the queue and exits.
46
+ def handle_extremely_fast_sigterm(signum, frame):
47
+ queue.put((False, InterruptedError("Run was canceled before setup complete.")))
48
+ sys.exit(1)
49
+
50
+ signal.signal(signal.SIGTERM, handle_extremely_fast_sigterm)
51
+ setup_complete_flag.set()
52
+
53
+ try:
54
+ if initializer:
55
+ initializer(*initargs)
56
+ result = target(*args, **kwargs)
57
+ if hasattr(result, "exit_code") and getattr(result, "exit_code", 0) != 0:
58
+ raise RunFailedError(result.exit_code, result.command, getattr(result, "raw_output", None))
59
+ queue.put((True, result))
60
+ except Exception as e:
61
+ queue.put((False, failure_from_exception(e)))
62
+
63
+
64
+ class RunHandler:
65
+ mp = multiprocessing.get_context("spawn")
66
+ _success: bool = False
67
+ _result: Optional[Union["RunResult", Failure]] = None
68
+ _setup_complete: Optional[multiprocessing.synchronize.Event] = None
69
+
70
+ def __init__(self, request: "RunRequest"):
71
+ self.request = request
72
+ self.run_id = _get_unique_id()
73
+ self.run_path = TMP_DIR / self.run_id
74
+
75
+ self._is_cancelled: bool = False
76
+
77
+ self._output_log = self.run_path / "output" / "output.txt"
78
+ self._output_dir = self.run_path / "output"
79
+
80
+ self.run_path.mkdir(parents=True, exist_ok=False)
81
+
82
+ self._process = None
83
+ self._done = threading.Event()
84
+ self._queue = self.mp.SimpleQueue()
85
+
86
+ self._result_listener: threading.Thread = None
87
+
88
+ def _set(self, result=Tuple[bool, object]):
89
+ self._success, self._result = result
90
+ if isinstance(self._result, Failure):
91
+ self._success = False
92
+ self._done.set()
93
+
94
+ def ready(self) -> bool:
95
+ return self._done.is_set()
96
+
97
+ @property
98
+ def output(self) -> str:
99
+ if self.is_running():
100
+ try:
101
+ with self._output_log.open("r") as output_file:
102
+ return output_file.read()
103
+ except (FileNotFoundError, PermissionError):
104
+ return ""
105
+
106
+ if self.ready() and self.successful():
107
+ return self._result.raw_output
108
+
109
+ def successful(self) -> bool:
110
+ """
111
+ Returns True if the run was successful, False otherwise.
112
+
113
+ :raises StillRunningError: If the run is still running.
114
+ :raises ValueError: If the run has not been started yet.
115
+ """
116
+ if self.ready():
117
+ return self._success
118
+
119
+ if self._process is None:
120
+ raise ValueError("Run has not been started yet.")
121
+
122
+ raise StillRunningError("The run is still running.")
123
+
124
+ @property
125
+ def output_files(self) -> Generator[str, None, None]:
126
+ """
127
+ Names of the files produced by the run.
128
+ """
129
+ for root, _, files in os.walk(self._output_dir):
130
+ for file in files:
131
+ yield os.path.relpath(os.path.join(root, file), self._output_dir)
132
+
133
+ def is_running(self):
134
+ """
135
+ A run handler is running, if it has been started and is not finished yet.
136
+ """
137
+ return self._process and (not self.ready())
138
+
139
+ def is_canceled(self):
140
+ """
141
+ Returns True if the run was really canceled, i.e. the underlying process terminated, False otherwise.
142
+ Since cancelling a finished run has no effect, calling `cancel_run` will not necessarily
143
+ result in is_canceled() returning True.
144
+ """
145
+
146
+ return self._is_cancelled
147
+
148
+ def _apply(self, func, args, kwds):
149
+ self._setup_complete = self.mp.Event()
150
+ self._process = self.mp.Process(
151
+ target=worker,
152
+ args=(self._setup_complete, self._queue, os.chdir, (str(self.run_path.absolute()),), func, args, kwds),
153
+ daemon=True,
154
+ )
155
+ self._result_listener = threading.Thread(target=self._listen_for_result, daemon=True)
156
+ self._result_listener.start()
157
+ self._process.start()
158
+
159
+ def _listen_for_result(self):
160
+ result = self._queue.get()
161
+ self._process.join()
162
+ self._process.close()
163
+ self._queue.close()
164
+ # Make sure resources are released before
165
+ # setting the result.
166
+ self._set(result)
167
+
168
+ def start(self):
169
+ c_program = self.get_c_program(self.request)
170
+ data_model = self.request.data_model
171
+
172
+ fm_data = self.get_tool(self.request)
173
+ property_path = self.get_property(self.request)
174
+
175
+ tool_version = None
176
+ if self.request.tool.HasField("tool_version"):
177
+ tool_version = self.request.tool.tool_version
178
+
179
+ config = Config()
180
+ config.load()
181
+
182
+ self._apply(
183
+ func=run_guided,
184
+ args=(
185
+ fm_data.absolute(),
186
+ tool_version,
187
+ config,
188
+ property_path.absolute(),
189
+ [c_program],
190
+ ),
191
+ kwds=dict(
192
+ additional_args=[],
193
+ data_model=DataModel[data_model],
194
+ log_output_to=self._output_log.absolute(),
195
+ output_files_to=self._output_dir.absolute(),
196
+ ),
197
+ )
198
+
199
+ def start_expert(self, command: str):
200
+ fm_data = self.get_tool(self.request)
201
+ tool_version = None
202
+ if self.request.tool.HasField("tool_version"):
203
+ tool_version = self.request.tool.tool_version
204
+ command = list(self.request.command)
205
+
206
+ config = Config()
207
+ config.load()
208
+
209
+ self._apply(
210
+ func=run_manual,
211
+ args=(
212
+ fm_data.absolute(),
213
+ tool_version,
214
+ config,
215
+ command,
216
+ ),
217
+ kwds=dict(
218
+ log_output_to=self._output_log.absolute(),
219
+ output_files_to=self._output_dir.absolute(),
220
+ ),
221
+ )
222
+
223
+ def join(self, timeout=None):
224
+ done = self._done.wait(timeout)
225
+ if not done:
226
+ raise TimeoutError(f"Timeout while joining run {self.run_id}.")
227
+
228
+ def cleanup(self):
229
+ if self.is_running():
230
+ raise StillRunningError("The run is still running.")
231
+
232
+ rmtree(self.run_path, ignore_errors=True)
233
+
234
+ def get_tool(self, request: "RunRequest") -> Path:
235
+ tool = request.tool
236
+
237
+ if tool.HasField("tool_id"):
238
+ return fm_tools_choice_map()[tool.tool_id]
239
+ else:
240
+ return self.get_custom_tool(tool.tool_file)
241
+
242
+ def get_custom_tool(self, data: "File") -> Path:
243
+ custom_tool_path = self.run_path / "_custom_tool.yml"
244
+
245
+ with custom_tool_path.open("wb") as custom_tool_file:
246
+ custom_tool_file.write(data.file)
247
+ return custom_tool_path
248
+
249
+ def get_property(self, request: "RunRequest") -> Path:
250
+ property = request.property
251
+
252
+ if property.HasField("property_id"):
253
+ return property_choice_map()[property.property_id]
254
+ else:
255
+ return self.get_custom_property(property.property_file)
256
+
257
+ def get_custom_property(self, property_file: "File") -> Path:
258
+ custom_property_path = self.run_path / "_custom_property.prp"
259
+
260
+ with custom_property_path.open("wb") as custom_property_file:
261
+ custom_property_file.write(property_file.file)
262
+ return custom_property_path
263
+
264
+ def get_c_program(self, request) -> Path:
265
+ c_program = "_c_program.c"
266
+ c_program_path = self.run_path / c_program
267
+ c_program_path.parent.mkdir(parents=True, exist_ok=True)
268
+ with open(c_program_path, "wb") as c_file:
269
+ c_file.write(request.c_program)
270
+ return c_program
271
+
272
+ def cancel_run(self):
273
+ if self.ready():
274
+ # Canceling a run that is already finished has no effect.
275
+ return
276
+
277
+ if self._setup_complete is None:
278
+ raise RuntimeError("Run has not been started yet.")
279
+
280
+ self._setup_complete.wait()
281
+
282
+ if self._process.is_alive():
283
+ self._process.terminate()
284
+ self._is_cancelled = True
285
+
286
+ def get_file(self, file_name: str) -> Path:
287
+ """
288
+ Finds the file with the given name in the output directory of the run.
289
+ :param file_name: The name of the file.
290
+ :return: The path to the file.
291
+ :raises FileNotFoundError: If the file does not exist.
292
+ """
293
+
294
+ file_path = self._output_dir / file_name
295
+ if not file_path.exists():
296
+ raise FileNotFoundError(f"File {file_name} not found.")
297
+ return file_path
298
+
299
+ def glob(self, name_pattern: str) -> Generator[Path, None, None]:
300
+ """
301
+ Finds all files in the output directory of the run that match the given pattern.
302
+ :param name_pattern: The pattern to match.
303
+ :return: The paths to the files.
304
+ """
305
+
306
+ return self._output_dir.glob(name_pattern)
307
+
308
+ def close(self):
309
+ """
310
+ Close the process and the result listener.
311
+ It is a StillRunningError to call this method if the process is still running.
312
+ """
313
+
314
+ if self._process is None or self.ready():
315
+ return
316
+
317
+ if self._process.is_alive():
318
+ raise StillRunningError("Process is still running.")
319
+
320
+ # Should normally not occur, as this would mean, that the process is not alive,
321
+ # but the result listener is still running.
322
+ with contextlib.suppress(ValueError):
323
+ self._process.close()
324
+
325
+ def failed(self) -> bool:
326
+ return self.ready() and not self._success
327
+
328
+ def failure(self):
329
+ """Return the error object/text when failed, else None."""
330
+ if self.failed():
331
+ return self._result
332
+ return None