<commit_before>#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2005 onwards University of Deusto
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution.
#
# This software consists of contributions made by many individuals,
# listed below:
#
# Author: <NAME> <<EMAIL>>
#         <NAME> <<EMAIL>>
#

import weblab.experiment.concurrent_experiment as ConcurrentExperiment

import os
import sys
import glob
import httplib
import urllib2
import urllib
import threading
import traceback
import time
import random
import xml.dom.minidom as xml
import json

from voodoo.log import logged
from voodoo.override import Override

import voodoo.sessions.manager as SessionManager
import voodoo.sessions.session_type as SessionType
from voodoo.sessions.exc import SessionNotFoundError
from voodoo.typechecker import typecheck, ANY

CFG_USE_VISIR_PHP = "vt_use_visir_php"

CFG_MEASURE_SERVER_ADDRESS = "vt_measure_server_addr"
CFG_MEASURE_SERVER_TARGET = "vt_measure_server_target"
CFG_LOGIN_URL = "vt_login_url"
CFG_BASE_URL = "vt_base_url"
CFG_LOGIN_EMAIL = "vt_login_email"
CFG_LOGIN_PASSWORD = "<PASSWORD>"
CFG_SAVEDATA = "vt_savedata"
CFG_TEACHER  = "vt_teacher"
CFG_CLIENT_URL = "vt_client_url"
CFG_HEARTBEAT_PERIOD = "vt_heartbeat_period"
CFG_CIRCUITS = "vt_circuits"
CFG_CIRCUITS_DIR = "vt_circuits_dir"
CFG_DEBUG_PRINTS = "vt_debug_prints"
CFG_LIBRARY_XML  = "vt_library"

DEFAULT_USE_VISIR_PHP = True
DEFAULT_MEASURE_SERVER_ADDRESS = "172.16.58.3:8080"
DEFAULT_MEASURE_SERVER_TARGET = "/measureserver"
DEFAULT_LOGIN_URL = """https://weblab-visir.deusto.es/electronics/student.php"""
DEFAULT_BASE_URL = """https://weblab-visir.deusto.es/"""
DEFAULT_LOGIN_EMAIL = "guest"
DEFAULT_LOGIN_PASSWORD = "<PASSWORD>"
DEFAULT_SAVEDATA = ""
DEFAULT_TEACHER  = True
DEFAULT_CLIENT_URL = "../web/visir/loader.swf"
DEFAULT_HEARTBEAT_PERIOD = 30
DEFAULT_CIRCUITS = {}
DEFAULT_DEBUG_PRINTS = False


HEARTBEAT_REQUEST = """<protocol version="1.3"><request "%s"/></protocol>"""
HEARTBEAT_MAX_SLEEP = 5

# Actually defined through the configuration.
DEBUG = None

