1
1
"""Match class, provides functionality for setting up and executing battles between given teams."""
2
+ from pathlib import Path
2
3
import subprocess
3
4
import os
4
5
9
10
import algobattle .sighandler as sigh
10
11
from algobattle .team import Team
11
12
from algobattle .problem import Problem
12
- from algobattle .util import run_subprocess , update_nested_dict
13
+ from algobattle .util import build_image , run_subprocess , update_nested_dict
13
14
from algobattle .subject import Subject
14
15
from algobattle .observer import Observer
15
16
from algobattle .battle_wrappers .averaged import Averaged
@@ -25,9 +26,16 @@ class Match(Subject):
25
26
generating_team = None
26
27
solving_team = None
27
28
battle_wrapper = None
29
+ creationflags = 0
28
30
29
31
def __init__ (self , problem : Problem , config_path : str , teams : list ,
30
- runtime_overhead = 0 , approximation_ratio = 1.0 , cache_docker_containers = True ) -> None :
32
+ runtime_overhead = 0 , approximation_ratio = 1.0 ,
33
+ cache_docker_containers = True , unsafe_build : bool = False ) -> None :
34
+
35
+ if os .name != 'posix' :
36
+ self .creationflags = subprocess .CREATE_NEW_PROCESS_GROUP
37
+ else :
38
+ self .creationflags = 0
31
39
32
40
config = configparser .ConfigParser ()
33
41
logger .debug ('Using additional configuration options from file "%s".' , config_path )
@@ -43,7 +51,7 @@ def __init__(self, problem: Problem, config_path: str, teams: list,
43
51
self .config = config
44
52
self .approximation_ratio = approximation_ratio
45
53
46
- self .build_successful = self ._build (teams , cache_docker_containers )
54
+ self .build_successful = self ._build (teams , cache_docker_containers , unsafe_build )
47
55
48
56
if approximation_ratio != 1.0 and not problem .approximable :
49
57
logger .error ('The given problem is not approximable and can only be run with an approximation ratio of 1.0!' )
@@ -92,11 +100,8 @@ def wrapper(self, *args, **kwargs):
92
100
def docker_running (function : Callable ) -> Callable :
93
101
"""Ensure that internal methods are only callable if docker is running."""
94
102
def wrapper (self , * args , ** kwargs ):
95
- creationflags = 0
96
- if os .name != 'posix' :
97
- creationflags = subprocess .CREATE_NEW_PROCESS_GROUP
98
103
docker_running = subprocess .Popen (['docker' , 'info' ], stdout = subprocess .PIPE ,
99
- stderr = subprocess .PIPE , creationflags = creationflags )
104
+ stderr = subprocess .PIPE , creationflags = self . creationflags )
100
105
_ = docker_running .communicate ()
101
106
if docker_running .returncode :
102
107
logger .error ('Could not connect to the docker daemon. Is docker running?' )
@@ -132,7 +137,7 @@ def update_match_data(self, new_data: dict) -> bool:
132
137
return True
133
138
134
139
@docker_running
135
- def _build (self , teams : list , cache_docker_containers = True ) -> bool :
140
+ def _build (self , teams : list , cache_docker_containers = True , unsafe_build : bool = False ) -> bool :
136
141
"""Build docker containers for the given generators and solvers of each team.
137
142
138
143
Any team for which either the generator or solver does not build successfully
@@ -144,6 +149,8 @@ def _build(self, teams: list, cache_docker_containers=True) -> bool:
144
149
List of Team objects.
145
150
cache_docker_containers : bool
146
151
Flag indicating whether to cache built docker containers.
152
+ unsafe_build: bool
153
+ If set, images are not removed after building, risking exposure to build process of other teams.
147
154
148
155
Returns
149
156
-------
@@ -169,36 +176,31 @@ def _build(self, teams: list, cache_docker_containers=True) -> bool:
169
176
170
177
self .single_player = (len (teams ) == 1 )
171
178
179
+ image_archives : list = []
172
180
for team in teams :
173
181
build_commands = []
174
- build_commands .append (base_build_command + [ "solver-" + str (team .name ), team .solver_path ] )
175
- build_commands .append (base_build_command + [ "generator-" + str (team .name ), team .generator_path ] )
182
+ build_commands .append (( "solver-" + str (team .name ), team .solver_path ) )
183
+ build_commands .append (( "generator-" + str (team .name ), team .generator_path ) )
176
184
177
185
build_successful = True
178
- for command in build_commands :
179
- logger .debug ('Building docker container with the following command: {}' .format (command ))
180
- creationflags = 0
181
- if os .name != 'posix' :
182
- creationflags = subprocess .CREATE_NEW_PROCESS_GROUP
183
- with subprocess .Popen (command , stdout = subprocess .PIPE ,
184
- stderr = subprocess .PIPE , creationflags = creationflags ) as process :
185
- try :
186
- output , _ = process .communicate (timeout = self .timeout_build )
187
- logger .debug (output .decode (errors = "ignore" ))
188
- except subprocess .TimeoutExpired :
189
- process .kill ()
190
- process .wait ()
191
- logger .error ('Build process for {} ran into a timeout!' .format (command [5 ]))
192
- build_successful = False
193
- if process .returncode != 0 :
194
- process .kill ()
195
- process .wait ()
196
- logger .error ('Build process for {} failed!' .format (command [5 ]))
197
- build_successful = False
198
- if not build_successful :
199
- logger .error ("Removing team {} as their containers did not build successfully." .format (team .name ))
200
- self .team_names .remove (team .name )
201
-
186
+ for name , path in build_commands :
187
+ logger .debug (f"Building docker container with the following command: { base_build_command } { name } { path } " )
188
+ build_successful = build_image (base_build_command , name , path , timeout = self .timeout_build )
189
+ if not build_successful :
190
+ logger .error ("Removing team {} as their containers did not build successfully." .format (team .name ))
191
+ self .team_names .remove (team .name )
192
+ if name .startswith ("generator" ) and not unsafe_build :
193
+ image_archives .pop ().unlink ()
194
+ break
195
+ elif not unsafe_build :
196
+ path = Path (path ) / f"{ name } -archive.tar"
197
+ subprocess .run (["docker" , "save" , name , "-o" , str (path )], stdout = subprocess .PIPE )
198
+ image_archives .append (path )
199
+ subprocess .run (["docker" , "image" , "rm" , "-f" , name ], stdout = subprocess .PIPE )
200
+
201
+ for path in image_archives :
202
+ subprocess .run (["docker" , "load" , "-q" , "-i" , str (path )], stdout = subprocess .PIPE )
203
+ path .unlink ()
202
204
return len (self .team_names ) > 0
203
205
204
206
@build_successful
@@ -296,6 +298,9 @@ def run(self, battle_type: str = 'iterated', rounds: int = 5, iterated_cap: int
296
298
self .solving_team = pair [1 ]
297
299
self .battle_wrapper .wrapper (self , options )
298
300
301
+ for team in self .team_names :
302
+ for role in ("generator" , "solver" ):
303
+ subprocess .run (["docker" , "image" , "rm" , "-f" , f"{ role } -{ team } " ], stdout = subprocess .PIPE )
299
304
return self .match_data
300
305
301
306
@docker_running
0 commit comments