platform: add libexperiments

New and improved, now building on *all* platforms:

This provides a library for C/C++ programs that enables them to easily
register experiments and check if they are enabled.

In order to accommodate programs like Sagesrv which may be checking
experiment states at higher frequencies, a caching logic is used that
stores the current state of registered experiments and updates them in
(configurable) intervals (e.g., Sagesrv defaults to 60 secs).

Please see experiments.h for details on usage.

Change-Id: I17dbd1079a2f724fe645b427b606a94a5219bbae
diff --git a/Makefile b/Makefile
index 0e46102..2f3aa95 100644
--- a/Makefile
+++ b/Makefile
@@ -20,7 +20,7 @@
 
 # note: libgpio is not built here.  It's conditionally built
 # via buildroot/packages/google/google_platform/google_platform.mk
-DIRS=libstacktrace ginstall cmds \
+DIRS=libstacktrace libexperiments ginstall cmds \
 	antirollback tvstat gpio-mailbox spectralanalyzer wifi wifiblaster \
 	sysvar py_mtd devcert
 
@@ -98,9 +98,9 @@
 	set -e; for d in $(DIRS); do $(MAKE) -C $$d install; done
 	$(MAKE) install-optionspy
 
-sysmgr/all: base/all libstacktrace/all
-cmds/all: libstacktrace/all
-gpio-mailbox/all: libstacktrace/all
+sysmgr/all: base/all libstacktrace/all libexperiments/all
+cmds/all: libstacktrace/all libexperiments/all
+gpio-mailbox/all: libstacktrace/all libexperiments/all
 
 %/all:
 	$(MAKE) -C $* all