class Heartbeater(threading.Thread):
    """
    The Heartbeater is a different thread, which will periodically send a heartbeat
    request if no other requests have been sent recently.
    """

    @typecheck(ANY, basestring, int, basestring, SessionManager.SessionManager)
    def __init__(self, experiment, heartbeat_period, session_manager):
        """
        Creates the Heartbeater object. The Heartbeater is a thread which will periodically
        send requests as heartbeats (because actual heartbeat or even login requests do not work).

        There should be a single Heartbeater, which will send the periodical requests to every
        user that needs so.

        The heartbeat may be inhibited through periodical tick() calls.

        For the Heartbeater to start working, it needs to be started through start(). To stop it,
        stop() must be called. It is noteworthy that stop is not immediate.

        Every heartbeat will be carried out from the same thread, sequentially. Hence, especially
        with a high number of users, it may theoretically take a long time to send a specific
        heartbeat. This should not be an issue unless the heartbeat frequency is set too low.

        @param experiment Reference to the VisirTestExperiment. Will make use of the user map within it.
        @param heartbeat_period Number of seconds between heartbeats.
        @param session_manager The session manager from which retrieve the sessions

        @see start
        @see stop
        @see tick
        """
        threading.Thread.__init__(self)
        self.is_stopped = False
        self.experiment = experiment
        self.heartbeat_period = heartbeat_period
        self.session_manager  = session_manager

    def stop(self):
        """
        Stops the thread. The thread is not stopped immediately. Instead, a flag is
        internally set and the actual thread will finish when possible.
        Once called, stopped() will return true (without waiting for the thread to
        actually finish).

        @see stopped
        """
        self.is_stopped = True

    def stopped(self):
        """
        Returns true if the thread has been explicitly stopped. False otherwise.
        Note that before the thread has been started, stopped will still be false.
        That is, it will start returning true just as soon as stop is called.

        @see stop
        @see is_alive
        """
        return self.is_stopped

    def tick(self, session_object):
        """
        tick()
        Ticks to update the time for a given session. If the session has not been registered
        the tick operation does nothing.

        @param lab_session_id Session id of the user counter to tick
        Should be called, both internally or externally, whenever a heartbeat or any
        other packet is sent to reset OR INITIALIZE the heartbeat timer.
        """

        # If the session id is not registered within the sessions map, we will simply
        # return without doing anything. This can happen for the initial login request.
        session_object['last_heartbeat_sent'] = time.time()
        if DEBUG: print "[DBG] HB TICK"


    def run(self):
        """
        run()

        Thread process. Thread starts running here when start() is called.
        @see start
        """

        if DEBUG: dbg("[DBG] HB INIT")

        while(True):
            if self.stopped():
                return
            try:
                # Sleep at most HEARTBEAT_MAX_SLEEP. We will most likely overwrite this
                # with a lower value, depending on the pending requests.
                time_to_sleep = HEARTBEAT_MAX_SLEEP

                # Loop through every user that is using the experiment concurrently, and consider
                # whether we should update it or not.
                for session_id in self.session_manager.list_sessions():
                    try:
                        sessiondata = self.session_manager.get_session_locking(session_id)
                    except SessionNotFoundError:
                        continue


                    if 'sessionkey' not in sessiondata:
                        self.session_manager.modify_session_unlocking(session_id, sessiondata)
                        continue

                    try:

                        # Evaluate the time left for the next potential heartbeat.
                        if 'last_heartbeat_sent' not in sessiondata:
                            # If we actually don't have a last_heartbeat_sent yet,
                            # initialize it.
                            sessiondata['last_heartbeat_sent'] = time.time()
                        last_sent = sessiondata['last_heartbeat_sent']
                        session_key = sessiondata['sessionkey']
                    finally:
                        self.session_manager.modify_session_unlocking(session_id, sessiondata)

                    time_left = (last_sent + self.heartbeat_period) - time.time()

                    # If time_left is zero or negative, a heartbeat IS due.
                    if(time_left <= 0):
                        if DEBUG_HEARTBEAT_MESSAGES: dbg("[DBG] HB FORWARDING")
                        ret = self.experiment.forward_request(session_id, HEARTBEAT_REQUEST % (session_key))
                        if DEBUG_HEARTBEAT_MESSAGES: dbg("[DBG] Heartbeat response: %s" % ret)

                    else:
                        # Otherwise, we will just sleep.
                        if DEBUG_HEARTBEAT_MESSAGES: print "[DBG] HB SLEEPING FOR %d" % (time_left)

                        # We wish to sleep the maximum only if there are no pending
                        # requests (and if it doesn't go over the MAX).
                        time_to_sleep = min(time_left, time_to_sleep)

                # We will actually not sleep the whole time_to_sleep at once, so that
                # we can check whether the thread has finished more often.
                # TODO: Consider doing this through semaphores so that we do not need
                # this work-around.
                step_time = 0.1
                steps = time_to_sleep / step_time
                while not self.stopped() and steps > 0:
                    time.sleep(step_time)
                    steps -= 1
                if DEBUG_HEARTBEAT_MESSAGES: print "[DBG] Not sleeping anymore"

            except:
                # TODO: use log
                traceback.print_exc()
                time.sleep(5)

