6
6
# >> IMPORTS
7
7
# =============================================================================
8
8
# Python Imports
9
+ import json
10
+ import time
11
+
9
12
from zipfile import ZipFile
10
13
from urllib .request import urlopen
11
14
12
15
# Source.Python Imports
13
16
# Core
17
+ from core import PLATFORM
14
18
from core import core_logger
19
+ from core import SOURCE_ENGINE_BRANCH
15
20
# Paths
21
+ from paths import ADDONS_PATH
22
+ from paths import GAME_PATH
23
+ from paths import UPDATE_PATH
16
24
from paths import DATA_PATH
17
25
from paths import SP_DATA_PATH
26
+ # KeyValues
27
+ from keyvalues import KeyValues
18
28
19
29
20
30
# =============================================================================
21
31
# >> ALL DECLARATION
22
32
# =============================================================================
23
33
__all__ = (
34
+ 'ARTIFACTS_URL' ,
35
+ 'BASE_DOWNLOAD_URL' ,
24
36
'CHECKSUM_URL' ,
25
37
'DATA_URL' ,
26
38
'DATA_ZIP_FILE' ,
27
- 'download_latest_data' ,
28
- 'get_latest_data_checksum' ,
29
- 'is_new_data_available' ,
30
- 'unpack_data' ,
39
+ 'apply_update_stage1'
40
+ 'clean_update_dir'
41
+ 'download_file'
42
+ 'download_latest_data'
43
+ 'download_latest_version'
44
+ 'get_artifacts'
45
+ 'get_download_url'
46
+ 'get_latest_data_checksum'
47
+ 'is_new_data_available'
48
+ 'unpack_data'
31
49
'update_data'
32
50
)
33
51
38
56
# Don't use __getattr__ here. 'update' is a method of the _LogInstance class.
39
57
update_logger = core_logger ['update' ]
40
58
59
+ BINARY_EXT = 'so' if PLATFORM == 'linux' else 'dll'
60
+
61
+ SP_VDF1 = 'addons/source-python'
62
+ SP_VDF2 = 'addons/source-python2'
63
+
41
64
DATA_ZIP_FILE = DATA_PATH / 'source-python-data.zip'
65
+ UPDATE_ZIP_FILE = UPDATE_PATH / 'source-python.zip'
66
+ VDF_FILE = ADDONS_PATH / 'source-python.vdf'
67
+ LOADER_FILE = ADDONS_PATH / f'source-python.{ BINARY_EXT } '
68
+ LOADER_UPDATE_FILE = UPDATE_PATH / 'addons' / f'source-python.{ BINARY_EXT } '
69
+ VDF_UPDATE_FILE = UPDATE_PATH / 'addons' / 'source-python.vdf'
70
+
42
71
CHECKSUM_URL = 'http://data.sourcepython.com/checksum.txt'
43
72
DATA_URL = 'http://data.sourcepython.com/source-python-data.zip'
73
+ ARTIFACTS_URL = 'http://builds.sourcepython.com/job/Source.Python/lastSuccessfulBuild/api/json?tree=artifacts[relativePath]'
74
+ BASE_DOWNLOAD_URL = 'http://builds.sourcepython.com/job/Source.Python/lastSuccessfulBuild/artifact/'
44
75
45
76
46
77
# =============================================================================
@@ -62,12 +93,7 @@ def download_latest_data(timeout=3):
62
93
:param float timeout:
63
94
Number of seconds that need to pass until a timeout occurs.
64
95
"""
65
- update_logger .log_debug ('Downloading data to {} ...' .format (DATA_ZIP_FILE ))
66
- with urlopen (DATA_URL , timeout = timeout ) as url :
67
- data = url .read ()
68
-
69
- with DATA_ZIP_FILE .open ('wb' ) as f :
70
- f .write (data )
96
+ download_file (DATA_URL , DATA_ZIP_FILE , timeout )
71
97
72
98
def unpack_data ():
73
99
"""Unpack ``source-python-data.zip``."""
@@ -101,3 +127,114 @@ def is_new_data_available(timeout=3):
101
127
return True
102
128
103
129
return DATA_ZIP_FILE .read_hexhash ('md5' ) != get_latest_data_checksum (timeout )
130
+
131
+ def get_artifacts ():
132
+ """Return the artifacts of the latest build."""
133
+ update_logger .log_debug ('Getting artifacts...' )
134
+ with urlopen (ARTIFACTS_URL ) as url :
135
+ data = json .loads (url .read ())
136
+
137
+ for d in data ['artifacts' ]:
138
+ yield d ['relativePath' ]
139
+
140
+ def get_download_url (game = SOURCE_ENGINE_BRANCH ):
141
+ """Get the latest download URL for a specific game."""
142
+ for relative_path in get_artifacts ():
143
+ if f'-{ game } -' in relative_path :
144
+ return BASE_DOWNLOAD_URL + relative_path
145
+
146
+ raise ValueError (f'Unable to find a download URL for game "{ game } ".' )
147
+
148
+ def download_file (url_path , file_path , timeout = 3 ):
149
+ """Download a file from an URL to a specific file."""
150
+ update_logger .log_debug (f'Downloading file ({ url_path } ) to { file_path } ...' )
151
+ now = time .time ()
152
+
153
+ with urlopen (url_path , timeout = timeout ) as url :
154
+ data = url .read ()
155
+
156
+ with file_path .open ('wb' ) as f :
157
+ f .write (data )
158
+
159
+ update_logger .log_info (
160
+ 'File has been downloaded. Time elapsed: {:0.2f} seconds' .format (
161
+ time .time ()- now ))
162
+
163
+ def clean_update_dir ():
164
+ """Clear or create the update directory."""
165
+ if UPDATE_PATH .exists ():
166
+ for f in UPDATE_PATH .listdir ():
167
+ if f .isfile ():
168
+ f .remove ()
169
+ else :
170
+ f .rmtree ()
171
+ else :
172
+ UPDATE_PATH .mkdir ()
173
+
174
+ def download_latest_version (timeout = 3 ):
175
+ """Download the latest version."""
176
+ download_file (get_download_url (), UPDATE_ZIP_FILE , timeout )
177
+
178
+ def apply_update_stage1 ():
179
+ """Apply stage 1 of the version update."""
180
+ update_logger .log_message ('Applying Source.Python update stage 1...' )
181
+
182
+ # Extract all files to the update directory
183
+ with ZipFile (UPDATE_ZIP_FILE ) as zip :
184
+ zip .extractall (UPDATE_PATH )
185
+
186
+ UPDATE_ZIP_FILE .remove ()
187
+ VDF_UPDATE_FILE .remove ()
188
+
189
+ if PLATFORM == 'windows' :
190
+ _apply_update_stage1_windows ()
191
+ else :
192
+ _apply_update_stage1_linux ()
193
+
194
+ def _apply_update_stage1_windows ():
195
+ """Apply the Windows specific part of stage 1.
196
+
197
+ On Windows files that are currently in use (``source-python.dll``) can't be
198
+ replaced. Thus, this function checks if ``source-python.vdf`` exists. If it
199
+ does, the new ``source-python.dll`` is copied to the addons directory with
200
+ a new name (``source-python2.dll``). After that the VDF entry is modified
201
+ to point to the new loader.
202
+ If ``source-python.vdf`` does not exist, manual action is required.
203
+ """
204
+ if not VDF_FILE .isfile ():
205
+ update_logger .log_message (
206
+ f'Stage 1 has been applied. Please shutdown your server and move '
207
+ f'(do not copy) { LOADER_UPDATE_FILE } to { LOADER_FILE } . After that '
208
+ f'start your server to apply stage 2.' )
209
+ else :
210
+ update_logger .log_debug ('Determining current VDF entry...' )
211
+ kv = KeyValues .load_from_file (VDF_FILE )
212
+
213
+ # Get the current and new entry for the VDF file
214
+ current_entry = kv .get_string ('file' )
215
+ if current_entry == SP_VDF2 :
216
+ new_entry = SP_VDF1
217
+ elif current_entry == SP_VDF1 :
218
+ new_entry = SP_VDF2
219
+ else :
220
+ raise ValueError (f'Unexpected entry in VDF: { current_entry } ' )
221
+
222
+ update_logger .log_debug (f'Current VDF entry: { current_entry } ' )
223
+ update_logger .log_debug (f'New VDF entry: { new_entry } ' )
224
+
225
+ update_logger .log_debug ('Moving new loader binary to game directory...' )
226
+ LOADER_UPDATE_FILE .move (GAME_PATH / f'{ new_entry } .{ BINARY_EXT } ' )
227
+
228
+ kv .set_string ('file' , new_entry )
229
+ kv .save_to_file (VDF_FILE )
230
+
231
+ update_logger .log_message (
232
+ 'Stage 1 has been applied. Restart your server to apply stage 2.' )
233
+
234
+ def _apply_update_stage1_linux ():
235
+ """Apply the Linux specific part of stage 1."""
236
+ update_logger .log_debug ('Moving new loader binary to game directory...' )
237
+ LOADER_UPDATE_FILE .move (LOADER_FILE )
238
+
239
+ update_logger .log_message (
240
+ 'Stage 1 has been applied. Restart your server to apply stage 2.' )
0 commit comments