11"""Match class, provides functionality for setting up and executing battles between given teams."""
2+ from pathlib import Path
23import subprocess
34import os
45
910import algobattle .sighandler as sigh
1011from algobattle .team import Team
1112from 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
1314from algobattle .subject import Subject
1415from algobattle .observer import Observer
1516from algobattle .battle_wrappers .averaged import Averaged
@@ -25,9 +26,16 @@ class Match(Subject):
2526 generating_team = None
2627 solving_team = None
2728 battle_wrapper = None
29+ creationflags = 0
2830
2931 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
3139
3240 config = configparser .ConfigParser ()
3341 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,
4351 self .config = config
4452 self .approximation_ratio = approximation_ratio
4553
46- self .build_successful = self ._build (teams , cache_docker_containers )
54+ self .build_successful = self ._build (teams , cache_docker_containers , unsafe_build )
4755
4856 if approximation_ratio != 1.0 and not problem .approximable :
4957 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):
92100 def docker_running (function : Callable ) -> Callable :
93101 """Ensure that internal methods are only callable if docker is running."""
94102 def wrapper (self , * args , ** kwargs ):
95- creationflags = 0
96- if os .name != 'posix' :
97- creationflags = subprocess .CREATE_NEW_PROCESS_GROUP
98103 docker_running = subprocess .Popen (['docker' , 'info' ], stdout = subprocess .PIPE ,
99- stderr = subprocess .PIPE , creationflags = creationflags )
104+ stderr = subprocess .PIPE , creationflags = self . creationflags )
100105 _ = docker_running .communicate ()
101106 if docker_running .returncode :
102107 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:
132137 return True
133138
134139 @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 :
136141 """Build docker containers for the given generators and solvers of each team.
137142
138143 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:
144149 List of Team objects.
145150 cache_docker_containers : bool
146151 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.
147154
148155 Returns
149156 -------
@@ -169,36 +176,31 @@ def _build(self, teams: list, cache_docker_containers=True) -> bool:
169176
170177 self .single_player = (len (teams ) == 1 )
171178
179+ image_archives : list = []
172180 for team in teams :
173181 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 ) )
176184
177185 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 ()
202204 return len (self .team_names ) > 0
203205
204206 @build_successful
@@ -296,6 +298,9 @@ def run(self, battle_type: str = 'iterated', rounds: int = 5, iterated_cap: int
296298 self .solving_team = pair [1 ]
297299 self .battle_wrapper .wrapper (self , options )
298300
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 )
299304 return self .match_data
300305
301306 @docker_running
0 commit comments