DEBUG_MESSAGES = DEBUG
DEBUG_HEARTBEAT_MESSAGES = DEBUG_MESSAGES

def dbg(message):
    print message
    sys.stdout.flush()

class VisirExperiment(ConcurrentExperiment.ConcurrentExperiment):

    def __init__(self, coord_address, locator, cfg_manager, *args, **kwargs):
        super(VisirExperiment, self).__init__(*args, **kwargs)
        self._cfg_manager = cfg_manager
        self.read_config()
        self._requesting_lock = threading.Lock()

        # We will initialize and start it later
        self.heartbeater = None
        self.heartbeater_lock = threading.Lock()
        self._users_counter_lock = threading.Lock()
        self.users_counter = 0

        # XXX It must be SessionType.Memory, since we are storing an HTTPConnection object
        self._session_manager = SessionManager.SessionManager( cfg_manager, SessionType.Memory, "visir" )

    @Override(ConcurrentExperiment.ConcurrentExperiment)
    def do_get_api(self):
        return "2_concurrent"

    def read_config(self):
        """
        Reads the config parameters from the config file (such as the
        measurement server address)
        """

        # Common configuration values
        self.savedata   = self._cfg_manager.get_value(CFG_SAVEDATA, DEFAULT_SAVEDATA)
        self.teacher    = self._cfg_manager.get_value(CFG_TEACHER, DEFAULT_TEACHER)
        self.client_url = self._cfg_manager.get_value(CFG_CLIENT_URL, DEFAULT_CLIENT_URL)
        self.measure_server_addr = self._cfg_manager.get_value(CFG_MEASURE_SERVER_ADDRESS, DEFAULT_MEASURE_SERVER_ADDRESS)
        self.measure_server_target = self._cfg_manager.get_value(CFG_MEASURE_SERVER_TARGET, DEFAULT_MEASURE_SERVER_TARGET)
        self.heartbeat_period = self._cfg_manager.get_value(CFG_HEARTBEAT_PERIOD, DEFAULT_HEARTBEAT_PERIOD)
        self.circuits     = self._cfg_manager.get_value(CFG_CIRCUITS, DEFAULT_CIRCUITS)
        self.circuits_dir = self._cfg_manager.get_value(CFG_CIRCUITS_DIR, None)
        self.library_xml  = self._cfg_manager.get_value(CFG_LIBRARY_XML, "failed")

        global DEBUG
        DEBUG = self._cfg_manager.get_value(CFG_DEBUG_PRINTS, DEFAULT_DEBUG_PRINTS)

        #
        # There are two ways of deploying VISIR:
        # - with all the OpenLabs Web code
        # - directly without authentication against the measurement server
        #
        # We support both, depending on this variable.
        #
        self.use_visir_php = self._cfg_manager.get_value(CFG_USE_VISIR_PHP, DEFAULT_USE_VISIR_PHP)

        if self.use_visir_php:
            self.loginurl       = self._cfg_manager.get_value(CFG_LOGIN_URL, DEFAULT_LOGIN_URL)
            self.basephpurl     = self._cfg_manager.get_value(CFG_BASE_URL, DEFAULT_BASE_URL)
            self.login_email    = self._cfg_manager.get_value(CFG_LOGIN_EMAIL, DEFAULT_LOGIN_EMAIL)
            self.login_password = self._cfg_manager.get_value(CFG_LOGIN_PASSWORD, DEFAULT_LOGIN_PASSWORD)

    def get_circuits(self):
        all_circuits = self.circuits.copy()
        try:
            if self.circuits_dir is not None:
                for fname in glob.glob("%s*cir" % self.circuits_dir):
                    name = os.path.basename(fname)[:-4]
                    all_circuits[name] = urllib.quote(open(fname, "rb").read(), '')
        except:
            traceback.print_exc()
        return all_circuits

    @Override(ConcurrentExperiment.ConcurrentExperiment)
    @logged()
    def do_start_experiment(self, lab_session_id, *args, **kwargs):
        """
        Callback run when the experiment is started
        """

        # Consider whether we should initialize the heartbeater now. If we are the first
        # user, the heartbeater will not have started yet.
        with self.heartbeater_lock:
            if self.heartbeater is None:
                self.heartbeater = Heartbeater(self, self.heartbeat_period, self._session_manager)
                self.heartbeater.setDaemon(True)
                self.heartbeater.setName('Heartbeater')
                self.heartbeater.start()

        if DEBUG: dbg("[DBG] Current number of users: %s" % len(self._session_manager.list_sessions()))
        if DEBUG: dbg("[DBG] Lab Session Id: %s" % lab_session_id)
        if DEBUG: dbg("[DBG] Measure server address: %s" % self.measure_server_addr)
        if DEBUG: dbg("[DBG] Measure server target: %s" % self.measure_server_target)

        setup_data = self.build_setup_data("", self.client_url, self.get_circuits().keys())

        self._session_manager.create_session(lab_session_id.id)
        self._session_manager.modify_session(lab_session_id, {'cookie' : "", 'electro_lab_cookie' : ""})

        # Increment the user's counter, which indicates how many users are using the experiment.
        with self._users_counter_lock:
            self.users_counter += 1

        if(DEBUG): dbg("[VisirTestExperiment][Start]: Current users: %s" % self.users_counter)

        return json.dumps({ "initial_configuration" : setup_data, "batch" : False })

    @Override(ConcurrentExperiment.ConcurrentExperiment)
    @logged()
    def do_send_command_to_device(self, lab_session_id, command):
        """
        Callback run when the client sends a command to the experiment
        @param command Command sent by the client, as a string.
        """

        if DEBUG: dbg("[DBG] Lab Session Id: %s" % lab_session_id)

        # This command is currently not used.
        if command == "GIVE_ME_CIRCUIT_LIST":<commit_msg>Add empty data<commit_after>#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2005 onwards University of Deusto
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution.
#
# This software consists of contributions made by many individuals,
# listed below:
#
# Author: <NAME> <<EMAIL>>
#         <NAME> <<EMAIL>>
#

