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.
- fm_weck/__init__.py +1 -1
- fm_weck/capture.py +31 -0
- fm_weck/cli.py +217 -22
- fm_weck/engine.py +144 -12
- fm_weck/exceptions.py +58 -0
- fm_weck/file_util.py +5 -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/run_result.py +1 -1
- fm_weck/serve.py +21 -14
- fm_weck/smoke_test_mode.py +82 -0
- {fm_weck-1.4.8.dist-info → fm_weck-1.5.0.dist-info}/METADATA +3 -1
- {fm_weck-1.4.8.dist-info → fm_weck-1.5.0.dist-info}/RECORD +29 -14
- {fm_weck-1.4.8.dist-info → fm_weck-1.5.0.dist-info}/WHEEL +0 -0
- {fm_weck-1.4.8.dist-info → fm_weck-1.5.0.dist-info}/entry_points.txt +0 -0
|
@@ -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
|