Skip to content

Commit

Permalink
Python bindings: Fix gdal.config_options to prevent migration of config
Browse files Browse the repository at this point in the history
options in and out of thread-local storage

Fixes OSGeo#8018
  • Loading branch information
dbaston committed Jul 5, 2023
1 parent afbf08d commit 1aadcc6
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 16 deletions.
79 changes: 79 additions & 0 deletions autotest/gcore/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,85 @@ def handle1(ecls, ecode, emsg):
gdal.SetConfigOption("CPL_DEBUG", prev_debug)


###############################################################################
# Test config option context managers


def test_misc_config_context_mgrs_1():
# Make sure that config_options context manager does not convert a
# global config option to a thread-local config option

try:
gdal.SetConfigOption("A", "1")

assert gdal.GetConfigOption("A") == "1"
assert gdal.GetThreadLocalConfigOption("A") is None

# temporarily set new thread-local value for A
with gdal.config_option("A", "2", thread_local=True):
assert gdal.GetConfigOption("A") == "2"
assert gdal.GetThreadLocalConfigOption("A") == "2"

# value of A is restored, and thread-local value is unset
assert gdal.GetConfigOption("A") == "1"
assert gdal.GetThreadLocalConfigOption("A") is None

finally:
gdal.SetConfigOption("A", None)


def test_misc_config_context_mgrs_2():
# Make sure that config_options context manager does not convert a
# thread-local config option to a global config option

try:
gdal.SetThreadLocalConfigOption("B", "5")
assert gdal.GetConfigOption("B") == "5"
assert gdal.GetThreadLocalConfigOption("B") == "5"

# temporarily set new global value for B
# this has no effect, because the thread-local value overrides it
with gdal.config_option("B", "6", thread_local=False):
assert gdal.GetConfigOption("B") == "5"
assert gdal.GetThreadLocalConfigOption("B") == "5"

# value of B is restored
assert gdal.GetThreadLocalConfigOption("B") == "5"

gdal.SetThreadLocalConfigOption("B", None)
assert gdal.GetConfigOption("B") is None

finally:
gdal.SetThreadLocalConfigOption("B", None)


def test_misc_config_context_mgrs_3():
# Make sure that config_options correctly restores state when
# a configuration option is set in both thread-local and global
# contexts.

try:
gdal.SetConfigOption("C", "GLOBAL")
gdal.SetThreadLocalConfigOption("C", "TL")

# temporarily set new global value for C
# this has no effect, because the thread-local value overrides it
with gdal.config_option("C", "XX", thread_local=False):
assert gdal.GetConfigOption("C") == "TL"
assert gdal.GetThreadLocalConfigOption("C") == "TL"

# value of C is restored
assert gdal.GetThreadLocalConfigOption("C") == "TL"

# clear thread-local value of C, exposing global value
gdal.SetThreadLocalConfigOption("C", None)
assert gdal.GetConfigOption("C") == "GLOBAL"

finally:
gdal.SetConfigOption("C", None)
gdal.SetThreadLocalConfigOption("C", None)


###############################################################################


Expand Down
44 changes: 29 additions & 15 deletions port/cpl_conv.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1689,24 +1689,11 @@ const char *CPL_STDCALL CPLGetConfigOption(const char *pszKey,
const char *pszDefault)