diff --git a/libexperiments/.gitignore b/libexperiments/.gitignore
new file mode 100644
index 0000000..1d2719e
--- /dev/null
+++ b/libexperiments/.gitignore
@@ -0,0 +1 @@
+experiments_test
diff --git a/libexperiments/Makefile b/libexperiments/Makefile
new file mode 100644
index 0000000..08ba166
--- /dev/null
+++ b/libexperiments/Makefile
@@ -0,0 +1,47 @@
+CC=$(CROSS_COMPILE)gcc
+CXX=$(CROSS_COMPILE)g++
+INSTALL=install
+PREFIX=/usr
+LIBDIR=$(DESTDIR)$(PREFIX)/lib
+INCLUDEDIR=$(DESTDIR)$(PREFIX)/include
+
+all: libexperiments.so
+
+CPPFLAGS=$(EXTRACFLAGS)
+CFLAGS=-Wall -Werror -g -fPIC -Wswitch-enum -Wextra -fno-omit-frame-pointer \
+    -Wno-sign-compare -Wno-unused-parameter $(EXTRACFLAGS)
+CXXFLAGS=-Wall -Werror -g -fPIC -Wswitch-enum -Wextra -fno-omit-frame-pointer \
+    -Wno-sign-compare -Wno-unused-parameter -std=c++0x $(EXTRACXXFLAGS)
+LDFLAGS+=$(EXTRALDFLAGS)
+
+libexperiments.so: experiments.o utils.o
+	$(CC) -shared -Wl,-soname,libexperiments.so -Wl,-export-dynamic -o $@ $^
+
+experiments_test: experiments.o experiments_test.o experiments_c_api_test.o utils.o
+	$(CXX) -o $@ $^ $(LDFLAGS) $(CPPFLAGS) -lgtest -lpthread
+
+%.o: %.c
+	$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
+%.o: %.cc
+	$(CXX) -c $(CXXFLAGS) $(CPPFLAGS) $< -o $@
+
+# all *.o depend on all the header files
+$(patsubst %.cc,%.o,$(wildcard *.cc)) $(patsubst %.c,%.o,$(wildcard *.c)): \
+  $(wildcard *.h)
+
+install: all
+	echo 'target-install=$(INSTALL)'
+	mkdir -p $(LIBDIR)
+	$(INSTALL) -m 0755 libexperiments.so $(LIBDIR)/
+
+install-libs: all
+	echo 'staging-install=$(INSTALL)'
+	mkdir -p $(INCLUDEDIR) $(LIBDIR) $(LIBDIR)/pkgconfig
+	$(INSTALL) -m 0644 experiments.h $(INCLUDEDIR)/
+	$(INSTALL) -m 0755 libexperiments.so $(LIBDIR)/
+
+test: experiments_test
+	./experiments_test
+
+clean:
+	rm -rf *.[oa] *.so *~
diff --git a/libexperiments/experiments.cc b/libexperiments/experiments.cc
new file mode 100644
index 0000000..d3dcb2b
--- /dev/null
+++ b/libexperiments/experiments.cc
@@ -0,0 +1,176 @@
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#ifdef __cplusplus
+#define __STDC_FORMAT_MACROS
+#endif
+
+#include "experiments.h"
+
+#include <inttypes.h>
+
+#include <sstream>
+#include <string>
+
+#include "utils.h"
+
+using namespace libexperiments_utils;  // NOLINT
+
+Experiments *experiments = NULL;
+
+int DefaultExperimentsRegisterFunc(const char *name) {
+  std::vector<std::string> cmd({"register_experiment", name});
+  std::ostringstream out, err;
+  int64_t timeout_usec = secs_to_usecs(5);
+  int status;
+  int ret = run_cmd(cmd, "", &status, &out, &err, timeout_usec);
+  if (ret < 0 || status != 0) {
+    log("experiments:Error-Cannot register '%s', ret:%d status:%d stdout:%s "
+        "stderr:%s", name, ret, status, out.str().c_str(),
+        err.str().c_str());
+    return 0;  // boolean false
+  }
+  return 1;  // boolean true
+}
+
+int DummyExperimentsRegisterFunc(const char *name) {
+  return 1;  // boolean true
+}
+
+bool Experiments::Initialize(
+    const std::string &config_dir, int64_t min_time_between_refresh_usec,
+    experiments_register_func_t register_func,
+    const std::vector<std::string> &names_to_register) {
+  log("experiments:initializing - config_dir:%s min_time_between_refresh:%"
+      PRId64 " us", config_dir.c_str(), min_time_between_refresh_usec);
+
+  std::lock_guard<std::mutex> lock_guard(lock_);
+
+  if (register_func == NULL) {
+    log("experiments:Error-register_func is NULL");
+    return false;
+  }
+
+  if (!directory_exists(config_dir.c_str())) {
+    log("experiments:Error-config_dir '%s' does not exist", config_dir.c_str());
+    return false;
+  }
+
+  if (min_time_between_refresh_usec < 0)
+    min_time_between_refresh_usec = 0;
+
+  config_dir_ = config_dir;
+  register_func_ = register_func;
+  min_time_between_refresh_usec_ = min_time_between_refresh_usec;
+
+  if (!Register_Locked(names_to_register))
+    return false;
+
+  // initial read of registered experiments states
+  Refresh();
+
+  initialized_ = true;
+  return true;
+}
+
+bool Experiments::Register(const std::vector<std::string> &names) {
+  if (!IsInitialized()) {
+    log("experiments:Cannot register, not initialized!");
+    return false;
+  }
+  return Register_Unlocked(names);
+}
+
+bool Experiments::Register_Unlocked(const std::vector<std::string> &names) {
+  std::lock_guard<std::mutex> lock_guard(lock_);
+  return Register_Locked(names);
+}
+
+bool Experiments::Register_Locked(const std::vector<std::string> &names) {
+  for (const auto &name : names) {
+    if (IsInRegisteredList(name)) {
+      log("experiments:'%s' already registered", name.c_str());
+      continue;
+    }
+
+    // call external register function
+    if (!register_func_(name.c_str()))
+      return false;  // no reason to continue
+
+    registered_experiments_.insert(name);
+    log("experiments:Registered '%s'", name.c_str());
+  }
+  return true;
+}
+
+bool Experiments::IsRegistered(const std::string &name) {
+  std::lock_guard<std::mutex> lock_guard(lock_);
+  return IsInRegisteredList(name);
+}
+
+bool Experiments::IsEnabled(const std::string &name) {
+  if (!IsInitialized())
+    return false;  // silent return to avoid log flooding
+
+  std::lock_guard<std::mutex> lock_guard(lock_);
+
+  if (us_elapse(last_time_refreshed_usec_) >= min_time_between_refresh_usec_) {
+    Refresh();
+  }
+
+  return IsInEnabledList(name);
+}
+
+void Experiments::Refresh() {
+  for (const auto &name : registered_experiments_)
+    UpdateState(name);
+  last_time_refreshed_usec_ = us_elapse(0);
+}
+
+void Experiments::UpdateState(const std::string &name) {
+  if (!IsInRegisteredList(name)) {
+    log("experiments:'%s' not registered", name.c_str());
+    return;
+  }
+
+  std::string file_path = config_dir_ + "/" + name + ".active";
+  bool was_enabled = IsInEnabledList(name);
+  bool is_enabled = file_exists(file_path.c_str());
+  if (is_enabled && !was_enabled) {
+    log("experiments:'%s' is now enabled", name.c_str());
+    enabled_experiments_.insert(name);
+  } else if (!is_enabled && was_enabled) {
+    log("experiments:'%s' is now disabled", name.c_str());
+    enabled_experiments_.erase(name);
+  }
+}
+
+
+// API for C programs
+int experiments_initialize(const char *config_dir,
+                           int64_t min_time_between_refresh_usec,
+                           experiments_register_func_t register_func) {
+  if (register_func == NULL)
+    register_func = DefaultExperimentsRegisterFunc;
+
+  experiments = new Experiments();
+  return experiments->Initialize(config_dir, min_time_between_refresh_usec,
+                                 register_func, {""});
+}
+
+int experiments_is_initialized() {
+  return experiments ? experiments->IsInitialized() : false;
+}
+
+int experiments_register(const char *name) {
+  return experiments ? experiments->Register(name) : false;
+}
+
+int experiments_is_registered(const char *name) {
+  return experiments ? experiments->IsRegistered(name) : false;
+}
+
+int experiments_is_enabled(const char *name) {
+  return experiments ? experiments->IsEnabled(name) : false;
+}
diff --git a/libexperiments/experiments.h b/libexperiments/experiments.h
new file mode 100644
index 0000000..54c6389
--- /dev/null
+++ b/libexperiments/experiments.h
@@ -0,0 +1,217 @@
+#ifndef _LIBEXPERIMENTS_EXPERIMENTS_H
+#define _LIBEXPERIMENTS_EXPERIMENTS_H
+
+#include <inttypes.h>
+
+
+// Implements a library that supports the Gfiber Experiments framework, as
+// explained in the following doc: go/gfiber-experiments-framework.
+//
+// Both C and C++ (class) implementations are available.
+//
+// C++ example:
+// ====================================
+//   const char* kConfigFolderPath[] = "/config/experiments";
+//   int64_t kMinTimeBetweenRefreshUs = 60 * 1000 * 1000;  // 60 secs
+//   e = new Experiments();
+//   if (!e->Initialize(kConfigFolderPath, kMinTimeBetweenRefreshUs,
+//                      {"exp1", "exp2"})) {
+//     // handle error case
+//   }
+//
+//   // later in the code
+//   if (e->IsEnabled("exp1")) {
+//     // exp1 is enabled
+//     [..]
+//   }
+//
+// C example:
+// ===================================
+//   const char* kConfigFolderPath[] = "/config/experiments";
+//   int64_t kMinTimeBetweenRefreshUs = 60 * 1000 * 1000;  // 60 secs
+//   if (!experiments_initialize(kConfigFolderPath, kMinTimeBetweenRefreshUs,
+//                               NULL);  // use default register function
+//     // handle error case
+//   }
+//
+//   experiments_register("exp1");
+//   experiments_register("exp2");
+//
+//   // later in the code
+//   if (experiments_is_enabled("exp1")) {
+//     // exp1 is enabled
+//     [..]
+//   }
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Function called when registering a new experiment.
+// Returns non-zero (boolean true) for success, else 0 (boolean false).
+typedef int (*experiments_register_func_t) (const char *name);
+
+// Default experiment register function. Calls the shell script
+// "register_experiment <name>".
+int DefaultExperimentsRegisterFunc(const char *name);
+
+// Dummy experiment register function. Just returns true.
+int DummyExperimentsRegisterFunc(const char *name);
+
+#ifdef __cplusplus
+}
+#endif
+
+
+//
+// C++ implementation
+//
+#ifdef __cplusplus
+
+#include <atomic>
+#include <mutex>  // NOLINT
+#include <set>
+#include <string>
+#include <vector>
+
+
+class Experiments {
+ public:
+  Experiments()
+      : initialized_(false),
+        min_time_between_refresh_usec_(0),
+        last_time_refreshed_usec_(0) {}
+  virtual ~Experiments() {}
+
+  // Initializes the instance:
+  // * Sets the provided experiments config directory and register function.
+  // * Calls the register function for the provided experiment names.
+  // * Scans the config folder to determine initial state of all registered
+  //   experiments.
+  // The min_time_between_refresh_usec values sets a lower boundary on how
+  // often the config folder is scanned for updated experiment states.
+  // Returns true if successful.
+  bool Initialize(const std::string &config_dir,
+                  int64_t min_time_between_refresh_usec,
+                  experiments_register_func_t register_func,
+                  const std::vector<std::string> &names_to_register);
+  // Convenience version, using default experiments register function.
+  bool Initialize(const std::string &config_dir,
+                  int64_t min_time_between_refresh_usec,
+                  const std::vector<std::string> &names_to_register) {
+    return Initialize(config_dir, min_time_between_refresh_usec,
+                      &DefaultExperimentsRegisterFunc, names_to_register);
+  }
+
+  bool IsInitialized() const { return initialized_; }
+
+  // Registers the provided experiment(s).
+  bool Register(const std::vector<std::string> &names);
+  bool Register(const std::string &name) {
+    std::vector<std::string> names{name};
+    return Register(names);
+  }
+
+  // Returns true if the given experiment is registered.
+  bool IsRegistered(const std::string &name);
+
+  // Returns true if the given experiment is active, else false. If the minimum
+  // time between refreshes has passed, re-scans the config folder for updates
+  // first.
+  bool IsEnabled(const std::string &name);
+
+ private:
+  // Registers the given experiments. Unlocked version takes lock_ first.
+  // Returns true if successful, else false.
+  bool Register_Unlocked(const std::vector<std::string> &names);
+  bool Register_Locked(const std::vector<std::string> &names);
+
+  // Returns true if the given experiment is in the list of registered
+  // experiments.
+  bool IsInRegisteredList(const std::string &name) const {
+    return registered_experiments_.find(name) != registered_experiments_.end();
+  }
+
+  // Refreshes all registered experiment states by scanning the config folder.
+  void Refresh();
+
+  // Updates the state of the given experiment by checking its file in the
+  // config folder.
+  void UpdateState(const std::string &name);
+
+  // Returns true if the given experiment is in the list of enabled
+  // experiments.
+  bool IsInEnabledList(const std::string &name) {
+    return enabled_experiments_.find(name) != enabled_experiments_.end();
+  }
+
+  std::atomic<bool> initialized_;
+  std::mutex lock_;
+
+  // Experiments config folder, containing the system-wide list of experiments.
+  // An experiment is marked active if the folder contains the file named
+  // "<experiment_name>.active".
+  std::string config_dir_;
+
+  // External function called to register an experiment.
+  experiments_register_func_t register_func_;
+
+  std::set<std::string> registered_experiments_;
+  std::set<std::string> enabled_experiments_;
+
+  // Minimum time between accessing the config folder to refresh the experiment
+  // states. When set to 0 it refreshes on every call to IsEnabled().
+  uint64_t min_time_between_refresh_usec_;
+  uint64_t last_time_refreshed_usec_;
+};
+
+extern Experiments *experiments;
+
+#endif  // __cplusplus
+
+
+//
+// C-based API
+//
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Creates and initializes the experiments object:
+// * Sets the provided experiments config directory and register function.
+// * Calls the register function for the provided experiment names.
+// * Scans the config folder to determine initial state of all registered
+//   experiments.
+// The min_time_between_refresh_usec values sets a lower boundary on how often
+// the config folder is scanned for updated experiment states. Set
+// register_func to NULL to use the default register function
+// (DefaultExperimentsRegisterFunc()).
+// Returns non-zero (boolean true) if successful, 0 (boolean false) for error.
+int experiments_initialize(const char *config_dir,
+                            int64_t min_time_between_refresh_usec,
+                            experiments_register_func_t register_func);
+
+// Returns non-zero (boolean true) if the experiments object is initialized,
+// else 0 (boolean false).
+int experiments_is_initialized();
+
+// Registers the provided experiment.
+// Returns non-zero (boolean true) if successful, 0 (boolean false) for error.
+int experiments_register(const char *name);
+
+// Returns non-zero (boolean true) if the given experiment name is registered,
+// else 0 (boolean false).
+int experiments_is_registered(const char *name);
+
+// Returns non-zero (boolean true) if the given experiment is active, else 0
+// (boolean false). If the minimum time between refreshes has passed, re-scans
+// the config folder for updates first.
+int experiments_is_enabled(const char *name);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  // _LIBEXPERIMENTS_EXPERIMENTS_H
diff --git a/libexperiments/experiments_c_api_test.c b/libexperiments/experiments_c_api_test.c
new file mode 100644
index 0000000..fc90849
--- /dev/null
+++ b/libexperiments/experiments_c_api_test.c
@@ -0,0 +1,21 @@
+#include "experiments_c_api_test.h"
+
+int test_experiments_initialize(const char *config_dir) {
+  return experiments_initialize(config_dir, 0, DummyExperimentsRegisterFunc);
+}
+
+int test_experiments_is_initialized() {
+  return experiments_is_initialized();
+}
+
+int test_experiments_register(const char *name) {
+  return experiments_register(name);
+}
+
+int test_experiments_is_registered(const char *name) {
+  return experiments_is_registered(name);
+}
+
+int test_experiments_is_enabled(const char *name) {
+  return experiments_is_enabled(name);
+}
diff --git a/libexperiments/experiments_c_api_test.h b/libexperiments/experiments_c_api_test.h
new file mode 100644
index 0000000..bed21b6
--- /dev/null
+++ b/libexperiments/experiments_c_api_test.h
@@ -0,0 +1,24 @@
+#ifndef _LIBEXPERIMENTS_EXPERIMENTS_C_API_TEST_H
+#define _LIBEXPERIMENTS_EXPERIMENTS_C_API_TEST_H
+
+// Provides C-compiled functions to test the C-API functionality. The main
+// purpose of this is to verify that one can use libexperiments from a purely C
+// environment.
+
+#include "experiments.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+int test_experiments_initialize(const char *config_dir);
+int test_experiments_is_initialized();
+int test_experiments_register(const char *name);
+int test_experiments_is_registered(const char *name);
+int test_experiments_is_enabled(const char *name);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  // _LIBEXPERIMENTS_EXPERIMENTS_C_API_TEST_H
diff --git a/libexperiments/experiments_test.cc b/libexperiments/experiments_test.cc
new file mode 100644
index 0000000..e0370cc
--- /dev/null
+++ b/libexperiments/experiments_test.cc
@@ -0,0 +1,280 @@
+#include <gtest/gtest.h>
+
+#include "experiments.h"
+#include "experiments_c_api_test.h"
+
+#include <fcntl.h>
+#include <linux/limits.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include "utils.h"
+
+using namespace libexperiments_utils;  // NOLINT
+
+int FailingExperimentsRegisterFunc(const char *name) {
+  return false;
+}
+
+class ExperimentsTest : public ::testing::Test {
+ protected:
+  static void SetUpTestCase() {
+    ASSERT_TRUE(realpath(".", root_path_));
+    snprintf(test_folder_path_, sizeof(test_folder_path_), "%s/exps-XXXXXX",
+             root_path_);
+    char strerrbuf[1024] = {'\0'};
+    ASSERT_TRUE(mkdtemp(test_folder_path_)) <<
+        strerror_r(errno, strerrbuf, sizeof(strerrbuf)) << "(" << errno << ")";
+    ASSERT_EQ(chdir(test_folder_path_), 0);
+  }
+
+  static void TearDownTestCase() {
+    // change out of the test directory and remove it
+    ASSERT_EQ(chdir(root_path_), 0);
+    std::string cmd = StringPrintf("rm -r %s", test_folder_path_);
+    ASSERT_EQ(0, system(cmd.c_str()));
+  }
+
+  bool CreateFile(const std::string &name) {
+    int fd = open(name.c_str(), O_CREAT | O_TRUNC,
+                  S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
+    if (fd < 0) {
+      log_perror(errno, "Cannot create file '%s':", name.c_str());
+      return false;
+    } else {
+      close(fd);
+    }
+    return true;
+  }
+
+  bool RenameFile(const std::string &from_name, const std::string &to_name) {
+    if (rename(from_name.c_str(), to_name.c_str()) < 0) {
+      log_perror(errno, "Cannot rename file '%s' to '%s':", from_name.c_str(),
+                 to_name.c_str());
+      return false;
+    }
+    return true;
+  }
+
+  bool DeleteFile(const std::string &name) {
+    if (remove(name.c_str()) < 0) {
+      log_perror(errno, "Cannot delete file '%s':", name.c_str());
+      return false;
+    }
+    return true;
+  }
+
+  bool SwitchFromTo(Experiments *e, const std::string &name,
+                    const std::string &from_ext, const std::string &to_ext) {
+    std::string from_file = name + from_ext;
+    std::string to_file = name + to_ext;
+    if (file_exists(from_file.c_str())) {
+      return RenameFile(from_file, to_file);
+    } else {
+      return CreateFile(to_file);
+    }
+  }
+
+  bool SetActive(Experiments *e, const std::string &name) {
+    return SwitchFromTo(e, name, ".inactive", ".active");
+  }
+
+  bool SetInactive(Experiments *e, const std::string &name) {
+    return SwitchFromTo(e, name, ".active", ".inactive");
+  }
+
+  bool Remove(Experiments *e, const std::string &name) {
+    std::string active_file = name + ".active";
+    if (file_exists(active_file.c_str())) {
+      if (!DeleteFile(active_file)) {
+        return false;
+      }
+    }
+    std::string inactive_file = name + ".inactive";
+    if (file_exists(inactive_file.c_str())) {
+      if (!DeleteFile(inactive_file)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  static char root_path_[PATH_MAX];
+  static char test_folder_path_[PATH_MAX];
+};
+
+char ExperimentsTest::test_folder_path_[PATH_MAX] = {0};
+char ExperimentsTest::root_path_[PATH_MAX] = {0};
+
+
+TEST_F(ExperimentsTest, InvalidConfigPath) {
+  Experiments e;
+  char invalid_path[1024];
+  snprintf(invalid_path, sizeof(invalid_path), "%s/nope", test_folder_path_);
+  ASSERT_FALSE(e.Initialize(invalid_path, 0, &DummyExperimentsRegisterFunc,
+                            {"exp1"}));
+}
+
+TEST_F(ExperimentsTest, InvalidRegisterFunc) {
+  Experiments e;
+  ASSERT_FALSE(e.Initialize(test_folder_path_, 0, NULL, {"exp1"}));
+}
+
+TEST_F(ExperimentsTest, RegisterFuncFails) {
+  Experiments e;
+  ASSERT_FALSE(e.Initialize(test_folder_path_, 0,
+                            &FailingExperimentsRegisterFunc, {"exp1"}));
+}
+
+TEST_F(ExperimentsTest, Register) {
+  Experiments e;
+  ASSERT_TRUE(e.Initialize(test_folder_path_, 0, &DummyExperimentsRegisterFunc,
+                           {"exp1"}));
+  EXPECT_TRUE(e.IsRegistered("exp1"));
+
+  // add one more
+  EXPECT_FALSE(e.IsRegistered("exp2"));
+  EXPECT_TRUE(e.Register("exp2"));
+  EXPECT_TRUE(e.IsRegistered("exp1"));
+  EXPECT_TRUE(e.IsRegistered("exp2"));
+
+  // repeated registration is ignored
+  EXPECT_TRUE(e.Register("exp2"));
+  EXPECT_TRUE(e.IsRegistered("exp1"));
+  EXPECT_TRUE(e.IsRegistered("exp2"));
+
+  // register vector
+  EXPECT_FALSE(e.IsRegistered("exp3"));
+  EXPECT_FALSE(e.IsRegistered("exp4"));
+  EXPECT_FALSE(e.IsRegistered("exp5"));
+  EXPECT_TRUE(e.Register({"exp3", "exp4", "exp5"}));
+  EXPECT_TRUE(e.IsRegistered("exp1"));
+  EXPECT_TRUE(e.IsRegistered("exp2"));
+  EXPECT_TRUE(e.IsRegistered("exp3"));
+  EXPECT_TRUE(e.IsRegistered("exp4"));
+  EXPECT_TRUE(e.IsRegistered("exp5"));
+}
+
+TEST_F(ExperimentsTest, Single) {
+  Experiments e;
+  ASSERT_TRUE(e.Initialize(test_folder_path_, 0, &DummyExperimentsRegisterFunc,
+                           {"exp1"}));
+  EXPECT_FALSE(e.IsEnabled("exp1"));
+
+  EXPECT_TRUE(SetActive(&e, "exp1"));
+  EXPECT_TRUE(e.IsEnabled("exp1"));
+
+  EXPECT_TRUE(SetInactive(&e, "exp1"));
+  EXPECT_FALSE(e.IsEnabled("exp1"));
+
+  EXPECT_TRUE(SetActive(&e, "exp1"));
+  EXPECT_TRUE(e.IsEnabled("exp1"));
+
+  EXPECT_TRUE(Remove(&e, "exp1"));
+  EXPECT_FALSE(e.IsEnabled("exp1"));
+}
+
+TEST_F(ExperimentsTest, Multiple) {
+  Experiments e;
+  ASSERT_TRUE(e.Initialize(test_folder_path_, 0, &DummyExperimentsRegisterFunc,
+                           {"exp1", "exp2", "exp3"}));
+  EXPECT_FALSE(e.IsEnabled("exp1"));
+  EXPECT_FALSE(e.IsEnabled("exp2"));
+  EXPECT_FALSE(e.IsEnabled("exp3"));
+
+  // activate exp1 - AII
+  EXPECT_TRUE(SetActive(&e, "exp1"));
+  EXPECT_TRUE(e.IsEnabled("exp1"));
+  EXPECT_FALSE(e.IsEnabled("exp2"));
+  EXPECT_FALSE(e.IsEnabled("exp3"));
+  // activate exp2 - AAI
+  EXPECT_TRUE(SetActive(&e, "exp2"));
+  EXPECT_TRUE(e.IsEnabled("exp1"));
+  EXPECT_TRUE(e.IsEnabled("exp2"));
+  EXPECT_FALSE(e.IsEnabled("exp3"));
+  // active exp3 - AAA
+  EXPECT_TRUE(SetActive(&e, "exp3"));
+  EXPECT_TRUE(e.IsEnabled("exp1"));
+  EXPECT_TRUE(e.IsEnabled("exp2"));
+  EXPECT_TRUE(e.IsEnabled("exp3"));
+  // inactivate exp2 - AIA
+  EXPECT_TRUE(SetInactive(&e, "exp2"));
+  EXPECT_TRUE(e.IsEnabled("exp1"));
+  EXPECT_FALSE(e.IsEnabled("exp2"));
+  EXPECT_TRUE(e.IsEnabled("exp3"));
+  // remove exp1 file - IIA
+  EXPECT_TRUE(Remove(&e, "exp1"));
+  EXPECT_FALSE(e.IsEnabled("exp1"));
+  EXPECT_FALSE(e.IsEnabled("exp2"));
+  EXPECT_TRUE(e.IsEnabled("exp3"));
+  // re-activate exp2 - IAA
+  EXPECT_TRUE(SetActive(&e, "exp2"));
+  EXPECT_FALSE(e.IsEnabled("exp1"));
+  EXPECT_TRUE(e.IsEnabled("exp2"));
+  EXPECT_TRUE(e.IsEnabled("exp3"));
+  // inactivate exp1 (re-create file) - IAA
+  EXPECT_TRUE(SetInactive(&e, "exp1"));
+  EXPECT_FALSE(e.IsEnabled("exp1"));
+  EXPECT_TRUE(e.IsEnabled("exp2"));
+  EXPECT_TRUE(e.IsEnabled("exp3"));
+  // remove all - III
+  EXPECT_TRUE(Remove(&e, "exp1"));
+  EXPECT_TRUE(Remove(&e, "exp2"));
+  EXPECT_TRUE(Remove(&e, "exp3"));
+  EXPECT_FALSE(e.IsEnabled("exp1"));
+  EXPECT_FALSE(e.IsEnabled("exp2"));
+  EXPECT_FALSE(e.IsEnabled("exp3"));
+}
+
+TEST_F(ExperimentsTest, TimeBetweenRefresh) {
+  int64_t kMinTimeBetweenRefresh = secs_to_usecs(3);
+  int64_t kTimeout =  secs_to_usecs(5);
+  uint64_t start_time = us_elapse(0);
+  Experiments e;
+  ASSERT_TRUE(e.Initialize(test_folder_path_, kMinTimeBetweenRefresh,
+                           &DummyExperimentsRegisterFunc, {"exp1"}));
+  EXPECT_FALSE(e.IsEnabled("exp1"));
+  EXPECT_TRUE(SetActive(&e, "exp1"));
+
+  // measure time until we see "exp1" active
+  uint64_t duration = us_elapse(start_time);
+  while (!e.IsEnabled("exp1") && duration < kTimeout) {
+    us_sleep(100);
+    duration = us_elapse(start_time);
+  }
+
+  EXPECT_GE(duration, kMinTimeBetweenRefresh) << "time:" << duration;
+  EXPECT_LT(duration, kTimeout) << "time:" << duration;
+
+  // clean up
+  EXPECT_TRUE(Remove(&e, "exp1"));
+}
+
+TEST_F(ExperimentsTest, C_API_Test) {
+  // returns false on all API functions until initialized is called
+  EXPECT_FALSE(test_experiments_is_initialized());
+  EXPECT_FALSE(test_experiments_register("exp1"));
+  EXPECT_FALSE(test_experiments_is_registered("exp1"));
+  EXPECT_FALSE(test_experiments_is_enabled("exp1"));
+  EXPECT_TRUE(SetActive(experiments, "exp1"));
+  EXPECT_FALSE(test_experiments_is_enabled("exp1"));
+  EXPECT_TRUE(Remove(experiments, "exp1"));
+
+  // initialize
+  EXPECT_TRUE(test_experiments_initialize(test_folder_path_));
+  EXPECT_TRUE(test_experiments_is_initialized());
+
+  EXPECT_TRUE(test_experiments_register("exp1"));
+  EXPECT_TRUE(test_experiments_is_registered("exp1"));
+
+  EXPECT_FALSE(test_experiments_is_enabled("exp1"));
+  EXPECT_TRUE(SetActive(experiments, "exp1"));
+  EXPECT_TRUE(test_experiments_is_enabled("exp1"));
+  EXPECT_FALSE(test_experiments_is_enabled("exp2"));
+
+  EXPECT_TRUE(SetInactive(experiments, "exp1"));
+  EXPECT_FALSE(test_experiments_is_enabled("exp1"));
+
+  // clean up
+  EXPECT_TRUE(Remove(experiments, "exp1"));
+}
diff --git a/libexperiments/utils.cc b/libexperiments/utils.cc
new file mode 100644
index 0000000..b82d34b
--- /dev/null
+++ b/libexperiments/utils.cc
@@ -0,0 +1,300 @@
+#include "utils.h"
+
+#include <errno.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+namespace libexperiments_utils {
+
+void log(const char* cstr, ...) {
+  va_list va;
+  va_start(va, cstr);
+  vprintf(cstr, va);
+  va_end(va);
+  printf("\n");
+  fflush(stdout);
+}
+
+void log_perror(int err, const char* cstr, ...) {
+  va_list va;
+  va_start(va, cstr);
+  vprintf(cstr, va);
+  va_end(va);
+  char strerrbuf[1024] = {'\0'};
+  printf("'%s'[%d]\n", strerror_r(err, strerrbuf, sizeof(strerrbuf)), err);
+  fflush(stdout);
+}
+
+uint64_t us_elapse(uint64_t start_time_us) {
+  struct timespec tv;
+  clock_gettime(CLOCK_MONOTONIC, &tv);
+  return tv.tv_sec * kUsecsPerSec + tv.tv_nsec / kNsecsPerUsec - start_time_us;
+}
+
+void us_sleep(uint64_t usecs) {
+  uint64_t nsecs = kNsecsPerUsec * usecs;
+  struct timespec tv;
+  // tv_nsec field must be [0..kNsecsPerSec-1]
+  tv.tv_sec = nsecs / kNsecsPerSec;
+  tv.tv_nsec = nsecs % kNsecsPerSec;
+  nanosleep(&tv, NULL);
+}
+
+// Maximum output (stdout+stderr) accepted by run_cmd()
+const int kMaxRunCmdOutput = 4 * 1024 * 1024;
+
+static int nice_snprintf(char *str, size_t size, const char *format, ...) {
+  va_list ap;
+  int bi;
+  va_start(ap, format);
+  // http://stackoverflow.com/a/100991
+  bi = vsnprintf(str, size, format, ap);
+  va_end(ap);
+  if (bi > size) {
+    // From printf(3):
+    // "snprintf() [returns] the number of characters (not including the
+    // trailing '\0') which would have been written to the final string
+    // if enough space had been available" [printf(3)]
+    bi = size;
+  }
+  return bi;
+}
+
+/* snprintf's a run_cmd command */
+int snprintf_cmd(char *buf, int bufsize, const std::vector<std::string> &cmd) {
+  int bi = 0;
+
+  // ensure we always return something valid
+  buf[0] = '\0';
+  for (const auto &item : cmd) {
+    bool blanks = (item.find_first_of(" \n\r\t") != std::string::npos);
+    if (blanks)
+      bi += nice_snprintf(buf+bi, bufsize-bi, "\"");
+    for (int i = 0; i < item.length() && bi < bufsize; ++i) {
+      if (isprint(item.at(i)))
+        bi += nice_snprintf(buf+bi, bufsize-bi, "%c", item.at(i));
+      else if (item.at(i) == '\n')
+        bi += nice_snprintf(buf+bi, bufsize-bi, "\\n");
+      else
+        bi += nice_snprintf(buf+bi, bufsize-bi, "\\x%02x", item.at(i));
+    }
+    if (blanks)
+      bi += nice_snprintf(buf+bi, bufsize-bi, "\"");
+    bi += nice_snprintf(buf+bi, bufsize-bi, " ");
+  }
+  return bi;
+}
+
+int run_cmd(const std::vector<std::string> &cmd, const std::string &in,
+            int *status,
+            std::ostream *out,
+            std::ostream *err,
+            int64_t timeout_usec) {
+  if (cmd.empty() || cmd[0].empty()) {
+    *status = -1;
+    return -1;
+  }
+
+  int pipe_in[2];
+  int pipe_out[2];
+  int pipe_err[2];
+  int pid;
+
+  // init the 3 pipes
+  int ret;
+  for (auto the_pipe : { pipe_in, pipe_out, pipe_err }) {
+    if ((ret = pipe(the_pipe)) < 0) {
+      log_perror(errno, "run_cmd:Error-pipe failed-");
+      return -1;
+    }
+  }
+
+  char cmd_buf[1024];
+  snprintf_cmd(cmd_buf, sizeof(cmd_buf), cmd);
+  log("run_cmd:running command: %s", cmd_buf);
+
+  pid = fork();
+  if (pid == 0) {
+    // child: set stdin/stdout/stderr
+    dup2(pipe_in[0], STDIN_FILENO);
+    dup2(pipe_out[1], STDOUT_FILENO);
+    dup2(pipe_err[1], STDERR_FILENO);
+
+    // close unused pipe ends
+    close(pipe_in[1]);
+    close(pipe_out[0]);
+    close(pipe_err[0]);
+    close(pipe_in[0]);
+    close(pipe_out[1]);
+    close(pipe_err[1]);
+
+    // convert strings to "const char *" and "char * []"
+    const char *file = cmd[0].c_str();
+    char *argv[cmd.size() + 1];
+    for (int i = 0; i < cmd.size(); ++i)
+      argv[i] = const_cast<char *>(cmd[i].c_str());
+    argv[cmd.size()] = NULL;
+    // run command
+    execvp(file, argv);
+    // exec() functions return only if an error has occurred
+    _exit(errno);
+  }
+
+  // parent: close unused pipe ends
+  close(pipe_in[0]);
+  close(pipe_out[1]);
+  close(pipe_err[1]);
+  // process stdin
+  if (!in.empty()) {
+    if ((ret = write(pipe_in[1], in.c_str(), in.length())) < in.length()) {
+      log_perror(errno, "run_cmd:Error-write() failed-");
+      // kill the child
+      kill(pid, SIGKILL);
+      wait(NULL);
+      return -4;
+    }
+  }
+  close(pipe_in[1]);
+
+  // start reading stdout/stderr
+  struct FancyPipe {
+      int fd;
+      std::ostream *stream_ptr;
+  } fancypipes[] = {
+      { pipe_out[0], out },
+      { pipe_err[0], err },
+  };
+  fd_set fdread;
+  char buf[1024];
+
+  int total_output = 0;
+  int retcode = 0;
+  while (fancypipes[0].fd >= 0 || fancypipes[1].fd >= 0) {
+    if (total_output > kMaxRunCmdOutput) {
+      log("run_cmd:Error-command output is too large (%i bytes > %i)",
+          total_output, kMaxRunCmdOutput);
+      // kill the child
+      kill(pid, SIGKILL);
+      retcode = -3;
+      break;
+    }
+    FD_ZERO(&fdread);
+    int max_fd = -1;
+    struct timeval tv, *timeout = NULL;
+    if (timeout_usec >= 0) {
+      tv.tv_sec = timeout_usec / 1000000;
+      tv.tv_usec = (timeout_usec % 1000000) * 1000000;
+      timeout = &tv;
+    }
+    for (const auto &fancypipe : fancypipes) {
+      if (fancypipe.fd >= 0) {
+        FD_SET(fancypipe.fd, &fdread);
+        max_fd = MAX(max_fd, fancypipe.fd);
+      }
+    }
+    int select_ret = select(max_fd + 1, &fdread, NULL, NULL, timeout);
+    if (select_ret == 0) {
+      // timeout
+      log("run_cmd:Error-command timed out");
+      // kill the child
+      kill(pid, SIGKILL);
+      retcode = -2;
+      break;
+    } else if (select_ret == -1) {
+      if (errno == EINTR) {
+        // interrupted by signal
+        retcode = -1;
+        break;
+      }
+      log_perror(errno, "run_cmd:Error-pipe select failed-");
+      retcode = -1;
+      break;
+    }
+    for (auto &fancypipe : fancypipes) {
+      if (fancypipe.fd >= 0 && FD_ISSET(fancypipe.fd, &fdread)) {
+        ssize_t len;
+        len = read(fancypipe.fd, buf, sizeof(buf));
+        if (len <= 0) {
+          close(fancypipe.fd);
+          fancypipe.fd = -1;
+          if (len < 0)
+            retcode = -1;
+          continue;
+        }
+        total_output += len;
+        if (fancypipe.stream_ptr)
+          fancypipe.stream_ptr->write(buf, len);
+      }
+    }
+  }
+
+  *status = 0;
+  wait(status);
+  // interpret child exit status
+  if (WIFEXITED(*status))
+    *status = WEXITSTATUS(*status);
+  return retcode;
+}
+
+//
+// String printf functions, ported from stringprintf.cc/h
+//
+
+void StringAppendV(std::string* dst, const char* format, va_list ap) {
+  // First try with a small fixed size buffer
+  static const int kSpaceLength = 1024;
+  char space[kSpaceLength];
+
+  // It's possible for methods that use a va_list to invalidate
+  // the data in it upon use.  The fix is to make a copy
+  // of the structure before using it and use that copy instead.
+  va_list backup_ap;
+  va_copy(backup_ap, ap);
+  int result = vsnprintf(space, kSpaceLength, format, backup_ap);
+  va_end(backup_ap);
+
+  if (result < kSpaceLength) {
+    if (result >= 0) {
+      // Normal case -- everything fit.
+      dst->append(space, result);
+      return;
+    }
+    if (result < 0) {
+      // Just an error.
+      return;
+    }
+  }
+
+  // Increase the buffer size to the size requested by vsnprintf,
+  // plus one for the closing \0.
+  int length = result+1;
+  char* buf = new char[length];
+
+  // Restore the va_list before we use it again
+  va_copy(backup_ap, ap);
+  result = vsnprintf(buf, length, format, backup_ap);
+  va_end(backup_ap);
+
+  if (result >= 0 && result < length) {
+    // It fit
+    dst->append(buf, result);
+  }
+  delete[] buf;
+}
+
+std::string StringPrintf(const char* format, ...) {
+  va_list ap;
+  va_start(ap, format);
+  std::string result;
+  StringAppendV(&result, format, ap);
+  va_end(ap);
+  return result;
+}
+
+}  // namespace libexperiments_utils
diff --git a/libexperiments/utils.h b/libexperiments/utils.h
new file mode 100644
index 0000000..3c1adb4
--- /dev/null
+++ b/libexperiments/utils.h
@@ -0,0 +1,83 @@
+#ifndef _LIBEXPERIMENTS_UTILS_H_
+#define _LIBEXPERIMENTS_UTILS_H_
+
+#include <stdint.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <string>
+#include <ostream>
+#include <vector>
+
+//
+// Subset of utils functions copied from vendor/google/mcastcapture/utils/.
+//
+
+// Namespace is needed to avoid conflicts when other modules with identical
+// function names are linking against libexperiments.so.
+namespace libexperiments_utils {
+
+#define MAX(x, y) ((x) > (y) ? (x) : (y))
+#define MIN(x, y) ((x) < (y) ? (x) : (y))
+
+const int64_t kUsecsPerSec = 1000000LL;
+const int64_t kNsecsPerSec = 1000000000LL;
+const int64_t kNsecsPerUsec = 1000LL;
+
+static inline int64_t secs_to_usecs(int64_t secs) {
+  return secs * kUsecsPerSec;
+}
+
+void log(const char* cstr, ...)
+    __attribute__((format(printf, 1, 2)));
+
+void log_perror(int err, const char* cstr, ...)
+    __attribute__((format(printf, 2, 3)));
+
+// Measures elapsed time in usecs.
+uint64_t us_elapse(uint64_t start_time_us);
+
+void us_sleep(uint64_t usecs);
+
+static inline bool file_exists(const char *name) {
+  return access(name, F_OK) == 0;
+}
+
+static inline bool directory_exists(const char *path) {
+  struct stat dir_stat;
+  if (stat(path, &dir_stat) != 0) {
+    return false;
+  }
+  return S_ISDIR(dir_stat.st_mode);
+}
+
+// This function runs the command cmd (but not in a shell), providing the
+// return code, stdout, and stderr. It also allows to specify the stdin,
+// and a command timeout value (timeout_usec, use <0 to block indefinitely).
+// Note that the timeout is fired only if the command stops producing
+// either stdout or stderr. A process that periodically produces output
+// will never be killed.
+// Returns an extended error code:
+// - 0 if everything is successful,
+//  -1 if any step fails,
+//  -2 if timeout, and
+//  -3 if the command output (stdout+stderr) is too large (kMaxRunCmdOutput).
+int run_cmd(const std::vector<std::string> &cmd, const std::string &in,
+            int *status,
+            std::ostream *out,
+            std::ostream *err,
+            int64_t timeout_usec);
+
+//
+// String printf functions, ported from stringprintf.cc/h
+//
+
+// Return a C++ string
+std::string StringPrintf(const char* format, ...)
+    __attribute__((format(printf, 1, 2)));
+
+}  // namespace libexperiments_utils
+
+#endif  // _LIBEXPERIMENTS_UTILS_H_