import weblab.experiment.concurrent_experiment as ConcurrentExperiment

import os
import sys
import glob
import httplib
import urllib2
import urllib
import threading
import traceback
import time
import random
import xml.dom.minidom as xml
import json

from voodoo.log import logged
from voodoo.override import Override

import voodoo.sessions.manager as SessionManager
import voodoo.sessions.session_type as SessionType
from voodoo.sessions.exc import SessionNotFoundError
from voodoo.typechecker import typecheck, ANY

CFG_USE_VISIR_PHP = "vt_use_visir_php"

CFG_MEASURE_SERVER_ADDRESS = "vt_measure_server_addr"
CFG_MEASURE_SERVER_TARGET = "vt_measure_server_target"
CFG_LOGIN_URL = "vt_login_url"
CFG_BASE_URL = "vt_base_url"
CFG_LOGIN_EMAIL = "vt_login_email"
CFG_LOGIN_PASSWORD = "<PASSWORD>"
CFG_SAVEDATA = "vt_savedata"
CFG_TEACHER  = "vt_teacher"
CFG_CLIENT_URL = "vt_client_url"
CFG_HEARTBEAT_PERIOD = "vt_heartbeat_period"
CFG_CIRCUITS = "vt_circuits"
CFG_CIRCUITS_DIR = "vt_circuits_dir"
CFG_DEBUG_PRINTS = "vt_debug_prints"
CFG_LIBRARY_XML  = "vt_library"

DEFAULT_USE_VISIR_PHP = True
DEFAULT_MEASURE_SERVER_ADDRESS = "172.16.58.3:8080"
DEFAULT_MEASURE_SERVER_TARGET = "/measureserver"
DEFAULT_LOGIN_URL = """https://weblab-visir.deusto.es/electronics/student.php"""
DEFAULT_BASE_URL = """https://weblab-visir.deusto.es/"""
DEFAULT_LOGIN_EMAIL = "guest"
DEFAULT_LOGIN_PASSWORD = "<PASSWORD>"
DEFAULT_SAVEDATA = ""
DEFAULT_TEACHER  = True
DEFAULT_CLIENT_URL = "../web/visir/loader.swf"
DEFAULT_HEARTBEAT_PERIOD = 30
DEFAULT_CIRCUITS = {}
DEFAULT_DEBUG_PRINTS = False