{
#ifdef DEBUG_CONFIG_OPTIONS
CPLAccessConfigOption(pszKey, TRUE);
#endif

const char *pszResult = nullptr;

int bMemoryError = FALSE;
char **papszTLConfigOptions = reinterpret_cast<char **>(
CPLGetTLSEx(CTLS_CONFIGOPTIONS, &bMemoryError));
if (papszTLConfigOptions != nullptr)
pszResult = CSLFetchNameValue(papszTLConfigOptions, pszKey);
const char *pszResult = CPLGetThreadLocalConfigOption(pszKey, nullptr);

if (pszResult == nullptr)
{
CPLMutexHolderD(&hConfigMutex);

pszResult = CSLFetchNameValue(const_cast<char **>(g_papszConfigOptions),
pszKey);
pszResult = CPLGetGlobalConfigOption(pszKey, nullptr);
}

if (gbIgnoreEnvVariables)
Expand Down Expand Up @@ -1808,6 +1795,33 @@ const char *CPL_STDCALL CPLGetThreadLocalConfigOption(const char *pszKey,
return pszResult;
}

/************************************************************************/
/* CPLGetGlobalConfigOption() */
/************************************************************************/

/** Same as CPLGetConfigOption() but excludes environment variables and
* options set with CPLSetThreadLocalConfigOption().
* This function should generally not be used by applications, which should
* use CPLGetConfigOption() instead.
* @since 3.8 */
const char *CPL_STDCALL CPLGetGlobalConfigOption(const char *pszKey,
const char *pszDefault)
{
#ifdef DEBUG_CONFIG_OPTIONS
CPLAccessConfigOption(pszKey, TRUE);
#endif

CPLMutexHolderD(&hConfigMutex);

const char *pszResult =
CSLFetchNameValue(const_cast<char **>(g_papszConfigOptions), pszKey);

if (pszResult == nullptr)
return pszDefault;

return pszResult;
}

/************************************************************************/
/* CPLSubscribeToSetConfigOption() */
/************************************************************************/
Expand Down
2 changes: 2 additions & 0 deletions port/cpl_conv.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ const char CPL_DLL *CPL_STDCALL CPLGetConfigOption(const char *, const char *)
CPL_WARN_UNUSED_RESULT;
const char CPL_DLL *CPL_STDCALL CPLGetThreadLocalConfigOption(
const char *, const char *) CPL_WARN_UNUSED_RESULT;
const char CPL_DLL *CPL_STDCALL
CPLGetGlobalConfigOption(const char *, const char *) CPL_WARN_UNUSED_RESULT;
void CPL_DLL CPL_STDCALL CPLSetConfigOption(const char *, const char *);
void CPL_DLL CPL_STDCALL CPLSetThreadLocalConfigOption(const char *pszKey,
const char *pszValue);
Expand Down
5 changes: 5 additions & 0 deletions swig/include/cpl.i
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ void CPL_STDCALL PyCPLErrorHandler(CPLErr eErrClass, int err_no, const char* psz
%rename (GetFileSystemOptions) VSIGetFileSystemOptions;
%rename (SetConfigOption) CPLSetConfigOption;
%rename (GetConfigOption) wrapper_CPLGetConfigOption;
%rename (GetGlobalConfigOption) wrapper_CPLGetGlobalConfigOption;
%rename (SetThreadLocalConfigOption) CPLSetThreadLocalConfigOption;
%rename (GetThreadLocalConfigOption) wrapper_CPLGetThreadLocalConfigOption;
%rename (SetCredential) wrapper_VSISetCredential;
Expand Down Expand Up @@ -494,6 +495,10 @@ const char *wrapper_CPLGetConfigOption( const char * pszKey, const char * pszDef
{
return CPLGetConfigOption( pszKey, pszDefault );
}
const char *wrapper_CPLGetGlobalConfigOption( const char * pszKey, const char * pszDefault = NULL )
{
return CPLGetGlobalConfigOption( pszKey, pszDefault );
}
const char *wrapper_CPLGetThreadLocalConfigOption( const char * pszKey, const char * pszDefault = NULL )
{
return CPLGetThreadLocalConfigOption( pszKey, pszDefault );
Expand Down
5 changes: 4 additions & 1 deletion swig/include/python/gdal_python.i
Original file line number Diff line number Diff line change
Expand Up @@ -3565,8 +3565,11 @@ def config_options(options, thread_local=True):
with gdal.config_options({"GDAL_NUM_THREADS": "ALL_CPUS"}):
gdal.Warp("out.tif", "in.tif", dstSRS="EPSG:4326")
"""
oldvals = {key: GetConfigOption(key) for key in options}
get_config_option = GetThreadLocalConfigOption if thread_local else GetGlobalConfigOption
set_config_option = SetThreadLocalConfigOption if thread_local else SetConfigOption

oldvals = {key: get_config_option(key) for key in options}

for key in options:
set_config_option(key, options[key])
try:
Expand Down

0 comments on commit 1aadcc6

Please sign in to comment.