forked from Dentosal/python-sc2
-
Notifications
You must be signed in to change notification settings - Fork 0
/
sc2process.py
148 lines (120 loc) · 4.16 KB
/
sc2process.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
from typing import Any, Optional, List
import sys
import signal
import time
import asyncio
import os.path
import shutil
import tempfile
import subprocess
import portpicker
import aiohttp
import logging
logger = logging.getLogger(__name__)
from .paths import Paths
from .controller import Controller
class kill_switch:
_to_kill: List[Any] = []
@classmethod
def add(cls, value):
logger.debug("kill_switch: Add switch")
cls._to_kill.append(value)
@classmethod
def kill_all(cls):
logger.info("kill_switch: Process cleanup")
for p in cls._to_kill:
p._clean()
class SC2Process:
def __init__(self, host: str = "127.0.0.1", port: Optional[int] = None, fullscreen: bool = False,
render: bool = False) -> None:
assert isinstance(host, str)
assert isinstance(port, int) or port is None
self._render = render
self._fullscreen = fullscreen
self._host = host
if port is None:
self._port = portpicker.pick_unused_port()
else:
self._port = port
self._tmp_dir = tempfile.mkdtemp(prefix="SC2_")
self._process = None
self._session = None
self._ws = None
async def __aenter__(self):
kill_switch.add(self)
def signal_handler():
kill_switch.kill_all()
signal.signal(signal.SIGINT, signal_handler)
try:
self._process = self._launch()
self._ws = await self._connect()
except:
await self._close_connection()
self._clean()
raise
return Controller(self._ws, self)
async def __aexit__(self, *args):
kill_switch.kill_all()
signal.signal(signal.SIGINT, signal.SIG_DFL)
@property
def ws_url(self):
return f"ws://{self._host}:{self._port}/sc2api"
def _launch(self):
args = [
str(Paths.EXECUTABLE),
"-listen", self._host,
"-port", str(self._port),
"-displayMode", "1" if self._fullscreen else "0",
"-dataDir", str(Paths.BASE),
"-tempDir", self._tmp_dir,
]
if self._render:
args.extend(["-eglpath", "libEGL.so"])
if logger.getEffectiveLevel() <= logging.DEBUG:
args.append("-verbose")
return subprocess.Popen(args,
cwd=(str(Paths.CWD) if Paths.CWD else None),
#, env=run_config.env
)
async def _connect(self):
for i in range(60):
if self._process is None:
# The ._clean() was called, clearing the process
logger.debug("Process cleanup complete, exit")
sys.exit()
await asyncio.sleep(1)
try:
self._session = aiohttp.ClientSession()
ws = await self._session.ws_connect(self.ws_url, timeout=120)
logger.debug("Websocket connection ready")
return ws
except aiohttp.client_exceptions.ClientConnectorError:
await self._session.close()
if i > 15:
logger.debug("Connection refused (startup not complete (yet))")
logger.debug("Websocket connection to SC2 process timed out")
raise TimeoutError("Websocket")
async def _close_connection(self):
logger.info("Closing connection...")
if self._ws is not None:
await self._ws.close()
if self._session is not None:
await self._session.close()
def _clean(self):
logger.info("Cleaning up...")
if self._process is not None:
if self._process.poll() is None:
for _ in range(3):
self._process.terminate()
time.sleep(0.5)
if self._process.poll() is not None:
break
else:
self._process.kill()
self._process.wait()
logger.error("KILLED")
if os.path.exists(self._tmp_dir):
shutil.rmtree(self._tmp_dir)
self._process = None
self._ws = None
logger.info("Cleanup complete")