HEARTBEAT_REQUEST = """<protocol version="1.3"><request "%s"/></protocol>"""
HEARTBEAT_MAX_SLEEP = 5

# Actually defined through the configuration.
DEBUG = None

class Heartbeater(threading.Thread):
    """
    The Heartbeater is a different thread, which will periodically send a heartbeat
    request if no other requests have been sent recently.
    """

    @typecheck(ANY, basestring, int, basestring, SessionManager.SessionManager)
    def __init__(self, experiment, heartbeat_period, session_manager):
        """
        Creates the Heartbeater object. The Heartbeater is a thread which will periodically
        send requests as heartbeats (because actual heartbeat or even login requests do not work).

        There should be a single Heartbeater, which will send the periodical requests to every
        user that needs so.

        The heartbeat may be inhibited through periodical tick() calls.

        For the Heartbeater to start working, it needs to be started through start(). To stop it,
        stop() must be called. It is noteworthy that stop is not immediate.

        Every heartbeat will be carried out from the same thread, sequentially. Hence, especially
        with a high number of users, it may theoretically take a long time to send a specific
        heartbeat. This should not be an issue unless the heartbeat frequency is set too low.

        @param experiment Reference to the VisirTestExperiment. Will make use of the user map within it.
        @param heartbeat_period Number of seconds between heartbeats.
        @param session_manager The session manager from which retrieve the sessions

        @see start
        @see stop
        @see tick
        """
        threading.Thread.__init__(self)
        self.is_stopped = False
        self.experiment = experiment
        self.heartbeat_period = heartbeat_period
        self.session_manager  = session_manager

    def stop(self):
        """
        Stops the thread. The thread is not stopped immediately. Instead, a flag is
        internally set and the actual thread will finish when possible.
        Once called, stopped() will return true (without waiting for the thread to
        actually finish).

        @see stopped
        """
        self.is_stopped = True

    def stopped(self):
        """
        Returns true if the thread has been explicitly stopped. False otherwise.
        Note that before the thread has been started, stopped will still be false.
        That is, it will start returning true just as soon as stop is called.

        @see stop
        @see is_alive
        """
        return self.is_stopped

    def tick(self, session_object):
        """
        tick()
        Ticks to update the time for a given session. If the session has not been registered
        the tick operation does nothing.

        @param lab_session_id Session id of the user counter to tick
        Should be called, both internally or externally, whenever a heartbeat or any
        other packet is sent to reset OR INITIALIZE the heartbeat timer.
        """

        # If the session id is not registered within the sessions map, we will simply
        # return without doing anything. This can happen for the initial login request.
        session_object['last_heartbeat_sent'] = time.time()
        if DEBUG: print "[DBG] HB TICK"


    def run(self):
        """
        run()

        Thread process. Thread starts running here when start() is called.
        @see start
        """

        if DEBUG: dbg("[DBG] HB INIT")

        while(True):
            if self.stopped():
                return
            try:
                # Sleep at most HEARTBEAT_MAX_SLEEP. We will most likely overwrite this
                # with a lower value, depending on the pending requests.
                time_to_sleep = HEARTBEAT_MAX_SLEEP

                # Loop through every user that is using the experiment concurrently, and consider
                # whether we should update it or not.
                for session_id in self.session_manager.list_sessions():
                    try:
                        sessiondata = self.session_manager.get_session_locking(session_id)
                    except SessionNotFoundError:
                        continue


                    if 'sessionkey' not in sessiondata:
                        self.session_manager.modify_session_unlocking(session_id, sessiondata)
                        continue

                    try:

                        # Evaluate the time left for the next potential heartbeat.
                        if 'last_heartbeat_sent' not in sessiondata:
                            # If we actually don't have a last_heartbeat_sent yet,
                            # initialize it.
                            sessiondata['last_heartbeat_sent'] = time.time()
                        last_sent = sessiondata['last_heartbeat_sent']
                        session_key = sessiondata['sessionkey']
                    finally:
                        self.session_manager.modify_session_unlocking(session_id, sessiondata)

                    time_left = (last_sent + self.heartbeat_period) - time.time()

                    # If time_left is zero or negative, a heartbeat IS due.
                    if(time_left <= 0):
                        if DEBUG_HEARTBEAT_MESSAGES: dbg("[DBG] HB FORWARDING")
                        ret = self.experiment.forward_request(session_id, HEARTBEAT_REQUEST % (session_key))
                        if DEBUG_HEARTBEAT_MESSAGES: dbg("[DBG] Heartbeat response: %s" % ret)

                    else:
                        # Otherwise, we will just sleep.
                        if DEBUG_HEARTBEAT_MESSAGES: print "[DBG] HB SLEEPING FOR %d" % (time_left)

                        # We wish to sleep the maximum only if there are no pending
                        # requests (and if it doesn't go over the MAX).
                        time_to_sleep = min(time_left, time_to_sleep)

                # We will actually not sleep the whole time_to_sleep at once, so that
                # we can check whether the thread has finished more often.
                # TODO: Consider doing this through semaphores so that we do not need
                # this work-around.
                step_time = 0.1
                steps = time_to_sleep / step_time
                while not self.stopped() and steps > 0:
                    time.sleep(step_time)
                    steps -= 1
                if DEBUG_HEARTBEAT_MESSAGES: print "[DBG] Not sleeping anymore"

            except:
                # TODO: use log
                traceback.print_exc()
                time.sleep(5)

