Skip to content

Commit 13f13e1

Browse files
authored
sp update (#242)
* Initial commit for "sp update" * Fixed update detection Added missing message prefixes * Changed ' to " * Use Boost.Filesystem instead if IFileSystem IFileSystem had some problems with not seeing all files on Linux. * Added platform specific parts to stage 1 * Updated get_artifacts() * Added "sp update" command to the wiki * Updated update instructions on the wiki * Removed unused macros * Don't create copies of bfs::path
1 parent 05aa830 commit 13f13e1

File tree

15 files changed

+397
-18
lines changed

15 files changed

+397
-18
lines changed

addons/source-python/docs/source-python/source/general/sp-commands.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,3 +493,15 @@ Unload a plugin by name.
493493
494494
// Unload the plugin 'test'
495495
sp plugin unload test
496+
497+
498+
update
499+
------
500+
501+
Update Source.Python to the latest version. A restart of the server is required
502+
to apply the new update.
503+
504+
.. code-block:: none
505+
506+
// Usage
507+
// sp update
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
Updating
22
========
33

4+
Manually
5+
--------
6+
47
1. Delete the entire ``../addons/source-python/data/source-python/`` directory.
58
2. Delete the entire ``../addons/source-python/packages/source-python/`` directory.
6-
3. Continue with the :doc:`installation tutorial <installation>`.
9+
3. Delete the entire ``../addons/source-python/docs/source-python/`` directory.
10+
4. Delete the entire ``../addons/source-python/Python3/`` directory.
11+
5. Continue with the :doc:`installation tutorial <installation>`.
12+
13+
14+
Automatically
15+
-------------
16+
17+
1. Run the command ``sp update`` and follow the instructions printed to the console (mostly, it's just a restart).

addons/source-python/packages/source-python/core/command/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
from core import core_logger
2323
from core import create_checksum
2424
from core import SOURCE_ENGINE_BRANCH
25+
from core.update import clean_update_dir
26+
from core.update import download_latest_version
27+
from core.update import apply_update_stage1
28+
from core.version import get_last_successful_build_number
29+
from core.version import is_unversioned
2530
from core.version import VERSION
2631
from core.version import GIT_COMMIT
2732
# Engines
@@ -158,6 +163,27 @@ def print_info(info):
158163
f'Checksum : {checksum}{result}\n{sep}\n')
159164

160165

166+
@core_command.server_sub_command(['update'])
167+
def update_sp(info):
168+
"""Update Source.Python to the latest version. A restart of the server is
169+
required.
170+
"""
171+
if not is_unversioned() and VERSION >= get_last_successful_build_number():
172+
core_command_logger.log_message('No new version available.')
173+
return
174+
175+
# Make sure there is a clean update directory
176+
clean_update_dir()
177+
try:
178+
download_latest_version()
179+
apply_update_stage1()
180+
except:
181+
# Make sure to leave a clean update directory, so the loader doesn't
182+
# get confused.
183+
clean_update_dir()
184+
raise
185+
186+
161187
# =============================================================================
162188
# >> DESCRIPTION
163189
# =============================================================================

addons/source-python/packages/source-python/core/update.py

Lines changed: 147 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,46 @@
66
# >> IMPORTS
77
# =============================================================================
88
# Python Imports
9+
import json
10+
import time
11+
912
from zipfile import ZipFile
1013
from urllib.request import urlopen
1114

1215
# Source.Python Imports
1316
# Core
17+
from core import PLATFORM
1418
from 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
1624
from paths import DATA_PATH
1725
from 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

@@ -38,9 +56,22 @@
3856
# Don't use __getattr__ here. 'update' is a method of the _LogInstance class.
3957
update_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+
4164
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+
4271
CHECKSUM_URL = 'http://data.sourcepython.com/checksum.txt'
4372
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/'
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

7298
def 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.')

addons/source-python/packages/source-python/paths.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
# =============================================================================
1414
# >> ALL DECLARATION
1515
# =============================================================================
16-
__all__ = ('BASE_PATH',
16+
__all__ = ('ADDONS_PATH',
17+
'BASE_PATH',
1718
'CFG_PATH',
1819
'CUSTOM_DATA_PATH',
1920
'CUSTOM_PACKAGES_DOCS_PATH',
@@ -34,6 +35,7 @@
3435
'TRANSLATION_PATH',
3536
'AUTH_CFG_PATH',
3637
'BACKENDS_PATH',
38+
'UPDATE_PATH'
3739
)
3840

3941

@@ -43,8 +45,14 @@
4345
# ../<game>
4446
GAME_PATH = Path(Path(__file__).rsplit('addons', 1)[0][:~0])
4547

48+
# ../addons
49+
ADDONS_PATH = GAME_PATH / 'addons'
50+
4651
# ../addons/source-python
47-
BASE_PATH = GAME_PATH / 'addons' / 'source-python'
52+
BASE_PATH = ADDONS_PATH / 'source-python'
53+
54+
# ../addons/source-python/update
55+
UPDATE_PATH = BASE_PATH / 'update'
4856

4957
# ../addons/source-python/docs
5058
DOCS_PATH = BASE_PATH / 'docs'

src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ include("makefiles/shared.cmake")
1414
# ------------------------------------------------------------------
1515
Set(SOURCEPYTHON_LOADER_HEADERS
1616
loader/loader_main.h
17+
loader/updater.h
1718
loader/definitions.h
1819
)
1920

2021
Set(SOURCEPYTHON_LOADER_SOURCES
2122
loader/loader_main.cpp
23+
loader/updater.cpp
2224
)
2325

2426
Set(SOURCEPYTHON_LOADER_FILES

src/loader/loader_main.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
// Source includes
2929
//---------------------------------------------------------------------------------
3030
#include "loader_main.h"
31+
#include "updater.h"
3132
#include "interface.h"
3233
#include "eiface.h"
3334
#include "strtools.h"
@@ -36,6 +37,7 @@
3637
#endif
3738

3839
#include "../core/utilities/shared_utils.h"
40+
#include <exception>
3941

4042
//---------------------------------------------------------------------------------
4143
// Disable warnings.
@@ -51,6 +53,7 @@
5153
// Interfaces.
5254
//---------------------------------------------------------------------------------
5355
ICvar* g_pCVar = NULL; // This is required for linux linking..
56+
IVEngineServer* engine = NULL;
5457

5558
//
5659
// The plugin is a static singleton that is exported as an interface
@@ -162,7 +165,7 @@ bool CSourcePython::Load( CreateInterfaceFn interfaceFactory, CreateInterfaceFn
162165
{
163166
Msg(MSG_PREFIX "Loading...\n");
164167

165-
IVEngineServer* engine = (IVEngineServer*)interfaceFactory(INTERFACEVERSION_VENGINESERVER, NULL);
168+
engine = (IVEngineServer*)interfaceFactory(INTERFACEVERSION_VENGINESERVER, NULL);
166169

167170
// Was the IVEngineServer interface retrieved properly?
168171
if (!engine)
@@ -184,6 +187,18 @@ bool CSourcePython::Load( CreateInterfaceFn interfaceFactory, CreateInterfaceFn
184187
DevMsg(1, MSG_PREFIX "Game directory: %s\n", szGameDir);
185188
GenerateSymlink(szGameDir);
186189

190+
if (UpdateAvailable())
191+
{
192+
try
193+
{
194+
ApplyUpdateStage2();
195+
}
196+
catch (const std::exception& e)
197+
{
198+
Msg(MSG_PREFIX "An error occured during update stage 2:\n%s\n", e.what());
199+
}
200+
}
201+
187202
// ------------------------------------------------------------------
188203
// Load windows dependencies.
189204
// ------------------------------------------------------------------

0 commit comments

Comments
 (0)