66# >> IMPORTS
77# =============================================================================
88# Python Imports
9+ import json
10+ import time
11+
912from zipfile import ZipFile
1013from urllib .request import urlopen
1114
1215# Source.Python Imports
1316# Core
17+ from core import PLATFORM
1418from core import core_logger
19+ from core import SOURCE_ENGINE_BRANCH
1520# Paths
21+ from paths import ADDONS_PATH
22+ from paths import GAME_PATH
23+ from paths import UPDATE_PATH
1624from paths import DATA_PATH
1725from paths import SP_DATA_PATH
26+ # KeyValues
27+ from keyvalues import KeyValues
1828
1929
2030# =============================================================================
2131# >> ALL DECLARATION
2232# =============================================================================
2333__all__ = (
34+ 'ARTIFACTS_URL' ,
35+ 'BASE_DOWNLOAD_URL' ,
2436 'CHECKSUM_URL' ,
2537 'DATA_URL' ,
2638 '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'
3149 'update_data'
3250)
3351
3856# Don't use __getattr__ here. 'update' is a method of the _LogInstance class.
3957update_logger = core_logger ['update' ]
4058
59+ BINARY_EXT = 'so' if PLATFORM == 'linux' else 'dll'
60+
61+ SP_VDF1 = 'addons/source-python'
62+ SP_VDF2 = 'addons/source-python2'
63+
4164DATA_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+
4271CHECKSUM_URL = 'http://data.sourcepython.com/checksum.txt'
4372DATA_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/'
4475
4576
4677# =============================================================================
@@ -62,12 +93,7 @@ def download_latest_data(timeout=3):
6293 :param float timeout:
6394 Number of seconds that need to pass until a timeout occurs.
6495 """
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 )
7197
7298def unpack_data ():
7399 """Unpack ``source-python-data.zip``."""
@@ -101,3 +127,114 @@ def is_new_data_available(timeout=3):
101127 return True
102128
103129 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