DEBUG_MESSAGES = DEBUG
DEBUG_HEARTBEAT_MESSAGES = DEBUG_MESSAGES

def dbg(message):
    print message
    sys.stdout.flush()

class VisirExperiment(ConcurrentExperiment.ConcurrentExperiment):

    def __init__(self, coord_address, locator, cfg_manager, *args, **kwargs):
        super(VisirExperiment, self).__init__(*args, **kwargs)
        self._cfg_manager = cfg_manager
        self.read_config()
        self._requesting_lock = threading.Lock()

        # We will initialize and start it later
        self.heartbeater = None
        self.heartbeater_lock = threading.Lock()
        self._users_counter_lock = threading.Lock()
        self.users_counter = 0

        # XXX It must be SessionType.Memory, since we are storing an HTTPConnection object
        self._session_manager = SessionManager.SessionManager( cfg_manager, SessionType.Memory, "visir" )

    @Override(ConcurrentExperiment.ConcurrentExperiment)
    def do_get_api(self):
        return "2_concurrent"

    def read_config(self):
        """
        Reads the config parameters from the config file (such as the
        measurement server address)
        """

        # Common configuration values
        self.savedata   = self._cfg_manager.get_value(CFG_SAVEDATA, DEFAULT_SAVEDATA)
        self.teacher    = self._cfg_manager.get_value(CFG_TEACHER, DEFAULT_TEACHER)
        self.client_url = self._cfg_manager.get_value(CFG_CLIENT_URL, DEFAULT_CLIENT_URL)
        self.measure_server_addr = self._cfg_manager.get_value(CFG_MEASURE_SERVER_ADDRESS, DEFAULT_MEASURE_SERVER_ADDRESS)
        self.measure_server_target = self._cfg_manager.get_value(CFG_MEASURE_SERVER_TARGET, DEFAULT_MEASURE_SERVER_TARGET)
        self.heartbeat_period = self._cfg_manager.get_value(CFG_HEARTBEAT_PERIOD, DEFAULT_HEARTBEAT_PERIOD)
        self.circuits     = self._cfg_manager.get_value(CFG_CIRCUITS, DEFAULT_CIRCUITS)
        self.circuits_dir = self._cfg_manager.get_value(CFG_CIRCUITS_DIR, None)
        self.library_xml  = self._cfg_manager.get_value(CFG_LIBRARY_XML, "failed")

        global DEBUG
        DEBUG = self._cfg_manager.get_value(CFG_DEBUG_PRINTS, DEFAULT_DEBUG_PRINTS)

        #
        # There are two ways of deploying VISIR:
        # - with all the OpenLabs Web code
        # - directly without authentication against the measurement server
        #
        # We support both, depending on this variable.
        #
        self.use_visir_php = self._cfg_manager.get_value(CFG_USE_VISIR_PHP, DEFAULT_USE_VISIR_PHP)

        if self.use_visir_php:
            self.loginurl       = self._cfg_manager.get_value(CFG_LOGIN_URL, DEFAULT_LOGIN_URL)
            self.basephpurl     = self._cfg_manager.get_value(CFG_BASE_URL, DEFAULT_BASE_URL)
            self.login_email    = self._cfg_manager.get_value(CFG_LOGIN_EMAIL, DEFAULT_LOGIN_EMAIL)
            self.login_password = self._cfg_manager.get_value(CFG_LOGIN_PASSWORD, DEFAULT_LOGIN_PASSWORD)

    def get_circuits(self):
        all_circuits = self.circuits.copy()
        try:
            if self.circuits_dir is not None:
                for fname in glob.glob("%s*cir" % self.circuits_dir):
                    name = os.path.basename(fname)[:-4]
                    all_circuits[name] = urllib.quote(open(fname, "rb").read(), '')
        except:
            traceback.print_exc()
        return all_circuits

    @Override(ConcurrentExperiment.ConcurrentExperiment)
    @logged()
    def do_start_experiment(self, lab_session_id, *args, **kwargs):
        """
        Callback run when the experiment is started
        """

        # Consider whether we should initialize the heartbeater now. If we are the first
        # user, the heartbeater will not have started yet.
        with self.heartbeater_lock:
            if self.heartbeater is None:
                self.heartbeater = Heartbeater(self, self.heartbeat_period, self._session_manager)
                self.heartbeater.setDaemon(True)
                self.heartbeater.setName('Heartbeater')
                self.heartbeater.start()

        if DEBUG: dbg("[DBG] Current number of users: %s" % len(self._session_manager.list_sessions()))
        if DEBUG: dbg("[DBG] Lab Session Id: %s" % lab_session_id)
        if DEBUG: dbg("[DBG] Measure server address: %s" % self.measure_server_addr)
        if DEBUG: dbg("[DBG] Measure server target: %s" % self.measure_server_target)

        setup_data = self.build_setup_data("", self.client_url, self.get_circuits().keys())

        self._session_manager.create_session(lab_session_id.id)
        self._session_manager.modify_session(lab_session_id, {'cookie' : "", 'electro_lab_cookie' : ""})

        # Increment the user's counter, which indicates how many users are using the experiment.
        with self._users_counter_lock:
            self.users_counter += 1

        if(DEBUG): dbg("[VisirTestExperiment][Start]: Current users: %s" % self.users_counter)

        return json.dumps({ "initial_configuration" : setup_data, "batch" : False })

    @Override(ConcurrentExperiment.ConcurrentExperiment)
    @logged()
    def do_send_command_to_device(self, lab_session_id, command):
        """
        Callback run when the client sends a command to the experiment
        @param command Command sent by the client, as a string.
        """

        if DEBUG: dbg("[DBG] Lab Session Id: %s" % lab_session_id)

        data = ""

        # This command is currently not used.
        if command == "GIVE_ME_CIRCUIT_LIST":
{'fraction_non_alphanumeric': 0.07329232074671192, 'fraction_numerical': 0.002333474756045821, 'mean_word_length': 2.5369513567587845, 'pattern_counts': {'":': 2, '<': 27, '<?xml version=': 0, '>': 27, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 6}, 'pii_count': 2, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4316714', 'n_tokens_mistral': 8143, 'n_tokens_neox': 7791, 'n_words': 2308}