Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1124)

Unified Diff: recipe_engine/third_party/client-py/libs/luci_context/luci_context.py

Issue 2991053002: Vendor 'luci_context' library. (Closed)
Patch Set: Created 3 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: recipe_engine/third_party/client-py/libs/luci_context/luci_context.py
diff --git a/recipe_engine/third_party/client-py/libs/luci_context/luci_context.py b/recipe_engine/third_party/client-py/libs/luci_context/luci_context.py
new file mode 100644
index 0000000000000000000000000000000000000000..2c559e1f3eb368eb1d14b1a83a49fe2c0b1a5697
--- /dev/null
+++ b/recipe_engine/third_party/client-py/libs/luci_context/luci_context.py
@@ -0,0 +1,264 @@
+# Copyright 2016 The LUCI Authors. All rights reserved.
+# Use of this source code is governed under the Apache License, Version 2.0
+# that can be found in the LICENSE file.
+
+"""Implements a client library for reading and writing LUCI_CONTEXT compatible
+files.
+
+Due to arcane details of the UNIX process model and environment variables, this
+library is unfortunately NOT threadsafe; there's no way to have multiple
+LUCI_CONTEXTS live in a process safely at the same time. As such, this library
+will raise an exception if any attempt is made to use it improperly (for example
+by having multiple threads call 'write' at the same time).
+
+See ../LUCI_CONTEXT.md for details on the LUCI_CONTEXT concept/protocol."""
+
+import contextlib
+import copy
+import json
+import logging
+import os
+import sys
+import tempfile
+import threading
+
+_LOGGER = logging.getLogger(__name__)
+
+# _ENV_KEY is the environment variable that we look for to find out where the
+# LUCI context file is.
+_ENV_KEY = 'LUCI_CONTEXT'
+
+# _CUR_CONTEXT contains the cached LUCI Context that is currently available to
+# read. A value of None indicates that the value has not yet been populated.
+_CUR_CONTEXT = None
+_CUR_CONTEXT_LOCK = threading.Lock()
+
+# Write lock is a recursive mutex which is taken when using the write() method.
+# This allows the same thread to
+_WRITE_LOCK = threading.RLock()
+
+
+@contextlib.contextmanager
+def _tf(data, data_raw=False, workdir=None):
+ tf = tempfile.NamedTemporaryFile(prefix='luci_ctx.', suffix='.json',
+ delete=False, dir=workdir)
+ _LOGGER.debug('Writing LUCI_CONTEXT file %r', tf.name)
+ try:
+ if not data_raw:
+ json.dump(data, tf)
+ else:
+ # for testing, allows malformed json
+ tf.write(data)
+ tf.close() # close it so that winders subprocesses can read it.
+ yield tf.name
+ finally:
+ try:
+ os.unlink(tf.name)
+ except OSError as ex:
+ _LOGGER.error(
+ 'Failed to delete written LUCI_CONTEXT file %r: %s', tf.name, ex)
+
+
+def _to_utf8(obj):
+ if isinstance(obj, dict):
+ return {_to_utf8(key): _to_utf8(value) for key, value in obj.iteritems()}
+ if isinstance(obj, list):
+ return [_to_utf8(item) for item in obj]
+ if isinstance(obj, unicode):
+ return obj.encode('utf-8')
+ return obj
+
+
+class MultipleLUCIContextException(Exception):
+ def __init__(self):
+ super(MultipleLUCIContextException, self).__init__(
+ 'Attempted to write LUCI_CONTEXT in multiple threads')
+
+
+def _check_ok(data):
+ if not isinstance(data, dict):
+ _LOGGER.error(
+ 'LUCI_CONTEXT does not contain a dict: %s', type(data).__name__)
+ return False
+
+ bad = False
+ for k, v in data.iteritems():
+ if not isinstance(v, dict):
+ bad = True
+ _LOGGER.error(
+ 'LUCI_CONTEXT[%r] is not a dict: %s', k, type(v).__name__)
+
+ return not bad
+
+
+# this is a separate function from _read_full for testing purposes.
+def _initial_load():
+ global _CUR_CONTEXT
+ to_assign = {}
+
+ ctx_path = os.environ.get(_ENV_KEY)
+ if ctx_path:
+ ctx_path = ctx_path.decode(sys.getfilesystemencoding())
+ _LOGGER.debug('Loading LUCI_CONTEXT: %r', ctx_path)
+ try:
+ with open(ctx_path, 'r') as f:
+ loaded = _to_utf8(json.load(f))
+ if _check_ok(loaded):
+ to_assign = loaded
+ except OSError as ex:
+ _LOGGER.error('LUCI_CONTEXT failed to open: %s', ex)
+ except IOError as ex:
+ _LOGGER.error('LUCI_CONTEXT failed to read: %s', ex)
+ except ValueError as ex:
+ _LOGGER.error('LUCI_CONTEXT failed to decode: %s', ex)
+
+ _CUR_CONTEXT = to_assign
+
+
+def _read_full():
+ # double-check because I'm a hopeless diehard.
+ if _CUR_CONTEXT is None:
+ with _CUR_CONTEXT_LOCK:
+ if _CUR_CONTEXT is None:
+ _initial_load()
+ return _CUR_CONTEXT
+
+
+def _mutate(section_values):
+ new_val = read_full()
+ for section, value in section_values.iteritems():
+ if value is None:
+ new_val.pop(section, None)
+ elif isinstance(value, dict):
+ new_val[section] = value
+ else:
+ raise ValueError(
+ 'Bad type for LUCI_CONTEXT[%r]: %s', section, type(value).__name__)
+ return new_val
+
+
+def read_full():
+ """Returns a copy of the entire current contents of the LUCI_CONTEXT as
+ a dict.
+ """
+ return copy.deepcopy(_read_full())
+
+
+def read(section_key):
+ """Reads from the given section key. Returns the data in the section or None
+ if the data doesn't exist.
+
+ Args:
+ section_key (str) - The top-level key to read from the LUCI_CONTEXT.
+
+ Returns:
+ A copy of the requested section data (as a dict), or None if the section was
+ not present.
+
+ Example:
+ Given a LUCI_CONTEXT of:
+ {
+ "swarming": {
+ "secret_bytes": <bytes>
+ },
+ "other_service": {
+ "nested": {
+ "key": "something"
+ }
+ }
+ }
+
+ read('swarming') -> {'secret_bytes': <bytes>}
+ read('doesnt_exist') -> None
+ """
+ return copy.deepcopy(_read_full().get(section_key, None))
+
+
+@contextlib.contextmanager
+def write(_tmpdir=None, **section_values):
+ """Write is a contextmanager which will write all of the provided section
+ details to a new context, copying over the values from any unmentioned
+ sections. The new context file will be set in os.environ. When the
+ contextmanager exits, it will attempt to delete the context file.
+
+ Since each call to write produces a new context file on disk, it's beneficial
+ to group edits together into a single call to write when possible.
+
+ Calls to read*() within the context of a call to write will read from the
+ written value. This written value is stored on a per-thread basis.
+
+ NOTE: Because environment variables are per-process and not per-thread, it is
+ an error to call write() from multiple threads simultaneously. If this is
+ done, this function raises an exception.
+
+ Args:
+ _tmpdir (str) - an optional directory to use for the newly written
+ LUCI_CONTEXT file.
+ section_values (str -> value) - A mapping of section_key to the new value
+ for that section. A value of None will remove that section. Non-None
+ values must be of the type 'dict', and must be json serializable.
+
+ Raises:
+ MultipleLUCIContextException if called from multiple threads
+ simulataneously.
+
+ Example:
+ Given a LUCI_CONTEXT of:
+ {
+ "swarming": {
+ "secret_bytes": <bytes>
+ },
+ "other_service": {
+ ...
+ }
+ }
+
+ with write(swarming=None): ... # deletes 'swarming'
+ with write(something={...}): ... # sets 'something' section to {...}
+ """
+ # If there are no edits, just pass-through
+ if not section_values:
+ yield
+ return
+
+ new_val = _mutate(section_values)
+
+ global _CUR_CONTEXT
+ got_lock = _WRITE_LOCK.acquire(blocking=False)
+ if not got_lock:
+ raise MultipleLUCIContextException()
+ try:
+ with _tf(new_val, workdir=_tmpdir) as name:
+ try:
+ old_value = _CUR_CONTEXT
+ old_envvar = os.environ.get(_ENV_KEY, None)
+
+ os.environ[_ENV_KEY] = name.encode(sys.getfilesystemencoding())
+ _CUR_CONTEXT = new_val
+ yield
+ finally:
+ _CUR_CONTEXT = old_value
+ if old_envvar is None:
+ del os.environ[_ENV_KEY]
+ else:
+ os.environ[_ENV_KEY] = old_envvar
+ finally:
+ _WRITE_LOCK.release()
+
+
+@contextlib.contextmanager
+def stage(_tmpdir=None, **section_values):
+ """Prepares and writes new LUCI_CONTEXT file, but doesn't replace the env var.
+
+ This is useful when launching new process asynchronously in new LUCI_CONTEXT
+ environment. In this case, modifying the environment of the current process
+ (like 'write' does) may be harmful.
+
+ Calls the body with a path to the new LUCI_CONTEXT file or None if
+ 'section_values' is empty (meaning, no changes have been made).
+ """
+ if not section_values:
+ yield None
+ return
+ with _tf(_mutate(section_values), workdir=_tmpdir) as name:
+ yield name

Powered by Google App Engine
This is Rietveld 408576698